@blamejs/core 0.12.54 → 0.12.56
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/README.md +2 -1
- package/index.js +2 -0
- package/lib/canonical-json.js +117 -61
- package/lib/structured-fields.js +85 -7
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.12.x
|
|
10
10
|
|
|
11
|
+
- v0.12.56 (2026-05-25) — **`b.canonicalJson` — RFC 8785 JSON Canonicalization Scheme, now a public primitive.** The deterministic JSON serializer the framework uses internally for audit-chain and config-drift fingerprints is now an operator-facing primitive, for signing your own JSON (custom credentials, receipts, deterministic request signing). b.canonicalJson.stringifyJcs is strict RFC 8785: keys sorted in UTF-16 code-unit order at every depth, numbers in the ECMAScript format JCS references, and types JCS does not define (BigInt / Buffer / Date / Map / Set / circular references) refused rather than silently lost. b.canonicalJson.stringify is a lenient variant that also serializes Buffers (hex), Dates (ISO-8601), and BigInts. Exposing it surfaced and fixed a latent ordering bug: the serializer built a sorted-key object and let JSON.stringify emit it, but V8 hoists integer-like keys ("1", "10") to the front — so canonical output was wrong for objects with integer-like string keys. Members are now written in sorted order directly. Validated against the official cyberphone/json-canonicalization conformance vectors. **Added:** *`b.canonicalJson.stringifyJcs(value)` / `stringify(value, opts?)` / `sortKeys(obj)`* — `stringifyJcs` produces strict RFC 8785 canonical JSON — the byte-for-byte stable form to hash or sign — with UTF-16 code-unit key sorting and ECMAScript number formatting, refusing BigInt / Buffer / Date / Map / Set / RegExp / Symbol / function / circular references. `stringify` is the lenient framework variant (Buffers → hex, Dates → ISO-8601, BigInts → decimal; `opts.bufferAs: "reject"` to forbid binary). `sortKeys` returns an object's own keys in the canonical UTF-16 ordering. These were framework-internal; they are now documented public API. **Fixed:** *Canonical JSON now emits integer-like keys in sorted order* — The canonical serializer built a sorted-key object and serialized it with JSON.stringify, which hoists integer-like string keys ("1", "10") to the front per V8 own-property ordering — producing non-canonical output for objects containing such keys (a violation of RFC 8785 §3.2.3). Members are now written in sorted-key order directly. Real-world consumers (audit-chain, config-drift) use named fields and are unaffected; only objects with integer-like string keys change, and the new output is the correct canonical form.
|
|
12
|
+
|
|
13
|
+
- v0.12.55 (2026-05-25) — **`b.structuredFields` — RFC 9651 Date and Display String types.** 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. **Added:** *RFC 9651 Date (`@…`) and Display String (`%"…"`) in `b.structuredFields`* — `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
|
+
|
|
11
15
|
- v0.12.54 (2026-05-25) — **`b.structuredFields.parse` / `serialize` — full RFC 8941 Structured Fields codec.** 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. **Added:** *`b.structuredFields.parse(input, type, opts?)` / `serialize(value, type, opts?)` / `Token` / `ByteSequence`* — `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.
|
|
12
16
|
|
|
13
17
|
- v0.12.53 (2026-05-25) — **`b.contentDigest` — HTTP Content-Digest / Repr-Digest fields (RFC 9530).** Emit and verify the Content-Digest / Repr-Digest HTTP fields so a recipient can detect a corrupted or tampered message body. b.contentDigest.create builds the RFC 8941 dictionary value (sha-256=:base64:, sha-512=:base64:) over a body; b.contentDigest.verify recomputes each modern digest over the body and compares it in constant time. Only SHA-256 and SHA-512 are computed — the legacy algorithms RFC 9530 §6 marks insecure (MD5, SHA-1, the unix checksums) are ignored on verify, and a field carrying no modern digest is refused, so an attacker cannot downgrade integrity to an MD5-only digest. Content-Digest is the integrity companion to HTTP Message Signatures (b.httpSig, RFC 9421): sign the digest rather than the whole body. Verified against the RFC 9530 Appendix D worked examples. **Added:** *`b.contentDigest.create(body, opts?)` / `b.contentDigest.verify(fieldValue, body, opts?)`* — `create` returns a Content-Digest / Repr-Digest field value over the body — SHA-256 by default, or any subset of `["sha-256","sha-512"]` via `opts.algorithms` — and refuses insecure or unknown algorithms. `verify` parses the field, recomputes each SHA-256 / SHA-512 entry over the body, and compares constant-time; it throws `content-digest/mismatch` on any mismatch, ignores legacy / unknown entries, throws `content-digest/no-modern-digest` if the field has no SHA-256 / SHA-512 entry at all, and honours `opts.required` to force specific algorithms to be present and match. Composes the framework's structured-field helpers and constant-time compare; Repr-Digest is the same machinery over the selected representation (RFC 9110).
|
package/README.md
CHANGED
|
@@ -98,7 +98,8 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
|
|
|
98
98
|
- **AAD-bound sealed columns** — AEAD tag tied to `(table, rowId, column, schemaVersion)`; copy-paste between rows or schema-version replay surfaces as refused decrypt (`b.vault.aad`)
|
|
99
99
|
- **Signed webhooks + API encryption** — SLH-DSA-SHAKE-256f default; ML-DSA-65 opt-in; ECIES API encryption (`b.webhook`, `b.crypto`)
|
|
100
100
|
- **HPKE / HTTP signatures** — RFC 9180 HPKE with ML-KEM-1024 + HKDF-SHA3-512 + ChaCha20-Poly1305 (`b.crypto.hpke`); RFC 9421 HTTP Message Signatures with derived components and ed25519 / ML-DSA-65 (`b.crypto.httpSig`); RFC 9530 Content-Digest / Repr-Digest body-integrity fields (SHA-256 / SHA-512, legacy algorithms refused — `b.contentDigest`) to sign the digest rather than the whole body
|
|
101
|
-
- **
|
|
101
|
+
- **Canonical JSON** — RFC 8785 JSON Canonicalization Scheme (`b.canonicalJson.stringifyJcs`): the deterministic, sorted-key byte form to hash or sign (custom credentials, receipts, deterministic request signing); UTF-16 key ordering + ECMAScript number formatting, with a lenient `stringify` variant for Buffers / Dates / BigInts
|
|
102
|
+
- **Structured Fields** — full RFC 9651 codec (`b.structuredFields.parse` / `serialize`): Items / Lists / Dictionaries, Inner Lists, Parameters, and every bare-item type (Integer / Decimal / String / Token / Byte Sequence / Boolean / Date / Display String) with strict grammar + range enforcement — the parser behind Content-Digest, Client Hints, and HTTP Message Signatures
|
|
102
103
|
- **CMS codec** — RFC 5652 Cryptographic Message Syntax encoder + decoder with PQC signers (ML-DSA-65 / ML-DSA-87 / SLH-DSA-SHAKE-256f; RFC 9909 + 9881) and KEMRecipientInfo recipients (ML-KEM-1024; RFC 9629 + 9936); ChaCha20-Poly1305 content encryption (RFC 8103) so Efail-class malleability cannot apply (`b.cms`)
|
|
103
104
|
- **Stream throttle** — shared token-bucket bandwidth limiter (RFC 2697 srTCM shape); N concurrent `node:stream` pipelines draw from one operator-configured `bytesPerSec` budget (`b.streamThrottle`)
|
|
104
105
|
- **TLS-RPT receiver** — RFC 8460 inbound aggregate-report ingest; HTTPS POST handler + §4.4 schema parser with gzip-bomb / ratio-bomb / depth-bomb defenses (`b.mail.deploy.parseTlsRptReport` / `b.mail.deploy.tlsRptIngestHttp`)
|
package/index.js
CHANGED
|
@@ -396,6 +396,7 @@ var dbsc = require("./lib/dbsc");
|
|
|
396
396
|
var importmapIntegrity = require("./lib/importmap-integrity");
|
|
397
397
|
var privacyPass = require("./lib/privacy-pass");
|
|
398
398
|
var contentDigest = require("./lib/content-digest");
|
|
399
|
+
var canonicalJson = require("./lib/canonical-json");
|
|
399
400
|
var standardWebhooks = require("./lib/standard-webhooks");
|
|
400
401
|
var lro = require("./lib/lro");
|
|
401
402
|
var jsonApi = require("./lib/jsonapi");
|
|
@@ -413,6 +414,7 @@ module.exports = {
|
|
|
413
414
|
importmapIntegrity: importmapIntegrity,
|
|
414
415
|
privacyPass: privacyPass,
|
|
415
416
|
contentDigest: contentDigest,
|
|
417
|
+
canonicalJson: canonicalJson,
|
|
416
418
|
standardWebhooks: standardWebhooks,
|
|
417
419
|
lro: lro,
|
|
418
420
|
jsonApi: jsonApi,
|
package/lib/canonical-json.js
CHANGED
|
@@ -1,79 +1,83 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* @module b.canonicalJson
|
|
4
|
+
* @nav Data
|
|
5
|
+
* @title Canonical JSON
|
|
4
6
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
7
|
+
* @intro
|
|
8
|
+
* Deterministic JSON serialization with keys sorted at every depth —
|
|
9
|
+
* the byte-for-byte stable form you hash or sign so two parties that
|
|
10
|
+
* build the same data produce the same bytes. <code>stringifyJcs</code>
|
|
11
|
+
* is strict RFC 8785 (JSON Canonicalization Scheme); <code>stringify</code>
|
|
12
|
+
* is a lenient variant that additionally serializes Buffers (as hex),
|
|
13
|
+
* Dates (ISO-8601), and BigInts (decimal) for the framework's own audit
|
|
14
|
+
* / config-drift fingerprints.
|
|
13
15
|
*
|
|
14
|
-
*
|
|
16
|
+
* Both walks close the silent-data-loss class that ad-hoc
|
|
17
|
+
* <code>Object.keys(...).sort()</code> serializers fall into: Map /
|
|
18
|
+
* Set / RegExp / class instances, Symbols, functions, and circular
|
|
19
|
+
* references all throw a clean error rather than emitting <code>{}</code>
|
|
20
|
+
* or stack-overflowing. RFC 8785 strict mode additionally refuses
|
|
21
|
+
* BigInt / Buffer / Date (types JCS does not define) so the operator
|
|
22
|
+
* converts them to JSON-native shapes before signing.
|
|
15
23
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* Map / Set / RegExp → throw with constructor name
|
|
22
|
-
* symbol / function → throw with type name
|
|
23
|
-
* circular reference → throw via WeakSet detection
|
|
24
|
-
* plain array → recurse, preserve order
|
|
25
|
-
* plain object → recurse with sorted keys
|
|
24
|
+
* Key ordering is V8's <code>Object.keys(...).sort()</code> —
|
|
25
|
+
* lexicographic UTF-16 code-unit order, which is exactly RFC 8785
|
|
26
|
+
* §3.2.3 — and numbers are formatted by <code>JSON.stringify</code>,
|
|
27
|
+
* whose output is the ECMA-262 Number-to-string algorithm that RFC
|
|
28
|
+
* 8785 §3.2.2.3 references.
|
|
26
29
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* canonical output is reversible.
|
|
34
|
-
* bufferAs: "reject" pagination — cursor state is operator-supplied
|
|
35
|
-
* primitive data; binary in a cursor is almost
|
|
36
|
-
* always a bug; reject loudly.
|
|
37
|
-
*
|
|
38
|
-
* Operators don't call this directly — it's a framework-internal walker.
|
|
30
|
+
* @card
|
|
31
|
+
* Canonical JSON (RFC 8785 JCS) — the deterministic, sorted-key byte
|
|
32
|
+
* form you sign or hash. Strict <code>stringifyJcs</code> for
|
|
33
|
+
* interop, plus a lenient framework variant that serializes Buffers /
|
|
34
|
+
* Dates / BigInts. Lossy ad-hoc serializers (Map / Set / circular →
|
|
35
|
+
* <code>{}</code>) are refused.
|
|
39
36
|
*/
|
|
40
37
|
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
// Emit the canonical JSON STRING in one ordered pass. Object members are
|
|
39
|
+
// written in sorted-key order directly — building a plain object and
|
|
40
|
+
// relying on JSON.stringify would silently hoist integer-like keys
|
|
41
|
+
// ("1", "10") to the front (V8 own-property ordering), breaking the
|
|
42
|
+
// RFC 8785 §3.2.3 sort. Primitives, strings, and numbers use
|
|
43
|
+
// JSON.stringify, whose escaping (§3.2.2.2) and ECMAScript number format
|
|
44
|
+
// (§3.2.2.3) are exactly what JCS references.
|
|
45
|
+
function _emit(value, seen, bufferAs) {
|
|
46
|
+
if (value === null || typeof value === "undefined") return "null";
|
|
43
47
|
var t = typeof value;
|
|
44
|
-
if (t === "
|
|
48
|
+
if (t === "number" || t === "string" || t === "boolean") return JSON.stringify(value);
|
|
45
49
|
if (t === "bigint") {
|
|
46
50
|
if (bufferAs === "reject-jcs") {
|
|
47
51
|
throw new Error("canonical-json: BigInt is not serialisable under " +
|
|
48
52
|
"RFC 8785 (JCS); convert to a string or number before passing in");
|
|
49
53
|
}
|
|
50
|
-
return String(value);
|
|
54
|
+
return JSON.stringify(String(value));
|
|
51
55
|
}
|
|
52
56
|
if (t === "symbol" || t === "function") {
|
|
53
57
|
throw new Error("canonical-json: " + t + " value is not " +
|
|
54
58
|
"serialisable; convert to a string before passing in");
|
|
55
59
|
}
|
|
56
60
|
// Buffer / Uint8Array — policy-driven
|
|
57
|
-
if (Buffer.isBuffer(value))
|
|
61
|
+
if (Buffer.isBuffer(value)) {
|
|
58
62
|
if (bufferAs === "reject" || bufferAs === "reject-jcs") {
|
|
59
63
|
throw new Error("canonical-json: Buffer is not serialisable in this " +
|
|
60
64
|
"context (bufferAs=reject); convert to a string or hex first");
|
|
61
65
|
}
|
|
62
|
-
return value.toString("hex");
|
|
66
|
+
return JSON.stringify(value.toString("hex"));
|
|
63
67
|
}
|
|
64
68
|
if (value instanceof Uint8Array) {
|
|
65
69
|
if (bufferAs === "reject" || bufferAs === "reject-jcs") {
|
|
66
70
|
throw new Error("canonical-json: Uint8Array is not serialisable in " +
|
|
67
71
|
"this context (bufferAs=reject); convert to a string or hex first");
|
|
68
72
|
}
|
|
69
|
-
return Buffer.from(value).toString("hex");
|
|
73
|
+
return JSON.stringify(Buffer.from(value).toString("hex"));
|
|
70
74
|
}
|
|
71
75
|
if (value instanceof Date) {
|
|
72
76
|
if (bufferAs === "reject-jcs") {
|
|
73
77
|
throw new Error("canonical-json: Date is not serialisable under " +
|
|
74
78
|
"RFC 8785 (JCS); convert to ISO-8601 string before passing in");
|
|
75
79
|
}
|
|
76
|
-
return value.toISOString();
|
|
80
|
+
return JSON.stringify(value.toISOString());
|
|
77
81
|
}
|
|
78
82
|
// After primitives + Date + Buffer + Uint8Array, any remaining "object"
|
|
79
83
|
// must be a plain object or array. Map / Set / RegExp / class instances
|
|
@@ -88,35 +92,73 @@ function _scrub(value, seen, bufferAs) {
|
|
|
88
92
|
}
|
|
89
93
|
seen.add(value);
|
|
90
94
|
if (Array.isArray(value)) {
|
|
91
|
-
|
|
95
|
+
// Index loop, not .map(): map() skips holes in a sparse array,
|
|
96
|
+
// which join() would then render as invalid elisions ([,1]). A hole
|
|
97
|
+
// reads as undefined → _emit returns "null" (matching JSON.stringify).
|
|
98
|
+
var items = [];
|
|
99
|
+
for (var ai = 0; ai < value.length; ai += 1) {
|
|
100
|
+
items.push(_emit(value[ai], seen, bufferAs));
|
|
101
|
+
}
|
|
102
|
+
return "[" + items.join(",") + "]";
|
|
92
103
|
}
|
|
93
104
|
// Canonical-json IS the destination for sorted-keys walks across the
|
|
94
105
|
// codebase; the keys-then-sort here is the canonical primitive itself.
|
|
95
106
|
var keys = Object.keys(value);
|
|
96
107
|
keys.sort();
|
|
97
|
-
var
|
|
108
|
+
var parts = [];
|
|
98
109
|
for (var i = 0; i < keys.length; i++) {
|
|
99
|
-
|
|
110
|
+
parts.push(JSON.stringify(keys[i]) + ":" + _emit(value[keys[i]], seen, bufferAs));
|
|
100
111
|
}
|
|
101
|
-
return
|
|
112
|
+
return "{" + parts.join(",") + "}";
|
|
102
113
|
}
|
|
103
114
|
|
|
104
|
-
|
|
105
|
-
|
|
115
|
+
/**
|
|
116
|
+
* @primitive b.canonicalJson.stringify
|
|
117
|
+
* @signature b.canonicalJson.stringify(value, opts?)
|
|
118
|
+
* @since 0.5.0
|
|
119
|
+
* @status stable
|
|
120
|
+
* @related b.canonicalJson.stringifyJcs, b.canonicalJson.sortKeys
|
|
121
|
+
*
|
|
122
|
+
* Deterministic JSON with keys sorted at every depth — the lenient
|
|
123
|
+
* framework variant. Beyond JSON-native values it serializes Buffers /
|
|
124
|
+
* Uint8Arrays (hex), Dates (ISO-8601), and BigInts (decimal string); Map
|
|
125
|
+
* / Set / RegExp / class instances, Symbols, functions, and circular
|
|
126
|
+
* references throw rather than silently emitting <code>{}</code>. Use
|
|
127
|
+
* <code>stringifyJcs</code> for strict RFC 8785 interop.
|
|
128
|
+
*
|
|
129
|
+
* @opts
|
|
130
|
+
* bufferAs: string, // "hex" (default) | "reject" — Buffer / Uint8Array policy
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* b.canonicalJson.stringify({ b: 1, a: 2 });
|
|
134
|
+
* // → '{"a":2,"b":1}'
|
|
135
|
+
*/
|
|
106
136
|
function stringify(value, opts) {
|
|
107
137
|
var bufferAs = (opts && opts.bufferAs) || "hex";
|
|
108
138
|
if (bufferAs !== "hex" && bufferAs !== "reject" && bufferAs !== "reject-jcs") {
|
|
109
139
|
throw new Error("canonical-json: bufferAs must be 'hex' / 'reject' / 'reject-jcs'; got " +
|
|
110
140
|
JSON.stringify(bufferAs));
|
|
111
141
|
}
|
|
112
|
-
return
|
|
142
|
+
return _emit(value, null, bufferAs);
|
|
113
143
|
}
|
|
114
144
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
145
|
+
/**
|
|
146
|
+
* @primitive b.canonicalJson.sortKeys
|
|
147
|
+
* @signature b.canonicalJson.sortKeys(obj)
|
|
148
|
+
* @since 0.5.0
|
|
149
|
+
* @status stable
|
|
150
|
+
* @related b.canonicalJson.stringify
|
|
151
|
+
*
|
|
152
|
+
* The object's own keys in the framework's single canonical ordering —
|
|
153
|
+
* lexicographic UTF-16 code-unit sort (the same ordering the canonical
|
|
154
|
+
* serializers use). Returns an empty array for a non-object. Route
|
|
155
|
+
* fingerprint / report ordering through this rather than re-implementing
|
|
156
|
+
* the keys-then-sort dance inline.
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* b.canonicalJson.sortKeys({ b: 1, a: 2, c: 3 });
|
|
160
|
+
* // → ["a", "b", "c"]
|
|
161
|
+
*/
|
|
120
162
|
function sortKeys(obj) {
|
|
121
163
|
if (!obj || typeof obj !== "object") return [];
|
|
122
164
|
var keys = Object.keys(obj);
|
|
@@ -124,16 +166,30 @@ function sortKeys(obj) {
|
|
|
124
166
|
return keys;
|
|
125
167
|
}
|
|
126
168
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
169
|
+
/**
|
|
170
|
+
* @primitive b.canonicalJson.stringifyJcs
|
|
171
|
+
* @signature b.canonicalJson.stringifyJcs(value)
|
|
172
|
+
* @since 0.12.56
|
|
173
|
+
* @status stable
|
|
174
|
+
* @compliance soc2
|
|
175
|
+
* @related b.canonicalJson.stringify, b.vc.issue, b.scitt.signStatement
|
|
176
|
+
*
|
|
177
|
+
* Strict RFC 8785 JSON Canonicalization Scheme — the deterministic byte
|
|
178
|
+
* form to hash or sign when two parties must agree on the exact bytes
|
|
179
|
+
* (signed JSON credentials, receipts, deterministic request signing).
|
|
180
|
+
* Keys are sorted in UTF-16 code-unit order at every depth (§3.2.3) and
|
|
181
|
+
* numbers use the ECMAScript Number-to-string formatting §3.2.2.3
|
|
182
|
+
* references. Inputs JCS does not define — BigInt, Buffer / Uint8Array,
|
|
183
|
+
* Date, Map, Set, RegExp, Symbol, function, and circular references —
|
|
184
|
+
* are refused, so the operator converts them to JSON-native shapes
|
|
185
|
+
* before signing rather than getting a silently lossy result.
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* b.canonicalJson.stringifyJcs({ "€": 1, "$": 2 });
|
|
189
|
+
* // → '{"$":2,"€":1}' (keys sorted by UTF-16 code unit)
|
|
190
|
+
*/
|
|
135
191
|
function stringifyJcs(value) {
|
|
136
|
-
return
|
|
192
|
+
return _emit(value, null, "reject-jcs");
|
|
137
193
|
}
|
|
138
194
|
|
|
139
195
|
module.exports = {
|
package/lib/structured-fields.js
CHANGED
|
@@ -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 !== ";") {
|
|
@@ -237,13 +241,15 @@ function containsControlBytes(value, opts) {
|
|
|
237
241
|
}
|
|
238
242
|
|
|
239
243
|
// ---------------------------------------------------------------------------
|
|
240
|
-
// Full RFC
|
|
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
|
|
241
246
|
// quote-aware splitters individual parsers reach for; the codec below is
|
|
242
247
|
// the complete grammar — Items, Lists, Dictionaries, Inner Lists,
|
|
243
248
|
// Parameters, and every bare-item type.
|
|
244
249
|
//
|
|
245
|
-
// Value model:
|
|
246
|
-
// bare item → number (Integer) | SfDecimal | string | boolean | SfToken
|
|
250
|
+
// Value model (RFC 9651, which obsoletes RFC 8941):
|
|
251
|
+
// bare item → number (Integer) | SfDecimal | string | boolean | SfToken
|
|
252
|
+
// | SfByteSequence | SfDate | SfDisplayString
|
|
247
253
|
// item → { value: bareItem, params: Map<string, bareItem> }
|
|
248
254
|
// inner list → { items: item[], params: Map<string, bareItem> }
|
|
249
255
|
// list → (item | innerList)[]
|
|
@@ -268,6 +274,18 @@ function SfDecimal(value) {
|
|
|
268
274
|
if (!(this instanceof SfDecimal)) return new SfDecimal(value);
|
|
269
275
|
this.value = Number(value);
|
|
270
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
|
+
}
|
|
271
289
|
|
|
272
290
|
function _sfErr(opts) {
|
|
273
291
|
if (opts && typeof opts.ErrorClass === "function") {
|
|
@@ -359,12 +377,47 @@ function _parseToken(cx) {
|
|
|
359
377
|
return new SfToken(cx.s.slice(start, cx.i));
|
|
360
378
|
}
|
|
361
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
|
+
|
|
362
413
|
function _parseBareItem(cx, E) {
|
|
363
414
|
var c = cx.s.charAt(cx.i);
|
|
364
415
|
if (c === "-" || _isDigit(c)) return _parseNumber(cx, E);
|
|
365
416
|
if (c === "\"") return _parseString(cx, E);
|
|
366
417
|
if (c === ":") return _parseByteSeq(cx, E);
|
|
367
418
|
if (c === "?") return _parseBoolean(cx, E);
|
|
419
|
+
if (c === "@") return _parseDate(cx, E);
|
|
420
|
+
if (c === "%") return _parseDisplayString(cx, E);
|
|
368
421
|
if (c === "*" || _isAlpha(c)) return _parseToken(cx);
|
|
369
422
|
throw E("structured-fields/parse", "unexpected character '" + (c || "<eof>") + "' at index " + cx.i);
|
|
370
423
|
}
|
|
@@ -455,9 +508,11 @@ function _parseDict(cx, E) {
|
|
|
455
508
|
* <code>"item"</code>, <code>"list"</code>, or <code>"dictionary"</code>.
|
|
456
509
|
* Returns the value model: an item is <code>{ value, params }</code>
|
|
457
510
|
* (params is a <code>Map</code>); a list is an array of items / inner
|
|
458
|
-
* lists; a dictionary is a <code>Map</code>. Tokens
|
|
459
|
-
* come back as <code>SfToken</code> /
|
|
460
|
-
*
|
|
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
|
|
461
516
|
* the grammar — integer / decimal digit caps, printable-ASCII strings,
|
|
462
517
|
* canonical base64, no trailing characters — and throws on any malformed
|
|
463
518
|
* input (pass <code>opts.ErrorClass</code> for a typed error).
|
|
@@ -493,10 +548,31 @@ function _serDecimal(v, E) {
|
|
|
493
548
|
if (s.indexOf(".") === -1) s += ".0"; // a Decimal must carry a fractional part
|
|
494
549
|
return s;
|
|
495
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
|
+
}
|
|
496
567
|
function _serBareItem(v, E) {
|
|
497
568
|
if (v === true) return "?1";
|
|
498
569
|
if (v === false) return "?0";
|
|
499
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);
|
|
500
576
|
if (typeof v === "number") {
|
|
501
577
|
if (!isFinite(v)) throw E("structured-fields/serialize", "cannot serialize a non-finite number");
|
|
502
578
|
if (Number.isInteger(v)) {
|
|
@@ -603,4 +679,6 @@ module.exports = {
|
|
|
603
679
|
Token: SfToken,
|
|
604
680
|
ByteSequence: SfByteSequence,
|
|
605
681
|
Decimal: SfDecimal,
|
|
682
|
+
Date: SfDate,
|
|
683
|
+
DisplayString: SfDisplayString,
|
|
606
684
|
};
|
package/package.json
CHANGED
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.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:e2f305ce-05d6-4941-bcd8-41d2de6298c4",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-25T21:50:24.224Z",
|
|
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.12.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.12.56",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.12.
|
|
25
|
+
"version": "0.12.56",
|
|
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.12.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.12.56",
|
|
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.12.
|
|
57
|
+
"ref": "@blamejs/core@0.12.56",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|