@blamejs/core 0.12.40 → 0.12.42

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.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.
12
+
13
+ - v0.12.41 (2026-05-24) — **`b.did` — W3C DID resolution (did:key + did:web) feeding the credential verifiers.** Resolve W3C Decentralized Identifiers (DID Core 1.0) to verification keys — the link that lets a credential's issuer be named by a DID rather than a raw key. Resolve the issuer DID of a b.vc / b.mdoc / b.scitt credential to a node:crypto KeyObject and hand it to the verifier. did:key encodes the public key in the identifier (multicodec + base58btc), so resolution is deterministic and offline — Ed25519, P-256, P-384, and secp256k1 round-trip; did:web places the DID document at an HTTPS URL derived from the identifier, with the network fetch left to the operator (the framework parses the operator-fetched document and extracts its verification methods, as publicKeyMultibase or publicKeyJwk). b.did.keyToDid encodes a KeyObject as a did:key (an issuer naming itself), b.did.parse splits the identifier (and returns the did:web URL to fetch), and b.did.resolve returns the document and verification keys. DID Core 1.0 is a W3C Recommendation; the method specs (did:key W3C CCG report, did:web DID method registry — EUDI-mandated) are deployed-stable. Composes node:crypto; no new runtime dependency. **Added:** *`b.did.resolve(did, opts?)` / `b.did.keyToDid(publicKey)` / `b.did.parse(did)`* — `resolve` returns `{ didDocument, verificationMethods: [{ id, controller, type, publicKey }] }` with each `publicKey` a `node:crypto` KeyObject ready for `b.vc.verify` / `b.mdoc.verifyIssuerSigned` / `b.scitt.verifyStatement`. did:key resolves deterministically and offline (base58btc + multicodec → Ed25519 raw key or EC compressed point, rebuilt via SPKI); did:web requires the operator to pass the fetched DID document as `opts.document` (the URL to GET is on `b.did.parse(did).url`) and the document `id` must match the requested DID. A publicKeyJwk in a DID document is imported only after its `kty`/`crv` is allowlisted (Ed25519 / P-256 / P-384 / secp256k1) — an unexpected key type from an untrusted document is refused, not blindly imported. `keyToDid` encodes an Ed25519 / P-256 / P-384 / secp256k1 KeyObject as a did:key; `parse` derives the did:web HTTPS URL (`host[:port][:path]` → `https://host/path/did.json`, or `/.well-known/did.json`). Unknown methods, malformed base58, unsupported multicodec codes, and unsupported key types are each refused.
14
+
11
15
  - v0.12.40 (2026-05-24) — **`b.mdoc` — ISO 18013-5 mdoc / mDL issuer-data verification.** Verify the issuer-signed data of an ISO/IEC 18013-5 mdoc — the credential format behind mobile driving licences (mDL) and the ISO track of the EU Digital Identity Wallet. This is the relying-party side: confirm that the data elements a holder presents were signed by the issuer and have not been altered. An mdoc's IssuerSigned carries the disclosed data elements and an issuerAuth that is a COSE_Sign1 (b.cose) over a Mobile Security Object (MSO) holding a per-element digest. b.mdoc.verifyIssuerSigned verifies the COSE signature with the issuer certificate from the COSE x5chain header, parses the MSO, enforces its validityInfo window, and recomputes each disclosed element's digest (the full Tag-24 IssuerSignedItemBytes) to match it against the MSO constant-time — the integrity check that makes selective disclosure trustworthy. An absent or mismatched digest is refused. Signing algorithms follow b.cose verification (the classical ES256/384/512 + EdDSA that real mDL issuers use; the caller names the allowlist); opts.trustAnchorsPem additionally verifies the issuer certificate chain. This completes the credential trio alongside W3C VCDM (b.vc) and IETF SD-JWT VC (b.auth.sdJwtVc). Composes b.cose + b.cbor; no new runtime dependency. **Added:** *`b.mdoc.verifyIssuerSigned(issuerSigned, opts)`* — Takes the CBOR `IssuerSigned` map (the operator extracts it from the device response / QR) and returns `{ docType, version, digestAlgorithm, validityInfo, namespaces, signerCert, alg }`. Verifies the COSE_Sign1 `issuerAuth` against the mandatory `opts.algorithms` allowlist using the issuer certificate from its `x5chain` (label 33) header; parses the Tag-24 Mobile Security Object; enforces the MSO `validityInfo` window against `opts.at` (default now; must be a valid Date; malformed dates fail closed); and recomputes the digest of every disclosed `IssuerSignedItem` (over the full Tag-24 bytes, with the MSO `digestAlgorithm` — SHA-256/384/512) to match the MSO `valueDigests` constant-time — an absent or mismatched digest is refused with `mdoc/digest-mismatch`. `opts.expectedDocType` pins the document type; `opts.trustAnchorsPem` (a PEM string or array) additionally verifies the issuer certificate chain and validity at the asserted time. A malformed `x5chain` certificate is refused with a clean `mdoc/bad-cert`. The mdoc device-authentication half (the SessionTranscript-bound holder-binding proof) is a presentation-protocol concern and is not part of issuer-data verification.
12
16
 
13
17
  - v0.12.39 (2026-05-24) — **`b.vc` — W3C Verifiable Credentials 2.0 (issue / verify, JOSE + COSE securing).** Issue and verify W3C Verifiable Credentials (VC Data Model 2.0, a W3C Recommendation) secured per Securing Verifiable Credentials using JOSE and COSE (VC-JOSE-COSE, also a W3C Recommendation, May 2025). A verifiable credential is a tamper-evident, signed set of claims an issuer makes about a subject — a diploma, a membership, a license, an age assertion. Two securing mechanisms are supported, both signing the credential itself (no JWT/CWT claims wrapper): JOSE produces a compact JWS with the vc+jwt media type, signed with ES256/384/512 or EdDSA; COSE produces a COSE_Sign1 (application/vc+cose) over b.cose, which also accepts ML-DSA-87 for PQC-forward deployments. b.vc.verify auto-detects the form from the input, requires an algorithm allowlist, always refuses the JOSE none algorithm, re-checks the VCDM 2.0 structural rules, and enforces the validFrom / validUntil window. This is the W3C credential model, distinct from the IETF SD-JWT VC already at b.auth.sdJwtVc. Composes b.cose; no new runtime dependency. **Added:** *`b.vc.issue(credential, opts)` / `b.vc.verify(secured, opts)`* — `issue` validates the credential against the VCDM 2.0 structural rules (the `credentials/v2` context first, a `VerifiableCredential` type, an issuer, a credential subject) and signs it: `securing: "jose"` returns a compact JWS string (`typ` header `vc+jwt`), `securing: "cose"` returns COSE_Sign1 bytes (`typ` header `application/vc+cose`, content type `application/vc`) via `b.cose`. The credential is the exact signed payload — no JWT/CWT claims are injected. `verify` auto-detects the securing form from the input (compact-JWS string vs. COSE_Sign1 bytes), verifies the signature against the mandatory `opts.algorithms` allowlist (the JOSE `none` algorithm is always refused), re-checks the structural rules, enforces the `validFrom` / `validUntil` window against `opts.at` (default now; must be a valid Date), and optionally matches `opts.expectedIssuer` against the credential issuer id. Returns `{ credential, securing, alg, issuer }`.
package/README.md CHANGED
@@ -131,8 +131,9 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
131
131
  - **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
132
132
  - **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
133
133
  - **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
134
- - **Verifiable Credentials** — `b.vc` W3C Verifiable Credentials Data Model 2.0 (VC-JOSE-COSE): `issue` / `verify` a signed credential as a compact JWS (`vc+jwt`, ES256/384/512 + EdDSA) or a COSE_Sign1 (`vc+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`
134
+ - **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`
135
135
  - **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`
136
+ - **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
136
137
  - **Document parsers** — `b.parsers` (XML / TOML / YAML / .env); `b.config` (schema-validated env)
137
138
  - **File-type detection** — `b.fileType` magic-byte content classification with deny-on-upload categories (image / document / archive / executable / etc.)
138
139
  ### Content-safety gates
package/index.js CHANGED
@@ -463,6 +463,7 @@ module.exports = {
463
463
  tsa: require("./lib/tsa"),
464
464
  vc: require("./lib/vc"),
465
465
  mdoc: require("./lib/mdoc"),
466
+ did: require("./lib/did"),
466
467
  queue: queue,
467
468
  logStream: logStream,
468
469
  redact: redact,
package/lib/did.js ADDED
@@ -0,0 +1,367 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.did
4
+ * @nav Crypto
5
+ * @title Decentralized Identifiers (DID)
6
+ *
7
+ * @intro
8
+ * Resolve W3C Decentralized Identifiers (DID Core 1.0, a W3C
9
+ * Recommendation) to verification keys — the missing link that lets a
10
+ * credential's issuer be named by a DID rather than a raw key. Resolve
11
+ * the issuer DID of a <code>b.vc</code> / <code>b.mdoc</code> /
12
+ * <code>b.scitt</code> credential to a <code>node:crypto</code>
13
+ * KeyObject, then hand that key to the verifier.
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.
23
+ *
24
+ * <code>b.did.keyToDid(publicKey)</code> produces a did:key from a
25
+ * KeyObject (an issuer naming itself); <code>b.did.parse(did)</code>
26
+ * splits the identifier (and, for did:web, returns the HTTPS URL to
27
+ * fetch); <code>b.did.resolve(did, opts)</code> returns the DID
28
+ * document and its verification methods as KeyObjects. Verification
29
+ * methods expressed as <code>publicKeyMultibase</code> or
30
+ * <code>publicKeyJwk</code> are both understood.
31
+ *
32
+ * <strong>Maturity.</strong> DID Core 1.0 is a Recommendation, but the
33
+ * method specs are deployed-stable rather than Recommendations:
34
+ * did:key is a W3C CCG report and did:web is a registered DID method
35
+ * (mandated by the EU Digital Identity Wallet). They are widely
36
+ * deployed and interoperable today; pin the dependency deliberately.
37
+ *
38
+ * @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.
43
+ */
44
+
45
+ var nodeCrypto = require("node:crypto");
46
+ var validateOpts = require("./validate-opts");
47
+ var { defineClass } = require("./framework-error");
48
+
49
+ var DidError = defineClass("DidError", { alwaysPermanent: true });
50
+
51
+ var B58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
52
+ var B58_MAP = (function () {
53
+ var m = {};
54
+ for (var i = 0; i < B58_ALPHABET.length; i += 1) m[B58_ALPHABET[i]] = i;
55
+ return m;
56
+ })();
57
+ var MAX_MULTIBASE_CHARS = 1024; // allow:raw-byte-literal — bounded did:key multibase length (DoS cap)
58
+
59
+ // multicodec public-key codes (unsigned-varint) → curve descriptor.
60
+ // keyLen is the multicodec payload: Ed25519 raw 32; EC compressed point.
61
+ var MULTICODEC = {
62
+ 0xed: { name: "Ed25519", kind: "okp" }, // ed25519-pub
63
+ 0x1200: { name: "P-256", kind: "ec", curveOid: "1.2.840.10045.3.1.7" }, // allow:raw-byte-literal allow:raw-time-literal — p256-pub multicodec code + OID dotted-form
64
+ 0x1201: { name: "P-384", kind: "ec", curveOid: "1.3.132.0.34" }, // allow:raw-byte-literal — p384-pub multicodec code
65
+ 0xe7: { name: "secp256k1", kind: "ec", curveOid: "1.3.132.0.10" }, // secp256k1-pub
66
+ };
67
+ var NAME_TO_CODE = {};
68
+ Object.keys(MULTICODEC).forEach(function (c) { NAME_TO_CODE[MULTICODEC[c].name] = Number(c); });
69
+
70
+ // ---- base58btc (bounded) ----
71
+
72
+ function _b58decode(str) {
73
+ if (str.length > MAX_MULTIBASE_CHARS) {
74
+ throw new DidError("did/too-long", "did: multibase value exceeds the " + MAX_MULTIBASE_CHARS + "-char cap");
75
+ }
76
+ var bytes = [0];
77
+ for (var i = 0; i < str.length; i += 1) {
78
+ var v = B58_MAP[str[i]];
79
+ if (v === undefined) throw new DidError("did/bad-base58", "did: invalid base58btc character '" + str[i] + "'");
80
+ var carry = v;
81
+ for (var j = 0; j < bytes.length; j += 1) {
82
+ carry += bytes[j] * 58;
83
+ bytes[j] = carry & 0xff;
84
+ carry >>= 8; // allow:raw-byte-literal — base-256 carry
85
+ }
86
+ while (carry > 0) { bytes.push(carry & 0xff); carry >>= 8; } // allow:raw-byte-literal — base-256 carry
87
+ }
88
+ // Leading '1's are leading zero bytes.
89
+ for (var k = 0; k < str.length && str[k] === "1"; k += 1) bytes.push(0);
90
+ return Buffer.from(bytes.reverse());
91
+ }
92
+
93
+ function _b58encode(buf) {
94
+ var digits = [0];
95
+ for (var i = 0; i < buf.length; i += 1) {
96
+ var carry = buf[i];
97
+ for (var j = 0; j < digits.length; j += 1) {
98
+ carry += digits[j] << 8; // allow:raw-byte-literal — base-256 shift
99
+ digits[j] = carry % 58;
100
+ carry = (carry / 58) | 0;
101
+ }
102
+ while (carry > 0) { digits.push(carry % 58); carry = (carry / 58) | 0; }
103
+ }
104
+ var out = "";
105
+ for (var z = 0; z < buf.length && buf[z] === 0; z += 1) out += "1";
106
+ for (var d = digits.length - 1; d >= 0; d -= 1) out += B58_ALPHABET[digits[d]];
107
+ return out;
108
+ }
109
+
110
+ // Read an unsigned LEB128 varint (multicodec code). Bounded to 4 bytes.
111
+ function _readVarint(buf) {
112
+ var value = 0, shift = 0, len = 0;
113
+ for (var i = 0; i < buf.length && i < 4; i += 1) { // allow:raw-byte-literal — multicodec varint ≤ 4 bytes
114
+ var b = buf[i];
115
+ value |= (b & 0x7f) << shift;
116
+ len += 1;
117
+ if ((b & 0x80) === 0) return { value: value >>> 0, length: len };
118
+ shift += 7; // allow:raw-byte-literal — 7 bits per varint byte
119
+ }
120
+ throw new DidError("did/bad-multicodec", "did: multicodec varint did not terminate");
121
+ }
122
+ function _encodeVarint(code) {
123
+ var out = [];
124
+ var n = code;
125
+ do { var b = n & 0x7f; n >>>= 7; if (n > 0) b |= 0x80; out.push(b); } while (n > 0); // allow:raw-byte-literal — LEB128 7-bit groups
126
+ return Buffer.from(out);
127
+ }
128
+
129
+ // ---- key <-> bytes ----
130
+
131
+ var ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex"); // RFC 8410 Ed25519 SubjectPublicKeyInfo header
132
+
133
+ function _keyObjectFromMulticodec(code, keyBytes) {
134
+ var desc = MULTICODEC[code];
135
+ if (!desc) throw new DidError("did/unsupported-key", "did: unsupported multicodec key code 0x" + code.toString(16)); // allow:raw-byte-literal — hex radix
136
+ if (desc.kind === "okp") {
137
+ if (keyBytes.length !== 32) { // allow:raw-byte-literal — Ed25519 public key is 32 bytes
138
+ throw new DidError("did/bad-key", "did: Ed25519 key must be 32 bytes (got " + keyBytes.length + ")");
139
+ }
140
+ return nodeCrypto.createPublicKey({ key: Buffer.concat([ED25519_SPKI_PREFIX, keyBytes]), format: "der", type: "spki" });
141
+ }
142
+ // EC: keyBytes is a compressed point (0x02/0x03 + X). Build an SPKI and
143
+ // let node decompress.
144
+ if (keyBytes.length < 2 || (keyBytes[0] !== 0x02 && keyBytes[0] !== 0x03)) {
145
+ throw new DidError("did/bad-key", "did: EC key must be a compressed point (0x02/0x03 prefix)");
146
+ }
147
+ var algid = _ecAlgId(desc.curveOid);
148
+ var bitstr = Buffer.concat([Buffer.from([0x03, keyBytes.length + 1, 0x00]), keyBytes]);
149
+ var body = Buffer.concat([algid, bitstr]);
150
+ var spki = Buffer.concat([Buffer.from([0x30, body.length]), body]); // allow:raw-byte-literal — SEQUENCE tag; single-byte DER length holds for these curves
151
+ try { return nodeCrypto.createPublicKey({ key: spki, format: "der", type: "spki" }); }
152
+ catch (e) { throw new DidError("did/bad-key", "did: could not import EC key: " + ((e && e.message) || e)); }
153
+ }
154
+
155
+ // AlgorithmIdentifier SEQUENCE { id-ecPublicKey, namedCurve OID }.
156
+ function _ecAlgId(curveOid) {
157
+ var idEcPublicKey = Buffer.from("06072a8648ce3d0201", "hex"); // allow:raw-byte-literal allow:raw-time-literal — DER OID for id-ecPublicKey
158
+ var curve = _oidDer(curveOid);
159
+ var inner = Buffer.concat([idEcPublicKey, curve]);
160
+ return Buffer.concat([Buffer.from([0x30, inner.length]), inner]);
161
+ }
162
+ function _oidDer(dotted) {
163
+ var parts = dotted.split(".").map(Number);
164
+ var bytes = [parts[0] * 40 + parts[1]]; // allow:raw-byte-literal — X.690 first-arc encoding
165
+ for (var i = 2; i < parts.length; i += 1) {
166
+ var arc = parts[i], stack = [];
167
+ do { stack.unshift(arc & 0x7f); arc >>>= 7; } while (arc > 0); // allow:raw-byte-literal — base-128 OID arc
168
+ for (var j = 0; j < stack.length - 1; j += 1) stack[j] |= 0x80; // allow:raw-byte-literal — continuation bit
169
+ bytes = bytes.concat(stack);
170
+ }
171
+ return Buffer.concat([Buffer.from([0x06, bytes.length]), Buffer.from(bytes)]);
172
+ }
173
+
174
+ // Compressed point + curve name from an EC KeyObject's JWK.
175
+ function _compressedPoint(jwk) {
176
+ var x = Buffer.from(jwk.x, "base64url");
177
+ var y = Buffer.from(jwk.y, "base64url");
178
+ return Buffer.concat([Buffer.from([(y[y.length - 1] & 1) ? 0x03 : 0x02]), x]);
179
+ }
180
+
181
+ /**
182
+ * @primitive b.did.parse
183
+ * @signature b.did.parse(did)
184
+ * @since 0.12.41
185
+ * @status experimental
186
+ * @related b.did.resolve, b.did.keyToDid
187
+ *
188
+ * Split a DID string into its method and method-specific id. For
189
+ * <code>did:web</code> the HTTPS URL of the DID document is also
190
+ * returned (host[:port][:path] → <code>https://host/path/did.json</code>,
191
+ * or <code>/.well-known/did.json</code> with no path).
192
+ *
193
+ * @example
194
+ * b.did.parse("did:web:example.com:issuers:42");
195
+ * // → { method: "web", id: "example.com:issuers:42", url: "https://example.com/issuers/42/did.json" }
196
+ */
197
+ function parse(did) {
198
+ if (typeof did !== "string" || did.indexOf("did:") !== 0) {
199
+ throw new DidError("did/bad-did", "did.parse: not a DID (must start with 'did:')");
200
+ }
201
+ var rest = did.slice(4);
202
+ var colon = rest.indexOf(":");
203
+ if (colon <= 0) throw new DidError("did/bad-did", "did.parse: DID is missing a method-specific id");
204
+ var method = rest.slice(0, colon);
205
+ var id = rest.slice(colon + 1);
206
+ var out = { method: method, id: id };
207
+ if (method === "web") out.url = _didWebUrl(id);
208
+ return out;
209
+ }
210
+
211
+ function _didWebUrl(id) {
212
+ // did-method-web §read: ':' separates segments (→ '/'); only the host
213
+ // may carry a percent-encoded port (%3A → ':'). Path segments are kept
214
+ // verbatim — NOT percent-decoded — so an escaped reserved char (e.g.
215
+ // %3F) stays path data rather than becoming URL control syntax, and a
216
+ // malformed escape never throws a raw URIError.
217
+ var segs = id.split(":");
218
+ var host = segs[0].replace(/%3[Aa]/g, ":");
219
+ if (!host) throw new DidError("did/bad-did", "did:web: missing host");
220
+ var path = segs.slice(1);
221
+ var base = "https://" + host;
222
+ return path.length ? base + "/" + path.join("/") + "/did.json" : base + "/.well-known/did.json";
223
+ }
224
+
225
+ /**
226
+ * @primitive b.did.keyToDid
227
+ * @signature b.did.keyToDid(publicKey)
228
+ * @since 0.12.41
229
+ * @status experimental
230
+ * @related b.did.resolve
231
+ *
232
+ * 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.
236
+ *
237
+ * @example
238
+ * var did = b.did.keyToDid(issuerPublicKey); // → "did:key:z6Mk…"
239
+ */
240
+ function keyToDid(publicKey) {
241
+ var key = (publicKey && typeof publicKey === "object" && publicKey.asymmetricKeyType)
242
+ ? publicKey : nodeCrypto.createPublicKey(publicKey);
243
+ var jwk = key.export({ format: "jwk" });
244
+ var code, payload;
245
+ if (jwk.kty === "OKP" && jwk.crv === "Ed25519") {
246
+ code = NAME_TO_CODE["Ed25519"];
247
+ payload = Buffer.from(jwk.x, "base64url");
248
+ } else if (jwk.kty === "EC") {
249
+ var name = jwk.crv === "P-256" ? "P-256" : jwk.crv === "P-384" ? "P-384" : jwk.crv === "secp256k1" ? "secp256k1" : null;
250
+ if (!name) throw new DidError("did/unsupported-key", "did.keyToDid: unsupported EC curve '" + jwk.crv + "'");
251
+ code = NAME_TO_CODE[name];
252
+ payload = _compressedPoint(jwk);
253
+ } else {
254
+ throw new DidError("did/unsupported-key", "did.keyToDid: unsupported key type '" + jwk.kty + "/" + jwk.crv + "'");
255
+ }
256
+ return "did:key:z" + _b58encode(Buffer.concat([_encodeVarint(code), payload]));
257
+ }
258
+
259
+ /**
260
+ * @primitive b.did.resolve
261
+ * @signature b.did.resolve(did, opts?)
262
+ * @since 0.12.41
263
+ * @status experimental
264
+ * @compliance soc2
265
+ * @related b.did.parse, b.vc.verify, b.mdoc.verifyIssuerSigned
266
+ *
267
+ * Resolve a DID to its document and verification methods (each with a
268
+ * <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
271
+ * document as <code>opts.document</code> (the network fetch is the
272
+ * operator's; the URL to fetch is on <code>b.did.parse(did).url</code>).
273
+ *
274
+ * @opts
275
+ * {
276
+ * document: object, // did:web — the fetched did.json (required for did:web)
277
+ * }
278
+ *
279
+ * @example
280
+ * var r = b.did.resolve("did:key:z6Mk…");
281
+ * var key = r.verificationMethods[0].publicKey; // → KeyObject for b.vc.verify / b.mdoc / b.scitt
282
+ */
283
+ function resolve(did, opts) {
284
+ opts = opts || {};
285
+ validateOpts.requireObject(opts, "did.resolve", DidError);
286
+ validateOpts(opts, ["document"], "did.resolve");
287
+ var parsed = parse(did);
288
+
289
+ if (parsed.method === "key") {
290
+ if (parsed.id[0] !== "z") {
291
+ throw new DidError("did/bad-did", "did:key: method-specific id must be base58btc multibase (start with 'z')");
292
+ }
293
+ var raw = _b58decode(parsed.id.slice(1));
294
+ var vh = _readVarint(raw);
295
+ var key = _keyObjectFromMulticodec(vh.value, raw.slice(vh.length));
296
+ var vmId = did + "#" + parsed.id;
297
+ var vm = { id: vmId, controller: did, type: MULTICODEC[vh.value].name, publicKey: key };
298
+ var doc = {
299
+ "@context": ["https://www.w3.org/ns/did/v1"],
300
+ id: did,
301
+ verificationMethod: [{ id: vmId, controller: did, type: "Multikey", publicKeyMultibase: parsed.id }],
302
+ assertionMethod: [vmId], authentication: [vmId],
303
+ };
304
+ return { didDocument: doc, verificationMethods: [vm] };
305
+ }
306
+
307
+ if (parsed.method === "web") {
308
+ if (!opts.document || typeof opts.document !== "object") {
309
+ throw new DidError("did/document-required",
310
+ "did:web: the DID document must be fetched by the operator and passed as opts.document (GET " + parsed.url + ")");
311
+ }
312
+ var docW = opts.document;
313
+ if (docW.id !== did) {
314
+ throw new DidError("did/document-mismatch", "did:web: document id '" + docW.id + "' does not match the requested DID");
315
+ }
316
+ return { didDocument: docW, verificationMethods: _extractVerificationMethods(docW) };
317
+ }
318
+
319
+ throw new DidError("did/unsupported-method", "did.resolve: unsupported DID method '" + parsed.method + "' (did:key and did:web only)");
320
+ }
321
+
322
+ // Import a publicKeyJwk after allowlisting its kty/crv — a DID document
323
+ // is untrusted input, so an unexpected key type (RSA / oct / unknown
324
+ // curve) is refused before it reaches node:crypto rather than blindly
325
+ // imported (the DID-context equivalent of the JWT alg/kty cross-check;
326
+ // there is no single verification `alg` in a DID document).
327
+ function _jwkToKey(jwk) {
328
+ var ok = (jwk.kty === "OKP" && jwk.crv === "Ed25519") ||
329
+ (jwk.kty === "EC" && (jwk.crv === "P-256" || jwk.crv === "P-384" || jwk.crv === "secp256k1"));
330
+ if (!ok) {
331
+ throw new DidError("did/unsupported-key",
332
+ "did: verificationMethod publicKeyJwk has unsupported kty/crv (" + jwk.kty + "/" + jwk.crv + ")");
333
+ }
334
+ try { return nodeCrypto.createPublicKey({ key: jwk, format: "jwk" }); }
335
+ catch (e) { throw new DidError("did/bad-key", "did: verificationMethod publicKeyJwk is invalid: " + ((e && e.message) || e)); }
336
+ }
337
+
338
+ // Extract verification methods from a DID document → KeyObjects.
339
+ function _extractVerificationMethods(doc) {
340
+ var vms = Array.isArray(doc.verificationMethod) ? doc.verificationMethod : [];
341
+ var out = [];
342
+ for (var i = 0; i < vms.length; i += 1) {
343
+ var vm = vms[i];
344
+ if (!vm || typeof vm !== "object") continue;
345
+ var key = null;
346
+ if (typeof vm.publicKeyMultibase === "string" && vm.publicKeyMultibase[0] === "z") {
347
+ var raw = _b58decode(vm.publicKeyMultibase.slice(1));
348
+ var vh = _readVarint(raw);
349
+ key = _keyObjectFromMulticodec(vh.value, raw.slice(vh.length));
350
+ } else if (vm.publicKeyJwk && typeof vm.publicKeyJwk === "object") {
351
+ key = _jwkToKey(vm.publicKeyJwk);
352
+ } else {
353
+ continue; // unknown key encoding — skip rather than guess
354
+ }
355
+ out.push({ id: vm.id, controller: vm.controller, type: vm.type, publicKey: key });
356
+ }
357
+ if (!out.length) throw new DidError("did/no-keys", "did: document has no resolvable verification methods");
358
+ return out;
359
+ }
360
+
361
+ module.exports = {
362
+ parse: parse,
363
+ keyToDid: keyToDid,
364
+ resolve: resolve,
365
+ MULTICODEC: MULTICODEC,
366
+ DidError: DidError,
367
+ };
package/lib/vc.js CHANGED
@@ -51,6 +51,11 @@ var VCDM_V2_CONTEXT = "https://www.w3.org/ns/credentials/v2";
51
51
  var JOSE_TYP = "vc+jwt";
52
52
  var COSE_TYP = "application/vc+cose";
53
53
  var COSE_CONTENT_TYPE = "application/vc";
54
+ var VP_JOSE_TYP = "vp+jwt";
55
+ var VP_COSE_TYP = "application/vp+cose";
56
+ var VP_COSE_CONTENT_TYPE = "application/vp";
57
+ var MAX_PRESENTATION_CREDENTIALS = 64; // allow:raw-byte-literal — bounded count of enveloped VCs per presentation
58
+ var ENVELOPED_VC_TYPE = "EnvelopedVerifiableCredential";
54
59
  var HDR_COSE_TYP = 16; // allow:raw-byte-literal — COSE "typ" header label (RFC 9596)
55
60
 
56
61
  // JOSE signature algorithms (final RFC 7518 / 8037), mapped to node
@@ -165,36 +170,43 @@ async function issue(credential, opts) {
165
170
  _validateVcdm(credential, null);
166
171
  if (!opts.privateKey) throw new VcError("vc/no-key", "vc.issue: opts.privateKey is required");
167
172
 
173
+ return _sign(credential, opts, JOSE_TYP, COSE_TYP, COSE_CONTENT_TYPE, "vc.issue");
174
+ }
175
+
176
+ // Secure a JSON document (credential or presentation) as a compact JWS
177
+ // (jose) or COSE_Sign1 (cose) with the given media-type headers. The
178
+ // document is the exact signed payload — no claims wrapper.
179
+ function _sign(doc, opts, joseTyp, coseTyp, coseContentType, fnName) {
168
180
  if (opts.securing === "cose") {
169
181
  var protectedHeaders = {};
170
- protectedHeaders[HDR_COSE_TYP] = COSE_TYP;
171
- return cose.sign(Buffer.from(JSON.stringify(credential), "utf8"), {
182
+ protectedHeaders[HDR_COSE_TYP] = coseTyp;
183
+ return cose.sign(Buffer.from(JSON.stringify(doc), "utf8"), {
172
184
  alg: opts.alg,
173
185
  privateKey: opts.privateKey,
174
186
  kid: opts.kid,
175
- contentType: COSE_CONTENT_TYPE,
187
+ contentType: coseContentType,
176
188
  protectedHeaders: protectedHeaders,
177
189
  });
178
190
  }
179
191
  if (opts.securing === "jose") {
180
192
  var params = JOSE_ALGS[opts.alg];
181
193
  if (!params) {
182
- throw new VcError("vc/bad-alg", "vc.issue: JOSE securing requires alg ES256/384/512 or EdDSA (got " + opts.alg + ")");
194
+ throw new VcError("vc/bad-alg", fnName + ": JOSE securing requires alg ES256/384/512 or EdDSA (got " + opts.alg + ")");
183
195
  }
184
196
  var key = _toKey(opts.privateKey, "private");
185
- var header = { alg: opts.alg, typ: JOSE_TYP };
197
+ var header = { alg: opts.alg, typ: joseTyp };
186
198
  if (typeof opts.kid === "string") header.kid = opts.kid;
187
199
  if (typeof opts.cty === "string") header.cty = opts.cty;
188
- var signingInput = _b64urlJson(header) + "." + _b64urlJson(credential);
200
+ var signingInput = _b64urlJson(header) + "." + _b64urlJson(doc);
189
201
  var sig = params.nodeHash === null
190
202
  ? nodeCrypto.sign(null, Buffer.from(signingInput, "ascii"), key)
191
203
  : nodeCrypto.sign(params.nodeHash, Buffer.from(signingInput, "ascii"), { key: key, dsaEncoding: params.dsaEncoding });
192
204
  return signingInput + "." + sig.toString("base64url");
193
205
  }
194
- throw new VcError("vc/bad-securing", "vc.issue: securing must be 'jose' or 'cose'");
206
+ throw new VcError("vc/bad-securing", fnName + ": securing must be 'jose' or 'cose'");
195
207
  }
196
208
 
197
- function _verifyJose(token, opts) {
209
+ function _verifyJose(token, opts, expectedTyp) {
198
210
  var parts = token.split(".");
199
211
  if (parts.length !== 3) {
200
212
  throw new VcError("vc/malformed", "vc.verify: not a compact JWS (expected three dot-separated segments)");
@@ -202,8 +214,8 @@ function _verifyJose(token, opts) {
202
214
  var header;
203
215
  try { header = safeJson.parse(Buffer.from(parts[0], "base64url").toString("utf8")); }
204
216
  catch (_e) { throw new VcError("vc/malformed", "vc.verify: JWS header is not valid base64url-JSON"); }
205
- if (!header || header.typ !== JOSE_TYP) {
206
- throw new VcError("vc/bad-typ", "vc.verify: JWS typ must be '" + JOSE_TYP + "'");
217
+ if (!header || header.typ !== expectedTyp) {
218
+ throw new VcError("vc/bad-typ", "vc.verify: JWS typ must be '" + expectedTyp + "'");
207
219
  }
208
220
  // crit-bypass defense (RFC 7515 §4.1.11): a `crit` header marks
209
221
  // extensions the verifier MUST understand and process. This verifier
@@ -228,16 +240,16 @@ function _verifyJose(token, opts) {
228
240
  ? nodeCrypto.verify(null, Buffer.from(signingInput, "ascii"), pub, sig)
229
241
  : nodeCrypto.verify(params.nodeHash, Buffer.from(signingInput, "ascii"), { key: pub, dsaEncoding: params.dsaEncoding }, sig);
230
242
  if (!ok) throw new VcError("vc/bad-signature", "vc.verify: JWS signature did not verify");
231
- var credential;
232
- try { credential = safeJson.parse(Buffer.from(parts[1], "base64url").toString("utf8")); }
243
+ var payload;
244
+ try { payload = safeJson.parse(Buffer.from(parts[1], "base64url").toString("utf8")); }
233
245
  catch (_e) { throw new VcError("vc/malformed", "vc.verify: JWS payload is not valid base64url-JSON"); }
234
- return { credential: credential, alg: header.alg };
246
+ return { payload: payload, alg: header.alg };
235
247
  }
236
248
 
237
- async function _verifyCose(bytes, opts) {
249
+ async function _verifyCose(bytes, opts, expectedTyp) {
238
250
  var algorithms = opts.algorithms.filter(function (a) { return a in cose.ALGORITHMS; });
239
251
  if (!algorithms.length) {
240
- throw new VcError("vc/no-cose-alg", "vc.verify: opts.algorithms has no COSE algorithm for a vc+cose credential");
252
+ throw new VcError("vc/no-cose-alg", "vc.verify: opts.algorithms has no COSE algorithm for a COSE-secured credential");
241
253
  }
242
254
  var out = await cose.verify(bytes, {
243
255
  algorithms: algorithms,
@@ -245,13 +257,25 @@ async function _verifyCose(bytes, opts) {
245
257
  keyResolver: opts.keyResolver,
246
258
  });
247
259
  var typ = out.protectedHeaders.get(HDR_COSE_TYP);
248
- if (typ !== undefined && typ !== COSE_TYP) {
249
- throw new VcError("vc/bad-typ", "vc.verify: COSE typ header is '" + typ + "', expected '" + COSE_TYP + "'");
260
+ if (typ !== undefined && typ !== expectedTyp) {
261
+ throw new VcError("vc/bad-typ", "vc.verify: COSE typ header is '" + typ + "', expected '" + expectedTyp + "'");
250
262
  }
251
- var credential;
252
- try { credential = safeJson.parse(out.payload.toString("utf8")); }
263
+ var payload;
264
+ try { payload = safeJson.parse(out.payload.toString("utf8")); }
253
265
  catch (_e) { throw new VcError("vc/malformed", "vc.verify: COSE payload is not valid JSON"); }
254
- return { credential: credential, alg: out.alg };
266
+ return { payload: payload, alg: out.alg };
267
+ }
268
+
269
+ // Verify a secured JSON document (the JOSE/COSE envelope) → { payload,
270
+ // alg, securing }. Shared by credential + presentation verification.
271
+ async function _verifySecured(secured, opts, joseTyp, coseTyp) {
272
+ if (typeof secured === "string") {
273
+ return Object.assign({ securing: "jose" }, _verifyJose(secured, opts, joseTyp));
274
+ }
275
+ if (Buffer.isBuffer(secured) || secured instanceof Uint8Array) {
276
+ return Object.assign({ securing: "cose" }, await _verifyCose(Buffer.from(secured), opts, coseTyp));
277
+ }
278
+ throw new VcError("vc/bad-input", "vc.verify: secured must be a compact-JWS string or COSE_Sign1 bytes");
255
279
  }
256
280
 
257
281
  /**
@@ -300,28 +324,202 @@ async function verify(secured, opts) {
300
324
  at = opts.at;
301
325
  }
302
326
 
303
- var securing, result;
304
- if (typeof secured === "string") {
305
- securing = "jose";
306
- result = _verifyJose(secured, opts);
307
- } else if (Buffer.isBuffer(secured) || secured instanceof Uint8Array) {
308
- securing = "cose";
309
- result = await _verifyCose(Buffer.from(secured), opts);
310
- } else {
311
- throw new VcError("vc/bad-input", "vc.verify: secured must be a compact-JWS string or COSE_Sign1 bytes");
312
- }
327
+ var result = await _verifySecured(secured, opts, JOSE_TYP, COSE_TYP);
313
328
 
314
- _validateVcdm(result.credential, { temporal: true, at: at });
315
- var issuer = _issuerId(result.credential);
329
+ _validateVcdm(result.payload, { temporal: true, at: at });
330
+ var issuer = _issuerId(result.payload);
316
331
  if (opts.expectedIssuer !== undefined && issuer !== opts.expectedIssuer) {
317
332
  throw new VcError("vc/issuer-mismatch", "vc.verify: credential issuer does not match expectedIssuer");
318
333
  }
319
- return { credential: result.credential, securing: securing, alg: result.alg, issuer: issuer };
334
+ return { credential: result.payload, securing: result.securing, alg: result.alg, issuer: issuer };
335
+ }
336
+
337
+ // VCDM 2.0 presentation structural rules.
338
+ function _validateVp(vp) {
339
+ if (!vp || typeof vp !== "object" || Array.isArray(vp)) {
340
+ throw new VcError("vc/bad-presentation", "vc: presentation must be a JSON object");
341
+ }
342
+ var ctx = vp["@context"];
343
+ if (!Array.isArray(ctx) || ctx[0] !== VCDM_V2_CONTEXT) {
344
+ throw new VcError("vc/bad-context", "vc: presentation @context must start with '" + VCDM_V2_CONTEXT + "'");
345
+ }
346
+ var types = Array.isArray(vp.type) ? vp.type : [vp.type];
347
+ if (types.indexOf("VerifiablePresentation") === -1) {
348
+ throw new VcError("vc/bad-type", "vc: type must include 'VerifiablePresentation'");
349
+ }
350
+ // verifiableCredential, when present, MUST be an array — a non-array
351
+ // value must fail closed rather than coerce to empty (which would let
352
+ // a holder bypass credential verification with a malformed container).
353
+ if (vp.verifiableCredential !== undefined && !Array.isArray(vp.verifiableCredential)) {
354
+ throw new VcError("vc/bad-presentation", "vc: verifiableCredential must be an array");
355
+ }
356
+ }
357
+
358
+ // An enveloped VC (VC-JOSE-COSE §enveloping): a data: URI whose media
359
+ // type selects the securing and whose body is the secured credential.
360
+ function _envelopeVc(securedVc) {
361
+ if (typeof securedVc === "string") {
362
+ return { "@context": [VCDM_V2_CONTEXT], type: ENVELOPED_VC_TYPE, id: "data:application/vc+jwt," + securedVc };
363
+ }
364
+ if (Buffer.isBuffer(securedVc) || securedVc instanceof Uint8Array) {
365
+ return { "@context": [VCDM_V2_CONTEXT], type: ENVELOPED_VC_TYPE,
366
+ id: "data:application/vc+cose;base64," + Buffer.from(securedVc).toString("base64") };
367
+ }
368
+ throw new VcError("vc/bad-credential", "vc.present: each credential must be a compact-JWS string or COSE_Sign1 bytes");
369
+ }
370
+
371
+ function _parseEnvelopedVc(entry) {
372
+ if (!entry || typeof entry !== "object" || entry.type !== ENVELOPED_VC_TYPE || typeof entry.id !== "string") {
373
+ throw new VcError("vc/bad-enveloped", "vc.verifyPresentation: verifiableCredential entries must be EnvelopedVerifiableCredential data: URIs");
374
+ }
375
+ var comma = entry.id.indexOf(",");
376
+ if (entry.id.indexOf("data:") !== 0 || comma === -1) {
377
+ throw new VcError("vc/bad-enveloped", "vc.verifyPresentation: enveloped credential id is not a data: URI");
378
+ }
379
+ var meta = entry.id.slice("data:".length, comma);
380
+ var body = entry.id.slice(comma + 1);
381
+ if (meta.indexOf("application/vc+cose") === 0) return Buffer.from(body, "base64");
382
+ if (meta.indexOf("application/vc+jwt") === 0) return body;
383
+ throw new VcError("vc/bad-enveloped", "vc.verifyPresentation: unsupported enveloped media type '" + meta + "'");
384
+ }
385
+
386
+ /**
387
+ * @primitive b.vc.present
388
+ * @signature b.vc.present(opts)
389
+ * @since 0.12.42
390
+ * @status experimental
391
+ * @compliance gdpr, soc2
392
+ * @related b.vc.verifyPresentation, b.vc.issue
393
+ *
394
+ * Build and sign a W3C Verifiable Presentation: a holder-signed envelope
395
+ * wrapping one or more secured credentials (each enveloped per
396
+ * VC-JOSE-COSE). <code>securing</code> and the algorithms match
397
+ * <code>b.vc.issue</code> (compact JWS <code>vp+jwt</code>, or COSE_Sign1
398
+ * <code>application/vp+cose</code>). Supply <code>nonce</code> /
399
+ * <code>audience</code> for holder-binding / replay protection — they
400
+ * are embedded in the signed presentation and checked at verification.
401
+ *
402
+ * @opts
403
+ * {
404
+ * credentials: array, // secured VCs (compact-JWS strings or COSE_Sign1 bytes)
405
+ * holder: string, // the presenter (a DID or other id)
406
+ * securing: string, // "jose" | "cose"
407
+ * alg: string, // JOSE: ES256/384/512 | EdDSA. COSE: + ML-DSA-87
408
+ * privateKey: object, // the holder's key
409
+ * kid: string, // optional key id
410
+ * nonce: string, // optional verifier challenge (embedded + checked)
411
+ * audience: string, // optional intended verifier (embedded + checked)
412
+ * }
413
+ *
414
+ * @example
415
+ * var vp = await b.vc.present({ credentials: [jws], holder: holderDid, securing: "jose", alg: "ES256", privateKey: holderKey, nonce: challenge });
416
+ */
417
+ async function present(opts) {
418
+ validateOpts.requireObject(opts, "vc.present", VcError);
419
+ validateOpts(opts, ["credentials", "holder", "securing", "alg", "privateKey", "kid", "nonce", "audience"], "vc.present");
420
+ if (!Array.isArray(opts.credentials) || opts.credentials.length === 0) {
421
+ throw new VcError("vc/no-credentials", "vc.present: opts.credentials must be a non-empty array");
422
+ }
423
+ if (opts.credentials.length > MAX_PRESENTATION_CREDENTIALS) {
424
+ throw new VcError("vc/too-many-credentials", "vc.present: at most " + MAX_PRESENTATION_CREDENTIALS + " credentials per presentation");
425
+ }
426
+ if (typeof opts.holder !== "string" || !opts.holder) {
427
+ throw new VcError("vc/no-holder", "vc.present: opts.holder is required (the presenter id / DID)");
428
+ }
429
+ if (!opts.privateKey) throw new VcError("vc/no-key", "vc.present: opts.privateKey is required");
430
+
431
+ var vp = {
432
+ "@context": [VCDM_V2_CONTEXT],
433
+ type: ["VerifiablePresentation"],
434
+ holder: opts.holder,
435
+ verifiableCredential: opts.credentials.map(_envelopeVc),
436
+ };
437
+ if (typeof opts.nonce === "string") vp.nonce = opts.nonce;
438
+ if (typeof opts.audience === "string") vp.audience = opts.audience;
439
+
440
+ return _sign(vp, opts, VP_JOSE_TYP, VP_COSE_TYP, VP_COSE_CONTENT_TYPE, "vc.present");
441
+ }
442
+
443
+ /**
444
+ * @primitive b.vc.verifyPresentation
445
+ * @signature b.vc.verifyPresentation(secured, opts)
446
+ * @since 0.12.42
447
+ * @status experimental
448
+ * @compliance gdpr, soc2
449
+ * @related b.vc.present, b.vc.verify
450
+ *
451
+ * Verify a Verifiable Presentation: the holder signature (auto-detected
452
+ * jose / cose, mandatory algorithm allowlist, JOSE <code>none</code>
453
+ * refused), the VCDM structure, and the embedded <code>nonce</code> /
454
+ * <code>audience</code> / <code>expectedHolder</code> when given. With
455
+ * <code>verifyCredentials: true</code> each enveloped credential is
456
+ * verified through <code>b.vc.verify</code> (using
457
+ * <code>opts.credentialOpts</code>) and returned.
458
+ *
459
+ * @opts
460
+ * {
461
+ * algorithms: string[], // required — holder-signature alg allowlist
462
+ * publicKey: object, // the holder verification key
463
+ * keyResolver: function, // (header) → holder key
464
+ * expectedHolder: string, // require presentation holder to match
465
+ * nonce: string, // require embedded nonce to match
466
+ * audience: string, // require embedded audience to match
467
+ * verifyCredentials: boolean, // verify each enveloped VC via b.vc.verify
468
+ * credentialOpts: object, // opts passed to b.vc.verify for each VC
469
+ * }
470
+ *
471
+ * @example
472
+ * var out = await b.vc.verifyPresentation(vp, {
473
+ * algorithms: ["ES256"], publicKey: holderKey, nonce: challenge,
474
+ * verifyCredentials: true, credentialOpts: { algorithms: ["ES256"], publicKey: issuerKey },
475
+ * });
476
+ * // → { presentation, holder, credentials: [verified VCs], securing, alg }
477
+ */
478
+ async function verifyPresentation(secured, opts) {
479
+ validateOpts.requireObject(opts, "vc.verifyPresentation", VcError);
480
+ validateOpts(opts, ["algorithms", "publicKey", "keyResolver", "expectedHolder", "nonce", "audience", "verifyCredentials", "credentialOpts"], "vc.verifyPresentation");
481
+ if (!Array.isArray(opts.algorithms) || opts.algorithms.length === 0) {
482
+ throw new VcError("vc/algorithms-required", "vc.verifyPresentation: opts.algorithms is required");
483
+ }
484
+ if (!opts.publicKey && typeof opts.keyResolver !== "function") {
485
+ throw new VcError("vc/no-key", "vc.verifyPresentation: pass publicKey or keyResolver");
486
+ }
487
+
488
+ var result = await _verifySecured(secured, opts, VP_JOSE_TYP, VP_COSE_TYP);
489
+ var vp = result.payload;
490
+ _validateVp(vp);
491
+
492
+ if (opts.expectedHolder !== undefined && vp.holder !== opts.expectedHolder) {
493
+ throw new VcError("vc/holder-mismatch", "vc.verifyPresentation: presentation holder does not match expectedHolder");
494
+ }
495
+ if (opts.nonce !== undefined && vp.nonce !== opts.nonce) {
496
+ throw new VcError("vc/nonce-mismatch", "vc.verifyPresentation: presentation nonce does not match");
497
+ }
498
+ if (opts.audience !== undefined && vp.audience !== opts.audience) {
499
+ throw new VcError("vc/audience-mismatch", "vc.verifyPresentation: presentation audience does not match");
500
+ }
501
+
502
+ var entries = vp.verifiableCredential || []; // _validateVp guarantees array-or-absent
503
+ if (entries.length > MAX_PRESENTATION_CREDENTIALS) {
504
+ throw new VcError("vc/too-many-credentials", "vc.verifyPresentation: presentation carries more than " + MAX_PRESENTATION_CREDENTIALS + " credentials");
505
+ }
506
+ var credentials = [];
507
+ if (opts.verifyCredentials) {
508
+ var credOpts = opts.credentialOpts;
509
+ validateOpts.requireObject(credOpts, "vc.verifyPresentation.credentialOpts", VcError);
510
+ for (var i = 0; i < entries.length; i += 1) {
511
+ credentials.push(await verify(_parseEnvelopedVc(entries[i]), credOpts));
512
+ }
513
+ }
514
+
515
+ return { presentation: vp, holder: vp.holder, credentials: credentials, securing: result.securing, alg: result.alg };
320
516
  }
321
517
 
322
518
  module.exports = {
323
519
  issue: issue,
324
520
  verify: verify,
521
+ present: present,
522
+ verifyPresentation: verifyPresentation,
325
523
  JOSE_ALGS: JOSE_ALGS,
326
524
  VCDM_V2_CONTEXT: VCDM_V2_CONTEXT,
327
525
  VcError: VcError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.12.40",
3
+ "version": "0.12.42",
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:6a95f7d3-c5ac-4994-955e-1fb82f6bb11b",
5
+ "serialNumber": "urn:uuid:313dd43c-bb3f-4c16-a801-2af63046b56f",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-25T04:51:15.499Z",
8
+ "timestamp": "2026-05-25T06:48:54.423Z",
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.40",
22
+ "bom-ref": "@blamejs/core@0.12.42",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.12.40",
25
+ "version": "0.12.42",
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.40",
29
+ "purl": "pkg:npm/%40blamejs/core@0.12.42",
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.40",
57
+ "ref": "@blamejs/core@0.12.42",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]