@bedrock/vc-delivery 1.0.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/LICENSE.md +115 -0
- package/README.md +1 -0
- package/lib/config.js +10 -0
- package/lib/exchanges.js +295 -0
- package/lib/helpers.js +63 -0
- package/lib/http.js +195 -0
- package/lib/index.js +118 -0
- package/lib/issue.js +59 -0
- package/lib/logger.js +6 -0
- package/lib/oidc4vci.js +345 -0
- package/lib/verify.js +153 -0
- package/package.json +66 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
Bedrock Non-Commercial License v1.0
|
|
2
|
+
===================================
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2011-2021 Digital Bazaar, Inc.
|
|
5
|
+
All rights reserved.
|
|
6
|
+
|
|
7
|
+
Summary
|
|
8
|
+
=======
|
|
9
|
+
|
|
10
|
+
This license allows the licensee to use Bedrock and its software modules
|
|
11
|
+
for non-commercial purposes such as self-study, research, personal
|
|
12
|
+
projects, or for evaluation purposes. If the licensee uses Bedrock
|
|
13
|
+
directly or indirectly to generate revenue, or to provide products or
|
|
14
|
+
services to more than 500 people (users), the licensee must immediately
|
|
15
|
+
obtain a non-profit or commercial license.
|
|
16
|
+
|
|
17
|
+
Examples
|
|
18
|
+
========
|
|
19
|
+
|
|
20
|
+
These are examples of cases that are allowed by this license:
|
|
21
|
+
|
|
22
|
+
* The licensee is an individual that creates Bedrock-dependent software for
|
|
23
|
+
personal use only.
|
|
24
|
+
* The licensee is an individual or group of students/researchers that uses
|
|
25
|
+
Bedrock to experiment with an idea for a non-commercial project.
|
|
26
|
+
* The licensee is a startup company that prototypes a Bedrock-dependent
|
|
27
|
+
product before they have cash flow and will be testing the prototype
|
|
28
|
+
software with less than 500 users. The service will not generate revenue
|
|
29
|
+
of any kind.
|
|
30
|
+
* The licensee is a for-profit organization that creates a product or
|
|
31
|
+
service that is used by less than 500 users and is built with or
|
|
32
|
+
integrates with Bedrock. The service must be exclusively provided for free
|
|
33
|
+
and no parent, subsidiary, agent, or affiliate organization may profit
|
|
34
|
+
from its use.
|
|
35
|
+
|
|
36
|
+
These cases require a non-profit or commercial license:
|
|
37
|
+
|
|
38
|
+
* The licensee is a non-profit that receives funding to create and/or run a
|
|
39
|
+
Bedrock-dependent service.
|
|
40
|
+
* The licensee is a startup company with Bedrock-dependent software that is
|
|
41
|
+
funded by another organization.
|
|
42
|
+
* The licensee is a startup company that is going into production with
|
|
43
|
+
Bedrock-dependent software.
|
|
44
|
+
* The licensee has more than 500 users using a Bedrock-dependent service
|
|
45
|
+
either directly or indirectly.
|
|
46
|
+
* The licensee is a medium to large organization that builds or integrates a
|
|
47
|
+
commercial product or service with Bedrock.
|
|
48
|
+
|
|
49
|
+
THE LICENSE
|
|
50
|
+
===========
|
|
51
|
+
|
|
52
|
+
This section and all subsequent sections of this document constitute the
|
|
53
|
+
agreement between the licensee and Digital Bazaar, Inc.
|
|
54
|
+
|
|
55
|
+
DEFINITIONS
|
|
56
|
+
===========
|
|
57
|
+
|
|
58
|
+
* Product - The Bedrock software and any modules associated with Bedrock
|
|
59
|
+
where Digital Bazaar, Inc. owns the copyright.
|
|
60
|
+
|
|
61
|
+
CONDITIONS
|
|
62
|
+
==========
|
|
63
|
+
|
|
64
|
+
Redistribution and use in source and binary forms, with or without
|
|
65
|
+
modification, are permitted for NON-COMMERCIAL PURPOSES as long as the
|
|
66
|
+
following conditions are met:
|
|
67
|
+
|
|
68
|
+
1. Any use of the Product must not generate revenue for the licensee or
|
|
69
|
+
any parent, subsidiary, agent, or affiliate of the licensee. Use of
|
|
70
|
+
Product includes, but is not limited to, interacting with any of the
|
|
71
|
+
licensee's Product-dependent products or services over a network.
|
|
72
|
+
|
|
73
|
+
2. The aggregate number of individual people (users) of the licensee's
|
|
74
|
+
products or services that use Product must be less than 500.
|
|
75
|
+
|
|
76
|
+
3. Redistributions of source code must retain the above copyright notice
|
|
77
|
+
intact, this list of conditions and the following disclaimer.
|
|
78
|
+
|
|
79
|
+
4. Redistributions in binary form must reproduce the above copyright
|
|
80
|
+
notice, this license and the following disclaimer in the documentation and
|
|
81
|
+
on a web page available via interactive use and/or other materials
|
|
82
|
+
provided with the distribution.
|
|
83
|
+
|
|
84
|
+
5. Neither the name of the copyright holder, the names of its
|
|
85
|
+
contributors, nor any trademarks held by the copyright holder may be used
|
|
86
|
+
to endorse or promote products or services built using the Product without
|
|
87
|
+
specific prior written permission.
|
|
88
|
+
|
|
89
|
+
6. Any modifications are clearly outlined in release documentation and are
|
|
90
|
+
specifically mentioned as not being a part of an official Product release.
|
|
91
|
+
No additional restrictions to this license may be made when distributing
|
|
92
|
+
modifications.
|
|
93
|
+
|
|
94
|
+
7. For the avoidance of doubt, this license prohibits sublicensing of the
|
|
95
|
+
Product.
|
|
96
|
+
|
|
97
|
+
8. Any breach of this license by licensee must be resolved within 30 days.
|
|
98
|
+
Failure to do so results in the termination of this license.
|
|
99
|
+
|
|
100
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
|
101
|
+
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
102
|
+
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
103
|
+
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
|
104
|
+
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
105
|
+
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
106
|
+
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
|
107
|
+
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
|
108
|
+
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
109
|
+
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
110
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
111
|
+
|
|
112
|
+
To obtain a non-profit or commercial license for Product, please contact
|
|
113
|
+
Digital Bazaar, Inc. at the following email address:
|
|
114
|
+
|
|
115
|
+
Digital Bazaar <support@digitalbazaar.com>
|
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# bedrock-module-template-http
|
package/lib/config.js
ADDED
package/lib/exchanges.js
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2022 Digital Bazaar, Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import * as bedrock from '@bedrock/core';
|
|
5
|
+
import * as database from '@bedrock/mongodb';
|
|
6
|
+
import assert from 'assert-plus';
|
|
7
|
+
import {logger} from './logger.js';
|
|
8
|
+
import {parseLocalId} from './helpers.js';
|
|
9
|
+
|
|
10
|
+
const {util: {BedrockError}} = bedrock;
|
|
11
|
+
|
|
12
|
+
/* Note: Exchanges either have TTLs and can be "completed" or they persist
|
|
13
|
+
and never "complete" nor expire.
|
|
14
|
+
|
|
15
|
+
Exchanges are always in one of three states: `pending`, `complete`, or
|
|
16
|
+
`invalid`. They can only transistion from `pending` to `complete` or from
|
|
17
|
+
`complete` to `invalid`.
|
|
18
|
+
|
|
19
|
+
If an exchange is marked as complete, any attempt to mark it complete again
|
|
20
|
+
will result in an action, as specified in the exchange record, being taken
|
|
21
|
+
such as auto-revocation or notification.
|
|
22
|
+
|
|
23
|
+
Each pending exchange may include optionally encrypted VCs for pickup and / or
|
|
24
|
+
VC templates and required variables (which may come from other VCs) that must
|
|
25
|
+
be provided to populate those templates. If any templates are provided, then
|
|
26
|
+
a capability to issue the VC must also be provided. If any VCs are to be
|
|
27
|
+
provided during the exchange a capability to verify them must be provided. */
|
|
28
|
+
|
|
29
|
+
const COLLECTION_NAME = 'vc-exchange';
|
|
30
|
+
|
|
31
|
+
bedrock.events.on('bedrock-mongodb.ready', async () => {
|
|
32
|
+
await database.openCollections([COLLECTION_NAME]);
|
|
33
|
+
|
|
34
|
+
await database.createIndexes([{
|
|
35
|
+
// cover exchange queries by exchanger ID + exchange ID
|
|
36
|
+
collection: COLLECTION_NAME,
|
|
37
|
+
fields: {localExchangerId: 1, 'exchange.id': 1},
|
|
38
|
+
options: {unique: true, background: false}
|
|
39
|
+
}, {
|
|
40
|
+
// expire exchanges based on `expires` field
|
|
41
|
+
collection: COLLECTION_NAME,
|
|
42
|
+
fields: {'meta.expires': 1},
|
|
43
|
+
options: {
|
|
44
|
+
partialFilterExpression: {
|
|
45
|
+
'meta.expires': {$exists: true}
|
|
46
|
+
},
|
|
47
|
+
unique: false,
|
|
48
|
+
background: false,
|
|
49
|
+
expireAfterSeconds: 0
|
|
50
|
+
}
|
|
51
|
+
}]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Inserts an exchange record.
|
|
56
|
+
*
|
|
57
|
+
* @param {object} options - The options to use.
|
|
58
|
+
* @param {string} options.exchangerId - The ID of the exchanger that the
|
|
59
|
+
* exchange is associated with.
|
|
60
|
+
* @param {object} options.exchange - The exchange to insert.
|
|
61
|
+
*
|
|
62
|
+
* @returns {Promise<object>} Resolves to the database record.
|
|
63
|
+
*/
|
|
64
|
+
export async function insert({exchangerId, exchange}) {
|
|
65
|
+
assert.string(exchangerId, 'exchangerId');
|
|
66
|
+
assert.object(exchange, 'exchange');
|
|
67
|
+
assert.string(exchange.id, 'exchange.id');
|
|
68
|
+
// optional time to live in seconds
|
|
69
|
+
assert.optionalNumber(exchange.ttl);
|
|
70
|
+
// optional variables to use in VC templates
|
|
71
|
+
assert.optionalObject(exchange.variables);
|
|
72
|
+
// optional current step in the exchange
|
|
73
|
+
assert.optionalString(exchange.step);
|
|
74
|
+
|
|
75
|
+
// build exchange record
|
|
76
|
+
const now = Date.now();
|
|
77
|
+
const meta = {created: now, updated: now};
|
|
78
|
+
if(exchange.ttl !== undefined) {
|
|
79
|
+
// TTL is in seconds
|
|
80
|
+
meta.expires = new Date(now + exchange.ttl * 1000);
|
|
81
|
+
}
|
|
82
|
+
const {localId: localExchangerId} = parseLocalId({id: exchangerId});
|
|
83
|
+
const record = {
|
|
84
|
+
localExchangerId,
|
|
85
|
+
meta,
|
|
86
|
+
// possible states are: `pending`, `complete`, or `invalid`
|
|
87
|
+
exchange: {...exchange, state: 'pending'}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// insert the exchange and get the updated record
|
|
91
|
+
try {
|
|
92
|
+
const collection = database.collections[COLLECTION_NAME];
|
|
93
|
+
const result = await collection.insertOne(record);
|
|
94
|
+
return result.ops[0];
|
|
95
|
+
} catch(e) {
|
|
96
|
+
if(!database.isDuplicateError(e)) {
|
|
97
|
+
throw e;
|
|
98
|
+
}
|
|
99
|
+
throw new BedrockError('Duplicate document.', {
|
|
100
|
+
name: 'DuplicateError',
|
|
101
|
+
details: {
|
|
102
|
+
public: true,
|
|
103
|
+
httpStatusCode: 409
|
|
104
|
+
},
|
|
105
|
+
cause: e
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Gets an exchange record.
|
|
112
|
+
*
|
|
113
|
+
* @param {object} options - The options to use.
|
|
114
|
+
* @param {string} options.exchangerId - The ID of the exchanger that the
|
|
115
|
+
* exchange is associated with.
|
|
116
|
+
* @param {string} options.id - The ID of the exchange to retrieve.
|
|
117
|
+
* @param {boolean} [options.explain=false] - An optional explain boolean.
|
|
118
|
+
*
|
|
119
|
+
* @returns {Promise<object | ExplainObject>} Resolves with the record that
|
|
120
|
+
* matches the query or an ExplainObject if `explain=true`.
|
|
121
|
+
*/
|
|
122
|
+
export async function get({exchangerId, id, explain = false} = {}) {
|
|
123
|
+
assert.string(exchangerId, 'exchangerId');
|
|
124
|
+
assert.string(id, 'id');
|
|
125
|
+
|
|
126
|
+
const {localId: localExchangerId} = parseLocalId({id: exchangerId});
|
|
127
|
+
const collection = database.collections[COLLECTION_NAME];
|
|
128
|
+
const query = {
|
|
129
|
+
localExchangerId,
|
|
130
|
+
'exchange.id': id,
|
|
131
|
+
// treat exchange as not found if invalid
|
|
132
|
+
'exchange.state': {$ne: 'invalid'}
|
|
133
|
+
};
|
|
134
|
+
const projection = {_id: 0, exchange: 1, meta: 1};
|
|
135
|
+
|
|
136
|
+
if(explain) {
|
|
137
|
+
// 'find().limit(1)' is used here because 'findOne()' doesn't return a
|
|
138
|
+
// cursor which allows the use of the explain function.
|
|
139
|
+
const cursor = await collection.find(query, {projection}).limit(1);
|
|
140
|
+
return cursor.explain('executionStats');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const record = await collection.findOne(query, {projection});
|
|
144
|
+
if(!record) {
|
|
145
|
+
throw new BedrockError('Exchange not found.', {
|
|
146
|
+
name: 'NotFoundError',
|
|
147
|
+
details: {
|
|
148
|
+
exchanger: exchangerId,
|
|
149
|
+
exchange: id,
|
|
150
|
+
httpStatusCode: 404,
|
|
151
|
+
public: true
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return record;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Marks an exchange as complete.
|
|
161
|
+
*
|
|
162
|
+
* @param {object} options - The options to use.
|
|
163
|
+
* @param {string} options.exchangerId - The ID of the exchanger the exchange
|
|
164
|
+
* is associated with.
|
|
165
|
+
* @param {object} options.id - The ID of the exchange to mark as complete.
|
|
166
|
+
* @param {boolean} [options.explain=false] - An optional explain boolean.
|
|
167
|
+
*
|
|
168
|
+
* @returns {Promise<boolean | ExplainObject>} Resolves with `true` on update
|
|
169
|
+
* success or an ExplainObject if `explain=true`.
|
|
170
|
+
*/
|
|
171
|
+
export async function complete({exchangerId, id, explain = false} = {}) {
|
|
172
|
+
assert.string(exchangerId, 'exchangerId');
|
|
173
|
+
assert.string(id, 'id');
|
|
174
|
+
|
|
175
|
+
// build update
|
|
176
|
+
const now = Date.now();
|
|
177
|
+
const update = {
|
|
178
|
+
$set: {
|
|
179
|
+
'exchange.state': 'complete',
|
|
180
|
+
'meta.updated': now
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const {localId: localExchangerId} = parseLocalId({id: exchangerId});
|
|
185
|
+
|
|
186
|
+
const collection = database.collections[COLLECTION_NAME];
|
|
187
|
+
const query = {
|
|
188
|
+
localExchangerId,
|
|
189
|
+
'exchange.id': id,
|
|
190
|
+
// previous state must be `pending` in order to change to `complete`
|
|
191
|
+
'exchange.state': 'pending'
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
if(explain) {
|
|
195
|
+
// 'find().limit(1)' is used here because 'updateOne()' doesn't return a
|
|
196
|
+
// cursor which allows the use of the explain function.
|
|
197
|
+
const cursor = await collection.find(query).limit(1);
|
|
198
|
+
return cursor.explain('executionStats');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
const result = await collection.updateOne(query, update);
|
|
203
|
+
if(result.result.n > 0) {
|
|
204
|
+
// document modified: success
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
} catch(e) {
|
|
208
|
+
throw new BedrockError('Could not complete exchange.', {
|
|
209
|
+
name: 'OperationError',
|
|
210
|
+
details: {
|
|
211
|
+
public: true,
|
|
212
|
+
httpStatusCode: 500
|
|
213
|
+
},
|
|
214
|
+
cause: e
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// if no document was matched, try to get an existing exchange; if the
|
|
219
|
+
// exchange does not exist, a not found error will be automatically thrown
|
|
220
|
+
const record = await get({exchangerId, id});
|
|
221
|
+
|
|
222
|
+
/* Note: Here the exchange *does* exist, but was already completed. This is
|
|
223
|
+
an error condition that must result in invalidating the exchange. */
|
|
224
|
+
|
|
225
|
+
// invalidate exchange, but do not throw any error to client; only log it
|
|
226
|
+
_invalidateExchange({record}).catch(
|
|
227
|
+
error => logger.error(`Could not invalidate exchange "${id}".`, {error}));
|
|
228
|
+
|
|
229
|
+
// throw duplicate completed exchange error
|
|
230
|
+
throw new BedrockError('Could not complete exchange; already completed.', {
|
|
231
|
+
name: 'DuplicateError',
|
|
232
|
+
details: {
|
|
233
|
+
public: true,
|
|
234
|
+
// this is a client-side conflict error
|
|
235
|
+
httpStatusCode: 409
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function _invalidateExchange({record}) {
|
|
241
|
+
try {
|
|
242
|
+
// mark exchange invalid, but do not throw any error to client; only log it
|
|
243
|
+
await _markExchangeInvalid({record});
|
|
244
|
+
} catch(error) {
|
|
245
|
+
logger.error(
|
|
246
|
+
`Could not mark exchange "${record.exchange.id}" invalid.`, {error});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/* Perform auto-revocation of the VCs or notification (the action to take
|
|
250
|
+
is specified in the exchange record). */
|
|
251
|
+
// FIXME: handle auto-revocation / notification in background; do not throw
|
|
252
|
+
// errors to client
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function _markExchangeInvalid({record}) {
|
|
256
|
+
const now = Date.now();
|
|
257
|
+
|
|
258
|
+
// mark exchange invalid
|
|
259
|
+
try {
|
|
260
|
+
const query = {
|
|
261
|
+
localExchangerId: record.localExchangerId,
|
|
262
|
+
'exchange.id': record.exchange.id
|
|
263
|
+
};
|
|
264
|
+
const update = {
|
|
265
|
+
$set: {
|
|
266
|
+
'exchange.state': 'invalid',
|
|
267
|
+
'meta.updated': now,
|
|
268
|
+
// allow up to 3 days to resolve invalid exchange issues
|
|
269
|
+
// (86400 seconds in 24 hours)
|
|
270
|
+
'meta.expires': new Date(now + 86400 * 3 * 1000)
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
const collection = database.collections[COLLECTION_NAME];
|
|
274
|
+
const result = await collection.updateOne(query, update);
|
|
275
|
+
if(result.result.n > 0) {
|
|
276
|
+
// document modified: success
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
} catch(e) {
|
|
280
|
+
throw new BedrockError('Could not mark exchange invalid.', {
|
|
281
|
+
name: 'OperationError',
|
|
282
|
+
details: {
|
|
283
|
+
public: true,
|
|
284
|
+
httpStatusCode: 500
|
|
285
|
+
},
|
|
286
|
+
cause: e
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* An object containing information on the query plan.
|
|
293
|
+
*
|
|
294
|
+
* @typedef {object} ExplainObject
|
|
295
|
+
*/
|
package/lib/helpers.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2022 Digital Bazaar, Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import * as bedrock from '@bedrock/core';
|
|
5
|
+
import {decodeId, generateId} from 'bnid';
|
|
6
|
+
import {Ed25519Signature2020} from '@digitalbazaar/ed25519-signature-2020';
|
|
7
|
+
import {httpsAgent} from '@bedrock/https-agent';
|
|
8
|
+
import {serviceAgents} from '@bedrock/service-agent';
|
|
9
|
+
import {ZcapClient} from '@digitalbazaar/ezcap';
|
|
10
|
+
|
|
11
|
+
const {config} = bedrock;
|
|
12
|
+
|
|
13
|
+
export function getExchangerId({routePrefix, localId} = {}) {
|
|
14
|
+
return `${config.server.baseUri}${routePrefix}/${localId}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function generateRandom() {
|
|
18
|
+
// 128-bit random number, base58 multibase + multihash encoded
|
|
19
|
+
return generateId({
|
|
20
|
+
bitLength: 128,
|
|
21
|
+
encoding: 'base58',
|
|
22
|
+
multibase: true,
|
|
23
|
+
multihash: true
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function getZcapClient({exchanger} = {}) {
|
|
28
|
+
// get service agent for communicating with the issuer instance
|
|
29
|
+
const {serviceAgent} = await serviceAgents.get(
|
|
30
|
+
{serviceType: 'vc-exchanger'});
|
|
31
|
+
const {capabilityAgent, zcaps} = await serviceAgents.getEphemeralAgent(
|
|
32
|
+
{config: exchanger, serviceAgent});
|
|
33
|
+
|
|
34
|
+
// create zcap client for issuing VCs
|
|
35
|
+
const zcapClient = new ZcapClient({
|
|
36
|
+
agent: httpsAgent,
|
|
37
|
+
invocationSigner: capabilityAgent.getSigner(),
|
|
38
|
+
SuiteClass: Ed25519Signature2020
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return {zcapClient, zcaps};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function parseLocalId({id}) {
|
|
45
|
+
// format: <base>/<localId>
|
|
46
|
+
const idx = id.lastIndexOf('/');
|
|
47
|
+
const localId = id.slice(idx + 1);
|
|
48
|
+
return {
|
|
49
|
+
base: id.slice(0, idx),
|
|
50
|
+
localId: decodeLocalId({localId})
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function decodeLocalId({localId} = {}) {
|
|
55
|
+
// convert to `Buffer` for database storage savings
|
|
56
|
+
return Buffer.from(decodeId({
|
|
57
|
+
id: localId,
|
|
58
|
+
encoding: 'base58',
|
|
59
|
+
multibase: true,
|
|
60
|
+
multihash: true,
|
|
61
|
+
expectedSize: 16
|
|
62
|
+
}));
|
|
63
|
+
}
|
package/lib/http.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2018-2022 Digital Bazaar, Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import * as bedrock from '@bedrock/core';
|
|
5
|
+
import * as exchanges from './exchanges.js';
|
|
6
|
+
import * as oidc4vci from './oidc4vci.js';
|
|
7
|
+
import {createChallenge as _createChallenge, verify} from './verify.js';
|
|
8
|
+
import {
|
|
9
|
+
createExchangeBody, useExchangeBody
|
|
10
|
+
} from '../schemas/bedrock-vc-exchanger.js';
|
|
11
|
+
import {metering, middleware} from '@bedrock/service-core';
|
|
12
|
+
import {asyncHandler} from '@bedrock/express';
|
|
13
|
+
import bodyParser from 'body-parser';
|
|
14
|
+
import cors from 'cors';
|
|
15
|
+
import {generateRandom} from './helpers.js';
|
|
16
|
+
import {issue} from './issue.js';
|
|
17
|
+
import {klona} from 'klona';
|
|
18
|
+
import {logger} from './logger.js';
|
|
19
|
+
import {createValidateMiddleware as validate} from '@bedrock/validation';
|
|
20
|
+
|
|
21
|
+
const {util: {BedrockError}} = bedrock;
|
|
22
|
+
|
|
23
|
+
// FIXME: remove and apply at top-level application
|
|
24
|
+
bedrock.events.on('bedrock-express.configure.bodyParser', app => {
|
|
25
|
+
app.use(bodyParser.json({
|
|
26
|
+
// allow json values that are not just objects or arrays
|
|
27
|
+
strict: false,
|
|
28
|
+
limit: '10MB',
|
|
29
|
+
type: ['json', '+json']
|
|
30
|
+
}));
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export async function addRoutes({app, service} = {}) {
|
|
34
|
+
const {routePrefix} = service;
|
|
35
|
+
|
|
36
|
+
const baseUrl = `${routePrefix}/:localId`;
|
|
37
|
+
const routes = {
|
|
38
|
+
exchanges: `${baseUrl}/exchanges`,
|
|
39
|
+
exchange: `${baseUrl}/exchanges/:exchangeId`
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// used to retrieve service object (exchanger) config
|
|
43
|
+
const getConfigMiddleware = middleware.createGetConfigMiddleware({service});
|
|
44
|
+
|
|
45
|
+
// used to fetch exchange record in parallel
|
|
46
|
+
const getExchange = asyncHandler(async (req, res, next) => {
|
|
47
|
+
const {localId, exchangeId: id} = req.params;
|
|
48
|
+
const {baseUri} = bedrock.config.server;
|
|
49
|
+
const exchangerId = `${baseUri}${routePrefix}/${localId}`;
|
|
50
|
+
// save promise in request, do not wait for it to settle
|
|
51
|
+
req.exchange = exchanges.get({exchangerId, id});
|
|
52
|
+
next();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
/* Note: CORS is used on all endpoints. This is safe because authorization
|
|
56
|
+
uses HTTP signatures + capabilities or OAuth2, not cookies; CSRF is not
|
|
57
|
+
possible. */
|
|
58
|
+
|
|
59
|
+
// create an exchange
|
|
60
|
+
app.options(routes.exchanges, cors());
|
|
61
|
+
app.post(
|
|
62
|
+
routes.exchanges,
|
|
63
|
+
cors(),
|
|
64
|
+
validate({bodySchema: createExchangeBody}),
|
|
65
|
+
getConfigMiddleware,
|
|
66
|
+
middleware.authorizeServiceObjectRequest(),
|
|
67
|
+
asyncHandler(async (req, res) => {
|
|
68
|
+
// FIXME: check available storage via meter before allowing operation
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const {config} = req.serviceObject;
|
|
72
|
+
const {
|
|
73
|
+
ttl, oidc4vci, variables = {},
|
|
74
|
+
// allow steps to be skipped by creator as needed
|
|
75
|
+
step = config.initialStep
|
|
76
|
+
} = req.body;
|
|
77
|
+
|
|
78
|
+
// validate exchange step, if given
|
|
79
|
+
if(step && !(step in config.steps)) {
|
|
80
|
+
throw new BedrockError(`Undefined step "${step}".`, {
|
|
81
|
+
name: 'DataError',
|
|
82
|
+
details: {httpStatusCode: 400, public: true}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// insert exchange
|
|
87
|
+
const {id: exchangerId} = config;
|
|
88
|
+
const exchange = {
|
|
89
|
+
id: await generateRandom(),
|
|
90
|
+
ttl,
|
|
91
|
+
variables,
|
|
92
|
+
oidc4vci,
|
|
93
|
+
step
|
|
94
|
+
};
|
|
95
|
+
await exchanges.insert({exchangerId, exchange});
|
|
96
|
+
const location = `${exchangerId}/exchanges/${exchange.id}`;
|
|
97
|
+
res.status(204).location(location).send();
|
|
98
|
+
} catch(error) {
|
|
99
|
+
logger.error(error.message, {error});
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// meter operation usage
|
|
104
|
+
metering.reportOperationUsage({req});
|
|
105
|
+
}));
|
|
106
|
+
|
|
107
|
+
// VC-API exchange endpoint
|
|
108
|
+
app.options(routes.exchange, cors());
|
|
109
|
+
app.post(
|
|
110
|
+
routes.exchange,
|
|
111
|
+
cors(),
|
|
112
|
+
validate({bodySchema: useExchangeBody()}),
|
|
113
|
+
getExchange,
|
|
114
|
+
getConfigMiddleware,
|
|
115
|
+
asyncHandler(async (req, res) => {
|
|
116
|
+
const {config: exchanger} = req.serviceObject;
|
|
117
|
+
const {exchange} = await req.exchange;
|
|
118
|
+
|
|
119
|
+
// process exchange step if present
|
|
120
|
+
if(exchange.step) {
|
|
121
|
+
const step = exchanger.steps[exchange.step];
|
|
122
|
+
|
|
123
|
+
// handle VPR; if step requires it, then `verifiablePresentation` must
|
|
124
|
+
// be in the request
|
|
125
|
+
if(step.verifiablePresentationRequest) {
|
|
126
|
+
const {createChallenge} = step;
|
|
127
|
+
const isInitialStep = exchange.step === exchanger.initialStep;
|
|
128
|
+
|
|
129
|
+
// if `verifiablePresentation` is not in the body...
|
|
130
|
+
const presentation = req?.body?.verifiablePresentation;
|
|
131
|
+
if(!presentation) {
|
|
132
|
+
const verifiablePresentationRequest = klona(
|
|
133
|
+
step.verifiablePresentationRequest);
|
|
134
|
+
if(createChallenge) {
|
|
135
|
+
/* Note: When creating a challenge, the initial step always
|
|
136
|
+
uses the local exchange ID because the initial step itself
|
|
137
|
+
is one-time use. Subsequent steps, which only VC-API (as opposed
|
|
138
|
+
to other protocols) supports creating additional challenges via
|
|
139
|
+
the VC-API verifier API. */
|
|
140
|
+
let challenge;
|
|
141
|
+
if(isInitialStep) {
|
|
142
|
+
challenge = exchange.id;
|
|
143
|
+
} else {
|
|
144
|
+
// generate a new challenge using verifier API
|
|
145
|
+
({challenge} = await _createChallenge({exchanger}));
|
|
146
|
+
}
|
|
147
|
+
verifiablePresentationRequest.challenge = challenge;
|
|
148
|
+
}
|
|
149
|
+
// send VPR
|
|
150
|
+
res.json({verifiablePresentationRequest});
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// verify the VP
|
|
155
|
+
const expectedChallenge = isInitialStep ? exchange.id : undefined;
|
|
156
|
+
const {verificationMethod} = await verify(
|
|
157
|
+
{exchanger, presentation, expectedChallenge});
|
|
158
|
+
|
|
159
|
+
// FIXME: ensure VP satisfies step VPR; implement templates (jsonata)
|
|
160
|
+
// to convert VPR responses to more exchange variables
|
|
161
|
+
|
|
162
|
+
// store VP in variables
|
|
163
|
+
exchange.variables[exchange.step] = {
|
|
164
|
+
did: verificationMethod.controller,
|
|
165
|
+
verifiablePresentation: presentation
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// FIXME: update the exchange to go to the next step if there is one
|
|
169
|
+
// if(step.nextStep) {
|
|
170
|
+
// exchange.step = step.nextStep;
|
|
171
|
+
// // FIXME: break exchange step processor into its own local API;
|
|
172
|
+
// // ensure VPR has been met, store VP in variables, and
|
|
173
|
+
// // loop to send next VPR
|
|
174
|
+
// return;
|
|
175
|
+
// }
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// mark exchange complete
|
|
180
|
+
await exchanges.complete({exchangerId: exchanger.id, id: exchange.id});
|
|
181
|
+
|
|
182
|
+
// FIXME: decide what the best recovery path is if delivery fails (but no
|
|
183
|
+
// replay attack detected) after exchange has been marked complete
|
|
184
|
+
|
|
185
|
+
// issue VCs
|
|
186
|
+
const {verifiablePresentation} = await issue({exchanger, exchange});
|
|
187
|
+
|
|
188
|
+
// send VP
|
|
189
|
+
res.json({verifiablePresentation});
|
|
190
|
+
}));
|
|
191
|
+
|
|
192
|
+
// create OIDC4VCI routes to be used with each individual exchange
|
|
193
|
+
await oidc4vci.createRoutes(
|
|
194
|
+
{app, exchangeRoute: routes.exchange, getConfigMiddleware, getExchange});
|
|
195
|
+
}
|