@nuggetslife/vc 0.1.0 → 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/interop-allowlist.json +3 -0
- package/interop-smoke-allowlist.json +3 -0
- package/package.json +8 -7
- package/src/jsonld.rs +2 -1
- 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/tools/regen_expected.mjs +108 -0
- package/w3c-baseline.json +27 -30
- 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
|
+
});
|
|
@@ -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);
|
package/w3c-baseline.json
CHANGED
|
@@ -1297,15 +1297,35 @@
|
|
|
1297
1297
|
},
|
|
1298
1298
|
"frame": {
|
|
1299
1299
|
"passing": [
|
|
1300
|
+
"#t0001",
|
|
1301
|
+
"#t0002",
|
|
1302
|
+
"#t0003",
|
|
1303
|
+
"#t0004",
|
|
1304
|
+
"#t0005",
|
|
1305
|
+
"#t0006",
|
|
1306
|
+
"#t0007",
|
|
1307
|
+
"#t0008",
|
|
1308
|
+
"#t0009",
|
|
1309
|
+
"#t0010",
|
|
1300
1310
|
"#t0011",
|
|
1301
1311
|
"#t0012",
|
|
1302
1312
|
"#t0013",
|
|
1313
|
+
"#t0014",
|
|
1303
1314
|
"#t0015",
|
|
1315
|
+
"#t0016",
|
|
1316
|
+
"#t0017",
|
|
1317
|
+
"#t0018",
|
|
1304
1318
|
"#t0019",
|
|
1319
|
+
"#t0020",
|
|
1320
|
+
"#t0021",
|
|
1321
|
+
"#t0022",
|
|
1305
1322
|
"#t0023",
|
|
1306
1323
|
"#t0024",
|
|
1324
|
+
"#t0025",
|
|
1307
1325
|
"#t0026",
|
|
1308
1326
|
"#t0027",
|
|
1327
|
+
"#t0028",
|
|
1328
|
+
"#t0029",
|
|
1309
1329
|
"#t0030",
|
|
1310
1330
|
"#t0031",
|
|
1311
1331
|
"#t0032",
|
|
@@ -1325,39 +1345,27 @@
|
|
|
1325
1345
|
"#tg001",
|
|
1326
1346
|
"#tg003",
|
|
1327
1347
|
"#tg004",
|
|
1348
|
+
"#tp021",
|
|
1328
1349
|
"#tra02"
|
|
1329
1350
|
],
|
|
1330
1351
|
"failing": [
|
|
1331
|
-
"#t0001",
|
|
1332
|
-
"#t0002",
|
|
1333
|
-
"#t0003",
|
|
1334
|
-
"#t0004",
|
|
1335
|
-
"#t0005",
|
|
1336
|
-
"#t0006",
|
|
1337
|
-
"#t0007",
|
|
1338
|
-
"#t0008",
|
|
1339
|
-
"#t0010",
|
|
1340
|
-
"#t0014",
|
|
1341
|
-
"#t0016",
|
|
1342
|
-
"#t0017",
|
|
1343
|
-
"#t0018",
|
|
1344
|
-
"#t0020",
|
|
1345
|
-
"#t0021",
|
|
1346
|
-
"#t0022",
|
|
1347
|
-
"#t0028",
|
|
1348
1352
|
"#t0035",
|
|
1353
|
+
"#t0045",
|
|
1349
1354
|
"#t0046",
|
|
1350
1355
|
"#t0047",
|
|
1351
1356
|
"#t0048",
|
|
1352
1357
|
"#t0049",
|
|
1353
1358
|
"#t0050",
|
|
1354
1359
|
"#t0051",
|
|
1360
|
+
"#t0055",
|
|
1355
1361
|
"#t0056",
|
|
1356
1362
|
"#t0057",
|
|
1357
1363
|
"#t0058",
|
|
1364
|
+
"#t0059",
|
|
1358
1365
|
"#t0061",
|
|
1359
1366
|
"#t0062",
|
|
1360
1367
|
"#t0063",
|
|
1368
|
+
"#t0064",
|
|
1361
1369
|
"#t0065",
|
|
1362
1370
|
"#t0066",
|
|
1363
1371
|
"#t0067",
|
|
@@ -1368,10 +1376,10 @@
|
|
|
1368
1376
|
"#tg005",
|
|
1369
1377
|
"#tg006",
|
|
1370
1378
|
"#tg007",
|
|
1379
|
+
"#tg008",
|
|
1371
1380
|
"#tg009",
|
|
1372
1381
|
"#tg010",
|
|
1373
1382
|
"#tp020",
|
|
1374
|
-
"#tp021",
|
|
1375
1383
|
"#tp046",
|
|
1376
1384
|
"#tp049",
|
|
1377
1385
|
"#tp050",
|
|
@@ -1379,20 +1387,9 @@
|
|
|
1379
1387
|
"#tra03"
|
|
1380
1388
|
],
|
|
1381
1389
|
"erroring": [
|
|
1382
|
-
"#t0009",
|
|
1383
|
-
"#t0025",
|
|
1384
|
-
"#t0029",
|
|
1385
|
-
"#t0045",
|
|
1386
1390
|
"#t0052",
|
|
1387
1391
|
"#t0053",
|
|
1388
|
-
"#t0054"
|
|
1389
|
-
"#t0055",
|
|
1390
|
-
"#t0059",
|
|
1391
|
-
"#t0064",
|
|
1392
|
-
"#tg008",
|
|
1393
|
-
"#tin01",
|
|
1394
|
-
"#tin02",
|
|
1395
|
-
"#tin03"
|
|
1392
|
+
"#t0054"
|
|
1396
1393
|
]
|
|
1397
1394
|
}
|
|
1398
1395
|
}
|