feat(relay): wire the live Nucleic Private Relay into the desktop + iOS apps (mesh P2 complete)
The relay Worker (nucleic-edge at relay.nucleic.blakeslee.xyz) is deployed, so land the formerly deploy-gated client side of the data path: - NucleicProtocol/Sync/RelayTransport.swift: RelayAPI (one base URL for REST + WS, membership -> connection token trade), RelayWebSocket (ordered sends, ping keepalive, ping-confirmed connect), RelayFrameChannel (client leg, WireFraming inside WS binary, presence fail-fast when the room has no host), RelayPresence. - NucleicCore/Sync/RelayAccess.swift: X25519 PoP enrollment (RelayEnrollment), room credential in the login Keychain (separate from the push credential), membership minting with re-enroll-on-401. - NucleicCore/Sync/RelayListener.swift: host SyncListener demuxing the room socket into per-tag virtual FrameChannels via RelayEnvelope; presence-driven reaping; backoff redial; injectable RelayRoomSocket seam for tests. - Wire: additive HostMsg.relayMembership(WireRelayMembership) pushed after every hello (SyncHost.register -> AppStore mint) so devices paired before the relay adopt it and the ~90-day token refreshes on each connect; the pairing QR also carries a bootstrap membership so first contact can ride the relay. Old clients ignore the unknown tag. - AppStore: .relay joins the listener composite behind the Connection-methods checkbox (failure degrades to a status row), advertises relayRoomID in PeerAddresses, mints the QR bootstrap in beginPairing. - Desktop UI: the Nucleic Private Relay toggle is enabled (was "coming soon"); the LAN-only banner offers it alongside Tailnet. - iOS: relay is the last dial candidate in HostConnection pair + reconnect (10s handshake watchdog); PairedHost persists relayRoomID/relayMembershipToken/relayURL; the relayMembership push updates the registry in place; Settings shows Relay in Transports. Tests: RelayTransportTests, RelayListenerTests, SyncHostTests relay push + QR bootstrap. Full suite green (783 core + 113 protocol + 2 new); iOS simulator build succeeds. Live smoke test against the deployed Worker passed end-to-end (PoP enroll, both token tiers, two-socket frame round-trip through the Room DO with correct envelope tags). Known limits: host revoke-on-unpair not wired (endpoint is admin-only); PeerClient (Mac<->Mac) doesn't dial the relay yet. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
@@ -21,6 +21,13 @@ struct PairedHost: Codable, Equatable {
|
||||
var transport: String?
|
||||
var tailnetHost: String?
|
||||
var tailnetPort: UInt16?
|
||||
/// Nucleic Private Relay bootstrap (mesh P2): the host's room, this device's membership
|
||||
/// token, and an optional base-URL override. Set from the pairing QR and refreshed by
|
||||
/// the host's `relayMembership` push on every connect; nil while the host has the relay
|
||||
/// method off (records predating the relay decode them as nil).
|
||||
var relayRoomID: String?
|
||||
var relayMembershipToken: String?
|
||||
var relayURL: String?
|
||||
|
||||
var transportHint: SyncTransportHint { transport.flatMap(SyncTransportHint.init(rawValue:)) ?? .lan }
|
||||
}
|
||||
@@ -105,6 +112,16 @@ enum IdentityStore {
|
||||
savePairedHosts(pairedHosts().filter { $0.fingerprint != hostID })
|
||||
}
|
||||
|
||||
/// Mutate one host's record **in place**, preserving registry order — unlike
|
||||
/// `upsertPairedHost`, this must not make the host active (it backs background
|
||||
/// refreshes like the relay-membership push, which can arrive from a non-active Mac).
|
||||
static func updatePairedHost(id hostID: String, mutate: (inout PairedHost) -> Void) {
|
||||
var hosts = pairedHosts()
|
||||
guard let index = hosts.firstIndex(where: { $0.fingerprint == hostID }) else { return }
|
||||
mutate(&hosts[index])
|
||||
savePairedHosts(hosts)
|
||||
}
|
||||
|
||||
private static func savePairedHosts(_ hosts: [PairedHost]) {
|
||||
if let data = try? JSONEncoder().encode(hosts) {
|
||||
UserDefaults.standard.set(data, forKey: pairedHostsKey)
|
||||
|
||||
Reference in New Issue
Block a user