@blamejs/core 0.8.11 → 0.8.12

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 CHANGED
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.8.x
10
10
 
11
+ - **0.8.12** (2026-05-07) — WebSocket upgrade refuses credential-shaped query parameters by default. `validateUpgradeRequest(req, opts)` now scans the request URL for the credential-leak names `access_token`, `bearer`, `bearer_token`, `apikey`, `api_key`, `api-key`, `authorization` (case-insensitive, with percent-decoding) and refuses the upgrade with HTTP 400 when one is present. URL query strings leak through web-server access logs, browser history, the Referer header forwarded to third-party CDN / analytics, in-process / proxy log captures, and crash dumps — RFC 6750 §2.3 explicitly cautions against bearer tokens in URI query parameters for these reasons. Operators with a non-credential parameter that happens to share a credential-shaped name opt out per route via `opts.allowQueryAuthParams: true` with an audited operator reason. The refused list is deliberately narrow: overloaded names (`token`, `auth`, `key`, `session`) have non-credential meanings (CSRF tokens, file-share tokens, session-resume identifiers) and are NOT refused.
12
+
11
13
  - **0.8.11** (2026-05-07) — Three new state-and-federal regulatory primitives + a per-primitive test-coverage gate. **`b.breach.deadline` + `b.breach.report`** — all-50-states data-breach-notification deadline registry. `b.breach.deadline.forStates(states, detectedAt)` returns per-state `{ state, kind, dueBy, citation }` records (`kind: "as-soon-as-possible"` for AS-OF / `"hard-deadline"` for fixed-day deadlines like Texas / Florida / Maine). `b.breach.report.create()` opens a multi-state breach with a single record, tracks per-state filings via `fileNotice(id, state, ...)`, exposes `pending(id)` for dashboards, and auto-closes once every affected state has filed. Every transition records a `breach.report.*` audit event. Statutory citations + day counts wired in `lib/breach-deadline.js` per-state. **`b.ai.adverseDecision`** — wraps an operator-supplied `decide(subject)` predicate, automatically attaches a consumer-rights notice when the outcome is `"adverse"` / `"denied"` / `"rejected"`. Built-in regulation templates for `gdpr-22` (Article 22 automated-decision rights), `ai-act-86` (EU AI Act high-risk consumer recourse), `ecoa-1002.9` (US Equal Credit Opportunity Act adverse-action notice), `colorado-ai-act` (CO SB 24-205 §6-1-1701), `nyc-ll-144` (NYC Local Law 144 employment AEDT), `fcra-615` (US FCRA adverse action), and `operator-defined`. Notice carries `principalReasons` + `consumerRights: { requestData, requestExplanation, contestDecision, requestHumanReview }` shaped per regime. **`b.middleware.ageGate`** — request-level age-classification middleware. Operator-supplied `getAge(req)` returns the subject age (or null/undefined when unknown); middleware classifies as `"above-threshold"` / `"below-threshold"` / `"unknown"` against `consentRequired`, sets `X-Privacy-Posture` header, and refuses with 451 + audited reason when `requireAge` is set and `hasParentalConsent(req)` is unmet. Composes upstream of session / authn for COPPA / AADC / UK Children's Code postures. **Per-primitive test-coverage gate** — new `test/layer-0-primitives/test-coverage.test.js` walks every operator-facing `b.*` primitive and refuses release unless the primitive has at least one test reference (or an explicit `UNTESTED_BACKLOG` entry naming the reason). Closes the drift class where a primitive landed on `b.*` but never gained a unit test.
12
14
 
13
15
  - **0.8.10** (2026-05-07) — Five new compliance / regulatory primitives composing on v0.8.9's `b.incident.report`. **`b.cra.report`** — EU Cyber Resilience Act (Regulation (EU) 2024/2847) Article 14 §1 incident reporting wrapper. Three-stage statutory deadlines: 24h early warning / 72h incident notification / 14d final report. Required `productId` + `manufacturer` per Annex VII §1. Optional ENISA submission via `opts.enisaEndpoint` + `b.httpClient`; submission is operator-opt-in per stage call (regulators uniformly require operator review before filing). **`b.nis2.report`** — NIS2 Directive (Directive (EU) 2022/2555) Article 23 §4 incident reporting wrapper. Three-stage deadlines: 24h / 72h / 1 month. Annex I (essential) / Annex II (important) entity classification + sector codes (`I.6` drinking water / `II.6` digital providers / etc.). **`b.gdpr.ropa`** — GDPR Article 30 Records of Processing Activities registry + JSON / CSV / Markdown exporter. Validates required fields per Article 30 §1; legal-basis enum per Article 6(1); produces a regulator-friendly RoPA document for the operator's DPO to file. **`b.compliance.eaa`** — EU Accessibility Act (Directive (EU) 2019/882) Article 13 declared-conformance generator. Operators declare per-criterion conformance against WCAG 2.1/2.2 AA / EN 301 549; non-conformances ship with reason + mitigation. JSON / Markdown export for the operator's accessibility statement. **`b.middleware.botDisclose`** — California SB 1001 (Cal. Bus. & Prof. Code §17941) bot-disclosure middleware. Injects a disclosure banner into HTML responses, sets `X-Bot-Disclosure` header for API consumers, audits every conversation-initiating request. Operators wire `mountPaths` to scope and `bannerHtml` for visual customization.
package/lib/websocket.js CHANGED
@@ -108,6 +108,36 @@ var GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
108
108
  // catches the typo class.
109
109
  var GUID_RE = /^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$/;
110
110
 
111
+ // Credential-shaped query parameter names refused at upgrade time. URL
112
+ // query strings end up in: web-server access logs, the browser's
113
+ // history + Referer header forwarded to third-party CDN / analytics
114
+ // requests, in-process / proxy log captures, and crash dumps. Any
115
+ // authentication credential placed in the query string is leaked
116
+ // through one of those channels by default. RFC 6750 §2.3 explicitly
117
+ // cautions against bearer tokens in URI query parameters for exactly
118
+ // these reasons.
119
+ //
120
+ // Operators with a non-credential query parameter that happens to
121
+ // match one of these names (e.g. an "apikey" field passed to a
122
+ // downstream tenant API by mistake) opt out per route via
123
+ // `opts.allowQueryAuthParams: true` with an audited operator reason —
124
+ // the lift exists, but the operator owns the audit trail.
125
+ //
126
+ // The list is deliberately narrow — overloaded names like `token`,
127
+ // `auth`, `key`, `session` have non-credential meanings (CSRF tokens,
128
+ // file-share tokens, ICE candidates, session-resume identifiers) and
129
+ // would create false-positive friction without closing a genuine
130
+ // leak vector. The names below are unambiguously credential-shaped.
131
+ var REFUSED_AUTH_QUERY_PARAMS = Object.freeze([
132
+ "access_token", // OAuth 2.0 bearer (RFC 6750)
133
+ "bearer", // synonym
134
+ "bearer_token", // synonym
135
+ "apikey", // common convention
136
+ "api_key", // common convention
137
+ "api-key", // common convention
138
+ "authorization", // literal Authorization-header value
139
+ ]);
140
+
111
141
  var OPCODE_CONTINUATION = 0x0;
112
142
  var OPCODE_TEXT = 0x1;
113
143
  var OPCODE_BINARY = 0x2;
@@ -186,7 +216,7 @@ function computeAcceptKey(secWebSocketKey, handshakeGuid) {
186
216
  return hash.digest("base64");
187
217
  }
188
218
 
189
- function validateUpgradeRequest(req) {
219
+ function validateUpgradeRequest(req, opts) {
190
220
  if (req.method !== "GET") {
191
221
  return { ok: false, status: HTTP.METHOD_NOT_ALLOWED, reason: "method must be GET" };
192
222
  }
@@ -205,9 +235,54 @@ function validateUpgradeRequest(req) {
205
235
  if (h["sec-websocket-version"] !== "13") {
206
236
  return { ok: false, status: HTTP.BAD_REQUEST, reason: "Sec-WebSocket-Version must be 13" };
207
237
  }
238
+ if (!(opts && opts.allowQueryAuthParams === true)) {
239
+ var leaked = _findCredentialQueryParam(req.url);
240
+ if (leaked) {
241
+ return {
242
+ ok: false,
243
+ status: HTTP.BAD_REQUEST,
244
+ reason: "credential-shaped query parameter '" + leaked +
245
+ "' refused — query strings leak via logs / Referer / history. " +
246
+ "Move the credential to the Authorization header, or set " +
247
+ "opts.allowQueryAuthParams: true with an audited operator reason " +
248
+ "if this parameter is not actually a credential.",
249
+ };
250
+ }
251
+ }
208
252
  return { ok: true };
209
253
  }
210
254
 
255
+ // _findCredentialQueryParam walks the request's query string and
256
+ // returns the first credential-shaped parameter name it finds, or
257
+ // null. Comparison is case-insensitive; an attacker who URL-encodes
258
+ // the parameter name (e.g. "%41ccess_token") still hits the check
259
+ // because URL parsing decodes the name before comparison.
260
+ function _findCredentialQueryParam(reqUrl) {
261
+ if (typeof reqUrl !== "string" || reqUrl.length === 0) return null;
262
+ var qIdx = reqUrl.indexOf("?");
263
+ if (qIdx === -1) return null;
264
+ var query = reqUrl.slice(qIdx + 1);
265
+ // Strip a fragment if any (defensive — real HTTP requests don't carry
266
+ // one, but req.url has been observed with appended fragments behind
267
+ // misconfigured proxies).
268
+ var fIdx = query.indexOf("#");
269
+ if (fIdx !== -1) query = query.slice(0, fIdx);
270
+ if (query.length === 0) return null;
271
+ var pairs = query.split("&");
272
+ for (var p = 0; p < pairs.length; p++) {
273
+ var eqIdx = pairs[p].indexOf("=");
274
+ var rawName = eqIdx === -1 ? pairs[p] : pairs[p].slice(0, eqIdx);
275
+ if (rawName.length === 0) continue;
276
+ var name;
277
+ try { name = decodeURIComponent(rawName).toLowerCase(); }
278
+ catch (_e) { name = rawName.toLowerCase(); }
279
+ for (var r = 0; r < REFUSED_AUTH_QUERY_PARAMS.length; r++) {
280
+ if (name === REFUSED_AUTH_QUERY_PARAMS[r]) return name;
281
+ }
282
+ }
283
+ return null;
284
+ }
285
+
211
286
  function negotiateSubprotocol(req, supported) {
212
287
  if (!supported || supported.length === 0) return null;
213
288
  var raw = (req.headers || {})["sec-websocket-protocol"] || "";
@@ -885,9 +960,9 @@ function handleUpgrade(req, socket, head, opts) {
885
960
  // Validate handshake first — refusing here writes a plain HTTP/1.1
886
961
  // response and closes the socket, matching what the upgrade-event
887
962
  // consumer would expect for a malformed request.
888
- var v = validateUpgradeRequest(req);
963
+ var v = validateUpgradeRequest(req, opts);
889
964
  if (!v.ok) {
890
- _refuseUpgrade(socket, v.status || 400, v.reason);
965
+ _refuseUpgrade(socket, v.status || 400, v.reason); // allow:raw-byte-literal — HTTP 400 fallback
891
966
  return null;
892
967
  }
893
968
 
@@ -1047,6 +1122,7 @@ module.exports = {
1047
1122
  handleExtendedConnect: handleExtendedConnect, // h2 — RFC 8441 Extended CONNECT
1048
1123
  // Constants
1049
1124
  GUID: GUID,
1125
+ REFUSED_AUTH_QUERY_PARAMS: REFUSED_AUTH_QUERY_PARAMS,
1050
1126
  OPCODE_CONTINUATION: OPCODE_CONTINUATION,
1051
1127
  OPCODE_TEXT: OPCODE_TEXT,
1052
1128
  OPCODE_BINARY: OPCODE_BINARY,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.8.11",
3
+ "version": "0.8.12",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:ae68f706-2c92-4dfa-b852-f29896f91c62",
5
+ "serialNumber": "urn:uuid:02642e79-38a6-4075-930b-85e7d621de64",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-07T03:30:10.287Z",
8
+ "timestamp": "2026-05-07T04:37:53.397Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/core@0.8.11",
22
+ "bom-ref": "@blamejs/core@0.8.12",
23
23
  "type": "library",
24
24
  "name": "blamejs",
25
- "version": "0.8.11",
25
+ "version": "0.8.12",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "The Node framework that owns its stack.",
29
- "purl": "pkg:npm/%40blamejs/core@0.8.11",
29
+ "purl": "pkg:npm/%40blamejs/core@0.8.12",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/core@0.8.11",
57
+ "ref": "@blamejs/core@0.8.12",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]