@ultimat3/http 22.15.0 → 23.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 +1 -1
- package/README.md +9 -0
- package/package.json +6 -5
- package/src/error-map.ts +22 -0
- package/src/hooks.ts +12 -1
- package/src/stages.ts +3 -1
package/CLAUDE.md
CHANGED
|
@@ -203,7 +203,7 @@ Owned request lifecycle over `Bun.serve`. Tier 2.
|
|
|
203
203
|
| `navigation.ts` | the client router's server half: the gate `context` runs after the match (a prefetch to a route whose `meta.navigation` did not opt in → `204`; a soft GET to anything but a page of the router's `<app>:<surface>` → `204` + `x-ultimate-location`, before auth or app code) and the `response` stage's rewrite of a 3xx to a router request into the same hand-over, cookies kept |
|
|
204
204
|
| `error-map.ts` | the code → status table, closed, plus the app's half (`registerErrorStatus`) |
|
|
205
205
|
| `error-facts.ts` | every RENDERING of a throwable: `factsOf()`, the problem document (including the issue list and the opacity rule over it), the three terminal lines |
|
|
206
|
-
| `hooks.ts` | the seams: `authenticate`, `authorize`, `devNotices` + the app's `configureAuthenticator()` |
|
|
206
|
+
| `hooks.ts` | the seams: `authenticate`, `authorize`, `devNotices`, `errorPage`, `explainMiss` (asked ONLY on a `not-found` match, synchronously, with the request line alone; its error replaces `X_ROUTE_NOT_FOUND`) + the app's `configureAuthenticator()` |
|
|
207
207
|
| `type-pins.ts` | compile-time claims about `AuthzDecision`'s shape — source, because `tsc` never reads a `.test.ts` |
|
|
208
208
|
| `overlay.ts` | the dev error page: the same code/cause/fix as the terminal, plus any notices |
|
|
209
209
|
| `overlay-style.ts` | the overlay's one stylesheet, split out so `security-headers.ts` hashes it |
|
package/README.md
CHANGED
|
@@ -303,6 +303,9 @@ exactly three things. Apps declare it through `defineApi({ http: { mounts } })`
|
|
|
303
303
|
|
|
304
304
|
`RouteMeta.authenticate` is the mechanism: a route that states one is reached only through it.
|
|
305
305
|
|
|
306
|
+
For API keys, `resolveToken` is `@ultimat3/auth`'s `apiKeyResolver(() => store)` — a live key is
|
|
307
|
+
`{ actor, scopes }`, every wrong one is the same `null`, and a store fault stays a throw.
|
|
308
|
+
|
|
306
309
|
## Inbound webhooks
|
|
307
310
|
|
|
308
311
|
`verifyWebhookSignature(request, { secret })` is the receiving half of the framework's webhook
|
|
@@ -439,3 +442,9 @@ Tier 2. Imports `@ultimat3/core`, `@ultimat3/schema`, `@ultimat3/i18n` and `@ult
|
|
|
439
442
|
tiers 0 and 1, which is the whole rule. Authentication and policy evaluation arrive through
|
|
440
443
|
`ServerHooks`, declared structurally, because `@ultimat3/policy` is a sibling tier. There is no
|
|
441
444
|
plugin API: `Middleware` wraps a handler, the pipeline is everything else.
|
|
445
|
+
|
|
446
|
+
`hooks.explainMiss(method, pathname)` is the same kind of seam for a request the router matched
|
|
447
|
+
nothing for: the error it returns is thrown in place of `X_ROUTE_NOT_FOUND`, `undefined` keeps the
|
|
448
|
+
plain miss. Synchronous, given only the request line, never asked about a matched route or a 405.
|
|
449
|
+
`@ultimat3/action` answers it for a path that is one of its actions under a `pathStyle` this server
|
|
450
|
+
does not serve (`X_CONTRACT_DRIFT`, 404).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/http",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "23.0.0",
|
|
4
4
|
"description": "Owned request lifecycle over Bun.serve: router, ordered pipeline, problem+json errors",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"files": [
|
|
20
20
|
"src",
|
|
21
21
|
"!src/**/*.test.ts",
|
|
22
|
+
"!src/**/*-fixture.ts",
|
|
22
23
|
"CLAUDE.md",
|
|
23
24
|
"README.md",
|
|
24
25
|
"LICENSE"
|
|
@@ -31,9 +32,9 @@
|
|
|
31
32
|
"test": "bun test"
|
|
32
33
|
},
|
|
33
34
|
"dependencies": {
|
|
34
|
-
"@ultimat3/core": "
|
|
35
|
-
"@ultimat3/i18n": "
|
|
36
|
-
"@ultimat3/schema": "
|
|
37
|
-
"@ultimat3/time": "
|
|
35
|
+
"@ultimat3/core": "23.0.0",
|
|
36
|
+
"@ultimat3/i18n": "23.0.0",
|
|
37
|
+
"@ultimat3/schema": "23.0.0",
|
|
38
|
+
"@ultimat3/time": "23.0.0"
|
|
38
39
|
}
|
|
39
40
|
}
|
package/src/error-map.ts
CHANGED
|
@@ -103,6 +103,12 @@ export const ERROR_STATUS = {
|
|
|
103
103
|
// it, and because a missing row made a typo'd uuid a 500: the caller was told the server broke,
|
|
104
104
|
// and the `error-map` stage reported the caller's mistake to the on-call monitor.
|
|
105
105
|
X_INPUT_INVALID: 400,
|
|
106
|
+
// The one way a SERVER answers this code: a POST to the path an action has under a path style
|
|
107
|
+
// this app does not serve (`hooks.explainMiss`). 404 because it is still a miss — nothing is
|
|
108
|
+
// routed there and nothing ran, so the status a proxy or a monitor counts is the one the plain
|
|
109
|
+
// `X_ROUTE_NOT_FOUND` had; the code and the `fix:` are what changed. Every other raise of it is
|
|
110
|
+
// client-side or a contract test, and never reaches this table.
|
|
111
|
+
X_CONTRACT_DRIFT: 404,
|
|
106
112
|
// A retried `Idempotency-Key` naming a different payload, or one still in flight. 409 because
|
|
107
113
|
// that is what the action's own OpenAPI operation publishes for it — the runtime answered 500
|
|
108
114
|
// while the document promised 409, and a client written against the spec read the framework
|
|
@@ -215,6 +221,10 @@ export const ERROR_STATUS = {
|
|
|
215
221
|
// 500, the same shelf as `X_SEARCH_UNDECLARED`: a column with no machine is a declaration the
|
|
216
222
|
// app has not written, and no request changes that.
|
|
217
223
|
X_STATE_UNDECLARED: 500,
|
|
224
|
+
// A sealed column named where the database would compare it, or in a view. Both are the app's
|
|
225
|
+
// own declaration or call, never the caller's input — the same shelf as the two above.
|
|
226
|
+
X_ENTITY_SEALED_PREDICATE: 500,
|
|
227
|
+
X_ENTITY_SEALED_IN_VIEW: 500,
|
|
218
228
|
// @ultimat3/db — the constraints a request trips, both 409. db's own `fix:` for the unique
|
|
219
229
|
// violation says "answer 409, which is what a raced signup is", and `X_ENTITY_DUPLICATE` — the
|
|
220
230
|
// same event one layer up — is 409 above; a foreign key rides with it because both halves of it
|
|
@@ -313,6 +323,12 @@ export const ERROR_STATUS = {
|
|
|
313
323
|
// reaching `formatDate` / `formatMoney` / `describeCron`. Those raised it and answered 500,
|
|
314
324
|
// paging the on-call for a string the caller typed.
|
|
315
325
|
X_LOCALE_INVALID: 400,
|
|
326
|
+
// @ultimat3/core — `seal()` / `open()`. All three are reachable from a request (a sealed column
|
|
327
|
+
// read inside an action) and none is the caller's doing: the deploy has no key, the ring lost a
|
|
328
|
+
// retired key, or a stored value did not authenticate. 500, stated rather than defaulted.
|
|
329
|
+
X_SEAL_INVALID: 500,
|
|
330
|
+
X_SEAL_KEY_MISSING: 500,
|
|
331
|
+
X_SEAL_KEY_UNKNOWN: 500,
|
|
316
332
|
// @ultimat3/money — a well-formed code this process carries no row for. The currency table is
|
|
317
333
|
// OPEN (`registerCurrency`), and every surface between the wire and the throw accepts any
|
|
318
334
|
// `^[A-Z]{3}$`: `@ultimat3/schema`'s `CURRENCY_CODE_PATTERN`, the OpenAPI `pattern` emitted from
|
|
@@ -457,6 +473,12 @@ export const ERROR_STATUS = {
|
|
|
457
473
|
X_RECORD_REJECTED: 500,
|
|
458
474
|
X_SYNC_UNCONFIGURED: 500,
|
|
459
475
|
X_INTERNAL: 500,
|
|
476
|
+
// @ultimat3/jobs — the job is running and cannot be removed
|
|
477
|
+
X_JOB_NOT_REMOVABLE: 409,
|
|
478
|
+
// @ultimat3/jobs — the job is not waiting on its run time
|
|
479
|
+
X_JOB_NOT_PROMOTABLE: 409,
|
|
480
|
+
// @ultimat3/jobs — a job list page was asked for outside its bounds
|
|
481
|
+
X_JOB_PAGE_INVALID: 400,
|
|
460
482
|
// The keys are LITERAL — deliberately not `Readonly<Record<string, number>>`, which is what the
|
|
461
483
|
// annotation used to say. This table is the closed one, so `ERROR_STATUS.X_QUERY_NOT_PAGABLE`
|
|
462
484
|
// has to be a compile error rather than an `undefined` a test then asserts `toBeNumber()` on.
|
package/src/hooks.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// be imported here), and — a seam of a different kind, deciding nothing — what a dev diagnostic
|
|
4
4
|
// found. All three are declared structurally, which keeps the import boundary intact and keeps
|
|
5
5
|
// the pipeline testable.
|
|
6
|
-
import type { Actor } from '@ultimat3/core';
|
|
6
|
+
import type { Actor, UltimateError } from '@ultimat3/core';
|
|
7
7
|
import type { RequestContext } from './context';
|
|
8
8
|
import type { OverlayNotice } from './overlay';
|
|
9
9
|
import type { UltimateRequest } from './request';
|
|
@@ -51,6 +51,17 @@ export interface ServerHooks {
|
|
|
51
51
|
status: number,
|
|
52
52
|
ctx: RequestContext,
|
|
53
53
|
) => Promise<string | undefined> | string | undefined;
|
|
54
|
+
/**
|
|
55
|
+
* Why nothing is routed at this path, when a higher tier knows more than "no route". Consulted
|
|
56
|
+
* ONLY on a miss — never for a matched route, a refused method or an unreadable path — and the
|
|
57
|
+
* error it returns is thrown in place of `X_ROUTE_NOT_FOUND`; `undefined` keeps the plain miss.
|
|
58
|
+
* `@ultimat3/action` answers here for a path that is one of its actions under a path style this
|
|
59
|
+
* server does not serve.
|
|
60
|
+
*
|
|
61
|
+
* Synchronous and given only the request line: a miss is reached by any anonymous caller, so
|
|
62
|
+
* what runs on it must not read a body, a session or a database.
|
|
63
|
+
*/
|
|
64
|
+
readonly explainMiss?: (method: string, pathname: string) => UltimateError | undefined;
|
|
54
65
|
}
|
|
55
66
|
|
|
56
67
|
export type Authenticator = NonNullable<ServerHooks['authenticate']>;
|
package/src/stages.ts
CHANGED
|
@@ -181,7 +181,9 @@ export const stageRunners = (input: StageRunnersInput): Record<StageName, StageR
|
|
|
181
181
|
if (routed instanceof Response) return routed;
|
|
182
182
|
const match = matchRoute(input.table, ctx.method, routed);
|
|
183
183
|
if (!match.ok) {
|
|
184
|
-
if (match.reason === 'not-found')
|
|
184
|
+
if (match.reason === 'not-found') {
|
|
185
|
+
throw hooks.explainMiss?.(ctx.method, pathname) ?? routeNotFound(ctx.method, pathname);
|
|
186
|
+
}
|
|
185
187
|
if (match.reason === 'path-invalid') throw pathInvalid(pathname, match.segment);
|
|
186
188
|
ctx.headers.set('allow', match.allow.join(', '));
|
|
187
189
|
throw methodNotAllowed(ctx.method, pathname, match.allow);
|