@nuggetslife/vc 0.0.10 → 0.0.15

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.
Files changed (31) hide show
  1. package/Cargo.toml +5 -2
  2. package/index.d.ts +303 -3
  3. package/index.js +15 -1
  4. package/package.json +11 -11
  5. package/src/bls_signatures/bbs_bls_holder_bound_signature_2022/mod.rs +268 -0
  6. package/src/bls_signatures/bbs_bls_holder_bound_signature_2022/types.rs +26 -0
  7. package/src/bls_signatures/bbs_bls_holder_bound_signature_proof_2022/mod.rs +100 -0
  8. package/src/bls_signatures/bbs_bls_holder_bound_signature_proof_2022/types.rs +17 -0
  9. package/src/bls_signatures/bbs_bls_signature_2020/mod.rs +329 -0
  10. package/src/bls_signatures/bbs_bls_signature_2020/types.rs +37 -0
  11. package/src/bls_signatures/bbs_bls_signature_proof_2020/mod.rs +92 -0
  12. package/src/bls_signatures/bbs_bls_signature_proof_2020/types.rs +13 -0
  13. package/src/bls_signatures/bls_12381_g2_keypair/mod.rs +470 -0
  14. package/src/{types.rs → bls_signatures/bls_12381_g2_keypair/types.rs} +0 -11
  15. package/src/{validators.rs → bls_signatures/bls_12381_g2_keypair/validators.rs} +1 -1
  16. package/src/bls_signatures/bound_bls_12381_g2_keypair/mod.rs +70 -0
  17. package/src/bls_signatures/bound_bls_12381_g2_keypair/types.rs +11 -0
  18. package/src/bls_signatures/mod.rs +6 -0
  19. package/src/jsonld.rs +200 -0
  20. package/src/ld_signatures.rs +311 -0
  21. package/src/lib.rs +3 -463
  22. package/test-data/bbs.json +92 -0
  23. package/test-data/citizenVocab.json +57 -0
  24. package/test-data/controllerDocument.json +5 -0
  25. package/test-data/credentialsContext.json +315 -0
  26. package/test-data/deriveProofFrame.json +15 -0
  27. package/test-data/inputDocument.json +29 -0
  28. package/test-data/keyPair.json +6 -0
  29. package/test-data/suiteContext.json +82 -0
  30. package/test.mjs +1088 -22
  31. package/test_jsonld_crossverify.mjs +256 -0
package/test.mjs CHANGED
@@ -1,13 +1,19 @@
1
1
  import test from 'node:test';
2
2
  import assert from 'node:assert';
3
+ import { readFileSync } from 'node:fs';
4
+ import { resolve, dirname } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { randomFillSync } from 'node:crypto';
3
7
  import { Bls12381G2KeyPair } from '@mattrglobal/bls12381-key-pair';
4
- import { Bls12381G2KeyPair as NewKeyPairClass } from './index.js'
5
- import bs58 from 'bs58'
6
- import bbs from '@nuggetslife/ffi-bbs-signatures'
8
+ import { Bls12381G2KeyPair as NewBls12381G2KeyPair, BbsBlsSignature2020 as NewBbsBlsSignature2020, BbsBlsSignatureProof2020 as NewBbsBlsSignatureProof2020, BbsBlsHolderBoundSignature2022, BbsBlsHolderBoundSignatureProof2022, BoundBls12381G2KeyPair, ldSign, ldVerify, ldDeriveProof, deriveProof as napiDeriveProof, createCommitment, verifyCommitment, unblindSignature, deriveProofHolderBound, JsonLd } from './index.js'
9
+ import { BbsBlsSignature2020 } from '@mattrglobal/jsonld-signatures-bbs';
7
10
 
11
+ const __dirname = dirname(fileURLToPath(import.meta.url));
12
+
13
+ //
14
+ // Bls12381G2KeyPair
15
+ //
8
16
  const seed = Buffer.alloc(32, 0)
9
- const address = '0x581510277Bc56802dE75BA021b66873437e0169f'
10
- const addressBase58 = bs58.encode(Buffer.from(address.slice(2), 'hex'))
11
17
 
12
18
  //Set of messages we wish to sign
13
19
  const messages = [
@@ -17,24 +23,19 @@ const messages = [
17
23
 
18
24
  test('gen keypair for same seed', async () => {
19
25
  const mattr = await Bls12381G2KeyPair.generate({ seed, });
20
- const harry = await NewKeyPairClass.generate({ seed })
21
- const ffi = await bbs.generateBls12381G2KeyPair(seed)
22
- const ffi_sec_key = bs58.encode(Buffer.from(ffi.secretKey))
23
- const ffi_pub_key = bs58.encode(Buffer.from(ffi.publicKey))
26
+ const harry = await NewBls12381G2KeyPair.generate({ seed })
24
27
 
25
28
  // private keys match
26
29
  assert.equal(mattr.privateKey, harry.privateKey)
27
- assert.equal(ffi_sec_key, harry.privateKey)
28
30
 
29
31
  // public keys match
30
32
  assert.equal(mattr.publicKey, harry.publicKey)
31
- assert.equal(ffi_pub_key, harry.publicKey)
32
33
  })
33
34
 
34
35
 
35
36
  test('keypair from keypair', async () => {
36
37
  const mattr = await Bls12381G2KeyPair.generate({ seed, });
37
- const harry = await NewKeyPairClass.from({
38
+ const harry = await NewBls12381G2KeyPair.from({
38
39
  id: mattr.id,
39
40
  controller: mattr.controller,
40
41
  publicKeyBase58: mattr.publicKey,
@@ -51,7 +52,7 @@ test('keypair from keypair', async () => {
51
52
 
52
53
  test('sign and verify', async () => {
53
54
  const mattr = await Bls12381G2KeyPair.generate({ seed, });
54
- const harry = await NewKeyPairClass.generate({ seed })
55
+ const harry = await NewBls12381G2KeyPair.generate({ seed })
55
56
 
56
57
  const ms = mattr.signer();
57
58
  const hs = harry.signer();
@@ -77,16 +78,1081 @@ test('sign and verify', async () => {
77
78
  assert.equal(ms_verify_hs_sig, true)
78
79
  })
79
80
 
80
- test('issuer jwk', async () => {
81
81
 
82
- const mattr = await Bls12381G2KeyPair.generate({ seed, });
83
- const harry = await NewKeyPairClass.generate({ seed })
84
- console.log(mattr.publicKeyBuffer)
85
- console.log(mattr.privateKeyBuffer)
82
+ //
83
+ // LD Signatures End-to-End Tests
84
+ //
85
+ // Matches the demo_single flow: sign → verify → derive → verify derived
86
+ //
86
87
 
87
- console.log(harry.publicKeyBuffer)
88
- console.log(harry.publicKeyBuffer2)
89
- console.log(harry.privateKeyBuffer)
88
+ const dataDir = resolve(__dirname, 'test-data');
89
+ const loadJson = (name) => JSON.parse(readFileSync(resolve(dataDir, name), 'utf8'));
90
90
 
91
+ const inputDocument = loadJson('inputDocument.json');
92
+ const keyPair = loadJson('keyPair.json');
93
+ const controllerDocument = loadJson('controllerDocument.json');
94
+ const citizenVocab = loadJson('citizenVocab.json');
95
+ const revealDocument = loadJson('deriveProofFrame.json');
91
96
 
92
- })
97
+ // Additional contexts not in the built-in nuggets context cache
98
+ const contexts = {
99
+ "did:example:489398593#test": keyPair,
100
+ "did:example:489398593": controllerDocument,
101
+ "https://w3id.org/citizenship/v1": citizenVocab,
102
+ };
103
+
104
+ test('ldSign produces a signed document with proof', async () => {
105
+ const signed = await ldSign({
106
+ document: inputDocument,
107
+ keyPair,
108
+ contexts,
109
+ });
110
+
111
+ assert.ok(signed, 'ldSign returned a result');
112
+ assert.ok(signed.proof, 'signed document has a proof');
113
+ assert.equal(signed.proof.type, 'BbsBlsSignature2020');
114
+ assert.ok(signed.proof.proofValue, 'proof has a proofValue');
115
+ assert.equal(signed.proof.verificationMethod, 'did:example:489398593#test');
116
+ assert.equal(signed.proof.proofPurpose, 'assertionMethod');
117
+ });
118
+
119
+ test('ldVerify confirms a signed document is valid', async () => {
120
+ const signed = await ldSign({
121
+ document: inputDocument,
122
+ keyPair,
123
+ contexts,
124
+ });
125
+
126
+ const result = await ldVerify({
127
+ document: signed,
128
+ contexts,
129
+ });
130
+
131
+ assert.equal(result.verified, true, `expected verified=true, got error: ${result.error}`);
132
+ });
133
+
134
+ test('ldVerify detects tampered documents', async () => {
135
+ const signed = await ldSign({
136
+ document: inputDocument,
137
+ keyPair,
138
+ contexts,
139
+ });
140
+
141
+ // Tamper with the document
142
+ const tampered = JSON.parse(JSON.stringify(signed));
143
+ tampered.credentialSubject.givenName = 'TAMPERED';
144
+
145
+ const result = await ldVerify({
146
+ document: tampered,
147
+ contexts,
148
+ });
149
+
150
+ assert.equal(result.verified, false, 'tampered document should not verify');
151
+ });
152
+
153
+ test('ldDeriveProof produces a derived document with selective disclosure', async () => {
154
+ const signed = await ldSign({
155
+ document: inputDocument,
156
+ keyPair,
157
+ contexts,
158
+ });
159
+
160
+ const derived = await ldDeriveProof({
161
+ document: signed,
162
+ revealDocument,
163
+ contexts,
164
+ });
165
+
166
+ assert.ok(derived, 'ldDeriveProof returned a result');
167
+ assert.ok(derived.proof, 'derived document has a proof');
168
+ assert.equal(derived.proof.type, 'BbsBlsSignatureProof2020');
169
+ assert.ok(derived.proof.proofValue, 'derived proof has a proofValue');
170
+ assert.ok(derived.proof.nonce, 'derived proof has a nonce');
171
+
172
+ // Verify selective disclosure: only requested fields present in credentialSubject
173
+ // Note: compact.rs may use full IRI keys instead of short names (known limitation)
174
+ const subject = derived.credentialSubject
175
+ || derived['https://www.w3.org/2018/credentials#credentialSubject'];
176
+ assert.ok(subject, 'derived document has credentialSubject');
177
+
178
+ const givenName = subject.givenName
179
+ || subject['http://schema.org/givenName'];
180
+ const familyName = subject.familyName
181
+ || subject['http://schema.org/familyName'];
182
+ const gender = subject.gender
183
+ || subject['http://schema.org/gender'];
184
+ const birthDate = subject.birthDate
185
+ ?? subject['http://schema.org/birthDate'];
186
+ const lprNumber = subject.lprNumber
187
+ ?? subject['https://w3id.org/citizenship#lprNumber'];
188
+
189
+ assert.ok(givenName, 'givenName is revealed');
190
+ assert.ok(familyName, 'familyName is revealed');
191
+ assert.ok(gender, 'gender is revealed');
192
+ assert.equal(birthDate, undefined, 'birthDate should not be revealed');
193
+ assert.equal(lprNumber, undefined, 'lprNumber should not be revealed');
194
+ });
195
+
196
+ test('ldVerify confirms a derived proof is valid', async () => {
197
+ const signed = await ldSign({
198
+ document: inputDocument,
199
+ keyPair,
200
+ contexts,
201
+ });
202
+
203
+ const derived = await ldDeriveProof({
204
+ document: signed,
205
+ revealDocument,
206
+ contexts,
207
+ });
208
+
209
+ const result = await ldVerify({
210
+ document: derived,
211
+ contexts,
212
+ });
213
+
214
+ assert.equal(result.verified, true, `expected derived proof verified=true, got error: ${result.error}`);
215
+ });
216
+
217
+ test('full demo_single flow: sign → verify → derive → verify derived', async () => {
218
+ // Step 1: Sign
219
+ const signed = await ldSign({
220
+ document: inputDocument,
221
+ keyPair,
222
+ contexts,
223
+ });
224
+ assert.ok(signed.proof, 'signed document has proof');
225
+
226
+ // Step 2: Verify signed document
227
+ const verifyResult = await ldVerify({ document: signed, contexts });
228
+ assert.equal(verifyResult.verified, true, `sign verify failed: ${verifyResult.error}`);
229
+
230
+ // Step 3: Derive selective disclosure proof
231
+ const derived = await ldDeriveProof({
232
+ document: signed,
233
+ revealDocument,
234
+ contexts,
235
+ });
236
+ assert.equal(derived.proof.type, 'BbsBlsSignatureProof2020');
237
+
238
+ // Step 4: Verify derived proof
239
+ const derivedVerifyResult = await ldVerify({ document: derived, contexts });
240
+ assert.equal(derivedVerifyResult.verified, true, `derived verify failed: ${derivedVerifyResult.error}`);
241
+ });
242
+
243
+
244
+ //
245
+ // BbsBlsSignature2020 class-based API tests
246
+ //
247
+
248
+ test('BbsBlsSignature2020 constructor with key material', () => {
249
+ const suite = new NewBbsBlsSignature2020({
250
+ key: keyPair,
251
+ });
252
+
253
+ assert.ok(suite, 'constructor returned an instance');
254
+ assert.equal(suite.type, 'sec:BbsBlsSignature2020');
255
+ });
256
+
257
+ test('BbsBlsSignature2020 constructor without args (verifier-only)', () => {
258
+ const suite = new NewBbsBlsSignature2020();
259
+
260
+ assert.ok(suite, 'constructor returned an instance');
261
+ assert.equal(suite.type, 'sec:BbsBlsSignature2020');
262
+ });
263
+
264
+ test('BbsBlsSignature2020.createProof produces valid proof', async () => {
265
+ const suite = new NewBbsBlsSignature2020({ key: keyPair });
266
+
267
+ const proof = await suite.createProof({
268
+ document: inputDocument,
269
+ contexts,
270
+ });
271
+
272
+ assert.ok(proof, 'createProof returned a result');
273
+ assert.equal(proof.type, 'BbsBlsSignature2020');
274
+ assert.ok(proof.proofValue, 'proof has a proofValue');
275
+ assert.equal(proof.verificationMethod, 'did:example:489398593#test');
276
+ assert.equal(proof.proofPurpose, 'assertionMethod');
277
+ });
278
+
279
+ test('BbsBlsSignature2020.verifyProof confirms signed document', async () => {
280
+ const suite = new NewBbsBlsSignature2020({ key: keyPair });
281
+
282
+ const proof = await suite.createProof({
283
+ document: inputDocument,
284
+ contexts,
285
+ });
286
+
287
+ // Embed proof in document for verification
288
+ const signed = { ...inputDocument, proof };
289
+
290
+ const result = await suite.verifyProof({
291
+ document: signed,
292
+ contexts,
293
+ });
294
+
295
+ assert.equal(result.verified, true, `expected verified=true, got error: ${result.error}`);
296
+ });
297
+
298
+ test('BbsBlsSignature2020.verifyProof detects tampered document', async () => {
299
+ const suite = new NewBbsBlsSignature2020({ key: keyPair });
300
+
301
+ const proof = await suite.createProof({
302
+ document: inputDocument,
303
+ contexts,
304
+ });
305
+
306
+ const tampered = { ...inputDocument, proof };
307
+ tampered.credentialSubject = { ...tampered.credentialSubject, givenName: 'TAMPERED' };
308
+
309
+ const result = await suite.verifyProof({
310
+ document: tampered,
311
+ contexts,
312
+ });
313
+
314
+ assert.equal(result.verified, false, 'tampered document should not verify');
315
+ });
316
+
317
+ test('BbsBlsSignature2020.ensureSuiteContext adds BBS context', () => {
318
+ const suite = new NewBbsBlsSignature2020();
319
+ const doc = { "@context": "https://www.w3.org/2018/credentials/v1", "type": "VerifiableCredential" };
320
+ const result = suite.ensureSuiteContext(doc);
321
+
322
+ const ctx = result["@context"];
323
+ assert.ok(Array.isArray(ctx), '@context should be an array');
324
+ assert.ok(ctx.includes('https://w3id.org/security/bbs/v1'), 'BBS context should be added');
325
+ });
326
+
327
+ test('BbsBlsSignature2020.ensureSuiteContext does not duplicate', () => {
328
+ const suite = new NewBbsBlsSignature2020();
329
+ const doc = {
330
+ "@context": ["https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/bbs/v1"],
331
+ "type": "VerifiableCredential",
332
+ };
333
+ const result = suite.ensureSuiteContext(doc);
334
+
335
+ const ctx = result["@context"];
336
+ const bbsCount = ctx.filter(c => c === 'https://w3id.org/security/bbs/v1').length;
337
+ assert.equal(bbsCount, 1, 'BBS context should not be duplicated');
338
+ });
339
+
340
+
341
+ //
342
+ // BbsBlsSignatureProof2020 class-based API tests
343
+ //
344
+
345
+ test('BbsBlsSignatureProof2020 constructor', () => {
346
+ const suite = new NewBbsBlsSignatureProof2020();
347
+
348
+ assert.ok(suite, 'constructor returned an instance');
349
+ assert.equal(suite.type, 'sec:BbsBlsSignatureProof2020');
350
+ assert.ok(Array.isArray(suite.proofType), 'proofType is an array');
351
+ assert.ok(suite.proofType.includes('BbsBlsSignatureProof2020'));
352
+ assert.ok(Array.isArray(suite.supportedDerivedProofType), 'supportedDerivedProofType is an array');
353
+ assert.ok(suite.supportedDerivedProofType.includes('BbsBlsSignature2020'));
354
+ });
355
+
356
+ test('BbsBlsSignatureProof2020.deriveProof produces derived proof', async () => {
357
+ // First sign with the high-level API
358
+ const signed = await ldSign({
359
+ document: inputDocument,
360
+ keyPair,
361
+ contexts,
362
+ });
363
+
364
+ const proofSuite = new NewBbsBlsSignatureProof2020();
365
+ const derived = await proofSuite.deriveProof({
366
+ document: signed,
367
+ revealDocument,
368
+ contexts,
369
+ });
370
+
371
+ assert.ok(derived, 'deriveProof returned a result');
372
+ assert.ok(derived.proof, 'derived document has a proof');
373
+ assert.equal(derived.proof.type, 'BbsBlsSignatureProof2020');
374
+ assert.ok(derived.proof.proofValue, 'derived proof has a proofValue');
375
+ assert.ok(derived.proof.nonce, 'derived proof has a nonce');
376
+ });
377
+
378
+ test('BbsBlsSignatureProof2020.verifyProof verifies derived proof', async () => {
379
+ const signed = await ldSign({
380
+ document: inputDocument,
381
+ keyPair,
382
+ contexts,
383
+ });
384
+
385
+ const proofSuite = new NewBbsBlsSignatureProof2020();
386
+ const derived = await proofSuite.deriveProof({
387
+ document: signed,
388
+ revealDocument,
389
+ contexts,
390
+ });
391
+
392
+ const result = await proofSuite.verifyProof({
393
+ document: derived,
394
+ contexts,
395
+ });
396
+
397
+ assert.equal(result.verified, true, `expected derived verified=true, got error: ${result.error}`);
398
+ });
399
+
400
+
401
+ //
402
+ // Standalone deriveProof function
403
+ //
404
+
405
+ test('standalone deriveProof function', async () => {
406
+ const signed = await ldSign({
407
+ document: inputDocument,
408
+ keyPair,
409
+ contexts,
410
+ });
411
+
412
+ const derived = await napiDeriveProof(signed, revealDocument, { contexts });
413
+
414
+ assert.ok(derived, 'deriveProof returned a result');
415
+ assert.ok(derived.proof, 'derived document has a proof');
416
+ assert.equal(derived.proof.type, 'BbsBlsSignatureProof2020');
417
+ });
418
+
419
+
420
+ //
421
+ // Full class-based flow: construct → createProof → verifyProof → deriveProof → verifyProof
422
+ //
423
+
424
+ test('full class-based flow: createProof → verifyProof → deriveProof → verifyProof', async () => {
425
+ // Step 1: Create proof with BbsBlsSignature2020
426
+ const sigSuite = new NewBbsBlsSignature2020({ key: keyPair });
427
+ const proof = await sigSuite.createProof({
428
+ document: inputDocument,
429
+ contexts,
430
+ });
431
+ assert.equal(proof.type, 'BbsBlsSignature2020');
432
+
433
+ // Step 2: Verify signed document
434
+ const signed = { ...inputDocument, proof };
435
+ const verifyResult = await sigSuite.verifyProof({
436
+ document: signed,
437
+ contexts,
438
+ });
439
+ assert.equal(verifyResult.verified, true, `sign verify failed: ${verifyResult.error}`);
440
+
441
+ // Step 3: Derive selective disclosure proof
442
+ const proofSuite = new NewBbsBlsSignatureProof2020();
443
+ const derived = await proofSuite.deriveProof({
444
+ document: signed,
445
+ revealDocument,
446
+ contexts,
447
+ });
448
+ assert.equal(derived.proof.type, 'BbsBlsSignatureProof2020');
449
+
450
+ // Step 4: Verify derived proof
451
+ const derivedResult = await proofSuite.verifyProof({
452
+ document: derived,
453
+ contexts,
454
+ });
455
+ assert.equal(derivedResult.verified, true, `derived verify failed: ${derivedResult.error}`);
456
+ });
457
+
458
+
459
+ //
460
+ // JsonLd — jsonld.js NAPI bindings
461
+ //
462
+
463
+ const credentialsContext = loadJson('credentialsContext.json');
464
+ const bbsContext = loadJson('bbs.json');
465
+ const suiteContext = loadJson('suiteContext.json');
466
+
467
+ const processorContexts = {
468
+ "did:example:489398593#test": keyPair,
469
+ "did:example:489398593": controllerDocument,
470
+ "https://w3id.org/citizenship/v1": citizenVocab,
471
+ "https://w3id.org/security/bbs/v1": bbsContext,
472
+ "https://www.w3.org/2018/credentials/v1": credentialsContext,
473
+ "https://w3id.org/security/suites/jws-2020/v1": suiteContext,
474
+ };
475
+
476
+ test('JsonLd constructor', () => {
477
+ const proc = new JsonLd();
478
+ assert.ok(proc, 'default constructor works');
479
+
480
+ const proc2 = new JsonLd({ contexts: processorContexts });
481
+ assert.ok(proc2, 'constructor with contexts works');
482
+ });
483
+
484
+ test('JsonLd.expand expands a JSON-LD document', async () => {
485
+ const proc = new JsonLd({ contexts: processorContexts });
486
+ const result = await proc.expand(inputDocument);
487
+
488
+ assert.ok(Array.isArray(result), 'expand returns an array');
489
+ assert.equal(result.length, 1, 'expand returns one expanded node');
490
+
491
+ const node = result[0];
492
+ assert.ok(node['https://www.w3.org/2018/credentials#credentialSubject'], 'has expanded credentialSubject');
493
+ assert.ok(node['@type'], 'has @type');
494
+ assert.ok(Array.isArray(node['@type']), '@type is an array');
495
+ assert.ok(node['@type'].includes('https://www.w3.org/2018/credentials#VerifiableCredential'), 'includes VerifiableCredential type');
496
+ });
497
+
498
+ test('JsonLd.expand with simple inline context', async () => {
499
+ const proc = new JsonLd();
500
+ const input = {
501
+ "@context": { "name": "http://schema.org/name" },
502
+ "name": "Alice",
503
+ };
504
+ const result = await proc.expand(input);
505
+
506
+ assert.ok(Array.isArray(result), 'expand returns an array');
507
+ assert.equal(result.length, 1);
508
+ assert.deepStrictEqual(result[0]['http://schema.org/name'], [{ '@value': 'Alice' }]);
509
+ });
510
+
511
+ test('JsonLd.expand of null returns empty array', async () => {
512
+ const proc = new JsonLd();
513
+ const result = await proc.expand(null);
514
+ assert.deepStrictEqual(result, []);
515
+ });
516
+
517
+ test('JsonLd.compact compacts an expanded document', async () => {
518
+ const proc = new JsonLd({ contexts: processorContexts });
519
+
520
+ // First expand
521
+ const expanded = await proc.expand(inputDocument);
522
+
523
+ // Then compact with the original contexts
524
+ const ctx = [
525
+ "https://www.w3.org/2018/credentials/v1",
526
+ "https://w3id.org/citizenship/v1",
527
+ "https://w3id.org/security/bbs/v1",
528
+ ];
529
+ const compacted = await proc.compact(expanded, ctx);
530
+
531
+ assert.ok(compacted, 'compact returned a result');
532
+ assert.ok(compacted['@context'], 'compacted has @context');
533
+
534
+ // Should have compacted type
535
+ assert.ok(
536
+ compacted.type || compacted['@type'],
537
+ `compacted should have type: ${JSON.stringify(Object.keys(compacted))}`
538
+ );
539
+ });
540
+
541
+ test('JsonLd.compact with simple context', async () => {
542
+ const proc = new JsonLd();
543
+ const expanded = [
544
+ { "http://schema.org/name": [{ "@value": "Alice" }] }
545
+ ];
546
+ const ctx = { "name": "http://schema.org/name" };
547
+ const compacted = await proc.compact(expanded, ctx);
548
+
549
+ assert.ok(compacted, 'compact returned a result');
550
+ assert.equal(compacted.name, 'Alice', 'name is compacted');
551
+ });
552
+
553
+ test('JsonLd.flatten without context returns expanded array', async () => {
554
+ const proc = new JsonLd({ contexts: processorContexts });
555
+ const result = await proc.flatten(inputDocument, null);
556
+
557
+ assert.ok(Array.isArray(result), 'flatten without ctx returns array');
558
+ assert.ok(result.length > 0, 'flattened result not empty');
559
+
560
+ // Each node should have @id
561
+ for (const node of result) {
562
+ assert.ok(node['@id'], `each flattened node should have @id: ${JSON.stringify(Object.keys(node))}`);
563
+ }
564
+ });
565
+
566
+ test('JsonLd.flatten with context returns compacted object', async () => {
567
+ const proc = new JsonLd({ contexts: processorContexts });
568
+ const ctx = [
569
+ "https://www.w3.org/2018/credentials/v1",
570
+ "https://w3id.org/citizenship/v1",
571
+ "https://w3id.org/security/bbs/v1",
572
+ ];
573
+ const result = await proc.flatten(inputDocument, ctx);
574
+
575
+ assert.ok(result && typeof result === 'object' && !Array.isArray(result), 'flatten with ctx returns object');
576
+ assert.ok(result['@context'], 'flattened result has @context');
577
+ // Should have @graph
578
+ const hasGraph = result['@graph'] || Object.keys(result).some(k => k !== '@context');
579
+ assert.ok(hasGraph, 'flattened result has @graph');
580
+ });
581
+
582
+ test('JsonLd.frame frames a document', async () => {
583
+ const proc = new JsonLd({ contexts: processorContexts });
584
+ const frameDoc = {
585
+ "@context": [
586
+ "https://www.w3.org/2018/credentials/v1",
587
+ "https://w3id.org/citizenship/v1",
588
+ "https://w3id.org/security/bbs/v1",
589
+ ],
590
+ "type": ["VerifiableCredential"],
591
+ };
592
+
593
+ const result = await proc.frame(inputDocument, frameDoc);
594
+
595
+ assert.ok(result, 'frame returned a result');
596
+ assert.ok(result['@context'], 'framed result has @context');
597
+ assert.ok(
598
+ result.type || result['@type'],
599
+ `framed result has type: ${JSON.stringify(Object.keys(result))}`
600
+ );
601
+ });
602
+
603
+ test('JsonLd.toRDF returns N-Quads string', async () => {
604
+ const proc = new JsonLd({ contexts: processorContexts });
605
+ const result = await proc.toRDF(inputDocument, { format: 'application/n-quads' });
606
+
607
+ assert.ok(typeof result === 'string', 'toRDF with n-quads format returns string');
608
+ assert.ok(result.length > 0, 'N-Quads output not empty');
609
+ // Each line should end with " ."
610
+ for (const line of result.split('\n').filter(l => l.trim())) {
611
+ assert.ok(line.trim().endsWith(' .'), `N-Quad line should end with ' .': ${line}`);
612
+ }
613
+ });
614
+
615
+ test('JsonLd.toRDF returns dataset array without format', async () => {
616
+ const proc = new JsonLd({ contexts: processorContexts });
617
+ const result = await proc.toRDF(inputDocument);
618
+
619
+ assert.ok(Array.isArray(result), 'toRDF without format returns array');
620
+ assert.ok(result.length > 0, 'dataset not empty');
621
+ // Each quad should have subject, predicate, object, graph
622
+ const first = result[0];
623
+ assert.ok(first.subject, 'quad has subject');
624
+ assert.ok(first.predicate, 'quad has predicate');
625
+ assert.ok(first.object, 'quad has object');
626
+ assert.ok('graph' in first, 'quad has graph field');
627
+ });
628
+
629
+ test('JsonLd.fromRDF converts N-Quads to JSON-LD', async () => {
630
+ const proc = new JsonLd({ contexts: processorContexts });
631
+
632
+ // First get N-Quads
633
+ const nquads = await proc.toRDF(inputDocument, { format: 'application/n-quads' });
634
+
635
+ // Then convert back
636
+ const result = proc.fromRDF(nquads);
637
+
638
+ assert.ok(Array.isArray(result), 'fromRDF returns array');
639
+ assert.ok(result.length > 0, 'fromRDF result not empty');
640
+ });
641
+
642
+ test('JsonLd.canonize produces canonical N-Quads', async () => {
643
+ const proc = new JsonLd({ contexts: processorContexts });
644
+ const result = await proc.canonize(inputDocument);
645
+
646
+ assert.ok(typeof result === 'string', 'canonize returns string');
647
+ assert.ok(result.length > 0, 'canonical N-Quads not empty');
648
+
649
+ // Canonical N-Quads should be deterministic — calling twice gives same result
650
+ const result2 = await proc.canonize(inputDocument);
651
+ assert.equal(result, result2, 'canonize is deterministic');
652
+ });
653
+
654
+ test('JsonLd.canonize with N-Quads input', async () => {
655
+ const proc = new JsonLd({ contexts: processorContexts });
656
+
657
+ // Get N-Quads from a document
658
+ const nquads = await proc.toRDF(inputDocument, { format: 'application/n-quads' });
659
+
660
+ // Canonize the N-Quads directly
661
+ const result = await proc.canonize(nquads, { inputFormat: 'application/n-quads' });
662
+
663
+ assert.ok(typeof result === 'string', 'canonize of N-Quads returns string');
664
+ assert.ok(result.length > 0, 'canonical output not empty');
665
+ });
666
+
667
+ test('JsonLd round-trip: expand → compact', async () => {
668
+ const proc = new JsonLd({ contexts: processorContexts });
669
+
670
+ const expanded = await proc.expand(inputDocument);
671
+ assert.ok(Array.isArray(expanded), 'expanded is array');
672
+
673
+ const ctx = inputDocument['@context'];
674
+ const compacted = await proc.compact(expanded, ctx);
675
+
676
+ // The round-tripped document should preserve key fields
677
+ assert.ok(compacted['@context'], 'round-tripped has @context');
678
+
679
+ // Should have the credential ID
680
+ const id = compacted.id || compacted['@id'];
681
+ assert.ok(id, 'round-tripped has id');
682
+ });
683
+
684
+ test('JsonLd reuses loader across multiple calls', async () => {
685
+ // Verify that creating the processor once and calling multiple methods works
686
+ const proc = new JsonLd({ contexts: processorContexts });
687
+
688
+ // Call multiple methods in sequence
689
+ const expanded = await proc.expand(inputDocument);
690
+ assert.ok(Array.isArray(expanded), 'first expand works');
691
+
692
+ const nquads = await proc.toRDF(inputDocument, { format: 'application/n-quads' });
693
+ assert.ok(typeof nquads === 'string', 'toRDF works after expand');
694
+
695
+ const canonical = await proc.canonize(inputDocument);
696
+ assert.ok(typeof canonical === 'string', 'canonize works after toRDF');
697
+
698
+ const expanded2 = await proc.expand(inputDocument);
699
+ assert.deepStrictEqual(expanded, expanded2, 'second expand returns same result');
700
+ });
701
+
702
+
703
+ //
704
+ // Holder-Bound BBS+ Blind Signatures — E2E Tests
705
+ //
706
+
707
+ test('BoundBls12381G2KeyPair constructor', async () => {
708
+ const kp = await NewBls12381G2KeyPair.generate({ seed });
709
+ const commitment = new Uint8Array(48); // dummy commitment
710
+ const blinded = [0, 1];
711
+
712
+ const bound = new BoundBls12381G2KeyPair({
713
+ publicKeyBase58: kp.publicKey,
714
+ privateKeyBase58: kp.privateKey,
715
+ commitment,
716
+ blinded,
717
+ });
718
+
719
+ assert.ok(bound, 'BoundBls12381G2KeyPair created');
720
+ assert.deepStrictEqual(Array.from(bound.commitment), Array.from(commitment), 'commitment matches');
721
+ assert.deepStrictEqual(bound.blinded, blinded, 'blinded indices match');
722
+ });
723
+
724
+ test('BoundBls12381G2KeyPair.from factory', async () => {
725
+ const kp = await NewBls12381G2KeyPair.generate({ seed });
726
+ const commitment = new Uint8Array(48);
727
+ const blinded = [0];
728
+
729
+ const bound = BoundBls12381G2KeyPair.from({
730
+ publicKeyBase58: kp.publicKey,
731
+ privateKeyBase58: kp.privateKey,
732
+ commitment,
733
+ blinded,
734
+ });
735
+
736
+ assert.ok(bound, 'BoundBls12381G2KeyPair.from works');
737
+ assert.deepStrictEqual(bound.blinded, [0], 'blinded indices from factory');
738
+ });
739
+
740
+ test('createCommitment + verifyCommitment round-trip', async () => {
741
+ const kp = await NewBls12381G2KeyPair.generate({ seed });
742
+ const pkBytes = bs58Decode(kp.publicKey);
743
+
744
+ // Holder creates a commitment over hidden messages
745
+ const hiddenMsg0 = new Uint8Array(Buffer.from("secret-attribute-0", "utf8"));
746
+ const hiddenMsg1 = new Uint8Array(Buffer.from("secret-attribute-1", "utf8"));
747
+ const nonce = new Uint8Array(50);
748
+ randomFillSync(nonce);
749
+ const knownMessageCount = 3; // issuer sees 3 messages
750
+
751
+ const result = await createCommitment(
752
+ pkBytes,
753
+ [hiddenMsg0, hiddenMsg1],
754
+ [0, 1],
755
+ nonce,
756
+ knownMessageCount,
757
+ );
758
+
759
+ assert.ok(result.commitment, 'commitment present');
760
+ assert.ok(result.challengeHash, 'challengeHash present');
761
+ assert.ok(result.blindingFactor, 'blindingFactor present');
762
+ assert.ok(result.proofOfHiddenMessages, 'proofOfHiddenMessages present');
763
+
764
+ // Issuer verifies the commitment
765
+ const commitmentBytes = Buffer.from(result.commitment, 'base64');
766
+ const challengeHashBytes = Buffer.from(result.challengeHash, 'base64');
767
+ const proofBytes = Buffer.from(result.proofOfHiddenMessages, 'base64');
768
+
769
+ const verified = await verifyCommitment(
770
+ pkBytes,
771
+ new Uint8Array(commitmentBytes),
772
+ new Uint8Array(proofBytes),
773
+ new Uint8Array(challengeHashBytes),
774
+ [0, 1],
775
+ nonce,
776
+ knownMessageCount,
777
+ );
778
+
779
+ assert.equal(verified, true, 'commitment verification should pass');
780
+ });
781
+
782
+ test('unblindSignature returns 112-byte signature', async () => {
783
+ // This is a unit test for the unblind function
784
+ // We need to generate a blind signature first via the crypto layer
785
+ const kp = await NewBls12381G2KeyPair.generate({ seed });
786
+ const pkBytes = bs58Decode(kp.publicKey);
787
+
788
+ const hiddenMsg = new Uint8Array(Buffer.from("hidden", "utf8"));
789
+ const nonce = new Uint8Array(50);
790
+ randomFillSync(nonce);
791
+
792
+ const commitResult = await createCommitment(
793
+ pkBytes,
794
+ [hiddenMsg],
795
+ [0],
796
+ nonce,
797
+ 2,
798
+ );
799
+
800
+ // The unblind function needs an actual blind signature, which requires bls_blind_sign
801
+ // Since we can't call that directly from JS yet, just test that the function exists
802
+ assert.ok(typeof unblindSignature === 'function', 'unblindSignature is exported');
803
+ });
804
+
805
+ test('BbsBlsHolderBoundSignature2022 constructor', () => {
806
+ const suite = new BbsBlsHolderBoundSignature2022();
807
+ assert.ok(suite, 'suite created without args');
808
+ assert.equal(suite.type, 'sec:BbsBlsHolderBoundSignature2022');
809
+ });
810
+
811
+ test('BbsBlsHolderBoundSignature2022 constructor with key', async () => {
812
+ const kp = await NewBls12381G2KeyPair.generate({ seed });
813
+
814
+ const suite = new BbsBlsHolderBoundSignature2022({
815
+ key: {
816
+ publicKeyBase58: kp.publicKey,
817
+ privateKeyBase58: kp.privateKey,
818
+ },
819
+ commitment: new Uint8Array(48),
820
+ blinded: [0],
821
+ });
822
+
823
+ assert.ok(suite, 'suite created with key and commitment');
824
+ assert.equal(suite.type, 'sec:BbsBlsHolderBoundSignature2022');
825
+ });
826
+
827
+ test('BbsBlsHolderBoundSignature2022.ensureSuiteContext adds both contexts', () => {
828
+ const suite = new BbsBlsHolderBoundSignature2022();
829
+ const doc = { '@context': 'https://www.w3.org/2018/credentials/v1' };
830
+ const result = suite.ensureSuiteContext(doc);
831
+
832
+ const ctx = result['@context'];
833
+ assert.ok(Array.isArray(ctx), '@context is array');
834
+ assert.ok(ctx.includes('https://w3id.org/security/bbs/v1'), 'has BBS context');
835
+ assert.ok(ctx.includes('https://schemas.nuggets.life/bbsBoundv1.json'), 'has bbsBound context');
836
+ });
837
+
838
+ test('BbsBlsHolderBoundSignature2022.proofType getter', () => {
839
+ const suite = new BbsBlsHolderBoundSignature2022();
840
+ const types = suite.proofType;
841
+ assert.ok(Array.isArray(types), 'proofType is array');
842
+ assert.ok(types.includes('BbsBlsHolderBoundSignature2022'), 'includes compact name');
843
+ });
844
+
845
+ test('BbsBlsHolderBoundSignatureProof2022 constructor', () => {
846
+ const suite = new BbsBlsHolderBoundSignatureProof2022();
847
+ assert.ok(suite, 'proof suite created');
848
+ assert.equal(suite.type, 'sec:BbsBlsHolderBoundSignatureProof2022');
849
+ });
850
+
851
+ test('BbsBlsHolderBoundSignatureProof2022.proofType getter', () => {
852
+ const suite = new BbsBlsHolderBoundSignatureProof2022();
853
+ const types = suite.proofType;
854
+ assert.ok(Array.isArray(types), 'proofType is array');
855
+ assert.ok(types.includes('BbsBlsHolderBoundSignatureProof2022'), 'includes compact name');
856
+ });
857
+
858
+ test('BbsBlsHolderBoundSignatureProof2022.supportedDerivedProofType getter', () => {
859
+ const suite = new BbsBlsHolderBoundSignatureProof2022();
860
+ const types = suite.supportedDerivedProofType;
861
+ assert.ok(Array.isArray(types), 'supportedDerivedProofType is array');
862
+ assert.ok(types.includes('BbsBlsHolderBoundSignature2022'), 'includes compact name');
863
+ });
864
+
865
+ test('deriveProofHolderBound function is exported', () => {
866
+ assert.ok(typeof deriveProofHolderBound === 'function', 'deriveProofHolderBound is exported');
867
+ });
868
+
869
+ //
870
+ // Holder-Bound E2E Flow: commitment → blind sign → verify → derive → verify derived
871
+ //
872
+
873
+ // Holder-bound input document: same as standard, but with bbsBoundv1.json context
874
+ const holderBoundDocument = {
875
+ ...inputDocument,
876
+ '@context': [
877
+ ...inputDocument['@context'],
878
+ 'https://schemas.nuggets.life/bbsBoundv1.json',
879
+ ],
880
+ };
881
+
882
+ // Holder-bound reveal frame: same fields but with bbsBound context
883
+ const holderBoundRevealDocument = {
884
+ '@context': [
885
+ 'https://www.w3.org/2018/credentials/v1',
886
+ 'https://w3id.org/citizenship/v1',
887
+ 'https://w3id.org/security/bbs/v1',
888
+ 'https://schemas.nuggets.life/bbsBoundv1.json',
889
+ ],
890
+ type: ['VerifiableCredential', 'PermanentResidentCard'],
891
+ credentialSubject: {
892
+ '@explicit': true,
893
+ type: ['PermanentResident', 'Person'],
894
+ givenName: {},
895
+ familyName: {},
896
+ gender: {},
897
+ },
898
+ };
899
+
900
+ test('createBlindSignCommitmentContext returns knownMessageCount and nonce', async () => {
901
+ const suite = new BbsBlsHolderBoundSignature2022({
902
+ key: {
903
+ id: keyPair.id,
904
+ controller: keyPair.controller,
905
+ publicKeyBase58: keyPair.publicKeyBase58,
906
+ privateKeyBase58: keyPair.privateKeyBase58,
907
+ },
908
+ });
909
+
910
+ const result = await suite.createBlindSignCommitmentContext({
911
+ document: holderBoundDocument,
912
+ contexts,
913
+ });
914
+
915
+ assert.ok(result, 'createBlindSignCommitmentContext returned a result');
916
+ assert.equal(typeof result.knownMessageCount, 'number', 'knownMessageCount is a number');
917
+ assert.equal(result.knownMessageCount, 25, 'knownMessageCount matches expected (25 N-Quad statements)');
918
+ assert.equal(typeof result.nonce, 'string', 'nonce is a string');
919
+
920
+ // nonce should be base64-encoded 50 bytes
921
+ const nonceBytes = Buffer.from(result.nonce, 'base64');
922
+ assert.equal(nonceBytes.length, 50, 'nonce is 50 bytes');
923
+ });
924
+
925
+ test('holder-bound E2E: blind sign → verify → derive → verify derived', async () => {
926
+ // Use the SAME key pair for commitment and signing — keyPair from the static JSON file.
927
+ // The commitment must be created with the issuer's public key (same key that will sign).
928
+ const pkBytes = bs58Decode(keyPair.publicKeyBase58);
929
+
930
+ // === Step 1: Holder creates commitment over hidden messages ===
931
+ const blindedMessages = [
932
+ new Uint8Array(Buffer.from('blind message 1', 'utf8')),
933
+ new Uint8Array(Buffer.from('blind message 2', 'utf8')),
934
+ ];
935
+ const blindedIndices = [0, 1];
936
+
937
+ // Get knownMessageCount dynamically from createBlindSignCommitmentContext
938
+ const commitCtx = await new BbsBlsHolderBoundSignature2022({
939
+ key: {
940
+ id: keyPair.id,
941
+ controller: keyPair.controller,
942
+ publicKeyBase58: keyPair.publicKeyBase58,
943
+ privateKeyBase58: keyPair.privateKeyBase58,
944
+ },
945
+ }).createBlindSignCommitmentContext({ document: holderBoundDocument, contexts });
946
+ const knownMessageCount = commitCtx.knownMessageCount;
947
+
948
+ const commitNonce = new Uint8Array(50);
949
+ randomFillSync(commitNonce);
950
+
951
+ const commitResult = await createCommitment(
952
+ pkBytes,
953
+ blindedMessages,
954
+ blindedIndices,
955
+ commitNonce,
956
+ knownMessageCount,
957
+ );
958
+
959
+ assert.ok(commitResult.commitment, 'commitment created');
960
+ assert.ok(commitResult.blindingFactor, 'blindingFactor returned');
961
+
962
+ // === Step 2: Issuer verifies commitment ===
963
+ const commitmentBytes = Buffer.from(commitResult.commitment, 'base64');
964
+ const challengeHashBytes = Buffer.from(commitResult.challengeHash, 'base64');
965
+ const proofOfHiddenBytes = Buffer.from(commitResult.proofOfHiddenMessages, 'base64');
966
+
967
+ const commitVerified = await verifyCommitment(
968
+ pkBytes,
969
+ new Uint8Array(commitmentBytes),
970
+ new Uint8Array(proofOfHiddenBytes),
971
+ new Uint8Array(challengeHashBytes),
972
+ blindedIndices,
973
+ commitNonce,
974
+ knownMessageCount,
975
+ );
976
+ assert.equal(commitVerified, true, 'commitment verification should pass');
977
+
978
+ // === Step 3: Issuer blind-signs the document ===
979
+ const sigSuite = new BbsBlsHolderBoundSignature2022({
980
+ key: {
981
+ id: keyPair.id,
982
+ controller: keyPair.controller,
983
+ publicKeyBase58: keyPair.publicKeyBase58,
984
+ privateKeyBase58: keyPair.privateKeyBase58,
985
+ },
986
+ commitment: new Uint8Array(commitmentBytes),
987
+ blinded: blindedIndices,
988
+ });
989
+
990
+ const blindProof = await sigSuite.createProof({
991
+ document: holderBoundDocument,
992
+ contexts,
993
+ });
994
+
995
+ assert.ok(blindProof, 'createProof returned a result');
996
+ assert.equal(blindProof.type, 'BbsBlsHolderBoundSignature2022', 'proof type is holder-bound');
997
+ assert.ok(blindProof.proofValue, 'proof has proofValue');
998
+ assert.equal(blindProof.proofPurpose, 'assertionMethod');
999
+
1000
+ // proofValue should be 112 bytes (blind signature)
1001
+ const proofValueBytes = Buffer.from(blindProof.proofValue, 'base64');
1002
+ assert.equal(proofValueBytes.length, 112, 'blind signature is 112 bytes');
1003
+
1004
+ // Build signed document with embedded proof
1005
+ const blindSignedDoc = { ...holderBoundDocument, proof: blindProof };
1006
+
1007
+ // === Step 4: Holder verifies the blind-signed document ===
1008
+ const blindingFactorBytes = Buffer.from(commitResult.blindingFactor, 'base64');
1009
+ const blindedMsgBuffers = blindedMessages.map(m => Buffer.from(m).toString('base64'));
1010
+
1011
+ const verifyResult = await sigSuite.verifyProof({
1012
+ document: blindSignedDoc,
1013
+ blindingFactor: new Uint8Array(blindingFactorBytes),
1014
+ blindedMessages: blindedMessages,
1015
+ contexts,
1016
+ });
1017
+
1018
+ assert.equal(
1019
+ verifyResult.verified, true,
1020
+ `holder-bound verify should pass, got error: ${verifyResult.error}`
1021
+ );
1022
+
1023
+ // === Step 5: Derive selective disclosure proof ===
1024
+ const derivedDoc = await deriveProofHolderBound(
1025
+ blindSignedDoc,
1026
+ holderBoundRevealDocument,
1027
+ {
1028
+ blindingFactor: commitResult.blindingFactor,
1029
+ blindedMessages: blindedMsgBuffers,
1030
+ nonce: 'test-nonce-12345',
1031
+ contexts,
1032
+ },
1033
+ );
1034
+
1035
+ assert.ok(derivedDoc, 'deriveProofHolderBound returned a result');
1036
+ assert.ok(derivedDoc.proof, 'derived document has proof');
1037
+ assert.equal(
1038
+ derivedDoc.proof.type, 'BbsBlsHolderBoundSignatureProof2022',
1039
+ 'derived proof type is BbsBlsHolderBoundSignatureProof2022'
1040
+ );
1041
+ assert.ok(derivedDoc.proof.proofValue, 'derived proof has proofValue');
1042
+ assert.ok(derivedDoc.proof.nonce, 'derived proof has nonce');
1043
+
1044
+ // Check selective disclosure: only revealed fields should be present
1045
+ assert.ok(derivedDoc.credentialSubject, 'has credentialSubject');
1046
+ assert.ok(derivedDoc.credentialSubject.givenName, 'has givenName (revealed)');
1047
+ assert.ok(derivedDoc.credentialSubject.familyName, 'has familyName (revealed)');
1048
+ assert.ok(derivedDoc.credentialSubject.gender, 'has gender (revealed)');
1049
+
1050
+ // === Step 6: Verifier verifies the derived proof ===
1051
+ const verifyDerivedResult = await ldVerify({
1052
+ document: derivedDoc,
1053
+ contexts,
1054
+ });
1055
+
1056
+ assert.equal(
1057
+ verifyDerivedResult.verified, true,
1058
+ `derived proof should verify, got error: ${verifyDerivedResult.error}`
1059
+ );
1060
+ });
1061
+
1062
+ test('holder-bound class-based deriveProof + verifyProof', async () => {
1063
+ // Use the SAME key pair for commitment and signing
1064
+ const pkBytes = bs58Decode(keyPair.publicKeyBase58);
1065
+
1066
+ const blindedMessages = [
1067
+ new Uint8Array(Buffer.from('holder-secret-1', 'utf8')),
1068
+ ];
1069
+ const blindedIndices = [0];
1070
+
1071
+ const commitNonce = new Uint8Array(50);
1072
+ randomFillSync(commitNonce);
1073
+
1074
+ // Get knownMessageCount dynamically
1075
+ const commitCtx = await new BbsBlsHolderBoundSignature2022({
1076
+ key: {
1077
+ id: keyPair.id,
1078
+ controller: keyPair.controller,
1079
+ publicKeyBase58: keyPair.publicKeyBase58,
1080
+ privateKeyBase58: keyPair.privateKeyBase58,
1081
+ },
1082
+ }).createBlindSignCommitmentContext({ document: holderBoundDocument, contexts });
1083
+ const knownMessageCount = commitCtx.knownMessageCount;
1084
+
1085
+ const commitResult = await createCommitment(
1086
+ pkBytes, blindedMessages, blindedIndices, commitNonce, knownMessageCount,
1087
+ );
1088
+
1089
+ // Issuer blind-signs
1090
+ const sigSuite = new BbsBlsHolderBoundSignature2022({
1091
+ key: {
1092
+ id: keyPair.id,
1093
+ controller: keyPair.controller,
1094
+ publicKeyBase58: keyPair.publicKeyBase58,
1095
+ privateKeyBase58: keyPair.privateKeyBase58,
1096
+ },
1097
+ commitment: new Uint8Array(Buffer.from(commitResult.commitment, 'base64')),
1098
+ blinded: blindedIndices,
1099
+ });
1100
+
1101
+ const blindProof = await sigSuite.createProof({
1102
+ document: holderBoundDocument,
1103
+ contexts,
1104
+ });
1105
+ const blindSignedDoc = { ...holderBoundDocument, proof: blindProof };
1106
+
1107
+ // Class-based derive
1108
+ const deriveSuite = new BbsBlsHolderBoundSignatureProof2022();
1109
+ const blindingFactorBytes = Buffer.from(commitResult.blindingFactor, 'base64');
1110
+
1111
+ const derivedDoc = await deriveSuite.deriveProof({
1112
+ document: blindSignedDoc,
1113
+ revealDocument: holderBoundRevealDocument,
1114
+ blindingFactor: new Uint8Array(blindingFactorBytes),
1115
+ blindedMessages: blindedMessages, // Array<Uint8Array>
1116
+ nonce: 'class-based-nonce',
1117
+ contexts,
1118
+ });
1119
+
1120
+ assert.ok(derivedDoc, 'class-based deriveProof returned a result');
1121
+ assert.equal(derivedDoc.proof.type, 'BbsBlsHolderBoundSignatureProof2022');
1122
+
1123
+ // Class-based verify
1124
+ const verifyResult = await deriveSuite.verifyProof({
1125
+ document: derivedDoc,
1126
+ contexts,
1127
+ });
1128
+
1129
+ assert.equal(
1130
+ verifyResult.verified, true,
1131
+ `class-based verify should pass, got error: ${verifyResult.error}`
1132
+ );
1133
+ });
1134
+
1135
+ // Helper: decode base58 string to Uint8Array
1136
+ function bs58Decode(str) {
1137
+ const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
1138
+ let result = [0];
1139
+ for (let i = 0; i < str.length; i++) {
1140
+ const p = ALPHABET.indexOf(str[i]);
1141
+ if (p < 0) throw new Error(`Invalid base58 char: ${str[i]}`);
1142
+ let carry = p;
1143
+ for (let j = 0; j < result.length; j++) {
1144
+ carry += result[j] * 58;
1145
+ result[j] = carry & 0xff;
1146
+ carry >>= 8;
1147
+ }
1148
+ while (carry > 0) {
1149
+ result.push(carry & 0xff);
1150
+ carry >>= 8;
1151
+ }
1152
+ }
1153
+ // Leading zeros
1154
+ for (let i = 0; i < str.length && str[i] === '1'; i++) {
1155
+ result.push(0);
1156
+ }
1157
+ return new Uint8Array(result.reverse());
1158
+ }