/** * Vault element acceptance: * - a missing secret fails boot listing every gap * - a secret value never appears in a log or trace even when passed to fx.log * - fingerprints instead of values * - resolution chain (first hit wins) */ import { afterEach, describe, expect, test } from "bun:test"; import { envVaultDriver, memoryVaultDriver } from "../drivers/index.ts"; import { createFx } from "../kernel/fx.ts"; import { REDACTED_PLACEHOLDER, Redacted } from "../kernel/redacted.ts"; import { createVaultRuntime, fingerprintSecretSync, listRequiredEnvNames, resetRequiredEnvNames, SECRET_MASK, vault, VaultBootError, } from "./vault.ts"; describe("vault declaration", () => { test("vault() and vault.secret() shapes", () => { const a = vault("STRIPE_KEY", { description: "Payments gateway key", rotate: "90d", }); expect(a.kind).toBe("secret"); expect(a.sensitive).toBe(true); expect(a.name).toBe("STRIPE_KEY"); expect(a.description).toBe("Payments gateway key"); const b = vault.secret("DATABASE_URL", { dev: "postgres://local" }); expect(b.name).toBe("DATABASE_URL"); expect(b.dev).toBe("postgres://local"); const cfg = vault.config("PUBLIC_APP_URL", { dev: "http://localhost" }); expect(cfg.kind).toBe("config"); expect(cfg.sensitive).toBe(false); const fromDocker = vault.fromDocker("store.sql"); expect(fromDocker).toStartWith("__oke_from_docker__:"); const c = vault.secret("DATABASE_URL", { dev: fromDocker }); expect(c.dev).toBe(fromDocker); }); test("fromDocker resolves via OKE__URL without env-var names in the kernel", async () => { const prev = process.env.OKE_STORE_SQL_URL; process.env.OKE_STORE_SQL_URL = "postgres://oke:x@127.0.0.1:5432/oke"; try { const runtime = createVaultRuntime({ secrets: [vault.secret("DATABASE_URL", { dev: vault.fromDocker("store.sql") })], chain: [], allowDevFallbacks: true, }); await runtime.boot(); expect(runtime.read("DATABASE_URL")).toBe("postgres://oke:x@127.0.0.1:5432/oke"); } finally { if (prev === undefined) delete process.env.OKE_STORE_SQL_URL; else process.env.OKE_STORE_SQL_URL = prev; } }); }); describe("boot lists every missing secret", () => { test("VaultBootError enumerates all gaps at once", async () => { const runtime = createVaultRuntime({ secrets: [ vault("STRIPE_KEY", { description: "Payments gateway key" }), vault("DATABASE_URL", { description: "Primary SQL URL" }), vault("PRESENT"), ], chain: [ { driver: memoryVaultDriver, options: { secrets: { PRESENT: "yes" } }, }, ], }); try { await runtime.boot(); expect.unreachable("boot should fail"); } catch (err) { expect(err).toBeInstanceOf(VaultBootError); const boot = err as VaultBootError; expect(boot.gaps).toHaveLength(2); expect(boot.gaps.map((g) => g.name).sort()).toEqual(["DATABASE_URL", "STRIPE_KEY"]); expect(boot.message).toContain("STRIPE_KEY"); expect(boot.message).toContain("Payments gateway key"); expect(boot.message).toContain("DATABASE_URL"); expect(boot.message).toContain("2 missing"); } }); test("dev fallbacks fill gaps when allowDevFallbacks", async () => { const runtime = createVaultRuntime({ secrets: [vault("STRIPE_KEY", { dev: "sk_test_local" })], chain: [], allowDevFallbacks: true, }); await runtime.boot(); expect(runtime.read("STRIPE_KEY")).toBe("sk_test_local"); }); }); describe("resolution chain", () => { test("first layer wins", async () => { const runtime = createVaultRuntime({ secrets: [vault("KEY")], chain: [ { driver: memoryVaultDriver, source: "process.env", options: { secrets: { KEY: "from-a" } }, }, { driver: memoryVaultDriver, source: "driver", options: { secrets: { KEY: "from-b" } }, }, ], }); await runtime.boot(); expect(runtime.read("KEY")).toBe("from-a"); expect(runtime.resolution("KEY")).toBe("process.env"); const chain = runtime.resolutionChain("KEY"); expect(chain.map((s) => s.source)).toEqual(["driver", "process.env"]); expect(chain.find((s) => s.source === "process.env")?.won).toBe(true); expect(chain.find((s) => s.source === "driver")?.present).toBe(true); expect(chain.find((s) => s.source === "driver")?.won).toBe(false); }); test("env layer resolves process env", async () => { const runtime = createVaultRuntime({ secrets: [vault("OKE_TEST_SECRET")], chain: [ { driver: envVaultDriver, source: "process.env", options: { env: { OKE_TEST_SECRET: "from-env" } }, }, ], }); await runtime.boot(); expect(runtime.read("OKE_TEST_SECRET")).toBe("from-env"); expect(runtime.resolution("OKE_TEST_SECRET")).toBe("process.env"); }); test("last-read timestamp updates on read", async () => { let now = 1000; const runtime = createVaultRuntime({ secrets: [vault("KEY")], chain: [ { driver: memoryVaultDriver, options: { secrets: { KEY: "v" } }, }, ], now: () => now, }); await runtime.boot(); expect(runtime.lastReadAt("KEY")).toBeUndefined(); runtime.read("KEY"); expect(runtime.lastReadAt("KEY")).toBe(1000); now = 2000; runtime.read("KEY"); expect(runtime.lastReadAt("KEY")).toBe(2000); }); test("config cleartext is exposed; secrets are not", async () => { const runtime = createVaultRuntime({ secrets: [vault.secret("STRIPE_KEY"), vault.config("PUBLIC_URL")], chain: [ { driver: memoryVaultDriver, options: { secrets: { STRIPE_KEY: "sk_secret", PUBLIC_URL: "https://example.com", }, }, }, ], }); await runtime.boot(); expect(runtime.cleartext("STRIPE_KEY")).toBeUndefined(); expect(runtime.cleartext("PUBLIC_URL")).toBe("https://example.com"); expect(runtime.fingerprint("STRIPE_KEY")).toBeDefined(); expect(runtime.fingerprint("PUBLIC_URL")).toBeUndefined(); }); }); describe("vault.env", () => { const NAME = "OKE_TEST_VAULT_ENV"; afterEach(() => { delete process.env[NAME]; resetRequiredEnvNames(); }); test("reads, coerces, and treats an empty value as unset", () => { expect(vault.env(NAME)).toBeUndefined(); process.env[NAME] = ""; expect(vault.env(NAME)).toBeUndefined(); process.env[NAME] = "42"; expect(vault.env(NAME)).toBe("42"); expect(vault.env.int(NAME)).toBe(42); process.env[NAME] = "YES"; expect(vault.env.bool(NAME)).toBe(true); process.env[NAME] = "off"; expect(vault.env.bool(NAME)).toBe(false); process.env[NAME] = '{"a":1}'; expect(vault.env.json<{ a: number }>(NAME)).toEqual({ a: 1 }); delete process.env[NAME]; expect(vault.env.int(NAME, 7)).toBe(7); expect(vault.env.bool(NAME, true)).toBe(true); expect(vault.env.json(NAME)).toBeUndefined(); }); test("malformed values and missing defaults throw TypeError", () => { process.env[NAME] = "not-a-number"; expect(() => vault.env.int(NAME)).toThrow(TypeError); process.env[NAME] = "maybe"; expect(() => vault.env.bool(NAME)).toThrow(TypeError); process.env[NAME] = "{"; expect(() => vault.env.json(NAME)).toThrow(TypeError); delete process.env[NAME]; expect(() => vault.env.int(NAME)).toThrow(TypeError); }); test("required registers the name and throws when unset", () => { expect(() => vault.env.required(NAME)).toThrow(TypeError); expect(listRequiredEnvNames()).toContain(NAME); process.env[NAME] = "present"; expect(vault.env.required(NAME)).toBe("present"); expect(listRequiredEnvNames().filter((n) => n === NAME)).toHaveLength(1); }); test("a missing required env var joins the secret gaps in one boot failure", async () => { const runtime = createVaultRuntime({ secrets: [vault("STRIPE_KEY")], requiredEnv: [NAME], chain: [{ driver: memoryVaultDriver, options: { secrets: {} } }], }); const error = await runtime.boot().then( () => undefined, (e: unknown) => e as VaultBootError, ); expect(error).toBeInstanceOf(VaultBootError); expect(error?.gaps.map((g) => g.name).sort()).toEqual([NAME, "STRIPE_KEY"]); }); }); describe("redaction + fingerprints", () => { test("secret value never appears in fx.log even when passed explicitly", async () => { const secret = "sk_live_super_secret_value_do_not_leak"; const runtime = createVaultRuntime({ secrets: [vault("STRIPE_KEY")], chain: [{ driver: memoryVaultDriver, options: { secrets: { STRIPE_KEY: secret } } }], }); await runtime.boot(); const lines: Array<{ message: string; data?: Record }> = []; const { fx } = createFxContextWithVault(runtime, lines); const wrapped = await fx.vault.get("STRIPE_KEY"); expect(wrapped.reveal()).toBe(secret); fx.log.info(`charging with ${wrapped}`, { key: wrapped, nested: { k: wrapped } }); // Revealed cleartext still hits the known-value substring scrub. fx.log.error(secret, { stripe: secret }); for (const line of lines) { expect(line.message).not.toContain(secret); expect(JSON.stringify(line.data ?? {})).not.toContain(secret); } expect(lines[0]!.message).toContain(REDACTED_PLACEHOLDER); expect(lines[1]!.message).toContain(SECRET_MASK); }); test("Redacted from fx.vault is masked in fx.log without a vault runtime", async () => { const secret = "sk_no_vault_runtime"; const lines: Array<{ message: string; data?: Record }> = []; const fx = createFx({ flow: "test.redacted", secrets: { STRIPE_KEY: secret }, onLog: (_level, message, data) => lines.push({ message, data }), }); const wrapped = await fx.vault.get("STRIPE_KEY"); expect(wrapped).toBeInstanceOf(Redacted); expect(String(wrapped)).toBe(REDACTED_PLACEHOLDER); expect(`${wrapped}`).toBe(REDACTED_PLACEHOLDER); expect(JSON.stringify({ v: wrapped })).toBe(`{"v":"${REDACTED_PLACEHOLDER}"}`); fx.log.info("paying", { key: wrapped, nested: { k: [wrapped] } }); expect(JSON.stringify(lines[0]!.data)).not.toContain(secret); expect(lines[0]!.data?.key).toBe(REDACTED_PLACEHOLDER); }); test("fingerprint is stable and not the value", async () => { const secret = "sk_test_abc"; const runtime = createVaultRuntime({ secrets: [vault("STRIPE_KEY")], chain: [ { driver: memoryVaultDriver, options: { secrets: { STRIPE_KEY: secret } }, }, ], }); await runtime.boot(); const fp = runtime.fingerprint("STRIPE_KEY"); expect(fp).toBe(fingerprintSecretSync(secret)); expect(fp).not.toContain(secret); expect(fp?.startsWith("sha256:")).toBe(true); }); }); /** * Helper — createFx with vault runtime + log sink. */ function createFxContextWithVault( vaultRuntime: Awaited>, lines: Array<{ message: string; data?: Record }>, ) { return { fx: createFx({ flow: "payments.charge", effects: { secrets: ["STRIPE_KEY"] }, vaultRuntime, onLog(level, message, data) { void level; lines.push({ message, data }); }, }), }; }