Parent
#130
What to build
Client-side pairing that attaches a machine to the relay space, plus the Settings "Devices" screen to manage it.
The first machine calls POST /v1/space, stores its device token, and generates the space data key K0 (32 random bytes) into the #132 safeStorage slot — this ticket owns K0's creation and storage; #134 consumes it. Adding a device is uniform (a third device later works exactly like the second): the existing device generates a fresh random 16-byte join_token, registers SHA-256(join_token) via POST /v1/pairings, and displays the pairing secret base32(join_token ‖ K0 ‖ checksum) — the 4-byte truncated-SHA-256 checksum covers the whole payload, so a transcription error in either half fails at entry instead of surfacing later as a mysterious decrypt failure. The new machine decodes and checksum-verifies the secret, calls POST /v1/join {join_hash}, and stores the returned device token + space_id plus the decoded K0 machine-locally. At space creation the app offers a recovery code base32(K0 ‖ checksum) to save, labeled honestly (decrypts relay data; does not grant relay access — total-device-loss access recovery is an operator runbook step in the relay README).
Copy-paste (with a copy button) is the primary flow. The emdash://join?secret=… deep link requires OS-level custom-protocol support that does not exist in the app today: electron-builder protocols configuration plus open-url (macOS) / second-instance argv (Windows/Linux) handling in the main process — in scope here.
The Devices screen (pattern of SshConnectionsSettingsCard) lists paired devices, adds a device (mints a secret), and offers a single Remove device action with confirmation (revokes the token and removes membership).
Acceptance criteria
Blocked by
- 1 (relay), 2 (schema: device identity + safeStorage slots)
Parent
#130
What to build
Client-side pairing that attaches a machine to the relay space, plus the Settings "Devices" screen to manage it.
The first machine calls
POST /v1/space, stores its device token, and generates the space data keyK0(32 random bytes) into the #132 safeStorage slot — this ticket owns K0's creation and storage; #134 consumes it. Adding a device is uniform (a third device later works exactly like the second): the existing device generates a fresh random 16-bytejoin_token, registersSHA-256(join_token)viaPOST /v1/pairings, and displays the pairing secretbase32(join_token ‖ K0 ‖ checksum)— the 4-byte truncated-SHA-256 checksum covers the whole payload, so a transcription error in either half fails at entry instead of surfacing later as a mysterious decrypt failure. The new machine decodes and checksum-verifies the secret, callsPOST /v1/join {join_hash}, and stores the returned device token +space_idplus the decodedK0machine-locally. At space creation the app offers a recovery codebase32(K0 ‖ checksum)to save, labeled honestly (decrypts relay data; does not grant relay access — total-device-loss access recovery is an operator runbook step in the relay README).Copy-paste (with a copy button) is the primary flow. The
emdash://join?secret=…deep link requires OS-level custom-protocol support that does not exist in the app today: electron-builderprotocolsconfiguration plusopen-url(macOS) / second-instance argv (Windows/Linux) handling in the main process — in scope here.The Devices screen (pattern of
SshConnectionsSettingsCard) lists paired devices, adds a device (mints a secret), and offers a single Remove device action with confirmation (revokes the token and removes membership).Acceptance criteria
K0in the Schéma local : path nullable + clock de sync + identité device #132 safeStorage slot; a recovery code (K0-only, checksummed) is offered at creation.join_tokenregistered viaPOST /v1/pairings); a second machine joins with it and ends up holding a device token, thespace_id, and the sameK0; a corrupted secret fails the checksum at entry with a clear error.emdash://joindeep link is registered at the OS level (electron-builderprotocols+ main-processopen-url/second-instance handling) and routes into the join flow; pairing respects single-use, TTL, and attempt limits (surfaced as clear errors).nodevitest project; the Devices screen UI itself is browser-suite coverage (not part of the agent gate — reportnot-runwhen the infra is absent).Blocked by