// RPC Client for connecting to Archipelago backend export interface RPCOptions { method: string params?: Record timeout?: number } export interface RPCResponse { result?: T error?: { code: number message: string data?: unknown } } 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 maxRetries = 3 for (let attempt = 0; attempt < maxRetries; attempt++) { 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) { const err = new Error(`HTTP ${response.status}: ${response.statusText}`) const isRetryable = response.status === 502 || response.status === 503 if (isRetryable && attempt < maxRetries - 1) { await new Promise((r) => setTimeout(r, 600 * (attempt + 1))) continue } throw err } 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') { const timeoutErr = new Error('Request timeout') if (attempt < maxRetries - 1) { await new Promise((r) => setTimeout(r, 600 * (attempt + 1))) continue } throw timeoutErr } const msg = error.message const isRetryable = /502|503|Bad Gateway|fetch|network/i.test(msg) if (isRetryable && attempt < maxRetries - 1) { await new Promise((r) => setTimeout(r, 600 * (attempt + 1))) continue } throw error } throw new Error('Unknown error occurred') } } throw new Error('Request failed after retries') } // Convenience methods async login(password: string): Promise<{ requires_totp?: boolean } | null> { return this.call({ method: 'auth.login', params: { password, }, }) } async loginTotp(code: string): Promise<{ success: boolean }> { return this.call({ method: 'auth.login.totp', params: { code } }) } async loginBackup(code: string): Promise<{ success: boolean }> { return this.call({ method: 'auth.login.backup', params: { code } }) } async totpSetupBegin(password: string): Promise<{ qr_svg: string secret_base32: string pending_token: string }> { return this.call({ method: 'auth.totp.setup.begin', params: { password } }) } async totpSetupConfirm(params: { code: string password: string pendingToken: string }): Promise<{ enabled: boolean; backup_codes: string[] }> { return this.call({ method: 'auth.totp.setup.confirm', params }) } async totpDisable(password: string, code: string): Promise<{ disabled: boolean }> { return this.call({ method: 'auth.totp.disable', params: { password, code } }) } async totpStatus(): Promise<{ enabled: boolean }> { return this.call({ method: 'auth.totp.status', params: {} }) } 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 resetOnboarding(): Promise { return this.call({ method: 'auth.resetOnboarding', params: {}, }) } async getNodeDid(): Promise<{ did: string; pubkey: string }> { return this.call({ method: 'node.did', params: {}, }) } async signChallenge(challenge: string): Promise<{ signature: string }> { return this.call({ method: 'node.signChallenge', params: { challenge }, }) } async createBackup(passphrase: string): Promise<{ version: number did: string pubkey: string kid: string encrypted: boolean blob: string timestamp: string }> { return this.call({ method: 'node.createBackup', params: { passphrase }, }) } 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 }, }) } } export const rpcClient = new RPCClient()