> ## Documentation Index
> Fetch the complete documentation index at: https://growthx-chore-remove-output-wrapper.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# @outputai/credentials

> Encrypted secrets management with scoped access across global, environment, and workflow levels

AI apps need a lot of API keys. Sharing `.env` files is risky, and coding agents shouldn't see your secrets. The `@outputai/credentials` package keeps API keys, database passwords, and third-party tokens encrypted at rest in your repository. Secrets are stored as encrypted YAML files that you commit alongside your code — no more `.env` files drifting out of sync between developers, no more external vault subscriptions. At runtime, a single encryption key decrypts everything. Each workflow can have its own scoped credentials that override shared defaults, so you get both team-wide consistency and per-workflow flexibility.

You manage credentials through the [CLI](/packages/cli) and read them in your steps with a simple dot-notation API.

## What's in the Package

```typescript theme={null}
import {
  // Credential access
  credentials,

  // Encryption utilities
  encrypt, decrypt, generateKey,

  // Provider system
  setProvider, getProvider,
  encryptedYamlProvider,

  // Error types
  MissingCredentialError, MissingKeyError,

  // Path resolution
  resolveCredentialsPath, resolveKeyPath, resolveKeyEnvVar,
  resolveWorkflowCredentialsPath, resolveWorkflowKeyPath, resolveWorkflowKeyEnvVar,
  getNestedValue
} from '@outputai/credentials';
```

| Export                                            | Description                                                |
| ------------------------------------------------- | ---------------------------------------------------------- |
| `credentials`                                     | Read credentials via dot-notation paths (`get`, `require`) |
| `encrypt` / `decrypt` / `generateKey`             | Low-level AES-256-GCM encryption utilities                 |
| `setProvider` / `getProvider`                     | Swap the credential storage backend                        |
| `encryptedYamlProvider`                           | Default provider: encrypted YAML files on disk             |
| `MissingCredentialError`                          | Thrown by `credentials.require()` when a path has no value |
| `MissingKeyError`                                 | Thrown when no decryption key can be found                 |
| `resolveCredentialsPath` / `resolveKeyPath` / ... | Path resolution helpers for credentials and key files      |

## Quick Start

### 1. Initialize credentials

```bash theme={null}
output credentials init
```

This generates a random 256-bit encryption key at `config/credentials.key` and an encrypted YAML file at `config/credentials.yml.enc` with a starter template.

### 2. Add your secrets

```bash theme={null}
output credentials edit
```

This decrypts the file, opens it in `$EDITOR`, and re-encrypts on save. Fill in your secrets:

```yaml config/credentials.yml.enc (decrypted content) theme={null}
anthropic:
  api_key: sk-ant-xxxxx
openai:
  api_key: sk-xxxxx
stripe:
  secret_key: sk_live_xxxxx
```

### 3. Read credentials in a step

```typescript steps.ts theme={null}
import { step, z } from '@outputai/core';
import { credentials } from '@outputai/credentials';

export const callApi = step({
  name: 'callApi',
  inputSchema: z.object({ prompt: z.string() }),
  outputSchema: z.string(),
  fn: async (input) => {
    const apiKey = credentials.require('anthropic.api_key');
    // use apiKey to call the Anthropic API...
  }
});
```

## The credentials Object

The `credentials` object is the primary API for reading secrets at runtime.

| Method                                 | Arguments                                | Returns                                           | Throws                                |
| -------------------------------------- | ---------------------------------------- | ------------------------------------------------- | ------------------------------------- |
| `credentials.get(path, defaultValue?)` | `path: string`, `defaultValue?: unknown` | The credential value, the default, or `undefined` | Never                                 |
| `credentials.require(path)`            | `path: string`                           | The credential value                              | `MissingCredentialError` if not found |

The `path` argument uses dot-notation to navigate nested YAML. For example, `'anthropic.api_key'` reads the `api_key` field under the `anthropic` key.

```typescript theme={null}
// Safe — returns the default when missing
const region = credentials.get('aws.region', 'us-east-1');

// Strict — throws MissingCredentialError if the credential doesn't exist
const apiKey = credentials.require('anthropic.api_key');
```

Credentials are loaded and decrypted once per scope (global or per-workflow), then cached for the process lifetime.

## Credential Scopes

Credentials are organized into three levels. More specific scopes override broader ones.

**Global** — shared across all workflows:

| File                  | Path                         |
| --------------------- | ---------------------------- |
| Encrypted credentials | `config/credentials.yml.enc` |
| Encryption key        | `config/credentials.key`     |

**Environment-specific** — per `NODE_ENV` (`production` or `development`):

| File                  | Path                                       |
| --------------------- | ------------------------------------------ |
| Encrypted credentials | `config/credentials/{environment}.yml.enc` |
| Encryption key        | `config/credentials/{environment}.key`     |

**Per-workflow** — scoped to a single workflow:

| File                  | Path                                       |
| --------------------- | ------------------------------------------ |
| Encrypted credentials | `src/workflows/{name}/credentials.yml.enc` |
| Encryption key        | `src/workflows/{name}/credentials.key`     |

```text theme={null}
my-project/
├── config/
│   ├── credentials.yml.enc           # Global credentials (encrypted)
│   ├── credentials.key               # Global key (DO NOT commit)
│   └── credentials/
│       ├── production.yml.enc        # Production credentials
│       ├── production.key
│       ├── development.yml.enc       # Development credentials
│       └── development.key
└── src/workflows/
    └── payment_processing/
        ├── workflow.ts
        ├── steps.ts
        ├── credentials.yml.enc       # Workflow-specific credentials
        └── credentials.key           # Workflow-specific key
```

<Note>
  Environment is auto-detected from `NODE_ENV`. Only `"production"` and `"development"` are recognized — other values are ignored. If an environment-specific file doesn't exist, the system falls back to the default `config/credentials.yml.enc`.
</Note>

## Key Resolution

The system resolves decryption keys using a fallback chain:

**Global credentials:**

1. Environment variable `OUTPUT_CREDENTIALS_KEY` (or `OUTPUT_CREDENTIALS_KEY_{ENVIRONMENT}` for env-specific files)
2. Key file at `config/credentials.key` (or `config/credentials/{environment}.key`)
3. Throws `MissingKeyError` if neither is found

**Workflow credentials:**

1. Environment variable `OUTPUT_CREDENTIALS_KEY_{WORKFLOW_NAME}` (uppercased)
2. Key file at `src/workflows/{name}/credentials.key`
3. Falls back to the global key resolution chain

| Scope       | Environment Variable                     | Example                                               |
| ----------- | ---------------------------------------- | ----------------------------------------------------- |
| Global      | `OUTPUT_CREDENTIALS_KEY`                 | `OUTPUT_CREDENTIALS_KEY=a1b2c3...`                    |
| Environment | `OUTPUT_CREDENTIALS_KEY_{ENVIRONMENT}`   | `OUTPUT_CREDENTIALS_KEY_PRODUCTION=a1b2c3...`         |
| Workflow    | `OUTPUT_CREDENTIALS_KEY_{WORKFLOW_NAME}` | `OUTPUT_CREDENTIALS_KEY_PAYMENT_PROCESSING=a1b2c3...` |

<Tip>
  Use environment variables for keys in CI/CD and production deployments. Use key files for local development.
</Tip>

## Credential Merging

When a step runs inside a workflow that has its own credentials file, the workflow credentials are deep-merged over the global credentials. Workflow values override global values at the same path.

**Global credentials** (`config/credentials.yml.enc`):

```yaml theme={null}
anthropic:
  api_key: sk-ant-global
aws:
  region: us-east-1
  secret: aws-global-secret
```

**Workflow credentials** (`src/workflows/my_workflow/credentials.yml.enc`):

```yaml theme={null}
anthropic:
  api_key: sk-ant-workflow-specific
stripe:
  secret_key: sk_live_workflow
```

**Merged result** (what `credentials.get()` sees inside the workflow):

```yaml theme={null}
anthropic:
  api_key: sk-ant-workflow-specific   # Overridden by workflow
aws:
  region: us-east-1                   # From global
  secret: aws-global-secret           # From global
stripe:
  secret_key: sk_live_workflow        # Added by workflow
```

## CLI Commands

The CLI provides four commands for managing credentials. See [CLI](/packages/cli) for the full reference.

| Command                         | Description                                               |
| ------------------------------- | --------------------------------------------------------- |
| `output credentials init`       | Generate an encryption key and encrypted credentials file |
| `output credentials edit`       | Decrypt, open in `$EDITOR`, re-encrypt on save            |
| `output credentials show`       | Print decrypted credentials (for debugging)               |
| `output credentials get <path>` | Print a single credential value by dot-notation path      |

### Shared Flags

| Flag            | Short | Description                                            |
| --------------- | ----- | ------------------------------------------------------ |
| `--environment` | `-e`  | Target environment (e.g., `production`, `development`) |
| `--workflow`    | `-w`  | Target a specific workflow directory                   |
| `--force`       | `-f`  | Overwrite existing credentials (`init` only)           |

<Note>
  `--environment` and `--workflow` are mutually exclusive.
</Note>

### Examples

```bash theme={null}
# Initialize global credentials
output credentials init

# Initialize production credentials
output credentials init --environment production

# Initialize workflow-specific credentials
output credentials init --workflow payment_processing

# Edit global credentials in $EDITOR
output credentials edit

# Edit production credentials
output credentials edit -e production

# Show decrypted credentials (debugging)
output credentials show

# Get a specific credential value
output credentials get anthropic.api_key

# Get a workflow-specific credential
output credentials get stripe.key --workflow payment_processing
```

## Environment Variable Convention

Many libraries read their API keys from environment variables. Rather than duplicating secrets between your `.env` file and encrypted credentials, you can wire credentials directly into environment variables using the `credential:` prefix.

In your `.env` file, set any environment variable value to `credential:<dot.path>`:

```bash theme={null}
ANTHROPIC_API_KEY=credential:anthropic.api_key
OPENAI_API_KEY=credential:openai.api_key
```

When the worker starts, these references are resolved before any workflow code runs. `ANTHROPIC_API_KEY` becomes the actual decrypted value from `config/credentials.yml.enc`, so LLM SDKs and any library reading from `process.env` work without changes.

### How Resolution Works

1. Worker loads `.env` — `ANTHROPIC_API_KEY` is set to `"credential:anthropic.api_key"`
2. Worker reads `outputai.hookFiles` from `package.json` and imports each listed file at startup. Including `node_modules/@outputai/credentials/dist/hooks.js` registers the credential resolution hook:

```json package.json theme={null}
{
  "outputai": {
    "hookFiles": ["node_modules/@outputai/credentials/dist/hooks.js"]
  }
}
```

3. Worker calls `runStartupHooks()` — `resolveCredentialRefs()` scans `process.env` for `credential:` prefixed values and replaces each with the decrypted credential
4. `ANTHROPIC_API_KEY` is now the real API key string
5. LLM SDK reads it normally when a workflow activity runs

The worker logs the resolved variable names on startup:

```
Startup hooks resolved env vars {"vars":["ANTHROPIC_API_KEY","OPENAI_API_KEY"]}
```

### Precedence

Real environment variable values are never overwritten. If `ANTHROPIC_API_KEY` is already set to a non-`credential:` value (from the shell, CI secret injection, etc.), it is left unchanged. This lets you override credentials at deploy time without modifying files.

### The `_env` Section

New projects are scaffolded with an `_env` section in the credentials template that documents the intended mapping:

```yaml theme={null}
anthropic:
  api_key: sk-ant-...
openai:
  api_key: sk-...

_env:
  ANTHROPIC_API_KEY: anthropic.api_key
  OPENAI_API_KEY: openai.api_key
```

The `_env` section is metadata only — it documents the intent but doesn't drive resolution. Resolution is driven by the `credential:` values in `.env`. Keep both in sync when you add credentials.

### Programmatic Access

To call resolution outside the worker startup context:

```typescript theme={null}
import { resolveCredentialRefs } from '@outputai/credentials';

// Returns array of env var names that were resolved
const resolved = resolveCredentialRefs();
console.log('Resolved:', resolved);
// → ["ANTHROPIC_API_KEY", "OPENAI_API_KEY"]
```

## Security

The credentials system uses **AES-256-GCM** encryption via the `@noble/ciphers` library. Each encryption generates a unique random nonce, so the same plaintext produces different ciphertext every time.

<Warning>
  Never commit `.key` files to version control. Add `*.key` to your `.gitignore`. The encrypted `.yml.enc` files are safe to commit — they cannot be decrypted without the key.
</Warning>

Additional protections:

* Key files are created with file mode `0o600` (owner read/write only)
* During `credentials edit`, the temporary plaintext file is overwritten with null bytes before deletion

## Custom Providers

The default provider reads encrypted YAML files from disk. You can replace it with a custom provider for alternative backends (e.g., AWS Secrets Manager, HashiCorp Vault) using `setProvider()`:

```typescript theme={null}
import { setProvider } from '@outputai/credentials';
import type { CredentialsProvider } from '@outputai/credentials';

const vaultProvider: CredentialsProvider = {
  loadGlobal: ({ environment }) => {
    // Return credentials as a plain object
    return { anthropic: { api_key: fetchFromVault('anthropic-key') } };
  },
  loadForWorkflow: ({ workflowName, workflowDir, environment }) => {
    // Return workflow-specific credentials, or null to use global only
    return null;
  }
};

setProvider(vaultProvider);
```

| Method                     | Arguments                                                                          | Returns                           |
| -------------------------- | ---------------------------------------------------------------------------------- | --------------------------------- |
| `loadGlobal(context)`      | `{ environment: string \| undefined }`                                             | `Record<string, unknown>`         |
| `loadForWorkflow(context)` | `{ workflowName: string, workflowDir: string \| undefined, environment?: string }` | `Record<string, unknown> \| null` |

Returning `null` from `loadForWorkflow` means the workflow has no overrides — global credentials are used as-is.

## API Reference

For complete TypeScript API documentation, see the [Credentials Module API Reference](https://output-ai-reference-code-docs.onrender.com/modules/credentials_src.html).
