entry.ts

3.6 KB · raw

1import { setupMessageHandler, setRestoreReady } from "./messages";2import { setupAutoLock } from "./alarms";3import { setupPasskeyProxy } from "./passkey";4import { setupCommands } from "./commands";5import { setupClipboardClearAlarm } from "./clipboard";6import { restoreFromSession, isUnlocked, lock } from "./vault";7import { ensureSettingsLoaded } from "./vault";8import { isRecoveryConfirmPending } from "./recovery-pending";9import { denyAllPendingPasskeyRequests } from "./passkey";10import { hydratePersistedSyncState, setSyncRunner } from "./sync-trigger";11import { productionSyncRunner } from "./chirp-sync-runner";1213// Keep alive for MV3 service worker14chrome.runtime.onInstalled.addListener(() => {});1516// The chrome.idle.onStateChanged listener lives in ./alarms (the architectural17// home for auto-lock timing). Keeping it in only one place avoids drift.1819// Lock-on-close via port disconnect. The popup opens a nonce'd port20// ("popup-<uuid>"); when that port disconnects (popup closed), lock if21// lockOnClose is enabled. Previously also declared in the never-bundled22// index.ts (deleted in LV-5) — this is now the single registration.23//24// LV-5: the lock port name carries a per-popup-open random suffix. Another25// extension cannot open ports INTO this extension (onConnectExternal is not26// registered), so the nonce is defense-in-depth — a port named exactly27// "popup" from anything that isn't our popup no longer reliably triggers28// deny-all + lock once the legacy window closes.29const POPUP_PORT_PREFIX = "popup-";3031chrome.runtime.onConnect.addListener((port) => {32  // Accept legacy bare "popup" for one release so an in-flight popup from the33  // previous version straddling the update doesn't lose lock-on-close.34  // TODO(2026-Q3): drop the bare "popup" acceptance once v3.8.0 has rolled out.35  if (port.name !== "popup" && !port.name.startsWith(POPUP_PORT_PREFIX)) return;36  port.onDisconnect.addListener(async () => {37    await denyAllPendingPasskeyRequests();38    const s = await ensureSettingsLoaded();39    if (!isUnlocked() || s.lockOnClose === false) return;40    if (await isRecoveryConfirmPending()) return;41    lock();42  });43});4445export function start(): void {46  // MV3: the chrome.runtime.onMessage listener MUST be registered during the47  // worker's initial SYNCHRONOUS evaluation. If registration is deferred behind48  // an await, a popup/content message that wakes a cold worker is dispatched49  // before the listener exists → "receiving end does not exist", and the popup50  // misreads the failed HAS_VAULT as "no vault" and shows onboarding.51  //52  // So: kick off session-restore, hand its promise to the message layer as a53  // gate, and register the handler synchronously. Unlock-dependent handlers54  // await the gate (see handleMessage), which preserves the SEV-3-B fix — no55  // spurious lock screen — without delaying listener registration.56  const restored = restoreFromSession();57  setRestoreReady(restored);58  setupMessageHandler();59  void setupAutoLock();60  setupCommands();61  // HV-2: clipboard auto-clear backstop must survive worker eviction; the62  // listener has to be registered during initial synchronous evaluation so63  // a cold worker woken BY the alarm still handles it.64  setupClipboardClearAlarm();65  void setupPasskeyProxy();66  // Wire the chirp-sync production runner into the dispatcher. Without this67  // `triggerSyncIfReachable` is a no-op (the runner slot starts null) — the68  // pairing/transport/coordinator stack ships dormant until this assignment.69  setSyncRunner(productionSyncRunner);70  void restored.then(() => hydratePersistedSyncState()).catch(() => undefined);71}7273start();