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",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"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": {
|
"scripts": {
|
||||||
"build": "pnpm --filter @personal-blog/config build && pnpm --filter @personal-blog/database-postgres build && tsc -p tsconfig.json",
|
"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",
|
"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
|
* setting (the admin-session secret `EPPP_SESSION_SECRET` — the schema's
|
||||||
* required field, Security-and-Operations §32/§26) fails fast at startup
|
* required field, Security-and-Operations §32/§26) fails fast at startup
|
||||||
* with an error naming the missing field instead of booting with an invalid
|
* with an error naming the missing field instead of booting with an invalid
|
||||||
* configuration. The parsed config keeps the committed defaults for
|
* configuration.
|
||||||
* `host`/`port`/`databaseUrl` (the `process.env` adapter that centralizes
|
*
|
||||||
* these reads is E00-S04-T04 and lands later); `assertValidConfig` throws
|
* [E00-S04-T04] environment adapter: ALL settings flow through the config
|
||||||
* `MissingRequiredSettingError` naming the missing field.
|
* 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
|
* [E00-S04-T03] secret redaction: ALL log output goes through the redacting
|
||||||
* logger (`createLogger`, defined below — every line is scrubbed of the
|
* logger (`createLogger`, defined below — every line is scrubbed of the
|
||||||
@@ -43,25 +50,21 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
|
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 { redactConfig, redactText, type Config } from '@personal-blog/config';
|
||||||
import { Pool } from '@personal-blog/database-postgres';
|
import { Pool } from '@personal-blog/database-postgres';
|
||||||
import { MigrationLedger, MigrationRunner } from '@personal-blog/database-postgres';
|
import { MigrationLedger, MigrationRunner } from '@personal-blog/database-postgres';
|
||||||
import type { Migration } from '@personal-blog/database-postgres';
|
import type { Migration } from '@personal-blog/database-postgres';
|
||||||
|
|
||||||
/** Port the server listens on; `PORT` overrides the container default (3000). */
|
// [E00-S04-T04] environment adapter: ALL settings flow through the config
|
||||||
const PORT = resolvePort(process.env.PORT);
|
// package's adapter — the workspace's single owner of process.env reads — so
|
||||||
|
// the server never reads process.env directly. The adapter maps the
|
||||||
// [E00-S04-T02] field-specific startup error: validate the startup
|
// environment onto the validated config shape and validates it with
|
||||||
// configuration before anything else, so a missing required setting (e.g.
|
// assertValidConfig (E00-S04-T02) before the server binds, so a missing
|
||||||
// EPPP_SESSION_SECRET) crashes the process at startup with an error naming
|
// required setting (e.g. EPPP_SESSION_SECRET) still crashes the process at
|
||||||
// the missing field — never boots with an invalid configuration.
|
// startup with an error naming the missing field — never boots with an
|
||||||
const config = assertValidConfig({
|
// invalid configuration.
|
||||||
host: '0.0.0.0',
|
const config = loadConfigFromEnv();
|
||||||
port: PORT,
|
|
||||||
databaseUrl: process.env.DATABASE_URL,
|
|
||||||
sessionSecret: process.env.EPPP_SESSION_SECRET,
|
|
||||||
});
|
|
||||||
|
|
||||||
// [E00-S04-T03] secret redaction: every log line goes through the redacting
|
// [E00-S04-T03] secret redaction: every log line goes through the redacting
|
||||||
// logger, seeded with the validated config's secrets — and the resolved
|
// logger, seeded with the validated config's secrets — and the resolved
|
||||||
@@ -96,17 +99,6 @@ const MIGRATIONS: readonly Migration[] = [];
|
|||||||
*/
|
*/
|
||||||
let migrationsComplete = false;
|
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. */
|
/** Writes a JSON response with an explicit content-length. */
|
||||||
function sendJson(res: ServerResponse, statusCode: number, body: string): void {
|
function sendJson(res: ServerResponse, statusCode: number, body: string): void {
|
||||||
res.writeHead(statusCode, {
|
res.writeHead(statusCode, {
|
||||||
@@ -177,7 +169,7 @@ function handleRequest(req: IncomingMessage, res: ServerResponse): void {
|
|||||||
|
|
||||||
const server = createServer(handleRequest);
|
const server = createServer(handleRequest);
|
||||||
|
|
||||||
const databaseUrl = process.env.DATABASE_URL;
|
const databaseUrl = config.databaseUrl;
|
||||||
|
|
||||||
if (databaseUrl === undefined) {
|
if (databaseUrl === undefined) {
|
||||||
// No DATABASE_URL configured (e.g. local non-container dev): there are no
|
// No DATABASE_URL configured (e.g. local non-container dev): there are no
|
||||||
@@ -207,8 +199,8 @@ if (databaseUrl === undefined) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
server.listen(PORT, () => {
|
server.listen(config.port, () => {
|
||||||
logger.log(`@personal-blog/server listening on http://0.0.0.0:${PORT} (health: GET /health)`);
|
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
|
// `docker stop` (Compose down) and Ctrl-C send SIGTERM/SIGINT — close the
|
||||||
|
|||||||
Reference in New Issue
Block a user