@blamejs/core 0.8.35 → 0.8.36
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.36** (2026-05-08) — HTTP/web G-class LOW cleanup + scope-aware bearer challenge. **WS handshake (RFC 6455 §4.1)** — `Sec-WebSocket-Key` validated as base64 of 16 random bytes (`/^[A-Za-z0-9+/]{22}==$/`). Pre-v0.8.36 only the presence was checked; truncated / arbitrary-token values flowed through. **Permissions-Policy default** — `fullscreen` flipped to `()` (deny) instead of `(self)`; operators wanting fullscreen pass an explicit override. **`b.middleware.bearerAuth` insufficient_scope (RFC 6750 §3)** — new `requiredScopes: ["scope1", "scope2"]` opt enforces operator-declared scopes. Token's `user.scope` (string, space-separated) or `user.scopes` (array) is checked; missing scopes refuse with HTTP 403 + `WWW-Authenticate: Bearer error="insufficient_scope", scope="..."`. **`b.requestHelpers.parseListHeader({strictToken: true})`** — RFC 9110 §5.6.2 token-grammar enforcement. Refuses non-token entries (anything outside `!#$%&'*+-.^_\\`|~` + alnum). **Multipart boundary validation (RFC 2046 §5.1.1)** — `_parseMultipart` refuses boundaries longer than 70 chars OR violating the `bcharsnospace` grammar. Closes the quadratic-match risk on pathological boundaries.
|
|
12
|
+
|
|
11
13
|
- **0.8.35** (2026-05-08) — `b.tcpa10dlc` + `b.iabMspa` primitives. **`b.tcpa10dlc`** — TCPA 10DLC consent-record audit. 47 USC §227 + 47 CFR §64.1200 + FCC 1:1 disclosure rule (effective 2025-01-27, vacated 11th Circuit IMC v. FCC 2025 but TCPA standard still applies). $500-$1,500/violation exposure. `recordConsent({phoneE164, brand, disclosureText, disclosurePartyKind, formUrl, ip, userAgent})` writes a tamper-evident audit row with the carrier-required fields; `lookup(phoneE164)` for the carrier "produce-on-demand" workflow; `revoke(phoneE164, reason)` records consumer-initiated opt-out with audit trail. **`b.iabMspa`** — IAB Multi-State Privacy Agreement / Global Privacy Platform (GPP) universal opt-out signal codec. `parseGpp(gppString)` decodes the framing (header + per-section payloads, section-id mapping for usnat / usca / usva / usco / usct / usut / usnv / usia / usde / usnj / ustx / usor / usmt / usnh). `checkOptOut(parsed, {dataUse, state})` returns `{ mustHonor, signals }` against operator-decoded section opt-outs (sale / sharing / targeted-ads / sensitive / child-data). `refuseProcessing(parsed, opts)` throws `IabMspaError` to halt the operator's data-flow at the same point a CCPA "do-not-sell" header would. `gpcFromHeaders(req)` reads the W3C `Sec-GPC: 1` browser signal — universal opt-out per CCPA / CPRA §1798.135(b)(1) and similar state laws.
|
|
12
14
|
|
|
13
15
|
- **0.8.34** (2026-05-08) — `lib/middleware/body-parser.js` BiDi-strip regex uses Unicode-escape form (`\\u202A`-style) instead of literal codepoints to satisfy ESLint's `no-irregular-whitespace`. v0.8.33 publish workflow blocked on this; v0.8.33 tag exists on git but never reached npm — v0.8.34 cumulative includes the v0.8.33 G-class MEDIUM fixes.
|
|
@@ -199,6 +199,42 @@ function create(opts) {
|
|
|
199
199
|
return;
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
// RFC 6750 §3 — `insufficient_scope` challenge with `scope=` when
|
|
203
|
+
// the verified token is missing one or more required scopes.
|
|
204
|
+
// Operators pass `requiredScopes: ["write", "admin"]` to enforce.
|
|
205
|
+
// The verifier returns the user's scope list at `user.scope`
|
|
206
|
+
// (string, space-separated) OR `user.scopes` (array). When the
|
|
207
|
+
// request lacks a required scope, refuse with 403 + the standard
|
|
208
|
+
// challenge (NOT 401 — token was valid).
|
|
209
|
+
if (Array.isArray(opts.requiredScopes) && opts.requiredScopes.length > 0) {
|
|
210
|
+
var userScopes = Array.isArray(user.scopes) ? user.scopes :
|
|
211
|
+
typeof user.scope === "string" ? user.scope.split(/\s+/).filter(function (s) { return s.length > 0; }) :
|
|
212
|
+
[];
|
|
213
|
+
var missing = opts.requiredScopes.filter(function (s) {
|
|
214
|
+
return userScopes.indexOf(s) === -1;
|
|
215
|
+
});
|
|
216
|
+
if (missing.length > 0) {
|
|
217
|
+
_emitAudit("auth.bearer.failure", "failure", req, "insufficient-scope:" + missing.join(","));
|
|
218
|
+
_emitObs("auth.bearer.rejected", 1, { reason: "insufficient-scope" });
|
|
219
|
+
if (!res.headersSent) {
|
|
220
|
+
var scopeChallenge = scheme + ' error="insufficient_scope"' +
|
|
221
|
+
', scope="' + opts.requiredScopes.join(" ") + '"' +
|
|
222
|
+
(realm ? ', realm="' + realm + '"' : "");
|
|
223
|
+
var scopeBody = JSON.stringify({
|
|
224
|
+
error: "insufficient_scope",
|
|
225
|
+
required: opts.requiredScopes.slice(),
|
|
226
|
+
});
|
|
227
|
+
res.writeHead(403, { // allow:raw-byte-literal — HTTP 403 status
|
|
228
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
229
|
+
"Content-Length": Buffer.byteLength(scopeBody),
|
|
230
|
+
"WWW-Authenticate": scopeChallenge,
|
|
231
|
+
});
|
|
232
|
+
res.end(scopeBody);
|
|
233
|
+
}
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
202
238
|
req[tokenAttach] = token;
|
|
203
239
|
req[userAttach] = user;
|
|
204
240
|
// Signal to attach-user (and any other downstream auth middleware)
|
|
@@ -568,6 +568,19 @@ async function _parseMultipart(req, opts, ctParams) {
|
|
|
568
568
|
true, HTTP_STATUS.BAD_REQUEST
|
|
569
569
|
);
|
|
570
570
|
}
|
|
571
|
+
// RFC 2046 §5.1.1 — boundary length 1-70 chars, bcharsnospace
|
|
572
|
+
// grammar. Pathological boundaries (zero-length / very long /
|
|
573
|
+
// newlines) drive quadratic match cost in scanners. Refuse at
|
|
574
|
+
// the parse boundary so the rest of the engine doesn't have to
|
|
575
|
+
// defend against them.
|
|
576
|
+
if (boundary.length > 70 || // allow:raw-byte-literal — RFC 2046 §5.1.1 boundary length cap
|
|
577
|
+
!/^[A-Za-z0-9'()+_,\-./:=?]{1,70}$/.test(boundary)) { // allow:raw-byte-literal — RFC 2046 §5.1.1 bchars + cap
|
|
578
|
+
throw new BodyParserError(
|
|
579
|
+
"body-parser/multipart-bad-boundary",
|
|
580
|
+
"multipart boundary violates RFC 2046 §5.1.1 (1-70 chars, bcharsnospace grammar)",
|
|
581
|
+
true, HTTP_STATUS.BAD_REQUEST
|
|
582
|
+
);
|
|
583
|
+
}
|
|
571
584
|
// Resolve tmpDir per-request so directory-creation failure surfaces as a
|
|
572
585
|
// structured error rather than a deferred fs throw.
|
|
573
586
|
var tmpDir = opts.tmpDir || path.join(os.tmpdir(), "blamejs-uploads");
|
|
@@ -40,7 +40,7 @@ var validateOpts = require("../validate-opts");
|
|
|
40
40
|
|
|
41
41
|
var DEFAULT_PERMISSIONS = [
|
|
42
42
|
"accelerometer=()", "ambient-light-sensor=()", "autoplay=()",
|
|
43
|
-
"camera=()", "display-capture=()", "encrypted-media=()", "fullscreen=(
|
|
43
|
+
"camera=()", "display-capture=()", "encrypted-media=()", "fullscreen=()",
|
|
44
44
|
"geolocation=()", "gyroscope=()", "magnetometer=()", "microphone=()",
|
|
45
45
|
"midi=()", "payment=()", "picture-in-picture=()", "publickey-credentials-get=()",
|
|
46
46
|
"screen-wake-lock=()", "sync-xhr=()", "usb=()", "web-share=()", "xr-spatial-tracking=()",
|
package/lib/request-helpers.js
CHANGED
|
@@ -212,6 +212,13 @@ function requestProtocol(req, opts) {
|
|
|
212
212
|
// Tolerant read: non-string input returns [] — these are read from
|
|
213
213
|
// request headers that the network might omit. Callers needing stricter
|
|
214
214
|
// checks layer their own validation on the result.
|
|
215
|
+
// RFC 9110 §5.6.2 token grammar — letters, digits, and the
|
|
216
|
+
// punctuation set `!#$%&'*+-.^_`|~`. Used by header-list parsers
|
|
217
|
+
// that consume protocol tokens (Connection, Sec-WebSocket-
|
|
218
|
+
// Protocol, etc.). Operator handlers parsing comma-separated
|
|
219
|
+
// human-supplied values (Origin lists, etc.) opt out by passing
|
|
220
|
+
// `lax: true`.
|
|
221
|
+
var RFC_9110_TOKEN_RE = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
215
222
|
function parseListHeader(value, opts) {
|
|
216
223
|
if (value == null) return [];
|
|
217
224
|
opts = opts || {};
|
|
@@ -222,6 +229,13 @@ function parseListHeader(value, opts) {
|
|
|
222
229
|
for (var i = 0; i < parts.length; i++) {
|
|
223
230
|
var t = parts[i].trim();
|
|
224
231
|
if (t.length === 0) continue;
|
|
232
|
+
if (opts.strictToken && !RFC_9110_TOKEN_RE.test(t)) {
|
|
233
|
+
// Refuse non-token entries when caller asked for strict-token
|
|
234
|
+
// grammar (RFC 9110 §5.6.2). Used by ws subprotocol negotiation
|
|
235
|
+
// and other places where only token-shaped values are valid.
|
|
236
|
+
throw new TypeError("parseListHeader: '" + t +
|
|
237
|
+
"' is not a valid RFC 9110 token");
|
|
238
|
+
}
|
|
225
239
|
out.push(opts.lowercase ? t.toLowerCase() : t);
|
|
226
240
|
}
|
|
227
241
|
return out;
|
package/lib/websocket.js
CHANGED
|
@@ -245,6 +245,16 @@ function validateUpgradeRequest(req, opts) {
|
|
|
245
245
|
if (!h["sec-websocket-key"]) {
|
|
246
246
|
return { ok: false, status: HTTP.BAD_REQUEST, reason: "missing Sec-WebSocket-Key" };
|
|
247
247
|
}
|
|
248
|
+
// RFC 6455 §4.1 — Sec-WebSocket-Key MUST be a base64-encoded
|
|
249
|
+
// 16-byte nonce. Encoded length is 24 chars including the
|
|
250
|
+
// `==` padding. Strict check refuses malformed values that
|
|
251
|
+
// some clients send (truncated or arbitrary token); lets
|
|
252
|
+
// server-side anomaly detection see the malformation rather
|
|
253
|
+
// than passing through.
|
|
254
|
+
if (!/^[A-Za-z0-9+/]{22}==$/.test(h["sec-websocket-key"])) {
|
|
255
|
+
return { ok: false, status: HTTP.BAD_REQUEST,
|
|
256
|
+
reason: "Sec-WebSocket-Key must be base64 of 16 random bytes (RFC 6455 §4.1)" };
|
|
257
|
+
}
|
|
248
258
|
if (h["sec-websocket-version"] !== "13") {
|
|
249
259
|
return { ok: false, status: HTTP.BAD_REQUEST, reason: "Sec-WebSocket-Version must be 13" };
|
|
250
260
|
}
|
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:bc456840-498c-445d-b9a6-d087f3ab5715",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-07T15:
|
|
8
|
+
"timestamp": "2026-05-07T15:39:09.436Z",
|
|
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.36",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.8.
|
|
25
|
+
"version": "0.8.36",
|
|
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.36",
|
|
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.36",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|