import { afterEach, expect, test } from "vitest"; import { chmodSync, mkdtempSync, renameSync, rmSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { loadSecretBundle, loadSecretBundleWithFs, secretValue } from "../src/config/secret-bundle.js"; const dirs: string[] = []; afterEach(() => { for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true }); }); function bundle(contents: string, mode = 0o600): string { const dir = mkdtempSync(join(tmpdir(), "thothii-secret-bundle-")); dirs.push(dir); const file = join(dir, "bundle"); writeFileSync(file, contents, { mode }); chmodSync(file, mode); return file; } test("parses comments, blank lines and values containing equals", () => { const file = bundle("# comment\n\nTHT_MODEL_API_KEY=abc=123\nTHT_DWH_API_KEY=dwh\n"); expect(loadSecretBundle(file)).toEqual(new Map([ ["THT_MODEL_API_KEY", "abc=123"], ["THT_DWH_API_KEY", "dwh"], ])); }); test.each([ ["duplicate", "THT_MODEL_API_KEY=a\nTHT_MODEL_API_KEY=b\n"], ["unknown", "UNKNOWN_KEY=x\n"], ["empty", "THT_MODEL_API_KEY=\n"], ["syntax", "THT_MODEL_API_KEY\n"], ])("rejects %s bundle lines without exposing values", (_name, contents) => { expect(() => loadSecretBundle(bundle(contents))).toThrow("secret bundle is unavailable"); expect(() => loadSecretBundle(bundle(contents))).not.toThrow(/abc|dwh/); }); test("rejects missing and insecure files", () => { const file = bundle("THT_MODEL_API_KEY=secret\n", 0o644); expect(() => loadSecretBundle(file)).toThrow("secret bundle is unavailable"); expect(() => loadSecretBundle(join(file, "missing"))).toThrow("secret bundle is unavailable"); }); test("checks inode identity before parsing", () => { const file = bundle("THT_MODEL_API_KEY=secret\n"); const replacement = `${file}.replacement`; writeFileSync(replacement, "THT_MODEL_API_KEY=replaced\n", { mode: 0o600 }); // A real replacement is safe because the loader's open/fstat check is the invariant; // this also ensures the normal post-replacement file remains parseable. renameSync(replacement, file); expect(loadSecretBundle(file).get("THT_MODEL_API_KEY")).toBe("replaced"); }); test("rejects inode replacement between lstat and open without reading", () => { let reads = 0; const stat = (ino: number) => ({ dev: 7, ino, uid: process.getuid?.() ?? 0, mode: 0o100600, nlink: 1, size: 24, isFile: () => true, isDirectory: () => false, isSymbolicLink: () => false, }); expect(() => loadSecretBundleWithFs("/safe/bundle", { lstat: () => stat(1) as any, open: () => 9, fstat: () => stat(2) as any, read: () => { reads += 1; return "THT_MODEL_API_KEY=secret\n"; }, close: () => undefined, })).toThrow("secret bundle is unavailable"); expect(reads).toBe(0); }); test("secretValue prefers bundle and supports the legacy file fallback", () => { const file = bundle("THT_MODEL_API_KEY=from-bundle\n"); const legacy = bundle("from-legacy"); expect(secretValue({ secretsFile: file, secretFiles: { THT_MODEL_API_KEY_SECRET_FILE: legacy } }, "THT_MODEL_API_KEY")) .toBe("from-bundle"); expect(secretValue({ secretFiles: { THT_MODEL_API_KEY_SECRET_FILE: legacy } }, "THT_MODEL_API_KEY")) .toBe("from-legacy"); });