entry.ts

4.7 KB · raw

1import { setupMessageHandler, setRestoreReady } from "./messages";2import { setupAutoLock } from "./alarms";3import { setupSyncAlarm } from "./sync-alarm";4import { setupPasskeyProxy } from "./passkey";5import { setupCommands } from "./commands";6import { setupClipboardClearAlarm } from "./clipboard";7import { restoreFromSession, isUnlocked, lock } from "./vault";8import { ensureSettingsLoaded } from "./vault";9import { isRecoveryConfirmPending } from "./recovery-pending";10import { denyAllPendingPasskeyRequests } from "./passkey";11import {12  hydratePersistedSyncState,13  setSyncDialTargetSelector,14  setSyncRunner,15  triggerSyncIfReachable,16} from "./sync-trigger";17import { productionSyncRunner } from "./sync-runner";18import {19  drainAndRefreshLockedSyncSpool,20  refreshLockedSyncSpool,21} from "./sync-vault-adapter";22import { setLockedSyncHooks } from "./vault";23import { repairPairedRevocationOverlaps } from "./paired-devices";24import {25  requestMeshRelayReconcile,26  selectProductionDialTargets,27  setupMeshRelayListener,28} from "./mesh-relay-listener";2930// Keep alive for MV3 service worker31chrome.runtime.onInstalled.addListener(() => {});3233// The chrome.idle.onStateChanged listener lives in ./alarms (the architectural34// home for auto-lock timing). Keeping it in only one place avoids drift.3536// Lock-on-close via port disconnect. The popup opens a nonce'd port37// ("popup-<uuid>"); when that port disconnects (popup closed), lock if38// lockOnClose is enabled. Previously also declared in the never-bundled39// index.ts (deleted in LV-5) — this is now the single registration.40//41// LV-5: the lock port name carries a per-popup-open random suffix. Another42// extension cannot open ports INTO this extension (onConnectExternal is not43// registered), so the nonce is defense-in-depth — a port named exactly44// "popup" from anything that isn't our popup no longer reliably triggers45// deny-all + lock once the legacy window closes.46const POPUP_PORT_PREFIX = "popup-";4748chrome.runtime.onConnect.addListener((port) => {49  if (!port.name.startsWith(POPUP_PORT_PREFIX)) return;50  port.onDisconnect.addListener(async () => {51    await denyAllPendingPasskeyRequests();52    const s = await ensureSettingsLoaded();53    if (!isUnlocked() || s.lockOnClose !== true) return;54    if (await isRecoveryConfirmPending()) return;55    lock();56  });57});5859export function start(): void {60  setLockedSyncHooks({61    refresh: refreshLockedSyncSpool,62    afterUnlock: drainAndRefreshLockedSyncSpool,63  });64  // MV3: the chrome.runtime.onMessage listener MUST be registered during the65  // worker's initial SYNCHRONOUS evaluation. If registration is deferred behind66  // an await, a popup/content message that wakes a cold worker is dispatched67  // before the listener exists → "receiving end does not exist", and the popup68  // misreads the failed HAS_VAULT as "no vault" and shows onboarding.69  //70  // So: kick off session-restore, hand its promise to the message layer as a71  // gate, and register the handler synchronously. Unlock-dependent handlers72  // await the gate (see handleMessage), which preserves the SEV-3-B fix — no73  // spurious lock screen — without delaying listener registration.74  const restored = restoreFromSession();75  setRestoreReady(restored);76  setupMessageHandler();77  void setupAutoLock();78  setupCommands();79  // HV-2: clipboard auto-clear backstop must survive worker eviction; the80  // listener has to be registered during initial synchronous evaluation so81  // a cold worker woken BY the alarm still handles it.82  setupClipboardClearAlarm();83  void setupPasskeyProxy();84  // Wire the sync production runner into the dispatcher. Without this85  // `triggerSyncIfReachable` is a no-op (the runner slot starts null) — the86  // pairing/transport/coordinator stack ships dormant until this assignment.87  setSyncRunner(productionSyncRunner);88  setSyncDialTargetSelector(selectProductionDialTargets);89  setupMeshRelayListener();90  // 10-minute background sync. Must be registered during initial synchronous91  // evaluation for the same reason as the clipboard alarm: a cold worker92  // woken BY the sync alarm needs the listener already attached. Setup is93  // idempotent — calling it after a SW revival is safe.94  setupSyncAlarm();95  // First dial without waiting for the alarm tick. The runner is wired and96  // the vault is unlocked enough that listPairedDevices works — the in-flight97  // guard inside triggerSyncIfReachable handles the case where another path98  // (e.g. a fresh pairing completing on the same wake) is already running.99  void restored100    .then(() => requestMeshRelayReconcile())101    .then(() => repairPairedRevocationOverlaps())102    .then(() => hydratePersistedSyncState())103    .then(() => triggerSyncIfReachable({ reason: "startup" }))104    .catch(() => undefined);105}106107start();