import { createHash } from "node:crypto"; import { fork } from "node:child_process"; import { chmodSync, existsSync, linkSync, lstatSync, mkdirSync, mkdtempSync, opendirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, utimesSync, unlinkSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; import { basename, dirname, 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), beforeReaddir: undefined as undefined | ((path: string) => void), 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; }, readdirSync: (...args: Parameters) => { fsHooks.beforeReaddir?.(String(args[0])); return actual.readdirSync(...args); }, }; }); import { createFileAuthSessionStore, deriveCsrfToken, type AuthSessionStore, type FileAuthSessionStoreOptions, type SessionCreateInput, } from "../src/auth/session-store.js"; import { createWindowsAuthStorageBridge, type WindowsAuthStorageBridge, type WindowsAuthStorageDirectory, } 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 }; function oidcInput(nonce: string, codeVerifier: string) { return { nonce, codeVerifier, returnTo: "/" as const, authConfigRevision: revision, issuer: "https://issuer.example.test", browserTransactionDigest: "b".repeat(64), browserTransactionTransport: "https" as const, }; } function sessionFilename(index: number): string { return `${index.toString(16).padStart(64, "0")}.json`; } afterEach(() => { fsHooks.afterRead = undefined; fsHooks.afterWrite = undefined; fsHooks.beforeLstat = undefined; fsHooks.afterLstat = undefined; fsHooks.beforeReaddir = 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); for (const child of ["sessions", "oidc"]) { mkdirSync(join(path, child), { mode: 0o700 }); chmodSync(join(path, child), 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`); } // This fixture adapter keeps session-store behavior tests self-contained. Production has no // Node filesystem fallback: it always uses the hidden Go bridge. The Go authstorage suite owns // descriptor-pinning/race assertions; this adapter only supplies ordinary fixture semantics. function testPosixStorageBridge( ensureOverride?: (rootPath: string) => Promise, ): WindowsAuthStorageBridge { let overrideUsed = false; const ensure = async (rootPath: string): Promise => { if (ensureOverride && !overrideUsed) { overrideUsed = true; await ensureOverride(rootPath); return; } if (!existsSync(rootPath)) { if (realpathSync(dirname(rootPath)) !== dirname(rootPath)) throw new Error("invalid"); mkdirSync(rootPath, { mode: 0o700 }); chmodSync(rootPath, 0o700); } const rootInfo = lstatSync(rootPath); if (!rootInfo.isDirectory() || rootInfo.isSymbolicLink() || (rootInfo.mode & 0o7777) !== 0o700) throw new Error("invalid"); for (const child of ["sessions", "oidc"]) { const path = join(rootPath, child); if (!existsSync(path)) { mkdirSync(path, { mode: 0o700 }); chmodSync(path, 0o700); } const info = lstatSync(path); if (!info.isDirectory() || info.isSymbolicLink() || (info.mode & 0o7777) !== 0o700) throw new Error("invalid"); } }; const directory = async (rootPath: string, name: WindowsAuthStorageDirectory): Promise => { await ensure(rootPath); return join(rootPath, name); }; const record = (path: string, links: readonly number[]): import("node:fs").Stats => { const info = lstatSync(path); if (!info.isFile() || info.isSymbolicLink() || !links.includes(info.nlink) || (info.mode & 0o7777) !== 0o600 || (typeof process.geteuid === "function" && info.uid !== process.geteuid())) { throw new Error("invalid"); } return info; }; const same = (left: import("node:fs").Stats, right: import("node:fs").Stats): boolean => left.dev === right.dev && left.ino === right.ino && left.nlink === right.nlink; const paths = async (rootPath: string, name: WindowsAuthStorageDirectory, filename: string): Promise => join(await directory(rootPath, name), filename); const listNames = async ( rootPath: string, name: WindowsAuthStorageDirectory, maximumEntries: number, ): Promise<{ name: string; modifiedUnixMs: number }[]> => { const handle = opendirSync(await directory(rootPath, name)); const entries: string[] = []; try { for (;;) { const entry = handle.readSync(); if (entry === null) break; entries.push(entry.name); if (entries.length > maximumEntries) throw new Error("invalid"); } } finally { handle.closeSync(); } return entries.sort().map((filename) => { const links = name === "oidc" ? [1, 2] : [1]; const info = record(join(rootPath, name, filename), links); return { name: filename, modifiedUnixMs: info.mtimeMs }; }); }; return { validateRoot: async (rootPath) => { await ensure(rootPath); }, ensureLayout: ensure, readAuthConfig: (path) => readFileSync(path), readLocalUsers: async (path) => readFileSync(path), create: async (rootPath, name, filename, contents) => { const path = await paths(rootPath, name, filename); try { writeFileSync(path, contents, { flag: "wx", mode: 0o600 }); chmodSync(path, 0o600); return true; } catch (error: any) { if (error?.code !== "EEXIST") throw error; record(path, [1]); return false; } }, read: async (rootPath, name, filename) => { const path = await paths(rootPath, name, filename); try { record(path, [1]); } catch (error: any) { if (error?.code === "ENOENT") return undefined; throw error; } return readFileSync(path); }, replace: async (rootPath, name, filename, contents) => { const path = await paths(rootPath, name, filename); record(path, [1]); const temporary = `${path}.test-replacement`; writeFileSync(temporary, contents, { flag: "wx", mode: 0o600 }); chmodSync(temporary, 0o600); renameSync(temporary, path); }, remove: async (rootPath, name, filename) => { const path = await paths(rootPath, name, filename); try { record(path, [1]); } catch (error: any) { if (error?.code === "ENOENT") return false; throw error; } unlinkSync(path); return true; }, list: async (rootPath, name, maximumEntries = 256) => await listNames(rootPath, name, maximumEntries), listPage: async (rootPath, name, afterName, maximumEntries) => { if (name !== "sessions") throw new Error("invalid"); const handle = opendirSync(await directory(rootPath, name)); const all: string[] = []; try { for (;;) { const entry = handle.readSync(); if (entry === null) break; all.push(entry.name); } } finally { handle.closeSync(); } all.sort(); for (const filename of all) record(join(rootPath, name, filename), [1]); const selected = all.filter((filename) => afterName === undefined || filename > afterName).slice(0, maximumEntries + 1); return { entries: selected.slice(0, maximumEntries).map((filename) => { const info = statSync(join(rootPath, name, filename)); return { name: filename, modifiedUnixMs: info.mtimeMs }; }), more: selected.length > maximumEntries, }; }, claimConsume: async (rootPath, filename) => { const source = await paths(rootPath, "oidc", filename); const claim = source.replace(/\.json$/, ".claim"); try { record(source, [1]); } catch (error: any) { if (error?.code === "ENOENT") return undefined; return undefined; } try { linkSync(source, claim); } catch (error: any) { if (error?.code === "EEXIST") return undefined; throw error; } const contents = readFileSync(source); unlinkSync(source); unlinkSync(claim); return contents; }, readClaim: async (rootPath, filename) => { const source = await paths(rootPath, "oidc", filename); const claim = source.replace(/\.json$/, ".claim"); try { const sourceInfo = record(source, [2]); const claimInfo = record(claim, [2]); if (!same(sourceInfo, claimInfo)) throw new Error("invalid"); return readFileSync(source); } catch (error: any) { if (error?.code === "ENOENT") return undefined; return undefined; } }, removeClaim: async (rootPath, filename) => { const source = await paths(rootPath, "oidc", filename); const claim = source.replace(/\.json$/, ".claim"); try { const sourceInfo = record(source, [2]); const claimInfo = record(claim, [2]); if (!same(sourceInfo, claimInfo)) throw new Error("invalid"); } catch (error: any) { if (error?.code === "ENOENT") return false; return false; } unlinkSync(source); unlinkSync(claim); return true; }, }; } function validStore(storageRoot: string, options: FileAuthSessionStoreOptions = {}): AuthSessionStore { const posixStorageBridge = options.posixStorageBridge ?? testPosixStorageBridge(); return createFileAuthSessionStore(storageRoot, { currentAuthConfigRevision: () => revision, findLocalUser: async () => validLocalUser, }, { ...options, posixStorageBridge }); } 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 }; } async function isolatedOidcCreator(storageRoot: string, attempts: number): Promise<{ start(): void; result: Promise; }> { const child = fork(new URL("./fixtures/oidc-state-creator.mts", import.meta.url), [], { cwd: process.cwd(), execArgv: ["--import", "tsx"], env: { ...process.env, THT_TEST_SESSION_ROOT: storageRoot, THT_TEST_OIDC_ATTEMPTS: String(attempts), THT_TEST_OIDC_NOW: base.toISOString(), THT_TEST_OIDC_CAPACITY: "8", }, silent: true, }); const ready = new Promise((resolve, reject) => { child.once("message", (message) => { if (message === "ready") resolve(); else reject(new Error("OIDC creator did not become ready")); }); child.once("error", reject); child.once("exit", (code) => { if (code !== null && code !== 0) reject(new Error("OIDC creator exited before ready")); }); }); const result = new Promise((resolve, reject) => { let outcome: { created: unknown; failed?: unknown; error?: unknown } | undefined; child.on("message", (message) => { if (message && typeof message === "object" && "created" in message) { outcome = message as { created: unknown; failed?: unknown; error?: unknown }; } }); child.on("error", reject); child.on("exit", (code) => { if (code !== 0 || outcome?.failed === true || typeof outcome?.created !== "number") { reject(new Error(`OIDC creator failed: ${String(outcome?.error)}`)); } else { resolve(outcome.created); } }); }); await ready; return { start: () => child.send("create"), result }; } describe("file-backed auth session store", () => { test.skipIf(process.platform === "win32")("delegates missing layout creation to the retained native bridge", async () => { const storageRoot = join(root(), "auth"); const ensureLayout = vi.fn(async (requestedRoot: string) => { expect(requestedRoot).toBe(storageRoot); mkdirSync(requestedRoot, { mode: 0o700 }); chmodSync(requestedRoot, 0o700); for (const child of ["sessions", "oidc"]) { mkdirSync(join(requestedRoot, child), { mode: 0o700 }); chmodSync(join(requestedRoot, child), 0o700); } }); const store = validStore(storageRoot, { posixStorageBridge: testPosixStorageBridge(ensureLayout) }); await expect(create(store)).resolves.toMatchObject({ record: { method: "local" } }); expect(ensureLayout).toHaveBeenCalledOnce(); chmodSync(join(storageRoot, "oidc"), 0o750); await expectStoreInvalid(store.createOidcState(oidcInput("n".repeat(16), "v".repeat(43)), base)); }); test.skipIf(process.platform === "win32")("does not perform a path-based fallback when bridge layout creation loses an ancestor race", async () => { const outer = root(); const outside = root(); const parent = join(outer, "parent"); const movedParent = join(outer, "parent-original"); mkdirSync(parent, { mode: 0o700 }); chmodSync(parent, 0o700); const storageRoot = join(parent, "auth"); const ensureLayout = vi.fn(async () => { renameSync(parent, movedParent); symlinkSync(outside, parent); throw new Error(`${storageRoot} rejected`); }); const store = validStore(storageRoot, { posixStorageBridge: testPosixStorageBridge(ensureLayout) }); await expectStoreInvalid(create(store)); expect(ensureLayout).toHaveBeenCalledOnce(); expect(existsSync(join(outside, "auth"))).toBe(false); expect(existsSync(join(movedParent, "auth"))).toBe(false); }); test.skipIf(process.platform === "win32")("never follows a symlinked ancestor while creating a missing session root", async () => { const outer = root(); const outside = root(); const linkedParent = join(outer, "linked-parent"); symlinkSync(outside, linkedParent); const storageRoot = join(linkedParent, "auth"); await expectStoreInvalid(create(validStore(storageRoot))); expect(existsSync(join(outside, "auth"))).toBe(false); }); test.skipIf(process.platform === "win32")("rejects an uncreatable missing root without side effects", async () => { const outer = root(); const lockedParent = join(outer, "locked-parent"); mkdirSync(lockedParent, { mode: 0o700 }); chmodSync(lockedParent, 0o500); const storageRoot = join(lockedParent, "auth"); try { await expectStoreInvalid(create(validStore(storageRoot))); expect(existsSync(storageRoot)).toBe(false); } finally { chmodSync(lockedParent, 0o700); } }); test("does not fall back to Node paths when the retained POSIX bridge rejects creation", async () => { const storageRoot = join(root(), "auth"); const bridge = { ...testPosixStorageBridge(), create: async () => { throw new Error("native create rejected"); }, } as WindowsAuthStorageBridge; await expectStoreInvalid(create(validStore(storageRoot, { posixStorageBridge: bridge }))); expect(existsSync(storageRoot)).toBe(false); }); test("fails closed and revokes a session when constructed without validity dependencies", async () => { const storageRoot = root(); const store = createFileAuthSessionStore(storageRoot, undefined, { posixStorageBridge: testPosixStorageBridge() }); 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, }, { posixStorageBridge: testPosixStorageBridge() }); 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(oidcInput("n".repeat(43), "v".repeat(43)), 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.skipIf(process.platform === "win32")("allows startup maintenance past 512 live records and keeps OIDC available", async () => { const storageRoot = root(); const store = validStore(storageRoot); const liveSeed = await create(store, { idleTtlMs: 30 * 60_000, absoluteTtlMs: 30 * 60_000 }); const liveContents = readFileSync(digestPath(storageRoot, "sessions", liveSeed.token)); unlinkSync(digestPath(storageRoot, "sessions", liveSeed.token)); const expiredSeed = await create(store, { idleTtlMs: 60_000, absoluteTtlMs: 60_000 }); const expiredContents = readFileSync(digestPath(storageRoot, "sessions", expiredSeed.token)); unlinkSync(digestPath(storageRoot, "sessions", expiredSeed.token)); for (let index = 0; index < 512; index += 1) { writeFileSync(join(storageRoot, "sessions", sessionFilename(index)), liveContents, { mode: 0o600 }); } const expiredName = sessionFilename(512); writeFileSync(join(storageRoot, "sessions", expiredName), expiredContents, { mode: 0o600 }); await expect(store.prune(new Date(base.getTime() + 2 * 60_000))).resolves.toBe(0); expect(existsSync(join(storageRoot, "sessions", expiredName))).toBe(true); expect(existsSync(join(storageRoot, "sessions", sessionFilename(0)))).toBe(true); await expect(store.createOidcState(oidcInput("n".repeat(43), "v".repeat(43)), base)) .resolves.toMatchObject({ record: { nonce: "n".repeat(43) } }); await expect(store.prune(new Date(base.getTime() + 2 * 60_000))).resolves.toBe(1); expect(existsSync(join(storageRoot, "sessions", expiredName))).toBe(false); expect(existsSync(join(storageRoot, "sessions", sessionFilename(0)))).toBe(true); }); test.skipIf(process.platform === "win32")("serializes concurrent continuation passes without stranding a later page", async () => { const storageRoot = root(); const store = validStore(storageRoot); const liveSeed = await create(store, { idleTtlMs: 30 * 60_000, absoluteTtlMs: 30 * 60_000 }); const liveContents = readFileSync(digestPath(storageRoot, "sessions", liveSeed.token)); unlinkSync(digestPath(storageRoot, "sessions", liveSeed.token)); const expiredSeed = await create(store, { idleTtlMs: 60_000, absoluteTtlMs: 60_000 }); const expiredContents = readFileSync(digestPath(storageRoot, "sessions", expiredSeed.token)); unlinkSync(digestPath(storageRoot, "sessions", expiredSeed.token)); for (let index = 0; index < 512; index += 1) { writeFileSync(join(storageRoot, "sessions", sessionFilename(index)), liveContents, { mode: 0o600 }); } const expiredName = sessionFilename(512); writeFileSync(join(storageRoot, "sessions", expiredName), expiredContents, { mode: 0o600 }); await expect(Promise.all([ store.prune(new Date(base.getTime() + 2 * 60_000)), store.prune(new Date(base.getTime() + 2 * 60_000)), ])).resolves.toEqual([0, 1]); expect(existsSync(join(storageRoot, "sessions", expiredName))).toBe(false); expect(existsSync(join(storageRoot, "sessions", sessionFilename(0)))).toBe(true); }); test.skipIf(process.platform === "win32")("fails closed on an unsafe ordinary record beyond the first continuation page", async () => { const storageRoot = root(); const store = validStore(storageRoot); const seed = await create(store, { idleTtlMs: 30 * 60_000, absoluteTtlMs: 30 * 60_000 }); const contents = readFileSync(digestPath(storageRoot, "sessions", seed.token)); unlinkSync(digestPath(storageRoot, "sessions", seed.token)); for (let index = 0; index < 512; index += 1) { writeFileSync(join(storageRoot, "sessions", sessionFilename(index)), contents, { mode: 0o600 }); } symlinkSync(join(storageRoot, "missing-target"), join(storageRoot, "sessions", sessionFilename(512))); await expect(store.prune(new Date(base.getTime() + 2 * 60_000))) .rejects.toThrow("auth_session_store_invalid"); }); test("fails closed when the retained POSIX bridge rejects a session continuation page", async () => { const storageRoot = root(); const bridge = { ...testPosixStorageBridge(), listPage: async () => { throw new Error("native page rejected"); }, } as WindowsAuthStorageBridge; const store = validStore(storageRoot, { posixStorageBridge: bridge }); await expect(store.prune(new Date(base.getTime() + 2 * 60_000))) .rejects.toThrow("auth_session_store_invalid"); }); test("persists the OIDC transaction transport as consumed state metadata", async () => { const storageRoot = root(); const store = validStore(storageRoot); const created = await store.createOidcState({ ...oidcInput("n".repeat(43), "v".repeat(43)), browserTransactionTransport: "loopback_http", }); await expect(store.consumeOidcState(created.state)).resolves.toMatchObject({ browserTransactionTransport: "loopback_http", }); }); 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(oidcInput("n".repeat(43), "v".repeat(43)), 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: "/", authConfigRevision: revision, issuer: "https://issuer.example.test", browserTransactionDigest: "b".repeat(64), browserTransactionTransport: "https", }); await expect(store.consumeOidcState(created.state)).resolves.toBeUndefined(); const expired = await store.createOidcState(oidcInput("x".repeat(43), "y".repeat(43)), base); await expect(store.consumeOidcState(expired.state, new Date(base.getTime() + 10 * 60_000))) .resolves.toBeUndefined(); }); test("rejects an OIDC-state flood without deleting any valid live state", async () => { const storageRoot = root(); const store = validStore(storageRoot, { oidcStateCapacity: 8 }); const created = await Promise.all(Array.from({ length: 8 }, (_unused, index) => store.createOidcState(oidcInput(`n${String(index).padStart(42, "0")}`, `v${String(index).padStart(42, "0")}`), base))); await expect(store.createOidcState(oidcInput("x".repeat(43), "y".repeat(43)), base)) .rejects.toThrow("auth_oidc_state_capacity"); await expect(store.consumeOidcState(created[0].state, new Date(base.getTime() + 60_000))) .resolves.toMatchObject({ nonce: "n" + "0".repeat(42) }); expect(created.slice(1).every(({ state }) => existsSync(digestPath(storageRoot, "oidc", state)))).toBe(true); }); test("prunes expired OIDC states before admitting a new transaction at capacity", async () => { const storageRoot = root(); const store = validStore(storageRoot, { oidcStateCapacity: 8 }); const expired = await Promise.all(Array.from({ length: 8 }, (_unused, index) => store.createOidcState(oidcInput(`n${String(index).padStart(42, "0")}`, `v${String(index).padStart(42, "0")}`), base))); const admitted = await store.createOidcState( oidcInput("x".repeat(43), "y".repeat(43)), new Date(base.getTime() + 10 * 60_000), ); expect(expired.every(({ state }) => !existsSync(digestPath(storageRoot, "oidc", state)))).toBe(true); expect(existsSync(digestPath(storageRoot, "oidc", admitted.state))).toBe(true); }); test.skipIf(process.platform === "win32")("starts OIDC without scanning more than 256 ordinary session records", async () => { const storageRoot = root(); const store = validStore(storageRoot); const liveSession = await create(store); for (let index = 0; index < 300; index += 1) { writeFileSync(join(storageRoot, "sessions", `${index.toString(16).padStart(64, "0")}.json`), "{}", { encoding: "utf8", mode: 0o600, }); } const oidc = await store.createOidcState(oidcInput("n".repeat(43), "v".repeat(43)), base); expect(oidc.state).toMatch(/^[A-Za-z0-9_-]{43}$/); expect(existsSync(digestPath(storageRoot, "sessions", liveSession.token))).toBe(true); }); test.skipIf(process.platform === "win32")("uses bounded directory reads for ordinary and OIDC pruning", async () => { const storageRoot = root(); const store = validStore(storageRoot); const expiredSession = await create(store, { idleTtlMs: 60_000, absoluteTtlMs: 60_000 }); const expiredOidc = await store.createOidcState(oidcInput("n".repeat(43), "v".repeat(43)), base); fsHooks.beforeReaddir = (path) => { if (path === join(storageRoot, "sessions") || path === join(storageRoot, "oidc")) { throw new Error("unbounded directory read"); } }; await expect(store.prune(new Date(base.getTime() + 11 * 60_000))).resolves.toBe(2); expect(existsSync(digestPath(storageRoot, "sessions", expiredSession.token))).toBe(false); expect(existsSync(digestPath(storageRoot, "oidc", expiredOidc.state))).toBe(false); }); test.skipIf(process.platform === "win32")("rejects an overfull OIDC directory without deleting a live state", async () => { const storageRoot = root(); const store = validStore(storageRoot); const live = await store.createOidcState(oidcInput("n".repeat(43), "v".repeat(43)), base); for (let index = 0; index < 193; index += 1) { writeFileSync(join(storageRoot, "oidc", `${(index + 1_000).toString(16).padStart(64, "0")}.json`), "{}", { encoding: "utf8", mode: 0o600, }); } await expect(store.createOidcState(oidcInput("x".repeat(43), "y".repeat(43)), base)) .rejects.toThrow("auth_session_store_invalid"); expect(existsSync(digestPath(storageRoot, "oidc", live.state))).toBe(true); }); test("OIDC-only cleanup removes expired state while preserving a live transaction", async () => { const storageRoot = root(); const store = validStore(storageRoot); const expired = await store.createOidcState(oidcInput("e".repeat(43), "f".repeat(43)), base); const live = await store.createOidcState( oidcInput("l".repeat(43), "m".repeat(43)), new Date(base.getTime() + 60_000), ); const admitted = await store.createOidcState( oidcInput("n".repeat(43), "v".repeat(43)), new Date(base.getTime() + 10 * 60_000), ); expect(existsSync(digestPath(storageRoot, "oidc", expired.state))).toBe(false); expect(existsSync(digestPath(storageRoot, "oidc", admitted.state))).toBe(true); await expect(store.consumeOidcState(live.state, new Date(base.getTime() + 10 * 60_000))) .resolves.toMatchObject({ nonce: "l".repeat(43) }); }); test("bounds OIDC state creation across concurrent Node processes", async () => { const storageRoot = root(); const creators = await Promise.all([ isolatedOidcCreator(storageRoot, 6), isolatedOidcCreator(storageRoot, 6), ]); creators.forEach(({ start }) => start()); const winners = await Promise.all(creators.map(({ result }) => result)); expect(winners.reduce((sum, value) => sum + value, 0)).toBe(8); const names = readdirSync(join(storageRoot, "oidc")); expect(names.filter((name) => /^[a-f0-9]{64}\.json$/.test(name))).toHaveLength(8); }); 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(oidcInput("n".repeat(43), "v".repeat(43)), 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(oidcInput("n".repeat(43), "v".repeat(43)), 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(oidcInput("n".repeat(43), "v".repeat(43)), 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(oidcInput("n".repeat(43), "v".repeat(43)), 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(oidcInput("n".repeat(43), "v".repeat(43)), 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("fails closed when the retained POSIX bridge rejects a session read", async () => { const storageRoot = root(); const bridge = { ...testPosixStorageBridge(), read: async () => { throw new Error("native read rejected"); }, } as WindowsAuthStorageBridge; const store = validStore(storageRoot, { posixStorageBridge: bridge }); const created = await create(store); await expectStoreInvalid(store.resolve(created.token)); }); test("does not bypass a retained POSIX bridge read failure", async () => { const storageRoot = root(); const bridge = { ...testPosixStorageBridge(), read: async () => { throw new Error("native root replacement rejected"); }, } as WindowsAuthStorageBridge; const store = validStore(storageRoot, { posixStorageBridge: bridge }); const created = await create(store); await expectStoreInvalid(store.resolve(created.token)); }); test("fails closed when the retained POSIX bridge rejects a session replacement", async () => { const storageRoot = root(); const bridge = { ...testPosixStorageBridge(), replace: async () => { throw new Error("native replace rejected"); }, } as WindowsAuthStorageBridge; const store = validStore(storageRoot, { posixStorageBridge: bridge }); const created = await create(store); 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("fails closed when the retained POSIX bridge rejects a session removal", async () => { const storageRoot = root(); const bridge = { ...testPosixStorageBridge(), remove: async () => { throw new Error("native remove rejected"); }, } as WindowsAuthStorageBridge; const store = validStore(storageRoot, { posixStorageBridge: bridge }); const created = await create(store); await expectStoreInvalid(store.revoke(created.token)); }); test("does not bypass a retained POSIX bridge removal failure", async () => { const storageRoot = root(); const bridge = { ...testPosixStorageBridge(), remove: async () => { throw new Error("native root replacement rejected"); }, } as WindowsAuthStorageBridge; const store = validStore(storageRoot, { posixStorageBridge: bridge }); const created = await create(store); 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() })); }, listPage: async (_root: string, directory: string, after: string | undefined, maximumEntries: number) => { calls.push("list"); if (directory !== "sessions") throw new Error("only ordinary sessions use continuation pages"); const names = [...records.keys()] .filter((entry) => entry.startsWith("sessions/")) .map((entry) => entry.slice("sessions/".length)) .filter((name) => after === undefined || name > after) .sort(); return { entries: names.slice(0, maximumEntries).map((name) => ({ name, modifiedUnixMs: base.getTime() })), more: names.length > maximumEntries, }; }, 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(oidcInput("n".repeat(43), "v".repeat(43)), 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(oidcInput("x".repeat(43), "y".repeat(43)), 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("continues Windows ordinary-session maintenance with a strict 512-record page cursor", async () => { const records = new Map(); const pages: Array<{ after: string | undefined; maximumEntries: number }> = []; const key = (directory: string, filename: string) => `${directory}/${filename}`; const bridge = { create: async (_root: string, directory: string, filename: string, contents: Buffer) => { 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) => { const value = records.get(key(directory, filename)); return value === undefined ? undefined : Buffer.from(value); }, remove: async (_root: string, directory: string, filename: string) => records.delete(key(directory, filename)), list: async (_root: string, directory: string) => { if (directory === "sessions") throw new Error("ordinary maintenance must use a continuation page"); return []; }, listPage: async (_root: string, directory: string, after: string | undefined, maximumEntries: number) => { if (directory !== "sessions") throw new Error("only ordinary sessions use continuation pages"); pages.push({ after, maximumEntries }); const names = [...records.keys()] .filter((entry) => entry.startsWith("sessions/")) .map((entry) => entry.slice("sessions/".length)) .filter((name) => after === undefined || name > after) .sort(); return { entries: names.slice(0, maximumEntries).map((name) => ({ name, modifiedUnixMs: base.getTime() })), more: names.length > maximumEntries, }; }, }; 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 }); await create(store, { idleTtlMs: 30 * 60_000, absoluteTtlMs: 30 * 60_000 }); const liveContents = [...records.values()][0]; records.clear(); const expiredSeed = await create(store, { idleTtlMs: 60_000, absoluteTtlMs: 60_000 }); const expiredContents = [...records.values()][0]; records.clear(); if (!liveContents || !expiredContents) throw new Error("seed records missing"); for (let index = 0; index < 512; index += 1) { records.set(key("sessions", sessionFilename(index)), Buffer.from(liveContents)); } const expiredName = sessionFilename(512); records.set(key("sessions", expiredName), Buffer.from(expiredContents)); await expect(store.prune(new Date(base.getTime() + 2 * 60_000))).resolves.toBe(0); expect(records.has(key("sessions", expiredName))).toBe(true); await expect(store.prune(new Date(base.getTime() + 2 * 60_000))).resolves.toBe(1); expect(records.has(key("sessions", expiredName))).toBe(false); expect(records.has(key("sessions", sessionFilename(0)))).toBe(true); expect(pages).toEqual([ { after: undefined, maximumEntries: 512 }, { after: sessionFilename(511), maximumEntries: 512 }, ]); } finally { Object.defineProperty(process, "platform", originalPlatform); } }); test("starts OIDC on Windows without listing more than 256 ordinary session records", async () => { const records = new Map(); for (let index = 0; index < 300; index += 1) { records.set(`sessions/${index.toString(16).padStart(64, "0")}.json`, Buffer.from("{}")); } const key = (directory: string, filename: string) => `${directory}/${filename}`; const listed: string[] = []; const bridge = { create: async (_root: string, directory: string, filename: string, contents: Buffer) => { 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) => records.get(key(directory, filename)), remove: async (_root: string, directory: string, filename: string) => records.delete(key(directory, filename)), list: async (_root: string, directory: string) => { listed.push(directory); if (directory === "sessions") throw new Error("ordinary sessions must not be listed during OIDC start"); return [...records.keys()] .filter((entry) => entry.startsWith("oidc/")) .map((entry) => ({ name: entry.slice("oidc/".length), modifiedUnixMs: base.getTime() })); }, }; 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", undefined, { windowsStorageBridge: bridge as never, }); await expect(store.createOidcState(oidcInput("n".repeat(43), "v".repeat(43)), base)) .resolves.toMatchObject({ record: { nonce: "n".repeat(43) } }); expect(listed).toEqual(["oidc", "oidc"]); expect([...records.keys()].filter((entry) => entry.startsWith("sessions/"))).toHaveLength(300); } finally { Object.defineProperty(process, "platform", originalPlatform); } }); test("rejects an overfull Windows OIDC directory before reading or deleting live entries", async () => { const entries = Array.from({ length: 193 }, (_unused, index) => ({ name: `${index.toString(16).padStart(64, "0")}.json`, modifiedUnixMs: base.getTime(), })); const read = vi.fn(async () => undefined); const remove = vi.fn(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", undefined, { windowsStorageBridge: { list: async () => entries, read, remove, } as never, }); await expect(store.createOidcState(oidcInput("n".repeat(43), "v".repeat(43)), base)) .rejects.toThrow("auth_session_store_invalid"); expect(read).not.toHaveBeenCalled(); expect(remove).not.toHaveBeenCalled(); } 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; continuation?: boolean; afterName?: 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": if (request.continuation === true) { const names = [...records.keys()] .filter((value) => value.startsWith(`${request.directory}/`)) .map((value) => value.slice(request.directory.length + 1)) .filter((name) => request.afterName === undefined || name > request.afterName) .sort(); return response({ entries: names.slice(0, 512).map((name) => ({ name, modifiedUnixMs: base.getTime() })), more: names.length > 512, }); } 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(oidcInput("n".repeat(43), "v".repeat(43)), 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 ({ input }) => { const request = JSON.parse(input.toString("utf8")) as { continuation?: boolean }; return { code: 0, stdout: Buffer.from(`${JSON.stringify({ version: 1, ok: true, entries: [], ...(request.continuation === true ? { more: false } : {}), })}\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; continuation?: boolean; }; 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() }] : [], ...(request.continuation === true ? { more: false } : {}), }); } 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, }, { posixStorageBridge: testPosixStorageBridge() }); 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); }); });