From 1214a33adbd5502c982c26761b5ec962a72a40a8 Mon Sep 17 00:00:00 2001 From: implementer Date: Sun, 30 Aug 2026 04:17:19 +0000 Subject: [PATCH] feat: server loads all settings through the config environment adapter (E00-S04-T04) --- apps/server/package.json | 2 +- apps/server/src/index.ts | 56 +++++++++++++++++----------------------- 2 files changed, 25 insertions(+), 33 deletions(-) diff --git a/apps/server/package.json b/apps/server/package.json index a0358fb..9a1d8c1 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "private": true, "type": "module", - "description": "EPPP public server application. Serves the application health endpoint (E00-S02-T03) gated on the startup migration run (E00-S03-T06), with a field-specific startup error when a required setting is missing (E00-S04-T02) and automatic secret redaction from all log output (E00-S04-T03); the Fastify 5 application shell lands in a later story.", + "description": "EPPP public server application. Serves the application health endpoint (E00-S02-T03) gated on the startup migration run (E00-S03-T06), with a field-specific startup error when a required setting is missing (E00-S04-T02), automatic secret redaction from all log output (E00-S04-T03) and all settings flowing through the config package's environment adapter — the server reads no process.env directly (E00-S04-T04); the Fastify 5 application shell lands in a later story.", "scripts": { "build": "pnpm --filter @personal-blog/config build && pnpm --filter @personal-blog/database-postgres build && tsc -p tsconfig.json", "typecheck": "pnpm --filter @personal-blog/config build && pnpm --filter @personal-blog/database-postgres build && tsc -p tsconfig.json --noEmit", diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 013b9dd..838562d 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -23,10 +23,17 @@ * setting (the admin-session secret `EPPP_SESSION_SECRET` — the schema's * required field, Security-and-Operations §32/§26) fails fast at startup * with an error naming the missing field instead of booting with an invalid - * configuration. The parsed config keeps the committed defaults for - * `host`/`port`/`databaseUrl` (the `process.env` adapter that centralizes - * these reads is E00-S04-T04 and lands later); `assertValidConfig` throws - * `MissingRequiredSettingError` naming the missing field. + * configuration. + * + * [E00-S04-T04] environment adapter: ALL settings flow through the config + * package's environment adapter (`loadConfigFromEnv` from + * `@personal-blog/config`) — the workspace's single owner of `process.env` + * reads — so this module (and every other module outside the config package) + * never reads `process.env` directly. The adapter maps the environment + * (`HOST`/`PORT`/`DATABASE_URL`/`EPPP_SESSION_SECRET`) onto the validated + * config shape and validates it with `assertValidConfig` (E00-S04-T02) before + * the server binds, so a missing required setting is still a startup error + * naming the missing field. * * [E00-S04-T03] secret redaction: ALL log output goes through the redacting * logger (`createLogger`, defined below — every line is scrubbed of the @@ -43,25 +50,21 @@ */ import { createServer, type IncomingMessage, type ServerResponse } from 'node:http'; -import { assertValidConfig } from '@personal-blog/config'; +import { loadConfigFromEnv } from '@personal-blog/config'; import { redactConfig, redactText, type Config } from '@personal-blog/config'; import { Pool } from '@personal-blog/database-postgres'; import { MigrationLedger, MigrationRunner } from '@personal-blog/database-postgres'; import type { Migration } from '@personal-blog/database-postgres'; -/** Port the server listens on; `PORT` overrides the container default (3000). */ -const PORT = resolvePort(process.env.PORT); - -// [E00-S04-T02] field-specific startup error: validate the startup -// configuration before anything else, so a missing required setting (e.g. -// EPPP_SESSION_SECRET) crashes the process at startup with an error naming -// the missing field — never boots with an invalid configuration. -const config = assertValidConfig({ - host: '0.0.0.0', - port: PORT, - databaseUrl: process.env.DATABASE_URL, - sessionSecret: process.env.EPPP_SESSION_SECRET, -}); +// [E00-S04-T04] environment adapter: ALL settings flow through the config +// package's adapter — the workspace's single owner of process.env reads — so +// the server never reads process.env directly. The adapter maps the +// environment onto the validated config shape and validates it with +// assertValidConfig (E00-S04-T02) before the server binds, so a missing +// required setting (e.g. EPPP_SESSION_SECRET) still crashes the process at +// startup with an error naming the missing field — never boots with an +// invalid configuration. +const config = loadConfigFromEnv(); // [E00-S04-T03] secret redaction: every log line goes through the redacting // logger, seeded with the validated config's secrets — and the resolved @@ -96,17 +99,6 @@ const MIGRATIONS: readonly Migration[] = []; */ let migrationsComplete = false; -/** - * 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; -} - /** Writes a JSON response with an explicit content-length. */ function sendJson(res: ServerResponse, statusCode: number, body: string): void { res.writeHead(statusCode, { @@ -177,7 +169,7 @@ function handleRequest(req: IncomingMessage, res: ServerResponse): void { const server = createServer(handleRequest); -const databaseUrl = process.env.DATABASE_URL; +const databaseUrl = config.databaseUrl; if (databaseUrl === undefined) { // No DATABASE_URL configured (e.g. local non-container dev): there are no @@ -207,8 +199,8 @@ if (databaseUrl === undefined) { }); } -server.listen(PORT, () => { - logger.log(`@personal-blog/server listening on http://0.0.0.0:${PORT} (health: GET /health)`); +server.listen(config.port, () => { + logger.log(`@personal-blog/server listening on http://${config.host}:${config.port} (health: GET /health)`); }); // `docker stop` (Compose down) and Ctrl-C send SIGTERM/SIGINT — close the