import { createHash } from "node:crypto"; import { fork } from "node:child_process"; import { chmodSync, existsSync, linkSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, renameSync, rmSync, symlinkSync, unlinkSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, test, vi } from "vitest"; const fsHooks = vi.hoisted(() => ({ afterRead: undefined as undefined | (() => void), afterWrite: undefined as undefined | (() => void), beforeLstat: undefined as undefined | ((path: string) => boolean), afterLstat: undefined as undefined | ((path: string) => boolean), transformLstat: undefined as undefined | ((path: string, info: import("node:fs").Stats) => import("node:fs").Stats), })); vi.mock("node:fs", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, readSync: (...args: Parameters) => { const result = actual.readSync(...args); const callback = fsHooks.afterRead; fsHooks.afterRead = undefined; callback?.(); return result; }, writeSync: (...args: Parameters) => { const result = actual.writeSync(...args); const callback = fsHooks.afterWrite; fsHooks.afterWrite = undefined; callback?.(); return result; }, lstatSync: (...args: Parameters) => { const before = fsHooks.beforeLstat; if (before?.(String(args[0]))) fsHooks.beforeLstat = undefined; const original = actual.lstatSync(...args); const result = fsHooks.transformLstat?.(String(args[0]), original) ?? original; const callback = fsHooks.afterLstat; if (callback?.(String(args[0]))) fsHooks.afterLstat = undefined; return result; }, }; }); import { createFileAuthSessionStore, deriveCsrfToken, type AuthSessionStore, type SessionCreateInput, } from "../src/auth/session-store.js"; import { createWindowsAuthStorageBridge } from "../src/auth/windows-auth-storage.js"; const roots: string[] = []; const base = new Date("2030-01-02T03:04:05.000Z"); const revision = "a".repeat(64); const validLocalUser = { enabled: true, authRevision: 7, roles: ["admin"] as const }; afterEach(() => { fsHooks.afterRead = undefined; fsHooks.afterWrite = undefined; fsHooks.beforeLstat = undefined; fsHooks.afterLstat = undefined; fsHooks.transformLstat = undefined; for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); }); function root(): string { const path = mkdtempSync(join(realpathSync(tmpdir()), "thothii-auth-session-")); chmodSync(path, 0o700); roots.push(path); return path; } function digestPath(rootPath: string, directory: "sessions" | "oidc", rawValue: string): string { return join(rootPath, directory, `${createHash("sha256").update(rawValue).digest("hex")}.json`); } function claimPath(rootPath: string, rawState: string): string { return join(rootPath, "oidc", `${createHash("sha256").update(rawState).digest("hex")}.claim`); } function validStore(storageRoot: string): AuthSessionStore { return createFileAuthSessionStore(storageRoot, { currentAuthConfigRevision: () => revision, findLocalUser: async () => validLocalUser, }); } async function create( store: AuthSessionStore, overrides: Partial = {}, now = base, ) { return store.create({ principal: { issuer: "local", subject: "6ba7b810-9dad-4ed1-80b4-00c04fd430c8", displayName: "Local administrator", roles: ["admin"], permissions: ["session.use", "settings.manage"], isAdmin: true, }, method: "local", remembered: true, userAuthRevision: 7, authConfigRevision: revision, idleTtlMs: 10 * 60_000, absoluteTtlMs: 30 * 60_000, ...overrides, }, now); } async function expectStoreInvalid(operation: Promise): Promise { await expect(operation).rejects.toThrow("auth_session_store_invalid"); } async function isolatedOidcConsumer(storageRoot: string, state: string): Promise<{ start(): void; result: Promise; }> { const child = fork(new URL("./fixtures/oidc-state-consumer.mts", import.meta.url), [], { cwd: process.cwd(), execArgv: ["--import", "tsx"], env: { ...process.env, THT_TEST_SESSION_ROOT: storageRoot, THT_TEST_OIDC_STATE: state, }, silent: true, }); const ready = new Promise((resolve, reject) => { child.once("message", (message) => { if (message === "ready") resolve(); else reject(new Error("OIDC consumer did not become ready")); }); child.once("error", reject); child.once("exit", (code) => { if (code !== null && code !== 0) reject(new Error("OIDC consumer exited before ready")); }); }); const result = new Promise((resolve, reject) => { child.on("message", (message) => { if (message && typeof message === "object" && "consumed" in message) { const outcome = message as { consumed: unknown; failed?: unknown }; if (outcome.failed === true) reject(new Error("OIDC consumer failed")); else resolve(outcome.consumed === true); } }); child.on("error", reject); child.on("exit", (code) => { if (code !== 0) reject(new Error("OIDC consumer exited without a result")); }); }); await ready; return { start: () => child.send("consume"), result }; } describe("file-backed auth session store", () => { test("fails closed and revokes a session when constructed without validity dependencies", async () => { const storageRoot = root(); const store = createFileAuthSessionStore(storageRoot); const created = await create(store); await expect(store.resolve(created.token)).resolves.toBeUndefined(); expect(existsSync(digestPath(storageRoot, "sessions", created.token))).toBe(false); }); test("revokes a session when a validity dependency throws", async () => { const storageRoot = root(); const store = createFileAuthSessionStore(storageRoot, { currentAuthConfigRevision: () => { throw new Error("dependency unavailable"); }, findLocalUser: async () => validLocalUser, }); const created = await create(store); await expectStoreInvalid(store.resolve(created.token)); expect(existsSync(digestPath(storageRoot, "sessions", created.token))).toBe(false); }); test("creates 256-bit opaque tokens, digest-only files, and derived CSRF values", async () => { const storageRoot = root(); const store = validStore(storageRoot); const first = await create(store); const second = await create(store); const path = digestPath(storageRoot, "sessions", first.token); const contents = readFileSync(path, "utf8"); expect(first.token).toMatch(/^[A-Za-z0-9_-]{43}$/); expect(Buffer.from(first.token, "base64url")).toHaveLength(32); expect(second.token).not.toBe(first.token); expect(lstatSync(path).isFile()).toBe(true); expect(path).toMatch(/[a-f0-9]{64}\.json$/); expect(contents).not.toContain(first.token); expect(contents).not.toContain(first.csrfToken); expect(deriveCsrfToken(first.token)).toBe(first.csrfToken); expect(first.csrfToken).toMatch(/^[A-Za-z0-9_-]{43}$/); expect(first.csrfToken).not.toBe(first.token); if (process.platform !== "win32") { expect(lstatSync(storageRoot).mode & 0o7777).toBe(0o700); expect(lstatSync(join(storageRoot, "sessions")).mode & 0o7777).toBe(0o700); expect(lstatSync(join(storageRoot, "oidc")).mode & 0o7777).toBe(0o700); expect(lstatSync(path).mode & 0o7777).toBe(0o600); } }); test("survives a backend restart and respects idle and absolute expiry", async () => { const storageRoot = root(); const firstStore = validStore(storageRoot); const created = await create(firstStore); const restartedStore = validStore(storageRoot); await expect(restartedStore.resolve(created.token, new Date(base.getTime() + 9 * 60_000))) .resolves.toMatchObject({ subject: created.record.subject, remembered: true }); await expect(restartedStore.resolve(created.token, new Date(base.getTime() + 10 * 60_000))) .resolves.toBeUndefined(); expect(existsSync(digestPath(storageRoot, "sessions", created.token))).toBe(false); const absolute = await create(restartedStore, { idleTtlMs: 30 * 60_000, absoluteTtlMs: 10 * 60_000 }); await expect(restartedStore.resolve(absolute.token, new Date(base.getTime() + 10 * 60_000))) .resolves.toBeUndefined(); }); test("touches at most once per five minutes and never extends absolute expiry", async () => { const storageRoot = root(); const store = validStore(storageRoot); const created = await create(store); const path = digestPath(storageRoot, "sessions", created.token); const before = readFileSync(path, "utf8"); await store.touch(created.token, new Date(base.getTime() + 4 * 60_000)); expect(readFileSync(path, "utf8")).toBe(before); await store.touch(created.token, new Date(base.getTime() + 5 * 60_000)); await expect(store.resolve(created.token, new Date(base.getTime() + 5 * 60_000))).resolves.toMatchObject({ lastSeenAt: "2030-01-02T03:09:05.000Z", idleExpiresAt: "2030-01-02T03:19:05.000Z", absoluteExpiresAt: "2030-01-02T03:34:05.000Z", }); }); test("revokes sessions and prunes expired session and OIDC-state records", async () => { const storageRoot = root(); const store = validStore(storageRoot); const revoked = await create(store); const expired = await create(store, { idleTtlMs: 60_000, absoluteTtlMs: 60_000 }); const oidc = await store.createOidcState({ nonce: "n".repeat(43), codeVerifier: "v".repeat(43), returnTo: "/" }, base); await store.revoke(revoked.token); await expect(store.resolve(revoked.token)).resolves.toBeUndefined(); expect(existsSync(digestPath(storageRoot, "sessions", revoked.token))).toBe(false); await expect(store.prune(new Date(base.getTime() + 11 * 60_000))).resolves.toBe(2); expect(existsSync(digestPath(storageRoot, "sessions", expired.token))).toBe(false); expect(existsSync(digestPath(storageRoot, "oidc", oidc.state))).toBe(false); }); test("creates bounded OIDC state records that expire and are single-use", async () => { const storageRoot = root(); const store = validStore(storageRoot); const created = await store.createOidcState({ nonce: "n".repeat(43), codeVerifier: "v".repeat(43), returnTo: "/", }, base); const path = digestPath(storageRoot, "oidc", created.state); expect(created.state).toMatch(/^[A-Za-z0-9_-]{43}$/); expect(readFileSync(path, "utf8")).not.toContain(created.state); await expect(store.consumeOidcState(created.state, new Date(base.getTime() + 9 * 60_000))) .resolves.toMatchObject({ nonce: "n".repeat(43), codeVerifier: "v".repeat(43), returnTo: "/" }); await expect(store.consumeOidcState(created.state)).resolves.toBeUndefined(); const expired = await store.createOidcState({ nonce: "x".repeat(43), codeVerifier: "y".repeat(43), returnTo: "/" }, base); await expect(store.consumeOidcState(expired.state, new Date(base.getTime() + 10 * 60_000))) .resolves.toBeUndefined(); }); test("fails closed when an OIDC state already has an atomic filesystem claim", async () => { const storageRoot = root(); const store = validStore(storageRoot); const created = await store.createOidcState({ nonce: "n".repeat(43), codeVerifier: "v".repeat(43), returnTo: "/" }, base); const statePath = digestPath(storageRoot, "oidc", created.state); linkSync(statePath, claimPath(storageRoot, created.state)); await expect(store.consumeOidcState(created.state)).resolves.toBeUndefined(); expect(existsSync(statePath)).toBe(true); }); test("treats a competing OIDC claim installed between availability and state checks as unavailable", async () => { const storageRoot = root(); const store = validStore(storageRoot); const created = await store.createOidcState({ nonce: "n".repeat(43), codeVerifier: "v".repeat(43), returnTo: "/" }, base); const statePath = digestPath(storageRoot, "oidc", created.state); const stateClaimPath = claimPath(storageRoot, created.state); fsHooks.beforeLstat = (observed) => { if (observed !== statePath) return false; linkSync(statePath, stateClaimPath); return true; }; await expect(store.consumeOidcState(created.state)).resolves.toBeUndefined(); expect(existsSync(statePath)).toBe(true); expect(existsSync(stateClaimPath)).toBe(true); }); test("prunes an expired OIDC state abandoned after an atomic claim", async () => { const storageRoot = root(); const store = validStore(storageRoot); const created = await store.createOidcState({ nonce: "n".repeat(43), codeVerifier: "v".repeat(43), returnTo: "/" }, base); const statePath = digestPath(storageRoot, "oidc", created.state); const stateClaimPath = claimPath(storageRoot, created.state); linkSync(statePath, stateClaimPath); await expect(store.prune(new Date(base.getTime() + 10 * 60_000))).resolves.toBe(1); expect(existsSync(statePath)).toBe(false); expect(existsSync(stateClaimPath)).toBe(false); }); test("retains an in-flight orphan claim but removes it after the bounded recovery window", async () => { const storageRoot = root(); const store = validStore(storageRoot); const created = await store.createOidcState({ nonce: "n".repeat(43), codeVerifier: "v".repeat(43), returnTo: "/" }, base); const statePath = digestPath(storageRoot, "oidc", created.state); const stateClaimPath = claimPath(storageRoot, created.state); linkSync(statePath, stateClaimPath); unlinkSync(statePath); await expect(store.consumeOidcState(created.state)).resolves.toBeUndefined(); expect(existsSync(stateClaimPath)).toBe(true); await expect(store.prune(new Date("2031-01-02T03:04:05.000Z"))).resolves.toBe(1); expect(existsSync(stateClaimPath)).toBe(false); }); test("allows exactly one separate Node isolate to consume an OIDC state", async () => { const storageRoot = root(); const store = validStore(storageRoot); const created = await store.createOidcState({ nonce: "n".repeat(43), codeVerifier: "v".repeat(43), returnTo: "/" }, base); const [first, second] = await Promise.all([ isolatedOidcConsumer(storageRoot, created.state), isolatedOidcConsumer(storageRoot, created.state), ]); first.start(); second.start(); const outcomes = await Promise.all([first.result, second.result]); expect(outcomes.filter(Boolean)).toHaveLength(1); expect(existsSync(digestPath(storageRoot, "oidc", created.state))).toBe(false); }); test.skipIf(process.platform === "win32")("refuses symlinked and hard-linked session records", async () => { const storageRoot = root(); const store = validStore(storageRoot); const symlinked = await create(store); const symlinkPath = digestPath(storageRoot, "sessions", symlinked.token); const target = `${symlinkPath}.target`; renameSync(symlinkPath, target); symlinkSync(target, symlinkPath); await expectStoreInvalid(store.resolve(symlinked.token)); const hardLinked = await create(store); const hardLinkPath = digestPath(storageRoot, "sessions", hardLinked.token); linkSync(hardLinkPath, `${hardLinkPath}.link`); await expectStoreInvalid(store.resolve(hardLinked.token)); }); test("refuses malformed and oversized session records without disclosing their contents", async () => { const storageRoot = root(); const store = validStore(storageRoot); const malformed = await create(store); const malformedPath = digestPath(storageRoot, "sessions", malformed.token); writeFileSync(malformedPath, "{}", { encoding: "utf8", mode: 0o600 }); chmodSync(malformedPath, 0o600); await expectStoreInvalid(store.resolve(malformed.token)); const oversized = await create(store); const oversizedPath = digestPath(storageRoot, "sessions", oversized.token); writeFileSync(oversizedPath, "#".repeat(20_000), { encoding: "utf8", mode: 0o600 }); chmodSync(oversizedPath, 0o600); await expectStoreInvalid(store.resolve(oversized.token)); }); test.skipIf(process.platform === "win32")("refuses unsafe existing roots and storage subdirectories", async () => { const unsafeRoot = root(); chmodSync(unsafeRoot, 0o755); await expectStoreInvalid(create(validStore(unsafeRoot))); const storageRoot = root(); const store = validStore(storageRoot); const created = await create(store); chmodSync(join(storageRoot, "sessions"), 0o755); await expectStoreInvalid(store.resolve(created.token)); const outer = root(); const realRoot = join(outer, "real-auth"); mkdirSync(realRoot, { mode: 0o700 }); chmodSync(realRoot, 0o700); const linkedRoot = join(outer, "linked-auth"); symlinkSync(realRoot, linkedRoot); await expectStoreInvalid(create(validStore(linkedRoot))); }); test.skipIf(process.platform === "win32")("refuses storage owned by a different identity", async () => { const storageRoot = root(); const store = validStore(storageRoot); const created = await create(store); const sessions = join(storageRoot, "sessions"); fsHooks.transformLstat = (observed, info) => { if (observed !== sessions) return info; const foreign = Object.create(info) as import("node:fs").Stats; Object.defineProperty(foreign, "uid", { value: info.uid + 1 }); return foreign; }; await expectStoreInvalid(store.resolve(created.token)); }); test.skipIf(process.platform === "win32")("refuses directory replacement during a session read", async () => { const storageRoot = root(); const store = validStore(storageRoot); const created = await create(store); const sessions = join(storageRoot, "sessions"); const replacement = join(storageRoot, "sessions-replacement"); fsHooks.afterRead = () => { renameSync(sessions, replacement); symlinkSync(replacement, sessions); }; await expectStoreInvalid(store.resolve(created.token)); }); test("refuses file replacement during a touch", async () => { const storageRoot = root(); const store = validStore(storageRoot); const created = await create(store); const path = digestPath(storageRoot, "sessions", created.token); const replacement = `${path}.replacement`; fsHooks.afterWrite = () => { writeFileSync(replacement, "{}", { encoding: "utf8", mode: 0o600 }); chmodSync(replacement, 0o600); renameSync(replacement, path); }; await expectStoreInvalid(store.touch(created.token, new Date(base.getTime() + 5 * 60_000))); }); test.skipIf(process.platform === "win32")("refuses directory replacement during a touch", async () => { const storageRoot = root(); const store = validStore(storageRoot); const created = await create(store); const path = digestPath(storageRoot, "sessions", created.token); const sessions = join(storageRoot, "sessions"); const replacement = join(storageRoot, "sessions-replacement"); fsHooks.afterLstat = (observed) => { if (observed !== path) return false; renameSync(sessions, replacement); symlinkSync(replacement, sessions); return true; }; await expectStoreInvalid(store.touch(created.token, new Date(base.getTime() + 5 * 60_000))); }); test("refuses file replacement during revoke", async () => { const storageRoot = root(); const store = validStore(storageRoot); const created = await create(store); const path = digestPath(storageRoot, "sessions", created.token); const replacement = `${path}.replacement`; writeFileSync(replacement, "{}", { encoding: "utf8", mode: 0o600 }); chmodSync(replacement, 0o600); fsHooks.afterLstat = (observed) => { if (observed !== path) return false; renameSync(replacement, path); return true; }; await expectStoreInvalid(store.revoke(created.token)); expect(readFileSync(path, "utf8")).toBe("{}"); }); test.skipIf(process.platform === "win32")("refuses directory replacement during revoke", async () => { const storageRoot = root(); const store = validStore(storageRoot); const created = await create(store); const path = digestPath(storageRoot, "sessions", created.token); const sessions = join(storageRoot, "sessions"); const replacement = join(storageRoot, "sessions-replacement"); fsHooks.afterLstat = (observed) => { if (observed !== path) return false; renameSync(sessions, replacement); symlinkSync(replacement, sessions); return true; }; await expectStoreInvalid(store.revoke(created.token)); }); test("routes native Windows session creation through an injected storage bridge", async () => { const storageRoot = root(); const store = validStore(join(storageRoot, "windows-auth-state")); const createRecord = vi.fn(async () => true); const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform"); if (!originalPlatform) throw new Error("platform descriptor unavailable"); Object.defineProperty(process, "platform", { configurable: true, value: "win32" }); try { const created = await create( createFileAuthSessionStore(join(storageRoot, "windows-auth-state"), { currentAuthConfigRevision: () => revision, findLocalUser: async () => validLocalUser, }, { windowsStorageBridge: { create: createRecord } } as never), ); expect(created.token).toMatch(/^[A-Za-z0-9_-]{43}$/); expect(createRecord).toHaveBeenCalledTimes(1); expect(existsSync(join(storageRoot, "windows-auth-state"))).toBe(false); } finally { Object.defineProperty(process, "platform", originalPlatform); } }); test("routes native Windows lifecycle and OIDC operations only through the storage bridge", async () => { const records = new Map(); const calls: string[] = []; const key = (directory: string, filename: string) => `${directory}/${filename}`; const bridge = { create: async (_root: string, directory: string, filename: string, contents: Buffer) => { calls.push("create"); const entry = key(directory, filename); if (records.has(entry)) return false; records.set(entry, Buffer.from(contents)); return true; }, read: async (_root: string, directory: string, filename: string) => { calls.push("read"); const value = records.get(key(directory, filename)); return value === undefined ? undefined : Buffer.from(value); }, replace: async (_root: string, directory: string, filename: string, contents: Buffer) => { calls.push("replace"); records.set(key(directory, filename), Buffer.from(contents)); }, remove: async (_root: string, directory: string, filename: string) => { calls.push("remove"); return records.delete(key(directory, filename)); }, list: async (_root: string, directory: string) => { calls.push("list"); return [...records.keys()] .filter((entry) => entry.startsWith(`${directory}/`)) .map((entry) => ({ name: entry.slice(directory.length + 1), modifiedUnixMs: base.getTime() })); }, claimConsume: async (_root: string, filename: string) => { calls.push("claim-consume"); const entry = key("oidc", filename); const value = records.get(entry); records.delete(entry); return value === undefined ? undefined : Buffer.from(value); }, readClaim: async () => undefined, removeClaim: async () => false, }; const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform"); if (!originalPlatform) throw new Error("platform descriptor unavailable"); Object.defineProperty(process, "platform", { configurable: true, value: "win32" }); try { const store = createFileAuthSessionStore("C:\\ProgramData\\ThothII\\auth", { currentAuthConfigRevision: () => revision, findLocalUser: async () => validLocalUser, }, { windowsStorageBridge: bridge } as never); const session = await create(store); await expect(store.resolve(session.token)).resolves.toMatchObject({ subject: session.record.subject }); await store.touch(session.token, new Date(base.getTime() + 5 * 60_000)); await store.revoke(session.token); await expect(store.resolve(session.token)).resolves.toBeUndefined(); const oidc = await store.createOidcState({ nonce: "n".repeat(43), codeVerifier: "v".repeat(43), returnTo: "/" }, base); await expect(store.consumeOidcState(oidc.state, new Date(base.getTime() + 9 * 60_000))) .resolves.toMatchObject({ nonce: "n".repeat(43) }); await expect(store.consumeOidcState(oidc.state)).resolves.toBeUndefined(); await create(store, { idleTtlMs: 60_000, absoluteTtlMs: 60_000 }); await store.createOidcState({ nonce: "x".repeat(43), codeVerifier: "y".repeat(43), returnTo: "/" }, base); await expect(store.prune(new Date(base.getTime() + 11 * 60_000))).resolves.toBe(2); expect(calls).toEqual(expect.arrayContaining(["create", "read", "replace", "remove", "claim-consume", "list"])); expect(records).toHaveLength(0); } finally { Object.defineProperty(process, "platform", originalPlatform); } }); test("prunes nonempty Windows directories through the Go lower-camel list DTO", async () => { const records = new Map(); const key = (directory: string, entry: string) => `${directory}/${entry}`; const response = (value: Record) => ({ code: 0, stdout: Buffer.from(`${JSON.stringify({ version: 1, ok: true, ...value })}\n`), stderr: Buffer.alloc(0), }); const bridge = createWindowsAuthStorageBridge({ thtExecutable: "C:\\tht.exe", invoke: async ({ input }) => { const request = JSON.parse(input.toString("utf8")) as { operation: string; directory: "sessions" | "oidc"; filename?: string; contentBase64?: string; }; const entry = request.filename === undefined ? undefined : key(request.directory, request.filename); switch (request.operation) { case "create": if (entry === undefined || request.contentBase64 === undefined || records.has(entry)) return response({ created: false }); records.set(entry, Buffer.from(request.contentBase64, "base64")); return response({ created: true }); case "read": return entry === undefined || !records.has(entry) ? response({}) : response({ found: true, contentBase64: records.get(entry)?.toString("base64") }); case "remove": return response({ removed: entry !== undefined && records.delete(entry) }); case "list": return response({ // Raw lower-camel entry objects, exactly as authstorage's Go response emits them. entries: [...records.keys()] .filter((value) => value.startsWith(`${request.directory}/`)) .map((value) => ({ name: value.slice(request.directory.length + 1), modifiedUnixMs: base.getTime() })), }); default: throw new Error("unexpected bridge operation"); } }, }); const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform"); if (!originalPlatform) throw new Error("platform descriptor unavailable"); Object.defineProperty(process, "platform", { configurable: true, value: "win32" }); try { const store = createFileAuthSessionStore("C:\\ProgramData\\ThothII\\auth", { currentAuthConfigRevision: () => revision, findLocalUser: async () => validLocalUser, }, { windowsStorageBridge: bridge }); await create(store, { idleTtlMs: 60_000, absoluteTtlMs: 60_000 }); await store.createOidcState({ nonce: "n".repeat(43), codeVerifier: "v".repeat(43), returnTo: "/" }, base); await expect(store.prune(new Date(base.getTime() + 11 * 60_000))).resolves.toBe(2); expect(records).toHaveLength(0); } finally { Object.defineProperty(process, "platform", originalPlatform); } }); test("prunes empty Windows directories through the required Go entries array", async () => { const bridge = createWindowsAuthStorageBridge({ thtExecutable: "C:\\tht.exe", invoke: async () => ({ code: 0, stdout: Buffer.from('{"version":1,"ok":true,"entries":[]}\n'), stderr: Buffer.alloc(0), }), }); const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform"); if (!originalPlatform) throw new Error("platform descriptor unavailable"); Object.defineProperty(process, "platform", { configurable: true, value: "win32" }); try { const store = createFileAuthSessionStore("C:\\ProgramData\\ThothII\\auth", { currentAuthConfigRevision: () => revision, findLocalUser: async () => validLocalUser, }, { windowsStorageBridge: bridge }); await expect(store.prune(base)).resolves.toBe(0); } finally { Object.defineProperty(process, "platform", originalPlatform); } }); test("prunes a stale Windows OIDC orphan claim through the Go DTO path", async () => { const claim = `${"d".repeat(64)}.claim`; let removed = false; const bridge = createWindowsAuthStorageBridge({ thtExecutable: "C:\\tht.exe", invoke: async ({ input }) => { const request = JSON.parse(input.toString("utf8")) as { operation: string; directory: string; filename?: string }; const response = (value: Record) => ({ code: 0, stdout: Buffer.from(`${JSON.stringify({ version: 1, ok: true, ...value })}\n`), stderr: Buffer.alloc(0), }); if (request.operation === "list") { return response({ entries: request.directory === "oidc" ? [{ name: claim, modifiedUnixMs: base.getTime() }] : [], }); } if (request.operation === "remove" && request.directory === "oidc" && request.filename === claim) { removed = true; return response({ removed: true }); } throw new Error("unexpected bridge request"); }, }); const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform"); if (!originalPlatform) throw new Error("platform descriptor unavailable"); Object.defineProperty(process, "platform", { configurable: true, value: "win32" }); try { const store = createFileAuthSessionStore("C:\\ProgramData\\ThothII\\auth", { currentAuthConfigRevision: () => revision, findLocalUser: async () => validLocalUser, }, { windowsStorageBridge: bridge }); await expect(store.prune(new Date(base.getTime() + 11 * 60_000))).resolves.toBe(1); expect(removed).toBe(true); } finally { Object.defineProperty(process, "platform", originalPlatform); } }); test("revokes on config, local-user, revision, enabled, or role mismatch before returning", async () => { const storageRoot = root(); let currentRevision = revision; let localUser: { enabled: boolean; authRevision: number; roles: readonly ("user" | "admin")[] } | undefined = { enabled: true, authRevision: 7, roles: ["admin"], }; const store = createFileAuthSessionStore(storageRoot, { currentAuthConfigRevision: () => currentRevision, findLocalUser: async () => localUser, }); const configChanged = await create(store); currentRevision = "b".repeat(64); await expect(store.resolve(configChanged.token)).resolves.toBeUndefined(); expect(existsSync(digestPath(storageRoot, "sessions", configChanged.token))).toBe(false); currentRevision = revision; for (const mismatch of [ undefined, { enabled: false, authRevision: 7, roles: ["admin"] as const }, { enabled: true, authRevision: 8, roles: ["admin"] as const }, { enabled: true, authRevision: 7, roles: ["user"] as const }, ]) { localUser = mismatch; const session = await create(store); await expect(store.resolve(session.token)).resolves.toBeUndefined(); expect(existsSync(digestPath(storageRoot, "sessions", session.token))).toBe(false); } }); test("serializes concurrent resolve and revoke without resurrecting a record", async () => { const storageRoot = root(); const firstStore = validStore(storageRoot); const secondStore = validStore(storageRoot); const created = await create(firstStore); await Promise.all([ ...Array.from({ length: 8 }, () => firstStore.resolve(created.token)), ...Array.from({ length: 8 }, () => secondStore.revoke(created.token)), ]); await expect(firstStore.resolve(created.token)).resolves.toBeUndefined(); expect(existsSync(digestPath(storageRoot, "sessions", created.token))).toBe(false); }); });