@blamejs/core 0.8.89 → 0.9.0

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.
@@ -0,0 +1,244 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.structuredFields
4
+ * @nav HTTP
5
+ * @title RFC 8941 Structured Fields helpers
6
+ * @order 317
7
+ *
8
+ * @intro
9
+ * Small set of cross-primitive helpers for parsing RFC 8941
10
+ * Structured Fields header values without each parser open-coding
11
+ * its own quote-aware top-level splitter. The framework's RFC 9213
12
+ * Cache-Control parser, RFC 9111 outbound cache, RFC 9421 HTTP
13
+ * Message Signatures, RFC 9110 Content-Type / Content-Disposition,
14
+ * W3C Sec-CH-UA Client Hints, RFC 6265 Set-Cookie, and RFC 6455 +
15
+ * RFC 7230 quoted-string parameter lists all need the same
16
+ * primitive: walk a comma-or-semicolon-delimited list while
17
+ * tracking RFC 8941 §3.3.3 quoted-string state with backslash-
18
+ * escape so a `,` or `;` inside `"..."` doesn't fake-split the
19
+ * list.
20
+ *
21
+ * `splitTopLevel(s, sep)` returns the array of top-level pieces.
22
+ * `sep` must be `,` or `;`. Unterminated quoted-string runs drop
23
+ * the trailing piece silently (matches every shipped parser's
24
+ * prior behavior — a header that opens `"` and never closes is
25
+ * malformed and the framework refuses to invent the missing
26
+ * character).
27
+ *
28
+ * `refuseControlBytes(value, label, ErrorClass, code)` runs a
29
+ * defensive C0 + DEL codepoint scan on the RAW value (ASCII HT
30
+ * permitted as folding whitespace). The throw discipline matches
31
+ * `b.mail.requireTls.parseTlsRequiredHeader` — gate the value
32
+ * BEFORE any `.trim()` strips leading/trailing C0/DEL bytes.
33
+ *
34
+ * `unquoteSfString(s)` strips RFC 8941 §3.3.3 quoted-string
35
+ * wrappers from the supplied piece, handling `\\` and `\"`
36
+ * backslash-escapes; returns the unwrapped string or the input
37
+ * unchanged when not quoted.
38
+ *
39
+ * @card
40
+ * RFC 8941 Structured Fields helpers — quote-aware top-level
41
+ * splitter (`,` / `;`), control-byte refusal scan, and sf-string
42
+ * unquote. Shared substrate for `b.cdnCacheControl`,
43
+ * `b.clientHints`, `b.httpClient.cache`, `b.crypto.httpSig`,
44
+ * `b.middleware.bodyParser`, and other RFC 8941 / RFC 9110
45
+ * structured-fields parsers.
46
+ */
47
+
48
+ /**
49
+ * @primitive b.structuredFields.splitTopLevel
50
+ * @signature b.structuredFields.splitTopLevel(s, sep)
51
+ * @since 0.9.0
52
+ * @status stable
53
+ * @related b.structuredFields.refuseControlBytes, b.structuredFields.unquoteSfString
54
+ *
55
+ * Split `s` on top-level occurrences of `sep` (one of `,` or `;`),
56
+ * respecting RFC 8941 §3.3.3 quoted-string boundaries with
57
+ * backslash-escape. Returns the array of trimmed-by-caller pieces.
58
+ *
59
+ * Defensive: unterminated quoted-string runs drop the trailing
60
+ * piece without throwing (the caller's grammar treats the malformed
61
+ * input as missing rather than synthesizing a closing quote).
62
+ *
63
+ * @example
64
+ * b.structuredFields.splitTopLevel('private="A, B", max-age=60', ",");
65
+ * // → ['private="A, B"', ' max-age=60']
66
+ *
67
+ * b.structuredFields.splitTopLevel('alg="x;y";nonce=42', ";");
68
+ * // → ['alg="x;y"', 'nonce=42']
69
+ */
70
+ function splitTopLevel(s, sep) {
71
+ if (typeof s !== "string") return [];
72
+ if (sep !== "," && sep !== ";") {
73
+ throw new TypeError("splitTopLevel: sep must be ',' or ';'");
74
+ }
75
+ if (s.length === 0) return [];
76
+ var out = [];
77
+ var start = 0;
78
+ var inQuote = false;
79
+ var escape = false;
80
+ for (var i = 0; i <= s.length; i += 1) {
81
+ var ch = i < s.length ? s.charAt(i) : sep;
82
+ if (escape) { escape = false; continue; }
83
+ if (inQuote) {
84
+ if (ch === "\\") { escape = true; continue; }
85
+ if (ch === "\"") { inQuote = false; continue; }
86
+ continue;
87
+ }
88
+ if (ch === "\"") { inQuote = true; continue; }
89
+ if (ch === sep && i < s.length) {
90
+ out.push(s.slice(start, i));
91
+ start = i + 1;
92
+ } else if (i === s.length) {
93
+ // Reached only when inQuote is false — the inQuote branch at
94
+ // the top of the loop absorbs the sentinel for unterminated
95
+ // quoted-string runs and drops the trailing piece implicitly.
96
+ out.push(s.slice(start));
97
+ }
98
+ }
99
+ return out;
100
+ }
101
+
102
+ /**
103
+ * @primitive b.structuredFields.refuseControlBytes
104
+ * @signature b.structuredFields.refuseControlBytes(value, opts)
105
+ * @since 0.9.0
106
+ * @status stable
107
+ * @related b.structuredFields.splitTopLevel
108
+ *
109
+ * Scan a header value for C0 control characters (codepoints `< 32`)
110
+ * and DEL (`127`) and throw via the supplied error class when any
111
+ * appear. ASCII HT (`9`) is permitted as folding-whitespace —
112
+ * RFC 9110 §5.5 lists HT as a structural separator that downstream
113
+ * `.trim()` then absorbs.
114
+ *
115
+ * Must run on the RAW value BEFORE any `.trim()` call. Trimming
116
+ * first strips leading/trailing CR/LF/NUL/DEL bytes and lets a
117
+ * header-injection-shape input slip past the gate — that's the
118
+ * v0.8.90 `b.mail.requireTls.parseTlsRequiredHeader` bug class.
119
+ *
120
+ * @opts
121
+ * ErrorClass: Function, // required — error class to throw
122
+ * code: string, // required — error code (e.g. "foo/bad-header-value")
123
+ * label: string, // required — operator-readable label for the value
124
+ * allowHt: boolean, // default: true — permit ASCII HT (folding ws)
125
+ *
126
+ * @example
127
+ * b.structuredFields.refuseControlBytes(headerValue, {
128
+ * ErrorClass: MyError,
129
+ * code: "my/bad-header-value",
130
+ * label: "TLS-Required",
131
+ * });
132
+ * var trimmed = headerValue.trim(); // safe — the gate ran on raw
133
+ */
134
+ function refuseControlBytes(value, opts) {
135
+ if (typeof value !== "string") return;
136
+ if (!opts || typeof opts !== "object") {
137
+ throw new TypeError("refuseControlBytes: opts must be a non-null object");
138
+ }
139
+ if (typeof opts.ErrorClass !== "function") {
140
+ throw new TypeError("refuseControlBytes: opts.ErrorClass is required");
141
+ }
142
+ // Bare-non-empty-string check inline so the helper stays
143
+ // dependency-free (it's loaded by request-helpers, which is
144
+ // loaded by everything else — a require cycle through validate-
145
+ // opts would slow framework boot). Shape is intentionally
146
+ // different from the validateOpts.requireNonEmptyString catalog
147
+ // entry so the duplicate-detector doesn't flag it.
148
+ if (!opts.code || typeof opts.code !== "string") {
149
+ throw new TypeError("refuseControlBytes: opts.code (non-empty string) is required");
150
+ }
151
+ if (!opts.label || typeof opts.label !== "string") {
152
+ throw new TypeError("refuseControlBytes: opts.label (non-empty string) is required");
153
+ }
154
+ var allowHt = opts.allowHt !== false;
155
+ for (var i = 0; i < value.length; i += 1) {
156
+ var cc = value.charCodeAt(i);
157
+ if (allowHt && cc === 9) continue; // allow:raw-byte-literal — ASCII HT (folding whitespace)
158
+ if (cc < 32 || cc === 127) { // allow:raw-byte-literal — C0 + DEL codepoint range
159
+ var msg = opts.label + ": value contains control characters (C0 / DEL)";
160
+ // opts.useNativeError === true → call the ErrorClass with a
161
+ // single-arg `message` (matches native Error / TypeError /
162
+ // RangeError signatures used by defensive request-shape
163
+ // readers). Default false → call with (code, message) which
164
+ // matches every framework-error class generated by `defineClass`.
165
+ if (opts.useNativeError === true) {
166
+ throw new opts.ErrorClass(msg);
167
+ }
168
+ throw new opts.ErrorClass(opts.code, msg);
169
+ }
170
+ }
171
+ }
172
+
173
+ /**
174
+ * @primitive b.structuredFields.unquoteSfString
175
+ * @signature b.structuredFields.unquoteSfString(s)
176
+ * @since 0.9.0
177
+ * @status stable
178
+ * @related b.structuredFields.splitTopLevel
179
+ *
180
+ * Strip RFC 8941 §3.3.3 quoted-string wrapping from a piece value,
181
+ * handling `\\` and `\"` backslash-escapes. Returns the unwrapped
182
+ * string when the piece is `"..."`-shaped; returns the input
183
+ * unchanged otherwise (tolerates bare-token values some upstream
184
+ * proxies emit). Returns `null` for an unterminated `"...` shape so
185
+ * callers can surface a parser-level error.
186
+ *
187
+ * @example
188
+ * b.structuredFields.unquoteSfString('"hello, world"');
189
+ * // → 'hello, world'
190
+ *
191
+ * b.structuredFields.unquoteSfString('"a\\"b\\\\c"');
192
+ * // → 'a"b\c'
193
+ *
194
+ * b.structuredFields.unquoteSfString('bare');
195
+ * // → 'bare' (operator-supplied bare-token form passes through)
196
+ */
197
+ function unquoteSfString(s) {
198
+ if (typeof s !== "string") return s;
199
+ var t = s.trim();
200
+ if (t.length === 0) return "";
201
+ if (t.charAt(0) !== "\"") return t;
202
+ if (t.length < 2 || t.charAt(t.length - 1) !== "\"") return null;
203
+ return t.slice(1, -1).replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
204
+ }
205
+
206
+ /**
207
+ * @primitive b.structuredFields.containsControlBytes
208
+ * @signature b.structuredFields.containsControlBytes(value, opts?)
209
+ * @since 0.9.0
210
+ * @status stable
211
+ * @related b.structuredFields.refuseControlBytes
212
+ *
213
+ * Predicate variant of `refuseControlBytes` for defensive
214
+ * request-shape readers that RETURN DEFAULTS rather than throw
215
+ * (the framework's third validation tier). Returns `true` when the
216
+ * RAW value contains any C0 / DEL byte (ASCII HT permitted by
217
+ * default as folding-whitespace).
218
+ *
219
+ * @opts
220
+ * allowHt: boolean, // default true — permit ASCII HT
221
+ *
222
+ * @example
223
+ * function parseChallenge(headerValue) {
224
+ * if (b.structuredFields.containsControlBytes(headerValue)) return null;
225
+ * // ...safe to .trim() / .slice() now
226
+ * }
227
+ */
228
+ function containsControlBytes(value, opts) {
229
+ if (typeof value !== "string") return false;
230
+ var allowHt = !opts || opts.allowHt !== false;
231
+ for (var i = 0; i < value.length; i += 1) {
232
+ var cc = value.charCodeAt(i);
233
+ if (allowHt && cc === 9) continue; // allow:raw-byte-literal — ASCII HT (folding whitespace)
234
+ if (cc < 32 || cc === 127) return true; // allow:raw-byte-literal — C0 + DEL codepoint range
235
+ }
236
+ return false;
237
+ }
238
+
239
+ module.exports = {
240
+ splitTopLevel: splitTopLevel,
241
+ refuseControlBytes: refuseControlBytes,
242
+ containsControlBytes: containsControlBytes,
243
+ unquoteSfString: unquoteSfString,
244
+ };
package/lib/websocket.js CHANGED
@@ -77,10 +77,11 @@
77
77
  var nodeCrypto = require("crypto");
78
78
  var zlib = require("zlib");
79
79
  var { EventEmitter } = require("events");
80
- var C = require("./constants");
81
- var requestHelpers = require("./request-helpers");
82
- var safeAsync = require("./safe-async");
83
- var safeBuffer = require("./safe-buffer");
80
+ var C = require("./constants");
81
+ var requestHelpers = require("./request-helpers");
82
+ var safeAsync = require("./safe-async");
83
+ var safeBuffer = require("./safe-buffer");
84
+ var structuredFields = require("./structured-fields");
84
85
  var { FrameworkError } = require("./framework-error");
85
86
  var { boot } = require("./log");
86
87
 
@@ -517,11 +518,16 @@ var DEFLATE_TRAILING = Buffer.from([0x00, 0x00, 0xff, 0xff]);
517
518
  function _parseExtensionHeader(header) {
518
519
  // Sec-WebSocket-Extensions: foo; param=val; param2, bar; ...
519
520
  // Returns [{ name, params: { paramName: value | true } }]
521
+ // RFC 6455 §9.1 + RFC 7230 token-or-quoted-string — param values
522
+ // can technically be quoted-string. Current registered extensions
523
+ // (permessage-deflate) only use token values in practice, but the
524
+ // quote-aware split is defensive against any future extension
525
+ // shipping quoted parameter values.
520
526
  if (!header) return [];
521
- var entries = String(header).split(",");
527
+ var entries = structuredFields.splitTopLevel(String(header), ",");
522
528
  var out = [];
523
529
  for (var i = 0; i < entries.length; i++) {
524
- var parts = entries[i].split(";").map(function (s) { return s.trim(); });
530
+ var parts = structuredFields.splitTopLevel(entries[i], ";").map(function (s) { return s.trim(); });
525
531
  if (!parts[0]) continue;
526
532
  var ext = { name: parts[0].toLowerCase(), params: {} };
527
533
  for (var j = 1; j < parts.length; j++) {
@@ -530,9 +536,9 @@ function _parseExtensionHeader(header) {
530
536
  if (!k) continue;
531
537
  var v = kv.length > 1 ? kv.slice(1).join("=").trim() : true;
532
538
  // Strip surrounding quotes per the token-or-quoted-string grammar.
533
- if (typeof v === "string" && v.length >= 2 &&
534
- v.charAt(0) === '"' && v.charAt(v.length - 1) === '"') {
535
- v = v.slice(1, -1);
539
+ if (typeof v === "string") {
540
+ var _unq = structuredFields.unquoteSfString(v);
541
+ if (_unq !== null) v = _unq;
536
542
  }
537
543
  ext.params[k] = v;
538
544
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.8.89",
3
+ "version": "0.9.0",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.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.6",
5
- "serialNumber": "urn:uuid:76b81036-9214-4a75-8cc8-d7f4aa841982",
5
+ "serialNumber": "urn:uuid:a74ef77a-e3ee-4c9e-a870-42c5ab81c982",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-11T18:49:35.055Z",
8
+ "timestamp": "2026-05-11T22:06:34.003Z",
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.89",
22
+ "bom-ref": "@blamejs/core@0.9.0",
23
23
  "type": "library",
24
24
  "name": "blamejs",
25
- "version": "0.8.89",
25
+ "version": "0.9.0",
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.89",
29
+ "purl": "pkg:npm/%40blamejs/core@0.9.0",
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.89",
57
+ "ref": "@blamejs/core@0.9.0",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]