Skip to content

Commit

Permalink
docs(changeset): feat: presentation matches
Browse files Browse the repository at this point in the history
  • Loading branch information
auer-martin committed Nov 21, 2024
1 parent b44a33c commit ba6c09b
Show file tree
Hide file tree
Showing 9 changed files with 200 additions and 33 deletions.
5 changes: 5 additions & 0 deletions .changeset/stale-days-call.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'dcql': patch
---

feat: presentation matches
45 changes: 25 additions & 20 deletions dcql/src/dcql-presentation/m-dcql-presentation-record.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import * as v from 'valibot';
import { performDcqlQuery } from '../dcql-query/dcql-query.js';
import type { DcqlQuery } from '../dcql-query/m-dcql-query.js';
import {
DcqlEmptyPresentationRecordError as DcqlEmptyPresentationRecord,
DcqlInvalidPresentationRecordError,
} from '../e-dcql.js';
import type { DcqlCredentialRepresentation } from '../u-dcql-credential-representation.js';
import { DcqlInvalidPresentationRecordError } from '../e-dcql.js';
import type { DcqlPresentationRepresentation } from '../u-dcql-credential-representation.js';
import { idRegex, vJsonRecord, vStringToJson } from '../u-dcql.js';
import { DcqlPresentationQueryResult } from './m-dcql-valid-dcql-query-result.js';

export namespace DcqlPresentationRecord {
export const vModel = v.record(
Expand All @@ -29,34 +27,41 @@ export namespace DcqlPresentationRecord {
};

/**
* Todo: Do the mapping from record to credential internally via a callback
* Query if the presentation record can be satisfied by the provided presentations
* considering the dcql query
*
* @param dcqlQuery
* @param credentials
* @param presentations
*/
export const validate = (
credentials: DcqlCredentialRepresentation[],
ctx: {
dcqlQuery: DcqlQuery;
}
) => {
export const query = (
presentations: DcqlPresentationRepresentation[],
ctx: { dcqlQuery: DcqlQuery }
): DcqlPresentationQueryResult.UnknownResult => {
const { dcqlQuery } = ctx;
if (Object.values(credentials).length === 0) {
throw new DcqlEmptyPresentationRecord({
message: 'No credentials provided',
});
}

const result = performDcqlQuery(dcqlQuery, {
credentials,
credentials: presentations,
presentation: true,
});

if (!result.canBeSatisfied) {
return { ...result, canBeSatisfied: false };
}

return DcqlPresentationQueryResult.fromDcqlQueryResult(result);
};

export const validate = (
dcqlQueryResult: DcqlPresentationQueryResult.UnknownResult
): DcqlPresentationQueryResult => {
if (!dcqlQueryResult.canBeSatisfied) {
throw new DcqlInvalidPresentationRecordError({
message: 'Invalid Presentation record',
cause: result,
cause: dcqlQueryResult,
});
}

return dcqlQueryResult;
};
}
export type DcqlPresentationRecord = DcqlPresentationRecord.Output;
106 changes: 106 additions & 0 deletions dcql/src/dcql-presentation/m-dcql-valid-dcql-query-result.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import * as v from 'valibot';

import { DcqlQueryResult } from '../dcql-query-result/m-dcql-query-result.js';
import { DcqlInvalidPresentationRecordError } from '../e-dcql.js';
import { idRegex } from '../u-dcql.js';

export namespace DcqlPresentationQueryResult {
export const vModel = v.object({
...DcqlQueryResult.vModel.entries,
canBeSatisfied: v.literal(true),
presentation_matches: v.record(
v.pipe(v.string(), v.regex(idRegex)),
v.object({
...v.omit(
DcqlQueryResult.vModel.entries.credential_matches.value.options[0],
['all', 'issues', 'credential_index']
).entries,
presentation_index: v.number(),
})
),
});

export type Input = v.InferInput<typeof vModel>;
export type Output = v.InferOutput<typeof vModel>;

export type UnknownResult =
| (DcqlQueryResult & {
canBeSatisfied: false;
presentation_matches?: undefined;
})
| DcqlPresentationQueryResult;

export const parse = (input: Input | DcqlQueryResult) => {
return v.parse(vModel, input);
};

export const fromDcqlQueryResult = (
dcqlQueryResult: DcqlQueryResult
): DcqlPresentationQueryResult => {
const { canBeSatisfied } = dcqlQueryResult;
if (!canBeSatisfied) {
throw new DcqlInvalidPresentationRecordError({
message: 'Invalid Presentation record',
cause: dcqlQueryResult,
});
}

const presentation_matches: DcqlPresentationQueryResult['presentation_matches'] =
{};

if (!dcqlQueryResult.credential_sets) {
for (const credentialQueryId of dcqlQueryResult.credentials.map(
c => c.id
)) {
const match = dcqlQueryResult.credential_matches[credentialQueryId];
if (!match?.success) {
throw new DcqlInvalidPresentationRecordError({
message: `Credential query ${credentialQueryId} is required but not satisfied.`,
});
}

const { all, issues, credential_index, ...rest } = match;
presentation_matches[credentialQueryId] = {
...rest,
presentation_index: credential_index,
};
}

return {
...dcqlQueryResult,
canBeSatisfied,
presentation_matches,
};
}

for (const credentialSet of dcqlQueryResult.credential_sets ?? []) {
const matchingOption = credentialSet.matching_options?.find(Boolean);
if (!matchingOption) throw new Error('Invalid matching option');

for (const credentialQueryId of matchingOption) {
const match = dcqlQueryResult.credential_matches[credentialQueryId];
if (match?.success) {
const { all, issues, credential_index, ...rest } = match;
presentation_matches[credentialQueryId] = {
...rest,
presentation_index: credential_index,
};
continue;
}

if (credentialSet.required) {
throw new DcqlInvalidPresentationRecordError({
message: `Credential query ${credentialQueryId} is required but not satisfied.`,
});
}
}
}

return {
...dcqlQueryResult,
canBeSatisfied,
presentation_matches,
};
};
}
export type DcqlPresentationQueryResult = DcqlPresentationQueryResult.Output;
2 changes: 1 addition & 1 deletion dcql/src/dcql-query-result/dcql-claims-query-result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ const getMdocCredentialParser = (
};

const getJsonCredentialParser = (
credentialQuery: DcqlCredentialQuery.SdJwtVc | DcqlCredentialQuery.W3c,
credentialQuery: DcqlCredentialQuery.SdJwtVc | DcqlCredentialQuery.W3cVc,
ctx: {
claimSet?: NonNullable<DcqlCredentialQuery['claim_sets']>[number];
presentation: boolean;
Expand Down
57 changes: 54 additions & 3 deletions dcql/src/dcql-query/dcql-query.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,26 @@ void describe('credential-parser', () => {
},
});

DcqlPresentationRecord.validate(
const presentationQueryResult = DcqlPresentationRecord.query(
[res.credential_matches.my_credential.output],
{ dcqlQuery: query }
);

assert.deepStrictEqual(presentationQueryResult.presentation_matches, {
my_credential: {
success: true,
typed: true,
presentation_index: 0,
claim_set_index: undefined,
output: {
docType: 'org.iso.7367.1.mVRC',
namespaces: {
'org.iso.7367.1': { vehicle_holder: 'Martin Auer' },
'org.iso.18013.5.1': { first_name: 'Martin Auer' },
},
},
},
});
});

void it('mdocMvrc example with multiple credentials succeeds', _t => {
Expand Down Expand Up @@ -154,10 +170,26 @@ void describe('credential-parser', () => {
},
});

DcqlPresentationRecord.validate(
const presentationQueryResult = DcqlPresentationRecord.query(
[res.credential_matches.my_credential.output],
{ dcqlQuery: query }
);

assert.deepStrictEqual(presentationQueryResult.presentation_matches, {
my_credential: {
success: true,
typed: true,
presentation_index: 0,
claim_set_index: undefined,
output: {
docType: 'org.iso.7367.1.mVRC',
namespaces: {
'org.iso.7367.1': { vehicle_holder: 'Martin Auer' },
'org.iso.18013.5.1': { first_name: 'Martin Auer' },
},
},
},
});
});

void it('sdJwtVc example with multiple credentials succeeds', _t => {
Expand Down Expand Up @@ -188,9 +220,28 @@ void describe('credential-parser', () => {
},
});

DcqlPresentationRecord.validate(
const presentationQueryResult = DcqlPresentationRecord.query(
[res.credential_matches.my_credential.output],
{ dcqlQuery: query }
);

assert.deepStrictEqual(presentationQueryResult.presentation_matches, {
my_credential: {
success: true,
typed: true,
presentation_index: 0,
claim_set_index: undefined,
output: {
vct: 'https://credentials.example.com/identity_credential',
claims: {
first_name: 'Arthur',
last_name: 'Dent',
address: {
street_address: '42 Market Street',
},
},
},
},
});
});
});
6 changes: 3 additions & 3 deletions dcql/src/dcql-query/m-dcql-credential-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,16 +94,16 @@ export namespace DcqlCredentialQuery {
});
export type SdJwtVc = v.InferOutput<typeof vSdJwtVc>;

export const vW3c = v.object({
export const vW3cVc = v.object({
...vBase.entries,
format: v.picklist(['jwt_vc_json', 'jwt_vc_json-ld']),
claims: v.optional(
v.pipe(v.array(DcqlClaimsQuery.vW3cSdJwtVc), vNonEmptyArray())
),
});
export type W3c = v.InferOutput<typeof vW3c>;
export type W3cVc = v.InferOutput<typeof vW3cVc>;

export const vModel = v.variant('format', [vMdoc, vSdJwtVc, vW3c]);
export const vModel = v.variant('format', [vMdoc, vSdJwtVc, vW3cVc]);
export type Input = v.InferInput<typeof vModel>;
export type Output = v.InferOutput<typeof vModel>;

Expand Down
6 changes: 0 additions & 6 deletions dcql/src/e-dcql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,6 @@ export class DcqlInvalidClaimsQueryIdError extends DcqlError {
}
}

export class DcqlEmptyPresentationRecordError extends DcqlError {
constructor(opts: { message: string; cause?: unknown }) {
super({ code: 'BAD_REQUEST', ...opts });
}
}

export class DcqlMissingClaimSetParseError extends DcqlError {
constructor(opts: { message: string; cause?: unknown }) {
super({ code: 'BAD_REQUEST', ...opts });
Expand Down
1 change: 1 addition & 0 deletions dcql/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export * from './e-dcql.js';
export type {
DcqlCredentialRepresentation,
DcqlMdocRepresentation,
DcqlPresentationRepresentation,
DcqlSdJwtVcRepresentation,
DcqlW3cVcRepresentation,
} from './u-dcql-credential-representation.js';
5 changes: 5 additions & 0 deletions dcql/src/u-dcql-credential-representation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,8 @@ export type DcqlCredentialRepresentation =
| DcqlMdocRepresentation
| DcqlSdJwtVcRepresentation
| DcqlW3cVcRepresentation;

export type DcqlPresentationRepresentation =
| DcqlMdocRepresentation
| DcqlSdJwtVcRepresentation
| DcqlW3cVcRepresentation;

0 comments on commit ba6c09b

Please sign in to comment.