1import { beforeEach, describe, expect, it, vi } from "vitest";2import { decrypt } from "./aes";3import { deriveKey } from "../crypto/argon2";4import type { StoredVault } from "../shared/types";56type StorageShape = Record<string, unknown>;78describe("background vault", () => {9 let storage: StorageShape;10 let alarms = { created: [] as Array<{ name: string; delayInMinutes: number }>, cleared: [] as string[] };1112 beforeEach(() => {13 vi.resetModules();14 storage = {};15 alarms = { created: [], cleared: [] };1617 const chromeMock = {18 storage: {19 local: {20 get: vi.fn(async (key?: string | string[]) => {21 if (!key) return storage;22 if (typeof key === "string") {23 return { [key]: storage[key] };24 }2526 return key.reduce<Record<string, unknown>>((result, entry) => {27 result[entry] = storage[entry];28 return result;29 }, {});30 }),31 set: vi.fn(async (next: Record<string, unknown>) => {32 Object.assign(storage, next);33 }),34 remove: vi.fn(async (keys: string | string[]) => {35 const list = Array.isArray(keys) ? keys : [keys];36 for (const key of list) {37 delete storage[key];38 }39 }),40 },41 },42 alarms: {43 clear: vi.fn((name: string) => {44 alarms.cleared.push(name);45 }),46 create: vi.fn((name: string, info: { delayInMinutes: number }) => {47 alarms.created.push({ name, delayInMinutes: info.delayInMinutes });48 }),49 onAlarm: { addListener: vi.fn() },50 },51 idle: {52 setDetectionInterval: vi.fn(),53 onStateChanged: { addListener: vi.fn() },54 },55 } as unknown as typeof chrome;5657 (globalThis as { chrome?: typeof chrome }).chrome = chromeMock;5859 60 vi.stubGlobal("fetch", vi.fn(async () => {61 throw new Error("network disabled in tests");62 }));63 });6465 it("creates a vault and stores encrypted payload", async () => {66 const vault = await import("./vault");6768 await vault.createVault("correct horse battery staple");6970 expect(storage.peach_vault_a).toBeDefined();71 expect((storage.peach_vault_a as StoredVault).ciphertext).toBeTruthy();72 expect(vault.getEntries()).toEqual([]);73 expect(alarms.created).toContainEqual({74 name: "autolock",75 delayInMinutes: 5,76 });77 });7879 it("writes both slots A and B on vault creation", async () => {80 const vault = await import("./vault");81 await vault.createVault("correct horse battery staple");8283 expect(storage.peach_vault_a).toBeDefined();84 expect(storage.peach_vault_b).toBeDefined();85 expect(storage.peach_duress).toBeUndefined();86 expect(storage.peach_vault).toBeUndefined();87 expect(storage.peach_decoy_vault).toBeUndefined();88 });8990 it("slot A and slot B have matching ciphertext size at fresh install", async () => {91 const vault = await import("./vault");92 await vault.createVault("correct horse battery staple");9394 const a = storage.peach_vault_a as StoredVault;95 const b = storage.peach_vault_b as StoredVault;96 expect(a.ciphertext.length).toBe(b.ciphertext.length);97 });9899 it("does not expose a plaintext duress flag after setupDuress", async () => {100 const vault = await import("./vault");101 await vault.createVault("real-password-12345");102 await vault.setupDuress("decoy-password-67890");103104 expect(storage.peach_duress).toBeUndefined();105 expect(storage.peach_decoy_vault).toBeUndefined();106 const keys = Object.keys(storage).filter((k) => k.startsWith("peach_"));107 108 109 for (const key of keys) {110 expect(111 [112 "peach_vault_a",113 "peach_vault_b",114 "peach_settings",115 "peach_vault_history",116 "peach_last_modified_at",117 ]118 ).toContain(key);119 }120 expect(keys).toContain("peach_vault_a");121 expect(keys).toContain("peach_vault_b");122 });123124 it("unlocks with real password and loads real entries", async () => {125 const vault = await import("./vault");126 await vault.createVault("real-password-12345");127 await vault.addEntry({128 title: "GitHub",129 username: "june",130 password: "realsecret",131 url: "github.com",132 });133 vault.lock();134135 const ok = await vault.unlock("real-password-12345");136 expect(ok).toBe(true);137 expect(vault.getEntries()).toHaveLength(1);138 expect(vault.getEntries()[0].title).toBe("GitHub");139 });140141 it("unlocks with duress password and loads empty entries", async () => {142 const vault = await import("./vault");143 await vault.createVault("real-password-12345");144 await vault.addEntry({145 title: "GitHub",146 username: "june",147 password: "realsecret",148 url: "github.com",149 });150 await vault.setupDuress("decoy-password-67890");151 vault.lock();152153 const ok = await vault.unlock("decoy-password-67890");154 expect(ok).toBe(true);155 expect(vault.getEntries()).toEqual([]);156 });157158 it("runs both slot derivations in parallel for constant-time unlock (DUR-002)", async () => {159 160 161 162 163 const argon2 = await import("../crypto/argon2");164 const deriveSpy = vi.spyOn(argon2, "deriveKey");165166 const vault = await import("./vault");167 await vault.createVault("real-password-12345");168 await vault.setupDuress("decoy-password-67890");169 vault.lock();170171 deriveSpy.mockClear();172 await vault.unlock("real-password-12345");173 expect(deriveSpy).toHaveBeenCalledTimes(2);174 vault.lock();175176 deriveSpy.mockClear();177 await vault.unlock("decoy-password-67890");178 expect(deriveSpy).toHaveBeenCalledTimes(2);179 vault.lock();180181 deriveSpy.mockClear();182 await vault.unlock("wrong-password-xxxxx");183 expect(deriveSpy).toHaveBeenCalledTimes(2);184185 deriveSpy.mockRestore();186 });187188 it("rejects wrong passwords", async () => {189 const vault = await import("./vault");190 await vault.createVault("real-password-12345");191 vault.lock();192193 const ok = await vault.unlock("nope-nope-nope");194 expect(ok).toBe(false);195 });196197 it("persists duressConfigured across lock/unlock in encrypted payload", async () => {198 const vault = await import("./vault");199 await vault.createVault("real-password-12345");200 await vault.setupDuress("decoy-password-67890");201 vault.lock();202203 await vault.unlock("real-password-12345");204 expect(await vault.isDuressEnabled()).toBe(true);205206 await vault.disableDuress();207 expect(await vault.isDuressEnabled()).toBe(false);208 vault.lock();209210 await vault.unlock("real-password-12345");211 expect(await vault.isDuressEnabled()).toBe(false);212 });213214 it("migrates legacy peach_vault / peach_decoy_vault / peach_duress schema", async () => {215 216 const firstVault = await import("./vault");217 await firstVault.createVault("legacy-password-abcde");218 const legacySlotA = storage.peach_vault_a as StoredVault;219 storage.peach_vault = legacySlotA;220 delete storage.peach_vault_a;221 delete storage.peach_vault_b;222 storage.peach_duress = { enabled: true };223 storage.peach_decoy_vault = legacySlotA; 224 vi.resetModules();225226 227 const vault = await import("./vault");228 await vault.hasVault();229230 231 expect(storage.peach_vault).toBeUndefined();232 expect(storage.peach_decoy_vault).toBeUndefined();233 expect(storage.peach_duress).toBeUndefined();234 expect(storage.peach_vault_a).toBeDefined();235 expect(storage.peach_vault_b).toBeDefined();236 });237238 it("soft deletes entries while keeping deletedAt in encrypted storage", async () => {239 const vault = await import("./vault");240241 await vault.createVault("correct horse battery staple");242 const entry = await vault.addEntry({243 title: "GitHub",244 username: "june",245 password: "secret123",246 url: "github.com",247 });248249 await vault.deleteEntry(entry.id);250251 expect(vault.getEntries()).toEqual([]);252253 const stored = storage.peach_vault_a as StoredVault;254 const salt = Uint8Array.from(255 stored.salt.match(/.{1,2}/g)!.map((value: string) => parseInt(value, 16))256 );257 const key = await deriveKey("correct horse battery staple", salt);258 const plain = await decrypt(key, stored.nonce, stored.ciphertext, stored.tag, "slot_a_v1");259 const parsed = JSON.parse(plain) as { entries: Array<{ id: string; deletedAt?: number }> };260261 expect(parsed.entries).toHaveLength(1);262 expect(parsed.entries[0].id).toBe(entry.id);263 expect(typeof parsed.entries[0].deletedAt).toBe("number");264 });265266 it("accepts imported entries without a username or url", async () => {267 const vault = await import("./vault");268269 await vault.createVault("correct horse battery staple");270 const entry = await vault.addEntry({271 title: "Imported Login",272 username: "",273 password: "secret123",274 url: "",275 notes: "migrated from another manager",276 });277278 expect(entry.username).toBe("");279 expect(entry.url).toBe("");280 expect(vault.getEntries()).toHaveLength(1);281 });282283 it("changePassword happy path — new password unlocks, old is rejected", async () => {284 const vault = await import("./vault");285286 await vault.createVault("old-pass-abc123");287 await vault.addEntry({ title: "GitHub", username: "june", password: "secret1", url: "github.com" });288 await vault.addEntry({ title: "Gmail", username: "june", password: "secret2", url: "mail.google.com" });289290 expect(await vault.changePassword("old-pass-abc123", "new-pass-xyz789")).toBe(true);291292 vault.lock();293 expect(await vault.unlock("new-pass-xyz789")).toBe(true);294 expect(vault.getEntries()).toHaveLength(2);295296 vault.lock();297 expect(await vault.unlock("old-pass-abc123")).toBe(false);298 });299300 it("stamps peach_last_modified_at on createVault", async () => {301 const vault = await import("./vault");302 const before = Date.now();303 await new Promise((resolve) => setTimeout(resolve, 1));304305 await vault.createVault("stamp-create-pass");306307 expect(Number(storage.peach_last_modified_at)).toBeGreaterThanOrEqual(before);308 });309310 it("stamps peach_last_modified_at on entry mutations through persistEntries", async () => {311 const vault = await import("./vault");312 await vault.createVault("stamp-entry-pass");313314 let previous = Number(storage.peach_last_modified_at);315 await new Promise((resolve) => setTimeout(resolve, 1));316 const entry = await vault.addEntry({317 title: "Stamp",318 username: "",319 password: "p",320 url: "",321 });322 expect(Number(storage.peach_last_modified_at)).toBeGreaterThan(previous);323324 previous = Number(storage.peach_last_modified_at);325 await new Promise((resolve) => setTimeout(resolve, 1));326 await vault.updateEntry(entry.id, { title: "Stamped" });327 expect(Number(storage.peach_last_modified_at)).toBeGreaterThan(previous);328329 previous = Number(storage.peach_last_modified_at);330 await new Promise((resolve) => setTimeout(resolve, 1));331 await vault.deleteEntry(entry.id);332 expect(Number(storage.peach_last_modified_at)).toBeGreaterThan(previous);333 });334335 it("stamps peach_last_modified_at on setupDuress and changePassword", async () => {336 const vault = await import("./vault");337 await vault.createVault("stamp-real-pass");338339 let previous = Number(storage.peach_last_modified_at);340 await new Promise((resolve) => setTimeout(resolve, 1));341 await vault.setupDuress("stamp-decoy-pass");342 expect(Number(storage.peach_last_modified_at)).toBeGreaterThan(previous);343344 previous = Number(storage.peach_last_modified_at);345 await new Promise((resolve) => setTimeout(resolve, 1));346 expect(await vault.changePassword("stamp-real-pass", "stamp-new-pass")).toBe(true);347 expect(Number(storage.peach_last_modified_at)).toBeGreaterThan(previous);348 });349350 it("changePassword with wrong current password returns false and leaves slot A unchanged", async () => {351 const vault = await import("./vault");352353 await vault.createVault("correct-pass-abcde");354 const slotABefore = JSON.stringify(storage.peach_vault_a);355356 expect(await vault.changePassword("wrong-pass", "new-pass")).toBe(false);357 expect(JSON.stringify(storage.peach_vault_a)).toBe(slotABefore);358359 vault.lock();360 expect(await vault.unlock("correct-pass-abcde")).toBe(true);361 });362363 it("changePassword with duress configured leaves slot B byte-identical", async () => {364 const vault = await import("./vault");365366 await vault.createVault("real-pass-abcde");367 await vault.setupDuress("duress-pass-xyzzz");368369 const slotBBefore = JSON.stringify(storage.peach_vault_b);370 expect(await vault.changePassword("real-pass-abcde", "new-real-pass-fghij")).toBe(true);371 expect(JSON.stringify(storage.peach_vault_b)).toBe(slotBBefore);372373 374 vault.lock();375 expect(await vault.unlock("duress-pass-xyzzz")).toBe(true);376 expect(vault.getEntries()).toEqual([]);377 });378379 it("restoreEntries with mode 'replace' and empty incoming throws and leaves entries intact", async () => {380 const vault = await import("./vault");381382 await vault.createVault("pass-abcdef");383 await vault.addEntry({ title: "One", username: "", password: "1", url: "" });384 await vault.addEntry({ title: "Two", username: "", password: "2", url: "" });385 await vault.addEntry({ title: "Three", username: "", password: "3", url: "" });386387 await expect(vault.restoreEntries([], "replace")).rejects.toThrow(388 /refuse to replace with empty entry set/i389 );390 expect(vault.getEntries()).toHaveLength(3);391 });392393 it("changePassword swaps the active key before Slot B is regenerated", async () => {394 const vault = await import("./vault");395396 await vault.createVault("correct-pass-12345");397 const entry = await vault.addEntry({398 title: "GitHub",399 username: "june",400 password: "secret123",401 url: "https://github.com",402 });403404 const setMock = chrome.storage.local.set as unknown as ReturnType<typeof vi.fn>;405 const originalSet = setMock.getMockImplementation();406 let injectedDuringSlotBWrite = false;407408 setMock.mockImplementation(async (next: Record<string, unknown>) => {409 Object.assign(storage, next);410411 if (!injectedDuringSlotBWrite && "peach_vault_b" in next) {412 injectedDuringSlotBWrite = true;413 await vault.updateEntry(entry.id, { notes: "updated during password change" });414 }415 });416417 expect(await vault.changePassword("correct-pass-12345", "new-pass-67890")).toBe(true);418419 setMock.mockImplementation(originalSet ?? (async (next: Record<string, unknown>) => {420 Object.assign(storage, next);421 }));422423 vault.lock();424 expect(await vault.unlock("new-pass-67890")).toBe(true);425 expect(vault.getEntryById(entry.id)?.notes).toBe("updated during password change");426427 vault.lock();428 expect(await vault.unlock("correct-pass-12345")).toBe(false);429 });430431 it("restoreEntries replace mode replaces existing entries", async () => {432 const vault = await import("./vault");433434 await vault.createVault("replace-pass-12345");435 await vault.addEntry({ title: "A", username: "june", password: "one", url: "https://a.test" });436 await vault.addEntry({ title: "B", username: "june", password: "two", url: "https://b.test" });437438 const incoming = [439 {440 id: "entry-c",441 type: "login" as const,442 title: "C",443 username: "june",444 password: "three",445 url: "https://c.test",446 createdAt: 10,447 modifiedAt: 10,448 },449 {450 id: "entry-d",451 type: "login" as const,452 title: "D",453 username: "june",454 password: "four",455 url: "https://d.test",456 createdAt: 20,457 modifiedAt: 20,458 },459 ];460461 await expect(vault.restoreEntries(incoming, "replace")).resolves.toEqual({462 added: 2,463 updated: 0,464 skipped: 0,465 });466467 expect(vault.getEntries().map((entry) => entry.id)).toEqual(["entry-c", "entry-d"]);468 });469470 it("restoreEntries merge mode lets newer incoming entries win", async () => {471 const vault = await import("./vault");472473 await vault.createVault("merge-pass-12345");474 const existing = await vault.addEntry({475 title: "GitHub",476 username: "june",477 password: "old-secret",478 url: "https://github.com",479 });480481 await expect(482 vault.restoreEntries(483 [484 {485 ...existing,486 password: "new-secret",487 modifiedAt: existing.modifiedAt + 100,488 },489 ],490 "merge"491 )492 ).resolves.toEqual({ added: 0, updated: 1, skipped: 0 });493494 expect(vault.getEntryById(existing.id)?.password).toBe("new-secret");495 });496497 it("restoreEntries merge mode keeps newer local entries when incoming is older", async () => {498 const vault = await import("./vault");499500 await vault.createVault("merge-pass-67890");501 const existing = await vault.addEntry({502 title: "GitHub",503 username: "june",504 password: "current-secret",505 url: "https://github.com",506 });507508 await expect(509 vault.restoreEntries(510 [511 {512 ...existing,513 password: "stale-secret",514 modifiedAt: existing.modifiedAt - 100,515 },516 ],517 "merge"518 )519 ).resolves.toEqual({ added: 0, updated: 0, skipped: 1 });520521 expect(vault.getEntryById(existing.id)?.password).toBe("current-secret");522 });523524 it("clearVaultEntries without explicit confirmation throws", async () => {525 const vault = await import("./vault");526527 await vault.createVault("clear-pass-12345");528 await vault.addEntry({ title: "A", username: "", password: "one", url: "" });529530 const clearVaultEntries = vault.clearVaultEntries as unknown as (531 confirmation?: unknown532 ) => Promise<void>;533534 await expect(clearVaultEntries()).rejects.toThrow(/explicit confirmation/i);535 await expect(clearVaultEntries("wrong")).rejects.toThrow(/explicit confirmation/i);536 expect(vault.getEntries()).toHaveLength(1);537 });538539 it("clearVaultEntries with REPLACE_ALL soft-deletes all entries", async () => {540 const vault = await import("./vault");541542 await vault.createVault("clear-pass-67890");543 await vault.addEntry({ title: "A", username: "", password: "one", url: "" });544 await vault.addEntry({ title: "B", username: "", password: "two", url: "" });545 await vault.addEntry({ title: "C", username: "", password: "three", url: "" });546547 await vault.clearVaultEntries("REPLACE_ALL");548549 expect(vault.getEntries()).toEqual([]);550 expect(vault.getDeletedEntries()).toHaveLength(3);551 expect(vault.getDeletedEntries().every((entry) => typeof entry.deletedAt === "number")).toBe(true);552 });553554 it("deduplicateEntries throws in decoy mode", async () => {555 const vault = await import("./vault");556557 await vault.createVault("real-pass-12345");558 await vault.addEntry({559 title: "GitHub",560 username: "june",561 password: "secret123",562 url: "https://github.com",563 });564 await vault.setupDuress("duress-pass-12345");565 vault.lock();566567 expect(await vault.unlock("duress-pass-12345")).toBe(true);568 await expect(vault.deduplicateEntries()).rejects.toThrow(/decoy mode/i);569 });570571 it("deduplicateEntries removes older duplicates in real mode", async () => {572 const vault = await import("./vault");573574 await vault.createVault("dedupe-pass-12345");575 await vault.addEntry({576 title: "GitHub",577 username: "june",578 password: "old-secret",579 url: "https://github.com",580 });581 await new Promise<void>((resolve) => setTimeout(resolve, 5));582 const newer = await vault.addEntry({583 title: "GitHub",584 username: "june",585 password: "new-secret",586 url: "https://github.com",587 });588589 expect(await vault.deduplicateEntries()).toBe(1);590 expect(vault.getEntries()).toHaveLength(1);591 expect(vault.getEntries()[0].id).toBe(newer.id);592 expect(vault.getDeletedEntries()).toHaveLength(1);593 });594595 it("updateEntry with an empty password on a login entry clears the password", async () => {596 const vault = await import("./vault");597598 await vault.createVault("update-pass-12345");599 const entry = await vault.addEntry({600 title: "GitHub",601 username: "june",602 password: "secret123",603 url: "https://github.com",604 });605606 await expect(vault.updateEntry(entry.id, { password: "" })).resolves.toBeUndefined();607 expect(vault.getEntryById(entry.id)?.password).toBeUndefined();608 });609610 it("updateEntry with an empty password on a note entry is allowed", async () => {611 const vault = await import("./vault");612613 await vault.createVault("note-pass-12345");614 const entry = await vault.addEntry({615 type: "note",616 title: "Secure note",617 notes: "keep me",618 });619620 await expect(vault.updateEntry(entry.id, { password: "", notes: "updated" })).resolves.toBeUndefined();621 expect(vault.getEntryById(entry.id)).toMatchObject({622 type: "note",623 notes: "updated",624 });625 });626627 it("lock zeroes the derived key and resets unlock state", async () => {628 const aes = await import("./aes");629 const zeroKeySpy = vi.spyOn(aes, "zeroKey");630 const vault = await import("./vault");631632 await vault.createVault("lock-pass-12345");633 expect(vault.isUnlocked()).toBe(true);634635 vault.lock();636637 expect(zeroKeySpy).toHaveBeenCalled();638 expect(vault.isUnlocked()).toBe(false);639 zeroKeySpy.mockRestore();640 });641642 it("lock clears in-memory entries", async () => {643 const vault = await import("./vault");644645 await vault.createVault("lock-pass-67890");646 const entry = await vault.addEntry({647 title: "GitHub",648 username: "june",649 password: "secret123",650 url: "https://github.com",651 });652653 expect(vault.getEntryById(entry.id)?.title).toBe("GitHub");654 vault.lock();655 expect(vault.getEntryById(entry.id)).toBeUndefined();656 });657658 it("unlock with neither slot decryptable returns false and leaves the vault locked", async () => {659 const vault = await import("./vault");660661 await vault.createVault("real-pass-67890");662 vault.lock();663664 expect(await vault.unlock("totally-wrong-password")).toBe(false);665 expect(vault.isUnlocked()).toBe(false);666 });667668 it("isUnlocked tracks the full lock lifecycle", async () => {669 const vault = await import("./vault");670671 expect(vault.isUnlocked()).toBe(false);672 await vault.createVault("unlock-pass-12345");673 expect(vault.isUnlocked()).toBe(true);674 vault.lock();675 expect(vault.isUnlocked()).toBe(false);676 expect(await vault.unlock("unlock-pass-12345")).toBe(true);677 expect(vault.isUnlocked()).toBe(true);678 vault.lock();679 expect(vault.isUnlocked()).toBe(false);680 });681682 it("persistEntries surfaces STORAGE_QUOTA_EXCEEDED when slot writes hit quota", async () => {683 const vault = await import("./vault");684685 await vault.createVault("quota-pass-12345");686687 const setMock = chrome.storage.local.set as unknown as ReturnType<typeof vi.fn>;688 setMock.mockImplementation(async (next: Record<string, unknown>) => {689 if ("peach_vault_a" in next || "peach_vault_b" in next) {690 throw new Error("QUOTA_BYTES quota reached");691 }692 Object.assign(storage, next);693 });694695 await expect(696 vault.addEntry({697 title: "GitHub",698 username: "june",699 password: "secret123",700 url: "https://github.com",701 })702 ).rejects.toThrow("STORAGE_QUOTA_EXCEEDED");703 });704705 it("persistSettings surfaces STORAGE_QUOTA_EXCEEDED when settings writes hit quota", async () => {706 const vault = await import("./vault");707708 await vault.createVault("settings-pass-12345");709710 const setMock = chrome.storage.local.set as unknown as ReturnType<typeof vi.fn>;711 setMock.mockImplementation(async (next: Record<string, unknown>) => {712 if ("peach_settings" in next) {713 throw new Error("QUOTA_BYTES quota reached");714 }715 Object.assign(storage, next);716 });717718 await expect(vault.updateSettings({ autoLockMinutes: 10 })).rejects.toThrow(719 "STORAGE_QUOTA_EXCEEDED"720 );721 });722723 it("card entry round-trips through vault — all fields preserved", async () => {724 const vault = await import("./vault");725726 await vault.createVault("card-test-pass-12345");727 const entry = await vault.addEntry({728 type: "card",729 title: "Personal Visa",730 cardholderName: "Jane Smith",731 cardNumber: "4111111111111111",732 expMonth: "05",733 expYear: "2028",734 cvc: "123",735 cardNickname: "Personal Visa",736 network: "visa",737 last4: "1111",738 });739740 expect(entry.type).toBe("card");741 expect(entry.cardholderName).toBe("Jane Smith");742 expect(entry.cardNumber).toBe("4111111111111111");743 expect(entry.expMonth).toBe("05");744 expect(entry.expYear).toBe("2028");745 expect(entry.cvc).toBe("123");746 expect(entry.network).toBe("visa");747 expect(entry.last4).toBe("1111");748749 vault.lock();750 await vault.unlock("card-test-pass-12345");751 const entries = vault.getEntries();752 expect(entries).toHaveLength(1);753 expect(entries[0].cardNumber).toBe("4111111111111111");754 expect(entries[0].cvc).toBe("123");755 });756757 it("updateEntry — cannot clear card number on card entry", async () => {758 const vault = await import("./vault");759760 await vault.createVault("card-guard-pass-12345");761 const entry = await vault.addEntry({762 type: "card",763 title: "My Card",764 cardholderName: "Jane Smith",765 cardNumber: "4111111111111111",766 expMonth: "05",767 expYear: "2028",768 cvc: "123",769 });770771 await expect(772 vault.updateEntry(entry.id, { cardNumber: "" })773 ).rejects.toThrow("card number is required for card entries");774775 const entries = vault.getEntries();776 expect(entries[0].cardNumber).toBe("4111111111111111");777 });778779 it("declineCardSaveOffer — stores fingerprint in settings", async () => {780 const vault = await import("./vault");781782 await vault.createVault("decline-pass-12345");783 await vault.declineCardSaveOffer("1111", "05", "2028");784785 const settings = vault.getSettings();786 expect(settings.declinedCardFingerprints).toHaveLength(1);787 });788789 it("isCardDeclined — returns true for previously declined card", async () => {790 const vault = await import("./vault");791792 await vault.createVault("declined-check-pass-12345");793 await vault.declineCardSaveOffer("1111", "05", "2028");794795 expect(await vault.isCardDeclined("1111", "05", "2028")).toBe(true);796 });797798 it("isCardDeclined — returns false for card with same last4 but different expiry", async () => {799 const vault = await import("./vault");800801 await vault.createVault("declined-diff-pass-12345");802 await vault.declineCardSaveOffer("1111", "05", "2028");803804 expect(await vault.isCardDeclined("1111", "06", "2028")).toBe(false);805 });806807 it("queryDomain matches an entry against its additionalUrls", async () => {808 const vault = await import("./vault");809810 await vault.createVault("multi-domain-pass-12345");811 const entry = await vault.addEntry({812 type: "login",813 title: "Foo",814 username: "june",815 password: "secret",816 url: "https://foo.com",817 additionalUrls: ["https://bar.io", "https://baz.org"],818 });819820 821 822 expect(vault.queryDomain("foo.com").map((e) => e.id)).toContain(entry.id);823 expect(vault.queryDomain("bar.io").map((e) => e.id)).toContain(entry.id);824 expect(vault.queryDomain("baz.org").map((e) => e.id)).toContain(entry.id);825 expect(vault.queryDomain("unrelated.com")).toEqual([]);826 });827828 it("updateEntry trims, dedupes, and clears empty additionalUrls", async () => {829 const vault = await import("./vault");830831 await vault.createVault("dedupe-additional-pass-12345");832 const entry = await vault.addEntry({833 type: "login",834 title: "Foo",835 username: "june",836 password: "secret",837 url: "https://foo.com",838 });839840 await vault.updateEntry(entry.id, {841 additionalUrls: [842 " https://login.foo.com ",843 "https://login.foo.com",844 "",845 " ",846 "https://accounts.foo.com",847 ],848 });849850 const stored = vault.getEntryById(entry.id);851 expect(stored?.additionalUrls).toEqual([852 "https://login.foo.com",853 "https://accounts.foo.com",854 ]);855856 await vault.updateEntry(entry.id, { additionalUrls: ["", " "] });857 expect(vault.getEntryById(entry.id)?.additionalUrls).toBeUndefined();858 });859860 it("preserves email field through lock/unlock cycle", async () => {861 const vault = await import("./vault");862 await vault.createVault("preserve-email-pass-12345");863 await vault.addEntry({864 type: "login",865 title: "GitHub",866 username: "june",867 email: "[email protected]",868 password: "secret",869 url: "https://github.com",870 });871 vault.lock();872873 const ok = await vault.unlock("preserve-email-pass-12345");874 expect(ok).toBe(true);875 const entry = vault.getEntries().find((e) => e.title === "GitHub");876 expect(entry?.email).toBe("[email protected]");877 });878879 it("preserves additionalUrls through lock/unlock cycle", async () => {880 const vault = await import("./vault");881 await vault.createVault("preserve-urls-pass-12345");882 await vault.addEntry({883 type: "login",884 title: "Example",885 username: "june",886 password: "secret",887 url: "https://example.com",888 additionalUrls: [889 "https://beta.example.com",890 "https://staging.example.com",891 ],892 });893 vault.lock();894895 const ok = await vault.unlock("preserve-urls-pass-12345");896 expect(ok).toBe(true);897 const entry = vault.getEntries().find((e) => e.title === "Example");898 expect(entry?.additionalUrls).toEqual([899 "https://beta.example.com",900 "https://staging.example.com",901 ]);902 });903904 it("normalizeEntry preserves passwordChangedAt through lock/unlock", async () => {905 const vault = await import("./vault");906 await vault.createVault("pwchanged-preserve-pass");907 const entry = await vault.addEntry({908 title: "Aged",909 username: "june",910 password: "secret",911 url: "https://aged.test",912 });913 914 915 const ts = entry.passwordChangedAt;916 expect(typeof ts).toBe("number");917 vault.lock();918919 expect(await vault.unlock("pwchanged-preserve-pass")).toBe(true);920 const restored = vault.getEntries().find((e) => e.id === entry.id);921 expect(restored?.passwordChangedAt).toBe(ts);922 });923924 it("normalizeEntry handles entries without passwordChangedAt", async () => {925 const vault = await import("./vault");926 await vault.createVault("pwchanged-absent-pass");927 928 const note = await vault.addEntry({ type: "note", title: "Memo", notes: "x" });929 vault.lock();930931 expect(await vault.unlock("pwchanged-absent-pass")).toBe(true);932 const restored = vault.getEntries().find((e) => e.id === note.id);933 expect(restored?.passwordChangedAt).toBeUndefined();934 });935936 it("applyMergedRemoteEntries does not advance the vault version", async () => {937 const vault = await import("./vault");938 await vault.createVault("syncver-pass-aaaaa");939 await vault.addEntry({ title: "Local", username: "june", password: "p", url: "https://l.test" });940941 const before = await vault.getVaultVersion();942 await new Promise((r) => setTimeout(r, 2));943 await vault.applyMergedRemoteEntries(944 [945 {946 id: "remote-1",947 type: "login",948 title: "Remote",949 username: "r",950 password: "x",951 url: "https://r.test",952 createdAt: Date.now(),953 modifiedAt: Date.now(),954 },955 ],956 {},957 );958959 expect(await vault.getVaultVersion()).toBe(before);960 });961962 it("addEntry advances the vault version", async () => {963 const vault = await import("./vault");964 await vault.createVault("syncver-pass-bbbbb");965966 const before = await vault.getVaultVersion();967 await new Promise((r) => setTimeout(r, 2));968 await vault.addEntry({ title: "Local", username: "june", password: "p", url: "https://l.test" });969970 expect(await vault.getVaultVersion()).toBeGreaterThan(before);971 });972973 it("modifiedAt is monotonic even when the wall clock regresses (SEV-2-E)", async () => {974 const vault = await import("./vault");975 await vault.createVault("monotonic-pass-aaaaa");976 const e1 = await vault.addEntry({ title: "M", username: "june", password: "p1", url: "https://m.test" });977 const before = vault.getEntryById(e1.id)!.modifiedAt;978979 980 const spy = vi.spyOn(Date, "now").mockReturnValue(before - 10_000);981 await vault.updateEntry(e1.id, { password: "p2" });982 const after = vault.getEntryById(e1.id)!.modifiedAt;983 expect(after).toBeGreaterThan(before);984 spy.mockRestore();985 });986987 it("deletedAt is strictly greater than the entry's modifiedAt", async () => {988 const vault = await import("./vault");989 await vault.createVault("monotonic-pass-bbbbb");990 const e = await vault.addEntry({ title: "D", username: "june", password: "p", url: "https://d.test" });991 const modAt = vault.getEntryById(e.id)!.modifiedAt;992 await vault.deleteEntry(e.id);993 const tomb = vault.getDeletedEntries().find((t) => t.id === e.id)!;994 expect(tomb.deletedAt!).toBeGreaterThan(modAt);995 });996997 it("updateEntry rolls back in-memory state when the disk write fails (SEV-3-A)", async () => {998 const vault = await import("./vault");999 await vault.createVault("rollback-pass-aaaaa");1000 const e = await vault.addEntry({ title: "R", username: "june", password: "original", url: "https://r.test" });10011002 const setSpy = chrome.storage.local.set as unknown as ReturnType<typeof vi.fn>;1003 1004 setSpy.mockImplementation(async (next: Record<string, unknown>) => {1005 if ("peach_vault_a" in next) throw new Error("QUOTA_BYTES quota exceeded");1006 Object.assign(storage, next);1007 });10081009 await expect(vault.updateEntry(e.id, { password: "changed" })).rejects.toThrow();10101011 setSpy.mockImplementation(async (next: Record<string, unknown>) => { Object.assign(storage, next); });1012 1013 expect(vault.getEntryById(e.id)!.password).toBe("original");1014 });10151016 it("getRootKeyHex is stable across a password change (SEV-2-B)", async () => {1017 const vault = await import("./vault");1018 await vault.createVault("rootkey-pass-aaaaa");1019 const before = vault.getRootKeyHex();1020 expect(before).toMatch(/^[0-9a-f]{64}$/);10211022 expect(await vault.changePassword("rootkey-pass-aaaaa", "rootkey-pass-bbbbb")).toBe(true);1023 expect(vault.getRootKeyHex()).toBe(before);10241025 1026 vault.lock();1027 expect(await vault.unlock("rootkey-pass-bbbbb")).toBe(true);1028 expect(vault.getRootKeyHex()).toBe(before);1029 });10301031 it("getRootKeyHex returns null when the vault is locked", async () => {1032 const vault = await import("./vault");1033 await vault.createVault("rootkey-pass-ccccc");1034 vault.lock();1035 expect(vault.getRootKeyHex()).toBeNull();1036 });10371038 it("unlock_with_key_rehydrates_root_key (PCH-01-009)", async () => {1039 const vault = await import("./vault");1040 await vault.createVault("biometric-pass-12345");1041 const rootBefore = vault.getRootKeyHex();1042 const keyHex = vault.getDerivedKeyHex();1043 expect(rootBefore).not.toBeNull();1044 vault.lock();1045 1046 const r = await vault.unlockWithKey(keyHex);1047 expect(r.ok).toBe(true);1048 expect(vault.getRootKeyHex()).toBe(rootBefore);1049 });10501051 it("unlock_with_key_succeeds_without_escrow (PCH-01-009)", async () => {1052 const vault = await import("./vault");1053 await vault.createVault("legacy-pass-12345");1054 const keyHex = vault.getDerivedKeyHex();1055 vault.lock();1056 1057 1058 const slot = storage.peach_vault_a as Record<string, unknown>;1059 delete slot.recovery_salt;1060 delete slot.recovery_wrapped_root_key;1061 delete slot.pw_root_ciphertext;1062 delete slot.pw_root_nonce;1063 delete slot.pw_root_tag;1064 const r = await vault.unlockWithKey(keyHex);1065 expect(r.ok).toBe(true);1066 expect(vault.getRootKeyHex()).toBeNull();1067 });10681069 it("duress keeps both slots equal length at rest, even as the real vault grows (audit MEDIUM)", async () => {1070 const vault = await import("./vault");1071 await vault.createVault("duress-real-pass-aaaa");1072 await vault.setupDuress("duress-decoy-pass-bbbb");10731074 const lenA = () => (storage.peach_vault_a as StoredVault).ciphertext.length;1075 const lenB = () => (storage.peach_vault_b as StoredVault).ciphertext.length;10761077 1078 expect(lenA()).toBe(lenB());10791080 1081 1082 await vault.addEntry({ title: "One", username: "u", password: "p", url: "https://a.test" });1083 await vault.addEntry({ title: "Two", username: "u", password: "p", url: "https://b.test" });1084 expect(lenA()).toBe(lenB());1085 });10861087 it("deleteEntry rolls back the tombstone when the disk write fails (SEV-3-A)", async () => {1088 const vault = await import("./vault");1089 await vault.createVault("rollback-pass-bbbbb");1090 const e = await vault.addEntry({ title: "R", username: "june", password: "p", url: "https://r.test" });10911092 const setSpy = chrome.storage.local.set as unknown as ReturnType<typeof vi.fn>;1093 setSpy.mockImplementation(async (next: Record<string, unknown>) => {1094 if ("peach_vault_a" in next) throw new Error("QUOTA_BYTES quota exceeded");1095 Object.assign(storage, next);1096 });10971098 await expect(vault.deleteEntry(e.id)).rejects.toThrow();10991100 setSpy.mockImplementation(async (next: Record<string, unknown>) => { Object.assign(storage, next); });1101 1102 expect(vault.getEntryById(e.id)).toBeDefined();1103 expect(vault.getDeletedEntries().find((t) => t.id === e.id)).toBeUndefined();1104 });11051106 describe("password history", () => {1107 it("captures the old password on change, newest first", async () => {1108 const vault = await import("./vault");1109 await vault.createVault("history-pass-12345");1110 const e = await vault.addEntry({ title: "H", username: "june", password: "first", url: "https://h.test" });1111 expect(vault.getEntryById(e.id)?.passwordHistory).toBeUndefined();11121113 await vault.updateEntry(e.id, { password: "second" });1114 await vault.updateEntry(e.id, { password: "third" });11151116 const hist = vault.getEntryById(e.id)?.passwordHistory ?? [];1117 expect(hist.map((h) => h.password)).toEqual(["second", "first"]);1118 expect(vault.getEntryById(e.id)?.password).toBe("third");1119 });11201121 it("does not record history on a metadata-only update", async () => {1122 const vault = await import("./vault");1123 await vault.createVault("history-meta-12345");1124 const e = await vault.addEntry({ title: "M", username: "june", password: "keepme", url: "https://m.test" });11251126 await vault.updateEntry(e.id, { title: "Renamed" });1127 await vault.updateEntry(e.id, { username: "newuser" });11281129 expect(vault.getEntryById(e.id)?.passwordHistory).toBeUndefined();1130 });11311132 it("records each history item with the age the password carried", async () => {1133 const vault = await import("./vault");1134 await vault.createVault("history-stamp-12345");1135 const e = await vault.addEntry({ title: "S", username: "june", password: "v1", url: "https://s.test" });1136 const firstChangedAt = vault.getEntryById(e.id)!.passwordChangedAt;1137 expect(firstChangedAt).toBeGreaterThan(0);11381139 await new Promise((resolve) => setTimeout(resolve, 2));1140 await vault.updateEntry(e.id, { password: "v2" });11411142 const hist = vault.getEntryById(e.id)!.passwordHistory!;1143 1144 expect(hist[0].changedAt).toBe(firstChangedAt);1145 });11461147 it("skips capture when re-saving the identical password", async () => {1148 const vault = await import("./vault");1149 await vault.createVault("history-noop-12345");1150 const e = await vault.addEntry({ title: "N", username: "june", password: "same", url: "https://n.test" });11511152 await vault.updateEntry(e.id, { password: "same" });11531154 expect(vault.getEntryById(e.id)?.passwordHistory).toBeUndefined();1155 });11561157 it("does not capture an empty prior password (clear then set)", async () => {1158 const vault = await import("./vault");1159 await vault.createVault("history-empty-12345");1160 const e = await vault.addEntry({ title: "E", username: "june", password: "", url: "https://e.test" });11611162 await vault.updateEntry(e.id, { password: "now-has-one" });11631164 1165 expect(vault.getEntryById(e.id)?.passwordHistory ?? []).toHaveLength(0);1166 });11671168 it("restoring an old password re-records the swapped-out current one", async () => {1169 const vault = await import("./vault");1170 await vault.createVault("history-restore-12345");1171 const e = await vault.addEntry({ title: "R", username: "june", password: "old", url: "https://r.test" });1172 await vault.updateEntry(e.id, { password: "new" });11731174 1175 await vault.updateEntry(e.id, { password: "old" });11761177 const hist = vault.getEntryById(e.id)!.passwordHistory!;1178 expect(vault.getEntryById(e.id)?.password).toBe("old");1179 expect(hist.map((h) => h.password)).toEqual(["new", "old"]);1180 });11811182 it("preserves history across lock/unlock (normalizeEntry whitelist)", async () => {1183 const vault = await import("./vault");1184 await vault.createVault("history-persist-12345");1185 const e = await vault.addEntry({ title: "P", username: "june", password: "p1", url: "https://p.test" });1186 await vault.updateEntry(e.id, { password: "p2" });11871188 vault.lock();1189 expect(await vault.unlock("history-persist-12345")).toBe(true);11901191 const hist = vault.getEntryById(e.id)?.passwordHistory ?? [];1192 expect(hist.map((h) => h.password)).toEqual(["p1"]);1193 });11941195 it("caps history at 50 entries, dropping the oldest", async () => {1196 const vault = await import("./vault");1197 await vault.createVault("history-cap-12345");1198 const e = await vault.addEntry({ title: "C", username: "june", password: "pw-0", url: "https://c.test" });11991200 for (let i = 1; i <= 55; i++) {1201 await vault.updateEntry(e.id, { password: `pw-${i}` });1202 }12031204 const hist = vault.getEntryById(e.id)!.passwordHistory!;1205 expect(hist).toHaveLength(50);1206 1207 expect(hist[0].password).toBe("pw-54");1208 expect(hist[hist.length - 1].password).toBe("pw-5");1209 });1210 });12111212 describe("MV-6 import field whitelist (sanitizeRestoredEntry)", () => {1213 it("drops unknown/attacker keys from a restored entry", async () => {1214 const vault = await import("./vault");1215 await vault.createVault("correct horse battery staple");1216 await vault.restoreEntries(1217 [1218 {1219 id: "e1",1220 type: "login",1221 title: "Bank",1222 username: "june",1223 password: "pw",1224 createdAt: 1,1225 modifiedAt: 2,1226 evil: "payload",1227 isAdmin: true,1228 nested: { junk: 1 },1229 } as unknown as import("../shared/types").VaultEntry,1230 ],1231 "replace",1232 );1233 const restored = vault.getEntries().find((e) => e.id === "e1")!;1234 expect(restored.password).toBe("pw");1235 expect((restored as unknown as Record<string, unknown>).evil).toBeUndefined();1236 expect((restored as unknown as Record<string, unknown>).isAdmin).toBeUndefined();1237 expect((restored as unknown as Record<string, unknown>).nested).toBeUndefined();1238 });12391240 it("strips a non-HTTPS canary webhook but keeps the entry", async () => {1241 const vault = await import("./vault");1242 await vault.createVault("correct horse battery staple");1243 await vault.restoreEntries(1244 [1245 {1246 id: "e2",1247 type: "login",1248 title: "Bank",1249 password: "pw",1250 createdAt: 1,1251 modifiedAt: 2,1252 canary: { webhookUrl: "http://attacker.example.com/x" },1253 } as unknown as import("../shared/types").VaultEntry,1254 ],1255 "replace",1256 );1257 const restored = vault.getEntries().find((e) => e.id === "e2")!;1258 expect(restored).toBeDefined();1259 expect(restored.canary).toBeUndefined();1260 });12611262 it("preserves a well-formed HTTPS canary webhook", async () => {1263 const vault = await import("./vault");1264 await vault.createVault("correct horse battery staple");1265 await vault.restoreEntries(1266 [1267 {1268 id: "e3",1269 type: "login",1270 title: "Bank",1271 password: "pw",1272 createdAt: 1,1273 modifiedAt: 2,1274 canary: { webhookUrl: "https://hooks.example.com/x", label: "watch" },1275 } as unknown as import("../shared/types").VaultEntry,1276 ],1277 "replace",1278 );1279 const restored = vault.getEntries().find((e) => e.id === "e3")!;1280 expect(restored.canary?.webhookUrl).toBe("https://hooks.example.com/x");1281 expect(restored.canary?.label).toBe("watch");1282 });12831284 it("rejects an entry whose passkey shape is malformed (missing credentialId)", async () => {1285 const vault = await import("./vault");1286 await vault.createVault("correct horse battery staple");1287 await vault.restoreEntries(1288 [1289 {1290 id: "e4",1291 type: "login",1292 title: "Has bad passkey",1293 createdAt: 1,1294 modifiedAt: 2,1295 passkey: { rpId: "example.com", publicKeyCose: "x" },1296 } as unknown as import("../shared/types").VaultEntry,1297 ],1298 "replace",1299 );1300 expect(vault.getEntries().some((e) => e.id === "e4")).toBe(false);1301 });13021303 it("migrates a pre-v2.11 flat-passkey backup entry into the passkey sub-object", async () => {1304 const vault = await import("./vault");1305 await vault.createVault("correct horse battery staple");1306 await vault.restoreEntries(1307 [1308 {1309 id: "e5",1310 type: "passkey",1311 title: "Legacy Passkey",1312 createdAt: 1,1313 modifiedAt: 2,1314 rpId: "example.com",1315 rpName: "Example",1316 credentialId: "cred-123",1317 publicKeyCose: "cose-blob",1318 userHandle: "uh",1319 userName: "june",1320 userDisplayName: "June",1321 algorithm: -7,1322 } as unknown as import("../shared/types").VaultEntry,1323 ],1324 "replace",1325 );1326 const restored = vault.getEntries().find((e) => e.id === "e5")!;1327 expect(restored).toBeDefined();1328 expect(restored.passkey?.credentialId).toBe("cred-123");1329 1330 expect((restored as unknown as Record<string, unknown>).credentialId).toBeUndefined();1331 });1332 });13331334 describe("MV-8 rotateSyncIdentity", () => {1335 it("re-keys the root, clears paired devices, and keeps entries unlockable with the same password", async () => {1336 const vault = await import("./vault");1337 await vault.createVault("correct horse battery staple");1338 const e = await vault.addEntry({ type: "login", title: "Bank", password: "pw" });1339 const { upsertPairedDevice, listPairedDevices } = await import(1340 "./paired-devices"1341 );1342 await upsertPairedDevice({1343 deviceId: "d1",1344 deviceName: "Phone",1345 deviceType: "android",1346 longTermPublicKeyHex: "a".repeat(64),1347 signingPublicKeyHex: "b".repeat(64),1348 pairedAt: 1,1349 trustSource: "direct",1350 });13511352 const rootBefore = vault.getRootKeyHex();1353 const res = await vault.rotateSyncIdentity();1354 expect(res.ok).toBe(true);1355 expect(vault.getRootKeyHex()).not.toBe(rootBefore);1356 expect(await listPairedDevices()).toHaveLength(0);1357 1358 expect(vault.getEntries().find((x) => x.id === e.id)?.title).toBe("Bank");13591360 vault.lock();1361 expect(await vault.unlock("correct horse battery staple")).toBe(true);1362 expect(vault.getEntries().find((x) => x.id === e.id)?.title).toBe("Bank");1363 });13641365 it("invalidates the old recovery words and flags recovery-pending", async () => {1366 const vault = await import("./vault");1367 await vault.createVault("correct horse battery staple");1368 const wordsBefore = (await vault.getRecoveryWords()).join(" ");13691370 await vault.rotateSyncIdentity();13711372 const wordsAfter = (await vault.getRecoveryWords()).join(" ");1373 expect(wordsAfter).not.toBe(wordsBefore);1374 const { isRecoveryConfirmPending } = await import("./recovery-pending");1375 expect(await isRecoveryConfirmPending()).toBe(true);1376 });13771378 it("changes the derived sync vault_id so an old device fails the vault-id gate", async () => {1379 const vault = await import("./vault");1380 await vault.createVault("correct horse battery staple");1381 const { createChirpSyncVaultAdapter } = await import(1382 "./chirp-sync-vault-adapter"1383 );1384 const adapter = createChirpSyncVaultAdapter();1385 const vidBefore = await adapter.computeVaultId();13861387 await vault.rotateSyncIdentity();13881389 const vidAfter = await adapter.computeVaultId();1390 expect(vidAfter).not.toBe(vidBefore);1391 });1392 });13931394 describe("LV-1 NFC master password normalization", () => {1395 const CAFE_BASE = "café";1396 const PW_NFD = `${CAFE_BASE.normalize("NFD")}-pw-12345`;1397 const PW_NFC = `${CAFE_BASE.normalize("NFC")}-pw-12345`;13981399 it("a vault created with the NFD form unlocks with the NFC form", async () => {1400 const vault = await import("./vault");1401 expect(PW_NFD).not.toBe(PW_NFC);1402 await vault.createVault(PW_NFD);1403 vault.lock();1404 expect(await vault.unlock(PW_NFC)).toBe(true);1405 });14061407 it("a legacy vault sealed with raw non-NFC bytes still unlocks and is healed to NFC", async () => {1408 const vault = await import("./vault");1409 const { deriveKeyRaw, generateSalt } = await import("../crypto/argon2");1410 await vault.createVault("base-password-12345");14111412 1413 1414 const rawSalt = generateSalt();1415 const rawKey = await deriveKeyRaw(PW_NFD, rawSalt);1416 await vault.changePasswordWithNewKey(rawSalt, rawKey);1417 vault.lock();14181419 1420 expect(await vault.unlock(PW_NFD)).toBe(true);14211422 1423 1424 vault.lock();1425 expect(await vault.unlock(PW_NFC)).toBe(true);1426 });1427 });14281429 describe("LV-7 updateSettings proto-pollution filter", () => {1430 it("strips __proto__/constructor/prototype keys while persisting real settings", async () => {1431 const vault = await import("./vault");1432 await vault.createVault("correct horse battery staple");14331434 await vault.updateSettings({1435 theme: "dark",1436 ["__proto__"]: { polluted: true },1437 constructor: { polluted: true },1438 } as never);14391440 expect(({} as Record<string, unknown>).polluted).toBeUndefined();1441 expect((Object.prototype as Record<string, unknown>).polluted).toBeUndefined();1442 expect(vault.getSettings().theme).toBe("dark");1443 });1444 });1445});