@daloyjs/core 1.0.0-rc.4 → 1.0.0-rc.6
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 +34 -22
- package/dist/adapters/bun.d.ts +20 -2
- package/dist/adapters/bun.js +42 -7
- 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 +117 -46
- package/dist/app.d.ts +30 -4
- package/dist/app.js +187 -45
- package/dist/auto-ban.js +1 -3
- package/dist/bot-guard.js +30 -3
- package/dist/cli.js +9 -6
- package/dist/client.d.ts +36 -7
- package/dist/client.js +7 -0
- package/dist/compression.d.ts +9 -0
- package/dist/compression.js +72 -1
- package/dist/config.js +1 -3
- package/dist/conn-info.d.ts +5 -2
- package/dist/conn-info.js +5 -2
- package/dist/errors.d.ts +12 -3
- package/dist/errors.js +14 -8
- package/dist/etag.js +12 -2
- package/dist/fetch-guard.d.ts +27 -19
- package/dist/fetch-guard.js +50 -8
- package/dist/geo-block.js +4 -9
- package/dist/hashing.js +1 -1
- package/dist/http-signatures.d.ts +4 -1
- package/dist/http-signatures.js +16 -9
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/ip-reputation.js +1 -1
- package/dist/ip-restriction.js +3 -12
- package/dist/jwt.js +12 -14
- package/dist/logger.d.ts +45 -0
- package/dist/logger.js +135 -0
- package/dist/mcp.js +10 -9
- package/dist/middleware.js +33 -3
- package/dist/mtls.js +6 -1
- package/dist/multipart.js +9 -12
- package/dist/openapi.d.ts +1 -1
- package/dist/openapi.js +2 -2
- package/dist/rate-limit-redis.d.ts +4 -4
- package/dist/response-cache.d.ts +179 -21
- package/dist/response-cache.js +338 -29
- package/dist/router.d.ts +2 -2
- package/dist/router.js +24 -9
- package/dist/safe-redirect.d.ts +5 -1
- package/dist/safe-redirect.js +27 -3
- package/dist/sbom.cdx.json +9 -9
- package/dist/sbom.spdx.json +5 -5
- package/dist/security-schemes.js +1 -2
- package/dist/security.d.ts +41 -0
- package/dist/security.js +131 -15
- package/dist/session.d.ts +13 -2
- package/dist/session.js +111 -17
- package/dist/subdomains.js +1 -4
- package/dist/tenancy.d.ts +40 -0
- package/dist/tenancy.js +54 -3
- package/dist/time-claims.js +3 -1
- package/dist/waf.js +124 -32
- package/dist/webhook-delivery.js +19 -3
- package/dist/websocket.d.ts +8 -0
- package/dist/websocket.js +19 -4
- package/package.json +6 -5
package/dist/jwt.js
CHANGED
|
@@ -18,11 +18,7 @@
|
|
|
18
18
|
* @since 0.21.0
|
|
19
19
|
*/
|
|
20
20
|
import { assertTemporalClaims, TemporalClaimError } from "./time-claims.js";
|
|
21
|
-
const SYMMETRIC = new Set([
|
|
22
|
-
"HS256",
|
|
23
|
-
"HS384",
|
|
24
|
-
"HS512",
|
|
25
|
-
]);
|
|
21
|
+
const SYMMETRIC = new Set(["HS256", "HS384", "HS512"]);
|
|
26
22
|
const ASYMMETRIC = new Set([
|
|
27
23
|
"RS256",
|
|
28
24
|
"RS384",
|
|
@@ -254,10 +250,14 @@ export function createJwtSigner(opts) {
|
|
|
254
250
|
opts.key.byteLength < MIN_HS_KEY_BYTES) {
|
|
255
251
|
throw new JwtError("weak_hs_secret", `jwt(): ${alg} secret must be at least ${MIN_HS_KEY_BYTES} bytes (RFC 7518 §3.2); got ${opts.key.byteLength}.`);
|
|
256
252
|
}
|
|
257
|
-
if (typeof opts.maxLifetimeSeconds !== "number" ||
|
|
253
|
+
if (typeof opts.maxLifetimeSeconds !== "number" ||
|
|
254
|
+
!Number.isFinite(opts.maxLifetimeSeconds) ||
|
|
255
|
+
opts.maxLifetimeSeconds <= 0) {
|
|
258
256
|
throw new JwtError("missing_max_lifetime", "jwt(): maxLifetimeSeconds is required and must be a positive number — a token that never expires is wrong in every threat model.");
|
|
259
257
|
}
|
|
260
|
-
if (opts.acknowledgeNoExp === true &&
|
|
258
|
+
if (opts.acknowledgeNoExp === true &&
|
|
259
|
+
isProductionEnv(opts.env) &&
|
|
260
|
+
opts.secureDefaults !== false) {
|
|
261
261
|
throw new JwtError("ack_no_exp_refused_in_production", "jwt(): acknowledgeNoExp: true is refused in production under secureDefaults — every issued JWT must carry an exp claim.");
|
|
262
262
|
}
|
|
263
263
|
const resolved = (async () => {
|
|
@@ -359,18 +359,16 @@ export function createJwtVerifier(opts) {
|
|
|
359
359
|
allow.add(alg);
|
|
360
360
|
}
|
|
361
361
|
const hasSym = [...allow].some((a) => SYMMETRIC.has(a));
|
|
362
|
-
if (hasSym &&
|
|
363
|
-
opts.key instanceof Uint8Array &&
|
|
364
|
-
opts.key.byteLength < MIN_HS_KEY_BYTES) {
|
|
362
|
+
if (hasSym && opts.key instanceof Uint8Array && opts.key.byteLength < MIN_HS_KEY_BYTES) {
|
|
365
363
|
throw new JwtError("weak_hs_secret", `jwt(): HS* secret must be at least ${MIN_HS_KEY_BYTES} bytes (RFC 7518 §3.2); got ${opts.key.byteLength}.`);
|
|
366
364
|
}
|
|
367
|
-
if (hasSym &&
|
|
368
|
-
opts.refuseSymmetricWithJwk !== false &&
|
|
369
|
-
looksLikeJwkSource(opts.key)) {
|
|
365
|
+
if (hasSym && opts.refuseSymmetricWithJwk !== false && looksLikeJwkSource(opts.key)) {
|
|
370
366
|
throw new JwtError("sym_with_jwk_refused", "jwt(): symmetric algorithms (HS*) mixed with a JWK / JWKS key source are refused — this is the documented JWKS confused-deputy attack. Use asymmetric algorithms (RS/PS/ES/EdDSA), or pass refuseSymmetricWithJwk: false to override (not recommended).");
|
|
371
367
|
}
|
|
372
368
|
if (opts.clockSkewSeconds !== undefined) {
|
|
373
|
-
if (typeof opts.clockSkewSeconds !== "number" ||
|
|
369
|
+
if (typeof opts.clockSkewSeconds !== "number" ||
|
|
370
|
+
!Number.isFinite(opts.clockSkewSeconds) ||
|
|
371
|
+
opts.clockSkewSeconds < 0) {
|
|
374
372
|
throw new JwtError("invalid_clock_skew", "jwt(): clockSkewSeconds must be a non-negative finite number.");
|
|
375
373
|
}
|
|
376
374
|
}
|
package/dist/logger.d.ts
CHANGED
|
@@ -133,4 +133,49 @@ export declare function createLogger(opts?: ConsoleLoggerOptions): Logger;
|
|
|
133
133
|
* @since 0.1.0
|
|
134
134
|
*/
|
|
135
135
|
export declare const noopLogger: Logger;
|
|
136
|
+
/**
|
|
137
|
+
* Query parameter names whose values are redacted when a request URL is
|
|
138
|
+
* bound into a log record. Case-insensitive. Covers OAuth redirect params,
|
|
139
|
+
* API keys in query strings, signed-URL tokens, session identifiers, and the
|
|
140
|
+
* exact-named parameters of AWS SigV4 / GCS V4 presigned URLs (the `x-amz-*`
|
|
141
|
+
* and `x-goog-*` families are additionally matched by prefix — see
|
|
142
|
+
* {@link SENSITIVE_URL_QUERY_KEY_PREFIXES}).
|
|
143
|
+
*
|
|
144
|
+
* @since 1.0.0
|
|
145
|
+
*/
|
|
146
|
+
export declare const SENSITIVE_URL_QUERY_KEYS: readonly string[];
|
|
147
|
+
/**
|
|
148
|
+
* Case-insensitive query-key prefixes whose values are always redacted in a
|
|
149
|
+
* logged URL. Covers the full AWS SigV4 (`X-Amz-*`) and GCS V4 (`X-Goog-*`)
|
|
150
|
+
* presigned-URL parameter families so a signature never leaks even if a
|
|
151
|
+
* provider adds a new signed parameter name. Redacting the non-secret members
|
|
152
|
+
* of the bundle (`X-Amz-Date`, `X-Amz-Expires`, …) is harmless in a log line.
|
|
153
|
+
*
|
|
154
|
+
* @since 1.0.0
|
|
155
|
+
*/
|
|
156
|
+
export declare const SENSITIVE_URL_QUERY_KEY_PREFIXES: readonly string[];
|
|
157
|
+
/**
|
|
158
|
+
* Produce a log-safe form of a request URL.
|
|
159
|
+
*
|
|
160
|
+
* Keeps scheme, host, and path for operability. Redacts values of
|
|
161
|
+
* {@link SENSITIVE_URL_QUERY_KEYS} / {@link SENSITIVE_URL_QUERY_KEY_PREFIXES}
|
|
162
|
+
* (and JWT-like / credential-like query values) so OAuth `?code=`,
|
|
163
|
+
* `?access_token=`, and presigned-URL signatures (`?X-Amz-Signature=`,
|
|
164
|
+
* `?X-Goog-Signature=`) never land in durable error logs under the field name
|
|
165
|
+
* `url` (which the structured redactor does not rename-match).
|
|
166
|
+
*
|
|
167
|
+
* Malformed URLs fall back to the path-only prefix before `?` / `#`.
|
|
168
|
+
*
|
|
169
|
+
* This runs once per request on the logging path, so it fast-paths the common
|
|
170
|
+
* case: a URL with no query, no fragment, and no userinfo (`@`) delimiter is
|
|
171
|
+
* already log-safe and is returned verbatim without the WHATWG URL parse (about
|
|
172
|
+
* an order of magnitude cheaper). The `@` guard preserves userinfo stripping
|
|
173
|
+
* for the rare inputs that carry credentials in the authority — `request.url`
|
|
174
|
+
* itself never does, but this is a public utility.
|
|
175
|
+
*
|
|
176
|
+
* @param url - Absolute or relative request URL (typically `request.url`).
|
|
177
|
+
* @returns A string safe to attach as a logger binding.
|
|
178
|
+
* @since 1.0.0
|
|
179
|
+
*/
|
|
180
|
+
export declare function sanitizeUrlForLog(url: string): string;
|
|
136
181
|
export {};
|
package/dist/logger.js
CHANGED
|
@@ -34,6 +34,17 @@ export const DEFAULT_REDACT_KEYS = Object.freeze([
|
|
|
34
34
|
"refresh_token",
|
|
35
35
|
"id_token",
|
|
36
36
|
"client_secret",
|
|
37
|
+
// A structured field literally named `private_key` is always a secret and
|
|
38
|
+
// has a negligible false-positive rate as a log-field name (mirrors the
|
|
39
|
+
// existing `client_secret`). The broader OAuth/session query-string names
|
|
40
|
+
// (`code`, `state`, `id`, `key`, `sid`, `session`, `signature`, `sig`,
|
|
41
|
+
// `auth`, …) are DELIBERATELY NOT here: those are extremely common,
|
|
42
|
+
// non-secret structured field names (record ids, sort keys, UI state) and
|
|
43
|
+
// redacting them at every depth would corrupt normal operational logs.
|
|
44
|
+
// Secrets that ride in a *URL query string* are handled instead by
|
|
45
|
+
// {@link sanitizeUrlForLog} / {@link SENSITIVE_URL_QUERY_KEYS}, which only
|
|
46
|
+
// applies to the `url` field where the query context makes them sensitive.
|
|
47
|
+
"private_key",
|
|
37
48
|
// AI / LLM provider credential headers and body fields. Added in response
|
|
38
49
|
// to the LiteLLM 2026 "AI blast radius" incident class (Snyk 2026,
|
|
39
50
|
// CVE-2026-42208 + CVE-2026-33634) — an AI gateway that brokers prompts
|
|
@@ -265,3 +276,127 @@ export const noopLogger = {
|
|
|
265
276
|
return noopLogger;
|
|
266
277
|
},
|
|
267
278
|
};
|
|
279
|
+
/**
|
|
280
|
+
* Query parameter names whose values are redacted when a request URL is
|
|
281
|
+
* bound into a log record. Case-insensitive. Covers OAuth redirect params,
|
|
282
|
+
* API keys in query strings, signed-URL tokens, session identifiers, and the
|
|
283
|
+
* exact-named parameters of AWS SigV4 / GCS V4 presigned URLs (the `x-amz-*`
|
|
284
|
+
* and `x-goog-*` families are additionally matched by prefix — see
|
|
285
|
+
* {@link SENSITIVE_URL_QUERY_KEY_PREFIXES}).
|
|
286
|
+
*
|
|
287
|
+
* @since 1.0.0
|
|
288
|
+
*/
|
|
289
|
+
export const SENSITIVE_URL_QUERY_KEYS = Object.freeze([
|
|
290
|
+
"authorization",
|
|
291
|
+
"access_token",
|
|
292
|
+
"refresh_token",
|
|
293
|
+
"id_token",
|
|
294
|
+
"token",
|
|
295
|
+
"api_key",
|
|
296
|
+
"apikey",
|
|
297
|
+
"api-key",
|
|
298
|
+
"key",
|
|
299
|
+
"password",
|
|
300
|
+
"passwd",
|
|
301
|
+
"secret",
|
|
302
|
+
"client_secret",
|
|
303
|
+
"code",
|
|
304
|
+
"state",
|
|
305
|
+
"session_state",
|
|
306
|
+
"session",
|
|
307
|
+
"sid",
|
|
308
|
+
"signature",
|
|
309
|
+
"sig",
|
|
310
|
+
"auth",
|
|
311
|
+
"private_key",
|
|
312
|
+
"x-api-key",
|
|
313
|
+
// AWS SigV4 presigned URL parameters. `X-Amz-Signature` is the secret; the
|
|
314
|
+
// credential (embeds the access-key id) and session token are equally
|
|
315
|
+
// sensitive. Also covered by the `x-amz-` prefix below.
|
|
316
|
+
"x-amz-signature",
|
|
317
|
+
"x-amz-credential",
|
|
318
|
+
"x-amz-security-token",
|
|
319
|
+
// Google Cloud Storage V4 signed URL parameters. Also covered by `x-goog-`.
|
|
320
|
+
"x-goog-signature",
|
|
321
|
+
"x-goog-credential",
|
|
322
|
+
"googleaccessid",
|
|
323
|
+
]);
|
|
324
|
+
/**
|
|
325
|
+
* Case-insensitive query-key prefixes whose values are always redacted in a
|
|
326
|
+
* logged URL. Covers the full AWS SigV4 (`X-Amz-*`) and GCS V4 (`X-Goog-*`)
|
|
327
|
+
* presigned-URL parameter families so a signature never leaks even if a
|
|
328
|
+
* provider adds a new signed parameter name. Redacting the non-secret members
|
|
329
|
+
* of the bundle (`X-Amz-Date`, `X-Amz-Expires`, …) is harmless in a log line.
|
|
330
|
+
*
|
|
331
|
+
* @since 1.0.0
|
|
332
|
+
*/
|
|
333
|
+
export const SENSITIVE_URL_QUERY_KEY_PREFIXES = Object.freeze([
|
|
334
|
+
"x-amz-",
|
|
335
|
+
"x-goog-",
|
|
336
|
+
]);
|
|
337
|
+
const SENSITIVE_URL_QUERY_KEY_SET = new Set(SENSITIVE_URL_QUERY_KEYS.map((k) => k.toLowerCase()));
|
|
338
|
+
/**
|
|
339
|
+
* Whether a URL query-parameter name is treated as secret-bearing when a
|
|
340
|
+
* request URL is bound into a log record. True when the lower-cased name is in
|
|
341
|
+
* {@link SENSITIVE_URL_QUERY_KEYS} or starts with a
|
|
342
|
+
* {@link SENSITIVE_URL_QUERY_KEY_PREFIXES} entry.
|
|
343
|
+
*
|
|
344
|
+
* @param lowerKey - Already-lower-cased query-parameter name.
|
|
345
|
+
* @returns `true` if the value should be redacted.
|
|
346
|
+
*/
|
|
347
|
+
function isSensitiveUrlQueryKey(lowerKey) {
|
|
348
|
+
if (SENSITIVE_URL_QUERY_KEY_SET.has(lowerKey))
|
|
349
|
+
return true;
|
|
350
|
+
for (const prefix of SENSITIVE_URL_QUERY_KEY_PREFIXES) {
|
|
351
|
+
if (lowerKey.startsWith(prefix))
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Produce a log-safe form of a request URL.
|
|
358
|
+
*
|
|
359
|
+
* Keeps scheme, host, and path for operability. Redacts values of
|
|
360
|
+
* {@link SENSITIVE_URL_QUERY_KEYS} / {@link SENSITIVE_URL_QUERY_KEY_PREFIXES}
|
|
361
|
+
* (and JWT-like / credential-like query values) so OAuth `?code=`,
|
|
362
|
+
* `?access_token=`, and presigned-URL signatures (`?X-Amz-Signature=`,
|
|
363
|
+
* `?X-Goog-Signature=`) never land in durable error logs under the field name
|
|
364
|
+
* `url` (which the structured redactor does not rename-match).
|
|
365
|
+
*
|
|
366
|
+
* Malformed URLs fall back to the path-only prefix before `?` / `#`.
|
|
367
|
+
*
|
|
368
|
+
* This runs once per request on the logging path, so it fast-paths the common
|
|
369
|
+
* case: a URL with no query, no fragment, and no userinfo (`@`) delimiter is
|
|
370
|
+
* already log-safe and is returned verbatim without the WHATWG URL parse (about
|
|
371
|
+
* an order of magnitude cheaper). The `@` guard preserves userinfo stripping
|
|
372
|
+
* for the rare inputs that carry credentials in the authority — `request.url`
|
|
373
|
+
* itself never does, but this is a public utility.
|
|
374
|
+
*
|
|
375
|
+
* @param url - Absolute or relative request URL (typically `request.url`).
|
|
376
|
+
* @returns A string safe to attach as a logger binding.
|
|
377
|
+
* @since 1.0.0
|
|
378
|
+
*/
|
|
379
|
+
export function sanitizeUrlForLog(url) {
|
|
380
|
+
if (url.indexOf("?") === -1 && url.indexOf("#") === -1 && url.indexOf("@") === -1) {
|
|
381
|
+
return url;
|
|
382
|
+
}
|
|
383
|
+
try {
|
|
384
|
+
const parsed = new URL(url);
|
|
385
|
+
if (parsed.search === "" && parsed.hash === "") {
|
|
386
|
+
return `${parsed.origin}${parsed.pathname}`;
|
|
387
|
+
}
|
|
388
|
+
const safe = new URL(parsed.origin + parsed.pathname);
|
|
389
|
+
for (const [key, value] of parsed.searchParams) {
|
|
390
|
+
const lower = key.toLowerCase();
|
|
391
|
+
const sensitiveKey = isSensitiveUrlQueryKey(lower);
|
|
392
|
+
const sensitiveValue = JWT_LIKE_RE.test(value) || CREDENTIAL_LIKE_RE.test(value);
|
|
393
|
+
CREDENTIAL_LIKE_RE.lastIndex = 0;
|
|
394
|
+
safe.searchParams.append(key, sensitiveKey || sensitiveValue ? "[REDACTED]" : value);
|
|
395
|
+
}
|
|
396
|
+
return safe.toString();
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
const cut = url.search(/[?#]/);
|
|
400
|
+
return cut === -1 ? url : url.slice(0, cut);
|
|
401
|
+
}
|
|
402
|
+
}
|
package/dist/mcp.js
CHANGED
|
@@ -355,10 +355,16 @@ function compileUriTemplate(template) {
|
|
|
355
355
|
const LOOPBACK_HOSTNAMES = new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
356
356
|
/**
|
|
357
357
|
* Streamable HTTP DNS-rebinding defense: decide whether a browser `Origin`
|
|
358
|
-
* may talk to this MCP endpoint.
|
|
359
|
-
*
|
|
358
|
+
* may talk to this MCP endpoint.
|
|
359
|
+
*
|
|
360
|
+
* Loopback origins (`localhost` / `127.0.0.1` / `[::1]` / `*.localhost`) are
|
|
361
|
+
* allowed for local development. Every non-loopback origin must appear in
|
|
362
|
+
* the configured allowlist. We deliberately do **not** treat
|
|
363
|
+
* `Origin.host === request Host` as sufficient: under DNS rebinding both
|
|
364
|
+
* can be the attacker hostname resolving to the target IP, which would
|
|
365
|
+
* silently bypass an implicit same-origin check.
|
|
360
366
|
*/
|
|
361
|
-
function isAllowedOrigin(origin,
|
|
367
|
+
function isAllowedOrigin(origin, _request, allowlist) {
|
|
362
368
|
const normalized = origin.toLowerCase();
|
|
363
369
|
if (allowlist.has(normalized))
|
|
364
370
|
return true;
|
|
@@ -374,12 +380,7 @@ function isAllowedOrigin(origin, request, allowlist) {
|
|
|
374
380
|
const hostname = parsed.hostname;
|
|
375
381
|
if (LOOPBACK_HOSTNAMES.has(hostname) || hostname.endsWith(".localhost"))
|
|
376
382
|
return true;
|
|
377
|
-
|
|
378
|
-
return parsed.host === new URL(request.url).host;
|
|
379
|
-
}
|
|
380
|
-
catch {
|
|
381
|
-
return false;
|
|
382
|
-
}
|
|
383
|
+
return false;
|
|
383
384
|
}
|
|
384
385
|
/**
|
|
385
386
|
* Create a dependency-free MCP Streamable HTTP endpoint handler.
|
package/dist/middleware.js
CHANGED
|
@@ -257,6 +257,8 @@ export function secureHeaders(opts = {}) {
|
|
|
257
257
|
}
|
|
258
258
|
}
|
|
259
259
|
const headerEntries = Object.entries(headers);
|
|
260
|
+
// Lowercased name set for the common-path fast apply below.
|
|
261
|
+
const headerKeySet = headerEntries.length > 0 ? new Set(headerEntries.map(([k]) => k)) : null;
|
|
260
262
|
const hooks = {};
|
|
261
263
|
if (cspIsDynamic) {
|
|
262
264
|
hooks.beforeHandle = (ctx) => {
|
|
@@ -276,12 +278,40 @@ export function secureHeaders(opts = {}) {
|
|
|
276
278
|
return undefined;
|
|
277
279
|
};
|
|
278
280
|
}
|
|
279
|
-
if (headerEntries.length > 0) {
|
|
281
|
+
if (headerEntries.length > 0 && headerKeySet !== null) {
|
|
282
|
+
// Apply baseline security headers without overwriting values the handler
|
|
283
|
+
// (or an earlier hook) already set. Two paths, same accept/reject
|
|
284
|
+
// semantics:
|
|
285
|
+
//
|
|
286
|
+
// 1. Fast path (common): response carries none of our keys (typical
|
|
287
|
+
// after serializeResult: content-type + content-length +
|
|
288
|
+
// x-request-id only). One cheap forEach over the small response
|
|
289
|
+
// header map, then unconditional set of each default — avoids N
|
|
290
|
+
// `has()` probes that miss on every request.
|
|
291
|
+
// 2. Careful path: at least one of our keys is already present; fall
|
|
292
|
+
// back to set-if-absent so user-supplied CSP / frame-options / etc.
|
|
293
|
+
// still win.
|
|
280
294
|
hooks.onResponse = (res) => {
|
|
281
|
-
|
|
282
|
-
|
|
295
|
+
let conflict = false;
|
|
296
|
+
// `for...of` over Headers.entries() is faster than the callback-based
|
|
297
|
+
// forEach and lets us break the instant we find a conflicting header.
|
|
298
|
+
// WHATWG Headers yields lowercased names, matching headerKeySet.
|
|
299
|
+
for (const [name] of res.headers) {
|
|
300
|
+
if (headerKeySet.has(name)) {
|
|
301
|
+
conflict = true;
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (!conflict) {
|
|
306
|
+
for (const [k, v] of headerEntries)
|
|
283
307
|
res.headers.set(k, v);
|
|
284
308
|
}
|
|
309
|
+
else {
|
|
310
|
+
for (const [k, v] of headerEntries) {
|
|
311
|
+
if (!res.headers.has(k))
|
|
312
|
+
res.headers.set(k, v);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
285
315
|
};
|
|
286
316
|
}
|
|
287
317
|
hooks[SECURE_HEADERS_MARKER] = true;
|
package/dist/mtls.js
CHANGED
|
@@ -415,7 +415,12 @@ function certFromHeaders(request, cfg) {
|
|
|
415
415
|
const sanRaw = readHeader(request, cfg.san);
|
|
416
416
|
const verifyRaw = cfg.verify ? readHeader(request, cfg.verify) : undefined;
|
|
417
417
|
const successValue = (cfg.verifySuccessValue ?? "SUCCESS").toLowerCase();
|
|
418
|
-
|
|
418
|
+
// Without a configured verification header there is no cryptographic proof
|
|
419
|
+
// the terminator validated the chain. Default to unverified so
|
|
420
|
+
// requireVerified (default true) rejects spoofed identity-only headers.
|
|
421
|
+
// Operators that intentionally trust a proxy which only forwards identity
|
|
422
|
+
// must set requireVerified: false (and keep a strict behindProxy posture).
|
|
423
|
+
const verified = cfg.verify === undefined ? false : (verifyRaw ?? "").toLowerCase() === successValue;
|
|
419
424
|
const sans = [];
|
|
420
425
|
if (sanRaw) {
|
|
421
426
|
for (const piece of sanRaw.split(",")) {
|
package/dist/multipart.js
CHANGED
|
@@ -55,9 +55,7 @@ function isBlobLike(v) {
|
|
|
55
55
|
if (v == null || typeof v !== "object")
|
|
56
56
|
return false;
|
|
57
57
|
const b = v;
|
|
58
|
-
return (typeof b.size === "number" &&
|
|
59
|
-
typeof b.type === "string" &&
|
|
60
|
-
typeof b.arrayBuffer === "function");
|
|
58
|
+
return (typeof b.size === "number" && typeof b.type === "string" && typeof b.arrayBuffer === "function");
|
|
61
59
|
}
|
|
62
60
|
function mimeMatches(actual, pattern) {
|
|
63
61
|
const a = actual.toLowerCase();
|
|
@@ -137,11 +135,7 @@ function normalizeCustomMagicSignature(value) {
|
|
|
137
135
|
throw new Error("fileField(): magicBytes.bytes entries must be integers in [0, 255].");
|
|
138
136
|
}
|
|
139
137
|
}
|
|
140
|
-
const mimes = value.mime === undefined
|
|
141
|
-
? []
|
|
142
|
-
: typeof value.mime === "string"
|
|
143
|
-
? [value.mime]
|
|
144
|
-
: [...value.mime];
|
|
138
|
+
const mimes = value.mime === undefined ? [] : typeof value.mime === "string" ? [value.mime] : [...value.mime];
|
|
145
139
|
return {
|
|
146
140
|
label: value.label ?? bytes.map((byte) => byte.toString(16).padStart(2, "0")).join(" "),
|
|
147
141
|
mimes,
|
|
@@ -186,9 +180,10 @@ function asciiPrefix(bytes) {
|
|
|
186
180
|
const byte = bytes[i];
|
|
187
181
|
// Keep printable ASCII + common whitespace; replace everything else with
|
|
188
182
|
// a space so keyword searches still work across NULs / UTF-16 padding.
|
|
189
|
-
out +=
|
|
190
|
-
|
|
191
|
-
|
|
183
|
+
out +=
|
|
184
|
+
byte === 0x09 || byte === 0x0a || byte === 0x0d || (byte >= 0x20 && byte <= 0x7e)
|
|
185
|
+
? String.fromCharCode(byte)
|
|
186
|
+
: " ";
|
|
192
187
|
}
|
|
193
188
|
return out.toLowerCase();
|
|
194
189
|
}
|
|
@@ -200,7 +195,9 @@ function detectScriptableImagePayload(bytes) {
|
|
|
200
195
|
}
|
|
201
196
|
// ImageMagick MVG / MSL — vector / scripting formats that can shell out
|
|
202
197
|
// through the `url:`, `ephemeral:`, `msl:` coders (ImageTragick).
|
|
203
|
-
if (prefix.includes("push graphic-context") ||
|
|
198
|
+
if (prefix.includes("push graphic-context") ||
|
|
199
|
+
prefix.startsWith("<msl>") ||
|
|
200
|
+
prefix.includes("<image ")) {
|
|
204
201
|
return "mvg-or-msl";
|
|
205
202
|
}
|
|
206
203
|
// SVG — XML-based and routinely carries `<script>` / external references.
|
package/dist/openapi.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export { httpBearerScheme, httpBasicScheme, apiKeyScheme, oauth2Scheme, openIdCo
|
|
|
14
14
|
export type { ApiKeyLocation, ApiKeyScheme, ApiKeySchemeOptions, HttpBasicScheme, HttpBasicSchemeOptions, HttpBearerScheme, HttpBearerSchemeOptions, OAuth2AuthorizationCodeFlow, OAuth2ClientCredentialsFlow, OAuth2Flows, OAuth2ImplicitFlow, OAuth2PasswordFlow, OAuth2Scheme, OAuth2SchemeOptions, OpenIdConnectScheme, OpenIdConnectSchemeOptions, SecurityScheme, } from "./security-schemes.js";
|
|
15
15
|
export { discriminator, discriminatedUnion } from "./discriminator.js";
|
|
16
16
|
export type { DiscriminatorObject, DiscriminatedUnion, DiscriminatedUnionOptions, } from "./discriminator.js";
|
|
17
|
-
export type { CallbackDefinition, CallbackMap, CallbackOperation
|
|
17
|
+
export type { CallbackDefinition, CallbackMap, CallbackOperation } from "./types.js";
|
|
18
18
|
/** OpenAPI [Info Object](https://spec.openapis.org/oas/v3.1.0#info-object) header fields. */
|
|
19
19
|
export interface OpenAPIInfo {
|
|
20
20
|
/** Human-readable API title shown by Swagger UI / Scalar. */
|
package/dist/openapi.js
CHANGED
|
@@ -102,8 +102,8 @@ function buildOperation(route, path) {
|
|
|
102
102
|
const mergedTags = mergeTags(route.tags, meta?.tags);
|
|
103
103
|
const op = {
|
|
104
104
|
...(route.operationId ? { operationId: route.operationId } : {}),
|
|
105
|
-
...(route.summary ?? meta?.summary ? { summary: route.summary ?? meta?.summary } : {}),
|
|
106
|
-
...(route.description ?? meta?.description
|
|
105
|
+
...((route.summary ?? meta?.summary) ? { summary: route.summary ?? meta?.summary } : {}),
|
|
106
|
+
...((route.description ?? meta?.description)
|
|
107
107
|
? { description: route.description ?? meta?.description }
|
|
108
108
|
: {}),
|
|
109
109
|
...(mergedTags.length ? { tags: mergedTags } : {}),
|
|
@@ -64,10 +64,10 @@ export interface RedisRateLimitStoreOptions {
|
|
|
64
64
|
*/
|
|
65
65
|
prefix?: string;
|
|
66
66
|
/**
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
* Called when the underlying Redis call throws. The default behavior is
|
|
68
|
+
* fail-open, which allows the request and reports it as the first hit in a
|
|
69
|
+
* fresh local window. Override to fail-closed or to wire into your
|
|
70
|
+
* structured logger.
|
|
71
71
|
*/
|
|
72
72
|
onError?: (err: unknown) => "fail-open" | "fail-closed";
|
|
73
73
|
}
|