[E00-S04-T04] No module reads process.env except configuration adapter #403
No Reviewers
Labels
Clear labels
agent/analyst-drafted
agent/analyst-drafted
needs/human-decision
needs/human-decision
needs/security-review
needs/security-review
tier/t0
tier/t1
tier/t2
tier/t3
kind
bug
kind
bug
kind
epic
kind
epic
kind
initiative
EPPP programme initiative
kind
story
kind
story
kind
task
EPPP engineering card/task decomposed from a story
kind
toil
kind
toil
loop
1
loop
1
loop
2
loop
2
loop
3
loop
3
risk
agent-full
risk
agent-full
risk
human-gated
risk
human-gated
risk
human-only
risk
human-only
size
l
size
l
size
m
size
m
size
s
size
s
status
blocked
status
blocked
status
done
Workflow: Done
status
in-progress
status
in-progress
status
proposed
status
proposed
status
ready
status
ready
status
review
status
review
stream
checkout
stream
checkout
stream
onboarding
stream
onboarding
stream
platform
stream
platform
trivial — implementer only, auto-merge
standard — implementer + reviewer + tester
complex — security if triggered, human merge
critical — full chain + security, human merge
No labels
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: Fabrika/PersonalBlog#403
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What changed
packages/config— the environment adapter (newsrc/env.ts, re-exported from the boundary asloadConfigFromEnv) is the workspace's single owner ofprocess.envreads. It mapsHOST/PORT/DATABASE_URL/EPPP_SESSION_SECRETonto the validated config shape and validates it withassertValidConfig(E00-S04-T02) before returning: a missing required setting (EPPP_SESSION_SECRET) is still a field-specific startup error naming the field, and a badPORTkeeps the pre-adapter fallback-to-3000 behavior.HOSTis validated at the adapter boundary (resolveHost): it must be a hostname (RFC 1123) or an IP address (IPv4/IPv6 vianode:netisIP); an invalidHOSTthrows a field-specificConfigStartupErrornaminghost, so arbitrary env content is never used for binding or echoed verbatim into the startup log (resolves security review SEC-3).apps/serverloads all of its settings through the adapter (const config = loadConfigFromEnv(), bindingconfig.port, takingconfig.databaseUrl) and no longer readsprocess.envat all. The server passesconfig.hosttoserver.listen(config.port, config.host, ...), so a configuredHOSTbinds exactly that interface and the startup log reflects the actual bind — it never claims a bind the process does not enforce (resolves security review SEC-2).tests/config-env-adapter.test.mjsextended (HOST validation in the deterministic boundary probe; boot probes for loopback-only binding withHOST=127.0.0.1and for an invalidHOSTfailing startup without echoing the raw value; mutation probes forresolveHostandconfig.host); suites locked to the old direct-read wiring updated (config-startup-error,config-log-redaction,app-readiness,health-endpoint).config-env-adapterjob (E00-S04-T04) gates the staticprocess.envcheck on every PR; docs — non-container guide updated.Criterion → test mapping
process.envdirectly except the configuration adapterconfig-env-adapter: comment-stripped workspace scan overapps/+packages/+extensions/proves everyprocess.envreference lives inpackages/config/src/env.ts(issue test plan: "static check confirms only the adapter readsprocess.env"); mutation probes inject a direct read into the server / move it intostartup.tsin a temp tree copy and the scan fails naming the file; clean-copy sanity passesconfig-env-adapter: static assertions onenv.ts(maps HOST/PORT/DATABASE_URL/EPPP_SESSION_SECRET, validates viaassertValidConfig) and on the server (importsloadConfigFromEnv, bindsconfig.port/config.host, takesconfig.databaseUrl, noprocess.env); deterministic boundary probe (full env mapping, defaults0.0.0.0/3000, bad-PORTfallback, missing secret →MissingRequiredSettingErrornamingsessionSecret, emptyDATABASE_URL→ConfigStartupError, no-arg call reads the realprocess.env); server-boot probes (aPORT/HOSToverride appears in the resolved-configuration log; missing secret still exits non-zero naming the field)HOSTsetting controls the actual bind interface: the server passesconfig.hosttoserver.listen, and the startup log never claims a bind the process does not enforceconfig-env-adapter: static assertion that the server passesconfig.hosttoserver.listen(config.port, config.host, ...)(mutation probe: droppingconfig.hostfails); boot probe: withHOST=127.0.0.1the server answersGET /healthon loopback, does not answer on a non-loopback interface (canConnectto the host's non-internal IPv4 fails), and the startup log showshttp://127.0.0.1:<port>— the log reflects the actual bind (issue test plan: "boot withHOST=127.0.0.1and confirm the server binds loopback only, not all interfaces"; "startup log reflects the actual bind interface")HOSTis validated at the adapter boundary as a hostname or IP address before it is used for binding or logged, so arbitrary env content is never echoed verbatim into logsconfig-env-adapter: static assertions onresolveHostinenv.ts; deterministic boundary probe (valid forms pass: IPv4/IPv6/hostnames incl.127.0.0.1,0.0.0.0,::1,localhost,db; invalid forms —not a host!,127.0.0.1:3000,-bad— throwConfigStartupErrornaminghost); boot probe: an invalidHOSTexits non-zero naming the field and the raw value never appears in the process output (mutation probe: bypassingresolveHostfails)config-env-adapterjob in.gitea/workflows/ci.ymlis an intended, in-scope part of this task's test plan (it implements the staticprocess.envcheck); additive only, alters no existing gating stepconfig-env-adapter: "the config-env-adapter criterion is enforced in CI" — the rootscripts.testglob picks up the suite and the CI job runsnode --test tests/config-env-adapter.test.mjsafter building the config/database-postgres packages; the workflow diff is purely additive (+job insert, no existing job removed or weakened) — per the issue body, this job is the intended test-plan vehicle (evidence for the SEC-1 human decision)config-startup-error: updated static wiring assertions + mutation probes (dropping/moving the adapter call, server readingprocess.envdirectly) + boot probes unchangedconfig-log-redaction: updated wiring assertions (adapter-loaded config seeds the redacting logger) + boot probes unchangedapp-readiness: updated source anchors (config.databaseUrl) + static/deterministic/docker probes unchangedhealth-endpoint: server bindsconfig.port; the 3000 default now lives in the adapterRisks
0.0.0.0/3000, redacted secret); env-derived values (HOST/PORT) now appear from the adapter instead of hardcoded/read-inline.HOSTis now a read setting (default0.0.0.0, validated as hostname/IP at the adapter boundary) — behavior identical when unset, matching the schema's documented environment source. An invalidHOSTnow fails startup with a field-specific error instead of being logged; this is the intended stricter behavior (SEC-2/SEC-3)..env.exampleplaceholders (T05).