@blamejs/core 0.12.56 → 0.12.57
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 +2 -0
- package/README.md +1 -0
- package/index.js +2 -0
- package/lib/link-header.js +169 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.12.x
|
|
10
10
|
|
|
11
|
+
- v0.12.57 (2026-05-25) — **`b.linkHeader` — RFC 8288 Web Linking (HTTP Link header) codec.** Parse and build the HTTP Link header (RFC 8288) — the standard way to convey resource relations, most visibly REST pagination (Link: <…?page=2>; rel="next"). b.linkHeader.parse returns one { uri, rel, params } per link, splitting multiple links on top-level commas and unwrapping quoted parameter values via the framework's RFC 8941 structured-field helpers, so a comma inside a quoted title never fake-splits the list; rel is exposed as its array of space-separated relation types. b.linkHeader.serialize is the inverse, angle-bracketing the URI, emitting rel first, and double-quoting parameter values. Verified against the RFC 8288 §3.5 examples and GitHub-style pagination links. **Added:** *`b.linkHeader.parse(headerValue)` / `b.linkHeader.serialize(links)`* — `parse` reads an HTTP Link header into `[{ uri, rel, params }]` — `uri` is the angle-bracketed target, `rel` the array of space-separated relation types, `params` the remaining parameters with quoted values unwrapped (a repeated parameter keeps the first occurrence per RFC 8288 §3.4). It refuses a link without a bracketed URI, an unterminated URI or quoted parameter, control bytes, and oversized headers. `serialize` builds the header value from `{ uri, rel, params? }` objects (or a single one), angle-bracketing the URI and double-quoting parameter values. Composes the framework's structured-field splitter so quoting is handled consistently; pair with `b.pagination` to emit standard `rel="next"` / `rel="prev"` navigation.
|
|
12
|
+
|
|
11
13
|
- 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
14
|
|
|
13
15
|
- 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.
|
package/README.md
CHANGED
|
@@ -98,6 +98,7 @@ 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
|
+
- **Link header** — RFC 8288 Web Linking codec (`b.linkHeader.parse` / `serialize`): parse and build `Link: <uri>; rel="next"` relations, the standard REST pagination mechanism; quote-aware (a comma inside a quoted parameter never splits the list)
|
|
101
102
|
- **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
103
|
- **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
|
|
103
104
|
- **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`)
|
package/index.js
CHANGED
|
@@ -397,6 +397,7 @@ var importmapIntegrity = require("./lib/importmap-integrity");
|
|
|
397
397
|
var privacyPass = require("./lib/privacy-pass");
|
|
398
398
|
var contentDigest = require("./lib/content-digest");
|
|
399
399
|
var canonicalJson = require("./lib/canonical-json");
|
|
400
|
+
var linkHeader = require("./lib/link-header");
|
|
400
401
|
var standardWebhooks = require("./lib/standard-webhooks");
|
|
401
402
|
var lro = require("./lib/lro");
|
|
402
403
|
var jsonApi = require("./lib/jsonapi");
|
|
@@ -415,6 +416,7 @@ module.exports = {
|
|
|
415
416
|
privacyPass: privacyPass,
|
|
416
417
|
contentDigest: contentDigest,
|
|
417
418
|
canonicalJson: canonicalJson,
|
|
419
|
+
linkHeader: linkHeader,
|
|
418
420
|
standardWebhooks: standardWebhooks,
|
|
419
421
|
lro: lro,
|
|
420
422
|
jsonApi: jsonApi,
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.linkHeader
|
|
4
|
+
* @nav HTTP
|
|
5
|
+
* @title Link header
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* Parse and build the HTTP <code>Link</code> header (RFC 8288 Web
|
|
9
|
+
* Linking) — the standard way to convey relations between resources,
|
|
10
|
+
* most visibly REST pagination
|
|
11
|
+
* (<code>Link: <…?page=2>; rel="next"</code>). A header carries
|
|
12
|
+
* one or more comma-separated links, each an angle-bracketed URI
|
|
13
|
+
* reference followed by <code>;</code>-separated parameters
|
|
14
|
+
* (<code>rel</code>, <code>title</code>, <code>type</code>, …).
|
|
15
|
+
*
|
|
16
|
+
* <code>parse</code> returns one object per link with its
|
|
17
|
+
* <code>uri</code>, the (space-split) <code>rel</code> relation types,
|
|
18
|
+
* and the remaining <code>params</code>; <code>serialize</code> is the
|
|
19
|
+
* inverse. The comma split and quoted-parameter unwrapping reuse the
|
|
20
|
+
* framework's RFC 8941 structured-field helpers so a comma inside a
|
|
21
|
+
* quoted <code>title</code> never fake-splits the list.
|
|
22
|
+
*
|
|
23
|
+
* @card
|
|
24
|
+
* HTTP Link header codec (RFC 8288 Web Linking) — parse and build
|
|
25
|
+
* <code>Link: <uri>; rel="next"</code> relations, the standard
|
|
26
|
+
* REST pagination mechanism. Quote-aware so a comma inside a quoted
|
|
27
|
+
* parameter never splits the list.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
var structuredFields = require("./structured-fields");
|
|
31
|
+
var { defineClass } = require("./framework-error");
|
|
32
|
+
|
|
33
|
+
var LinkHeaderError = defineClass("LinkHeaderError", { alwaysPermanent: true });
|
|
34
|
+
|
|
35
|
+
var MAX_HEADER_BYTES = 16384; // allow:raw-byte-literal — defensive cap on a parsed Link header
|
|
36
|
+
|
|
37
|
+
// Split a Link header on the commas that separate links — those OUTSIDE
|
|
38
|
+
// a <uri-reference> and outside a quoted-string. structuredFields'
|
|
39
|
+
// splitter is quote-aware but not angle-bracket-aware, so a comma inside
|
|
40
|
+
// a URI (`<https://x/a,b>`) must not split the list (RFC 8288 §3.5).
|
|
41
|
+
function _splitLinks(s) {
|
|
42
|
+
var out = [], start = 0, inUri = false, inQuote = false, esc = false;
|
|
43
|
+
for (var i = 0; i < s.length; i += 1) {
|
|
44
|
+
var c = s.charAt(i);
|
|
45
|
+
if (esc) { esc = false; continue; }
|
|
46
|
+
if (inQuote) { if (c === "\\") esc = true; else if (c === "\"") inQuote = false; continue; }
|
|
47
|
+
if (c === "\"") { inQuote = true; }
|
|
48
|
+
else if (c === "<") { inUri = true; }
|
|
49
|
+
else if (c === ">") { inUri = false; }
|
|
50
|
+
else if (c === "," && !inUri) { out.push(s.slice(start, i)); start = i + 1; }
|
|
51
|
+
}
|
|
52
|
+
out.push(s.slice(start));
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @primitive b.linkHeader.parse
|
|
58
|
+
* @signature b.linkHeader.parse(headerValue)
|
|
59
|
+
* @since 0.12.57
|
|
60
|
+
* @status stable
|
|
61
|
+
* @related b.linkHeader.serialize, b.pagination.cursor
|
|
62
|
+
*
|
|
63
|
+
* Parse an HTTP <code>Link</code> header value (RFC 8288) into an array
|
|
64
|
+
* of <code>{ uri, rel, params }</code> — one per link. <code>uri</code>
|
|
65
|
+
* is the angle-bracketed target, <code>rel</code> is the array of
|
|
66
|
+
* (space-separated) relation types, and <code>params</code> is the
|
|
67
|
+
* remaining parameters with quoted values unwrapped. A comma inside a
|
|
68
|
+
* quoted parameter value does not split the list. A link without a
|
|
69
|
+
* bracketed URI is refused.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* b.linkHeader.parse('<https://api/x?page=2>; rel="next", <https://api/x?page=9>; rel="last"');
|
|
73
|
+
* // → [ { uri: "https://api/x?page=2", rel: ["next"], params: {} },
|
|
74
|
+
* // { uri: "https://api/x?page=9", rel: ["last"], params: {} } ]
|
|
75
|
+
*/
|
|
76
|
+
function parse(headerValue) {
|
|
77
|
+
if (typeof headerValue !== "string") throw new LinkHeaderError("link-header/bad-input", "linkHeader.parse: headerValue must be a string");
|
|
78
|
+
if (headerValue.length > MAX_HEADER_BYTES) throw new LinkHeaderError("link-header/too-large", "linkHeader.parse: Link header exceeds " + MAX_HEADER_BYTES + " bytes");
|
|
79
|
+
structuredFields.refuseControlBytes(headerValue, { ErrorClass: LinkHeaderError, code: "link-header/bad-input", label: "Link header" });
|
|
80
|
+
var out = [];
|
|
81
|
+
var members = _splitLinks(headerValue);
|
|
82
|
+
for (var i = 0; i < members.length; i++) {
|
|
83
|
+
var raw = members[i].trim();
|
|
84
|
+
if (raw === "") continue;
|
|
85
|
+
if (raw.charAt(0) !== "<") throw new LinkHeaderError("link-header/bad-link", "linkHeader.parse: link must start with a <uri-reference>");
|
|
86
|
+
var close = raw.indexOf(">");
|
|
87
|
+
if (close === -1) throw new LinkHeaderError("link-header/bad-link", "linkHeader.parse: unterminated <uri-reference>");
|
|
88
|
+
var uri = raw.slice(1, close);
|
|
89
|
+
var rest = raw.slice(close + 1);
|
|
90
|
+
var paramParts = structuredFields.splitTopLevel(rest, ";");
|
|
91
|
+
var rel = [], relSet = false, params = Object.create(null);
|
|
92
|
+
for (var p = 0; p < paramParts.length; p++) {
|
|
93
|
+
var piece = paramParts[p].trim();
|
|
94
|
+
if (piece === "") continue;
|
|
95
|
+
var eq = piece.indexOf("=");
|
|
96
|
+
var name = (eq === -1 ? piece : piece.slice(0, eq)).trim().toLowerCase();
|
|
97
|
+
if (name === "") continue;
|
|
98
|
+
var value = eq === -1 ? "" : structuredFields.unquoteSfString(piece.slice(eq + 1).trim());
|
|
99
|
+
if (value === null) throw new LinkHeaderError("link-header/bad-link", "linkHeader.parse: unterminated quoted parameter on '" + name + "'");
|
|
100
|
+
// RFC 8288 §3.3 / §3.4: a repeated rel (or any parameter) keeps the
|
|
101
|
+
// FIRST occurrence; later ones are ignored.
|
|
102
|
+
if (name === "rel") { if (!relSet) { rel = value.split(/\s+/).filter(Boolean); relSet = true; } continue; }
|
|
103
|
+
if (!(name in params)) params[name] = value;
|
|
104
|
+
}
|
|
105
|
+
out.push({ uri: uri, rel: rel, params: params });
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Quote every parameter value (always valid RFC 8288, and required for
|
|
111
|
+
// space-separated multi-rel and non-token values like "text/html"); the
|
|
112
|
+
// common convention (RFC 8288 examples, REST pagination) quotes too.
|
|
113
|
+
function _serParam(name, value) {
|
|
114
|
+
if (value === "" || value === true) return name; // valueless parameter
|
|
115
|
+
return name + "=\"" + String(value).replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + "\"";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @primitive b.linkHeader.serialize
|
|
120
|
+
* @signature b.linkHeader.serialize(links)
|
|
121
|
+
* @since 0.12.57
|
|
122
|
+
* @status stable
|
|
123
|
+
* @related b.linkHeader.parse
|
|
124
|
+
*
|
|
125
|
+
* Build an HTTP <code>Link</code> header value from an array of
|
|
126
|
+
* <code>{ uri, rel, params? }</code> (or a single such object). The URI
|
|
127
|
+
* is angle-bracketed, <code>rel</code> (string or array) is emitted
|
|
128
|
+
* first, and parameters are token-valued when they fit RFC 7230 token
|
|
129
|
+
* grammar or double-quoted otherwise. Useful for emitting standard REST
|
|
130
|
+
* pagination links.
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* b.linkHeader.serialize([
|
|
134
|
+
* { uri: "https://api/x?page=2", rel: "next" },
|
|
135
|
+
* { uri: "https://api/x?page=9", rel: "last", params: { title: "end" } },
|
|
136
|
+
* ]);
|
|
137
|
+
* // → '<https://api/x?page=2>; rel="next", <https://api/x?page=9>; rel="last"; title="end"'
|
|
138
|
+
*/
|
|
139
|
+
function serialize(links) {
|
|
140
|
+
var arr = Array.isArray(links) ? links : [links];
|
|
141
|
+
var parts = [];
|
|
142
|
+
for (var i = 0; i < arr.length; i++) {
|
|
143
|
+
var link = arr[i];
|
|
144
|
+
if (!link || typeof link !== "object" || typeof link.uri !== "string" || link.uri === "") {
|
|
145
|
+
throw new LinkHeaderError("link-header/bad-link", "linkHeader.serialize: links[" + i + "] requires a non-empty uri");
|
|
146
|
+
}
|
|
147
|
+
if (link.uri.indexOf(">") !== -1 || link.uri.indexOf("<") !== -1) {
|
|
148
|
+
throw new LinkHeaderError("link-header/bad-link", "linkHeader.serialize: uri must not contain angle brackets");
|
|
149
|
+
}
|
|
150
|
+
var seg = "<" + link.uri + ">";
|
|
151
|
+
var rel = Array.isArray(link.rel) ? link.rel.join(" ") : (link.rel || "");
|
|
152
|
+
if (rel !== "") seg += "; " + _serParam("rel", rel);
|
|
153
|
+
if (link.params && typeof link.params === "object") {
|
|
154
|
+
var keys = Object.keys(link.params);
|
|
155
|
+
for (var k = 0; k < keys.length; k++) {
|
|
156
|
+
if (keys[k].toLowerCase() === "rel") continue; // rel is emitted from link.rel
|
|
157
|
+
seg += "; " + _serParam(keys[k].toLowerCase(), link.params[keys[k]]);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
parts.push(seg);
|
|
161
|
+
}
|
|
162
|
+
return parts.join(", ");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = {
|
|
166
|
+
parse: parse,
|
|
167
|
+
serialize: serialize,
|
|
168
|
+
LinkHeaderError: LinkHeaderError,
|
|
169
|
+
};
|
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:a145d843-4618-44c4-8e2b-082f70687064",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-25T23:07:17.164Z",
|
|
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.57",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.12.
|
|
25
|
+
"version": "0.12.57",
|
|
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.57",
|
|
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.57",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|