@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.
- package/CHANGELOG.md +4 -0
- package/README.md +1 -0
- package/lib/a2a-tasks.js +6 -6
- package/lib/ai-input.js +1 -1
- package/lib/auth/sd-jwt-vc.js +1 -1
- package/lib/calendar.js +6 -6
- package/lib/content-credentials.js +2 -2
- package/lib/cra-report.js +3 -3
- package/lib/guard-cidr.js +1 -1
- package/lib/http-client-cache.js +1 -1
- package/lib/mail-auth.js +1 -1
- package/lib/mail-crypto-smime.js +1 -1
- package/lib/mail-deploy.js +1 -1
- package/lib/mail-dkim.js +1 -1
- package/lib/mail-server-jmap.js +2 -2
- package/lib/mcp.js +6 -6
- package/lib/middleware/age-gate.js +20 -7
- package/lib/middleware/bearer-auth.js +36 -35
- package/lib/middleware/bot-guard.js +17 -5
- package/lib/middleware/compose-pipeline.js +1 -1
- package/lib/middleware/cors.js +28 -12
- package/lib/middleware/csrf-protect.js +22 -14
- package/lib/middleware/daily-byte-quota.js +27 -13
- package/lib/middleware/deny-response.js +140 -0
- package/lib/middleware/dpop.js +32 -19
- package/lib/middleware/fetch-metadata.js +21 -12
- package/lib/middleware/host-allowlist.js +19 -8
- package/lib/middleware/index.js +3 -0
- package/lib/middleware/network-allowlist.js +24 -10
- package/lib/middleware/rate-limit.js +22 -5
- package/lib/middleware/require-aal.js +25 -10
- package/lib/middleware/require-auth.js +32 -16
- package/lib/middleware/require-bound-key.js +49 -18
- package/lib/middleware/require-content-type.js +19 -8
- package/lib/middleware/require-methods.js +17 -7
- package/lib/middleware/require-mtls.js +27 -14
- package/lib/network.js +4 -4
- package/lib/safe-decompress.js +1 -1
- package/lib/safe-url.js +1 -1
- package/lib/stream-throttle.js +2 -2
- package/lib/websocket.js +2 -2
- package/package.json +1 -1
- 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-
|
|
66
|
-
var JSONRPC_INVALID_REQUEST = -32600; // allow:raw-
|
|
67
|
-
var JSONRPC_METHOD_NOT_FOUND = -32601; // allow:raw-
|
|
68
|
-
var JSONRPC_INVALID_PARAMS = -32602; // allow:raw-
|
|
69
|
-
var JSONRPC_INTERNAL_ERROR = -32603; // allow:raw-
|
|
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-
|
|
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-
|
|
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:
|
package/lib/auth/sd-jwt-vc.js
CHANGED
|
@@ -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
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
package/lib/http-client-cache.js
CHANGED
|
@@ -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-
|
|
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 —
|
|
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();
|
package/lib/mail-crypto-smime.js
CHANGED
|
@@ -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-
|
|
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 +
|
package/lib/mail-deploy.js
CHANGED
|
@@ -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)
|
|
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-
|
|
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)"] });
|
package/lib/mail-server-jmap.js
CHANGED
|
@@ -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-
|
|
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
|
|
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-
|
|
49
|
-
var JSONRPC_INVALID_REQUEST = -32600; // allow:raw-
|
|
50
|
-
var JSONRPC_METHOD_NOT_FOUND= -32601; // allow:raw-
|
|
51
|
-
var JSONRPC_INVALID_PARAMS = -32602; // allow:raw-
|
|
52
|
-
var JSONRPC_INTERNAL_ERROR = -32603; // allow:raw-
|
|
53
|
-
var JSONRPC_AUTH_REQUIRED = -32001; // allow:raw-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
201
|
-
|
|
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
|
-
|
|
225
|
-
|
|
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
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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-
|
|
100
|
+
handler: 60, // allow:raw-time-literal — pipeline position int, not seconds
|
|
101
101
|
errorHandler: 90, // canonical position bucket
|
|
102
102
|
});
|
|
103
103
|
|