/** * passkeys-firefox-relay.ts — extension-world relay for the MAIN-world * passkey proxy (passkeys-firefox.ts). * * The MAIN-world script cannot call chrome.runtime.sendMessage — it lives in * the page's JS context, not the extension's. This relay listens for the * MAIN-world's PEACH_PASSKEY_REQUEST window messages, forwards each payload * to the background, and posts the result back as PEACH_PASSKEY_RESPONSE * with the same request id. * * Trust boundary: anything the page can synthesize on `window.message` lands * here. Only the message types in ALLOWED_RELAY_TYPES are allowed through to * the background. Vault/admin messages must never be proxied from page JS. * * Removal: deleted alongside the MAIN-world proxy when Firefox ships * chrome.webAuthenticationProxy. See src/shared/passkey-flags.ts. */ const ALLOWED_RELAY_TYPES = new Set([ "PASSKEY_CREATE", "PASSKEY_GET", "PASSKEY_HAS_CREDENTIALS", "INITIATE_FIREFOX_PASSKEY_CREATE", "PASSKEY_OFFER_DECLINE", "GET_PASSKEY_OFFER_DECLINED", "GET_PASSKEY_FOR_DOMAIN", ]); // NOTE: COMPLETE_FIREFOX_PASSKEY_CREATE is deliberately NOT relayable — it is // only ever sent by the approval popup (extension page) and is additionally // sender-gated to extension origins in messages.ts (Codex Security 2026-08-04). // PASSKEY_AUTH_RESPONSE is also NOT relayable: approval originates from the // extension-side toast (content script sends chrome.runtime.sendMessage // directly, not via this relay) or the approval popup (APPROVE_PASSKEY / // DENY_PASSKEY). No MAIN-world path needs it; allowing it here would let a // page brute-force the approval requestId. (Audit 2026-08-20 #1.) window.addEventListener("message", async (event: MessageEvent) => { if (event.source !== window) return; const data = event.data as | { type?: string; id?: string; payload?: unknown } | undefined; if (!data || data.type !== "PEACH_PASSKEY_REQUEST") return; const { id, payload } = data; if (!payload || typeof payload !== "object") return; const payloadType = (payload as { type?: unknown }).type; if (typeof payloadType !== "string" || !ALLOWED_RELAY_TYPES.has(payloadType)) { return; } if (typeof id !== "string") return; // NOTE: page-supplied fields in `payload` (including any `origin`) are NOT // trusted. The background re-derives the true caller origin from the message // sender (browser-attested) for all passkey rpId<->origin enforcement — see // attestedSenderOrigin in messages.ts. This relay only gates the message type. const reply = (replyPayload: unknown) => { window.postMessage( { type: "PEACH_PASSKEY_RESPONSE", id, payload: replyPayload }, window.location.origin ); }; try { const response = await chrome.runtime.sendMessage(payload); reply(response); } catch { // The background may be momentarily unreachable (service worker spin-up, // extension reload). Tell the proxy to fall through to the platform // rather than throwing into the page. reply({ ok: false, error: "extension_error" }); } }); export {};