entry.ts

4.3 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 { hydratePersistedSyncState, setSyncRunner, triggerSyncIfReachable } from "./sync-trigger";12import { productionSyncRunner } from "./sync-runner";1314// Keep alive for MV3 service worker15chrome.runtime.onInstalled.addListener(() => {});1617// The chrome.idle.onStateChanged listener lives in ./alarms (the architectural18// home for auto-lock timing). Keeping it in only one place avoids drift.1920// Lock-on-close via port disconnect. The popup opens a nonce'd port21// ("popup-<uuid>"); when that port disconnects (popup closed), lock if22// lockOnClose is enabled. Previously also declared in the never-bundled23// index.ts (deleted in LV-5) — this is now the single registration.24//25// LV-5: the lock port name carries a per-popup-open random suffix. Another26// extension cannot open ports INTO this extension (onConnectExternal is not27// registered), so the nonce is defense-in-depth — a port named exactly28// "popup" from anything that isn't our popup no longer reliably triggers29// deny-all + lock once the legacy window closes.30const POPUP_PORT_PREFIX = "popup-";3132chrome.runtime.onConnect.addListener((port) => {33  // Accept legacy bare "popup" for one release so an in-flight popup from the34  // previous version straddling the update doesn't lose lock-on-close.35  // TODO(2026-Q3): drop the bare "popup" acceptance once v3.8.0 has rolled out.36  if (port.name !== "popup" && !port.name.startsWith(POPUP_PORT_PREFIX)) return;37  port.onDisconnect.addListener(async () => {38    await denyAllPendingPasskeyRequests();39    const s = await ensureSettingsLoaded();40    if (!isUnlocked() || s.lockOnClose === false) return;41    if (await isRecoveryConfirmPending()) return;42    lock();43  });44});4546export function start(): void {47  // MV3: the chrome.runtime.onMessage listener MUST be registered during the48  // worker's initial SYNCHRONOUS evaluation. If registration is deferred behind49  // an await, a popup/content message that wakes a cold worker is dispatched50  // before the listener exists → "receiving end does not exist", and the popup51  // misreads the failed HAS_VAULT as "no vault" and shows onboarding.52  //53  // So: kick off session-restore, hand its promise to the message layer as a54  // gate, and register the handler synchronously. Unlock-dependent handlers55  // await the gate (see handleMessage), which preserves the SEV-3-B fix — no56  // spurious lock screen — without delaying listener registration.57  const restored = restoreFromSession();58  setRestoreReady(restored);59  setupMessageHandler();60  void setupAutoLock();61  setupCommands();62  // HV-2: clipboard auto-clear backstop must survive worker eviction; the63  // listener has to be registered during initial synchronous evaluation so64  // a cold worker woken BY the alarm still handles it.65  setupClipboardClearAlarm();66  void setupPasskeyProxy();67  // Wire the sync production runner into the dispatcher. Without this68  // `triggerSyncIfReachable` is a no-op (the runner slot starts null) — the69  // pairing/transport/coordinator stack ships dormant until this assignment.70  setSyncRunner(productionSyncRunner);71  // 10-minute background sync. Must be registered during initial synchronous72  // evaluation for the same reason as the clipboard alarm: a cold worker73  // woken BY the sync alarm needs the listener already attached. Setup is74  // idempotent — calling it after a SW revival is safe.75  setupSyncAlarm();76  // First dial without waiting for the alarm tick. The runner is wired and77  // the vault is unlocked enough that listPairedDevices works — the in-flight78  // guard inside triggerSyncIfReachable handles the case where another path79  // (e.g. a fresh pairing completing on the same wake) is already running.80  void restored81    .then(() => hydratePersistedSyncState())82    .then(() => triggerSyncIfReachable({ reason: "startup" }))83    .catch(() => undefined);84}8586start();