/** * Centralised credential resolution for the CLEO LLM layer (T1677). * * ONE canonical entry point: `resolveCredentials(provider, options)`. * Every LLM consumer — extraction, dream-cycle, hygiene-scan, dup-detect, * observer-reflector, deriver, adapters — MUST use this function. * * ## 6-tier resolution chain (first match wins) * * 1. **explicit** — `options.apiKey` passed by the caller * 2. **env** — provider-specific environment variable * (`ANTHROPIC_API_KEY` | `OPENAI_API_KEY` | * `GEMINI_API_KEY` | `MOONSHOT_API_KEY`) * 3. **cred-file** — `~/.cleo/llm-credentials.json` (multi-credential * pool, file-locked, 0600). T-LLM-CRED Phase 2. * 4. **claude-creds** — `~/.claude/.credentials.json` OAuth token * (only for `anthropic` provider; Claude Code zero-config) * 5. **global-config** — `~/.config/cleo/config.json` (XDG config dir, post-T9405) * → `llm.providers..apiKey`. The legacy * `~/.local/share/cleo/config.json` location is still * read as a fallback during the transition window. * 6. **project-config** — `.cleo/config.json` → `llm.providers..apiKey` * * Returns `null` when no key is found in any tier. * * @module llm/credentials * @task T1677 * @epic T1676 */ import type { SealedCredential } from '@cleocode/contracts'; import type { ModelTransport } from './types-config.js'; /** * The resolution source that yielded the API key. * * - `explicit` — caller provided `options.apiKey` directly * - `env` — provider-specific environment variable * - `cred-file` — `~/.cleo/llm-credentials.json` multi-credential pool * (T-LLM-CRED-CENTRALIZATION Phase 2) * - `claude-creds` — `~/.claude/.credentials.json` OAuth token (anthropic only) * - `global-config` — `~/.cleo/config.json` `llm.providers[p].apiKey` * - `project-config` — `.cleo/config.json` `llm.providers[p].apiKey` */ export type CredentialSource = 'explicit' | 'env' | 'cred-file' | 'claude-creds' | 'global-config' | 'project-config'; /** * Authentication scheme used to send the credential to the provider. * * - `api_key` — provider-issued long-lived key sent as `x-api-key` (Anthropic) * or `Authorization: Bearer …` (OpenAI, Gemini, Moonshot). * - `oauth` — short-lived OAuth bearer token (Anthropic Claude Code) sent as * `Authorization: Bearer …` with the matching beta header. * * The set is intentionally small in Phase 1. Phase 3 widens it to add SDK-based * auth (`aws_sdk`, `gcp_sdk`) alongside the Bedrock / Vertex transports. * * @task T-LLM-CRED-CENTRALIZATION Phase 1 */ export type AuthType = 'api_key' | 'oauth'; /** * Result returned by `resolveCredentials()`. * * `apiKey` is null only when all 5 tiers are exhausted without a match. * `authType` indicates the scheme to use when sending the credential — callers * should pass the result through `authHeaders(cred)` rather than hard-coding * `x-api-key` or `Authorization` themselves. */ export interface CredentialResult { /** Provider transport that was resolved for. */ provider: ModelTransport; /** API key or OAuth bearer token string, or null when no credential is available. */ apiKey: string | null; /** Which resolution tier produced the credential (undefined when apiKey is null). */ source: CredentialSource | undefined; /** * Scheme used to present this credential to the provider. Defaults to `'api_key'` * for every source except `claude-creds`, and for tokens whose prefix marks * them as Anthropic OAuth (`sk-ant-oat-*` access / `sk-ant-ort-*` refresh). */ authType: AuthType; } /** * Options accepted by `resolveCredentials()`. */ export interface CredentialResolveOptions { /** * Explicit API key override (tier 1 — highest priority). * Pass this when the caller already has a key and wants to skip all * filesystem / config reads. */ apiKey?: string | null; /** * Absolute path to the project root used for tier 5 (project-config). * Omit to skip project-config resolution. */ projectRoot?: string; } /** * Maps each provider transport to its canonical environment variable name. * * NOTE: `bedrock` is informational — AWS Bedrock uses the AWS SDK credential * chain (`AWS_PROFILE` / `~/.aws/credentials` / IAM role / SSO), not a single * API-key env var. The credential-pool resolves Bedrock via `authType: 'aws_sdk'` * and never reads the `accessToken` field from env. * * Exported so concrete seeders (e.g. the env seeder under * `./credential-seeders/env-seeder.ts`) reuse the same mapping instead of * inlining a duplicate. Treat this as the single source of truth for * `(provider → env var name)`. * * @task T9409 */ export declare const ENV_VARS: Record; /** * Resolve the API key for a provider using the synchronous tier chain. * * Resolution order (first non-empty match wins): * 1. `options.apiKey` — explicit caller override * 2. `ENV_VARS[provider]` environment variable * 3. `~/.cleo/llm-credentials.json` — unified credential pool (read-only; * seeding is the unified pool's responsibility — the sync path does not * re-seed) * 4a. `~/.cleo/config.json` → `llm.providers[provider].apiKey` — **deprecated** * (emits stderr warning; still resolves during the transition window) * 5. `/.cleo/config.json` → `llm.providers[provider].apiKey` — * **rejected** (T-E2-6 footgun kill; emits stderr warning, never resolves) * * Direct reading of `~/.claude/.credentials.json` was removed in T9413; the * `claude-code` seeder is now the sole owner of that file and its imported * entries land in the pool, which the tier-3 read picks up. * * For new code prefer {@link resolveCredentialsAsync}, which delegates to the * {@link UnifiedCredentialPool} singleton (`getCredentialPool().pick()`) and * triggers a lazy seed pass on first call. The sync variant is retained for * call-sites that cannot move to async (e.g. `defaultTransportApiKey`, * `resolveModelCredentials`). * * Never throws. All filesystem errors are caught and treated as "not found". * * @param provider - The LLM provider transport to resolve credentials for. * @param options - Optional overrides and project root for tier 5 warning. * @returns A `CredentialResult` with `provider`, `apiKey`, and `source`. * * @example * ```ts * const cred = resolveCredentials('anthropic', { projectRoot: cwd }); * if (!cred.apiKey) throw new Error('No Anthropic key found'); * ``` * * @task T9413 * @epic E-CONFIG-AUTH-UNIFY */ export declare function resolveCredentials(provider: ModelTransport, options?: CredentialResolveOptions): CredentialResult; /** * Async resolver that delegates to the unified credential pool (T9413). * * Resolution order: * * 1. `options.apiKey` — explicit caller override (identical to the sync * behaviour; short-circuits before touching the pool). * 2. `await getCredentialPool().pick(provider, ...)` — the unified pool * transparently runs a lazy seed pass on first call (60s cache; force * via `force: true` on the pool directly). The returned entry's * `authType` is narrowed to the on-wire {@link AuthType}: `'oauth'` for * OAuth tokens, `'api_key'` for everything else (including `'aws_sdk'`). * * Returns `{ apiKey: null }` with `source: undefined` when the pool has no * eligible entry for the provider — callers should treat this as * "no credential available" and emit a setup hint. * * Unlike the sync resolver, the async path does NOT consult the legacy * tiers 4a/4b/5: any deprecated source must be seeded into the pool first * (via a seeder) to be picked up here. This is the steady-state code path * the rest of E-CONFIG-AUTH-UNIFY converges on. * * @param provider - The LLM provider transport to resolve credentials for. * @param options - Optional explicit `apiKey` override. * @returns A `CredentialResult` shaped identically to the sync resolver. * * @example * ```ts * const cred = await resolveCredentialsAsync('anthropic'); * if (!cred.apiKey) { * throw new Error('No anthropic credential — run `cleo auth add anthropic`'); * } * ``` * * @task T9413 * @epic E-CONFIG-AUTH-UNIFY */ export declare function resolveCredentialsAsync(provider: ModelTransport, options?: CredentialResolveOptions): Promise; /** * Test-only: clear the one-shot deprecation latches so a test asserting * the warning emission can run independently of sibling tests. * * Production code MUST NOT call this — re-emitting the warning on every * hit would flood stderr. * * @internal */ export declare function _resetCredentialDeprecationLatchesForTests(): void; /** * Build the authentication HTTP headers for a resolved credential. * * For raw-fetch call-sites (e.g. memory/sleep-consolidation, memory/observer-reflector) * this returns the full bag of provider-specific auth headers including the * `anthropic-version` or `anthropic-beta` markers that Anthropic requires. * The caller still owns `Content-Type` and the request body. * * Returns an empty object when `cred.apiKey` is null — callers should never * reach this helper without verifying they have a credential, but the no-op * fallback avoids accidental `undefined` header injection. * * @task T-LLM-CRED-CENTRALIZATION Phase 1 */ export declare function authHeaders(cred: CredentialResult): Record; /** * Build the provider auth headers AT THE WIRE directly from a sealed credential * handle — the E10 boundary primitive (T11754 · AC2). * * ## Why this exists * * The pre-E10 / interim pattern materialized the plaintext into a caller-visible * variable first — `const token = (await sealed.fetch()).value;` — and only then * called {@link authHeaders}. That intermediate `token` binding is a leak surface: * any code added between the `fetch()` and the header build could log, serialize, * or forward the bare secret. * * `authHeadersFromSealed` collapses those two steps into ONE chokepoint. It is * the SOLE place (alongside daemon worker-injection) that invokes * {@link SealedCredential.fetch} — the crypto decrypt happens inside the handle's * `fetch()`, the materialized {@link DecryptedToken} is consumed in-place to build * the `x-api-key` / `Authorization: Bearer` headers (per provider + scheme), and * the plaintext goes out of scope WITHOUT ever being returned, logged, or bound * to a caller variable. Callers receive ONLY the finished header bag. * * Invoke this ONLY at a wire boundary — `transportForProvider` / * `session-factory.ts:56`, the raw-fetch consumers (hygiene-scan, * duplicate-detector), or daemon worker-injection. Never to surface a key up the * resolver stack. * * @param sealed - The opaque credential handle returned by the resolver. * @param authType - The auth scheme to present the credential with (mirrors the * resolved `credential.authType`). `'aws_sdk'` yields an empty bag — the AWS * SDK injects credentials out-of-band, so there is no header to build. * @returns The provider-specific auth headers. The plaintext token is consumed * internally and never escapes this function. * @task T11754 */ export declare function authHeadersFromSealed(sealed: SealedCredential, authType: 'api_key' | 'oauth' | 'aws_sdk'): Promise>; /** * Resolve the credential status for a single provider. * * Calls the 6-tier resolution chain and maps the result to a human-facing * `LlmProviderSourceWire` value. Does NOT cache — always re-reads. * * Used by `cleo memory llm-status` to build the `providers[]` array without * branching on provider names. * * @param provider - The provider transport to check. * @returns Status entry with `resolvedSource` and `hasCredential`. * @task T9323 */ export declare function resolveProviderStatus(provider: ModelTransport): { provider: ModelTransport; resolvedSource: 'env' | 'cred-file' | 'claude-creds' | 'config' | 'none'; hasCredential: boolean; }; /** * The set of OAuth-capable provider transports that surface in `llm-status`. * * Extend this tuple when adding new OAuth-capable providers. The order is * preserved in the `providers[]` array of `cleo memory llm-status` output. * * @task T9323 */ export declare const OAUTH_STATUS_PROVIDERS: readonly ModelTransport[]; /** * Store an Anthropic API key in the CLEO global config directory. * * Writes to `~/.local/share/cleo/anthropic-key` with 0600 permissions. * * @param apiKey - The API key to store. */ export declare function storeAnthropicApiKey(apiKey: string): void; /** * No-op retained for test call-site compatibility. * * `resolveCredentials` does not maintain an internal cache, so there is * nothing to invalidate. Tests that call this function between assertions * continue to work correctly — they simply rely on the file-system / env-var * isolation they already set up. */ export declare function clearAnthropicKeyCache(): void; import type { ModelConfig } from './types-config.js'; /** * Resolve API key and base URL for a fully-specified `ModelConfig`. * * This is the lower-level variant used by the LLM layer internals. * Prefer `resolveCredentials(provider, options)` for new callers. * * @param config - ModelConfig with transport and optional apiKey/baseUrl. * @returns `{ apiKey, apiBase }` pair where null means "use SDK default". */ export declare function resolveModelCredentials(config: ModelConfig): { apiKey: string | null; apiBase: string | null; }; /** * Resolve the global LLM API key for the matching transport from environment * variables only (no config file cascade). * * Used internally by `clientForModelConfig` as a last-resort key source when * the ModelConfig carries no explicit key. New callers should prefer * `resolveCredentials(transport)` for the full 5-tier resolution chain. * * @param transport - The provider transport to look up. * @returns The environment variable value, or null. */ export declare function defaultTransportApiKey(transport: ModelTransport): string | null; //# sourceMappingURL=credentials.d.ts.map