@ultimat3/http 24.0.0 → 25.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 +37 -19
- package/README.md +59 -17
- package/package.json +9 -6
- package/src/app-config.ts +3 -1
- package/src/auth-redirect.ts +56 -17
- package/src/bearer-mount.ts +81 -26
- package/src/config.ts +27 -29
- package/src/context.ts +10 -10
- package/src/correlation.ts +2 -2
- package/src/cors.ts +3 -2
- package/src/deadline.ts +2 -5
- package/src/error-map-tier-0.ts +2 -0
- package/src/error-map-tier-1.ts +2 -0
- package/src/error-map-tier-2.ts +2 -0
- package/src/error-map-tier-3.ts +8 -0
- package/src/error-map-tier-4.ts +24 -0
- package/src/error-page.ts +2 -2
- package/src/error-titles.ts +93 -0
- package/src/errors.ts +44 -94
- package/src/forwarded.ts +20 -5
- package/src/health-disclosure.ts +0 -3
- package/src/hooks.ts +6 -5
- package/src/index.ts +31 -21
- package/src/navigation.ts +14 -2
- package/src/pipeline.ts +12 -8
- package/src/rate-limit-buckets.ts +26 -2
- package/src/rate-limit-errors.ts +36 -10
- package/src/rate-limit-headers.ts +40 -0
- package/src/rate-limit-installed.ts +79 -0
- package/src/rate-limit-postgres.ts +3 -3
- package/src/rate-limit-stage.ts +13 -4
- package/src/rate-limit.ts +22 -15
- package/src/request-facts.ts +44 -0
- package/src/request.ts +2 -2
- package/src/response.ts +2 -2
- package/src/route-cache.ts +1 -1
- package/src/router.ts +16 -1
- package/src/server.ts +74 -24
- package/src/set-cookie.ts +60 -0
- package/src/stages.ts +15 -9
- package/src/validate.ts +2 -2
package/src/stages.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
8
|
anonymousActor,
|
|
9
|
+
BUILD_ID_HEADER,
|
|
9
10
|
inflightCount,
|
|
10
11
|
isAnonymous,
|
|
11
12
|
isDraining,
|
|
@@ -48,7 +49,7 @@ import type { UltimateRequest } from './request';
|
|
|
48
49
|
import { addVary, problem, redirect } from './response';
|
|
49
50
|
import { matchRoute, type Route, type RouteHandler, type RouteTable } from './router';
|
|
50
51
|
import { responseSecurityHeaders } from './security-headers';
|
|
51
|
-
import {
|
|
52
|
+
import { validateBody } from './validate';
|
|
52
53
|
|
|
53
54
|
export type StageName =
|
|
54
55
|
| 'request-id'
|
|
@@ -173,12 +174,12 @@ export const stageRunners = (input: StageRunnersInput): Record<StageName, StageR
|
|
|
173
174
|
const answered = preflight(request.raw, config.cors);
|
|
174
175
|
if (answered !== undefined) return answered;
|
|
175
176
|
|
|
176
|
-
ctx.clientBuildId = request.header(
|
|
177
|
+
ctx.clientBuildId = request.header(BUILD_ID_HEADER);
|
|
177
178
|
// Stamped BEFORE the assertion, so the refusal carries it too. A skew answer that
|
|
178
179
|
// withholds the server's own id tells a client it is stale without telling it what to
|
|
179
180
|
// become — and the one caller that can act on that, the service worker holding the
|
|
180
181
|
// stale id, then has no way to tell this 409 from any other.
|
|
181
|
-
if (config.buildId !== null) ctx.headers.set(
|
|
182
|
+
if (config.buildId !== null) ctx.headers.set(BUILD_ID_HEADER, config.buildId);
|
|
182
183
|
request.assertBuild();
|
|
183
184
|
|
|
184
185
|
// A `/<locale>/` prefix is stripped BEFORE the match, never by middleware: middleware wraps
|
|
@@ -273,7 +274,7 @@ export const stageRunners = (input: StageRunnersInput): Record<StageName, StageR
|
|
|
273
274
|
body: async (request, ctx) => {
|
|
274
275
|
const schema = ctx.route?.meta.input;
|
|
275
276
|
if (schema === undefined) return undefined;
|
|
276
|
-
const outcome = await
|
|
277
|
+
const outcome = await validateBody(schema, await request.bodyRaw());
|
|
277
278
|
if (!outcome.ok) throw bodyInvalid(ctx.url.pathname, outcome.issues);
|
|
278
279
|
ctx.input = outcome.value;
|
|
279
280
|
return undefined;
|
|
@@ -375,7 +376,15 @@ export const stageRunners = (input: StageRunnersInput): Record<StageName, StageR
|
|
|
375
376
|
// never handed a page. Which of the two a browser gets is the ENVIRONMENT — the overlay
|
|
376
377
|
// prints the cause, the fix and the stack, which is what a visitor may never see.
|
|
377
378
|
if (acceptsHtml(request.raw)) {
|
|
378
|
-
|
|
379
|
+
// A 4xx is not a defect to debug, it is a page a visitor will see, so dev asks for the
|
|
380
|
+
// app's own file first (#492, O-492). A 5xx keeps the overlay, and so does a 4xx the app
|
|
381
|
+
// has no file for: in dev the diagnostic beats the framework's generic page.
|
|
382
|
+
// A throw from the hook is caught by `recoverWith` and degrades to the problem document.
|
|
383
|
+
const override =
|
|
384
|
+
config.dev && facts.status >= 500
|
|
385
|
+
? undefined
|
|
386
|
+
: await hooks.errorPage?.(facts.status, ctx);
|
|
387
|
+
if (config.dev && override === undefined) {
|
|
379
388
|
// Asked for inside the branch, never above it: the overlay is the only surface a notice
|
|
380
389
|
// has, so a production process — or an agent that asked for json — must not pay a
|
|
381
390
|
// diagnostic's per-request cost to produce findings nothing will render.
|
|
@@ -401,10 +410,7 @@ export const stageRunners = (input: StageRunnersInput): Record<StageName, StageR
|
|
|
401
410
|
...(seconds === undefined ? {} : { retryAfterSeconds: seconds }),
|
|
402
411
|
signInPath: config.signInPath,
|
|
403
412
|
},
|
|
404
|
-
|
|
405
|
-
// caught by `recoverWith` and degrades to the problem document, which is the answer a
|
|
406
|
-
// page whose renderer failed can still give.
|
|
407
|
-
{ override: await hooks.errorPage?.(facts.status, ctx), headers: retryAfter },
|
|
413
|
+
{ override, headers: retryAfter },
|
|
408
414
|
);
|
|
409
415
|
}
|
|
410
416
|
return problem(error, {
|
package/src/validate.ts
CHANGED
|
@@ -55,12 +55,12 @@ const outcome = <Out>(result: {
|
|
|
55
55
|
export const validateSync = <Out>(schema: Schema<Out>, value: unknown): ValidationOutcome<Out> => {
|
|
56
56
|
const result = schema['~standard'].validate(value);
|
|
57
57
|
if (result instanceof Promise) {
|
|
58
|
-
return { ok: false, issues: ['schema is async; use
|
|
58
|
+
return { ok: false, issues: ['schema is async; use validateBody() for request bodies'] };
|
|
59
59
|
}
|
|
60
60
|
return outcome<Out>(result);
|
|
61
61
|
};
|
|
62
62
|
|
|
63
|
-
export const
|
|
63
|
+
export const validateBody = async <Out>(
|
|
64
64
|
schema: Schema<Out>,
|
|
65
65
|
value: unknown,
|
|
66
66
|
): Promise<ValidationOutcome<Out>> => outcome<Out>(await schema['~standard'].validate(value));
|