// src/mobile/entry.ts — the mobile WebView's single entrypoint (god document §4.3). // // Order is load-bearing: the chrome shim must install `globalThis.chrome` // before any other module evaluates, and the brain boots before the UI // mounts so `bridge.sendMessage`'s first call has a handler waiting. // // Found on a real device (emulator run, 2026-07-01): a plain static // `import "./chrome-shim"` followed by other static imports does NOT // guarantee this ordering once bundled. ES module *evaluation* order follows // the import graph, not source-line order within one file — and Rollup // inlined chrome-shim.ts (small, single-use) directly into this chunk's own // body, positioned AFTER this chunk's own `import` statements for // background/entry's heavier dependencies (which get hoisted regardless of // where the `import "../background/entry"` line sits in the source). Net // effect: those dependencies evaluated and threw `chrome is not defined` // before chrome-shim's inlined code ever ran. // // The fix: keep `./chrome-shim` as the ONLY static import in this file, and // load everything else via dynamic `import()` — a dynamic import is a plain // expression evaluated in this module's own top-level synchronous flow, not // a hoisted declaration, so it genuinely cannot start until chrome-shim's // static import has fully finished (chrome-shim has no dependencies of its // own to delay it). import "./chrome-shim"; async function boot(): Promise { // `background/entry.ts` self-invokes `start()` at its own module bottom // (verified against source — diverges from the god document's illustrative // sketch, which shows calling `start()` again explicitly; doing that would // run session-restore, setupAutoLock, and setupClipboardClearAlarm a second // time). Importing it for the side effect is sufficient. await import("../background/entry"); // `popup/index.tsx` self-mounts `` and, on native platforms, also // calls `setupMessageHandler()` again — harmless, since that registration // is idempotent (see chrome-shim.ts's dispatch comment for why running it // twice doesn't invoke `handleMessage` twice). await import("../popup/index"); const { ensureSettingsLoaded, isUnlocked, lock } = await import("../background/vault"); const { isRecoveryConfirmPending } = await import("../background/recovery-pending"); const { denyAllPendingPasskeyRequests } = await import("../background/passkey"); const { nativePlugin } = await import("../shared/native"); // Lock-on-close, mobile-shaped (§4.4 point 1): entry.ts's real lock-on-close // listens for the popup's `chrome.runtime.onConnect` port disconnecting — // there is no such port here, so peach-android's NativeBridge pushes // foreground/background over the bridge instead. Same intent (mirrors // entry.ts's onDisconnect body exactly), native trigger. nativePlugin<{ onLifecycle(cb: (s: "foreground" | "background") => void): void }>("PeachNative")?.onLifecycle( (state) => { if (state !== "background") return; void (async () => { await denyAllPendingPasskeyRequests(); const settings = await ensureSettingsLoaded(); if (!isUnlocked() || settings.lockOnClose === false) return; if (await isRecoveryConfirmPending()) return; lock(); })(); } ); } void boot();