@bedrock/vc-delivery
Version:
Bedrock Verifiable Credential Delivery
207 lines (186 loc) • 6.4 kB
JavaScript
/*!
* Copyright (c) 2022-2026 Digital Bazaar, Inc. All rights reserved.
*/
import * as bedrock from '@bedrock/core';
import {deepEqual, getWorkflowIssuerInstances} from '../helpers.js';
import {randomUUID as uuid} from 'node:crypto';
const {util: {BedrockError}} = bedrock;
export function createSupportedCredentialConfigurations({
exchange, issuerInstances, step
} = {}) {
// get legacy `expectedCredentialRequests`
const {
openId: {expectedCredentialRequests}
} = exchange;
// build legacy supported credential configurations...
const supported = new Map();
// get supported formats from issuer instances
const supportedFormats = new Set();
issuerInstances.forEach(instance => instance.supportedFormats.forEach(
supportedFormats.add, supportedFormats));
// for every expected credential request and supported format, generate a
// supported credential configuration
for(const credentialRequest of expectedCredentialRequests) {
const configurations = _createCredentialConfigurations({
credentialRequest, supportedFormats, step
});
for(const {id, configuration} of configurations) {
supported.set(id, configuration);
}
}
return Object.fromEntries(supported.entries());
}
export function createSupportedCredentialRequests({
workflow, exchange, issueRequestsParams, step
} = {}) {
const issuerInstances = getWorkflowIssuerInstances({workflow});
const supported = createSupportedCredentialConfigurations({
exchange, issuerInstances, step
});
// for each `issueRequest` params, create a duplicate for each supported
// credential configuration
const ids = Object.keys(supported);
const supportedCredentialRequests = [];
for(let i = 0; i < issueRequestsParams.length; ++i) {
for(const credentialConfigurationId of ids) {
supportedCredentialRequests.push({
credentialConfigurationId,
credentialIdentifier: uuid(),
issueRequestsParamsIndex: i,
processed: false,
});
}
}
return supportedCredentialRequests;
}
export function normalizeCredentialRequestsToVersion1({
credentialRequests, supportedCredentialConfigurations
}) {
// normalize credential requests to use `type` instead of `types`; this is to
// allow for OID4VCI draft implementers that followed the non-normative
// examples
credentialRequests = _normalizeCredentialDefinitionTypes({
credentialRequests
});
// match draft 13 requests against credential configurations
return _matchCredentialRequests({
credentialRequests, supportedCredentialConfigurations
});
}
function _createCredentialConfigurationId({
format, credential_definition
}) {
let types = (credential_definition.type ?? credential_definition.types);
if(types.length > 1) {
types = types.filter(t => t !== 'VerifiableCredential');
}
return types.join('_') + '_' + format;
}
function _matchCredentialRequest(expected, cr) {
const {credential_definition: {'@context': c1, type: t1}} = expected;
const {credential_definition: {'@context': c2, type: t2}} = cr;
// contexts must match exactly but types can have different order
return (c1.length === c2.length && t1.length === t2.length &&
deepEqual(c1, c2) && t1.every(t => t2.some(x => t === x)));
}
function _matchCredentialRequests({
credentialRequests, supportedCredentialConfigurations
}) {
// ensure that every credential request is for the same format
/* credential requests look like:
{
format: 'ldp_vc',
credential_definition: { '@context': [Array], type: [Array] }
}
*/
let sharedFormat;
if(!credentialRequests.every(({format}) => {
if(sharedFormat === undefined) {
sharedFormat = format;
}
return sharedFormat === format;
})) {
throw new BedrockError(
'Credential requests in a batch must all use the same format.', {
name: 'DataError',
details: {httpStatusCode: 400, public: true}
});
}
// ensure every credential request matches a supported configuration
const entries = Object.entries(supportedCredentialConfigurations);
return credentialRequests.map(cr => {
for(const [credential_configuration_id, configuration] of entries) {
if(_matchCredentialRequest(configuration, cr)) {
const newRequest = {
type: 'openid_credential',
credential_configuration_id
};
if(cr.proof) {
newRequest.proofs = {};
for(const [key, value] of Object.entries(cr.proof)) {
if(key === 'proof_type') {
newRequest.proof_type = value;
continue;
}
newRequest.proofs[key] = [value];
}
}
return newRequest;
}
}
throw new BedrockError(
'Unexpected credential request.', {
name: 'DataError',
details: {httpStatusCode: 400, public: true}
});
});
}
function _createCredentialConfigurations({
credentialRequest, supportedFormats, step
}) {
const configurations = [];
let {format: formats = supportedFormats} = credentialRequest;
if(!Array.isArray(formats)) {
formats = [formats];
}
for(const format of formats) {
const {credential_definition} = credentialRequest;
const id = _createCredentialConfigurationId({
format, credential_definition
});
const configuration = {format, credential_definition};
if(step.divpDidProofRequest) {
configuration.proof_types_supported = {
di_vp: {
proof_signing_alg_values_supported: [
'ecdsa-rdfc-2019', 'eddsa-rdfc-2022'
]
}
};
}
if(step.jwtDidProofRequest) {
if(!configuration.proof_types_supported) {
configuration.proof_types_supported = {};
}
configuration.proof_types_supported.jwt = {
proof_signing_alg_values_supported: ['ES256']
};
}
configurations.push({id, configuration});
}
return configurations;
}
function _normalizeCredentialDefinitionTypes({credentialRequests}) {
// normalize credential requests to use `type` instead of `types`
return credentialRequests.map(cr => {
if(!cr?.credential_definition?.types) {
return cr;
}
cr = {...cr};
if(!cr.credential_definition.type) {
cr.credential_definition.type = cr.credential_definition.types;
}
delete cr.credential_definition.types;
return cr;
});
}