@blamejs/core 0.12.65 → 0.12.68
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 +4 -0
- package/lib/acme.js +2 -2
- package/lib/auth/dpop.js +14 -44
- package/lib/dbsc.js +5 -18
- package/lib/jwk.js +127 -0
- package/lib/uri-template.js +286 -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.68 (2026-05-26) — **`b.jwk` — RFC 7638 JWK thumbprint.** Compute the RFC 7638 thumbprint of a JSON Web Key — the canonical base64url(SHA-256(canonical-JSON)) identifier used to name a key (DPoP jkt bindings, ACME account-key thumbprints, DBSC session pins, kid derivation). b.jwk.thumbprint(jwk) returns the digest; b.jwk.canonicalize(jwk) returns the exact JSON that is hashed — only the key-type's required members, member names in lexicographic order, no whitespace, so the same key always yields the same thumbprint regardless of how its JWK was serialized. The standard key types are supported (EC, RSA, oct, OKP per RFC 8037) plus AKP, the IANA key type Node uses for ML-DSA / SLH-DSA post-quantum public keys; SHA-256 is the default, with hash: "sha384" | "sha512" for RFC 9278 thumbprint-with-hash. Verified against the RFC 7638 §3.1 worked example. b.auth.dpop, b.acme, and b.dbsc now compute their thumbprints through this primitive. **Added:** *`b.jwk.thumbprint` / `b.jwk.canonicalize`* — RFC 7638 JWK thumbprint. `thumbprint(jwk, opts)` returns `base64url(hash(canonical-JSON))` — only the key-type's required members feed the hash, so optional fields (`kid`, `use`, `alg`, …) never change the result. `canonicalize(jwk)` returns the canonical JSON string itself. Supports EC / RSA / oct / OKP and the AKP post-quantum key type; SHA-256 default, `hash` selects SHA-384 / SHA-512. Throws `JwkError` on an invalid key or unknown hash. **Changed:** *DPoP, ACME, and DBSC compose `b.jwk`* — `b.auth.dpop` (the `jkt` proof-key thumbprint), `b.acme` (the RFC 8555 account-key authorization), and `b.dbsc` (the session-pin thumbprint) now compute RFC 7638 thumbprints through `b.jwk` instead of carrying their own implementations. Behavior is unchanged — DPoP still refuses symmetric key types, and each surface keeps its own error codes.
|
|
12
|
+
|
|
13
|
+
- v0.12.66 (2026-05-26) — **`b.uriTemplate` — RFC 6570 URI Template expansion.** Expand RFC 6570 URI Templates — the {var} syntax that OpenAPI links, HAL _links, and hypermedia API clients use to turn a template plus a set of variables into a concrete URI. The full Level 4 grammar is supported: every operator ({+var} reserved, {#var} fragment, {.var} label, {/var} path, {;var} path-style parameters, {?var} query, {&var} query continuation), the {var:3} prefix modifier, and the {var*} explode modifier for lists and associative arrays. b.uriTemplate.expand(template, vars) returns the expanded string; b.uriTemplate.compile(template) parses once for templates applied to many variable sets. A malformed template (unclosed expression, reserved operator, non-numeric prefix, unmatched brace) throws UriTemplateError. Verified against the official uritemplate-test conformance suite (all 135 spec, extended, and negative cases). **Added:** *`b.uriTemplate.expand` / `b.uriTemplate.compile`* — RFC 6570 URI Template expansion, full Level 4. `expand(template, vars)` substitutes variables into a template and returns the URI; `compile(template)` returns a reusable `{ expand }` for repeated use. Variable values may be strings, numbers, booleans, arrays (lists), or plain objects (associative arrays); undefined, null, and empty list/map variables are omitted. All eight operators, the `:N` prefix modifier, and the `*` explode modifier follow §3.2, including reserved-set encoding for `{+var}` / `{#var}`. Composes naturally with `b.hal`, `b.linkHeader`, and `b.openapi` link objects. A malformed template throws `UriTemplateError`.
|
|
14
|
+
|
|
11
15
|
- v0.12.65 (2026-05-26) — **`b.base32` — RFC 4648 Base32 encode / decode.** Encode and decode RFC 4648 Base32 — the case-insensitive alphabet behind TOTP / 2FA secrets, DNSSEC NSEC3 hashes, and human-transcribable identifiers. Both RFC 4648 variants are supported: the standard alphabet (the default) and the extended-hex alphabet. b.base32.encode pads to an 8-character boundary by default (pass padding: false for the bare form TOTP key URIs use); b.base32.decode is strict by default but accepts the real-world shapes humans produce — lower-case, embedded spaces and dashes, missing padding — under loose: true. Verified against the RFC 4648 §10 test vectors for both alphabets. The TOTP primitive now composes this codec instead of carrying its own Base32 implementation. **Added:** *`b.base32.encode` / `b.base32.decode`* — RFC 4648 Base32 codec. `encode(buf, opts)` takes a Buffer or Uint8Array and returns a Base32 string, padded to an 8-character boundary unless `padding: false`; `decode(str, opts)` returns a Buffer. The `variant` option selects the standard (`"rfc4648"`, default) or extended-hex (`"rfc4648-hex"`) alphabet. Decoding is strict by default — any character outside the alphabet throws `Base32Error` — and `loose: true` up-cases the input and ignores embedded spaces, dashes, and missing padding, which is how copied TOTP keys and hand-typed codes arrive. **Changed:** *TOTP composes `b.base32`* — `b.auth.totp` now encodes and decodes its secrets through `b.base32` rather than a private Base32 implementation. Behavior is unchanged — secrets are still emitted unpadded and parsed leniently (case-insensitive, ignoring spaces and dashes).
|
|
12
16
|
|
|
13
17
|
- v0.12.64 (2026-05-25) — **`b.jsonSchema` — JSON Schema 2020-12 validation.** Validate JSON against a JSON Schema 2020-12 document — the dialect OpenAPI 3.1 adopted and the most widely implemented schema language. b.jsonSchema.compile(schema) returns a reusable validator; b.jsonSchema.validate(schema, instance) compiles and runs in one call, returning { valid, errors } where each error names the failing instance location, keyword, and schema path. The full 2020-12 vocabulary is supported: every applicator (allOf / anyOf / oneOf / not / if-then-else, properties / patternProperties / additionalProperties / prefixItems / items / contains), the annotation-aware unevaluatedProperties / unevaluatedItems, every assertion keyword, and reference resolution ($ref / $anchor / $dynamicRef / $dynamicAnchor / $defs / $id base URIs). format is an annotation by default (opt in to assertion with assertFormat). External references resolve through an operator-supplied schema map — never a network fetch. Verified against the official JSON-Schema-Test-Suite (1292 of 1295 draft2020-12 cases; the remainder need the bundled dialect metaschema or $vocabulary selection, both opt-in). This is the standards-track counterpart to the fluent b.safeSchema builder and the portable b.jtd. **Added:** *`b.jsonSchema` — JSON Schema 2020-12* — `compile(schema, opts)` returns `{ validate, isValid }`; `validate(schema, instance, opts)` and `isValid(schema, instance, opts)` compile and run in one call. `validate` returns `{ valid, errors }`, each error a `{ instancePath, keyword, schemaPath, message }`. The full 2020-12 vocabulary is implemented — applicators, annotation-aware `unevaluatedProperties` / `unevaluatedItems`, every assertion keyword, and `$ref` / `$anchor` / `$dynamicRef` / `$dynamicAnchor` / `$defs` / `$id` resolution. `format` is an annotation by default (`assertFormat: true` to assert). External references resolve through `opts.schemas` (a URI→schema map), never a network fetch. Reach for it when the schema is an existing JSON Schema document (an API contract, OpenAPI component, or config schema); `b.safeSchema` remains the fluent in-process builder and `b.jtd` the portable codegen-friendly option. Validating a schema document against the dialect metaschema requires supplying that metaschema via `opts.schemas`, and `$vocabulary`-based keyword selection is not honored (every standard keyword always asserts).
|
package/README.md
CHANGED
|
@@ -71,6 +71,7 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
|
|
|
71
71
|
|
|
72
72
|
- **Passwords** — Argon2id + policy primitive (`b.auth.password`); NIST 800-63B / PCI-DSS 4.0 / HIPAA-AAL2 profiles; HaveIBeenPwned k-anonymity breach check; length / context / dictionary / complexity rules; rotation + history
|
|
73
73
|
- **Multi-factor + WebAuthn** — passkeys (WebAuthn), TOTP, JWT (PQ-default)
|
|
74
|
+
- **JWK thumbprint** — RFC 7638 `base64url(SHA-256(canonical-JSON))` key identifier (`b.jwk.thumbprint` / `canonicalize`): EC / RSA / oct / OKP + the AKP post-quantum key type, SHA-256/384/512; the canonical key name behind DPoP `jkt`, ACME account keys, and DBSC session pins
|
|
74
75
|
- **OAuth / OIDC RP** — `b.auth.oauth`
|
|
75
76
|
- RP-Initiated / Front-Channel / Back-Channel Logout 1.0 (`parseFrontchannelLogoutRequest` + `verifyBackchannelLogoutToken` with jti-replay defense)
|
|
76
77
|
- RFC 9207 AS Issuer Identifier validation on callbacks (`parseCallback` — refuses iss mismatch + OP `error=` redirect)
|
|
@@ -99,6 +100,7 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
|
|
|
99
100
|
- **Signed webhooks + API encryption** — SLH-DSA-SHAKE-256f default; ML-DSA-65 opt-in; ECIES API encryption (`b.webhook`, `b.crypto`)
|
|
100
101
|
- **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
102
|
- **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)
|
|
103
|
+
- **URI Templates** — RFC 6570 expansion (`b.uriTemplate.expand` / `compile`): full Level 4 — every operator, the `:N` prefix and `*` explode modifiers — turning `{/path}{?q*}` plus variables into a concrete URI; validated against the official uritemplate-test suite. The `{var}` syntax behind OpenAPI links and HAL `_links`
|
|
102
104
|
- **JSON Type Definition** — RFC 8927 validation (`b.jtd.validate` / `isValid`): portable, cross-implementation schema validation (all eight forms — type / enum / elements / properties / values / discriminator / ref / empty), returning instancePath / schemaPath errors; validated against the official 316-case suite. Interop companion to the fluent `b.safeSchema` builder
|
|
103
105
|
- **JSON Schema 2020-12** — the OpenAPI 3.1 dialect (`b.jsonSchema.compile` / `validate` / `isValid`): full vocabulary including every applicator, annotation-aware `unevaluatedProperties` / `unevaluatedItems`, and `$ref` / `$dynamicRef` / `$anchor` / `$id` resolution (external refs via an operator-supplied schema map, never a network fetch); `format` is an annotation unless `assertFormat` is set; returns located `{ valid, errors }`. Validated against the official JSON-Schema-Test-Suite. Standards-track counterpart to `b.safeSchema` and `b.jtd`
|
|
104
106
|
- **Base32** — RFC 4648 codec (`b.base32.encode` / `decode`): standard + extended-hex alphabets, padded or bare, strict or lenient decode (case-insensitive, ignoring spaces / dashes for copied TOTP keys); validated against the RFC 4648 §10 vectors. The codec behind `b.auth.totp` secrets
|
package/index.js
CHANGED
|
@@ -405,6 +405,8 @@ var jsonPath = require("./lib/json-path");
|
|
|
405
405
|
var jtd = require("./lib/jtd");
|
|
406
406
|
var jsonSchema = require("./lib/json-schema");
|
|
407
407
|
var base32 = require("./lib/base32");
|
|
408
|
+
var uriTemplate = require("./lib/uri-template");
|
|
409
|
+
var jwk = require("./lib/jwk");
|
|
408
410
|
var standardWebhooks = require("./lib/standard-webhooks");
|
|
409
411
|
var lro = require("./lib/lro");
|
|
410
412
|
var jsonApi = require("./lib/jsonapi");
|
|
@@ -431,6 +433,8 @@ module.exports = {
|
|
|
431
433
|
jtd: jtd,
|
|
432
434
|
jsonSchema: jsonSchema,
|
|
433
435
|
base32: base32,
|
|
436
|
+
uriTemplate: uriTemplate,
|
|
437
|
+
jwk: jwk,
|
|
434
438
|
standardWebhooks: standardWebhooks,
|
|
435
439
|
lro: lro,
|
|
436
440
|
jsonApi: jsonApi,
|
package/lib/acme.js
CHANGED
|
@@ -45,6 +45,7 @@ var nodeCrypto = require("node:crypto");
|
|
|
45
45
|
|
|
46
46
|
var C = require("./constants");
|
|
47
47
|
var asn1 = require("./asn1-der");
|
|
48
|
+
var jwk = require("./jwk");
|
|
48
49
|
var safeUrl = require("./safe-url");
|
|
49
50
|
var safeJson = require("./safe-json");
|
|
50
51
|
var validateOpts = require("./validate-opts");
|
|
@@ -114,8 +115,7 @@ function _publicJwkFromKeyObject(keyObject) {
|
|
|
114
115
|
|
|
115
116
|
function _jwkThumbprint(publicJwk) {
|
|
116
117
|
// RFC 7638 §3 — base64url(SHA-256(canonical JSON of required members)).
|
|
117
|
-
|
|
118
|
-
return _b64u(nodeCrypto.createHash("sha256").update(canon).digest());
|
|
118
|
+
return jwk.thumbprint(publicJwk);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
function _signJws(privateKey, protectedHeader, payload) {
|
package/lib/auth/dpop.js
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
var nodeCrypto = require("node:crypto");
|
|
30
30
|
var bCrypto = require("../crypto");
|
|
31
|
+
var jwk = require("../jwk");
|
|
31
32
|
var safeJson = require("../safe-json");
|
|
32
33
|
var safeUrl = require("../safe-url");
|
|
33
34
|
var validateOpts = require("../validate-opts");
|
|
@@ -84,52 +85,21 @@ function _b64urlDecode(s) {
|
|
|
84
85
|
}
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (typeof
|
|
94
|
-
throw new AuthError("auth-dpop/bad-jwk", "jwk
|
|
95
|
-
}
|
|
96
|
-
if (jwk.kty === "EC") {
|
|
97
|
-
if (typeof jwk.crv !== "string" || typeof jwk.x !== "string" || typeof jwk.y !== "string") {
|
|
98
|
-
throw new AuthError("auth-dpop/bad-jwk", "EC jwk requires crv, x, y");
|
|
99
|
-
}
|
|
100
|
-
return JSON.stringify({ crv: jwk.crv, kty: "EC", x: jwk.x, y: jwk.y });
|
|
88
|
+
// Asymmetric key types DPoP accepts (its proof model relies on a
|
|
89
|
+
// signature, so symmetric "oct" keys are refused). AKP is the IANA key
|
|
90
|
+
// type for ML-DSA / SLH-DSA PQC public keys.
|
|
91
|
+
var DPOP_KTY = { EC: 1, OKP: 1, RSA: 1, AKP: 1 };
|
|
92
|
+
|
|
93
|
+
function thumbprint(key) {
|
|
94
|
+
if (!key || typeof key !== "object" || typeof key.kty !== "string" || key.kty.length === 0) {
|
|
95
|
+
throw new AuthError("auth-dpop/bad-jwk", "jwk must be an object with a kty");
|
|
101
96
|
}
|
|
102
|
-
if (
|
|
103
|
-
|
|
104
|
-
throw new AuthError("auth-dpop/bad-jwk", "OKP jwk requires crv, x");
|
|
105
|
-
}
|
|
106
|
-
return JSON.stringify({ crv: jwk.crv, kty: "OKP", x: jwk.x });
|
|
97
|
+
if (!DPOP_KTY[key.kty]) {
|
|
98
|
+
throw new AuthError("auth-dpop/refused-kty", "jwk.kty='" + key.kty + "' is not allowed (DPoP requires asymmetric kty)");
|
|
107
99
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
return JSON.stringify({ e: jwk.e, kty: "RSA", n: jwk.n });
|
|
113
|
-
}
|
|
114
|
-
if (jwk.kty === "AKP") {
|
|
115
|
-
// PQC asymmetric key package (draft-ietf-cose-cnsa-pqc / IANA AKP
|
|
116
|
-
// registry). Node:crypto exports ML-DSA / SLH-DSA public keys with
|
|
117
|
-
// kty=AKP, alg=<algId>, pub=<base64url public bytes>.
|
|
118
|
-
if (typeof jwk.alg !== "string" || typeof jwk.pub !== "string") {
|
|
119
|
-
throw new AuthError("auth-dpop/bad-jwk", "AKP jwk requires alg, pub");
|
|
120
|
-
}
|
|
121
|
-
return JSON.stringify({ alg: jwk.alg, kty: "AKP", pub: jwk.pub });
|
|
122
|
-
}
|
|
123
|
-
// Symmetric keys (oct) and any other kty are refused outright — DPoP's
|
|
124
|
-
// proof model requires asymmetric.
|
|
125
|
-
throw new AuthError("auth-dpop/refused-kty",
|
|
126
|
-
"jwk.kty='" + jwk.kty + "' is not allowed (DPoP requires asymmetric kty)");
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function thumbprint(jwk) {
|
|
130
|
-
var canonical = _canonicalJwk(jwk);
|
|
131
|
-
var hash = nodeCrypto.createHash("sha256").update(canonical, "utf8").digest();
|
|
132
|
-
return _b64urlEncode(hash);
|
|
100
|
+
// The RFC 7638 thumbprint itself is computed by b.jwk.
|
|
101
|
+
try { return jwk.thumbprint(key); }
|
|
102
|
+
catch (e) { throw new AuthError("auth-dpop/bad-jwk", (e && e.message) || "invalid jwk"); }
|
|
133
103
|
}
|
|
134
104
|
|
|
135
105
|
function _sha256B64Url(input) {
|
package/lib/dbsc.js
CHANGED
|
@@ -36,7 +36,7 @@ var nodeCrypto = require("node:crypto");
|
|
|
36
36
|
var validateOpts = require("./validate-opts");
|
|
37
37
|
var safeJson = require("./safe-json");
|
|
38
38
|
var bCrypto = require("./crypto");
|
|
39
|
-
var
|
|
39
|
+
var jwk = require("./jwk");
|
|
40
40
|
var jwtExternal = require("./auth/jwt-external");
|
|
41
41
|
var C = require("./constants");
|
|
42
42
|
var { defineClass } = require("./framework-error");
|
|
@@ -272,23 +272,10 @@ function _trimLeadingZeros(buf) {
|
|
|
272
272
|
return buf.slice(i);
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
-
function _jwkThumbprint(
|
|
276
|
-
// RFC 7638
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
var members;
|
|
280
|
-
if (jwk.kty === "EC") {
|
|
281
|
-
members = { crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y };
|
|
282
|
-
} else if (jwk.kty === "RSA") {
|
|
283
|
-
members = { e: jwk.e, kty: jwk.kty, n: jwk.n };
|
|
284
|
-
} else {
|
|
285
|
-
throw new DbscError("dbsc/bad-jwk-kty",
|
|
286
|
-
"jwkThumbprint: unsupported kty " + jwk.kty);
|
|
287
|
-
}
|
|
288
|
-
var canonical = canonicalJson.stringify(members);
|
|
289
|
-
return bCrypto.toBase64Url(
|
|
290
|
-
nodeCrypto.createHash("sha256").update(Buffer.from(canonical, "utf8")).digest()
|
|
291
|
-
);
|
|
275
|
+
function _jwkThumbprint(key) {
|
|
276
|
+
// RFC 7638 thumbprint (base64url(SHA-256(canonical JWK))) via b.jwk.
|
|
277
|
+
try { return jwk.thumbprint(key); }
|
|
278
|
+
catch (e) { throw new DbscError("dbsc/bad-jwk-kty", "jwkThumbprint: " + ((e && e.message) || "invalid jwk")); }
|
|
292
279
|
}
|
|
293
280
|
|
|
294
281
|
module.exports = {
|
package/lib/jwk.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.jwk
|
|
4
|
+
* @nav Identity
|
|
5
|
+
* @title JWK Thumbprint
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* Compute the <a href="https://www.rfc-editor.org/rfc/rfc7638">RFC 7638</a>
|
|
9
|
+
* thumbprint of a JSON Web Key — the canonical, hash-based identifier used
|
|
10
|
+
* to name a key (DPoP <code>jkt</code> bindings, ACME account-key
|
|
11
|
+
* thumbprints per RFC 8555, DBSC session pins, and <code>kid</code>
|
|
12
|
+
* derivation). The thumbprint is
|
|
13
|
+
* <code>base64url(SHA-256(canonical-JSON))</code>, where the canonical
|
|
14
|
+
* JSON contains only the key-type's required members, with member names
|
|
15
|
+
* in lexicographic order and no whitespace — so the same key always
|
|
16
|
+
* produces the same thumbprint regardless of how its JWK was serialized.
|
|
17
|
+
*
|
|
18
|
+
* <code>thumbprint(jwk)</code> returns the base64url digest;
|
|
19
|
+
* <code>canonicalize(jwk)</code> returns the exact JSON string that is
|
|
20
|
+
* hashed. The standard key types are supported — EC, RSA, oct, and OKP
|
|
21
|
+
* (RFC 8037 Ed25519 / X25519) — plus AKP, the IANA key type Node uses for
|
|
22
|
+
* ML-DSA / SLH-DSA post-quantum public keys. SHA-256 is the default;
|
|
23
|
+
* <code>hash: "sha384" | "sha512"</code> selects a longer digest
|
|
24
|
+
* (RFC 9278 thumbprint-with-hash).
|
|
25
|
+
*
|
|
26
|
+
* @card
|
|
27
|
+
* RFC 7638 JWK thumbprint — the canonical
|
|
28
|
+
* <code>base64url(SHA-256(canonical-JSON))</code> identifier for a JSON
|
|
29
|
+
* Web Key (EC / RSA / oct / OKP / AKP), behind DPoP <code>jkt</code>,
|
|
30
|
+
* ACME account keys, and DBSC session pins.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
var nodeCrypto = require("node:crypto");
|
|
34
|
+
var canonicalJson = require("./canonical-json");
|
|
35
|
+
var { defineClass } = require("./framework-error");
|
|
36
|
+
|
|
37
|
+
var JwkError = defineClass("JwkError", { alwaysPermanent: true });
|
|
38
|
+
|
|
39
|
+
var HASHES = { sha256: "sha256", sha384: "sha384", sha512: "sha512" };
|
|
40
|
+
|
|
41
|
+
// RFC 7638 §3.2 + JWA: the required members per key type, which (and only
|
|
42
|
+
// which) participate in the thumbprint. Listed for documentation; the
|
|
43
|
+
// canonical form is produced with lexicographic ordering regardless.
|
|
44
|
+
var REQUIRED = {
|
|
45
|
+
EC: ["crv", "kty", "x", "y"],
|
|
46
|
+
RSA: ["e", "kty", "n"],
|
|
47
|
+
oct: ["k", "kty"],
|
|
48
|
+
OKP: ["crv", "kty", "x"], // RFC 8037
|
|
49
|
+
AKP: ["alg", "kty", "pub"], // IANA AKP — ML-DSA / SLH-DSA public keys
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function _requiredMembers(jwk) {
|
|
53
|
+
if (!jwk || typeof jwk !== "object" || Array.isArray(jwk)) {
|
|
54
|
+
throw new JwkError("jwk/bad-jwk", "jwk: must be a JWK object");
|
|
55
|
+
}
|
|
56
|
+
if (typeof jwk.kty !== "string" || jwk.kty.length === 0) {
|
|
57
|
+
throw new JwkError("jwk/bad-jwk", "jwk: 'kty' is required");
|
|
58
|
+
}
|
|
59
|
+
var names = REQUIRED[jwk.kty];
|
|
60
|
+
if (!names) throw new JwkError("jwk/unsupported-kty", "jwk: unsupported kty '" + jwk.kty + "'");
|
|
61
|
+
var out = {};
|
|
62
|
+
for (var i = 0; i < names.length; i++) {
|
|
63
|
+
var n = names[i];
|
|
64
|
+
if (typeof jwk[n] !== "string" || jwk[n].length === 0) {
|
|
65
|
+
throw new JwkError("jwk/bad-jwk", "jwk: " + jwk.kty + " key requires a string '" + n + "' member");
|
|
66
|
+
}
|
|
67
|
+
out[n] = jwk[n];
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @primitive b.jwk.canonicalize
|
|
74
|
+
* @signature b.jwk.canonicalize(jwk)
|
|
75
|
+
* @since 0.12.68
|
|
76
|
+
* @status stable
|
|
77
|
+
* @related b.jwk.thumbprint
|
|
78
|
+
*
|
|
79
|
+
* Return the RFC 7638 canonical JSON string for a JWK — only the key-type's
|
|
80
|
+
* required members, member names in lexicographic order, no whitespace.
|
|
81
|
+
* This is the exact input that <code>thumbprint</code> hashes. Throws
|
|
82
|
+
* <code>JwkError</code> for a missing <code>kty</code>, an unsupported key
|
|
83
|
+
* type, or a missing required member.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* b.jwk.canonicalize({ kty: "EC", crv: "P-256", x: "...", y: "...", use: "sig" });
|
|
87
|
+
* // → '{"crv":"P-256","kty":"EC","x":"...","y":"..."}' (use omitted)
|
|
88
|
+
*/
|
|
89
|
+
function canonicalize(jwk) {
|
|
90
|
+
return canonicalJson.stringify(_requiredMembers(jwk));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @primitive b.jwk.thumbprint
|
|
95
|
+
* @signature b.jwk.thumbprint(jwk, opts?)
|
|
96
|
+
* @since 0.12.68
|
|
97
|
+
* @status stable
|
|
98
|
+
* @related b.jwk.canonicalize
|
|
99
|
+
*
|
|
100
|
+
* Compute the RFC 7638 thumbprint of a JWK:
|
|
101
|
+
* <code>base64url(hash(canonicalJSON))</code>. Only the key-type's required
|
|
102
|
+
* members feed the hash, so optional fields (<code>kid</code>,
|
|
103
|
+
* <code>use</code>, <code>alg</code>, …) never change the result. SHA-256
|
|
104
|
+
* is the default digest; <code>hash</code> selects a longer one. Throws
|
|
105
|
+
* <code>JwkError</code> on an invalid JWK or unknown hash.
|
|
106
|
+
*
|
|
107
|
+
* @opts
|
|
108
|
+
* hash: "sha256" | "sha384" | "sha512", // default: "sha256"
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* b.jwk.thumbprint({ kty: "RSA", e: "AQAB", n: "0vx7ago...DKgw" });
|
|
112
|
+
* // → "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs"
|
|
113
|
+
*/
|
|
114
|
+
function thumbprint(jwk, opts) {
|
|
115
|
+
opts = opts || {};
|
|
116
|
+
var hash = HASHES[opts.hash || "sha256"];
|
|
117
|
+
if (!hash) throw new JwkError("jwk/bad-hash", "jwk.thumbprint: hash must be sha256, sha384, or sha512");
|
|
118
|
+
var canon = canonicalize(jwk);
|
|
119
|
+
return nodeCrypto.createHash(hash).update(canon, "utf8").digest("base64url");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = {
|
|
123
|
+
thumbprint: thumbprint,
|
|
124
|
+
canonicalize: canonicalize,
|
|
125
|
+
REQUIRED: REQUIRED,
|
|
126
|
+
JwkError: JwkError,
|
|
127
|
+
};
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.uriTemplate
|
|
4
|
+
* @nav HTTP
|
|
5
|
+
* @title URI Templates
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* Expand <a href="https://www.rfc-editor.org/rfc/rfc6570">RFC 6570</a> URI
|
|
9
|
+
* Templates — the <code>{var}</code> syntax that OpenAPI links, HAL
|
|
10
|
+
* <code>_links</code>, and hypermedia API clients use to turn a template
|
|
11
|
+
* plus a set of variables into a concrete URI. The full Level 4 grammar
|
|
12
|
+
* is supported: every operator (<code>{+var}</code> reserved,
|
|
13
|
+
* <code>{#var}</code> fragment, <code>{.var}</code> label,
|
|
14
|
+
* <code>{/var}</code> path, <code>{;var}</code> path-style parameters,
|
|
15
|
+
* <code>{?var}</code> query, <code>{&var}</code> query continuation),
|
|
16
|
+
* the <code>{var:3}</code> prefix modifier, and the <code>{var*}</code>
|
|
17
|
+
* explode modifier for lists and associative arrays.
|
|
18
|
+
*
|
|
19
|
+
* <code>expand(template, vars)</code> returns the expanded string;
|
|
20
|
+
* <code>compile(template)</code> parses once and returns a reusable
|
|
21
|
+
* <code>{ expand }</code> for templates applied to many variable sets. A
|
|
22
|
+
* malformed template (an unclosed expression, an unknown operator, or a
|
|
23
|
+
* non-numeric prefix) throws <code>UriTemplateError</code>.
|
|
24
|
+
*
|
|
25
|
+
* @card
|
|
26
|
+
* RFC 6570 URI Template expansion (full Level 4 — every operator, the
|
|
27
|
+
* <code>:N</code> prefix and <code>*</code> explode modifiers) — the
|
|
28
|
+
* <code>{var}</code> syntax behind OpenAPI links and HAL hypermedia.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
var { defineClass } = require("./framework-error");
|
|
32
|
+
|
|
33
|
+
var UriTemplateError = defineClass("UriTemplateError", { alwaysPermanent: true });
|
|
34
|
+
|
|
35
|
+
var MAX_PREFIX = 10000; // allow:raw-byte-literal — RFC 6570 caps prefix length at 9999
|
|
36
|
+
|
|
37
|
+
// Operator table (RFC 6570 §2.2 / §3.2.1). first = prefix when any value is
|
|
38
|
+
// present; sep = separator between values; named = emit "name=value";
|
|
39
|
+
// ifemp = string used when a named value is empty; reserved = allow the
|
|
40
|
+
// reserved set through unencoded.
|
|
41
|
+
var OPERATORS = {
|
|
42
|
+
"": { first: "", sep: ",", named: false, ifemp: "", reserved: false },
|
|
43
|
+
"+": { first: "", sep: ",", named: false, ifemp: "", reserved: true },
|
|
44
|
+
"#": { first: "#", sep: ",", named: false, ifemp: "", reserved: true },
|
|
45
|
+
".": { first: ".", sep: ".", named: false, ifemp: "", reserved: false },
|
|
46
|
+
"/": { first: "/", sep: "/", named: false, ifemp: "", reserved: false },
|
|
47
|
+
";": { first: ";", sep: ";", named: true, ifemp: "", reserved: false },
|
|
48
|
+
"?": { first: "?", sep: "&", named: true, ifemp: "=", reserved: false },
|
|
49
|
+
"&": { first: "&", sep: "&", named: true, ifemp: "=", reserved: false },
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
var UNRESERVED = /[A-Za-z0-9\-._~]/;
|
|
53
|
+
// Reserved = gen-delims + sub-delims (RFC 3986 §2.2).
|
|
54
|
+
var RESERVED = /[:/?#[\]@!$&'()*+,;=]/;
|
|
55
|
+
|
|
56
|
+
function _pctEncode(str, allowReserved) {
|
|
57
|
+
var out = "";
|
|
58
|
+
for (var i = 0; i < str.length; i++) {
|
|
59
|
+
var ch = str.charAt(i);
|
|
60
|
+
// Preserve existing percent-encoded triplets when the reserved set is
|
|
61
|
+
// allowed (operators "+" and "#").
|
|
62
|
+
if (allowReserved && ch === "%" && /^[0-9A-Fa-f]{2}$/.test(str.substr(i + 1, 2))) {
|
|
63
|
+
out += str.substr(i, 3); i += 2; continue;
|
|
64
|
+
}
|
|
65
|
+
if (UNRESERVED.test(ch) || (allowReserved && RESERVED.test(ch))) { out += ch; continue; }
|
|
66
|
+
// Percent-encode the character's raw UTF-8 bytes (handles surrogate
|
|
67
|
+
// pairs). encodeURIComponent is not used — it leaves !*'() unencoded,
|
|
68
|
+
// which RFC 6570 unreserved-only expansion must escape.
|
|
69
|
+
var cp = str.codePointAt(i);
|
|
70
|
+
var bytes = Buffer.from(String.fromCodePoint(cp), "utf8");
|
|
71
|
+
for (var b = 0; b < bytes.length; b++) out += "%" + bytes[b].toString(16).toUpperCase().padStart(2, "0"); // allow:raw-byte-literal — hex radix
|
|
72
|
+
if (cp > 0xFFFF) i++; // consumed a surrogate pair // allow:raw-byte-literal — BMP boundary for surrogate-pair detection
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function _allDigits(s) {
|
|
78
|
+
if (s.length === 0) return false;
|
|
79
|
+
for (var i = 0; i < s.length; i++) { var c = s.charCodeAt(i); if (c < 48 || c > 57) return false; } // allow:raw-byte-literal — ASCII '0'..'9' code-point bounds
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// A composite member/value is "defined" unless it is undefined or null
|
|
84
|
+
// (an empty string, 0, or false is defined and expands).
|
|
85
|
+
function _memberDefined(v) { return v !== undefined && v !== null; }
|
|
86
|
+
|
|
87
|
+
function _isDefined(v) {
|
|
88
|
+
if (v === undefined || v === null) return false;
|
|
89
|
+
if (Array.isArray(v)) return v.length > 0;
|
|
90
|
+
if (typeof v === "object") return Object.keys(v).length > 0;
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
function _toStr(v) {
|
|
94
|
+
if (typeof v === "string") return v;
|
|
95
|
+
if (typeof v === "number" || typeof v === "boolean") return String(v);
|
|
96
|
+
return String(v);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// A literal run may not contain a stray "}" (an unmatched expression close)
|
|
100
|
+
// — RFC 6570 literals exclude "{" and "}".
|
|
101
|
+
function _checkLiteral(lit) {
|
|
102
|
+
if (lit.indexOf("}") !== -1) throw new UriTemplateError("uri-template/unmatched-brace", "uriTemplate: unmatched '}' in template literal");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Parse a template into an array of literal strings + expression objects.
|
|
106
|
+
function _parse(template) {
|
|
107
|
+
if (typeof template !== "string") throw new UriTemplateError("uri-template/bad-template", "uriTemplate: template must be a string");
|
|
108
|
+
var parts = [];
|
|
109
|
+
var i = 0;
|
|
110
|
+
while (i < template.length) {
|
|
111
|
+
var open = template.indexOf("{", i);
|
|
112
|
+
if (open === -1) { _checkLiteral(template.slice(i)); parts.push({ literal: template.slice(i) }); break; }
|
|
113
|
+
if (open > i) { _checkLiteral(template.slice(i, open)); parts.push({ literal: template.slice(i, open) }); }
|
|
114
|
+
var close = template.indexOf("}", open);
|
|
115
|
+
if (close === -1) throw new UriTemplateError("uri-template/unclosed", "uriTemplate: unclosed expression at index " + open);
|
|
116
|
+
parts.push(_parseExpr(template.slice(open + 1, close)));
|
|
117
|
+
i = close + 1;
|
|
118
|
+
}
|
|
119
|
+
return parts;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function _parseExpr(body) {
|
|
123
|
+
if (body.length === 0) throw new UriTemplateError("uri-template/empty-expression", "uriTemplate: empty expression {}");
|
|
124
|
+
var op = "";
|
|
125
|
+
var c0 = body.charAt(0);
|
|
126
|
+
if ("+#./;?&".indexOf(c0) !== -1) { op = c0; body = body.slice(1); }
|
|
127
|
+
else if ("=,!@|".indexOf(c0) !== -1) {
|
|
128
|
+
// Operators reserved by RFC 6570 §2.2 for future extensions → error.
|
|
129
|
+
throw new UriTemplateError("uri-template/reserved-operator", "uriTemplate: operator '" + c0 + "' is reserved");
|
|
130
|
+
}
|
|
131
|
+
var specs = body.split(",").map(function (raw) {
|
|
132
|
+
if (raw.length === 0) throw new UriTemplateError("uri-template/bad-varspec", "uriTemplate: empty variable name");
|
|
133
|
+
var explode = false, prefix = null;
|
|
134
|
+
var name = raw;
|
|
135
|
+
if (raw.charAt(raw.length - 1) === "*") { explode = true; name = raw.slice(0, -1); }
|
|
136
|
+
else {
|
|
137
|
+
var colon = raw.indexOf(":");
|
|
138
|
+
if (colon !== -1) {
|
|
139
|
+
name = raw.slice(0, colon);
|
|
140
|
+
var n = raw.slice(colon + 1);
|
|
141
|
+
if (!_allDigits(n)) throw new UriTemplateError("uri-template/bad-prefix", "uriTemplate: prefix length must be a non-negative integer");
|
|
142
|
+
prefix = parseInt(n, 10);
|
|
143
|
+
if (prefix >= MAX_PREFIX) throw new UriTemplateError("uri-template/bad-prefix", "uriTemplate: prefix length exceeds 9999");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (!/^(?:[A-Za-z0-9_]|%[0-9A-Fa-f]{2})(?:\.?(?:[A-Za-z0-9_]|%[0-9A-Fa-f]{2}))*$/.test(name)) {
|
|
147
|
+
throw new UriTemplateError("uri-template/bad-varname", "uriTemplate: invalid variable name '" + name + "'");
|
|
148
|
+
}
|
|
149
|
+
return { name: name, explode: explode, prefix: prefix };
|
|
150
|
+
});
|
|
151
|
+
return { op: op, specs: specs };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function _expandExpr(expr, vars) {
|
|
155
|
+
var o = OPERATORS[expr.op];
|
|
156
|
+
var pieces = [];
|
|
157
|
+
expr.specs.forEach(function (spec) {
|
|
158
|
+
var value = vars[spec.name];
|
|
159
|
+
if (!_isDefined(value)) return;
|
|
160
|
+
|
|
161
|
+
if (typeof value !== "object") {
|
|
162
|
+
// Simple string/number/boolean value.
|
|
163
|
+
var s = _toStr(value);
|
|
164
|
+
if (spec.prefix !== null) s = _sliceChars(s, spec.prefix);
|
|
165
|
+
pieces.push(_named(o, spec.name, _pctEncode(s, o.reserved), s.length === 0));
|
|
166
|
+
} else if (Array.isArray(value)) {
|
|
167
|
+
if (spec.prefix !== null) throw new UriTemplateError("uri-template/prefix-on-list", "uriTemplate: prefix modifier cannot apply to a list");
|
|
168
|
+
// Undefined / null members are ignored (RFC 6570 §3.2.1); a list with
|
|
169
|
+
// no defined members is treated as undefined and omitted entirely.
|
|
170
|
+
var members = value.filter(_memberDefined);
|
|
171
|
+
if (members.length === 0) return;
|
|
172
|
+
if (!spec.explode) {
|
|
173
|
+
var joined = members.map(function (m) { return _pctEncode(_toStr(m), o.reserved); }).join(",");
|
|
174
|
+
pieces.push(_named(o, spec.name, joined, false));
|
|
175
|
+
} else {
|
|
176
|
+
members.forEach(function (m) {
|
|
177
|
+
pieces.push(_named(o, spec.name, _pctEncode(_toStr(m), o.reserved), _toStr(m).length === 0));
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
// Associative array (object). Pairs whose value is undefined / null
|
|
182
|
+
// are omitted (RFC 6570 §3.2.1).
|
|
183
|
+
if (spec.prefix !== null) throw new UriTemplateError("uri-template/prefix-on-map", "uriTemplate: prefix modifier cannot apply to a map");
|
|
184
|
+
var keys = Object.keys(value).filter(function (k) { return _memberDefined(value[k]); });
|
|
185
|
+
if (keys.length === 0) return;
|
|
186
|
+
if (!spec.explode) {
|
|
187
|
+
var pairs = [];
|
|
188
|
+
keys.forEach(function (k) { pairs.push(_pctEncode(k, o.reserved)); pairs.push(_pctEncode(_toStr(value[k]), o.reserved)); });
|
|
189
|
+
pieces.push(_named(o, spec.name, pairs.join(","), false));
|
|
190
|
+
} else {
|
|
191
|
+
keys.forEach(function (k) {
|
|
192
|
+
// Exploded map: the key becomes the name.
|
|
193
|
+
pieces.push(_namedPair(o, _pctEncode(k, o.reserved), _pctEncode(_toStr(value[k]), o.reserved)));
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
if (pieces.length === 0) return "";
|
|
199
|
+
return o.first + pieces.join(o.sep);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Build one "name=value" (or bare value) piece for a non-exploded or
|
|
203
|
+
// string varspec.
|
|
204
|
+
function _named(o, name, encodedValue, isEmpty) {
|
|
205
|
+
if (!o.named) return encodedValue;
|
|
206
|
+
if (isEmpty) return name + o.ifemp;
|
|
207
|
+
return name + "=" + encodedValue;
|
|
208
|
+
}
|
|
209
|
+
// Exploded map pair: key is already the name.
|
|
210
|
+
function _namedPair(o, encodedKey, encodedValue) {
|
|
211
|
+
if (!o.named) return encodedKey + "=" + encodedValue;
|
|
212
|
+
if (encodedValue.length === 0) return encodedKey + o.ifemp;
|
|
213
|
+
return encodedKey + "=" + encodedValue;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Truncate to N Unicode code points (RFC 6570 prefix length is in chars).
|
|
217
|
+
function _sliceChars(s, n) {
|
|
218
|
+
var out = "", count = 0;
|
|
219
|
+
for (var i = 0; i < s.length && count < n; i++) {
|
|
220
|
+
var cp = s.codePointAt(i);
|
|
221
|
+
out += String.fromCodePoint(cp);
|
|
222
|
+
if (cp > 0xFFFF) i++; // allow:raw-byte-literal — BMP boundary for surrogate pairs
|
|
223
|
+
count++;
|
|
224
|
+
}
|
|
225
|
+
return out;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* @primitive b.uriTemplate.compile
|
|
230
|
+
* @signature b.uriTemplate.compile(template)
|
|
231
|
+
* @since 0.12.66
|
|
232
|
+
* @status stable
|
|
233
|
+
* @related b.uriTemplate.expand, b.hal, b.linkHeader
|
|
234
|
+
*
|
|
235
|
+
* Parse an RFC 6570 URI Template once and return a reusable
|
|
236
|
+
* <code>{ expand(vars) }</code>, so a template applied to many variable
|
|
237
|
+
* sets is parsed a single time. Throws <code>UriTemplateError</code> if the
|
|
238
|
+
* template is malformed.
|
|
239
|
+
*
|
|
240
|
+
* @example
|
|
241
|
+
* var t = b.uriTemplate.compile("/users/{id}{?fields*}");
|
|
242
|
+
* t.expand({ id: 7, fields: ["name", "email"] });
|
|
243
|
+
* // → "/users/7?fields=name&fields=email"
|
|
244
|
+
*/
|
|
245
|
+
function compile(template) {
|
|
246
|
+
var parts = _parse(template);
|
|
247
|
+
return {
|
|
248
|
+
expand: function (vars) {
|
|
249
|
+
vars = vars || {};
|
|
250
|
+
var out = "";
|
|
251
|
+
for (var i = 0; i < parts.length; i++) {
|
|
252
|
+
out += Object.prototype.hasOwnProperty.call(parts[i], "literal") ? parts[i].literal : _expandExpr(parts[i], vars);
|
|
253
|
+
}
|
|
254
|
+
return out;
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* @primitive b.uriTemplate.expand
|
|
261
|
+
* @signature b.uriTemplate.expand(template, vars)
|
|
262
|
+
* @since 0.12.66
|
|
263
|
+
* @status stable
|
|
264
|
+
* @related b.uriTemplate.compile
|
|
265
|
+
*
|
|
266
|
+
* Expand an RFC 6570 URI Template against a set of variables and return the
|
|
267
|
+
* resulting URI string. Variable values may be strings, numbers, booleans,
|
|
268
|
+
* arrays (lists), or plain objects (associative arrays); an undefined,
|
|
269
|
+
* null, or empty list/map variable is omitted. Reserved-set encoding,
|
|
270
|
+
* <code>:N</code> prefixes, and <code>*</code> explosion follow RFC 6570
|
|
271
|
+
* §3.2. Throws <code>UriTemplateError</code> on a malformed template.
|
|
272
|
+
*
|
|
273
|
+
* @example
|
|
274
|
+
* b.uriTemplate.expand("{/path}/here{?q,limit}",
|
|
275
|
+
* { path: "search", q: "json schema", limit: 10 });
|
|
276
|
+
* // → "/search/here?q=json%20schema&limit=10"
|
|
277
|
+
*/
|
|
278
|
+
function expand(template, vars) {
|
|
279
|
+
return compile(template).expand(vars);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
module.exports = {
|
|
283
|
+
expand: expand,
|
|
284
|
+
compile: compile,
|
|
285
|
+
UriTemplateError: UriTemplateError,
|
|
286
|
+
};
|
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:fafe52fa-5ff1-4704-af03-e5aeca4e55cf",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-26T14:23:38.200Z",
|
|
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.68",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.12.
|
|
25
|
+
"version": "0.12.68",
|
|
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.68",
|
|
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.68",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|