@nuggetslife/vc 0.0.30 → 0.2.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.
Files changed (66) hide show
  1. package/Cargo.toml +11 -0
  2. package/W3C_CONFORMANCE.md +6 -5
  3. package/bench/frame_compare.mjs +203 -0
  4. package/bench/v2_internals.mjs +115 -0
  5. package/bench/vc_ops.mjs +308 -0
  6. package/interop-allowlist.json +3 -0
  7. package/interop-smoke-allowlist.json +3 -0
  8. package/package.json +8 -7
  9. package/scripts/fetch-w3c-tests.sh +8 -0
  10. package/src/jsonld.rs +211 -140
  11. package/src/ld_signatures.rs +11 -2
  12. package/test-fixtures/interop/README.md +46 -0
  13. package/test-fixtures/interop/_contexts/README.md +51 -0
  14. package/test-fixtures/interop/_contexts/bbs-bound-v1.jsonld +92 -0
  15. package/test-fixtures/interop/_contexts/citizenship-v1.jsonld +58 -0
  16. package/test-fixtures/interop/_contexts/credentials-v1.jsonld +316 -0
  17. package/test-fixtures/interop/_contexts/elm-edc-ap.jsonld +809 -0
  18. package/test-fixtures/interop/_contexts/essif-schemas-vc-2020-v1.jsonld +47 -0
  19. package/test-fixtures/interop/_contexts/identity-v2.jsonld +195 -0
  20. package/test-fixtures/interop/_contexts/nuggets-identity-v1.jsonld +175 -0
  21. package/test-fixtures/interop/_contexts/nuggets-kyb-v1.jsonld +333 -0
  22. package/test-fixtures/interop/_contexts/openbadges-v3.jsonld +445 -0
  23. package/test-fixtures/interop/_contexts/security-bbs-v1.jsonld +93 -0
  24. package/test-fixtures/interop/ebsi/diploma-elm/README.md +29 -0
  25. package/test-fixtures/interop/ebsi/diploma-elm/expected.nq +70 -0
  26. package/test-fixtures/interop/ebsi/diploma-elm/frame.jsonld +24 -0
  27. package/test-fixtures/interop/ebsi/diploma-elm/input.jsonld +444 -0
  28. package/test-fixtures/interop/ebsi/diploma-simple/README.md +19 -0
  29. package/test-fixtures/interop/ebsi/diploma-simple/expected.nq +9 -0
  30. package/test-fixtures/interop/ebsi/diploma-simple/frame.jsonld +13 -0
  31. package/test-fixtures/interop/ebsi/diploma-simple/input.jsonld +24 -0
  32. package/test-fixtures/interop/idv2/full-disclosure/README.md +9 -0
  33. package/test-fixtures/interop/idv2/full-disclosure/expected.nq +59 -0
  34. package/test-fixtures/interop/idv2/full-disclosure/frame.jsonld +9 -0
  35. package/test-fixtures/interop/idv2/full-disclosure/input.jsonld +82 -0
  36. package/test-fixtures/interop/nuggets/identity-v1/README.md +17 -0
  37. package/test-fixtures/interop/nuggets/identity-v1/expected.nq +21 -0
  38. package/test-fixtures/interop/nuggets/identity-v1/frame.jsonld +17 -0
  39. package/test-fixtures/interop/nuggets/identity-v1/input.jsonld +31 -0
  40. package/test-fixtures/interop/nuggets/kyb-v1/README.md +15 -0
  41. package/test-fixtures/interop/nuggets/kyb-v1/expected.nq +18 -0
  42. package/test-fixtures/interop/nuggets/kyb-v1/frame.jsonld +24 -0
  43. package/test-fixtures/interop/nuggets/kyb-v1/input.jsonld +60 -0
  44. package/test-fixtures/interop/openbadges-v3/basic-achievement/README.md +17 -0
  45. package/test-fixtures/interop/openbadges-v3/basic-achievement/expected.nq +12 -0
  46. package/test-fixtures/interop/openbadges-v3/basic-achievement/frame.jsonld +17 -0
  47. package/test-fixtures/interop/openbadges-v3/basic-achievement/input.jsonld +25 -0
  48. package/test-fixtures/interop/openbadges-v3/with-allowed-values/README.md +11 -0
  49. package/test-fixtures/interop/openbadges-v3/with-allowed-values/expected.nq +25 -0
  50. package/test-fixtures/interop/openbadges-v3/with-allowed-values/frame.jsonld +22 -0
  51. package/test-fixtures/interop/openbadges-v3/with-allowed-values/input.jsonld +40 -0
  52. package/test-fixtures/interop/vp/single-vc-wrap/README.md +6 -0
  53. package/test-fixtures/interop/vp/single-vc-wrap/expected.nq +7 -0
  54. package/test-fixtures/interop/vp/single-vc-wrap/frame.jsonld +17 -0
  55. package/test-fixtures/interop/vp/single-vc-wrap/input.jsonld +27 -0
  56. package/test-fixtures/interop/w3c-vc-v1/permanent-resident-card/README.md +5 -0
  57. package/test-fixtures/interop/w3c-vc-v1/permanent-resident-card/expected.nq +13 -0
  58. package/test-fixtures/interop/w3c-vc-v1/permanent-resident-card/frame.jsonld +14 -0
  59. package/test-fixtures/interop/w3c-vc-v1/permanent-resident-card/input.jsonld +29 -0
  60. package/test_interop.mjs +184 -0
  61. package/test_interop_smoke.mjs +388 -0
  62. package/test_jsonld_crossverify.mjs +8 -2
  63. package/test_w3c_conformance.mjs +80 -7
  64. package/tools/regen_expected.mjs +108 -0
  65. package/w3c-baseline.json +881 -263
  66. package/w3c-denylist.json +6 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuggetslife/vc",
3
- "version": "0.0.30",
3
+ "version": "0.2.0",
4
4
  "main": "index.js",
5
5
  "types": "index.d.ts",
6
6
  "napi": {
@@ -22,7 +22,8 @@
22
22
  "@mattrglobal/jsonld-signatures-bbs": "^1.2.0",
23
23
  "@napi-rs/cli": "^2.18.3",
24
24
  "@types/node": "^20.14.9",
25
- "jsonld": "^8.3.3"
25
+ "jsonld": "^8.3.3",
26
+ "jsonld-signatures": "^7.0.0"
26
27
  },
27
28
  "engines": {
28
29
  "node": ">= 10"
@@ -41,11 +42,11 @@
41
42
  },
42
43
  "packageManager": "yarn@4.3.1",
43
44
  "optionalDependencies": {
44
- "@nuggetslife/vc-darwin-arm64": "0.0.30",
45
- "@nuggetslife/vc-linux-arm64-gnu": "0.0.30",
46
- "@nuggetslife/vc-linux-arm64-musl": "0.0.30",
47
- "@nuggetslife/vc-linux-x64-gnu": "0.0.30",
48
- "@nuggetslife/vc-linux-x64-musl": "0.0.30"
45
+ "@nuggetslife/vc-darwin-arm64": "0.2.0",
46
+ "@nuggetslife/vc-linux-arm64-gnu": "0.2.0",
47
+ "@nuggetslife/vc-linux-arm64-musl": "0.2.0",
48
+ "@nuggetslife/vc-linux-x64-gnu": "0.2.0",
49
+ "@nuggetslife/vc-linux-x64-musl": "0.2.0"
49
50
  },
50
51
  "dependencies": {}
51
52
  }
@@ -12,6 +12,9 @@ JLD_API_SHA="04a4eb7dc7cbc313f3f5be7ad9a3b06e87741693"
12
12
  RDF_CANON_REPO="https://github.com/w3c/rdf-canon.git"
13
13
  RDF_CANON_SHA="15619df2fda7a4ca88308733789b6774517f9638"
14
14
 
15
+ JLD_FRAMING_REPO="https://github.com/w3c/json-ld-framing.git"
16
+ JLD_FRAMING_SHA="fa228743e890499c35bc61aabf01e44cf5bbc3bc"
17
+
15
18
  mkdir -p tmp-w3c-tests
16
19
  cd tmp-w3c-tests
17
20
 
@@ -25,4 +28,9 @@ if [ ! -d rdf-canon ]; then
25
28
  fi
26
29
  (cd rdf-canon && git fetch --depth=1 origin "$RDF_CANON_SHA" && git checkout --detach "$RDF_CANON_SHA")
27
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
+
28
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,229 @@ 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 additional = self.additional_contexts();
155
+ let join = tokio::task::spawn_blocking(move || {
156
+ vc::jsonld_v2::frame_native_sync(input, frame, loader, cr, opts, &additional)
157
+ .map_err(|e| napi::Error::from_reason(format!("frame (native) failed: {e}")))
158
+ });
159
+ join
160
+ .await
161
+ .map_err(|e| napi::Error::from_reason(format!("frame (v2) join: {e}")))?
162
+ } else {
163
+ jsonld::frame(input, frame, self.loader.clone(), self.cr.clone(), opts)
164
+ .await
165
+ .map_err(|e| napi::Error::from_reason(format!("frame failed: {e}")))
119
166
  }
167
+ }
120
168
 
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}")))
169
+ /// Convert a JSON-LD document to an RDF dataset.
170
+ ///
171
+ /// `options` can include: `base`, `expandContext`, `skipExpansion`,
172
+ /// `format` (`"application/n-quads"` for string output), `processingMode`.
173
+ ///
174
+ /// Returns an array of quads or an N-Quads string (depending on `format`).
175
+ #[napi(js_name = "toRDF")]
176
+ pub async fn to_rdf(&self, input: Value, options: Option<Value>) -> napi::Result<Value> {
177
+ let opts = options.unwrap_or(json!({}));
178
+ // v2 only produces N-Quads string output. If caller expects
179
+ // a quads-array (default when format is omitted), fall back to v1.
180
+ let wants_nquads = opts
181
+ .get("format")
182
+ .and_then(|v| v.as_str())
183
+ .map(|s| s == "application/n-quads")
184
+ .unwrap_or(false);
185
+ if vc::jsonld_v2::use_v2() && wants_nquads {
186
+ let additional = self.additional_contexts();
187
+ let nquads = vc::jsonld_v2::to_nquads_v2_sync(input, &additional, opts)
188
+ .map_err(|e| napi::Error::from_reason(format!("toRDF (v2) failed: {e}")))?;
189
+ Ok(Value::String(nquads))
190
+ } else {
191
+ jsonld::to_rdf(input, self.loader.clone(), self.cr.clone(), opts)
192
+ .await
193
+ .map_err(|e| napi::Error::from_reason(format!("toRDF failed: {e}")))
137
194
  }
195
+ }
138
196
 
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
- }
197
+ /// Convert an RDF dataset to a JSON-LD document.
198
+ ///
199
+ /// `dataset` — N-Quads string or array of quads.
200
+ /// `options` can include: `useRdfType`, `useNativeTypes`, `rdfDirection`.
201
+ ///
202
+ /// Returns an array of JSON-LD objects.
203
+ #[napi(js_name = "fromRDF")]
204
+ pub fn from_rdf(&self, dataset: Value, options: Option<Value>) -> napi::Result<Value> {
205
+ let opts = options.unwrap_or(json!({}));
206
+ jsonld::from_rdf_api(dataset, opts)
207
+ .map_err(|e| napi::Error::from_reason(format!("fromRDF failed: {e}")))
208
+ }
151
209
 
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))
210
+ /// Canonize (normalize) a JSON-LD document.
211
+ ///
212
+ /// `options` can include: `base`, `expandContext`, `skipExpansion`,
213
+ /// `inputFormat` (`"application/n-quads"`), `format`, `algorithm`,
214
+ /// `processingMode`.
215
+ ///
216
+ /// Returns the canonical N-Quads string.
217
+ #[napi]
218
+ pub async fn canonize(&self, input: Value, options: Option<Value>) -> napi::Result<Value> {
219
+ let mut opts = options.unwrap_or(json!({}));
220
+ // Defaults matching JS jsonld.js canonize()
221
+ if opts.get("algorithm").is_none() {
222
+ opts["algorithm"] = json!("RDFC-1.0");
223
+ }
224
+ if opts.get("format").is_none() {
225
+ opts["format"] = json!("application/n-quads");
177
226
  }
227
+ let input_is_nquads = opts
228
+ .get("inputFormat")
229
+ .and_then(|v| v.as_str())
230
+ .map(|s| s == "application/n-quads")
231
+ .unwrap_or(false);
232
+ let result = if vc::jsonld_v2::use_v2() && !input_is_nquads {
233
+ let additional = self.additional_contexts();
234
+ vc::jsonld_v2::canonize_v2_sync(
235
+ input,
236
+ &additional,
237
+ self.loader.clone(),
238
+ self.cr.clone(),
239
+ opts,
240
+ )
241
+ .map_err(|e| napi::Error::from_reason(format!("canonize (v2) failed: {e}")))?
242
+ } else {
243
+ jsonld::canonize(input, self.loader.clone(), self.cr.clone(), opts)
244
+ .await
245
+ .map_err(|e| napi::Error::from_reason(format!("canonize failed: {e}")))?
246
+ };
247
+ Ok(Value::String(result))
248
+ }
178
249
  }
@@ -28,18 +28,27 @@ use vc::{
28
28
  };
29
29
 
30
30
  /// Create a DocumentLoader with nuggets contexts plus additional caller-provided contexts.
31
+ ///
32
+ /// Extras are layered into the V1 context map AND retained separately so
33
+ /// the V2 dispatch chain (canonize_dispatch) can hand them to its own
34
+ /// build_loader. See clientffi#69 / PR #59.
31
35
  pub(crate) fn create_document_loader(
32
36
  additional_contexts: HashMap<String, Value>,
33
37
  ) -> (Arc<DocumentLoader>, Arc<ContextResolver>) {
34
38
  let mut ctx = load_nuggets_context();
35
- ctx.extend(additional_contexts);
39
+ ctx.extend(additional_contexts.clone());
36
40
 
37
41
  let resolver = Arc::new(tokio::sync::RwLock::new(Resolver::new(
38
42
  ResolverRegistry::new(),
39
43
  None,
40
44
  )));
41
45
  let opts = DidResolutionOptions::default();
42
- let loader = Arc::new(DocumentLoader::new(ctx, resolver, opts));
46
+ let loader = Arc::new(DocumentLoader::with_additional_contexts(
47
+ ctx,
48
+ additional_contexts,
49
+ resolver,
50
+ opts,
51
+ ));
43
52
  let cr = Arc::new(ContextResolver::new(LruCache::new(
44
53
  NonZeroUsize::new(10).unwrap(),
45
54
  )));
@@ -0,0 +1,46 @@
1
+ # Cross-stack BBS+ interop fixtures
2
+
3
+ Fixtures for `vc/js/test_interop.mjs` — the cross-stack interop harness comparing V2 native (Rust `json-ld@0.21.4`) against `jsonld.js@^8.3.3` (the de facto third-party reference). Each fixture is a per-VC-schema test case in the form `<family>/<name>/`.
4
+
5
+ ## Layout
6
+
7
+ ```
8
+ test-fixtures/interop/
9
+ ├── _contexts/ # bundled JSON-LD contexts (see _contexts/README.md)
10
+ ├── <family>/<name>/
11
+ │ ├── input.jsonld # the VC document
12
+ │ ├── frame.jsonld # BBS+-style reveal frame
13
+ │ ├── expected.nq # canonized N-Quads from jsonld.js (committed; ground truth)
14
+ │ └── README.md # provenance + what this fixture tests
15
+ ```
16
+
17
+ `_<dir>` (underscore-prefixed) directories are excluded from fixture discovery.
18
+
19
+ ## Workflow
20
+
21
+ **Adding a new fixture:**
22
+
23
+ 1. Create `<family>/<name>/` with `input.jsonld`, `frame.jsonld`, and a brief `README.md` describing provenance.
24
+ 2. If the input references a context not yet in `_contexts/`, add it (see `_contexts/README.md`).
25
+ 3. Generate ground truth: `cd vc/js && node tools/regen_expected.mjs test-fixtures/interop/<family>/<name>`.
26
+ 4. Run the harness: `node test_interop.mjs --check` — if `pass`, commit; if `fail`, see "When the harness fails" below.
27
+
28
+ **Regenerating after upstream context updates:** `node tools/regen_expected.mjs --all` from `vc/js/`. Re-run `--check` afterwards.
29
+
30
+ ## When the harness fails
31
+
32
+ Three modes:
33
+
34
+ - **Regression** (a previously-passing fixture fails): a real V2 native bug. Investigate via `node test_interop.mjs --report` to see the unified diff. Fix in `vc/rs/src/jsonld_v2/frame_native.rs` (or wherever the divergence originates).
35
+ - **Expected fail** (fixture is in `interop-allowlist.json`): no action; the gap is tracked. The reason field + linked GitHub issue document next steps.
36
+ - **Improvement** (an allowlisted fixture starts passing): the harness exits 1 with a "newly passing — please ratchet" message. Remove the allowlist entry and close the linked GitHub issue.
37
+
38
+ The harness gates `vc-publish` via the `vc-interop-conformance` CI job, so a regression blocks release.
39
+
40
+ ## Related files
41
+
42
+ - `vc/js/test_interop.mjs` — the harness (`--check`, `--report`)
43
+ - `vc/js/tools/regen_expected.mjs` — ground-truth generator (jsonld.js + URDNA2015, offline document loader)
44
+ - `vc/js/interop-allowlist.json` — `expect_fail` ratchet
45
+ - `vc/js/test-fixtures/interop/_contexts/README.md` — bundled context conventions + Rust sync targets
46
+ - `docs/plans/2026-05-05-sprint-4-phase-a.md` — Phase A implementation plan
@@ -0,0 +1,51 @@
1
+ # Interop fixture contexts
2
+
3
+ Bundled JSON-LD contexts for the cross-stack interop test harness (`vc/js/test_interop.mjs`) and its ground-truth generator (`vc/js/tools/regen_expected.mjs`).
4
+
5
+ ## Convention
6
+
7
+ Each `<name>.jsonld` here is loaded by the harness's offline document loader and indexed by its embedded `__url__` marker. Shape:
8
+
9
+ ```json
10
+ {
11
+ "__url__": "<the URL the input.jsonld references>",
12
+ "@context": <verbatim copy of the upstream context body>
13
+ }
14
+ ```
15
+
16
+ The harness deletes `__url__` after reading and exposes the rest to JsonLd as a context document.
17
+
18
+ Files starting with `_` (this `README.md`, etc.) are not loaded — only `*.jsonld` and `*.json` siblings.
19
+
20
+ ## Sync targets
21
+
22
+ Each bundle here is a hand-copy of a context body. Most mirror a constant in the Rust codebase ("Rust-mirror" bundles); a few mirror an external spec that Nuggets does not issue ("spec-sourced" bundles, e.g. OpenBadges v3 added in Sprint 4 Phase B for cross-stack interop coverage). **When the canonical source updates, this bundle must be updated in lockstep** — otherwise cross-stack interop silently drifts and the harness will start producing false confidence in jsonld.js parity. There is no automated drift check today; this README is the audit trail.
23
+
24
+ | bundle file | URL | Source |
25
+ |---|---|---|
26
+ | `credentials-v1.jsonld` | `https://www.w3.org/2018/credentials/v1` | Rust-mirror: `vc/rs/src/jsonld/signatures/bbs/data/sample_data.rs::CREDENTIALS_CONTEXT` |
27
+ | `citizenship-v1.jsonld` | `https://w3id.org/citizenship/v1` | Rust-mirror: `vc/rs/src/jsonld/signatures/bbs/data/sample_data.rs::CITIZEN_VOCAB` |
28
+ | `security-bbs-v1.jsonld` | `https://w3id.org/security/bbs/v1` | Rust-mirror: `vc/rs/src/jsonld/signatures/bbs/data/sample_data.rs::BBS` |
29
+ | `identity-v2.jsonld` | `https://schemas.nuggets.life/identityV2.json` | Rust-mirror: `vc/rs/src/document_loader/context/identity_v2.rs::IDENTITY_V2` |
30
+ | `bbs-bound-v1.jsonld` | `https://schemas.nuggets.life/bbsBoundv1.json` | Rust-mirror: `vc/rs/src/document_loader/context/bbs_bound_v1.rs::BBS_BOUND_V1` |
31
+ | `nuggets-identity-v1.jsonld` | `https://schemas.nuggets.life/identity.json` | Rust-mirror: `vc/rs/src/document_loader/context/identity.rs::IDENTITY` |
32
+ | `nuggets-kyb-v1.jsonld` | `https://schemas.nuggets.life/kybV1.json` | Rust-mirror: `vc/rs/src/document_loader/context/kyb.rs::KYB_V1` |
33
+ | `openbadges-v3.jsonld` | `https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json` | Spec-sourced (no Rust mirror): IMS Global OpenBadges v3.0.3 context, retrieved 2026-05-06 from the canonical PURL above. The `https://w3id.org/openbadges/v3` alias currently 302s to a 404 (`https://openbadgespec.org/v3`), so OB v3 fixtures cite the IMS PURL directly. |
34
+ | `essif-schemas-vc-2020-v1.jsonld` | `https://essif.europa.eu/schemas/vc/2020/v1` | Spec-sourced (no Rust mirror): EBSI/ESSIF schemas v1 context as distributed by the EBSI4Austria pilot at `https://github.com/danubetech/ebsi4austria-examples/blob/main/context/essif-schemas-vc-2020-v1.jsonld`, retrieved 2026-05-06. Used by `ebsi/diploma-simple`. |
35
+ | `elm-edc-ap.jsonld` | `http://data.europa.eu/snb/model/context/edc-ap` | Spec-sourced (no Rust mirror): European Learning Model v3 EDC-AP context, retrieved 2026-05-06 from `https://github.com/european-commission-empl/European-Learning-Model/blob/master/rdf/ap/edc/edc-ap-context.jsonld`. The canonical `data.europa.eu` URL currently sits behind an Azure WAF JS challenge (cannot be fetched cleanly with curl/WebFetch); the European Commission's official ELM repo is the verbatim source. Used by `ebsi/diploma-elm`. |
36
+
37
+ ### Bundle classes
38
+
39
+ - **Rust-mirror** — the upstream-of-record is a Rust constant in this repo. The bundle is a verbatim copy and must be re-synced when the Rust source changes.
40
+ - **Spec-sourced** — the upstream-of-record is an external spec (e.g. OpenBadges v3). Nuggets does not issue these credentials; the bundle exists purely to feed cross-stack interop fixtures. Re-sync when the spec publishes a new revision.
41
+
42
+ ## Adding a new context
43
+
44
+ When a new fixture references a context not yet bundled here:
45
+
46
+ 1. **Rust-mirror case** — find the canonical body in the Rust codebase (search `vc/rs/src/document_loader/` and `vc/rs/src/jsonld/signatures/bbs/data/`). **Don't network-fetch.**
47
+ **Spec-sourced case** — fetch the canonical context from the spec's PURL/URL with `curl -sL` and save the body verbatim.
48
+ 2. Create `_contexts/<short-name>.jsonld` with `{ "__url__": "<URL>", "@context": <body> }`.
49
+ 3. Verify the body is byte-equal to the source (Rust constant or fetched spec body).
50
+ 4. Add a row to the table above naming the source (Rust path + constant for mirrors; spec name + retrieval date for spec-sourced).
51
+ 5. Re-run `node tools/regen_expected.mjs --all` so all `expected.nq` files account for the new context.