12345678import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";910import { base64urlEncode, generatePasskeyKeypair } from "./passkey-crypto";11import type { Message, MessageResponse } from "../shared/types";1213vi.mock("./license", async (importOriginal) => {14 const actual = await importOriginal<typeof import("./license")>();15 return {16 ...actual,17 isProUser: vi.fn(async () => true),18 };19});20212223242526vi.mock("../shared/passkey-flags", () => ({27 FIREFOX_USE_CONTENT_SCRIPT_PROXY: true,28}));2930type StorageShape = Record<string, unknown>;31type MessageListener = (32 msg: unknown,33 sender: chrome.runtime.MessageSender,34 sendResponse: (response: MessageResponse) => void35) => boolean;3637describe("firefox passkey proxy handlers", () => {38 let storage: StorageShape;39 let sessionStorage: StorageShape;40 let listener: MessageListener;4142 beforeEach(async () => {43 vi.resetModules();44 vi.useRealTimers();45 storage = { peach_settings: { breachMonitoring: false } };46 sessionStorage = {};4748 const tabs = [{ id: 1 }];49 const chromeMock = {50 runtime: {51 id: "peach-extension",52 getURL: vi.fn((_path: string) => "moz-extension://abcd/"),53 onMessage: {54 addListener: vi.fn((fn: MessageListener) => {55 listener = fn;56 }),57 },58 sendMessage: vi.fn(async () => ({ ok: true })),59 },60 tabs: {61 query: vi.fn((_q: unknown, callback?: (t: Array<{ id?: number }>) => void) => {62 if (typeof callback === "function") { callback(tabs); return; }63 return Promise.resolve(tabs);64 }),65 sendMessage: vi.fn(async () => ({ ok: true })),66 },67 storage: {68 local: {69 get: vi.fn(async (key?: string | string[]) => {70 if (!key) return storage;71 if (typeof key === "string") return { [key]: storage[key] };72 return key.reduce<Record<string, unknown>>((r, k) => { r[k] = storage[k]; return r; }, {});73 }),74 set: vi.fn(async (next: Record<string, unknown>) => Object.assign(storage, next)),75 remove: vi.fn(async (keys: string | string[]) => {76 const list = Array.isArray(keys) ? keys : [keys];77 for (const k of list) delete storage[k];78 }),79 },80 session: {81 get: vi.fn(async (key?: string | string[]) => {82 if (!key) return sessionStorage;83 if (typeof key === "string") return { [key]: sessionStorage[key] };84 return key.reduce<Record<string, unknown>>((r, k) => { r[k] = sessionStorage[k]; return r; }, {});85 }),86 set: vi.fn(async (next: Record<string, unknown>) => Object.assign(sessionStorage, next)),87 remove: vi.fn(async (keys: string | string[]) => {88 const list = Array.isArray(keys) ? keys : [keys];89 for (const k of list) delete sessionStorage[k];90 }),91 clear: vi.fn(async () => { sessionStorage = {}; }),92 },93 },94 action: {95 setBadgeText: vi.fn(async () => {}),96 setBadgeBackgroundColor: vi.fn(async () => {}),97 openPopup: vi.fn(async () => {}),98 },99 alarms: { clear: vi.fn(), create: vi.fn(), onAlarm: { addListener: vi.fn() } },100 idle: { setDetectionInterval: vi.fn(), onStateChanged: { addListener: vi.fn() } },101 scripting: { executeScript: vi.fn(async () => []) },102 } as unknown as typeof chrome;103104 (globalThis as { chrome?: typeof chrome }).chrome = chromeMock;105 vi.stubGlobal("__PEACH_E2E_PASSKEYS__", false);106107 const messages = await import("./messages");108 messages.setupMessageHandler();109 const vault = await import("./vault");110 await vault.ensureSettingsLoaded();111 });112113 afterEach(() => {114 vi.unstubAllGlobals();115 });116117 118 119 120 async function dispatch(message: Message, senderUrl = "https://github.com/login"): Promise<MessageResponse> {121 return await new Promise<MessageResponse>((resolve) => {122 const open = listener(message, { id: "peach-extension", url: senderUrl, origin: new URL(senderUrl).origin }, (r) => resolve(r));123 expect(open).toBe(true);124 });125 }126127 128 129 async function autoApprove() {130 const passkey = await import("./passkey");131 const wait = async () => {132 for (let i = 0; i < 50; i += 1) {133 const pending = await passkey.getPendingPasskeyRequest();134 if (pending) {135 await passkey.resolvePendingPasskeyRequest(pending.requestId, true);136 return;137 }138 await new Promise((r) => setTimeout(r, 10));139 }140 };141 void wait();142 }143144 it.each([[undefined, true], [true, true], [false, false]] as const)("defaults passkey interception on while preserving %s", async (preference, enabled) => {145 storage.peach_settings = { breachMonitoring: false, ...(preference === undefined ? {} : { passkeyProviderEnabled: preference }) };146 vi.stubGlobal("navigator", { userAgent: "Mozilla/5.0 Firefox/157.0" });147 const passkey = await import("./passkey");148 await passkey.setupPasskeyProxy();149 const state = await passkey.getPasskeyProviderState();150 expect(state).toMatchObject({ supported: true, enabled, attached: enabled });151 });152153 it.each(["PASSKEY_HAS_CREDENTIALS", "PASSKEY_GET", "PASSKEY_CREATE"] as const)("honors explicit disabled interception for %s", async (type) => {154 const vault = await import("./vault");155 await vault.createVault("correct-pass-12345");156 await vault.updateSettings({ passkeyProviderEnabled: false });157 await expect(dispatch({158 type,159 payload: { rpId: "github.com", userName: "june", userId: "AQID", challenge: "x", origin: "https://github.com", clientDataJSON: Array.from(new TextEncoder().encode("{}")), allowCredentials: [] },160 })).resolves.toMatchObject({ ok: false, error: "not_applicable" });161 expect(chrome.action.openPopup).not.toHaveBeenCalled();162 });163164 it("PASSKEY_HAS_CREDENTIALS returns false when vault has no passkey for rpId", async () => {165 const vault = await import("./vault");166 await vault.createVault("correct-pass-12345");167168 await expect(169 dispatch({170 type: "PASSKEY_HAS_CREDENTIALS",171 payload: { rpId: "github.com", allowCredentials: [] },172 })173 ).resolves.toMatchObject({ ok: true, hasCredentials: false });174 });175176 it("PASSKEY_HAS_CREDENTIALS returns true when allowCredentials is empty and rpId matches", async () => {177 const vault = await import("./vault");178 await vault.createVault("correct-pass-12345");179 const generated = await generatePasskeyKeypair(-7);180 await vault.addEntry({181 type: "login",182 title: "GitHub",183 passkey: {184 rpId: "github.com",185 rpName: "GitHub",186 credentialId: base64urlEncode(crypto.getRandomValues(new Uint8Array(32))),187 publicKeyCose: base64urlEncode(generated.publicKeyCose),188 privateKeyJwk: generated.privateKeyJwk,189 userHandle: base64urlEncode(crypto.getRandomValues(new Uint8Array(32))),190 userName: "june",191 userDisplayName: "June",192 algorithm: -7,193 signCount: 0,194 aaguid: "AAAAAAAAAAAAAAAAAAAAAA",195 backupEligible: true,196 backupState: false,197 createdAt: Date.now(),198 },199 });200201 await expect(202 dispatch({203 type: "PASSKEY_HAS_CREDENTIALS",204 payload: { rpId: "github.com", allowCredentials: [] },205 })206 ).resolves.toMatchObject({ ok: true, hasCredentials: true });207 });208209 it("PASSKEY_HAS_CREDENTIALS matches by credentialId in allowCredentials", async () => {210 const vault = await import("./vault");211 await vault.createVault("correct-pass-12345");212 const generated = await generatePasskeyKeypair(-7);213 const credentialId = base64urlEncode(crypto.getRandomValues(new Uint8Array(32)));214 await vault.addEntry({215 type: "login",216 title: "GitHub",217 passkey: {218 rpId: "github.com",219 rpName: "GitHub",220 credentialId,221 publicKeyCose: base64urlEncode(generated.publicKeyCose),222 privateKeyJwk: generated.privateKeyJwk,223 userHandle: base64urlEncode(crypto.getRandomValues(new Uint8Array(32))),224 userName: "june",225 userDisplayName: "June",226 algorithm: -7,227 signCount: 0,228 aaguid: "AAAAAAAAAAAAAAAAAAAAAA",229 backupEligible: true,230 backupState: false,231 createdAt: Date.now(),232 },233 });234235 await expect(236 dispatch({237 type: "PASSKEY_HAS_CREDENTIALS",238 payload: {239 rpId: "github.com",240 allowCredentials: [{ id: "different-id", type: "public-key" }],241 },242 })243 ).resolves.toMatchObject({ ok: true, hasCredentials: false });244245 await expect(246 dispatch({247 type: "PASSKEY_HAS_CREDENTIALS",248 payload: {249 rpId: "github.com",250 allowCredentials: [{ id: credentialId, type: "public-key" }],251 },252 })253 ).resolves.toMatchObject({ ok: true, hasCredentials: true });254 });255256 it("PASSKEY_HAS_CREDENTIALS reports a locked vault without enumerating credentials", async () => {257 const vault = await import("./vault");258 await vault.createVault("correct-pass-12345");259 vault.lock();260261 await expect(262 dispatch({263 type: "PASSKEY_HAS_CREDENTIALS",264 payload: { rpId: "github.com", allowCredentials: [] },265 })266 ).resolves.toMatchObject({ ok: false, error: "vault_locked" });267 });268269 it("PASSKEY_CREATE rejects a cross-origin rpId without minting a credential (SEV-1-A)", async () => {270 const vault = await import("./vault");271 await vault.createVault("correct-pass-12345");272273 autoApprove();274 275 const res = await dispatch({276 type: "PASSKEY_CREATE",277 payload: {278 rpId: "google.com",279 rpName: "Google",280 userId: base64urlEncode(crypto.getRandomValues(new Uint8Array(16))),281 userName: "june",282 userDisplayName: "June",283 challenge: "x",284 clientDataJSON: Array.from(new TextEncoder().encode("{}")),285 origin: "https://evil.com",286 pubKeyCredParams: [{ alg: -7, type: "public-key" }],287 },288 }, "https://evil.com/attack");289290 expect(res.ok).toBe(false);291 expect(String(res.error)).toContain("SecurityError");292 293 expect(vault.getEntries().find((e) => e.passkey?.rpId === "google.com")).toBeUndefined();294 });295296 it("PASSKEY_GET rejects a cross-origin rpId (SEV-1-A)", async () => {297 const vault = await import("./vault");298 await vault.createVault("correct-pass-12345");299300 const res = await dispatch({301 type: "PASSKEY_GET",302 payload: {303 rpId: "google.com",304 challenge: "x",305 clientDataJSON: Array.from(new TextEncoder().encode("{}")),306 origin: "https://evil.com",307 allowCredentials: [],308 },309 }, "https://evil.com/attack");310311 expect(res.ok).toBe(false);312 expect(String(res.error)).toContain("SecurityError");313 });314315 it("PASSKEY_GET refuses a FORGED payload.origin that matches rpId when the attested sender origin differs (audit CRITICAL — origin spoofing)", async () => {316 const vault = await import("./vault");317 await vault.createVault("correct-pass-12345");318319 320 321 322 const forgedClientData = Array.from(new TextEncoder().encode(323 JSON.stringify({ type: "webauthn.get", challenge: "x", origin: "https://victimbank.com", crossOrigin: false }),324 ));325 const res = await dispatch({326 type: "PASSKEY_GET",327 payload: {328 rpId: "victimbank.com",329 origin: "https://victimbank.com",330 challenge: "x",331 clientDataJSON: forgedClientData,332 allowCredentials: [],333 },334 }, "https://evil.com/attack");335336 expect(res.ok).toBe(false);337 expect(String(res.error)).toContain("SecurityError");338 });339340 it("PASSKEY_HAS_CREDENTIALS is scoped to the caller origin (no cross-site enumeration)", async () => {341 const vault = await import("./vault");342 await vault.createVault("correct-pass-12345");343 await vault.addEntry({344 type: "login", title: "GitHub", url: "https://github.com", username: "june",345 passkey: {346 rpId: "github.com", rpName: "GitHub", credentialId: "c1", publicKeyCose: "pk",347 privateKeyJwk: { kty: "EC", crv: "P-256", x: "x", y: "y", d: "d" } as JsonWebKey,348 userHandle: "u", userName: "june", userDisplayName: "June", algorithm: -7,349 signCount: 0, aaguid: "", backupEligible: false, backupState: false, createdAt: 1,350 },351 });352353 354 await expect(dispatch({355 type: "PASSKEY_HAS_CREDENTIALS", payload: { rpId: "github.com", allowCredentials: [] },356 }, "https://evil.com/")).resolves.toMatchObject({ ok: true, hasCredentials: false });357358 359 await expect(dispatch({360 type: "PASSKEY_HAS_CREDENTIALS", payload: { rpId: "github.com", allowCredentials: [] },361 }, "https://github.com/")).resolves.toMatchObject({ ok: true, hasCredentials: true });362 });363364 it("GET_PASSKEY_FOR_DOMAIN is scoped to the caller origin (no username leak cross-site)", async () => {365 const vault = await import("./vault");366 await vault.createVault("correct-pass-12345");367 await vault.addEntry({368 type: "login", title: "GitHub", url: "https://github.com", username: "june",369 passkey: {370 rpId: "github.com", rpName: "GitHub", credentialId: "c1", publicKeyCose: "pk",371 privateKeyJwk: { kty: "EC", crv: "P-256", x: "x", y: "y", d: "d" } as JsonWebKey,372 userHandle: "u", userName: "[email protected]", userDisplayName: "June", algorithm: -7,373 signCount: 0, aaguid: "", backupEligible: false, backupState: false, createdAt: 1,374 },375 });376377 await expect(dispatch({378 type: "GET_PASSKEY_FOR_DOMAIN", payload: { domain: "github.com" },379 }, "https://evil.com/")).resolves.toMatchObject({ ok: true, data: null });380381 const own = await dispatch({382 type: "GET_PASSKEY_FOR_DOMAIN", payload: { domain: "github.com" },383 }, "https://github.com/");384 expect((own.data as { userName?: string } | null)?.userName).toBe("[email protected]");385 });386387 it("MARK_PASSKEY_BANNER_SHOWN is origin-bound and surfaces via bannerShown (PB-26-203)", async () => {388 const vault = await import("./vault");389 await vault.createVault("correct-pass-12345");390 await vault.addEntry({391 type: "login", title: "GitHub", url: "https://github.com", username: "june",392 passkey: {393 rpId: "github.com", rpName: "GitHub", credentialId: "c1", publicKeyCose: "pk",394 privateKeyJwk: { kty: "EC", crv: "P-256", x: "x", y: "y", d: "d" } as JsonWebKey,395 userHandle: "u", userName: "[email protected]", userDisplayName: "June", algorithm: -7,396 signCount: 0, aaguid: "", backupEligible: false, backupState: false, createdAt: 1,397 },398 });399400 401 await expect(dispatch({402 type: "MARK_PASSKEY_BANNER_SHOWN", payload: { domain: "github.com" },403 }, "https://evil.com/")).resolves.toMatchObject({ ok: true });404 const before = await dispatch({405 type: "GET_PASSKEY_FOR_DOMAIN", payload: { domain: "github.com" },406 }, "https://github.com/");407 expect((before.data as { bannerShown?: boolean } | null)?.bannerShown).toBe(false);408409 410 await expect(dispatch({411 type: "MARK_PASSKEY_BANNER_SHOWN", payload: { domain: "github.com" },412 }, "https://github.com/")).resolves.toMatchObject({ ok: true });413 const after = await dispatch({414 type: "GET_PASSKEY_FOR_DOMAIN", payload: { domain: "github.com" },415 }, "https://github.com/");416 expect((after.data as { bannerShown?: boolean } | null)?.bannerShown).toBe(true);417 });418419 it("handleFirefoxPasskeyCreate throws a SecurityError directly on rpId mismatch", async () => {420 const { handleFirefoxPasskeyCreate } = await import("./passkey");421 await expect(422 handleFirefoxPasskeyCreate({423 rpId: "google.com",424 rpName: "Google",425 userId: "dGVzdA",426 userName: "june",427 challenge: "x",428 clientDataJSON: [],429 origin: "https://evil.com",430 pubKeyCredParams: [{ alg: -7, type: "public-key" }],431 })432 ).rejects.toThrow("SecurityError");433 });434435 it("PASSKEY_GET returns no_credentials when nothing matches", async () => {436 const vault = await import("./vault");437 await vault.createVault("correct-pass-12345");438439 const clientDataJSON = Array.from(new TextEncoder().encode(440 JSON.stringify({ type: "webauthn.get", challenge: "x", origin: "https://github.com", crossOrigin: false })441 ));442443 await expect(444 dispatch({445 type: "PASSKEY_GET",446 payload: {447 rpId: "github.com",448 challenge: "x",449 clientDataJSON,450 origin: "https://github.com",451 allowCredentials: [],452 },453 })454 ).resolves.toMatchObject({ ok: false, error: "no_credentials" });455 });456457 it.each(["unlock", "dismiss", "timeout"] as const)("PASSKEY_GET waits for a locked vault: %s", async (outcome) => {458 const vault = await import("./vault");459 const passkey = await import("./passkey");460 await vault.createVault("correct-pass-12345");461 vault.lock();462 if (outcome === "timeout") vi.useFakeTimers();463 const resultPromise = dispatch({464 type: "PASSKEY_GET",465 payload: { rpId: "github.com", challenge: "x", origin: "https://github.com", clientDataJSON: Array.from(new TextEncoder().encode("{}")), allowCredentials: [] },466 });467 await vi.waitFor(() => expect(chrome.action.openPopup).toHaveBeenCalled());468 let settled = false;469 void resultPromise.then(() => { settled = true; });470 await Promise.resolve();471 expect(settled).toBe(false);472 if (outcome === "unlock") {473 await vault.unlock("correct-pass-12345");474 passkey.onVaultUnlocked();475 await expect(resultPromise).resolves.toMatchObject({ ok: false, error: "no_credentials" });476 } else if (outcome === "dismiss") {477 await passkey.denyAllPendingPasskeyRequests();478 await expect(resultPromise).resolves.toMatchObject({ ok: false, error: "user_cancelled" });479 } else {480 await vi.advanceTimersByTimeAsync(60_000);481 await expect(resultPromise).resolves.toMatchObject({ ok: false, error: "user_cancelled" });482 vi.useRealTimers();483 }484 });485486 it.each([["chrome", true, false], ["firefox", false, false], ["chrome", true, true]] as const)("PASSKEY_GET preserves legacy %s backup eligibility %s (locked: %s)", async (browser, expectedBackupEligible, locked) => {487 const vault = await import("./vault");488 await vault.createVault("correct-pass-12345");489 const generated = await generatePasskeyKeypair(-7);490 const credentialId = base64urlEncode(crypto.getRandomValues(new Uint8Array(32)));491 const entry = await vault.addEntry({492 type: "login",493 title: "GitHub",494 passkey: {495 rpId: "github.com", rpName: "GitHub", credentialId,496 publicKeyCose: base64urlEncode(generated.publicKeyCose),497 privateKeyJwk: generated.privateKeyJwk,498 userHandle: base64urlEncode(crypto.getRandomValues(new Uint8Array(16))),499 userName: "june", userDisplayName: "June", algorithm: -7, signCount: 0, aaguid: "",500 backupEligible: false, backupState: false, createdAt: 1, browser,501 },502 });503 const challenge = "challenge-base64url";504 const origin = "https://github.com";505 const clientDataJSON = Array.from(new TextEncoder().encode(506 JSON.stringify({ type: "webauthn.get", challenge, origin, crossOrigin: false })507 ));508509 if (locked) vault.lock();510 const resultPromise = dispatch({511 type: "PASSKEY_GET",512 payload: { rpId: "github.com", challenge, origin, clientDataJSON, allowCredentials: [{ id: credentialId, type: "public-key" }] },513 });514 const passkey = await import("./passkey");515 if (locked) {516 await vi.waitFor(() => expect(chrome.action.openPopup).toHaveBeenCalled());517 await vault.unlock("correct-pass-12345");518 passkey.onVaultUnlocked();519 }520 await vi.waitFor(async () => {521 expect(await passkey.getPendingPasskeyRequest()).not.toBeNull();522 });523 const pending = await passkey.getPendingPasskeyRequest();524 await passkey.resolvePendingPasskeyRequest(pending!.requestId, true);525 const result = await resultPromise;526 expect(result.ok).toBe(true);527 const assertion = (result as unknown as { credential: { authenticatorData: string } }).credential;528 const authenticatorData = Uint8Array.from(atob(assertion.authenticatorData.replace(/-/g, "+").replace(/_/g, "/")), (char) => char.charCodeAt(0));529 expect(Boolean(authenticatorData[32]! & 0x08)).toBe(expectedBackupEligible);530 expect(vault.getEntries().find((candidate) => candidate.id === entry.id)!.passkey!.backupEligible).toBe(expectedBackupEligible);531 });532533 it("finishes saving a Firefox assertion before approval lets the popup close and lock", async () => {534 const vault = await import("./vault");535 const passkey = await import("./passkey");536 await vault.createVault("correct-pass-12345");537 autoApprove();538 const created = await dispatch({539 type: "PASSKEY_CREATE",540 payload: { rpId: "github.com", userId: "AQID", userName: "june", challenge: "x", origin: "https://github.com", clientDataJSON: Array.from(new TextEncoder().encode("{}")), pubKeyCredParams: [{ alg: -7, type: "public-key" }] },541 });542 expect(created.ok).toBe(true);543 const resultPromise = dispatch({544 type: "PASSKEY_GET",545 payload: { rpId: "github.com", challenge: "x", origin: "https://github.com", clientDataJSON: Array.from(new TextEncoder().encode("{}")), allowCredentials: [] },546 });547 await vi.waitFor(async () => expect(await passkey.getPendingPasskeyRequest()).not.toBeNull());548 const pending = await passkey.getPendingPasskeyRequest();549 await passkey.resolvePendingPasskeyRequest(pending!.requestId, true);550 551 vault.lock();552 await expect(resultPromise).resolves.toMatchObject({ ok: true });553 await vault.unlock("correct-pass-12345");554 expect(vault.getEntries().find((entry) => entry.passkey)?.passkey?.signCount).toBe(1);555 });556557 it("accepts approval delivered while the popup is opening", async () => {558 const vault = await import("./vault");559 const passkey = await import("./passkey");560 await vault.createVault("correct-pass-12345");561 vi.mocked(chrome.action.openPopup).mockImplementationOnce(async () => {562 const pending = await passkey.getPendingPasskeyRequest();563 expect(pending).not.toBeNull();564 void passkey.resolvePendingPasskeyRequest(pending!.requestId, true);565 });566 const resultPromise = dispatch({567 type: "PASSKEY_CREATE",568 payload: { rpId: "github.com", userId: "AQID", userName: "june", challenge: "x", origin: "https://github.com", clientDataJSON: Array.from(new TextEncoder().encode("{}")), pubKeyCredParams: [{ alg: -7, type: "public-key" }] },569 });570 await vi.waitFor(() => expect(chrome.action.openPopup).toHaveBeenCalled());571 await new Promise((resolve) => setTimeout(resolve, 20));572 await passkey.denyAllPendingPasskeyRequests();573 await expect(resultPromise).resolves.toMatchObject({ ok: true });574 });575576 it("PASSKEY_CREATE registers a passkey and increments signCount on PASSKEY_GET", async () => {577 const vault = await import("./vault");578 await vault.createVault("correct-pass-12345");579580 const challenge = "challenge-base64url";581 const origin = "https://github.com";582 const clientDataCreate = Array.from(new TextEncoder().encode(583 JSON.stringify({ type: "webauthn.create", challenge, origin, crossOrigin: false })584 ));585586 autoApprove();587 const createRes = await dispatch({588 type: "PASSKEY_CREATE",589 payload: {590 rpId: "github.com",591 rpName: "GitHub",592 userId: base64urlEncode(crypto.getRandomValues(new Uint8Array(16))),593 userName: "june",594 userDisplayName: "June",595 challenge,596 clientDataJSON: clientDataCreate,597 origin,598 pubKeyCredParams: [{ alg: -7, type: "public-key" }],599 },600 });601602 expect(createRes).toMatchObject({ ok: true });603 const cred = (createRes as any).credential;604 expect(cred.type).toBe("create");605 expect(typeof cred.id).toBe("string");606 expect(cred.algorithm).toBe(-7);607608 const stored = vault.getEntries().find((e) => e.passkey?.rpId === "github.com");609 expect(stored).toBeDefined();610 expect(stored!.passkey!.signCount).toBe(0);611 expect(stored!.passkey!.browser).toBe("firefox");612 expect(stored!.passkey!.backupEligible).toBe(true);613614 const clientDataGet = Array.from(new TextEncoder().encode(615 JSON.stringify({ type: "webauthn.get", challenge, origin, crossOrigin: false })616 ));617618 autoApprove();619 const getRes = await dispatch({620 type: "PASSKEY_GET",621 payload: {622 rpId: "github.com",623 challenge,624 clientDataJSON: clientDataGet,625 origin,626 allowCredentials: [{ id: cred.id, type: "public-key" }],627 },628 });629630 expect(getRes).toMatchObject({ ok: true });631 const assertion = (getRes as any).credential;632 expect(assertion.type).toBe("get");633 expect(typeof assertion.signature).toBe("string");634 expect(typeof assertion.authenticatorData).toBe("string");635 const authenticatorData = Uint8Array.from(atob(assertion.authenticatorData.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(assertion.authenticatorData.length / 4) * 4, "=")), (char) => char.charCodeAt(0));636 expect(authenticatorData[32]! & 0x08).toBe(0x08);637638 const after = vault.getEntries().find((e) => e.passkey?.rpId === "github.com");639 expect(after!.passkey!.signCount).toBe(1);640 expect(after!.passkey!.lastUsedAt).toBeDefined();641 });642});