@nuggetslife/vc 0.0.21 → 0.0.23

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/src/jose.rs ADDED
@@ -0,0 +1,415 @@
1
+ use serde_json::Value;
2
+
3
+ use vc::jose::{
4
+ functions::{
5
+ jose_compact_sign_json, jose_compact_verify_json, jose_decrypt, jose_decrypt_json,
6
+ jose_encrypt, jose_flattened_sign_json, jose_general_encrypt_json, jose_general_sign_json,
7
+ jose_generate_key_pair, jose_generate_key_pair_jwk, jose_verify_json,
8
+ },
9
+ types::{
10
+ TOKEN_TYPE_DIDCOM_ENCRYPTED, TOKEN_TYPE_DIDCOM_SIGNED, TOKEN_TYPE_JWT,
11
+ },
12
+ };
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Enums — numeric values match @nuggetslife/ffi-jose exactly
16
+ // ---------------------------------------------------------------------------
17
+
18
+ #[napi]
19
+ pub enum JoseNamedCurve {
20
+ P256 = 0,
21
+ P384 = 1,
22
+ P521 = 2,
23
+ Secp256k1 = 3,
24
+ Ed25519 = 4,
25
+ Ed448 = 5,
26
+ X25519 = 6,
27
+ X448 = 7,
28
+ }
29
+
30
+ impl JoseNamedCurve {
31
+ fn to_rust_string(&self) -> &'static str {
32
+ match self {
33
+ JoseNamedCurve::P256 => "P-256",
34
+ JoseNamedCurve::P384 => "P-384",
35
+ JoseNamedCurve::P521 => "P-521",
36
+ JoseNamedCurve::Secp256k1 => "SECP256K1",
37
+ JoseNamedCurve::Ed25519 => "ED25519",
38
+ JoseNamedCurve::Ed448 => "ED448",
39
+ JoseNamedCurve::X25519 => "X25519",
40
+ JoseNamedCurve::X448 => "X448",
41
+ }
42
+ }
43
+ }
44
+
45
+ #[napi]
46
+ pub enum JoseContentEncryption {
47
+ A128gcm = 0,
48
+ A192gcm = 1,
49
+ A256gcm = 2,
50
+ A128cbcHs256 = 3,
51
+ A192cbcHs384 = 4,
52
+ A256cbcHs512 = 5,
53
+ }
54
+
55
+ impl JoseContentEncryption {
56
+ fn to_rust_string(&self) -> &'static str {
57
+ match self {
58
+ JoseContentEncryption::A128gcm => "A128GCM",
59
+ JoseContentEncryption::A192gcm => "A192GCM",
60
+ JoseContentEncryption::A256gcm => "A256GCM",
61
+ JoseContentEncryption::A128cbcHs256 => "A128CBC-HS256",
62
+ JoseContentEncryption::A192cbcHs384 => "A192CBC-HS384",
63
+ JoseContentEncryption::A256cbcHs512 => "A256CBC-HS512",
64
+ }
65
+ }
66
+ }
67
+
68
+ #[napi]
69
+ pub enum JoseKeyEncryption {
70
+ Dir = 0,
71
+ EcdhEs = 1,
72
+ EcdhEsA128kw = 2,
73
+ EcdhEsA192kw = 3,
74
+ EcdhEsA256kw = 4,
75
+ Rsa1_5 = 5,
76
+ RsaOaep = 6,
77
+ RsaOaep256 = 7,
78
+ RsaOaep384 = 8,
79
+ RsaOaep512 = 9,
80
+ Pbes2Hs256A128kw = 10,
81
+ Pbes2Hs384A192kw = 11,
82
+ Pbes2Hs512A256kw = 12,
83
+ A128kw = 13,
84
+ A192kw = 14,
85
+ A256kw = 15,
86
+ A128gcmkw = 16,
87
+ A192gcmkw = 17,
88
+ A256gcmkw = 18,
89
+ }
90
+
91
+ impl JoseKeyEncryption {
92
+ fn to_rust_string(&self) -> &'static str {
93
+ match self {
94
+ JoseKeyEncryption::Dir => "dir",
95
+ JoseKeyEncryption::EcdhEs => "ECDH-ES",
96
+ JoseKeyEncryption::EcdhEsA128kw => "ECDH-ES+A128KW",
97
+ JoseKeyEncryption::EcdhEsA192kw => "ECDH-ES+A192KW",
98
+ JoseKeyEncryption::EcdhEsA256kw => "ECDH-ES+A256KW",
99
+ JoseKeyEncryption::Rsa1_5 => "RSA1_5",
100
+ JoseKeyEncryption::RsaOaep => "RSA-OAEP",
101
+ JoseKeyEncryption::RsaOaep256 => "RSA-OAEP-256",
102
+ JoseKeyEncryption::RsaOaep384 => "RSA-OAEP-384",
103
+ JoseKeyEncryption::RsaOaep512 => "RSA-OAEP-512",
104
+ JoseKeyEncryption::Pbes2Hs256A128kw => "PBES2-HS256+A128KW",
105
+ JoseKeyEncryption::Pbes2Hs384A192kw => "PBES2-HS384+A192KW",
106
+ JoseKeyEncryption::Pbes2Hs512A256kw => "PBES2-HS512+A256KW",
107
+ JoseKeyEncryption::A128kw => "A128KW",
108
+ JoseKeyEncryption::A192kw => "A192KW",
109
+ JoseKeyEncryption::A256kw => "A256KW",
110
+ JoseKeyEncryption::A128gcmkw => "A128GCMKW",
111
+ JoseKeyEncryption::A192gcmkw => "A192GCMKW",
112
+ JoseKeyEncryption::A256gcmkw => "A256GCMKW",
113
+ }
114
+ }
115
+ }
116
+
117
+ #[napi]
118
+ pub enum JoseSigningAlgorithm {
119
+ Es256 = 0,
120
+ Es384 = 1,
121
+ Es512 = 2,
122
+ Es256k = 3,
123
+ Eddsa = 4,
124
+ Hs256 = 5,
125
+ Hs384 = 6,
126
+ Hs512 = 7,
127
+ Rs256 = 8,
128
+ Rs384 = 9,
129
+ Rs512 = 10,
130
+ Ps256 = 11,
131
+ Ps384 = 12,
132
+ Ps512 = 13,
133
+ }
134
+
135
+ impl JoseSigningAlgorithm {
136
+ fn to_rust_string(&self) -> &'static str {
137
+ match self {
138
+ JoseSigningAlgorithm::Es256 => "ES256",
139
+ JoseSigningAlgorithm::Es384 => "ES384",
140
+ JoseSigningAlgorithm::Es512 => "ES512",
141
+ JoseSigningAlgorithm::Es256k => "ES256K",
142
+ JoseSigningAlgorithm::Eddsa => "EDDSA",
143
+ JoseSigningAlgorithm::Hs256 => "HS256",
144
+ JoseSigningAlgorithm::Hs384 => "HS384",
145
+ JoseSigningAlgorithm::Hs512 => "HS512",
146
+ JoseSigningAlgorithm::Rs256 => "RS256",
147
+ JoseSigningAlgorithm::Rs384 => "RS384",
148
+ JoseSigningAlgorithm::Rs512 => "RS512",
149
+ JoseSigningAlgorithm::Ps256 => "PS256",
150
+ JoseSigningAlgorithm::Ps384 => "PS384",
151
+ JoseSigningAlgorithm::Ps512 => "PS512",
152
+ }
153
+ }
154
+ }
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // Result types
158
+ // ---------------------------------------------------------------------------
159
+
160
+ #[napi(object)]
161
+ pub struct JoseEncryptResult {
162
+ pub ciphertext: String,
163
+ pub tag: Option<String>,
164
+ }
165
+
166
+ // ---------------------------------------------------------------------------
167
+ // Key Generation (Task 9)
168
+ // ---------------------------------------------------------------------------
169
+
170
+ /// Generate a JWK key pair and return the JWK (public + private) as a JSON object.
171
+ /// Matches ffi-jose `generateJWK({ namedCurve })`.
172
+ #[napi]
173
+ pub fn generate_jwk(named_curve: JoseNamedCurve) -> napi::Result<Value> {
174
+ let curve = named_curve.to_rust_string().to_string();
175
+ let jwk_str = jose_generate_key_pair_jwk(curve)
176
+ .map_err(|e| napi::Error::from_reason(format!("generateJWK failed: {e}")))?;
177
+ serde_json::from_str(&jwk_str)
178
+ .map_err(|e| napi::Error::from_reason(format!("generateJWK parse failed: {e}")))
179
+ }
180
+
181
+ /// Generate a full key pair (JWK, PEM, DER formats) and return as a JSON object.
182
+ /// Matches ffi-jose `generateKeyPair(type, { namedCurve })`.
183
+ #[napi]
184
+ pub fn generate_key_pair(named_curve: JoseNamedCurve) -> napi::Result<Value> {
185
+ let curve = named_curve.to_rust_string().to_string();
186
+ let kp_str = jose_generate_key_pair(curve)
187
+ .map_err(|e| napi::Error::from_reason(format!("generateKeyPair failed: {e}")))?;
188
+ serde_json::from_str(&kp_str)
189
+ .map_err(|e| napi::Error::from_reason(format!("generateKeyPair parse failed: {e}")))
190
+ }
191
+
192
+ // ---------------------------------------------------------------------------
193
+ // Symmetric Encrypt/Decrypt (Task 10)
194
+ // ---------------------------------------------------------------------------
195
+
196
+ /// Low-level symmetric encryption. Key and IV as hex strings, plaintext as base64.
197
+ /// Matches ffi-jose `encrypt(enc, plaintext, cek, iv, aad, didcomm)`.
198
+ #[napi(js_name = "joseEncrypt")]
199
+ pub fn jose_encrypt_napi(
200
+ enc: JoseContentEncryption,
201
+ key: String,
202
+ iv: String,
203
+ message: String,
204
+ aad: Option<String>,
205
+ ) -> napi::Result<JoseEncryptResult> {
206
+ let result = jose_encrypt(
207
+ enc.to_rust_string().to_string(),
208
+ key,
209
+ iv,
210
+ message,
211
+ aad.unwrap_or_default(),
212
+ )
213
+ .map_err(|e| napi::Error::from_reason(format!("joseEncrypt failed: {e}")))?;
214
+
215
+ Ok(JoseEncryptResult {
216
+ ciphertext: result.ciphertext,
217
+ tag: result.tag,
218
+ })
219
+ }
220
+
221
+ /// Low-level symmetric decryption. Returns base64-encoded plaintext.
222
+ /// Matches ffi-jose `decrypt(enc, cek, ciphertext, iv, tag, aad)`.
223
+ #[napi(js_name = "joseDecrypt")]
224
+ pub fn jose_decrypt_napi(
225
+ enc: JoseContentEncryption,
226
+ key: String,
227
+ iv: String,
228
+ ciphertext: String,
229
+ aad: Option<String>,
230
+ tag: Option<String>,
231
+ ) -> napi::Result<String> {
232
+ jose_decrypt(
233
+ enc.to_rust_string().to_string(),
234
+ key,
235
+ iv,
236
+ ciphertext,
237
+ aad.unwrap_or_default(),
238
+ tag,
239
+ )
240
+ .map_err(|e| napi::Error::from_reason(format!("joseDecrypt failed: {e}")))
241
+ }
242
+
243
+ // ---------------------------------------------------------------------------
244
+ // JWE — JSON Web Encryption (Task 11)
245
+ // ---------------------------------------------------------------------------
246
+
247
+ /// Resolve the `typ` header from the `didcomm` flag.
248
+ fn resolve_typ_encrypted(didcomm: Option<bool>) -> &'static str {
249
+ if didcomm.unwrap_or(false) {
250
+ TOKEN_TYPE_DIDCOM_ENCRYPTED
251
+ } else {
252
+ TOKEN_TYPE_JWT
253
+ }
254
+ }
255
+
256
+ /// Resolve the `typ` header from the `didcomm` flag for signed tokens.
257
+ fn resolve_typ_signed(didcomm: Option<bool>) -> &'static str {
258
+ if didcomm.unwrap_or(false) {
259
+ TOKEN_TYPE_DIDCOM_SIGNED
260
+ } else {
261
+ TOKEN_TYPE_JWT
262
+ }
263
+ }
264
+
265
+ /// Encrypt a JSON payload for one or more recipients using JWE General JSON serialization.
266
+ /// Matches ffi-jose `generalEncryptJson(alg, enc, payload, recipients, didcomm)`.
267
+ #[napi]
268
+ pub fn general_encrypt_json(
269
+ alg: JoseKeyEncryption,
270
+ enc: JoseContentEncryption,
271
+ payload: Value,
272
+ recipients: Vec<Value>,
273
+ didcomm: Option<bool>,
274
+ ) -> napi::Result<Value> {
275
+ let typ = resolve_typ_encrypted(didcomm);
276
+ let payload_str = serde_json::to_string(&payload)
277
+ .map_err(|e| napi::Error::from_reason(format!("generalEncryptJson serialize payload: {e}")))?;
278
+ let recipients_str = serde_json::to_string(&recipients)
279
+ .map_err(|e| napi::Error::from_reason(format!("generalEncryptJson serialize recipients: {e}")))?;
280
+
281
+ let result = jose_general_encrypt_json(
282
+ alg.to_rust_string().to_string(),
283
+ enc.to_rust_string().to_string(),
284
+ typ.to_string(),
285
+ payload_str,
286
+ recipients_str,
287
+ None,
288
+ )
289
+ .map_err(|e| napi::Error::from_reason(format!("generalEncryptJson failed: {e}")))?;
290
+
291
+ serde_json::from_str(&result)
292
+ .map_err(|e| napi::Error::from_reason(format!("generalEncryptJson parse result: {e}")))
293
+ }
294
+
295
+ /// Decrypt a JWE JSON object using a JWK private key.
296
+ /// Matches ffi-jose `decryptJson(jwe, jwk)`.
297
+ #[napi]
298
+ pub fn decrypt_json(jwe: Value, jwk: Value) -> napi::Result<Value> {
299
+ let jwe_str = serde_json::to_string(&jwe)
300
+ .map_err(|e| napi::Error::from_reason(format!("decryptJson serialize jwe: {e}")))?;
301
+ let jwk_str = serde_json::to_string(&jwk)
302
+ .map_err(|e| napi::Error::from_reason(format!("decryptJson serialize jwk: {e}")))?;
303
+
304
+ let result = jose_decrypt_json(jwe_str, jwk_str)
305
+ .map_err(|e| napi::Error::from_reason(format!("decryptJson failed: {e}")))?;
306
+
307
+ serde_json::from_str(&result)
308
+ .map_err(|e| napi::Error::from_reason(format!("decryptJson parse result: {e}")))
309
+ }
310
+
311
+ // ---------------------------------------------------------------------------
312
+ // JWS — JSON Web Signature (Task 12)
313
+ // ---------------------------------------------------------------------------
314
+
315
+ /// Sign a JSON payload using JWS Compact serialization.
316
+ /// Matches ffi-jose `compactSignJson(alg, payload, jwk, didcomm)`.
317
+ #[napi]
318
+ pub fn compact_sign_json(
319
+ alg: JoseSigningAlgorithm,
320
+ payload: Value,
321
+ jwk: Value,
322
+ didcomm: Option<bool>,
323
+ ) -> napi::Result<String> {
324
+ let typ = resolve_typ_signed(didcomm);
325
+ let payload_str = serde_json::to_string(&payload)
326
+ .map_err(|e| napi::Error::from_reason(format!("compactSignJson serialize payload: {e}")))?;
327
+ let jwk_str = serde_json::to_string(&jwk)
328
+ .map_err(|e| napi::Error::from_reason(format!("compactSignJson serialize jwk: {e}")))?;
329
+
330
+ jose_compact_sign_json(
331
+ alg.to_rust_string().to_string(),
332
+ typ.to_string(),
333
+ payload_str,
334
+ jwk_str,
335
+ )
336
+ .map_err(|e| napi::Error::from_reason(format!("compactSignJson failed: {e}")))
337
+ }
338
+
339
+ /// Verify a JWS Compact serialization and return the payload.
340
+ /// Matches ffi-jose `compactJsonVerify(jws, jwk)`.
341
+ #[napi]
342
+ pub fn compact_json_verify(jws: String, jwk: Value) -> napi::Result<Value> {
343
+ let jwk_str = serde_json::to_string(&jwk)
344
+ .map_err(|e| napi::Error::from_reason(format!("compactJsonVerify serialize jwk: {e}")))?;
345
+
346
+ let result = jose_compact_verify_json(jws, jwk_str)
347
+ .map_err(|e| napi::Error::from_reason(format!("compactJsonVerify failed: {e}")))?;
348
+
349
+ serde_json::from_str(&result)
350
+ .map_err(|e| napi::Error::from_reason(format!("compactJsonVerify parse result: {e}")))
351
+ }
352
+
353
+ /// Sign a JSON payload using JWS Flattened JSON serialization.
354
+ /// Matches ffi-jose `flattenedSignJson(alg, payload, jwk, didcomm)`.
355
+ #[napi]
356
+ pub fn flattened_sign_json(
357
+ alg: JoseSigningAlgorithm,
358
+ payload: Value,
359
+ jwk: Value,
360
+ didcomm: Option<bool>,
361
+ ) -> napi::Result<Value> {
362
+ let typ = resolve_typ_signed(didcomm);
363
+ let payload_str = serde_json::to_string(&payload)
364
+ .map_err(|e| napi::Error::from_reason(format!("flattenedSignJson serialize payload: {e}")))?;
365
+ let jwk_str = serde_json::to_string(&jwk)
366
+ .map_err(|e| napi::Error::from_reason(format!("flattenedSignJson serialize jwk: {e}")))?;
367
+
368
+ let result = jose_flattened_sign_json(
369
+ alg.to_rust_string().to_string(),
370
+ typ.to_string(),
371
+ payload_str,
372
+ jwk_str,
373
+ )
374
+ .map_err(|e| napi::Error::from_reason(format!("flattenedSignJson failed: {e}")))?;
375
+
376
+ serde_json::from_str(&result)
377
+ .map_err(|e| napi::Error::from_reason(format!("flattenedSignJson parse result: {e}")))
378
+ }
379
+
380
+ /// Verify a JWS Flattened or General JSON serialization and return the payload.
381
+ /// Matches ffi-jose `jsonVerify(jws, jwk)`.
382
+ #[napi]
383
+ pub fn json_verify(jws: Value, jwk: Value) -> napi::Result<Value> {
384
+ let jws_str = serde_json::to_string(&jws)
385
+ .map_err(|e| napi::Error::from_reason(format!("jsonVerify serialize jws: {e}")))?;
386
+ let jwk_str = serde_json::to_string(&jwk)
387
+ .map_err(|e| napi::Error::from_reason(format!("jsonVerify serialize jwk: {e}")))?;
388
+
389
+ let result = jose_verify_json(jws_str, jwk_str)
390
+ .map_err(|e| napi::Error::from_reason(format!("jsonVerify failed: {e}")))?;
391
+
392
+ serde_json::from_str(&result)
393
+ .map_err(|e| napi::Error::from_reason(format!("jsonVerify parse result: {e}")))
394
+ }
395
+
396
+ /// Sign a JSON payload using JWS General JSON serialization with multiple signers.
397
+ /// Matches ffi-jose `generalSignJson(payload, jwks, didcomm)`.
398
+ #[napi]
399
+ pub fn general_sign_json(
400
+ payload: Value,
401
+ jwks: Vec<Value>,
402
+ didcomm: Option<bool>,
403
+ ) -> napi::Result<Value> {
404
+ let typ = resolve_typ_signed(didcomm);
405
+ let payload_str = serde_json::to_string(&payload)
406
+ .map_err(|e| napi::Error::from_reason(format!("generalSignJson serialize payload: {e}")))?;
407
+ let jwks_str = serde_json::to_string(&jwks)
408
+ .map_err(|e| napi::Error::from_reason(format!("generalSignJson serialize jwks: {e}")))?;
409
+
410
+ let result = jose_general_sign_json(typ.to_string(), payload_str, jwks_str)
411
+ .map_err(|e| napi::Error::from_reason(format!("generalSignJson failed: {e}")))?;
412
+
413
+ serde_json::from_str(&result)
414
+ .map_err(|e| napi::Error::from_reason(format!("generalSignJson parse result: {e}")))
415
+ }
package/src/jsonld.rs CHANGED
@@ -1,16 +1,12 @@
1
1
  use serde_json::{json, Value};
2
- use std::collections::HashMap;
3
- use std::num::NonZeroUsize;
4
2
  use std::sync::Arc;
5
3
 
6
- use lru::LruCache;
7
-
8
4
  use vc::{
9
- document_loader::nuggets::{load_nuggets_context, DocumentLoader},
5
+ document_loader::nuggets::DocumentLoader,
10
6
  jsonld::{self, context_resolver::ContextResolver},
11
7
  };
12
8
 
13
- use crate::ld_signatures::parse_contexts;
9
+ use crate::ld_signatures::loader_from_contexts;
14
10
 
15
11
  /// JSON-LD processor — drop-in replacement for the jsonld.js API.
16
12
  ///
@@ -37,26 +33,8 @@ impl JsonLd {
37
33
  /// of the built-in nuggets contexts).
38
34
  #[napi(constructor)]
39
35
  pub fn new(options: Option<Value>) -> Self {
40
- let additional: HashMap<String, Value> = options
41
- .as_ref()
42
- .map(|o| parse_contexts(o))
43
- .unwrap_or_default();
44
-
45
- let mut ctx = load_nuggets_context();
46
- ctx.extend(additional);
47
-
48
- let resolver = Arc::new(tokio::sync::RwLock::new(
49
- vc::did_resolver::resolver::Resolver::new(
50
- vc::did_resolver::resolver::ResolverRegistry::new(),
51
- None,
52
- ),
53
- ));
54
- let opts = vc::did_resolver::resolver::DidResolutionOptions::default();
55
- let loader = Arc::new(DocumentLoader::new(ctx, resolver, opts));
56
- let cr = Arc::new(ContextResolver::new(LruCache::new(
57
- NonZeroUsize::new(10).unwrap(),
58
- )));
59
-
36
+ let contexts = options.as_ref().and_then(|o| o.get("contexts"));
37
+ let (loader, cr) = loader_from_contexts(contexts);
60
38
  Self { loader, cr }
61
39
  }
62
40
 
@@ -47,6 +47,18 @@ pub(crate) fn create_document_loader(
47
47
  (loader, cr)
48
48
  }
49
49
 
50
+ /// Parse an optional nonce string: try base64 decode, fall back to raw bytes.
51
+ fn parse_nonce(options: &Value) -> Option<Vec<u8>> {
52
+ options
53
+ .get("nonce")
54
+ .and_then(|v| v.as_str())
55
+ .map(|s| {
56
+ BASE64_STANDARD
57
+ .decode(s)
58
+ .unwrap_or_else(|_| s.as_bytes().to_vec())
59
+ })
60
+ }
61
+
50
62
  /// Parse the `contexts` field from options JSON into a HashMap.
51
63
  pub(crate) fn parse_contexts(options: &Value) -> HashMap<String, Value> {
52
64
  options
@@ -60,6 +72,20 @@ pub(crate) fn parse_contexts(options: &Value) -> HashMap<String, Value> {
60
72
  .unwrap_or_default()
61
73
  }
62
74
 
75
+ /// Create a DocumentLoader + ContextResolver from an optional contexts Value.
76
+ ///
77
+ /// This is the common pattern used by all class-based suite methods:
78
+ /// takes the `contexts` field from options and creates the loader pair.
79
+ pub(crate) fn loader_from_contexts(
80
+ contexts: Option<&Value>,
81
+ ) -> (Arc<DocumentLoader>, Arc<ContextResolver>) {
82
+ let additional = contexts
83
+ .and_then(|v| v.as_object())
84
+ .map(|obj| obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
85
+ .unwrap_or_default();
86
+ create_document_loader(additional)
87
+ }
88
+
63
89
  /// Sign a document with BbsBlsSignature2020 and embed the proof.
64
90
  ///
65
91
  /// Input: `{ document, keyPair: {id, controller, publicKeyBase58, privateKeyBase58}, contexts? }`
@@ -135,14 +161,7 @@ pub async fn ld_derive_proof(options: Value) -> napi::Result<Value> {
135
161
  .cloned()
136
162
  .ok_or_else(|| napi::Error::from_reason("missing 'revealDocument' in options"))?;
137
163
 
138
- let nonce = options
139
- .get("nonce")
140
- .and_then(|v| v.as_str())
141
- .map(|s| {
142
- BASE64_STANDARD
143
- .decode(s)
144
- .unwrap_or_else(|_| s.as_bytes().to_vec())
145
- });
164
+ let nonce = parse_nonce(&options);
146
165
 
147
166
  let additional_contexts = parse_contexts(&options);
148
167
 
@@ -163,14 +182,7 @@ pub async fn derive_proof(
163
182
  reveal_document: Value,
164
183
  options: Value,
165
184
  ) -> napi::Result<Value> {
166
- let nonce = options
167
- .get("nonce")
168
- .and_then(|v| v.as_str())
169
- .map(|s| {
170
- BASE64_STANDARD
171
- .decode(s)
172
- .unwrap_or_else(|_| s.as_bytes().to_vec())
173
- });
185
+ let nonce = parse_nonce(&options);
174
186
 
175
187
  let additional_contexts = parse_contexts(&options);
176
188
 
@@ -298,14 +310,7 @@ pub async fn derive_proof_holder_bound(
298
310
  })
299
311
  .collect::<napi::Result<_>>()?;
300
312
 
301
- let nonce = options
302
- .get("nonce")
303
- .and_then(|v| v.as_str())
304
- .map(|s| {
305
- BASE64_STANDARD
306
- .decode(s)
307
- .unwrap_or_else(|_| s.as_bytes().to_vec())
308
- });
313
+ let nonce = parse_nonce(&options);
309
314
 
310
315
  let additional_contexts = parse_contexts(&options);
311
316
 
package/src/lib.rs CHANGED
@@ -4,5 +4,6 @@
4
4
  extern crate napi_derive;
5
5
 
6
6
  pub mod bls_signatures;
7
+ pub mod jose;
7
8
  pub mod jsonld;
8
9
  pub mod ld_signatures;
package/test.mjs CHANGED
@@ -240,6 +240,44 @@ test('full demo_single flow: sign → verify → derive → verify derived', asy
240
240
  assert.equal(derivedVerifyResult.verified, true, `derived verify failed: ${derivedVerifyResult.error}`);
241
241
  });
242
242
 
243
+ test('demo_multi flow: sign twice → verify → derive → verify derived', async () => {
244
+ // Step 1: First sign
245
+ const signed = await ldSign({
246
+ document: inputDocument,
247
+ keyPair,
248
+ contexts,
249
+ });
250
+ assert.ok(signed.proof, 'signed document has proof');
251
+ assert.ok(!Array.isArray(signed.proof), 'single sign produces a single proof (not array)');
252
+
253
+ // Step 2: Second sign (creates multi-proof array)
254
+ const multiSigned = await ldSign({
255
+ document: signed,
256
+ keyPair,
257
+ contexts,
258
+ });
259
+ assert.ok(Array.isArray(multiSigned.proof), 'double sign produces proof array');
260
+ assert.equal(multiSigned.proof.length, 2, 'proof array has 2 proofs');
261
+ assert.equal(multiSigned.proof[0].type, 'BbsBlsSignature2020');
262
+ assert.equal(multiSigned.proof[1].type, 'BbsBlsSignature2020');
263
+
264
+ // Step 3: Verify multi-proof document
265
+ const verifyResult = await ldVerify({ document: multiSigned, contexts });
266
+ assert.equal(verifyResult.verified, true, `multi-proof verify failed: ${verifyResult.error}`);
267
+
268
+ // Step 4: Derive selective disclosure from multi-proof document
269
+ const derived = await ldDeriveProof({
270
+ document: multiSigned,
271
+ revealDocument,
272
+ contexts,
273
+ });
274
+ assert.ok(derived.proof, 'derived document has proof');
275
+
276
+ // Step 5: Verify derived proof
277
+ const derivedVerifyResult = await ldVerify({ document: derived, contexts });
278
+ assert.equal(derivedVerifyResult.verified, true, `multi-proof derived verify failed: ${derivedVerifyResult.error}`);
279
+ });
280
+
243
281
 
244
282
  //
245
283
  // BbsBlsSignature2020 class-based API tests