2026-01-24 22:59:20 +00:00
|
|
|
// RPC Client for connecting to Archipelago backend
|
|
|
|
|
|
|
|
|
|
export interface RPCOptions {
|
|
|
|
|
method: string
|
2026-03-04 05:23:42 +00:00
|
|
|
params?: Record<string, unknown>
|
2026-01-24 22:59:20 +00:00
|
|
|
timeout?: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface RPCResponse<T> {
|
|
|
|
|
result?: T
|
|
|
|
|
error?: {
|
|
|
|
|
code: number
|
|
|
|
|
message: string
|
2026-03-04 05:23:42 +00:00
|
|
|
data?: unknown
|
2026-01-24 22:59:20 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class RPCClient {
|
|
|
|
|
private baseUrl: string
|
|
|
|
|
|
|
|
|
|
constructor(baseUrl: string = '/rpc/v1') {
|
|
|
|
|
this.baseUrl = baseUrl
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async call<T>(options: RPCOptions): Promise<T> {
|
|
|
|
|
const { method, params = {}, timeout = 30000 } = options
|
2026-03-01 17:53:18 +00:00
|
|
|
const maxRetries = 3
|
2026-01-24 22:59:20 +00:00
|
|
|
|
2026-03-01 17:53:18 +00:00
|
|
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
|
|
|
const controller = new AbortController()
|
|
|
|
|
const timeoutId = setTimeout(() => controller.abort(), timeout)
|
2026-01-24 22:59:20 +00:00
|
|
|
|
2026-03-01 17:53:18 +00:00
|
|
|
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,
|
|
|
|
|
})
|
2026-01-24 22:59:20 +00:00
|
|
|
|
2026-03-01 17:53:18 +00:00
|
|
|
clearTimeout(timeoutId)
|
2026-01-24 22:59:20 +00:00
|
|
|
|
2026-03-01 17:53:18 +00:00
|
|
|
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
|
|
|
|
|
}
|
2026-01-24 22:59:20 +00:00
|
|
|
|
2026-03-01 17:53:18 +00:00
|
|
|
const data: RPCResponse<T> = await response.json()
|
2026-01-24 22:59:20 +00:00
|
|
|
|
2026-03-01 17:53:18 +00:00
|
|
|
if (data.error) {
|
|
|
|
|
throw new Error(data.error.message || 'RPC Error')
|
|
|
|
|
}
|
2026-01-24 22:59:20 +00:00
|
|
|
|
2026-03-01 17:53:18 +00:00
|
|
|
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
|
2026-01-24 22:59:20 +00:00
|
|
|
}
|
2026-03-01 17:53:18 +00:00
|
|
|
throw new Error('Unknown error occurred')
|
2026-01-24 22:59:20 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-01 17:53:18 +00:00
|
|
|
|
|
|
|
|
throw new Error('Request failed after retries')
|
2026-01-24 22:59:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Convenience methods
|
feat: add TOTP 2FA, API key switcher, login progress bar, and alpha hardening plan
- TOTP 2FA: full setup/confirm/disable/login flow with Argon2id + ChaCha20-Poly1305
encrypted secret storage, QR code generation, and bcrypt-hashed backup codes
- API key switcher: OAuth vs personal API key toggle in AIUI chat settings with
status indicator, key validation, and help text
- Login progress bar: server startup detection with health check polling, form
disabled until server is ready
- AI quarantine docs: comprehensive HTML page documenting all 6 security layers
- Settings: AI Data Access permission toggles with per-category control
- Alpha hardening plan: 28-task overnight automation plan across 7 phases
(onboarding, login, app install, AIUI, UI polish, security, ISO build)
- Backlog: node discovery spatial map feature for alpha demo
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 12:23:57 +00:00
|
|
|
async login(password: string): Promise<{ requires_totp?: boolean } | null> {
|
2026-01-24 22:59:20 +00:00
|
|
|
return this.call({
|
|
|
|
|
method: 'auth.login',
|
|
|
|
|
params: {
|
|
|
|
|
password,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
feat: add TOTP 2FA, API key switcher, login progress bar, and alpha hardening plan
- TOTP 2FA: full setup/confirm/disable/login flow with Argon2id + ChaCha20-Poly1305
encrypted secret storage, QR code generation, and bcrypt-hashed backup codes
- API key switcher: OAuth vs personal API key toggle in AIUI chat settings with
status indicator, key validation, and help text
- Login progress bar: server startup detection with health check polling, form
disabled until server is ready
- AI quarantine docs: comprehensive HTML page documenting all 6 security layers
- Settings: AI Data Access permission toggles with per-category control
- Alpha hardening plan: 28-task overnight automation plan across 7 phases
(onboarding, login, app install, AIUI, UI polish, security, ISO build)
- Backlog: node discovery spatial map feature for alpha demo
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 12:23:57 +00:00
|
|
|
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: {} })
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 15:03:34 +00:00
|
|
|
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,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-24 22:59:20 +00:00
|
|
|
async logout(): Promise<void> {
|
|
|
|
|
return this.call({
|
|
|
|
|
method: 'auth.logout',
|
|
|
|
|
params: {},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 15:03:34 +00:00
|
|
|
async completeOnboarding(): Promise<boolean> {
|
|
|
|
|
return this.call({
|
|
|
|
|
method: 'auth.onboardingComplete',
|
|
|
|
|
params: {},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async isOnboardingComplete(): Promise<boolean> {
|
|
|
|
|
return this.call({
|
|
|
|
|
method: 'auth.isOnboardingComplete',
|
|
|
|
|
params: {},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-02 08:34:13 +00:00
|
|
|
async resetOnboarding(): Promise<boolean> {
|
|
|
|
|
return this.call({
|
|
|
|
|
method: 'auth.resetOnboarding',
|
|
|
|
|
params: {},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 15:03:34 +00:00
|
|
|
async getNodeDid(): Promise<{ did: string; pubkey: string }> {
|
|
|
|
|
return this.call({
|
|
|
|
|
method: 'node.did',
|
|
|
|
|
params: {},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-02 08:34:13 +00:00
|
|
|
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 },
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 15:03:34 +00:00
|
|
|
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,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-24 22:59:20 +00:00
|
|
|
async echo(message: string): Promise<string> {
|
|
|
|
|
return this.call({
|
|
|
|
|
method: 'server.echo',
|
|
|
|
|
params: { message },
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getSystemTime(): Promise<{ now: string; uptime: number }> {
|
|
|
|
|
return this.call({
|
|
|
|
|
method: 'server.time',
|
|
|
|
|
params: {},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-04 05:23:42 +00:00
|
|
|
async getMetrics(): Promise<Record<string, unknown>> {
|
2026-01-24 22:59:20 +00:00
|
|
|
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<void> {
|
|
|
|
|
return this.call({
|
|
|
|
|
method: 'server.restart',
|
|
|
|
|
params: {},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async shutdownServer(): Promise<void> {
|
|
|
|
|
return this.call({
|
|
|
|
|
method: 'server.shutdown',
|
|
|
|
|
params: {},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async installPackage(id: string, marketplaceUrl: string, version: string): Promise<string> {
|
|
|
|
|
return this.call({
|
|
|
|
|
method: 'package.install',
|
|
|
|
|
params: { id, 'marketplace-url': marketplaceUrl, version },
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async uninstallPackage(id: string): Promise<void> {
|
|
|
|
|
return this.call({
|
|
|
|
|
method: 'package.uninstall',
|
|
|
|
|
params: { id },
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async startPackage(id: string): Promise<void> {
|
|
|
|
|
return this.call({
|
|
|
|
|
method: 'package.start',
|
|
|
|
|
params: { id },
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async stopPackage(id: string): Promise<void> {
|
|
|
|
|
return this.call({
|
|
|
|
|
method: 'package.stop',
|
|
|
|
|
params: { id },
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async restartPackage(id: string): Promise<void> {
|
|
|
|
|
return this.call({
|
|
|
|
|
method: 'package.restart',
|
|
|
|
|
params: { id },
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-04 05:23:42 +00:00
|
|
|
async getMarketplace(url: string): Promise<Record<string, unknown>> {
|
2026-01-24 22:59:20 +00:00
|
|
|
return this.call({
|
|
|
|
|
method: 'marketplace.get',
|
|
|
|
|
params: { url },
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const rpcClient = new RPCClient()
|
|
|
|
|
|