01-vault-id-in-pair-identity.md
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_idis?[]const u8 = nullin peach-sync and parsed withignore_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_idkeep working unchanged — the field is optional on both the TS interface and the helper struct. - The helper does not send
vault_idback (pairing.zig:173-180), so the extension'sdecodeFramedoes 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
LocalIdentityfor pairing must callvaultId = await computeVaultId(masterKey)(sync-identity.ts:75) and set it on the identity. If the caller has no master key (locked vault), it leavesvaultIdundefined 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.
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.
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).
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).
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,
});
encodeFrameisJSON.stringify(line 63-65): whenvaultIdisundefined,JSON.stringifydrops 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:
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_idformat is the 64-char lowercase hex BLAKE3 output ofcomputeVaultId(sync-identity.ts:28,81). Keep it lowercase hex to matchSYNC_HELLO'svaultIdso 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).