// RPC Client for connecting to Archipelago backend export interface RPCOptions { method: string params?: any timeout?: number } export interface RPCResponse { result?: T error?: { code: number message: string data?: any } } class RPCClient { private baseUrl: string constructor(baseUrl: string = '/rpc/v1') { this.baseUrl = baseUrl } async call(options: RPCOptions): Promise { const { method, params = {}, timeout = 30000 } = options const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), timeout) try { const response = await fetch(this.baseUrl, { method: 'POST', credentials: 'include', // Important for session cookies headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ method, params }), signal: controller.signal, }) clearTimeout(timeoutId) if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`) } const data: RPCResponse = await response.json() if (data.error) { throw new Error(data.error.message || 'RPC Error') } return data.result as T } catch (error) { clearTimeout(timeoutId) if (error instanceof Error) { if (error.name === 'AbortError') { throw new Error('Request timeout') } throw error } throw new Error('Unknown error occurred') } } // Convenience methods async login(password: string): Promise { return this.call({ method: 'auth.login', params: { password, metadata: { // Add any metadata needed }, }, }) } async changePassword(params: { currentPassword: string newPassword: string alsoChangeSsh?: boolean }): Promise<{ success: boolean }> { return this.call({ method: 'auth.changePassword', params: { currentPassword: params.currentPassword, newPassword: params.newPassword, alsoChangeSsh: params.alsoChangeSsh ?? true, }, }) } async logout(): Promise { return this.call({ method: 'auth.logout', params: {}, }) } async completeOnboarding(): Promise { return this.call({ method: 'auth.onboardingComplete', params: {}, }) } async isOnboardingComplete(): Promise { return this.call({ method: 'auth.isOnboardingComplete', params: {}, }) } async getNodeDid(): Promise<{ did: string; pubkey: string }> { return this.call({ method: 'node.did', params: {}, }) } async publishNostrIdentity(): Promise<{ event_id: string; success: number; failed: number }> { return this.call({ method: 'node.nostr-publish', params: {}, }) } async getNostrPubkey(): Promise<{ nostr_pubkey: string }> { return this.call({ method: 'node.nostr-pubkey', params: {}, }) } async listPeers(): Promise<{ peers: Array<{ onion: string; pubkey: string; name?: string }> }> { return this.call({ method: 'node-list-peers', params: {}, }) } async addPeer(params: { onion: string; pubkey: string; name?: string }): Promise<{ peers: unknown[] }> { return this.call({ method: 'node-add-peer', params, }) } async removePeer(pubkey: string): Promise<{ peers: unknown[] }> { return this.call({ method: 'node-remove-peer', params: { pubkey }, }) } async sendMessageToPeer(onion: string, message: string): Promise<{ ok: boolean; sent_to: string }> { return this.call({ method: 'node-send-message', params: { onion, message }, timeout: 90000, }) } async checkPeerReachable(onion: string): Promise<{ onion: string; reachable: boolean }> { return this.call({ method: 'node-check-peer', params: { onion }, timeout: 35000, }) } async getReceivedMessages(): Promise<{ messages: Array<{ from_pubkey: string; message: string; timestamp: string }> }> { return this.call({ method: 'node-messages-received', params: {}, }) } async discoverNodes(): Promise<{ nodes: Array<{ did: string; onion: string; pubkey: string; node_address: string }> }> { return this.call({ method: 'node-nostr-discover', params: {}, timeout: 20000, }) } async getTorAddress(): Promise<{ tor_address: string | null }> { return this.call({ method: 'node.tor-address', params: {}, }) } async verifyNostrRevoked(): Promise<{ revoked: boolean nostr_pubkey: string latest_content?: string error?: string }> { return this.call({ method: 'node-nostr-verify-revoked', params: {}, timeout: 25000, }) } async echo(message: string): Promise { return this.call({ method: 'server.echo', params: { message }, }) } async getSystemTime(): Promise<{ now: string; uptime: number }> { return this.call({ method: 'server.time', params: {}, }) } async getMetrics(): Promise { return this.call({ method: 'server.metrics', params: {}, }) } async updateServer(marketplaceUrl: string): Promise<'updating' | 'no-updates'> { return this.call({ method: 'server.update', params: { 'marketplace-url': marketplaceUrl }, }) } async restartServer(): Promise { return this.call({ method: 'server.restart', params: {}, }) } async shutdownServer(): Promise { return this.call({ method: 'server.shutdown', params: {}, }) } async installPackage(id: string, marketplaceUrl: string, version: string): Promise { return this.call({ method: 'package.install', params: { id, 'marketplace-url': marketplaceUrl, version }, }) } async uninstallPackage(id: string): Promise { return this.call({ method: 'package.uninstall', params: { id }, }) } async startPackage(id: string): Promise { return this.call({ method: 'package.start', params: { id }, }) } async stopPackage(id: string): Promise { return this.call({ method: 'package.stop', params: { id }, }) } async restartPackage(id: string): Promise { return this.call({ method: 'package.restart', params: { id }, }) } async getMarketplace(url: string): Promise { return this.call({ method: 'marketplace.get', params: { url }, }) } async sideloadPackage(manifest: any, icon: string): Promise { return this.call({ method: 'package.sideload', params: { manifest, icon }, timeout: 120000, // 2 minutes for upload }) } } export const rpcClient = new RPCClient()