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
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 00:46:52 +00:00
|
|
|
function getCsrfToken(): string | null {
|
|
|
|
|
const match = document.cookie.match(/(?:^|;\s*)csrf_token=([^;]+)/)
|
|
|
|
|
return match ? match[1]! : null
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-24 22:59:20 +00:00
|
|
|
class RPCClient {
|
feat: rootless podman, session hardening, boot stability, sidebar fix
Rootless podman migration (TASK-11):
- Remove sudo from all podman calls in PodmanClient + 8 backend files
- Remove sudo from all podman/docker calls in deploy script
- Restore full systemd security hardening: NoNewPrivileges,
RestrictAddressFamilies, MemoryDenyWriteExecute, RestrictRealtime,
RestrictNamespaces, RestrictSUIDSGID, SystemCallFilter, ProtectSystem=strict
- Enable loginctl linger for rootless container persistence
- Remove Ollama from auto-deploy (marketplace-only)
Session & auth hardening:
- Increase MAX_CONCURRENT_SESSIONS 20→50 (prevents eviction storms)
- Debounced 401 redirect in rpc-client.ts (prevents redirect storms)
Boot stability:
- optimize-debian.sh: adds chrony, swap, removes policy-rc.d
- deploy script: pre-restart chrony + swap setup
- ISO build: chrony package, swap file creation
- BootScreen: no longer clears localStorage (prevents splash replay)
- RootRedirect: sole owner of localStorage clearing on server ready
UI fixes:
- Sidebar opacity default changed from 0→visible (fixes missing sidebar
after page-persistence login without entrance animation)
- Console.log/error wrapped in import.meta.env.DEV guards
- Remove unused route import from RootRedirect
Beta tracking:
- CLAUDE.md: beta freeze protocol added
- MASTER_PLAN.md: TASK-11, TASK-17, phase structure
- BETA-PROGRESS.md: initial tracking doc
- Tagged v1.2.0-alpha.1 as pre-rootless baseline
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 13:53:27 +00:00
|
|
|
private static _sessionExpiredRedirecting = false
|
2026-01-24 22:59:20 +00:00
|
|
|
private baseUrl: string
|
|
|
|
|
|
|
|
|
|
constructor(baseUrl: string = '/rpc/v1') {
|
|
|
|
|
this.baseUrl = baseUrl
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async call<T>(options: RPCOptions): Promise<T> {
|
2026-03-21 01:57:05 +00:00
|
|
|
const { method, params = {}, timeout = 15000 } = 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 {
|
2026-03-11 00:46:52 +00:00
|
|
|
const headers: Record<string, string> = {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
}
|
|
|
|
|
const csrfToken = getCsrfToken()
|
|
|
|
|
if (csrfToken) {
|
|
|
|
|
headers['X-CSRF-Token'] = csrfToken
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-01 17:53:18 +00:00
|
|
|
const response = await fetch(this.baseUrl, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
credentials: 'include', // Important for session cookies
|
2026-03-11 00:46:52 +00:00
|
|
|
headers,
|
2026-03-01 17:53:18 +00:00
|
|
|
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) {
|
feat: rootless podman, session hardening, boot stability, sidebar fix
Rootless podman migration (TASK-11):
- Remove sudo from all podman calls in PodmanClient + 8 backend files
- Remove sudo from all podman/docker calls in deploy script
- Restore full systemd security hardening: NoNewPrivileges,
RestrictAddressFamilies, MemoryDenyWriteExecute, RestrictRealtime,
RestrictNamespaces, RestrictSUIDSGID, SystemCallFilter, ProtectSystem=strict
- Enable loginctl linger for rootless container persistence
- Remove Ollama from auto-deploy (marketplace-only)
Session & auth hardening:
- Increase MAX_CONCURRENT_SESSIONS 20→50 (prevents eviction storms)
- Debounced 401 redirect in rpc-client.ts (prevents redirect storms)
Boot stability:
- optimize-debian.sh: adds chrony, swap, removes policy-rc.d
- deploy script: pre-restart chrony + swap setup
- ISO build: chrony package, swap file creation
- BootScreen: no longer clears localStorage (prevents splash replay)
- RootRedirect: sole owner of localStorage clearing on server ready
UI fixes:
- Sidebar opacity default changed from 0→visible (fixes missing sidebar
after page-persistence login without entrance animation)
- Console.log/error wrapped in import.meta.env.DEV guards
- Remove unused route import from RootRedirect
Beta tracking:
- CLAUDE.md: beta freeze protocol added
- MASTER_PLAN.md: TASK-11, TASK-17, phase structure
- BETA-PROGRESS.md: initial tracking doc
- Tagged v1.2.0-alpha.1 as pre-rootless baseline
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 13:53:27 +00:00
|
|
|
// Session expired — debounced redirect to login
|
|
|
|
|
// Use a single shared timeout to prevent redirect storms when
|
|
|
|
|
// multiple parallel requests all get 401 at once
|
2026-03-14 17:12:41 +00:00
|
|
|
if (response.status === 401 && method !== 'auth.login') {
|
feat: rootless podman, session hardening, boot stability, sidebar fix
Rootless podman migration (TASK-11):
- Remove sudo from all podman calls in PodmanClient + 8 backend files
- Remove sudo from all podman/docker calls in deploy script
- Restore full systemd security hardening: NoNewPrivileges,
RestrictAddressFamilies, MemoryDenyWriteExecute, RestrictRealtime,
RestrictNamespaces, RestrictSUIDSGID, SystemCallFilter, ProtectSystem=strict
- Enable loginctl linger for rootless container persistence
- Remove Ollama from auto-deploy (marketplace-only)
Session & auth hardening:
- Increase MAX_CONCURRENT_SESSIONS 20→50 (prevents eviction storms)
- Debounced 401 redirect in rpc-client.ts (prevents redirect storms)
Boot stability:
- optimize-debian.sh: adds chrony, swap, removes policy-rc.d
- deploy script: pre-restart chrony + swap setup
- ISO build: chrony package, swap file creation
- BootScreen: no longer clears localStorage (prevents splash replay)
- RootRedirect: sole owner of localStorage clearing on server ready
UI fixes:
- Sidebar opacity default changed from 0→visible (fixes missing sidebar
after page-persistence login without entrance animation)
- Console.log/error wrapped in import.meta.env.DEV guards
- Remove unused route import from RootRedirect
Beta tracking:
- CLAUDE.md: beta freeze protocol added
- MASTER_PLAN.md: TASK-11, TASK-17, phase structure
- BETA-PROGRESS.md: initial tracking doc
- Tagged v1.2.0-alpha.1 as pre-rootless baseline
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 13:53:27 +00:00
|
|
|
if (!RPCClient._sessionExpiredRedirecting) {
|
|
|
|
|
RPCClient._sessionExpiredRedirecting = true
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
window.location.href = '/login'
|
|
|
|
|
}, 300)
|
|
|
|
|
}
|
2026-03-14 17:12:41 +00:00
|
|
|
throw new Error('Session expired')
|
|
|
|
|
}
|
2026-03-18 22:05:21 +00:00
|
|
|
// CSRF 403: retry once after short delay (cookie may have been
|
|
|
|
|
// updated by a concurrent Set-Cookie response not yet visible to JS)
|
|
|
|
|
if (response.status === 403 && attempt < maxRetries - 1) {
|
|
|
|
|
await new Promise((r) => setTimeout(r, 300))
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-03-01 17:53:18 +00:00
|
|
|
const err = new Error(`HTTP ${response.status}: ${response.statusText}`)
|
|
|
|
|
const isRetryable = response.status === 502 || response.status === 503
|
|
|
|
|
if (isRetryable && attempt < maxRetries - 1) {
|
2026-03-21 01:57:05 +00:00
|
|
|
const delay = 600 * (attempt + 1)
|
|
|
|
|
await new Promise((r) => setTimeout(r, Math.floor(delay * (0.5 + Math.random() * 0.5))))
|
2026-03-01 17:53:18 +00:00
|
|
|
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) {
|
2026-03-21 01:57:05 +00:00
|
|
|
const delay = 600 * (attempt + 1)
|
|
|
|
|
await new Promise((r) => setTimeout(r, Math.floor(delay * (0.5 + Math.random() * 0.5))))
|
2026-03-01 17:53:18 +00:00
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
throw timeoutErr
|
|
|
|
|
}
|
|
|
|
|
const msg = error.message
|
|
|
|
|
const isRetryable = /502|503|Bad Gateway|fetch|network/i.test(msg)
|
|
|
|
|
if (isRetryable && attempt < maxRetries - 1) {
|
2026-03-21 01:57:05 +00:00
|
|
|
const delay = 600 * (attempt + 1)
|
|
|
|
|
await new Promise((r) => setTimeout(r, Math.floor(delay * (0.5 + Math.random() * 0.5))))
|
2026-03-01 17:53:18 +00:00
|
|
|
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-03-12 00:19:30 +00:00
|
|
|
async resolveDid(did?: string): Promise<Record<string, unknown>> {
|
|
|
|
|
return this.call({
|
|
|
|
|
method: 'identity.resolve-did',
|
|
|
|
|
params: did ? { did } : {},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async createPresentation(params: {
|
|
|
|
|
holderId: string
|
|
|
|
|
credentialIds: string[]
|
|
|
|
|
}): Promise<Record<string, unknown>> {
|
|
|
|
|
return this.call({
|
|
|
|
|
method: 'identity.create-presentation',
|
|
|
|
|
params: { holder_id: params.holderId, credential_ids: params.credentialIds },
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async verifyPresentation(presentation: Record<string, unknown>): Promise<{
|
|
|
|
|
valid: boolean
|
|
|
|
|
holder_valid: boolean
|
|
|
|
|
credentials: Array<{ id: string; valid: boolean; revoked: boolean }>
|
|
|
|
|
}> {
|
|
|
|
|
return this.call({
|
|
|
|
|
method: 'identity.verify-presentation',
|
|
|
|
|
params: { presentation },
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async createPsbt(params: {
|
|
|
|
|
outputs: Array<{ address: string; amount_sats: number }>
|
|
|
|
|
feeRateSatPerVbyte?: number
|
|
|
|
|
}): Promise<{
|
|
|
|
|
psbt_base64: string
|
|
|
|
|
change_output_index: number
|
|
|
|
|
total_amount_sats: number
|
|
|
|
|
fee_rate_sat_per_vbyte: number
|
|
|
|
|
}> {
|
|
|
|
|
return this.call({
|
|
|
|
|
method: 'lnd.create-psbt',
|
|
|
|
|
params: {
|
|
|
|
|
outputs: params.outputs,
|
|
|
|
|
fee_rate_sat_per_vbyte: params.feeRateSatPerVbyte ?? 10,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async finalizePsbt(signedPsbtBase64: string): Promise<{
|
|
|
|
|
raw_final_tx: string
|
|
|
|
|
broadcast: boolean
|
|
|
|
|
}> {
|
|
|
|
|
return this.call({
|
|
|
|
|
method: 'lnd.finalize-psbt',
|
|
|
|
|
params: { signed_psbt_base64: signedPsbtBase64 },
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
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: {},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-12 12:56:59 +00:00
|
|
|
async getNostrPubkey(): Promise<{ nostr_pubkey: string; nostr_npub?: string }> {
|
2026-02-17 15:03:34 +00:00
|
|
|
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: {},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-13 00:13:38 +00:00
|
|
|
async torListServices(): Promise<{ services: Array<{ name: string; local_port: number; onion_address: string | null; enabled: boolean }> }> {
|
|
|
|
|
return this.call({ method: 'tor.list-services' })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async torRotateService(name: string): Promise<{ rotated: boolean; name: string; old_onion: string | null; new_onion: string | null; transition_hours: number }> {
|
|
|
|
|
return this.call({ method: 'tor.rotate-service', params: { name } })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async torToggleApp(appId: string, enabled: boolean): Promise<{ app_id: string; enabled: boolean; changed: boolean; onion_address: string | null }> {
|
|
|
|
|
return this.call({ method: 'tor.toggle-app', params: { app_id: appId, enabled } })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async torCleanupRotated(): Promise<{ cleaned: string[]; count: number }> {
|
|
|
|
|
return this.call({ method: 'tor.cleanup-rotated' })
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 15:03:34 +00:00
|
|
|
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 },
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-12 00:19:30 +00:00
|
|
|
async detectUsbDevices(): Promise<{
|
|
|
|
|
devices: Array<{
|
|
|
|
|
type: string
|
|
|
|
|
vendor_id: string
|
|
|
|
|
product_id: string
|
|
|
|
|
manufacturer: string
|
|
|
|
|
product: string
|
|
|
|
|
}>
|
|
|
|
|
}> {
|
|
|
|
|
return this.call({
|
|
|
|
|
method: 'system.detect-usb-devices',
|
|
|
|
|
params: {},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-24 22:59:20 +00:00
|
|
|
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 },
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-12 00:19:30 +00:00
|
|
|
// Federation
|
|
|
|
|
async federationInvite(): Promise<{ code: string; did: string; onion: string }> {
|
|
|
|
|
return this.call({
|
|
|
|
|
method: 'federation.invite',
|
|
|
|
|
params: {},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async federationJoin(code: string): Promise<{
|
|
|
|
|
joined: boolean
|
|
|
|
|
node: { did: string; onion: string; pubkey: string; trust_level: string }
|
|
|
|
|
}> {
|
|
|
|
|
return this.call({
|
|
|
|
|
method: 'federation.join',
|
|
|
|
|
params: { code },
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async federationListNodes(): Promise<{
|
|
|
|
|
nodes: Array<{
|
|
|
|
|
did: string
|
|
|
|
|
pubkey: string
|
|
|
|
|
onion: string
|
|
|
|
|
trust_level: string
|
|
|
|
|
added_at: string
|
|
|
|
|
name?: string
|
|
|
|
|
last_seen?: string
|
|
|
|
|
last_state?: {
|
|
|
|
|
timestamp: string
|
|
|
|
|
apps: Array<{ id: string; status: string; version?: string }>
|
|
|
|
|
cpu_usage_percent?: number
|
|
|
|
|
mem_used_bytes?: number
|
|
|
|
|
mem_total_bytes?: number
|
|
|
|
|
disk_used_bytes?: number
|
|
|
|
|
disk_total_bytes?: number
|
|
|
|
|
uptime_secs?: number
|
|
|
|
|
tor_active?: boolean
|
|
|
|
|
}
|
|
|
|
|
}>
|
|
|
|
|
}> {
|
|
|
|
|
return this.call({
|
|
|
|
|
method: 'federation.list-nodes',
|
|
|
|
|
params: {},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async federationRemoveNode(did: string): Promise<{ removed: boolean; nodes_remaining: number }> {
|
|
|
|
|
return this.call({
|
|
|
|
|
method: 'federation.remove-node',
|
|
|
|
|
params: { did },
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async federationSetTrust(
|
|
|
|
|
did: string,
|
|
|
|
|
trustLevel: 'trusted' | 'observer' | 'untrusted',
|
|
|
|
|
): Promise<{ updated: boolean; did: string; trust_level: string }> {
|
|
|
|
|
return this.call({
|
|
|
|
|
method: 'federation.set-trust',
|
|
|
|
|
params: { did, trust_level: trustLevel },
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async federationSyncState(): Promise<{
|
|
|
|
|
synced: number
|
|
|
|
|
failed: number
|
|
|
|
|
results: Array<{ did: string; status: string; apps?: number; error?: string }>
|
|
|
|
|
}> {
|
|
|
|
|
return this.call({
|
|
|
|
|
method: 'federation.sync-state',
|
|
|
|
|
params: {},
|
|
|
|
|
timeout: 120000,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async federationDeployApp(params: {
|
|
|
|
|
did: string
|
|
|
|
|
appId: string
|
|
|
|
|
version?: string
|
|
|
|
|
marketplaceUrl?: string
|
|
|
|
|
}): Promise<{ deployed: boolean; app_id: string; peer_did: string; peer_onion: string }> {
|
|
|
|
|
return this.call({
|
|
|
|
|
method: 'federation.deploy-app',
|
|
|
|
|
params: {
|
|
|
|
|
did: params.did,
|
|
|
|
|
app_id: params.appId,
|
|
|
|
|
version: params.version ?? 'latest',
|
|
|
|
|
marketplace_url: params.marketplaceUrl ?? '',
|
|
|
|
|
},
|
|
|
|
|
timeout: 180000,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// VPN
|
|
|
|
|
async vpnStatus(): Promise<{
|
|
|
|
|
connected: boolean
|
|
|
|
|
provider?: string
|
|
|
|
|
interface?: string
|
|
|
|
|
ip_address?: string
|
|
|
|
|
hostname?: string
|
|
|
|
|
peers_connected: number
|
|
|
|
|
bytes_in: number
|
|
|
|
|
bytes_out: number
|
|
|
|
|
configured: boolean
|
|
|
|
|
configured_provider: string
|
|
|
|
|
}> {
|
|
|
|
|
return this.call({
|
|
|
|
|
method: 'vpn.status',
|
|
|
|
|
params: {},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async vpnConfigure(params: {
|
|
|
|
|
provider: 'tailscale' | 'wireguard'
|
|
|
|
|
auth_key?: string
|
|
|
|
|
address?: string
|
|
|
|
|
dns?: string
|
|
|
|
|
peer?: {
|
|
|
|
|
public_key: string
|
|
|
|
|
endpoint: string
|
|
|
|
|
allowed_ips?: string
|
|
|
|
|
persistent_keepalive?: number
|
|
|
|
|
}
|
|
|
|
|
}): Promise<{ configured: boolean; provider: string; public_key?: string; address?: string }> {
|
|
|
|
|
return this.call({
|
|
|
|
|
method: 'vpn.configure',
|
|
|
|
|
params,
|
|
|
|
|
timeout: 60000,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async vpnDisconnect(): Promise<{ disconnected: boolean }> {
|
|
|
|
|
return this.call({
|
|
|
|
|
method: 'vpn.disconnect',
|
|
|
|
|
params: {},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Marketplace
|
|
|
|
|
async marketplaceDiscover(): Promise<{
|
|
|
|
|
apps: Array<{
|
|
|
|
|
manifest: {
|
|
|
|
|
app_id: string
|
|
|
|
|
name: string
|
|
|
|
|
version: string
|
|
|
|
|
description: { short: string; long: string } | string
|
|
|
|
|
author: { name: string; did: string; nostr_pubkey: string }
|
|
|
|
|
container: { image: string; ports: Array<{ container: number; host: number }>; }
|
|
|
|
|
category: string
|
|
|
|
|
icon_url: string
|
|
|
|
|
repo_url: string
|
|
|
|
|
license: string
|
|
|
|
|
}
|
|
|
|
|
trust_score: number
|
|
|
|
|
trust_tier: string
|
|
|
|
|
relay_count: number
|
|
|
|
|
first_seen: string
|
|
|
|
|
nostr_pubkey: string
|
|
|
|
|
}>
|
|
|
|
|
relay_count: number
|
|
|
|
|
}> {
|
|
|
|
|
return this.call({
|
|
|
|
|
method: 'marketplace.discover',
|
|
|
|
|
params: {},
|
|
|
|
|
timeout: 30000,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DNS
|
|
|
|
|
async dnsStatus(): Promise<{
|
|
|
|
|
provider: string
|
|
|
|
|
servers: string[]
|
|
|
|
|
doh_enabled: boolean
|
|
|
|
|
doh_url: string | null
|
|
|
|
|
resolv_conf_servers: string[]
|
|
|
|
|
}> {
|
|
|
|
|
return this.call({
|
|
|
|
|
method: 'network.dns-status',
|
|
|
|
|
params: {},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async configureDns(params: {
|
|
|
|
|
provider: 'system' | 'cloudflare' | 'google' | 'quad9' | 'mullvad' | 'custom'
|
|
|
|
|
servers?: string[]
|
|
|
|
|
}): Promise<{
|
|
|
|
|
ok: boolean
|
|
|
|
|
provider: string
|
|
|
|
|
servers: string[]
|
|
|
|
|
doh_enabled: boolean
|
|
|
|
|
doh_url: string | null
|
|
|
|
|
}> {
|
|
|
|
|
return this.call({
|
|
|
|
|
method: 'network.configure-dns',
|
|
|
|
|
params,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Disk management
|
|
|
|
|
async diskStatus(): Promise<{
|
|
|
|
|
used_bytes: number
|
|
|
|
|
total_bytes: number
|
|
|
|
|
free_bytes: number
|
|
|
|
|
used_percent: number
|
|
|
|
|
level: 'ok' | 'warning' | 'critical'
|
|
|
|
|
}> {
|
|
|
|
|
return this.call({ method: 'system.disk-status' })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async diskCleanup(): Promise<{
|
|
|
|
|
freed_bytes: number
|
|
|
|
|
freed_human: string
|
|
|
|
|
actions: string[]
|
|
|
|
|
}> {
|
|
|
|
|
return this.call({
|
|
|
|
|
method: 'system.disk-cleanup',
|
|
|
|
|
timeout: 60000,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-24 22:59:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const rpcClient = new RPCClient()
|
|
|
|
|
|