ISO build no longer copies netavark from build host (Debian 13/GLIBC 2.41) which broke container networking on Debian 12 targets. Rootfs already installs netavark from Debian 12 repos — just configure the backend. Install RPC now adopts existing containers (from first-boot) instead of erroring on duplicates. Container scanner extracts real versions from image tags and detects available updates against pinned versions. Frontend shows update button with version info when updates are available. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
792 lines
21 KiB
TypeScript
792 lines
21 KiB
TypeScript
// RPC Client for connecting to Archipelago backend
|
|
|
|
export interface RPCOptions {
|
|
method: string
|
|
params?: Record<string, unknown>
|
|
timeout?: number
|
|
}
|
|
|
|
export interface RPCResponse<T> {
|
|
result?: T
|
|
error?: {
|
|
code: number
|
|
message: string
|
|
data?: unknown
|
|
}
|
|
}
|
|
|
|
function getCsrfToken(): string | null {
|
|
const match = document.cookie.match(/(?:^|;\s*)csrf_token=([^;]+)/)
|
|
if (match) return match[1]!
|
|
// Fallback: check for a meta tag (useful when cookies are blocked or not yet set)
|
|
return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') ?? null
|
|
}
|
|
|
|
class RPCClient {
|
|
private static _sessionExpiredRedirecting = false
|
|
private baseUrl: string
|
|
|
|
constructor(baseUrl: string = '/rpc/v1') {
|
|
this.baseUrl = baseUrl
|
|
}
|
|
|
|
async call<T>(options: RPCOptions): Promise<T> {
|
|
const { method, params = {}, timeout = 15000 } = options
|
|
const maxRetries = 3
|
|
|
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
const controller = new AbortController()
|
|
const timeoutId = setTimeout(() => controller.abort(), timeout)
|
|
|
|
try {
|
|
const headers: Record<string, string> = {
|
|
'Content-Type': 'application/json',
|
|
}
|
|
const csrfToken = getCsrfToken()
|
|
if (csrfToken) {
|
|
headers['X-CSRF-Token'] = csrfToken
|
|
}
|
|
|
|
const response = await fetch(this.baseUrl, {
|
|
method: 'POST',
|
|
credentials: 'include', // Important for session cookies
|
|
headers,
|
|
body: JSON.stringify({ method, params }),
|
|
signal: controller.signal,
|
|
})
|
|
|
|
clearTimeout(timeoutId)
|
|
|
|
if (!response.ok) {
|
|
// Session expired — debounced redirect to login
|
|
// Use a single shared timeout to prevent redirect storms when
|
|
// multiple parallel requests all get 401 at once
|
|
if (response.status === 401 && method !== 'auth.login') {
|
|
// Clear stale auth immediately — stops App.vue watcher from
|
|
// firing more requests and prevents the router from
|
|
// optimistically navigating to /dashboard
|
|
try { localStorage.removeItem('neode-auth') } catch { /* noop */ }
|
|
|
|
const isOnboarding = window.location.pathname.startsWith('/onboarding')
|
|
console.warn(`[RPC] 401 on ${method} | path=${window.location.pathname} | onboarding=${isOnboarding} | redirecting=${RPCClient._sessionExpiredRedirecting}`)
|
|
if (!isOnboarding && !RPCClient._sessionExpiredRedirecting) {
|
|
RPCClient._sessionExpiredRedirecting = true
|
|
console.warn(`[RPC] Session expired — redirecting to /login in 300ms`)
|
|
setTimeout(() => {
|
|
window.location.href = '/login'
|
|
}, 300)
|
|
}
|
|
throw new Error('Session expired')
|
|
}
|
|
// 403: read body to distinguish CSRF (retryable) from RBAC (permanent)
|
|
if (response.status === 403) {
|
|
let reason = ''
|
|
try {
|
|
const body: RPCResponse<unknown> = await response.json()
|
|
reason = body.error?.message || ''
|
|
} catch { /* body parse failed */ }
|
|
|
|
const isCsrf = !reason || reason.toLowerCase().includes('csrf')
|
|
if (isCsrf && attempt < maxRetries - 1) {
|
|
// CSRF mismatch — cookie may have been updated by a concurrent
|
|
// Set-Cookie response not yet visible to JS. Retry after delay.
|
|
await new Promise((r) => setTimeout(r, 500))
|
|
continue
|
|
}
|
|
throw new Error(reason || `HTTP 403: Forbidden`)
|
|
}
|
|
const err = new Error(`HTTP ${response.status}: ${response.statusText}`)
|
|
const isRetryable = response.status === 502 || response.status === 503
|
|
if (isRetryable && attempt < maxRetries - 1) {
|
|
const delay = 600 * (attempt + 1)
|
|
await new Promise((r) => setTimeout(r, Math.floor(delay * (0.5 + Math.random() * 0.5))))
|
|
continue
|
|
}
|
|
throw err
|
|
}
|
|
|
|
const data: RPCResponse<T> = 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) {
|
|
const delay = 600 * (attempt + 1)
|
|
await new Promise((r) => setTimeout(r, Math.floor(delay * (0.5 + Math.random() * 0.5))))
|
|
continue
|
|
}
|
|
throw timeoutErr
|
|
}
|
|
const msg = error.message
|
|
const isRetryable = /502|503|Bad Gateway|fetch|network/i.test(msg)
|
|
if (isRetryable && attempt < maxRetries - 1) {
|
|
const delay = 600 * (attempt + 1)
|
|
await new Promise((r) => setTimeout(r, Math.floor(delay * (0.5 + Math.random() * 0.5))))
|
|
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<void> {
|
|
return this.call({
|
|
method: 'auth.logout',
|
|
params: {},
|
|
})
|
|
}
|
|
|
|
async completeOnboarding(): Promise<boolean> {
|
|
return this.call({
|
|
method: 'auth.onboardingComplete',
|
|
params: {},
|
|
})
|
|
}
|
|
|
|
async isOnboardingComplete(): Promise<boolean> {
|
|
return this.call({
|
|
method: 'auth.isOnboardingComplete',
|
|
params: {},
|
|
})
|
|
}
|
|
|
|
async resetOnboarding(): Promise<boolean> {
|
|
return this.call({
|
|
method: 'auth.resetOnboarding',
|
|
params: {},
|
|
})
|
|
}
|
|
|
|
// ─── Seed Management ───────────────────────────────────────────────
|
|
|
|
async generateSeed(): Promise<{ words: string[] }> {
|
|
return this.call({ method: 'seed.generate' })
|
|
}
|
|
|
|
async verifySeed(words: string[], indices: number[]): Promise<{
|
|
verified: boolean
|
|
did: string
|
|
nostr_npub: string
|
|
}> {
|
|
return this.call({ method: 'seed.verify', params: { words, indices } })
|
|
}
|
|
|
|
async restoreSeed(words: string[]): Promise<{
|
|
did: string
|
|
nostr_npub: string
|
|
restored: boolean
|
|
}> {
|
|
return this.call({ method: 'seed.restore', params: { words } })
|
|
}
|
|
|
|
async saveSeedEncrypted(passphrase: string): Promise<{ saved: boolean }> {
|
|
return this.call({ method: 'seed.save-encrypted', params: { passphrase } })
|
|
}
|
|
|
|
async seedStatus(): Promise<{
|
|
has_seed: boolean
|
|
is_legacy: boolean
|
|
identity_count: number
|
|
next_index: number
|
|
}> {
|
|
return this.call({ method: 'seed.status' })
|
|
}
|
|
|
|
// ─── Node Identity ───────────────────────────────────────────────
|
|
|
|
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 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 },
|
|
})
|
|
}
|
|
|
|
async publishNostrIdentity(): Promise<{ event_id: string; success: number; failed: number }> {
|
|
return this.call({
|
|
method: 'node.nostr-publish',
|
|
params: {},
|
|
})
|
|
}
|
|
|
|
async getNostrPubkey(): Promise<{ nostr_pubkey: string; nostr_npub?: 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 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' })
|
|
}
|
|
|
|
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<string> {
|
|
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<Record<string, unknown>> {
|
|
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 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: {},
|
|
})
|
|
}
|
|
|
|
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 },
|
|
timeout: 660000, // Bitcoin Knots needs up to 600s for UTXO flush
|
|
})
|
|
}
|
|
|
|
async startPackage(id: string): Promise<void> {
|
|
return this.call({
|
|
method: 'package.start',
|
|
params: { id },
|
|
timeout: 60000,
|
|
})
|
|
}
|
|
|
|
async stopPackage(id: string): Promise<void> {
|
|
return this.call({
|
|
method: 'package.stop',
|
|
params: { id },
|
|
timeout: 120000,
|
|
})
|
|
}
|
|
|
|
async restartPackage(id: string): Promise<void> {
|
|
return this.call({
|
|
method: 'package.restart',
|
|
params: { id },
|
|
timeout: 120000,
|
|
})
|
|
}
|
|
|
|
async updatePackage(id: string): Promise<{ status: string }> {
|
|
return this.call({
|
|
method: 'package.update',
|
|
params: { id },
|
|
timeout: 660000, // Bitcoin Knots needs up to 600s for graceful shutdown
|
|
})
|
|
}
|
|
|
|
async getMarketplace(url: string): Promise<Record<string, unknown>> {
|
|
return this.call({
|
|
method: 'marketplace.get',
|
|
params: { url },
|
|
})
|
|
}
|
|
|
|
// 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
|
|
wg_ip?: string | null
|
|
node_npub?: string | null
|
|
relay_url?: string | null
|
|
relay_onion?: string | null
|
|
relay_direct?: string | null
|
|
}> {
|
|
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,
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
export const rpcClient = new RPCClient()
|
|
|