Skip to content

Commit

Permalink
Refactor SLC publication code and implement auto-publish.
Browse files Browse the repository at this point in the history
  • Loading branch information
dlongley committed Feb 19, 2024
1 parent e24c4a6 commit 4484829
Show file tree
Hide file tree
Showing 6 changed files with 189 additions and 145 deletions.
7 changes: 5 additions & 2 deletions lib/CredentialStatusWriter.js
Original file line number Diff line number Diff line change
Expand Up @@ -286,10 +286,11 @@ export class CredentialStatusWriter {
// include all status information in `meta`
credentialStatus: {
id: `${statusListCredential}#${statusListIndex}`,
type,
// `type` is set below
type: undefined,
statusListCredential,
// this is the index of the VC's status within the status list
statusListIndex: `${statusListIndex}`,
statusListIndex,
statusPurpose
}
};
Expand Down Expand Up @@ -336,6 +337,8 @@ export class CredentialStatusWriter {
credentialStatus.statusListIndex = `${statusListIndex}`;
delete credentialStatus.terseStatusListIndex;
}
// ensure `meta.credentialStatus.type` is set
meta.credentialStatus.type = credentialStatus.type;

// add credential status if it did not already exist
if(Array.isArray(credential.credentialStatus)) {
Expand Down
37 changes: 11 additions & 26 deletions lib/ListSource.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/*!
* Copyright (c) 2020-2024 Digital Bazaar, Inc. All rights reserved.
*/
import * as slcs from './slcs.js';
import {
createList as createList2020,
createCredential as createRlc
Expand Down Expand Up @@ -61,7 +60,7 @@ export class ListSource {

// FIXME: status service should handle this instead
async _ensureStatusListCredentialExists({id, statusPurpose, length}) {
const {documentLoader, edvClient} = this;
const {statusListConfig, documentLoader, edvClient} = this;

// try to create SLC credential and EDV doc...
const {suite, issuer} = this;
Expand All @@ -76,41 +75,27 @@ export class ListSource {
`This credential expresses status information for some ` +
'other credentials in an encoded and compressed list.';
// express date without milliseconds
const now = (new Date()).toJSON();
credential.issuanceDate = `${now.slice(0, -5)}Z`;
const date = new Date();
// TODO: use `validFrom` and `validUntil` for v2 VCs
credential.issuanceDate = `${date.toISOString().slice(0, -5)}Z`;
// FIXME: get validity period via status service instance config
date.setDate(date.getDate() + 1);
credential.expirationDate = `${date.toISOString().slice(0, -5)}Z`;
const verifiableCredential = await issue(
{credential, documentLoader, suite});
let doc = {
const doc = {
id: await edvClient.generateId(),
content: verifiableCredential,
// include `meta.type` as a non-user input type to validate against
meta: {type: 'VerifiableCredential'}
meta: {type: 'VerifiableCredential', statusListConfig}
};
try {
doc = await edvClient.update({doc});
await edvClient.update({doc});
} catch(e) {
if(e.name !== 'DuplicateError') {
throw e;
}
// duplicate, ignore as another process created the SLC... get it
doc = await edvClient.get({id: doc.id});
}

// ensure SLC is published
const isPublished = await slcs.exists({id});
if(!isPublished) {
const {content: credential, sequence} = doc;
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;
}
// duplicate, ignore as another process created the SLC
}
}
}
63 changes: 38 additions & 25 deletions lib/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,25 @@ import {AsymmetricKey, KmsClient} from '@digitalbazaar/webkms-client';
import {didIo} from '@bedrock/did-io';
import {documentStores} from '@bedrock/service-agent';
import {generateId} from 'bnid';
import {
getCredentialStatus as get2020CredentialStatus
} from '@digitalbazaar/vc-revocation-list';
import {getCredentialStatus} from '@digitalbazaar/vc-status-list';
import {getSuiteParams} from './suites.js';
import {httpsAgent} from '@bedrock/https-agent';
import {serviceAgents} from '@bedrock/service-agent';
import {serviceType} from './constants.js';

const {util: {BedrockError}} = bedrock;

export function assertSlcDoc({slcDoc, id} = {}) {
if(!(slcDoc?.meta.type === 'VerifiableCredential' &&
_isStatusListCredential({credential: slcDoc?.content}))) {
throw new BedrockError(
`Credential "${id}" is not a supported status list credential.`,
'DataError', {
httpStatusCode: 400,
public: true
});
}
}

export async function generateLocalId() {
// 128-bit random number, base58 multibase + multihash encoded
return generateId({
Expand All @@ -28,27 +36,6 @@ export async function generateLocalId() {
});
}

// FIXME: move elsewhere?
export function getCredentialStatusInfo({credential, statusListConfig}) {
const {type, statusPurpose} = statusListConfig;
let credentialStatus;
let statusListIndex;
let statusListCredential;
if(type === 'RevocationList2020') {
// use legacy credential status
credentialStatus = get2020CredentialStatus({credential});
statusListIndex = parseInt(credentialStatus.revocationListIndex, 10);
({revocationListCredential: statusListCredential} = credentialStatus);
} else {
// use modern status list (2021)
credentialStatus = getCredentialStatus({credential, statusPurpose});
statusListIndex = parseInt(credentialStatus.statusListIndex, 10);
({statusListCredential} = credentialStatus);
}
// FIXME: support other `credentialStatus` types
return {credentialStatus, statusListIndex, statusListCredential};
}

export async function getDocumentStore({config}) {
// ensure indexes are set for VCs
const {documentStore} = await documentStores.get({config, serviceType});
Expand Down Expand Up @@ -103,3 +90,29 @@ export async function issue({credential, documentLoader, suite}) {
credential = {...credential};
return vc.issue({credential, documentLoader, suite});
}

// 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;
}
45 changes: 38 additions & 7 deletions lib/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/
import * as bedrock from '@bedrock/core';
import * as slcs from './slcs.js';
import {getMatchingStatusListConfig, issue, setStatus} from './issuer.js';
import {issue, setStatus} from './issuer.js';
import {
issueCredentialBody,
publishSlcBody,
Expand Down Expand Up @@ -91,12 +91,9 @@ export async function addRoutes({app, service} = {}) {
const {config} = req.serviceObject;
const {credentialId, credentialStatus} = req.body;

// check `credentialStatus` against status list options from `config`
const match = getMatchingStatusListConfig({config, credentialStatus});

// FIXME: support client requesting `status=false` as well
await setStatus({
id: credentialId, config, statusListConfig: match, status: true
id: credentialId, config, credentialStatus, status: true
});

res.status(200).end();
Expand Down Expand Up @@ -138,7 +135,41 @@ export async function addRoutes({app, service} = {}) {
const {config} = req.serviceObject;
const {slcId} = req.params;
const id = `${config.id}${cfg.routes.slcs}/${encodeURIComponent(slcId)}`;
const record = await slcs.get({id});
res.json(record.credential);
while(true) {
console.log('in get while loop');
try {
const record = await slcs.get({id});
// refresh SLC if it has expired; get `now` as a minute into the
// future to ensure the returned VC is still valid once returned to
// the client
const now = new Date();
now.setDate(now.getTime() + 1000 * 60);
// FIXME: support v2 VCs w/`validUntil`
const validUntil = new Date(record.credential.expirationDate);
if(validUntil <= now) {
res.json(record.credential);
return;
}
console.log('**********AUTO REFRESHING SLCS************');
const doc = await slcs.refresh({id, config});
res.json(doc.content);
return;
} catch(e) {
if(e.name !== 'NotFoundError') {
// unrecoverable error
throw e;
}
try {
// SLC not found, perhaps not published; try auto-publish
console.log('***********AUTO PUBLISHING***********');
await slcs.publish({id, config});
} catch(publishError) {
// log publication error
logger.error(publishError.message, {error: publishError});
// if publication fails, throw original `NotFoundError`
throw e;
}
}
}
}));
}
Loading

0 comments on commit 4484829

Please sign in to comment.