12345678910111213141516171819202122232425import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";2627vi.mock("./license", async (importOriginal) => {28 const actual = await importOriginal<typeof import("./license")>();29 return {30 ...actual,31 isProUser: vi.fn(async () => true),32 };33});3435vi.mock("../shared/passkey-flags", () => ({36 FIREFOX_USE_CONTENT_SCRIPT_PROXY: false,37}));3839type StorageShape = Record<string, unknown>;4041interface ProxyListeners {42 create?: (req: { requestId: number; requestDetailsJson: string }) => void;43 get?: (req: { requestId: number; requestDetailsJson: string }) => void;44 uvpaa?: (req: { requestId: number }) => void;45 cancel?: (requestId: number) => void;46}4748interface CompleteCaptures {49 create: Array<{ requestId: number; responseJson?: string; error?: { name: string; message: string } }>;50 get: Array<{ requestId: number; responseJson?: string; error?: { name: string; message: string } }>;51 uvpaa: Array<{ requestId: number; isUvpaa: boolean }>;52}5354describe("chrome webAuthenticationProxy ceremony", () => {55 let storage: StorageShape;56 let sessionStorage: StorageShape;57 let proxyListeners: ProxyListeners;58 let completes: CompleteCaptures;59 let tabsSendMessage: ReturnType<typeof vi.fn>;60 let activeTabs: Array<{ id?: number; url?: string }>;61 let broadcastTabs: Array<{ id?: number }>;62 let openPopup: ReturnType<typeof vi.fn>;6364 beforeEach(async () => {65 vi.resetModules();66 vi.useRealTimers();67 storage = { peach_settings: { breachMonitoring: false, passkeyProviderEnabled: true } };68 sessionStorage = {};69 proxyListeners = {};70 completes = { create: [], get: [], uvpaa: [] };71 tabsSendMessage = vi.fn(async () => ({ ok: true }));72 activeTabs = [{ id: 1, url: "https://myaccount.google.com/security/passkeys" }];73 broadcastTabs = [{ id: 1 }, { id: 2 }];74 openPopup = vi.fn(async () => {});7576 const chromeMock = {77 runtime: {78 id: "peach-extension",79 getURL: vi.fn(() => "chrome-extension://abcd/"),80 onMessage: { addListener: vi.fn() },81 sendMessage: vi.fn(async () => ({ ok: true })),82 },83 tabs: {84 query: vi.fn((q: unknown, callback?: (t: Array<{ id?: number; url?: string }>) => void) => {85 const query = q as { active?: boolean; currentWindow?: boolean; lastFocusedWindow?: boolean };86 const list = query.active ? activeTabs : broadcastTabs;87 if (typeof callback === "function") { callback(list); return; }88 return Promise.resolve(list);89 }),90 sendMessage: tabsSendMessage,91 },92 storage: {93 local: {94 get: vi.fn(async (key?: string | string[]) => {95 if (!key) return storage;96 if (typeof key === "string") return { [key]: storage[key] };97 return key.reduce<Record<string, unknown>>((r, k) => { r[k] = storage[k]; return r; }, {});98 }),99 set: vi.fn(async (next: Record<string, unknown>) => Object.assign(storage, next)),100 remove: vi.fn(async (keys: string | string[]) => {101 const list = Array.isArray(keys) ? keys : [keys];102 for (const k of list) delete storage[k];103 }),104 },105 session: {106 get: vi.fn(async (key?: string | string[] | null) => {107 if (key === null || key === undefined) return sessionStorage;108 if (typeof key === "string") return { [key]: sessionStorage[key] };109 return (key as string[]).reduce<Record<string, unknown>>((r, k) => { r[k] = sessionStorage[k]; return r; }, {});110 }),111 set: vi.fn(async (next: Record<string, unknown>) => Object.assign(sessionStorage, next)),112 remove: vi.fn(async (keys: string | string[]) => {113 const list = Array.isArray(keys) ? keys : [keys];114 for (const k of list) delete sessionStorage[k];115 }),116 clear: vi.fn(async () => { sessionStorage = {}; }),117 },118 },119 action: {120 setBadgeText: vi.fn(async () => {}),121 setBadgeBackgroundColor: vi.fn(async () => {}),122 openPopup,123 },124 alarms: { clear: vi.fn(), create: vi.fn(), onAlarm: { addListener: vi.fn() } },125 idle: { setDetectionInterval: vi.fn(), onStateChanged: { addListener: vi.fn() } },126 scripting: { executeScript: vi.fn(async () => []) },127 webAuthenticationProxy: {128 attach: vi.fn(async () => {}),129 detach: vi.fn(async () => {}),130 completeCreateRequest: vi.fn(async (details: typeof completes.create[number]) => {131 completes.create.push(details);132 }),133 completeGetRequest: vi.fn(async (details: typeof completes.get[number]) => {134 completes.get.push(details);135 }),136 completeIsUvpaaRequest: vi.fn(async (details: typeof completes.uvpaa[number]) => {137 completes.uvpaa.push(details);138 }),139 onCreateRequest: { addListener: vi.fn((fn: ProxyListeners["create"]) => { proxyListeners.create = fn; }) },140 onGetRequest: { addListener: vi.fn((fn: ProxyListeners["get"]) => { proxyListeners.get = fn; }) },141 onIsUvpaaRequest: { addListener: vi.fn((fn: ProxyListeners["uvpaa"]) => { proxyListeners.uvpaa = fn; }) },142 onRequestCanceled: { addListener: vi.fn((fn: ProxyListeners["cancel"]) => { proxyListeners.cancel = fn; }) },143 },144 } as unknown as typeof chrome;145146 (globalThis as { chrome?: typeof chrome }).chrome = chromeMock;147 vi.stubGlobal("__PEACH_E2E_PASSKEYS__", false);148149 const vaultModule = await import("./vault");150 await vaultModule.ensureSettingsLoaded();151 const passkey = await import("./passkey");152 await passkey.setupPasskeyProxy();153 });154155 afterEach(() => {156 vi.unstubAllGlobals();157 });158159 it("leaves Chrome native passkeys available when interception has no saved preference", async () => {160 vi.resetModules();161 storage.peach_settings = { breachMonitoring: false };162 vi.mocked(chrome.webAuthenticationProxy.attach).mockClear();163 const passkey = await import("./passkey");164 await passkey.setupPasskeyProxy();165 expect(chrome.webAuthenticationProxy.attach).not.toHaveBeenCalled();166 await expect(passkey.getPasskeyProviderState()).resolves.toMatchObject({ enabled: false, attached: false });167 });168169 function fireCreate(options: object, requestId = 100): void {170 proxyListeners.create!({171 requestId,172 requestDetailsJson: JSON.stringify(options),173 });174 }175176 function fireGet(options: object, requestId = 200): void {177 proxyListeners.get!({178 requestId,179 requestDetailsJson: JSON.stringify(options),180 });181 }182183 async function waitForCreateComplete(requestId: number, timeoutMs = 2000): Promise<typeof completes.create[number]> {184 const start = Date.now();185 while (Date.now() - start < timeoutMs) {186 const found = completes.create.find((c) => c.requestId === requestId);187 if (found) return found;188 await new Promise((r) => setTimeout(r, 10));189 }190 throw new Error(`completeCreateRequest never called for requestId=${requestId}`);191 }192193 async function waitForGetComplete(requestId: number, timeoutMs = 2000): Promise<typeof completes.get[number]> {194 const start = Date.now();195 while (Date.now() - start < timeoutMs) {196 const found = completes.get.find((c) => c.requestId === requestId);197 if (found) return found;198 await new Promise((r) => setTimeout(r, 10));199 }200 throw new Error(`completeGetRequest never called for requestId=${requestId}`);201 }202203 204 function autoApprove() {205 void (async () => {206 const passkey = await import("./passkey");207 for (let i = 0; i < 200; i += 1) {208 const pending = await passkey.getPendingPasskeyRequest();209 if (pending) {210 await passkey.resolvePendingPasskeyRequest(pending.requestId, true);211 return;212 }213 await new Promise((r) => setTimeout(r, 10));214 }215 })();216 }217218 function autoDeny() {219 void (async () => {220 const passkey = await import("./passkey");221 for (let i = 0; i < 200; i += 1) {222 const pending = await passkey.getPendingPasskeyRequest();223 if (pending) {224 await passkey.resolvePendingPasskeyRequest(pending.requestId, false);225 return;226 }227 await new Promise((r) => setTimeout(r, 10));228 }229 })();230 }231232 function buildCreateOptions(overrides: Partial<{233 rpId: string;234 rpName: string;235 userName: string;236 userDisplayName: string;237 userIdB64: string;238 challenge: string;239 pubKeyCredParams: Array<{ alg: number; type: string }>;240 }> = {}) {241 return {242 rp: { id: overrides.rpId ?? "google.com", name: overrides.rpName ?? "Google" },243 user: {244 id: overrides.userIdB64 ?? "dXNlci1pZA",245 name: overrides.userName ?? "[email protected]",246 displayName: overrides.userDisplayName ?? "June",247 },248 challenge: overrides.challenge ?? "Y2hhbGxlbmdl",249 pubKeyCredParams: overrides.pubKeyCredParams ?? [250 { alg: -7, type: "public-key" },251 { alg: -257, type: "public-key" },252 ],253 };254 }255256 function decodeClientData(b64url: string): { type: string; challenge: string; origin: string; crossOrigin: boolean } {257 const padded = b64url.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(b64url.length / 4) * 4, "=");258 const json = atob(padded);259 const bytes = new Uint8Array(json.length);260 for (let i = 0; i < json.length; i += 1) bytes[i] = json.charCodeAt(i);261 return JSON.parse(new TextDecoder().decode(bytes));262 }263264 265 266 267268 it("isUvpaa returns true when the vault is locked (regression — was 'A passkey can't be created on this device')", async () => {269 proxyListeners.uvpaa!({ requestId: 1 });270 await new Promise((r) => setTimeout(r, 0));271 expect(completes.uvpaa).toEqual([{ requestId: 1, isUvpaa: true }]);272 });273274 it("isUvpaa returns true when the vault is unlocked", async () => {275 const vault = await import("./vault");276 await vault.createVault("correct-pass-12345");277 proxyListeners.uvpaa!({ requestId: 7 });278 await new Promise((r) => setTimeout(r, 0));279 expect(completes.uvpaa).toEqual([{ requestId: 7, isUvpaa: true }]);280 });281282 283 284 285286 it("handleCreateRequest on a locked vault summons inline unlock and proceeds once unlocked", async () => {287 const vault = await import("./vault");288 await vault.createVault("correct-pass-12345");289 vault.lock();290291 autoApprove();292 fireCreate(buildCreateOptions(), 101);293294 295 296 await new Promise((r) => setTimeout(r, 50));297 expect(tabsSendMessage).toHaveBeenCalledWith(298 1,299 expect.objectContaining({ type: "SUMMON_AUTOFILL", source: "passkey-unlock" })300 );301302 303 304 305 306 await vault.unlock("correct-pass-12345");307 const passkey = await import("./passkey");308 passkey.onVaultUnlocked();309310 const done = await waitForCreateComplete(101);311 expect(done.error).toBeUndefined();312 expect(done.responseJson).toBeDefined();313 });314315 it("handleCreateRequest rejects with NotAllowedError when unlock waits timeout", async () => {316 vi.useFakeTimers();317 const vault = await import("./vault");318 await vault.createVault("correct-pass-12345");319 vault.lock();320321 fireCreate(buildCreateOptions(), 102);322323 324 await vi.advanceTimersByTimeAsync(10);325 326 await vi.advanceTimersByTimeAsync(60_500);327 vi.useRealTimers();328329 const done = await waitForCreateComplete(102);330 expect(done.error).toMatchObject({ name: "NotAllowedError" });331 expect(done.responseJson).toBeUndefined();332 });333334 335 336 337 338339 it("create encodes the active tab origin into clientDataJSON when hostname is a subdomain of the rpId", async () => {340 const vault = await import("./vault");341 await vault.createVault("correct-pass-12345");342343 activeTabs = [{ id: 1, url: "https://myaccount.google.com/security/passkeys" }];344345 autoApprove();346 fireCreate(buildCreateOptions({ rpId: "google.com" }), 110);347 const done = await waitForCreateComplete(110);348349 const response = JSON.parse(done.responseJson!);350 const clientData = decodeClientData(response.response.clientDataJSON);351 expect(clientData.origin).toBe("https://myaccount.google.com");352 expect(clientData.type).toBe("webauthn.create");353 });354355 it("create encodes the active tab origin when hostname equals the rpId exactly", async () => {356 const vault = await import("./vault");357 await vault.createVault("correct-pass-12345");358359 activeTabs = [{ id: 1, url: "https://github.com/settings/security" }];360361 autoApprove();362 fireCreate(buildCreateOptions({ rpId: "github.com", rpName: "GitHub" }), 111);363 const done = await waitForCreateComplete(111);364365 const response = JSON.parse(done.responseJson!);366 const clientData = decodeClientData(response.response.clientDataJSON);367 expect(clientData.origin).toBe("https://github.com");368 });369370 it("create falls back to https://<rpId> when the active tab is not a registrable-suffix match (safety)", async () => {371 const vault = await import("./vault");372 await vault.createVault("correct-pass-12345");373374 375 376 377 activeTabs = [{ id: 1, url: "https://evil.example/passkey" }];378379 autoApprove();380 fireCreate(buildCreateOptions({ rpId: "google.com" }), 112);381 const done = await waitForCreateComplete(112);382383 const response = JSON.parse(done.responseJson!);384 const clientData = decodeClientData(response.response.clientDataJSON);385 expect(clientData.origin).toBe("https://google.com");386 });387388 it("create falls back to https://<rpId> when the active tab has no URL", async () => {389 const vault = await import("./vault");390 await vault.createVault("correct-pass-12345");391392 activeTabs = [{ id: 1 }]; 393394 autoApprove();395 fireCreate(buildCreateOptions({ rpId: "google.com" }), 113);396 const done = await waitForCreateComplete(113);397398 const response = JSON.parse(done.responseJson!);399 const clientData = decodeClientData(response.response.clientDataJSON);400 expect(clientData.origin).toBe("https://google.com");401 });402403 404 405 406407 it("create rejects with NotSupportedError when rp.id is missing", async () => {408 const vault = await import("./vault");409 await vault.createVault("correct-pass-12345");410411 fireCreate({ rp: { name: "No id" }, user: { id: "x", name: "u" }, challenge: "c", pubKeyCredParams: [{ alg: -7, type: "public-key" }] }, 120);412 const done = await waitForCreateComplete(120);413 expect(done.error).toMatchObject({ name: "NotSupportedError" });414 });415416 it("create rejects with NotSupportedError when no supported algorithm is offered", async () => {417 const vault = await import("./vault");418 await vault.createVault("correct-pass-12345");419420 autoApprove();421 fireCreate(buildCreateOptions({ pubKeyCredParams: [{ alg: -257, type: "public-key" }] }), 121);422 const done = await waitForCreateComplete(121);423 expect(done.error).toMatchObject({ name: "NotSupportedError" });424 });425426 it("create rejects with NotAllowedError when the user denies the approval", async () => {427 const vault = await import("./vault");428 await vault.createVault("correct-pass-12345");429430 autoDeny();431 fireCreate(buildCreateOptions(), 122);432 const done = await waitForCreateComplete(122);433 expect(done.error).toMatchObject({ name: "NotAllowedError" });434 });435436 it("create rejects with NotAllowedError + broadcasts the pro gate when the user is not Pro", async () => {437 const license = await import("./license");438 (license.isProUser as unknown as ReturnType<typeof vi.fn>).mockResolvedValueOnce(false);439440 const vault = await import("./vault");441 await vault.createVault("correct-pass-12345");442443 fireCreate(buildCreateOptions(), 123);444 const done = await waitForCreateComplete(123);445 expect(done.error).toMatchObject({ name: "NotAllowedError" });446 447 expect(tabsSendMessage).toHaveBeenCalledWith(448 expect.any(Number),449 expect.objectContaining({ type: "PASSKEY_PRO_GATE" })450 );451 });452453 it("create persists the passkey to the vault and tags it browser:'chrome'", async () => {454 const vault = await import("./vault");455 await vault.createVault("correct-pass-12345");456457 autoApprove();458 fireCreate(buildCreateOptions({ rpId: "github.com", rpName: "GitHub" }), 124);459 await waitForCreateComplete(124);460461 const stored = vault.getEntries().find((e) => e.passkey?.rpId === "github.com");462 expect(stored).toBeDefined();463 expect(stored!.passkey!.browser).toBe("chrome");464 expect(stored!.passkey!.backupEligible).toBe(true);465 expect(stored!.passkey!.signCount).toBe(0);466 expect(stored!.passkey!.privateKeyJwk).toBeDefined();467468 const createResponse = JSON.parse(completes.create.find((c) => c.requestId === 124)!.responseJson!);469 const authenticatorData = Uint8Array.from(atob(createResponse.response.authenticatorData.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(createResponse.response.authenticatorData.length / 4) * 4, "=")), (char) => char.charCodeAt(0));470 expect(authenticatorData[32]! & 0x08).toBe(0x08);471 });472473 it("create attaches the passkey to an existing login entry for the same domain instead of creating a new one", async () => {474 const vault = await import("./vault");475 await vault.createVault("correct-pass-12345");476 const existing = await vault.addEntry({477 type: "login",478 title: "GitHub",479 url: "https://github.com",480 username: "june",481 password: "hunter2",482 });483 const beforeCount = vault.getEntries().length;484485 autoApprove();486 fireCreate(buildCreateOptions({ rpId: "github.com", rpName: "GitHub" }), 125);487 await waitForCreateComplete(125);488489 expect(vault.getEntries().length).toBe(beforeCount);490 const updated = vault.getEntries().find((e) => e.id === existing.id);491 expect(updated?.passkey?.rpId).toBe("github.com");492 });493494 495 496 497498 async function seedPasskey(rpId: string, opts?: { signCount?: number }) {499 const vault = await import("./vault");500 const { base64urlEncode, generatePasskeyKeypair } = await import("./passkey-crypto");501 const generated = await generatePasskeyKeypair(-7);502 const credentialId = base64urlEncode(crypto.getRandomValues(new Uint8Array(32)));503 const entry = await vault.addEntry({504 type: "login",505 title: rpId,506 url: `https://${rpId}`,507 username: "june",508 passkey: {509 rpId,510 rpName: rpId,511 credentialId,512 publicKeyCose: base64urlEncode(generated.publicKeyCose),513 privateKeyJwk: generated.privateKeyJwk,514 userHandle: base64urlEncode(crypto.getRandomValues(new Uint8Array(16))),515 userName: "june",516 userDisplayName: "June",517 algorithm: -7,518 signCount: opts?.signCount ?? 0,519 aaguid: "AAAAAAAAAAAAAAAAAAAAAA",520 backupEligible: false,521 backupState: false,522 browser: "chrome",523 transports: ["internal"],524 createdAt: Date.now(),525 },526 });527 return { entry, credentialId };528 }529530 it("get passes through to platform when no stored credential matches the rpId", async () => {531 const vault = await import("./vault");532 await vault.createVault("correct-pass-12345");533534 fireGet({ rpId: "github.com", challenge: "Y2g", allowCredentials: [] }, 200);535 const done = await waitForGetComplete(200);536 537 expect(done.error).toBeUndefined();538 expect(done.responseJson).toBeUndefined();539 });540541 542 543 it("get does not match a credential whose stored rpId differs from the request rpId", async () => {544 const vault = await import("./vault");545 await vault.createVault("correct-pass-12345");546 const { credentialId } = await seedPasskey("github.com");547548 fireGet(549 {550 rpId: "evil.example",551 challenge: "Y2g",552 allowCredentials: [{ type: "public-key", id: credentialId }],553 },554 205,555 );556 const done = await waitForGetComplete(205);557 expect(done.error).toBeUndefined();558 expect(done.responseJson).toBeUndefined();559 });560561 it("get encodes the active tab page origin into clientDataJSON (not https://<rpId>)", async () => {562 const vault = await import("./vault");563 await vault.createVault("correct-pass-12345");564 await seedPasskey("google.com");565566 activeTabs = [{ id: 1, url: "https://accounts.google.com/login" }];567568 autoApprove();569 fireGet({ rpId: "google.com", challenge: "Y2g", allowCredentials: [] }, 201);570 const done = await waitForGetComplete(201);571572 const response = JSON.parse(done.responseJson!);573 const clientData = decodeClientData(response.response.clientDataJSON);574 expect(clientData.origin).toBe("https://accounts.google.com");575 expect(clientData.type).toBe("webauthn.get");576 });577578 it("get increments signCount and updates lastUsedAt on success", async () => {579 const vault = await import("./vault");580 await vault.createVault("correct-pass-12345");581 const { entry } = await seedPasskey("github.com", { signCount: 7 });582583 autoApprove();584 fireGet({ rpId: "github.com", challenge: "Y2g", allowCredentials: [] }, 202);585 await waitForGetComplete(202);586587 const after = vault.getEntries().find((e) => e.id === entry.id);588 expect(after!.passkey!.signCount).toBe(8);589 expect(after!.passkey!.lastUsedAt).toBeDefined();590 });591592 it("get preserves the BE flag used to register legacy Chrome passkeys", async () => {593 const vault = await import("./vault");594 await vault.createVault("correct-pass-12345");595 const { entry } = await seedPasskey("github.com");596597 expect(entry.passkey!.browser).toBe("chrome");598 expect(entry.passkey!.backupEligible).toBe(false);599600 autoApprove();601 fireGet({ rpId: "github.com", challenge: "Y2g", allowCredentials: [] }, 206);602 const done = await waitForGetComplete(206);603 const response = JSON.parse(done.responseJson!);604 const authenticatorData = Uint8Array.from(605 atob(response.response.authenticatorData.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(response.response.authenticatorData.length / 4) * 4, "=")),606 (char) => char.charCodeAt(0),607 );608609 expect(authenticatorData[32]! & 0x08).toBe(0x08);610 expect(vault.getEntries().find((candidate) => candidate.id === entry.id)!.passkey!.backupEligible).toBe(true);611 });612613 it("get passes through to platform when the stored passkey has no private key (Firefox-created)", async () => {614 const vault = await import("./vault");615 await vault.createVault("correct-pass-12345");616 const { base64urlEncode, generatePasskeyKeypair } = await import("./passkey-crypto");617 const generated = await generatePasskeyKeypair(-7);618 await vault.addEntry({619 type: "login",620 title: "Cross-browser passkey",621 passkey: {622 rpId: "webauthn.io",623 rpName: "WebAuthn.io",624 credentialId: base64urlEncode(crypto.getRandomValues(new Uint8Array(32))),625 publicKeyCose: base64urlEncode(generated.publicKeyCose),626 627 628 userHandle: base64urlEncode(crypto.getRandomValues(new Uint8Array(16))),629 userName: "june",630 userDisplayName: "June",631 algorithm: -7,632 signCount: 0,633 aaguid: "AAAAAAAAAAAAAAAAAAAAAA",634 backupEligible: false,635 backupState: false,636 browser: "firefox",637 transports: ["internal"],638 createdAt: Date.now(),639 },640 });641642 fireGet({ rpId: "webauthn.io", challenge: "Y2g", allowCredentials: [] }, 203);643 const done = await waitForGetComplete(203);644 645 expect(done.error).toBeUndefined();646 expect(done.responseJson).toBeUndefined();647 });648649 it("get on a locked vault parks the request and proceeds after unlock", async () => {650 const vault = await import("./vault");651 await vault.createVault("correct-pass-12345");652 await seedPasskey("github.com");653 vault.lock();654655 autoApprove();656 fireGet({ rpId: "github.com", challenge: "Y2g", allowCredentials: [] }, 204);657658 await new Promise((r) => setTimeout(r, 50));659 expect(tabsSendMessage).toHaveBeenCalledWith(660 1,661 expect.objectContaining({ type: "SUMMON_AUTOFILL", source: "passkey-unlock" })662 );663664 await vault.unlock("correct-pass-12345");665 const passkey = await import("./passkey");666 passkey.onVaultUnlocked();667668 const done = await waitForGetComplete(204);669 expect(done.error).toBeUndefined();670 expect(done.responseJson).toBeDefined();671 });672673 674 675 676677 678 679 680681 it("pass-through swallows 'Invalid requestId' from a cancelled conditional-UI get without throwing", async () => {682 const vault = await import("./vault");683 await vault.createVault("correct-pass-12345");684685 686 687 const ce = (globalThis as { chrome: typeof chrome }).chrome.webAuthenticationProxy688 .completeGetRequest as unknown as ReturnType<typeof vi.fn>;689 ce.mockImplementationOnce(async () => {690 throw new Error("Invalid requestId");691 });692693 694 695 let resolved = false;696 proxyListeners.get!({697 requestId: 999,698 requestDetailsJson: JSON.stringify({ rpId: "unknown.example", challenge: "x", allowCredentials: [] }),699 });700 701 await new Promise((r) => setTimeout(r, 30));702 resolved = true;703 expect(resolved).toBe(true);704 });705706 it("rejectCreate swallows 'Invalid requestId' from a cancelled create", async () => {707 const vault = await import("./vault");708 await vault.createVault("correct-pass-12345");709710 const ce = (globalThis as { chrome: typeof chrome }).chrome.webAuthenticationProxy711 .completeCreateRequest as unknown as ReturnType<typeof vi.fn>;712 ce.mockImplementationOnce(async () => {713 throw new Error("Invalid requestId");714 });715716 717 fireCreate({ rp: {}, user: { id: "x", name: "u" }, challenge: "c", pubKeyCredParams: [{ alg: -7, type: "public-key" }] }, 998);718 await new Promise((r) => setTimeout(r, 30));719 720 721 722 });723724 it("onRequestCanceled clears a parked unlock waiter so the handler exits promptly", async () => {725 const vault = await import("./vault");726 await vault.createVault("correct-pass-12345");727 vault.lock();728729 fireCreate(buildCreateOptions(), 997);730 731 await new Promise((r) => setTimeout(r, 30));732733 proxyListeners.cancel!(997);734735 const done = await waitForCreateComplete(997, 500);736 expect(done.error).toMatchObject({ name: "NotAllowedError" });737 });738739 it("onRequestCanceled resolves a still-pending approval as denied", async () => {740 const vault = await import("./vault");741 await vault.createVault("correct-pass-12345");742743 fireCreate(buildCreateOptions(), 301);744745 746 const passkey = await import("./passkey");747 let pending: Awaited<ReturnType<typeof passkey.getPendingPasskeyRequest>> = null;748 for (let i = 0; i < 50; i += 1) {749 pending = await passkey.getPendingPasskeyRequest();750 if (pending) break;751 await new Promise((r) => setTimeout(r, 10));752 }753 expect(pending).toBeTruthy();754755 proxyListeners.cancel!(301);756757 758 for (let i = 0; i < 50; i += 1) {759 const after = await passkey.getPendingPasskeyRequest();760 if (!after) return;761 await new Promise((r) => setTimeout(r, 10));762 }763 throw new Error("pending passkey request was not cleared after onRequestCanceled");764 });765});