@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 +2 -0
- package/lib/websocket.js +79 -3
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
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
package/sbom.cyclonedx.json
CHANGED
|
@@ -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:
|
|
5
|
+
"serialNumber": "urn:uuid:02642e79-38a6-4075-930b-85e7d621de64",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.8.12",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.8.
|
|
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.
|
|
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.
|
|
57
|
+
"ref": "@blamejs/core@0.8.12",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|