1# Change 1 — Add `vault_id` to the PAIR_IDENTITY frame23> **DRAFT.** Conforms to `_GROUND-TRUTH.md`. Target file:4> `/Users/june/projects/peach-browser/src/shared/chirp-handshake.ts`.56## Goal7Have the extension **send** `vault_id` in the `PAIR_IDENTITY` frame so the8desktop helper can persist which vault it was paired to. peach-sync already9accepts `vault_id` as an **optional** field10(`peach-sync/src/pairing.zig:54`, tolerated/ignored when absent), so this is11purely additive and backwards-compatible.1213## Backwards-compatibility statement14- `vault_id` is `?[]const u8 = null` in peach-sync and parsed with15 `ignore_unknown_fields` (`pairing.zig:54,57,167`); it is consumed defensively16 (`orelse ""`, persisted only when non-empty — `pairing.zig:209,212-214`).17- Existing chirp peers (mobile Android/iOS) that **omit** `vault_id` keep18 working unchanged — the field is optional on both the TS interface and the19 helper struct.20- The helper does not send `vault_id` back (`pairing.zig:173-180`), so the21 extension's `decodeFrame` does not require it on receive. The patch below only22 affects the OUTBOUND frame.2324## Privacy consideration (carry the existing note forward)25`vault_id` is the same stable, LAN-broadcast identifier documented at26`src/shared/sync-identity.ts:62-74` (PRIVACY NOTE / PCH-02-006): it is stable27across sessions and lets a passive LAN observer link every device sharing a28vault. Putting it in `PAIR_IDENTITY` keeps it inside the **encrypted** NK29transport, so this change does not add network exposure beyond what mDNS sync30discovery already leaks. The patch comment references that note rather than31restating it.3233## Threading: where `vault_id` enters the handshake34`computeVaultId` is async and needs the in-memory `masterKey`35(`sync-identity.ts:75-82`), neither of which is in scope inside36`runPairingInitiator`/`runPairingResponder`. The clean seam is the existing37`LocalIdentity` argument already passed into both functions: add an optional38`vaultId` field, computed by the caller that already holds the master key (the39pairing-flow entry point in the popup/background that builds `LocalIdentity`).40No master key is introduced into `chirp-handshake.ts`.4142- Caller obligation (documented, not patched here): the code that constructs43 `LocalIdentity` for pairing must call44 `vaultId = await computeVaultId(masterKey)` (`sync-identity.ts:75`) and set it45 on the identity. If the caller has no master key (locked vault), it leaves46 `vaultId` undefined and the field is omitted — still valid.4748---4950## Patch (apply to `src/shared/chirp-handshake.ts`)5152### Hunk A — extend `LocalIdentity` with optional `vaultId`53Context: lines 43-51.54```diff55 export interface LocalIdentity {56 deviceId: string;57 deviceName: string;58 deviceType: DeviceType;59 longTermPublicKeyHex: string;60 signingPublicKeyHex?: string;61 /** Server-only field — Android/iOS/desktop sync hosts announce their port. */62 wsPort?: number;63+ /**64+ * Stable per-vault id (`computeVaultId`, sync-identity.ts). Sent in the65+ * outbound PAIR_IDENTITY so a desktop helper can record which vault it66+ * paired to. Optional: the caller omits it when the vault is locked (no67+ * master key). Privacy: this is the LAN-linkable id — see the PRIVACY NOTE68+ * at sync-identity.ts:62 (PCH-02-006).69+ */70+ vaultId?: string;71 }72```7374### Hunk B — add `vault_id` to the `PairingFrame` interface75Context: lines 53-61.76```diff77 interface PairingFrame {78 type: "PAIR_IDENTITY";79 device_id: string;80 device_name: string;81 device_type: DeviceType;82 long_term_public_key_hex: string;83 signing_public_key_hex?: string;84 ws_port?: number;85+ // Optional: present only when the sender knows its vault id. peach-sync86+ // treats this as optional (pairing.zig:54); chirp mobile peers omit it.87+ vault_id?: string;88 }89```9091### Hunk C — emit `vault_id` from the INITIATOR frame92Context: lines 106-114 (`runPairingInitiator`).93```diff94 const ourFrame = encodeFrame({95 type: "PAIR_IDENTITY",96 device_id: ourIdentity.deviceId,97 device_name: ourIdentity.deviceName,98 device_type: ourIdentity.deviceType,99 long_term_public_key_hex: ourIdentity.longTermPublicKeyHex,100 signing_public_key_hex: ourIdentity.signingPublicKeyHex,101 ws_port: ourIdentity.wsPort,102+ vault_id: ourIdentity.vaultId,103 });104```105106### Hunk D — emit `vault_id` from the RESPONDER frame107Context: lines 209-217 (`runPairingResponder`).108```diff109 const ourFrame = encodeFrame({110 type: "PAIR_IDENTITY",111 device_id: ourIdentity.deviceId,112 device_name: ourIdentity.deviceName,113 device_type: ourIdentity.deviceType,114 long_term_public_key_hex: ourIdentity.longTermPublicKeyHex,115 signing_public_key_hex: ourIdentity.signingPublicKeyHex,116 ws_port: ourIdentity.wsPort,117+ vault_id: ourIdentity.vaultId,118 });119```120121> `encodeFrame` is `JSON.stringify` (line 63-65): when `vaultId` is `undefined`,122> `JSON.stringify` **drops the key entirely**, so the omitted-field123> backwards-compat behaviour is automatic — no extra guard needed.124125### (Optional) Hunk E — validate `vault_id` on decode if present126`decodeFrame` (lines 67-85) currently ignores extra fields. The helper never127sends `vault_id`, so receive-side validation is **not required** for desktop128pairing. If a future symmetric flow has the peer send it, add a lenient guard:129```diff130 if (131 parsed.signing_public_key_hex !== undefined &&132 !/^[0-9a-f]{64}$/.test(parsed.signing_public_key_hex)133 ) {134 throw new Error("signing_public_key_hex is not 32-byte hex");135 }136+ // vault_id, when present, is a 64-char lowercase hex BLAKE3 digest137+ // (computeVaultId). Lenient: only validate shape if the peer sent one.138+ if (parsed.vault_id !== undefined && !/^[0-9a-f]{64}$/.test(parsed.vault_id)) {139+ throw new Error("vault_id is not 32-byte hex");140+ }141 return parsed as PairingFrame;142```143**Recommendation:** ship Hunks A–D only for the desktop-pairing milestone; defer144Hunk E until a peer actually sends `vault_id`, to avoid over-constraining.145146## Notes for the implementer147- `vault_id` format is the 64-char lowercase hex BLAKE3 output of148 `computeVaultId` (`sync-identity.ts:28,81`). Keep it lowercase hex to match149 `SYNC_HELLO`'s `vaultId` so the helper's later vault check150 (`sync_coordinator.zig:141`) compares equal.151- Do not log `vault_id`. It is the LAN-linkable id (privacy note above).