@daloyjs/core 1.0.0-rc.4 → 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/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,129 @@ 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 &&
381
+ url.indexOf("#") === -1 &&
382
+ url.indexOf("@") === -1) {
383
+ return url;
384
+ }
385
+ try {
386
+ const parsed = new URL(url);
387
+ if (parsed.search === "" && parsed.hash === "") {
388
+ return `${parsed.origin}${parsed.pathname}`;
389
+ }
390
+ const safe = new URL(parsed.origin + parsed.pathname);
391
+ for (const [key, value] of parsed.searchParams) {
392
+ const lower = key.toLowerCase();
393
+ const sensitiveKey = isSensitiveUrlQueryKey(lower);
394
+ const sensitiveValue = JWT_LIKE_RE.test(value) || CREDENTIAL_LIKE_RE.test(value);
395
+ CREDENTIAL_LIKE_RE.lastIndex = 0;
396
+ safe.searchParams.append(key, sensitiveKey || sensitiveValue ? "[REDACTED]" : value);
397
+ }
398
+ return safe.toString();
399
+ }
400
+ catch {
401
+ const cut = url.search(/[?#]/);
402
+ return cut === -1 ? url : url.slice(0, cut);
403
+ }
404
+ }
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. Same-origin and loopback origins are always
359
- * allowed; anything else must be explicitly allowlisted.
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, request, allowlist) {
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
- try {
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.
@@ -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
- for (const [k, v] of headerEntries) {
282
- if (!res.headers.has(k))
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
- const verified = cfg.verify === undefined ? true : (verifyRaw ?? "").toLowerCase() === successValue;
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/router.d.ts CHANGED
@@ -2,9 +2,9 @@
2
2
  * Trie / radix-style router with a static-route fast path.
3
3
  *
4
4
  * Performance:
5
- * - Static (parameter-free) paths resolve via a single Map.get — O(1).
5
+ * - Exact static (parameter-free) paths resolve via a Map lookup — O(1).
6
6
  * - Dynamic paths walk a trie, O(path-segments) regardless of route count.
7
- * - Path string is split by `indexOf` rather than a regex/replace.
7
+ * - Path normalization and splitting avoid regular expressions.
8
8
  *
9
9
  * Safety:
10
10
  * - Path traversal (`..`) and empty segments are rejected at lookup time.
package/dist/router.js CHANGED
@@ -2,9 +2,9 @@
2
2
  * Trie / radix-style router with a static-route fast path.
3
3
  *
4
4
  * Performance:
5
- * - Static (parameter-free) paths resolve via a single Map.get — O(1).
5
+ * - Exact static (parameter-free) paths resolve via a Map lookup — O(1).
6
6
  * - Dynamic paths walk a trie, O(path-segments) regardless of route count.
7
- * - Path string is split by `indexOf` rather than a regex/replace.
7
+ * - Path normalization and splitting avoid regular expressions.
8
8
  *
9
9
  * Safety:
10
10
  * - Path traversal (`..`) and empty segments are rejected at lookup time.
@@ -102,9 +102,12 @@ export class Router {
102
102
  if (path.includes("/../") || path.endsWith("/..") || path.includes("//")) {
103
103
  return undefined;
104
104
  }
105
- // Static fast path.
106
- const normalized = path.replace(/\/+$/, "") || "/";
107
- const staticEntry = this.staticTable.get(normalized);
105
+ // Static fast path. Avoid allocating a normalized string for the common
106
+ // exact-path case; only trim when a trailing slash is actually present.
107
+ let staticEntry = this.staticTable.get(path);
108
+ if (!staticEntry && path.endsWith("/")) {
109
+ staticEntry = this.staticTable.get(trimTrailingSlashes(path));
110
+ }
108
111
  if (staticEntry && staticEntry[method]) {
109
112
  return { handler: staticEntry[method], params: {} };
110
113
  }
@@ -120,8 +123,10 @@ export class Router {
120
123
  }
121
124
  /** Returns the set of methods registered at this exact path (for 405 responses). */
122
125
  allowedMethods(path) {
123
- const normalized = path.replace(/\/+$/, "") || "/";
124
- const fromStatic = this.staticTable.get(normalized);
126
+ let fromStatic = this.staticTable.get(path);
127
+ if (!fromStatic && path.endsWith("/")) {
128
+ fromStatic = this.staticTable.get(trimTrailingSlashes(path));
129
+ }
125
130
  if (fromStatic)
126
131
  return Object.keys(fromStatic);
127
132
  const segments = splitPath(path);
@@ -165,6 +170,8 @@ export class Router {
165
170
  * than letting a `URIError` bubble up as a generic 500.
166
171
  */
167
172
  function safeDecodeURIComponent(segment) {
173
+ if (!segment.includes("%"))
174
+ return segment;
168
175
  try {
169
176
  return decodeURIComponent(segment);
170
177
  }
@@ -187,9 +194,17 @@ function decodeSegments(segs, index) {
187
194
  }
188
195
  return parts.join("/");
189
196
  }
197
+ function trimTrailingSlashes(path) {
198
+ if (path.length === 0)
199
+ return "/";
200
+ let end = path.length;
201
+ while (end > 1 && path.charCodeAt(end - 1) === 47)
202
+ end--;
203
+ return end === path.length ? path : path.slice(0, end);
204
+ }
190
205
  function splitPath(path) {
191
- const clean = path.replace(/\/+$/, "") || "/";
206
+ const clean = trimTrailingSlashes(path);
192
207
  if (clean === "/")
193
208
  return [];
194
- return clean.replace(/^\//, "").split("/");
209
+ return (clean.charCodeAt(0) === 47 ? clean.slice(1) : clean).split("/");
195
210
  }
@@ -19,6 +19,10 @@
19
19
  * Percent-encoded protocol-relative path prefixes such as `/%2f%2f`
20
20
  * are also refused so downstream decoders cannot turn a same-origin
21
21
  * `Location` into an origin-escaping redirect.
22
+ * - Same-origin paths carrying any code point above `U+00FF` are refused:
23
+ * they cannot be represented in the ISO-8859-1 `Location` header, and this
24
+ * also blocks the Unicode slash homographs (`U+2044` `⁄`, `U+2215` `∕`,
25
+ * `U+FF0F` `/`) that `NFKC` normalization can fold into `/`.
22
26
  * - Absolute URLs are only allowed when their `origin` exactly matches
23
27
  * one of the entries in `allowedOrigins`.
24
28
  * - `javascript:`, `data:`, `vbscript:`, and `file:` schemes are always
@@ -47,7 +51,7 @@
47
51
  * @since 0.35.0
48
52
  */
49
53
  /** Reason an open-redirect candidate was refused. */
50
- export type SafeRedirectBlockReason = "empty-target" | "invalid-control-characters" | "protocol-relative" | "backslash-path" | "path-not-allowed" | "origin-not-allowed" | "scheme-not-allowed" | "parse-failed";
54
+ export type SafeRedirectBlockReason = "empty-target" | "invalid-control-characters" | "non-latin1-target" | "protocol-relative" | "backslash-path" | "path-not-allowed" | "origin-not-allowed" | "scheme-not-allowed" | "parse-failed";
51
55
  /** Thrown when {@link safeRedirect} refuses a candidate URL and no `fallback` is configured. */
52
56
  export declare class OpenRedirectBlockedError extends Error {
53
57
  /** Machine-readable {@link SafeRedirectBlockReason} explaining the refusal. */
@@ -19,6 +19,10 @@
19
19
  * Percent-encoded protocol-relative path prefixes such as `/%2f%2f`
20
20
  * are also refused so downstream decoders cannot turn a same-origin
21
21
  * `Location` into an origin-escaping redirect.
22
+ * - Same-origin paths carrying any code point above `U+00FF` are refused:
23
+ * they cannot be represented in the ISO-8859-1 `Location` header, and this
24
+ * also blocks the Unicode slash homographs (`U+2044` `⁄`, `U+2215` `∕`,
25
+ * `U+FF0F` `/`) that `NFKC` normalization can fold into `/`.
22
26
  * - Absolute URLs are only allowed when their `origin` exactly matches
23
27
  * one of the entries in `allowedOrigins`.
24
28
  * - `javascript:`, `data:`, `vbscript:`, and `file:` schemes are always
@@ -65,6 +69,12 @@ const ALLOWED_REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
65
69
  // response-splitting via the `Location` header.
66
70
  // eslint-disable-next-line no-control-regex
67
71
  const CONTROL_CHAR_RE = /[\u0000-\u001f\u007f-\u009f]/;
72
+ // Any code point above U+00FF (outside Latin-1). Such characters cannot be
73
+ // written to a `Location` header — which is serialized as an ISO-8859-1
74
+ // ByteString, so `Headers.set` throws a raw `TypeError` — and they cover the
75
+ // Unicode slash homographs (U+2044, U+2215, U+FF0F) used to smuggle a
76
+ // protocol-relative redirect past a same-origin path check.
77
+ const NON_LATIN1_RE = /[^\x00-\xff]/;
68
78
  function buildResponse(location, status, headers) {
69
79
  const merged = new Headers(headers);
70
80
  merged.set("Location", location);
@@ -97,6 +107,14 @@ function classify(target, allowedPaths, allowedOrigins) {
97
107
  // user agents and proxies — refuse them outright.
98
108
  if (target.includes("\\"))
99
109
  return { ok: false, reason: "backslash-path" };
110
+ // The path is written verbatim into the `Location` header (an ISO-8859-1
111
+ // ByteString). A code point above U+00FF cannot live there — `Headers.set`
112
+ // would throw a raw `TypeError`, escaping this helper's typed error
113
+ // contract and surfacing to callers as an uncaught 500 — so refuse it here
114
+ // with a proper `OpenRedirectBlockedError`. This also blocks Unicode slash
115
+ // homographs that normalization can fold into an origin-escaping `//`.
116
+ if (NON_LATIN1_RE.test(target))
117
+ return { ok: false, reason: "non-latin1-target" };
100
118
  if (allowedPaths.length === 0) {
101
119
  return { ok: false, reason: "path-not-allowed" };
102
120
  }
@@ -191,10 +209,14 @@ export function safeRedirect(target, options = {}) {
191
209
  if (result.ok)
192
210
  return buildResponse(result.location, status, options.headers);
193
211
  if (options.fallback !== undefined) {
194
- if (!options.fallback.startsWith("/") || options.fallback.startsWith("//")) {
195
- throw new TypeError(`safeRedirect: fallback must be a same-origin path starting with "/"; got ${options.fallback}`);
212
+ // Fallback must pass the same path safety checks as a primary same-origin
213
+ // target (no protocol-relative, no backslash confusion, no controls).
214
+ // Do not widen Location emission beyond what classify() would accept.
215
+ const fallbackResult = classify(options.fallback, ["/*"], []);
216
+ if (!fallbackResult.ok || !options.fallback.startsWith("/") || options.fallback.startsWith("//")) {
217
+ throw new TypeError(`safeRedirect: fallback must be a safe same-origin path starting with "/"; got ${options.fallback}`);
196
218
  }
197
- return buildResponse(options.fallback, status, options.headers);
219
+ return buildResponse(fallbackResult.location, status, options.headers);
198
220
  }
199
221
  throw new OpenRedirectBlockedError(result.reason, target);
200
222
  }
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "bomFormat": "CycloneDX",
3
3
  "specVersion": "1.5",
4
- "serialNumber": "urn:uuid:6aa5f3fd-e017-5e4a-b206-49dc6d141768",
4
+ "serialNumber": "urn:uuid:4d7ef2ca-7207-5a53-933a-75950561ff0c",
5
5
  "version": 1,
6
6
  "metadata": {
7
- "timestamp": "2026-07-12T20:39:15.454Z",
7
+ "timestamp": "2026-07-20T09:04:35.490Z",
8
8
  "tools": [
9
9
  {
10
10
  "vendor": "DaloyJS",
11
11
  "name": "daloy-generate-sbom",
12
- "version": "1.0.0-rc.4"
12
+ "version": "1.0.0-rc.5"
13
13
  }
14
14
  ],
15
15
  "authors": [
@@ -19,11 +19,11 @@
19
19
  ],
20
20
  "component": {
21
21
  "type": "library",
22
- "bom-ref": "pkg:npm/@daloyjs/core@1.0.0-rc.4",
22
+ "bom-ref": "pkg:npm/@daloyjs/core@1.0.0-rc.5",
23
23
  "name": "@daloyjs/core",
24
- "version": "1.0.0-rc.4",
24
+ "version": "1.0.0-rc.5",
25
25
  "description": "DaloyJS is a runtime-portable, contract-first TypeScript web framework with built-in OpenAPI (Hey API), typed client generation, large-scale maintainability, and security-first defaults. Hono-grade portability, Elysia-grade DX, FastAPI-grade docs, Fastify-grade ops — distributed via pnpm.",
26
- "purl": "pkg:npm/@daloyjs/core@1.0.0-rc.4",
26
+ "purl": "pkg:npm/@daloyjs/core@1.0.0-rc.5",
27
27
  "licenses": [
28
28
  {
29
29
  "license": {
@@ -46,9 +46,9 @@
46
46
  }
47
47
  ],
48
48
  "swid": {
49
- "tagId": "swidtag--daloyjs-core-1.0.0-rc.4",
49
+ "tagId": "swidtag--daloyjs-core-1.0.0-rc.5",
50
50
  "name": "@daloyjs/core",
51
- "version": "1.0.0-rc.4",
51
+ "version": "1.0.0-rc.5",
52
52
  "tagVersion": 0,
53
53
  "patch": false
54
54
  }
@@ -57,7 +57,7 @@
57
57
  "components": [],
58
58
  "dependencies": [
59
59
  {
60
- "ref": "pkg:npm/@daloyjs/core@1.0.0-rc.4",
60
+ "ref": "pkg:npm/@daloyjs/core@1.0.0-rc.5",
61
61
  "dependsOn": []
62
62
  }
63
63
  ]
@@ -2,10 +2,10 @@
2
2
  "spdxVersion": "SPDX-2.3",
3
3
  "dataLicense": "CC0-1.0",
4
4
  "SPDXID": "SPDXRef-DOCUMENT",
5
- "name": "@daloyjs/core-1.0.0-rc.4",
6
- "documentNamespace": "https://github.com/daloyjs/daloy/sbom/@daloyjs/core-1.0.0-rc.4-6aa5f3fd-e017-5e4a-b206-49dc6d141768",
5
+ "name": "@daloyjs/core-1.0.0-rc.5",
6
+ "documentNamespace": "https://github.com/daloyjs/daloy/sbom/@daloyjs/core-1.0.0-rc.5-4d7ef2ca-7207-5a53-933a-75950561ff0c",
7
7
  "creationInfo": {
8
- "created": "2026-07-12T20:39:15.454Z",
8
+ "created": "2026-07-20T09:04:35.490Z",
9
9
  "creators": [
10
10
  "Tool: daloy-generate-sbom",
11
11
  "Organization: DaloyJS"
@@ -16,7 +16,7 @@
16
16
  {
17
17
  "SPDXID": "SPDXRef-Package--daloyjs-core",
18
18
  "name": "@daloyjs/core",
19
- "versionInfo": "1.0.0-rc.4",
19
+ "versionInfo": "1.0.0-rc.5",
20
20
  "downloadLocation": "https://github.com/daloyjs/daloy",
21
21
  "filesAnalyzed": false,
22
22
  "licenseConcluded": "MIT",
@@ -27,7 +27,7 @@
27
27
  {
28
28
  "referenceCategory": "PACKAGE-MANAGER",
29
29
  "referenceType": "purl",
30
- "referenceLocator": "pkg:npm/@daloyjs/core@1.0.0-rc.4"
30
+ "referenceLocator": "pkg:npm/@daloyjs/core@1.0.0-rc.5"
31
31
  }
32
32
  ]
33
33
  }
@@ -141,6 +141,11 @@ export declare const SMUGGLING_SINGLETON_HEADERS: readonly string[];
141
141
  * Throws {@link BadRequestError} so the framework returns a structured
142
142
  * `400 problem+json` instead of forwarding a smuggling-class request.
143
143
  *
144
+ * The framework's dispatch path runs this check, the reserved-prefix check,
145
+ * and the header-count cap in a single shared walk internally (see
146
+ * {@link assertInboundHeaderGuards}); calling this helper directly is only
147
+ * needed for custom pipelines.
148
+ *
144
149
  * @param headers - Normalized request headers to inspect.
145
150
  * @since 0.15.0
146
151
  */
@@ -179,6 +184,10 @@ export declare const RESERVED_INBOUND_HEADER_PREFIXES: readonly string[];
179
184
  * `400 problem+json` instead of routing a request that may be probing
180
185
  * for an internal-dispatch bypass.
181
186
  *
187
+ * The framework's dispatch path runs this check and the header-count cap
188
+ * in a single shared walk internally (see {@link assertInboundHeaderGuards});
189
+ * calling this helper directly is only needed for custom pipelines.
190
+ *
182
191
  * @param headers - Normalized request headers to inspect (names arrive lowercased).
183
192
  * @since 0.36.0
184
193
  */
@@ -212,11 +221,43 @@ export declare const DEFAULT_MAX_HEADER_COUNT = 100;
212
221
  * framework returns a structured `problem+json` response instead of routing
213
222
  * a flood.
214
223
  *
224
+ * The framework's dispatch path runs this check and the reserved-prefix
225
+ * check in a single shared walk internally (see
226
+ * {@link assertInboundHeaderGuards}); calling this helper directly is only
227
+ * needed for custom pipelines.
228
+ *
215
229
  * @param headers - The incoming request headers.
216
230
  * @param limit - Maximum distinct header fields to allow. `0` disables.
217
231
  * @since 0.38.0
218
232
  */
219
233
  export declare function assertHeaderCountWithinLimit(headers: Headers, limit: number): void;
234
+ /**
235
+ * Combined inbound header guards for the dispatch hot path (internal).
236
+ *
237
+ * Runs the singleton-duplicate check ({@link assertNoDuplicateSingletonHeaders}),
238
+ * the reserved-internal-prefix check ({@link assertNoReservedInternalHeaders}),
239
+ * and the header-count cap ({@link assertHeaderCountWithinLimit}) in a
240
+ * **single** walk of the header map, with the same observable semantics as
241
+ * calling the helpers in sequence: any `400`-class violation (duplicate
242
+ * singleton header or reserved internal header) anywhere in the map is
243
+ * rejected with `400` even when the map also exceeds the count cap. To
244
+ * preserve that precedence, the `431` count-cap rejection is deferred until
245
+ * the scan has covered every header — the same full-walk cost the
246
+ * sequential helpers already paid.
247
+ *
248
+ * A non-positive / non-finite `limit` disables the count cap (same as
249
+ * {@link assertHeaderCountWithinLimit}) while still rejecting duplicate
250
+ * singleton headers and reserved internal prefixes.
251
+ *
252
+ * @param headers - Normalized request headers (names arrive lowercased).
253
+ * @param limit - Maximum distinct header fields to allow. `0` disables the count cap.
254
+ * @throws {BadRequestError} When a duplicate singleton header or a reserved
255
+ * internal header is present (takes precedence over the count cap).
256
+ * @throws {RequestHeaderFieldsTooLargeError} When no `400`-class violation is
257
+ * present and the distinct-header count exceeds `limit`.
258
+ * @since 1.0.0
259
+ */
260
+ export declare function assertInboundHeaderGuards(headers: Headers, limit: number): void;
220
261
  /**
221
262
  * Minimum acceptable secret length in bytes for HMAC / signing material in
222
263
  * production (boot guard). Matches the OWASP "Secret Management"