@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/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 = { description: spec.description };
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
- * which is built up as you **chain** `app.route(...)` calls. If the `App` type
23
- * is widened back to its bare default — e.g. a `const app: App` annotation, a
24
- * `: App` factory return type, or registering routes as separate statements
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, in-process fetch client whose methods are keyed by
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
- * so chain your `app.route(...)` registrations and let TypeScript infer the
71
- * variable's type. A widening `const app: App` annotation, a `: App` factory
72
- * return type, or registering routes as separate statements erases the
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, in-process fetch client whose methods are keyed by
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
- * so chain your `app.route(...)` registrations and let TypeScript infer the
28
- * variable's type. A widening `const app: App` annotation, a `: App` factory
29
- * return type, or registering routes as separate statements erases the
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 `beforeHandle`
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 `beforeHandle`
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 `beforeHandle` that passes wins.
67
- * @returns A merged {@link Hooks} bundle with the OR-style `beforeHandle` strategy.
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
- * Only the `beforeHandle` phase is gated — the surrounding
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` is skipped on a match.
100
- * @returns A {@link Hooks} bundle whose `beforeHandle` is gated by `when`.
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 `beforeHandle`
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 `beforeHandle`
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 `beforeHandle` that passes wins.
68
- * @returns A merged {@link Hooks} bundle with the OR-style `beforeHandle` strategy.
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 candidates = layers
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
- return {
82
- ...base,
83
- async beforeHandle(ctx) {
84
- let firstFailure;
85
- for (const fn of candidates) {
86
- try {
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: "throw", err };
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
- if (firstFailure?.kind === "response")
103
- return firstFailure.res;
104
- throw firstFailure.err;
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
- * Only the `beforeHandle` phase is gated — the surrounding
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` is skipped on a match.
128
- * @returns A {@link Hooks} bundle whose `beforeHandle` is gated by `when`.
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 matches = compileExceptMatcher(when);
134
- const original = hooks.beforeHandle;
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
- return {
138
- ...hooks,
139
- async beforeHandle(ctx) {
140
- if (await matches(ctx))
141
- return undefined;
142
- return original(ctx);
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 pin Subresource Integrity (SRI) hashes for the docs
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
- * Supplying an `*Integrity` value emits an `integrity="…"` attribute plus a
240
- * `crossorigin` attribute on the matching `<script>` / `<link>` tag so the
241
- * browser refuses to execute a CDN asset whose bytes don't match the pinned
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 scriptUrl = escapeHtml(opts.assets?.scalarScriptUrl ?? `${JSDELIVR_ORIGIN}/npm/@scalar/api-reference`);
57
- const scriptSri = integrityAttr(opts.assets?.scalarScriptIntegrity, opts.assets?.crossOrigin);
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 cssUrl = escapeHtml(opts.assets?.swaggerUiCssUrl ?? `${JSDELIVR_ORIGIN}/npm/swagger-ui-dist/swagger-ui.css`);
86
- const bundleUrl = escapeHtml(opts.assets?.swaggerUiBundleUrl ?? `${JSDELIVR_ORIGIN}/npm/swagger-ui-dist/swagger-ui-bundle.js`);
87
- const cssSri = integrityAttr(opts.assets?.swaggerUiCssIntegrity, opts.assets?.crossOrigin);
88
- const bundleSri = integrityAttr(opts.assets?.swaggerUiBundleIntegrity, opts.assets?.crossOrigin);
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 scriptUrl = escapeHtml(opts.assets?.redocScriptUrl ?? `${JSDELIVR_ORIGIN}/npm/redoc/bundles/redoc.standalone.js`);
129
- const scriptSri = integrityAttr(opts.assets?.redocScriptIntegrity, opts.assets?.crossOrigin);
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 scriptUrl = escapeHtml(opts.assets?.asyncapiScriptUrl ??
167
- `${JSDELIVR_ORIGIN}/npm/@asyncapi/react-component/browser/standalone/index.js`);
168
- const styleUrl = escapeHtml(opts.assets?.asyncapiStyleUrl ??
169
- `${JSDELIVR_ORIGIN}/npm/@asyncapi/react-component/styles/default.min.css`);
170
- const scriptSri = integrityAttr(opts.assets?.asyncapiScriptIntegrity, opts.assets?.crossOrigin);
171
- const styleSri = integrityAttr(opts.assets?.asyncapiStyleIntegrity, opts.assets?.crossOrigin);
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 } });
@@ -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
  /**