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. Check no PEACH_PASSKEY_RESPONSE postMessage listener exists.132 // Dispatch a forged response and verify no handler picks it up.133 let forgedResponseReceived = false;134 const forgedListener = (event) => {135 if (event.data?.type === "PEACH_PASSKEY_RESPONSE") {136 forgedResponseReceived = true;137 }138 };139 window.addEventListener("message", forgedListener);140 window.postMessage(141 { type: "PEACH_PASSKEY_RESPONSE", id: "forged-uuid-test", payload: { ok: true } },142 window.location.origin,143 );144 await new Promise((r) => setTimeout(r, 200));145 window.removeEventListener("message", forgedListener);146147 // 3. Verify no PEACH_PASSKEY_REQUEST postMessage is used by the bridge.148 // We check the page for any existing listeners that post PEACH_PASSKEY_REQUEST.149 const pageSourceHasRelay = document.documentElement.innerHTML.includes("PEACH_PASSKEY_REQUEST");150151 // 4. Call navigator.credentials.create with a publicKey request.152 // With a locked vault, the bridge should fall through to native153 // (return null in headless Firefox without a real authenticator).154 let createResult = null;155 let createError = null;156 try {157 createResult = await navigator.credentials.create({158 publicKey: {159 rp: { id: "example.com", name: "Test" },160 user: { id: new Uint8Array(16), name: "test", displayName: "Test" },161 challenge: new Uint8Array(16),162 pubKeyCredParams: [{ alg: -7, type: "public-key" }],163 },164 });165 } catch (e) {166 createError = e?.name ?? String(e);167 }168169 // 5. Call navigator.credentials.get — should also fall through.170 let getResult = null;171 let getError = null;172 try {173 getResult = await navigator.credentials.get({174 publicKey: {175 rpId: "example.com",176 challenge: new Uint8Array(16),177 allowCredentials: [{ id: new Uint8Array(32), type: "public-key" }],178 },179 });180 } catch (e) {181 getError = e?.name ?? String(e);182 }183184 done({185 pageOrigin: location.origin,186 createIsNative,187 getIsNative,188 forgedResponseReceived,189 pageSourceHasRelay,190 createResult,191 createError,192 getResult,193 getError,194 });195`;196197async function run() {198 if (!existsSync(DIST)) throw new Error("dist-firefox is missing; run npm run build:firefox first");199 if (!existsSync(WEB_EXT)) throw new Error("web-ext is missing; run bun install first");200201 const tempRoot = await mkdtemp(join(tmpdir(), "peach-firefox-passkey-"));202 let driver;203 let driverPort;204 let sessionId;205 try {206 const xpiPath = await buildTestExtension(tempRoot);207 const firefoxBinary = findFirefoxBinary();208 const port = await findFreePort();209 driverPort = port;210 driver = await start({211 host: "127.0.0.1",212 port,213 geckoDriverVersion: "0.37.1",214 ...(firefoxBinary ? { binary: firefoxBinary } : {}),215 log: "error",216 });217 await waitForDriver(port);218219 const capabilities = {220 browserName: "firefox",221 acceptInsecureCerts: true,222 "moz:firefoxOptions": {223 args: ["-headless"],224 ...(firefoxBinary ? { binary: firefoxBinary } : {}),225 prefs: {226 "browser.shell.checkDefaultBrowser": false,227 "browser.startup.page": 0,228 "browser.startup.homepage": "about:blank",229 },230 },231 };232 const session = await webdriverRequest(port, "/session", {233 method: "POST",234 body: JSON.stringify({ capabilities: { alwaysMatch: capabilities } }),235 });236 sessionId = session.sessionId;237238 239 await webdriverRequest(port, `/session/${sessionId}/moz/addon/install`, {240 method: "POST",241 body: JSON.stringify({242 addon: (await readFile(xpiPath)).toString("base64"),243 temporary: true,244 }),245 });246247 248 await webdriverRequest(port, `/session/${sessionId}/url`, {249 method: "POST",250 body: JSON.stringify({ url: "https://example.com/" }),251 });252 253 await new Promise((r) => setTimeout(r, 2000));254255 const result = await webdriverRequest(port, `/session/${sessionId}/execute/async`, {256 method: "POST",257 body: JSON.stringify({ script: PROBE_SCRIPT, args: [] }),258 });259260 if (result.pageOrigin !== "https://example.com") {261 throw new Error(`Unexpected page origin: ${result.pageOrigin}`);262 }263264 const failures = [];265266 267 if (result.createIsNative) {268 failures.push("navigator.credentials.create is not wrapped by exportFunction — the privileged bridge was not installed");269 }270271 272 if (result.getIsNative) {273 failures.push("navigator.credentials.get is not wrapped by exportFunction — the privileged bridge was not installed");274 }275276 277 if (result.forgedResponseReceived) {278 failures.push("a PEACH_PASSKEY_RESPONSE postMessage listener exists — the forgeable relay was not removed");279 }280281 282 if (result.pageSourceHasRelay) {283 failures.push("PEACH_PASSKEY_REQUEST found in page source — the old relay may still be present");284 }285286 287 288 if (result.createError && result.createError !== "NotAllowedError" && result.createError !== "AbortError") {289 failures.push(`navigator.credentials.create threw unexpected error: ${result.createError}`);290 }291292 293 if (result.getError && result.getError !== "NotAllowedError" && result.getError !== "AbortError") {294 failures.push(`navigator.credentials.get threw unexpected error: ${result.getError}`);295 }296297 if (failures.length > 0) {298 throw new Error(`Firefox passkey matrix failures:\n - ${failures.join("\n - ")}`);299 }300301 console.log("Firefox passkey matrix passed: exportFunction bridge installed, no forgeable relay, bridge falls through with locked vault.");302 } finally {303 if (driver && sessionId) {304 try {305 await webdriverRequest(driverPort, `/session/${sessionId}`, { method: "DELETE" });306 } catch {307 308 }309 }310 driver?.kill("SIGTERM");311 await rm(tempRoot, { recursive: true, force: true });312 }313}314315run().catch((error) => {316 console.error(error instanceof Error ? error.message : error);317 process.exitCode = 1;318});