import { a as SecretDef, c as file, d as validateConfig, i as FerryConfig, l as op, n as BackendRef, o as defineFerry, r as DEFAULT_AUDIT_PATH, s as env, t as BackendKind, u as parseConfig } from "./schema-CZgZlno6.js"; import { Transform, TransformCallback, Writable } from "node:stream"; import { spawn } from "node:child_process"; //#region src/glob.d.ts /** * Tiny argv-aware command matcher (zero-dep). * * A pattern is a whitespace-separated list of token globs, matched * POSITIONALLY against the command's argv — NOT against a flattened * `" "` string. Flattening let two attacks through: * * 1. extra trailing args slipped past a pattern (a trailing `*` in a joined * string matched "…and anything else", so `aws s3 cp * s3://ok` also * allowed a second `s3://evil` destination); and * 2. an executable literally named `"vercel deploy"` (one argv entry with a * space) spoofed the pattern `vercel deploy *`. * * Positional matching closes both. Within a single token, `*` matches any run * of characters (so a single arg may contain spaces). A pattern token that is * exactly `*` AND is the LAST token soaks up zero or more remaining args. With * no trailing bare `*`, the argv must match the pattern's arity EXACTLY — so a * caller cannot append an extra argument the author never authorized. * * Examples (argv shown as a list): * "vercel *" → ["vercel", …anything] (incl. bare ["vercel"]) * "convex deploy" → ["convex", "deploy"] (exactly, no extra args) * "convex env *" → ["convex", "env", …anything] * "aws s3 cp * s3://mybucket/*" * → ["aws","s3","cp",,] * and NOTHING after it. */ /** Compile ONE token glob to an anchored RegExp (`*` = any run of characters). */ declare function tokenToRegExp(token: string): RegExp; /** Does a single pattern match this argv, position by position? */ declare function matchCommand(pattern: string, argv: readonly string[]): boolean; /** Is this argv permitted by ANY glob in the allowlist? */ declare function isCommandAllowed(argv: readonly string[], allow: readonly string[]): boolean; //#endregion //#region src/redactor.d.ts /** A value to scrub from a stream, and the NAME its placeholder reveals. */ interface RedactionTarget { name: string; value: string; } /** * Choose a placeholder for a secret that provably cannot contain the value. * * Normally `[redacted:NAME]`. But if the value is a substring of that text * (pathologically: the value IS `"redacted"`, the NAME, or literally * `"[redacted:NAME]"`), emitting the placeholder would re-introduce the value — * violating the byte-level "the value never appears" guarantee. In that case * emit an opaque mask built from a character the value does NOT contain, so the * mask can never reproduce it. */ declare function placeholderFor(name: string, value: string): string; /** * Boundary-safe redaction state. Feed it decoded text with `push()`; it returns * the text that is safe to emit now, holding back the final * `maxSecretValueLength - 1` characters in case a value straddles the boundary. * Call `flush()` exactly once, after every contributing stream has ended, to * release the held tail. * * A SINGLE engine can be shared across several streams (e.g. a child's stdout * AND stderr). Because the held-back tail is shared, a secret split half-to- * stdout / half-to-stderr — which a naive per-stream redactor would reassemble * in a combined capture — is still caught. Node is single-threaded, so the * `push()` calls from different streams never interleave mid-scan. */ declare class RedactionEngine { private readonly targets; private readonly keep; private pending; constructor(targets: readonly RedactionTarget[]); private redact; /** Feed decoded text; get back the prefix that is safe to emit right now. */ push(text: string): string; /** Release the held tail. Call once, after all contributing streams end. */ flush(): string; } /** * A Transform that redacts a single stream. This is the last line of the * agent-safety guarantee: even if a child echoes a secret, the calling agent * only ever sees `[redacted:NAME]`. A `StringDecoder` keeps multi-byte UTF-8 * characters intact across chunk boundaries. For cross-stream safety (stdout + * stderr sharing one held-back tail) the runner drives a single * `RedactionEngine` directly instead of two of these. */ declare class Redactor extends Transform { private readonly engine; private readonly decoder; constructor(targets: readonly RedactionTarget[]); _transform(chunk: Buffer | string, _encoding: BufferEncoding, callback: TransformCallback): void; _flush(callback: TransformCallback): void; } //#endregion //#region src/backends/types.d.ts /** * A backend resolves a secret's plaintext value from a `ref`. The value is * returned to Ferry's transient memory only — backends must never log it. */ interface Backend { resolve(ref: string): Promise; } //#endregion //#region src/backends/op.d.ts /** * Runs a command and returns its stdout/stderr. Injectable so tests can stub * `op` without invoking the real 1Password CLI (and so a real value never has * to exist in a test). */ type OpExec = (bin: string, args: string[]) => Promise<{ stdout: string; stderr: string; }>; /** * 1Password backend: resolves a secret via `op read `. The value is read * from the child's stdout into transient memory and returned; it is never * logged. Because `op`'s biometric prompt fails headless, pair this with * `ferry cache ` to decrypt once into the encrypted `file` backend. */ declare class OpBackend implements Backend { private readonly exec; constructor(exec?: OpExec); resolve(ref: string): Promise; } //#endregion //#region src/backends/env.d.ts /** * Process-env backend (lowest security; local dev). Reads from a provided env * source (defaults to `process.env`, which `ferry` may have hydrated from a * local `.env`). Injectable source keeps it testable without touching the real * environment. */ declare class EnvBackend implements Backend { private readonly source; constructor(source?: NodeJS.ProcessEnv); resolve(ref: string): Promise; } //#endregion //#region src/backends/file.d.ts /** Default on-disk location of the encrypted secret store. */ declare const DEFAULT_FILE_STORE = ".ferry/secrets.enc.json"; interface FileBackendOptions { /** Store path. Default: `.ferry/secrets.enc.json`. */ path?: string; /** * Passphrase used to derive the AES-256 key (scrypt). Sourced from * `FERRY_FILE_KEY`. A real OS-keychain integration is post-v0.1. */ key?: string; } /** * Encrypted-local backend (AES-256-GCM, scrypt-derived key). This is the * headless-friendly store that fixes the ".env juggling / op biometric * timeout" pain: `ferry cache ` decrypts a value from another backend * once and writes it here; later runs read it without any prompt. */ declare class FileBackend implements Backend { private readonly path; private readonly key; constructor(options?: FileBackendOptions); private requireKey; private readStore; resolve(name: string): Promise; /** * Encrypt and store `value` under `name`. Used by `ferry cache`: the caller * resolves the plaintext from the configured backend once, then hands it here * for headless reuse. The plaintext is never written anywhere else. */ cache(name: string, value: string): void; } //#endregion //#region src/backends/index.d.ts /** Injectable dependencies for backend construction (tests + config). */ interface BackendDeps { /** Env source for the `env` backend. Defaults to `process.env`. */ env?: NodeJS.ProcessEnv; /** Command runner for the `op` backend. Defaults to a real `execFile`. */ opExec?: OpExec; /** Store path for the `file` backend. Defaults to `.ferry/secrets.enc.json`. */ fileStorePath?: string; /** Passphrase for the `file` backend. Defaults to `FERRY_FILE_KEY`. */ fileKey?: string; } /** Construct the concrete backend for a ref, wiring injectable deps. */ declare function resolveBackend(ref: BackendRef, deps?: BackendDeps): Backend; //#endregion //#region src/audit.d.ts /** Whether a secret was injected into the child, or refused by policy. */ type AuditDecision = "inject" | "deny"; /** * One append-only audit row. Records that an access DECISION happened — never * the value. `command` is a non-sensitive descriptor: the executable name + arg * count. Arguments are deliberately NOT recorded, because they are * caller-controlled and may contain secrets Ferry never resolved. */ interface AuditRow { ts: string; secret: string; command: string; backend: string; decision: AuditDecision; } interface AuditOptions { path: string; /** Injectable clock for deterministic tests. Defaults to `Date.now`. */ now?: () => number; } /** Append one JSONL row to the audit log, creating the directory if needed. */ declare function appendAudit(row: Omit, opts: AuditOptions): AuditRow; /** Read audit rows (optionally only the last `tail`). Missing file → `[]`. */ declare function readAudit(path: string, tail?: number): AuditRow[]; //#endregion //#region src/runner.d.ts /** Dependencies for a run — all injectable for tests; all optional at runtime. */ interface RunDeps extends BackendDeps { /** Where redacted child stdout goes. Defaults to `process.stdout`. */ stdout?: Writable; /** Where redacted child stderr goes. Defaults to `process.stderr`. */ stderr?: Writable; /** Injectable clock for audit rows. */ now?: () => number; /** Override the child spawner (tests). */ spawnFn?: typeof spawn; /** * Override secret resolution wholesale (tests). Given a name + its backend * ref, return the plaintext value. When set, backend deps are ignored. */ resolveSecret?: (name: string, backend: BackendRef) => Promise; } interface RunOptions { config: FerryConfig; /** The command + args, e.g. `["vercel", "deploy", "--prod"]`. */ commandArgv: string[]; /** Restrict injection to these secret NAMES (must be declared). */ only?: string[]; /** Working directory for the child. */ cwd?: string; /** * Forward only a minimal, safe base environment (PATH/HOME/…) to the child * instead of Ferry's full ambient env. Closes the blast radius of UNDECLARED * ambient secrets (AWS creds, other API keys) that Ferry does not manage and * therefore cannot redact. Overrides `config.cleanEnv`. */ cleanEnv?: boolean; deps?: RunDeps; } /** Non-sensitive result metadata. Contains NO secret values. */ interface RunResult { exitCode: number; /** NAMES injected into the child env. */ injected: string[]; /** NAMES refused by policy for this command. */ denied: string[]; /** * A non-sensitive command descriptor: the executable name + arg count (e.g. * `"vercel (+2 args)"`). Arguments are deliberately NOT recorded — they are * caller-controlled and may contain secrets. */ command: string; } /** * The core path: resolve declared secrets (optionally filtered by `--only`), * ENFORCE the per-command allow policy (a secret is injected only if one of its * globs matches the argv), spawn the child with the injected env, stream its * stdout/stderr through a SHARED redaction engine, write audit rows, and * propagate the child's exit code. * * The secret value only ever lives in (a) the backend, (b) the child env, and * (c) transient memory here. It is never written to `stdout`, the audit log, or * the returned metadata. */ declare function run(opts: RunOptions): Promise; //#endregion //#region src/config-loader.d.ts /** * Config loading (Node-only). * * SECURITY NOTE: a `ferry.config.*` is TRUSTED code you author, the same way a * Makefile, a `vite.config`, or a shell profile is. `.mjs`/`.js` configs run * in-process via dynamic `import()`; a `.ts` config runs in a `tsx` child. * Ferry only guarantees policy + redaction for the SECRETS it brokers at run * time; it does not sandbox your config. So don't `console.log` a secret inside * it, and don't run `ferry` against a config from an untrusted repo. This is * the same trust model as every other config-file tool, not a Ferry-specific * hole. */ /** Supported config filenames, in resolution priority order. */ declare const CONFIG_CANDIDATES: readonly ["ferry.config.ts", "ferry.config.mjs", "ferry.config.js"]; /** First existing config file in `cwd`, or `null`. */ declare function findConfigPath(cwd: string): string | null; /** * Load + validate the config. `.mjs` / `.js` load with zero dependencies via * dynamic import. `.ts` is loaded by spawning the same Node with the `tsx` * loader (`node --import tsx`), which requires `tsx` to be installed (the only * situation where Ferry leans on a dev tool). The resolved config is * JSON-serializable, so the `.ts` loader round-trips it as JSON through a * sentinel-delimited child stdout. */ declare function loadConfig(cwd?: string): Promise<{ path: string; config: FerryConfig; }>; //#endregion //#region src/dotenv.d.ts /** * Minimal `.env` loader (zero-dep): `KEY=VALUE`, `#` comments, optional single * or double quotes. Does NOT override variables already present in `env`, so a * real environment always wins over the file. */ declare function loadDotenv(cwd?: string, env?: NodeJS.ProcessEnv): void; //#endregion export { type AuditDecision, type AuditOptions, type AuditRow, type Backend, type BackendDeps, type BackendKind, type BackendRef, CONFIG_CANDIDATES, DEFAULT_AUDIT_PATH, DEFAULT_FILE_STORE, EnvBackend, type FerryConfig, FileBackend, OpBackend, type OpExec, RedactionEngine, type RedactionTarget, Redactor, type RunDeps, type RunOptions, type RunResult, type SecretDef, appendAudit, defineFerry, env, file, findConfigPath, isCommandAllowed, loadConfig, loadDotenv, matchCommand, op, parseConfig, placeholderFor, readAudit, resolveBackend, run, tokenToRegExp, validateConfig }; //# sourceMappingURL=index.d.ts.map