feat: server loads all settings through the config environment adapter (E00-S04-T04)
This commit is contained in:
@@ -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",
|
||||
|
||||
+24
-32
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user