@bedrock/vc-delivery 4.5.0 → 4.7.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/constants.js +5 -0
- package/lib/helpers.js +14 -0
- package/lib/http.js +39 -24
- package/lib/index.js +40 -12
- package/lib/issue.js +42 -18
- package/lib/openId.js +97 -27
- package/lib/vcapi.js +5 -5
- package/package.json +1 -1
- package/schemas/bedrock-vc-workflow.js +65 -13
package/lib/constants.js
ADDED
package/lib/helpers.js
CHANGED
|
@@ -48,6 +48,20 @@ export async function generateRandom() {
|
|
|
48
48
|
});
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
export function getWorkflowIssuerInstances({workflow} = {}) {
|
|
52
|
+
let {issuerInstances} = workflow;
|
|
53
|
+
if(!issuerInstances && workflow.zcaps.issue) {
|
|
54
|
+
// generate dynamic issuer instance config
|
|
55
|
+
issuerInstances = [{
|
|
56
|
+
supportedFormats: ['application/vc', 'ldp_vc'],
|
|
57
|
+
zcapReferenceIds: {
|
|
58
|
+
issue: 'issue'
|
|
59
|
+
}
|
|
60
|
+
}];
|
|
61
|
+
}
|
|
62
|
+
return issuerInstances;
|
|
63
|
+
}
|
|
64
|
+
|
|
51
65
|
export async function getZcapClient({workflow} = {}) {
|
|
52
66
|
// get service agent for communicating with the issuer instance
|
|
53
67
|
const {pathname} = new URL(workflow.id);
|
package/lib/http.js
CHANGED
|
@@ -88,31 +88,46 @@ export async function addRoutes({app, service} = {}) {
|
|
|
88
88
|
});
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
91
|
+
if(openId) {
|
|
92
|
+
// either issuer instances or a single issuer zcap be given if
|
|
93
|
+
// any expected credential requests are given
|
|
94
|
+
const {expectedCredentialRequests} = openId;
|
|
95
|
+
if(expectedCredentialRequests &&
|
|
96
|
+
!(config.issuerInstances || config.zcaps.issue)) {
|
|
97
|
+
throw new BedrockError(
|
|
98
|
+
'Credential requests are not supported by this workflow.', {
|
|
99
|
+
name: 'DataError',
|
|
100
|
+
details: {httpStatusCode: 400, public: true}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// perform key generation if requested
|
|
105
|
+
if(openId.oauth2?.generateKeyPair) {
|
|
106
|
+
const {oauth2} = openId;
|
|
107
|
+
const {algorithm} = oauth2.generateKeyPair;
|
|
108
|
+
const kp = await generateKeyPair(algorithm, {extractable: true});
|
|
109
|
+
const [privateKeyJwk, publicKeyJwk] = await Promise.all([
|
|
110
|
+
exportJWK(kp.privateKey),
|
|
111
|
+
exportJWK(kp.publicKey),
|
|
109
112
|
]);
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
oauth2.keyPair = {privateKeyJwk, publicKeyJwk};
|
|
114
|
+
delete oauth2.generateKeyPair;
|
|
115
|
+
} else {
|
|
116
|
+
// ensure key pair can be imported
|
|
117
|
+
try {
|
|
118
|
+
const {oauth2: {keyPair}} = openId;
|
|
119
|
+
await Promise.all([
|
|
120
|
+
importJWK(keyPair.privateKeyJwk),
|
|
121
|
+
importJWK(keyPair.publicKeyJwk)
|
|
122
|
+
]);
|
|
123
|
+
} catch(e) {
|
|
124
|
+
throw new BedrockError(
|
|
125
|
+
'Could not import OpenID OAuth2 key pair.', {
|
|
126
|
+
name: 'DataError',
|
|
127
|
+
details: {httpStatusCode: 400, public: true},
|
|
128
|
+
cause: e
|
|
129
|
+
});
|
|
130
|
+
}
|
|
116
131
|
}
|
|
117
132
|
}
|
|
118
133
|
|
package/lib/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import {createService, schemas} from '@bedrock/service-core';
|
|
|
7
7
|
import {addRoutes} from './http.js';
|
|
8
8
|
import {initializeServiceAgent} from '@bedrock/service-agent';
|
|
9
9
|
import {klona} from 'klona';
|
|
10
|
+
import {MAX_ISSUER_INSTANCES} from './constants.js';
|
|
10
11
|
import {parseLocalId} from './helpers.js';
|
|
11
12
|
import '@bedrock/express';
|
|
12
13
|
|
|
@@ -26,12 +27,20 @@ async function _initService({serviceType, routePrefix}) {
|
|
|
26
27
|
const createConfigBody = klona(schemas.createConfigBody);
|
|
27
28
|
const updateConfigBody = klona(schemas.updateConfigBody);
|
|
28
29
|
const schemasToUpdate = [createConfigBody, updateConfigBody];
|
|
29
|
-
const {
|
|
30
|
+
const {
|
|
31
|
+
credentialTemplates, steps, initialStep, issuerInstances
|
|
32
|
+
} = workflowSchemas;
|
|
30
33
|
for(const schema of schemasToUpdate) {
|
|
31
34
|
// add config requirements to workflow configs
|
|
32
35
|
schema.properties.credentialTemplates = credentialTemplates;
|
|
33
36
|
schema.properties.steps = steps;
|
|
34
37
|
schema.properties.initialStep = initialStep;
|
|
38
|
+
schema.properties.issuerInstances = issuerInstances;
|
|
39
|
+
// allow zcaps by custom reference ID
|
|
40
|
+
schema.properties.zcaps = klona(schemas.zcaps);
|
|
41
|
+
// max of 4 basic zcaps + max issuer instances
|
|
42
|
+
schema.properties.zcaps.maxProperties = 4 + MAX_ISSUER_INSTANCES;
|
|
43
|
+
schema.properties.zcaps.additionalProperties = schemas.delegatedZcap;
|
|
35
44
|
// note: credential templates are not required; if any other properties
|
|
36
45
|
// become required, add them here
|
|
37
46
|
// schema.required.push('credentialTemplates');
|
|
@@ -117,17 +126,36 @@ async function validateConfigFn({config, op, routePrefix} = {}) {
|
|
|
117
126
|
|
|
118
127
|
// if credential templates are specified, then `zcaps` MUST include at
|
|
119
128
|
// least `issue`
|
|
120
|
-
const {credentialTemplates = [], zcaps = {}} = config;
|
|
121
|
-
if(credentialTemplates.length > 0
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
const {credentialTemplates = [], zcaps = {}, issuerInstances} = config;
|
|
130
|
+
if(credentialTemplates.length > 0) {
|
|
131
|
+
if(!(zcaps.issue || issuerInstances)) {
|
|
132
|
+
throw new BedrockError(
|
|
133
|
+
'A capability to issue credentials is required when credential ' +
|
|
134
|
+
'templates are provided.', {
|
|
135
|
+
name: 'DataError',
|
|
136
|
+
details: {
|
|
137
|
+
httpStatusCode: 400,
|
|
138
|
+
public: true
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
// ensure that, if `issuerInstances` is given, that every zcap
|
|
143
|
+
// referenced in each issuer instance config's `zcapReferenceIds` can
|
|
144
|
+
// be found in `zcaps`
|
|
145
|
+
if(issuerInstances) {
|
|
146
|
+
if(!issuerInstances.every(
|
|
147
|
+
instance => !!zcaps[instance.zcapReferenceIds.issue])) {
|
|
148
|
+
throw new BedrockError(
|
|
149
|
+
'An issuer instance configuration zcap reference ID is not ' +
|
|
150
|
+
'present in "config.zcaps".', {
|
|
151
|
+
name: 'DataError',
|
|
152
|
+
details: {
|
|
153
|
+
httpStatusCode: 400,
|
|
154
|
+
public: true
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
131
159
|
}
|
|
132
160
|
|
|
133
161
|
// if `steps` are specified, then `initialStep` MUST be included
|
package/lib/issue.js
CHANGED
|
@@ -1,24 +1,30 @@
|
|
|
1
1
|
/*!
|
|
2
2
|
* Copyright (c) 2022-2024 Digital Bazaar, Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
evaluateTemplate,
|
|
6
|
+
getWorkflowIssuerInstances,
|
|
7
|
+
getZcapClient
|
|
8
|
+
} from './helpers.js';
|
|
5
9
|
import {createPresentation} from '@digitalbazaar/vc';
|
|
6
10
|
|
|
7
|
-
export async function issue({
|
|
11
|
+
export async function issue({
|
|
12
|
+
workflow, exchange, format = 'application/vc'
|
|
13
|
+
} = {}) {
|
|
8
14
|
// use any templates from workflow and variables from exchange to produce
|
|
9
15
|
// credentials to be issued; issue via the configured issuer instance
|
|
10
16
|
const verifiableCredential = [];
|
|
11
17
|
const {credentialTemplates = []} = workflow;
|
|
12
18
|
if(!credentialTemplates || credentialTemplates.length === 0) {
|
|
13
19
|
// nothing to issue
|
|
14
|
-
return {};
|
|
20
|
+
return {response: {}};
|
|
15
21
|
}
|
|
16
22
|
|
|
17
23
|
// evaluate template
|
|
18
|
-
const
|
|
24
|
+
const issueRequests = await Promise.all(credentialTemplates.map(
|
|
19
25
|
typedTemplate => evaluateTemplate({workflow, exchange, typedTemplate})));
|
|
20
26
|
// issue all VCs
|
|
21
|
-
const vcs = await _issue({workflow,
|
|
27
|
+
const vcs = await _issue({workflow, issueRequests, format});
|
|
22
28
|
verifiableCredential.push(...vcs);
|
|
23
29
|
|
|
24
30
|
// generate VP to return VCs
|
|
@@ -29,15 +35,16 @@ export async function issue({workflow, exchange} = {}) {
|
|
|
29
35
|
if(verifiableCredential.length > 0) {
|
|
30
36
|
verifiablePresentation.verifiableCredential = verifiableCredential;
|
|
31
37
|
}
|
|
32
|
-
return {verifiablePresentation};
|
|
38
|
+
return {response: {verifiablePresentation}, format};
|
|
33
39
|
}
|
|
34
40
|
|
|
35
|
-
async function _issue({workflow,
|
|
41
|
+
async function _issue({workflow, issueRequests, format} = {}) {
|
|
36
42
|
// create zcap client for issuing VCs
|
|
37
43
|
const {zcapClient, zcaps} = await getZcapClient({workflow});
|
|
38
44
|
|
|
39
|
-
//
|
|
40
|
-
const capability = zcaps
|
|
45
|
+
// get the zcap to use for the issue requests
|
|
46
|
+
const capability = _getIssueZcap({workflow, zcaps, format});
|
|
47
|
+
|
|
41
48
|
// specify URL to `/credentials/issue` to handle case that capability
|
|
42
49
|
// is not specific to it
|
|
43
50
|
let url = capability.invocationTarget;
|
|
@@ -45,15 +52,32 @@ async function _issue({workflow, credentialRequests} = {}) {
|
|
|
45
52
|
url += capability.invocationTarget.endsWith('/credentials') ?
|
|
46
53
|
'/issue' : '/credentials/issue';
|
|
47
54
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
55
|
+
|
|
56
|
+
const issuedVCs = [];
|
|
57
|
+
|
|
58
|
+
// issue VCs in parallel
|
|
59
|
+
await Promise.all(issueRequests.map(async issueRequest => {
|
|
60
|
+
/* Note: Issue request formats can be any one of these:
|
|
61
|
+
|
|
62
|
+
1. `{credential, options?}`
|
|
63
|
+
2. `credential`
|
|
64
|
+
|
|
65
|
+
Normalize issue requests that use the full VC API issue request and those
|
|
66
|
+
that return only the `credential` param directly. */
|
|
67
|
+
const json = issueRequest.credential ?
|
|
68
|
+
issueRequest : {credential: issueRequest};
|
|
69
|
+
const {
|
|
70
|
+
data: {verifiableCredential}
|
|
71
|
+
} = await zcapClient.write({url, capability, json});
|
|
72
|
+
issuedVCs.push(verifiableCredential);
|
|
53
73
|
}));
|
|
54
74
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
75
|
+
return issuedVCs;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function _getIssueZcap({workflow, zcaps, format}) {
|
|
79
|
+
const issuerInstances = getWorkflowIssuerInstances({workflow});
|
|
80
|
+
const {zcapReferenceIds: {issue: issueRefId}} = issuerInstances.find(
|
|
81
|
+
({supportedFormats}) => supportedFormats.includes(format));
|
|
82
|
+
return zcaps[issueRefId];
|
|
59
83
|
}
|
package/lib/openId.js
CHANGED
|
@@ -6,6 +6,7 @@ import * as exchanges from './exchanges.js';
|
|
|
6
6
|
import {
|
|
7
7
|
compile, schemas, createValidateMiddleware as validate
|
|
8
8
|
} from '@bedrock/validation';
|
|
9
|
+
import {evaluateTemplate, getWorkflowIssuerInstances} from './helpers.js';
|
|
9
10
|
import {importJWK, SignJWT} from 'jose';
|
|
10
11
|
import {
|
|
11
12
|
openIdAuthorizationResponseBody,
|
|
@@ -19,7 +20,6 @@ import {asyncHandler} from '@bedrock/express';
|
|
|
19
20
|
import bodyParser from 'body-parser';
|
|
20
21
|
import {checkAccessToken} from '@bedrock/oauth2-verifier';
|
|
21
22
|
import cors from 'cors';
|
|
22
|
-
import {evaluateTemplate} from './helpers.js';
|
|
23
23
|
import {issue} from './issue.js';
|
|
24
24
|
import {klona} from 'klona';
|
|
25
25
|
import {oid4vp} from '@digitalbazaar/oid4-client';
|
|
@@ -107,7 +107,8 @@ export async function createRoutes({
|
|
|
107
107
|
jwks_uri: `${exchangeId}/openid/jwks`,
|
|
108
108
|
token_endpoint: `${exchangeId}/openid/token`,
|
|
109
109
|
credential_endpoint: `${exchangeId}/openid/credential`,
|
|
110
|
-
batch_credential_endpoint: `${exchangeId}/openid/batch_credential
|
|
110
|
+
batch_credential_endpoint: `${exchangeId}/openid/batch_credential`,
|
|
111
|
+
'pre-authorized_grant_anonymous_access_supported': true
|
|
111
112
|
};
|
|
112
113
|
res.json(oauth2Config);
|
|
113
114
|
}));
|
|
@@ -128,7 +129,8 @@ export async function createRoutes({
|
|
|
128
129
|
jwks_uri: `${exchangeId}/openid/jwks`,
|
|
129
130
|
token_endpoint: `${exchangeId}/openid/token`,
|
|
130
131
|
credential_endpoint: `${exchangeId}/openid/credential`,
|
|
131
|
-
batch_credential_endpoint: `${exchangeId}/openid/batch_credential
|
|
132
|
+
batch_credential_endpoint: `${exchangeId}/openid/batch_credential`,
|
|
133
|
+
'pre-authorized_grant_anonymous_access_supported': true
|
|
132
134
|
};
|
|
133
135
|
res.json(oauth2Config);
|
|
134
136
|
}));
|
|
@@ -252,6 +254,7 @@ export async function createRoutes({
|
|
|
252
254
|
}
|
|
253
255
|
}
|
|
254
256
|
*/
|
|
257
|
+
|
|
255
258
|
const result = await _processCredentialRequests(
|
|
256
259
|
{req, res, isBatchRequest: false});
|
|
257
260
|
if(!result) {
|
|
@@ -259,14 +262,27 @@ export async function createRoutes({
|
|
|
259
262
|
return;
|
|
260
263
|
}
|
|
261
264
|
|
|
262
|
-
|
|
265
|
+
/* Note: The `/credential` route only supports sending a single VC;
|
|
266
|
+
assume here that this workflow is configured for a single VC and an
|
|
267
|
+
error code would have been sent to the client to use the batch
|
|
268
|
+
endpoint if there was more than one VC to deliver. */
|
|
269
|
+
const {response, format} = result;
|
|
270
|
+
const {verifiablePresentation: {verifiableCredential: [vc]}} = response;
|
|
271
|
+
|
|
272
|
+
// parse any enveloped VC
|
|
273
|
+
let credential;
|
|
274
|
+
if(vc.type === 'EnvelopedVerifiableCredential' &&
|
|
275
|
+
vc.id?.startsWith('data:application/jwt,')) {
|
|
276
|
+
credential = vc.id.slice('data:application/jwt,'.length);
|
|
277
|
+
} else {
|
|
278
|
+
credential = vc;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// send OID4VCI response
|
|
263
282
|
res.json({
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
error code would have been sent to the client to use the batch
|
|
268
|
-
endpoint if there was more than one VC to deliver. */
|
|
269
|
-
credential: result.verifiablePresentation.verifiableCredential[0]
|
|
283
|
+
// FIXME: this doesn't seem to be in the spec anymore (draft 14+)...
|
|
284
|
+
format,
|
|
285
|
+
credential
|
|
270
286
|
});
|
|
271
287
|
}));
|
|
272
288
|
|
|
@@ -315,8 +331,19 @@ export async function createRoutes({
|
|
|
315
331
|
}
|
|
316
332
|
|
|
317
333
|
// send VCs
|
|
318
|
-
const
|
|
319
|
-
|
|
334
|
+
const {response: {verifiablePresentation}, format} = result;
|
|
335
|
+
// FIXME: "format" doesn't seem to be in the spec anymore (draft 14+)...
|
|
336
|
+
const responses = verifiablePresentation.verifiableCredential.map(vc => {
|
|
337
|
+
// parse any enveloped VC
|
|
338
|
+
let credential;
|
|
339
|
+
if(vc.type === 'EnvelopedVerifiableCredential' &&
|
|
340
|
+
vc.id?.startsWith('data:application/jwt,')) {
|
|
341
|
+
credential = vc.id.slice('data:application/jwt,'.length);
|
|
342
|
+
} else {
|
|
343
|
+
credential = vc;
|
|
344
|
+
}
|
|
345
|
+
return {format, credential};
|
|
346
|
+
});
|
|
320
347
|
res.json({credential_responses: responses});
|
|
321
348
|
}));
|
|
322
349
|
|
|
@@ -526,11 +553,12 @@ async function _processCredentialRequests({req, res, isBatchRequest}) {
|
|
|
526
553
|
}
|
|
527
554
|
|
|
528
555
|
// before asserting, normalize credential requests to use `type` instead of
|
|
529
|
-
// `types`; this is to allow for OID4VCI draft
|
|
556
|
+
// `types`; this is to allow for OID4VCI draft implementers that followed
|
|
530
557
|
// the non-normative examples
|
|
531
558
|
_normalizeCredentialDefinitionTypes({credentialRequests});
|
|
532
|
-
_assertCredentialRequests(
|
|
533
|
-
|
|
559
|
+
const {format} = _assertCredentialRequests({
|
|
560
|
+
workflow, credentialRequests, expectedCredentialRequests
|
|
561
|
+
});
|
|
534
562
|
|
|
535
563
|
// process exchange step if present
|
|
536
564
|
const currentStep = exchange.step;
|
|
@@ -615,7 +643,7 @@ async function _processCredentialRequests({req, res, isBatchRequest}) {
|
|
|
615
643
|
// replay attack detected) after exchange has been marked complete
|
|
616
644
|
|
|
617
645
|
// issue VCs
|
|
618
|
-
return issue({workflow, exchange});
|
|
646
|
+
return issue({workflow, exchange, format});
|
|
619
647
|
}
|
|
620
648
|
|
|
621
649
|
async function _requestDidProof({res, exchangeRecord}) {
|
|
@@ -811,24 +839,66 @@ async function _getAuthorizationRequest({req}) {
|
|
|
811
839
|
}
|
|
812
840
|
|
|
813
841
|
function _assertCredentialRequests({
|
|
814
|
-
credentialRequests, expectedCredentialRequests
|
|
842
|
+
workflow, credentialRequests, expectedCredentialRequests
|
|
815
843
|
}) {
|
|
844
|
+
// ensure that every credential request is for the same format
|
|
845
|
+
/* credential requests look like:
|
|
846
|
+
{
|
|
847
|
+
format: 'ldp_vc',
|
|
848
|
+
credential_definition: { '@context': [Array], type: [Array] }
|
|
849
|
+
}
|
|
850
|
+
*/
|
|
851
|
+
let sharedFormat;
|
|
852
|
+
if(!credentialRequests.every(({format}) => {
|
|
853
|
+
if(sharedFormat === undefined) {
|
|
854
|
+
sharedFormat = format;
|
|
855
|
+
}
|
|
856
|
+
return sharedFormat === format;
|
|
857
|
+
})) {
|
|
858
|
+
throw new BedrockError(
|
|
859
|
+
'Credential requests must all use the same format in this workflow.', {
|
|
860
|
+
name: 'DataError',
|
|
861
|
+
details: {httpStatusCode: 400, public: true}
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// get all supported formats from available issuer instances; for simple
|
|
866
|
+
// workflow configs, a single issuer instance is used with only
|
|
867
|
+
// ensure that every credential request uses a format supported by
|
|
868
|
+
// issuer instances
|
|
869
|
+
const supportedFormats = new Set();
|
|
870
|
+
const issuerInstances = getWorkflowIssuerInstances({workflow});
|
|
871
|
+
issuerInstances.forEach(
|
|
872
|
+
instance => instance.supportedFormats.forEach(
|
|
873
|
+
supportedFormats.add, supportedFormats));
|
|
874
|
+
if(!supportedFormats.has(sharedFormat)) {
|
|
875
|
+
throw new BedrockError(
|
|
876
|
+
`Credential request format "${sharedFormat}" is not supported ` +
|
|
877
|
+
'by this workflow.', {
|
|
878
|
+
name: 'DataError',
|
|
879
|
+
details: {httpStatusCode: 400, public: true}
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
|
|
816
883
|
// ensure every credential request matches against an expected one and none
|
|
817
|
-
// are missing
|
|
884
|
+
// are missing; `expectedCredentialRequests` formats are ignored based on the
|
|
885
|
+
// issuer instance supported formats and have already been checked
|
|
818
886
|
if(!(credentialRequests.length === expectedCredentialRequests.length &&
|
|
819
887
|
credentialRequests.every(cr => expectedCredentialRequests.some(
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
888
|
+
expected => _matchCredentialRequest(expected, cr))))) {
|
|
889
|
+
throw new BedrockError(
|
|
890
|
+
'Unexpected credential request.', {
|
|
891
|
+
name: 'DataError',
|
|
892
|
+
details: {httpStatusCode: 400, public: true}
|
|
893
|
+
});
|
|
823
894
|
}
|
|
895
|
+
|
|
896
|
+
return {format: sharedFormat};
|
|
824
897
|
}
|
|
825
898
|
|
|
826
|
-
function _matchCredentialRequest(
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
}
|
|
830
|
-
const {credential_definition: {'@context': c1, type: t1}} = a;
|
|
831
|
-
const {credential_definition: {'@context': c2, type: t2}} = b;
|
|
899
|
+
function _matchCredentialRequest(expected, cr) {
|
|
900
|
+
const {credential_definition: {'@context': c1, type: t1}} = expected;
|
|
901
|
+
const {credential_definition: {'@context': c2, type: t2}} = cr;
|
|
832
902
|
// contexts must match exact order but types can have different order
|
|
833
903
|
return (c1.length === c2.length && t1.length === t2.length &&
|
|
834
904
|
c1.every((c, i) => c === c2[i]) && t1.every(t => t2.some(x => t === x)));
|
package/lib/vcapi.js
CHANGED
|
@@ -156,15 +156,15 @@ export async function processExchange({req, res, workflow, exchange}) {
|
|
|
156
156
|
// FIXME: decide what the best recovery path is if delivery fails (but no
|
|
157
157
|
// replay attack detected) after exchange has been marked complete
|
|
158
158
|
|
|
159
|
-
// issue any VCs; may return an empty
|
|
159
|
+
// issue any VCs; may return an empty response if the step defines no
|
|
160
160
|
// VCs to issue
|
|
161
|
-
const
|
|
161
|
+
const {response} = await issue({workflow, exchange});
|
|
162
162
|
|
|
163
163
|
// if last `step` has a redirect URL, include it in the response
|
|
164
164
|
if(step?.redirectUrl) {
|
|
165
|
-
|
|
165
|
+
response.redirectUrl = step.redirectUrl;
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
// send
|
|
169
|
-
res.json(
|
|
168
|
+
// send response
|
|
169
|
+
res.json(response);
|
|
170
170
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/*!
|
|
2
2
|
* Copyright (c) 2022-2024 Digital Bazaar, Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
|
+
import {MAX_ISSUER_INSTANCES} from '../lib/constants.js';
|
|
4
5
|
import {schemas} from '@bedrock/validation';
|
|
5
6
|
|
|
6
7
|
const credentialDefinition = {
|
|
@@ -34,6 +35,19 @@ const credentialDefinition = {
|
|
|
34
35
|
}
|
|
35
36
|
};
|
|
36
37
|
|
|
38
|
+
const expectedCredentialRequest = {
|
|
39
|
+
type: 'object',
|
|
40
|
+
additionalProperties: false,
|
|
41
|
+
required: ['credential_definition'],
|
|
42
|
+
properties: {
|
|
43
|
+
credential_definition: credentialDefinition,
|
|
44
|
+
format: {
|
|
45
|
+
type: 'string',
|
|
46
|
+
enum: ['ldp_vc', 'jwt_vc_json-ld']
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
37
51
|
const openIdExchangeOptions = {
|
|
38
52
|
title: 'OpenID Exchange options',
|
|
39
53
|
type: 'object',
|
|
@@ -44,18 +58,7 @@ const openIdExchangeOptions = {
|
|
|
44
58
|
title: 'OpenID Expected Credential Requests',
|
|
45
59
|
type: 'array',
|
|
46
60
|
minItems: 1,
|
|
47
|
-
items:
|
|
48
|
-
type: 'object',
|
|
49
|
-
additionalProperties: false,
|
|
50
|
-
required: ['credential_definition', 'format'],
|
|
51
|
-
properties: {
|
|
52
|
-
credential_definition: credentialDefinition,
|
|
53
|
-
format: {
|
|
54
|
-
type: 'string',
|
|
55
|
-
enum: ['ldp_vc']
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
61
|
+
items: expectedCredentialRequest
|
|
59
62
|
},
|
|
60
63
|
preAuthorizedCode: {
|
|
61
64
|
type: 'string'
|
|
@@ -123,6 +126,9 @@ const typedTemplate = {
|
|
|
123
126
|
required: ['type', 'template'],
|
|
124
127
|
additionalProperties: false,
|
|
125
128
|
properties: {
|
|
129
|
+
id: {
|
|
130
|
+
type: 'string'
|
|
131
|
+
},
|
|
126
132
|
type: {
|
|
127
133
|
type: 'string',
|
|
128
134
|
enum: ['jsonata']
|
|
@@ -140,6 +146,52 @@ export const credentialTemplates = {
|
|
|
140
146
|
items: typedTemplate
|
|
141
147
|
};
|
|
142
148
|
|
|
149
|
+
// to be updated in specific locations with `properties` and `required`
|
|
150
|
+
const zcapReferenceIds = {
|
|
151
|
+
title: 'Authorization Capability Reference IDs',
|
|
152
|
+
type: 'object',
|
|
153
|
+
additionalProperties: false
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const vcFormats = {
|
|
157
|
+
title: 'Verifiable Credential formats',
|
|
158
|
+
type: 'array',
|
|
159
|
+
minItems: 1,
|
|
160
|
+
items: {
|
|
161
|
+
type: 'string'
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const issuerInstance = {
|
|
166
|
+
title: 'Issuer Instance',
|
|
167
|
+
type: 'object',
|
|
168
|
+
required: ['zcapReferenceIds'],
|
|
169
|
+
additionalProperties: false,
|
|
170
|
+
properties: {
|
|
171
|
+
id: {
|
|
172
|
+
type: 'string'
|
|
173
|
+
},
|
|
174
|
+
supportedFormats: vcFormats,
|
|
175
|
+
zcapReferenceIds: {
|
|
176
|
+
...zcapReferenceIds,
|
|
177
|
+
required: ['issue'],
|
|
178
|
+
properties: {
|
|
179
|
+
issue: {
|
|
180
|
+
type: 'string'
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
export const issuerInstances = {
|
|
188
|
+
title: 'Issuer Instances',
|
|
189
|
+
type: 'array',
|
|
190
|
+
minItems: 1,
|
|
191
|
+
maxItems: MAX_ISSUER_INSTANCES,
|
|
192
|
+
items: issuerInstance
|
|
193
|
+
};
|
|
194
|
+
|
|
143
195
|
const step = {
|
|
144
196
|
title: 'Exchange Step',
|
|
145
197
|
type: 'object',
|
|
@@ -276,7 +328,7 @@ const openIdCredentialRequest = {
|
|
|
276
328
|
credential_definition: credentialDefinition,
|
|
277
329
|
format: {
|
|
278
330
|
type: 'string',
|
|
279
|
-
enum: ['ldp_vc']
|
|
331
|
+
enum: ['ldp_vc', 'jwt_vc_json-ld']
|
|
280
332
|
},
|
|
281
333
|
did: {
|
|
282
334
|
type: 'string'
|