diff --git a/lib/http.js b/lib/http.js index 532d173a..0fe24e95 100644 --- a/lib/http.js +++ b/lib/http.js @@ -1,11 +1,9 @@ /*! - * Copyright (c) 2018-2023 Digital Bazaar, Inc. All rights reserved. + * Copyright (c) 2018-2024 Digital Bazaar, Inc. All rights reserved. */ import * as bedrock from '@bedrock/core'; import * as slcs from './slcs.js'; -import { - getMatchingStatusListConfig, issue, publishSlc, setStatus -} from './issuer.js'; +import {getMatchingStatusListConfig, issue, setStatus} from './issuer.js'; import { issueCredentialBody, publishSlcBody, @@ -124,7 +122,7 @@ export async function addRoutes({app, service} = {}) { const {slcId} = req.params; const id = `${config.id}${cfg.routes.slcs}/${encodeURIComponent(slcId)}`; - await publishSlc({id, config}); + await slcs.publish({id, config}); res.sendStatus(204); // meter operation usage diff --git a/lib/issuer.js b/lib/issuer.js index 28853aa2..8f5c4568 100644 --- a/lib/issuer.js +++ b/lib/issuer.js @@ -2,7 +2,6 @@ * Copyright (c) 2020-2024 Digital Bazaar, Inc. All rights reserved. */ import * as bedrock from '@bedrock/core'; -import * as slcs from './slcs.js'; import { issue as _issue, getCredentialStatusInfo, getDocumentStore, getIssuerAndSuite } from './helpers.js'; @@ -11,6 +10,7 @@ import {createDocumentLoader} from './documentLoader.js'; import {CredentialStatusIssuer} from './CredentialStatusIssuer.js'; import {CredentialStatusWriter} from './CredentialStatusWriter.js'; import {decodeList} from '@digitalbazaar/vc-status-list'; +import {publish} from './slcs.js'; const {util: {BedrockError}} = bedrock; @@ -122,36 +122,6 @@ export async function issue({credential, config} = {}) { return verifiableCredential; } -export async function publishSlc({id, config} = {}) { - assert.string(id, 'id'); - assert.object(config, 'config'); - - // do not use cache to ensure latest doc is published - const documentStore = await getDocumentStore({config}); - const slcDoc = await documentStore.get({id, useCache: false}); - const {content: credential, meta, sequence} = slcDoc; - if(!(meta.type === 'VerifiableCredential' && - _isStatusListCredential({credential}))) { - throw new BedrockError( - `Credential "${id}" is not a supported status list credential.`, - 'DataError', { - httpStatusCode: 400, - public: true - }); - } - try { - // store SLC Doc for public serving - await slcs.set({credential, sequence}); - } catch(e) { - // safe to ignore conflicts, a newer version of the SLC was published - // than the one that was retrieved - if(e.name === 'InvalidStateError' || e.name === 'DuplicateError') { - return; - } - throw e; - } -} - export async function setStatus({id, config, statusListConfig, status} = {}) { assert.string(id, 'id'); assert.object(config, 'config'); @@ -207,8 +177,8 @@ export async function setStatus({id, config, statusListConfig, status} = {}) { // express date without milliseconds const now = (new Date()).toJSON(); slc.issuanceDate = `${now.slice(0, -5)}Z`; - // TODO: we want to be using `issued` and/or `validFrom`, right? - //slc.issued = issuanceDate; + // FIXME: add `slc.expirationDate` + // TODO: use `validFrom` and `validUntil` for v2 VCs // delete existing proof and reissue SLC VC delete slc.proof; @@ -226,8 +196,9 @@ export async function setStatus({id, config, statusListConfig, status} = {}) { } } + // FIXME: auto-publish should handle this // publish latest version of SLC for non-authz consumption - await publishSlc({id: slcId, config}); + await publish({id: slcId, config}); } export function getMatchingStatusListConfig({config, credentialStatus} = {}) { @@ -261,29 +232,3 @@ export function getMatchingStatusListConfig({config, credentialStatus} = {}) { public: true }); } - -// check if `credential` is some known type of status list credential -function _isStatusListCredential({credential}) { - // FIXME: check for VC context as well - if(!(credential['@context'] && Array.isArray(credential['@context']))) { - return false; - } - if(!(credential.type && Array.isArray(credential.type) && - credential.type.includes('VerifiableCredential'))) { - return false; - } - - for(const type of credential.type) { - if(type === 'RevocationList2020Credential') { - // FIXME: check for matching `@context` as well - return true; - } - if(type === 'StatusList2021Credential') { - // FIXME: check for matching `@context as well - return true; - } - } - // FIXME: check other types - - return false; -} diff --git a/lib/slcs.js b/lib/slcs.js index 2b0c06ef..51b60713 100644 --- a/lib/slcs.js +++ b/lib/slcs.js @@ -1,9 +1,10 @@ /*! - * Copyright (c) 2020-2023 Digital Bazaar, Inc. All rights reserved. + * Copyright (c) 2020-2024 Digital Bazaar, Inc. All rights reserved. */ import * as bedrock from '@bedrock/core'; import * as database from '@bedrock/mongodb'; import assert from 'assert-plus'; +import {getDocumentStore} from './helpers.js'; import {LruCache} from '@digitalbazaar/lru-memoize'; const {util: {BedrockError}} = bedrock; @@ -115,6 +116,47 @@ export async function exists({id}) { return !!record; } +/** + * Publishes the status list credential with the given ID for the given + * issuer `config` -- if a newer version (since the time at which this function + * was called) has not already been published. + * + * @param {object} options - The options to use. + * @param {string} options.id - The SLC ID. + * @param {object} options.config - The issuer config. + * + * @returns {Promise} Settles once the operation completes. + */ +export async function publish({id, config} = {}) { + assert.string(id, 'id'); + assert.object(config, 'config'); + + // do not use cache to ensure latest doc is published + const documentStore = await getDocumentStore({config}); + const slcDoc = await documentStore.get({id, useCache: false}); + const {content: credential, meta, sequence} = slcDoc; + if(!(meta.type === 'VerifiableCredential' && + _isStatusListCredential({credential}))) { + throw new BedrockError( + `Credential "${id}" is not a supported status list credential.`, + 'DataError', { + httpStatusCode: 400, + public: true + }); + } + try { + // store SLC Doc for public serving + await set({credential, sequence}); + } catch(e) { + // safe to ignore conflicts, a newer version of the SLC was published + // than the one that was retrieved + if(e.name === 'InvalidStateError' || e.name === 'DuplicateError') { + return; + } + throw e; + } +} + async function _getUncachedRecord({id}) { const collection = database.collections[COLLECTION_NAME]; const record = await collection.findOne( @@ -130,3 +172,29 @@ async function _getUncachedRecord({id}) { } return record; } + +// check if `credential` is some known type of status list credential +function _isStatusListCredential({credential}) { + // FIXME: check for VC context as well + if(!(credential['@context'] && Array.isArray(credential['@context']))) { + return false; + } + if(!(credential.type && Array.isArray(credential.type) && + credential.type.includes('VerifiableCredential'))) { + return false; + } + + for(const type of credential.type) { + if(type === 'RevocationList2020Credential') { + // FIXME: check for matching `@context` as well + return true; + } + if(type === 'StatusList2021Credential') { + // FIXME: check for matching `@context as well + return true; + } + } + // FIXME: check other types + + return false; +}