# Change 1 — Add `vault_id` to the PAIR_IDENTITY frame > **DRAFT.** Conforms to `_GROUND-TRUTH.md`. Target file: > `/Users/june/projects/peach-browser/src/shared/chirp-handshake.ts`. ## Goal Have the extension **send** `vault_id` in the `PAIR_IDENTITY` frame so the desktop helper can persist which vault it was paired to. peach-sync already accepts `vault_id` as an **optional** field (`peach-sync/src/pairing.zig:54`, tolerated/ignored when absent), so this is purely additive and backwards-compatible. ## Backwards-compatibility statement - `vault_id` is `?[]const u8 = null` in peach-sync and parsed with `ignore_unknown_fields` (`pairing.zig:54,57,167`); it is consumed defensively (`orelse ""`, persisted only when non-empty — `pairing.zig:209,212-214`). - Existing chirp peers (mobile Android/iOS) that **omit** `vault_id` keep working unchanged — the field is optional on both the TS interface and the helper struct. - The helper does not send `vault_id` back (`pairing.zig:173-180`), so the extension's `decodeFrame` does not require it on receive. The patch below only affects the OUTBOUND frame. ## Privacy consideration (carry the existing note forward) `vault_id` is the same stable, LAN-broadcast identifier documented at `src/shared/sync-identity.ts:62-74` (PRIVACY NOTE / PCH-02-006): it is stable across sessions and lets a passive LAN observer link every device sharing a vault. Putting it in `PAIR_IDENTITY` keeps it inside the **encrypted** NK transport, so this change does not add network exposure beyond what mDNS sync discovery already leaks. The patch comment references that note rather than restating it. ## Threading: where `vault_id` enters the handshake `computeVaultId` is async and needs the in-memory `masterKey` (`sync-identity.ts:75-82`), neither of which is in scope inside `runPairingInitiator`/`runPairingResponder`. The clean seam is the existing `LocalIdentity` argument already passed into both functions: add an optional `vaultId` field, computed by the caller that already holds the master key (the pairing-flow entry point in the popup/background that builds `LocalIdentity`). No master key is introduced into `chirp-handshake.ts`. - Caller obligation (documented, not patched here): the code that constructs `LocalIdentity` for pairing must call `vaultId = await computeVaultId(masterKey)` (`sync-identity.ts:75`) and set it on the identity. If the caller has no master key (locked vault), it leaves `vaultId` undefined and the field is omitted — still valid. --- ## Patch (apply to `src/shared/chirp-handshake.ts`) ### Hunk A — extend `LocalIdentity` with optional `vaultId` Context: lines 43-51. ```diff export interface LocalIdentity { deviceId: string; deviceName: string; deviceType: DeviceType; longTermPublicKeyHex: string; signingPublicKeyHex?: string; /** Server-only field — Android/iOS/desktop sync hosts announce their port. */ wsPort?: number; + /** + * Stable per-vault id (`computeVaultId`, sync-identity.ts). Sent in the + * outbound PAIR_IDENTITY so a desktop helper can record which vault it + * paired to. Optional: the caller omits it when the vault is locked (no + * master key). Privacy: this is the LAN-linkable id — see the PRIVACY NOTE + * at sync-identity.ts:62 (PCH-02-006). + */ + vaultId?: string; } ``` ### Hunk B — add `vault_id` to the `PairingFrame` interface Context: lines 53-61. ```diff interface PairingFrame { type: "PAIR_IDENTITY"; device_id: string; device_name: string; device_type: DeviceType; long_term_public_key_hex: string; signing_public_key_hex?: string; ws_port?: number; + // Optional: present only when the sender knows its vault id. peach-sync + // treats this as optional (pairing.zig:54); chirp mobile peers omit it. + vault_id?: string; } ``` ### Hunk C — emit `vault_id` from the INITIATOR frame Context: lines 106-114 (`runPairingInitiator`). ```diff const ourFrame = encodeFrame({ type: "PAIR_IDENTITY", device_id: ourIdentity.deviceId, device_name: ourIdentity.deviceName, device_type: ourIdentity.deviceType, long_term_public_key_hex: ourIdentity.longTermPublicKeyHex, signing_public_key_hex: ourIdentity.signingPublicKeyHex, ws_port: ourIdentity.wsPort, + vault_id: ourIdentity.vaultId, }); ``` ### Hunk D — emit `vault_id` from the RESPONDER frame Context: lines 209-217 (`runPairingResponder`). ```diff const ourFrame = encodeFrame({ type: "PAIR_IDENTITY", device_id: ourIdentity.deviceId, device_name: ourIdentity.deviceName, device_type: ourIdentity.deviceType, long_term_public_key_hex: ourIdentity.longTermPublicKeyHex, signing_public_key_hex: ourIdentity.signingPublicKeyHex, ws_port: ourIdentity.wsPort, + vault_id: ourIdentity.vaultId, }); ``` > `encodeFrame` is `JSON.stringify` (line 63-65): when `vaultId` is `undefined`, > `JSON.stringify` **drops the key entirely**, so the omitted-field > backwards-compat behaviour is automatic — no extra guard needed. ### (Optional) Hunk E — validate `vault_id` on decode if present `decodeFrame` (lines 67-85) currently ignores extra fields. The helper never sends `vault_id`, so receive-side validation is **not required** for desktop pairing. If a future symmetric flow has the peer send it, add a lenient guard: ```diff if ( parsed.signing_public_key_hex !== undefined && !/^[0-9a-f]{64}$/.test(parsed.signing_public_key_hex) ) { throw new Error("signing_public_key_hex is not 32-byte hex"); } + // vault_id, when present, is a 64-char lowercase hex BLAKE3 digest + // (computeVaultId). Lenient: only validate shape if the peer sent one. + if (parsed.vault_id !== undefined && !/^[0-9a-f]{64}$/.test(parsed.vault_id)) { + throw new Error("vault_id is not 32-byte hex"); + } return parsed as PairingFrame; ``` **Recommendation:** ship Hunks A–D only for the desktop-pairing milestone; defer Hunk E until a peer actually sends `vault_id`, to avoid over-constraining. ## Notes for the implementer - `vault_id` format is the 64-char lowercase hex BLAKE3 output of `computeVaultId` (`sync-identity.ts:28,81`). Keep it lowercase hex to match `SYNC_HELLO`'s `vaultId` so the helper's later vault check (`sync_coordinator.zig:141`) compares equal. - Do not log `vault_id`. It is the LAN-linkable id (privacy note above).