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(); // tokenConstructors
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;