Completes PR #3 of the multi-device roadmap. Two devices of the same
identity can now be linked via a six-digit code + relay envelope
handshake. Chain-level fan-out (alpha2) and self-wipe on revoke (this
release, earlier commit) already work end-to-end.
New device — app/(auth)/pair.tsx:
* Generates fresh X25519 keypair + six-digit code locally.
* Displays them for transcription on the primary device.
* Polls /relay/inbox on its own x25519 pub every 2.5s.
* Decrypts each envelope with the session priv + sender_pub.
* Accepts only payloads where {v=1, type=pair-handshake, code matches}.
* On success, assembles a KeyFile (master Ed25519 from envelope,
x25519 from session) and redirects into (app).
Primary device — app/(app)/devices.tsx:
* "Link new device" opens a modal asking for {code, device key, name}.
* On submit: builds+submits LINK_DEVICE tx for the new pub, then
sends a one-shot relay envelope carrying {master_pub, master_priv,
master_x25519_pub, code} encrypted for the new device's x25519 pub.
* Optimistic local insert so the new row appears immediately.
Entry point — app/index.tsx:
* Third button on welcome slide 3: "Pair". Routes to /auth/pair.
Create / Import remain unchanged for fresh identities.
Security:
* Master Ed25519 priv leaves the source device ONLY inside an envelope
sealed with NaCl box for the new device's X25519 pub. The relay node
sees only ciphertext.
* Six-digit code (~20 bits) gates acceptance — an attacker who guesses
both a session pub AND the code is still filtered by the X25519
decryption itself (code match is belt-and-suspenders).
* Envelope stays in relay mailbox until TTL — no DELETE call yet;
idempotent on our side (saveKeyFile overwrites, session pub
never polled after redirect).
Known trade-offs:
* Manual transcription of a 64-char hex key is ugly. Alpha4 will
offer a QR fallback on phones with cameras; desktop keeps typing.
* No rate limit on the polling. Fine for a 1-minute handshake, needs
cap-on-stale if a user leaves the screen open.