@blamejs/core 0.12.43 → 0.12.45

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 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.45 (2026-05-25) — **`b.cose` adds detached-payload sign/verify + `b.cose.importKey` (COSE_Key).** Two RFC 9052 / 9053 completions to the COSE substrate, both useable today and the prerequisites for mdoc device authentication and C2PA claim verification. Detached payloads (RFC 9052 §4.1): b.cose.sign with detached:true emits a COSE_Sign1 whose payload slot is nil — the signature still covers the payload, and the caller transmits it out of band; b.cose.verify takes the payload back as opts.externalPayload and binds it into the Sig_structure. A detached token verified without externalPayload is refused, and supplying externalPayload for an attached token is refused as ambiguous. COSE_Key import (RFC 9052 §7): b.cose.importKey turns a COSE_Key CBOR map into a node:crypto public KeyObject for b.cose.verify, accepting EC2 (P-256 / P-384 / P-521) and OKP (Ed25519) with the curve allowlisted so an unexpected key type is refused. No new runtime dependency. **Added:** *Detached COSE_Sign1 payloads + `b.cose.importKey(coseKey)`* — `b.cose.sign(payload, { detached: true })` emits a nil-payload COSE_Sign1 (the signature covers the payload regardless); `b.cose.verify(coseSign1, { externalPayload })` reconstructs the Sig_structure from the supplied payload, refusing a detached token with no `externalPayload` (`cose/detached-no-payload`) and refusing `externalPayload` on an attached token (`cose/payload-ambiguous`). `b.cose.importKey(coseKey)` maps a COSE_Key map (`kty` 2/EC2 with `crv` P-256/384/521, or `kty` 1/OKP with Ed25519) to a public KeyObject, allowlisting `kty`/`crv` and refusing anything else with `cose/unsupported-key` — the verification key embedded in an mdoc MSO or COSE_Key header is consumed this way.
12
+
13
+ - v0.12.44 (2026-05-25) — **`b.did` adds the did:jwk method.** Completes b.did's method set with did:jwk alongside did:key and did:web. did:jwk encodes a public key as a base64url-encoded JWK directly in the identifier, so resolution is deterministic and offline — the same self-contained shape as did:key but in JWK form, which is what OpenID4VCI and the EU Digital Identity Wallet ecosystem commonly use. b.did.resolve("did:jwk:…") returns the verification key as a node:crypto KeyObject (kty/crv allowlisted — Ed25519 / P-256 / P-384 / secp256k1 — so an unexpected key type is refused, not blindly imported), and b.did.keyToDid(publicKey, { method: "jwk" }) produces a did:jwk from a key (the private member is stripped). No new runtime dependency. **Added:** *did:jwk in `b.did.resolve` / `b.did.keyToDid`* — `resolve` decodes the base64url JWK (bounded via `b.safeJson`), allowlists its `kty`/`crv`, and returns `{ didDocument, verificationMethods: [{ publicKey, … }] }` with the key as a KeyObject ready for `b.vc` / `b.mdoc` / `b.scitt`; `keyToDid(publicKey, { method: "jwk" })` encodes a public key as `did:jwk:<base64url-JWK>` (default remains `did:key`). Malformed base64url-JSON is refused with `did/bad-jwk` and an unsupported key type with `did/unsupported-key`.
14
+
11
15
  - v0.12.43 (2026-05-25) — **`b.crypto.selfTest` — FIPS 140-3-style power-on self-test for the crypto stack.** A power-on self-test over the framework's cryptographic primitives — the integrity check a FIPS 140-3-validated module runs at start-up. The hash / XOF checks are known-answer tests against NIST FIPS 202 published vectors (SHA3-256 / SHA3-512 / SHAKE256), so they confirm the framework's hashing matches the standard rather than merely itself; the AEAD check round-trips XChaCha20-Poly1305 and confirms a tampered ciphertext is rejected; and the post-quantum checks run a pairwise-consistency + negative test for ML-KEM-1024, ML-DSA-87, and SLH-DSA-SHAKE-256f (a fresh keypair must encaps/decaps and sign/verify consistently and reject a tampered signature — FIPS 140-3 §10.3 pairwise consistency, since the runtime exposes no seed-injection API for a fixed-seed KAT). selfTest returns a structured report and, by default, throws on any failure so a broken crypto stack fails closed at boot rather than silently producing bad output. Operators in regulated deployments can run it at start-up as a self-integrity gate. **Added:** *`b.crypto.selfTest(opts?)`* — Runs eight checks — SHA3-512 / SHA3-256 / SHAKE256 known-answer tests (NIST FIPS 202), HMAC-SHA3-512 determinism, XChaCha20-Poly1305 round-trip + tamper-detect, and ML-KEM-1024 / ML-DSA-87 / SLH-DSA-SHAKE-256f pairwise-consistency + negative tests — and returns `{ ok, results: [{ name, ok, detail? }], failures, ranAt }`. Throws `crypto/self-test-failed` (with the report attached) on any failure unless `opts.throwOnFailure` is `false`. Exercises the framework's real primitive paths so a self-test failure means the shipped crypto is broken.
12
16
 
13
17
  - v0.12.42 (2026-05-24) — **`b.vc.present` / `b.vc.verifyPresentation` — W3C Verifiable Presentations.** Completes b.vc with the holder side: a Verifiable Presentation is a holder-signed envelope wrapping one or more credentials, proving the presenter controls the key the credentials were issued to. b.vc.present builds and signs a VerifiablePresentation (each credential enveloped per VC-JOSE-COSE) as a compact JWS (vp+jwt) or COSE_Sign1 (application/vp+cose), matching b.vc.issue's algorithms; an optional nonce / audience is embedded in the signed presentation for holder-binding and replay protection. b.vc.verifyPresentation verifies the holder signature (auto-detected jose/cose, mandatory algorithm allowlist, JOSE none refused), the VCDM structure, and the embedded nonce / audience / expectedHolder when given, and — with verifyCredentials: true — verifies each enveloped credential through b.vc.verify and returns them. The holder is typically a DID, resolved to a key via b.did. Composes b.cose; no new runtime dependency. **Added:** *`b.vc.present(opts)` / `b.vc.verifyPresentation(secured, opts)`* — `present` wraps `opts.credentials` (secured VCs — compact-JWS strings or COSE_Sign1 bytes, each enveloped as an `EnvelopedVerifiableCredential` data: URI) in a `VerifiablePresentation` signed by the holder, with optional `nonce` / `audience` embedded for binding. `verifyPresentation` verifies the holder signature against the mandatory `opts.algorithms` allowlist (JOSE `none` always refused), re-checks the VCDM structure, enforces `expectedHolder` / `nonce` / `audience` when supplied, and with `verifyCredentials: true` verifies each enveloped credential through `b.vc.verify` (using `opts.credentialOpts`), returning `{ presentation, holder, credentials, securing, alg }`. The enveloped-credential count is bounded. A `vp+jwt` presentation is refused by `b.vc.verify` and a `vc+jwt` credential is refused by `verifyPresentation` — the media-type binding keeps the two surfaces distinct.
package/README.md CHANGED
@@ -127,14 +127,14 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
127
127
  - **JSON / SQL / schema** — `b.safeJson` (with `maxKeys` cap defending CVE-2026-21717 V8 HashDoS), `b.safeBuffer`, `b.safeSql`, `b.safeSchema`
128
128
  - **URL + path** — `b.safeUrl` (IDN mixed-script / homograph refuse); `b.safeJsonPath` (refuses filter `?(...)`, deep-scan `$..`, script-shape `(@.x)` for safe Postgres JSONB ops)
129
129
  - **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
130
- - **COSE signing + encryption** — `b.cose` COSE_Sign1 sign/verify + COSE_Encrypt0 (RFC 9052) over `b.cbor`: classical ES256/384/512 + EdDSA (final COSE ids, interoperable today) plus ML-DSA-87 (PQC-forward, draft id); bounded + alg-allowlisted + crit-bypass-checked verification; single-recipient AEAD (ChaCha20/Poly1305 default, AES-GCM opt-in) with Enc_structure-bound AAD; the signed-statement substrate under SCITT / CWT / C2PA
130
+ - **COSE signing + encryption** — `b.cose` COSE_Sign1 sign/verify (attached or detached payload) + COSE_Encrypt0 + `importKey` (COSE_Key → KeyObject) (RFC 9052) over `b.cbor`: classical ES256/384/512 + EdDSA (final COSE ids, interoperable today) plus ML-DSA-87 (PQC-forward, draft id); bounded + alg-allowlisted + crit-bypass-checked verification; single-recipient AEAD (ChaCha20/Poly1305 default, AES-GCM opt-in) with Enc_structure-bound AAD; the signed-statement substrate under SCITT / CWT / mdoc / C2PA
131
131
  - **CBOR Web Token** — `b.cwt` CWT sign/verify (RFC 8392) over `b.cose`: standard-claim mapping (iss/sub/aud/exp/nbf/iat/cti) + `exp`/`nbf` clock-skew enforcement + `iss`/`aud` matching; the CBOR-native JWT for constrained / IoT / FIDO / verifiable-credential contexts
132
132
  - **Entity Attestation Token** — `b.eat` EAT sign/verify (RFC 9711) over `b.cwt`: device + software attestation claims (ueid / oemid / hwmodel / measurements / submods) with verifier-nonce freshness binding, `dbgstat` debug-status policy, and `eat_profile` pinning
133
133
  - **SCITT signed statements** — `b.scitt` sign/verify a signed, attributable claim about an artifact (signed SBOM, build attestation, release approval) over `b.cose`: the issuer + subject bind in the integrity-protected CWT_Claims header (RFC 9597); verification refuses any statement missing the iss/sub binding. The issuer side, on finalized RFCs; the transparency receipt (COSE Receipts draft) opts in on publication
134
134
  - **Trusted timestamping** — `b.tsa` RFC 3161 timestamp client: `buildRequest` a TimeStampReq, `parseResponse`, and `verifyToken` against your data — the message imprint, sent nonce, critical/sole `id-kp-timeStamping` EKU, and CMS signature are all checked, with optional certificate-chain verification. Timestamp a release artifact, audit checkpoint, or signed statement against any RFC 3161 TSA. Composes `b.cms` + the in-tree ASN.1 DER codec
135
135
  - **Verifiable Credentials** — `b.vc` W3C Verifiable Credentials Data Model 2.0 (VC-JOSE-COSE): `issue` / `verify` a signed credential, and `present` / `verifyPresentation` a holder-signed Verifiable Presentation wrapping credentials (with `nonce`/`audience` holder-binding) — as a compact JWS (`vc+jwt` / `vp+jwt`, ES256/384/512 + EdDSA) or a COSE_Sign1 (`vc+cose` / `vp+cose`, + ML-DSA-87) over `b.cose`. VCDM structural + `validFrom`/`validUntil` checks; the JOSE `none` algorithm is always refused. The W3C model, distinct from the IETF SD-JWT VC at `b.auth.sdJwtVc`
136
136
  - **Mobile credentials (mDL)** — `b.mdoc` ISO/IEC 18013-5 issuer-data verification: `verifyIssuerSigned` checks the COSE_Sign1 IssuerAuth (issuer cert from the `x5chain` header), the Mobile Security Object validity window, and every disclosed element's digest against the MSO `valueDigests` (the selective-disclosure integrity check), with optional issuer-chain verification. The ISO credential ecosystem alongside `b.vc` and `b.auth.sdJwtVc`. Composes `b.cose` + `b.cbor`
137
- - **Decentralized Identifiers** — `b.did` W3C DID resolution (DID Core 1.0): `resolve` a `did:key` (deterministic, offline — Ed25519 / P-256 / P-384 / secp256k1) or `did:web` (operator-fetched document) to `node:crypto` verification keys, so a credential's issuer DID resolves to the key that verifies it (`b.vc` / `b.mdoc` / `b.scitt`). `keyToDid` names a key as a `did:key`; document JWKs are kty/crv-allowlisted before import
137
+ - **Decentralized Identifiers** — `b.did` W3C DID resolution (DID Core 1.0): `resolve` a `did:key` / `did:jwk` (deterministic, offline — Ed25519 / P-256 / P-384 / secp256k1) or `did:web` (operator-fetched document) to `node:crypto` verification keys, so a credential's issuer DID resolves to the key that verifies it (`b.vc` / `b.mdoc` / `b.scitt`). `keyToDid` names a key as a `did:key` or `did:jwk`; document/JWK keys are kty/crv-allowlisted before import
138
138
  - **Document parsers** — `b.parsers` (XML / TOML / YAML / .env); `b.config` (schema-validated env)
139
139
  - **File-type detection** — `b.fileType` magic-byte content classification with deny-on-upload categories (image / document / archive / executable / etc.)
140
140
  ### Content-safety gates
package/lib/cose.js CHANGED
@@ -143,6 +143,7 @@ function _toBeSigned(protectedBstr, externalAad, payload) {
143
143
  * externalAad?: Buffer, // default empty — bound into the signature
144
144
  * unprotectedHeaders?: object, // extra unprotected map entries (numeric keys)
145
145
  * protectedHeaders?: object, // extra INTEGRITY-PROTECTED map entries (numeric keys); label 1 (alg) is reserved
146
+ * detached?: boolean, // emit a nil payload (RFC 9052 §4.1) — signature still covers it; caller transmits the payload separately
146
147
  * }
147
148
  *
148
149
  * @example
@@ -152,7 +153,7 @@ function _toBeSigned(protectedBstr, externalAad, payload) {
152
153
  */
153
154
  async function sign(payload, opts) {
154
155
  validateOpts.requireObject(opts, "cose.sign", CoseError);
155
- validateOpts(opts, ["alg", "privateKey", "kid", "contentType", "externalAad", "unprotectedHeaders", "protectedHeaders"], "cose.sign");
156
+ validateOpts(opts, ["alg", "privateKey", "kid", "contentType", "externalAad", "unprotectedHeaders", "protectedHeaders", "detached"], "cose.sign");
156
157
  if (SIGNABLE.indexOf(opts.alg) === -1) {
157
158
  throw new CoseError("cose/unsignable-alg",
158
159
  "cose.sign: alg must be one of " + SIGNABLE.join(" / ") +
@@ -213,7 +214,10 @@ async function sign(payload, opts) {
213
214
  ? nodeCrypto.sign(null, toBeSigned, key)
214
215
  : nodeCrypto.sign(params.nodeAlg, toBeSigned, { key: key, dsaEncoding: params.dsaEncoding });
215
216
 
216
- var sign1 = [protectedBstr, unprot, payloadBytes, signature];
217
+ // Detached payload (RFC 9052 §4.1): the COSE_Sign1 carries nil in the
218
+ // payload slot; the signature still covers the payload (above), and the
219
+ // caller transmits / re-supplies it out of band as externalPayload.
220
+ var sign1 = [protectedBstr, unprot, opts.detached ? null : payloadBytes, signature];
217
221
  return cbor.encode(new cbor.Tag(COSE_SIGN1_TAG, sign1));
218
222
  }
219
223
 
@@ -237,6 +241,7 @@ async function sign(payload, opts) {
237
241
  * publicKey?: object, // the verification key (KeyObject / PEM)
238
242
  * keyResolver?: function, // (protectedHeaders, unprotectedHeaders) → key
239
243
  * externalAad?: Buffer, // must match what was signed
244
+ * externalPayload?: Buffer, // required when the COSE_Sign1 payload is detached (nil); bound into the Sig_structure
240
245
  * maxBytes?: number, // forwarded to b.cbor.decode
241
246
  * maxDepth?: number,
242
247
  * }
@@ -247,7 +252,7 @@ async function sign(payload, opts) {
247
252
  */
248
253
  async function verify(coseSign1, opts) {
249
254
  validateOpts.requireObject(opts, "cose.verify", CoseError);
250
- validateOpts(opts, ["algorithms", "publicKey", "keyResolver", "externalAad", "maxBytes", "maxDepth"], "cose.verify");
255
+ validateOpts(opts, ["algorithms", "publicKey", "keyResolver", "externalAad", "externalPayload", "maxBytes", "maxDepth"], "cose.verify");
251
256
  if (!Array.isArray(opts.algorithms) || opts.algorithms.length === 0) {
252
257
  throw new CoseError("cose/algorithms-required",
253
258
  "cose.verify: opts.algorithms is required (no defaults — name the accepted algorithms)");
@@ -278,14 +283,23 @@ async function verify(coseSign1, opts) {
278
283
  if (!Buffer.isBuffer(protectedBstr) || !Buffer.isBuffer(signature)) {
279
284
  throw new CoseError("cose/malformed", "cose.verify: protected header and signature must be byte strings");
280
285
  }
286
+ // Detached payload (RFC 9052 §4.1): a nil payload slot means the caller
287
+ // must supply the payload out of band via opts.externalPayload, which
288
+ // is then bound into the Sig_structure. Supplying externalPayload for
289
+ // an attached (non-nil) token is ambiguous and refused.
281
290
  if (payload === null || payload === undefined) {
282
- throw new CoseError("cose/detached-unsupported",
283
- "cose.verify: detached payload (nil) is not supported in v1 — attached payload only");
284
- }
285
- // COSE_Sign1 payload is a bstr (RFC 9052 §4.2) — refuse a non-byte
286
- // payload rather than return a value that violates the documented
287
- // { payload: Buffer } shape.
288
- if (!Buffer.isBuffer(payload)) {
291
+ if (opts.externalPayload == null) {
292
+ throw new CoseError("cose/detached-no-payload",
293
+ "cose.verify: COSE_Sign1 has a detached (nil) payload — pass opts.externalPayload to verify it");
294
+ }
295
+ payload = _bstr(opts.externalPayload);
296
+ } else if (opts.externalPayload != null) {
297
+ throw new CoseError("cose/payload-ambiguous",
298
+ "cose.verify: opts.externalPayload was supplied but the COSE_Sign1 carries an attached payload");
299
+ } else if (!Buffer.isBuffer(payload)) {
300
+ // COSE_Sign1 payload is a bstr (RFC 9052 §4.2) — refuse a non-byte
301
+ // payload rather than return a value that violates the documented
302
+ // { payload: Buffer } shape.
289
303
  throw new CoseError("cose/malformed", "cose.verify: payload must be a byte string (bstr)");
290
304
  }
291
305
  // The unprotected header is a CBOR map — refuse a non-map rather
@@ -534,11 +548,82 @@ function decrypt0(coseEncrypt0, opts) {
534
548
  return { plaintext: pt, alg: algName, protectedHeaders: protMap, unprotectedHeaders: unprotected };
535
549
  }
536
550
 
551
+ // ---- COSE_Key (RFC 9052 §7 / RFC 9053 §7) → KeyObject ----
552
+
553
+ // COSE_Key EC2 curve identifiers (RFC 9053 §7.1) → JWK crv names. Only
554
+ // the curves b.cose.verify has an algorithm for are accepted: P-256
555
+ // (ES256), P-384 (ES384), P-521 (ES512). secp256k1 is intentionally
556
+ // absent — there is no ES256K path here, so importing one would let a
557
+ // secp256k1 key be verified under ES256, breaking the COSE alg/curve
558
+ // binding (RFC 9053). Re-add with an explicit ES256K algorithm.
559
+ var COSE_EC2_CRV = { 1: "P-256", 2: "P-384", 3: "P-521" };
560
+ var COSE_KTY_OKP = 1;
561
+ var COSE_KTY_EC2 = 2;
562
+ var COSE_OKP_ED25519 = 6; // allow:raw-byte-literal — COSE OKP Ed25519 crv id (RFC 9053)
563
+
564
+ function _coseKeyBytes(v, what) {
565
+ if (Buffer.isBuffer(v)) return v;
566
+ if (v instanceof Uint8Array) return Buffer.from(v);
567
+ throw new CoseError("cose/bad-cose-key", "cose.importKey: COSE_Key " + what + " must be a byte string");
568
+ }
569
+
570
+ /**
571
+ * @primitive b.cose.importKey
572
+ * @signature b.cose.importKey(coseKey)
573
+ * @since 0.12.45
574
+ * @status stable
575
+ * @related b.cose.verify, b.cbor.decode
576
+ *
577
+ * Import a COSE_Key (RFC 9052 §7) — a CBOR map keyed by integer labels —
578
+ * as a <code>node:crypto</code> public KeyObject for
579
+ * <code>b.cose.verify</code>. Accepts the EC2 (<code>kty</code> 2:
580
+ * P-256 / P-384 / P-521) and OKP (<code>kty</code> 1: Ed25519) key
581
+ * types — the curves <code>b.cose.verify</code> has an algorithm for;
582
+ * the curve is allowlisted, so an unexpected key type (including
583
+ * secp256k1, which has no ES256K path here) is refused rather than
584
+ * imported. The verification key embedded in an mdoc MSO or a COSE_Key
585
+ * header is consumed this way.
586
+ *
587
+ * @example
588
+ * var key = b.cose.importKey(coseKeyMap); // → public KeyObject
589
+ * var out = await b.cose.verify(sign1, { algorithms: ["ES256"], publicKey: key });
590
+ */
591
+ function importKey(coseKey) {
592
+ if (!(coseKey instanceof Map)) {
593
+ if (coseKey && typeof coseKey === "object" && !Array.isArray(coseKey)) {
594
+ var m = new Map();
595
+ Object.keys(coseKey).forEach(function (k) { m.set(Number(k), coseKey[k]); });
596
+ coseKey = m;
597
+ } else {
598
+ throw new CoseError("cose/bad-cose-key", "cose.importKey: expected a COSE_Key map");
599
+ }
600
+ }
601
+ var kty = coseKey.get(1);
602
+ var x = _coseKeyBytes(coseKey.get(-2), "x");
603
+ var jwk;
604
+ if (kty === COSE_KTY_OKP) {
605
+ if (coseKey.get(-1) !== COSE_OKP_ED25519) {
606
+ throw new CoseError("cose/unsupported-key", "cose.importKey: only OKP curve Ed25519 is supported");
607
+ }
608
+ jwk = { kty: "OKP", crv: "Ed25519", x: x.toString("base64url") };
609
+ } else if (kty === COSE_KTY_EC2) {
610
+ var crvName = COSE_EC2_CRV[coseKey.get(-1)];
611
+ if (!crvName) throw new CoseError("cose/unsupported-key", "cose.importKey: unsupported EC2 curve id " + coseKey.get(-1));
612
+ var y = _coseKeyBytes(coseKey.get(-3), "y");
613
+ jwk = { kty: "EC", crv: crvName, x: x.toString("base64url"), y: y.toString("base64url") };
614
+ } else {
615
+ throw new CoseError("cose/unsupported-key", "cose.importKey: kty must be OKP (1) or EC2 (2), got " + kty);
616
+ }
617
+ try { return nodeCrypto.createPublicKey({ key: jwk, format: "jwk" }); }
618
+ catch (e) { throw new CoseError("cose/bad-cose-key", "cose.importKey: could not import COSE_Key: " + ((e && e.message) || e)); }
619
+ }
620
+
537
621
  module.exports = {
538
622
  sign: sign,
539
623
  verify: verify,
540
624
  encrypt0: encrypt0,
541
625
  decrypt0: decrypt0,
626
+ importKey: importKey,
542
627
  ALGORITHMS: ALG_NAME_TO_ID,
543
628
  AEAD_ALGORITHMS: AEAD_NAME_TO_ID,
544
629
  COSE_SIGN1_TAG: COSE_SIGN1_TAG,
package/lib/did.js CHANGED
@@ -12,14 +12,16 @@
12
12
  * <code>b.scitt</code> credential to a <code>node:crypto</code>
13
13
  * KeyObject, then hand that key to the verifier.
14
14
  *
15
- * Two methods are supported. <strong>did:key</strong> encodes a public
16
- * key directly in the identifier (multicodec + base58btc multibase),
17
- * so resolution is deterministic and offline Ed25519, P-256, P-384,
18
- * and secp256k1 keys round-trip. <strong>did:web</strong> places the
19
- * DID document at an HTTPS URL derived from the identifier; the network
20
- * fetch is the operator's to make (the same operator-supplied-input
21
- * stance as the rest of the framework), and <code>resolve</code> takes
22
- * the fetched document and extracts its verification methods.
15
+ * Three methods are supported. <strong>did:key</strong> encodes a
16
+ * public key directly in the identifier (multicodec + base58btc
17
+ * multibase) and <strong>did:jwk</strong> encodes it as a base64url
18
+ * public JWK both resolve deterministically and offline (Ed25519,
19
+ * P-256, P-384, and secp256k1 round-trip). <strong>did:web</strong>
20
+ * places the DID document at an HTTPS URL derived from the identifier;
21
+ * the network fetch is the operator's to make (the same
22
+ * operator-supplied-input stance as the rest of the framework), and
23
+ * <code>resolve</code> takes the fetched document and extracts its
24
+ * verification methods.
23
25
  *
24
26
  * <code>b.did.keyToDid(publicKey)</code> produces a did:key from a
25
27
  * KeyObject (an issuer naming itself); <code>b.did.parse(did)</code>
@@ -36,13 +38,15 @@
36
38
  * deployed and interoperable today; pin the dependency deliberately.
37
39
  *
38
40
  * @card
39
- * W3C DID resolution (did:key + did:web) → verification KeyObjects for
40
- * the credential verifiers. did:key is deterministic + offline
41
- * (Ed25519 / P-256 / P-384 / secp256k1); did:web parses an
42
- * operator-fetched DID document. Composes node:crypto; no new dep.
41
+ * W3C DID resolution (did:key + did:jwk + did:web) → verification
42
+ * KeyObjects for the credential verifiers. did:key + did:jwk are
43
+ * deterministic + offline (Ed25519 / P-256 / P-384 / secp256k1);
44
+ * did:web parses an operator-fetched DID document. Composes
45
+ * node:crypto; no new dep.
43
46
  */
44
47
 
45
48
  var nodeCrypto = require("node:crypto");
49
+ var safeJson = require("./safe-json");
46
50
  var validateOpts = require("./validate-opts");
47
51
  var { defineClass } = require("./framework-error");
48
52
 
@@ -55,6 +59,7 @@ var B58_MAP = (function () {
55
59
  return m;
56
60
  })();
57
61
  var MAX_MULTIBASE_CHARS = 1024; // allow:raw-byte-literal — bounded did:key multibase length (DoS cap)
62
+ var MAX_JWK_B64_CHARS = 8192; // allow:raw-byte-literal — bounded did:jwk encoded-JWK length (DoS cap)
58
63
 
59
64
  // multicodec public-key codes (unsigned-varint) → curve descriptor.
60
65
  // keyLen is the multicodec payload: Ed25519 raw 32; EC compressed point.
@@ -224,23 +229,43 @@ function _didWebUrl(id) {
224
229
 
225
230
  /**
226
231
  * @primitive b.did.keyToDid
227
- * @signature b.did.keyToDid(publicKey)
232
+ * @signature b.did.keyToDid(publicKey, opts?)
228
233
  * @since 0.12.41
229
234
  * @status experimental
230
235
  * @related b.did.resolve
231
236
  *
232
237
  * Encode a public key (a <code>node:crypto</code> KeyObject or PEM) as a
233
- * <code>did:key</code> — the inverse of resolution, for an issuer that
234
- * names itself by its key. Ed25519, P-256, P-384, and secp256k1 are
235
- * supported.
238
+ * DID — the inverse of resolution, for an issuer that names itself by
239
+ * its key. Defaults to <code>did:key</code> (multicodec + base58btc);
240
+ * pass <code>opts.method = "jwk"</code> for <code>did:jwk</code>
241
+ * (base64url-encoded public JWK). Ed25519, P-256, P-384, and secp256k1
242
+ * are supported.
243
+ *
244
+ * @opts
245
+ * {
246
+ * method: string, // "key" (default) | "jwk"
247
+ * }
236
248
  *
237
249
  * @example
238
- * var did = b.did.keyToDid(issuerPublicKey); // → "did:key:z6Mk…"
250
+ * var did = b.did.keyToDid(issuerPublicKey); // → "did:key:z6Mk…"
251
+ * var dj = b.did.keyToDid(issuerPublicKey, { method: "jwk" }); // → "did:jwk:eyJr…"
239
252
  */
240
- function keyToDid(publicKey) {
253
+ function keyToDid(publicKey, opts) {
241
254
  var key = (publicKey && typeof publicKey === "object" && publicKey.asymmetricKeyType)
242
255
  ? publicKey : nodeCrypto.createPublicKey(publicKey);
243
256
  var jwk = key.export({ format: "jwk" });
257
+ if (opts && opts.method === "jwk") {
258
+ // did:jwk — base64url(UTF-8(JSON of the PUBLIC JWK)). Strip any
259
+ // private member defensively (a public KeyObject has none, but a
260
+ // caller could pass a private key by mistake).
261
+ var pub = {};
262
+ Object.keys(jwk).forEach(function (k) { if (k !== "d") pub[k] = jwk[k]; });
263
+ // Gate on the same kty/crv allowlist resolution enforces, so a
264
+ // produced did:jwk always round-trips (no generate-succeeds /
265
+ // resolve-fails RSA-style identifiers).
266
+ _jwkToKey(pub);
267
+ return "did:jwk:" + Buffer.from(JSON.stringify(pub), "utf8").toString("base64url");
268
+ }
244
269
  var code, payload;
245
270
  if (jwk.kty === "OKP" && jwk.crv === "Ed25519") {
246
271
  code = NAME_TO_CODE["Ed25519"];
@@ -266,8 +291,8 @@ function keyToDid(publicKey) {
266
291
  *
267
292
  * Resolve a DID to its document and verification methods (each with a
268
293
  * <code>node:crypto</code> public KeyObject ready for a verifier).
269
- * <code>did:key</code> resolves deterministically and offline.
270
- * <code>did:web</code> requires the operator to supply the fetched DID
294
+ * <code>did:key</code> and <code>did:jwk</code> resolve deterministically
295
+ * and offline. <code>did:web</code> requires the operator to supply the fetched DID
271
296
  * document as <code>opts.document</code> (the network fetch is the
272
297
  * operator's; the URL to fetch is on <code>b.did.parse(did).url</code>).
273
298
  *
@@ -304,6 +329,30 @@ function resolve(did, opts) {
304
329
  return { didDocument: doc, verificationMethods: [vm] };
305
330
  }
306
331
 
332
+ if (parsed.method === "jwk") {
333
+ if (parsed.id.length > MAX_JWK_B64_CHARS) {
334
+ throw new DidError("did/too-long", "did:jwk: encoded JWK exceeds the " + MAX_JWK_B64_CHARS + "-char cap");
335
+ }
336
+ var jwkJson = Buffer.from(parsed.id, "base64url").toString("utf8");
337
+ var jwk;
338
+ try { jwk = safeJson.parse(jwkJson, { maxBytes: MAX_JWK_B64_CHARS }); } catch (_e) {
339
+ throw new DidError("did/bad-jwk", "did:jwk: method-specific id is not base64url-encoded JSON");
340
+ }
341
+ if (!jwk || typeof jwk !== "object" || Array.isArray(jwk)) {
342
+ throw new DidError("did/bad-jwk", "did:jwk: decoded value is not a JWK object");
343
+ }
344
+ var jwkKey = _jwkToKey(jwk); // kty/crv allowlisted
345
+ var jwkVmId = did + "#0";
346
+ var jwkVm = { id: jwkVmId, controller: did, type: "JsonWebKey2020", publicKey: jwkKey };
347
+ var jwkDoc = {
348
+ "@context": ["https://www.w3.org/ns/did/v1"],
349
+ id: did,
350
+ verificationMethod: [{ id: jwkVmId, controller: did, type: "JsonWebKey2020", publicKeyJwk: jwk }],
351
+ assertionMethod: [jwkVmId], authentication: [jwkVmId],
352
+ };
353
+ return { didDocument: jwkDoc, verificationMethods: [jwkVm] };
354
+ }
355
+
307
356
  if (parsed.method === "web") {
308
357
  if (!opts.document || typeof opts.document !== "object") {
309
358
  throw new DidError("did/document-required",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.12.43",
3
+ "version": "0.12.45",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
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:2659855c-303c-4b3d-931b-a39839633612",
5
+ "serialNumber": "urn:uuid:f35a1ab6-b9b1-49fa-af2e-e468ce543070",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-25T07:30:43.059Z",
8
+ "timestamp": "2026-05-25T09:29:59.667Z",
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.43",
22
+ "bom-ref": "@blamejs/core@0.12.45",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.12.43",
25
+ "version": "0.12.45",
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.43",
29
+ "purl": "pkg:npm/%40blamejs/core@0.12.45",
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.43",
57
+ "ref": "@blamejs/core@0.12.45",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]