// extension/src/shared/state.ts // // Single channel for popup and vault-tab UI to read/write app state and // dispatch messages to the service worker. Two registered hosts (popup, // vault tab) implement StateHost; each surface calls registerHost(this) at // boot. // // The vault_locked intercept (lines 47-74 in vault.ts pre-Phase-4) lifts // into sendMessage() here in Phase 4. Phase 1 lays the wrapper signature; // the body is a thin pass-through until Phase 4. import type { Request, Response } from './messages'; import type { PopupState, View } from './popup-state'; export interface StateHost { getState(): PopupState; setState(partial: Partial): void; navigate(view: View, extras?: Partial): void; sendMessage(request: Request): Promise; escapeHtml(s: string): string; popOutToTab(): void; isInTab(): boolean; openVaultTab(hash?: string): void; } let host: StateHost | null = null; export function registerHost(h: StateHost): void { if (host) throw new Error('state host already registered'); host = h; } /** Test-only — vitest beforeEach() calls this to break inter-test leakage. */ export function __resetHostForTests(): void { host = null; } export function getState(): PopupState { if (!host) throw new Error('No state host registered'); return host.getState(); } export function setState(partial: Partial): void { if (!host) throw new Error('No state host registered'); host.setState(partial); } export function navigate(view: View, extras?: Partial): void { if (!host) throw new Error('No state host registered'); host.navigate(view, extras); } // Requests that must NOT trigger the lock screen on a vault_locked response: // they run during cold start / unlock, before a session exists, so a // vault_locked here is expected rather than a lost session. const VAULT_LOCKED_BYPASS: ReadonlySet = new Set([ 'unlock', 'is_unlocked', ]); /** * Dispatches a request to the service worker and intercepts the `vault_locked` * response. MV3 evicts the service worker after ~30s idle, wiping the in-memory * session/manifest; the next RPC comes back `vault_locked`. Any surface (popup * or vault tab) that gets that on a non-bypassed request treats it as "session * lost" and navigates to the lock screen so the user can re-enter their * passphrase. Lifted here from vault.ts's local sendMessage in Plan C Phase 4 * so both surfaces share one channel. */ export async function sendMessage(request: Request): Promise { if (!host) throw new Error('No state host registered'); const response = await host.sendMessage(request); if ( !response.ok && response.error === 'vault_locked' && !VAULT_LOCKED_BYPASS.has(request.type) ) { host.navigate('locked', { error: 'Session expired — please unlock again.' }); } return response; } export function escapeHtml(s: string): string { if (!host) throw new Error('No state host registered'); return host.escapeHtml(s); } export function popOutToTab(): void { if (!host) throw new Error('No state host registered'); host.popOutToTab(); } export function isInTab(): boolean { if (!host) throw new Error('No state host registered'); return host.isInTab(); } export function openVaultTab(hash?: string): void { if (!host) throw new Error('No state host registered'); host.openVaultTab(hash); }