@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.
- package/Cargo.toml +11 -0
- package/W3C_CONFORMANCE.md +6 -5
- package/bench/frame_compare.mjs +203 -0
- package/bench/v2_internals.mjs +115 -0
- package/bench/vc_ops.mjs +308 -0
- package/interop-allowlist.json +3 -0
- package/interop-smoke-allowlist.json +3 -0
- package/package.json +8 -7
- package/scripts/fetch-w3c-tests.sh +8 -0
- package/src/jsonld.rs +211 -140
- package/src/ld_signatures.rs +11 -2
- package/test-fixtures/interop/README.md +46 -0
- package/test-fixtures/interop/_contexts/README.md +51 -0
- package/test-fixtures/interop/_contexts/bbs-bound-v1.jsonld +92 -0
- package/test-fixtures/interop/_contexts/citizenship-v1.jsonld +58 -0
- package/test-fixtures/interop/_contexts/credentials-v1.jsonld +316 -0
- package/test-fixtures/interop/_contexts/elm-edc-ap.jsonld +809 -0
- package/test-fixtures/interop/_contexts/essif-schemas-vc-2020-v1.jsonld +47 -0
- package/test-fixtures/interop/_contexts/identity-v2.jsonld +195 -0
- package/test-fixtures/interop/_contexts/nuggets-identity-v1.jsonld +175 -0
- package/test-fixtures/interop/_contexts/nuggets-kyb-v1.jsonld +333 -0
- package/test-fixtures/interop/_contexts/openbadges-v3.jsonld +445 -0
- package/test-fixtures/interop/_contexts/security-bbs-v1.jsonld +93 -0
- package/test-fixtures/interop/ebsi/diploma-elm/README.md +29 -0
- package/test-fixtures/interop/ebsi/diploma-elm/expected.nq +70 -0
- package/test-fixtures/interop/ebsi/diploma-elm/frame.jsonld +24 -0
- package/test-fixtures/interop/ebsi/diploma-elm/input.jsonld +444 -0
- package/test-fixtures/interop/ebsi/diploma-simple/README.md +19 -0
- package/test-fixtures/interop/ebsi/diploma-simple/expected.nq +9 -0
- package/test-fixtures/interop/ebsi/diploma-simple/frame.jsonld +13 -0
- package/test-fixtures/interop/ebsi/diploma-simple/input.jsonld +24 -0
- package/test-fixtures/interop/idv2/full-disclosure/README.md +9 -0
- package/test-fixtures/interop/idv2/full-disclosure/expected.nq +59 -0
- package/test-fixtures/interop/idv2/full-disclosure/frame.jsonld +9 -0
- package/test-fixtures/interop/idv2/full-disclosure/input.jsonld +82 -0
- package/test-fixtures/interop/nuggets/identity-v1/README.md +17 -0
- package/test-fixtures/interop/nuggets/identity-v1/expected.nq +21 -0
- package/test-fixtures/interop/nuggets/identity-v1/frame.jsonld +17 -0
- package/test-fixtures/interop/nuggets/identity-v1/input.jsonld +31 -0
- package/test-fixtures/interop/nuggets/kyb-v1/README.md +15 -0
- package/test-fixtures/interop/nuggets/kyb-v1/expected.nq +18 -0
- package/test-fixtures/interop/nuggets/kyb-v1/frame.jsonld +24 -0
- package/test-fixtures/interop/nuggets/kyb-v1/input.jsonld +60 -0
- package/test-fixtures/interop/openbadges-v3/basic-achievement/README.md +17 -0
- package/test-fixtures/interop/openbadges-v3/basic-achievement/expected.nq +12 -0
- package/test-fixtures/interop/openbadges-v3/basic-achievement/frame.jsonld +17 -0
- package/test-fixtures/interop/openbadges-v3/basic-achievement/input.jsonld +25 -0
- package/test-fixtures/interop/openbadges-v3/with-allowed-values/README.md +11 -0
- package/test-fixtures/interop/openbadges-v3/with-allowed-values/expected.nq +25 -0
- package/test-fixtures/interop/openbadges-v3/with-allowed-values/frame.jsonld +22 -0
- package/test-fixtures/interop/openbadges-v3/with-allowed-values/input.jsonld +40 -0
- package/test-fixtures/interop/vp/single-vc-wrap/README.md +6 -0
- package/test-fixtures/interop/vp/single-vc-wrap/expected.nq +7 -0
- package/test-fixtures/interop/vp/single-vc-wrap/frame.jsonld +17 -0
- package/test-fixtures/interop/vp/single-vc-wrap/input.jsonld +27 -0
- package/test-fixtures/interop/w3c-vc-v1/permanent-resident-card/README.md +5 -0
- package/test-fixtures/interop/w3c-vc-v1/permanent-resident-card/expected.nq +13 -0
- package/test-fixtures/interop/w3c-vc-v1/permanent-resident-card/frame.jsonld +14 -0
- package/test-fixtures/interop/w3c-vc-v1/permanent-resident-card/input.jsonld +29 -0
- package/test_interop.mjs +184 -0
- package/test_interop_smoke.mjs +388 -0
- package/test_jsonld_crossverify.mjs +8 -2
- package/test_w3c_conformance.mjs +80 -7
- package/tools/regen_expected.mjs +108 -0
- package/w3c-baseline.json +881 -263
- 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
|
|
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
|
|
45
|
-
"@nuggetslife/vc-linux-arm64-gnu": "0.0
|
|
46
|
-
"@nuggetslife/vc-linux-arm64-musl": "0.0
|
|
47
|
-
"@nuggetslife/vc-linux-x64-gnu": "0.0
|
|
48
|
-
"@nuggetslife/vc-linux-x64-musl": "0.0
|
|
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
|
-
|
|
6
|
-
|
|
6
|
+
document_loader::nuggets::DocumentLoader,
|
|
7
|
+
jsonld::{self, context_resolver::ContextResolver},
|
|
7
8
|
};
|
|
8
9
|
|
|
9
|
-
use crate::ld_signatures::
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
}
|
package/src/ld_signatures.rs
CHANGED
|
@@ -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::
|
|
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.
|