11/*!
2- * Copyright (c) 2019-2024 Digital Bazaar, Inc. All rights reserved.
2+ * Copyright (c) 2019-2025 Digital Bazaar, Inc. All rights reserved.
33 */
4+ import * as bedrock from '@bedrock/core' ;
45import {
56 checkStatus as bitstringStatusListCheckStatus ,
67 statusTypeMatches as bitstringStatusListStatusTypeMatches
78} from '@digitalbazaar/vc-bitstring-status-list' ;
9+ import { createDocumentLoader , webLoader } from './documentLoader.js' ;
810import {
911 checkStatus as revocationListCheckStatus ,
1012 statusTypeMatches as revocationListStatusTypeMatches
@@ -14,13 +16,21 @@ import {
1416 statusTypeMatches as statusList2020StatusTypeMatches
1517} from '@digitalbazaar/vc-status-list' ;
1618import 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' ;
1827
1928const handlerMap = new Map ( ) ;
2029handlerMap . set ( 'BitstringStatusListEntry' , {
2130 checkStatus : bitstringStatusListCheckStatus ,
2231 statusTypeMatches : bitstringStatusListStatusTypeMatches
2332} ) ;
33+ // legacy status entry types
2434handlerMap . set ( 'RevocationList2020Status' , {
2535 checkStatus : revocationListCheckStatus ,
2636 statusTypeMatches : revocationListStatusTypeMatches
@@ -36,30 +46,66 @@ export function createCheckStatus({config} = {}) {
3646 assert . object ( options . credential , 'options.credential' ) ;
3747
3848 try {
39- const { credential} = options ;
40- const { credentialStatus} = credential ;
41- if ( ! credentialStatus ) {
49+ if ( ! options . credential . credentialStatus ) {
4250 // no status to check
4351 return { verified : true } ;
4452 }
4553
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 ) ;
4781 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+ }
5099 }
51100
52101 // document loader needs to only allow web loading of status
53102 // list VCs, nothing else
54103 const documentLoader = await createDocumentLoader ( {
55- config,
56- remoteUrlAllowList : new Set ( [
57- credentialStatus . statusListCredential ??
58- credentialStatus . revocationListCredential
59- ] )
104+ config, remoteUrlAllowList, cache
60105 } ) ;
61106 options = {
62107 ...options ,
108+ credential,
63109 documentLoader
64110 } ;
65111 return await handlers . checkStatus ( options ) ;
@@ -68,3 +114,122 @@ export function createCheckStatus({config} = {}) {
68114 }
69115 } ;
70116}
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+ }
0 commit comments