@bedrock/vc-verifier 23.2.0 → 23.3.1

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.
@@ -21,7 +21,7 @@ import '@bedrock/vc-status-list-context';
21
21
  import '@bedrock/veres-one-context';
22
22
 
23
23
  const serviceType = 'vc-verifier';
24
- let webLoader;
24
+ export let webLoader;
25
25
 
26
26
  bedrock.events.on('bedrock.init', () => {
27
27
  // build web loader if configuration calls for it
@@ -48,14 +48,24 @@ bedrock.events.on('bedrock.init', () => {
48
48
  * @param {object} options.config - The verifier instance config.
49
49
  * @param {Set} [options.remoteUrlAllowList] - Remote URLs that are
50
50
  * specifically allowed to be loaded (used for status list checks).
51
+ * @param {Map} [options.cache] - An optional cache of URL => document.
51
52
  *
52
53
  * @returns {Promise<Function>} The document loader.
53
54
  */
54
- export async function createDocumentLoader({config, remoteUrlAllowList} = {}) {
55
+ export async function createDocumentLoader({
56
+ config, remoteUrlAllowList, cache = new Map()
57
+ } = {}) {
55
58
  const contextDocumentLoader = await createContextDocumentLoader(
56
59
  {config, serviceType});
57
60
 
58
61
  return async function documentLoader(url) {
62
+ if(cache) {
63
+ const document = cache.get(url);
64
+ if(document) {
65
+ return {contextUrl: null, documentUrl: url, document};
66
+ }
67
+ }
68
+
59
69
  // handle DID URLs...
60
70
  if(url.startsWith('did:')) {
61
71
  let document;
package/lib/http.js CHANGED
@@ -90,7 +90,7 @@ export async function addRoutes({app, service} = {}) {
90
90
  if(response.verified) {
91
91
  res.status(200).json(response);
92
92
  } else {
93
- res.status(400).json(response).end();
93
+ res.status(400).json(response);
94
94
  }
95
95
 
96
96
  // meter operation usage
package/lib/status.js CHANGED
@@ -1,10 +1,12 @@
1
1
  /*!
2
- * Copyright (c) 2019-2024 Digital Bazaar, Inc. All rights reserved.
2
+ * Copyright (c) 2019-2025 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
+ import * as bedrock from '@bedrock/core';
4
5
  import {
5
6
  checkStatus as bitstringStatusListCheckStatus,
6
7
  statusTypeMatches as bitstringStatusListStatusTypeMatches
7
8
  } from '@digitalbazaar/vc-bitstring-status-list';
9
+ import {createDocumentLoader, webLoader} from './documentLoader.js';
8
10
  import {
9
11
  checkStatus as revocationListCheckStatus,
10
12
  statusTypeMatches as revocationListStatusTypeMatches
@@ -14,13 +16,21 @@ import {
14
16
  statusTypeMatches as statusList2020StatusTypeMatches
15
17
  } from '@digitalbazaar/vc-status-list';
16
18
  import assert from 'assert-plus';
17
- import {createDocumentLoader} from './documentLoader.js';
19
+
20
+ const {util: {BedrockError}} = bedrock;
21
+
22
+ const TERSE_BITSTRING_STATUS_LIST_ENTRY = 'TerseBitstringStatusListEntry';
23
+ // always 2^26 = 67108864 per vc-barcodes spec
24
+ const TERSE_BITSTRING_STATUS_LIST_LENGTH = 67108864;
25
+ const TERSE_STATUS_PURPOSES = ['revocation', 'suspension'];
26
+ const VC_BARCODES_V1_CONTEXT_URL = 'https://w3id.org/vc-barcodes/v1';
18
27
 
19
28
  const handlerMap = new Map();
20
29
  handlerMap.set('BitstringStatusListEntry', {
21
30
  checkStatus: bitstringStatusListCheckStatus,
22
31
  statusTypeMatches: bitstringStatusListStatusTypeMatches
23
32
  });
33
+ // legacy status entry types
24
34
  handlerMap.set('RevocationList2020Status', {
25
35
  checkStatus: revocationListCheckStatus,
26
36
  statusTypeMatches: revocationListStatusTypeMatches
@@ -36,30 +46,66 @@ export function createCheckStatus({config} = {}) {
36
46
  assert.object(options.credential, 'options.credential');
37
47
 
38
48
  try {
39
- const {credential} = options;
40
- const {credentialStatus} = credential;
41
- if(!credentialStatus) {
49
+ if(!options.credential.credentialStatus) {
42
50
  // no status to check
43
51
  return {verified: true};
44
52
  }
45
53
 
46
- const handlers = handlerMap.get(credentialStatus.type);
54
+ // expand every `TerseBitstringStatusListEntry`
55
+ const cache = new Map();
56
+ const credential = await _expandAllTerseEntries({
57
+ credential: options.credential, cache
58
+ });
59
+ const {credentialStatus} = credential;
60
+
61
+ // normalize credential status to an array
62
+ const credentialStatuses = Array.isArray(credentialStatus) ?
63
+ credentialStatus : [credentialStatus];
64
+
65
+ // combination of different status types not supported at this time
66
+ const expectedType = credentialStatuses?.[0]?.type;
67
+ if(credentialStatuses.some(({type}) => type !== expectedType)) {
68
+ throw new BedrockError(
69
+ 'Combinations of different credential status types are not ' +
70
+ 'presently supported.', {
71
+ name: 'NotSupportedError',
72
+ details: {
73
+ httpStatusCode: 400,
74
+ public: true
75
+ }
76
+ });
77
+ }
78
+
79
+ // get handlers for `expectedType`
80
+ const handlers = handlerMap.get(expectedType);
47
81
  if(!(handlers && handlers.statusTypeMatches({credential}))) {
48
- throw new Error(
49
- `Unsupported credentialStatus type "${credentialStatus.type}".`);
82
+ throw new BedrockError(
83
+ `Unsupported credentialStatus type "${expectedType}".`, {
84
+ name: 'NotSupportedError',
85
+ details: {
86
+ httpStatusCode: 400,
87
+ public: true
88
+ }
89
+ });
90
+ }
91
+
92
+ // create remote URL allow list from status lists
93
+ const remoteUrlAllowList = new Set();
94
+ for(const cs of credentialStatuses) {
95
+ const url = cs.statusListCredential ?? cs.revocationListCredential;
96
+ if(url) {
97
+ remoteUrlAllowList.add(url);
98
+ }
50
99
  }
51
100
 
52
101
  // document loader needs to only allow web loading of status
53
102
  // list VCs, nothing else
54
103
  const documentLoader = await createDocumentLoader({
55
- config,
56
- remoteUrlAllowList: new Set([
57
- credentialStatus.statusListCredential ??
58
- credentialStatus.revocationListCredential
59
- ])
104
+ config, remoteUrlAllowList, cache
60
105
  });
61
106
  options = {
62
107
  ...options,
108
+ credential,
63
109
  documentLoader
64
110
  };
65
111
  return await handlers.checkStatus(options);
@@ -68,3 +114,122 @@ export function createCheckStatus({config} = {}) {
68
114
  }
69
115
  };
70
116
  }
117
+
118
+ async function _expandAllTerseEntries({credential, cache} = {}) {
119
+ try {
120
+ // check for any terse entries
121
+ let hasTerseEntries = false;
122
+ const {credentialStatus} = credential;
123
+ if(Array.isArray(credentialStatus)) {
124
+ hasTerseEntries = credentialStatus.some(
125
+ cs => cs?.type === TERSE_BITSTRING_STATUS_LIST_ENTRY);
126
+ } else if(credentialStatus?.type === TERSE_BITSTRING_STATUS_LIST_ENTRY) {
127
+ hasTerseEntries = true;
128
+ }
129
+
130
+ if(!hasTerseEntries) {
131
+ return credential;
132
+ }
133
+
134
+ // check for expected context
135
+ const {'@context': contexts} = credential;
136
+ if(!Array.isArray(contexts)) {
137
+ throw new TypeError('"@context" must be an array.');
138
+ }
139
+ if(!contexts.includes(VC_BARCODES_V1_CONTEXT_URL)) {
140
+ throw new TypeError(
141
+ `The "@context" array must include "${VC_BARCODES_V1_CONTEXT_URL}".`);
142
+ }
143
+
144
+ // expand any `TerseBitstringStatusListEntry` to `BitstringStatusListEntry`
145
+ credential = structuredClone(credential);
146
+ if(Array.isArray(credentialStatus)) {
147
+ credential.credentialStatus = (await Promise.all(
148
+ credentialStatus.map(
149
+ async credentialStatus => _expandIfTerseEntry({
150
+ credentialStatus, cache
151
+ })))).flat();
152
+ } else {
153
+ credential.credentialStatus = await _expandIfTerseEntry({
154
+ credentialStatus, cache
155
+ });
156
+ }
157
+ return credential;
158
+ } catch(cause) {
159
+ throw new BedrockError(
160
+ `Could not expand terse bitstring status list entries: ${cause.message}`,
161
+ {
162
+ name: 'DataError',
163
+ cause,
164
+ details: {
165
+ httpStatusCode: 400,
166
+ public: true
167
+ }
168
+ });
169
+ }
170
+ }
171
+
172
+ async function _expandIfTerseEntry({credentialStatus, cache}) {
173
+ if(credentialStatus?.type !== TERSE_BITSTRING_STATUS_LIST_ENTRY) {
174
+ // nothing to expand
175
+ return credentialStatus;
176
+ }
177
+
178
+ if(!webLoader) {
179
+ throw new BedrockError(
180
+ `Web loader disabled; cannot load credential status list(s) for `
181
+ `status type "${credentialStatus.type}".`, {
182
+ name: 'NotSupportedError',
183
+ details: {
184
+ httpStatusCode: 400,
185
+ public: true
186
+ }
187
+ });
188
+ }
189
+
190
+ // compute two possible expanded statuses, for purposes `revocation` and
191
+ // `suspension`...
192
+ const credentialStatuses = (await Promise.all(
193
+ TERSE_STATUS_PURPOSES.map(async statusPurpose => {
194
+ const expanded = _expandTerseEntry({credentialStatus, statusPurpose});
195
+ const exists = await _fetchStatusListIfExists({expanded, cache});
196
+ return exists ? expanded : undefined;
197
+ }))).filter(cs => !!cs);
198
+
199
+ return credentialStatuses;
200
+ }
201
+
202
+ function _expandTerseEntry({credentialStatus, statusPurpose}) {
203
+ // compute `statusListCredential` from other params
204
+ const listIndex = Math.floor(
205
+ credentialStatus.terseStatusListIndex / TERSE_BITSTRING_STATUS_LIST_LENGTH);
206
+ const statusListIndex = credentialStatus.terseStatusListIndex %
207
+ TERSE_BITSTRING_STATUS_LIST_LENGTH;
208
+ const {terseStatusListBaseUrl} = credentialStatus;
209
+ const statusListCredential =
210
+ `${terseStatusListBaseUrl}/${statusPurpose}/${listIndex}`;
211
+ return {
212
+ type: 'BitstringStatusListEntry',
213
+ statusListCredential,
214
+ statusListIndex: `${statusListIndex}`,
215
+ statusPurpose
216
+ };
217
+ }
218
+
219
+ async function _fetchStatusListIfExists({expanded, cache}) {
220
+ try {
221
+ const {statusListCredential} = expanded;
222
+ if(cache.has(statusListCredential)) {
223
+ return true;
224
+ }
225
+ const {document} = await webLoader(statusListCredential);
226
+ cache.set(statusListCredential, document);
227
+ return true;
228
+ } catch(e) {
229
+ if(e.message === 'NotFoundError') {
230
+ // ok for a terse bitstring list to not exist
231
+ return false;
232
+ }
233
+ throw e;
234
+ }
235
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bedrock/vc-verifier",
3
- "version": "23.2.0",
3
+ "version": "23.3.1",
4
4
  "type": "module",
5
5
  "description": "Bedrock VC Verifier",
6
6
  "main": "./lib/index.js",