Last updated: 2026-10-21
The whole proxy process. Wire behaviour is in the root README; each directory that owns a contract has its own README. The boundary review behind this file is #95.
Role. The root holds the composition root (app.mts, the process entry point), the mode selection it calls, and the pieces both modes share: the logger, graceful shutdown, request coalescing and the provider-body helpers. The directories are the two modes (modes/), the route assembly both modes mount (router/), the inbound-header helpers (express/) and client authentication (oauth/). Configuration is ../config/, outside src/.
Owns: assembling the server and the order it mounts things in; the process lifecycle (listen, drain, exit); the logger and what an error looks like in a log line; the modules both modes share.
Does not own: any mode's decision, provider client or cache (modes/*, each with its own README); header grammar (express/); client authentication at the provider (oauth/); configuration defaults and validation (config/).
Why separate. The root holds only what is assembly or shared by both modes. A mode may import the root's shared modules but never the other mode, so a module both modes need lives here rather than in either mode (#95 F6, F39).
No README of its own; this is its description.
- Role: route assembly that is not a mode — the liveness probe and CORS, which
app.mtsmounts, and the stages both mode routers share: the body limit ahead of the mode, the upstream proxy stage after it, and the error handler that ends the router. - Owns: the
/_healthcheckpath and its answer; CORS (cors.mts) — whether the proxy answers it (http.cors.origin.patternset), the preflights it answers204, the grant — to an origin the pattern admits only; the proxy sets noAccess-Control-*field on any other answer — that the grant on a proxied answer is the proxy's alone, and the boot-time warning about a pattern that likely admits too much;http.bodyLimitSizeas the one byte count the body limit, the upstream proxy and the error handler enforce; building the upstream proxy fromupstream.baseURL; which requests the shared stages refuse and with which status — a body over the limit is413, an upstream that could not be reached or dropped the exchange before its answer started502(504when it timed out), and one that drops it partway through its answer the caller's connection closed, anything else that reaches the end of a mode router its own status or500— and theModeRefusalscontract (refusal.mts) a mode says those refusals through. Whatever the mode, the upstream proxy stage sends each request on a connection of its own that carries no other request, sends the body as the bytes that arrived, frames it byContent-Lengthalone, dropsExpect, adds the proxy's own member toVia(<version> auth.proxy, after the inbound members), tells the upstream where the request came from as this hop saw it — the peer appended toX-Forwarded-Forand set asX-Real-IP, the receivedHostasX-Forwarded-Host, noForwarded, andX-Forwarded-Protoas it arrived, since the proxy sees no TLS — sends the caller'sAccept-Charseton unchanged, and keeps the caller's connection-scoped fields off the upstream request — the fields itsConnectionnames, except those the proxy decides (Authorization,x-request-id, its ownconnection: close), the hop-by-hop fields, andProxy-Authorization— and the upstream's off the caller's response: itsConnection, the fields that names except those the proxy set before forwarding, the hop-by-hop fields,TrailerandProxy-Authenticate; Node writes the caller'sConnectionitself. On a proxied answer, a field the proxy set before forwarding keeps the proxy's value, exceptVary, which lists both sides' members. - Does not own: what else reaches upstream — each mode decides that on
req.headersbefore the stage; how a refusal is logged and what body answers it, which are the mode's vocabulary; the mode routers themselves (modes/*/router.mts). - Why separate: none of it is a mode's decision (#95 F18). The body limit decides only on a declared length, and no stage decides anything about a credential.
No README of its own; this is its description.
- Role: the two alternative request paths.
resolveRoutermounts exactly one, chosen byauth.modeat boot. - Owns: nothing at the directory level;
modes/injection/andmodes/validation/state their own responsibility. - Does not own: anything the two share; that sits at the
src/root or inrouter/,express/oroauth/. - Why separate: each mode is a complete, alternative request path chosen once at boot, and neither imports the other.
Code both modes need sits outside modes/, because neither mode may import the other: at the src/ root (the logger, coalescing, provider-body handling), in oauth/ (how the proxy authenticates itself to the provider), in router/ (route assembly) or in express/ (inbound-header parsing).
modes/*may import thesrc/root's shared modules,express/,oauth/,router/except the healthcheck and CORS, andconfig/. Neither mode imports the other.- Nothing under
express/,oauth/,router/orconfig/imports a mode, and the root's shared modules import no mode. In production code, onlyapp-internal.mtsimports a mode, to select its router. express/andoauth/import nothing else insrc/.config/importsoauth/private-key-jwt.mts, to read a client key at boot,byte-size.mts, to readhttp.bodyLimitSize, andaddress-range.mts, to readauth.injection.trustedProxieswith the grammar the injection mode builds its trust list from.- The test support in
__tests__/that is not itself a test is imported by tests only, never by production code: the fake provider (fake-provider.mts), a localnode:httpserver the provider clients reach through the realfetch, and the controlled provider timeout beside it (#143); and, for the composition tests, the entry point run in a child process (app-process.mts, with the preloadchild-preload.mts) and a recording upstream (recording-upstream.mts) (#144); and a client that sends a chunked body (post-chunked.mts).
- The healthcheck is outside
pathPrefixand CORS (#95 F25). It is mounted first and at the root, so/_healthcheckanswers whatever the prefix is, without CORS headers and without a request id: none is read, generated or echoed. It is liveness only — the process is up — and says nothing about the provider. No test importsapp.mts, which runs at import;__tests__/app-entry.test.mtsruns it in a child process and pins this placement, the refusal of a bad configuration before the process listens, and CORS under the prefix. The probe's path and method are pinned byrouter/__tests__/Healthcheck.test.mts. - CORS is under the prefix, ahead of the mode router, and answers preflights only. A request outside
pathPrefixgets no CORS fields, and anOPTIONSthere is404like any other. Under it, a preflight (OPTIONSwithOriginandAccess-Control-Request-Method) is answered204and never reaches the mode router, the upstream or the log; any otherOPTIONSis an ordinary request. Pinned byrouter/__tests__/cors.test.mtsand__tests__/app-entry.test.mts. - The proxy announces no framework.
app.mtsturns Express'sX-Powered-Byoff, so the proxy's own answers carry none and a proxied answer carries the upstream's. Pinned by__tests__/app-entry.test.mts. - Shutdown drains; it does not wipe. The process passes no cleanup to the shutdown, so the token caches die with the process. The shutdown guarantees are stated in the header of
shutdown.mts. - State belongs to whoever built it. Parsed configuration is immutable after boot and passed down as an argument; caches and coalescing tables live in the closure that built them, and a router never clears state it was handed rather than built.
- One logger. Every module logs through the
Loggerinterface; the mode routers and CORS default to the singleton, the mode routers hand it to their decisions, and tests inject a fake throughdeps. Configuration comes from the HOCON parse inapp.mts; the one environment read outside it isLOG_LEVEL, inlogger.mts. - An Error reaches a log line only through an allowlist (#95 F48). Some errors carry bytes nobody chose to log — undici's
HTTPParserError.datais the unparsed rest of the provider's response, which may echo the token the proxy just sent — so anErrorlogged under theerroror theerrkey keeps only named fields, with URL credentials redacted; everything else on it is dropped. The validation path logs undererrorandshutdown.mtsundererr; an Error logged under any other key gets pino's handling, not the allowlist. A value undererrorthat is not anErrorpasses through unchanged: injection logsString(err)there for a throw it could not classify, whatever that message holds. The mechanics are in the header oflogger.mts, pinned by__tests__/logger.test.mts. - One log vocabulary for both modes (#134). Every line about a request — each router's
incoming requestand every decision line — carries the request id asrequestIdand aneventnamed after its mode (injection.*,validation.*), so a query onrequestIdcovers both modes; the event names are per mode. Theerrorfield is not shared: validation logs theErroritself, serialised through the allowlist below into an object withmessage,stackandcause, while injection logs a string — the error's message, orString(err)for a throw it could not classify. A query onerror.messagematches validation lines only. The level of a provider failure says whose it was: the provider refusing the caller's credential isinfo(a refusal of what the credential may do, injection'sexchange_not_permitted, iswarn); the proxy's own failures — its client refused, a redirect, a provider5xx, a timeout, an invalid response, an unexpected throw — areerror. One timeout is not: on the session grant, a provider401's body is read to tellinvalid_clientfrom an expired session (#95 F47), and a timeout during that read is answered as the expired session it almost always is —401 session_required, loggedinjection.session_unauthorizedatinfo. Pinned by each mode'sdecision.test.mtsandrouter.test.mts. - A provider failure never stops the process. A configuration error stops it before it listens; a provider failure is answered per mode; SIGTERM / SIGINT start the drain.