@sphereon/pex
Version:
A Typescript implementation of the v1 and v2 DIF Presentation Exchange specification
857 lines (856 loc) • 115 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.EvaluationClientWrapper = void 0;
const jsonpath_1 = require("@astronautlabs/jsonpath");
const pex_models_1 = require("@sphereon/pex-models");
const ssi_types_1 = require("@sphereon/ssi-types");
const ConstraintUtils_1 = require("../ConstraintUtils");
const PEX_1 = require("../PEX");
const signing_1 = require("../signing");
const types_1 = require("../types");
const utils_1 = require("../utils");
const formatMap_1 = require("../utils/formatMap");
const core_1 = require("./core");
const evaluationClient_1 = require("./evaluationClient");
class EvaluationClientWrapper {
constructor() {
this._client = new evaluationClient_1.EvaluationClient();
}
getEvaluationClient() {
return this._client;
}
selectFrom(presentationDefinition, wrappedVerifiableCredentials, opts) {
var _a;
let selectResults;
this._client.evaluate(presentationDefinition, wrappedVerifiableCredentials, opts);
const warnings = [...this.formatNotInfo(ConstraintUtils_1.Status.WARN)];
const errors = [...this.formatNotInfo(ConstraintUtils_1.Status.ERROR)];
if (presentationDefinition.submission_requirements) {
const info = this._client.results.filter((result) => result.evaluator === 'MarkForSubmissionEvaluation' && result.payload.group && result.status !== ConstraintUtils_1.Status.ERROR);
const marked = Array.from(new Set(info));
let matchSubmissionRequirements;
try {
matchSubmissionRequirements = this.matchSubmissionRequirements(presentationDefinition, presentationDefinition.submission_requirements, marked);
}
catch (e) {
const matchingError = {
status: ConstraintUtils_1.Status.ERROR,
message: JSON.stringify(e),
tag: 'matchSubmissionRequirements',
};
return {
errors: errors ? [...errors, matchingError] : [matchingError],
warnings: warnings,
areRequiredCredentialsPresent: ConstraintUtils_1.Status.ERROR,
};
}
const matches = this.extractMatches(matchSubmissionRequirements);
const credentials = matches.map((e) => jsonpath_1.JSONPath.nodes(this._client.wrappedVcs.map((wrapped) => wrapped.original), e)[0].value);
const areRequiredCredentialsPresent = this.determineAreRequiredCredentialsPresent(presentationDefinition, matchSubmissionRequirements);
selectResults = {
errors: areRequiredCredentialsPresent === ConstraintUtils_1.Status.INFO ? [] : errors,
matches: [...matchSubmissionRequirements],
areRequiredCredentialsPresent,
verifiableCredential: credentials,
warnings,
};
}
else {
const marked = this._client.results.filter((result) => result.evaluator === 'MarkForSubmissionEvaluation' && result.status !== ConstraintUtils_1.Status.ERROR);
const checkWithoutSRResults = this.checkWithoutSubmissionRequirements(marked, presentationDefinition);
if (!checkWithoutSRResults.length) {
const matchSubmissionRequirements = this.matchWithoutSubmissionRequirements(marked, presentationDefinition);
const matches = this.extractMatches(matchSubmissionRequirements);
const credentials = matches.map((e) => jsonpath_1.JSONPath.nodes(this._client.wrappedVcs.map((wrapped) => wrapped.original), e)[0].value);
selectResults = {
errors: [],
matches: [...matchSubmissionRequirements],
areRequiredCredentialsPresent: ConstraintUtils_1.Status.INFO,
verifiableCredential: credentials,
warnings,
};
}
else {
return {
errors: errors,
matches: [],
areRequiredCredentialsPresent: ConstraintUtils_1.Status.ERROR,
verifiableCredential: wrappedVerifiableCredentials.map((value) => value.original),
warnings: warnings,
};
}
}
this.fillSelectableCredentialsToVerifiableCredentialsMapping(selectResults, wrappedVerifiableCredentials);
selectResults.areRequiredCredentialsPresent = this.determineAreRequiredCredentialsPresent(presentationDefinition, selectResults === null || selectResults === void 0 ? void 0 : selectResults.matches);
this.remapMatches(wrappedVerifiableCredentials.map((wrapped) => wrapped.original), selectResults.matches, selectResults === null || selectResults === void 0 ? void 0 : selectResults.verifiableCredential);
(_a = selectResults.matches) === null || _a === void 0 ? void 0 : _a.forEach((m) => {
this.updateSubmissionRequirementMatchPathToAlias(m, 'verifiableCredential');
});
if (selectResults.areRequiredCredentialsPresent === ConstraintUtils_1.Status.INFO) {
selectResults.errors = [];
}
else {
selectResults.errors = errors;
selectResults.warnings = warnings;
selectResults.verifiableCredential = wrappedVerifiableCredentials.map((value) => value.original);
}
return selectResults;
}
remapMatches(verifiableCredentials, submissionRequirementMatches, vcsToSend) {
submissionRequirementMatches === null || submissionRequirementMatches === void 0 ? void 0 : submissionRequirementMatches.forEach((srm) => {
if (srm.from_nested) {
this.remapMatches(verifiableCredentials, srm.from_nested, vcsToSend);
}
else {
srm.vc_path.forEach((match, index, matches) => {
const vc = jsonpath_1.JSONPath.query(verifiableCredentials, match)[0];
const newIndex = vcsToSend === null || vcsToSend === void 0 ? void 0 : vcsToSend.findIndex((svc) => JSON.stringify(svc) === JSON.stringify(vc));
if (newIndex === -1) {
throw new Error(`The index of the VerifiableCredential in your current call can't be found in your previously submitted credentials. Are you trying to send a new Credential?\nverifiableCredential: ${vc}`);
}
matches[index] = `$[${newIndex}]`;
});
srm.name;
}
});
}
extractMatches(matchSubmissionRequirements) {
const matches = [];
matchSubmissionRequirements.forEach((e) => {
matches.push(...e.vc_path);
if (e.from_nested) {
matches.push(...this.extractMatches(e.from_nested));
}
});
return Array.from(new Set(matches));
}
/**
* Since this is without SubmissionRequirements object, each InputDescriptor has to have at least one corresponding VerifiableCredential
* @param marked: info logs for `MarkForSubmissionEvaluation` handler
* @param pd
* @private
*/
checkWithoutSubmissionRequirements(marked, pd) {
const checkResult = [];
if (!pd.input_descriptors) {
return [];
}
if (!marked.length) {
return [
{
input_descriptor_path: '',
evaluator: 'checkWithoutSubmissionRequirement',
verifiable_credential_path: '',
status: ConstraintUtils_1.Status.ERROR,
payload: `Not all the InputDescriptors are addressed`,
},
];
}
const inputDescriptors = pd.input_descriptors;
const markedInputDescriptorPaths = utils_1.ObjectUtils.getDistinctFieldInObject(marked, 'input_descriptor_path');
if (markedInputDescriptorPaths.length !== inputDescriptors.length) {
const inputDescriptorsFromLogs = markedInputDescriptorPaths.map((value) => utils_1.JsonPathUtils.extractInputField(pd, [value])[0].value).map((value) => value.id);
for (let i = 0; i < pd.input_descriptors.length; i++) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (inputDescriptorsFromLogs.indexOf(pd.input_descriptors[i].id) == -1) {
checkResult.push({
input_descriptor_path: `$.input_descriptors[${i}]`,
evaluator: 'checkWithoutSubmissionRequirement',
verifiable_credential_path: '',
status: ConstraintUtils_1.Status.ERROR,
payload: `Not all the InputDescriptors are addressed`,
});
}
}
}
return checkResult;
}
matchSubmissionRequirements(pd, submissionRequirements, marked) {
const submissionRequirementMatches = [];
for (const [srIndex, sr] of Object.entries(submissionRequirements)) {
// Create a default SubmissionRequirementMatch object
const srm = {
rule: sr.rule,
vc_path: [],
name: sr.name,
type: core_1.SubmissionRequirementMatchType.SubmissionRequirement,
id: Number(srIndex),
};
if (sr.from) {
srm.from = sr.from;
}
// Assign min, max, and count regardless of 'from' or 'from_nested'
sr.min ? (srm.min = sr.min) : undefined;
sr.max ? (srm.max = sr.max) : undefined;
sr.count ? (srm.count = sr.count) : undefined;
if (sr.from) {
const matchingVcPaths = this.getMatchingVcPathsForSubmissionRequirement(pd, sr, marked);
srm.vc_path.push(...matchingVcPaths);
submissionRequirementMatches.push(srm);
}
else if (sr.from_nested) {
// Recursive call to matchSubmissionRequirements for nested requirements
try {
srm.from_nested = this.matchSubmissionRequirements(pd, sr.from_nested, marked);
submissionRequirementMatches.push(srm);
}
catch (err) {
throw new Error(`Error in handling value of from_nested: ${sr.from_nested}: err: ${err}`);
}
}
else {
// Throw an error if neither 'from' nor 'from_nested' is found
throw new Error("Invalid SubmissionRequirement object: Must contain either 'from' or 'from_nested'");
}
}
return submissionRequirementMatches;
}
matchWithoutSubmissionRequirements(marked, pd) {
const submissionRequirementMatches = [];
const partitionedIdToVcMap = this.createIdToVcMap(marked);
for (const [idPath, sameIdVcs] of partitionedIdToVcMap.entries()) {
if (!sameIdVcs || !sameIdVcs.length) {
continue;
}
for (const vcPath of sameIdVcs) {
const inputDescriptorResults = utils_1.JsonPathUtils.extractInputField(pd, [idPath]);
if (inputDescriptorResults.length) {
const inputDescriptor = inputDescriptorResults[0].value;
submissionRequirementMatches.push({
name: inputDescriptor.name || inputDescriptor.id,
rule: pex_models_1.Rules.All,
vc_path: [vcPath],
type: core_1.SubmissionRequirementMatchType.InputDescriptor,
id: inputDescriptor.id,
});
}
}
}
return this.removeDuplicateSubmissionRequirementMatches(submissionRequirementMatches);
}
getMatchingVcPathsForSubmissionRequirement(pd, sr, marked) {
const vcPaths = new Set();
if (!sr.from)
return Array.from(vcPaths);
for (const m of marked) {
const inputDescriptor = jsonpath_1.JSONPath.query(pd, m.input_descriptor_path)[0];
if (inputDescriptor.group && inputDescriptor.group.indexOf(sr.from) === -1) {
continue;
}
if (m.payload.group.includes(sr.from)) {
vcPaths.add(m.verifiable_credential_path);
}
}
return Array.from(vcPaths);
}
evaluate(pd, wvcs, opts) {
var _a, _b, _c;
this._client.evaluate(pd, wvcs, opts);
const result = {
areRequiredCredentialsPresent: ConstraintUtils_1.Status.INFO,
// TODO: we should handle the string case
verifiableCredential: wvcs.map((wrapped) => wrapped.original),
};
result.warnings = this.formatNotInfo(ConstraintUtils_1.Status.WARN);
result.errors = this.formatNotInfo(ConstraintUtils_1.Status.ERROR);
this._client.assertPresentationSubmission();
if ((_a = this._client.presentationSubmission) === null || _a === void 0 ? void 0 : _a.descriptor_map.length) {
this._client.presentationSubmission.descriptor_map = this._client.presentationSubmission.descriptor_map.filter((v) => v !== undefined);
result.value = JSON.parse(JSON.stringify(this._client.presentationSubmission));
}
if (this._client.generatePresentationSubmission) {
this.updatePresentationSubmissionPathToVpPath(result.value);
}
result.verifiableCredential = this._client.wrappedVcs.map((wrapped) => wrapped.original);
result.areRequiredCredentialsPresent = ((_c = (_b = result.value) === null || _b === void 0 ? void 0 : _b.descriptor_map) === null || _c === void 0 ? void 0 : _c.length) ? ConstraintUtils_1.Status.INFO : ConstraintUtils_1.Status.ERROR;
return result;
}
evaluatePresentations(pd, wvps, opts) {
var _a, _b, _c;
// If submission is provided as input, we match the presentations against the submission. In this case the submission MUST be valid
if (opts === null || opts === void 0 ? void 0 : opts.presentationSubmission) {
return this.evaluatePresentationsAgainstSubmission(pd, wvps, opts.presentationSubmission, opts);
}
const wrappedPresentations = Array.isArray(wvps) ? wvps : [wvps];
const allWvcs = wrappedPresentations.reduce((all, wvp) => [...all, ...wvp.vcs], []);
const result = {
areRequiredCredentialsPresent: ConstraintUtils_1.Status.INFO,
presentations: Array.isArray(wvps) ? wvps.map((wvp) => wvp.original) : [wvps.original],
errors: [],
warnings: [],
};
// Reset and configure the evaluation client on each iteration
this._client = new evaluationClient_1.EvaluationClient();
this._client.evaluate(pd, allWvcs, opts);
result.warnings = this.formatNotInfo(ConstraintUtils_1.Status.WARN);
result.errors = this.formatNotInfo(ConstraintUtils_1.Status.ERROR);
this._client.assertPresentationSubmission();
if ((_a = this._client.presentationSubmission) === null || _a === void 0 ? void 0 : _a.descriptor_map.length) {
this._client.presentationSubmission.descriptor_map = this._client.presentationSubmission.descriptor_map.filter((v) => v !== undefined);
result.value = JSON.parse(JSON.stringify(this._client.presentationSubmission));
}
const useExternalSubmission = (opts === null || opts === void 0 ? void 0 : opts.presentationSubmissionLocation) !== undefined
? opts.presentationSubmissionLocation === signing_1.PresentationSubmissionLocation.EXTERNAL
: Array.isArray(wvps);
if (this._client.generatePresentationSubmission && result.value && useExternalSubmission) {
// we map the descriptors of the generated submission to take into account the nexted values
result.value.descriptor_map = result.value.descriptor_map.map((descriptor) => {
const [wvcResult] = utils_1.JsonPathUtils.extractInputField(allWvcs, [descriptor.path]);
if (!wvcResult) {
throw new Error(`Could not find descriptor path ${descriptor.path} in wrapped verifiable credentials`);
}
const matchingWvc = wvcResult.value;
const matchingVpIndex = wrappedPresentations.findIndex((wvp) => wvp.vcs.includes(matchingWvc));
const matchingVp = wrappedPresentations[matchingVpIndex];
const matcingWvcIndexInVp = matchingVp.vcs.findIndex((wvc) => wvc === matchingWvc);
return this.updateDescriptorToExternal(descriptor, {
// We don't want to add vp index if the input to evaluate was a single presentation
vpIndex: Array.isArray(wvps) ? matchingVpIndex : undefined,
vcIndex: matcingWvcIndexInVp,
});
});
}
else if (this._client.generatePresentationSubmission && result.value) {
this.updatePresentationSubmissionPathToVpPath(result.value);
}
result.areRequiredCredentialsPresent = ((_c = (_b = result.value) === null || _b === void 0 ? void 0 : _b.descriptor_map) === null || _c === void 0 ? void 0 : _c.length) ? ConstraintUtils_1.Status.INFO : ConstraintUtils_1.Status.ERROR;
return result;
}
extractWrappedVcFromWrappedVp(descriptor, descriptorIndex, wvp) {
// Decoded won't work for sd-jwt or jwt?!?!
const [vcResult] = utils_1.JsonPathUtils.extractInputField(wvp.decoded, [descriptor.path]);
if (!vcResult) {
return {
error: {
status: ConstraintUtils_1.Status.ERROR,
tag: 'SubmissionPathNotFound',
message: `Unable to extract path ${descriptor.path} for submission.descriptor_path[${descriptorIndex}] from verifiable presentation`,
},
wvc: undefined,
};
}
// FIXME figure out possible types, can't see that in debug mode...
const isCredential = ssi_types_1.CredentialMapper.isCredential(vcResult.value);
if (!vcResult.value ||
(typeof vcResult.value === 'string' && !isCredential) ||
(typeof vcResult.value !== 'string' && !isCredential && !('verifiableCredential' in vcResult.value || 'vp' in vcResult.value))) {
return {
error: {
status: ConstraintUtils_1.Status.ERROR,
tag: 'NoVerifiableCredentials',
message: `No verifiable credentials found at path "${descriptor.path}" for submission.descriptor_path[${descriptorIndex}]`,
},
wvc: undefined,
};
}
// When result is an array, extract the first Verifiable Credential from the array FIXME figure out proper types, can't see that in debug mode...
let originalVc;
if (isCredential) {
originalVc = vcResult.value;
}
else if (typeof vcResult.value !== 'string') {
if ('verifiableCredential' in vcResult.value) {
originalVc = Array.isArray(vcResult.value.verifiableCredential)
? vcResult.value.verifiableCredential[0]
: vcResult.value.verifiableCredential;
}
else {
throw Error('Could not deduce original VC from evaluation result');
}
}
else {
throw Error('Could not deduce original VC from evaluation result');
}
// Find the corresponding Wrapped Verifiable Credential (wvc) based on the original VC
const wvc = wvp.vcs.find((wrappedVc) => ssi_types_1.CredentialMapper.areOriginalVerifiableCredentialsEqual(wrappedVc.original, originalVc));
if (!wvc) {
return {
error: {
status: ConstraintUtils_1.Status.ERROR,
tag: 'SubmissionPathNotFound',
message: `Unable to find wrapped VC for the extracted credential at path "${descriptor.path}" in descriptor_path[${descriptorIndex}]`,
},
wvc: undefined,
};
}
return {
wvc,
error: undefined,
};
}
evaluatePresentationsAgainstSubmission(pd, wvps, submission, opts) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
const result = {
areRequiredCredentialsPresent: ConstraintUtils_1.Status.INFO,
presentations: Array.isArray(wvps) ? wvps.map((wvp) => wvp.original) : [wvps.original],
errors: [],
warnings: [],
value: submission,
};
if (submission.definition_id !== pd.id) {
result.areRequiredCredentialsPresent = ConstraintUtils_1.Status.ERROR;
(_a = result.errors) === null || _a === void 0 ? void 0 : _a.push({
status: ConstraintUtils_1.Status.ERROR,
tag: 'SubmissionDefinitionIdNotFound',
message: `Presentation submission defines definition_id '${submission.definition_id}', but the provided definition has id '${pd.id}'`,
});
}
// If only a single VP is passed that is not w3c and no presentationSubmissionLocation, we set the default location to presentation. Otherwise we assume it's external
const presentationSubmissionLocation = (_b = opts === null || opts === void 0 ? void 0 : opts.presentationSubmissionLocation) !== null && _b !== void 0 ? _b : (Array.isArray(wvps) || !ssi_types_1.CredentialMapper.isW3cPresentation(Array.isArray(wvps) ? wvps[0].presentation : wvps.presentation)
? signing_1.PresentationSubmissionLocation.EXTERNAL
: signing_1.PresentationSubmissionLocation.PRESENTATION);
// Iterate over each descriptor in the submission
for (const [descriptorIndex, descriptor] of submission.descriptor_map.entries()) {
let matchingVp;
if (presentationSubmissionLocation === signing_1.PresentationSubmissionLocation.EXTERNAL) {
// Extract VPs matching the descriptor path
const vpResults = utils_1.JsonPathUtils.extractInputField(wvps, [descriptor.path]);
if (!vpResults.length) {
result.areRequiredCredentialsPresent = ConstraintUtils_1.Status.ERROR;
(_c = result.errors) === null || _c === void 0 ? void 0 : _c.push({
status: ConstraintUtils_1.Status.ERROR,
tag: 'SubmissionPathNotFound',
message: `Unable to extract path ${descriptor.path} for submission.descriptor_map[${descriptorIndex}] from presentation(s)`,
});
continue;
}
else if (vpResults.length > 1) {
result.areRequiredCredentialsPresent = ConstraintUtils_1.Status.ERROR;
(_d = result.errors) === null || _d === void 0 ? void 0 : _d.push({
status: ConstraintUtils_1.Status.ERROR,
tag: 'SubmissionPathMultipleEntries',
message: `Extraction of path ${descriptor.path} for submission.descriptor_map[${descriptorIndex}] resulted in multiple values being returned.`,
});
continue;
}
matchingVp = vpResults[0].value;
if (Array.isArray(matchingVp)) {
result.areRequiredCredentialsPresent = ConstraintUtils_1.Status.ERROR;
(_e = result.errors) === null || _e === void 0 ? void 0 : _e.push({
status: ConstraintUtils_1.Status.ERROR,
tag: 'SubmissionPathMultipleEntries',
message: `Extraction of path ${descriptor.path} for submission.descriptor_map[${descriptorIndex}] returned multiple entires. This is probably because the submission uses '$' to reference the presentation, while an array was used (thus all presentations are selected). Make sure the submission uses the correct path.`,
});
continue;
}
if (!matchingVp) {
result.areRequiredCredentialsPresent = ConstraintUtils_1.Status.ERROR;
(_f = result.errors) === null || _f === void 0 ? void 0 : _f.push({
status: ConstraintUtils_1.Status.ERROR,
tag: 'SubmissionPathNotFound',
message: `Extraction of path ${descriptor.path} for submission.descriptor_map[${descriptorIndex}] succeeded, but the value was undefined.`,
});
continue;
}
if (matchingVp.format !== descriptor.format) {
result.areRequiredCredentialsPresent = ConstraintUtils_1.Status.ERROR;
(_g = result.errors) === null || _g === void 0 ? void 0 : _g.push({
status: ConstraintUtils_1.Status.ERROR,
tag: 'SubmissionFormatNoMatch',
message: `The VP at path ${descriptor.path} does not match the required format ${descriptor.format}`,
});
continue;
}
}
else {
// When submission location is PRESENTATION, assume a single VP
matchingVp = Array.isArray(wvps) ? wvps[0] : wvps;
}
let vc;
let vcPath = `presentation ${descriptor.path}`;
if (presentationSubmissionLocation === signing_1.PresentationSubmissionLocation.EXTERNAL) {
if (descriptor.path_nested) {
const extractionResult = this.extractWrappedVcFromWrappedVp(descriptor.path_nested, descriptorIndex.toString(), matchingVp);
if (extractionResult.error) {
result.areRequiredCredentialsPresent = ConstraintUtils_1.Status.ERROR;
(_h = result.errors) === null || _h === void 0 ? void 0 : _h.push(extractionResult.error);
continue;
}
vc = extractionResult.wvc;
vcPath += ` with nested credential ${descriptor.path_nested.path}`;
}
else if (descriptor.format === 'vc+sd-jwt') {
if (!matchingVp.vcs || !matchingVp.vcs.length) {
result.areRequiredCredentialsPresent = ConstraintUtils_1.Status.ERROR;
(_j = result.errors) === null || _j === void 0 ? void 0 : _j.push({
status: ConstraintUtils_1.Status.ERROR,
tag: 'NoCredentialsFound',
message: `No credentials found in VP at path ${descriptor.path}`,
});
continue;
}
vc = matchingVp.vcs[0];
}
else if (descriptor.format === 'mso_mdoc') {
// We already know the format is mso_mdoc so this cast is safe
const vcs = matchingVp.vcs;
vcPath += ` with nested mdoc with doctype ${descriptor.id}`;
const matchingVc = vcs.find((vc) => descriptor.id === vc.credential.docType.asStr);
if (!matchingVc) {
const allDoctypes = vcs.map((vc) => `'${vc.credential.docType.asStr}'`).join(', ');
result.areRequiredCredentialsPresent = ConstraintUtils_1.Status.ERROR;
(_k = result.errors) === null || _k === void 0 ? void 0 : _k.push({
status: ConstraintUtils_1.Status.ERROR,
tag: 'NoCredentialsFound',
message: `No mdoc credential with doctype '${descriptor.id}' found in mdoc vp. Available documents are ${allDoctypes}`,
});
continue;
}
vc = matchingVc;
}
else {
result.areRequiredCredentialsPresent = ConstraintUtils_1.Status.ERROR;
(_l = result.errors) === null || _l === void 0 ? void 0 : _l.push({
status: ConstraintUtils_1.Status.ERROR,
tag: 'UnsupportedFormat',
message: `VP format ${matchingVp.format} is not supported`,
});
continue;
}
}
else {
const extractionResult = this.extractWrappedVcFromWrappedVp(descriptor, descriptorIndex.toString(), matchingVp);
if (extractionResult.error) {
result.areRequiredCredentialsPresent = ConstraintUtils_1.Status.ERROR;
(_m = result.errors) === null || _m === void 0 ? void 0 : _m.push(extractionResult.error);
continue;
}
vc = extractionResult.wvc;
vcPath = `credential ${descriptor.path}`;
}
// TODO: we should probably add support for holder dids in the kb-jwt of an SD-JWT. We can extract this from the
// `wrappedPresentation.original.compactKbJwt`, but as HAIP doesn't use dids, we'll leave it for now.
// Determine holder DIDs
const holderDIDs = ssi_types_1.CredentialMapper.isW3cPresentation(matchingVp.presentation) && matchingVp.presentation.holder
? [matchingVp.presentation.holder]
: (opts === null || opts === void 0 ? void 0 : opts.holderDIDs) || [];
if (pd.input_descriptors.findIndex((_id) => _id.id === descriptor.id) === -1) {
result.areRequiredCredentialsPresent = ConstraintUtils_1.Status.ERROR;
(_o = result.errors) === null || _o === void 0 ? void 0 : _o.push({
status: ConstraintUtils_1.Status.ERROR,
tag: 'SubmissionInputDescriptorIdNotFound',
message: `Submission references descriptor id '${descriptor.id}' but presentation definition with id '${pd.id}' does not have an input descriptor with this id. Available input descriptors are ${pd.input_descriptors.map((i) => `'${i.id}'`).join(', ')}`,
});
}
else {
// Get the presentation definition specific to the current descriptor
const pdForDescriptor = this.internalPresentationDefinitionForDescriptor(pd, descriptor.id);
// Reset and configure the evaluation client on each iteration
this._client = new evaluationClient_1.EvaluationClient();
this._client.evaluate(pdForDescriptor, [vc], Object.assign(Object.assign({}, opts), { holderDIDs, presentationSubmission: undefined, generatePresentationSubmission: undefined }));
// Check if the evaluation resulted in exactly one descriptor map entry
if (this._client.presentationSubmission.descriptor_map.length !== 1) {
const submissionDescriptor = `submission.descriptor_map[${descriptorIndex}]`;
result.areRequiredCredentialsPresent = ConstraintUtils_1.Status.ERROR;
(_p = result.errors) === null || _p === void 0 ? void 0 : _p.push(...this.formatNotInfo(ConstraintUtils_1.Status.ERROR, submissionDescriptor, vcPath));
(_q = result.warnings) === null || _q === void 0 ? void 0 : _q.push(...this.formatNotInfo(ConstraintUtils_1.Status.WARN, submissionDescriptor, vcPath));
}
}
}
// Output submission is same as input presentation submission, it's just that if it doesn't match, we return Error.
const submissionAgainstDefinitionResult = this.validateIfSubmissionSatisfiesDefinition(pd, submission);
if (!submissionAgainstDefinitionResult.doesSubmissionSatisfyDefinition) {
(_r = result.errors) === null || _r === void 0 ? void 0 : _r.push({
status: ConstraintUtils_1.Status.ERROR,
tag: 'SubmissionDoesNotSatisfyDefinition',
// TODO: it would be nice to add the nested errors here for beter understanding WHY the submission
// does not satisfy the definition, as we have that info, but we can only include one message here
message: submissionAgainstDefinitionResult.error,
});
result.areRequiredCredentialsPresent = ConstraintUtils_1.Status.ERROR;
}
return result;
}
checkIfSubmissionSatisfiesSubmissionRequirement(pd, submission, submissionRequirement, submissionRequirementName) {
if ((submissionRequirement.from && submissionRequirement.from_nested) || (!submissionRequirement.from && !submissionRequirement.from_nested)) {
return {
isSubmissionRequirementSatisfied: false,
totalMatches: 0,
errors: [
`Either 'from' OR 'from_nested' MUST be present on submission requirement ${submissionRequirementName}, but not neither and not both`,
],
};
}
const result = {
isSubmissionRequirementSatisfied: false,
totalMatches: 0,
maxRequiredMatches: submissionRequirement.rule === pex_models_1.Rules.Pick ? submissionRequirement.max : undefined,
minRequiredMatches: submissionRequirement.rule === pex_models_1.Rules.Pick ? submissionRequirement.min : undefined,
errors: [],
};
// Populate from_nested requirements
if (submissionRequirement.from_nested) {
const nestedResults = submissionRequirement.from_nested.map((nestedSubmissionRequirement, index) => this.checkIfSubmissionSatisfiesSubmissionRequirement(pd, submission, nestedSubmissionRequirement, `${submissionRequirementName}.from_nested[${index}]`));
result.totalRequiredMatches = submissionRequirement.rule === pex_models_1.Rules.All ? submissionRequirement.from_nested.length : submissionRequirement.count;
result.totalMatches = nestedResults.filter((n) => n.isSubmissionRequirementSatisfied).length;
result.nested = nestedResults;
}
// Populate from requirements
if (submissionRequirement.from) {
const inputDescriptorsForGroup = pd.input_descriptors.filter((descriptor) => { var _a; return (_a = descriptor.group) === null || _a === void 0 ? void 0 : _a.includes(submissionRequirement.from); });
const descriptorIdsInSubmission = submission.descriptor_map.map((descriptor) => descriptor.id);
const inputDescriptorsInSubmission = inputDescriptorsForGroup.filter((inputDescriptor) => descriptorIdsInSubmission.includes(inputDescriptor.id));
result.totalMatches = inputDescriptorsInSubmission.length;
result.totalRequiredMatches = submissionRequirement.rule === pex_models_1.Rules.All ? inputDescriptorsForGroup.length : submissionRequirement.count;
}
// Validate if the min/max/count requirements are satisfied
if (result.totalRequiredMatches !== undefined && result.totalMatches !== result.totalRequiredMatches) {
result.errors.push(`Expected ${result.totalRequiredMatches} requirements to be satisfied for submission requirement ${submissionRequirementName}, but found ${result.totalMatches}`);
}
if (result.minRequiredMatches !== undefined && result.totalMatches < result.minRequiredMatches) {
result.errors.push(`Expected at least ${result.minRequiredMatches} requirements to be satisfied from submission requirement ${submissionRequirementName}, but found ${result.totalMatches}`);
}
if (result.maxRequiredMatches !== undefined && result.totalMatches > result.maxRequiredMatches) {
result.errors.push(`Expected at most ${result.maxRequiredMatches} requirements to be satisfied from submission requirement ${submissionRequirementName}, but found ${result.totalMatches}`);
}
result.isSubmissionRequirementSatisfied = result.errors.length === 0;
return result;
}
/**
* Checks whether a submission satisfies the requirements of a presentation definition
*/
validateIfSubmissionSatisfiesDefinition(pd, submission) {
const submissionDescriptorIds = submission.descriptor_map.map((descriptor) => descriptor.id);
const result = {
doesSubmissionSatisfyDefinition: false,
totalMatches: 0,
totalRequiredMatches: 0,
};
// All MUST match
if (pd.submission_requirements) {
const submissionRequirementResults = pd.submission_requirements.map((submissionRequirement, index) => this.checkIfSubmissionSatisfiesSubmissionRequirement(pd, submission, submissionRequirement, `$.submission_requirements[${index}]`));
result.totalRequiredMatches = pd.submission_requirements.length;
result.totalMatches = submissionRequirementResults.filter((r) => r.isSubmissionRequirementSatisfied).length;
result.submissionRequirementResults = submissionRequirementResults;
if (result.totalMatches !== result.totalRequiredMatches) {
result.error = `Expected all submission requirements (${result.totalRequiredMatches}) to be satisfied in submission, but found ${result.totalMatches}.`;
}
}
else {
result.totalRequiredMatches = pd.input_descriptors.length;
result.totalMatches = submissionDescriptorIds.length;
const notInSubmission = pd.input_descriptors.filter((inputDescriptor) => !submissionDescriptorIds.includes(inputDescriptor.id));
if (notInSubmission.length > 0) {
result.error = `Expected all input descriptors (${pd.input_descriptors.map((i) => `'${i.id}'`).join(', ')}) to be satisfied in submission, but found ${submissionDescriptorIds.map((i) => `'${i}'`).join(',')}. Missing ${notInSubmission.map((d) => `'${d.id}'`).join(', ')}`;
}
}
result.doesSubmissionSatisfyDefinition = result.error === undefined;
return result;
}
internalPresentationDefinitionForDescriptor(pd, descriptorId) {
const inputDescriptorIndex = pd.input_descriptors.findIndex((i) => i.id === descriptorId);
// If we receive a submission with input descriptors that do not exist
if (inputDescriptorIndex === -1) {
throw new Error(`Input descriptor with id '${descriptorId}' not found in presentation definition with id '${pd.id}'. Available input descriptors are ${pd.input_descriptors.map((i) => `'${i.id}'`).join(', ')}`);
}
if (pd instanceof types_1.InternalPresentationDefinitionV2) {
return new types_1.InternalPresentationDefinitionV2(pd.id, [pd.input_descriptors[inputDescriptorIndex]], pd.format, pd.frame, pd.name, pd.purpose,
// we ignore submission requirements as we're verifying a single input descriptor here
undefined);
}
else if (pd instanceof types_1.InternalPresentationDefinitionV1) {
return new types_1.InternalPresentationDefinitionV1(pd.id, [pd.input_descriptors[inputDescriptorIndex]], pd.format, pd.name, pd.purpose,
// we ignore submission requirements as we're verifying a single input descriptor here
undefined);
}
throw new Error('Unrecognized presentation definition instance');
}
formatNotInfo(status, descriptorPath, vcPath) {
return this._client.results
.filter((result) => result.status === status)
.map((x) => {
const _vcPath = vcPath !== null && vcPath !== void 0 ? vcPath : `$.verifiableCredential${x.verifiable_credential_path.substring(1)}`;
const _descriptorPath = descriptorPath !== null && descriptorPath !== void 0 ? descriptorPath : x.input_descriptor_path;
return {
tag: x.evaluator,
status: x.status,
message: `${x.message}: ${_descriptorPath}: ${_vcPath}`,
};
});
}
submissionFrom(pd, vcs, opts) {
if (!this._client.results || this._client.results.length === 0) {
if (vcs.length === 0) {
throw Error('The WrappedVerifiableCredentials input array is empty');
}
throw Error('You need to call evaluate() before pex.presentationFrom()');
}
if (!this._client.generatePresentationSubmission) {
return this._client.presentationSubmission;
}
if (pd.submission_requirements) {
const marked = this._client.results.filter((result) => result.evaluator === 'MarkForSubmissionEvaluation' && result.payload.group && result.status !== ConstraintUtils_1.Status.ERROR);
const [updatedMarked, upIdx] = this.matchUserSelectedVcs(marked, vcs);
const groupCount = new Map();
//TODO instanceof fails in some cases, need to check how to fix it
if ('input_descriptors' in pd) {
pd.input_descriptors.forEach((e) => {
if (e.group) {
e.group.forEach((key) => {
if (groupCount.has(key)) {
groupCount.set(key, groupCount.get(key) + 1);
}
else {
groupCount.set(key, 1);
}
});
}
});
}
const result = this.evaluateRequirements(pd.submission_requirements, updatedMarked, groupCount, 0);
const finalIdx = upIdx.filter((ui) => result[1].find((r) => r.verifiable_credential_path === ui[1]));
this.updatePresentationSubmission(finalIdx);
this.updatePresentationSubmissionPathToVpPath();
if ((opts === null || opts === void 0 ? void 0 : opts.presentationSubmissionLocation) === signing_1.PresentationSubmissionLocation.EXTERNAL) {
this.updatePresentationSubmissionToExternal();
}
return this._client.presentationSubmission;
}
const marked = this._client.results.filter((result) => result.evaluator === 'MarkForSubmissionEvaluation' && result.status !== ConstraintUtils_1.Status.ERROR);
const updatedIndexes = this.matchUserSelectedVcs(marked, vcs);
this.updatePresentationSubmission(updatedIndexes[1]);
this.updatePresentationSubmissionPathToVpPath();
if ((opts === null || opts === void 0 ? void 0 : opts.presentationSubmissionLocation) === signing_1.PresentationSubmissionLocation.EXTERNAL) {
this.updatePresentationSubmissionToExternal();
}
return this._client.presentationSubmission;
}
updatePresentationSubmission(updatedIndexes) {
if (!this._client.generatePresentationSubmission) {
return; // never update a supplied submission
}
this._client.presentationSubmission.descriptor_map = this._client.presentationSubmission.descriptor_map
.filter((descriptor) => updatedIndexes.find((ui) => ui[0] === descriptor.path))
.map((descriptor) => {
const result = updatedIndexes.find((ui) => ui[0] === descriptor.path);
if (result) {
descriptor.path = result[1];
}
return descriptor;
});
}
updatePresentationSubmissionToExternal(presentationSubmission) {
var _a;
const descriptors = (_a = presentationSubmission === null || presentationSubmission === void 0 ? void 0 : presentationSubmission.descriptor_map) !== null && _a !== void 0 ? _a : this._client.presentationSubmission.descriptor_map;
// Get all VCs to check if they should be in separate VPs
const vcs = this._client.wrappedVcs.map((wvc) => wvc.original);
const useMultipleVPs = !PEX_1.PEX.allowMultipleVCsPerPresentation(vcs) && this._client.wrappedVcs.length > 1;
const updatedDescriptors = descriptors.map((d, index) => {
// If using multiple VPs, update path to include VP index
if (useMultipleVPs) {
return this.updateDescriptorToExternal(d, { vpIndex: index, vcIndex: 0 });
}
return this.updateDescriptorToExternal(d);
});
if (presentationSubmission) {
return Object.assign(Object.assign({}, presentationSubmission), { descriptor_map: updatedDescriptors });
}
this._client.presentationSubmission.descriptor_map = updatedDescriptors;
return this._client.presentationSubmission;
}
updateDescriptorToExternal(descriptor, { vpIndex, vcIndex, } = {}) {
if (descriptor.path_nested) {
return descriptor;
}
const { nestedCredentialPath, vpFormat } = (0, formatMap_1.getVpFormatForVcFormat)(descriptor.format);
const newDescriptor = Object.assign(Object.assign({}, descriptor), { format: vpFormat, path: vpIndex !== undefined ? `$[${vpIndex}]` : '$' });
if (nestedCredentialPath) {
newDescriptor.path_nested = Object.assign(Object.assign({}, descriptor), { path: vcIndex !== undefined
? `${nestedCredentialPath}[${vcIndex}]`
: descriptor.path.replace('$.verifiableCredential', nestedCredentialPath).replace('$[', `${nestedCredentialPath}[`) });
}
return newDescriptor;
}
matchUserSelectedVcs(marked, vcs) {
const userSelected = vcs.map((vc, index) => [index, JSON.stringify(vc.original)]);
const allCredentials = this._client.wrappedVcs.map((vc, index) => [index, JSON.stringify(vc.original)]);
const updatedIndexes = [];
userSelected.forEach((us, i) => {
allCredentials.forEach((ac, j) => {
if (ac[1] === us[1]) {
updatedIndexes.push([`$[${j}]`, `$[${i}]`]);
}
});
});
marked = marked
.filter((m) => updatedIndexes.find((ui) => ui[0] === m.verifiable_credential_path))
.map((m) => {
const index = updatedIndexes.find((ui) => ui[0] === m.verifiable_credential_path);
if (index) {
m.verifiable_credential_path = index[1];
}
return m;
});
return [marked, updatedIndexes];
}
evaluateRequirements(submissionRequirement, marked, groupCount, level) {
let total = 0;
const result = [];
for (const sr of submissionRequirement) {
if (sr.from) {
if (sr.rule === pex_models_1.Rules.All) {
const [count, matched] = this.countMatchingInputDescriptors(sr, marked);
if (count !== (groupCount.get(sr.from) || 0)) {
throw Error(`Not all input descriptors are members of group ${sr.from}`);
}
total++;
result.push(...matched);
}
else if (sr.rule === pex_models_1.Rules.Pick) {
const [count, matched] = this.countMatchingInputDescriptors(sr, marked);
try {
this.handleCount(sr, count, level);
total++;
}
catch (error) {
if (level === 0)
throw error;
}
result.push(...matched);
}
}
else if (sr.from_nested) {
const [count, matched] = this.evaluateRequirements(sr.from_nested, marked, groupCount, ++level);
total += count;
result.push(...matched);
this.handleCount(sr, count, level);
}
}
return [total, result];
}
countMatchingInputDescriptors(submissionRequirement, marked) {
let count = 0;
const matched = [];
for (const m of marked) {
if (m.payload.group.includes(submissionRequirement.from)) {
matched.push(m);
count++;
}
}
return [count, matched];
}
handleCount(submissionRequirement, count, level) {
if (submissionRequirement.count) {
if (count !== submissionRequirement.count) {
throw Error(`Count: expected: ${submissionRequirement.count} actual: ${count} at level: ${level}`);
}
}
if (submissionRequirement.min) {
if (count < submissionRequirement.min) {
throw Error(`Min: expected: ${submissionRequirement.min} actual: ${count} at level: ${level}`);
}
}
if (submissionRequirement.max) {
if (count > submissionRequirement.max) {
throw Error(`Max: expected: ${submissionRequirement.max} actual: ${count} at level: ${level}`);
}
}
}
removeDuplicateSubmissionRequirementMatches(matches) {
return matches.filter((match, index) => {
const _match = JSON.stringify(match);
return (index ===
matches.findIndex((obj) => {
return JSON.stringify(obj) === _match;
}));