UNPKG

@bedrock/vc-delivery

Version:
250 lines (223 loc) 8.11 kB
/*! * Copyright (c) 2022-2026 Digital Bazaar, Inc. All rights reserved. */ import * as bedrock from '@bedrock/core'; import { evaluateTemplate, getTemplateVariables, getWorkflowIssuerInstances, getZcapClient, setVariable, SUPPORTED_FORMAT_TO_MEDIA_TYPE, } from './helpers.js'; import {createPresentation} from '@digitalbazaar/vc'; const {util: {BedrockError}} = bedrock; export async function issue({ workflow, exchange, step, mediaType = 'application/vc', format, issueRequestsParams, verifiablePresentation, // FIXME: remove `filter` // by default do not issue any VCs that are to be stored in the exchange; // (`result` is NOT set) filter = params => !params.result } = {}) { // eval all issue requests for current step in exchange const issueRequests = await _evalIssueRequests({ workflow, exchange, step, issueRequestsParams, filter }); // return early if there is no explicit VP in step nor nothing to issue if(!step?.verifiablePresentation && issueRequests.length === 0) { return {response: {}, exchangeChanged: false}; } if(format) { // fall back to using `format` as media type if necessary mediaType = SUPPORTED_FORMAT_TO_MEDIA_TYPE.get(format) ?? format; } // run all issue requests const { credentials: issuedVcs, exchangeChanged } = await _issue({workflow, exchange, issueRequests, mediaType}); if(issuedVcs.length === 0 && !step?.verifiablePresentation) { // no issued VCs/no VP to return in response return {response: {}, exchangeChanged}; } // generate VP to return VCs; use any explicitly defined VP from the step // (which may include out-of-band issued VCs that are to be delivered) verifiablePresentation = verifiablePresentation ?? structuredClone(step?.verifiablePresentation) ?? createPresentation(); // add issued VCs to VP if(issuedVcs.length > 0) { let vcs = verifiablePresentation.verifiableCredential; if(!vcs) { vcs = issuedVcs; } else if(Array.isArray(vcs)) { vcs = [...vcs, ...issuedVcs]; } else { vcs = [vcs, ...issuedVcs]; } verifiablePresentation.verifiableCredential = vcs; } return {response: {verifiablePresentation}, format, exchangeChanged}; } export function getIssueRequestsParams({workflow, exchange, step}) { // use any templates from workflow and variables from exchange to produce // credentials to be issued; issue via the configured issuer instance const {credentialTemplates = []} = workflow; if(!(credentialTemplates.length > 0)) { // no issue requests params return []; } if(!workflow.steps || (!step.issueRequests && Object.keys(workflow.steps).length === 1)) { // backwards-compatibility: deprecated workflows with no step or a single // step do not explicitly define `issueRequests` but instead consider each // credential template as the `typedTemplate` parameter (and the only // parameter) for an issue request return credentialTemplates.map(typedTemplate => ({typedTemplate})); } if(!step.issueRequests) { // no issue requests params return []; } // resolve all issue requests params const variables = getTemplateVariables({workflow, exchange}); return step.issueRequests.map(r => { // find the typed template to use let typedTemplate; if(r.credentialTemplateIndex !== undefined) { typedTemplate = credentialTemplates[r.credentialTemplateIndex]; } else if(r.credentialTemplateId !== undefined) { typedTemplate = credentialTemplates.find( t => t.id === r.credentialTemplateId); } if(typedTemplate === undefined) { throw new BedrockError( 'Credential template ' + `"${r.credentialTemplateIndex ?? r.credentialTemplateId}" not found.`, { name: 'DataError', details: {httpStatusCode: 500, public: true} }); } // allow different variables to be specified for the typed template let vars = variables; if(r.variables !== undefined) { vars = typeof r.variables === 'string' ? variables[r.variables] : r.variables; if(!(vars && typeof vars === 'object')) { throw new BedrockError( `Issue request variables "${r.variables}" not found or invalid.`, { name: 'DataError', details: {httpStatusCode: 500, public: true} }); } } const params = { oid4vci: r.oid4vci, typedTemplate, variables: { // always include globals but allow local override globals: variables.globals, ...vars } }; if(r.result) { params.result = r.result; } return params; }); } async function _evalIssueRequests({ workflow, exchange, step, issueRequestsParams, filter }) { // evaluate all issue requests in parallel // FIXME: require `issueRequestsParams` and remove `filter` param const results = issueRequestsParams ?? getIssueRequestsParams({workflow, exchange, step}).filter(filter); return Promise.all(results.map(async params => { const {typedTemplate, variables} = params; return { params, body: await evaluateTemplate({ workflow, exchange, typedTemplate, variables }) }; })); } function _getIssueZcap({issuerInstances, zcaps, issueRequest, mediaType}) { const { issuerInstanceId, oid4vci: {credentialConfigurationId} = {} } = issueRequest; const {zcapReferenceIds: {issue: issueRefId}} = issuerInstances.find( ({id, oid4vci, supportedMediaTypes}) => { // find by `issuerInstanceId` if given if(id && id === issuerInstanceId) { return true; } // find by `credentialConfigurationId` if given if(credentialConfigurationId && oid4vci) { return oid4vci.supportedConfigurationIds.includes( credentialConfigurationId); } // find by `mediaType` instead return supportedMediaTypes.includes(mediaType); }); return zcaps[issueRefId]; } async function _issue({workflow, exchange, issueRequests, mediaType} = {}) { // create zcap client for issuing VCs const {zcapClient, zcaps} = await getZcapClient({workflow}); // get issuer instances const issuerInstances = getWorkflowIssuerInstances({workflow}); // issue VCs in parallel let exchangeChanged = false; const storedCredentials = []; const results = await Promise.all(issueRequests.map(async issueRequest => { const {params, body} = issueRequest; // get the zcap to use for the issue request const capability = _getIssueZcap({ issuerInstances, zcaps, issueRequest, mediaType }); // specify URL to `/credentials/issue` to handle case that capability // is not specific to it let url = capability.invocationTarget; if(!capability.invocationTarget.endsWith('/credentials/issue')) { url += capability.invocationTarget.endsWith('/credentials') ? '/issue' : '/credentials/issue'; } /* Note: Issue request body can be any one of these: 1. `{credential, options?}` 2. `credential` Normalize all issue request bodies to full VC API issue request bodies. */ const json = !body?.credential ? {credential: body} : body; const { data: {verifiableCredential} } = await zcapClient.write({url, capability, json}); // if the issue request specifies a location for storing the credential, // put it there and return `undefined`; otherwise, return the credential if(params.result) { exchangeChanged = true; setVariable({ variables: exchange.variables, name: params.result, value: verifiableCredential }); storedCredentials.push([{ name: params.result, value: verifiableCredential }]); return; } return verifiableCredential; })); // filter out any undefined results, which are for results that were written // to exchange variables and are not to be automatically returned in a // presentation return { credentials: results.filter(vc => vc), storedCredentials, exchangeChanged }; }