Docs/configurationv1.0.0-rc.1

Secret Providers

Secret providers fetch sensitive configuration values from external sources and inject them into the secrets layer, which sits above environment variables and manifests in the precedence order. Fields marked sensitive: true in their @AIEnv declaration are resolved exclusively through secret providers.

Key Concepts

  • Separation of concerns — secret providers keep credentials out of environment variables and source-controlled files.
  • Composable — CompositeSecretProvider chains multiple providers; the first one that returns a non-undefined value wins.
  • Automatic redaction — EnvironmentConfigManager calls provider.getSecret(key) for each sensitive field but stores only the redacted form in layers and provenance output.

SecretProvider Interface

Every provider implements this interface:

interface SecretProvider {
  name: string;
  getSecret(key: string): Promise<string | undefined> | string | undefined;
  describe(): string;   // Human-readable description used in provenance output
}

The key passed to getSecret is the environment variable name computed for the field (e.g., DATABASE_PASSWORD). Providers are free to transform the key before looking it up.

Built-in Providers

EnvSecretProvider

Reads secrets from process.env. An optional prefix is prepended to every key lookup.

import { EnvSecretProvider } from '@tagna/udiot';

// No prefix: looks up DATABASE_PASSWORD directly
const plain = new EnvSecretProvider();

// With prefix: looks up SECRET_DATABASE_PASSWORD
const prefixed = new EnvSecretProvider('SECRET');

Constructor

new EnvSecretProvider(prefix?: string)
Parameter Type Default Description
prefix string '' Prepended to every key after normalisation

Key normalisation: dots and hyphens are replaced with underscores, then the string is upper-cased. If a prefix is provided, it is prepended with an underscore separator.

FileSecretProvider

Reads secrets from a JSON file on disk. The file must export a flat Record<string, string>. Parsed content is cached in memory after the first read.

import { FileSecretProvider } from '@tagna/udiot';

const provider = new FileSecretProvider('/run/secrets/app-secrets.json');

Constructor

new FileSecretProvider(filePath: string)
Parameter Type Description
filePath string Absolute path to the JSON secrets file

File format

{
  "DATABASE_PASSWORD": "super-secret",
  "API_KEY": "abc123"
}

If the file does not exist the provider silently returns undefined for all keys. If the file exists but cannot be parsed as JSON, an error is thrown.

CompositeSecretProvider

Tries a list of providers in order and returns the first non-undefined value. Useful for layering a high-security source over a fallback.

import { CompositeSecretProvider, FileSecretProvider, EnvSecretProvider } from '@tagna/udiot';

const provider = new CompositeSecretProvider([
  new FileSecretProvider('/run/secrets/app-secrets.json'),
  new EnvSecretProvider('SECRET'),
]);

Constructor

new CompositeSecretProvider(providers: SecretProvider[])

The describe() method returns a comma-separated list of each child provider's description, which appears in provenance output.

Default Secret Provider

When no secretProvider is supplied to EnvironmentConfigManager, a default is created automatically:

// Equivalent to what the manager constructs internally
import { createDefaultSecretProvider } from '@tagna/udiot';

const provider = createDefaultSecretProvider(process.cwd());
// → CompositeSecretProvider([
//     EnvSecretProvider('SECRET'),       // reads SECRET_* env vars
//     FileSecretProvider('{root}/config/secrets.json'),
//   ])

API Reference

Export Description
SecretProvider Interface for all providers
EnvSecretProvider Reads from process.env with optional prefix
FileSecretProvider Reads from a JSON file; caches on first load
CompositeSecretProvider Tries multiple providers in order
createDefaultSecretProvider(rootDir) Factory for the built-in composite default

Basic Usage Example

import 'reflect-metadata';
import {
  AIConfig,
  AIEnv,
  buildConfigSchema,
  EnvironmentConfigManager,
  CompositeSecretProvider,
  FileSecretProvider,
  EnvSecretProvider,
} from '@tagna/udiot';

@AIConfig({ namespace: 'database', secrets: 'auto' })
class DatabaseConfig {
  @AIEnv({ required: true })
  host!: string;

  @AIEnv({ sensitive: true, required: true })
  password!: string;
}

const secretProvider = new CompositeSecretProvider([
  new FileSecretProvider('/run/secrets/db.json'),
  new EnvSecretProvider('SECRET'),
]);

const manager = new EnvironmentConfigManager({
  schemas: [buildConfigSchema(DatabaseConfig)],
  environment: 'production',
  workspaceRoot: process.cwd(),
  secretProvider,
});

const result = await manager.load();
// result.values['database'].password is the actual secret value
// result.provenance['database.password'][*].value is '[REDACTED]'

Common Patterns

Docker secrets (file-based)

Docker Swarm and Kubernetes both mount secrets as files under /run/secrets/. The file name is the secret name, but FileSecretProvider expects a single JSON file with all secrets. Wrap the individual files in a sidecar or init container that combines them:

{
  "DATABASE_PASSWORD": "<contents of /run/secrets/db_password>",
  "API_KEY": "<contents of /run/secrets/api_key>"
}

Then:

const provider = new FileSecretProvider('/run/secrets/combined.json');

Custom provider (e.g., HashiCorp Vault)

import { SecretProvider } from '@tagna/udiot';

class VaultSecretProvider implements SecretProvider {
  readonly name = 'vault';

  constructor(private readonly client: VaultClient, private readonly path: string) {}

  async getSecret(key: string): Promise<string | undefined> {
    const secret = await this.client.read(`${this.path}/${key.toLowerCase()}`);
    return secret?.data?.value;
  }

  describe(): string {
    return `HashiCorp Vault (${this.path})`;
  }
}

Security Notes

  • Sensitive values are never logged. EnvironmentConfigManager calls redactValue(value, true) before writing to layer snapshots and provenance entries. The raw value only exists in ConfigLoadResult.values.
  • File caching. FileSecretProvider caches the parsed file in memory on the first read. Secrets rotated on disk without restarting the process are not picked up. For high-rotation secrets use a provider that fetches on each call.
  • Prefix collisions. When using EnvSecretProvider with a prefix alongside a non-prefixed EnvSecretProvider in the same CompositeSecretProvider, ensure the prefix does not accidentally match legitimate application environment variables.
  • Secrets file permissions. The file read by FileSecretProvider should have 600 permissions (readable only by the process owner). UDIOT does not enforce this; it is the operator's responsibility.

Pitfalls

  • secrets: 'manual' on @AIConfig tells the manager not to automatically fetch secrets for fields in that schema. You are responsible for providing values another way (e.g., via overrides).
  • The provider field on @AIEnv is stored in the schema but not yet used for provider dispatch by the built-in manager. It is available for custom tooling and future versions.
  • If FileSecretProvider receives a file path that does not exist at startup, it silently returns undefined. Missing secrets only become visible as missingRequired violations for fields marked required: true.
From the canonical UDIOT public documentation.Try it in the playground ↗