entry.ts

5.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 { applyStorageHardening, 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, abortSyncRunnerSessions } from "./sync-runner";18import {19  drainAndRefreshLockedSyncSpool,20  refreshLockedSyncSpool,21} from "./sync-vault-adapter";22import { clearLockedSyncPrivateKey } from "./locked-sync-spool";23import { setLockedSyncHooks } from "./vault";24import { repairPairedRevocationOverlaps } from "./paired-devices";25import {26  requestMeshRelayReconcile,27  setupMeshRelayListener,28  abortMeshRelaySessions,29  selectProductionDialTargets,30} from "./mesh-relay-listener";31import { hardenExtensionStorage } from "./storage-access";32import { recordInstalledVersion } from "../shared/release-notes";3334chrome.runtime.onInstalled.addListener((details) => { void recordInstalledVersion(details); });3536// The chrome.idle.onStateChanged listener lives in ./alarms (the architectural37// home for auto-lock timing). Keeping it in only one place avoids drift.3839// Lock-on-close via port disconnect. The popup opens a nonce'd port40// ("popup-<uuid>"); when that port disconnects (popup closed), lock if41// lockOnClose is enabled. Previously also declared in the never-bundled42// index.ts (deleted in LV-5) — this is now the single registration.43//44// LV-5: the lock port name carries a per-popup-open random suffix. Another45// extension cannot open ports INTO this extension (onConnectExternal is not46// registered), so the nonce is defense-in-depth — a port named exactly47// "popup" from anything that isn't our popup no longer reliably triggers48// deny-all + lock once the legacy window closes.49const POPUP_PORT_PREFIX = "popup-";5051chrome.runtime.onConnect.addListener((port) => {52  if (!port.name.startsWith(POPUP_PORT_PREFIX)) return;53  const senderUrl = port.sender?.url;54  const extensionOrigin = new URL(chrome.runtime.getURL("")).origin;55  if (port.sender?.tab || !senderUrl || new URL(senderUrl).origin !== extensionOrigin) return;56  port.onDisconnect.addListener(async () => {57    await denyAllPendingPasskeyRequests();58    const s = await ensureSettingsLoaded();59    if (!isUnlocked() || s.lockOnClose !== true) return;60    if (await isRecoveryConfirmPending()) return;61    await lock();62  });63});6465export function start(): void {66  // PB-26-203: establish the storage trust boundary first, then decide67  // whether the session escrow may operate, then restore. The composed68  // promise is handed to the message layer as a gate; the handler itself is69  // still registered synchronously below (MV3 requirement).70  const hardening = hardenExtensionStorage();71  setLockedSyncHooks({72    refresh: refreshLockedSyncSpool,73    afterUnlock: drainAndRefreshLockedSyncSpool,74    onDecoy: () => {75      void clearLockedSyncPrivateKey();76      abortMeshRelaySessions();77      abortSyncRunnerSessions();78    },79  });80  // MV3: the chrome.runtime.onMessage listener MUST be registered during the81  // worker's initial SYNCHRONOUS evaluation. If registration is deferred behind82  // an await, a popup/content message that wakes a cold worker is dispatched83  // before the listener exists → "receiving end does not exist", and the popup84  // misreads the failed HAS_VAULT as "no vault" and shows onboarding.85  //86  // So: kick off session-restore, hand its promise to the message layer as a87  // gate, and register the handler synchronously. Unlock-dependent handlers88  // await the gate (see handleMessage), which preserves the SEV-3-B fix — no89  // spurious lock screen — without delaying listener registration.90  const restored = hardening91    .then((h) => applyStorageHardening(h.sessionEscrowAvailable))92    .then(() => restoreFromSession());93  setRestoreReady(restored);94  setupMessageHandler();95  void setupAutoLock();96  setupCommands();97  // HV-2: clipboard auto-clear backstop must survive worker eviction; the98  // listener has to be registered during initial synchronous evaluation so99  // a cold worker woken BY the alarm still handles it.100  setupClipboardClearAlarm();101  void setupPasskeyProxy();102  // Wire the sync production runner into the dispatcher. Without this103  // `triggerSyncIfReachable` is a no-op (the runner slot starts null) — the104  // pairing/transport/coordinator stack ships dormant until this assignment.105  setSyncRunner(productionSyncRunner);106  setSyncDialTargetSelector(selectProductionDialTargets);107  setupMeshRelayListener();108  // 10-minute background sync. Must be registered during initial synchronous109  // evaluation for the same reason as the clipboard alarm: a cold worker110  // woken BY the sync alarm needs the listener already attached. Setup is111  // idempotent — calling it after a SW revival is safe.112  setupSyncAlarm();113  // First dial without waiting for the alarm tick. The runner is wired and114  // the vault is unlocked enough that listPairedDevices works — the in-flight115  // guard inside triggerSyncIfReachable handles the case where another path116  // (e.g. a fresh pairing completing on the same wake) is already running.117  void restored118    .then(() => requestMeshRelayReconcile())119    .then(() => repairPairedRevocationOverlaps())120    .then(() => hydratePersistedSyncState())121    .then(() => triggerSyncIfReachable({ reason: "startup" }))122    .catch(() => undefined);123}124125start();