feat: deploy-to-target supports .253 + mesh/federation/VPN updates
Some checks failed
Build Archipelago ISO (dev) / build-iso (push) Failing after 3m27s

- Add deploy_secondary() function for deploying to multiple LAN nodes
- --both now deploys to .198 and .253 (previously .198 only)
- Fleet deploy updated for 3 LAN nodes
- Mesh DM fixes: protocol frame format, DM-via-channel routing
- Federation pending requests, discover modal
- VPN status UI improvements
- Image versions and container specs updates

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-04-18 11:07:08 -04:00
parent e210376e05
commit 9dd802998c
38 changed files with 3773 additions and 697 deletions

View File

@@ -15,6 +15,21 @@ export interface RPCResponse<T> {
}
}
/// Mirrors `crate::federation::pending::PendingPeerRequest` on the backend.
export type PendingState = 'pending' | 'sent' | 'approved' | 'rejected' | 'expired'
export interface PendingPeerRequest {
id: string
from_nostr_pubkey: string
from_nostr_npub: string
from_did: string
from_name: string | null
message: string | null
received_at: string
state: PendingState
outbound: boolean
}
function getCsrfToken(): string | null {
const match = document.cookie.match(/(?:^|;\s*)csrf_token=([^;]+)/)
if (match) return match[1]!
@@ -262,7 +277,7 @@ class RPCClient {
// ─── Node Identity ───────────────────────────────────────────────
async getNodeDid(): Promise<{ did: string; pubkey: string }> {
async getNodeDid(): Promise<{ did: string; pubkey: string; nostr_pubkey?: string; nostr_npub?: string }> {
return this.call({
method: 'node.did',
params: {},
@@ -579,6 +594,21 @@ class RPCClient {
})
}
async meshContactsList(): Promise<{
contacts: Array<{ pubkey: string; alias?: string | null; notes?: string | null; pinned?: boolean; blocked?: boolean }>
}> {
return this.call({ method: 'mesh.contacts-list', params: {} })
}
async meshContactsSave(
pubkey: string,
alias?: string | null,
): Promise<{ saved: boolean; pubkey: string; alias: string | null }> {
const params: Record<string, unknown> = { pubkey }
if (alias !== undefined) params.alias = alias
return this.call({ method: 'mesh.contacts-save', params })
}
async federationListNodes(): Promise<{
nodes: Array<{
did: string
@@ -590,6 +620,7 @@ class RPCClient {
last_seen?: string
last_state?: {
timestamp: string
node_name?: string
apps: Array<{ id: string; status: string; version?: string }>
cpu_usage_percent?: number
mem_used_bytes?: number
@@ -598,6 +629,7 @@ class RPCClient {
disk_total_bytes?: number
uptime_secs?: number
tor_active?: boolean
nostr_npub?: string
}
}>
}> {
@@ -624,6 +656,92 @@ class RPCClient {
})
}
// Nostr peer-discovery — see `core/archipelago/src/nostr_handshake.rs`.
// None of these methods ever exchange the local onion address on a public
// relay. `handshake.discover` returns presence-only events (DID + npub);
// `handshake.connect` ships a NIP-44-encrypted PeerRequest with no onion;
// `handshake.poll` queues inbound requests into the federation pending
// inbox for manual approval (it does NOT auto-accept).
async nostrDiscoveryStatus(): Promise<{ enabled: boolean }> {
return this.call({ method: 'nostr.discovery-status', params: {} })
}
async nostrSetDiscovery(enabled: boolean): Promise<{ enabled: boolean }> {
return this.call({
method: 'nostr.set-discovery',
params: { enabled },
timeout: 30000,
})
}
async handshakeDiscover(): Promise<{
nodes: Array<{
nostr_pubkey: string
nostr_npub: string
did: string
version: string
}>
}> {
return this.call({ method: 'handshake.discover', params: {}, timeout: 30000 })
}
async handshakeConnect(
recipient: string,
message?: string,
name?: string,
): Promise<{ ok: boolean; sent_to: string; id: string }> {
return this.call({
method: 'handshake.connect',
params: {
recipient_nostr_pubkey: recipient,
...(message ? { message } : {}),
...(name ? { name } : {}),
},
timeout: 30000,
})
}
async handshakePoll(): Promise<{
polled: number
new_requests: PendingPeerRequest[]
applied_invites: string[]
rejected_outbound: string[]
skipped: string[]
}> {
return this.call({ method: 'handshake.poll', params: {}, timeout: 30000 })
}
async federationListPendingRequests(): Promise<{
requests: PendingPeerRequest[]
}> {
return this.call({ method: 'federation.list-pending-requests', params: {} })
}
async federationApproveRequest(id: string): Promise<{ approved: boolean; id: string }> {
return this.call({
method: 'federation.approve-request',
params: { id },
timeout: 30000,
})
}
async federationRejectRequest(
id: string,
reason?: string,
notify = false,
): Promise<{ rejected: boolean; id: string }> {
return this.call({
method: 'federation.reject-request',
params: {
id,
...(reason ? { reason } : {}),
notify,
},
timeout: 30000,
})
}
async federationSyncState(): Promise<{
synced: number
failed: number