/** * passkeys-firefox.ts — MAIN-world detection layer for Firefox 150+. * * Firefox does not expose `chrome.webAuthenticationProxy`, so Peach cannot * intercept WebAuthn calls the way it does on Chrome. Instead, this script * runs in the page's main world (per `manifest.firefox.json`'s `world: "MAIN"`) * and monkey-patches `navigator.credentials.create` / `.get` to *observe* * passkey activity and emit CustomEvents the rest of Peach can react to. * * The original calls always proceed unchanged. Peach never blocks, modifies, * or replaces the page's own WebAuthn flow from here. Registration and * authentication on behalf of the user happen separately, from the popup * context, where Firefox 150+ allows extensions to call `navigator.credentials` * directly with their own host-permitted RP ID. * * Events emitted: * `peach:passkey-create` — page invoked navigator.credentials.create with * a publicKey option set. detail = { rpId, userName } * `peach:passkey-get` — page invoked navigator.credentials.get with a * publicKey option set. detail = { rpId, allowCredentials } */ (() => { const credentials = navigator.credentials; if (!credentials || typeof credentials.create !== "function" || typeof credentials.get !== "function") { return; } const FLAG = "__peachPasskeysFirefoxAttached"; const w = window as unknown as Record; if (w[FLAG]) return; w[FLAG] = true; const originalCreate = credentials.create.bind(credentials); const originalGet = credentials.get.bind(credentials); function emit(payload: Record) { try { // postMessage crosses the MAIN/ISOLATED-world boundary on Firefox; a // CustomEvent on `window` does not. Same-origin postMessage is the // documented bridge between page-world detectors and content scripts. window.postMessage({ source: "peach-passkey-firefox", ...payload }, location.origin); } catch { /* postMessage must never break the page */ } } credentials.create = async function patchedCreate(options) { const publicKey = options?.publicKey; if (publicKey) { emit({ type: "create", rpId: publicKey.rp?.id ?? location.hostname, rpName: publicKey.rp?.name, userName: publicKey.user?.name, userDisplayName: publicKey.user?.displayName, }); } return originalCreate(options); }; credentials.get = async function patchedGet(options) { const publicKey = options?.publicKey; if (publicKey) { emit({ type: "get", rpId: publicKey.rpId ?? location.hostname, }); } return originalGet(options); }; })();