passkeys-firefox-relay.ts
1/**2 * passkeys-firefox-relay.ts — extension-world relay for the MAIN-world3 * passkey proxy (passkeys-firefox.ts).4 *5 * The MAIN-world script cannot call chrome.runtime.sendMessage — it lives in6 * the page's JS context, not the extension's. This relay listens for the7 * MAIN-world's PEACH_PASSKEY_REQUEST window messages, forwards each payload8 * to the background, and posts the result back as PEACH_PASSKEY_RESPONSE9 * with the same request id.10 *11 * Trust boundary: anything the page can synthesize on `window.message` lands12 * here. Only the message types in ALLOWED_RELAY_TYPES are allowed through to13 * the background. Vault/admin messages must never be proxied from page JS.14 *15 * Removal: deleted alongside the MAIN-world proxy when Firefox ships16 * chrome.webAuthenticationProxy. See src/shared/passkey-flags.ts.17 */1819const ALLOWED_RELAY_TYPES = new Set([20 "PASSKEY_CREATE",21 "PASSKEY_GET",22 "PASSKEY_HAS_CREDENTIALS",23 "INITIATE_FIREFOX_PASSKEY_CREATE",24 "PASSKEY_OFFER_DECLINE",25 "GET_PASSKEY_OFFER_DECLINED",26 "GET_PASSKEY_FOR_DOMAIN",27]);28// NOTE: COMPLETE_FIREFOX_PASSKEY_CREATE is deliberately NOT relayable — it is29// only ever sent by the approval popup (extension page) and is additionally30// sender-gated to extension origins in messages.ts (Codex Security 2026-08-04).31// PASSKEY_AUTH_RESPONSE is also NOT relayable: approval originates from the32// extension-side toast (content script sends chrome.runtime.sendMessage33// directly, not via this relay) or the approval popup (APPROVE_PASSKEY /34// DENY_PASSKEY). No MAIN-world path needs it; allowing it here would let a35// page brute-force the approval requestId. (Audit 2026-08-20 #1.)3637window.addEventListener("message", async (event: MessageEvent) => {38 if (event.source !== window) return;39 const data = event.data as40 | { type?: string; id?: string; payload?: unknown }41 | undefined;42 if (!data || data.type !== "PEACH_PASSKEY_REQUEST") return;43 const { id, payload } = data;44 if (!payload || typeof payload !== "object") return;45 const payloadType = (payload as { type?: unknown }).type;46 if (typeof payloadType !== "string" || !ALLOWED_RELAY_TYPES.has(payloadType)) {47 return;48 }49 if (typeof id !== "string") return;5051 // NOTE: page-supplied fields in `payload` (including any `origin`) are NOT52 // trusted. The background re-derives the true caller origin from the message53 // sender (browser-attested) for all passkey rpId<->origin enforcement — see54 // attestedSenderOrigin in messages.ts. This relay only gates the message type.5556 const reply = (replyPayload: unknown) => {57 window.postMessage(58 { type: "PEACH_PASSKEY_RESPONSE", id, payload: replyPayload },59 window.location.origin60 );61 };6263 try {64 const response = await chrome.runtime.sendMessage(payload);65 reply(response);66 } catch {67 // The background may be momentarily unreachable (service worker spin-up,68 // extension reload). Tell the proxy to fall through to the platform69 // rather than throwing into the page.70 reply({ ok: false, error: "extension_error" });71 }72});7374export {};