@docknetwork/wallet-sdk-core 1.7.7-alpha.0 → 1.9.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/lib/cloud-wallet.d.ts +79 -3
- package/lib/cloud-wallet.d.ts.map +1 -1
- package/lib/cloud-wallet.js +147 -14
- package/lib/cloud-wallet.js.map +1 -1
- package/lib/credential-provider.d.ts.map +1 -1
- package/lib/credential-provider.js +10 -4
- package/lib/credential-provider.js.map +1 -1
- package/lib/delegation/delegation-chain.d.ts +8 -0
- package/lib/delegation/delegation-chain.d.ts.map +1 -0
- package/lib/delegation/delegation-chain.js +33 -0
- package/lib/delegation/delegation-chain.js.map +1 -0
- package/lib/delegation/delegation-fixtures.d.ts +69 -0
- package/lib/delegation/delegation-fixtures.d.ts.map +1 -0
- package/lib/delegation/delegation-fixtures.js +553 -0
- package/lib/delegation/delegation-fixtures.js.map +1 -0
- package/lib/delegation/delegation-issuance.d.ts +19 -0
- package/lib/delegation/delegation-issuance.d.ts.map +1 -0
- package/lib/delegation/delegation-issuance.js +60 -0
- package/lib/delegation/delegation-issuance.js.map +1 -0
- package/lib/delegation/delegation-offer.d.ts +84 -0
- package/lib/delegation/delegation-offer.d.ts.map +1 -0
- package/lib/delegation/delegation-offer.js +349 -0
- package/lib/delegation/delegation-offer.js.map +1 -0
- package/lib/delegation/delegation-policy-validation.d.ts +28 -0
- package/lib/delegation/delegation-policy-validation.d.ts.map +1 -0
- package/lib/delegation/delegation-policy-validation.js +170 -0
- package/lib/delegation/delegation-policy-validation.js.map +1 -0
- package/lib/delegation/delegation-policy.d.ts +21 -0
- package/lib/delegation/delegation-policy.d.ts.map +1 -0
- package/lib/delegation/delegation-policy.js +73 -0
- package/lib/delegation/delegation-policy.js.map +1 -0
- package/lib/delegation/delegation-tree.d.ts +17 -0
- package/lib/delegation/delegation-tree.d.ts.map +1 -0
- package/lib/delegation/delegation-tree.js +58 -0
- package/lib/delegation/delegation-tree.js.map +1 -0
- package/lib/delegation/delegation-types.d.ts +56 -0
- package/lib/delegation/delegation-types.d.ts.map +1 -0
- package/lib/delegation/delegation-types.js +3 -0
- package/lib/delegation/delegation-types.js.map +1 -0
- package/lib/delegation/delegation-utils.d.ts +3 -0
- package/lib/delegation/delegation-utils.d.ts.map +1 -0
- package/lib/delegation/delegation-utils.js +10 -0
- package/lib/delegation/delegation-utils.js.map +1 -0
- package/lib/did-provider.d.ts +2 -1
- package/lib/did-provider.d.ts.map +1 -1
- package/lib/did-provider.js +11 -7
- package/lib/did-provider.js.map +1 -1
- package/lib/message-provider.js +1 -1
- package/lib/message-provider.js.map +1 -1
- package/lib/verification-controller.d.ts +30 -11
- package/lib/verification-controller.d.ts.map +1 -1
- package/lib/verification-controller.js +372 -68
- package/lib/verification-controller.js.map +1 -1
- package/package.json +3 -3
- package/src/cloud-wallet.test.js +369 -0
- package/src/cloud-wallet.ts +206 -18
- package/src/credential-provider.ts +13 -4
- package/src/delegation/delegation-chain.test.ts +64 -0
- package/src/delegation/delegation-chain.ts +34 -0
- package/src/delegation/delegation-fixtures.ts +552 -0
- package/src/delegation/delegation-issuance.ts +92 -0
- package/src/delegation/delegation-offer.ts +488 -0
- package/src/delegation/delegation-policy-validation.test.ts +237 -0
- package/src/delegation/delegation-policy-validation.ts +281 -0
- package/src/delegation/delegation-policy.ts +100 -0
- package/src/delegation/delegation-tree.test.ts +110 -0
- package/src/delegation/delegation-tree.ts +60 -0
- package/src/delegation/delegation-types.ts +65 -0
- package/src/delegation/delegation-utils.ts +10 -0
- package/src/did-provider.ts +10 -6
- package/src/globals.d.ts +6 -0
- package/src/message-provider.ts +1 -1
- package/src/verification-controller.test.ts +23 -0
- package/src/verification-controller.ts +534 -82
- package/tsconfig.build.json +2 -1
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -3,6 +3,7 @@ import {pexService} from '@docknetwork/wallet-sdk-wasm/src/services/pex';
|
|
|
3
3
|
import {credentialServiceRPC} from '@docknetwork/wallet-sdk-wasm/src/services/credential';
|
|
4
4
|
import {
|
|
5
5
|
createCredentialProvider,
|
|
6
|
+
CredentialStatus,
|
|
6
7
|
ICredentialProvider,
|
|
7
8
|
} from './credential-provider';
|
|
8
9
|
import {IWallet} from './types';
|
|
@@ -21,17 +22,55 @@ export enum VerificationStatus {
|
|
|
21
22
|
SelectingCredentials = 'SelectingCredentials',
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
function isRangeProofTemplate(templateJSON) {
|
|
25
|
-
return templateJSON.proving_key;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
25
|
type CredentialId = string;
|
|
29
26
|
type CredentialSelection = {
|
|
30
27
|
credential: any;
|
|
28
|
+
/**
|
|
29
|
+
* Optional list of credential attributes to reveal in the presentation.
|
|
30
|
+
* When omitted, the credential-sdk automatically determines which attributes
|
|
31
|
+
* to reveal based on the PEX (Presentation Exchange) template requirements.
|
|
32
|
+
* This allows generating a default presentation without manual attribute selection.
|
|
33
|
+
*/
|
|
31
34
|
attributesToReveal?: string[];
|
|
32
35
|
};
|
|
33
36
|
type CredentialSelectionMap = Map<CredentialId, CredentialSelection>;
|
|
34
37
|
|
|
38
|
+
export interface IVerificationController {
|
|
39
|
+
emitter: EventEmitter;
|
|
40
|
+
selectedCredentials: CredentialSelectionMap;
|
|
41
|
+
getStatus: () => VerificationStatus;
|
|
42
|
+
getStatusData: () => any;
|
|
43
|
+
submitPresentation: (presentation: any) => Promise<any>;
|
|
44
|
+
getSelectedDID: () => string | null;
|
|
45
|
+
setSelectedDID: (did: string) => void;
|
|
46
|
+
start: (params: {template: string | any}) => Promise<void>;
|
|
47
|
+
isBBSPlusCredential: (credential: any) => Promise<boolean>;
|
|
48
|
+
loadCredentials: () => Promise<void>;
|
|
49
|
+
getFilteredCredentials: () => any[];
|
|
50
|
+
createPresentation: () => Promise<any>;
|
|
51
|
+
createDefaultPresentation: () => Promise<any>;
|
|
52
|
+
evaluatePresentation: (presentation: any) => {
|
|
53
|
+
isValid: boolean;
|
|
54
|
+
errors: any[];
|
|
55
|
+
warnings: any[];
|
|
56
|
+
};
|
|
57
|
+
getRequirementGroups: () => Array<{
|
|
58
|
+
descriptorKey: string;
|
|
59
|
+
descriptorName: string;
|
|
60
|
+
candidates: any[];
|
|
61
|
+
}>;
|
|
62
|
+
getSelectedCredentialsByDescriptor: () => any[];
|
|
63
|
+
getCredentialOptionsForDescriptor: (credentialId: string) => Promise<any>;
|
|
64
|
+
switchCredential: (
|
|
65
|
+
currentCredentialId: string,
|
|
66
|
+
replacementCredentialId: string,
|
|
67
|
+
) => Promise<void>;
|
|
68
|
+
getRequestedAttributes: (credentialId: string) => any[];
|
|
69
|
+
getCredentialStatus: (credentialId: string) => Promise<any>;
|
|
70
|
+
canSwitchCredential: (credentialId: string) => Promise<boolean>;
|
|
71
|
+
getTemplateJSON: () => any;
|
|
72
|
+
}
|
|
73
|
+
|
|
35
74
|
export function createVerificationController({
|
|
36
75
|
wallet,
|
|
37
76
|
credentialProvider,
|
|
@@ -40,7 +79,7 @@ export function createVerificationController({
|
|
|
40
79
|
wallet: IWallet;
|
|
41
80
|
credentialProvider?: ICredentialProvider;
|
|
42
81
|
didProvider?: IDIDProvider;
|
|
43
|
-
}) {
|
|
82
|
+
}): IVerificationController {
|
|
44
83
|
const emitter = new EventEmitter();
|
|
45
84
|
let templateJSON = null;
|
|
46
85
|
let status = VerificationStatus.Started;
|
|
@@ -50,9 +89,9 @@ export function createVerificationController({
|
|
|
50
89
|
*/
|
|
51
90
|
let statusData = null;
|
|
52
91
|
let filteredCredentials = [];
|
|
92
|
+
let filteredMatches = [];
|
|
53
93
|
let selectedCredentials: CredentialSelectionMap = new Map();
|
|
54
94
|
let selectedDID = null;
|
|
55
|
-
let provingKey = null;
|
|
56
95
|
|
|
57
96
|
if (!credentialProvider) {
|
|
58
97
|
credentialProvider = createCredentialProvider({wallet});
|
|
@@ -62,23 +101,6 @@ export function createVerificationController({
|
|
|
62
101
|
didProvider = createDIDProvider({wallet});
|
|
63
102
|
}
|
|
64
103
|
|
|
65
|
-
async function fetchProvingKey(templateJSON: any) {
|
|
66
|
-
if (templateJSON.proving_key) {
|
|
67
|
-
setState(VerificationStatus.FetchingProvingKey);
|
|
68
|
-
try {
|
|
69
|
-
provingKey = await axios
|
|
70
|
-
.get(templateJSON.proving_key)
|
|
71
|
-
.then(res => res.data);
|
|
72
|
-
} catch (err) {
|
|
73
|
-
setState(VerificationStatus.Error, {
|
|
74
|
-
message: 'failed_to_fetch_proving_key',
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
throw err;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
104
|
async function start({template}: {template: string | any}) {
|
|
83
105
|
setState(VerificationStatus.LoadingTemplate);
|
|
84
106
|
|
|
@@ -96,7 +118,6 @@ export function createVerificationController({
|
|
|
96
118
|
selectedDID = dids[0].didDocument.id;
|
|
97
119
|
templateJSON = await getJSON(template);
|
|
98
120
|
|
|
99
|
-
await fetchProvingKey(templateJSON);
|
|
100
121
|
await loadCredentials();
|
|
101
122
|
|
|
102
123
|
setState(VerificationStatus.SelectingCredentials);
|
|
@@ -127,6 +148,7 @@ export function createVerificationController({
|
|
|
127
148
|
});
|
|
128
149
|
|
|
129
150
|
filteredCredentials = result.verifiableCredential;
|
|
151
|
+
filteredMatches = result.matches || [];
|
|
130
152
|
} catch (err) {
|
|
131
153
|
console.error(
|
|
132
154
|
`Unable to filter credentials using the template: \n ${JSON.stringify(
|
|
@@ -154,70 +176,354 @@ export function createVerificationController({
|
|
|
154
176
|
return credentialServiceRPC.isKvacCredential({credential});
|
|
155
177
|
}
|
|
156
178
|
|
|
157
|
-
async function
|
|
158
|
-
|
|
159
|
-
assert(!!selectedCredentials.size, 'No credentials selected');
|
|
179
|
+
async function deriveNonBbsCredentials(sdJwtSelections, regularSelections) {
|
|
180
|
+
const credentials = [];
|
|
160
181
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
182
|
+
for (const sel of sdJwtSelections) {
|
|
183
|
+
const derived = await credentialServiceRPC.createSDJWTPresentation({
|
|
184
|
+
attributesToReveal: sel.attributesToReveal,
|
|
185
|
+
credential: sel.credential._sd_jwt.encoded,
|
|
186
|
+
});
|
|
187
|
+
credentials.push(derived);
|
|
164
188
|
}
|
|
165
189
|
|
|
166
|
-
const
|
|
190
|
+
for (const sel of regularSelections) {
|
|
191
|
+
credentials.push(sel.credential);
|
|
192
|
+
}
|
|
167
193
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const isKVAC = await isKvacCredential(credentialSelection.credential);
|
|
194
|
+
return credentials;
|
|
195
|
+
}
|
|
171
196
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
{
|
|
188
|
-
credential: credentialSelection.credential,
|
|
189
|
-
witness: await credentialProvider.getMembershipWitness(credentialSelection.credential.id),
|
|
190
|
-
attributesToReveal: [
|
|
191
|
-
...(credentialSelection.attributesToReveal || []),
|
|
192
|
-
'id',
|
|
193
|
-
],
|
|
194
|
-
},
|
|
195
|
-
],
|
|
196
|
-
});
|
|
197
|
+
function getKeyId(keyDoc) {
|
|
198
|
+
return keyDoc.controller.startsWith('did:key:')
|
|
199
|
+
? keyDoc.id
|
|
200
|
+
: `${keyDoc.controller}#keys-1`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function assembleSignedPresentation(credentials, keyDoc) {
|
|
204
|
+
return credentialServiceRPC.createPresentation({
|
|
205
|
+
credentials,
|
|
206
|
+
challenge: templateJSON.nonce,
|
|
207
|
+
keyDoc,
|
|
208
|
+
id: getKeyId(keyDoc),
|
|
209
|
+
domain: 'dock.io',
|
|
210
|
+
});
|
|
211
|
+
}
|
|
197
212
|
|
|
198
|
-
|
|
213
|
+
function getRequirementGroups() {
|
|
214
|
+
if (filteredMatches.length === 0) {
|
|
215
|
+
return [
|
|
216
|
+
{
|
|
217
|
+
descriptorKey: 'default',
|
|
218
|
+
descriptorName: 'default',
|
|
219
|
+
candidates: [...filteredCredentials],
|
|
220
|
+
},
|
|
221
|
+
];
|
|
222
|
+
}
|
|
199
223
|
|
|
200
|
-
|
|
224
|
+
const groups: Array<{
|
|
225
|
+
descriptorKey: string;
|
|
226
|
+
descriptorName: string;
|
|
227
|
+
candidates: any[];
|
|
228
|
+
}> = [];
|
|
229
|
+
const groupKeyFn = match =>
|
|
230
|
+
match.from ? JSON.stringify(match.from) : match.name || match.id || '';
|
|
231
|
+
const seen = new Map<string, number>();
|
|
232
|
+
|
|
233
|
+
for (const match of filteredMatches) {
|
|
234
|
+
const key = groupKeyFn(match);
|
|
235
|
+
const candidateIndices: number[] = [];
|
|
236
|
+
for (const path of match.vc_path || []) {
|
|
237
|
+
const indexMatch = path.match(/\[(\d+)\]/);
|
|
238
|
+
if (indexMatch) {
|
|
239
|
+
candidateIndices.push(parseInt(indexMatch[1], 10));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const candidates = candidateIndices
|
|
243
|
+
.map(idx => filteredCredentials[idx])
|
|
244
|
+
.filter(Boolean);
|
|
245
|
+
|
|
246
|
+
if (match.from || !seen.has(key)) {
|
|
247
|
+
seen.set(key, groups.length);
|
|
248
|
+
groups.push({
|
|
249
|
+
descriptorKey: key,
|
|
250
|
+
descriptorName: match.name || match.id || key,
|
|
251
|
+
candidates,
|
|
252
|
+
});
|
|
201
253
|
} else {
|
|
202
|
-
|
|
254
|
+
const existing = groups[seen.get(key)];
|
|
255
|
+
for (const cred of candidates) {
|
|
256
|
+
if (!existing.candidates.some(c => c.id === cred.id)) {
|
|
257
|
+
existing.candidates.push(cred);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
203
260
|
}
|
|
204
261
|
}
|
|
205
262
|
|
|
263
|
+
return groups;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function filterValidCredentials(candidates: any[]) {
|
|
267
|
+
const results = await Promise.all(
|
|
268
|
+
candidates.map(async cred => {
|
|
269
|
+
const statusResult = await credentialProvider.getCredentialStatus(cred);
|
|
270
|
+
return {
|
|
271
|
+
credential: cred,
|
|
272
|
+
status: statusResult.status,
|
|
273
|
+
};
|
|
274
|
+
}),
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
return results
|
|
278
|
+
.filter(
|
|
279
|
+
r =>
|
|
280
|
+
r.status !== CredentialStatus.Revoked &&
|
|
281
|
+
r.status !== CredentialStatus.Invalid &&
|
|
282
|
+
r.status !== CredentialStatus.Expired,
|
|
283
|
+
)
|
|
284
|
+
.map(r => r.credential);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function createDefaultPresentation() {
|
|
288
|
+
assert(filteredCredentials.length > 0, 'No filtered credentials available');
|
|
289
|
+
|
|
290
|
+
selectedCredentials.clear();
|
|
291
|
+
|
|
292
|
+
const groups = getRequirementGroups();
|
|
293
|
+
for (const group of groups) {
|
|
294
|
+
const validCandidates = await filterValidCredentials(group.candidates);
|
|
295
|
+
const chosen =
|
|
296
|
+
validCandidates.find(cred => !selectedCredentials.has(cred.id)) ||
|
|
297
|
+
validCandidates[0];
|
|
298
|
+
if (chosen) {
|
|
299
|
+
selectedCredentials.set(chosen.id, {credential: chosen});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
assert(
|
|
304
|
+
selectedCredentials.size > 0,
|
|
305
|
+
'No credentials could be selected for the presentation',
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
return createPresentation();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function getSelectedCredentialsByDescriptor() {
|
|
312
|
+
const groups = getRequirementGroups();
|
|
313
|
+
|
|
314
|
+
return groups.map(group => {
|
|
315
|
+
const selected =
|
|
316
|
+
group.candidates.find(cred => selectedCredentials.has(cred.id)) || null;
|
|
317
|
+
const alternatives = group.candidates.filter(
|
|
318
|
+
cred => !selected || cred.id !== selected.id,
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
descriptorId: group.descriptorKey,
|
|
323
|
+
descriptorName: group.descriptorName,
|
|
324
|
+
selected,
|
|
325
|
+
alternatives,
|
|
326
|
+
};
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function getCredentialOptionsForDescriptor(credentialId: string) {
|
|
331
|
+
const groups = getRequirementGroups();
|
|
332
|
+
const group = groups.find(g =>
|
|
333
|
+
g.candidates.some(c => c.id === credentialId),
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
assert(
|
|
337
|
+
group,
|
|
338
|
+
`Credential ${credentialId} not found in any descriptor group`,
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
const selected = group.candidates.find(c => c.id === credentialId);
|
|
342
|
+
const allAlternatives = group.candidates.filter(c => c.id !== credentialId);
|
|
343
|
+
const alternatives = await filterValidCredentials(allAlternatives);
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
descriptorId: group.descriptorKey,
|
|
347
|
+
descriptorName: group.descriptorName,
|
|
348
|
+
selected,
|
|
349
|
+
alternatives,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function switchCredential(
|
|
354
|
+
currentCredentialId: string,
|
|
355
|
+
replacementCredentialId: string,
|
|
356
|
+
) {
|
|
357
|
+
assert(
|
|
358
|
+
selectedCredentials.has(currentCredentialId),
|
|
359
|
+
`Credential ${currentCredentialId} is not currently selected`,
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
const options = await getCredentialOptionsForDescriptor(currentCredentialId);
|
|
363
|
+
const replacement = options.alternatives.find(
|
|
364
|
+
c => c.id === replacementCredentialId,
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
assert(
|
|
368
|
+
replacement,
|
|
369
|
+
`Credential ${replacementCredentialId} is not a valid replacement for ${currentCredentialId}`,
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
selectedCredentials.delete(currentCredentialId);
|
|
373
|
+
selectedCredentials.set(replacementCredentialId, {credential: replacement});
|
|
374
|
+
|
|
375
|
+
return createPresentation();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function getAttributesToRevealFromTemplate(credential) {
|
|
379
|
+
const definition = getPresentationDefinition();
|
|
380
|
+
if (!definition?.input_descriptors) {
|
|
381
|
+
return ['id'];
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const attributes = ['id'];
|
|
385
|
+
for (const descriptor of definition.input_descriptors) {
|
|
386
|
+
const fields = descriptor.constraints?.fields || [];
|
|
387
|
+
for (const field of fields) {
|
|
388
|
+
if (!field.path) {
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
const paths = Array.isArray(field.path) ? field.path : [field.path];
|
|
392
|
+
for (const p of paths) {
|
|
393
|
+
const attr = p.replace('$.', '');
|
|
394
|
+
if (
|
|
395
|
+
attr &&
|
|
396
|
+
!attributes.includes(attr) &&
|
|
397
|
+
!attr.startsWith('type') &&
|
|
398
|
+
!attr.startsWith('issuer') &&
|
|
399
|
+
!attr.startsWith('@context') &&
|
|
400
|
+
!attr.startsWith('proof') &&
|
|
401
|
+
!attr.startsWith('credentialSchema') &&
|
|
402
|
+
!attr.startsWith('issuanceDate')
|
|
403
|
+
) {
|
|
404
|
+
// Only include if the credential actually has this attribute
|
|
405
|
+
const value = attr
|
|
406
|
+
.split('.')
|
|
407
|
+
.reduce((obj, key) => obj?.[key], credential);
|
|
408
|
+
if (value !== undefined) {
|
|
409
|
+
attributes.push(attr);
|
|
410
|
+
}
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return attributes;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function ensureDescriptorMap(presentation) {
|
|
421
|
+
if (presentation?.presentation_submission?.descriptor_map?.length > 0) {
|
|
422
|
+
return presentation;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const definition = getPresentationDefinition();
|
|
426
|
+
if (!definition?.input_descriptors) {
|
|
427
|
+
return presentation;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const descriptorMap = definition.input_descriptors.map(
|
|
431
|
+
(descriptor, idx) => ({
|
|
432
|
+
id: descriptor.id,
|
|
433
|
+
format: 'ldp_vp',
|
|
434
|
+
path: `$.verifiableCredential[${idx}]`,
|
|
435
|
+
}),
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
presentation.presentation_submission = {
|
|
439
|
+
...presentation.presentation_submission,
|
|
440
|
+
definition_id: definition.id,
|
|
441
|
+
descriptor_map: descriptorMap,
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
return presentation;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async function createPresentation() {
|
|
448
|
+
assert(!!selectedDID, 'No DID selected');
|
|
449
|
+
assert(!!selectedCredentials.size, 'No credentials selected');
|
|
450
|
+
|
|
206
451
|
const didKeyPairList = await didProvider.getDIDKeyPairs();
|
|
207
452
|
const keyDoc = didKeyPairList.find(doc => doc.controller === selectedDID);
|
|
208
|
-
|
|
209
453
|
assert(keyDoc, `No key pair found for the selected DID ${selectedDID}`);
|
|
210
454
|
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
455
|
+
const sdJwtSelections = [];
|
|
456
|
+
const bbsKvacSelections = [];
|
|
457
|
+
const regularSelections = [];
|
|
458
|
+
|
|
459
|
+
for (const credentialSelection of selectedCredentials.values()) {
|
|
460
|
+
if (credentialSelection.credential._sd_jwt) {
|
|
461
|
+
sdJwtSelections.push(credentialSelection);
|
|
462
|
+
} else {
|
|
463
|
+
const isBBS = await isBBSPlusCredential(credentialSelection.credential);
|
|
464
|
+
const isKVAC = await isKvacCredential(credentialSelection.credential);
|
|
465
|
+
if (isBBS || isKVAC) {
|
|
466
|
+
bbsKvacSelections.push(credentialSelection);
|
|
467
|
+
} else {
|
|
468
|
+
regularSelections.push(credentialSelection);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (bbsKvacSelections.length > 0) {
|
|
474
|
+
// When attributesToReveal is undefined, the credential-sdk will automatically
|
|
475
|
+
// determine which attributes to reveal based on the PEX template requirements.
|
|
476
|
+
// This enables generating a default presentation without manual attribute selection.
|
|
477
|
+
const credentialsWithWitness = await Promise.all(
|
|
478
|
+
bbsKvacSelections.map(async sel => {
|
|
479
|
+
return {
|
|
480
|
+
credential: sel.credential,
|
|
481
|
+
witness: await credentialProvider.getMembershipWitness(
|
|
482
|
+
sel.credential.id,
|
|
483
|
+
),
|
|
484
|
+
attributesToReveal:
|
|
485
|
+
sel.attributesToReveal ||
|
|
486
|
+
getAttributesToRevealFromTemplate(sel.credential),
|
|
487
|
+
};
|
|
488
|
+
}),
|
|
489
|
+
);
|
|
220
490
|
|
|
491
|
+
// Derive each BBS+/KVAC credential separately, then assemble into a signed presentation.
|
|
492
|
+
// This approach uses deriveVCFromPresentation which properly handles range proof
|
|
493
|
+
// bound checks and produces presentations that the Truvera API can verify.
|
|
494
|
+
const derivedResults = await Promise.all(
|
|
495
|
+
credentialsWithWitness.map(c =>
|
|
496
|
+
credentialServiceRPC.deriveVCFromPresentation({
|
|
497
|
+
proofRequest: templateJSON,
|
|
498
|
+
credentials: [
|
|
499
|
+
{
|
|
500
|
+
credential: c.credential,
|
|
501
|
+
witness: c.witness,
|
|
502
|
+
attributesToReveal: c.attributesToReveal,
|
|
503
|
+
},
|
|
504
|
+
],
|
|
505
|
+
}),
|
|
506
|
+
),
|
|
507
|
+
);
|
|
508
|
+
const derivedCredentials = derivedResults.flat();
|
|
509
|
+
|
|
510
|
+
const nonBbsCredentials = await deriveNonBbsCredentials(
|
|
511
|
+
sdJwtSelections,
|
|
512
|
+
regularSelections,
|
|
513
|
+
);
|
|
514
|
+
const presentation = await assembleSignedPresentation(
|
|
515
|
+
[...derivedCredentials, ...nonBbsCredentials],
|
|
516
|
+
keyDoc,
|
|
517
|
+
);
|
|
518
|
+
return presentation;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// No BBS+/KVAC: handle SD-JWT and regular only
|
|
522
|
+
const credentials = await deriveNonBbsCredentials(
|
|
523
|
+
sdJwtSelections,
|
|
524
|
+
regularSelections,
|
|
525
|
+
);
|
|
526
|
+
const presentation = await assembleSignedPresentation(credentials, keyDoc);
|
|
221
527
|
return presentation;
|
|
222
528
|
}
|
|
223
529
|
|
|
@@ -245,13 +551,9 @@ export function createVerificationController({
|
|
|
245
551
|
*
|
|
246
552
|
* @param presentation
|
|
247
553
|
*/
|
|
248
|
-
function evaluatePresentation(presentation)
|
|
249
|
-
isValid: boolean;
|
|
250
|
-
errors: any[];
|
|
251
|
-
warnings: any[];
|
|
252
|
-
} {
|
|
554
|
+
function evaluatePresentation(presentation) {
|
|
253
555
|
const definition = getPresentationDefinition();
|
|
254
|
-
const result =
|
|
556
|
+
const result = pexService.evaluatePresentation({
|
|
255
557
|
presentation,
|
|
256
558
|
presentationDefinition: definition,
|
|
257
559
|
});
|
|
@@ -263,10 +565,152 @@ export function createVerificationController({
|
|
|
263
565
|
};
|
|
264
566
|
}
|
|
265
567
|
|
|
266
|
-
function
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
568
|
+
function getRequestedAttributes(credentialId: string) {
|
|
569
|
+
const definition = getPresentationDefinition();
|
|
570
|
+
if (!definition?.input_descriptors) {
|
|
571
|
+
return [];
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const groups = getRequirementGroups();
|
|
575
|
+
const group = groups.find(g =>
|
|
576
|
+
g.candidates.some(c => c.id === credentialId),
|
|
577
|
+
);
|
|
578
|
+
if (!group) {
|
|
579
|
+
return [];
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const credential = group.candidates.find(c => c.id === credentialId);
|
|
583
|
+
if (!credential) {
|
|
584
|
+
return [];
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const descriptor = definition.input_descriptors.find(
|
|
588
|
+
d => (d.name || d.id) === group.descriptorName,
|
|
589
|
+
);
|
|
590
|
+
if (!descriptor?.constraints?.fields) {
|
|
591
|
+
return [];
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const attributesToSkip = [
|
|
595
|
+
/^type/,
|
|
596
|
+
/^issuer/,
|
|
597
|
+
/^@context/,
|
|
598
|
+
/^proof/,
|
|
599
|
+
/^credentialSchema/,
|
|
600
|
+
/^issuanceDate/,
|
|
601
|
+
/^credentialStatus/,
|
|
602
|
+
/^cryptoVersion/,
|
|
603
|
+
];
|
|
604
|
+
|
|
605
|
+
return descriptor.constraints.fields
|
|
606
|
+
.map(field => {
|
|
607
|
+
const paths = Array.isArray(field.path) ? field.path : [field.path];
|
|
608
|
+
let resolvedPath = null;
|
|
609
|
+
let value;
|
|
610
|
+
|
|
611
|
+
for (const p of paths) {
|
|
612
|
+
const cleanPath = p.replace('$.', '');
|
|
613
|
+
const pathParts = cleanPath.split('.');
|
|
614
|
+
let current = credential;
|
|
615
|
+
let found = true;
|
|
616
|
+
for (const part of pathParts) {
|
|
617
|
+
if (current && typeof current === 'object' && part in current) {
|
|
618
|
+
current = current[part];
|
|
619
|
+
} else {
|
|
620
|
+
found = false;
|
|
621
|
+
break;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
if (found) {
|
|
625
|
+
resolvedPath = cleanPath;
|
|
626
|
+
value = current;
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (!resolvedPath) {
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (attributesToSkip.some(regex => regex.test(resolvedPath))) {
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const isRangeProof = !!(
|
|
640
|
+
field.filter &&
|
|
641
|
+
(field.filter.minimum !== undefined ||
|
|
642
|
+
field.filter.maximum !== undefined ||
|
|
643
|
+
field.filter.exclusiveMinimum !== undefined ||
|
|
644
|
+
field.filter.exclusiveMaximum !== undefined ||
|
|
645
|
+
field.filter.formatMinimum !== undefined ||
|
|
646
|
+
field.filter.formatMaximum !== undefined)
|
|
647
|
+
);
|
|
648
|
+
|
|
649
|
+
return {
|
|
650
|
+
name: resolvedPath,
|
|
651
|
+
value: isRangeProof ? null : value,
|
|
652
|
+
isRangeProof,
|
|
653
|
+
isOptional: field.optional === true,
|
|
654
|
+
...(isRangeProof && {
|
|
655
|
+
min:
|
|
656
|
+
field.filter.minimum ??
|
|
657
|
+
field.filter.exclusiveMinimum ??
|
|
658
|
+
field.filter.formatMinimum,
|
|
659
|
+
max:
|
|
660
|
+
field.filter.maximum ??
|
|
661
|
+
field.filter.exclusiveMaximum ??
|
|
662
|
+
field.filter.formatMaximum,
|
|
663
|
+
}),
|
|
664
|
+
};
|
|
665
|
+
})
|
|
666
|
+
.filter(Boolean);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
async function getCredentialStatus(credentialId: string) {
|
|
670
|
+
const groups = getRequirementGroups();
|
|
671
|
+
let credential = null;
|
|
672
|
+
|
|
673
|
+
for (const group of groups) {
|
|
674
|
+
credential = group.candidates.find(c => c.id === credentialId);
|
|
675
|
+
if (credential) {
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
assert(
|
|
681
|
+
credential,
|
|
682
|
+
`Credential ${credentialId} not found in any descriptor group`,
|
|
683
|
+
);
|
|
684
|
+
|
|
685
|
+
return credentialProvider.isValid(credential);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
async function canSwitchCredential(credentialId: string) {
|
|
689
|
+
try {
|
|
690
|
+
const options = await getCredentialOptionsForDescriptor(credentialId);
|
|
691
|
+
return options.alternatives.length > 0;
|
|
692
|
+
} catch {
|
|
693
|
+
return false;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
async function submitPresentation(presentation, maxRetries = 3) {
|
|
698
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
699
|
+
try {
|
|
700
|
+
const res = await axios.post(templateJSON.response_url, presentation);
|
|
701
|
+
return res.data;
|
|
702
|
+
} catch (err) {
|
|
703
|
+
const httpStatus = err?.response?.status;
|
|
704
|
+
const isRetryable =
|
|
705
|
+
!httpStatus || httpStatus === 429 || httpStatus >= 500;
|
|
706
|
+
|
|
707
|
+
if (!isRetryable || attempt === maxRetries - 1) {
|
|
708
|
+
throw err;
|
|
709
|
+
}
|
|
710
|
+
const delay = Math.min(1000 * 2 ** attempt, 8000);
|
|
711
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
712
|
+
}
|
|
713
|
+
}
|
|
270
714
|
}
|
|
271
715
|
|
|
272
716
|
return {
|
|
@@ -283,8 +727,16 @@ export function createVerificationController({
|
|
|
283
727
|
isBBSPlusCredential,
|
|
284
728
|
loadCredentials,
|
|
285
729
|
getFilteredCredentials,
|
|
730
|
+
createDefaultPresentation,
|
|
286
731
|
createPresentation,
|
|
287
732
|
evaluatePresentation,
|
|
733
|
+
getRequirementGroups,
|
|
734
|
+
getSelectedCredentialsByDescriptor,
|
|
735
|
+
getCredentialOptionsForDescriptor,
|
|
736
|
+
switchCredential,
|
|
737
|
+
getRequestedAttributes,
|
|
738
|
+
getCredentialStatus,
|
|
739
|
+
canSwitchCredential,
|
|
288
740
|
getTemplateJSON() {
|
|
289
741
|
return templateJSON;
|
|
290
742
|
},
|