@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.
- package/CHANGELOG.md +4 -0
- package/lib/admin.js +2 -0
- package/lib/checkout.js +133 -0
- package/lib/payment.js +252 -4
- package/lib/vendor/MANIFEST.json +2 -2
- package/lib/vendor/blamejs/CHANGELOG.md +4 -0
- package/lib/vendor/blamejs/README.md +1 -0
- package/lib/vendor/blamejs/api-snapshot.json +30 -2
- package/lib/vendor/blamejs/lib/structured-fields.js +441 -1
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.12.54.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.55.json +18 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/structured-fields-codec.test.js +207 -0
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* @module b.structuredFields
|
|
4
4
|
* @nav HTTP
|
|
5
|
-
* @title RFC
|
|
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
|
};
|
|
@@ -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
|
+
}
|