# Credentials and redaction

## What it does

Prism provides small helpers for host-owned credentials and known-secret redaction:

- `resolveCredentialValue()`: resolves a credential from a direct string, callback, or `CredentialResolver`.
- `createExplicitCredentialResolver()`: tries named resolver sources in caller-provided order, such as runtime override → stored → env object → fallback.
- `createEnvCredentialResolver()`: reads only a caller-supplied env-like object and map.
- `refreshOAuthCredential()`: calls a provider OAuth refresh function and writes the result to a caller-owned store when supplied.
- `revokeOAuthCredential()`: best-effort upstream revocation (`OAuthProvider.revoke?`) followed by a mandatory caller-owned store delete, so a revoked token fails closed locally even if the provider has no revocation endpoint.
- `CredentialValueSource`: the accepted source type for `resolveCredentialValue()`.
- `redactSecrets()`: replaces known secret string values inside strings, arrays, and plain objects.
- `errorToErrorInfo()`: converts unknown errors into `ErrorInfo` and redacts known secret values from error text.

These helpers do not persist credentials, scan environment variables, execute commands, or load settings.

## When to use it

Use these helpers inside provider adapters or host integration code that needs to resolve a credential at request time and prevent known secret values from appearing in emitted errors or logs.

Do not use them as a credential manager, general secret scanner, vault, settings loader, or permission system.

## Inputs / request

```ts
resolveCredentialValue(
  source: CredentialValueSource | undefined,
  request: CredentialRequest,
): Promise<string | undefined>
```

`CredentialValueSource` can be:

| Source | Behavior |
| --- | --- |
| `string` | Returned directly. |
| `() => string | undefined | Promise<string | undefined>` | Called when a credential is needed. |
| `CredentialResolver` | `resolve(request)` is called and `.value` is returned. |

```ts
createExplicitCredentialResolver(sources: readonly CredentialResolverSource[]): CredentialResolver
createEnvCredentialResolver(env: Readonly<Record<string, string | undefined>>, map: Readonly<Record<string, string>>): CredentialResolver
refreshOAuthCredential(options: { provider: OAuthProvider; credentials: OAuthCredentials; store?: OAuthCredentialStore }): Promise<OAuthCredentials>
revokeOAuthCredential(options: { provider: OAuthProvider; credentials: OAuthCredentials; store?: RevocableOAuthCredentialStore }): Promise<void>
redactSecrets<T>(value: T, secrets: readonly (string | undefined)[]): T
errorToErrorInfo(error: unknown, secrets?: readonly (string | undefined)[]): ErrorInfo
```

`secrets` must be the exact values to redact. Undefined and empty values are ignored.

## Outputs / response / events

- `resolveCredentialValue()` returns a credential string or `undefined`.
- `redactSecrets()` returns the same value shape with known string secrets replaced by `[REDACTED]`.
- `errorToErrorInfo()` returns `{ name?, message, code?, cause? }` with known secret values removed from message/cause text.

## Request/response example

```json
{
  "request": { "name": "apiKey", "provider": "demo" },
  "resolved": "<host-owned credential value>",
  "redactedError": { "message": "bad key [REDACTED]" }
}
```

## Implementation example

```ts
import {
  createEnvCredentialResolver,
  createExplicitCredentialResolver,
  errorToErrorInfo,
  redactSecrets,
  resolveCredentialValue,
} from "@arnilo/prism";

const runtime = { resolve: () => undefined };
const stored = { resolve: () => undefined };
const env = createEnvCredentialResolver({ DEMO_API_KEY: "fake-demo-key" }, { demo: "DEMO_API_KEY" });
const resolver = createExplicitCredentialResolver([
  { name: "runtime", resolver: runtime },
  { name: "stored", resolver: stored },
  { name: "env", resolver: env },
]);

const apiKey = await resolveCredentialValue(resolver, { name: "apiKey", provider: "demo" });

const message = redactSecrets(`request failed for ${apiKey}`, [apiKey]);
const error = errorToErrorInfo(new Error(`bad credential ${apiKey}`), [apiKey]);

console.log(message);
console.log(error.message);
```

## Extension and configuration notes

- Hosts and extension packages can implement `CredentialResolver` and pass it explicitly to code that needs credentials.
- Credentials stay host-owned outside `AgentConfig`. `createAgent()` / `session.run()` do not call `credentials.resolve()`. Provider adapters, compaction workers, or request policies should receive and resolve credentials at the provider edge.
- Use `createExplicitCredentialResolver()` when documenting a fixed order such as runtime override, stored credential, caller-provided env object, then fallback resolver.
- Use `createEnvCredentialResolver()` only with an object supplied by the host; Prism does not read `process.env` for you.
- Provider adapters should resolve credentials as late as possible, per request.
- Keep resolved credential values local to the request path. Do not put them in registries, model configs, messages, provider events, agent events, session entries, compaction summaries, or logs.
- Future settings/config loaders may provide credential resolver instances, but core helpers remain storage-free.

### Subscription OAuth eligibility

First-party subscription OAuth is explicit and host-invoked: OpenAI Codex (`createOpenAICodexOAuthProvider()`) and xAI SuperGrok / X Premium (`createXaiOAuthProvider()`). Hosts own login UI and may use `createOAuthCredentialStoreAdapter()` for deliberately selected durable storage. Do not import `~/.grok` or grok-cli auth files.

Anthropic and Google provider packages are API-key-only. Do not scrape or import Claude Code/Gemini CLI credential files, setup tokens, environment values, or browser sessions, and do not route a user's Claude.ai/Gemini subscription through Prism. Anthropic states that developers building products must use Claude Console API keys or a supported cloud provider and may not offer Claude.ai login or route Free/Pro/Max credentials ([legal and compliance](https://docs.anthropic.com/en/docs/claude-code/legal-and-compliance)). Gemini CLI states that third-party software using its OAuth to access backend services violates applicable terms; its FAQ names Vertex AI or Google AI Studio API keys as the supported third-party path ([terms](https://github.com/google-gemini/gemini-cli/blob/main/docs/resources/tos-privacy.md), [FAQ](https://github.com/google-gemini/gemini-cli/blob/main/docs/resources/faq.md)).

A future provider-local OAuth adapter needs published permission for third-party products, documented authorize/token/refresh endpoints and scopes, PKCE/state where required, abort/expiry/bounded-response/redaction/store-round-trip fixtures, and legal review before registration. Until then, absence is intentional.

## Security and performance notes

- Redaction only removes exact known secret values passed to the helper. It is not a general-purpose secret detector.
- Do not pass empty strings as secrets; they are ignored.
- `redactSecrets()` recursively walks arrays and object entries, so avoid using it on huge objects unless needed.
- Cycle and non-JSON value handling: `redactSecrets()` is cycle-safe via an active-path `WeakSet`. Ancestor cycles render `"[Circular]"` at the back-reference instead of throwing; shared diamond references on separate branches stay structured (they are not collapsed to `"[Circular]"`). String object keys and `Map` keys are redacted like values, with deterministic `__2`/`__3`/… suffixes on collisions. `Date` and `RegExp` values are passed through unchanged; `ArrayBuffer` and typed arrays are passed through unchanged; `Map` is normalized to a plain object and `Set` to an array so the output stays JSON-compatible. `errorToErrorInfo()` tolerates a cyclic `error.cause` (rendered via `String()`). Symbols and non-enumerable properties remain outside JSON-shaped redaction.
- Use placeholders in tests and docs. Never commit real tokens.
- Live provider/worker tests are gated behind explicit environment variables and skipped by default: `PRISM_LIVE_PROVIDER_TESTS`, `PRISM_LIVE_COMPACTION_TESTS`, `PRISM_LIVE_OBSERVATIONAL_MEMORY_TESTS`. Default `npm test` is network-free; do not add ungated network calls to default tests.
- Credentials are not eagerly resolved by the core runtime, serialized into provider requests/events/stores, or passed to loops/compaction.
- `resolveCredentialValue()` and `createExplicitCredentialResolver()` do not cache values. Add host-side caching only if a real credential source needs it.
- `refreshOAuthCredential()` only calls the supplied OAuth provider and optional store; it has no built-in persistence or retry loop.
- OpenAI Codex device-code OAuth polls inside `createOpenAICodexOAuthProvider().login()` with bounded delays and abort support via `OAuthLoginCallbacks.signal`. Token-endpoint failures redact authorization codes, PKCE verifiers, device/user codes, and access/refresh tokens when those values are known.
- The shared bounded device/token flow lives in core `pollDeviceCodeToken` (0.2.1) and is used by the OpenAI Codex provider and the credentials-node OAuth 2.0 provider (Microsoft 365 / Google Workspace). It owns the RFC 8628 device-code request and poll loop (`authorization_pending` continue, `slow_down` +5s backoff, expiry deadline, abort), reads every response body under the shared byte ceiling, parses success bodies with a fail-closed shape gate (an `access_token` string is required), and redacts device/user codes, authorization codes, PKCE verifiers, and tokens from every thrown error. Adapter-specific fields (message prefix, extra token params, account binding) are plain options, never subclasses. Optional `bodyEncoding: "form"` POSTs `application/x-www-form-urlencoded` for both the device-code request and every token poll (default remains `json` so existing callers stay byte-compatible). `extraDeviceParams` merge into the device-code body only. `verification_uri` and optional `verification_uri_complete` must be `https:`; the complete URI is what `onDeviceCode` receives when present.

## Related APIs

- [Public contracts](public-contracts.md): `CredentialRequest`, `Credential`, `CredentialResolver`, `CredentialResolverSource`, `OAuthLoginCallbacks`, `OAuthCredentials`, `OAuthProvider`, and `ErrorInfo`.
- [Provider layer](provider-layer.md): `providerError()` uses `errorToErrorInfo()` for redacted provider error events.
- [LLM compaction package](compaction-llm.md): resolves optional summary-provider credentials per compaction call and redacts exact known values.
- [OpenAI-compatible provider](providers/openai-compatible.md): resolves API keys per request and redacts known values from adapter errors.

Phase 10 added `createMemoryCredentialStore()`, `createChainedCredentialResolver()`, and `createSecretRedactor()` for opt-in in-memory auth and runtime redaction. By default the memory store serves a providerless record for a provider-scoped request of the same name — that record is then shared across every provider; pass `{ allowProviderFallback: false }` for exact-match-only resolution (strict provider scoping). Phase 11 adds OAuth/API-key contracts plus explicit resolver order helpers. Core still has no persistent secret store and does not read environment variables or files for credentials. For durable storage, use [`@arnilo/prism-core/credentials/node`](credential-storage.md) encrypted-file or keychain backends. See [Security/auth/trust](settings-auth-trust-security.md).
