@daloyjs/core 1.0.0-rc.2 → 1.0.0-rc.4
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/README.md +96 -30
- package/dist/adapters/vercel.d.ts +8 -10
- package/dist/adapters/vercel.js +0 -2
- package/dist/app.d.ts +125 -8
- package/dist/app.js +244 -183
- package/dist/cli.js +41 -1
- package/dist/client.d.ts +28 -11
- package/dist/client.js +29 -6
- package/dist/combine.d.ts +11 -11
- package/dist/combine.js +90 -47
- package/dist/docs.d.ts +5 -9
- package/dist/docs.js +36 -14
- package/dist/idempotency.js +2 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/internal-response.d.ts +15 -0
- package/dist/internal-response.js +27 -0
- package/dist/jwk.d.ts +11 -7
- package/dist/jwk.js +11 -7
- package/dist/mcp.js +11 -6
- package/dist/middleware.d.ts +48 -7
- package/dist/middleware.js +96 -40
- package/dist/mtls.d.ts +6 -5
- package/dist/mtls.js +3 -9
- package/dist/openapi.js +1 -1
- package/dist/pagination.js +4 -1
- package/dist/response-cache.js +2 -1
- package/dist/safe-redirect.d.ts +4 -1
- package/dist/safe-redirect.js +4 -1
- package/dist/sbom.cdx.json +9 -9
- package/dist/sbom.spdx.json +5 -5
- package/dist/security.d.ts +21 -0
- package/dist/security.js +89 -0
- package/dist/tenancy.d.ts +2 -2
- package/dist/types.d.ts +85 -20
- package/dist/types.js +16 -1
- package/package.json +7 -1
package/dist/cli.js
CHANGED
|
@@ -704,6 +704,44 @@ async function runDoctor(opts, io) {
|
|
|
704
704
|
"header-count amplification defence.",
|
|
705
705
|
});
|
|
706
706
|
}
|
|
707
|
+
// JSON structural limits audit. The new jsonMaxKeys / jsonMaxDepth
|
|
708
|
+
// guards protect against hash-flood / deep-nesting DoS inside the byte
|
|
709
|
+
// limit. Surface when disabled (0) or raised to an implausibly high
|
|
710
|
+
// value.
|
|
711
|
+
const jsonMaxKeys = o.jsonMaxKeys;
|
|
712
|
+
if (jsonMaxKeys === 0) {
|
|
713
|
+
findings.push({
|
|
714
|
+
level: "warn",
|
|
715
|
+
code: "audit.jsonMaxKeys.disabled",
|
|
716
|
+
message: "jsonMaxKeys is 0 — the wide-object / hash-flood structural limit " +
|
|
717
|
+
"is disabled. An attacker can send tens or hundreds of thousands " +
|
|
718
|
+
"of keys in a body that still fits under bodyLimitBytes.",
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
else if (typeof jsonMaxKeys === "number" && jsonMaxKeys > 100_000) {
|
|
722
|
+
findings.push({
|
|
723
|
+
level: "warn",
|
|
724
|
+
code: "audit.jsonMaxKeys.blanket",
|
|
725
|
+
message: `jsonMaxKeys is ${jsonMaxKeys} (> 100k). A cap this high weakens ` +
|
|
726
|
+
"protection against wide-object DoS payloads.",
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
const jsonMaxDepth = o.jsonMaxDepth;
|
|
730
|
+
if (jsonMaxDepth === 0) {
|
|
731
|
+
findings.push({
|
|
732
|
+
level: "warn",
|
|
733
|
+
code: "audit.jsonMaxDepth.disabled",
|
|
734
|
+
message: "jsonMaxDepth is 0 — deep nesting DoS protection is disabled.",
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
else if (typeof jsonMaxDepth === "number" && jsonMaxDepth > 200) {
|
|
738
|
+
findings.push({
|
|
739
|
+
level: "warn",
|
|
740
|
+
code: "audit.jsonMaxDepth.blanket",
|
|
741
|
+
message: `jsonMaxDepth is ${jsonMaxDepth} (> 200). Extremely deep JSON is ` +
|
|
742
|
+
"almost never legitimate and can amplify CPU during validation.",
|
|
743
|
+
});
|
|
744
|
+
}
|
|
707
745
|
// Idle-timeout / request-timeout audit. Reaffirms the
|
|
708
746
|
// existing requestTimeoutMs check; also surface an explicit zero
|
|
709
747
|
// idleTimeoutMs in production. The framework also keeps adapter
|
|
@@ -921,7 +959,9 @@ function aiResponses(responses) {
|
|
|
921
959
|
for (const [status, spec] of Object.entries(responses)) {
|
|
922
960
|
if (!spec)
|
|
923
961
|
continue;
|
|
924
|
-
const entry = {
|
|
962
|
+
const entry = {
|
|
963
|
+
description: spec.description ?? `HTTP ${status} response`,
|
|
964
|
+
};
|
|
925
965
|
if (spec.body)
|
|
926
966
|
entry.body = aiSchema(spec.body);
|
|
927
967
|
if (spec.examples)
|
package/dist/client.d.ts
CHANGED
|
@@ -19,11 +19,9 @@ export type RoutesOf<A extends App> = A["routes"][number];
|
|
|
19
19
|
* the route's request and response schemas.
|
|
20
20
|
*
|
|
21
21
|
* The per-method types are recovered from the `App`'s accumulated route tuple,
|
|
22
|
-
*
|
|
23
|
-
* is widened back to
|
|
24
|
-
*
|
|
25
|
-
* rather than a chain — the tuple is erased and this type collapses to an
|
|
26
|
-
* untyped, string-indexed record.
|
|
22
|
+
* built by chained registrations or `app.registerRoutes([...])`. If the result
|
|
23
|
+
* is widened back to a bare `App` annotation, the tuple is intentionally erased
|
|
24
|
+
* and this type becomes a string-indexed record.
|
|
27
25
|
*/
|
|
28
26
|
export type ClientFor<A extends App> = {
|
|
29
27
|
[R in Extract<RoutesOf<A>, {
|
|
@@ -52,8 +50,15 @@ export interface ClientOptions {
|
|
|
52
50
|
/** Default headers merged into every request (per-call `input.headers` wins). */
|
|
53
51
|
headers?: Record<string, string>;
|
|
54
52
|
}
|
|
53
|
+
/** Options for {@link createInProcessClient}. */
|
|
54
|
+
export interface InProcessClientOptions {
|
|
55
|
+
/** Synthetic absolute origin used while constructing requests. Default: `http://daloy.local`. */
|
|
56
|
+
baseUrl?: string;
|
|
57
|
+
/** Default headers merged into every request. Per-call headers win. */
|
|
58
|
+
headers?: Record<string, string>;
|
|
59
|
+
}
|
|
55
60
|
/**
|
|
56
|
-
* Build a typed
|
|
61
|
+
* Build a typed fetch client whose methods are keyed by
|
|
57
62
|
* `operationId`. Parameters and response types are inferred from the same
|
|
58
63
|
* route definitions registered on `app`, so the client and server cannot
|
|
59
64
|
* drift apart at the type level.
|
|
@@ -66,11 +71,10 @@ export interface ClientOptions {
|
|
|
66
71
|
* from the OpenAPI document instead.
|
|
67
72
|
*
|
|
68
73
|
* @remarks
|
|
69
|
-
* The method signatures are inferred from the `App`'s accumulated route tuple
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* per-route types and yields an untyped client.
|
|
74
|
+
* The method signatures are inferred from the `App`'s accumulated route tuple.
|
|
75
|
+
* Chain registrations or compose independently exported contracts with
|
|
76
|
+
* `app.registerRoutes([...])`, and avoid widening the result to a bare `App`
|
|
77
|
+
* annotation because that deliberately discards the per-route tuple.
|
|
74
78
|
*
|
|
75
79
|
* @example
|
|
76
80
|
* ```ts
|
|
@@ -96,4 +100,17 @@ export interface ClientOptions {
|
|
|
96
100
|
* @since 0.1.0
|
|
97
101
|
*/
|
|
98
102
|
export declare function createClient<A extends App>(app: A, opts: ClientOptions): ClientFor<A>;
|
|
103
|
+
/**
|
|
104
|
+
* Build a typed client that dispatches directly through an App without
|
|
105
|
+
* opening a socket or binding a port.
|
|
106
|
+
*
|
|
107
|
+
* Requests still traverse the complete validation, middleware, security, and
|
|
108
|
+
* serialization pipeline through {@link "./app.js".App.fetch}.
|
|
109
|
+
*
|
|
110
|
+
* @param app - App whose registered route tuple drives the client surface.
|
|
111
|
+
* @param opts - Optional synthetic origin and default request headers.
|
|
112
|
+
* @returns A typed operation-id client backed by in-process dispatch.
|
|
113
|
+
* @since 1.0.0
|
|
114
|
+
*/
|
|
115
|
+
export declare function createInProcessClient<A extends App>(app: A, opts?: InProcessClientOptions): ClientFor<A>;
|
|
99
116
|
export {};
|
package/dist/client.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* can still be generated from the OpenAPI doc for non-TS clients).
|
|
11
11
|
*/
|
|
12
12
|
/**
|
|
13
|
-
* Build a typed
|
|
13
|
+
* Build a typed fetch client whose methods are keyed by
|
|
14
14
|
* `operationId`. Parameters and response types are inferred from the same
|
|
15
15
|
* route definitions registered on `app`, so the client and server cannot
|
|
16
16
|
* drift apart at the type level.
|
|
@@ -23,11 +23,10 @@
|
|
|
23
23
|
* from the OpenAPI document instead.
|
|
24
24
|
*
|
|
25
25
|
* @remarks
|
|
26
|
-
* The method signatures are inferred from the `App`'s accumulated route tuple
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* per-route types and yields an untyped client.
|
|
26
|
+
* The method signatures are inferred from the `App`'s accumulated route tuple.
|
|
27
|
+
* Chain registrations or compose independently exported contracts with
|
|
28
|
+
* `app.registerRoutes([...])`, and avoid widening the result to a bare `App`
|
|
29
|
+
* annotation because that deliberately discards the per-route tuple.
|
|
31
30
|
*
|
|
32
31
|
* @example
|
|
33
32
|
* ```ts
|
|
@@ -93,6 +92,30 @@ export function createClient(app, opts) {
|
|
|
93
92
|
}
|
|
94
93
|
return out;
|
|
95
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Build a typed client that dispatches directly through an App without
|
|
97
|
+
* opening a socket or binding a port.
|
|
98
|
+
*
|
|
99
|
+
* Requests still traverse the complete validation, middleware, security, and
|
|
100
|
+
* serialization pipeline through {@link "./app.js".App.fetch}.
|
|
101
|
+
*
|
|
102
|
+
* @param app - App whose registered route tuple drives the client surface.
|
|
103
|
+
* @param opts - Optional synthetic origin and default request headers.
|
|
104
|
+
* @returns A typed operation-id client backed by in-process dispatch.
|
|
105
|
+
* @since 1.0.0
|
|
106
|
+
*/
|
|
107
|
+
export function createInProcessClient(app, opts = {}) {
|
|
108
|
+
const clientOptions = {
|
|
109
|
+
baseUrl: opts.baseUrl ?? "http://daloy.local",
|
|
110
|
+
fetch: (input, init) => {
|
|
111
|
+
const request = input instanceof Request ? input : new Request(input, init);
|
|
112
|
+
return app.fetch(request);
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
if (opts.headers)
|
|
116
|
+
clientOptions.headers = opts.headers;
|
|
117
|
+
return createClient(app, clientOptions);
|
|
118
|
+
}
|
|
96
119
|
function safeJson(text) {
|
|
97
120
|
try {
|
|
98
121
|
return JSON.parse(text);
|
package/dist/combine.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* @since 0.19.0
|
|
7
7
|
*/
|
|
8
|
-
import type { Hooks, BaseContext } from "./types.js";
|
|
8
|
+
import type { Hooks, BaseContext, PreBodyContext } from "./types.js";
|
|
9
9
|
/**
|
|
10
10
|
* Run every supplied {@link Hooks} bundle in order, pipeline-style.
|
|
11
11
|
* Equivalent to passing the bundles to `app.use(...)` one after another,
|
|
@@ -13,7 +13,7 @@ import type { Hooks, BaseContext } from "./types.js";
|
|
|
13
13
|
* stack for the admin section"). All lifecycle phases compose:
|
|
14
14
|
*
|
|
15
15
|
* - `onRequest` / `onResponse` run in registration order.
|
|
16
|
-
* - `beforeHandle` / `onError` short-circuit on the first `Response`.
|
|
16
|
+
* - `preBody` / `beforeHandle` / `onError` short-circuit on the first `Response`.
|
|
17
17
|
* - `afterHandle` / `onSend` thread the value through every bundle.
|
|
18
18
|
*
|
|
19
19
|
* Symbol-keyed security markers (CORS / CSRF / session / secure-headers)
|
|
@@ -35,14 +35,14 @@ import type { Hooks, BaseContext } from "./types.js";
|
|
|
35
35
|
*/
|
|
36
36
|
export declare function every(...layers: Hooks[]): Hooks;
|
|
37
37
|
/**
|
|
38
|
-
* Run the supplied bundles until one of them passes its
|
|
38
|
+
* Run the supplied bundles until one of them passes its auth gate
|
|
39
39
|
* check without throwing. Useful for "this route accepts a bearer token
|
|
40
40
|
* OR a signed cookie OR an API key" patterns where any single proof of
|
|
41
41
|
* identity is enough.
|
|
42
42
|
*
|
|
43
43
|
* Semantics:
|
|
44
44
|
*
|
|
45
|
-
* - The bundles' `beforeHandle` hooks are awaited in order. The first one
|
|
45
|
+
* - The bundles' `preBody` or `beforeHandle` hooks are awaited in order. The first one
|
|
46
46
|
* that resolves without throwing wins; its `ctx` mutations (headers,
|
|
47
47
|
* `ctx.state`, etc.) are preserved.
|
|
48
48
|
* - When a bundle returns a `Response`, that response is treated as a
|
|
@@ -52,7 +52,7 @@ export declare function every(...layers: Hooks[]): Hooks;
|
|
|
52
52
|
* client gets a deterministic status code. Place the auth method whose
|
|
53
53
|
* `WWW-Authenticate` challenge you want clients to see first.
|
|
54
54
|
* - `afterHandle`, `onSend`, `onResponse`, and `onError` from every bundle
|
|
55
|
-
* still compose normally — `some()` only changes the
|
|
55
|
+
* still compose normally — `some()` only changes the auth-gate
|
|
56
56
|
* evaluation strategy.
|
|
57
57
|
*
|
|
58
58
|
* @example
|
|
@@ -63,8 +63,8 @@ export declare function every(...layers: Hooks[]): Hooks;
|
|
|
63
63
|
* ));
|
|
64
64
|
* ```
|
|
65
65
|
*
|
|
66
|
-
* @param layers Candidate hook bundles; the first
|
|
67
|
-
* @returns A merged {@link Hooks} bundle with
|
|
66
|
+
* @param layers Candidate hook bundles; the first auth gate that passes wins.
|
|
67
|
+
* @returns A merged {@link Hooks} bundle with an OR-style auth-gate strategy.
|
|
68
68
|
* @since 0.19.0
|
|
69
69
|
*/
|
|
70
70
|
export declare function some(...layers: Hooks[]): Hooks;
|
|
@@ -76,7 +76,7 @@ export declare function some(...layers: Hooks[]): Hooks;
|
|
|
76
76
|
*
|
|
77
77
|
* @since 0.19.0
|
|
78
78
|
*/
|
|
79
|
-
export type ExceptPredicate = string | string[] | ((ctx: BaseContext<any, any>) => boolean | Promise<boolean>);
|
|
79
|
+
export type ExceptPredicate = string | string[] | ((ctx: PreBodyContext<any> | BaseContext<any, any>) => boolean | Promise<boolean>);
|
|
80
80
|
/**
|
|
81
81
|
* Run a hook bundle on every request EXCEPT those matching `when`. The
|
|
82
82
|
* canonical use is "apply auth everywhere except the public endpoints":
|
|
@@ -89,15 +89,15 @@ export type ExceptPredicate = string | string[] | ((ctx: BaseContext<any, any>)
|
|
|
89
89
|
* ));
|
|
90
90
|
* ```
|
|
91
91
|
*
|
|
92
|
-
*
|
|
92
|
+
* The `preBody` and `beforeHandle` phases are gated — the surrounding
|
|
93
93
|
* `onRequest`/`afterHandle`/`onSend`/`onResponse` phases still run so
|
|
94
94
|
* shared concerns like request-id propagation are not accidentally
|
|
95
95
|
* exempted. Wrap each bundle with {@link except} individually when you
|
|
96
96
|
* need to gate other phases.
|
|
97
97
|
*
|
|
98
98
|
* @param when Paths or predicate ({@link ExceptPredicate}) that exempt a request.
|
|
99
|
-
* @param hooks The hook bundle whose `beforeHandle`
|
|
100
|
-
* @returns A {@link Hooks} bundle whose
|
|
99
|
+
* @param hooks The hook bundle whose `preBody` and `beforeHandle` gates are skipped on a match.
|
|
100
|
+
* @returns A {@link Hooks} bundle whose request gates are controlled by `when`.
|
|
101
101
|
* @throws Error at composition time if a string pattern does not start with `/`.
|
|
102
102
|
* @since 0.19.0
|
|
103
103
|
*/
|
package/dist/combine.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* @since 0.19.0
|
|
7
7
|
*/
|
|
8
|
+
import { _mergePreBodyWithEarlyRejections, EARLY_REJECTION_HOOK_MARKER } from "./middleware.js";
|
|
8
9
|
/**
|
|
9
10
|
* Run every supplied {@link Hooks} bundle in order, pipeline-style.
|
|
10
11
|
* Equivalent to passing the bundles to `app.use(...)` one after another,
|
|
@@ -12,7 +13,7 @@
|
|
|
12
13
|
* stack for the admin section"). All lifecycle phases compose:
|
|
13
14
|
*
|
|
14
15
|
* - `onRequest` / `onResponse` run in registration order.
|
|
15
|
-
* - `beforeHandle` / `onError` short-circuit on the first `Response`.
|
|
16
|
+
* - `preBody` / `beforeHandle` / `onError` short-circuit on the first `Response`.
|
|
16
17
|
* - `afterHandle` / `onSend` thread the value through every bundle.
|
|
17
18
|
*
|
|
18
19
|
* Symbol-keyed security markers (CORS / CSRF / session / secure-headers)
|
|
@@ -36,14 +37,14 @@ export function every(...layers) {
|
|
|
36
37
|
return mergeCombineHooks(layers);
|
|
37
38
|
}
|
|
38
39
|
/**
|
|
39
|
-
* Run the supplied bundles until one of them passes its
|
|
40
|
+
* Run the supplied bundles until one of them passes its auth gate
|
|
40
41
|
* check without throwing. Useful for "this route accepts a bearer token
|
|
41
42
|
* OR a signed cookie OR an API key" patterns where any single proof of
|
|
42
43
|
* identity is enough.
|
|
43
44
|
*
|
|
44
45
|
* Semantics:
|
|
45
46
|
*
|
|
46
|
-
* - The bundles' `beforeHandle` hooks are awaited in order. The first one
|
|
47
|
+
* - The bundles' `preBody` or `beforeHandle` hooks are awaited in order. The first one
|
|
47
48
|
* that resolves without throwing wins; its `ctx` mutations (headers,
|
|
48
49
|
* `ctx.state`, etc.) are preserved.
|
|
49
50
|
* - When a bundle returns a `Response`, that response is treated as a
|
|
@@ -53,7 +54,7 @@ export function every(...layers) {
|
|
|
53
54
|
* client gets a deterministic status code. Place the auth method whose
|
|
54
55
|
* `WWW-Authenticate` challenge you want clients to see first.
|
|
55
56
|
* - `afterHandle`, `onSend`, `onResponse`, and `onError` from every bundle
|
|
56
|
-
* still compose normally — `some()` only changes the
|
|
57
|
+
* still compose normally — `some()` only changes the auth-gate
|
|
57
58
|
* evaluation strategy.
|
|
58
59
|
*
|
|
59
60
|
* @example
|
|
@@ -64,45 +65,65 @@ export function every(...layers) {
|
|
|
64
65
|
* ));
|
|
65
66
|
* ```
|
|
66
67
|
*
|
|
67
|
-
* @param layers Candidate hook bundles; the first
|
|
68
|
-
* @returns A merged {@link Hooks} bundle with
|
|
68
|
+
* @param layers Candidate hook bundles; the first auth gate that passes wins.
|
|
69
|
+
* @returns A merged {@link Hooks} bundle with an OR-style auth-gate strategy.
|
|
69
70
|
* @since 0.19.0
|
|
70
71
|
*/
|
|
71
72
|
export function some(...layers) {
|
|
72
73
|
if (layers.length === 0)
|
|
73
74
|
return {};
|
|
74
|
-
const stripped = layers.map(({ beforeHandle: _b, ...rest }) => rest);
|
|
75
|
+
const stripped = layers.map(({ preBody: _p, beforeHandle: _b, ...rest }) => rest);
|
|
75
76
|
const base = mergeCombineHooks(stripped);
|
|
76
|
-
const
|
|
77
|
+
const preBodyCandidates = layers
|
|
78
|
+
.map((h) => h.preBody)
|
|
79
|
+
.filter((f) => typeof f === "function");
|
|
80
|
+
const beforeHandleCandidates = layers
|
|
77
81
|
.map((h) => h.beforeHandle)
|
|
78
82
|
.filter((f) => typeof f === "function");
|
|
83
|
+
const usePreBody = preBodyCandidates.length > 0 && beforeHandleCandidates.length === 0;
|
|
84
|
+
const candidates = usePreBody
|
|
85
|
+
? preBodyCandidates
|
|
86
|
+
: layers.flatMap((hooks) => {
|
|
87
|
+
if (hooks.preBody && hooks.beforeHandle) {
|
|
88
|
+
return [
|
|
89
|
+
async (ctx) => {
|
|
90
|
+
const early = await hooks.preBody(ctx);
|
|
91
|
+
return early instanceof Response ? early : hooks.beforeHandle(ctx);
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
const gate = hooks.preBody ?? hooks.beforeHandle;
|
|
96
|
+
return gate ? [gate] : [];
|
|
97
|
+
});
|
|
79
98
|
if (candidates.length === 0)
|
|
80
99
|
return base;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const r = await fn(ctx);
|
|
88
|
-
if (r instanceof Response) {
|
|
89
|
-
// Treat as a denial — try the next layer.
|
|
90
|
-
if (!firstFailure)
|
|
91
|
-
firstFailure = { kind: "response", res: r };
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
// Undefined = pass; bundle accepts the request.
|
|
95
|
-
return undefined;
|
|
96
|
-
}
|
|
97
|
-
catch (err) {
|
|
100
|
+
const runCandidates = async (ctx) => {
|
|
101
|
+
let firstFailure;
|
|
102
|
+
for (const fn of candidates) {
|
|
103
|
+
try {
|
|
104
|
+
const r = await fn(ctx);
|
|
105
|
+
if (r instanceof Response) {
|
|
98
106
|
if (!firstFailure)
|
|
99
|
-
firstFailure = { kind: "
|
|
107
|
+
firstFailure = { kind: "response", res: r };
|
|
108
|
+
continue;
|
|
100
109
|
}
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
if (!firstFailure)
|
|
114
|
+
firstFailure = { kind: "throw", err };
|
|
101
115
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
116
|
+
}
|
|
117
|
+
if (firstFailure?.kind === "response")
|
|
118
|
+
return firstFailure.res;
|
|
119
|
+
throw firstFailure.err;
|
|
120
|
+
};
|
|
121
|
+
if (usePreBody) {
|
|
122
|
+
return { ...base, preBody: runCandidates };
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
...base,
|
|
126
|
+
beforeHandle: runCandidates,
|
|
106
127
|
};
|
|
107
128
|
}
|
|
108
129
|
/**
|
|
@@ -117,31 +138,41 @@ export function some(...layers) {
|
|
|
117
138
|
* ));
|
|
118
139
|
* ```
|
|
119
140
|
*
|
|
120
|
-
*
|
|
141
|
+
* The `preBody` and `beforeHandle` phases are gated — the surrounding
|
|
121
142
|
* `onRequest`/`afterHandle`/`onSend`/`onResponse` phases still run so
|
|
122
143
|
* shared concerns like request-id propagation are not accidentally
|
|
123
144
|
* exempted. Wrap each bundle with {@link except} individually when you
|
|
124
145
|
* need to gate other phases.
|
|
125
146
|
*
|
|
126
147
|
* @param when Paths or predicate ({@link ExceptPredicate}) that exempt a request.
|
|
127
|
-
* @param hooks The hook bundle whose `beforeHandle`
|
|
128
|
-
* @returns A {@link Hooks} bundle whose
|
|
148
|
+
* @param hooks The hook bundle whose `preBody` and `beforeHandle` gates are skipped on a match.
|
|
149
|
+
* @returns A {@link Hooks} bundle whose request gates are controlled by `when`.
|
|
129
150
|
* @throws Error at composition time if a string pattern does not start with `/`.
|
|
130
151
|
* @since 0.19.0
|
|
131
152
|
*/
|
|
132
153
|
export function except(when, hooks) {
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
if (!original)
|
|
154
|
+
const earlyRejectionHooks = hooks[EARLY_REJECTION_HOOK_MARKER];
|
|
155
|
+
if (!hooks.preBody && !hooks.beforeHandle && !Array.isArray(earlyRejectionHooks))
|
|
136
156
|
return hooks;
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
157
|
+
const matches = compileExceptMatcher(when);
|
|
158
|
+
const wrapped = { ...hooks };
|
|
159
|
+
if (Array.isArray(earlyRejectionHooks)) {
|
|
160
|
+
wrapped[EARLY_REJECTION_HOOK_MARKER] =
|
|
161
|
+
earlyRejectionHooks.map((hook) => {
|
|
162
|
+
if (typeof hook !== "function")
|
|
163
|
+
return hook;
|
|
164
|
+
return async (ctx) => (await matches(ctx)) ? undefined : hook(ctx);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
if (hooks.preBody) {
|
|
168
|
+
const original = hooks.preBody;
|
|
169
|
+
wrapped.preBody = async (ctx) => ((await matches(ctx)) ? undefined : original(ctx));
|
|
170
|
+
}
|
|
171
|
+
if (hooks.beforeHandle) {
|
|
172
|
+
const original = hooks.beforeHandle;
|
|
173
|
+
wrapped.beforeHandle = async (ctx) => ((await matches(ctx)) ? undefined : original(ctx));
|
|
174
|
+
}
|
|
175
|
+
return wrapped;
|
|
145
176
|
}
|
|
146
177
|
function compileExceptMatcher(when) {
|
|
147
178
|
if (typeof when === "function") {
|
|
@@ -175,9 +206,7 @@ function compilePathPattern(pattern) {
|
|
|
175
206
|
return (path) => regex.test(path);
|
|
176
207
|
}
|
|
177
208
|
function mergeCombineHooks(layers) {
|
|
178
|
-
const pick = (key) => layers
|
|
179
|
-
.map((h) => h[key])
|
|
180
|
-
.filter((f) => typeof f === "function");
|
|
209
|
+
const pick = (key) => layers.map((h) => h[key]).filter((f) => typeof f === "function");
|
|
181
210
|
const merged = {};
|
|
182
211
|
const onRequest = pick("onRequest");
|
|
183
212
|
if (onRequest.length > 0) {
|
|
@@ -186,6 +215,9 @@ function mergeCombineHooks(layers) {
|
|
|
186
215
|
await fn(req);
|
|
187
216
|
};
|
|
188
217
|
}
|
|
218
|
+
const preBody = _mergePreBodyWithEarlyRejections(layers);
|
|
219
|
+
if (preBody !== undefined)
|
|
220
|
+
merged.preBody = preBody;
|
|
189
221
|
const beforeHandle = pick("beforeHandle");
|
|
190
222
|
if (beforeHandle.length > 0) {
|
|
191
223
|
merged.beforeHandle = async (ctx) => {
|
|
@@ -245,6 +277,17 @@ function mergeCombineHooks(layers) {
|
|
|
245
277
|
for (const hooks of layers) {
|
|
246
278
|
const record = hooks;
|
|
247
279
|
for (const key of Object.getOwnPropertySymbols(record)) {
|
|
280
|
+
if (key === EARLY_REJECTION_HOOK_MARKER && merged.preBody !== undefined)
|
|
281
|
+
continue;
|
|
282
|
+
if (key === EARLY_REJECTION_HOOK_MARKER) {
|
|
283
|
+
const existing = merged[key];
|
|
284
|
+
const incoming = record[key];
|
|
285
|
+
merged[key] = [
|
|
286
|
+
...(Array.isArray(existing) ? existing : []),
|
|
287
|
+
...(Array.isArray(incoming) ? incoming : []),
|
|
288
|
+
];
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
248
291
|
if (!(key in merged)) {
|
|
249
292
|
merged[key] = record[key];
|
|
250
293
|
}
|
package/dist/docs.d.ts
CHANGED
|
@@ -233,16 +233,12 @@ export interface RedocConfiguration {
|
|
|
233
233
|
};
|
|
234
234
|
}
|
|
235
235
|
/**
|
|
236
|
-
* Override CDN URLs and
|
|
237
|
-
* UI assets.
|
|
236
|
+
* Override the version-pinned CDN URLs and Subresource Integrity (SRI) hashes
|
|
237
|
+
* used by the docs UI assets.
|
|
238
238
|
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
* hash. SRI is only meaningful against a **version-pinned** URL
|
|
243
|
-
* (e.g. `…/@scalar/api-reference@1.25.0`); pair each integrity hash with a
|
|
244
|
-
* pinned `*Url`, since the framework's default URLs intentionally track the
|
|
245
|
-
* latest upstream release and therefore cannot carry a stable hash.
|
|
239
|
+
* Defaults use exact upstream versions with matching SHA-384 digests. A custom
|
|
240
|
+
* URL without a custom `*Integrity` value intentionally omits SRI; pair URL
|
|
241
|
+
* overrides with hashes whose exact bytes you control or have verified.
|
|
246
242
|
*
|
|
247
243
|
* @since 0.37.0
|
|
248
244
|
*/
|
package/dist/docs.js
CHANGED
|
@@ -8,6 +8,18 @@
|
|
|
8
8
|
* (You can self-host the assets if your CSP forbids CDNs.)
|
|
9
9
|
*/
|
|
10
10
|
const JSDELIVR_ORIGIN = "https://cdn.jsdelivr.net";
|
|
11
|
+
const DEFAULT_SCALAR_SCRIPT_URL = `${JSDELIVR_ORIGIN}/npm/@scalar/api-reference@1.62.5`;
|
|
12
|
+
const DEFAULT_SCALAR_SCRIPT_INTEGRITY = "sha384-jVBCKhcCfx34USN27x4iQK1SBNdL/HxKq3KuBAxTS4WPaP5w80K4fjpwB+DezJL5";
|
|
13
|
+
const DEFAULT_SWAGGER_CSS_URL = `${JSDELIVR_ORIGIN}/npm/swagger-ui-dist@5.32.8/swagger-ui.css`;
|
|
14
|
+
const DEFAULT_SWAGGER_CSS_INTEGRITY = "sha384-9Q2fpS+xeS4ffJy6CagnwoUl+4ldAYhOs9pgZuEKxypVModhmZFzeMlvVsAjf7uT";
|
|
15
|
+
const DEFAULT_SWAGGER_BUNDLE_URL = `${JSDELIVR_ORIGIN}/npm/swagger-ui-dist@5.32.8/swagger-ui-bundle.js`;
|
|
16
|
+
const DEFAULT_SWAGGER_BUNDLE_INTEGRITY = "sha384-IKpAWwsTL0pcw7/Amtnt2eXF4P1BK64WNuY2E/RG15SWLUW5HXzFuyqCSAr/DP8C";
|
|
17
|
+
const DEFAULT_REDOC_SCRIPT_URL = `${JSDELIVR_ORIGIN}/npm/redoc@2.5.3/bundles/redoc.standalone.js`;
|
|
18
|
+
const DEFAULT_REDOC_SCRIPT_INTEGRITY = "sha384-xiEssMQFSpSfLbzRZCGfxxIM5QDb2DTrU6vyoZdp2sV1L6pmOMy6MpTtUoLbpC96";
|
|
19
|
+
const DEFAULT_ASYNCAPI_SCRIPT_URL = `${JSDELIVR_ORIGIN}/npm/@asyncapi/react-component@3.1.4/browser/standalone/index.js`;
|
|
20
|
+
const DEFAULT_ASYNCAPI_SCRIPT_INTEGRITY = "sha384-ZI+8twyvBIiWAquvsA8HFRvWjFn7l9/JlCHI3sekdQ6s7xK8ZDT6TDQOickRDQ0t";
|
|
21
|
+
const DEFAULT_ASYNCAPI_STYLE_URL = `${JSDELIVR_ORIGIN}/npm/@asyncapi/react-component@3.1.4/styles/default.min.css`;
|
|
22
|
+
const DEFAULT_ASYNCAPI_STYLE_INTEGRITY = "sha384-hcBf581bZwhXX8SyfsmPFkODqlIruk2b6gfX+b2WuK+am42GxstWJlJLiosKZoiL";
|
|
11
23
|
/**
|
|
12
24
|
* Matches a single Subresource Integrity digest: a `sha256-`/`sha384-`/
|
|
13
25
|
* `sha512-` prefix followed by standard base64 (with up to two `=` pads).
|
|
@@ -53,8 +65,10 @@ function integrityAttr(integrity, crossOrigin) {
|
|
|
53
65
|
export function scalarHtml(opts) {
|
|
54
66
|
const title = escapeHtml(opts.title ?? "API Reference");
|
|
55
67
|
const url = escapeHtml(opts.specUrl);
|
|
56
|
-
const
|
|
57
|
-
const
|
|
68
|
+
const usesDefaultScript = opts.assets?.scalarScriptUrl === undefined;
|
|
69
|
+
const scriptUrl = escapeHtml(opts.assets?.scalarScriptUrl ?? DEFAULT_SCALAR_SCRIPT_URL);
|
|
70
|
+
const scriptSri = integrityAttr(opts.assets?.scalarScriptIntegrity ??
|
|
71
|
+
(usesDefaultScript ? DEFAULT_SCALAR_SCRIPT_INTEGRITY : undefined), opts.assets?.crossOrigin);
|
|
58
72
|
const nonce = nonceAttr(opts.scriptNonce);
|
|
59
73
|
const configuration = scalarConfigurationAttr(opts.specUrl, opts.configuration);
|
|
60
74
|
return `<!doctype html>
|
|
@@ -82,10 +96,14 @@ ${docsAuthLauncherHtml(opts.auth, opts.scriptNonce)}
|
|
|
82
96
|
*/
|
|
83
97
|
export function swaggerUiHtml(opts) {
|
|
84
98
|
const title = escapeHtml(opts.title ?? "API Docs");
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
const
|
|
99
|
+
const usesDefaultCss = opts.assets?.swaggerUiCssUrl === undefined;
|
|
100
|
+
const usesDefaultBundle = opts.assets?.swaggerUiBundleUrl === undefined;
|
|
101
|
+
const cssUrl = escapeHtml(opts.assets?.swaggerUiCssUrl ?? DEFAULT_SWAGGER_CSS_URL);
|
|
102
|
+
const bundleUrl = escapeHtml(opts.assets?.swaggerUiBundleUrl ?? DEFAULT_SWAGGER_BUNDLE_URL);
|
|
103
|
+
const cssSri = integrityAttr(opts.assets?.swaggerUiCssIntegrity ??
|
|
104
|
+
(usesDefaultCss ? DEFAULT_SWAGGER_CSS_INTEGRITY : undefined), opts.assets?.crossOrigin);
|
|
105
|
+
const bundleSri = integrityAttr(opts.assets?.swaggerUiBundleIntegrity ??
|
|
106
|
+
(usesDefaultBundle ? DEFAULT_SWAGGER_BUNDLE_INTEGRITY : undefined), opts.assets?.crossOrigin);
|
|
89
107
|
const nonce = nonceAttr(opts.scriptNonce);
|
|
90
108
|
const configuration = jsonForScript({
|
|
91
109
|
persistAuthorization: true,
|
|
@@ -125,8 +143,10 @@ ${docsAuthLauncherHtml(opts.auth, opts.scriptNonce)}
|
|
|
125
143
|
*/
|
|
126
144
|
export function redocHtml(opts) {
|
|
127
145
|
const title = escapeHtml(opts.title ?? "API Docs");
|
|
128
|
-
const
|
|
129
|
-
const
|
|
146
|
+
const usesDefaultScript = opts.assets?.redocScriptUrl === undefined;
|
|
147
|
+
const scriptUrl = escapeHtml(opts.assets?.redocScriptUrl ?? DEFAULT_REDOC_SCRIPT_URL);
|
|
148
|
+
const scriptSri = integrityAttr(opts.assets?.redocScriptIntegrity ??
|
|
149
|
+
(usesDefaultScript ? DEFAULT_REDOC_SCRIPT_INTEGRITY : undefined), opts.assets?.crossOrigin);
|
|
130
150
|
const nonce = nonceAttr(opts.scriptNonce);
|
|
131
151
|
const specArg = jsonForScript(opts.specUrl);
|
|
132
152
|
const optionsArg = jsonForScript(opts.configuration ?? {});
|
|
@@ -163,12 +183,14 @@ ${docsAuthLauncherHtml(opts.auth, opts.scriptNonce)}
|
|
|
163
183
|
*/
|
|
164
184
|
export function asyncapiHtml(opts) {
|
|
165
185
|
const title = escapeHtml(opts.title ?? "AsyncAPI");
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
const scriptSri = integrityAttr(opts.assets?.asyncapiScriptIntegrity
|
|
171
|
-
|
|
186
|
+
const usesDefaultScript = opts.assets?.asyncapiScriptUrl === undefined;
|
|
187
|
+
const usesDefaultStyle = opts.assets?.asyncapiStyleUrl === undefined;
|
|
188
|
+
const scriptUrl = escapeHtml(opts.assets?.asyncapiScriptUrl ?? DEFAULT_ASYNCAPI_SCRIPT_URL);
|
|
189
|
+
const styleUrl = escapeHtml(opts.assets?.asyncapiStyleUrl ?? DEFAULT_ASYNCAPI_STYLE_URL);
|
|
190
|
+
const scriptSri = integrityAttr(opts.assets?.asyncapiScriptIntegrity ??
|
|
191
|
+
(usesDefaultScript ? DEFAULT_ASYNCAPI_SCRIPT_INTEGRITY : undefined), opts.assets?.crossOrigin);
|
|
192
|
+
const styleSri = integrityAttr(opts.assets?.asyncapiStyleIntegrity ??
|
|
193
|
+
(usesDefaultStyle ? DEFAULT_ASYNCAPI_STYLE_INTEGRITY : undefined), opts.assets?.crossOrigin);
|
|
172
194
|
const nonce = nonceAttr(opts.scriptNonce);
|
|
173
195
|
const specArg = jsonForScript(opts.specUrl);
|
|
174
196
|
const configArg = jsonForScript(opts.configuration ?? { show: { sidebar: true, errors: true } });
|
package/dist/idempotency.js
CHANGED
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
* @since 0.37.0
|
|
33
33
|
*/
|
|
34
34
|
import { BadRequestError, ConflictError, HttpError } from "./errors.js";
|
|
35
|
+
import { markSchemaValidatedResponse } from "./internal-response.js";
|
|
35
36
|
const enc = new TextEncoder();
|
|
36
37
|
/** Internal `ctx.state` key carrying the reservation between hooks. */
|
|
37
38
|
const PENDING_STATE_KEY = "__idempotencyPending";
|
|
@@ -200,7 +201,7 @@ function buildReplayResponse(stored, replayHeaderName) {
|
|
|
200
201
|
headers.set(name, value);
|
|
201
202
|
headers.set(replayHeaderName, "true");
|
|
202
203
|
const body = stored.body ? base64ToBytes(stored.body) : null;
|
|
203
|
-
return new Response(body, { status: stored.status, headers });
|
|
204
|
+
return markSchemaValidatedResponse(new Response(body, { status: stored.status, headers }));
|
|
204
205
|
}
|
|
205
206
|
// ---------- Middleware ----------
|
|
206
207
|
/**
|