config

js/config.ts

fino:config — explicit ordered config loading over fino:validate.

Config loading is intentionally explicit. Callers provide a sources list, and that list is both the set of enabled source types and the precedence order. Earlier sources are lower precedence; later sources override them.

The final merged value is validated through fino:validate, so config can use fluent builders or raw JSON Schema loaded from disk. Environment and argv sources produce strings by default, then the loader coerces scalar values according to the schema before validating.

import { loadConfig } from 'fino:config';
import { v } from 'fino:validate';

const loaded = await loadConfig({
  schema: v.object({
    server: v.object({ port: v.integer().default(3000) }),
  }),
  sources: [
    { type: 'defaults', value: { server: { port: 3000 } } },
    { type: 'file', path: './app.toml' },
    { type: 'env', prefix: 'APP_' },
    { type: 'argv', args: ['--server.port', '8080'] },
  ],
});

loaded.value.server.port; // 8080

JSON Schema specification: https://json-schema.org/specification

Types

type ConfigValue = Record<string, unknown>

Plain object value produced by config sources before schema validation.

Config sources merge object-shaped data. Nested values may be strings, numbers, booleans, arrays, nulls, or other objects at runtime, but the top-level source value must be an object so precedence merging can apply.

type ConfigSource = | { type: 'defaults'; value: ConfigValue } | { type: 'file'; path: string; format?: 'json' | 'toml' } | { type: 'dotenv'; path: string; map?: Record<string, string>; prefix?: string } | { type: 'env'; values?: Record<string, string>; map?: Record<string, string>; prefix?: string } | { type: 'secret-env'; values?: Record<string, string>; map?: Record<string, string>; prefix?: string; } | { type: 'secret-file'; path: string; key: BufferLike; format?: 'json' | 'toml' } | { type: 'argv'; args?: string[]; map?: Record<string, string> } | { type: 'override'; value: ConfigValue }

One config input source.

Sources are loaded in array order. The merged result from each source overrides values from all earlier sources.

Each union arm is selected by its type field. File-backed sources throw when the file cannot be read or parsed. Env-like sources produce string values first; loadConfig() performs schema-guided scalar coercion before validation.

import { loadConfig } from 'fino:config';

const loaded = await loadConfig({
  schema: { type: 'object' },
  sources: [
    { type: 'defaults', value: { server: { port: 3000 } } },
    { type: 'env', values: { APP_SERVER_PORT: '8080' }, prefix: 'APP_' },
  ],
});
loaded.value;

Classes

class SecretValue<T = unknown> {

A value tagged as secret after config validation.

Rendering a secret through strings, JSON, or template interpolation produces [redacted]. Call reveal() only at the boundary that consumes the secret.

import { SecretValue } from 'fino:config';

const token = new SecretValue('token');
String(token); // [redacted]
token.reveal(); // token

Constructors

constructor(value: T)

Tag a validated value as secret.

Methods

reveal(): T

Return the underlying secret value.

Keep this call close to the API that needs the plaintext so logs and telemetry continue to receive the redacting wrapper by default.

toString(): string

Render a redaction marker instead of the secret.

toJSON(): string

Serialize a redaction marker instead of the secret.

class ConfigSecretProvider implements SecretProvider {

Expose tagged values from loaded config through the SecretProvider seam.

Only values already tagged as SecretValue are returned.

Constructors

constructor(config: LoadedConfig)

Create a provider over one loaded config result.

Methods

async get(name: string): Promise<SecretValue | undefined>

Resolve one dotted config path when it contains a tagged secret.

class SecretGrant {

An exact allowlist granting selected provider secrets to a consumer.

The grant can be used directly or converted to a Realm Facade. The facade reveals plaintext only after its name passes the grant allowlist, avoiding ambient process environment access in the child.

Constructors

constructor(provider: SecretProvider, names: Iterable<string>)

Create an exact-name grant over a provider.

Methods

async get(name: string): Promise<SecretValue>

Resolve a granted secret.

Ungranted and missing names reject without disclosing any secret value.

facade(specifier = 'fino:config/secrets'): Facade

Create a synthetic module exposing get(name) to a child Realm.

The child receives only the revealed value of an explicitly granted name.

class ConfigError extends Error {

Error thrown when loading or validating config fails.

Validation failures include redacted ValidationIssue objects in issues. Source parsing and unknown-source errors may throw ConfigError without issues.

import { ConfigError, loadConfig } from 'fino:config';

try {
  await loadConfig({
    schema: { type: 'object', required: ['port'] },
    sources: [{ type: 'defaults', value: {} }],
  });
} catch (error) {
  if (error instanceof ConfigError) error.issues;
}

Properties

issues: ValidationIssue[]

Validation issues when the failure came from fino:validate.

This array is empty for loader errors that are not validation failures. Received values at secret paths are replaced with [redacted].

import { ConfigError, loadConfig } from 'fino:config';

try {
  await loadConfig({
    schema: { type: 'object', required: ['port'] },
    sources: [{ type: 'defaults', value: {} }],
  });
} catch (error) {
  if (error instanceof ConfigError) error.issues.map((issue) => String(issue));
}

Constructors

constructor(message: string, issues: ValidationIssue[] = [])

Create a config error.

When the failure came from validation, pass the ValidationIssue list as the second argument; it defaults to an empty array. The name property is set to 'ConfigError' for callers that distinguish config failures from other exceptions.

import { ConfigError } from 'fino:config';

throw new ConfigError('Invalid config');

Interfaces

interface SecretProvider {

Provider seam used to resolve secrets by deployment-defined name.

A local config provider, cluster secret store, or managed secret service can implement this interface without changing realm grant code.

Methods

get(name: string): Promise<SecretValue | undefined>

Resolve one named secret, or return undefined when it does not exist.

interface LoadConfigOptions<T = unknown> {

Options for loadConfig().

Provide a validation schema and the ordered list of enabled sources. The loader rejects with ConfigError when validation fails.

import { loadConfig } from 'fino:config';

const loaded = await loadConfig({
  schema: { type: 'object' },
  sources: [{ type: 'defaults', value: {} }],
});
loaded.sources;

Properties

schema: unknown

Fluent builder or raw JSON Schema object used for final validation.

Builders with toJSON() are converted before scalar coercion. Schema defaults are applied by fino:validate when supported by the provided schema.

import { loadConfig } from 'fino:config';

await loadConfig({
  schema: {
    type: 'object',
    properties: { port: { type: 'integer' } },
  },
  sources: [{ type: 'defaults', value: { port: 3000 } }],
});
sources: ConfigSource[]

Ordered source list. Later entries override earlier entries.

The array also controls which source types are enabled; there is no implicit loading from files, env, or argv.

import { loadConfig } from 'fino:config';

const loaded = await loadConfig({
  schema: { type: 'object' },
  sources: [
    { type: 'defaults', value: { debug: false } },
    { type: 'override', value: { debug: true } },
  ],
});
loaded.value;
secrets?: string[]

Config paths whose values should be tagged and redacted.

Paths use the same dotted form as source mappings. Validated values become SecretValue instances, and validation messages plus structured ConfigError.issues replace received values with [redacted].

import { ConfigError, loadConfig } from 'fino:config';

try {
  await loadConfig({
    schema: { type: 'object', required: ['database'] },
    sources: [{ type: 'defaults', value: { database: { password: 'secret' } } }],
    secrets: ['database.password'],
  });
} catch (error) {
  if (error instanceof ConfigError) error.message;
}

interface ConfigSourceReport {

Metadata describing values loaded from a source.

Reports are returned in the same order as the input sources array. They include source type, optional file path, and flattened keys produced by the source after mapping.

import { loadConfig } from 'fino:config';

const loaded = await loadConfig({
  schema: { type: 'object' },
  sources: [{ type: 'env', values: { APP_SERVER_PORT: '8080' }, prefix: 'APP_' }],
});
loaded.sources[0].keys;

Properties

type: string

Source type, matching the input source discriminant.

This is a string so reports can describe future source types without a type change.

import { loadConfig } from 'fino:config';

const loaded = await loadConfig({
  schema: { type: 'object' },
  sources: [{ type: 'env', values: { APP_DEBUG: 'true' }, prefix: 'APP_' }],
});
loaded.sources.find((source) => source.type === 'env');
path?: string

File path for file-backed sources.

This is present for file and dotenv reports and omitted for inline, environment, and argv sources.

import { loadConfig } from 'fino:config';

const loaded = await loadConfig({
  schema: { type: 'object' },
  sources: [{ type: 'file', path: './app.toml' }],
});
loaded.sources[0].path;
keys: string[]

Flattened config paths produced by this source.

Nested objects are represented as dotted paths. Empty sources produce an empty array.

import { loadConfig } from 'fino:config';

const loaded = await loadConfig({
  schema: { type: 'object' },
  sources: [{ type: 'defaults', value: { server: { port: 3000 } } }],
});
loaded.sources[0].keys.includes('server.port');

interface LoadedConfig<T = unknown> {

Result returned from loadConfig().

The value has already been merged, coerced, and validated. Source reports describe what each source contributed.

import { loadConfig } from 'fino:config';

const loaded = await loadConfig<{ debug: boolean }>({
  schema: { type: 'object', properties: { debug: { type: 'boolean' } } },
  sources: [{ type: 'override', value: { debug: true } }],
});
loaded.value.debug;

Properties

value: T

Validated config value, including defaults applied by the schema.

Its type is the generic T supplied to loadConfig<T>().

import { loadConfig } from 'fino:config';

const loaded = await loadConfig<{ port: number }>({
  schema: { type: 'object', properties: { port: { type: 'integer' } } },
  sources: [{ type: 'defaults', value: { port: 3000 } }],
});
loaded.value.port;
sources: ConfigSourceReport[]

Per-source load metadata in the same order as sources.

Reports are useful for diagnostics and for explaining where configuration came from. They do not include secret redaction metadata.

import { loadConfig } from 'fino:config';

const loaded = await loadConfig({
  schema: { type: 'object' },
  sources: [
    { type: 'defaults', value: { port: 3000 } },
    { type: 'argv', args: ['--port', '8080'] },
  ],
});
loaded.sources.map((source) => source.type);

Methods

get(path: string): unknown

Read a dotted path from the validated config value.

Missing paths return undefined. Only plain objects are traversed: a path segment that lands on an array, scalar, or null resolves to undefined rather than throwing.

import { loadConfig } from 'fino:config';

const loaded = await loadConfig({
  schema: { type: 'object' },
  sources: [{ type: 'defaults', value: { server: { port: 3000 } } }],
});
loaded.get('server.port');

Functions

async function loadConfig<T = unknown>( options: LoadConfigOptions<T>, ): Promise<LoadedConfig<T>>

Load, merge, coerce, and validate config from an explicit source list.

Source order is the precedence model: later sources override earlier sources. The returned value is the post-validation object.

The loader reads each source in order, deep-merges object values, performs conservative schema-guided coercion for strings from env and argv sources, and validates through fino:validate. Validation failures throw ConfigError with issues; file parse errors and unsupported sources also reject. There are no implicit defaults beyond what you provide in sources or the validation schema. The resolved LoadedConfig carries the validated value, per-source reports, and a dotted-path get() accessor.

import { loadConfig } from 'fino:config';

const loaded = await loadConfig<{ server: { port: number } }>({
  schema: {
    type: 'object',
    properties: {
      server: {
        type: 'object',
        properties: { port: { type: 'integer' } },
      },
    },
  },
  sources: [
    { type: 'defaults', value: { server: { port: 3000 } } },
    { type: 'argv', args: ['--server.port', '8080'] },
  ],
});
loaded.value.server.port;