Context
The sync wizard tries OPAQUE login, falls back to OPAQUE register on a 4xx, and falls back to a synthetic pending session on the special "user exists but pending approval" 403. The code path for that 403 builds a pending session with empty deviceId/saltSync/devicePubkey/devicePrivkey/ekFingerprint (placeholders, see runner.ts:135-148).
Problem / Observation
- extension/src/background/sync/runner.ts:135-148 saves a
PendingSyncSession with empty strings for saltSync, devicePubkey, devicePrivkey, ekFingerprint.
- The polling loop (runner.ts:195) hits
/auth/approval-status/<userId>. When the admin approves, the upgrade path runs (runner.ts:208-215) and writes status: "approved" — but the device keypair fields are still empty strings.
- Any later push (engine.ts:122-136) calls
deriveEncryptionKey(session, master) which calls splitMasterKey(mk, new Uint8Array(16)) and compares the resulting EK's fingerprint to the session's ekFingerprint. The latter is the empty string — so every push throws "master mismatch" (shared/sync/auth.ts:197), which the engine swallows. The user sees a connected, approved session that never actually pushes anything.
- Symptom from the user's perspective: "sync connected, but nothing reaches the server".
Force send returns { pushed: 0, failed: N }.
- Compounds with rotating-master scenarios: after a master change, the same path would lock the user out of sync.
Proposed approach
- Drop the synthetic pending-session shortcut. When the login leg returns 403 pending_approval, the runner should register (or skip if the server's identity for this email is incompatible). The synthetic session is a footgun.
- If the shortcut stays, complete the synthesis: derive saltSync, generate a real device keypair, derive
ekFingerprint, save those. The current placeholder design saves bytes for almost no UX win.
- On every approved push that throws
"master mismatch", surface a "Reconnect to sync" banner in the popup — currently the error is silently swallowed in syncAccountChange.
Acceptance criteria
Context
The sync wizard tries OPAQUE login, falls back to OPAQUE register on a 4xx, and falls back to a synthetic pending session on the special "user exists but pending approval" 403. The code path for that 403 builds a pending session with empty
deviceId/saltSync/devicePubkey/devicePrivkey/ekFingerprint(placeholders, seerunner.ts:135-148).Problem / Observation
PendingSyncSessionwith empty strings forsaltSync,devicePubkey,devicePrivkey,ekFingerprint./auth/approval-status/<userId>. When the admin approves, the upgrade path runs (runner.ts:208-215) and writesstatus: "approved"— but the device keypair fields are still empty strings.deriveEncryptionKey(session, master)which callssplitMasterKey(mk, new Uint8Array(16))and compares the resulting EK's fingerprint to the session'sekFingerprint. The latter is the empty string — so every push throws"master mismatch"(shared/sync/auth.ts:197), which the engine swallows. The user sees a connected, approved session that never actually pushes anything.Force sendreturns{ pushed: 0, failed: N }.Proposed approach
ekFingerprint, save those. The current placeholder design saves bytes for almost no UX win."master mismatch", surface a "Reconnect to sync" banner in the popup — currently the error is silently swallowed insyncAccountChange.Acceptance criteria
runner.test.tsadds a fixture exercising the 403 pending_approval path and asserts the saved session has non-emptysaltSync,devicePubkey,devicePrivkey,ekFingerprint(or no session at all).master mismatcherror fromderiveEncryptionKeyis surfaced as a popup-level notification, not silently swallowed.