@daloyjs/core 1.0.0-rc.3 → 1.0.0-rc.5
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 +103 -41
- package/dist/adapters/bun.d.ts +20 -2
- package/dist/adapters/bun.js +41 -5
- package/dist/adapters/deno.js +24 -7
- package/dist/adapters/lambda.d.ts +59 -2
- package/dist/adapters/lambda.js +136 -20
- package/dist/adapters/node.d.ts +8 -1
- package/dist/adapters/node.js +104 -19
- package/dist/app.d.ts +131 -11
- package/dist/app.js +305 -217
- package/dist/bot-guard.js +30 -3
- package/dist/cli.js +41 -1
- package/dist/client.d.ts +64 -18
- package/dist/client.js +36 -6
- package/dist/combine.d.ts +11 -11
- package/dist/combine.js +90 -47
- package/dist/compression.d.ts +9 -0
- package/dist/compression.js +72 -1
- package/dist/conn-info.d.ts +5 -2
- package/dist/conn-info.js +5 -2
- package/dist/docs.d.ts +5 -9
- package/dist/docs.js +36 -14
- package/dist/errors.d.ts +12 -3
- package/dist/errors.js +12 -3
- package/dist/fetch-guard.d.ts +27 -19
- package/dist/fetch-guard.js +50 -8
- package/dist/http-signatures.d.ts +4 -1
- package/dist/http-signatures.js +13 -1
- package/dist/idempotency.js +2 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +3 -3
- 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/logger.d.ts +45 -0
- package/dist/logger.js +137 -0
- package/dist/mcp.js +21 -15
- package/dist/middleware.d.ts +48 -7
- package/dist/middleware.js +129 -43
- package/dist/mtls.d.ts +6 -5
- package/dist/mtls.js +8 -9
- package/dist/openapi.js +1 -1
- package/dist/pagination.js +4 -1
- package/dist/response-cache.js +2 -1
- package/dist/router.d.ts +2 -2
- package/dist/router.js +24 -9
- package/dist/safe-redirect.d.ts +9 -2
- package/dist/safe-redirect.js +29 -4
- package/dist/sbom.cdx.json +9 -9
- package/dist/sbom.spdx.json +5 -5
- package/dist/security.d.ts +62 -0
- package/dist/security.js +220 -15
- package/dist/session.d.ts +13 -2
- package/dist/session.js +111 -17
- package/dist/tenancy.d.ts +2 -2
- package/dist/time-claims.js +3 -1
- package/dist/types.d.ts +85 -20
- package/dist/types.js +16 -1
- package/dist/waf.js +86 -26
- package/package.json +11 -4
package/dist/compression.js
CHANGED
|
@@ -217,6 +217,57 @@ function normalizeOptionTokens(values, optionName) {
|
|
|
217
217
|
}
|
|
218
218
|
return Object.freeze(normalized);
|
|
219
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* Read a response body up to `maxBytes`. Returns `null` if the stream
|
|
222
|
+
* exceeds the cap (body is cancelled; caller should leave the response
|
|
223
|
+
* uncompressed). Returns an empty buffer when there is no body.
|
|
224
|
+
*
|
|
225
|
+
* @param res - Response whose body will be consumed (pass a clone).
|
|
226
|
+
* @param maxBytes - Inclusive upper bound on buffered size.
|
|
227
|
+
*/
|
|
228
|
+
async function readBodyUpTo(res, maxBytes) {
|
|
229
|
+
if (!res.body)
|
|
230
|
+
return new Uint8Array(0);
|
|
231
|
+
const reader = res.body.getReader();
|
|
232
|
+
const chunks = [];
|
|
233
|
+
let total = 0;
|
|
234
|
+
try {
|
|
235
|
+
// eslint-disable-next-line no-constant-condition
|
|
236
|
+
while (true) {
|
|
237
|
+
const { done, value } = await reader.read();
|
|
238
|
+
if (done)
|
|
239
|
+
break;
|
|
240
|
+
if (!value || value.byteLength === 0)
|
|
241
|
+
continue;
|
|
242
|
+
total += value.byteLength;
|
|
243
|
+
if (total > maxBytes) {
|
|
244
|
+
await reader.cancel();
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
chunks.push(value);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
try {
|
|
252
|
+
await reader.cancel();
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
/* ignore */
|
|
256
|
+
}
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
if (chunks.length === 0)
|
|
260
|
+
return new Uint8Array(0);
|
|
261
|
+
if (chunks.length === 1)
|
|
262
|
+
return chunks[0];
|
|
263
|
+
const out = new Uint8Array(total);
|
|
264
|
+
let offset = 0;
|
|
265
|
+
for (const c of chunks) {
|
|
266
|
+
out.set(c, offset);
|
|
267
|
+
offset += c.byteLength;
|
|
268
|
+
}
|
|
269
|
+
return out;
|
|
270
|
+
}
|
|
220
271
|
async function compressBytes(bytes, encoding) {
|
|
221
272
|
const Stream = globalThis.CompressionStream;
|
|
222
273
|
const cs = new Stream(encoding);
|
|
@@ -297,6 +348,16 @@ export function compression(opts = {}) {
|
|
|
297
348
|
minimumSize > 2 ** 31 - 1) {
|
|
298
349
|
throw new TypeError("compression(): `minimumSize` must be a finite non-negative integer.");
|
|
299
350
|
}
|
|
351
|
+
const maxCompressibleBytes = opts.maxCompressibleBytes === undefined ? 1_048_576 : opts.maxCompressibleBytes;
|
|
352
|
+
if (!Number.isFinite(maxCompressibleBytes) ||
|
|
353
|
+
!Number.isInteger(maxCompressibleBytes) ||
|
|
354
|
+
maxCompressibleBytes <= 0 ||
|
|
355
|
+
maxCompressibleBytes > 2 ** 31 - 1) {
|
|
356
|
+
throw new TypeError("compression(): `maxCompressibleBytes` must be a positive integer <= 2**31-1.");
|
|
357
|
+
}
|
|
358
|
+
if (minimumSize > maxCompressibleBytes) {
|
|
359
|
+
throw new TypeError("compression(): `minimumSize` must not exceed `maxCompressibleBytes`.");
|
|
360
|
+
}
|
|
300
361
|
const serverPreferred = opts.encodings && opts.encodings.length > 0
|
|
301
362
|
? Object.freeze([...opts.encodings])
|
|
302
363
|
: Object.freeze(["br", "gzip", "deflate"]);
|
|
@@ -338,7 +399,17 @@ export function compression(opts = {}) {
|
|
|
338
399
|
const chosen = pickEncoding(accept, serverPreferred, runtimeSupported);
|
|
339
400
|
if (!chosen)
|
|
340
401
|
return undefined;
|
|
341
|
-
|
|
402
|
+
// Fast-path skip when Content-Length already exceeds the compress cap
|
|
403
|
+
// (avoids buffering a known-huge body just to discard it).
|
|
404
|
+
const declaredLength = res.headers.get("content-length");
|
|
405
|
+
if (declaredLength !== null) {
|
|
406
|
+
const n = Number(declaredLength);
|
|
407
|
+
if (Number.isFinite(n) && n > maxCompressibleBytes)
|
|
408
|
+
return undefined;
|
|
409
|
+
}
|
|
410
|
+
const original = await readBodyUpTo(res.clone(), maxCompressibleBytes);
|
|
411
|
+
if (original === null)
|
|
412
|
+
return undefined; // exceeded cap while streaming
|
|
342
413
|
if (original.byteLength < minimumSize)
|
|
343
414
|
return undefined;
|
|
344
415
|
const compressed = await compressBytes(original, chosen);
|
package/dist/conn-info.d.ts
CHANGED
|
@@ -67,8 +67,11 @@ interface MutableConnInfo {
|
|
|
67
67
|
}
|
|
68
68
|
/**
|
|
69
69
|
* @internal Adapter helper — attach {@link ConnInfo} to a `Request`. Called
|
|
70
|
-
* by the Node / Bun / Deno /
|
|
71
|
-
*
|
|
70
|
+
* by the Node / Bun / Deno / Lambda adapters before `app.fetch(request)`.
|
|
71
|
+
* The pure edge delegators (Cloudflare, Vercel, Fastly) expose no peer
|
|
72
|
+
* socket to attach — on those platforms the client address arrives via
|
|
73
|
+
* platform-set headers, which are governed by the `behindProxy` /
|
|
74
|
+
* `trustProxyHeaders` policies instead.
|
|
72
75
|
*
|
|
73
76
|
* @param request - Incoming request to tag (stored under a private symbol).
|
|
74
77
|
* @param info - Connection metadata gathered by the adapter.
|
package/dist/conn-info.js
CHANGED
|
@@ -21,8 +21,11 @@
|
|
|
21
21
|
const CONN_INFO_SYMBOL = Symbol.for("daloyjs.connInfo");
|
|
22
22
|
/**
|
|
23
23
|
* @internal Adapter helper — attach {@link ConnInfo} to a `Request`. Called
|
|
24
|
-
* by the Node / Bun / Deno /
|
|
25
|
-
*
|
|
24
|
+
* by the Node / Bun / Deno / Lambda adapters before `app.fetch(request)`.
|
|
25
|
+
* The pure edge delegators (Cloudflare, Vercel, Fastly) expose no peer
|
|
26
|
+
* socket to attach — on those platforms the client address arrives via
|
|
27
|
+
* platform-set headers, which are governed by the `behindProxy` /
|
|
28
|
+
* `trustProxyHeaders` policies instead.
|
|
26
29
|
*
|
|
27
30
|
* @param request - Incoming request to tag (stored under a private symbol).
|
|
28
31
|
* @param info - Connection metadata gathered by the adapter.
|
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/errors.d.ts
CHANGED
|
@@ -364,9 +364,18 @@ export declare class TooManyRequestsError extends HttpError {
|
|
|
364
364
|
}
|
|
365
365
|
/**
|
|
366
366
|
* `408 Request Timeout` — thrown when a handler exceeds
|
|
367
|
-
* {@link AppOptions.requestTimeoutMs}.
|
|
368
|
-
*
|
|
369
|
-
* `ctx.request.signal`
|
|
367
|
+
* {@link AppOptions.requestTimeoutMs}.
|
|
368
|
+
*
|
|
369
|
+
* When the timeout fires the framework aborts `ctx.request.signal` (with a
|
|
370
|
+
* `TimeoutError` reason) so a handler that forwarded that signal to downstream
|
|
371
|
+
* I/O — `fetch`, a DB driver — sees those calls reject and can unwind. It does
|
|
372
|
+
* **not** forcibly terminate the handler: single-threaded JS cannot preempt
|
|
373
|
+
* running code, so CPU-bound or non-cooperative work continues in the
|
|
374
|
+
* background until it observes the aborted signal or finishes. Forward
|
|
375
|
+
* `ctx.request.signal` into every cancellable downstream call to get the
|
|
376
|
+
* benefit. (Signal firing is wired on the Node adapter and any runtime whose
|
|
377
|
+
* request shim honors the abort hook; direct `app.fetch()` callers still get
|
|
378
|
+
* the `408` but no signal abort.)
|
|
370
379
|
*
|
|
371
380
|
* @param ms - The configured timeout that was exceeded.
|
|
372
381
|
* @since 0.1.0
|
package/dist/errors.js
CHANGED
|
@@ -483,9 +483,18 @@ export class TooManyRequestsError extends HttpError {
|
|
|
483
483
|
}
|
|
484
484
|
/**
|
|
485
485
|
* `408 Request Timeout` — thrown when a handler exceeds
|
|
486
|
-
* {@link AppOptions.requestTimeoutMs}.
|
|
487
|
-
*
|
|
488
|
-
* `ctx.request.signal`
|
|
486
|
+
* {@link AppOptions.requestTimeoutMs}.
|
|
487
|
+
*
|
|
488
|
+
* When the timeout fires the framework aborts `ctx.request.signal` (with a
|
|
489
|
+
* `TimeoutError` reason) so a handler that forwarded that signal to downstream
|
|
490
|
+
* I/O — `fetch`, a DB driver — sees those calls reject and can unwind. It does
|
|
491
|
+
* **not** forcibly terminate the handler: single-threaded JS cannot preempt
|
|
492
|
+
* running code, so CPU-bound or non-cooperative work continues in the
|
|
493
|
+
* background until it observes the aborted signal or finishes. Forward
|
|
494
|
+
* `ctx.request.signal` into every cancellable downstream call to get the
|
|
495
|
+
* benefit. (Signal firing is wired on the Node adapter and any runtime whose
|
|
496
|
+
* request shim honors the abort hook; direct `app.fetch()` callers still get
|
|
497
|
+
* the `408` but no signal abort.)
|
|
489
498
|
*
|
|
490
499
|
* @param ms - The configured timeout that was exceeded.
|
|
491
500
|
* @since 0.1.0
|
package/dist/fetch-guard.d.ts
CHANGED
|
@@ -54,13 +54,14 @@
|
|
|
54
54
|
* a `127.0.0.1` / `169.254.169.254` at connect time, slipping past the
|
|
55
55
|
* library-level check. To close the window:
|
|
56
56
|
*
|
|
57
|
-
* 0. **Built-in, `http:` only** (
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
57
|
+
* 0. **Built-in, `http:` only** (default on Node):
|
|
58
|
+
* {@link FetchGuardOptions.pinDns} defaults to `true` on Node-like
|
|
59
|
+
* runtimes (and `false` elsewhere). On Node, `http:` requests are
|
|
60
|
+
* then dispatched through `node:http` with the socket pinned to the
|
|
61
|
+
* validated IP (and the original `Host` header preserved), so there
|
|
62
|
+
* is no connect-time re-resolution to rebind. Set `pinDns: false` to
|
|
63
|
+
* opt out. `https:` is not pinned by this knob — see its docs — so
|
|
64
|
+
* the items below still matter for TLS upstreams.
|
|
64
65
|
* 1. **Operator-side** (recommended): block egress to RFC1918 /
|
|
65
66
|
* loopback / link-local at the VPC / firewall layer. This neutralises
|
|
66
67
|
* the rebinding even if the application is naïve.
|
|
@@ -93,7 +94,7 @@
|
|
|
93
94
|
*
|
|
94
95
|
* @since 0.34.0
|
|
95
96
|
*/
|
|
96
|
-
export type SsrfBlockReason = "protocol-not-allowed" | "host-not-allowed" | "dns-resolution-failed" | "address-not-allowed" | "too-many-redirects" | "invalid-url";
|
|
97
|
+
export type SsrfBlockReason = "protocol-not-allowed" | "host-not-allowed" | "dns-resolution-failed" | "address-not-allowed" | "too-many-redirects" | "credentials-in-url" | "invalid-url";
|
|
97
98
|
/**
|
|
98
99
|
* Thrown by {@link fetchGuard} when an outbound request is refused. Never
|
|
99
100
|
* thrown for ordinary network failures — those bubble through unchanged
|
|
@@ -188,14 +189,20 @@ export interface FetchGuardOptions {
|
|
|
188
189
|
* by connecting the socket to the exact IP that was validated, instead of
|
|
189
190
|
* letting the underlying client re-resolve the hostname at connect time.
|
|
190
191
|
*
|
|
191
|
-
* When `true
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
192
|
+
* When `true`, a request to a hostname that resolves to a validated address
|
|
193
|
+
* is dispatched through Node's built-in `node:http` with the connection
|
|
194
|
+
* pinned to that address and the original `Host` header preserved — so
|
|
195
|
+
* virtual-host routing still works while an attacker's TTL=0 rebinding to
|
|
196
|
+
* `127.0.0.1` / `169.254.169.254` can no longer take effect between
|
|
197
|
+
* validation and connect.
|
|
197
198
|
*
|
|
198
|
-
* **
|
|
199
|
+
* **Default:** `true` on Node-like runtimes (`process.versions.node` is a
|
|
200
|
+
* non-empty string), `false` elsewhere (Workers / edge sandboxes without
|
|
201
|
+
* `node:http`). Pass `pinDns: false` to opt out on Node, or `pinDns: true`
|
|
202
|
+
* on a non-Node runtime only if you can tolerate the loud error when the
|
|
203
|
+
* pin path cannot load `node:http`.
|
|
204
|
+
*
|
|
205
|
+
* **Scope and caveats** (read before changing the default):
|
|
199
206
|
*
|
|
200
207
|
* - **`http:` only.** `https:` is intentionally NOT pinned here: pinning a
|
|
201
208
|
* TLS connection to an IP while keeping hostname-based SNI / certificate
|
|
@@ -204,16 +211,17 @@ export interface FetchGuardOptions {
|
|
|
204
211
|
* the documented TOCTOU caveat. The prime rebinding target — cloud
|
|
205
212
|
* metadata at `http://169.254.169.254` — is `http:`, so this still closes
|
|
206
213
|
* the highest-value vector.
|
|
207
|
-
* - **Node only.** It uses `node:http`;
|
|
208
|
-
*
|
|
209
|
-
* the misconfiguration is loud rather than a
|
|
214
|
+
* - **Node only for the pin path.** It uses `node:http`; when `pinDns` is
|
|
215
|
+
* explicitly `true` on a runtime without it, an `http:` pinned dispatch
|
|
216
|
+
* throws a clear error so the misconfiguration is loud rather than a
|
|
217
|
+
* silent no-op.
|
|
210
218
|
* - **Bypasses `options.fetch`** for the pinned `http:` path (it must own the
|
|
211
219
|
* socket), and negotiates no response compression (`Accept-Encoding:
|
|
212
220
|
* identity`) so body semantics match a plain `fetch`.
|
|
213
221
|
*
|
|
214
222
|
* Requests to a literal-IP host or an `allowHosts` entry are never pinned
|
|
215
223
|
* (the former already connects to an exact IP; the latter is an explicit
|
|
216
|
-
* operator trust).
|
|
224
|
+
* operator trust).
|
|
217
225
|
*
|
|
218
226
|
* @since 0.44.0
|
|
219
227
|
*/
|
package/dist/fetch-guard.js
CHANGED
|
@@ -54,13 +54,14 @@
|
|
|
54
54
|
* a `127.0.0.1` / `169.254.169.254` at connect time, slipping past the
|
|
55
55
|
* library-level check. To close the window:
|
|
56
56
|
*
|
|
57
|
-
* 0. **Built-in, `http:` only** (
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
57
|
+
* 0. **Built-in, `http:` only** (default on Node):
|
|
58
|
+
* {@link FetchGuardOptions.pinDns} defaults to `true` on Node-like
|
|
59
|
+
* runtimes (and `false` elsewhere). On Node, `http:` requests are
|
|
60
|
+
* then dispatched through `node:http` with the socket pinned to the
|
|
61
|
+
* validated IP (and the original `Host` header preserved), so there
|
|
62
|
+
* is no connect-time re-resolution to rebind. Set `pinDns: false` to
|
|
63
|
+
* opt out. `https:` is not pinned by this knob — see its docs — so
|
|
64
|
+
* the items below still matter for TLS upstreams.
|
|
64
65
|
* 1. **Operator-side** (recommended): block egress to RFC1918 /
|
|
65
66
|
* loopback / link-local at the VPC / firewall layer. This neutralises
|
|
66
67
|
* the rebinding even if the application is naïve.
|
|
@@ -112,6 +113,20 @@ export class SsrfBlockedError extends Error {
|
|
|
112
113
|
this.address = address;
|
|
113
114
|
}
|
|
114
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Whether this runtime looks like Node (or a Node-compatible host such as Bun)
|
|
118
|
+
* where `node:http` DNS pinning is available.
|
|
119
|
+
*
|
|
120
|
+
* Used as the default for {@link FetchGuardOptions.pinDns} so Node apps get
|
|
121
|
+
* rebinding defense without an opt-in, while Workers / pure edge runtimes keep
|
|
122
|
+
* the non-pinning path.
|
|
123
|
+
*/
|
|
124
|
+
function defaultPinDnsEnabled() {
|
|
125
|
+
return (typeof process !== "undefined" &&
|
|
126
|
+
process.versions != null &&
|
|
127
|
+
typeof process.versions.node === "string" &&
|
|
128
|
+
process.versions.node.length > 0);
|
|
129
|
+
}
|
|
115
130
|
// Always-on deny matchers. No option flips these.
|
|
116
131
|
const ALWAYS_DENY = [
|
|
117
132
|
"0.0.0.0/8", // "this network"
|
|
@@ -185,7 +200,11 @@ export function fetchGuard(options = {}) {
|
|
|
185
200
|
throw new Error("fetchGuard(): no global fetch available; pass options.fetch.");
|
|
186
201
|
}
|
|
187
202
|
const resolveFn = options.resolve ?? createDefaultResolver();
|
|
188
|
-
|
|
203
|
+
// Secure default on Node when using the built-in fetch: pin http: sockets
|
|
204
|
+
// to the validated IP. A custom `options.fetch` owns its own socket / DNS
|
|
205
|
+
// policy, so pinDns stays off unless the caller opts in. Non-Node runtimes
|
|
206
|
+
// default off (no node:http). Opt out on Node with `pinDns: false`.
|
|
207
|
+
const pinDns = options.pinDns ?? (options.fetch === undefined && defaultPinDnsEnabled());
|
|
189
208
|
for (const c of ALWAYS_DENY)
|
|
190
209
|
hardDenyMatchers.push(compileCidrMatcher(c));
|
|
191
210
|
for (const c of options.denyAddresses ?? [])
|
|
@@ -269,6 +288,29 @@ export function fetchGuard(options = {}) {
|
|
|
269
288
|
return addrs[0];
|
|
270
289
|
}
|
|
271
290
|
const guarded = async (input, init) => {
|
|
291
|
+
// A URL carrying userinfo (`http://user:pass@internal/`) is a classic SSRF
|
|
292
|
+
// obfuscation — the real host hides after the `@`. undici's `Request`
|
|
293
|
+
// constructor refuses such URLs with a raw `TypeError`, which would fire
|
|
294
|
+
// *before* our host validation and escape the `SsrfBlockedError` contract,
|
|
295
|
+
// so callers misclassify a blocked SSRF attempt as an ordinary upstream
|
|
296
|
+
// failure. Detect and refuse it ourselves with a typed error first. The
|
|
297
|
+
// credentials are stripped from the URL recorded on the error so a
|
|
298
|
+
// caller-supplied secret never leaks into logs. Malformed URLs fall
|
|
299
|
+
// through to the handling below, which raises `SsrfBlockedError("invalid-url")`.
|
|
300
|
+
if (typeof input === "string" || input instanceof URL) {
|
|
301
|
+
let pre;
|
|
302
|
+
try {
|
|
303
|
+
pre = new URL(input);
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
pre = undefined;
|
|
307
|
+
}
|
|
308
|
+
if (pre && (pre.username !== "" || pre.password !== "")) {
|
|
309
|
+
pre.username = "";
|
|
310
|
+
pre.password = "";
|
|
311
|
+
throw new SsrfBlockedError(pre.toString(), "credentials-in-url");
|
|
312
|
+
}
|
|
313
|
+
}
|
|
272
314
|
let request = new Request(input, init);
|
|
273
315
|
const userRedirect = (init?.redirect ?? request.redirect);
|
|
274
316
|
// Always dispatch underlying calls with redirect: "manual" so we can
|
|
@@ -229,7 +229,10 @@ export interface VerifyMessageOptions {
|
|
|
229
229
|
label?: string;
|
|
230
230
|
/**
|
|
231
231
|
* Component identifiers that MUST be covered. Defaults to
|
|
232
|
-
* `["@method", "@
|
|
232
|
+
* `["@method", "@target-uri"]` so the verifier binds scheme, authority,
|
|
233
|
+
* path, **and query** (matching {@link signMessage}'s default covered set).
|
|
234
|
+
* Prefer this over bare `@path`, which leaves query parameters unsigned.
|
|
235
|
+
* Pass `[]` to disable the check (not recommended).
|
|
233
236
|
*/
|
|
234
237
|
requiredComponents?: string[];
|
|
235
238
|
/** Require the `created` parameter. Defaults to `true`. */
|
package/dist/http-signatures.js
CHANGED
|
@@ -435,6 +435,15 @@ function resolveComponentValue(c, msg) {
|
|
|
435
435
|
if (values.length === 0) {
|
|
436
436
|
throw new ComponentError(`@query-param;name="${c.paramName}" is not present in the query`);
|
|
437
437
|
}
|
|
438
|
+
// Reject multi-value params: signing only the first value while an app
|
|
439
|
+
// or intermediary uses the last value (or the full array) is a classic
|
|
440
|
+
// HTTP parameter-pollution differential. Prefer `@query` / `@target-uri`
|
|
441
|
+
// when multiple values are legitimate.
|
|
442
|
+
if (values.length > 1) {
|
|
443
|
+
throw new ComponentError(`@query-param;name="${c.paramName}" appears ${values.length} times; ` +
|
|
444
|
+
"duplicate query parameters are not supported (parameter pollution risk). " +
|
|
445
|
+
"Cover `@query` or `@target-uri` instead, or send a single value.");
|
|
446
|
+
}
|
|
438
447
|
return values[0];
|
|
439
448
|
}
|
|
440
449
|
case "@status":
|
|
@@ -631,7 +640,10 @@ export async function verifyMessage(opts) {
|
|
|
631
640
|
...(params.tag !== undefined ? { tag: params.tag } : {}),
|
|
632
641
|
};
|
|
633
642
|
// Required components.
|
|
634
|
-
|
|
643
|
+
// Align with signMessage()'s default covered components so a default sign
|
|
644
|
+
// is accepted by a default verify, and so query/authority cannot be swapped
|
|
645
|
+
// out under a signature that only bound `@path`.
|
|
646
|
+
const requiredComponents = opts.requiredComponents ?? ["@method", "@target-uri"];
|
|
635
647
|
const coveredIds = input.components.map(serializeComponentId);
|
|
636
648
|
for (const req of requiredComponents) {
|
|
637
649
|
const wanted = serializeComponentId(parseComponentSpec(req));
|
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
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { App } from "./app.js";
|
|
2
2
|
export { createApp } from "./app.js";
|
|
3
|
+
export { defineRoute } from "./types.js";
|
|
3
4
|
export { findRoutesMissingResponseBodySchema } from "./app.js";
|
|
4
|
-
export { _resetPackageJsonCacheForTests } from "./app.js";
|
|
5
5
|
export { _resetCrashHandlersForTests } from "./app.js";
|
|
6
6
|
export { _resetInsecureDefaultsLogForTests } from "./app.js";
|
|
7
7
|
export { _resetIndeterminateEnvWarningForTests } from "./app.js";
|
|
@@ -12,7 +12,7 @@ export { subdomains, PSL_SNAPSHOT_DATE, PSL_PUBLIC_SUFFIXES, MAX_SNAPSHOT_AGE_DA
|
|
|
12
12
|
export type { SubdomainsOptions, SubdomainsResult } from "./subdomains.js";
|
|
13
13
|
export { defineDependency, DEPENDENCY_MARKER } from "./dependency.js";
|
|
14
14
|
export type { DependencyHooks, DependencyOptions } from "./dependency.js";
|
|
15
|
-
export type { RouteDefinition, HttpMethod, PathString, RequestSchemas, ResponsesMap, ResponseSpec, AuthSpec, Hooks, BaseContext, AppState, AuthScheme, AuthContext, HandlerReturn, InferRequest, ParamsOf, PathParams, CallbackDefinition, CallbackMap, CallbackOperation, RouteExample, RouteMeta, } from "./types.js";
|
|
15
|
+
export type { RouteDefinition, HttpMethod, PathString, RequestSchemas, ResponsesMap, ResponseSpec, AuthSpec, Hooks, BaseContext, PreBodyContext, AppState, AuthScheme, AuthContext, HandlerReturn, InferRequest, ParamsOf, PathParams, CallbackDefinition, CallbackMap, CallbackOperation, RouteExample, RouteMeta, } from "./types.js";
|
|
16
16
|
export { HttpError, BadRequestError, ValidationError, NotFoundError, ConflictError, UnauthorizedError, ForbiddenError, MethodNotAllowedError, PayloadTooLargeError, RequestHeaderFieldsTooLargeError, UnsupportedMediaTypeError, TooManyRequestsError, RequestTimeoutError, InternalError, MessageLeakError, httpError, SAFE_CUSTOM_ERROR_RESPONSE_HEADERS, checkCustomErrorResponseHeaders, } from "./errors.js";
|
|
17
17
|
export type { ProblemDetails, ProblemRenderOptions, HttpErrorOptions } from "./errors.js";
|
|
18
18
|
export type { StandardSchemaV1 } from "./schema.js";
|
|
@@ -21,7 +21,7 @@ export { diffOpenAPI, hasBreakingChanges } from "./openapi-diff.js";
|
|
|
21
21
|
export type { ChangeSeverity, OpenAPIChange, OpenAPIDiffResult } from "./openapi-diff.js";
|
|
22
22
|
export { MCP_DEFAULT_MAX_BODY_BYTES, MCP_PROTOCOL_VERSION, MCP_PROTOCOL_VERSIONS, McpToolError, createMcpHandler, mcpRoutes, validateMcpInput, } from "./mcp.js";
|
|
23
23
|
export type { McpContent, McpEmbeddedResourceContent, McpHandler, McpHandlerOptions, McpIcon, McpImageContent, McpJsonObject, McpJsonRpcId, McpJsonSchema, McpJsonValue, McpPrompt, McpPromptArgument, McpPromptDefinition, McpPromptMessage, McpPromptResult, McpRequestContext, McpResource, McpResourceContents, McpResourceDefinition, McpResourceTemplate, McpResourceTemplateDefinition, McpRoutesOptions, McpServerInfo, McpTextContent, McpTool, McpToolAnnotations, McpToolHandler, McpToolResult, } from "./mcp.js";
|
|
24
|
-
export { readBodyLimited, safeJsonParse, isForbiddenObjectKey, sanitizeHeaderName, sanitizeHeaderValue, timingSafeEqual, randomId, assertNoDuplicateSingletonHeaders, assertNoReservedInternalHeaders, assertHeaderCountWithinLimit, DEFAULT_MAX_HEADER_COUNT, RESERVED_INBOUND_HEADER_PREFIXES, SMUGGLING_SINGLETON_HEADERS, verifyWebhookSignature, signWebhookPayload, WEBHOOK_DEFAULT_TOLERANCE_SECONDS, assertStrongSecret, MIN_PROD_SECRET_BYTES, WEAK_SECRET_STRINGS, sanitizeFilename, assertSafeRelativePath, hasMongoOperatorKeys, assertNoMongoOperators, } from "./security.js";
|
|
24
|
+
export { readBodyLimited, safeJsonParse, safeJsonParseLimited, isForbiddenObjectKey, sanitizeHeaderName, sanitizeHeaderValue, timingSafeEqual, randomId, assertNoDuplicateSingletonHeaders, assertNoReservedInternalHeaders, assertHeaderCountWithinLimit, DEFAULT_MAX_HEADER_COUNT, RESERVED_INBOUND_HEADER_PREFIXES, SMUGGLING_SINGLETON_HEADERS, verifyWebhookSignature, signWebhookPayload, WEBHOOK_DEFAULT_TOLERANCE_SECONDS, assertStrongSecret, MIN_PROD_SECRET_BYTES, WEAK_SECRET_STRINGS, sanitizeFilename, assertSafeRelativePath, hasMongoOperatorKeys, assertNoMongoOperators, } from "./security.js";
|
|
25
25
|
export type { WebhookHmacAlgorithm } from "./security.js";
|
|
26
26
|
export { requestId, secureHeaders, SECURE_HEADERS_MARKER, cors, CORS_HOOK_MARKER, CORS_ORIGIN_ALLOW_MARKER, CORS_WILDCARD_ORIGIN_MARKER, rateLimit, loginThrottle, timing, bearerAuth, basicAuth, markAuthHook, AUTH_HOOK_MARKER, csrf, CSRF_HOOK_MARKER, fetchMetadata, requireScopes, REQUIRE_SCOPES_AGGREGATE_KEY, REQUIRE_SCOPES_HOOK_MARKER, _resetSharedRateLimitStoresForTests, } from "./middleware.js";
|
|
27
27
|
export { etag } from "./etag.js";
|
|
@@ -72,9 +72,9 @@ export { loadShedding, LOAD_SHEDDING_MARKER } from "./load-shedding.js";
|
|
|
72
72
|
export type { LoadSheddingOptions, LoadSheddingSnapshot } from "./load-shedding.js";
|
|
73
73
|
export { defineConfig, ConfigValidationError } from "./config.js";
|
|
74
74
|
export type { ConfigSource, DefineConfigOptions } from "./config.js";
|
|
75
|
-
export type { RequestIdOptions, SecureHeadersOptions, CspDirectivesOptions, CorsOptions, CorsOriginAllow, RateLimitOptions, RateLimitStore, LoginThrottleOptions, CsrfOptions, CsrfCookieOptions, FetchMetadataOptions, BasicAuthOptions, } from "./middleware.js";
|
|
75
|
+
export type { RequestIdOptions, SecureHeadersOptions, CspDirectivesOptions, CorsOptions, CorsOriginAllow, RateLimitOptions, RateLimitContext, RateLimitStore, LoginThrottleOptions, CsrfOptions, CsrfCookieOptions, FetchMetadataOptions, BasicAuthOptions, } from "./middleware.js";
|
|
76
76
|
export type { BearerAuthOptions, BearerAuthVerifyHook } from "./middleware.js";
|
|
77
|
-
export { createLogger, noopLogger, DEFAULT_REDACT_KEYS } from "./logger.js";
|
|
77
|
+
export { createLogger, noopLogger, DEFAULT_REDACT_KEYS, SENSITIVE_URL_QUERY_KEYS, sanitizeUrlForLog, } from "./logger.js";
|
|
78
78
|
export type { Logger, LogLevel, ConsoleLoggerOptions, LoggerRedactionOptions } from "./logger.js";
|
|
79
79
|
export type { ScalarJsonPrimitive, ScalarJsonValue, ScalarReferenceConfiguration, ScalarTheme, RedocConfiguration, RedocHtmlOptions, SwaggerUiConfiguration, SwaggerUiHtmlOptions, AsyncApiHtmlOptions, DocsAssetOptions, DocsAuthLauncherOptions, } from "./docs.js";
|
|
80
80
|
export { formatStartupBanner, printStartupBanner } from "./banner.js";
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { App } from "./app.js";
|
|
2
2
|
export { createApp } from "./app.js";
|
|
3
|
+
export { defineRoute } from "./types.js";
|
|
3
4
|
export { findRoutesMissingResponseBodySchema } from "./app.js";
|
|
4
|
-
export { _resetPackageJsonCacheForTests } from "./app.js";
|
|
5
5
|
export { _resetCrashHandlersForTests } from "./app.js";
|
|
6
6
|
export { _resetInsecureDefaultsLogForTests } from "./app.js";
|
|
7
7
|
export { _resetIndeterminateEnvWarningForTests } from "./app.js";
|
|
@@ -12,7 +12,7 @@ export { HttpError, BadRequestError, ValidationError, NotFoundError, ConflictErr
|
|
|
12
12
|
export { validate, isStandardSchema } from "./schema.js";
|
|
13
13
|
export { diffOpenAPI, hasBreakingChanges } from "./openapi-diff.js";
|
|
14
14
|
export { MCP_DEFAULT_MAX_BODY_BYTES, MCP_PROTOCOL_VERSION, MCP_PROTOCOL_VERSIONS, McpToolError, createMcpHandler, mcpRoutes, validateMcpInput, } from "./mcp.js";
|
|
15
|
-
export { readBodyLimited, safeJsonParse, isForbiddenObjectKey, sanitizeHeaderName, sanitizeHeaderValue, timingSafeEqual, randomId, assertNoDuplicateSingletonHeaders, assertNoReservedInternalHeaders, assertHeaderCountWithinLimit, DEFAULT_MAX_HEADER_COUNT, RESERVED_INBOUND_HEADER_PREFIXES, SMUGGLING_SINGLETON_HEADERS, verifyWebhookSignature, signWebhookPayload, WEBHOOK_DEFAULT_TOLERANCE_SECONDS, assertStrongSecret, MIN_PROD_SECRET_BYTES, WEAK_SECRET_STRINGS, sanitizeFilename, assertSafeRelativePath, hasMongoOperatorKeys, assertNoMongoOperators, } from "./security.js";
|
|
15
|
+
export { readBodyLimited, safeJsonParse, safeJsonParseLimited, isForbiddenObjectKey, sanitizeHeaderName, sanitizeHeaderValue, timingSafeEqual, randomId, assertNoDuplicateSingletonHeaders, assertNoReservedInternalHeaders, assertHeaderCountWithinLimit, DEFAULT_MAX_HEADER_COUNT, RESERVED_INBOUND_HEADER_PREFIXES, SMUGGLING_SINGLETON_HEADERS, verifyWebhookSignature, signWebhookPayload, WEBHOOK_DEFAULT_TOLERANCE_SECONDS, assertStrongSecret, MIN_PROD_SECRET_BYTES, WEAK_SECRET_STRINGS, sanitizeFilename, assertSafeRelativePath, hasMongoOperatorKeys, assertNoMongoOperators, } from "./security.js";
|
|
16
16
|
export { requestId, secureHeaders, SECURE_HEADERS_MARKER, cors, CORS_HOOK_MARKER, CORS_ORIGIN_ALLOW_MARKER, CORS_WILDCARD_ORIGIN_MARKER, rateLimit, loginThrottle, timing, bearerAuth, basicAuth, markAuthHook, AUTH_HOOK_MARKER, csrf, CSRF_HOOK_MARKER, fetchMetadata, requireScopes, REQUIRE_SCOPES_AGGREGATE_KEY, REQUIRE_SCOPES_HOOK_MARKER, _resetSharedRateLimitStoresForTests, } from "./middleware.js";
|
|
17
17
|
export { etag } from "./etag.js";
|
|
18
18
|
export { compression, COMPRESSION_HOOK_MARKER, _resetCompressionRuntimeProbeForTests, } from "./compression.js";
|
|
@@ -38,7 +38,7 @@ export { waf } from "./waf.js";
|
|
|
38
38
|
export { safeRedirect, OpenRedirectBlockedError } from "./safe-redirect.js";
|
|
39
39
|
export { loadShedding, LOAD_SHEDDING_MARKER } from "./load-shedding.js";
|
|
40
40
|
export { defineConfig, ConfigValidationError } from "./config.js";
|
|
41
|
-
export { createLogger, noopLogger, DEFAULT_REDACT_KEYS } from "./logger.js";
|
|
41
|
+
export { createLogger, noopLogger, DEFAULT_REDACT_KEYS, SENSITIVE_URL_QUERY_KEYS, sanitizeUrlForLog, } from "./logger.js";
|
|
42
42
|
export { formatStartupBanner, printStartupBanner } from "./banner.js";
|
|
43
43
|
export { sseStream, sseResponse, ndjsonStream, ndjsonResponse } from "./streaming.js";
|
|
44
44
|
export { httpBearerScheme, httpBasicScheme, apiKeyScheme, oauth2Scheme, openIdConnectScheme, REQUIRE_PAYLOAD_AUTH_EXTENSION, securitySchemeRequiresPayloadAuth, toOpenAPISecurityScheme, } from "./security-schemes.js";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mark a framework-generated replay as having crossed response validation.
|
|
3
|
+
* This is an internal capability and is not exported from the package barrel.
|
|
4
|
+
*
|
|
5
|
+
* @param response - Reconstructed response containing previously validated bytes.
|
|
6
|
+
* @returns The same response for allocation-free call-site composition.
|
|
7
|
+
*/
|
|
8
|
+
export declare function markSchemaValidatedResponse(response: Response): Response;
|
|
9
|
+
/**
|
|
10
|
+
* Test whether a response is a trusted first-party replay of validated bytes.
|
|
11
|
+
*
|
|
12
|
+
* @param response - Hook response being considered for fail-closed handling.
|
|
13
|
+
* @returns `true` only for responses marked by first-party replay middleware.
|
|
14
|
+
*/
|
|
15
|
+
export declare function isSchemaValidatedResponse(response: Response): boolean;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Responses reconstructed by first-party cache/idempotency middleware from a
|
|
3
|
+
* previously finalized response. The original response already crossed the
|
|
4
|
+
* route's schema-validation boundary, so replaying its stored bytes does not
|
|
5
|
+
* create the opaque-success bypass guarded by `App`.
|
|
6
|
+
*/
|
|
7
|
+
const schemaValidatedResponses = new WeakSet();
|
|
8
|
+
/**
|
|
9
|
+
* Mark a framework-generated replay as having crossed response validation.
|
|
10
|
+
* This is an internal capability and is not exported from the package barrel.
|
|
11
|
+
*
|
|
12
|
+
* @param response - Reconstructed response containing previously validated bytes.
|
|
13
|
+
* @returns The same response for allocation-free call-site composition.
|
|
14
|
+
*/
|
|
15
|
+
export function markSchemaValidatedResponse(response) {
|
|
16
|
+
schemaValidatedResponses.add(response);
|
|
17
|
+
return response;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Test whether a response is a trusted first-party replay of validated bytes.
|
|
21
|
+
*
|
|
22
|
+
* @param response - Hook response being considered for fail-closed handling.
|
|
23
|
+
* @returns `true` only for responses marked by first-party replay middleware.
|
|
24
|
+
*/
|
|
25
|
+
export function isSchemaValidatedResponse(response) {
|
|
26
|
+
return schemaValidatedResponses.has(response);
|
|
27
|
+
}
|