@blamejs/core 0.12.30 → 0.12.32
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 -0
- package/index.js +2 -0
- package/lib/auth/jar.js +168 -0
- package/lib/cbor.js +478 -0
- 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.32 (2026-05-24) — **`b.cbor` — bounded, deterministic in-tree CBOR codec (RFC 8949).** CBOR is the binary serialization underneath COSE (RFC 9052), CWT, SCITT, and WebAuthn attestation — a foundational substrate the framework needs in-tree to build signed-statement primitives without a third-party parser. `b.cbor` is that codec, bounded by default like every parser the framework ships: a binary decoder is attack surface, so the defaults refuse the shapes a hostile encoder uses to exhaust memory or stack. The encoder emits Deterministically Encoded CBOR (RFC 8949 §4.2) — shortest-form heads, definite lengths, map keys sorted by encoded bytes, no indefinite-length items — so two semantically-equal values encode to byte-identical output, the property COSE signatures and SCITT receipts depend on. **Added:** *`b.cbor.encode(value, opts?)` / `b.cbor.decode(buffer, opts?)` / `b.cbor.Tag`* — `encode` produces deterministic CBOR from numbers (integers + float64), bigint (64-bit range), strings, `Buffer` / `Uint8Array`, arrays, `Map` or plain objects, `b.cbor.Tag`, and the simple values. `decode` returns the value with maps decoded to a `Map` (CBOR keys may be integers — COSE header labels are) and byte strings to `Buffer`. `b.cbor.Tag(tag, value)` carries a major-type-6 tagged item. `decode(buf, { requireDeterministic: true })` additionally asserts the input was itself canonically encoded (decode → re-encode → byte-compare), refusing a non-canonical re-encoding on a signature-verify path where it would be a malleability vector. **Security:** *Bounded-by-default decoder* — `maxDepth` (default 64, ceiling 256) caps nesting against stack exhaustion; `maxBytes` (default 16 MiB, ceiling 64 MiB) caps total input, and a declared string / array / map length exceeding the remaining bytes is refused before any allocation (no length-prefix memory bomb). Indefinite-length items (additional-info 31) are refused — a streaming-complexity / DoS vector forbidden by deterministic encoding. Reserved additional-info (28–30) is refused. Tags are refused unless allowlisted via `allowedTags` (a tag triggers semantic reprocessing — an un-vetted tag is a confused-deputy vector). Duplicate map keys (RFC 8949 §5.6) and trailing bytes after the data item are refused.
|
|
12
|
+
|
|
13
|
+
- v0.12.31 (2026-05-24) — **`b.auth.jar.parse` — verify RFC 9101 JWT-Secured Authorization Requests (server side).** A plain OAuth authorization request carries its parameters in the URL query string, where a browser, proxy, or referer log can tamper with or leak them. RFC 9101 JAR packs those parameters into a JWT the client signs — the request object — so the authorization server can confirm they arrived exactly as sent. `b.auth.jar.parse(jar, opts)` is the server-side verifier and the request-side counterpart to the existing JARM response handling (`b.auth.oauth.parseJarmResponse`). It delegates the signature check to `b.auth.jwt.verifyExternal` — which already enforces a mandatory `algorithms` allowlist and refuses the alg-confusion (`alg: "none"`, HMAC-vs-RSA) and JWE-on-a-JWS-verifier shapes against a JWKS public-key trust source — then pins `iss` and the `client_id` claim to the expected client, pins `aud` to this server's issuer identifier, refuses a nested `request` / `request_uri` (RFC 9101 §6.3 recursion / confused-deputy vector), and returns the authorization parameters with the JWT envelope claims stripped. **Added:** *`b.auth.jar.parse(jar, opts)` — request-object verification* — `opts.clientId` (the expected client — pins `iss` + the `client_id` claim), `opts.audience` (this server's issuer identifier — pins `aud`), `opts.algorithms` (required signature allowlist — no defaults, the alg-confusion defense), and one of `opts.jwks` / `opts.jwksUri` / `opts.keyResolver` (the client's verification key). Returns `{ params, claims }` where `params` is the authorization parameters (`response_type`, `redirect_uri`, `scope`, `state`, `nonce`, …) with the JWT envelope claims (`iss`, `aud`, `exp`, `iat`, `nbf`, `jti`) removed. A request object whose `client_id` claim disagrees with `opts.clientId`, or that nests a `request` / `request_uri`, is refused. Emitting a request object (the client side) is deferred-with-condition: it requires signing with the client's key under a classical JWS algorithm, and the framework's own JWT signer is PQC-only for the tokens it issues — a PQC-signed request object would not interoperate with a standard authorization server; client-side emission re-opens when a classical JWS signer lands or operators surface the need. Until then clients sign request objects with their existing JOSE tooling.
|
|
14
|
+
|
|
11
15
|
- v0.12.30 (2026-05-24) — **`bundleAdapterStorage.keyRotation(opts)` — verified whole-repository envelope key rotation.** Rotating the key that wraps a backup repository is only safe if you can prove every bundle still reads under the new key — a rotation that silently corrupts one bundle is a time-bomb the operator discovers at restore time, exactly when they can least afford it. `storage.keyRotation(opts)` rotates every bundle's envelope from the old key to the new key (composing `rewrapAllBundles`) and then re-reads every bundle under the NEW key (composing `verifyAllBundles`), so a bad rotation surfaces as `verifyFailed > 0` immediately instead of at restore. It emits a `backup/key-rotated` audit event with the rotation id + per-status counts — a key-rotation event is a compliance record (SOC 2 CC6.1, PCI DSS 3.6.4) operators wire into their signed audit chain. Works for both `recipient` (hybrid PQC envelope) and `passphrase` (Argon2id) storage; refused cleanly on plaintext (`cryptoStrategy: "none"`) storage and when the new key is missing. **Added:** *`bundleAdapterStorage.keyRotation(opts)` — rotate then prove* — `opts.newRecipient` / `opts.newPassphrase` is the key bundles rotate TO (matched to the storage's `cryptoStrategy`); `opts.oldRecipient` / `opts.oldPassphrase` unwraps the current envelope when it differs from the configured key. Returns `{ rotationId, rotatedAt, total, rotated, skipped, failed, verified, verifyFailed, rotateResults, verifyResults }`. `opts.verify` (default true) runs the post-rotation read-back under the new key; `opts.concurrency` / `opts.stopOnFirstFailure` forward to the batch passes. Plaintext bundles + non-wrappable formats are skipped cleanly; a rotation that leaves any bundle unreadable reports `verifyFailed > 0` and emits the audit event with `outcome: "failure"`. A true overlap window where BOTH the old and new key decrypt a bundle (`dualWrap: true`) is refused with `backup/dual-wrap-unsupported` — it needs multi-recipient archive envelopes `b.archive.wrap` does not yet emit, and re-opens when the wrap layer gains them; until then stage a rotation by keeping the old key available to readers until `keyRotation` reports `failed: 0` + `verifyFailed: 0`, then retire it.
|
|
12
16
|
|
|
13
17
|
- v0.12.29 (2026-05-24) — **`b.ai.dp` — float-safe differential privacy: snapping-mechanism Laplace + discrete Gaussian + Rényi-DP budgets.** Differential privacy adds calibrated noise so an aggregate is provably insensitive to any single record — but the guarantee is fragile: Mironov (2012) showed that a Laplace mechanism sampled with naive double-precision floats lets an attacker distinguish neighbouring datasets with > 35% probability from a single output, silently destroying the promise. `b.ai.dp` ships only mechanisms whose sampling is hardened against that attack class: Laplace via the snapping mechanism (clamp + CSPRNG sign + full-mantissa uniform + power-of-two-grid rounding) and the discrete Gaussian (Canonne–Kamath–Steinke 2020) via integer-exact rejection sampling built from Bernoulli(exp(−γ)) over exact rationals — no floating-point noise at all. All randomness comes from `b.crypto.generateBytes` (SHAKE256 over the OS CSPRNG), never `Math.random`. `b.ai.dp.budget({ scope, epsilon, delta })` tracks a privacy budget per scope and refuses a `consume` that would exceed it, accounting composition either by basic summation (default) or a Rényi-DP accountant (Mironov 2017) for a much tighter bound under repeated Gaussian releases. NIST SP 800-226 (2025) is the evaluation standard; Dwork & Roth is the canonical reference. The exponential and sparse-vector mechanisms are deferred-with-condition — their float-safe constructions (base-2 / permute-and-flip; snapped SVT) re-open on operator demand, since shipping them float-unsafe would defeat the module's purpose. **Added:** *`b.ai.dp.mechanism({ type, sensitivity, epsilon, ... })` — float-safe noise mechanisms* — `type: "laplace"` is the snapping mechanism (pure ε-DP, real-valued, requires a clamp `bound` the guarantee depends on); `type: "gaussian"` is the discrete Gaussian (integer-valued, (ε, δ)-DP, requires `delta`). The Gaussian uses the classic calibration σ = √(2 ln(1.25/δ))·Δ/ε, proven for ε ≤ 1 — larger ε is refused with a pointer to splitting the release under an rdp budget. Descriptors are validated + frozen at construction so a malformed parameter fails fast. · *`b.ai.dp.budget({ scope, epsilon, delta, accounting })` — per-scope privacy budget* — Returns `{ consume, remaining, spent, reset }`. `consume(mechanism, value)` adds the mechanism's noise, charges the accountant, and throws `aiDp/budget-exhausted` if the release would push the scope past its (ε, δ). `accounting: "basic"` (default) sums per-release ε and δ; `accounting: "rdp"` runs a Rényi-DP accountant across a grid of orders and converts to (ε, δ) at the scope's δ for a tight composition bound under repeated Gaussian releases (requires `delta > 0`). The scope budget is enforced on both ε and δ independently. **Security:** *`b.crypto.generateBytes` uniformity fix at 1-byte length* — Node's SHAKE256 XOF is non-uniform at `outputLength: 1` — the byte values 0x00 and 0xff never occur and the low bit skews to ~0.54. `b.crypto.generateBytes(1)` (and the underlying `random(1)`) now draws at least 2 bytes and slices, so a single-byte CSPRNG request is uniform. Surfaced by `b.ai.dp` per-byte noise sampling; any per-byte consumer of `generateBytes` inherits the fix. A regression test asserts 0x00 / 0xff occur and the low bit is balanced.
|
package/README.md
CHANGED
|
@@ -75,6 +75,7 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
|
|
|
75
75
|
- RP-Initiated / Front-Channel / Back-Channel Logout 1.0 (`parseFrontchannelLogoutRequest` + `verifyBackchannelLogoutToken` with jti-replay defense)
|
|
76
76
|
- RFC 9207 AS Issuer Identifier validation on callbacks (`parseCallback` — refuses iss mismatch + OP `error=` redirect)
|
|
77
77
|
- OAuth 2.0 JARM signed-response decode (`parseJarmResponse`)
|
|
78
|
+
- RFC 9101 JWT-Secured Authorization Request verification — server-side request-object parse with mandatory alg allowlist + iss/client_id/aud binding + anti-nesting (`b.auth.jar.parse`)
|
|
78
79
|
- One-time-use refresh-token rotation with operator-supplied replay-defense callback (RFC 9700 §4.13 / OAuth 2.1 §6.1 — `refreshAccessToken({ seen })`)
|
|
79
80
|
- **Federation / VC** — CIBA Core 1.0 (`b.auth.ciba`, poll/ping/push); OpenID Federation 1.0 trust chain + metadata_policy (`b.auth.openidFederation`); SAML 2.0 SP with XMLDSig signature-wrapping defense + RFC 9525 server-identity (`b.auth.saml`); OpenID4VCI 1.0 issuer (`b.auth.oid4vci`); OpenID4VP 1.0 verifier with DCQL (`b.auth.oid4vp`); SD-JWT VC with `key_attestation` extension (`b.auth.sdJwtVc`)
|
|
80
81
|
- **Sessions** — `b.session`
|
|
@@ -124,6 +125,7 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
|
|
|
124
125
|
|
|
125
126
|
- **JSON / SQL / schema** — `b.safeJson` (with `maxKeys` cap defending CVE-2026-21717 V8 HashDoS), `b.safeBuffer`, `b.safeSql`, `b.safeSchema`
|
|
126
127
|
- **URL + path** — `b.safeUrl` (IDN mixed-script / homograph refuse); `b.safeJsonPath` (refuses filter `?(...)`, deep-scan `$..`, script-shape `(@.x)` for safe Postgres JSONB ops)
|
|
128
|
+
- **Binary codec** — `b.cbor` bounded deterministic CBOR (RFC 8949 §4.2): depth/size caps, indefinite-length + reserved-info + tag + duplicate-key refusal, `requireDeterministic` canonical-form check; the in-tree substrate under COSE / CWT / SCITT / WebAuthn attestation
|
|
127
129
|
- **Document parsers** — `b.parsers` (XML / TOML / YAML / .env); `b.config` (schema-validated env)
|
|
128
130
|
- **File-type detection** — `b.fileType` magic-byte content classification with deny-on-upload categories (image / document / archive / executable / etc.)
|
|
129
131
|
### Content-safety gates
|
package/index.js
CHANGED
|
@@ -243,6 +243,7 @@ var auth = {
|
|
|
243
243
|
require("./lib/auth/jwt"),
|
|
244
244
|
{ verifyExternal: require("./lib/auth/jwt-external").verifyExternal }),
|
|
245
245
|
oauth: require("./lib/auth/oauth"),
|
|
246
|
+
jar: require("./lib/auth/jar"),
|
|
246
247
|
lockout: require("./lib/auth/lockout"),
|
|
247
248
|
dpop: require("./lib/auth/dpop"),
|
|
248
249
|
aal: require("./lib/auth/aal"),
|
|
@@ -454,6 +455,7 @@ module.exports = {
|
|
|
454
455
|
// b.jose.jwe.experimental — see lib/jose-jwe-experimental.js for
|
|
455
456
|
// the codepoint-stability contract.
|
|
456
457
|
jose: { jwe: { experimental: require("./lib/jose-jwe-experimental") } },
|
|
458
|
+
cbor: require("./lib/cbor"),
|
|
457
459
|
queue: queue,
|
|
458
460
|
logStream: logStream,
|
|
459
461
|
redact: redact,
|
package/lib/auth/jar.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.auth.jar
|
|
4
|
+
* @nav Identity
|
|
5
|
+
* @title JWT-Secured Authorization Request (JAR)
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* RFC 9101 JWT-Secured Authorization Request — the authorization-
|
|
9
|
+
* server side of the request object, the counterpart to the JARM
|
|
10
|
+
* response handling in <code>b.auth.oauth</code>. A plain OAuth
|
|
11
|
+
* authorization request passes its parameters as URL query string,
|
|
12
|
+
* where they can be tampered with in the browser or leaked into
|
|
13
|
+
* proxy / referer logs. JAR packs the parameters into a JWT signed
|
|
14
|
+
* by the client (the "request object") so the authorization server
|
|
15
|
+
* can verify they arrived exactly as the client sent them.
|
|
16
|
+
*
|
|
17
|
+
* <code>b.auth.jar.parse(jar, opts)</code> verifies an incoming
|
|
18
|
+
* request object: the signature is checked through
|
|
19
|
+
* <code>b.auth.jwt.verifyExternal</code> (mandatory <code>algorithms</code>
|
|
20
|
+
* allowlist — no <code>alg: "none"</code>, no HMAC-vs-RSA confusion,
|
|
21
|
+
* no JWE-on-a-JWS-verifier), <code>iss</code> is pinned to the
|
|
22
|
+
* expected <code>clientId</code>, <code>aud</code> to this server's
|
|
23
|
+
* issuer identifier, the request object's <code>client_id</code>
|
|
24
|
+
* claim must match the client, and the authorization parameters are
|
|
25
|
+
* returned with the JWT envelope claims stripped.
|
|
26
|
+
*
|
|
27
|
+
* <strong>Anti-nesting (RFC 9101 §6.3):</strong> a request object
|
|
28
|
+
* may not itself carry a <code>request</code> or <code>request_uri</code>
|
|
29
|
+
* parameter — <code>parse</code> refuses it, closing the recursion /
|
|
30
|
+
* confused-deputy vector.
|
|
31
|
+
*
|
|
32
|
+
* The signature verification — the security-critical step — is
|
|
33
|
+
* delegated to <code>verifyExternal</code>, which already enforces
|
|
34
|
+
* the alg allowlist and refuses the alg-confusion / JWE-bypass
|
|
35
|
+
* shapes against a JWKS public-key trust source. JAR adds the
|
|
36
|
+
* request-object-specific bindings on top.
|
|
37
|
+
*
|
|
38
|
+
* <strong>Emitting</strong> a request object (the client side) is
|
|
39
|
+
* deferred-with-condition: it requires signing with the client's
|
|
40
|
+
* key under a classical JWS algorithm (RS256 / ES256 / EdDSA), and
|
|
41
|
+
* the framework's own JWT signer (<code>b.auth.jwt.sign</code>) is
|
|
42
|
+
* PQC-only (ML-DSA / SLH-DSA) for the tokens the framework itself
|
|
43
|
+
* issues — a PQC-signed request object would not interoperate with
|
|
44
|
+
* any standard authorization server today. blamejs sits on the
|
|
45
|
+
* authorization-server side here (it verifies client request
|
|
46
|
+
* objects); client-side emission re-opens when a classical
|
|
47
|
+
* <code>b.auth.jws.sign</code> primitive lands or operators surface
|
|
48
|
+
* the need. Until then clients sign their request objects with
|
|
49
|
+
* their existing JOSE tooling.
|
|
50
|
+
*
|
|
51
|
+
* @card
|
|
52
|
+
* RFC 9101 JWT-Secured Authorization Request (server side) — verify
|
|
53
|
+
* the OAuth request object with mandatory alg allowlist, iss +
|
|
54
|
+
* client_id binding, audience pinning, and anti-nesting.
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
var jwtExternal = require("./jwt-external");
|
|
58
|
+
var validateOpts = require("../validate-opts");
|
|
59
|
+
var { defineClass } = require("../framework-error");
|
|
60
|
+
|
|
61
|
+
var AuthJarError = defineClass("AuthJarError", { alwaysPermanent: true });
|
|
62
|
+
|
|
63
|
+
var JAR_TYP = "oauth-authz-req+jwt";
|
|
64
|
+
|
|
65
|
+
// JWT-standard claims that are request-object envelope metadata, not
|
|
66
|
+
// OAuth authorization parameters — stripped from the returned params.
|
|
67
|
+
var ENVELOPE_CLAIMS = ["iss", "aud", "exp", "iat", "nbf", "jti"];
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @primitive b.auth.jar.parse
|
|
71
|
+
* @signature b.auth.jar.parse(jar, opts)
|
|
72
|
+
* @since 0.12.31
|
|
73
|
+
* @status stable
|
|
74
|
+
* @compliance soc2
|
|
75
|
+
* @related b.auth.oauth.parseJarmResponse
|
|
76
|
+
*
|
|
77
|
+
* Verify an RFC 9101 request object and return its authorization
|
|
78
|
+
* parameters. The signature is checked via
|
|
79
|
+
* <code>b.auth.jwt.verifyExternal</code> (mandatory <code>algorithms</code>
|
|
80
|
+
* allowlist), <code>iss</code> is pinned to <code>opts.clientId</code>,
|
|
81
|
+
* <code>aud</code> to <code>opts.audience</code>, and the request
|
|
82
|
+
* object's <code>client_id</code> claim must equal
|
|
83
|
+
* <code>opts.clientId</code>. A request object carrying a nested
|
|
84
|
+
* <code>request</code> / <code>request_uri</code> is refused
|
|
85
|
+
* (RFC 9101 §6.3). Returns <code>{ params, claims }</code> where
|
|
86
|
+
* <code>params</code> is the authorization parameters with the JWT
|
|
87
|
+
* envelope claims removed.
|
|
88
|
+
*
|
|
89
|
+
* @opts
|
|
90
|
+
* {
|
|
91
|
+
* clientId: string, // required — expected client (iss + client_id pin)
|
|
92
|
+
* audience: string, // required — this server's issuer identifier (aud pin)
|
|
93
|
+
* algorithms: string[], // required — accepted signature algorithms (allowlist)
|
|
94
|
+
* jwks?: object, // one of jwks / jwksUri / keyResolver (the client's key)
|
|
95
|
+
* jwksUri?: string,
|
|
96
|
+
* keyResolver?: function,
|
|
97
|
+
* clockSkewMs?: number,
|
|
98
|
+
* }
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* var out = await b.auth.jar.parse(jar, {
|
|
102
|
+
* clientId: "s6BhdRkqt3",
|
|
103
|
+
* audience: "https://as.example.com",
|
|
104
|
+
* algorithms: ["ES256"],
|
|
105
|
+
* jwks: clientJwks,
|
|
106
|
+
* });
|
|
107
|
+
* // → { params: { response_type: "code", redirect_uri: "...", ... }, claims: {...} }
|
|
108
|
+
*/
|
|
109
|
+
async function parse(jar, opts) {
|
|
110
|
+
if (typeof jar !== "string" || jar.length === 0) {
|
|
111
|
+
throw new AuthJarError("auth-jar/no-jar", "jar.parse: jar must be a non-empty string");
|
|
112
|
+
}
|
|
113
|
+
validateOpts.requireObject(opts, "jar.parse", AuthJarError);
|
|
114
|
+
validateOpts(opts, [
|
|
115
|
+
"clientId", "audience", "algorithms", "jwks", "jwksUri", "keyResolver", "clockSkewMs",
|
|
116
|
+
], "jar.parse");
|
|
117
|
+
validateOpts.requireNonEmptyString(opts.clientId, "jar.parse: clientId", AuthJarError, "auth-jar/bad-client-id");
|
|
118
|
+
validateOpts.requireNonEmptyString(opts.audience, "jar.parse: audience", AuthJarError, "auth-jar/bad-audience");
|
|
119
|
+
|
|
120
|
+
// Delegate signature + alg-allowlist + iss/aud/exp verification to
|
|
121
|
+
// verifyExternal (the hardened JWS verifier). It throws on alg
|
|
122
|
+
// confusion / none / JWE / bad signature / iss / aud / expiry and
|
|
123
|
+
// returns `{ header, claims }`.
|
|
124
|
+
var verified = await jwtExternal.verifyExternal(jar, {
|
|
125
|
+
algorithms: opts.algorithms,
|
|
126
|
+
jwks: opts.jwks,
|
|
127
|
+
jwksUri: opts.jwksUri,
|
|
128
|
+
keyResolver: opts.keyResolver,
|
|
129
|
+
issuer: opts.clientId,
|
|
130
|
+
audience: opts.audience,
|
|
131
|
+
clockSkewMs: opts.clockSkewMs,
|
|
132
|
+
});
|
|
133
|
+
var payload = verified.claims;
|
|
134
|
+
|
|
135
|
+
// RFC 9101 §5.2 — the request object MUST carry a client_id claim,
|
|
136
|
+
// and it MUST match the client. verifyExternal already pinned
|
|
137
|
+
// iss === clientId, but client_id is a distinct REQUIRED claim;
|
|
138
|
+
// accepting its absence would let a JAR pass on the strength of an
|
|
139
|
+
// outer (attacker-controllable) query-param client_id alone, so a
|
|
140
|
+
// missing client_id is refused rather than waved through.
|
|
141
|
+
if (payload.client_id === undefined) {
|
|
142
|
+
throw new AuthJarError("auth-jar/missing-client-id",
|
|
143
|
+
"jar.parse: request object is missing the required client_id claim (RFC 9101 §5.2)");
|
|
144
|
+
}
|
|
145
|
+
if (payload.client_id !== opts.clientId) {
|
|
146
|
+
throw new AuthJarError("auth-jar/client-id-mismatch",
|
|
147
|
+
"jar.parse: request object client_id does not match the expected client");
|
|
148
|
+
}
|
|
149
|
+
// RFC 9101 §6.3 — a request object must not nest another request /
|
|
150
|
+
// request_uri (recursion / confused-deputy vector).
|
|
151
|
+
if (payload.request !== undefined || payload.request_uri !== undefined) {
|
|
152
|
+
throw new AuthJarError("auth-jar/nested-request",
|
|
153
|
+
"jar.parse: request object must not carry `request` or `request_uri` (RFC 9101 §6.3)");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
var params = {};
|
|
157
|
+
var keys = Object.keys(payload);
|
|
158
|
+
for (var i = 0; i < keys.length; i++) {
|
|
159
|
+
if (ENVELOPE_CLAIMS.indexOf(keys[i]) === -1) params[keys[i]] = payload[keys[i]];
|
|
160
|
+
}
|
|
161
|
+
return { params: params, claims: payload };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = {
|
|
165
|
+
parse: parse,
|
|
166
|
+
JAR_TYP: JAR_TYP,
|
|
167
|
+
AuthJarError: AuthJarError,
|
|
168
|
+
};
|
package/lib/cbor.js
ADDED
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.cbor
|
|
4
|
+
* @nav Tools
|
|
5
|
+
* @title CBOR codec
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* A bounded, deterministic CBOR codec (RFC 8949). CBOR is the binary
|
|
9
|
+
* serialization underneath COSE (RFC 9052), CWT, SCITT, and WebAuthn
|
|
10
|
+
* attestation — a foundational substrate the framework needs in-tree
|
|
11
|
+
* to build signed-statement primitives without a third-party parser.
|
|
12
|
+
* Like every parser the framework ships, it is bounded by default:
|
|
13
|
+
* a binary decoder is attack surface, so the defaults refuse the
|
|
14
|
+
* shapes a hostile encoder uses to exhaust memory or stack.
|
|
15
|
+
*
|
|
16
|
+
* <strong>Decoder defences</strong> (all on by default):
|
|
17
|
+
* - <code>maxDepth</code> — nesting cap (refuses stack exhaustion).
|
|
18
|
+
* - <code>maxBytes</code> — total input cap; a declared string /
|
|
19
|
+
* array / map length that exceeds the remaining bytes is refused
|
|
20
|
+
* before any allocation (no length-prefix memory bomb).
|
|
21
|
+
* - <strong>Indefinite-length items refused</strong> (major-type
|
|
22
|
+
* additional-info 31) — they are a streaming-complexity / DoS
|
|
23
|
+
* vector and are forbidden by deterministic encoding (§4.2.1).
|
|
24
|
+
* - <strong>Reserved additional-info (28–30) refused.</strong>
|
|
25
|
+
* - <strong>Tags refused unless allowlisted</strong>
|
|
26
|
+
* (<code>allowedTags</code>) — a tag triggers semantic
|
|
27
|
+
* reprocessing; an un-vetted tag is a confused-deputy vector.
|
|
28
|
+
* - <strong>Duplicate map keys refused</strong> (§5.6 — ambiguous).
|
|
29
|
+
* - <strong>Trailing bytes refused</strong> — the buffer must be
|
|
30
|
+
* exactly one CBOR data item.
|
|
31
|
+
*
|
|
32
|
+
* <strong>Encoder</strong> emits Deterministically Encoded CBOR
|
|
33
|
+
* (§4.2): shortest-form integer / length heads, definite lengths,
|
|
34
|
+
* map keys sorted by their encoded bytes (bytewise lexicographic),
|
|
35
|
+
* no indefinite-length items. Two semantically equal values encode
|
|
36
|
+
* to byte-identical output — the property COSE signatures and SCITT
|
|
37
|
+
* receipts depend on.
|
|
38
|
+
*
|
|
39
|
+
* <code>decode(buf, { requireDeterministic: true })</code> additionally
|
|
40
|
+
* asserts the input was itself deterministically encoded (it decodes,
|
|
41
|
+
* re-encodes, and refuses on any byte difference) — use it on the
|
|
42
|
+
* verify side of a signature where a non-canonical re-encoding would
|
|
43
|
+
* otherwise be a malleability vector.
|
|
44
|
+
*
|
|
45
|
+
* Maps decode to a <code>Map</code> (CBOR map keys may be integers,
|
|
46
|
+
* not just strings — COSE header labels are integers); encode accepts
|
|
47
|
+
* a <code>Map</code> or a plain object (string keys). Tagged items
|
|
48
|
+
* are produced / consumed via <code>b.cbor.Tag</code>.
|
|
49
|
+
*
|
|
50
|
+
* @card
|
|
51
|
+
* Bounded, deterministic in-tree CBOR codec (RFC 8949 §4.2) —
|
|
52
|
+
* depth / size caps, indefinite-length + tag + duplicate-key
|
|
53
|
+
* refusal. The substrate under COSE / CWT / SCITT.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
var C = require("./constants");
|
|
57
|
+
var { defineClass } = require("./framework-error");
|
|
58
|
+
|
|
59
|
+
var CborError = defineClass("CborError", { alwaysPermanent: true });
|
|
60
|
+
|
|
61
|
+
var DEFAULT_MAX_DEPTH = 64; // allow:raw-byte-literal — nesting depth, not a size
|
|
62
|
+
var ABSOLUTE_MAX_DEPTH = 256; // allow:raw-byte-literal — nesting depth ceiling, not a size
|
|
63
|
+
var DEFAULT_MAX_BYTES = C.BYTES.mib(16);
|
|
64
|
+
var ABSOLUTE_MAX_BYTES = C.BYTES.mib(64);
|
|
65
|
+
|
|
66
|
+
// CBOR / IEEE-754 wire constants (not byte sizes — protocol values).
|
|
67
|
+
var CBOR_AI_1BYTE = 24; // allow:raw-byte-literal — RFC 8949 §3 additional-info boundary (inline vs 1-byte argument)
|
|
68
|
+
var BYTES_64BIT = 8; // allow:raw-byte-literal — width of a CBOR uint64 / float64 argument, not a cap
|
|
69
|
+
var FLOAT16_MANT_DIV = 1024; // allow:raw-byte-literal — IEEE 754 half-precision mantissa scale (2^10), not a size
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @primitive b.cbor.Tag
|
|
73
|
+
* @signature b.cbor.Tag(tag, value)
|
|
74
|
+
* @since 0.12.32
|
|
75
|
+
* @status stable
|
|
76
|
+
* @related b.cbor.encode, b.cbor.decode
|
|
77
|
+
*
|
|
78
|
+
* A tagged CBOR item (major type 6) — <code>tag</code> is the
|
|
79
|
+
* non-negative integer tag number, <code>value</code> the tagged
|
|
80
|
+
* content. <code>encode</code> accepts a <code>Tag</code>;
|
|
81
|
+
* <code>decode</code> returns one when the tag number is in
|
|
82
|
+
* <code>allowedTags</code>. Construct with or without <code>new</code>.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* var dt = new b.cbor.Tag(0, "2026-05-24T00:00:00Z"); // RFC 8949 §3.4.1
|
|
86
|
+
* var bytes = b.cbor.encode(dt);
|
|
87
|
+
* var back = b.cbor.decode(bytes, { allowedTags: [0] });
|
|
88
|
+
* // → b.cbor.Tag { tag: 0, value: "2026-05-24T00:00:00Z" }
|
|
89
|
+
*/
|
|
90
|
+
function Tag(tag, value) {
|
|
91
|
+
if (!(this instanceof Tag)) return new Tag(tag, value);
|
|
92
|
+
if (typeof tag !== "number" || !Number.isInteger(tag) || tag < 0) {
|
|
93
|
+
throw new CborError("cbor/bad-tag", "cbor.Tag: tag must be a non-negative integer");
|
|
94
|
+
}
|
|
95
|
+
this.tag = tag;
|
|
96
|
+
this.value = value;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function _capInt(v, dflt, absolute) {
|
|
100
|
+
if (v == null) return dflt;
|
|
101
|
+
if (typeof v !== "number" || !isFinite(v) || v < 1) return dflt;
|
|
102
|
+
var n = Math.floor(v);
|
|
103
|
+
return n > absolute ? absolute : n;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---- encoder (deterministic, RFC 8949 §4.2) ----
|
|
107
|
+
|
|
108
|
+
// Preferred float serialization (RFC 8949 §4.2.1): the shortest of
|
|
109
|
+
// float16 / float32 / float64 that round-trips the value exactly. COSE
|
|
110
|
+
// + SCITT depend on this — emitting float64 for a value representable
|
|
111
|
+
// in float16 is non-canonical and trips requireDeterministic.
|
|
112
|
+
function _encodeFloat(value) {
|
|
113
|
+
if (Number.isNaN(value)) return Buffer.from([0xf9, 0x7e, 0x00]); // allow:raw-byte-literal — canonical half NaN (RFC 8949 §4.2.1)
|
|
114
|
+
if (value === Infinity) return Buffer.from([0xf9, 0x7c, 0x00]); // allow:raw-byte-literal — half +Inf
|
|
115
|
+
if (value === -Infinity) return Buffer.from([0xf9, 0xfc, 0x00]); // allow:raw-byte-literal — half -Inf
|
|
116
|
+
var half = _doubleToHalfBits(value);
|
|
117
|
+
if (half >= 0) { var hb = Buffer.alloc(3); hb[0] = 0xf9; hb.writeUInt16BE(half, 1); return hb; }
|
|
118
|
+
var f4 = Buffer.alloc(5); f4[0] = 0xfa; f4.writeFloatBE(value, 1);
|
|
119
|
+
if (f4.readFloatBE(1) === value) return f4; // exactly representable in float32
|
|
120
|
+
var f8 = Buffer.alloc(9); f8[0] = 0xfb; f8.writeDoubleBE(value, 1); return f8;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Returns the 16-bit half-precision representation of a FINITE double
|
|
124
|
+
// if it is exactly representable, else -1. Goes via float32: a value
|
|
125
|
+
// not exact in float32 cannot be exact in float16; then the float32
|
|
126
|
+
// exponent must fit the half range and the low 13 mantissa bits must
|
|
127
|
+
// be zero (half has a 10-bit mantissa vs float32's 23).
|
|
128
|
+
function _doubleToHalfBits(value) {
|
|
129
|
+
var fbuf = Buffer.alloc(4);
|
|
130
|
+
fbuf.writeFloatBE(value, 0);
|
|
131
|
+
if (fbuf.readFloatBE(0) !== value) return -1; // not exact in float32 → not in float16
|
|
132
|
+
var f = fbuf.readUInt32BE(0);
|
|
133
|
+
var sign = (f >>> 16) & 0x8000;
|
|
134
|
+
var exp = (f >>> 23) & 0xff;
|
|
135
|
+
var mant = f & 0x7fffff;
|
|
136
|
+
var unbiased = exp - 127 + 15;
|
|
137
|
+
if (unbiased >= 0x1f) return -1; // overflow half's exponent range
|
|
138
|
+
if (unbiased <= 0) {
|
|
139
|
+
// subnormal half (or zero / underflow).
|
|
140
|
+
if (unbiased < -10) return -1; // too small for a half subnormal
|
|
141
|
+
var fullMant = mant | 0x800000; // restore implicit leading 1
|
|
142
|
+
var shift = 14 - unbiased;
|
|
143
|
+
if (fullMant & ((1 << shift) - 1)) return -1; // would drop set bits → inexact
|
|
144
|
+
return sign | (fullMant >>> shift);
|
|
145
|
+
}
|
|
146
|
+
if (mant & 0x1fff) return -1; // low 13 bits set → not exact in half
|
|
147
|
+
return sign | (unbiased << 10) | (mant >>> 13);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function _head(major, argument) {
|
|
151
|
+
// argument is a non-negative integer (Number or BigInt). Emit the
|
|
152
|
+
// shortest form: inline (<24), 1/2/4/8 byte. major is 0..7.
|
|
153
|
+
var mt = major << 5;
|
|
154
|
+
var big = (typeof argument === "bigint") ? argument : BigInt(argument);
|
|
155
|
+
if (big < 24n) return Buffer.from([mt | Number(big)]);
|
|
156
|
+
if (big < 256n) return Buffer.from([mt | 24, Number(big)]);
|
|
157
|
+
if (big < 65536n) {
|
|
158
|
+
var b2 = Buffer.alloc(3); b2[0] = mt | 25; b2.writeUInt16BE(Number(big), 1); return b2;
|
|
159
|
+
}
|
|
160
|
+
if (big < 4294967296n) {
|
|
161
|
+
var b4 = Buffer.alloc(5); b4[0] = mt | 26; b4.writeUInt32BE(Number(big), 1); return b4;
|
|
162
|
+
}
|
|
163
|
+
if (big < 18446744073709551616n) {
|
|
164
|
+
var b8 = Buffer.alloc(9); b8[0] = mt | 27; b8.writeBigUInt64BE(big, 1); return b8;
|
|
165
|
+
}
|
|
166
|
+
throw new CborError("cbor/int-overflow", "cbor.encode: integer exceeds 64-bit CBOR range");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function _encodeValue(value, opts) {
|
|
170
|
+
if (value === null) return Buffer.from([0xf6]); // allow:raw-byte-literal — CBOR null simple value
|
|
171
|
+
if (value === undefined) return Buffer.from([0xf7]); // allow:raw-byte-literal — CBOR undefined simple value
|
|
172
|
+
if (value === true) return Buffer.from([0xf5]); // allow:raw-byte-literal — CBOR true simple value
|
|
173
|
+
if (value === false) return Buffer.from([0xf4]); // allow:raw-byte-literal — CBOR false simple value
|
|
174
|
+
|
|
175
|
+
if (typeof value === "number") {
|
|
176
|
+
// Exact integers within the safe range encode as CBOR integers;
|
|
177
|
+
// an integer-VALUED number beyond 2^53 (e.g. 1e300) has lost
|
|
178
|
+
// integer precision and is a float — encode it as a float (use a
|
|
179
|
+
// bigint for exact 64-bit CBOR integers).
|
|
180
|
+
if (Number.isInteger(value) && Math.abs(value) <= Number.MAX_SAFE_INTEGER) {
|
|
181
|
+
return value >= 0 ? _head(0, value) : _head(1, -1 - value);
|
|
182
|
+
}
|
|
183
|
+
if (!isFinite(value) && !opts.allowNonFinite) {
|
|
184
|
+
throw new CborError("cbor/non-finite", "cbor.encode: NaN / Infinity refused (set allowNonFinite to emit them)");
|
|
185
|
+
}
|
|
186
|
+
return _encodeFloat(value);
|
|
187
|
+
}
|
|
188
|
+
if (typeof value === "bigint") {
|
|
189
|
+
return value >= 0n ? _head(0, value) : _head(1, -1n - value);
|
|
190
|
+
}
|
|
191
|
+
if (typeof value === "string") {
|
|
192
|
+
var u = Buffer.from(value, "utf8");
|
|
193
|
+
return Buffer.concat([_head(3, u.length), u]);
|
|
194
|
+
}
|
|
195
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
|
|
196
|
+
var bs = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
197
|
+
return Buffer.concat([_head(2, bs.length), bs]);
|
|
198
|
+
}
|
|
199
|
+
if (Array.isArray(value)) {
|
|
200
|
+
var parts = [_head(4, value.length)];
|
|
201
|
+
for (var i = 0; i < value.length; i++) parts.push(_encodeValue(value[i], opts));
|
|
202
|
+
return Buffer.concat(parts);
|
|
203
|
+
}
|
|
204
|
+
if (value instanceof Tag) {
|
|
205
|
+
return Buffer.concat([_head(6, value.tag), _encodeValue(value.value, opts)]);
|
|
206
|
+
}
|
|
207
|
+
if (value instanceof Map || (typeof value === "object")) {
|
|
208
|
+
return _encodeMap(value, opts);
|
|
209
|
+
}
|
|
210
|
+
throw new CborError("cbor/unencodable",
|
|
211
|
+
"cbor.encode: value of type " + (typeof value) + " is not CBOR-encodable");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function _encodeMap(value, opts) {
|
|
215
|
+
// Build [encodedKey, encodedValue] pairs, then sort by encoded-key
|
|
216
|
+
// bytes (bytewise lexicographic) per §4.2.1 so the output is
|
|
217
|
+
// deterministic regardless of insertion order.
|
|
218
|
+
var entries = [];
|
|
219
|
+
if (value instanceof Map) {
|
|
220
|
+
value.forEach(function (v, k) { entries.push([_encodeValue(k, opts), _encodeValue(v, opts)]); });
|
|
221
|
+
} else {
|
|
222
|
+
var keys = Object.keys(value);
|
|
223
|
+
for (var i = 0; i < keys.length; i++) {
|
|
224
|
+
entries.push([_encodeValue(keys[i], opts), _encodeValue(value[keys[i]], opts)]);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
entries.sort(function (a, b) { return Buffer.compare(a[0], b[0]); });
|
|
228
|
+
// Reject duplicate keys (equal encoded-key bytes) — ambiguous + a
|
|
229
|
+
// canonical-form violation.
|
|
230
|
+
for (var j = 1; j < entries.length; j++) {
|
|
231
|
+
if (Buffer.compare(entries[j - 1][0], entries[j][0]) === 0) {
|
|
232
|
+
throw new CborError("cbor/duplicate-key", "cbor.encode: duplicate map key");
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
var out = [_head(5, entries.length)];
|
|
236
|
+
for (var k = 0; k < entries.length; k++) { out.push(entries[k][0]); out.push(entries[k][1]); }
|
|
237
|
+
return Buffer.concat(out);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* @primitive b.cbor.encode
|
|
242
|
+
* @signature b.cbor.encode(value, opts?)
|
|
243
|
+
* @since 0.12.32
|
|
244
|
+
* @status stable
|
|
245
|
+
* @related b.cbor.decode, b.cbor.Tag
|
|
246
|
+
*
|
|
247
|
+
* Encode a JavaScript value to Deterministically Encoded CBOR
|
|
248
|
+
* (RFC 8949 §4.2): shortest-form integer / length heads, definite
|
|
249
|
+
* lengths, map keys sorted by their encoded bytes, no indefinite-
|
|
250
|
+
* length items. Two semantically-equal values produce byte-identical
|
|
251
|
+
* output. Accepts numbers (integers + float64), bigint (64-bit
|
|
252
|
+
* range), strings, <code>Buffer</code> / <code>Uint8Array</code>,
|
|
253
|
+
* arrays, <code>Map</code> or plain objects, <code>b.cbor.Tag</code>,
|
|
254
|
+
* and <code>true</code> / <code>false</code> / <code>null</code> /
|
|
255
|
+
* <code>undefined</code>.
|
|
256
|
+
*
|
|
257
|
+
* @opts
|
|
258
|
+
* {
|
|
259
|
+
* allowNonFinite?: boolean, // default false — NaN / Infinity refused
|
|
260
|
+
* }
|
|
261
|
+
*
|
|
262
|
+
* @example
|
|
263
|
+
* b.cbor.encode({ b: 2, a: 1 }).toString("hex"); // → "a2616101616202" (keys sorted)
|
|
264
|
+
*/
|
|
265
|
+
function encode(value, opts) {
|
|
266
|
+
opts = opts || {};
|
|
267
|
+
return _encodeValue(value, opts);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ---- decoder (bounded) ----
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* @primitive b.cbor.decode
|
|
274
|
+
* @signature b.cbor.decode(buffer, opts?)
|
|
275
|
+
* @since 0.12.32
|
|
276
|
+
* @status stable
|
|
277
|
+
* @related b.cbor.encode, b.cbor.Tag
|
|
278
|
+
*
|
|
279
|
+
* Decode one CBOR data item from a buffer, bounded by default. Maps
|
|
280
|
+
* decode to a <code>Map</code> (CBOR keys may be integers); byte
|
|
281
|
+
* strings to <code>Buffer</code>. Refuses indefinite-length items,
|
|
282
|
+
* reserved additional-info (28–30), tags not in
|
|
283
|
+
* <code>allowedTags</code>, duplicate map keys, and trailing bytes.
|
|
284
|
+
*
|
|
285
|
+
* @opts
|
|
286
|
+
* {
|
|
287
|
+
* maxDepth?: number, // default 64, ceiling 256 — nesting cap
|
|
288
|
+
* maxBytes?: number, // default 16 MiB, ceiling 64 MiB
|
|
289
|
+
* allowedTags?: number[], // default [] — tag numbers permitted
|
|
290
|
+
* requireDeterministic?: boolean, // default false — assert canonical encoding
|
|
291
|
+
* }
|
|
292
|
+
*
|
|
293
|
+
* @example
|
|
294
|
+
* var m = b.cbor.decode(bytes, { allowedTags: [0], requireDeterministic: true });
|
|
295
|
+
*/
|
|
296
|
+
function decode(buffer, opts) {
|
|
297
|
+
opts = opts || {};
|
|
298
|
+
if (!Buffer.isBuffer(buffer) && !(buffer instanceof Uint8Array)) {
|
|
299
|
+
throw new CborError("cbor/bad-input", "cbor.decode: input must be a Buffer / Uint8Array");
|
|
300
|
+
}
|
|
301
|
+
var buf = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer);
|
|
302
|
+
var maxBytes = _capInt(opts.maxBytes, DEFAULT_MAX_BYTES, ABSOLUTE_MAX_BYTES);
|
|
303
|
+
if (buf.length > maxBytes) {
|
|
304
|
+
throw new CborError("cbor/too-large",
|
|
305
|
+
"cbor.decode: input " + buf.length + " bytes exceeds maxBytes " + maxBytes);
|
|
306
|
+
}
|
|
307
|
+
var maxDepth = _capInt(opts.maxDepth, DEFAULT_MAX_DEPTH, ABSOLUTE_MAX_DEPTH);
|
|
308
|
+
var allowedTags = Array.isArray(opts.allowedTags) ? opts.allowedTags : [];
|
|
309
|
+
|
|
310
|
+
var state = { buf: buf, pos: 0, maxDepth: maxDepth, allowedTags: allowedTags };
|
|
311
|
+
var value = _decodeItem(state, 0);
|
|
312
|
+
if (state.pos !== buf.length) {
|
|
313
|
+
throw new CborError("cbor/trailing-bytes",
|
|
314
|
+
"cbor.decode: " + (buf.length - state.pos) + " trailing byte(s) after the data item");
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (opts.requireDeterministic === true) {
|
|
318
|
+
// Round-trip: a deterministically-encoded input re-encodes to the
|
|
319
|
+
// identical bytes. Any difference is a non-canonical encoding
|
|
320
|
+
// (long-form head, unsorted keys, indefinite length) — a
|
|
321
|
+
// malleability vector on a signature-verify path.
|
|
322
|
+
var reencoded = _encodeValue(value, {});
|
|
323
|
+
if (Buffer.compare(reencoded, buf) !== 0) {
|
|
324
|
+
throw new CborError("cbor/not-deterministic",
|
|
325
|
+
"cbor.decode: input is not deterministically encoded (requireDeterministic)");
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return value;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function _need(state, n) {
|
|
332
|
+
if (state.pos + n > state.buf.length) {
|
|
333
|
+
throw new CborError("cbor/truncated", "cbor.decode: unexpected end of input");
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function _readArgument(state, ai) {
|
|
338
|
+
// ai is the low-5-bits additional info. Returns the argument as a
|
|
339
|
+
// Number (or BigInt for 8-byte values beyond Number range).
|
|
340
|
+
if (ai < CBOR_AI_1BYTE) return ai;
|
|
341
|
+
if (ai === CBOR_AI_1BYTE) { _need(state, 1); var v1 = state.buf[state.pos]; state.pos += 1; return v1; }
|
|
342
|
+
if (ai === 25) { _need(state, 2); var v2 = state.buf.readUInt16BE(state.pos); state.pos += 2; return v2; }
|
|
343
|
+
if (ai === 26) { _need(state, 4); var v4 = state.buf.readUInt32BE(state.pos); state.pos += 4; return v4; }
|
|
344
|
+
if (ai === 27) {
|
|
345
|
+
_need(state, BYTES_64BIT);
|
|
346
|
+
var big = state.buf.readBigUInt64BE(state.pos); state.pos += BYTES_64BIT;
|
|
347
|
+
return big <= 9007199254740991n ? Number(big) : big; // safe-int → Number, else BigInt
|
|
348
|
+
}
|
|
349
|
+
if (ai === 31) {
|
|
350
|
+
throw new CborError("cbor/indefinite-refused",
|
|
351
|
+
"cbor.decode: indefinite-length items are refused (deterministic-encoding violation)");
|
|
352
|
+
}
|
|
353
|
+
throw new CborError("cbor/reserved-ai",
|
|
354
|
+
"cbor.decode: reserved additional-information value " + ai + " (28-30) refused");
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function _lenOf(arg) {
|
|
358
|
+
// A length / count must be a Number within array bounds — a BigInt
|
|
359
|
+
// length means a >2^53 declared size, which exceeds maxBytes anyway.
|
|
360
|
+
if (typeof arg === "bigint") {
|
|
361
|
+
throw new CborError("cbor/length-too-large", "cbor.decode: declared length exceeds addressable range");
|
|
362
|
+
}
|
|
363
|
+
return arg;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function _decodeItem(state, depth) {
|
|
367
|
+
if (depth > state.maxDepth) {
|
|
368
|
+
throw new CborError("cbor/max-depth", "cbor.decode: nesting exceeds maxDepth " + state.maxDepth);
|
|
369
|
+
}
|
|
370
|
+
_need(state, 1);
|
|
371
|
+
var ib = state.buf[state.pos]; state.pos += 1;
|
|
372
|
+
var major = ib >> 5;
|
|
373
|
+
var ai = ib & 0x1f;
|
|
374
|
+
|
|
375
|
+
switch (major) {
|
|
376
|
+
case 0: return _readArgument(state, ai); // unsigned int
|
|
377
|
+
case 1: { // negative int
|
|
378
|
+
var n = _readArgument(state, ai);
|
|
379
|
+
return (typeof n === "bigint") ? (-1n - n) : (-1 - n);
|
|
380
|
+
}
|
|
381
|
+
case 2: { // byte string
|
|
382
|
+
var blen = _lenOf(_readArgument(state, ai));
|
|
383
|
+
_need(state, blen);
|
|
384
|
+
var bytes = buf_slice(state, blen);
|
|
385
|
+
return bytes;
|
|
386
|
+
}
|
|
387
|
+
case 3: { // text string
|
|
388
|
+
var slen = _lenOf(_readArgument(state, ai));
|
|
389
|
+
_need(state, slen);
|
|
390
|
+
var sb = buf_slice(state, slen);
|
|
391
|
+
// CBOR text strings are defined as valid UTF-8 (RFC 8949 §3.1).
|
|
392
|
+
// Buffer.toString("utf8") silently substitutes U+FFFD for
|
|
393
|
+
// malformed bytes — that changes data and can slip an invalid
|
|
394
|
+
// payload past a canonicalization / signature check. Decode
|
|
395
|
+
// fatally so malformed UTF-8 is refused.
|
|
396
|
+
try {
|
|
397
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(sb);
|
|
398
|
+
} catch (_e) {
|
|
399
|
+
throw new CborError("cbor/invalid-utf8",
|
|
400
|
+
"cbor.decode: text string is not valid UTF-8 (RFC 8949 §3.1)");
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
case 4: { // array
|
|
404
|
+
var alen = _lenOf(_readArgument(state, ai));
|
|
405
|
+
var arr = [];
|
|
406
|
+
for (var i = 0; i < alen; i++) arr.push(_decodeItem(state, depth + 1));
|
|
407
|
+
return arr;
|
|
408
|
+
}
|
|
409
|
+
case 5: { // map
|
|
410
|
+
var mlen = _lenOf(_readArgument(state, ai));
|
|
411
|
+
var m = new Map();
|
|
412
|
+
var seen = [];
|
|
413
|
+
for (var j = 0; j < mlen; j++) {
|
|
414
|
+
var keyStart = state.pos;
|
|
415
|
+
var key = _decodeItem(state, depth + 1);
|
|
416
|
+
var keyBytes = state.buf.slice(keyStart, state.pos);
|
|
417
|
+
for (var s = 0; s < seen.length; s++) {
|
|
418
|
+
if (Buffer.compare(seen[s], keyBytes) === 0) {
|
|
419
|
+
throw new CborError("cbor/duplicate-key", "cbor.decode: duplicate map key (RFC 8949 §5.6)");
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
seen.push(keyBytes);
|
|
423
|
+
var val = _decodeItem(state, depth + 1);
|
|
424
|
+
m.set(key, val);
|
|
425
|
+
}
|
|
426
|
+
return m;
|
|
427
|
+
}
|
|
428
|
+
case 6: { // tag
|
|
429
|
+
var tag = _lenOf(_readArgument(state, ai));
|
|
430
|
+
if (state.allowedTags.indexOf(tag) === -1) {
|
|
431
|
+
throw new CborError("cbor/tag-refused",
|
|
432
|
+
"cbor.decode: tag " + tag + " refused (add it to allowedTags to permit)");
|
|
433
|
+
}
|
|
434
|
+
return new Tag(tag, _decodeItem(state, depth + 1));
|
|
435
|
+
}
|
|
436
|
+
default: return _decodeSimpleOrFloat(state, ai); // major 7
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function buf_slice(state, n) {
|
|
441
|
+
var out = state.buf.slice(state.pos, state.pos + n);
|
|
442
|
+
state.pos += n;
|
|
443
|
+
// Copy so the returned buffer doesn't pin the (larger) input buffer.
|
|
444
|
+
return Buffer.from(out);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function _decodeSimpleOrFloat(state, ai) {
|
|
448
|
+
if (ai === 20) return false;
|
|
449
|
+
if (ai === 21) return true;
|
|
450
|
+
if (ai === 22) return null;
|
|
451
|
+
if (ai === 23) return undefined;
|
|
452
|
+
if (ai === 25) { _need(state, 2); var h = _readFloat16(state); return h; }
|
|
453
|
+
if (ai === 26) { _need(state, 4); var f = state.buf.readFloatBE(state.pos); state.pos += 4; return f; }
|
|
454
|
+
if (ai === 27) { _need(state, BYTES_64BIT); var d = state.buf.readDoubleBE(state.pos); state.pos += BYTES_64BIT; return d; }
|
|
455
|
+
if (ai === 31) {
|
|
456
|
+
throw new CborError("cbor/indefinite-refused", "cbor.decode: indefinite-length break refused");
|
|
457
|
+
}
|
|
458
|
+
throw new CborError("cbor/bad-simple",
|
|
459
|
+
"cbor.decode: unsupported simple value " + ai + " (only false/true/null/undefined + float16/32/64)");
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function _readFloat16(state) {
|
|
463
|
+
// IEEE 754 half-precision → Number (RFC 8949 Appendix D).
|
|
464
|
+
var half = state.buf.readUInt16BE(state.pos); state.pos += 2;
|
|
465
|
+
var exp = (half >> 10) & 0x1f;
|
|
466
|
+
var mant = half & 0x3ff;
|
|
467
|
+
var sign = (half & 0x8000) ? -1 : 1;
|
|
468
|
+
if (exp === 0) return sign * Math.pow(2, -14) * (mant / FLOAT16_MANT_DIV);
|
|
469
|
+
if (exp === 31) return mant ? NaN : sign * Infinity;
|
|
470
|
+
return sign * Math.pow(2, exp - 25) * (FLOAT16_MANT_DIV + mant);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
module.exports = {
|
|
474
|
+
encode: encode,
|
|
475
|
+
decode: decode,
|
|
476
|
+
Tag: Tag,
|
|
477
|
+
CborError: CborError,
|
|
478
|
+
};
|
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:88fb98f7-bbba-4c61-8d9f-61fed73b049c",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-24T20:36:05.508Z",
|
|
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.32",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.12.
|
|
25
|
+
"version": "0.12.32",
|
|
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.32",
|
|
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.32",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|