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]>
The iOS [HostID: HostConnection] multiplexer: connect to all paired Macs at once, mirror the active
one, instant host switching, cross-host badge/Live Activity. Compile + demo-verified; the live
multi-connection path needs a two-Mac test (may be rolled back).
Resolves the RemoteStore.swift overlap with dev's Tailnet-node-name/auth-key change by taking the
multiplexer version (which moved the tailnet config into HostConnection); dev's authKey removal is
re-applied there.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
# Conflicts:
# ios/NucleicRemote/NucleicRemote/Models/RemoteStore.swift
Begins the simultaneous multiplexer. HostConnection.swift is the per-host connection engine lifted
out of RemoteStore: one instance owns a single Mac's SyncClient, LAN→tailnet candidate chain,
reconnect backoff, tailnet-node lifecycle, and event stream, and keeps that Mac's projection
(connectivity, sessions, dashboard, capabilities, meshPeers). It talks back to its owner through a
Callbacks struct so aggregate concerns — the badge, Live Activity, notifications, and the single open
transcript — can be merged across hosts by the coordinator.
Not yet wired: RemoteStore still runs its own inline single-connection machine. The next step swaps
RemoteStore onto a `[HostID: HostConnection]` map (connect all paired Macs, aggregate their sessions,
route intents by host) — the behavior-critical part, to be verified with two real Macs. Compiles
(iOS Simulator BUILD SUCCEEDED); no behavior change yet (the engine is unused until the rewire).
Also: RemoteStore.friendlyTransportError made non-private so the engine shares it.
Co-Authored-By: Claude Opus 4.8 <[email protected]>