@blamejs/core 0.14.4 → 0.14.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +1 -0
  3. package/lib/a2a-tasks.js +6 -6
  4. package/lib/ai-input.js +1 -1
  5. package/lib/auth/sd-jwt-vc.js +1 -1
  6. package/lib/calendar.js +6 -6
  7. package/lib/content-credentials.js +2 -2
  8. package/lib/cra-report.js +3 -3
  9. package/lib/guard-cidr.js +1 -1
  10. package/lib/http-client-cache.js +1 -1
  11. package/lib/mail-auth.js +1 -1
  12. package/lib/mail-crypto-smime.js +1 -1
  13. package/lib/mail-deploy.js +1 -1
  14. package/lib/mail-dkim.js +1 -1
  15. package/lib/mail-server-jmap.js +2 -2
  16. package/lib/mcp.js +6 -6
  17. package/lib/middleware/age-gate.js +20 -7
  18. package/lib/middleware/bearer-auth.js +36 -35
  19. package/lib/middleware/bot-guard.js +17 -5
  20. package/lib/middleware/compose-pipeline.js +1 -1
  21. package/lib/middleware/cors.js +28 -12
  22. package/lib/middleware/csrf-protect.js +22 -14
  23. package/lib/middleware/daily-byte-quota.js +27 -13
  24. package/lib/middleware/deny-response.js +140 -0
  25. package/lib/middleware/dpop.js +32 -19
  26. package/lib/middleware/fetch-metadata.js +21 -12
  27. package/lib/middleware/host-allowlist.js +19 -8
  28. package/lib/middleware/index.js +3 -0
  29. package/lib/middleware/network-allowlist.js +24 -10
  30. package/lib/middleware/rate-limit.js +22 -5
  31. package/lib/middleware/require-aal.js +25 -10
  32. package/lib/middleware/require-auth.js +32 -16
  33. package/lib/middleware/require-bound-key.js +49 -18
  34. package/lib/middleware/require-content-type.js +19 -8
  35. package/lib/middleware/require-methods.js +17 -7
  36. package/lib/middleware/require-mtls.js +27 -14
  37. package/lib/network.js +4 -4
  38. package/lib/safe-decompress.js +1 -1
  39. package/lib/safe-url.js +1 -1
  40. package/lib/stream-throttle.js +2 -2
  41. package/lib/websocket.js +2 -2
  42. package/package.json +1 -1
  43. package/sbom.cdx.json +6 -6
package/CHANGELOG.md CHANGED
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.14.x
10
10
 
11
+ - v0.14.6 (2026-05-30) — **Access-refusal middleware can return RFC 9457 problem+json or a custom response, and several documented-but-uncallable APIs are now reachable.** Every access-refusal middleware — the auth gates (bearer, DPoP, mTLS, AAL, bound-key), CSRF, CORS, rate-limit, bot-guard, age-gate, the host and network allowlists, and the method and content-type gates — now accepts two uniform options: `problemDetails: true` returns an RFC 9457 `application/problem+json` body, and `onDeny(req, res, info)` hands the response to the caller. With neither set the refusal is byte-for-byte what it was, so this is a drop-in change that lets a service standardize one error envelope across its API instead of working around each middleware's hardcoded body. Alongside that: `b.middleware.requireBoundKey` is now exported (it was documented and tested but never wired into the middleware surface), `b.middleware.bearerAuth` accepts `requiredScopes` (previously rejected at construction, which made its scope-enforcement path unreachable), API-key refusals send the RFC 6750 challenge code that matches the failure, two documented call paths that named a missing namespace segment are corrected, and the release flow now flags stale GitHub Actions and vendored bundles — with a ready-to-paste pin — before a dependency PR is needed. **Added:** *Uniform `onDeny` and `problemDetails` options on every access-refusal middleware* — Each request-lifecycle middleware that refuses a request now takes `problemDetails: true` to emit an RFC 9457 `application/problem+json` body (composing `b.problemDetails`) and `onDeny(req, res, info)` to take over the response entirely; `info` carries the status, a machine reason, and the middleware-specific fields. The deny-path response headers (`Allow`, `WWW-Authenticate`, `Retry-After`, `Accept`) survive every mode. When neither option is set the response is unchanged. Covers `requireAuth`, `requireAal`, `requireMethods`, `requireContentType`, `requireMtls`, `requireBoundKey`, `bearerAuth`, `dpop`, `csrfProtect`, `fetchMetadata`, `botGuard`, `ageGate`, `hostAllowlist`, `networkAllowlist`, `cors`, `rateLimit`, and `dailyByteQuota` (whose existing `onExceeded` keeps working as an alias of `onDeny`). **Fixed:** *`b.middleware.requireBoundKey` is now callable* — The Bearer-API-key middleware was documented (with examples and tests) but never exported on `b.middleware`, so `b.middleware.requireBoundKey(...)` threw `undefined is not a function`. It is now wired into the middleware surface. · *`b.middleware.bearerAuth` accepts `requiredScopes`* — The RFC 6750 scope-enforcement path read `opts.requiredScopes`, but the option was rejected at construction with `unknown option`, making the 403 `insufficient_scope` behavior unreachable. `requiredScopes` is now an accepted option. · *RFC 6750 challenge codes on API-key refusals* — `b.middleware.requireBoundKey` now sends the `WWW-Authenticate` error code that matches the failure: `insufficient_scope` on a 403 missing-scope, `invalid_token` on an unknown or revoked token, and no error code on a 401 that presented no credentials (RFC 6750 §3). It previously sent `invalid_request` for every refusal. · *Corrected two documented call paths* — The compliance and network references named a path that dropped a namespace segment: the conformity-assessment scaffold is at `b.cra.report.conformityAssessment` (not `b.cra.conformityAssessment`), and the per-socket tuning helper is at `b.network.socket.applyToSocket` (not `b.network.applyToSocket`). The documented signatures now match the callable paths. · *GitHub Actions pins refreshed* — `github/codeql-action` 4.35.5 to 4.36.0, and `docker/login-action`, `docker/setup-buildx-action`, and `docker/setup-qemu-action` to their latest releases. **Detectors:** *`@primitive` reachability gate* — A new check resolves every documented `b.X.Y` primitive against the actual public surface and fails the build when a documented path is not callable (factory-instance shorthands excluded). This is the gate that would have caught the `requireBoundKey` and call-path issues above. · *Deny-path composition gate* — A new check requires every access-refusal middleware to route its refusal through the shared deny-response writer, so a future middleware cannot reintroduce a hardcoded body that locks callers out of `onDeny` / `problemDetails`. · *Actions and vendor currency in the release flow* — The release flow now fails the cut when a SHA-pinned GitHub Action or a vendored bundle is behind its latest upstream release. The actions report prints a ready-to-paste `owner/repo@<sha> # vX.Y.Z` pin and every file and line that uses it, so the bump is copy-paste rather than an after-the-fact dependency PR. Transient registry or API errors stay advisory so a flaky network response does not block an unrelated release.
12
+
13
+ - v0.14.5 (2026-05-30) — **Finished cleaning up the mislabeled byte-literal lint suppressions, with no API or behavior changes.** A follow-up to the byte-literal lint tightening. The remaining suppression comments that named the byte-literal check on values that are not byte sizes — JSON-RPC error codes, HTTP status codes, octet ranges, day-in-milliseconds constants — are removed, keeping their explanatory text and any correctly-named companion suppression. Every byte-literal suppression that remains is now on genuine 1024-scale byte arithmetic. Source-comment hygiene only. **Changed:** *Remaining mislabeled byte-literal suppressions removed* — The byte-literal lint was previously a check on any multiple-of-8 integer, so suppression comments naming it were scattered across non-byte values. The last of those (in a handful of files, in mixed comment formats) are now removed — their explanatory text is retained as plain comments, and any correctly-named companion suppression is kept. The only byte-literal suppressions that remain are on genuine 1024-scale byte arithmetic. No change to any exported API, error code, wire format, or runtime behavior.
14
+
11
15
  - v0.14.4 (2026-05-30) — **Removed three pieces of dead code from the SAML, TLS, and JMAP surfaces; no API or behavior changes.** Cleanup of unreachable code. A reverse signature-algorithm lookup in the SAML verifier was never called — the actual verification path resolves the algorithm through the supported-signature table — so it is removed and a stale comment that referenced it is corrected. A leftover no-op placeholder in the TLS certificate re-encode path (a zero-length slice that was assigned and discarded) is removed, leaving the verbatim extension re-encode it sat next to. An unused JMAP well-known-path constant that existed only to be discarded is removed. None of this changes any exported API, error code, wire format, or runtime behavior. **Removed:** *Unreachable code in SAML, TLS, and JMAP* — Removed `_sigAlgFromUri` from the SAML module (a reverse alg lookup that was never called — the embedded XML-DSig verifier resolves the algorithm via the supported-signature table, and the redirect-binding path uses the forward `_sigAlgUrn`), a discarded zero-length-slice placeholder in the TLS certificate extension re-encode path, and an unused well-known-path constant in the JMAP server. Internal cleanup only — no change to any exported API, error code, wire format, or runtime behavior.
12
16
 
13
17
  - v0.14.3 (2026-05-30) — **A codebase check now ensures every lint-suppression marker names a real check, so a typo can't silently disable a guard.** Source files suppress an individual lint with an `// allow:<class>` comment. If the class is mistyped or stale, the comment suppresses nothing — the check it names does not exist — so the issue it was meant to explain ships unflagged. A new codebase check now verifies every `// allow:<class>` marker names a registered check class and fails if it does not, with the full set of valid classes maintained as an explicit registry. Two markers that named a non-kebab class were corrected as part of this. No runtime, API, or wire-format changes. **Detectors:** *Lint-suppression markers must name a registered check* — A new check flags any `// allow:<class>` suppression comment whose class is not one of the registered check classes — catching typos and stale markers (for example a marker that named a check which was later renamed) that would otherwise silently disable the guard they appear to explain. The valid classes are kept as an explicit registry, so adding a check with a new allow-class is a one-line registration. Source-comment hygiene only — no change to any exported API, error code, wire format, or runtime behavior.
package/README.md CHANGED
@@ -129,6 +129,7 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
129
129
  - CSRF protection — double-submit cookie + Origin/Referer cross-check; auto-skips Authorization-header / cookieless requests, which are not CSRF-able (`b.middleware.csrfProtect`)
130
130
  - CORS (W3C Private Network Access preflight refusal default + `allowPrivateNetwork` opt) and rate-limit are wired when configured via `middleware.cors` / `middleware.rateLimit`
131
131
  - `Cache-Control: no-store` on every 401 from `requireAuth` / `requireAal` / `requireStepUp` per RFC 9111 §5.2.2.5
132
+ - Every access-refusal layer takes a uniform `problemDetails: true` for an RFC 9457 `application/problem+json` body or `onDeny(req, res, info)` to render the refusal itself — so a service can standardize one error envelope across its API without working around hardcoded bodies (`b.problemDetails`)
132
133
  - **Additional middleware** to mount in your `routes` callback: compression, SSE, request logging, request-time DB role binding (`b.middleware.dbRoleFor`), in-process CIDR fence (`b.middleware.networkAllowlist`)
133
134
  - **Outbound HTTP client** — HTTP/1.1 + HTTP/2 with SSRF gate (cloud-metadata IPs hard-denied; private / loopback / link-local overridable per call); scheme + userinfo + per-host destination allowlist; redirects, multipart, interceptors, progress, encrypted cookie jar (`b.httpClient`, `b.ssrfGuard`, `b.safeUrl`)
134
135
  - **Network configurability (`b.network`)** — env-driven NTP / NTS (RFC 8915), IPv4/IPv6 NTP, DNS with IPv6 / DoH / DoT (private-CA pinning) / cache / lookup timeout; local DNSSEC signature verification (RFC 4035 — `b.network.dns.dnssec.verifyRrset` over a canonicalised RRset against RSA / ECDSA P-256·P-384 / Ed25519 DNSKEYs, plus DS-digest + key-tag, plus `verifyDenial` for NSEC / NSEC3 (RFC 5155) NXDOMAIN / NODATA proofs with iteration caps + Opt-Out handling, plus `verifyChain` to validate a full root→TLD→zone delegation chain against the pinned IANA root anchors) so a resolver client can verify both positive and negative answers instead of trusting the upstream AD bit; DANE / TLSA certificate matching (RFC 6698/7671 — `b.network.dns.dane.matchCertificate`) to pin a service's key through DNSSEC instead of a public CA; TSIG transaction signatures (RFC 8945 — `b.network.dns.tsig.sign` / `verify`) for shared-key HMAC authentication of zone transfers, dynamic updates, and query/response pairs, with constant-time MAC compare + fudge-window check (verified against dnspython); outbound HTTP proxy (`HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY`); runtime DPI trust-store CA additions; application-level heartbeats; TCP socket defaults
package/lib/a2a-tasks.js CHANGED
@@ -62,17 +62,17 @@ var A2aTasksError = defineClass("A2aTasksError", { alwaysPermanent: true });
62
62
  var JSONRPC_VERSION = "2.0";
63
63
 
64
64
  // JSON-RPC 2.0 fixed error codes — A2A inherits these.
65
- var JSONRPC_PARSE_ERROR = -32700; // allow:raw-byte-literal — JSON-RPC fixed code / allow:raw-time-literal — not seconds
66
- var JSONRPC_INVALID_REQUEST = -32600; // allow:raw-byte-literal — JSON-RPC fixed code / allow:raw-time-literal — not seconds
67
- var JSONRPC_METHOD_NOT_FOUND = -32601; // allow:raw-byte-literal — JSON-RPC fixed code / allow:raw-time-literal — not seconds
68
- var JSONRPC_INVALID_PARAMS = -32602; // allow:raw-byte-literal — JSON-RPC fixed code / allow:raw-time-literal — not seconds
69
- var JSONRPC_INTERNAL_ERROR = -32603; // allow:raw-byte-literal — JSON-RPC fixed code / allow:raw-time-literal — not seconds
65
+ var JSONRPC_PARSE_ERROR = -32700; // allow:raw-time-literal — not seconds
66
+ var JSONRPC_INVALID_REQUEST = -32600; // allow:raw-time-literal — not seconds
67
+ var JSONRPC_METHOD_NOT_FOUND = -32601; // allow:raw-time-literal — not seconds
68
+ var JSONRPC_INVALID_PARAMS = -32602; // allow:raw-time-literal — not seconds
69
+ var JSONRPC_INTERNAL_ERROR = -32603; // allow:raw-time-literal — not seconds
70
70
 
71
71
  // A2A-specific error codes per the spec's task-error vocabulary.
72
72
  // A2A_TASK_NOT_FOUND (-32002) + A2A_TASK_NOT_CANCELABLE (-32003) are
73
73
  // raised by operator handlers — they're reserved here for documentation
74
74
  // purposes only.
75
- var A2A_SCOPE_DENIED = -32001; // allow:raw-byte-literal — JSON-RPC server-error range / allow:raw-time-literal — not seconds
75
+ var A2A_SCOPE_DENIED = -32001; // allow:raw-time-literal — not seconds
76
76
 
77
77
  var ALLOWED_METHODS = Object.freeze(["tasks/send", "tasks/get", "tasks/cancel"]);
78
78
 
package/lib/ai-input.js CHANGED
@@ -26,7 +26,7 @@ var audit = require("./audit");
26
26
  var { AiInputError } = require("./framework-error");
27
27
 
28
28
  var SAMPLE_TRUNC = 80; // sample truncation length, not bytes
29
- var CONFIDENCE_BASE = 60; // allow:raw-byte-literal — confidence percentage base / allow:raw-time-literal — not seconds
29
+ var CONFIDENCE_BASE = 60; // allow:raw-time-literal — not seconds
30
30
 
31
31
  var PATTERNS = [
32
32
  { id: "ignore-prior-instructions", severity: 3, re:
@@ -597,7 +597,7 @@ async function verify(presentation, opts) {
597
597
  }
598
598
  // Verify KB-JWT signature
599
599
  var kbHeaderObj;
600
- try { kbHeaderObj = safeJson.parse(_b64uDecodeStr(maybeKbJwt.split(".")[0]), { maxBytes: 4096 }); } // allow:bare-json-parse — kb header from validated KB-JWT; signature verifies // allow:raw-byte-literal — kb-header cap (4 KB)
600
+ try { kbHeaderObj = safeJson.parse(_b64uDecodeStr(maybeKbJwt.split(".")[0]), { maxBytes: 4096 }); } // allow:bare-json-parse — kb header from validated KB-JWT; signature verifies
601
601
  catch (e) {
602
602
  throw new AuthError("auth-sd-jwt-vc/bad-kb-header",
603
603
  "verify: malformed KB-JWT header: " + e.message);
package/lib/calendar.js CHANGED
@@ -99,7 +99,7 @@ var JSCAL_NOTE_STATUS = Object.freeze({
99
99
  // Recurrence-expansion caps. Mirror b.safeIcal's RRULE limits so the
100
100
  // expand path can't outpace what the parser already permitted.
101
101
  var MAX_EXPAND_INSTANCES = 4096; // instance count cap, not bytes
102
- var MAX_EXPAND_SPAN_MS = 10 * 365 * 24 * 60 * 60 * 1000; // allow:raw-byte-literal + allow:raw-time-literal — 10 year max expansion span
102
+ var MAX_EXPAND_SPAN_MS = 10 * 365 * 24 * 60 * 60 * 1000; // allow:raw-time-literal — 10 year max expansion span
103
103
 
104
104
  /**
105
105
  * @primitive b.calendar.validate
@@ -697,7 +697,7 @@ function _expandSingleRule(rule, startMs, ctx) {
697
697
  }
698
698
  var byHourSet = _bySet(rule.byHour, 0, 23); // RFC 5545 hour range
699
699
  var byMinuteSet = _bySet(rule.byMinute, 0, 59); // RFC 5545 minute range
700
- var bySecondSet = _bySet(rule.bySecond, 0, 60); // allow:raw-byte-literal — RFC 5545 second range incl. leap second // allow:raw-time-literal — second-of-minute bound, not a duration
700
+ var bySecondSet = _bySet(rule.bySecond, 0, 60); // allow:raw-time-literal — second-of-minute bound, not a duration
701
701
 
702
702
  function _isoWeekParts(d) {
703
703
  // ISO 8601 week-of-year + week-year. The week-YEAR can differ
@@ -709,7 +709,7 @@ function _expandSingleRule(rule, startMs, ctx) {
709
709
  tmp.setUTCDate(tmp.getUTCDate() + 4 - dayOfWeek); // ISO week-year anchor (Thursday)
710
710
  var weekYear = tmp.getUTCFullYear();
711
711
  var yearStart = new Date(Date.UTC(weekYear, 0, 1));
712
- var week = Math.ceil((((tmp - yearStart) / 86400000) + 1) / 7); // allow:raw-time-literal — 86400000 ms/day, 7 days/week // allow:raw-byte-literal
712
+ var week = Math.ceil((((tmp - yearStart) / 86400000) + 1) / 7); // allow:raw-time-literal — 86400000 ms/day, 7 days/week
713
713
  return { week: week, year: weekYear };
714
714
  }
715
715
  function _isoWeekOf(d) {
@@ -717,7 +717,7 @@ function _expandSingleRule(rule, startMs, ctx) {
717
717
  }
718
718
  function _yearDayOf(d) {
719
719
  var startOfYear = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
720
- return Math.floor((d - startOfYear) / 86400000) + 1; // allow:raw-time-literal — 86400000 ms/day // allow:raw-byte-literal
720
+ return Math.floor((d - startOfYear) / 86400000) + 1; // allow:raw-time-literal — 86400000 ms/day
721
721
  }
722
722
  function _daysInYear(year) {
723
723
  return ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0) ? 366 : 365; // Gregorian leap-year rule
@@ -869,7 +869,7 @@ function _expandWithBysetpos(ctx) {
869
869
  stepBudgetRef.remaining -= 1;
870
870
  var candidate = _withTimeOfDay(dayMs, hh, mm, ss, ms);
871
871
  if (matchesBy(candidate)) candidates.push(candidate);
872
- dayMs += 86400000; // allow:raw-time-literal — 86400000 ms/day step // allow:raw-byte-literal — same constant in ms/day form
872
+ dayMs += 86400000; // allow:raw-time-literal — 86400000 ms/day step
873
873
  }
874
874
 
875
875
  // Sort + apply BYSETPOS. Positive index 1-based from start;
@@ -930,7 +930,7 @@ function _periodForIndex(freq, startDate, offset) {
930
930
  var dow = anchor.getUTCDay() || 7;
931
931
  anchor.setUTCDate(anchor.getUTCDate() - (dow - 1) + offset * 7); // days/week
932
932
  var ws = anchor.getTime();
933
- var we = ws + 7 * 86400000 - 1; // allow:raw-byte-literal + allow:raw-time-literal — 7-day window
933
+ var we = ws + 7 * 86400000 - 1; // allow:raw-time-literal — 7-day window
934
934
  return { startMs: ws, endMs: we };
935
935
  }
936
936
 
@@ -590,7 +590,7 @@ var CAC_KIND_ENUM = Object.freeze({
590
590
  text: true, image: true, audio: true, video: true,
591
591
  "virtual-scene": true, other: true,
592
592
  });
593
- var CAC_USCC_RE = /^[0-9A-HJ-NPQRTUWXY]{18}$/; // allow:raw-byte-literal — GB 32100-2015 USCC fixed length, not bytes // allow:raw-time-literal — 18 is char-count of the credit code, not seconds
593
+ var CAC_USCC_RE = /^[0-9A-HJ-NPQRTUWXY]{18}$/; // allow:raw-time-literal — 18 is char-count of the credit code, not seconds
594
594
  var ISO8601_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/;
595
595
 
596
596
  function cacImplicitLabel(opts) {
@@ -605,7 +605,7 @@ function cacImplicitLabel(opts) {
605
605
  throw new ContentCredentialsError("cac-implicit-label/oversize-provider-name",
606
606
  "cacImplicitLabel: providerName exceeds " + STR_LEN_MAX + " bytes (UTF-8)");
607
607
  }
608
- if (typeof opts.providerCode !== "string" || opts.providerCode.length !== 18 || // allow:raw-byte-literal — USCC fixed length (GB 32100-2015), not bytes // allow:raw-time-literal — string length, not seconds
608
+ if (typeof opts.providerCode !== "string" || opts.providerCode.length !== 18 || // allow:raw-time-literal — string length, not seconds
609
609
  !CAC_USCC_RE.test(opts.providerCode)) { // allow:regex-no-length-cap — length-bounded immediately above
610
610
  throw new ContentCredentialsError("cac-implicit-label/bad-provider-code",
611
611
  "cacImplicitLabel: providerCode must be an 18-char unified social credit code " +
package/lib/cra-report.js CHANGED
@@ -190,8 +190,8 @@ function create(opts) {
190
190
  }
191
191
 
192
192
  /**
193
- * @primitive b.cra.conformityAssessment
194
- * @signature b.cra.conformityAssessment(opts)
193
+ * @primitive b.cra.report.conformityAssessment
194
+ * @signature b.cra.report.conformityAssessment(opts)
195
195
  * @since 0.8.77
196
196
  *
197
197
  * EU Cyber Resilience Act (Regulation 2024/2847) — Annex VIII
@@ -223,7 +223,7 @@ function create(opts) {
223
223
  * }
224
224
  *
225
225
  * @example
226
- * var dossier = b.cra.conformityAssessment({
226
+ * var dossier = b.cra.report.conformityAssessment({
227
227
  * manufacturer: { name: "Acme Inc.", address: "1 St", contact: "ce@acme.example" },
228
228
  * product: { name: "Widget Pro", identifier: "WID-001", version: "1.0", description: "..." },
229
229
  * classification: "default",
package/lib/guard-cidr.js CHANGED
@@ -73,7 +73,7 @@ var IPV4_RESERVED = Object.freeze([
73
73
  { net: _ipv4ToUint32([127, 0, 0, 0]), prefix: 8, label: "loopback" }, // IPv4 octets
74
74
  { net: _ipv4ToUint32([169, 254, 0, 0]), prefix: 16, label: "link-local" }, // IPv4 octets
75
75
  { net: _ipv4ToUint32([224, 0, 0, 0]), prefix: 4, label: "multicast" }, // IPv4 octets
76
- { net: _ipv4ToUint32([240, 0, 0, 0]), prefix: 4, label: "reserved-class-e" }, // allow:raw-byte-literal — IPv4 octets allow:raw-time-literal — 240 is an IPv4 octet not seconds
76
+ { net: _ipv4ToUint32([240, 0, 0, 0]), prefix: 4, label: "reserved-class-e" }, // allow:raw-time-literal — 240 is an IPv4 octet not seconds
77
77
  { net: _ipv4ToUint32([192, 0, 2, 0]), prefix: 24, label: "documentation-test-net-1" }, // IPv4 octets
78
78
  { net: _ipv4ToUint32([198, 51, 100, 0]), prefix: 24, label: "documentation-test-net-2" }, // IPv4 octets
79
79
  { net: _ipv4ToUint32([203, 0, 113, 0]), prefix: 24, label: "documentation-test-net-3" }, // IPv4 octets
@@ -66,7 +66,7 @@ var HEURISTIC_MAX_AGE_MS = C.TIME.hours(24);
66
66
  // Statuses RFC 9110 designates as heuristically cacheable. (Plus 200/206
67
67
  // which are universally cacheable when a freshness lifetime is given.)
68
68
  var CACHEABLE_STATUSES = new Set([
69
- 200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501, // allow:raw-byte-literal — HTTP status codes per RFC 9110 / allow:raw-time-literal — same line, status codes not seconds
69
+ 200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501, // allow:raw-time-literal — same line, status codes not seconds
70
70
  ]);
71
71
 
72
72
  // Headers that MUST not be forwarded when serving a 304-updated entry.
package/lib/mail-auth.js CHANGED
@@ -1476,7 +1476,7 @@ async function _verifyAmsViaDkim(rfc822, hop, sigValue, tags, dkim, dnsLookup) {
1476
1476
 
1477
1477
  function _parseArcTagList(value) {
1478
1478
  var tags = {};
1479
- var parts = String(value).split(";"); // allow:bare-split-on-quoted-header — allow:raw-byte-literal — RFC 8617 §4 ARC tag-list grammar (same as the DKIM RFC's): `tag-spec *( ";" tag-spec )`, tag-value contains no DQUOTE
1479
+ var parts = String(value).split(";"); // allow:bare-split-on-quoted-header — RFC 8617 §4 ARC tag-list grammar (same as the DKIM RFC's): `tag-spec *( ";" tag-spec )`, tag-value contains no DQUOTE
1480
1480
 
1481
1481
  for (var i = 0; i < parts.length; i += 1) {
1482
1482
  var p = parts[i].trim();
@@ -754,7 +754,7 @@ function checkCert(opts) {
754
754
  if (pub && pub.asymmetricKeyType === "rsa") {
755
755
  var jwk = pub.export({ format: "jwk" });
756
756
  var nBytes = Buffer.from(jwk.n, "base64url");
757
- var bits = nBytes.length * 8; // allow:raw-byte-literal — bits-per-byte conversion // allow:raw-time-literal — RFC 5280 in comment, not seconds
757
+ var bits = nBytes.length * 8; // allow:raw-time-literal — RFC 5280 in comment, not seconds
758
758
  if (bits < RSA_MIN_BITS) {
759
759
  throw new MailCryptoError("mail-crypto/smime/rsa-too-small",
760
760
  "cert public key is " + bits + " RSA bits; minimum is " + RSA_MIN_BITS +
@@ -155,7 +155,7 @@ function mtaStsPublish(opts) {
155
155
  throw new MailDeployError("mail-deploy/bad-max-age",
156
156
  "mtaStsPublish: opts.maxAgeSec must be a positive integer");
157
157
  }
158
- if (opts.maxAgeSec > 31557600) { // allow:raw-time-literal — 1 year in seconds (RFC 8461 §3.2 max_age unit) // allow:raw-byte-literal — same numeric, no byte semantic
158
+ if (opts.maxAgeSec > 31557600) { // allow:raw-time-literal — 1 year in seconds (RFC 8461 §3.2 max_age unit)
159
159
  throw new MailDeployError("mail-deploy/bad-max-age",
160
160
  "mtaStsPublish: opts.maxAgeSec exceeds 1 year (RFC 8461 §3.2 SHOULD ≤ 31557600)");
161
161
  }
package/lib/mail-dkim.js CHANGED
@@ -950,7 +950,7 @@ async function verify(rfc822, opts) {
950
950
  // Allow up to 24h future-skew; beyond that, refuse — neither
951
951
  // operator clock drift nor delivery latency explains a future-
952
952
  // dated signing time of more than a day.
953
- if (isFinite(tSec) && tSec - (24 * 60 * 60) > nowSec) { // allow:raw-byte-literal — Unix-seconds offset, not bytes / allow:raw-time-literal — 24h future-date sanity ceiling
953
+ if (isFinite(tSec) && tSec - (24 * 60 * 60) > nowSec) { // allow:raw-time-literal — 24h future-date sanity ceiling
954
954
  results.push({ d: d || null, s: s || null, alg: alg || null,
955
955
  result: "permerror",
956
956
  errors: ["DKIM-Signature t=" + tSec + " is more than 24h in the future (RFC 6376 §3.5 sanity)"] });
@@ -595,7 +595,7 @@ function create(opts) {
595
595
  } else {
596
596
  pingN = parseInt(params.ping, 10);
597
597
  if (!isFinite(pingN) || pingN < 5) pingN = 30; // RFC 8620 §7.3 default ping seconds
598
- if (pingN > 900) pingN = 900; // allow:raw-byte-literal — operator-supplied ping seconds, not bytes // allow:raw-time-literal — explicit max-ping cap (15 minutes)
598
+ if (pingN > 900) pingN = 900; // allow:raw-time-literal — explicit max-ping cap (15 minutes)
599
599
  }
600
600
 
601
601
  // SSE wire headers per the HTML5 spec § "Server-sent events"
@@ -676,7 +676,7 @@ function create(opts) {
676
676
  }
677
677
  unsubscribe = typeof unsub === "function" ? unsub : null;
678
678
  if (!pingDisabled) {
679
- pingTimer = setInterval(_pingTick, pingN * 1000); // allow:raw-time-literal — seconds → ms conversion // allow:raw-byte-literal — not bytes, time conversion
679
+ pingTimer = setInterval(_pingTick, pingN * 1000); // allow:raw-time-literal — seconds → ms conversion
680
680
  if (pingTimer && typeof pingTimer.unref === "function") pingTimer.unref();
681
681
  }
682
682
  })
package/lib/mcp.js CHANGED
@@ -45,12 +45,12 @@ var METHOD_NAME_MAX = 256;
45
45
  // JSON-RPC 2.0 error codes (https://www.jsonrpc.org/specification#error_object).
46
46
  // Negative numerics by spec; mapped to HTTP status for the framework's
47
47
  // HTTP-shaped reply envelope.
48
- var JSONRPC_PARSE_ERROR = -32700; // allow:raw-byte-literal — JSON-RPC 2.0 fixed error code / allow:raw-time-literal — not seconds
49
- var JSONRPC_INVALID_REQUEST = -32600; // allow:raw-byte-literal — JSON-RPC 2.0 fixed error code / allow:raw-time-literal — not seconds
50
- var JSONRPC_METHOD_NOT_FOUND= -32601; // allow:raw-byte-literal — JSON-RPC 2.0 fixed error code / allow:raw-time-literal — not seconds
51
- var JSONRPC_INVALID_PARAMS = -32602; // allow:raw-byte-literal — JSON-RPC 2.0 fixed error code / allow:raw-time-literal — not seconds
52
- var JSONRPC_INTERNAL_ERROR = -32603; // allow:raw-byte-literal — JSON-RPC 2.0 fixed error code / allow:raw-time-literal — not seconds
53
- var JSONRPC_AUTH_REQUIRED = -32001; // allow:raw-byte-literal — JSON-RPC server-error reserved range / allow:raw-time-literal — not seconds
48
+ var JSONRPC_PARSE_ERROR = -32700; // allow:raw-time-literal — not seconds
49
+ var JSONRPC_INVALID_REQUEST = -32600; // allow:raw-time-literal — not seconds
50
+ var JSONRPC_METHOD_NOT_FOUND= -32601; // allow:raw-time-literal — not seconds
51
+ var JSONRPC_INVALID_PARAMS = -32602; // allow:raw-time-literal — not seconds
52
+ var JSONRPC_INTERNAL_ERROR = -32603; // allow:raw-time-literal — not seconds
53
+ var JSONRPC_AUTH_REQUIRED = -32001; // allow:raw-time-literal — not seconds
54
54
  var TOOL_NAME_RE = /^[a-zA-Z][a-zA-Z0-9._-]{0,63}$/;
55
55
  var RESOURCE_NAME_RE = /^[a-zA-Z][a-zA-Z0-9._/-]{0,255}$/;
56
56
 
@@ -38,6 +38,7 @@
38
38
  var defineClass = require("../framework-error").defineClass;
39
39
  var lazyRequire = require("../lazy-require");
40
40
  var validateOpts = require("../validate-opts");
41
+ var denyResponse = require("./deny-response").denyResponse;
41
42
 
42
43
  var audit = lazyRequire(function () { return require("../audit"); });
43
44
 
@@ -72,6 +73,8 @@ var AgeGateError = defineClass("AgeGateError", { alwaysPermanent: true });
72
73
  * errorMessage: string,
73
74
  * privacyPostureHeader: string, // default "X-Privacy-Posture"; null/false to suppress
74
75
  * audit: boolean, // default true
76
+ * onDeny: function(req, res, info): void, // own the 451; info = { status, reason, age, classification, requireAge }
77
+ * problemDetails: boolean, // default false — emit RFC 9457 application/problem+json instead of the default JSON envelope
75
78
  * }
76
79
  *
77
80
  * @example
@@ -89,6 +92,7 @@ function create(opts) {
89
92
  validateOpts(opts, [
90
93
  "audit", "getAge", "requireAge", "consentRequired",
91
94
  "hasParentalConsent", "skipPaths", "errorMessage", "privacyPostureHeader",
95
+ "onDeny", "problemDetails",
92
96
  ], "middleware.ageGate");
93
97
 
94
98
  if (typeof opts.getAge !== "function") {
@@ -105,6 +109,8 @@ function create(opts) {
105
109
  var auditOn = opts.audit !== false;
106
110
  var errorMessage = typeof opts.errorMessage === "string" && opts.errorMessage.length > 0
107
111
  ? opts.errorMessage : "service unavailable without parental consent";
112
+ var onDeny = typeof opts.onDeny === "function" ? opts.onDeny : null;
113
+ var problemMode = opts.problemDetails === true;
108
114
  // privacyPostureHeader (default "X-Privacy-Posture") names the response
109
115
  // header carrying the below-threshold classification. Pass null/false to
110
116
  // suppress it, or a string to rename it for a downstream convention.
@@ -168,13 +174,20 @@ function create(opts) {
168
174
  var hasConsent = hasParentalConsent ? !!hasParentalConsent(req) : false;
169
175
  if (!hasConsent) {
170
176
  _emitAudit("refused", "denied", { age: age, classification: classification, requireAge: requireAge });
171
- if (!res.writableEnded && typeof res.writeHead === "function") {
172
- res.writeHead(451, { // HTTP 451 Unavailable For Legal Reasons
173
- "Content-Type": "application/json; charset=utf-8",
174
- "Cache-Control": "no-store, private",
175
- });
176
- res.end(JSON.stringify({ error: errorMessage, requireAge: requireAge, parentalConsent: false }));
177
- }
177
+ denyResponse(req, res, {
178
+ onDeny: onDeny,
179
+ problem: problemMode,
180
+ status: 451, // HTTP 451 Unavailable For Legal Reasons
181
+ info: { status: 451, reason: "parental-consent-required",
182
+ age: age, classification: classification, requireAge: requireAge },
183
+ problemCode: "parental-consent-required",
184
+ problemTitle: "Unavailable For Legal Reasons",
185
+ problemDetail: errorMessage,
186
+ problemExt: { requireAge: requireAge, parentalConsent: false },
187
+ headers: { "Cache-Control": "no-store, private" },
188
+ contentType: "application/json; charset=utf-8",
189
+ body: JSON.stringify({ error: errorMessage, requireAge: requireAge, parentalConsent: false }),
190
+ });
178
191
  return;
179
192
  }
180
193
  }
@@ -37,21 +37,34 @@
37
37
  var lazyRequire = require("../lazy-require");
38
38
  var requestHelpers = require("../request-helpers");
39
39
  var validateOpts = require("../validate-opts");
40
+ var denyResponse = require("./deny-response").denyResponse;
40
41
  var { AuthError } = require("../framework-error");
41
42
 
42
43
  var audit = lazyRequire(function () { return require("../audit"); });
43
44
  var observability = lazyRequire(function () { return require("../observability"); });
44
45
 
45
- function _writeUnauthorized(res, scheme, message, realm) {
46
- if (res.headersSent) return;
47
- var body = JSON.stringify({ error: message });
48
- var challenge = scheme + (realm ? ' realm="' + realm + '"' : "");
49
- res.writeHead(401, { // HTTP 401 status
50
- "Content-Type": "application/json; charset=utf-8",
51
- "Content-Length": Buffer.byteLength(body),
52
- "WWW-Authenticate": challenge,
46
+ // Shared 401/403 refusal writer for every bearer-auth deny path —
47
+ // routes through denyResponse so a consumer can override via onDeny
48
+ // or emit RFC 9457 application/problem+json via problemDetails.
49
+ function _refuse(req, res, status, challenge, bodyObj, reason, problemExt, onDeny, problemMode) {
50
+ denyResponse(req, res, {
51
+ onDeny: onDeny,
52
+ problem: problemMode,
53
+ status: status,
54
+ info: Object.assign({ status: status, reason: reason }, problemExt || {}),
55
+ problemCode: "bearer-" + reason,
56
+ problemTitle: status === 403 ? "Forbidden" : "Unauthorized",
57
+ problemDetail: typeof bodyObj.error === "string" ? bodyObj.error : ("bearer authentication failed: " + reason),
58
+ problemExt: problemExt || null,
59
+ headers: { "WWW-Authenticate": challenge },
60
+ contentType: "application/json; charset=utf-8",
61
+ body: JSON.stringify(bodyObj),
53
62
  });
54
- res.end(body);
63
+ }
64
+
65
+ function _writeUnauthorized(req, res, scheme, message, realm, onDeny, problemMode) {
66
+ var challenge = scheme + (realm ? ' realm="' + realm + '"' : "");
67
+ _refuse(req, res, 401, challenge, { error: message }, "unauthorized", null, onDeny, problemMode);
55
68
  }
56
69
 
57
70
  // Three-state extractor: { state: "absent" } when no Authorization
@@ -102,10 +115,13 @@ function _extractToken(req, scheme) {
102
115
  * verify: async function(token): user|null, // required
103
116
  * scheme: string, // default "Bearer"; some ops use "Token"
104
117
  * realm: string,
118
+ * requiredScopes: string[], // RFC 6750 §3 — refuse 403 insufficient_scope when the verified token lacks one
105
119
  * errorMessage: string,
106
120
  * tokenAttachKey: string,
107
121
  * userAttachKey: string,
108
122
  * audit: boolean, // default true
123
+ * onDeny: function(req, res, info): void, // own the 401/403; info = { status, reason, ... }
124
+ * problemDetails: boolean, // default false — emit RFC 9457 application/problem+json instead of the default JSON envelope
109
125
  * }
110
126
  *
111
127
  * @example
@@ -122,7 +138,7 @@ function create(opts) {
122
138
  opts = opts || {};
123
139
  validateOpts(opts, [
124
140
  "verify", "audit", "scheme", "errorMessage", "realm",
125
- "tokenAttachKey", "userAttachKey",
141
+ "tokenAttachKey", "userAttachKey", "requiredScopes", "onDeny", "problemDetails",
126
142
  ], "middleware.bearerAuth");
127
143
 
128
144
  if (typeof opts.verify !== "function") {
@@ -131,6 +147,8 @@ function create(opts) {
131
147
  "the verification path (b.apiKey.verify / b.auth.jwt.verifyExternal / custom)");
132
148
  }
133
149
  var auditOn = opts.audit !== false;
150
+ var onDeny = typeof opts.onDeny === "function" ? opts.onDeny : null;
151
+ var problemMode = opts.problemDetails === true;
134
152
  var scheme = opts.scheme || "Bearer";
135
153
  var errorMessage = opts.errorMessage || "Bearer token required.";
136
154
  var realm = opts.realm || null;
@@ -197,13 +215,8 @@ function create(opts) {
197
215
  if (!res.headersSent) {
198
216
  var malformedChallenge = scheme + ' error="invalid_request"' +
199
217
  (realm ? ', realm="' + realm + '"' : "");
200
- var malformedBody = JSON.stringify({ error: errorMessage });
201
- res.writeHead(401, { // HTTP 401 status
202
- "Content-Type": "application/json; charset=utf-8",
203
- "Content-Length": Buffer.byteLength(malformedBody),
204
- "WWW-Authenticate": malformedChallenge,
205
- });
206
- res.end(malformedBody);
218
+ _refuse(req, res, 401, malformedChallenge, { error: errorMessage }, // HTTP 401 status
219
+ "malformed-authorization", null, onDeny, problemMode);
207
220
  }
208
221
  return;
209
222
  }
@@ -221,13 +234,8 @@ function create(opts) {
221
234
  var challenge = scheme + ' error="invalid_token"' +
222
235
  (realm ? ', realm="' + realm + '"' : "");
223
236
  if (!res.headersSent) {
224
- var body = JSON.stringify({ error: errorMessage });
225
- res.writeHead(401, { // HTTP 401 status
226
- "Content-Type": "application/json; charset=utf-8",
227
- "Content-Length": Buffer.byteLength(body),
228
- "WWW-Authenticate": challenge,
229
- });
230
- res.end(body);
237
+ _refuse(req, res, 401, challenge, { error: errorMessage }, // HTTP 401 status
238
+ "invalid-token", null, onDeny, problemMode);
231
239
  }
232
240
  return;
233
241
  }
@@ -235,7 +243,7 @@ function create(opts) {
235
243
  if (!user) {
236
244
  _emitAudit("auth.bearer.failure", "failure", req, "verifier-returned-null");
237
245
  _emitObs("auth.bearer.rejected", 1, { reason: "verifier-null" });
238
- _writeUnauthorized(res, scheme, errorMessage, realm);
246
+ _writeUnauthorized(req, res, scheme, errorMessage, realm, onDeny, problemMode);
239
247
  return;
240
248
  }
241
249
 
@@ -260,16 +268,9 @@ function create(opts) {
260
268
  var scopeChallenge = scheme + ' error="insufficient_scope"' +
261
269
  ', scope="' + opts.requiredScopes.join(" ") + '"' +
262
270
  (realm ? ', realm="' + realm + '"' : "");
263
- var scopeBody = JSON.stringify({
264
- error: "insufficient_scope",
265
- required: opts.requiredScopes.slice(),
266
- });
267
- res.writeHead(403, { // HTTP 403 status
268
- "Content-Type": "application/json; charset=utf-8",
269
- "Content-Length": Buffer.byteLength(scopeBody),
270
- "WWW-Authenticate": scopeChallenge,
271
- });
272
- res.end(scopeBody);
271
+ _refuse(req, res, 403, scopeChallenge, // HTTP 403 status
272
+ { error: "insufficient_scope", required: opts.requiredScopes.slice() },
273
+ "insufficient-scope", { required: opts.requiredScopes.slice() }, onDeny, problemMode);
273
274
  }
274
275
  return;
275
276
  }
@@ -51,6 +51,7 @@ var DEFAULT_BLOCKED_AGENTS = [
51
51
  var lazyRequire = require("../lazy-require");
52
52
  var requestHelpers = require("../request-helpers");
53
53
  var validateOpts = require("../validate-opts");
54
+ var denyResponse = require("./deny-response").denyResponse;
54
55
  var { defineClass } = require("../framework-error");
55
56
  var audit = lazyRequire(function () { return require("../audit"); });
56
57
 
@@ -112,6 +113,8 @@ function _xffIpFor(trustProxy) {
112
113
  * skipPaths: string[],
113
114
  * statusOnBlock: number, // default 403
114
115
  * bodyOnBlock: string,
116
+ * onDeny: function(req, res, info): void, // own the block response; info = { status, reason }
117
+ * problemDetails: boolean, // default false — emit RFC 9457 application/problem+json instead of text/plain
115
118
  * trustProxy: boolean|number,
116
119
  * }
117
120
  *
@@ -128,7 +131,7 @@ function create(opts) {
128
131
  opts = opts || {};
129
132
  validateOpts(opts, [
130
133
  "mode", "onlyForHtml", "allowedAgents", "blockedAgents",
131
- "skipPaths", "statusOnBlock", "bodyOnBlock", "trustProxy",
134
+ "skipPaths", "statusOnBlock", "bodyOnBlock", "onDeny", "problemDetails", "trustProxy",
132
135
  ], "middleware.botGuard");
133
136
  var trustProxy = opts.trustProxy === true || typeof opts.trustProxy === "number"
134
137
  ? opts.trustProxy : false;
@@ -144,6 +147,8 @@ function create(opts) {
144
147
  var skipPaths = opts.skipPaths || [];
145
148
  var statusOnBlock = opts.statusOnBlock || 403;
146
149
  var bodyOnBlock = opts.bodyOnBlock !== undefined ? opts.bodyOnBlock : "Forbidden";
150
+ var onDeny = typeof opts.onDeny === "function" ? opts.onDeny : null;
151
+ var problemMode = opts.problemDetails === true;
147
152
 
148
153
  function _shouldSkip(req) {
149
154
  var path = req.pathname || req.url || "/";
@@ -240,10 +245,17 @@ function create(opts) {
240
245
  } catch (_e) { /* audit best-effort */ }
241
246
 
242
247
  if (res.writableEnded) return;
243
- if (typeof res.writeHead === "function") {
244
- res.writeHead(statusOnBlock, { "Content-Type": "text/plain" });
245
- res.end(bodyOnBlock);
246
- }
248
+ denyResponse(req, res, {
249
+ onDeny: onDeny,
250
+ problem: problemMode,
251
+ status: statusOnBlock,
252
+ info: { status: statusOnBlock, reason: hit },
253
+ problemCode: "bot-blocked",
254
+ problemTitle: "Forbidden",
255
+ problemDetail: "The request was identified as automated traffic and refused.",
256
+ contentType: "text/plain",
257
+ body: bodyOnBlock,
258
+ });
247
259
  // Don't call next() — terminate the chain
248
260
  };
249
261
  }
@@ -97,7 +97,7 @@ var CANONICAL_POSITIONS = Object.freeze({
97
97
  botGuard: 42, // canonical position bucket
98
98
  requireAuth: 50, // canonical position bucket
99
99
  attachUser: 52, // canonical position bucket
100
- handler: 60, // allow:raw-byte-literal — canonical position bucket // allow:raw-time-literal — pipeline position int, not seconds
100
+ handler: 60, // allow:raw-time-literal — pipeline position int, not seconds
101
101
  errorHandler: 90, // canonical position bucket
102
102
  });
103
103