INTEGRATION-CONTRACT.md
Desktop Helper Pairing — Integration Contract
AUTHORITATIVE. The single document an integrator follows to apply the
DESKTOP-PAIRINGdraft set to the livepeach-browsersource. It reconciles every individual draft (01–06) into one apply plan: file map, order, message contract, new types, deps, CSS, test count, and the consolidated TODO list.All
file:linecitations are against source as read 2026-06-16. Where a draft and this contract disagree, this contract wins (it is the reconciliation).
1. File map — each draft → its destination on apply
| Draft file | What it describes | Destination on apply | Action |
|---|---|---|---|
_GROUND-TRUTH.md |
Wire contract pin | (none — reference only) | keep in repo as design record |
README.md |
Orientation | (none — reference only) | keep as design record |
INTEGRATION-CONTRACT.md |
This document | (none — reference only) | keep as design record |
01-vault-id-in-pair-identity.md |
Add vault_id to outbound PAIR_IDENTITY |
edit src/shared/chirp-handshake.ts (Hunks A–D; defer Hunk E) |
apply patch |
02-desktop-device-type.md |
"desktop" DeviceType |
NO-OP — already in src/shared/sync-types.ts:11; storage already accepts it |
optional regression test only |
03-desktop-pairing-handler.md |
Background code-pairing client | new file src/background/desktop-pairing.ts + 3 case blocks in src/background/messages.ts + openBinaryWebSocket helper |
create + wire |
04-desktop-pairing-ui.md |
Popup pairing UI | new file src/popup/components/DesktopPairingFlow.tsx + entry-point edits in src/popup/views/ChirpSync.tsx + CSS in src/popup/styles/popup.css |
create + wire |
05-subnet-probe.md |
Handshake-free port probe | new file src/background/desktop-pairing-probe.ts |
create |
06-sync-coordinator-desktop.md |
Include desktop in sync | edit src/background/sync-trigger.ts (one filter line) |
apply patch |
tests/01-pair-identity-vault-id.test.ts |
Change 1 spec | src/shared/chirp-pair-identity-vault-id.test.ts (or merge into chirp-handshake.test.ts) |
relocate |
tests/03-desktop-pairing.test.ts |
Change 3 spec | src/background/desktop-pairing.test.ts |
relocate, fix ../../→../ |
tests/04-desktop-pairing-ui.test.tsx |
Change 4 spec | src/popup/components/DesktopPairingFlow.test.tsx |
relocate (paths already written for that location) |
tests/06-sync-coordinator-desktop.test.ts |
Change 6 spec | src/background/sync-trigger.desktop.test.ts (or merge into sync-trigger.test.ts) |
relocate, fix ../../→../ |
On apply, the entire
src/drafts/DESKTOP-PAIRING/tree can be deleted (usetrash, neverrm) once the destinations above exist — it is design-time scaffolding, not shipped code.
2. Integration order (dependency-correct)
Apply in this sequence; each step compiles on its own.
- Change 1 —
vault_idintoPAIR_IDENTITY(chirp-handshake.ts, Hunks A–D). AddsLocalIdentity.vaultId+PairingFrame.vault_id. Nothing downstream type-checks againstvaultIduntil this lands. - Change 2 — verify-only. Confirm
"desktop"∈DeviceType(sync-types.ts:11); optionally add the round-trip regression test. No code. - Change 5 —
desktop-pairing-probe.ts(probeSubnetForPort). Standalone; Change 3 imports it. - Change 3 —
desktop-pairing.ts(the client +DesktopPairingError,DesktopPairingSocket, message-payload shapes) + theopenBinaryWebSockethelper. Depends on Change 1 (vaultIdon identity) and Change 5 (findHost). messages.tscases — the threePAIR_WITH_DESKTOP/DESKTOP_PAIR_CONFIRMhandlers and the two background pushes. Depends on Change 3's module. Gate exactly likeCHIRP_GET_LOCAL_IDENTITY.- Change 4 —
DesktopPairingFlow.tsx+ChirpSync.tsxentry point + thepeach-desktop-*/peach-code-inputCSS. Depends on the message contract (step 5) existing. - Change 6 — widen the
sync-trigger.tshost filter to include"desktop". Independent of the UI; can land any time after pairing persists a desktop device, but logically last (it's the "now actually sync it" step).
3. The reconciled popup ↔ background message contract (CANONICAL)
Reconciled across 03 (defined four messages) and 04 (uses all four; pinned
{ confirmed } not the brief's { accept }). 03 and 04 AGREE — 04 adopts
03's DESKTOP_PAIR_RESULT terminal push and 03's { confirmed: boolean }
payload. The brief's three-message / {accept:true} shape is superseded. Final
canonical shapes:
// ── popup → background ─────────────────────────────────────────────────────
/** Start pairing with the typed 6-digit code. */
{ type: "PAIR_WITH_DESKTOP", payload: { code: string } }
// response: { ok: true } | { ok: false, error: DesktopPairingError }
// (synchronous start failures — e.g. vault_locked — come back HERE;
// the SAS and the terminal result arrive LATER as pushes.)
/** User accepted/rejected the SAS shown by DESKTOP_PAIR_SAS. */
{ type: "DESKTOP_PAIR_CONFIRM", payload: { confirmed: boolean } }
// response: { ok: true }
// ── background → popup (pushes via chrome.runtime.sendMessage) ──────────────
/** SAS ready — show the confirm gate. */
{ type: "DESKTOP_PAIR_SAS", payload: { sas: string } }
/** Terminal outcome. */
{ type: "DESKTOP_PAIR_RESULT",
payload:
| { status: "complete", device: PairedDevice }
| { status: "error", error: DesktopPairingError } }
DesktopPairingError (defined in desktop-pairing.ts, Change 3):
type DesktopPairingError =
| "invalid_code" // socket closed before PEACH_HELLO_ACK (wrong/expired)
| "no_host_found"
| "handshake_failed"
| "sas_mismatch"
| "vault_locked";
Bridge: sendMessage (src/shared/bridge.ts:13) for popup→bg; pushes received
via chrome.runtime.onMessage.addListener (App.tsx:601 idiom).
Reconciliation flags (resolved)
{ confirmed }vs{ accept }: use{ confirmed: boolean }(03 + 04).- Three vs four messages: four —
DESKTOP_PAIR_RESULTis required so the popup can distinguish "Paired!" from a late error (04 §1). invalid_codecollapses wrong+expired: the wire cannot distinguish them (_GROUND-TRUTH.md§0.3) — one error, one copy string.
4. New message types — where the union lives
The three new type strings (PAIR_WITH_DESKTOP, DESKTOP_PAIR_CONFIRM
inbound; DESKTOP_PAIR_SAS, DESKTOP_PAIR_RESULT outbound) are handled in the
background switch in src/background/messages.ts, alongside the CHIRP_*
cases (messages.ts:1318-1384).
Where to add the type literals: the existing CHIRP_* messages are dispatched
by string case in that switch; the integrator should follow the same pattern
the repo already uses for chirp messages — add the four case labels (two
inbound handlers in the switch; the two outbound are chrome.runtime.sendMessage
pushes, not switch cases). If the repo maintains a discriminated message-union
type for sendMessage payloads, extend it where the CHIRP_* variants are
declared (co-located with the chirp definitions — verify the exact union file at
integration; the chirp messages are the template). No change to
src/shared/types.ts is required — the brief's guess that device/message types
live there is wrong (_GROUND-TRUTH.md §0.1); message handling is in
messages.ts and sync types are in sync-types.ts.
5. New dependencies — NONE.
Confirmed: no new npm packages. Everything reuses shipped modules:
noiseNKInitiator(src/shared/noise-js.ts:71) — already present.derivePairingSAS(src/shared/chirp-handshake.ts:171) — already present.computeVaultId(src/shared/sync-identity.ts:75) — already present.WebSocket/TextEncoder/TextDecoder— platform globals.- Solid (
solid-js),vitest, the localview-test-utils— already in repo.
No package.json change.
6. New CSS hooks (Change 4 → src/popup/styles/popup.css)
The UI reuses existing classes (peach-chirp-pair, peach-chirp-head,
peach-chirp-back, peach-chirp-headline, peach-chirp-sub,
peach-chirp-action, peach-chirp-meter*, peach-btn*, peach-sr-only,
peach-underline-input, peach-unlock-input, peach-label). New additive
hooks to add a small block for in popup.css:
| New class | Purpose |
|---|---|
peach-desktop-pair |
root container modifier on the flow |
peach-code-input |
the 6-digit code field (mono, wide letter-spacing) |
peach-desktop-enter / -searching / -confirm / -paired / -error |
per-phase containers |
peach-desktop-sas |
the big SAS glyph (load-bearing layout is inline for now) |
peach-desktop-connect |
Connect button modifier |
peach-chirp-pair-start--desktop |
the "Desktop Helper" entry button in ChirpSync.tsx |
Inline styles in the draft cover the SAS/confirm layout in the interim
(mirroring ChirpSync.tsx's inline-styled SAS gate at lines 445-472), so the
CSS block is polish, not a blocker.
7. Estimated test count across the four draft test files
| Test file | it() specs |
|---|---|
tests/01-pair-identity-vault-id.test.ts |
3 |
tests/03-desktop-pairing.test.ts |
9 |
tests/04-desktop-pairing-ui.test.tsx |
12 |
tests/06-sync-coordinator-desktop.test.ts |
2 |
| Total | 26 |
(Change 2 ships no required test; an optional paired-devices round-trip
regression would add 1 if the integrator wants it.)
8. Do existing tests need updating? — NO.
Desktop pairing is additive. Verified against the existing suites:
DeviceTypeis an OPEN-ENOUGH union already."desktop"is a member (sync-types.ts:11); no existing test asserts a closed union without desktop.paired-devices.tsvalidates only field presence, not membership (02-desktop-device-type.md§"storage gate"), so existingpaired-devices.test.tsspecs are unaffected.- No existing chirp/pairing test asserts the ABSENCE of
vault_id. Change 1 emitsvault_idonly whenidentity.vaultIdis set;JSON.stringifydrops it whenundefined. The existingchirp-handshake.test.tsidentities don't setvaultId, so their serialized frames are byte-identical to before — no assertion flips. sync-trigger.test.tsdoes NOT break (Change 6). Its fixtures areANDROID,IOS,EXT— nodesktop. The two filter-pinning specs (:75-81"no mobile hosts",:83-94"once per mobile host →[ANDROID, IOS]") are unchanged by adding"desktop"to the filter: EXT stays excluded and no desktop fixture is present. The new desktop behaviour is covered by the NEW spec (tests/06-...), not by mutating an existing one.
No existing test file requires edits. All four draft test files are net-new additions.
9. Consolidated remaining-integration TODO checklist
Carried forward from every pass (01–06):
- [ ]
openBinaryWebSockethelper (Change 3 TODO 1) — adaptWebSockettoDesktopPairingSocket:binaryType="arraybuffer", queued inbound frames, and aclosedBeforeFrame()that distinguishes "closed, no ACK" (invalid_code) from "ACK received". Pattern atchirp-sync-transport.ts:58-121. - [ ]
LocalChirpIdentityvsLocalIdentityreconciliation (Change 3 TODO 2) —buildLocalIdentityreturnsLocalChirpIdentity(chirp-identity.ts:79), which lacksvaultId/wsPort. Either confirm it's assignable toLocalIdentityafter the spread{ ...base, vaultId }, or addvaultId?toLocalChirpIdentitydirectly. Extension never setswsPort(never a host) — leaving it undefined is correct. - [ ]
messages.tsgating (Change 3 §Wiring / Change 4 TODO 4) — gate the three cases onvault.isUnlocked()andrequirePro("sync")exactly likeCHIRP_GET_LOCAL_IDENTITY(messages.ts:1319-1321). Master key obtained viagetRootKeyHex()(stable root key, SEV-2-B), zeroed infinally. - [ ] Single-flight confirm guard (Change 3 TODO 4 / Change 4 TODO 5) — one
in-flight pairing per worker; store the pending
confirmresolver in a module-level variable, cleared on complete/error. On popuponCancelmid-flow, release the resolver (aDESKTOP_PAIR_CONFIRM { confirmed:false }or a cancel message). - [ ] Timeouts (Change 3 TODO 5 / Change 5 TODO 1) — wrap
findHost/receivewith ceilings (e.g. ~10s probe, ~5s per receive) so a stalled helper surfacesno_host_found/handshake_failedinstead of hanging the popup. Idiom atchirp-sync-transport.ts:174. Pairing-port probe worst case is ~8s for a /24 at 16-wide 500ms batches. - [ ] Test path adjustments — fix
../../→../prefixes in the relocated 03 and 06 specs; reuse the realmakeStorageStubfromsync-trigger.test.ts; confirmpaired-devicestest-reset export name; the 04 spec's paths are already written for its destination. - [ ] CSS block for
peach-desktop-*/peach-code-inputinpopup.css(Change 4 TODO 3) — polish; inline styles cover the load-bearing layout. - [ ] ChirpSync.tsx entry point (Change 4 TODO 2) — add the "Desktop Helper
(enter code)" affordance to
restView()action block (ChirpSync.tsx:412-423) and render<DesktopPairingFlow/>frompairingView(). - [ ] Change 6 filter — the one-line
sync-trigger.ts:104-108widening. - [ ] (Optional) Change 2 regression test — desktop device round-trips
through
paired-devices.ts.
10. Version note
The brief said bump 4.0.1 → 4.0.2. But dev is already at 4.0.2
(manifest.json "version": "4.0.2", verified 2026-06-16). Repeated versions
are rejected by the stores (auto-memory: "every code change is a version bump").
Integration must bump 4.0.2 → 4.0.3 across all three manifest targets
(manifest.json, manifest.firefox.json, and the mobile manifest) on apply —
not 4.0.2. (Drafts touch only src/drafts/, so no version bump happens at draft
time; this is an apply-time obligation.)
11. Quality-bar checklist (from the brief), annotated
| Quality bar | Status in drafts | Integrator must verify |
|---|---|---|
SAS KAT 0fbab8cd… → 895245 |
✅ pinned: derivePairingSAS reused (not re-implemented); KAT asserted in tests/03 spec "derives the SAS via the real derivePairingSAS"; cross-checked against peach-sync/src/pairing.zig:275-279 (_GROUND-TRUTH.md §5) |
run the relocated tests/03 spec — must stay green |
tsc clean |
⚠️ drafts are apply-ready TS but not compiled in place | after apply, tsc --noEmit clean — esp. the LocalChirpIdentity vs LocalIdentity reconciliation (TODO 2) and ChirpSyncSession cast in tests/06 |
| All tests green | ⚠️ 26 new specs written to repo idiom (vitest, renderView, injected deps) |
relocate the 4 files, fix import prefixes, run the full suite — 26 new + existing unchanged |
| Additive only (no existing test edits) | ✅ verified §8 — no closed-union assertion, no absence-of-vault_id assertion, no desktop fixture in sync-trigger.test.ts |
confirm the suite passes with zero edits to existing test files |
vault_id backwards-compatible |
✅ optional both ends; JSON.stringify drops undefined; peach-sync ?[]const u8 = null + ignore_unknown_fields (pairing.zig:54,57) |
confirm a chirp (mobile) pairing still works after Change 1 |
| MITM defence = user-confirmed SAS | ✅ pairWithDesktopHelper gates persistence on confirm(sas) → true; rejects → sas_mismatch, persists nothing |
confirm the messages.ts round-trip actually blocks on the popup confirm |
| Correct ports / handshakes | ✅ pairing 49142 / Noise NK / browser initiator; sync 8765 / Noise XX / browser initiator (06-...md §"which handshake for which phase") |
confirm wsPort default 8765 matches helper announce |
| No new deps | ✅ §5 — none | confirm package.json untouched |