Files
jb fd63b0ccec
fast-browser scenarios / Offline scenario contract tests (push) Failing after 44s
fast-browser scenarios / Local Chromium headless scenarios (push) Failing after 23s
fast-browser-with-jev
2026-09-20 12:24:08 -04:00

98 lines
4.6 KiB
TypeScript

import { constants } from "node:fs";
import { open } from "node:fs/promises";
import { resolve } from "node:path";
import type { CredentialBindings, CredentialSource } from "./contracts.ts";
const MAX_VALUE = 8_000;
const MAX_FILE = 64 * 1024;
const NAME = /^[A-Za-z0-9_.-]{1,100}$/;
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
/** Messages from this class contain no credential values or file contents. */
export class CredentialError extends Error {}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function validateSource(source: unknown): asserts source is CredentialSource {
if (typeof source === "string") {
if (source.length > MAX_VALUE) throw new CredentialError("Credential literal is too long");
return;
}
if (!isRecord(source)) throw new CredentialError("Invalid credential source");
const keys = Object.keys(source).sort().join(",");
if (keys === "env" && typeof source.env === "string" && source.env.length <= 200 && ENV_NAME.test(source.env)) return;
if ((keys === "file" || keys === "file,key") && typeof source.file === "string"
&& source.file.length > 0 && source.file.length <= 4_000 && !source.file.includes("\0")
&& (source.key === undefined || (typeof source.key === "string" && source.key.length > 0 && source.key.length <= 200))) return;
throw new CredentialError("Credential source must be a literal, environment reference, or local file reference");
}
function validateValue(value: unknown): string {
if (typeof value !== "string" || value.length > MAX_VALUE) {
throw new CredentialError("Resolved credential must be a bounded string");
}
// Do not trim: spaces/newlines can be part of a test credential.
return value;
}
async function readCredentialFile(source: { file: string; key?: string }, cwd: string, signal: AbortSignal): Promise<string> {
signal.throwIfAborted();
// Nonblocking open permits rejecting FIFOs/devices before a read could hang.
const handle = await open(resolve(cwd, source.file), constants.O_RDONLY | constants.O_NONBLOCK);
try {
signal.throwIfAborted();
const stat = await handle.stat();
if (!stat.isFile() || stat.size > MAX_FILE) throw new CredentialError("Credential source must be a bounded regular file");
const bytes = Buffer.alloc(MAX_FILE + 1);
let used = 0;
while (used <= MAX_FILE) {
signal.throwIfAborted();
const { bytesRead } = await handle.read(bytes, used, bytes.length - used, null);
if (!bytesRead) break;
used += bytesRead;
if (used > MAX_FILE) throw new CredentialError("Credential file exceeds the size limit");
}
signal.throwIfAborted();
const contents = new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, used));
if (source.key === undefined) return validateValue(contents);
let json: unknown;
try { json = JSON.parse(contents); } catch { throw new CredentialError("Credential file is not valid JSON"); }
if (!isRecord(json) || !Object.hasOwn(json, source.key)) throw new CredentialError("Credential JSON entry is missing");
// key is an exact top-level key, never an expression or executable path.
return validateValue(json[source.key]);
} finally {
await handle.close();
}
}
/** Validate declarations now; access values only when the selected fill needs them. */
export function createCredentialResolver(
bindings: CredentialBindings,
options: { cwd: string; env?: NodeJS.ProcessEnv },
): (name: string, signal: AbortSignal) => Promise<string> {
if (!isRecord(bindings) || Object.keys(bindings).length > 40) throw new CredentialError("Invalid or oversized credential bindings");
const sources = new Map<string, CredentialSource>();
for (const [name, source] of Object.entries(bindings)) {
if (!NAME.test(name)) throw new CredentialError("Invalid credential reference name");
validateSource(source);
sources.set(name, typeof source === "string" ? source : { ...source });
}
const env = options.env ?? process.env;
return async (name, signal) => {
signal.throwIfAborted();
if (!sources.has(name)) throw new CredentialError("Requested credential reference was not supplied");
const source = sources.get(name)!;
try {
if (typeof source === "string") return source;
if ("env" in source) return validateValue(env[source.env]);
return await readCredentialFile(source, options.cwd, signal);
} catch (error) {
signal.throwIfAborted();
if (error instanceof CredentialError) throw error;
throw new CredentialError("Credential source could not be read");
}
};
}