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 function fireCreate(options: object, requestId = 100): void {160 proxyListeners.create!({161 requestId,162 requestDetailsJson: JSON.stringify(options),163 });164 }165166 function fireGet(options: object, requestId = 200): void {167 proxyListeners.get!({168 requestId,169 requestDetailsJson: JSON.stringify(options),170 });171 }172173 async function waitForCreateComplete(requestId: number, timeoutMs = 2000): Promise<typeof completes.create[number]> {174 const start = Date.now();175 while (Date.now() - start < timeoutMs) {176 const found = completes.create.find((c) => c.requestId === requestId);177 if (found) return found;178 await new Promise((r) => setTimeout(r, 10));179 }180 throw new Error(`completeCreateRequest never called for requestId=${requestId}`);181 }182183 async function waitForGetComplete(requestId: number, timeoutMs = 2000): Promise<typeof completes.get[number]> {184 const start = Date.now();185 while (Date.now() - start < timeoutMs) {186 const found = completes.get.find((c) => c.requestId === requestId);187 if (found) return found;188 await new Promise((r) => setTimeout(r, 10));189 }190 throw new Error(`completeGetRequest never called for requestId=${requestId}`);191 }192193 194 function autoApprove() {195 void (async () => {196 const passkey = await import("./passkey");197 for (let i = 0; i < 200; i += 1) {198 const pending = await passkey.getPendingPasskeyRequest();199 if (pending) {200 await passkey.resolvePendingPasskeyRequest(pending.requestId, true);201 return;202 }203 await new Promise((r) => setTimeout(r, 10));204 }205 })();206 }207208 function autoDeny() {209 void (async () => {210 const passkey = await import("./passkey");211 for (let i = 0; i < 200; i += 1) {212 const pending = await passkey.getPendingPasskeyRequest();213 if (pending) {214 await passkey.resolvePendingPasskeyRequest(pending.requestId, false);215 return;216 }217 await new Promise((r) => setTimeout(r, 10));218 }219 })();220 }221222 function buildCreateOptions(overrides: Partial<{223 rpId: string;224 rpName: string;225 userName: string;226 userDisplayName: string;227 userIdB64: string;228 challenge: string;229 pubKeyCredParams: Array<{ alg: number; type: string }>;230 }> = {}) {231 return {232 rp: { id: overrides.rpId ?? "google.com", name: overrides.rpName ?? "Google" },233 user: {234 id: overrides.userIdB64 ?? "dXNlci1pZA",235 name: overrides.userName ?? "[email protected]",236 displayName: overrides.userDisplayName ?? "June",237 },238 challenge: overrides.challenge ?? "Y2hhbGxlbmdl",239 pubKeyCredParams: overrides.pubKeyCredParams ?? [240 { alg: -7, type: "public-key" },241 { alg: -257, type: "public-key" },242 ],243 };244 }245246 function decodeClientData(b64url: string): { type: string; challenge: string; origin: string; crossOrigin: boolean } {247 const padded = b64url.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(b64url.length / 4) * 4, "=");248 const json = atob(padded);249 const bytes = new Uint8Array(json.length);250 for (let i = 0; i < json.length; i += 1) bytes[i] = json.charCodeAt(i);251 return JSON.parse(new TextDecoder().decode(bytes));252 }253254 255 256 257258 it("isUvpaa returns true when the vault is locked (regression — was 'A passkey can't be created on this device')", async () => {259 proxyListeners.uvpaa!({ requestId: 1 });260 await new Promise((r) => setTimeout(r, 0));261 expect(completes.uvpaa).toEqual([{ requestId: 1, isUvpaa: true }]);262 });263264 it("isUvpaa returns true when the vault is unlocked", async () => {265 const vault = await import("./vault");266 await vault.createVault("correct-pass-12345");267 proxyListeners.uvpaa!({ requestId: 7 });268 await new Promise((r) => setTimeout(r, 0));269 expect(completes.uvpaa).toEqual([{ requestId: 7, isUvpaa: true }]);270 });271272 273 274 275276 it("handleCreateRequest on a locked vault summons inline unlock and proceeds once unlocked", async () => {277 const vault = await import("./vault");278 await vault.createVault("correct-pass-12345");279 vault.lock();280281 autoApprove();282 fireCreate(buildCreateOptions(), 101);283284 285 286 await new Promise((r) => setTimeout(r, 50));287 expect(tabsSendMessage).toHaveBeenCalledWith(288 1,289 expect.objectContaining({ type: "SUMMON_AUTOFILL", source: "passkey-unlock" })290 );291292 293 294 295 296 await vault.unlock("correct-pass-12345");297 const passkey = await import("./passkey");298 passkey.onVaultUnlocked();299300 const done = await waitForCreateComplete(101);301 expect(done.error).toBeUndefined();302 expect(done.responseJson).toBeDefined();303 });304305 it("handleCreateRequest rejects with NotAllowedError when unlock waits timeout", async () => {306 vi.useFakeTimers();307 const vault = await import("./vault");308 await vault.createVault("correct-pass-12345");309 vault.lock();310311 fireCreate(buildCreateOptions(), 102);312313 314 await vi.advanceTimersByTimeAsync(10);315 316 await vi.advanceTimersByTimeAsync(60_500);317 vi.useRealTimers();318319 const done = await waitForCreateComplete(102);320 expect(done.error).toMatchObject({ name: "NotAllowedError" });321 expect(done.responseJson).toBeUndefined();322 });323324 325 326 327 328329 it("create encodes the active tab origin into clientDataJSON when hostname is a subdomain of the rpId", async () => {330 const vault = await import("./vault");331 await vault.createVault("correct-pass-12345");332333 activeTabs = [{ id: 1, url: "https://myaccount.google.com/security/passkeys" }];334335 autoApprove();336 fireCreate(buildCreateOptions({ rpId: "google.com" }), 110);337 const done = await waitForCreateComplete(110);338339 const response = JSON.parse(done.responseJson!);340 const clientData = decodeClientData(response.response.clientDataJSON);341 expect(clientData.origin).toBe("https://myaccount.google.com");342 expect(clientData.type).toBe("webauthn.create");343 });344345 it("create encodes the active tab origin when hostname equals the rpId exactly", async () => {346 const vault = await import("./vault");347 await vault.createVault("correct-pass-12345");348349 activeTabs = [{ id: 1, url: "https://github.com/settings/security" }];350351 autoApprove();352 fireCreate(buildCreateOptions({ rpId: "github.com", rpName: "GitHub" }), 111);353 const done = await waitForCreateComplete(111);354355 const response = JSON.parse(done.responseJson!);356 const clientData = decodeClientData(response.response.clientDataJSON);357 expect(clientData.origin).toBe("https://github.com");358 });359360 it("create falls back to https://<rpId> when the active tab is not a registrable-suffix match (safety)", async () => {361 const vault = await import("./vault");362 await vault.createVault("correct-pass-12345");363364 365 366 367 activeTabs = [{ id: 1, url: "https://evil.example/passkey" }];368369 autoApprove();370 fireCreate(buildCreateOptions({ rpId: "google.com" }), 112);371 const done = await waitForCreateComplete(112);372373 const response = JSON.parse(done.responseJson!);374 const clientData = decodeClientData(response.response.clientDataJSON);375 expect(clientData.origin).toBe("https://google.com");376 });377378 it("create falls back to https://<rpId> when the active tab has no URL", async () => {379 const vault = await import("./vault");380 await vault.createVault("correct-pass-12345");381382 activeTabs = [{ id: 1 }]; 383384 autoApprove();385 fireCreate(buildCreateOptions({ rpId: "google.com" }), 113);386 const done = await waitForCreateComplete(113);387388 const response = JSON.parse(done.responseJson!);389 const clientData = decodeClientData(response.response.clientDataJSON);390 expect(clientData.origin).toBe("https://google.com");391 });392393 394 395 396397 it("create rejects with NotSupportedError when rp.id is missing", async () => {398 const vault = await import("./vault");399 await vault.createVault("correct-pass-12345");400401 fireCreate({ rp: { name: "No id" }, user: { id: "x", name: "u" }, challenge: "c", pubKeyCredParams: [{ alg: -7, type: "public-key" }] }, 120);402 const done = await waitForCreateComplete(120);403 expect(done.error).toMatchObject({ name: "NotSupportedError" });404 });405406 it("create rejects with NotSupportedError when no supported algorithm is offered", async () => {407 const vault = await import("./vault");408 await vault.createVault("correct-pass-12345");409410 autoApprove();411 fireCreate(buildCreateOptions({ pubKeyCredParams: [{ alg: -257, type: "public-key" }] }), 121);412 const done = await waitForCreateComplete(121);413 expect(done.error).toMatchObject({ name: "NotSupportedError" });414 });415416 it("create rejects with NotAllowedError when the user denies the approval", async () => {417 const vault = await import("./vault");418 await vault.createVault("correct-pass-12345");419420 autoDeny();421 fireCreate(buildCreateOptions(), 122);422 const done = await waitForCreateComplete(122);423 expect(done.error).toMatchObject({ name: "NotAllowedError" });424 });425426 it("create rejects with NotAllowedError + broadcasts the pro gate when the user is not Pro", async () => {427 const license = await import("./license");428 (license.isProUser as unknown as ReturnType<typeof vi.fn>).mockResolvedValueOnce(false);429430 const vault = await import("./vault");431 await vault.createVault("correct-pass-12345");432433 fireCreate(buildCreateOptions(), 123);434 const done = await waitForCreateComplete(123);435 expect(done.error).toMatchObject({ name: "NotAllowedError" });436 437 expect(tabsSendMessage).toHaveBeenCalledWith(438 expect.any(Number),439 expect.objectContaining({ type: "PASSKEY_PRO_GATE" })440 );441 });442443 it("create persists the passkey to the vault and tags it browser:'chrome'", async () => {444 const vault = await import("./vault");445 await vault.createVault("correct-pass-12345");446447 autoApprove();448 fireCreate(buildCreateOptions({ rpId: "github.com", rpName: "GitHub" }), 124);449 await waitForCreateComplete(124);450451 const stored = vault.getEntries().find((e) => e.passkey?.rpId === "github.com");452 expect(stored).toBeDefined();453 expect(stored!.passkey!.browser).toBe("chrome");454 expect(stored!.passkey!.backupEligible).toBe(true);455 expect(stored!.passkey!.signCount).toBe(0);456 expect(stored!.passkey!.privateKeyJwk).toBeDefined();457458 const createResponse = JSON.parse(completes.create.find((c) => c.requestId === 124)!.responseJson!);459 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));460 expect(authenticatorData[32]! & 0x08).toBe(0x08);461 });462463 it("create attaches the passkey to an existing login entry for the same domain instead of creating a new one", async () => {464 const vault = await import("./vault");465 await vault.createVault("correct-pass-12345");466 const existing = await vault.addEntry({467 type: "login",468 title: "GitHub",469 url: "https://github.com",470 username: "june",471 password: "hunter2",472 });473 const beforeCount = vault.getEntries().length;474475 autoApprove();476 fireCreate(buildCreateOptions({ rpId: "github.com", rpName: "GitHub" }), 125);477 await waitForCreateComplete(125);478479 expect(vault.getEntries().length).toBe(beforeCount);480 const updated = vault.getEntries().find((e) => e.id === existing.id);481 expect(updated?.passkey?.rpId).toBe("github.com");482 });483484 485 486 487488 async function seedPasskey(rpId: string, opts?: { signCount?: number }) {489 const vault = await import("./vault");490 const { base64urlEncode, generatePasskeyKeypair } = await import("./passkey-crypto");491 const generated = await generatePasskeyKeypair(-7);492 const credentialId = base64urlEncode(crypto.getRandomValues(new Uint8Array(32)));493 const entry = await vault.addEntry({494 type: "login",495 title: rpId,496 url: `https://${rpId}`,497 username: "june",498 passkey: {499 rpId,500 rpName: rpId,501 credentialId,502 publicKeyCose: base64urlEncode(generated.publicKeyCose),503 privateKeyJwk: generated.privateKeyJwk,504 userHandle: base64urlEncode(crypto.getRandomValues(new Uint8Array(16))),505 userName: "june",506 userDisplayName: "June",507 algorithm: -7,508 signCount: opts?.signCount ?? 0,509 aaguid: "AAAAAAAAAAAAAAAAAAAAAA",510 backupEligible: false,511 backupState: false,512 browser: "chrome",513 transports: ["internal"],514 createdAt: Date.now(),515 },516 });517 return { entry, credentialId };518 }519520 it("get passes through to platform when no stored credential matches the rpId", async () => {521 const vault = await import("./vault");522 await vault.createVault("correct-pass-12345");523524 fireGet({ rpId: "github.com", challenge: "Y2g", allowCredentials: [] }, 200);525 const done = await waitForGetComplete(200);526 527 expect(done.error).toBeUndefined();528 expect(done.responseJson).toBeUndefined();529 });530531 532 533 it("get does not match a credential whose stored rpId differs from the request rpId", async () => {534 const vault = await import("./vault");535 await vault.createVault("correct-pass-12345");536 const { credentialId } = await seedPasskey("github.com");537538 fireGet(539 {540 rpId: "evil.example",541 challenge: "Y2g",542 allowCredentials: [{ type: "public-key", id: credentialId }],543 },544 205,545 );546 const done = await waitForGetComplete(205);547 expect(done.error).toBeUndefined();548 expect(done.responseJson).toBeUndefined();549 });550551 it("get encodes the active tab page origin into clientDataJSON (not https://<rpId>)", async () => {552 const vault = await import("./vault");553 await vault.createVault("correct-pass-12345");554 await seedPasskey("google.com");555556 activeTabs = [{ id: 1, url: "https://accounts.google.com/login" }];557558 autoApprove();559 fireGet({ rpId: "google.com", challenge: "Y2g", allowCredentials: [] }, 201);560 const done = await waitForGetComplete(201);561562 const response = JSON.parse(done.responseJson!);563 const clientData = decodeClientData(response.response.clientDataJSON);564 expect(clientData.origin).toBe("https://accounts.google.com");565 expect(clientData.type).toBe("webauthn.get");566 });567568 it("get increments signCount and updates lastUsedAt on success", async () => {569 const vault = await import("./vault");570 await vault.createVault("correct-pass-12345");571 const { entry } = await seedPasskey("github.com", { signCount: 7 });572573 autoApprove();574 fireGet({ rpId: "github.com", challenge: "Y2g", allowCredentials: [] }, 202);575 await waitForGetComplete(202);576577 const after = vault.getEntries().find((e) => e.id === entry.id);578 expect(after!.passkey!.signCount).toBe(8);579 expect(after!.passkey!.lastUsedAt).toBeDefined();580 });581582 it("get passes through to platform when the stored passkey has no private key (Firefox-created)", async () => {583 const vault = await import("./vault");584 await vault.createVault("correct-pass-12345");585 const { base64urlEncode, generatePasskeyKeypair } = await import("./passkey-crypto");586 const generated = await generatePasskeyKeypair(-7);587 await vault.addEntry({588 type: "login",589 title: "Cross-browser passkey",590 passkey: {591 rpId: "webauthn.io",592 rpName: "WebAuthn.io",593 credentialId: base64urlEncode(crypto.getRandomValues(new Uint8Array(32))),594 publicKeyCose: base64urlEncode(generated.publicKeyCose),595 596 597 userHandle: base64urlEncode(crypto.getRandomValues(new Uint8Array(16))),598 userName: "june",599 userDisplayName: "June",600 algorithm: -7,601 signCount: 0,602 aaguid: "AAAAAAAAAAAAAAAAAAAAAA",603 backupEligible: false,604 backupState: false,605 browser: "firefox",606 transports: ["internal"],607 createdAt: Date.now(),608 },609 });610611 fireGet({ rpId: "webauthn.io", challenge: "Y2g", allowCredentials: [] }, 203);612 const done = await waitForGetComplete(203);613 614 expect(done.error).toBeUndefined();615 expect(done.responseJson).toBeUndefined();616 });617618 it("get on a locked vault parks the request and proceeds after unlock", async () => {619 const vault = await import("./vault");620 await vault.createVault("correct-pass-12345");621 await seedPasskey("github.com");622 vault.lock();623624 autoApprove();625 fireGet({ rpId: "github.com", challenge: "Y2g", allowCredentials: [] }, 204);626627 await new Promise((r) => setTimeout(r, 50));628 expect(tabsSendMessage).toHaveBeenCalledWith(629 1,630 expect.objectContaining({ type: "SUMMON_AUTOFILL", source: "passkey-unlock" })631 );632633 await vault.unlock("correct-pass-12345");634 const passkey = await import("./passkey");635 passkey.onVaultUnlocked();636637 const done = await waitForGetComplete(204);638 expect(done.error).toBeUndefined();639 expect(done.responseJson).toBeDefined();640 });641642 643 644 645646 647 648 649650 it("pass-through swallows 'Invalid requestId' from a cancelled conditional-UI get without throwing", async () => {651 const vault = await import("./vault");652 await vault.createVault("correct-pass-12345");653654 655 656 const ce = (globalThis as { chrome: typeof chrome }).chrome.webAuthenticationProxy657 .completeGetRequest as unknown as ReturnType<typeof vi.fn>;658 ce.mockImplementationOnce(async () => {659 throw new Error("Invalid requestId");660 });661662 663 664 let resolved = false;665 proxyListeners.get!({666 requestId: 999,667 requestDetailsJson: JSON.stringify({ rpId: "unknown.example", challenge: "x", allowCredentials: [] }),668 });669 670 await new Promise((r) => setTimeout(r, 30));671 resolved = true;672 expect(resolved).toBe(true);673 });674675 it("rejectCreate swallows 'Invalid requestId' from a cancelled create", async () => {676 const vault = await import("./vault");677 await vault.createVault("correct-pass-12345");678679 const ce = (globalThis as { chrome: typeof chrome }).chrome.webAuthenticationProxy680 .completeCreateRequest as unknown as ReturnType<typeof vi.fn>;681 ce.mockImplementationOnce(async () => {682 throw new Error("Invalid requestId");683 });684685 686 fireCreate({ rp: {}, user: { id: "x", name: "u" }, challenge: "c", pubKeyCredParams: [{ alg: -7, type: "public-key" }] }, 998);687 await new Promise((r) => setTimeout(r, 30));688 689 690 691 });692693 it("onRequestCanceled clears a parked unlock waiter so the handler exits promptly", async () => {694 const vault = await import("./vault");695 await vault.createVault("correct-pass-12345");696 vault.lock();697698 fireCreate(buildCreateOptions(), 997);699 700 await new Promise((r) => setTimeout(r, 30));701702 proxyListeners.cancel!(997);703704 const done = await waitForCreateComplete(997, 500);705 expect(done.error).toMatchObject({ name: "NotAllowedError" });706 });707708 it("onRequestCanceled resolves a still-pending approval as denied", async () => {709 const vault = await import("./vault");710 await vault.createVault("correct-pass-12345");711712 fireCreate(buildCreateOptions(), 301);713714 715 const passkey = await import("./passkey");716 let pending: Awaited<ReturnType<typeof passkey.getPendingPasskeyRequest>> = null;717 for (let i = 0; i < 50; i += 1) {718 pending = await passkey.getPendingPasskeyRequest();719 if (pending) break;720 await new Promise((r) => setTimeout(r, 10));721 }722 expect(pending).toBeTruthy();723724 proxyListeners.cancel!(301);725726 727 for (let i = 0; i < 50; i += 1) {728 const after = await passkey.getPendingPasskeyRequest();729 if (!after) return;730 await new Promise((r) => setTimeout(r, 10));731 }732 throw new Error("pending passkey request was not cleared after onRequestCanceled");733 });734});