passkeys-firefox.ts
1/**2 * passkeys-firefox.ts — MAIN-world detection layer for Firefox 150+.3 *4 * Firefox does not expose `chrome.webAuthenticationProxy`, so Peach cannot5 * intercept WebAuthn calls the way it does on Chrome. Instead, this script6 * runs in the page's main world (per `manifest.firefox.json`'s `world: "MAIN"`)7 * and monkey-patches `navigator.credentials.create` / `.get` to *observe*8 * passkey activity and emit CustomEvents the rest of Peach can react to.9 *10 * The original calls always proceed unchanged. Peach never blocks, modifies,11 * or replaces the page's own WebAuthn flow from here. Registration and12 * authentication on behalf of the user happen separately, from the popup13 * context, where Firefox 150+ allows extensions to call `navigator.credentials`14 * directly with their own host-permitted RP ID.15 *16 * Events emitted:17 * `peach:passkey-create` — page invoked navigator.credentials.create with18 * a publicKey option set. detail = { rpId, userName }19 * `peach:passkey-get` — page invoked navigator.credentials.get with a20 * publicKey option set. detail = { rpId, allowCredentials }21 */2223(() => {24 const credentials = navigator.credentials;25 if (!credentials || typeof credentials.create !== "function" || typeof credentials.get !== "function") {26 return;27 }2829 const FLAG = "__peachPasskeysFirefoxAttached";30 const w = window as unknown as Record<string, unknown>;31 if (w[FLAG]) return;32 w[FLAG] = true;3334 const originalCreate = credentials.create.bind(credentials);35 const originalGet = credentials.get.bind(credentials);3637 credentials.create = async function patchedCreate(options) {38 const publicKey = options?.publicKey;39 if (publicKey) {40 try {41 window.dispatchEvent(42 new CustomEvent("peach:passkey-create", {43 detail: {44 rpId: publicKey.rp?.id ?? location.hostname,45 userName: publicKey.user?.name,46 },47 })48 );49 } catch { /* dispatching must never break the page */ }50 }51 return originalCreate(options);52 };5354 credentials.get = async function patchedGet(options) {55 const publicKey = options?.publicKey;56 if (publicKey) {57 try {58 window.dispatchEvent(59 new CustomEvent("peach:passkey-get", {60 detail: {61 rpId: publicKey.rpId ?? location.hostname,62 allowCredentials: publicKey.allowCredentials,63 },64 })65 );66 } catch { /* dispatching must never break the page */ }67 }68 return originalGet(options);69 };70})();