12345678910111213141516import { execFile } from "node:child_process";17import { promisify } from "node:util";18import { cp, mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";19import { existsSync } from "node:fs";20import { createServer as createHttpServer } from "node:http";21import { tmpdir } from "node:os";22import { join, resolve } from "node:path";23import net from "node:net";24import { start } from "geckodriver";2526const execFileAsync = promisify(execFile);27const ROOT = resolve(new URL("..", import.meta.url).pathname);28const DIST = join(ROOT, "dist-firefox");29const WEB_EXT = join(ROOT, "node_modules/.bin/web-ext");30const TEST_BACKGROUND = join(ROOT, "tests/fixtures/firefox-passkey-background.js");31const CONTEXT_PROBE = join(ROOT, "tests/fixtures/firefox-passkey-context-probe.js");32const CREATE_CREDENTIAL_ID = "AQIDBA";33const GET_CREDENTIAL_ID = "BQYHCA";34const EXTENSION_ID = "[email protected]";3536function findFirefoxBinary() {37 const candidates = [38 process.env.PEACH_FIREFOX_BINARY,39 process.env.FIREFOX_BIN,40 "/usr/bin/firefox",41 "/usr/local/bin/firefox",42 "/Applications/Firefox.app/Contents/MacOS/firefox",43 "/Applications/Firefox Nightly.app/Contents/MacOS/firefox",44 ].filter(Boolean);45 return candidates.find((c) => existsSync(c));46}4748function findFreePort() {49 return new Promise((resolvePort, reject) => {50 const server = net.createServer();51 server.once("error", reject);52 server.listen(0, "127.0.0.1", () => {53 const address = server.address();54 if (!address || typeof address === "string") {55 server.close();56 reject(new Error("Could not allocate a WebDriver port"));57 return;58 }59 server.close(() => resolvePort(address.port));60 });61 });62}6364async function waitForDriver(port) {65 const deadline = Date.now() + 30_000;66 while (Date.now() < deadline) {67 try {68 const response = await fetchWithTimeout(`http://127.0.0.1:${port}/status`, {}, 500);69 if (response.ok && (await response.json()).value?.ready) return;70 } catch {71 72 }73 await new Promise((r) => setTimeout(r, 250));74 }75 throw new Error("geckodriver did not become ready within 30 seconds");76}7778async function fetchWithTimeout(url, options, timeoutMs) {79 const controller = new AbortController();80 let timeoutId;81 const timeout = new Promise((_, reject) => {82 timeoutId = setTimeout(() => reject(new Error(`Request timed out after ${timeoutMs}ms`)), timeoutMs);83 });84 try {85 return await Promise.race([86 fetch(url, { ...options, signal: controller.signal }),87 timeout,88 ]);89 } finally {90 clearTimeout(timeoutId);91 controller.abort();92 }93}9495async function webdriverRequest(port, path, options = {}) {96 const response = await fetchWithTimeout(`http://127.0.0.1:${port}${path}`, {97 ...options,98 headers: { "content-type": "application/json", ...(options.headers ?? {}) },99 }, 10_000);100 const body = await response.json();101 if (!response.ok || body.value?.error) {102 throw new Error(`WebDriver ${options.method ?? "GET"} ${path} failed: ${JSON.stringify(body)}`);103 }104 return body.value;105}106107async function buildTestExtension(tempRoot) {108 const sourceDir = join(tempRoot, "extension");109 const artifactsDir = join(tempRoot, "artifacts");110 await cp(DIST, sourceDir, { recursive: true });111 await mkdir(artifactsDir, { recursive: true });112 await cp(TEST_BACKGROUND, join(sourceDir, "firefox-passkey-background.js"));113 await cp(CONTEXT_PROBE, join(sourceDir, "firefox-passkey-context-probe.js"));114115 const manifestPath = join(sourceDir, "manifest.json");116 const manifest = JSON.parse(await readFile(manifestPath, "utf8"));117 manifest.background = {118 scripts: ["firefox-passkey-background.js"],119 };120 manifest.content_scripts.push({121 matches: ["<all_urls>"],122 js: ["firefox-passkey-context-probe.js"],123 run_at: "document_start",124 });125 await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);126127 await execFileAsync(WEB_EXT, [128 "build",129 "--source-dir", sourceDir,130 "--artifacts-dir", artifactsDir,131 "--filename", "peach-firefox-passkey-matrix.xpi",132 "--overwrite-dest",133 "--no-input",134 ], { cwd: ROOT });135 return join(artifactsDir, "peach-firefox-passkey-matrix.xpi");136}137138const PROBE_SCRIPT = `139 const done = arguments[arguments.length - 1];140 void (async () => {141142 // Verify no PEACH_PASSKEY_REQUEST postMessage is used by the bridge.143 // We check the page for any existing listeners that post PEACH_PASSKEY_REQUEST.144 const pageSourceHasRelay = document.documentElement.innerHTML.includes("PEACH_PASSKEY_REQUEST");145146 const passkeyMessages = [];147 window.addEventListener("message", (event) => {148 if (event.data?.type === "PEACH_PASSKEY_REQUEST" || event.data?.type === "PEACH_PASSKEY_RESPONSE") {149 passkeyMessages.push(event.data.type);150 }151 });152153 // 4. Exercise both wrappers against the test-only background responder.154 // Abort quickly if the native authenticator receives either request.155 const createAbort = new AbortController();156 const createTimer = setTimeout(() => createAbort.abort(), 2000);157 let createResult = null;158 let createError = null;159 try {160 const credential = await navigator.credentials.create({161 signal: createAbort.signal,162 publicKey: {163 rp: { id: "127.0.0.1", name: "Test" },164 user: { id: new Uint8Array(16), name: "test", displayName: "Test" },165 challenge: new Uint8Array(16),166 pubKeyCredParams: [{ alg: -7, type: "public-key" }],167 },168 });169 createResult = credential ? { id: credential.id, type: credential.type } : null;170 } catch (e) {171 createError = { name: e?.name ?? "Error", message: e?.message ?? String(e), stack: e?.stack ?? null };172 } finally {173 clearTimeout(createTimer);174 }175176 const getAbort = new AbortController();177 const getTimer = setTimeout(() => getAbort.abort(), 2000);178 let getResult = null;179 let getError = null;180 try {181 const credential = await navigator.credentials.get({182 signal: getAbort.signal,183 publicKey: {184 rpId: "127.0.0.1",185 challenge: new Uint8Array(16),186 allowCredentials: [{ id: new Uint8Array(32), type: "public-key" }],187 },188 });189 getResult = credential ? { id: credential.id, type: credential.type } : null;190 } catch (e) {191 getError = { name: e?.name ?? "Error", message: e?.message ?? String(e), stack: e?.stack ?? null };192 } finally {193 clearTimeout(getTimer);194 }195196 done({197 pageOrigin: location.origin,198 context: JSON.parse(document.documentElement.dataset.peachFirefoxPasskeyContext ?? "null"),199 pageSourceHasRelay,200 createResult,201 createError,202 getResult,203 getError,204 passkeyMessages,205 });206 })().catch((error) => done({207 probeError: error?.stack ?? error?.message ?? String(error),208 }));209`;210211async function run() {212 if (!existsSync(DIST)) throw new Error("dist-firefox is missing; run npm run build:firefox first");213 if (!existsSync(WEB_EXT)) throw new Error("web-ext is missing; run bun install first");214215 const tempRoot = await mkdtemp(join(tmpdir(), "peach-firefox-passkey-"));216 let driver;217 let driverPort;218 let sessionId;219 let fixtureServer;220 try {221 const xpiPath = await buildTestExtension(tempRoot);222 const firefoxBinary = findFirefoxBinary();223 const port = await findFreePort();224 driverPort = port;225 driver = await start({226 host: "127.0.0.1",227 port,228 geckoDriverVersion: "0.37.1",229 ...(firefoxBinary ? { binary: firefoxBinary } : {}),230 log: "error",231 });232 await waitForDriver(port);233234 const capabilities = {235 browserName: "firefox",236 acceptInsecureCerts: true,237 "moz:firefoxOptions": {238 args: ["-headless"],239 ...(firefoxBinary ? { binary: firefoxBinary } : {}),240 prefs: {241 "browser.shell.checkDefaultBrowser": false,242 "browser.startup.page": 0,243 "browser.startup.homepage": "about:blank",244 "remote.system-access-check.enabled": false,245 },246 },247 };248 const session = await webdriverRequest(port, "/session", {249 method: "POST",250 body: JSON.stringify({ capabilities: { alwaysMatch: capabilities } }),251 });252 sessionId = session.sessionId;253254 fixtureServer = createHttpServer((_request, response) => {255 response.writeHead(200, { "content-type": "text/html; charset=utf-8" });256 response.end("<!doctype html><html><head><title>Peach passkey test</title></head><body></body></html>");257 });258 await new Promise((resolveListen, rejectListen) => {259 fixtureServer.once("error", rejectListen);260 fixtureServer.listen(0, "127.0.0.1", resolveListen);261 });262 const fixtureAddress = fixtureServer.address();263 if (!fixtureAddress || typeof fixtureAddress === "string") {264 throw new Error("Could not allocate the Firefox fixture server port");265 }266 const fixtureOrigin = `http://127.0.0.1:${fixtureAddress.port}`;267268 269 const install = await webdriverRequest(port, `/session/${sessionId}/moz/addon/install`, {270 method: "POST",271 body: JSON.stringify({272 path: xpiPath,273 temporary: true,274 }),275 });276 if (install?.id && install.id !== EXTENSION_ID) {277 throw new Error(`Unexpected Firefox add-on id: ${install.id}`);278 }279280 await webdriverRequest(port, `/session/${sessionId}/moz/context`, {281 method: "POST",282 body: JSON.stringify({ context: "chrome" }),283 });284 const addonInfo = await webdriverRequest(port, `/session/${sessionId}/execute/async`, {285 method: "POST",286 body: JSON.stringify({287 script: `288 const done = arguments[arguments.length - 1];289 const { AddonManager } = ChromeUtils.importESModule("resource://gre/modules/AddonManager.sys.mjs");290 const { ExtensionPermissions } = ChromeUtils.importESModule("resource://gre/modules/ExtensionPermissions.sys.mjs");291 AddonManager.getAddonByID(arguments[0]).then(async (addon) => {292 if (!addon) return null;293 const policy = WebExtensionPolicy.getByID(addon.id);294 await ExtensionPermissions.add(295 addon.id,296 { permissions: [], origins: ["<all_urls>"] },297 policy.extension,298 );299 await addon.reload();300 const reloadedAddon = await AddonManager.getAddonByID(addon.id);301 const reloadedPolicy = WebExtensionPolicy.getByID(addon.id);302 return {303 id: reloadedAddon.id,304 active: reloadedAddon.isActive,305 permissions: await ExtensionPermissions.get(addon.id),306 allowedOrigins: reloadedPolicy.allowedOrigins.patterns.map((pattern) => pattern.pattern),307 };308 }).then(done, (error) => done({ error: String(error), stack: error?.stack }));309 `,310 args: [EXTENSION_ID],311 }),312 });313 await webdriverRequest(port, `/session/${sessionId}/moz/context`, {314 method: "POST",315 body: JSON.stringify({ context: "content" }),316 });317 if (!addonInfo?.active || !addonInfo.permissions?.origins?.includes("<all_urls>")) {318 throw new Error(`Firefox add-on host permission was not granted: ${JSON.stringify(addonInfo)}`);319 }320321 322 await webdriverRequest(port, `/session/${sessionId}/url`, {323 method: "POST",324 body: JSON.stringify({ url: `${fixtureOrigin}/` }),325 });326 327 await new Promise((r) => setTimeout(r, 2000));328329 const result = await webdriverRequest(port, `/session/${sessionId}/execute/async`, {330 method: "POST",331 body: JSON.stringify({ script: PROBE_SCRIPT, args: [] }),332 });333334 if (result.pageOrigin !== fixtureOrigin) {335 throw new Error(`Unexpected page origin: ${result.pageOrigin}`);336 }337338 const failures = [];339340 if (result.probeError) {341 failures.push(`page probe failed: ${result.probeError}`);342 }343344 if (result.context?.exportFunctionType !== "function" || result.context?.cloneIntoType !== "function") {345 failures.push(`Firefox privileged bridge APIs were unavailable: ${JSON.stringify(result.context)}`);346 }347348 if (result.context?.installGuard !== true) {349 failures.push(`Firefox passkey bridge did not install: ${JSON.stringify(result.context)}`);350 }351352 353 if (result.pageSourceHasRelay) {354 failures.push("PEACH_PASSKEY_REQUEST found in page source — the old relay may still be present");355 }356357 if (result.createResult?.id !== CREATE_CREDENTIAL_ID || result.createResult?.type !== "public-key") {358 failures.push(`navigator.credentials.create did not return the extension credential: ${JSON.stringify({ result: result.createResult, error: result.createError })}`);359 }360361 if (result.getResult?.id !== GET_CREDENTIAL_ID || result.getResult?.type !== "public-key") {362 failures.push(`navigator.credentials.get did not return the extension credential: ${JSON.stringify({ result: result.getResult, error: result.getError })}`);363 }364365 if (result.passkeyMessages?.length > 0) {366 failures.push(`page observed passkey postMessage traffic: ${JSON.stringify(result.passkeyMessages)}`);367 }368369 if (failures.length > 0) {370 throw new Error(`Firefox passkey matrix failures:\n - ${failures.join("\n - ")}\n Context: ${JSON.stringify(result.context)}\n Add-on: ${JSON.stringify(addonInfo)}`);371 }372373 console.log("Firefox passkey matrix passed: exportFunction bridge returned extension credentials and emitted no forgeable postMessage traffic.");374 } finally {375 if (driver && sessionId) {376 try {377 await webdriverRequest(driverPort, `/session/${sessionId}`, { method: "DELETE" });378 } catch {379 380 }381 }382 if (fixtureServer) {383 await new Promise((resolveClose) => fixtureServer.close(resolveClose));384 }385 driver?.kill("SIGTERM");386 await rm(tempRoot, { recursive: true, force: true });387 }388}389390run().catch((error) => {391 console.error(error instanceof Error ? error.message : error);392 process.exitCode = 1;393});