import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterEach, describe, expect, test, vi } from "vitest"; import { SecureJsonStore } from "../src/web/secure-store.js"; const roots: string[] = []; afterEach(async () => { await Promise.all(roots.splice(0).map((root) => fs.rm(root, { recursive: true, force: true }))); }); describe("encrypted OAuth store", () => { test("persists authenticated ciphertext with owner-only modes and no plaintext", async () => { const root = await temporaryRoot(); const key = Buffer.alloc(32, 7); const sentinel = "refresh-token-private-sentinel"; const store = storeAt<{ token: string }>(root, "session", key); await store.set("did:plc:fixture", { token: sentinel }); const file = path.join(root, "session.enc.json"); const raw = await fs.readFile(file, "utf8"); expect(raw).not.toContain(sentinel); expect(raw).not.toContain("did:plc:fixture"); expect((await fs.stat(root)).mode & 0o777).toBe(0o700); expect((await fs.stat(file)).mode & 0o777).toBe(0o600); const restored = storeAt<{ token: string }>(root, "session", key); expect(await restored.get("did:plc:fixture")).toEqual({ token: sentinel }); const wrongKey = storeAt<{ token: string }>(root, "session", Buffer.alloc(32, 8)); await expect(wrongKey.initialize()).rejects.toThrow("authenticated or decoded"); }); test("rejects direct malformed encrypted-envelope fixtures", async () => { const root = await temporaryRoot(); await fs.writeFile(path.join(root, "malformed.enc.json"), "{not-json\n", { mode: 0o600 }); const malformed = storeAt<{ value: string }>(root, "malformed", Buffer.alloc(32, 17)); await expect(malformed.initialize()).rejects.toThrow("envelope is invalid"); await fs.writeFile(path.join(root, "unsupported.enc.json"), JSON.stringify({ version: 1, algorithm: "plaintext", iv: "", ciphertext: "", tag: "", }), { mode: 0o600 }); const unsupported = storeAt<{ value: string }>(root, "unsupported", Buffer.alloc(32, 17)); await expect(unsupported.initialize()).rejects.toThrow("version is unsupported"); }); test("rejects an oversized encrypted-envelope fixture before reading or decrypting it", async () => { const root = await temporaryRoot(); await fs.writeFile(path.join(root, "oversized.enc.json"), "x".repeat(6_000), { mode: 0o600 }); const oversized = new SecureJsonStore<{ value: string }>({ directory: root, name: "oversized", key: Buffer.alloc(32, 18), maxEntries: 2, maxSerializedBytes: 1_024, }); await expect(oversized.initialize()).rejects.toThrow("envelope exceeds its configured bound"); }); test("expires short-lived records and take is one-time", async () => { const root = await temporaryRoot(); let now = 1_000_000; const store = new SecureJsonStore<{ value: string }>({ directory: root, name: "state", key: Buffer.alloc(32, 9), maxEntries: 8, maxSerializedBytes: 8 * 1024, ttlMs: 1_000, now: () => now, }); await store.set("one", { value: "first" }); expect(await store.take("one")).toEqual({ value: "first" }); expect(await store.take("one")).toBeUndefined(); await store.set("two", { value: "second" }); now += 1_001; expect(await store.getWithExpiration("two")).toMatchObject({ expired: true, value: { value: "second" } }); expect(await store.get("two")).toBeUndefined(); }); test("rejects entry-count exhaustion without corrupting accepted state", async () => { const root = await temporaryRoot(); const store = new SecureJsonStore<{ value: string }>({ directory: root, name: "bounded-count", key: Buffer.alloc(32, 10), maxEntries: 2, maxSerializedBytes: 8 * 1024, }); await store.set("a", { value: "one" }); await store.set("b", { value: "two" }); await expect(store.set("c", { value: "three" })).rejects.toThrow("entry limit"); expect(await store.size()).toBe(2); expect(await store.get("a")).toEqual({ value: "one" }); expect(await store.get("c")).toBeUndefined(); }); test("rechecks authority after temporary write and before atomic rename", async () => { const root = await temporaryRoot(); const store = storeAt<{ value: string }>(root, "guarded-write", Buffer.alloc(32, 19)); await store.initialize(); let authoritative = true; const originalWriteFile = fs.writeFile.bind(fs); const writeFile = vi.spyOn(fs, "writeFile").mockImplementation(async (...args: Parameters) => { await originalWriteFile(...args); authoritative = false; }); try { await expect(store.set("late", { value: "must-not-commit" }, () => authoritative)) .rejects.toThrow("lost authority"); } finally { writeFile.mockRestore(); } expect(await store.get("late")).toBeUndefined(); const reopened = storeAt<{ value: string }>(root, "guarded-write", Buffer.alloc(32, 19)); expect(await reopened.get("late")).toBeUndefined(); }); test("has no fallible filesystem operation after the atomic rename commit point", async () => { const root = await temporaryRoot(); const key = Buffer.alloc(32, 15); const store = storeAt<{ value: string }>(root, "rename-commit", key); await store.initialize(); const chmod = vi.spyOn(fs, "chmod").mockRejectedValue(new Error("post-rename chmod must not run")); try { await store.set("committed", { value: "disk-and-memory-agree" }); expect(chmod).not.toHaveBeenCalled(); } finally { chmod.mockRestore(); } expect(await store.get("committed")).toEqual({ value: "disk-and-memory-agree" }); const reopened = storeAt<{ value: string }>(root, "rename-commit", key); expect(await reopened.get("committed")).toEqual({ value: "disk-and-memory-agree" }); expect((await fs.stat(path.join(root, "rename-commit.enc.json"))).mode & 0o777).toBe(0o600); }); test("atomically replaces prior browser-session entries", async () => { const root = await temporaryRoot(); const store = storeAt<{ value: string }>(root, "replace-all", Buffer.alloc(32, 13)); await store.set("old-a", { value: "a" }); await store.set("old-b", { value: "b" }); await store.replaceAll("new", { value: "current" }); expect(await store.size()).toBe(1); expect(await store.get("old-a")).toBeUndefined(); expect(await store.get("old-b")).toBeUndefined(); expect(await store.get("new")).toEqual({ value: "current" }); }); test("rejects serialized-byte exhaustion and rolls back the failed write", async () => { const root = await temporaryRoot(); const store = new SecureJsonStore<{ value: string }>({ directory: root, name: "bounded-bytes", key: Buffer.alloc(32, 11), maxEntries: 8, maxSerializedBytes: 1_024, }); await store.set("small", { value: "retained" }); await expect(store.set("large", { value: "x".repeat(2_000) })).rejects.toThrow("byte limit"); expect(await store.get("small")).toEqual({ value: "retained" }); expect(await store.get("large")).toBeUndefined(); }); }); function storeAt(root: string, name: string, key: Buffer): SecureJsonStore { return new SecureJsonStore({ directory: root, name, key, maxEntries: 16, maxSerializedBytes: 64 * 1024, }); } async function temporaryRoot(): Promise { const root = await fs.mkdtemp(path.join(os.tmpdir(), "thoughtstream-oauth-store-")); roots.push(root); return root; }