feat: environment adapter is the single owner of process.env reads (E00-S04-T04)
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "EPPP configuration service. Owns the TypeBox/Ajv configuration schema (E00-S04-T01), the field-specific startup error for a missing required setting (E00-S04-T02) and the secret redaction layer (E00-S04-T03); the environment adapter (E00-S04-T04) and the .env.example template (E00-S04-T05) land in later tasks.",
|
||||
"description": "EPPP configuration service. Owns the TypeBox/Ajv configuration schema (E00-S04-T01), the field-specific startup error for a missing required setting (E00-S04-T02), the secret redaction layer (E00-S04-T03) and the environment adapter — the single owner of process.env reads (E00-S04-T04); the .env.example template (E00-S04-T05) lands in a later task.",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* EPPP configuration environment adapter — [E00-S04-T04] no module reads
|
||||
* `process.env` except the configuration adapter.
|
||||
*
|
||||
* This module is the workspace's single owner of `process.env` reads.
|
||||
* `loadConfigFromEnv` maps the environment onto the validated config shape
|
||||
* (the E00-S04-T01 TypeBox/Ajv schema) and validates it with
|
||||
* `assertValidConfig` (the E00-S04-T02 startup validation) before returning
|
||||
* it, so every setting the application uses — `host`, `port`, `databaseUrl`,
|
||||
* `sessionSecret` — flows through the adapter and no other module reads
|
||||
* `process.env` directly (the issue's acceptance criteria: "no module reads
|
||||
* `process.env` directly except the configuration adapter", "all settings
|
||||
* flow through the adapter").
|
||||
*
|
||||
* The environment mapping (per the schema's documented environment sources,
|
||||
* `schema.ts`):
|
||||
*
|
||||
* - `HOST` → `host` — the interface the HTTP server binds, default `0.0.0.0`
|
||||
* (the container default).
|
||||
* - `PORT` → `port` — integer in the valid TCP port range (1–65535), default
|
||||
* `3000` (the Dockerfile `EXPOSE 3000` / compose `:3000` container port). A
|
||||
* non-numeric or out-of-range override falls back to the default so a bad
|
||||
* `PORT` cannot crash the process at startup (the behavior the server's
|
||||
* entrypoint had before this adapter existed).
|
||||
* - `DATABASE_URL` → `databaseUrl` — optional; when absent the app has no
|
||||
* startup migration run to wait for and reports ready immediately (the
|
||||
* local non-container developer path, E00-S01-T06/E00-S03-T06).
|
||||
* - `EPPP_SESSION_SECRET` → `sessionSecret` — the required admin-session
|
||||
* secret (Security-and-Operations §32/§26, ≥ 32 characters); a missing or
|
||||
* too-short value fails startup with the field-specific error from
|
||||
* `assertValidConfig`.
|
||||
*
|
||||
* `assertValidConfig` is what makes a missing required setting a startup
|
||||
* error: the adapter hands the mapped value to it, and it throws
|
||||
* `MissingRequiredSettingError` naming the missing field — the application
|
||||
* never boots with an invalid configuration.
|
||||
*
|
||||
* Rollback note from the issue: revert any module changes that read
|
||||
* `process.env`.
|
||||
*/
|
||||
|
||||
import { assertValidConfig } from './startup.js';
|
||||
import type { Config } from './schema.js';
|
||||
|
||||
/**
|
||||
* Resolves the listen port from `PORT` (default 3000, matching the Dockerfile
|
||||
* `EXPOSE 3000` and the compose `:3000` container port). A non-numeric or
|
||||
* out-of-range override falls back to the default so a bad `PORT` value cannot
|
||||
* crash the process at startup.
|
||||
*/
|
||||
function resolvePort(raw: string | undefined): number {
|
||||
const port = Number(raw ?? 3000);
|
||||
return Number.isInteger(port) && port > 0 && port <= 65535 ? port : 3000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the environment onto the validated configuration — the E00-S04-T04
|
||||
* environment adapter.
|
||||
*
|
||||
* Reads every setting from the given environment (defaulting to `process.env`
|
||||
* — this module is the workspace's single owner of `process.env` reads) and
|
||||
* returns the validated `Config`; an invalid environment fails fast with the
|
||||
* field-specific startup error, so a missing or malformed setting is a
|
||||
* startup error, never a silently-booted invalid configuration.
|
||||
*
|
||||
* @param env - the environment to read (defaults to `process.env`)
|
||||
* @returns the validated configuration
|
||||
* @throws {MissingRequiredSettingError} when a required setting is missing
|
||||
* @throws {ConfigStartupError} when the configuration violates the schema
|
||||
*/
|
||||
export function loadConfigFromEnv(env: NodeJS.ProcessEnv = process.env): Config {
|
||||
return assertValidConfig({
|
||||
host: env.HOST ?? '0.0.0.0',
|
||||
port: resolvePort(env.PORT),
|
||||
databaseUrl: env.DATABASE_URL,
|
||||
sessionSecret: env.EPPP_SESSION_SECRET,
|
||||
});
|
||||
}
|
||||
@@ -18,8 +18,13 @@
|
||||
* server's redacting logger applies to every log line, so secrets
|
||||
* automatically redact from logs.
|
||||
*
|
||||
* The environment adapter (E00-S04-T04) builds on this boundary in a later
|
||||
* task.
|
||||
* [E00-S04-T04] Environment adapter: the boundary also exposes
|
||||
* `loadConfigFromEnv` — the workspace's single owner of `process.env` reads.
|
||||
* It maps the environment (`HOST`/`PORT`/`DATABASE_URL`/`EPPP_SESSION_SECRET`)
|
||||
* onto the validated config shape and validates it with `assertValidConfig`
|
||||
* at startup, so every setting flows through the adapter and no other module
|
||||
* reads `process.env` directly. The `.env.example` template (E00-S04-T05)
|
||||
* builds on this boundary in a later task.
|
||||
*/
|
||||
|
||||
export { configSchema } from './schema.js';
|
||||
@@ -28,3 +33,4 @@ export { validateConfig } from './validate.js';
|
||||
export type { ConfigValidationResult } from './validate.js';
|
||||
export { assertValidConfig, ConfigStartupError, MissingRequiredSettingError } from './startup.js';
|
||||
export { REDACTED, SECRET_FIELD_NAMES, redactConfig, redactText } from './redact.js';
|
||||
export { loadConfigFromEnv } from './env.js';
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* logs.
|
||||
*
|
||||
* The config package owns which configuration fields are secrets, so the
|
||||
* redaction layer lives here (the environment adapter, E00-S04-T04, will feed
|
||||
* redaction layer lives here (the environment adapter, E00-S04-T04, feeds
|
||||
* the validated config into it via the app's logger):
|
||||
*
|
||||
* - `redactConfig(config)` — a copy of a config value with every secret
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
* - `port` — the port the HTTP server listens on. Integer in the valid TCP
|
||||
* port range (1–65535), default `3000` (the container default, matching
|
||||
* the Dockerfile `EXPOSE 3000` and the compose `:3000` container port;
|
||||
* `PORT` is read today, E00-S02-T03).
|
||||
* `PORT` is read by the environment adapter, E00-S04-T04).
|
||||
* - `databaseUrl` — the PostgreSQL connection string (the `pg` `Pool`
|
||||
* `connectionString`, E00-S03-T02). Optional: when absent the app has no
|
||||
* startup migration run to wait for and reports ready immediately (the
|
||||
|
||||
@@ -12,9 +12,10 @@
|
||||
* `ConfigStartupError` whose message names each violating field too.
|
||||
*
|
||||
* This is deliberately NOT the T04 environment adapter: nothing here reads
|
||||
* `process.env`. The adapter (E00-S04-T04) maps the environment onto the
|
||||
* validated config shape and passes it to `assertValidConfig` at startup;
|
||||
* secret redaction (E00-S04-T03) builds on the same boundary in a later task.
|
||||
* `process.env`. The adapter (E00-S04-T04, `env.ts`) maps the environment
|
||||
* onto the validated config shape and passes it to `assertValidConfig` at
|
||||
* startup; the secret redaction layer (E00-S04-T03) builds on the same
|
||||
* boundary.
|
||||
*
|
||||
* Rollback note from the issue: revert the validation error handling.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user