@nuggetslife/vc 0.0.29 → 0.1.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuggetslife/vc",
3
- "version": "0.0.29",
3
+ "version": "0.1.0",
4
4
  "main": "index.js",
5
5
  "types": "index.d.ts",
6
6
  "napi": {
@@ -34,15 +34,18 @@
34
34
  "prepublishOnly": "napi prepublish -t npm",
35
35
  "test": "node test.mjs && node test_jose.mjs && node test_sd_jwt.mjs && node test_bbs_ietf.mjs && node test_bbs_2023.mjs && node test_jsonld_crossverify.mjs && node test_backward_compat.mjs",
36
36
  "universal": "napi universal",
37
- "version": "napi version"
37
+ "version": "napi version",
38
+ "test:w3c": "node test_w3c_conformance.mjs --check",
39
+ "test:w3c:update": "node test_w3c_conformance.mjs --update",
40
+ "fetch:w3c-tests": "bash scripts/fetch-w3c-tests.sh"
38
41
  },
39
42
  "packageManager": "yarn@4.3.1",
40
43
  "optionalDependencies": {
41
- "@nuggetslife/vc-darwin-arm64": "0.0.29",
42
- "@nuggetslife/vc-linux-arm64-gnu": "0.0.29",
43
- "@nuggetslife/vc-linux-arm64-musl": "0.0.29",
44
- "@nuggetslife/vc-linux-x64-gnu": "0.0.29",
45
- "@nuggetslife/vc-linux-x64-musl": "0.0.29"
44
+ "@nuggetslife/vc-darwin-arm64": "0.1.0",
45
+ "@nuggetslife/vc-linux-arm64-gnu": "0.1.0",
46
+ "@nuggetslife/vc-linux-arm64-musl": "0.1.0",
47
+ "@nuggetslife/vc-linux-x64-gnu": "0.1.0",
48
+ "@nuggetslife/vc-linux-x64-musl": "0.1.0"
46
49
  },
47
50
  "dependencies": {}
48
51
  }
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env bash
2
+ # Clone the W3C JSON-LD 1.1 API and RDF Dataset Canonicalization test suites
3
+ # into vc/js/tmp-w3c-tests/. Pinned to specific commits for reproducibility.
4
+
5
+ set -euo pipefail
6
+
7
+ cd "$(dirname "$0")/.."
8
+
9
+ JLD_API_REPO="https://github.com/w3c/json-ld-api.git"
10
+ JLD_API_SHA="04a4eb7dc7cbc313f3f5be7ad9a3b06e87741693"
11
+
12
+ RDF_CANON_REPO="https://github.com/w3c/rdf-canon.git"
13
+ RDF_CANON_SHA="15619df2fda7a4ca88308733789b6774517f9638"
14
+
15
+ JLD_FRAMING_REPO="https://github.com/w3c/json-ld-framing.git"
16
+ JLD_FRAMING_SHA="fa228743e890499c35bc61aabf01e44cf5bbc3bc"
17
+
18
+ mkdir -p tmp-w3c-tests
19
+ cd tmp-w3c-tests
20
+
21
+ if [ ! -d json-ld-api ]; then
22
+ git clone --filter=blob:none "$JLD_API_REPO"
23
+ fi
24
+ (cd json-ld-api && git fetch --depth=1 origin "$JLD_API_SHA" && git checkout --detach "$JLD_API_SHA")
25
+
26
+ if [ ! -d rdf-canon ]; then
27
+ git clone --filter=blob:none "$RDF_CANON_REPO"
28
+ fi
29
+ (cd rdf-canon && git fetch --depth=1 origin "$RDF_CANON_SHA" && git checkout --detach "$RDF_CANON_SHA")
30
+
31
+ if [ ! -d json-ld-framing ]; then
32
+ git clone --filter=blob:none "$JLD_FRAMING_REPO"
33
+ fi
34
+ (cd json-ld-framing && git fetch --depth=1 origin "$JLD_FRAMING_SHA" && git checkout --detach "$JLD_FRAMING_SHA")
35
+
36
+ echo "W3C suites fetched at pinned SHAs."
package/src/jsonld.rs CHANGED
@@ -1,12 +1,13 @@
1
1
  use serde_json::{json, Value};
2
+ use std::collections::HashMap;
2
3
  use std::sync::Arc;
3
4
 
4
5
  use vc::{
5
- document_loader::nuggets::DocumentLoader,
6
- jsonld::{self, context_resolver::ContextResolver},
6
+ document_loader::nuggets::DocumentLoader,
7
+ jsonld::{self, context_resolver::ContextResolver},
7
8
  };
8
9
 
9
- use crate::ld_signatures::loader_from_contexts;
10
+ use crate::ld_signatures::create_document_loader;
10
11
 
11
12
  /// JSON-LD processor — drop-in replacement for the jsonld.js API.
12
13
  ///
@@ -20,159 +21,228 @@ use crate::ld_signatures::loader_from_contexts;
20
21
  /// ```
21
22
  #[napi]
22
23
  pub struct JsonLd {
23
- loader: Arc<DocumentLoader>,
24
- cr: Arc<ContextResolver>,
24
+ loader: Arc<DocumentLoader>,
25
+ cr: Arc<ContextResolver>,
26
+ additional_contexts: Arc<HashMap<String, Value>>,
27
+ }
28
+
29
+ impl JsonLd {
30
+ fn additional_contexts(&self) -> Arc<HashMap<String, Value>> {
31
+ Arc::clone(&self.additional_contexts)
32
+ }
25
33
  }
26
34
 
27
35
  #[napi]
28
36
  impl JsonLd {
29
- /// Create a new JSON-LD processor.
30
- ///
31
- /// Accepts optional `{ contexts?: Record<string, any> }` to register
32
- /// additional URL → document mappings in the document loader (on top
33
- /// of the built-in nuggets contexts).
34
- #[napi(constructor)]
35
- pub fn new(options: Option<Value>) -> Self {
36
- let contexts = options.as_ref().and_then(|o| o.get("contexts"));
37
- let (loader, cr) = loader_from_contexts(contexts);
38
- Self { loader, cr }
37
+ /// Create a new JSON-LD processor.
38
+ ///
39
+ /// Accepts optional `{ contexts?: Record<string, any> }` to register
40
+ /// additional URL → document mappings in the document loader (on top
41
+ /// of the built-in nuggets contexts).
42
+ #[napi(constructor)]
43
+ pub fn new(options: Option<Value>) -> Self {
44
+ let additional_contexts: HashMap<String, Value> = options
45
+ .as_ref()
46
+ .and_then(|o| o.get("contexts"))
47
+ .and_then(|v| v.as_object())
48
+ .map(|obj| obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
49
+ .unwrap_or_default();
50
+ let (loader, cr) = create_document_loader(additional_contexts.clone());
51
+ Self {
52
+ loader,
53
+ cr,
54
+ additional_contexts: Arc::new(additional_contexts),
39
55
  }
56
+ }
40
57
 
41
- /// Expand a JSON-LD document.
42
- ///
43
- /// `options` can include: `base`, `expandContext`, `keepFreeFloatingNodes`,
44
- /// `processingMode`.
45
- ///
46
- /// Returns an array of expanded JSON-LD objects.
47
- #[napi]
48
- pub async fn expand(
49
- &self,
50
- input: Value,
51
- options: Option<Value>,
52
- ) -> napi::Result<Value> {
53
- let opts = options.unwrap_or(json!({}));
54
- jsonld::expand(input, self.loader.clone(), self.cr.clone(), opts)
55
- .await
56
- .map_err(|e| napi::Error::from_reason(format!("expand failed: {e}")))
58
+ /// Expand a JSON-LD document.
59
+ ///
60
+ /// `options` can include: `base`, `expandContext`, `keepFreeFloatingNodes`,
61
+ /// `processingMode`.
62
+ ///
63
+ /// Returns an array of expanded JSON-LD objects.
64
+ #[napi]
65
+ pub async fn expand(&self, input: Value, options: Option<Value>) -> napi::Result<Value> {
66
+ let opts = options.unwrap_or(json!({}));
67
+ if vc::jsonld_v2::use_v2() {
68
+ let additional = self.additional_contexts();
69
+ vc::jsonld_v2::expand_v2_sync(input, &additional, opts)
70
+ .map_err(|e| napi::Error::from_reason(format!("expand (v2) failed: {e}")))
71
+ } else {
72
+ jsonld::expand(input, self.loader.clone(), self.cr.clone(), opts)
73
+ .await
74
+ .map_err(|e| napi::Error::from_reason(format!("expand failed: {e}")))
57
75
  }
76
+ }
58
77
 
59
- /// Compact a JSON-LD document using a context.
60
- ///
61
- /// `ctx` — the compaction context (object, array, or string URL).
62
- /// `options` can include: `base`, `compactArrays`, `graph`,
63
- /// `skipExpansion`, `processingMode`.
64
- ///
65
- /// Returns the compacted JSON-LD object.
66
- #[napi]
67
- pub async fn compact(
68
- &self,
69
- input: Value,
70
- ctx: Value,
71
- options: Option<Value>,
72
- ) -> napi::Result<Value> {
73
- let opts = options.unwrap_or(json!({}));
74
- jsonld::compact_api(input, ctx, self.loader.clone(), self.cr.clone(), opts)
75
- .await
76
- .map_err(|e| napi::Error::from_reason(format!("compact failed: {e}")))
78
+ /// Compact a JSON-LD document using a context.
79
+ ///
80
+ /// `ctx` — the compaction context (object, array, or string URL).
81
+ /// `options` can include: `base`, `compactArrays`, `graph`,
82
+ /// `skipExpansion`, `processingMode`.
83
+ ///
84
+ /// Returns the compacted JSON-LD object.
85
+ #[napi]
86
+ pub async fn compact(
87
+ &self,
88
+ input: Value,
89
+ ctx: Value,
90
+ options: Option<Value>,
91
+ ) -> napi::Result<Value> {
92
+ let opts = options.unwrap_or(json!({}));
93
+ if vc::jsonld_v2::use_v2() {
94
+ let additional = self.additional_contexts();
95
+ vc::jsonld_v2::compact_v2_sync(input, ctx, &additional, opts)
96
+ .map_err(|e| napi::Error::from_reason(format!("compact (v2) failed: {e}")))
97
+ } else {
98
+ jsonld::compact_api(input, ctx, self.loader.clone(), self.cr.clone(), opts)
99
+ .await
100
+ .map_err(|e| napi::Error::from_reason(format!("compact failed: {e}")))
77
101
  }
102
+ }
78
103
 
79
- /// Flatten a JSON-LD document.
80
- ///
81
- /// `ctx` — optional context for compacting the flattened result.
82
- /// Pass `null` to get the flattened expanded form.
83
- /// `options` can include: `base`, `expandContext`, `processingMode`.
84
- ///
85
- /// Returns flattened array (no context) or compacted object (with context).
86
- #[napi]
87
- pub async fn flatten(
88
- &self,
89
- input: Value,
90
- ctx: Option<Value>,
91
- options: Option<Value>,
92
- ) -> napi::Result<Value> {
93
- let opts = options.unwrap_or(json!({}));
94
- // Filter out null ctx (JS passes null, Rust expects None)
95
- let ctx = ctx.filter(|v| !v.is_null());
96
- jsonld::flatten(input, ctx, self.loader.clone(), self.cr.clone(), opts)
97
- .await
98
- .map_err(|e| napi::Error::from_reason(format!("flatten failed: {e}")))
104
+ /// Flatten a JSON-LD document.
105
+ ///
106
+ /// `ctx` — optional context for compacting the flattened result.
107
+ /// Pass `null` to get the flattened expanded form.
108
+ /// `options` can include: `base`, `expandContext`, `processingMode`.
109
+ ///
110
+ /// Returns flattened array (no context) or compacted object (with context).
111
+ #[napi]
112
+ pub async fn flatten(
113
+ &self,
114
+ input: Value,
115
+ ctx: Option<Value>,
116
+ options: Option<Value>,
117
+ ) -> napi::Result<Value> {
118
+ let opts = options.unwrap_or(json!({}));
119
+ // Filter out null ctx (JS passes null, Rust expects None)
120
+ let ctx = ctx.filter(|v| !v.is_null());
121
+ if vc::jsonld_v2::use_v2() {
122
+ let additional = self.additional_contexts();
123
+ vc::jsonld_v2::flatten_v2_sync(input, ctx, &additional, opts)
124
+ .map_err(|e| napi::Error::from_reason(format!("flatten (v2) failed: {e}")))
125
+ } else {
126
+ jsonld::flatten(input, ctx, self.loader.clone(), self.cr.clone(), opts)
127
+ .await
128
+ .map_err(|e| napi::Error::from_reason(format!("flatten failed: {e}")))
99
129
  }
130
+ }
100
131
 
101
- /// Frame a JSON-LD document.
102
- ///
103
- /// `frame` — the framing template.
104
- /// `options` can include: `base`, `embed`, `explicit`, `requireAll`,
105
- /// `omitDefault`, `omitGraph`, `processingMode`.
106
- ///
107
- /// Returns the framed JSON-LD object.
108
- #[napi]
109
- pub async fn frame(
110
- &self,
111
- input: Value,
112
- frame: Value,
113
- options: Option<Value>,
114
- ) -> napi::Result<Value> {
115
- let opts = options.unwrap_or(json!({}));
116
- jsonld::frame(input, frame, self.loader.clone(), self.cr.clone(), opts)
117
- .await
118
- .map_err(|e| napi::Error::from_reason(format!("frame failed: {e}")))
132
+ /// Frame a JSON-LD document.
133
+ ///
134
+ /// `frame` — the framing template.
135
+ /// `options` can include: `base`, `embed`, `explicit`, `requireAll`,
136
+ /// `omitDefault`, `omitGraph`, `processingMode`.
137
+ ///
138
+ /// Returns the framed JSON-LD object.
139
+ #[napi]
140
+ pub async fn frame(
141
+ &self,
142
+ input: Value,
143
+ frame: Value,
144
+ options: Option<Value>,
145
+ ) -> napi::Result<Value> {
146
+ let opts = options.unwrap_or(json!({}));
147
+ if vc::jsonld_v2::use_v2() {
148
+ // Sprint 3 Phase D cutover: V2 frame is always the native typed-tree
149
+ // walker. CLIENTFFI_FRAME_NATIVE / CLIENTFFI_FRAME_FAST flags are no
150
+ // longer consulted — frame_v2 / frame_v2_fast public APIs were
151
+ // retired in this commit.
152
+ let loader = self.loader.clone();
153
+ let cr = self.cr.clone();
154
+ let join = tokio::task::spawn_blocking(move || {
155
+ vc::jsonld_v2::frame_native_sync(input, frame, loader, cr, opts)
156
+ .map_err(|e| napi::Error::from_reason(format!("frame (native) failed: {e}")))
157
+ });
158
+ join
159
+ .await
160
+ .map_err(|e| napi::Error::from_reason(format!("frame (v2) join: {e}")))?
161
+ } else {
162
+ jsonld::frame(input, frame, self.loader.clone(), self.cr.clone(), opts)
163
+ .await
164
+ .map_err(|e| napi::Error::from_reason(format!("frame failed: {e}")))
119
165
  }
166
+ }
120
167
 
121
- /// Convert a JSON-LD document to an RDF dataset.
122
- ///
123
- /// `options` can include: `base`, `expandContext`, `skipExpansion`,
124
- /// `format` (`"application/n-quads"` for string output), `processingMode`.
125
- ///
126
- /// Returns an array of quads or an N-Quads string (depending on `format`).
127
- #[napi(js_name = "toRDF")]
128
- pub async fn to_rdf(
129
- &self,
130
- input: Value,
131
- options: Option<Value>,
132
- ) -> napi::Result<Value> {
133
- let opts = options.unwrap_or(json!({}));
134
- jsonld::to_rdf(input, self.loader.clone(), self.cr.clone(), opts)
135
- .await
136
- .map_err(|e| napi::Error::from_reason(format!("toRDF failed: {e}")))
168
+ /// Convert a JSON-LD document to an RDF dataset.
169
+ ///
170
+ /// `options` can include: `base`, `expandContext`, `skipExpansion`,
171
+ /// `format` (`"application/n-quads"` for string output), `processingMode`.
172
+ ///
173
+ /// Returns an array of quads or an N-Quads string (depending on `format`).
174
+ #[napi(js_name = "toRDF")]
175
+ pub async fn to_rdf(&self, input: Value, options: Option<Value>) -> napi::Result<Value> {
176
+ let opts = options.unwrap_or(json!({}));
177
+ // v2 only produces N-Quads string output. If caller expects
178
+ // a quads-array (default when format is omitted), fall back to v1.
179
+ let wants_nquads = opts
180
+ .get("format")
181
+ .and_then(|v| v.as_str())
182
+ .map(|s| s == "application/n-quads")
183
+ .unwrap_or(false);
184
+ if vc::jsonld_v2::use_v2() && wants_nquads {
185
+ let additional = self.additional_contexts();
186
+ let nquads = vc::jsonld_v2::to_nquads_v2_sync(input, &additional, opts)
187
+ .map_err(|e| napi::Error::from_reason(format!("toRDF (v2) failed: {e}")))?;
188
+ Ok(Value::String(nquads))
189
+ } else {
190
+ jsonld::to_rdf(input, self.loader.clone(), self.cr.clone(), opts)
191
+ .await
192
+ .map_err(|e| napi::Error::from_reason(format!("toRDF failed: {e}")))
137
193
  }
194
+ }
138
195
 
139
- /// Convert an RDF dataset to a JSON-LD document.
140
- ///
141
- /// `dataset` — N-Quads string or array of quads.
142
- /// `options` can include: `useRdfType`, `useNativeTypes`, `rdfDirection`.
143
- ///
144
- /// Returns an array of JSON-LD objects.
145
- #[napi(js_name = "fromRDF")]
146
- pub fn from_rdf(&self, dataset: Value, options: Option<Value>) -> napi::Result<Value> {
147
- let opts = options.unwrap_or(json!({}));
148
- jsonld::from_rdf_api(dataset, opts)
149
- .map_err(|e| napi::Error::from_reason(format!("fromRDF failed: {e}")))
150
- }
196
+ /// Convert an RDF dataset to a JSON-LD document.
197
+ ///
198
+ /// `dataset` — N-Quads string or array of quads.
199
+ /// `options` can include: `useRdfType`, `useNativeTypes`, `rdfDirection`.
200
+ ///
201
+ /// Returns an array of JSON-LD objects.
202
+ #[napi(js_name = "fromRDF")]
203
+ pub fn from_rdf(&self, dataset: Value, options: Option<Value>) -> napi::Result<Value> {
204
+ let opts = options.unwrap_or(json!({}));
205
+ jsonld::from_rdf_api(dataset, opts)
206
+ .map_err(|e| napi::Error::from_reason(format!("fromRDF failed: {e}")))
207
+ }
151
208
 
152
- /// Canonize (normalize) a JSON-LD document.
153
- ///
154
- /// `options` can include: `base`, `expandContext`, `skipExpansion`,
155
- /// `inputFormat` (`"application/n-quads"`), `format`, `algorithm`,
156
- /// `processingMode`.
157
- ///
158
- /// Returns the canonical N-Quads string.
159
- #[napi]
160
- pub async fn canonize(
161
- &self,
162
- input: Value,
163
- options: Option<Value>,
164
- ) -> napi::Result<Value> {
165
- let mut opts = options.unwrap_or(json!({}));
166
- // Defaults matching JS jsonld.js canonize()
167
- if opts.get("algorithm").is_none() {
168
- opts["algorithm"] = json!("RDFC-1.0");
169
- }
170
- if opts.get("format").is_none() {
171
- opts["format"] = json!("application/n-quads");
172
- }
173
- let result = jsonld::canonize(input, self.loader.clone(), self.cr.clone(), opts)
174
- .await
175
- .map_err(|e| napi::Error::from_reason(format!("canonize failed: {e}")))?;
176
- Ok(Value::String(result))
209
+ /// Canonize (normalize) a JSON-LD document.
210
+ ///
211
+ /// `options` can include: `base`, `expandContext`, `skipExpansion`,
212
+ /// `inputFormat` (`"application/n-quads"`), `format`, `algorithm`,
213
+ /// `processingMode`.
214
+ ///
215
+ /// Returns the canonical N-Quads string.
216
+ #[napi]
217
+ pub async fn canonize(&self, input: Value, options: Option<Value>) -> napi::Result<Value> {
218
+ let mut opts = options.unwrap_or(json!({}));
219
+ // Defaults matching JS jsonld.js canonize()
220
+ if opts.get("algorithm").is_none() {
221
+ opts["algorithm"] = json!("RDFC-1.0");
222
+ }
223
+ if opts.get("format").is_none() {
224
+ opts["format"] = json!("application/n-quads");
177
225
  }
226
+ let input_is_nquads = opts
227
+ .get("inputFormat")
228
+ .and_then(|v| v.as_str())
229
+ .map(|s| s == "application/n-quads")
230
+ .unwrap_or(false);
231
+ let result = if vc::jsonld_v2::use_v2() && !input_is_nquads {
232
+ let additional = self.additional_contexts();
233
+ vc::jsonld_v2::canonize_v2_sync(
234
+ input,
235
+ &additional,
236
+ self.loader.clone(),
237
+ self.cr.clone(),
238
+ opts,
239
+ )
240
+ .map_err(|e| napi::Error::from_reason(format!("canonize (v2) failed: {e}")))?
241
+ } else {
242
+ jsonld::canonize(input, self.loader.clone(), self.cr.clone(), opts)
243
+ .await
244
+ .map_err(|e| napi::Error::from_reason(format!("canonize failed: {e}")))?
245
+ };
246
+ Ok(Value::String(result))
247
+ }
178
248
  }
@@ -135,7 +135,10 @@ test('cross-verify: flatten(inputDocument, null) matches JS reference', async ()
135
135
  const napiResult = await proc.flatten(inputDocument, null);
136
136
  const jsResult = await jsonld.flatten(inputDocument, null, { documentLoader });
137
137
 
138
- assert.deepStrictEqual(napiResult, jsResult);
138
+ // Flatten without compaction returns an unordered set of node objects
139
+ // (each keyed by @id). Sort by @id before comparing.
140
+ const sortById = arr => [...arr].sort((a, b) => String(a['@id'] || '').localeCompare(String(b['@id'] || '')));
141
+ assert.deepStrictEqual(sortById(napiResult), sortById(jsResult));
139
142
  });
140
143
 
141
144
 
@@ -171,7 +174,10 @@ test('cross-verify: toRDF(inputDocument, n-quads) matches JS reference', async (
171
174
  const napiResult = await proc.toRDF(inputDocument, { format: 'application/n-quads' });
172
175
  const jsResult = await jsonld.toRDF(inputDocument, { documentLoader, format: 'application/n-quads' });
173
176
 
174
- assert.strictEqual(napiResult, jsResult);
177
+ // toRDF triple order is implementation-defined (RDFC-1.0 normalises it later).
178
+ // Compare as a set: split into lines, drop empties, sort.
179
+ const norm = s => s.split('\n').filter(l => l.length).sort().join('\n');
180
+ assert.strictEqual(norm(napiResult), norm(jsResult));
175
181
  });
176
182
 
177
183
 
@@ -328,3 +334,105 @@ test('cross-verify: canonize preserves nested typed objects without @id', async
328
334
 
329
335
  assert.strictEqual(napiResult, jsResult);
330
336
  });
337
+
338
+ //
339
+ // Regression: terms with @type: "@json" must produce JSON literal quads
340
+ // (parity with jsonld@3.1.0 / jsonld@8.3.3 — the libs that sign Nuggets VCs)
341
+ //
342
+
343
+ test('cross-verify: expand emits JSON literal for term with @type: @json (flat context)', async () => {
344
+ const doc = {
345
+ '@context': {
346
+ '@version': 1.1,
347
+ inner: { '@id': 'https://example.com/inner', '@type': '@json' }
348
+ },
349
+ inner: { hello: 'world' }
350
+ };
351
+
352
+ const proc = new JsonLd();
353
+ const napiResult = await proc.expand(doc);
354
+ const jsResult = await jsonld.expand(doc);
355
+
356
+ assert.deepStrictEqual(napiResult, jsResult);
357
+ });
358
+
359
+ test('cross-verify: expand emits JSON literal for term with @type: @json (term-scoped context)', async () => {
360
+ // Mirrors the KYB v1 shape where `dnbData` is defined inside `result`'s
361
+ // term-scoped context. See packages/document-loader/context/kybV1.json.
362
+ const doc = {
363
+ '@context': {
364
+ '@version': 1.1,
365
+ outer: {
366
+ '@id': 'https://example.com/outer',
367
+ '@context': {
368
+ '@version': 1.1,
369
+ inner: { '@id': 'https://example.com/inner', '@type': '@json' }
370
+ }
371
+ }
372
+ },
373
+ outer: { inner: { organization: { primaryName: 'Acme' } } }
374
+ };
375
+
376
+ const proc = new JsonLd();
377
+ const napiResult = await proc.expand(doc);
378
+ const jsResult = await jsonld.expand(doc);
379
+
380
+ assert.deepStrictEqual(napiResult, jsResult);
381
+ });
382
+
383
+ test('cross-verify: canonize emits JSON literal quad for term with @type: @json', async () => {
384
+ const doc = {
385
+ '@context': {
386
+ '@version': 1.1,
387
+ inner: { '@id': 'https://example.com/inner', '@type': '@json' }
388
+ },
389
+ inner: { b: 2, a: 1 }
390
+ };
391
+
392
+ const proc = new JsonLd();
393
+ const napiResult = await proc.canonize(doc);
394
+ const jsResult = await jsonld.canonize(doc, {
395
+ algorithm: 'URDNA2015',
396
+ format: 'application/n-quads',
397
+ safe: false,
398
+ });
399
+
400
+ assert.strictEqual(napiResult, jsResult);
401
+ });
402
+
403
+ //
404
+ // Regression: blank-node objects (no @id) in arrays must each produce a
405
+ // distinct quad. Earlier `add_value` deduped equal serde Values, collapsing
406
+ // `[{}, {}, {}]` to a single blank node and silently shrinking signed
407
+ // message sets — breaking any BBS+ proof over arrays that contain repeats.
408
+ //
409
+
410
+ test('cross-verify: array of empty objects keeps each blank node distinct', async () => {
411
+ const doc = {
412
+ '@context': { items: { '@id': 'https://example.com/items' } },
413
+ items: [{}, {}, {}]
414
+ };
415
+
416
+ const proc = new JsonLd();
417
+ const napiResult = await proc.expand(doc);
418
+ const jsResult = await jsonld.expand(doc);
419
+
420
+ assert.deepStrictEqual(napiResult, jsResult);
421
+ });
422
+
423
+ test('cross-verify: toRDF with array of empty objects emits one quad per element', async () => {
424
+ const doc = {
425
+ '@context': { items: { '@id': 'https://example.com/items' } },
426
+ items: [{}, {}, 'a', {}]
427
+ };
428
+
429
+ const proc = new JsonLd();
430
+ const napiNquads = await proc.toRDF(doc, { format: 'application/n-quads' });
431
+ const jsNquads = await jsonld.toRDF(doc, { format: 'application/n-quads' });
432
+
433
+ const napiCount = napiNquads.split('\n').filter(Boolean).length;
434
+ const jsCount = jsNquads.split('\n').filter(Boolean).length;
435
+
436
+ assert.strictEqual(napiCount, jsCount,
437
+ `expected ${jsCount} quads (one per array element), got ${napiCount}`);
438
+ });