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.
Part of PR #3. Pairing flow still to come.
Devices screen — app/(app)/devices.tsx:
* Lists every active device from /api/devices/{self}.
* "THIS DEVICE" badge on our own row, Unlink button on every other.
* Unlink confirms + submits UNLINK_DEVICE tx, optimistic local removal.
* Pull-to-refresh; empty state when balance is too low for auto-link.
* Placeholder row for "Link new device" — wired in next commit.
Settings → Devices entry row: added under a new "Devices" section.
Self-wipe on revoke — lib/storage.ts + app/(app)/_layout.tsx:
* New AsyncStorage marker `dchain_device_registered` tracks whether
this install ever made it into the on-chain registry.
* wipeAllLocalState() zeroes secure-store key + contacts + settings +
chats cache + marker. Safe-idempotent.
* Bootstrap effect in app layout splits three branches by
(our_pub in chain's active list × marker_set):
- in list → mark registered, done.
- not in list + was registered → REVOKED → wipe + redirect to auth.
- not in list + never registered → first boot, LINK_DEVICE.
* Network errors never trigger wipe — only an explicit "pub missing
from chain response" decides it. Belt-and-suspenders against a
misbehaving node spuriously dropping records.
Next: pairing flow so a second device (desktop, tablet, new phone)
can come online, show a 6-digit code, receive master priv via a
one-shot relay envelope encrypted to its fresh device X25519 pub,
then self-link.