@blamejs/blamejs-shop 0.1.13 → 0.1.15

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.
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * @module b.structuredFields
4
4
  * @nav HTTP
5
- * @title RFC 8941 Structured Fields helpers
5
+ * @title RFC 9651 Structured Fields
6
6
  * @order 317
7
7
  *
8
8
  * @intro
@@ -67,6 +67,10 @@
67
67
  * b.structuredFields.splitTopLevel('alg="x;y";nonce=42', ";");
68
68
  * // → ['alg="x;y"', 'nonce=42']
69
69
  */
70
+ // node:util is a builtin (no lib require cycle) — used for strict UTF-8
71
+ // validation of RFC 9651 Display Strings.
72
+ var TextDecoder = require("node:util").TextDecoder;
73
+
70
74
  function splitTopLevel(s, sep) {
71
75
  if (typeof s !== "string") return [];
72
76
  if (sep !== "," && sep !== ";") {
@@ -236,9 +240,445 @@ function containsControlBytes(value, opts) {
236
240
  return false;
237
241
  }
238
242
 
243
+ // ---------------------------------------------------------------------------
244
+ // Full RFC 9651 codec (parse + serialize; RFC 9651 obsoletes RFC 8941
245
+ // and adds the Date and Display String types). The helpers above are the
246
+ // quote-aware splitters individual parsers reach for; the codec below is
247
+ // the complete grammar — Items, Lists, Dictionaries, Inner Lists,
248
+ // Parameters, and every bare-item type.
249
+ //
250
+ // Value model (RFC 9651, which obsoletes RFC 8941):
251
+ // bare item → number (Integer) | SfDecimal | string | boolean | SfToken
252
+ // | SfByteSequence | SfDate | SfDisplayString
253
+ // item → { value: bareItem, params: Map<string, bareItem> }
254
+ // inner list → { items: item[], params: Map<string, bareItem> }
255
+ // list → (item | innerList)[]
256
+ // dictionary → Map<string, item | innerList>
257
+ // ---------------------------------------------------------------------------
258
+
259
+ // RFC 8941 §3.3.4 Token / §3.3.5 Byte Sequence are wrapped so they stay
260
+ // distinct from plain strings on both parse output and serialize input.
261
+ function SfToken(value) {
262
+ if (!(this instanceof SfToken)) return new SfToken(value);
263
+ this.value = String(value);
264
+ }
265
+ function SfByteSequence(value) {
266
+ if (!(this instanceof SfByteSequence)) return new SfByteSequence(value);
267
+ this.value = Buffer.isBuffer(value) ? value : Buffer.from(value);
268
+ }
269
+ // A Decimal preserves its type across parse → serialize even when its
270
+ // value is numerically integral ("1.0" must not serialize back to "1").
271
+ // A plain JS number serializes as an Integer when integral, a Decimal
272
+ // otherwise; wrap in SfDecimal to force the Decimal form.
273
+ function SfDecimal(value) {
274
+ if (!(this instanceof SfDecimal)) return new SfDecimal(value);
275
+ this.value = Number(value);
276
+ }
277
+ // RFC 9651 §3.3.7 Date (an Integer number of seconds since the Unix
278
+ // epoch) and §3.3.8 Display String (a Unicode string conveyed as
279
+ // percent-escaped UTF-8). Wrapped so they stay distinct from Integers
280
+ // and plain Strings.
281
+ function SfDate(value) {
282
+ if (!(this instanceof SfDate)) return new SfDate(value);
283
+ this.value = Number(value);
284
+ }
285
+ function SfDisplayString(value) {
286
+ if (!(this instanceof SfDisplayString)) return new SfDisplayString(value);
287
+ this.value = String(value);
288
+ }
289
+
290
+ function _sfErr(opts) {
291
+ if (opts && typeof opts.ErrorClass === "function") {
292
+ return function (code, msg) { return opts.useNativeError === true ? new opts.ErrorClass(msg) : new opts.ErrorClass(code, msg); };
293
+ }
294
+ return function (code, msg) { var e = new Error(msg); e.code = code; return e; };
295
+ }
296
+
297
+ var INT_MAX = 999999999999999; // 15 digits (RFC 8941 §3.3.1)
298
+ var INT_MIN = -999999999999999;
299
+ function _isDigit(c) { return c >= "0" && c <= "9"; }
300
+ function _isLcAlpha(c) { return c >= "a" && c <= "z"; }
301
+ function _isAlpha(c) { return (c >= "A" && c <= "Z") || (c >= "a" && c <= "z"); }
302
+ function _isTchar(c) { return _isAlpha(c) || _isDigit(c) || "!#$%&'*+-.^_`|~".indexOf(c) !== -1; }
303
+ function _isKeyChar(c) { return _isLcAlpha(c) || _isDigit(c) || c === "_" || c === "-" || c === "." || c === "*"; }
304
+
305
+ function _parseNumber(cx, E) {
306
+ var sign = 1, type = "integer", num = "";
307
+ if (cx.s.charAt(cx.i) === "-") { sign = -1; cx.i += 1; }
308
+ if (cx.i >= cx.s.length || !_isDigit(cx.s.charAt(cx.i))) throw E("structured-fields/parse", "expected a digit at index " + cx.i);
309
+ for (;;) {
310
+ if (cx.i >= cx.s.length) break;
311
+ var c = cx.s.charAt(cx.i);
312
+ if (_isDigit(c)) { num += c; cx.i += 1; }
313
+ else if (type === "integer" && c === ".") {
314
+ if (num.length > 12) throw E("structured-fields/parse", "integer part of a decimal exceeds 12 digits"); // allow:raw-byte-literal — RFC 8941 §4.2.4 decimal integer-part cap
315
+ num += "."; type = "decimal"; cx.i += 1;
316
+ } else break;
317
+ if (type === "integer" && num.length > 15) throw E("structured-fields/parse", "integer exceeds 15 digits"); // allow:raw-byte-literal — §3.3.1 integer digit cap
318
+ if (type === "decimal" && num.length > 16) throw E("structured-fields/parse", "decimal exceeds the digit limit"); // allow:raw-byte-literal — 12 int + "." + 3 frac
319
+ }
320
+ if (type === "integer") return sign * parseInt(num, 10);
321
+ if (num.charAt(num.length - 1) === ".") throw E("structured-fields/parse", "decimal must not end with '.'");
322
+ if (num.length - num.indexOf(".") - 1 > 3) throw E("structured-fields/parse", "decimal fraction exceeds 3 digits");
323
+ return new SfDecimal(sign * parseFloat(num));
324
+ }
325
+
326
+ function _parseString(cx, E) {
327
+ cx.i += 1; // opening DQUOTE
328
+ var out = "";
329
+ while (cx.i < cx.s.length) {
330
+ var c = cx.s.charAt(cx.i); cx.i += 1;
331
+ if (c === "\\") {
332
+ if (cx.i >= cx.s.length) throw E("structured-fields/parse", "trailing backslash in string");
333
+ var n = cx.s.charAt(cx.i); cx.i += 1;
334
+ if (n !== "\\" && n !== "\"") throw E("structured-fields/parse", "invalid backslash escape in string");
335
+ out += n;
336
+ } else if (c === "\"") { return out; }
337
+ else {
338
+ var cc = c.charCodeAt(0);
339
+ if (cc < 0x20 || cc > 0x7e) throw E("structured-fields/parse", "non-printable character in string"); // allow:raw-byte-literal — RFC 8941 §4.2.5 printable-ASCII range
340
+ out += c;
341
+ }
342
+ }
343
+ throw E("structured-fields/parse", "unterminated string");
344
+ }
345
+
346
+ function _parseByteSeq(cx, E) {
347
+ cx.i += 1; // opening ":"
348
+ var start = cx.i;
349
+ while (cx.i < cx.s.length && cx.s.charAt(cx.i) !== ":") cx.i += 1;
350
+ if (cx.i >= cx.s.length) throw E("structured-fields/parse", "unterminated byte sequence");
351
+ var b64 = cx.s.slice(start, cx.i); cx.i += 1; // closing ":"
352
+ // RFC 8941 §4.2.7 synthesizes padding, so an unpadded value like
353
+ // `:aGVsbG8:` is valid input. Pad an unpadded value to a base64
354
+ // quantum, then require the decoded bytes to re-encode to exactly that
355
+ // padded text — rejecting stray characters, misplaced "=" padding, and
356
+ // non-zero trailing bits (Node's decoder is otherwise permissive).
357
+ var padded = b64.indexOf("=") === -1 ? b64 + "====".slice(0, (4 - (b64.length % 4)) % 4) : b64;
358
+ var buf = Buffer.from(padded, "base64");
359
+ if (buf.toString("base64") !== padded) throw E("structured-fields/parse", "byte sequence is not valid base64");
360
+ return new SfByteSequence(buf);
361
+ }
362
+
363
+ function _parseBoolean(cx, E) {
364
+ cx.i += 1; // "?"
365
+ var c = cx.s.charAt(cx.i); cx.i += 1;
366
+ if (c === "1") return true;
367
+ if (c === "0") return false;
368
+ throw E("structured-fields/parse", "boolean must be ?0 or ?1");
369
+ }
370
+
371
+ function _parseToken(cx) {
372
+ var start = cx.i; cx.i += 1; // first char already ALPHA / "*"
373
+ while (cx.i < cx.s.length) {
374
+ var c = cx.s.charAt(cx.i);
375
+ if (_isTchar(c) || c === ":" || c === "/") cx.i += 1; else break;
376
+ }
377
+ return new SfToken(cx.s.slice(start, cx.i));
378
+ }
379
+
380
+ var _utf8Strict = new TextDecoder("utf-8", { fatal: true });
381
+
382
+ function _parseDate(cx, E) {
383
+ cx.i += 1; // "@"
384
+ var n = _parseNumber(cx, E);
385
+ if (n instanceof SfDecimal) throw E("structured-fields/parse", "date must be an integer number of seconds");
386
+ return new SfDate(n);
387
+ }
388
+
389
+ function _parseDisplayString(cx, E) {
390
+ cx.i += 1; // "%"
391
+ if (cx.s.charAt(cx.i) !== "\"") throw E("structured-fields/parse", "display string must open with %\"");
392
+ cx.i += 1;
393
+ var bytes = [];
394
+ while (cx.i < cx.s.length) {
395
+ var c = cx.s.charAt(cx.i); cx.i += 1;
396
+ if (c === "%") {
397
+ var h = cx.s.substr(cx.i, 2);
398
+ if (h.length !== 2 || !/^[0-9a-f]{2}$/.test(h)) throw E("structured-fields/parse", "display string escape must be %<lowercase-hex><lowercase-hex>"); // allow:raw-byte-literal — RFC 9651 §4.2.10 two-hex-digit escape
399
+ bytes.push(parseInt(h, 16));
400
+ cx.i += 2;
401
+ } else if (c === "\"") {
402
+ try { return new SfDisplayString(_utf8Strict.decode(Buffer.from(bytes))); }
403
+ catch (_e) { throw E("structured-fields/parse", "display string is not valid UTF-8"); }
404
+ } else {
405
+ var cc = c.charCodeAt(0);
406
+ if (cc < 0x20 || cc > 0x7e) throw E("structured-fields/parse", "display string contains a raw non-printable / non-ASCII character"); // allow:raw-byte-literal — RFC 9651 §4.2.10 printable-ASCII range
407
+ bytes.push(cc);
408
+ }
409
+ }
410
+ throw E("structured-fields/parse", "unterminated display string");
411
+ }
412
+
413
+ function _parseBareItem(cx, E) {
414
+ var c = cx.s.charAt(cx.i);
415
+ if (c === "-" || _isDigit(c)) return _parseNumber(cx, E);
416
+ if (c === "\"") return _parseString(cx, E);
417
+ if (c === ":") return _parseByteSeq(cx, E);
418
+ if (c === "?") return _parseBoolean(cx, E);
419
+ if (c === "@") return _parseDate(cx, E);
420
+ if (c === "%") return _parseDisplayString(cx, E);
421
+ if (c === "*" || _isAlpha(c)) return _parseToken(cx);
422
+ throw E("structured-fields/parse", "unexpected character '" + (c || "<eof>") + "' at index " + cx.i);
423
+ }
424
+
425
+ function _parseKey(cx, E) {
426
+ var c = cx.s.charAt(cx.i);
427
+ if (!(c === "*" || _isLcAlpha(c))) throw E("structured-fields/parse", "key must start with lcalpha or '*'");
428
+ var start = cx.i; cx.i += 1;
429
+ while (cx.i < cx.s.length && _isKeyChar(cx.s.charAt(cx.i))) cx.i += 1;
430
+ return cx.s.slice(start, cx.i);
431
+ }
432
+
433
+ function _parseParams(cx, E) {
434
+ var params = new Map();
435
+ while (cx.i < cx.s.length && cx.s.charAt(cx.i) === ";") {
436
+ cx.i += 1;
437
+ while (cx.s.charAt(cx.i) === " ") cx.i += 1;
438
+ var key = _parseKey(cx, E);
439
+ var val = true;
440
+ if (cx.s.charAt(cx.i) === "=") { cx.i += 1; val = _parseBareItem(cx, E); }
441
+ params.set(key, val); // last value wins (RFC 8941 §4.2.3.2)
442
+ }
443
+ return params;
444
+ }
445
+
446
+ function _parseItem(cx, E) {
447
+ var value = _parseBareItem(cx, E);
448
+ return { value: value, params: _parseParams(cx, E) };
449
+ }
450
+
451
+ function _parseInnerList(cx, E) {
452
+ cx.i += 1; // "("
453
+ var items = [];
454
+ for (;;) {
455
+ while (cx.s.charAt(cx.i) === " ") cx.i += 1;
456
+ if (cx.i >= cx.s.length) throw E("structured-fields/parse", "unterminated inner list");
457
+ if (cx.s.charAt(cx.i) === ")") { cx.i += 1; return { items: items, params: _parseParams(cx, E) }; }
458
+ items.push(_parseItem(cx, E));
459
+ var c = cx.s.charAt(cx.i);
460
+ if (c !== " " && c !== ")") throw E("structured-fields/parse", "inner-list items must be space-separated");
461
+ }
462
+ }
463
+
464
+ function _parseItemOrInnerList(cx, E) {
465
+ return cx.s.charAt(cx.i) === "(" ? _parseInnerList(cx, E) : _parseItem(cx, E);
466
+ }
467
+
468
+ function _skipOWS(cx) { while (cx.s.charAt(cx.i) === " " || cx.s.charAt(cx.i) === "\t") cx.i += 1; }
469
+
470
+ function _parseList(cx, E) {
471
+ var members = [];
472
+ if (cx.i >= cx.s.length) return members;
473
+ for (;;) {
474
+ members.push(_parseItemOrInnerList(cx, E));
475
+ _skipOWS(cx);
476
+ if (cx.i >= cx.s.length) return members;
477
+ if (cx.s.charAt(cx.i) !== ",") throw E("structured-fields/parse", "expected ',' between list members");
478
+ cx.i += 1; _skipOWS(cx);
479
+ if (cx.i >= cx.s.length) throw E("structured-fields/parse", "trailing comma in list");
480
+ }
481
+ }
482
+
483
+ function _parseDict(cx, E) {
484
+ var dict = new Map();
485
+ if (cx.i >= cx.s.length) return dict;
486
+ for (;;) {
487
+ var key = _parseKey(cx, E);
488
+ var member;
489
+ if (cx.s.charAt(cx.i) === "=") { cx.i += 1; member = _parseItemOrInnerList(cx, E); }
490
+ else { member = { value: true, params: _parseParams(cx, E) }; }
491
+ dict.set(key, member); // last key wins (RFC 8941 §4.2.2)
492
+ _skipOWS(cx);
493
+ if (cx.i >= cx.s.length) return dict;
494
+ if (cx.s.charAt(cx.i) !== ",") throw E("structured-fields/parse", "expected ',' between dictionary members");
495
+ cx.i += 1; _skipOWS(cx);
496
+ if (cx.i >= cx.s.length) throw E("structured-fields/parse", "trailing comma in dictionary");
497
+ }
498
+ }
499
+
500
+ /**
501
+ * @primitive b.structuredFields.parse
502
+ * @signature b.structuredFields.parse(input, type, opts?)
503
+ * @since 0.12.54
504
+ * @status stable
505
+ * @related b.structuredFields.serialize, b.structuredFields.splitTopLevel
506
+ *
507
+ * Parse an RFC 8941 Structured Field value. <code>type</code> is
508
+ * <code>"item"</code>, <code>"list"</code>, or <code>"dictionary"</code>.
509
+ * Returns the value model: an item is <code>{ value, params }</code>
510
+ * (params is a <code>Map</code>); a list is an array of items / inner
511
+ * lists; a dictionary is a <code>Map</code>. Tokens, byte sequences,
512
+ * dates, and display strings come back as <code>SfToken</code> /
513
+ * <code>SfByteSequence</code> / <code>SfDate</code> /
514
+ * <code>SfDisplayString</code> instances so they stay distinct from
515
+ * plain strings and integers. Strictly enforces
516
+ * the grammar — integer / decimal digit caps, printable-ASCII strings,
517
+ * canonical base64, no trailing characters — and throws on any malformed
518
+ * input (pass <code>opts.ErrorClass</code> for a typed error).
519
+ *
520
+ * @opts
521
+ * ErrorClass?: Function, // typed error class (default: native Error with .code)
522
+ *
523
+ * @example
524
+ * b.structuredFields.parse("a=1, b=(x y);q=2", "dictionary");
525
+ * // → Map { "a" => { value: 1, params: Map{} },
526
+ * // "b" => { items: [...], params: Map{ "q" => 2 } } }
527
+ */
528
+ function parse(input, type, opts) {
529
+ var E = _sfErr(opts);
530
+ if (typeof input !== "string") throw E("structured-fields/bad-input", "structuredFields.parse: input must be a string");
531
+ var cx = { s: input, i: 0 };
532
+ while (cx.s.charAt(cx.i) === " ") cx.i += 1; // §4.2 discard leading SP
533
+ var out;
534
+ if (type === "item") out = _parseItem(cx, E);
535
+ else if (type === "list") out = _parseList(cx, E);
536
+ else if (type === "dictionary") out = _parseDict(cx, E);
537
+ else throw E("structured-fields/bad-type", "structuredFields.parse: type must be 'item' | 'list' | 'dictionary'");
538
+ while (cx.s.charAt(cx.i) === " ") cx.i += 1; // §4.2 discard trailing SP
539
+ if (cx.i !== cx.s.length) throw E("structured-fields/parse", "trailing characters after the field value");
540
+ return out;
541
+ }
542
+
543
+ function _serDecimal(v, E) {
544
+ if (!isFinite(v)) throw E("structured-fields/serialize", "cannot serialize a non-finite decimal");
545
+ var n = Math.round(v * 1000) / 1000; // allow:raw-byte-literal allow:raw-time-literal — RFC 8941 §4.1.5 decimal scale 10^3 (3 fractional digits), not a size or duration
546
+ if (Math.abs(Math.trunc(n)).toString().length > 12) throw E("structured-fields/serialize", "decimal integer part exceeds 12 digits"); // allow:raw-byte-literal — §4.1.5 cap
547
+ var s = n.toString();
548
+ if (s.indexOf(".") === -1) s += ".0"; // a Decimal must carry a fractional part
549
+ return s;
550
+ }
551
+ function _serDisplayString(s, E) {
552
+ if (typeof s !== "string") throw E("structured-fields/serialize", "display string value must be a string");
553
+ // RFC 9651 §4.1.10: serialize fails unless the value is a sequence of
554
+ // Unicode scalar values. A lone UTF-16 surrogate would otherwise be
555
+ // silently replaced with U+FFFD by Buffer.from, corrupting the output.
556
+ if (typeof s.isWellFormed === "function" ? !s.isWellFormed() : /[\uD800-\uDFFF]/.test(s.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, ""))) {
557
+ throw E("structured-fields/serialize", "display string contains a lone surrogate (not a valid Unicode string)");
558
+ }
559
+ var bytes = Buffer.from(s, "utf8"), out = "%\"";
560
+ for (var i = 0; i < bytes.length; i += 1) {
561
+ var b = bytes[i];
562
+ if (b >= 0x20 && b <= 0x7e && b !== 0x25 && b !== 0x22) out += String.fromCharCode(b); // allow:raw-byte-literal — RFC 9651 §4.1.10 printable ASCII except % and "
563
+ else out += "%" + (b < 0x10 ? "0" : "") + b.toString(16); // allow:raw-byte-literal — lowercase 2-hex escape
564
+ }
565
+ return out + "\"";
566
+ }
567
+ function _serBareItem(v, E) {
568
+ if (v === true) return "?1";
569
+ if (v === false) return "?0";
570
+ if (v instanceof SfDecimal) return _serDecimal(v.value, E);
571
+ if (v instanceof SfDate) {
572
+ if (!Number.isInteger(v.value) || v.value > INT_MAX || v.value < INT_MIN) throw E("structured-fields/serialize", "date must be an integer in RFC 9651 range");
573
+ return "@" + String(v.value);
574
+ }
575
+ if (v instanceof SfDisplayString) return _serDisplayString(v.value, E);
576
+ if (typeof v === "number") {
577
+ if (!isFinite(v)) throw E("structured-fields/serialize", "cannot serialize a non-finite number");
578
+ if (Number.isInteger(v)) {
579
+ if (v > INT_MAX || v < INT_MIN) throw E("structured-fields/serialize", "integer out of RFC 8941 range");
580
+ return String(v);
581
+ }
582
+ return _serDecimal(v, E); // a fractional JS number serializes as a Decimal
583
+ }
584
+ if (typeof v === "string") {
585
+ var out = "\"";
586
+ for (var i = 0; i < v.length; i += 1) {
587
+ var c = v.charAt(i), cc = v.charCodeAt(i);
588
+ if (cc < 0x20 || cc > 0x7e) throw E("structured-fields/serialize", "string contains a non-printable character"); // allow:raw-byte-literal — §4.1.6 printable-ASCII range
589
+ if (c === "\\" || c === "\"") out += "\\";
590
+ out += c;
591
+ }
592
+ return out + "\"";
593
+ }
594
+ if (v instanceof SfToken) {
595
+ var t = v.value;
596
+ if (t.length === 0 || !(t.charAt(0) === "*" || _isAlpha(t.charAt(0)))) throw E("structured-fields/serialize", "invalid token");
597
+ for (var j = 1; j < t.length; j += 1) { var tc = t.charAt(j); if (!(_isTchar(tc) || tc === ":" || tc === "/")) throw E("structured-fields/serialize", "invalid token character"); }
598
+ return t;
599
+ }
600
+ if (v instanceof SfByteSequence) return ":" + v.value.toString("base64") + ":";
601
+ throw E("structured-fields/serialize", "unsupported bare-item type");
602
+ }
603
+
604
+ function _serParams(params, E) {
605
+ if (!params) return "";
606
+ var out = "";
607
+ params.forEach(function (val, key) {
608
+ out += ";" + _serKey(key, E);
609
+ if (val !== true) out += "=" + _serBareItem(val, E);
610
+ });
611
+ return out;
612
+ }
613
+ function _serKey(key, E) {
614
+ if (typeof key !== "string" || key.length === 0 || !(key.charAt(0) === "*" || _isLcAlpha(key.charAt(0)))) throw E("structured-fields/serialize", "invalid parameter/dictionary key");
615
+ for (var i = 1; i < key.length; i += 1) { if (!_isKeyChar(key.charAt(i))) throw E("structured-fields/serialize", "invalid key character"); }
616
+ return key;
617
+ }
618
+ function _serItem(item, E) { return _serBareItem(item.value, E) + _serParams(item.params, E); }
619
+ function _serMember(m, E) {
620
+ if (m && Array.isArray(m.items)) {
621
+ return "(" + m.items.map(function (it) { return _serItem(it, E); }).join(" ") + ")" + _serParams(m.params, E);
622
+ }
623
+ return _serItem(m, E);
624
+ }
625
+
626
+ /**
627
+ * @primitive b.structuredFields.serialize
628
+ * @signature b.structuredFields.serialize(value, type, opts?)
629
+ * @since 0.12.54
630
+ * @status stable
631
+ * @related b.structuredFields.parse
632
+ *
633
+ * Serialize a value model back to an RFC 8941 field value (the inverse
634
+ * of <code>parse</code>). <code>type</code> is <code>"item"</code>,
635
+ * <code>"list"</code>, or <code>"dictionary"</code>. Numbers serialize as
636
+ * Integers when integral and Decimals (rounded to 3 fractional digits)
637
+ * otherwise; wrap Tokens / byte strings in <code>SfToken</code> /
638
+ * <code>SfByteSequence</code>. Throws on values outside the RFC's ranges
639
+ * or grammar (out-of-range integers, non-printable string characters,
640
+ * invalid tokens / keys).
641
+ *
642
+ * @opts
643
+ * ErrorClass?: Function, // typed error class (default: native Error with .code)
644
+ *
645
+ * @example
646
+ * var sf = b.structuredFields;
647
+ * sf.serialize({ value: new sf.Token("gzip"), params: new Map([["q", 1]]) }, "item");
648
+ * // → "gzip;q=1"
649
+ */
650
+ function serialize(value, type, opts) {
651
+ var E = _sfErr(opts);
652
+ if (type === "item") {
653
+ if (!value || typeof value !== "object" || !("value" in value)) throw E("structured-fields/serialize", "item must be { value, params }");
654
+ return _serItem(value, E);
655
+ }
656
+ if (type === "list") {
657
+ if (!Array.isArray(value)) throw E("structured-fields/serialize", "list must be an array");
658
+ return value.map(function (m) { return _serMember(m, E); }).join(", ");
659
+ }
660
+ if (type === "dictionary") {
661
+ var entries = value instanceof Map ? Array.from(value.entries()) : (value && typeof value === "object" ? Object.keys(value).map(function (k) { return [k, value[k]]; }) : null);
662
+ if (!entries) throw E("structured-fields/serialize", "dictionary must be a Map or object");
663
+ return entries.map(function (e) {
664
+ var key = _serKey(e[0], E), m = e[1];
665
+ if (m && !Array.isArray(m.items) && m.value === true) return key + _serParams(m.params, E); // bare-true member omits "=?1"
666
+ return key + "=" + _serMember(m, E);
667
+ }).join(", ");
668
+ }
669
+ throw E("structured-fields/bad-type", "structuredFields.serialize: type must be 'item' | 'list' | 'dictionary'");
670
+ }
671
+
239
672
  module.exports = {
240
673
  splitTopLevel: splitTopLevel,
241
674
  refuseControlBytes: refuseControlBytes,
242
675
  containsControlBytes: containsControlBytes,
243
676
  unquoteSfString: unquoteSfString,
677
+ parse: parse,
678
+ serialize: serialize,
679
+ Token: SfToken,
680
+ ByteSequence: SfByteSequence,
681
+ Decimal: SfDecimal,
682
+ Date: SfDate,
683
+ DisplayString: SfDisplayString,
244
684
  };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.12.53",
3
+ "version": "0.12.55",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "../scripts/release-notes-schema.json",
3
+ "version": "0.12.54",
4
+ "date": "2026-05-25",
5
+ "headline": "`b.structuredFields.parse` / `serialize` — full RFC 8941 Structured Fields codec",
6
+ "summary": "The structured-fields module gains a complete RFC 8941 parser and serializer alongside its existing quote-aware helpers. b.structuredFields.parse reads an Item, List, or Dictionary into a typed value model — items are { value, params }, lists are arrays of items / inner lists, dictionaries are Maps — with Tokens and byte sequences returned as distinct SfToken / SfByteSequence instances. It enforces the grammar strictly: integer and decimal digit caps, printable-ASCII strings, canonical base64 byte sequences, valid token and key grammar, and no trailing characters. b.structuredFields.serialize is the exact inverse. This is the real parser the framework's Content-Digest, Client Hints, Web Push, and HTTP Message Signature surfaces can build on instead of open-coding each field. Validated against the official httpwg structured-field-tests conformance vectors.",
7
+ "sections": [
8
+ {
9
+ "heading": "Added",
10
+ "items": [
11
+ {
12
+ "title": "`b.structuredFields.parse(input, type, opts?)` / `serialize(value, type, opts?)` / `Token` / `ByteSequence`",
13
+ "body": "`parse` accepts `type` of `\"item\"`, `\"list\"`, or `\"dictionary\"` and returns the value model (items as `{ value, params }` with a `Map` of parameters; lists as arrays of items or inner lists; dictionaries as `Map`s). Bare items are JS numbers (Integer / Decimal), strings, booleans, `SfToken`, or `SfByteSequence`. Malformed input is rejected — out-of-range integers, over-long decimals, non-printable string bytes, non-canonical base64, invalid tokens / keys, and any trailing characters — and `opts.ErrorClass` yields a typed error. `serialize` is the inverse, rounding decimals to three fractional digits and refusing values outside the RFC's ranges or grammar. `b.structuredFields.Token` and `b.structuredFields.ByteSequence` wrap those bare-item types for serialization. The existing `splitTopLevel` / `refuseControlBytes` / `unquoteSfString` helpers are unchanged."
14
+ }
15
+ ]
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "../scripts/release-notes-schema.json",
3
+ "version": "0.12.55",
4
+ "date": "2026-05-25",
5
+ "headline": "`b.structuredFields` — RFC 9651 Date and Display String types",
6
+ "summary": "Brings the Structured Fields codec up to RFC 9651, which obsoletes RFC 8941 by adding two bare-item types. A Date (`@1659578233`) is an Integer number of seconds since the Unix epoch; a Display String (`%\"f%c3%bc%c3%bc\"`) is a Unicode string conveyed as percent-escaped UTF-8. parse returns them as distinct SfDate / SfDisplayString values, and serialize emits them canonically — a Date as `@` + integer, a Display String as `%\"`-wrapped lowercase-percent-escaped UTF-8 that escapes only what RFC 9651 requires. Parsing is strict: a Date rejects a decimal / out-of-range value, and a Display String rejects uppercase escapes, raw non-ASCII, bad hex, and invalid UTF-8. Validated against the official httpwg structured-field-tests date and display-string vectors.",
7
+ "sections": [
8
+ {
9
+ "heading": "Added",
10
+ "items": [
11
+ {
12
+ "title": "RFC 9651 Date (`@…`) and Display String (`%\"…\"`) in `b.structuredFields`",
13
+ "body": "`parse` now reads the two RFC 9651 types: `@` + an Integer yields an `SfDate` (rejecting a decimal `@1.5`, an empty `@`, a sign-only `@-`, and out-of-range values), and `%\"…\"` yields an `SfDisplayString` (decoding lowercase `%XX` escapes as UTF-8, rejecting uppercase escapes, raw non-ASCII or control characters, malformed hex, and invalid UTF-8). `serialize` is the inverse — a Date as `@` + the integer, a Display String percent-escaping only non-printable / non-ASCII bytes plus `%` and `\"`. The new `b.structuredFields.Date` and `b.structuredFields.DisplayString` wrappers construct these values. The module now tracks RFC 9651 (which obsoletes RFC 8941); the existing Item / List / Dictionary parsing is unchanged."
14
+ }
15
+ ]
16
+ }
17
+ ]
18
+ }