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 { tmpdir } from "node:os";21import { join, resolve } from "node:path";22import net from "node:net";23import { start } from "geckodriver";2425const execFileAsync = promisify(execFile);26const ROOT = resolve(new URL("..", import.meta.url).pathname);27const DIST = join(ROOT, "dist-firefox");28const WEB_EXT = join(ROOT, "node_modules/.bin/web-ext");2930function findFirefoxBinary() {31 const candidates = [32 process.env.PEACH_FIREFOX_BINARY,33 process.env.FIREFOX_BIN,34 "/usr/bin/firefox",35 "/usr/local/bin/firefox",36 "/Applications/Firefox.app/Contents/MacOS/firefox",37 "/Applications/Firefox Nightly.app/Contents/MacOS/firefox",38 ].filter(Boolean);39 return candidates.find((c) => existsSync(c));40}4142function findFreePort() {43 return new Promise((resolvePort, reject) => {44 const server = net.createServer();45 server.once("error", reject);46 server.listen(0, "127.0.0.1", () => {47 const address = server.address();48 if (!address || typeof address === "string") {49 server.close();50 reject(new Error("Could not allocate a WebDriver port"));51 return;52 }53 server.close(() => resolvePort(address.port));54 });55 });56}5758async function waitForDriver(port) {59 const deadline = Date.now() + 30_000;60 while (Date.now() < deadline) {61 try {62 const response = await fetchWithTimeout(`http://127.0.0.1:${port}/status`, {}, 500);63 if (response.ok && (await response.json()).value?.ready) return;64 } catch {65 66 }67 await new Promise((r) => setTimeout(r, 250));68 }69 throw new Error("geckodriver did not become ready within 30 seconds");70}7172async function fetchWithTimeout(url, options, timeoutMs) {73 const controller = new AbortController();74 let timeoutId;75 const timeout = new Promise((_, reject) => {76 timeoutId = setTimeout(() => reject(new Error(`Request timed out after ${timeoutMs}ms`)), timeoutMs);77 });78 try {79 return await Promise.race([80 fetch(url, { ...options, signal: controller.signal }),81 timeout,82 ]);83 } finally {84 clearTimeout(timeoutId);85 controller.abort();86 }87}8889async function webdriverRequest(port, path, options = {}) {90 const response = await fetchWithTimeout(`http://127.0.0.1:${port}${path}`, {91 ...options,92 headers: { "content-type": "application/json", ...(options.headers ?? {}) },93 }, 10_000);94 const body = await response.json();95 if (!response.ok || body.value?.error) {96 throw new Error(`WebDriver ${options.method ?? "GET"} ${path} failed: ${JSON.stringify(body)}`);97 }98 return body.value;99}100101async function buildTestExtension(tempRoot) {102 const sourceDir = join(tempRoot, "extension");103 const artifactsDir = join(tempRoot, "artifacts");104 await cp(DIST, sourceDir, { recursive: true });105 await mkdir(artifactsDir, { recursive: true });106107 await execFileAsync(WEB_EXT, [108 "build",109 "--source-dir", sourceDir,110 "--artifacts-dir", artifactsDir,111 "--filename", "peach-firefox-passkey-matrix.xpi",112 "--overwrite-dest",113 "--no-input",114 ], { cwd: ROOT });115 return join(artifactsDir, "peach-firefox-passkey-matrix.xpi");116}117118const PROBE_SCRIPT = `119 const done = arguments[arguments.length - 1];120121 // 1. Check if navigator.credentials.create is wrapped by exportFunction.122 // The wrapped function is from the page world via exportFunction, so123 // it will NOT be the native function. We check by calling toString()124 // — a native function returns "function create() { [native code] }"125 // while an exportFunction wrapper returns something else.126 const createStr = navigator.credentials.create.toString();127 const getStr = navigator.credentials.get.toString();128 const createIsNative = createStr.includes("[native code]");129 const getIsNative = getStr.includes("[native code]");130131 // 2. Verify no PEACH_PASSKEY_REQUEST postMessage is used by the bridge.132 // We check the page for any existing listeners that post PEACH_PASSKEY_REQUEST.133 const pageSourceHasRelay = document.documentElement.innerHTML.includes("PEACH_PASSKEY_REQUEST");134135 // 4. Call navigator.credentials.create with a publicKey request.136 // With a locked vault, the bridge should fall through to native137 // (return null in headless Firefox without a real authenticator).138 let createResult = null;139 let createError = null;140 try {141 createResult = await navigator.credentials.create({142 publicKey: {143 rp: { id: "example.com", name: "Test" },144 user: { id: new Uint8Array(16), name: "test", displayName: "Test" },145 challenge: new Uint8Array(16),146 pubKeyCredParams: [{ alg: -7, type: "public-key" }],147 },148 });149 } catch (e) {150 createError = e?.name ?? String(e);151 }152153 // 5. Call navigator.credentials.get — should also fall through.154 let getResult = null;155 let getError = null;156 try {157 getResult = await navigator.credentials.get({158 publicKey: {159 rpId: "example.com",160 challenge: new Uint8Array(16),161 allowCredentials: [{ id: new Uint8Array(32), type: "public-key" }],162 },163 });164 } catch (e) {165 getError = e?.name ?? String(e);166 }167168 done({169 pageOrigin: location.origin,170 createIsNative,171 getIsNative,172 pageSourceHasRelay,173 createResult,174 createError,175 getResult,176 getError,177 });178`;179180async function run() {181 if (!existsSync(DIST)) throw new Error("dist-firefox is missing; run npm run build:firefox first");182 if (!existsSync(WEB_EXT)) throw new Error("web-ext is missing; run bun install first");183184 const tempRoot = await mkdtemp(join(tmpdir(), "peach-firefox-passkey-"));185 let driver;186 let driverPort;187 let sessionId;188 try {189 const xpiPath = await buildTestExtension(tempRoot);190 const firefoxBinary = findFirefoxBinary();191 const port = await findFreePort();192 driverPort = port;193 driver = await start({194 host: "127.0.0.1",195 port,196 geckoDriverVersion: "0.37.1",197 ...(firefoxBinary ? { binary: firefoxBinary } : {}),198 log: "error",199 });200 await waitForDriver(port);201202 const capabilities = {203 browserName: "firefox",204 acceptInsecureCerts: true,205 "moz:firefoxOptions": {206 args: ["-headless"],207 ...(firefoxBinary ? { binary: firefoxBinary } : {}),208 prefs: {209 "browser.shell.checkDefaultBrowser": false,210 "browser.startup.page": 0,211 "browser.startup.homepage": "about:blank",212 },213 },214 };215 const session = await webdriverRequest(port, "/session", {216 method: "POST",217 body: JSON.stringify({ capabilities: { alwaysMatch: capabilities } }),218 });219 sessionId = session.sessionId;220221 222 await webdriverRequest(port, `/session/${sessionId}/moz/addon/install`, {223 method: "POST",224 body: JSON.stringify({225 addon: (await readFile(xpiPath)).toString("base64"),226 temporary: true,227 }),228 });229230 231 await webdriverRequest(port, `/session/${sessionId}/url`, {232 method: "POST",233 body: JSON.stringify({ url: "https://example.com/" }),234 });235 236 await new Promise((r) => setTimeout(r, 2000));237238 const result = await webdriverRequest(port, `/session/${sessionId}/execute/async`, {239 method: "POST",240 body: JSON.stringify({ script: PROBE_SCRIPT, args: [] }),241 });242243 if (result.pageOrigin !== "https://example.com") {244 throw new Error(`Unexpected page origin: ${result.pageOrigin}`);245 }246247 const failures = [];248249 250 if (result.createIsNative) {251 failures.push("navigator.credentials.create is not wrapped by exportFunction — the privileged bridge was not installed");252 }253254 255 if (result.getIsNative) {256 failures.push("navigator.credentials.get is not wrapped by exportFunction — the privileged bridge was not installed");257 }258259 260 if (result.pageSourceHasRelay) {261 failures.push("PEACH_PASSKEY_REQUEST found in page source — the old relay may still be present");262 }263264 265 266 if (result.createError && result.createError !== "NotAllowedError" && result.createError !== "AbortError") {267 failures.push(`navigator.credentials.create threw unexpected error: ${result.createError}`);268 }269270 271 if (result.getError && result.getError !== "NotAllowedError" && result.getError !== "AbortError") {272 failures.push(`navigator.credentials.get threw unexpected error: ${result.getError}`);273 }274275 if (failures.length > 0) {276 throw new Error(`Firefox passkey matrix failures:\n - ${failures.join("\n - ")}`);277 }278279 console.log("Firefox passkey matrix passed: exportFunction bridge installed, no forgeable relay, bridge falls through with locked vault.");280 } finally {281 if (driver && sessionId) {282 try {283 await webdriverRequest(driverPort, `/session/${sessionId}`, { method: "DELETE" });284 } catch {285 286 }287 }288 driver?.kill("SIGTERM");289 await rm(tempRoot, { recursive: true, force: true });290 }291}292293run().catch((error) => {294 console.error(error instanceof Error ? error.message : error);295 process.exitCode = 1;296});