import { beforeEach, describe, expect, it, vi } from "vitest"; type StorageShape = Record; function makeChromeMock(local: StorageShape, sync: StorageShape): typeof chrome { const area = (store: StorageShape) => ({ get: vi.fn(async (key?: string | string[]) => { if (!key) return store; if (typeof key === "string") return { [key]: store[key] }; return key.reduce>((out, item) => { out[item] = store[item]; return out; }, {}); }), set: vi.fn(async (next: Record) => { Object.assign(store, next); }), remove: vi.fn(async (keys: string | string[]) => { for (const key of Array.isArray(keys) ? keys : [keys]) delete store[key]; }), }); return { storage: { local: area(local), sync: area(sync) }, alarms: { clear: vi.fn(), create: vi.fn(), onAlarm: { addListener: vi.fn() } }, idle: { setDetectionInterval: vi.fn(), onStateChanged: { addListener: vi.fn() } }, runtime: { sendMessage: vi.fn(async () => undefined) }, } as unknown as typeof chrome; } describe("locked sync spool", () => { let local: StorageShape; beforeEach(() => { vi.resetModules(); local = {}; (globalThis as { chrome?: typeof chrome }).chrome = makeChromeMock(local, {}); vi.stubGlobal("fetch", vi.fn(async () => { throw new Error("network disabled"); })); }); it("retains only opaque envelopes and transport identity after a full lock", async () => { const vault = await import("./vault"); await vault.createVault("correct horse battery staple"); const entry = await vault.addEntry({ type: "login", title: "Top Secret", password: "swordfish" }); const { refreshLockedSyncSpool } = await import("./sync-vault-adapter"); await refreshLockedSyncSpool(); vault.lock(); const { createLockedSyncVaultAdapter, getLockedSyncIdentity } = await import("./locked-sync-spool"); const adapter = await createLockedSyncVaultAdapter(); const identity = await getLockedSyncIdentity(); expect(vault.isUnlocked()).toBe(false); expect(identity).toBeNull(); expect(JSON.stringify(local)).not.toContain("longTermPrivateKeyHex"); expect(JSON.stringify(local)).not.toContain("longTermPrivateKey"); expect(await adapter?.buildEntryManifest()).toEqual({ [entry.id]: entry.modifiedAt }); const envelopes = await adapter?.getEncryptedEntries([entry.id]); expect(envelopes?.[entry.id]).toMatch(/^[A-Za-z0-9+/]+=*$/); expect(JSON.stringify(local)).not.toContain("Top Secret"); expect(JSON.stringify(local)).not.toContain("swordfish"); }); it("stages bounded inbound ciphertext without promoting it to the outbound manifest", async () => { const vault = await import("./vault"); await vault.createVault("correct horse battery staple"); const entry = await vault.addEntry({ type: "login", title: "Cached", password: "secret" }); const { refreshLockedSyncSpool } = await import("./sync-vault-adapter"); await refreshLockedSyncSpool(); vault.lock(); const { createLockedSyncVaultAdapter } = await import("./locked-sync-spool"); const adapter = (await createLockedSyncVaultAdapter())!; await adapter.applyEntries({ remote: "AA==" }, { remote: entry.modifiedAt + 1 }); expect(await adapter.buildEntryManifest()).toEqual({ [entry.id]: entry.modifiedAt }); const stored = local.peach_locked_sync_spool_v1 as { inboxEntries: Record; entries: Record; }; expect(stored.inboxEntries.remote?.ciphertext).toBe("AA=="); expect(stored.entries.remote).toBeUndefined(); }); it("bounds the cumulative locked inbox across repeated sync sessions", async () => { const vault = await import("./vault"); await vault.createVault("correct horse battery staple"); const { refreshLockedSyncSpool } = await import("./sync-vault-adapter"); await refreshLockedSyncSpool(); vault.lock(); const { createLockedSyncVaultAdapter } = await import("./locked-sync-spool"); const adapter = (await createLockedSyncVaultAdapter())!; const stored = local.peach_locked_sync_spool_v1 as { inboxEntries: Record; inboxTombstones: Record; }; for (let index = 0; index < 100_000; index += 1) { stored.inboxEntries[`entry-${index}`] = { modifiedAt: 1, ciphertext: "AA==" }; stored.inboxTombstones[`tombstone-${index}`] = 1; } await expect(adapter.applyEntries({ overflow: "AA==" }, { overflow: 2 })) .rejects.toThrow("sync_entry_count_exceeded"); await expect(adapter.applyTombstones({ overflow: 2 })) .rejects.toThrow("sync_tombstone_count_exceeded"); }); it("fails closed on a legacy private-key-bearing record until unlock refreshes it", async () => { local.peach_locked_sync_spool_v1 = { version: 1, vaultId: "a".repeat(64), deviceId: "device", deviceName: "Browser", deviceType: "extension", localVersion: 1, longTermPublicKeyHex: "b".repeat(64), longTermPrivateKeyHex: "c".repeat(64), entries: {}, tombstones: {}, inboxEntries: { queued: { modifiedAt: 1, ciphertext: "AA==" } }, inboxTombstones: {}, }; const { createLockedSyncVaultAdapter, getLockedSyncIdentity } = await import("./locked-sync-spool"); expect(await getLockedSyncIdentity()).toBeNull(); expect(await createLockedSyncVaultAdapter()).toBeNull(); expect((local.peach_locked_sync_spool_v1 as { inboxEntries: object }).inboxEntries).toEqual({ queued: { modifiedAt: 1, ciphertext: "AA==" } }); }); it("drains staged envelopes through the live decrypt boundary after unlock", async () => { const vault = await import("./vault"); const syncAdapter = await import("./sync-vault-adapter"); vault.setLockedSyncHooks({ refresh: syncAdapter.refreshLockedSyncSpool, afterUnlock: syncAdapter.drainAndRefreshLockedSyncSpool, }); await vault.createVault("correct horse battery staple"); const entry = await vault.addEntry({ type: "login", title: "Local", password: "secret" }); vault.lock(); const { createLockedSyncVaultAdapter } = await import("./locked-sync-spool"); const adapter = (await createLockedSyncVaultAdapter())!; await adapter.applyEntries({ remote: "AA==" }, { remote: entry.modifiedAt + 1 }); expect(await vault.unlock("correct horse battery staple")).toBe(true); const stored = local.peach_locked_sync_spool_v1 as { inboxEntries: Record }; expect(stored.inboxEntries).toEqual({}); expect(vault.getEntryById(entry.id)?.title).toBe("Local"); }); });