utils/env.cts `validateEnv()` runs first in main(): exits 1 on clearly
invalid config (non-integer PORT / SESSION_CLEANUP_INTERVAL_MS, too-short
TOKEN_ENCRYPTION_KEY), and warns loudly in production when
TOKEN_ENCRYPTION_KEY is unset (secrets fall back to a DB-stored key).
Resolves the standing prod-hardening TODO. Verified: bad PORT → exit 1;
prod without key → warns but boots (health 200).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- formatError() now emits `code` (the field the client reads) and hides
internals on any 5xx (missing status treated as 500). Single body shape
everywhere: { error, message, code, field? }.
- The terminal error handler now delegates to formatError, so a thrown
ApiError produces its real status + message + code (previously every
error collapsed to a generic "Internal server error"). Only 5xx are
logged/recorded; 4xx client errors no longer spam the error tracker.
This unblocks the Express-5 `throw ApiError` route pattern.
- Resilience: added `uncaughtException` (record + exit for clean restart),
enriched `unhandledRejection` (record, don't crash), and a SIGTERM/SIGINT
graceful shutdown that drains connections, checkpoints the WAL, and
closes the DB — verified: kill -TERM → "database closed cleanly" + exit.
check:server + typecheck:server clean; 232/232 tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Full-deployment QA against a copy of the real DB surfaced launch/require
references the server .cts migration didn't update. All are non-source
tooling/edge paths, which is why typecheck/check/tests stayed green:
- e2e/setup/prepare-db.js: require('../../db/database') and
require('../../scripts/seedDemoData') → .cts. This broke `npm run
test:e2e*` entirely (harness couldn't load). [material]
- playwright.config.js + scripts/prod-smoke.sh: webServer/boot command
`node server.js` → `node server.cts`. Also blocked the e2e + prod-smoke
suites. [material]
- setup/firstRun.js → .cts, require('../services/auditService') → .cts,
and server.cts call site → require('./setup/firstRun.cts'). Latent:
server.cts only reaches firstRun when userCount===0, but database.cts
auto-seeds a default admin during getDb() first, so the path isn't hit
in normal boot — fixed defensively so it can't crash if ever reached.
- .env.example: doc reference `node server.js` → server.cts.
Verified: typecheck:server + check:server clean; server test suite
226/226; e2e probe 17/17 (was 0 — suite couldn't boot before); the
production Docker image builds and boots on a copy of the real
production DB (44 bills, 1159 payments) with /api/health 200 and all 64
page endpoints returning 2xx.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the JS→TS server migration. server.js → server.cts (Node
type-stripping, no build step), and every launch/config reference now
points at it: package.json (main/dev:api/start/check:server) and the
Docker CMD.
scripts/seedDemoData.js is required at runtime by routes/user.cts, so it
crosses into the server runtime — migrated to .cts and its require of
db/database updated to the .cts path. The remaining scripts/*.js are
standalone dev/ops tooling (outside the runtime and the check:server
boundary); their stale extensionless requires of now-.cts modules were
repaired so they still run, and the PM2 ecosystem config entry point was
updated to server.cts.
Verified: 0 runtime .js remain in the server tree; typecheck:server and
check:server clean; 226/226 tests pass; `node server.cts` boots and
/api/health returns 200.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>