/** * vaultCredentials — a {@link CredentialProvider} over a HashiCorp-Vault-compatible * KV v2 secret store, spoken as plain HTTP. * * import { vaultCredentials } from 'agentfootprint/security'; * * const credentials = vaultCredentials({ * address: 'https://vault.internal:8200', // https, or say `allowHttp` out loud * mount: 'secret', // KV v2 mount, default 'secret' * paths: { github: 'ci/github' }, // service → path INSIDE the mount * }); // token: VAULT_TOKEN, or `token` * * Zero dependencies and no SDK: one `GET` per resolution through the runtime's * own `fetch`. Vault's HTTP API is small, stable and the thing every * Vault-compatible store (OpenBao, and the Vault-API modes of several managed * stores) implements — so the adapter that speaks HTTP works against more * backends than the adapter that imports one vendor's client. * * ## V1 is deliberately one shape, and says so by name * * | Axis | V1 | Anything else | * |---|---|---| * | Auth | a **token** (`token` option, else `VAULT_TOKEN`) | AppRole / Kubernetes / JWT / AWS IAM login are **refused by name**, naming the option that would carry them | * | Secret engine | **KV v2** (`/data/`, the `data.data` envelope) | a KV v1 mount is refused by name once the response shape gives it away | * | Leases / renewal | **none** — every `getCredential` re-reads the secret | a lease-aware provider is a different object, and the library's model since 9.7.0 is re-resolve-per-call | * * That is not modesty, it is the honest edge: an auth method the author cannot * exercise against a real cluster would be a guess wearing an adapter's clothes. * Each refusal names the option it would arrive on, so "tell us your auth shape" * is a field report rather than an issue title. * * ## Field → credential kind * * A KV v2 read returns `{ data: { data: { …your fields… }, metadata: {…} } }`. * The inner object is mapped to a {@link Credential} by the FIRST rule that * matches, so a secret written the ordinary way needs no configuration: * * | Fields present | Becomes | Header it applies | * |---|---|---| * | `token` | `bearer(token)` | `authorization: Bearer …` | * | `api_key` \| `apiKey` \| `key` | `apiKey(value, header ?? 'x-api-key')` | that header | * | `username` + `password` | `basic(username, password)` | `authorization: Basic …` | * | `headers` (an object of strings) | `headers(map)` | all of them | * * A secret matching none of them is refused — naming the PATH and the four * shapes, never the secret. `toCredential` is the seam for a shop whose fields * are named otherwise; it sees the secret and returns a `Credential`, and * returning `undefined` falls back to the table above. * * ## Secrecy (the 8.6.0 two-clause law, applied here) * * A thrown message reaches the model as a tool result AND rides * `agentfootprint.credential.failed`. So every error this adapter raises names * **the service, the path and the HTTP status, and nothing from the response * body or the token**. Nothing here logs, and no secret value, no `X-Vault-Token` * header and no field name from the payload appears in any message it can throw * — pinned by a grep-shaped test over every failure path. The credential it * returns hides its own secret fields (non-enumerable) and carries `toHeaders`, * so `structuredClone` rejects it and it cannot enter tracked scope by accident. * * @example Dev → prod is the same two lines * ```ts * // dev * const credentials = staticTokens({ github: 'ghp_dev_xxx' }); * // prod — the tool code does not change * const credentials = vaultCredentials({ address: process.env.VAULT_ADDR! }); * Agent.create({ provider, model, credentials }).build(); * ``` */ import type { Credential, CredentialProvider } from '../../identity/types.js'; /** The base options every form shares. */ interface VaultCredentialsBase { /** Vault's base URL, e.g. `https://vault.internal:8200` — **required**, and * **https** unless {@link VaultCredentialsBase.allowHttp} says otherwise. No * `VAULT_ADDR` fallback: an agent that silently picks up an address from the * environment is an agent that reads a different vault when the environment * changes under it. Name it. */ readonly address: string; /** The Vault token. Falls back to `VAULT_TOKEN` (the variable every Vault * tool already sets). This is a secret: it is sent as `X-Vault-Token` and * appears in no message this adapter can throw. */ readonly token?: string; /** Auth method. **`'token'` is the only one V1 implements.** Anything else is * refused at construction, by name, with what it would take — see * {@link vaultCredentials}. */ readonly auth?: 'token'; /** KV v2 mount point. Default `'secret'` (Vault's own default for the KV v2 * engine). The read URL is `
/v1//data/`. */ readonly mount?: string; /** Vault Enterprise / HCP namespace, sent as `X-Vault-Namespace`. Omit for * open-source Vault and OpenBao, which have no namespaces. */ readonly namespace?: string; /** Map the secret's fields to a {@link Credential} yourself. Returns * `undefined` to fall back to the built-in table (`token` / `api_key` / * `username`+`password` / `headers`). The seam for a shop whose field names * are its own — and the reason this adapter does not need an option per * spelling. **Never log or return the fields from here**; they are the * secret. */ readonly toCredential?: (secret: Readonly>, service: string) => Credential | undefined; /** Header name for the `api_key` shape when the secret does not carry its own * `header` field. Default `'x-api-key'`. */ readonly apiKeyHeader?: string; /** Request timeout in ms. Default 5000 — a credential resolution sits in * front of a tool call, so a hung vault must fail rather than hang a run. */ readonly timeoutMs?: number; /** Allow a plain-`http://` address. **Refused unless you set this**, because * the Vault token travels in a request header: over plaintext HTTP, anyone * on the path reads a token that can usually read every secret it can reach. * Set it only for a loopback dev server (`http://127.0.0.1:8200`). */ readonly allowHttp?: boolean; /** Stable provider id (default `'vault'`). Shows up in "which provider vended * this". */ readonly id?: string; /** Test seam — inject `fetch`. Bypasses the network entirely. */ readonly _fetch?: typeof fetch; } /** * How a `service` becomes a path inside the mount. Three arms, and they * EXCLUDE each other — two spellings of one rule can disagree, so the type * refuses the pair and so does the constructor. */ type VaultPathMapping = { /** `service → path inside the mount`, the {@link staticTokens} shape one * level up: the same literal map, holding a path instead of a token. An * unknown service is refused by name, listing the known ones. */ readonly paths: Readonly>; readonly resolve?: never; } | { /** `service → path`, computed. Return `undefined` to refuse a service. * For the convention-driven shop: ``(s) => `agents/${s}` ``. */ readonly resolve: (service: string) => string | undefined; readonly paths?: never; } | { /** Neither: the **service id IS the path** under the mount, so * `service: 'github'` reads `/data/github`. */ readonly paths?: undefined; readonly resolve?: undefined; }; export type VaultCredentialsOptions = VaultCredentialsBase & VaultPathMapping; /** * Build a {@link CredentialProvider} that reads KV v2 secrets from a * Vault-compatible store. See {@link VaultCredentialsOptions} for the * per-option contract and this module's docstring for the V1 boundary. */ export declare function vaultCredentials(options: VaultCredentialsOptions): CredentialProvider; export {};