@nuggetslife/vc 0.1.0 → 0.3.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 +1 -0
- package/W3C_CONFORMANCE.md +9 -6
- package/bench/frame_compare.mjs +7 -20
- package/bench/v2_internals.mjs +3 -5
- package/bench/vc_ops.mjs +4 -10
- package/index.d.ts +2 -2
- package/interop-allowlist.json +3 -0
- package/interop-smoke-allowlist.json +3 -0
- package/package.json +8 -7
- package/src/bls_signatures/bbs_bls_holder_bound_signature_2022/mod.rs +18 -27
- package/src/bls_signatures/bbs_bls_holder_bound_signature_proof_2022/mod.rs +9 -10
- package/src/bls_signatures/bbs_bls_signature_2020/mod.rs +21 -25
- package/src/bls_signatures/bbs_bls_signature_proof_2020/mod.rs +6 -12
- package/src/jsonld.rs +40 -75
- package/src/ld_signatures.rs +210 -224
- package/src/lib.rs +8 -0
- 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_w3c_conformance.mjs +7 -0
- package/tools/regen_expected.mjs +108 -0
- package/w3c-baseline.json +63 -74
- package/w3c-denylist.json +6 -1
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Cross-stack BBS+ sign-verify smoke harness (Sprint 4 Phase D).
|
|
3
|
+
//
|
|
4
|
+
// Layered on top of the parity harness (test_interop.mjs): the parity gate
|
|
5
|
+
// ensures frame + canonize byte-equality between V2 native and jsonld.js;
|
|
6
|
+
// THIS gate ensures a full cryptographic round-trip across the two stacks.
|
|
7
|
+
//
|
|
8
|
+
// For each fixture × direction:
|
|
9
|
+
// 1. V2 native sign → @mattrglobal/jsonld-signatures-bbs verify
|
|
10
|
+
// 2. @mattrglobal/jsonld-signatures-bbs sign → V2 native verify
|
|
11
|
+
// Both directions must succeed (verified: true) for the fixture to "pass smoke".
|
|
12
|
+
//
|
|
13
|
+
// Usage:
|
|
14
|
+
// node test_interop_smoke.mjs --check (CI default; exits 1 on any
|
|
15
|
+
// non-allowlisted failure)
|
|
16
|
+
// node test_interop_smoke.mjs --report (local triage; prints diagnostic
|
|
17
|
+
// per failure, always exits 0)
|
|
18
|
+
//
|
|
19
|
+
// Fixture subset rationale:
|
|
20
|
+
// The corpus is 9 fixtures across 6 families. After clientffi#69 (Phase E
|
|
21
|
+
// canonize_dispatch fix — mirror of the Phase A frame_native hotfix), the
|
|
22
|
+
// smoke harness covers all 9: caller-registered contexts now thread
|
|
23
|
+
// through the V2 BBS+ sign / verify chain, so OB v3 / EBSI / VP fixtures
|
|
24
|
+
// that reference spec-sourced (non-bundled) contexts can now round-trip.
|
|
25
|
+
//
|
|
26
|
+
// Included (9):
|
|
27
|
+
// - w3c-vc-v1/permanent-resident-card — canonical W3C VC v1 + BBS+
|
|
28
|
+
// - idv2/full-disclosure — Nuggets identity-v2 (post-#56)
|
|
29
|
+
// - nuggets/identity-v1 — Nuggets identity-v1 (post-#63)
|
|
30
|
+
// - nuggets/kyb-v1 — Nuggets kyb-v1 (post-#65)
|
|
31
|
+
// - vp/single-vc-wrap — VP wrapping a VC (post-#66)
|
|
32
|
+
// - openbadges-v3/basic-achievement — OB v3 spec context
|
|
33
|
+
// - openbadges-v3/with-allowed-values — OB v3 spec context
|
|
34
|
+
// - ebsi/diploma-simple — EBSI essif spec context
|
|
35
|
+
// - ebsi/diploma-elm — EBSI ELM (V2-pinned, see #61)
|
|
36
|
+
//
|
|
37
|
+
// Hardcoded BBS+ keypair: `did:example:489398593#test`, the same keypair
|
|
38
|
+
// used by the existing test infrastructure (vc/js/test-data/keyPair.json,
|
|
39
|
+
// matching vc/rs/src/jsonld/signatures/bbs/data/sample_data.rs::KEY_PAIR).
|
|
40
|
+
|
|
41
|
+
import { readFileSync, existsSync, readdirSync } from 'node:fs';
|
|
42
|
+
import { resolve, dirname, join } from 'node:path';
|
|
43
|
+
import { fileURLToPath } from 'node:url';
|
|
44
|
+
import { createRequire } from 'node:module';
|
|
45
|
+
import { ldSign, ldVerify } from './index.js';
|
|
46
|
+
|
|
47
|
+
const require = createRequire(import.meta.url);
|
|
48
|
+
const { Bls12381G2KeyPair } = require('@mattrglobal/bls12381-key-pair');
|
|
49
|
+
const { BbsBlsSignature2020 } = require('@mattrglobal/jsonld-signatures-bbs');
|
|
50
|
+
const jsigs = require('jsonld-signatures');
|
|
51
|
+
const { extendContextLoader, purposes } = jsigs;
|
|
52
|
+
|
|
53
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
54
|
+
const fixturesRoot = resolve(__dirname, 'test-fixtures', 'interop');
|
|
55
|
+
|
|
56
|
+
const args = process.argv.slice(2);
|
|
57
|
+
const checkMode = args.includes('--check');
|
|
58
|
+
const reportMode = args.includes('--report');
|
|
59
|
+
if (!checkMode && !reportMode) {
|
|
60
|
+
console.error('Usage: test_interop_smoke.mjs [--check | --report]');
|
|
61
|
+
process.exit(2);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const allowlistPath = resolve(__dirname, 'interop-smoke-allowlist.json');
|
|
65
|
+
const allowlist = existsSync(allowlistPath)
|
|
66
|
+
? JSON.parse(readFileSync(allowlistPath, 'utf8'))
|
|
67
|
+
: { expect_fail: [] };
|
|
68
|
+
const expectFail = new Set((allowlist.expect_fail || []).map((e) => e.fixture));
|
|
69
|
+
|
|
70
|
+
// All corpus fixtures included in the smoke gate — see header rationale.
|
|
71
|
+
const SMOKE_FIXTURES = [
|
|
72
|
+
'w3c-vc-v1/permanent-resident-card',
|
|
73
|
+
'idv2/full-disclosure',
|
|
74
|
+
'nuggets/identity-v1',
|
|
75
|
+
'nuggets/kyb-v1',
|
|
76
|
+
'vp/single-vc-wrap',
|
|
77
|
+
'openbadges-v3/basic-achievement',
|
|
78
|
+
'openbadges-v3/with-allowed-values',
|
|
79
|
+
'ebsi/diploma-simple',
|
|
80
|
+
'ebsi/diploma-elm',
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
// BBS+ keypair — `did:example:489398593#test`. Single source of truth is
|
|
84
|
+
// vc/js/test-data/keyPair.json, which mirrors
|
|
85
|
+
// vc/rs/src/jsonld/signatures/bbs/data/sample_data.rs::KEY_PAIR. The on-disk
|
|
86
|
+
// file omits `type` (consumed by other tests that don't need it); we patch
|
|
87
|
+
// it in here so the BBS+ suites resolve the verification method correctly.
|
|
88
|
+
const KEYPAIR_PATH = resolve(__dirname, 'test-data', 'keyPair.json');
|
|
89
|
+
const KEYPAIR_JSON = {
|
|
90
|
+
...JSON.parse(readFileSync(KEYPAIR_PATH, 'utf8')),
|
|
91
|
+
type: 'Bls12381G2Key2020',
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const CONTROLLER_DOC = {
|
|
95
|
+
'@context': 'https://w3id.org/security/v2',
|
|
96
|
+
id: KEYPAIR_JSON.controller,
|
|
97
|
+
assertionMethod: [KEYPAIR_JSON.id],
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Load offline interop contexts the same way test_interop.mjs does.
|
|
101
|
+
function loadOfflineContexts() {
|
|
102
|
+
const dir = join(fixturesRoot, '_contexts');
|
|
103
|
+
const out = {};
|
|
104
|
+
if (!existsSync(dir)) return out;
|
|
105
|
+
for (const f of readdirSync(dir)) {
|
|
106
|
+
if (!f.endsWith('.jsonld') && !f.endsWith('.json')) continue;
|
|
107
|
+
const doc = JSON.parse(readFileSync(join(dir, f), 'utf8'));
|
|
108
|
+
if (!doc.__url__) continue;
|
|
109
|
+
const url = doc.__url__;
|
|
110
|
+
const copy = JSON.parse(JSON.stringify(doc));
|
|
111
|
+
delete copy.__url__;
|
|
112
|
+
out[url] = copy;
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Augment with the contexts the BBS+ suites resolve at sign/verify time:
|
|
118
|
+
// the keypair URL itself, the controller doc, and the JWS suite context the
|
|
119
|
+
// `@mattrglobal/jsonld-signatures-bbs` `getVerificationMethod` framing pulls
|
|
120
|
+
// in. `jsonld-signatures` already bundles `security/v1` + `security/v2`
|
|
121
|
+
// (via its `extendContextLoader`), so we don't need to re-add those.
|
|
122
|
+
function buildContexts(offline) {
|
|
123
|
+
const suiteContextPath = resolve(__dirname, 'test-data', 'suiteContext.json');
|
|
124
|
+
const suiteContext = JSON.parse(readFileSync(suiteContextPath, 'utf8'));
|
|
125
|
+
return {
|
|
126
|
+
...offline,
|
|
127
|
+
[KEYPAIR_JSON.id]: KEYPAIR_JSON,
|
|
128
|
+
[KEYPAIR_JSON.controller]: CONTROLLER_DOC,
|
|
129
|
+
'https://w3id.org/security/suites/jws-2020/v1': suiteContext,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function buildJsigsLoader(contextMap) {
|
|
134
|
+
const loader = (url) => {
|
|
135
|
+
const doc = contextMap[url];
|
|
136
|
+
if (doc) return { contextUrl: null, document: doc, documentUrl: url };
|
|
137
|
+
throw new Error(`Smoke harness: unbundled context requested: ${url}`);
|
|
138
|
+
};
|
|
139
|
+
return extendContextLoader(loader);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Strip an existing proof if present (some fixtures may carry one). For the
|
|
143
|
+
// smoke harness we always sign fresh.
|
|
144
|
+
function stripProof(doc) {
|
|
145
|
+
const copy = JSON.parse(JSON.stringify(doc));
|
|
146
|
+
delete copy.proof;
|
|
147
|
+
return copy;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// All current smoke fixtures are VCs — no wrapper unwrapping needed; we
|
|
151
|
+
// just strip any incidental embedded proof before signing.
|
|
152
|
+
function unwrapForSigning(_fixtureId, doc) {
|
|
153
|
+
return stripProof(doc);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Some inputs don't carry the BBS+ context — V2 ldSign auto-adds it; we
|
|
157
|
+
// ensure jsonld-signatures-bbs sees it the same way by injecting it before
|
|
158
|
+
// calling sign. (The suite would otherwise call its own `ensureSuiteContext`
|
|
159
|
+
// which mutates the input but produces an identical effect.)
|
|
160
|
+
function ensureBbsContext(doc) {
|
|
161
|
+
const BBS_URL = 'https://w3id.org/security/bbs/v1';
|
|
162
|
+
const ctx = doc['@context'];
|
|
163
|
+
if (!ctx) {
|
|
164
|
+
doc['@context'] = [BBS_URL];
|
|
165
|
+
return doc;
|
|
166
|
+
}
|
|
167
|
+
if (Array.isArray(ctx)) {
|
|
168
|
+
if (!ctx.includes(BBS_URL)) doc['@context'] = [...ctx, BBS_URL];
|
|
169
|
+
return doc;
|
|
170
|
+
}
|
|
171
|
+
// string or object
|
|
172
|
+
doc['@context'] = [ctx, BBS_URL];
|
|
173
|
+
return doc;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function v2Sign(input, contexts) {
|
|
177
|
+
return await ldSign({
|
|
178
|
+
document: input,
|
|
179
|
+
keyPair: KEYPAIR_JSON,
|
|
180
|
+
contexts,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function v2Verify(signedDoc, contexts) {
|
|
185
|
+
return await ldVerify({
|
|
186
|
+
document: signedDoc,
|
|
187
|
+
contexts,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function mattrSign(input, contexts) {
|
|
192
|
+
const keyPair = await Bls12381G2KeyPair.from({
|
|
193
|
+
id: KEYPAIR_JSON.id,
|
|
194
|
+
controller: KEYPAIR_JSON.controller,
|
|
195
|
+
publicKeyBase58: KEYPAIR_JSON.publicKeyBase58,
|
|
196
|
+
privateKeyBase58: KEYPAIR_JSON.privateKeyBase58,
|
|
197
|
+
});
|
|
198
|
+
const suite = new BbsBlsSignature2020({ key: keyPair });
|
|
199
|
+
const documentLoader = buildJsigsLoader(contexts);
|
|
200
|
+
// Mutate-safe: ensureBbsContext is applied before suite.sign so that
|
|
201
|
+
// expansion has the same vocabulary V2 ldSign would have used.
|
|
202
|
+
const docToSign = ensureBbsContext(JSON.parse(JSON.stringify(input)));
|
|
203
|
+
return await jsigs.sign(docToSign, {
|
|
204
|
+
suite,
|
|
205
|
+
purpose: new purposes.AssertionProofPurpose(),
|
|
206
|
+
documentLoader,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function mattrVerify(signedDoc, contexts) {
|
|
211
|
+
const documentLoader = buildJsigsLoader(contexts);
|
|
212
|
+
const result = await jsigs.verify(signedDoc, {
|
|
213
|
+
suite: new BbsBlsSignature2020(),
|
|
214
|
+
purpose: new purposes.AssertionProofPurpose(),
|
|
215
|
+
documentLoader,
|
|
216
|
+
});
|
|
217
|
+
return result;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function summariseError(err) {
|
|
221
|
+
if (!err) return 'unknown';
|
|
222
|
+
if (typeof err === 'string') return err;
|
|
223
|
+
if (err.message) return err.message;
|
|
224
|
+
if (err.errors && err.errors.length) {
|
|
225
|
+
return err.errors.map((e) => e.message || String(e)).join('; ');
|
|
226
|
+
}
|
|
227
|
+
return String(err);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function runDirection(directionId, signFn, verifyFn, input, contexts) {
|
|
231
|
+
let signed;
|
|
232
|
+
try {
|
|
233
|
+
signed = await signFn(input, contexts);
|
|
234
|
+
} catch (err) {
|
|
235
|
+
return { direction: directionId, status: 'error', stage: 'sign', error: summariseError(err) };
|
|
236
|
+
}
|
|
237
|
+
let verifyResult;
|
|
238
|
+
try {
|
|
239
|
+
verifyResult = await verifyFn(signed, contexts);
|
|
240
|
+
} catch (err) {
|
|
241
|
+
return { direction: directionId, status: 'error', stage: 'verify', error: summariseError(err) };
|
|
242
|
+
}
|
|
243
|
+
if (verifyResult && verifyResult.verified === true) {
|
|
244
|
+
return { direction: directionId, status: 'pass' };
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
direction: directionId,
|
|
248
|
+
status: 'fail',
|
|
249
|
+
stage: 'verify',
|
|
250
|
+
error: summariseError(verifyResult && verifyResult.error),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function runFixture(fixture, contexts) {
|
|
255
|
+
const inputPath = join(fixture.dir, 'input.jsonld');
|
|
256
|
+
if (!existsSync(inputPath)) {
|
|
257
|
+
return { id: fixture.id, status: 'skip', reason: 'missing input.jsonld' };
|
|
258
|
+
}
|
|
259
|
+
const raw = JSON.parse(readFileSync(inputPath, 'utf8'));
|
|
260
|
+
const input = unwrapForSigning(fixture.id, raw);
|
|
261
|
+
|
|
262
|
+
const v2ToMattr = await runDirection(
|
|
263
|
+
'v2->jsonld',
|
|
264
|
+
v2Sign,
|
|
265
|
+
mattrVerify,
|
|
266
|
+
input,
|
|
267
|
+
contexts,
|
|
268
|
+
);
|
|
269
|
+
const mattrToV2 = await runDirection(
|
|
270
|
+
'jsonld->v2',
|
|
271
|
+
mattrSign,
|
|
272
|
+
v2Verify,
|
|
273
|
+
input,
|
|
274
|
+
contexts,
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
const overall =
|
|
278
|
+
v2ToMattr.status === 'pass' && mattrToV2.status === 'pass'
|
|
279
|
+
? 'pass'
|
|
280
|
+
: 'fail';
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
id: fixture.id,
|
|
284
|
+
status: overall,
|
|
285
|
+
directions: { v2ToMattr, mattrToV2 },
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function main() {
|
|
290
|
+
const offline = loadOfflineContexts();
|
|
291
|
+
const contexts = buildContexts(offline);
|
|
292
|
+
|
|
293
|
+
const fixtures = SMOKE_FIXTURES.map((id) => ({
|
|
294
|
+
id,
|
|
295
|
+
dir: join(fixturesRoot, ...id.split('/')),
|
|
296
|
+
}));
|
|
297
|
+
|
|
298
|
+
const results = [];
|
|
299
|
+
for (const fx of fixtures) {
|
|
300
|
+
process.stderr.write(` smoke ${fx.id}…\n`);
|
|
301
|
+
results.push(await runFixture(fx, contexts));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const pass = results.filter((r) => r.status === 'pass').length;
|
|
305
|
+
const fail = results.filter((r) => r.status === 'fail').length;
|
|
306
|
+
const skip = results.filter((r) => r.status === 'skip').length;
|
|
307
|
+
|
|
308
|
+
console.log(`\n========== INTEROP SMOKE ==========`);
|
|
309
|
+
console.log(`pass: ${pass}`);
|
|
310
|
+
console.log(`fail: ${fail}`);
|
|
311
|
+
console.log(`skip: ${skip}`);
|
|
312
|
+
|
|
313
|
+
// Per-fixture results table
|
|
314
|
+
console.log(`\nfixture | v2->jsonld | jsonld->v2 | overall`);
|
|
315
|
+
console.log(`-------------------------------------------+------------+------------+--------`);
|
|
316
|
+
for (const r of results) {
|
|
317
|
+
const v2j = r.directions ? r.directions.v2ToMattr.status : '-';
|
|
318
|
+
const jv2 = r.directions ? r.directions.mattrToV2.status : '-';
|
|
319
|
+
const id = r.id.padEnd(42);
|
|
320
|
+
console.log(`${id} | ${v2j.padEnd(10)} | ${jv2.padEnd(10)} | ${r.status}`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const regressions = [];
|
|
324
|
+
const improvements = [];
|
|
325
|
+
const expectedFails = [];
|
|
326
|
+
const skips = [];
|
|
327
|
+
for (const r of results) {
|
|
328
|
+
if (r.status === 'pass') {
|
|
329
|
+
if (expectFail.has(r.id)) improvements.push(r);
|
|
330
|
+
} else if (r.status === 'fail') {
|
|
331
|
+
if (expectFail.has(r.id)) expectedFails.push(r);
|
|
332
|
+
else regressions.push(r);
|
|
333
|
+
} else if (r.status === 'skip') {
|
|
334
|
+
skips.push(r);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (reportMode) {
|
|
339
|
+
for (const r of [...regressions, ...expectedFails]) {
|
|
340
|
+
console.log(`\n--- ${r.id} (${r.status}) ---`);
|
|
341
|
+
if (r.directions) {
|
|
342
|
+
for (const d of [r.directions.v2ToMattr, r.directions.mattrToV2]) {
|
|
343
|
+
if (d.status === 'pass') continue;
|
|
344
|
+
console.log(` [${d.direction}] ${d.status} @ ${d.stage}: ${d.error}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
console.log(
|
|
349
|
+
`\n[--report] ${regressions.length} divergence(s); ${expectedFails.length} expected-fail; ${improvements.length} improvement(s); ${skips.length} skip(s).`,
|
|
350
|
+
);
|
|
351
|
+
process.exit(0);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// --check mode
|
|
355
|
+
if (improvements.length) {
|
|
356
|
+
console.log(`\n${improvements.length} fixture(s) newly passing — please ratchet interop-smoke-allowlist.json:`);
|
|
357
|
+
for (const r of improvements) console.log(` + ${r.id}`);
|
|
358
|
+
process.exit(1);
|
|
359
|
+
}
|
|
360
|
+
if (regressions.length) {
|
|
361
|
+
console.log(`\n${regressions.length} REGRESSION(S):`);
|
|
362
|
+
for (const r of regressions) {
|
|
363
|
+
console.log(` - ${r.id}`);
|
|
364
|
+
if (r.directions) {
|
|
365
|
+
for (const d of [r.directions.v2ToMattr, r.directions.mattrToV2]) {
|
|
366
|
+
if (d.status === 'pass') continue;
|
|
367
|
+
console.log(` [${d.direction}] ${d.status} @ ${d.stage}: ${d.error}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
process.exit(1);
|
|
372
|
+
}
|
|
373
|
+
if (skips.length) {
|
|
374
|
+
console.log(`\n${skips.length} skipped fixture(s):`);
|
|
375
|
+
for (const r of skips) console.log(` ! ${r.id}: ${r.reason}`);
|
|
376
|
+
process.exit(1);
|
|
377
|
+
}
|
|
378
|
+
console.log(
|
|
379
|
+
`\nAll smoke fixtures round-trip cleanly.${
|
|
380
|
+
expectedFails.length ? ' (' + expectedFails.length + ' tracked expect-fail.)' : ''
|
|
381
|
+
}`,
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
main().catch((e) => {
|
|
386
|
+
console.error(e);
|
|
387
|
+
process.exit(1);
|
|
388
|
+
});
|
package/test_w3c_conformance.mjs
CHANGED
|
@@ -271,6 +271,7 @@ async function runFraming() {
|
|
|
271
271
|
for (const entry of manifest.sequence) {
|
|
272
272
|
const types = Array.isArray(entry['@type']) ? entry['@type'] : [entry['@type']];
|
|
273
273
|
if (!types.includes('jld:PositiveEvaluationTest') && !types.includes('jld:NegativeEvaluationTest')) continue;
|
|
274
|
+
const isNegative = types.includes('jld:NegativeEvaluationTest');
|
|
274
275
|
const id = entry['@id'];
|
|
275
276
|
if (isDenied('frame', id)) continue;
|
|
276
277
|
if (!isInCheckScope('frame', id)) continue;
|
|
@@ -292,11 +293,17 @@ async function runFraming() {
|
|
|
292
293
|
const input = loadJson(inputPath);
|
|
293
294
|
const frame = loadJson(framePath);
|
|
294
295
|
const napiResult = await withTimeout(proc.frame(input, frame, callOpts), id);
|
|
296
|
+
// Negative tests: a returned result is a failure (the impl should
|
|
297
|
+
// have aborted with an "invalid frame" / "invalid @embed value"
|
|
298
|
+
// error per W3C Framing 1.1 §4 / §4.5).
|
|
299
|
+
if (isNegative) { record('frame', id, 'fail'); continue; }
|
|
295
300
|
if (!expectPath || !existsSync(expectPath)) { record('frame', id, 'err'); continue; }
|
|
296
301
|
const expected = loadJson(expectPath);
|
|
297
302
|
record('frame', id, jsonEqual(napiResult, expected) ? 'pass' : 'fail');
|
|
298
303
|
} catch (e) {
|
|
299
304
|
const isTimeout = e && /^timeout:/.test(e.message || '');
|
|
305
|
+
// Negative tests: a thrown error IS the expected outcome.
|
|
306
|
+
if (isNegative && !isTimeout) { record('frame', id, 'pass'); continue; }
|
|
300
307
|
record('frame', id, isTimeout ? 'err' : 'err');
|
|
301
308
|
}
|
|
302
309
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Regenerate `expected.nq` for one or more interop fixtures using jsonld.js
|
|
3
|
+
// (the third-party reference). The harness in `test_interop.mjs` then asserts
|
|
4
|
+
// V2 native produces byte-equal output. Never hand-edit `expected.nq`.
|
|
5
|
+
//
|
|
6
|
+
// Usage:
|
|
7
|
+
// node tools/regen_expected.mjs <fixture-dir> [<fixture-dir>...]
|
|
8
|
+
// node tools/regen_expected.mjs --all
|
|
9
|
+
//
|
|
10
|
+
// Examples:
|
|
11
|
+
// node tools/regen_expected.mjs test-fixtures/interop/w3c-vc-v1/permanent-resident-card
|
|
12
|
+
// node tools/regen_expected.mjs --all
|
|
13
|
+
|
|
14
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from 'node:fs';
|
|
15
|
+
import { resolve, dirname, join, relative } from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import jsonld from 'jsonld';
|
|
18
|
+
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const repoRoot = resolve(__dirname, '..');
|
|
21
|
+
const fixturesRoot = resolve(repoRoot, 'test-fixtures', 'interop');
|
|
22
|
+
|
|
23
|
+
const args = process.argv.slice(2);
|
|
24
|
+
if (args.length === 0) {
|
|
25
|
+
console.error('Usage: regen_expected.mjs <fixture-dir>... | --all');
|
|
26
|
+
process.exit(2);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Build the offline document loader from the fixtures' contexts. We don't
|
|
30
|
+
// fetch anything from the network — every context referenced by a fixture
|
|
31
|
+
// must resolve from the bundled `vc/js/test-fixtures/interop/_contexts/` dir
|
|
32
|
+
// or from inline contexts (i.e. embedded `@context` objects).
|
|
33
|
+
const contextDir = resolve(fixturesRoot, '_contexts');
|
|
34
|
+
const offlineContexts = {};
|
|
35
|
+
if (existsSync(contextDir)) {
|
|
36
|
+
for (const f of readdirSync(contextDir)) {
|
|
37
|
+
if (!f.endsWith('.jsonld') && !f.endsWith('.json')) continue;
|
|
38
|
+
const doc = JSON.parse(readFileSync(join(contextDir, f), 'utf8'));
|
|
39
|
+
const url = doc.__url__;
|
|
40
|
+
if (!url) {
|
|
41
|
+
console.error(`context ${f} missing __url__ marker`);
|
|
42
|
+
process.exit(2);
|
|
43
|
+
}
|
|
44
|
+
delete doc.__url__;
|
|
45
|
+
offlineContexts[url] = doc;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const documentLoader = async (url) => {
|
|
50
|
+
if (!offlineContexts[url]) {
|
|
51
|
+
throw new Error(`offline loader: unknown context ${url}`);
|
|
52
|
+
}
|
|
53
|
+
return { contextUrl: null, document: offlineContexts[url], documentUrl: url };
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
async function regen(fixtureDir) {
|
|
57
|
+
const inputPath = join(fixtureDir, 'input.jsonld');
|
|
58
|
+
const framePath = join(fixtureDir, 'frame.jsonld');
|
|
59
|
+
const expectedPath = join(fixtureDir, 'expected.nq');
|
|
60
|
+
if (!existsSync(inputPath) || !existsSync(framePath)) {
|
|
61
|
+
console.error(`skipping ${fixtureDir}: missing input.jsonld or frame.jsonld`);
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
const input = JSON.parse(readFileSync(inputPath, 'utf8'));
|
|
65
|
+
const frame = JSON.parse(readFileSync(framePath, 'utf8'));
|
|
66
|
+
const framed = await jsonld.frame(input, frame, { documentLoader });
|
|
67
|
+
const nquads = await jsonld.canonize(framed, {
|
|
68
|
+
algorithm: 'URDNA2015',
|
|
69
|
+
format: 'application/n-quads',
|
|
70
|
+
documentLoader,
|
|
71
|
+
});
|
|
72
|
+
// Normalize: LF, trailing newline, no BOM
|
|
73
|
+
const normalized = nquads.endsWith('\n') ? nquads : nquads + '\n';
|
|
74
|
+
writeFileSync(expectedPath, normalized, { encoding: 'utf8' });
|
|
75
|
+
console.log(`wrote ${relative(repoRoot, expectedPath)} (${normalized.length} bytes)`);
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function discoverFixtureDirs(root) {
|
|
80
|
+
const out = [];
|
|
81
|
+
for (const family of readdirSync(root)) {
|
|
82
|
+
if (family.startsWith('_')) continue; // skip _contexts/
|
|
83
|
+
const familyDir = join(root, family);
|
|
84
|
+
if (!statSync(familyDir).isDirectory()) continue;
|
|
85
|
+
for (const name of readdirSync(familyDir)) {
|
|
86
|
+
const fixtureDir = join(familyDir, name);
|
|
87
|
+
if (!statSync(fixtureDir).isDirectory()) continue;
|
|
88
|
+
out.push(fixtureDir);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const targets = args[0] === '--all'
|
|
95
|
+
? discoverFixtureDirs(fixturesRoot)
|
|
96
|
+
: args.map((a) => resolve(a));
|
|
97
|
+
|
|
98
|
+
let ok = 0, skipped = 0, failed = 0;
|
|
99
|
+
for (const dir of targets) {
|
|
100
|
+
try {
|
|
101
|
+
if (await regen(dir)) ok += 1; else skipped += 1;
|
|
102
|
+
} catch (err) {
|
|
103
|
+
failed += 1;
|
|
104
|
+
console.error(`failed ${dir}: ${err && err.message ? err.message : err}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
console.log(`\nregenerated: ${ok}; skipped: ${skipped}; failed: ${failed}`);
|
|
108
|
+
process.exit(failed > 0 ? 1 : 0);
|