From 7aeeeaaa973d857e555791aa1bbca9b02f370fb9 Mon Sep 17 00:00:00 2001 From: implementer Date: Sun, 30 Aug 2026 04:17:19 +0000 Subject: [PATCH] feat: environment adapter is the single owner of process.env reads (E00-S04-T04) --- packages/config/package.json | 2 +- packages/config/src/env.ts | 78 ++++++++++++++++++++++++++++++++++ packages/config/src/index.ts | 10 ++++- packages/config/src/redact.ts | 2 +- packages/config/src/schema.ts | 2 +- packages/config/src/startup.ts | 7 +-- 6 files changed, 93 insertions(+), 8 deletions(-) create mode 100644 packages/config/src/env.ts diff --git a/packages/config/package.json b/packages/config/package.json index 5ccdfc4..49c69c2 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -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" diff --git a/packages/config/src/env.ts b/packages/config/src/env.ts new file mode 100644 index 0000000..bca077c --- /dev/null +++ b/packages/config/src/env.ts @@ -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, + }); +} diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index 2e244e5..dce9fcc 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -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'; diff --git a/packages/config/src/redact.ts b/packages/config/src/redact.ts index 302062a..a85fc4c 100644 --- a/packages/config/src/redact.ts +++ b/packages/config/src/redact.ts @@ -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 diff --git a/packages/config/src/schema.ts b/packages/config/src/schema.ts index e07b6e4..5327cc5 100644 --- a/packages/config/src/schema.ts +++ b/packages/config/src/schema.ts @@ -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 diff --git a/packages/config/src/startup.ts b/packages/config/src/startup.ts index f4953c2..92e07d2 100644 --- a/packages/config/src/startup.ts +++ b/packages/config/src/startup.ts @@ -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. */