import assert from "node:assert/strict"; import { mkdtemp, writeFile, rm, mkdir } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; import { createCredentialResolver } from "./credentials.ts"; import type { CredentialBindings } from "./contracts.ts"; const signal = () => new AbortController().signal; async function fixture(fn: (cwd: string) => Promise) { const cwd = await mkdtemp(join(tmpdir(), "fast-browser-credentials-")); try { await fn(cwd); } finally { await rm(cwd, { recursive: true, force: true }); } } test("literals and explicit environment sources resolve without trimming or process-env fallback", async () => { const resolve = createCredentialResolver({ literal: " test-value\n", environment: { env: "TEST_PASSWORD" }, blank: "" }, { cwd: "/unused", env: { TEST_PASSWORD: "another-value" }, }); assert.equal(await resolve("literal", signal()), " test-value\n"); assert.equal(await resolve("environment", signal()), "another-value"); assert.equal(await resolve("blank", signal()), ""); await assert.rejects(resolve("missing", signal()), /not supplied/); }); test("plain file and exact JSON key sources resolve relative to the caller cwd", async () => { await fixture(async (cwd) => { await writeFile(join(cwd, "password.txt"), "plain-secret\n", { mode: 0o600 }); await writeFile(join(cwd, "accounts.json"), JSON.stringify({ "test.password": "json-secret", user: "demo" }), { mode: 0o600 }); const resolve = createCredentialResolver({ plain: { file: "password.txt" }, json: { file: "accounts.json", key: "test.password" }, }, { cwd, env: {} }); assert.equal(await resolve("plain", signal()), "plain-secret\n"); assert.equal(await resolve("json", signal()), "json-secret"); }); }); test("files are read lazily and malformed/missing values never appear in errors", async () => { await fixture(async (cwd) => { const resolve = createCredentialResolver({ secret: { file: "accounts.json", key: "password" } }, { cwd, env: {} }); await writeFile(join(cwd, "accounts.json"), "private-secret-is-not-json"); await assert.rejects(resolve("secret", signal()), (error: Error) => { assert.match(error.message, /not valid JSON/); assert.doesNotMatch(error.message, /private-secret/); return true; }); await writeFile(join(cwd, "accounts.json"), JSON.stringify({ password: "new-value" })); assert.equal(await resolve("secret", signal()), "new-value"); await writeFile(join(cwd, "accounts.json"), JSON.stringify({ elsewhere: "private-value" })); await assert.rejects(resolve("secret", signal()), /entry is missing/); }); }); test("missing environment and oversized values fail closed", async () => { const resolve = createCredentialResolver({ p: { env: "PRIVATE_VALUE" } }, { cwd: "/unused", env: {} }); await assert.rejects(resolve("p", signal()), /bounded string/); assert.throws(() => createCredentialResolver({ p: "x".repeat(8001) }, { cwd: "/unused" }), /too long/); }); test("unknown source fields and ambiguous bindings are rejected before any IO", () => { for (const bindings of [ { p: { env: "A", file: "x" } }, { p: { command: "print-secret" } }, { p: { env: "A B" } }, { p: { file: "x", key: 2 } }, { "bad name": "value" }, { p: null }, ]) { assert.throws(() => createCredentialResolver(bindings as unknown as CredentialBindings, { cwd: "/unused" })); } }); test("non-regular and oversized files are rejected", async () => { await fixture(async (cwd) => { await mkdir(join(cwd, "directory")); await writeFile(join(cwd, "large"), "x".repeat(65537)); for (const file of ["directory", "large"]) { const resolve = createCredentialResolver({ p: { file } }, { cwd, env: {} }); await assert.rejects(resolve("p", signal()), /bounded regular file/); } }); }); test("pre-aborted credential resolution does not read the source", async () => { const resolve = createCredentialResolver({ p: { file: "/nonexistent" } }, { cwd: "/unused", env: {} }); const abort = new AbortController(); abort.abort(new Error("test cancellation")); await assert.rejects(resolve("p", abort.signal), /test cancellation/); });