diff --git a/package.json b/package.json index a1b382d..bbec26d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@dcx-protocol/root", - "version": "5.0.1", + "version": "7.0.0", "description": "DCX: Decentralized Credential Exchange. DWN protocol for verifiable credential exchange.", "type": "module", "workspaces": [ @@ -80,4 +80,4 @@ "dependencies": { "typescript": "^5.5.4" } -} \ No newline at end of file +} diff --git a/packages/applicant/package.json b/packages/applicant/package.json index acee059..b42e0cb 100644 --- a/packages/applicant/package.json +++ b/packages/applicant/package.json @@ -1,6 +1,6 @@ { "name": "@dcx-protocol/applicant", - "version": "6.0.0", + "version": "7.0.0", "description": "DCX Applicant protocol and server", "type": "module", "main": "./dist/cjs/index.js", diff --git a/packages/applicant/src/dcx-applicant.ts b/packages/applicant/src/applicant.ts similarity index 66% rename from packages/applicant/src/dcx-applicant.ts rename to packages/applicant/src/applicant.ts index a63c191..4d565fb 100644 --- a/packages/applicant/src/dcx-applicant.ts +++ b/packages/applicant/src/applicant.ts @@ -1,43 +1,59 @@ import { - CreateCredentialApplicationParams, + applicationSchema, CredentialApplication, + CredentialApplicationVP, + CredentialManifest, DcxAgentRecovery, DcxDwnError, DcxError, DcxManager, DcxManagerStatus, - DcxValidated, DwnError, DwnUtils, - GetManifestsResponse, + Format, Logger, manifestSchema, OptionsUtil, - PresentationExchangeParams, + PresentationDefinition, PresentationSubmission, RecordCreateParams, - RecordReadParams, + RecordResponse, RecordsCreateParams, RecordsParams, RecordsQueryParams, RecordsQueryResponse, RecordsReadParams, RecordsReadResponse, - TrustedIssuer, - ValidateApplicationParams, - ValidateVerifiablePresentationResponse + TrustedIssuer } from '@dcx-protocol/common'; -import { Web5PlatformAgent } from '@web5/agent'; -import { - ProtocolsConfigureResponse, - ProtocolsQueryResponse, - Record, - Web5 -} from '@web5/api'; -import { PresentationExchange, VerifiablePresentation } from '@web5/credentials'; -import { ApplicantConfig, applicantConfig } from './dcx-applicant-config.js'; -import { dcxApplicant } from './index.js'; - +import { DwnResponseStatus, Web5PlatformAgent } from '@web5/agent'; +import { ProtocolsConfigureResponse, ProtocolsQueryResponse, Record, Web5 } from '@web5/api'; +import { PresentationDefinitionV2, PresentationExchange } from '@web5/credentials'; +import { ApplicantConfig, applicantConfig } from './config.js'; +import { applicant } from './index.js'; + +export type GetManifestsResponse = { manifests: CredentialManifest[] }; +export type ValidateVerifiablePresentationResponse = { + areRequiredCredentialsPresent: 'info' | 'warn' | 'error'; + verifiableCredential: Array; +}; +export type DcxValidated = { + tag: string; + status: string; + message: string +}; +export type ValidateApplicationParams = { + presentation: any; + presentationDefinition: PresentationDefinitionV2; +}; +export type CreateApplicationParams = { + id?: string; + spec_version?: string; + applicant?: string; + manifest_id: string; + format?: Format; + presentation_submission: PresentationSubmission; +}; /** * DcxApplicant is the core class for the applicant side of the DCX protocol. * It handles the credential issuance, verification, selection, as well as @@ -75,9 +91,10 @@ export class DcxApplicant implements DcxManager { public async queryProtocols(): Promise { // Query DWN for credential-applicant protocol const { status: query, protocols = [] } = await this.web5.dwn.protocols.query({ + from : this.did, message : { filter : { - protocol : dcxApplicant.protocol, + protocol : applicant.protocol, }, }, }); @@ -98,7 +115,7 @@ export class DcxApplicant implements DcxManager { */ public async configureProtocols(): Promise { const { status: configure, protocol } = await this.web5.dwn.protocols.configure({ - message : { definition: dcxApplicant }, + message : { definition: applicant }, }); if (DwnUtils.isFailure(configure.code) || !protocol) { @@ -108,7 +125,6 @@ export class DcxApplicant implements DcxManager { } const { status: send } = await protocol.send(this.did); - if (DwnUtils.isFailure(send.code)) { const { code, detail } = send; Logger.error('DWN protocols send failed', send); @@ -119,46 +135,8 @@ export class DcxApplicant implements DcxManager { return { status: send, protocol }; } - public async readRecord({ record }: RecordReadParams): Promise { - throw new DcxError('Method not implemented.', record); - } - - public async readApplicationResponseRecords( - { records: manifestRecords }: RecordsParams - ): Promise { - const records = await Promise.all( - manifestRecords.map(async (manifestRecord: Record) => { - const { record: read } = await this.web5.dwn.records.read({ - from : manifestRecord.author, - message : { - filter : { - recordId : manifestRecord.id, - }, - }, - }); - return read.data.json(); - }), - ); - return { records }; - } - - public async readManifestRecords( - { records: manifestRecords }: RecordsParams - ): Promise { - const records = await Promise.all( - manifestRecords.map(async (manifestRecord: Record) => { - const { record: read } = await this.web5.dwn.records.read({ - from : manifestRecord.author, - message : { - filter : { - recordId : manifestRecord.id, - }, - }, - }); - return read.data.json(); - }), - ); - return { records }; + public async readRecord(): Promise { + throw new DcxError('Method not implemented'); } /** @@ -180,7 +158,7 @@ export class DcxApplicant implements DcxManager { return read.data.json(); }), ); - return { records: reads }; + return { reads }; } /** @@ -193,7 +171,7 @@ export class DcxApplicant implements DcxManager { filter : { protocolPath, schema, - protocol : dcxApplicant.protocol, + protocol : applicant.protocol, dataFormat : 'application/json', }, ...options @@ -217,7 +195,7 @@ export class DcxApplicant implements DcxManager { from, message : { filter : { - protocol : dcxApplicant.protocol, + protocol : applicant.protocol, protocolPath : 'manifest', schema : manifestSchema.$id, dataFormat : 'application/json', @@ -234,62 +212,81 @@ export class DcxApplicant implements DcxManager { return { status, records, cursor }; } - public async createVerifiablePresentation( - { vcJwts, presentationDefinition }: PresentationExchangeParams - ): Promise<{vp: VerifiablePresentation}> { - const { presentationSubmission } = PresentationExchange.createPresentationFromCredentials({ - vcJwts, - presentationDefinition - }); - Logger.log('Presentation Submission', presentationSubmission); - const vp = await VerifiablePresentation.create({ - holder : this.did, - vcJwts : vcJwts, - additionalData : { presentationSubmission } - }); - Logger.log('Verifiable Presentation', vp); - return { vp }; + public async readManifestRecords( + { records: manifestRecords }: RecordsParams + ): Promise { + const records = await Promise.all( + manifestRecords.map(async (manifestRecord: Record) => { + const { record: read } = await this.web5.dwn.records.read({ + from : manifestRecord.author, + message : { + filter : { + recordId : manifestRecord.id, + }, + }, + }); + return read.data.json(); + }), + ); + return { records }; } - public async createCredentialApplication( - { presentationSubmission, manifestId }: CreateCredentialApplicationParams - ): Promise { - const app = { - id : crypto.randomUUID(), - spec_version : 'https://identity.foundation/credential-manifest/#versioning', - applicant : this.did, - manifest_id : manifestId, - format : { jwt_vc: { alg: ['EdDSA'] }}, - presentation_submission : presentationSubmission, - }; - return new CredentialApplication( - app.id, - app.spec_version, - app.applicant, - app.manifest_id, - app.format, - app.presentation_submission + public async readApplicationResponseRecords( + { records: manifestRecords }: RecordsParams + ): Promise { + const records = await Promise.all( + manifestRecords.map(async (manifestRecord: Record) => { + const { record: read } = await this.web5.dwn.records.read({ + from : manifestRecord.author, + message : { + filter : { + recordId : manifestRecord.id, + }, + }, + }); + return read.data.json(); + }), ); + return { records }; } - public validatePresentationSubmission(presentationSubmission: PresentationSubmission): DcxValidated { - const validation = PresentationExchange.validateSubmission({ presentationSubmission }) as DcxValidated[]; - Logger.log('Presentation Submission Validation', validation); - const { tag, status, message } = validation?.[0]; - Logger.log('Presentation Submission Checked: tag, status, message', tag, status, message); - return { tag, status, message }; + public createPresentationSubmission({ vcJwts, definition }: { vcJwts: string[], definition: PresentationDefinition }): PresentationSubmission { + return PresentationExchange.createPresentationFromCredentials({ vcJwts, presentationDefinition: definition })?.presentationSubmission; } + public createCredentialApplication({ manifest, presentation_submission }: { + manifest: CredentialManifest; + presentation_submission: PresentationSubmission + },): CredentialApplication { + return { + id : crypto.randomUUID(), + spec_version : 'https://identity.foundation/credential-manifest/spec/v1.0.0/', + applicant : this.did, + manifest_id : manifest.id, + format : { jwt: { alg: ['EdDSA'] }}, + presentation_submission + }; + } - public validateVerifiablePresentation( - { presentationDefinition, presentation }: ValidateApplicationParams - ): ValidateVerifiablePresentationResponse { - const validation = PresentationExchange.evaluatePresentation({ presentationDefinition, presentation }); - Logger.log('Verifiable Presentation Validation', validation); - const { areRequiredCredentialsPresent, verifiableCredential } = validation; - Logger.log('Are required credentials present?', areRequiredCredentialsPresent); - Logger.log('Verifiable Credentials', verifiableCredential); - return { areRequiredCredentialsPresent, verifiableCredential }; + public createCredentialApplicationVP({ manifest, verifiableCredential, submission }: { + manifest: CredentialManifest, + verifiableCredential: string[], + submission?: PresentationSubmission + }): CredentialApplicationVP { + submission ??= this.createPresentationSubmission({ vcJwts: verifiableCredential, definition: manifest.presentation_definition }); + return { + '@context' : ['https://www.w3.org/2018/credentials/v1', 'https://identity.foundation/credential-manifest/response/v1'], + 'type' : ['VerifiablePresentation', 'CredentialResponse'], + credential_application : { + id : crypto.randomUUID(), + spec_version : 'https://identity.foundation/credential-manifest/spec/v1.0.0/', + applicant : this.did, + manifest_id : manifest.id, + format : { jwt: { alg: ['EdDSA'] }}, + presentation_submission : submission, + }, + verifiableCredential + }; } public async createRecords({ data: creates, protocolPath, schema }: RecordsCreateParams): Promise<{records: Record[]}>{ @@ -309,7 +306,7 @@ export class DcxApplicant implements DcxManager { schema, protocolPath, dataFormat : 'application/json', - protocol : dcxApplicant.protocol, + protocol : applicant.protocol, }, }); @@ -322,30 +319,67 @@ export class DcxApplicant implements DcxManager { if (!record) { throw new DcxDwnError(`Record not returned from create: ${code} - ${detail}`); } + Logger.debug('Created application record in local dwn', status); - const { status: applicant } = await record.send(); - if (DwnUtils.isFailure(applicant.code)) { - const { code, detail } = applicant; - Logger.error('Failed to send record to applicant dwn', applicant); + const { status: applicantSend } = await record.send(); + if (DwnUtils.isFailure(applicantSend.code)) { + const { code, detail } = applicantSend; + Logger.error('Failed to send record to applicantSend dwn', applicantSend); throw new DwnError(code, detail); } - Logger.debug('Sent application record to local dwn', applicant); + Logger.debug('Sent application record to applicant dwn', applicantSend); const manifest = OptionsUtil.findManifest({ manifests: this.config.manifests, id: data.manifest_id }); - const { id: recipient } = OptionsUtil.findIssuer({ issuers: this.config.issuers, id: manifest?.issuer.id }); + const { id: issuer } = OptionsUtil.findIssuer({ issuers: this.config.issuers, id: manifest?.issuer.id }); - const { status: issuer } = await record.send(recipient); - if (DwnUtils.isFailure(issuer.code)) { - const { code, detail } = issuer; - Logger.error('Failed to send record to issuer dwn', issuer); + const { status: issuerSend } = await record.send(issuer); + if (DwnUtils.isFailure(issuerSend.code)) { + const { code, detail } = issuerSend; + Logger.error('Failed to send record to issuer dwn', issuerSend); throw new DwnError(code, detail); } - - Logger.debug('Sent application record to remote dwn', issuer); + Logger.debug('Sent application record to issuer dwn', issuerSend); return { record }; } + public async createApplicationRecord({ application, issuer }: { + application: CredentialApplicationVP; + issuer: string + }): Promise { + const { record, status } = await this.web5.dwn.records.create({ + data : application, + store : true, + message : { + protocol : applicant.protocol, + protocolPath : 'application', + schema : applicationSchema.$id, + dataFormat : 'application/json', + }, + }); + + const { code, detail } = status; + if (DwnUtils.isFailure(status.code)) { + Logger.error('Failed to create record', status); + throw new DwnError(code, detail); + } + + if (!record) { + throw new DcxDwnError(`Record not returned from create: ${code} - ${detail}`); + } + Logger.debug('Created application record in local dwn', status); + + const { status: issuerSend } = await record.send(issuer); + if (DwnUtils.isFailure(issuerSend.code)) { + const { code, detail } = issuerSend; + Logger.error('Failed to send record to issuer dwn', issuerSend); + throw new DwnError(code, detail); + } + Logger.debug('Sent application record to issuer dwn', issuerSend); + + return { status: issuerSend, record }; + } + /** * * Get manifests by issuer name or id @@ -354,13 +388,13 @@ export class DcxApplicant implements DcxManager { * @param param.id the id of the issuer to find * @returns RecordsReadParams; see {@link RecordsReadParams} */ - public async getManifests({ name, id }: Partial): Promise { + public async getManifestRecords({ name, id }: Partial): Promise { const issuer = OptionsUtil.findIssuer({ issuers: this.config.issuers, name, id }); const { records: query } = await this.queryRecords({ from: issuer.id, protocolPath: 'manifest', schema: manifestSchema.$id }); // TODO: application/response query // const { records: query } = await this.queryRecords({ protocolPath: 'application/response', schema: responseSchema.$id, options: { author: issuer.id } }); Logger.log(`Found ${query.length} manifest records in ${issuer.name} dwn`); - const { records: manifests } = await this.readRecords({ records: query }); + const { reads: manifests } = await this.readRecords({ records: query }); Logger.log(`Read ${manifests.length} manifest records from ${issuer.name} dwn`); return { manifests }; } diff --git a/packages/applicant/src/dcx-applicant-config.ts b/packages/applicant/src/config.ts similarity index 100% rename from packages/applicant/src/dcx-applicant-config.ts rename to packages/applicant/src/config.ts diff --git a/packages/applicant/src/index.ts b/packages/applicant/src/index.ts index 0083a41..d6c8ac2 100644 --- a/packages/applicant/src/index.ts +++ b/packages/applicant/src/index.ts @@ -1,3 +1,3 @@ -export * from './dcx-applicant-config.js'; -export * from './dcx-applicant-protocol.js'; -export * from './dcx-applicant.js'; \ No newline at end of file +export * from './applicant.js'; +export * from './config.js'; +export * from './protocol.js'; \ No newline at end of file diff --git a/packages/applicant/src/dcx-applicant-protocol.ts b/packages/applicant/src/protocol.ts similarity index 94% rename from packages/applicant/src/dcx-applicant-protocol.ts rename to packages/applicant/src/protocol.ts index 3809f1d..bed697b 100644 --- a/packages/applicant/src/dcx-applicant-protocol.ts +++ b/packages/applicant/src/protocol.ts @@ -1,9 +1,9 @@ import { responseSchema, invoiceSchema, manifestSchema, applicationSchema } from '@dcx-protocol/common'; -export const dcxApplicant = { +export const applicant = { // applicant protocol is a subset of exchange protocol // used on client side to interact with applicant & issuer dwn - protocol : 'https://decentralized.cx/protocol/credential-exchange', + protocol : 'https://decentralized.cx/protocol', published : true, types : { application : { diff --git a/packages/applicant/tests/dcx-applicant.spec.ts b/packages/applicant/tests/applicant.spec.ts similarity index 98% rename from packages/applicant/tests/dcx-applicant.spec.ts rename to packages/applicant/tests/applicant.spec.ts index 6775fa3..fdb721d 100644 --- a/packages/applicant/tests/dcx-applicant.spec.ts +++ b/packages/applicant/tests/applicant.spec.ts @@ -4,8 +4,7 @@ dotenv.config({ path: '.env.test' }); import { FileSystem } from '@dcx-protocol/common'; import { Protocol, Web5 } from '@web5/api'; import { expect } from 'chai'; -import { applicantConfig } from '../src/dcx-applicant-config.js'; -import { DcxApplicant } from '../src/index.js'; +import { DcxApplicant, applicantConfig } from '../src/index.js'; import { Web5UserAgent } from '@web5/user-agent'; process.env.NODE_ENV = 'test'; diff --git a/packages/applicant/tests/dcx-applicant-config.spec.ts b/packages/applicant/tests/config.spec.ts similarity index 95% rename from packages/applicant/tests/dcx-applicant-config.spec.ts rename to packages/applicant/tests/config.spec.ts index a2c71b1..96eb792 100644 --- a/packages/applicant/tests/dcx-applicant-config.spec.ts +++ b/packages/applicant/tests/config.spec.ts @@ -2,7 +2,7 @@ import dotenv from 'dotenv'; dotenv.config({ path: '.env.test' }); import { expect } from 'chai'; -import { applicantConfig } from '../src/dcx-applicant-config.js'; +import { applicantConfig } from '../src/index.js'; process.env.NODE_ENV = 'test'; diff --git a/packages/common/package.json b/packages/common/package.json index 69bbe5d..2924c42 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -1,6 +1,6 @@ { "name": "@dcx-protocol/common", - "version": "6.0.0", + "version": "7.0.0", "description": "Common library shared by the other @dcx-protocol packages", "type": "module", "main": "./dist/cjs/index.js", diff --git a/packages/common/src/dcx-manager.ts b/packages/common/src/dcx-manager.ts index 0141772..27a50f9 100644 --- a/packages/common/src/dcx-manager.ts +++ b/packages/common/src/dcx-manager.ts @@ -12,8 +12,8 @@ import { } from './index.js'; export type DcxManagerStatus = { - setup : boolean; - initialized : boolean; + setup : boolean; + initialized : boolean; } export type InitializeParams = { diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 7330c64..3f2f40e 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -5,12 +5,13 @@ export { schema as invoiceSchema } from './schemas/invoice.js'; export { schema as manifestSchema } from './schemas/manifest.js'; export type * from './types/did.js'; +export type * from './types/dwn.js'; export type * from './types/handlers.js'; -export type * from './types/dcx.js'; export type * from './types/server.js'; export type * from './types/web5.js'; export * from './utils/cipher.js'; +export * from './utils/dcx.js'; export * from './utils/dwn.js'; export * from './utils/error.js'; export * from './utils/file-system.js'; diff --git a/packages/common/src/manifests/handshake.ts b/packages/common/src/manifests/handshake.ts index ce7a28a..179949a 100644 --- a/packages/common/src/manifests/handshake.ts +++ b/packages/common/src/manifests/handshake.ts @@ -31,7 +31,7 @@ export const DcxHandshakeManifest = { } ], format : { - jwt_vc : { + jwt : { alg : [ 'EdDSA' ] diff --git a/packages/common/src/schemas/application.ts b/packages/common/src/schemas/application.ts index 108e1e9..7a82d75 100644 --- a/packages/common/src/schemas/application.ts +++ b/packages/common/src/schemas/application.ts @@ -1,19 +1,19 @@ export type ApplicationSchema = typeof schema; export const schema = { - $id : 'https://decentralized.cx/protocol/credential-exchange/schemas/application', + $id : 'https://decentralized.cx/protocol/schemas/application', $schema : 'http://json-schema.org/draft-07/schema#', title : 'Credential Application', type : 'object', properties : { '@context' : { type : 'array', - items : { type: 'string'}, + items : { type: 'string' }, description : 'The @context of the application', }, type : { type : 'array', - items : { type: 'string'}, + items : { type: 'string' }, description : 'The type property of the application', }, credential_application : { @@ -33,11 +33,7 @@ export const schema = { type : 'string', description : 'The id of a valid Credential Manifest' }, - format : { - jwt_vc : { - alg : ['EdDSA'] - } - }, + format : { jwt_vc: { alg: ['EdDSA'] } }, presentation_submission : { type : 'object', properties : { diff --git a/packages/common/src/schemas/invoice.ts b/packages/common/src/schemas/invoice.ts index 534fc32..e9ec6d4 100644 --- a/packages/common/src/schemas/invoice.ts +++ b/packages/common/src/schemas/invoice.ts @@ -1,7 +1,7 @@ export type InvoiceSchema = typeof schema; export const schema = { - $id : 'https://decentralized.cx/protocol/credential-exchange/schemas/invoice', + $id : 'https://decentralized.cx/protocol/schemas/invoice', $schema : 'http://json-schema.org/draft-07/schema#', type : 'object', title : 'Invoice Record Schema', diff --git a/packages/common/src/schemas/manifest.ts b/packages/common/src/schemas/manifest.ts index d94ce1e..c9c6182 100644 --- a/packages/common/src/schemas/manifest.ts +++ b/packages/common/src/schemas/manifest.ts @@ -1,7 +1,7 @@ export type ManifestSchema = typeof schema; export const schema = { - $id : 'https://decentralized.cx/protocol/credential-exchange/schemas/manifest', + $id : 'https://decentralized.cx/protocol/schemas/manifest', $schema : 'http://json-schema.org/draft-07/schema', title : 'Credential Manifest Record Schema', type : 'object', diff --git a/packages/common/src/schemas/response.ts b/packages/common/src/schemas/response.ts index 38a950b..53683a9 100644 --- a/packages/common/src/schemas/response.ts +++ b/packages/common/src/schemas/response.ts @@ -1,7 +1,7 @@ export type ResponseSchema = typeof schema; export const schema = { - $id : 'https://decentralized.cx/protocol/credential-exchange/schemas/response', + $id : 'https://decentralized.cx/protocol/schemas/response', $schema : 'http://json-schema.org/draft-07/schema#', title : 'Credential Response', type : 'object', diff --git a/packages/common/src/types/dcx.ts b/packages/common/src/types/dcx.ts deleted file mode 100644 index f806240..0000000 --- a/packages/common/src/types/dcx.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { DwnPaginationCursor, DwnResponseStatus } from '@web5/agent'; -import { Record as DwnRecord } from '@web5/api'; -import { PresentationDefinitionV2, VcDataModel, VerifiableCredential } from '@web5/credentials'; -import { CredentialApplication, CredentialManifest } from '../index.js'; - -export type HandlerFunction = (...args: any[]) => any | Promise; - -export type AdditionalProperties = Record; - -export type VerifiablePresentation = { - id?: string; - spec_version?: string; - applicant?: string; - manifest_id?: string; - application_id?: string; - response?: { - id: string; - path: string; - format: string; - }; - denial?: { - reason: string; - }; -} & AdditionalProperties; - -export type VcDataRequest = undefined | { - vcs: VerifiableCredential[] -}; - -export type ManifestOutputDescriptor = { - id: string; - name: string; - schema: string; -}; - -export type Filter = { - type: string; - pattern: string; -}; - -export type Field = { - path: string[]; - filter?: Filter; -}; - -export type Constraint = { - fields: Field[]; -}; - -export type InputDescriptor = { - id: string; - purpose: string; - constraints: Constraint; -}; - -export type ManifestFormat = { - jwt_vc: { alg: string[] }; -}; -export type PresentationDefinition = { - id: string; - input_descriptors: InputDescriptor[]; -}; - -export type VerifiedCredential = { - issuer: string; - subject: string; - vc: VcDataModel; -}; - -export type PresentationExchangeParams = { - vcJwts: string[]; - presentationDefinition: PresentationDefinitionV2 -}; - -export type DcxApplicationRecordsCreateResponse = { - record: DwnRecord; - applicant: DwnResponseStatus; - issuer: DwnResponseStatus; -} -export type Descriptor = { - id: string; - format: string; - path: string; -}; - -export type PresentationSubmission = { - id: string; - definition_id: string; - descriptor_map: Descriptor[]; -}; - -export type Format = ManifestFormat; - -export type VPCredentialApplication = { - '@context': string[]; - type: string[]; - credential_application: CredentialApplication; - verifiableCredential: VerifiableCredential[]; - presentation_submission: PresentationSubmission; -}; - -export type ValidateApplicationParams = { - presentationDefinition: PresentationDefinitionV2; - presentation: any -}; - -export type DcxValidated = { - tag: string; - status: string; - message: string -}; - -export type ValidateVerifiablePresentationResponse = { - areRequiredCredentialsPresent: 'info' | 'warn' | 'error'; - verifiableCredential: Array; -}; - -export type CreateCredentialApplicationParams = { presentationSubmission: PresentationSubmission; manifestId: string; }; -export type DcxProtocolPath = 'manifest' | 'application' | 'application/response' | 'response' | any; -export type IssuerProcessRecordParams = { record: DwnRecord, manifest: CredentialManifest, providerId?: string }; -export type ApplicantProcessRecordParams = { pex: PresentationExchangeParams, recipient: string } -export type GetManifestsResponse = { manifests: CredentialManifest[] }; -/** - * Params & Response types for performing CRUD operations on a list of Records and a single Record - * - * Generic - * For performing CRUD operations (inclusive) on a list of records or a single record - * - * Specific - * For performing specifc a CRUD operation (exclusive) on a list of records or a single record - * - */ -// TODO: define CredentialResponse and CredentialInvoice types -export type DwnStatus = { code: number; detail: string; } -export type DcxDwnResponseStatus = { status?: { issuer?: DwnStatus; applicant?: DwnStatus; }} -export type RecordsDataResponse = { data: CredentialManifest[] | CredentialApplication[] | any[] }; - -// Record - CRUD -export type RecordParams = { record: DwnRecord }; -export type RecordResponse = { record: DwnRecord }; -// Record - Create -export type RecordCreateParams = { data: any; protocolPath?: DcxProtocolPath; schema: string }; -export type RecordCreateResponse = RecordResponse; -// Record - Read -export type RecordReadParams = RecordParams; -export type RecordReadResponse = { records: any[] }; -// TODO: define these types once needed -// Record - Update -export type RecordUpdateParams = {}; -export type RecordUpdateResponse = {}; -// Record - Delete -export type RecordDeleteParams = {}; -export type RecordDeleteResponse = {}; - -// Records[] - CRUD -export type RecordsParams = { records: DwnRecord[] }; -export type RecordsResponse = { records: DwnRecord[] }; -// Records[] - Create -export type RecordsCreateParams = { data: any[]; protocolPath?: DcxProtocolPath; schema: string }; -export type RecordsCreateResponse = RecordsResponse; -// Records[] - Read -export type RecordsReadParams = RecordsParams; -export type RecordsReadResponse = { records: any[] }; -// TODO: define these types once needed -// Records[] - Update -export type RecordsUpdateParams = {}; -export type RecordsUpdateResponse = {}; -// Records[] - Delete -export type RecordsDeleteParams = {}; -export type RecordsDeleteResponse = {}; - -// Records[] - Query -export type RecordsQueryParams = { from?: string; protocolPath?: DcxProtocolPath; schema?: {}; options?: any }; -export type RecordsQueryResponse = DwnResponseStatus & { records: DwnRecord[]; cursor?: DwnPaginationCursor }; - -// Records[] - Filter -export type RecordsFilterParams = { records: CredentialManifest[]; type: 'manifests'}; -export type RecordsFilterResponse = { data: CredentialManifest[] }; - -export type VerifyCredentialsParams = { - vcJwts: string[]; - manifest: CredentialManifest; - subjectDid: string; -} -export type SelectCredentialsParams = { - vp: VerifiablePresentation; - manifest: CredentialManifest; -} -export type CreateCredentialParams = { - data: any, - subjectDid: string, - manifest: CredentialManifest, -} -export type Fulfillment = { fulfillment: { descriptor_map: DescriptorMap; } }; -export type DescriptorMap = { - id?: string; - format?: string; - path?: string; -} -export type VerifiableCredentialData = { - vcJwts?: string[]; - id?: string; - format?: string; - path?: string; -}; -export type VerifiableCredentialType = { - verifiableCredential: string[]; - fulfillment: Fulfillment -}; -export type IssueCredentialParams = { - vc: VerifiableCredentialType, - subjectDid: string -}; \ No newline at end of file diff --git a/packages/common/src/types/dwn.ts b/packages/common/src/types/dwn.ts new file mode 100644 index 0000000..b6d14f5 --- /dev/null +++ b/packages/common/src/types/dwn.ts @@ -0,0 +1,60 @@ +import { DwnPaginationCursor, DwnResponseStatus } from '@web5/agent'; +import { Record as DwnRecord } from '@web5/api'; +import { CredentialApplication, CredentialManifest, DcxProtocolPath } from '../index.js'; + +/** + * Params & Response types for performing CRUD operations on a list of Records and a single Record + * + * Generic + * For performing CRUD operations (inclusive) on a list of records or a single record + * + * Specific + * For performing specifc a CRUD operation (exclusive) on a list of records or a single record + * + */ +// TODO: define CredentialResponse and CredentialInvoice types +export type DwnStatus = { code: number; detail: string; } +export type DcxDwnResponseStatus = { status?: { issuer?: DwnStatus; applicant?: DwnStatus; }} +export type RecordsDataResponse = { data: CredentialManifest[] | CredentialApplication[] | any[] }; + +// Record - CRUD +export type RecordParams = { record: DwnRecord }; +export type RecordResponse = { record: DwnRecord }; +// Record - Create +export type RecordCreateParams = { data: any; protocolPath?: DcxProtocolPath; schema: string }; +export type RecordCreateResponse = RecordResponse; +// Record - Read +export type RecordReadParams = RecordParams; +export type RecordReadResponse = { records: any[] }; +// TODO: define these types once needed +// Record - Update +export type RecordUpdateParams = {}; +export type RecordUpdateResponse = {}; +// Record - Delete +export type RecordDeleteParams = {}; +export type RecordDeleteResponse = {}; + +// Records[] - CRUD +export type RecordsParams = { records: DwnRecord[] }; +export type RecordsResponse = { records: DwnRecord[] }; +// Records[] - Create +export type RecordsCreateParams = { data: any[]; protocolPath?: DcxProtocolPath; schema: string }; +export type RecordsCreateResponse = RecordsResponse; +// Records[] - Read +export type RecordsReadParams = RecordsParams; +export type RecordsReadResponse = { reads: any[] }; +// TODO: define these types once needed +// Records[] - Update +export type RecordsUpdateParams = {}; +export type RecordsUpdateResponse = {}; +// Records[] - Delete +export type RecordsDeleteParams = {}; +export type RecordsDeleteResponse = {}; + +// Records[] - Query +export type RecordsQueryParams = { from?: string; protocolPath?: DcxProtocolPath; schema?: {}; options?: any }; +export type RecordsQueryResponse = DwnResponseStatus & { records: DwnRecord[]; cursor?: DwnPaginationCursor }; + +// Records[] - Filter +export type RecordsFilterParams = { records: CredentialManifest[]; type: 'manifests'}; +export type RecordsFilterResponse = { data: CredentialManifest[] }; diff --git a/packages/common/src/utils/dcx.ts b/packages/common/src/utils/dcx.ts new file mode 100644 index 0000000..3498666 --- /dev/null +++ b/packages/common/src/utils/dcx.ts @@ -0,0 +1,52 @@ +/** + * ITrustedIssuer interface defines objects passed into DcxIssuer or DcxApplicant + * DCX issuers are entities that issue credentials to applicants either prior to an application submission + * or after a credential application response fulfillment. Meaning, the issuers listed in the DCX configuration + * are either issuers you trust to sign credentials that are required for an application and/or issuers that you trust + * to sign and issue credentials to the applicant on behalf of the DCX. These issuers can either be the DCX itself + * or 3rd parties that do verification of raw data and issuance of VCs. The list of issuers within each DCX actor object + * should be list of entities that are trusted to provide applicants with VCs or to issue applicants VCs. + * The input VCs required for an application are defined in Credential Manifests: specifically the presentation_definition.input_descriptors + * field. For more details, see {@link https://identity.foundation/credential-manifest/#input-evaluation} + */ +export interface ITrustedIssuer extends AdditionalProperties { + name: string; + id: string; +} +export class TrustedIssuer { + constructor( + public name: string, + public id: string + ) {} +} + +// Handler +export type HandlerFunction = (...args: any[]) => any | Promise; +export interface IHandler { + id: string; + handler: HandlerFunction; +} +export class Handler implements IHandler { + constructor( + public id: string, + public handler: HandlerFunction + ) {} +} + +// Provider +export interface IProvider extends AdditionalProperties { + id: string; + endpoint: string; + method?: string; + headers?: Record; +} +export class Provider implements IProvider { + constructor( + public id: string, + public endpoint: string, + public method?: string, + public headers?: Record, + ) {} +} +export type DcxProtocolPath = 'manifest' | 'application' | 'application/response' | 'response' | any; +export type AdditionalProperties = Record; \ No newline at end of file diff --git a/packages/common/src/utils/error.ts b/packages/common/src/utils/error.ts index 81ff051..b3e5c73 100644 --- a/packages/common/src/utils/error.ts +++ b/packages/common/src/utils/error.ts @@ -3,7 +3,7 @@ export const dwn500Error = { detail : 'DWN server error', }; export class DcxError extends Error { - constructor(name: string, error: any) { + constructor(error: any, name: string = 'DcxError') { super(error); this.name = name; } @@ -27,6 +27,12 @@ export class DcxIssuerError extends DcxError { } } +export class DcxApplicantError extends DcxError { + constructor(error: any) { + super( 'DcxApplicantError', error); + } +} + export class DcxDwnError extends DcxError { constructor(error: any) { super( 'DcxDwnError', error); @@ -64,7 +70,7 @@ export function handleDcxErrors(target: any, propertyKey: any, descriptor?: any) case error instanceof DcxDwnError: throw new DcxDwnError(error); default: - throw new DcxError('DcxError', error); + throw new DcxError(error); } } }; diff --git a/packages/common/src/utils/options.ts b/packages/common/src/utils/options.ts index bca61e6..0e8efe7 100644 --- a/packages/common/src/utils/options.ts +++ b/packages/common/src/utils/options.ts @@ -1,9 +1,9 @@ import { CredentialManifest, FF, TrustedIssuer } from '../index.js'; -export type FindManifestParams = FindManifestsParams; -export type FindMissingResponse = { missing: CredentialManifest[] }; -export type FindManifestsParams = { manifests: CredentialManifest[]; name?: string; id?: string }; -export type FindMissingParams = { dwnManifests: CredentialManifest[], localManifests: CredentialManifest[] }; +export type FindManifestParams = { manifests: CredentialManifest[]; name?: string; id?: string }; +export type FindIssuerParams = Partial & { issuers: TrustedIssuer[] } +export type FindMissingManifestResponse = { missing: CredentialManifest[] }; +export type FindMissingManifestParams = { dwnManifests: CredentialManifest[], localManifests: CredentialManifest[] }; export class OptionsUtil { /** * @@ -25,7 +25,7 @@ export class OptionsUtil { * @param param.id the id of the manifest to find * @returns CredentialManifest or undefined; see {@link CredentialManifest} */ - public static findManifests({ manifests, name, id }: FindManifestsParams): CredentialManifest[] { + public static findManifests({ manifests, name, id }: FindManifestParams): CredentialManifest[] { return manifests.filter((manifest: CredentialManifest) => this.findManifest({ manifests, name, id })?.id === manifest.id); } @@ -37,11 +37,11 @@ export class OptionsUtil { * @param param.id the id of the issuer to find * @returns TrustedIssuer or FF; see {@link TrustedIssuer}, {@link FF} */ - public static findIssuer({ issuers, name, id }: Partial & { issuers: TrustedIssuer[] }): TrustedIssuer { + public static findIssuer({ issuers, name, id }: FindIssuerParams): TrustedIssuer { return issuers.find((issuer: TrustedIssuer) => issuer.name === name || issuer.id === id) ?? FF; } - public static findMissingManifests({ dwnManifests, localManifests }: FindMissingParams): FindMissingResponse { + public static findMissingManifests({ dwnManifests, localManifests }: FindMissingManifestParams): FindMissingManifestResponse { const dwnManifestIds = new Set(dwnManifests.map(dwnManifest => dwnManifest.id)); const missing = localManifests.filter(localManifest => !dwnManifestIds.has(localManifest.id)); return { missing }; diff --git a/packages/common/src/utils/pex.ts b/packages/common/src/utils/pex.ts index 9b97ad6..c9e04c2 100644 --- a/packages/common/src/utils/pex.ts +++ b/packages/common/src/utils/pex.ts @@ -1,92 +1,46 @@ -import { - AdditionalProperties, - Format, - PresentationSubmission, - VerifiableCredentialData, - HandlerFunction, - ManifestFormat, - ManifestOutputDescriptor, - PresentationDefinition -} from '../index.js'; - -/** - * ITrustedIssuer interface defines objects passed into DcxIssuer or DcxApplicant - * DCX issuers are entities that issue credentials to applicants either prior to an application submission - * or after a credential application response fulfillment. Meaning, the issuers listed in the DCX configuration - * are either issuers you trust to sign credentials that are required for an application and/or issuers that you trust - * to sign and issue credentials to the applicant on behalf of the DCX. These issuers can either be the DCX itself - * or 3rd parties that do verification of raw data and issuance of VCs. The list of issuers within each DCX actor object - * should be list of entities that are trusted to provide applicants with VCs or to issue applicants VCs. - * The input VCs required for an application are defined in Credential Manifests: specifically the presentation_definition.input_descriptors - * field. For more details, see {@link https://identity.foundation/credential-manifest/#input-evaluation} - */ -export interface ITrustedIssuer extends AdditionalProperties { - name: string; - id: string; -} -export class TrustedIssuer implements ITrustedIssuer { - constructor( - public name: string, - public id: string) {} -} - -// Handler -export interface IHandler { - id: string; - handler: HandlerFunction; -} -export class Handler implements IHandler { - constructor( - public id: string, - public handler: HandlerFunction - ) {} -} - -// Provider -export interface IProvider extends AdditionalProperties { - id: string; - endpoint: string; - method?: string; - headers?: Record; -} -export class Provider implements IProvider { - [key: string]: any - constructor( - public id: string, - public endpoint: string, - public method?: string, - public headers?: Record, - ) {} -} - +import { TrustedIssuer } from '../index.js'; /** * CredentialManifest implements DIF spec * For more details, see {@link https://identity.foundation/credential-manifest/#credential-manifest} */ -export interface ICredentialManifest { +export type Format = { jwt: { alg: string[] }; }; +export type Filter = { type: string; pattern: string; }; +export type Field = { path: string[]; filter?: Filter; }; +export type Constraint = { fields: Field[]; }; +export type OutputDescriptor = { id: string; name: string; schema: string; }; +export type InputDescriptor = { id: string; purpose: string; constraints: Constraint; }; +export type PresentationDefinition = { id: string; input_descriptors: InputDescriptor[]; }; +export interface CredentialManifest { id: string, name?: string, description?: string spec_version: string, issuer: TrustedIssuer, - output_descriptors: ManifestOutputDescriptor[], - format?: ManifestFormat, - presentation_definition?: PresentationDefinition, + output_descriptors: OutputDescriptor[], + format: Format + presentation_definition: PresentationDefinition } -export class CredentialManifest implements ICredentialManifest { - constructor( - public id: string, - public spec_version: string, - public issuer: TrustedIssuer, - public output_descriptors: ManifestOutputDescriptor[], - public presentation_definition: PresentationDefinition, - public name?: string, - public format?: ManifestFormat, - public description?: string, - ) {} +// export class CredentialManifest { +// constructor(public credentialManifestModel: CredentialManifestModel) {} +// } +export interface CredentialManifestVP { + '@context': string[]; + type: string[]; + credential_manifest: CredentialManifest; + verifiableCredential: string[]; } -export interface ICredentialApplication { +/** + * CredentialApplication implements DIF spec + * For more details, see {@link https://identity.foundation/credential-manifest/#credential-application} + */ +export type Descriptor = { id: string; format: string; path: string; }; +export type PresentationSubmission = { + id: string; + definition_id: string; + descriptor_map: Descriptor[]; +}; +export interface CredentialApplication { id: string; spec_version: string; applicant: string; @@ -94,39 +48,39 @@ export interface ICredentialApplication { format: Format; presentation_submission: PresentationSubmission; } -export class CredentialApplication implements ICredentialApplication { - constructor( - public id: string, - public spec_version: string = 'https://identity.foundation/credential-manifest/#versioning', - public applicant: string, - public manifest_id: string, - public format: Format, - public presentation_submission: PresentationSubmission, - ) {} +// export class CredentialApplication { +// constructor(public credentialApplicationModel: CredentialApplicationModel) {} +// } +export interface CredentialApplicationVP { + '@context': string[]; + type: string[]; + credential_application: CredentialApplication; + verifiableCredential: string[]; } -export class VerifiableCredential { - constructor({ - vcJwts, - id, - format, - path - }: VerifiableCredentialData = { - format : 'jwt_vc', - path : '$.verifiableCredential[0]' - }) { - - return { - verifiableCredential : vcJwts, - fulfillment : { - descriptor_map : [ - { - id, - format, - path, - }, - ], - }, - }; - } -} \ No newline at end of file +/** + * CredentialResponse implements DIF spec + * For more details, see {@link https://identity.foundation/credential-manifest/#credential-response} + */ +export type Fulfillment = { descriptor_map: Descriptor[]; }; +export interface CredentialResponse { + id: string; + spec_version: string; + manifest_id: string; + applicant: string; + application_id?: string; + fulfillment?: { descriptor_map: Descriptor[]; }; + denial?: { reason: string; input_descriptors: string[] }; +} +// export class CredentialResponse { +// constructor(public credentialResponseModel: CredentialResponseModel) {} +// } +export interface CredentialResponseVP { + '@context': string[]; + type: string[]; + credential_response: CredentialResponse; + verifiableCredential: string[]; +} +// export class VPCredentialResponse { +// constructor(public vpCredentialResponseModel: VPCredentialResponseModel) {} +// } \ No newline at end of file diff --git a/packages/issuer/package.json b/packages/issuer/package.json index 0b458f5..7fb2efc 100644 --- a/packages/issuer/package.json +++ b/packages/issuer/package.json @@ -1,6 +1,6 @@ { "name": "@dcx-protocol/issuer", - "version": "6.0.0", + "version": "7.0.0", "description": "DCX Issuer protocol and server", "type": "module", "main": "./dist/cjs/index.js", diff --git a/packages/issuer/src/dcx-issuer-config.ts b/packages/issuer/src/config.ts similarity index 100% rename from packages/issuer/src/dcx-issuer-config.ts rename to packages/issuer/src/config.ts diff --git a/packages/issuer/src/index.ts b/packages/issuer/src/index.ts index b238222..5513dbc 100644 --- a/packages/issuer/src/index.ts +++ b/packages/issuer/src/index.ts @@ -1,3 +1,3 @@ -export * from './dcx-issuer-config.js'; -export * from './dcx-issuer-protocol.js'; -export * from './dcx-issuer.js'; +export * from './config.js'; +export * from './issuer.js'; +export * from './protocol.js'; diff --git a/packages/issuer/src/dcx-issuer.ts b/packages/issuer/src/issuer.ts similarity index 59% rename from packages/issuer/src/dcx-issuer.ts rename to packages/issuer/src/issuer.ts index c1855ea..7648b1e 100644 --- a/packages/issuer/src/dcx-issuer.ts +++ b/packages/issuer/src/issuer.ts @@ -1,6 +1,8 @@ import { - CreateCredentialParams, + CredentialApplication, + CredentialApplicationVP, CredentialManifest, + CredentialResponseVP, DcxAgent, DcxAgentRecovery, DcxDwnError, @@ -9,31 +11,27 @@ import { DcxManager, DcxManagerStatus, DcxProtocolHandlerError, + DcxProtocolPath, DwnError, DwnUtils, Handler, HandlerFunction, InitializeParams, - IssueCredentialParams, - IssuerProcessRecordParams, Logger, manifestSchema, Objects, OptionsUtil, Provider, - RecordCreateParams, RecordCreateResponse, + RecordResponse, RecordsCreateParams, RecordsParams, RecordsQueryResponse, RecordsReadResponse, RequestCredentialParams, responseSchema, - SelectCredentialsParams, stringifier, - TrustedIssuer, - VerifiableCredential, - VerifyCredentialsParams + TrustedIssuer } from '@dcx-protocol/common'; import { DwnResponseStatus } from '@web5/agent'; import { @@ -43,12 +41,8 @@ import { Web5 } from '@web5/api'; import { LevelStore } from '@web5/common'; -import { - PresentationExchange, - VerifiablePresentation, - VerifiableCredential as Web5VerifiableCredential, -} from '@web5/credentials'; -import { dcxIssuer, issuerConfig, IssuerConfig } from './index.js'; +import { PresentationExchange, VerifiableCredential } from '@web5/credentials'; +import { issuer, issuerConfig, IssuerConfig } from './index.js'; /** * DcxIssuer is the core class for the issuer side of the DCX protocol. @@ -85,8 +79,7 @@ export class DcxIssuer implements DcxManager { ? { ...issuerConfig, ...params.config } : issuerConfig; - const store = new LevelStore({ location: `${this.config.agentDataPath}/VAULT_STORE` }); - this.agentVault = new DcxIdentityVault({ store }); + this.agentVault = new DcxIdentityVault({ store: new LevelStore({ location: `${this.config.agentDataPath}/VAULT_STORE` }) }); /** * Set the default handlers if none are provided @@ -97,7 +90,8 @@ export class DcxIssuer implements DcxManager { { id: 'verifyCredentials', handler: this.verifyCredentials }, { id: 'requestCredentialData', handler: this.requestCredentialData }, { id: 'createCredential', handler: this.createCredential }, - { id: 'issueCredential', handler: this.issueCredential }, + { id: 'createCredentialResponseVP', handler: this.createCredentialResponseVP }, + { id: 'createResponseRecord', handler: this.createResponseRecord }, ]; } @@ -110,7 +104,8 @@ export class DcxIssuer implements DcxManager { this.verifyCredentials = this.findHandler('verifyCredentials', this.verifyCredentials); this.requestCredentialData = this.findHandler('requestCredentialData', this.requestCredentialData); this.createCredential = this.findHandler('createCredential', this.createCredential); - this.issueCredential = this.findHandler('issueCredential', this.issueCredential); + this.createCredentialResponseVP = this.findHandler('createCredentialResponseVP', this.createCredentialResponseVP); + this.createResponseRecord = this.findHandler('createResponseRecord', this.createResponseRecord); } @@ -135,36 +130,36 @@ export class DcxIssuer implements DcxManager { * @param subjectDid The DID of the subject of the credentials * @returns An array of verified credentials */ - public async verifyCredentials({ vcJwts, manifest, subjectDid }: VerifyCredentialsParams): Promise { + public async verifyCredentials({ vcJwts, manifest, applicant }: { + vcJwts: string[]; + manifest: CredentialManifest; + applicant: string; + }): Promise { try { - PresentationExchange.satisfiesPresentationDefinition({ - vcJwts, - presentationDefinition : manifest.presentation_definition, - }); + PresentationExchange.satisfiesPresentationDefinition({ vcJwts, presentationDefinition: manifest.presentation_definition }); } catch (error) { - Logger.error('VC does not satisfy Presentation Definition: ' + error); + Logger.error('VC does not satisfy Presentation Definition', error); } - const verifiedCredentials: Web5VerifiableCredential[] = []; + const verifiedCredentials: VerifiableCredential[] = []; for (const vcJwt of vcJwts) { Logger.debug('Parsing credential ...', vcJwt); - - const vc = Web5VerifiableCredential.parseJwt({ vcJwt }); + const vc = VerifiableCredential.parseJwt({ vcJwt }); Logger.debug('Parsed credential', stringifier(vc)); - if (vc.subject !== subjectDid) { - Logger.debug(`Credential subject ${vc.subject} doesn't match subjectDid ${subjectDid}`); + if (vc.subject !== applicant) { + Logger.debug(`Credential subject ${vc.subject} doesn't match applicant ${applicant}`); continue; } - const issuers = [...this.config.issuers, ...this.config.issuers].map((issuer: TrustedIssuer) => issuer.id); + const issuers = this.config.issuers.map((issuer: TrustedIssuer) => issuer.id); const issuerDidSet = new Set(issuers); if (!issuerDidSet.has(vc.vcDataModel.issuer as string)) { continue; } - const verified = await Web5VerifiableCredential.verify({ vcJwt }); + const verified = await VerifiableCredential.verify({ vcJwt }); if (!verified || Objects.isEmpty(verified)) { Logger.debug('Credential verification failed'); continue; @@ -181,10 +176,16 @@ export class DcxIssuer implements DcxManager { * @param manifest The credential manifest * @returns An array of selected credentials */ - public selectCredentials({ vp, manifest }: SelectCredentialsParams): string[] { - Logger.debug('Using verifiable presentation for credential selection', stringifier(vp)); + public selectCredentials({ verifiableCredential, manifest }: { + verifiableCredential: string[]; + manifest: CredentialManifest; + }): string[] { + Logger.debug('Selecting credentials from manifest ...'); + Logger.debug('Verifiable credentials', stringifier(verifiableCredential)); + Logger.debug('Credential manifest', stringifier(manifest)); + return PresentationExchange.selectCredentials({ - vcJwts : vp.Web5VerifiableCredential, + vcJwts : verifiableCredential, presentationDefinition : manifest.presentation_definition, }); } @@ -193,29 +194,30 @@ export class DcxIssuer implements DcxManager { * * Issue a credential * @param data The data to include in the credential - * @param subjectDid The DID of the subject of the credential + * @param applicant The DID of the subject of the credential * @param manifest The credential manifest * @returns The issued credential */ - public async createCredential({ data, subjectDid, manifest }: CreateCredentialParams): Promise { - const manifestOutputDescriptor = manifest.output_descriptors[0]; - Logger.debug(`Issuing ${manifestOutputDescriptor.id} credential`); - - const vc = await Web5VerifiableCredential.create({ + public async createCredential({ data, application, manifest }: { + data: any, + application: CredentialApplication, + manifest: CredentialManifest, + }): Promise<{ signedVc: string }> { + const { id: credentialId, name } = manifest.output_descriptors[0]; + Logger.debug(`Creating vc ${credentialId} ...`); + + const vc = await VerifiableCredential.create({ data, - subject : subjectDid, + subject : application.applicant, issuer : this.agent.agentDid.uri, - type : manifestOutputDescriptor.name, + type : name, }); - Logger.debug(`Created ${manifestOutputDescriptor.id} credential`, stringifier(vc)); + Logger.debug(`Created vc ${credentialId}`, stringifier(vc)); const signedVc = await vc.sign({ did: this.agent.agentDid }); - Logger.debug(`Signed ${manifestOutputDescriptor.id} credential`, stringifier(signedVc)); + Logger.debug(`Signed vc ${credentialId}`, stringifier(signedVc)); - return new VerifiableCredential({ - vcJwts : [signedVc], - id : manifestOutputDescriptor.id - }); + return { signedVc }; } /** @@ -247,43 +249,40 @@ export class DcxIssuer implements DcxManager { return data; } - public async issueCredential({ vc, subjectDid }: IssueCredentialParams): Promise { + public async createResponseRecord({ response, applicant }: { + response: CredentialResponseVP, + applicant: string + }): Promise { const { record, status } = await this.web5.dwn.records.create({ + data : response, store : true, - data : { - ...vc, - id : '', - spec_version : 'https://identity.foundation/credential-manifest/spec/v1.0.0/', - manifest_id : '' - }, message : { + protocol : issuer.protocol, + protocolPath : 'application/response', schema : responseSchema.$id, - protocol : dcxIssuer.protocol, dataFormat : 'application/json', - protocolPath : 'application/response', }, }); if (DwnUtils.isFailure(status.code)) { const { code, detail } = status; - Logger.error(`DWN records create failed`, status); + Logger.error('DWN records create failed', status); throw new DwnError(code, detail); } if (!record) { - throw new DcxProtocolHandlerError('Failed to create application response record.'); + throw new DcxProtocolHandlerError('Failed to create application response record'); } - const { status: send } = await record?.send(subjectDid); - if (DwnUtils.isFailure(send.code)) { - const { code, detail } = send; - Logger.error(`DWN records send failed`, send); + const { status: applicantSend } = await record.send(applicant); + if (DwnUtils.isFailure(applicantSend.code)) { + const { code, detail } = applicantSend; + Logger.error('DWN records send to applicant dwn failed', applicantSend); throw new DwnError(code, detail); } - Logger.debug(`Sent application response to applicant DWN`, send, status); - - return { status: send }; + Logger.debug('Sent application response to applicant dwn', applicantSend); + return { status: applicantSend, record }; } /** @@ -294,9 +293,10 @@ export class DcxIssuer implements DcxManager { public async queryProtocols(): Promise { // Query DWN for credential-issuer protocol const { status: query, protocols = [] } = await this.web5.dwn.protocols.query({ + from : this.agent.agentDid.uri, message : { filter : { - protocol : dcxIssuer.protocol, + protocol : issuer.protocol, }, }, }); @@ -318,7 +318,7 @@ export class DcxIssuer implements DcxManager { */ public async configureProtocols(): Promise { const { status: configure, protocol } = await this.web5.dwn.protocols.configure({ - message : { definition: dcxIssuer }, + message : { definition: issuer }, }); if (DwnUtils.isFailure(configure.code) || !protocol) { @@ -341,16 +341,18 @@ export class DcxIssuer implements DcxManager { /** * Query DWN for manifest records - * - * @returns Record[]; see {@link Record} + * @param protocolPath The protocol path to query + * @param schema The schema to query + * @returns { status, records, cursor }; see {@link RecordsQueryResponse} */ - public async queryRecords(): Promise { + public async queryRecords({ protocolPath, schema }: { protocolPath: string; schema: string }): Promise { const { status, records = [], cursor } = await this.web5.dwn.records.query({ + from : this.agent.agentDid.uri, message : { filter : { - protocol : dcxIssuer.protocol, - protocolPath : 'manifest', - schema : manifestSchema.$id, + schema, + protocolPath, + protocol : issuer.protocol, dataFormat : 'application/json', }, }, @@ -365,39 +367,72 @@ export class DcxIssuer implements DcxManager { return { status, records, cursor }; } + /** + * Query DWN for manifest records + * + * @returns Record[]; see {@link Record} + */ + public async queryManifestRecords(): Promise { + const { status, records = [], cursor } = await this.queryRecords({ + protocolPath : 'manifest', + schema : manifestSchema.$id, + }); + + if (DwnUtils.isFailure(status.code)) { + const { code, detail } = status; + Logger.error('DWN manifest records query failed', status); + throw new DwnError(code, detail); + } + + return { status, records, cursor }; + } + /** * Read records from DWN * * @param params.records list of Record objects to read; see {@link RecordsParams} * @returns a list of records that have been read into json; see {@link RecordsReadResponse} */ - public async readRecords({ records: manifestRecords }: RecordsParams): Promise { - const records = await Promise.all( - manifestRecords.map(async (manifestRecord: Record) => { - const { record } = await this.web5.dwn.records.read({ + public async readRecords({ records }: RecordsParams): Promise { + const reads = await Promise.all( + records.map(async (record: Record) => { + const { record: read } = await this.web5.dwn.records.read({ + from : this.agent.agentDid.uri, message : { filter : { - recordId : manifestRecord.id, + recordId : record.id, }, }, }); - return record.data.json(); + return read.data.json(); }), ); - return { records }; + return { reads }; } /** - * Create missing manifests - * @param missingManifests CredentialManifest[]; see {@link CredentialManifest} - * @returns Record[]; see {@link Record} + * Read records from DWN + * + * @param params.manifestRecords list of Record objects to read + * @returns { reads: CredentialManifest[] }; see {@link RecordsReadResponse} */ - public async createRecord( - { data, schema, protocolPath }: RecordCreateParams - ): Promise { - if(protocolPath === 'manifest') { - data = data as CredentialManifest; - } + public async readManifestRecords({ manifestRecords }: { manifestRecords: Record[] }): Promise<{ manifests: CredentialManifest[] }> { + const { reads } = await this.readRecords({ records: manifestRecords }); + return { manifests: reads }; + } + + /** + * Create a record in DWN + * @param params.data The data to include in the record + * @param params.schema The schema to use for the record + * @param params.protocolPath The protocol path to use for the record + * @returns { record: Record }; see {@link RecordCreateResponse} + */ + public async createRecord({ data, schema, protocolPath }: { + data: any; + protocolPath?: DcxProtocolPath; + schema: string + }): Promise { const { record, status } = await this.web5.dwn.records.create({ data, store : true, @@ -405,45 +440,40 @@ export class DcxIssuer implements DcxManager { schema, protocolPath, dataFormat : 'application/json', - protocol : dcxIssuer.protocol, + protocol : issuer.protocol, }, }); - const { code, detail } = status; if (DwnUtils.isFailure(status.code)) { + const { code, detail } = status; Logger.error('Failed to create record', status); throw new DwnError(code, detail); } if (!record) { - throw new DcxDwnError(`Record not returned from create: ${code} - ${detail}`); + throw new DcxDwnError(`Record not returned from create: ${status.code} - ${status.detail}`); } - if(process.env.NODE_ENV === 'test') return { record }; + if(process.env.NODE_ENV === 'test'){ + return { record }; + } + + const { status: send } = await record.send(); - const { status: issuer } = await record.send(); - if (DwnUtils.isFailure(issuer.code)) { - const { code, detail } = issuer; - Logger.error('Failed to send record to issuer dwn', issuer); + if (DwnUtils.isFailure(send.code)) { + const { code, detail } = send; + Logger.error('Failed to send record to send dwn', send); throw new DwnError(code, detail); } - Logger.debug('Sent application record to local dwn', issuer); - - if(protocolPath !== 'manifest') { - const manifest = OptionsUtil.findManifest({ manifests: this.config.manifests, id: data.manifest_id }); - const { id: recipient } = OptionsUtil.findIssuer({ issuers: this.config.issuers, id: manifest?.issuer.id }); - const { status: applicant } = await record.send(recipient); - if (DwnUtils.isFailure(applicant.code)) { - const { code, detail } = applicant; - Logger.error('Failed to send record to applicant dwn', applicant); - throw new DwnError(code, detail); - } - Logger.debug('Sent application record to remote dwn', applicant); - } + Logger.debug('Sent manifest record to issuer remote dwn', send); return { record }; } + public async createManifestRecord({ manifest }: { manifest: CredentialManifest }): Promise<{ record: Record }> { + return await this.createRecord({ data: manifest, protocolPath: 'manifest', schema: manifestSchema.$id }); + } + public async createRecords({ data: creates, protocolPath, schema }: RecordsCreateParams): Promise<{records: Record[]}>{ const records = await Promise.all( creates.map(async (data: any) => (await this.createRecord({ protocolPath, data, schema }))?.record) @@ -451,6 +481,32 @@ export class DcxIssuer implements DcxManager { return { records }; } + public createCredentialResponseVP({ verifiableCredential, application, manifest }: { + manifest: CredentialManifest, + verifiableCredential: string[], + application: CredentialApplication, + }): CredentialResponseVP { + const { id: credentialId } = manifest.output_descriptors[0]; + return { + '@context' : ['https://www.w3.org/2018/credentials/v1', 'https://identity.foundation/credential-manifest/response/v1'], + 'type' : ['VerifiablePresentation', 'CredentialResponse'], + credential_response : { + id : crypto.randomUUID(), + spec_version : 'https://identity.foundation/credential-manifest/spec/v1.0.0/', + manifest_id : manifest.id, + applicant : application.applicant, + application_id : application.id, + fulfillment : { + descriptor_map : [{ + id : credentialId, + format : 'jwt', + path : '$.verifiableCredential[0]' + }] + } + }, + verifiableCredential + }; + } /** * @@ -459,27 +515,46 @@ export class DcxIssuer implements DcxManager { * @param manifest The credential manifest * @returns The status of the application record processing */ - public async processRecord({ record, manifest, providerId }: IssuerProcessRecordParams): Promise { - Logger.debug('Processing application record', stringifier(record)); + public async processApplicationRecord({ record, manifest, providerId }: { + record: Record, + manifest: CredentialManifest, + providerId?: string + }): Promise { + Logger.debug('Processing application record ...', stringifier(record)); // Parse the JSON VP from the application record; this will contain the credentials - const vp: VerifiablePresentation = await record.data.json(); - Logger.debug('Application record verifiable presentation', stringifier(vp)); + const applicationVP: CredentialApplicationVP = await record.data.json(); + Logger.info('Processing vp credential application ...', stringifier(applicationVP)); // Select valid credentials against the manifest - const vcJwts = this.selectCredentials({ vp, manifest }); - Logger.debug(`Selected ${vcJwts.length} credentials`); + const vcJwts = this.selectCredentials({ + manifest, + verifiableCredential : applicationVP.verifiableCredential, + }); + Logger.info(`Selected ${vcJwts.length} credentials`, stringifier(vcJwts)); - const subjectDid = record.author; - const verified = await this.verifyCredentials({ vcJwts, manifest, subjectDid }); - Logger.debug(`Verified ${verified.length} credentials`); + const applicant = record.author; + const verified = await this.verifyCredentials({ vcJwts, manifest, applicant }); + Logger.info(`Verified ${verified.length} credentials`, stringifier(verified)); // request vc data - const data = await this.requestCredentialData({ body: { vcs: verified }, id: providerId }); - Logger.debug('VC data from provider', stringifier(data)); + const data = await this.requestCredentialData({ body: { vcs: verified }, id: providerId}); + Logger.info('Requested data from provider', stringifier(data)); + + const application = applicationVP.credential_application; + const { signedVc } = await this.createCredential({ data, manifest, application }); + Logger.info('Created credential', signedVc); + + const response = this.createCredentialResponseVP({ + manifest, + application, + verifiableCredential : [signedVc], + }); + Logger.info('Created credential response', stringifier(response)); + + const issuance = await this.createResponseRecord({ response, applicant }); + Logger.info('Issued credential', stringifier(issuance)); - const vc = await this.createCredential({ data, subjectDid, manifest }); - const issuance = await this.issueCredential({vc, subjectDid}); return { status: issuance.status }; } @@ -507,24 +582,22 @@ export class DcxIssuer implements DcxManager { if(!this.isInitialized()) { throw new DcxIssuerError('DcxIssuer not initialized'); } - // Query DWN for credential-issuer protocols + // Query dwn for issuer protocols const { protocols } = await this.queryProtocols(); Logger.log(`Found ${protocols.length} DcxIssuer dwn protocol(s)`, protocols); - // Configure DWN with credential-issuer protocol if not found - if (!protocols.length) { - Logger.log('Configuring DcxIssuer dwn protocol ...'); - const { status, protocol } = await this.configureProtocols(); - Logger.debug(`Dcx issuer protocol configured: ${status.code} - ${status.detail}`, protocol); - } + // Configure dwn with issuer protocol + Logger.log('Configuring DcxIssuer dwn protocol ...'); + const { status, protocol } = await this.configureProtocols(); + Logger.debug(`Dcx issuer protocol configured: ${status.code} - ${status.detail}`, protocol); - // Query DWN for manifest records - const { records: query } = await this.queryRecords(); - Logger.log(`Found ${query.length} manifest records in DcxIssuer dwn`); + // Query dwn for manifest records + const { records: manifestRecords } = await this.queryManifestRecords(); + Logger.log(`Found ${manifestRecords.length} manifest records in issuer remote dwn`); // Read manifest records data - const { records: manifests } = await this.readRecords({ records: query }); - Logger.debug(`Read ${manifests.length} manifest records from DcxIssuer dwn`, manifests); + const { manifests } = await this.readManifestRecords({ manifestRecords }); + Logger.debug(`Read ${manifestRecords.length} manifest records from DcxIssuer dwn`, manifestRecords); if (!manifests.length) { // Create missing manifest records @@ -604,6 +677,8 @@ export class DcxIssuer implements DcxManager { this.web5 = web5; this.agent = agent; + this.agent.sync.sync(); + // Set the server initialized flag this.status.initialized = true; } diff --git a/packages/issuer/src/dcx-issuer-protocol.ts b/packages/issuer/src/protocol.ts similarity index 95% rename from packages/issuer/src/dcx-issuer-protocol.ts rename to packages/issuer/src/protocol.ts index 1be4b13..a12c551 100644 --- a/packages/issuer/src/dcx-issuer-protocol.ts +++ b/packages/issuer/src/protocol.ts @@ -5,13 +5,13 @@ import { applicationSchema } from '@dcx-protocol/common'; -export const dcxIssuer = { +export const issuer = { // issuer protocol is a subset // of exchange protocol used on // server side to interact with // applicant & issuer dwn - protocol : 'https://decentralized.cx/protocol/', - published : false, + protocol : 'https://decentralized.cx/protocol', + published : true, types : { application : { schema : applicationSchema.$id, diff --git a/packages/issuer/tests/dcx-issuer-config.spec.ts b/packages/issuer/tests/config.spec.ts similarity index 96% rename from packages/issuer/tests/dcx-issuer-config.spec.ts rename to packages/issuer/tests/config.spec.ts index fb28497..3617c85 100644 --- a/packages/issuer/tests/dcx-issuer-config.spec.ts +++ b/packages/issuer/tests/config.spec.ts @@ -2,7 +2,7 @@ import dotenv from 'dotenv'; dotenv.config({ path: '.env.test' }); import { expect } from 'chai'; -import { issuerConfig } from '../src/dcx-issuer-config.js'; +import { issuerConfig } from '../src/index.js'; process.env.NODE_ENV = 'test'; diff --git a/packages/issuer/tests/dcx-issuer.spec.ts b/packages/issuer/tests/issuer.spec.ts similarity index 100% rename from packages/issuer/tests/dcx-issuer.spec.ts rename to packages/issuer/tests/issuer.spec.ts diff --git a/packages/server/package.json b/packages/server/package.json index 14da8f6..8b98068 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@dcx-protocol/server", - "version": "6.0.0", + "version": "7.0.0", "description": "DCX Server Implementation", "type": "module", "main": "./dist/cjs/index.js", diff --git a/packages/server/src/applicant-server.ts b/packages/server/src/applicant-server.ts index c7dc660..bd2887c 100644 --- a/packages/server/src/applicant-server.ts +++ b/packages/server/src/applicant-server.ts @@ -9,7 +9,6 @@ import { Time, TrustedIssuer } from '@dcx-protocol/common'; -import { Record } from '@web5/api'; import ms from 'ms'; import { DcxServer, IServer } from './dcx-server.js'; @@ -64,6 +63,7 @@ export class ApplicantServer implements IServer { * Listens for incoming records from the DWN * @param params.ms The time to sleep between each poll * Optional, default is 2 minutes + * * See {@link SleepTime} */ public async listen(params: SleepTime = { ms: '2m' }): Promise { @@ -73,6 +73,7 @@ export class ApplicantServer implements IServer { while (this.server.listening) { const { records = [] } = await this.applicant.queryRecords({ + from : this.applicant.did, protocolPath : 'application/response', }); @@ -88,10 +89,7 @@ export class ApplicantServer implements IServer { await Time.sleep(milliseconds); } - const responses = records.filter( - (record: Record) => record.protocolPath === 'application/response' - ); - const { records: reads } = await this.applicant.readRecords({ records: responses }); + const { reads } = await this.applicant.readRecords({ records }); Logger.debug(`Processed ${reads.length} application/responses`); } } diff --git a/packages/server/src/issuer-server.ts b/packages/server/src/issuer-server.ts index aa222b8..91ea82a 100644 --- a/packages/server/src/issuer-server.ts +++ b/packages/server/src/issuer-server.ts @@ -7,11 +7,10 @@ import { Objects, Provider, SleepTime, - stringifier, Time, TrustedIssuer } from '@dcx-protocol/common'; -import { DcxIssuer, dcxIssuer } from '@dcx-protocol/issuer'; +import { DcxIssuer, issuer } from '@dcx-protocol/issuer'; import { Record } from '@web5/api'; import ms from 'ms'; import { DcxServer, IServer } from './dcx-server.js'; @@ -64,49 +63,42 @@ export class IssuerServer implements IServer { this.server.useGateway(gateway); } + public async sync(): Promise { + Logger.log('IssuerServer syncing ...'); + await this.issuer.agent.sync.sync(); + } + /** * Listens for incoming records from the DWN */ public async listen(params: SleepTime = { ms: '2m' }): Promise { + await this.sync(); + this.server.listening = true; + Logger.log('IssuerServer listening ...'); const milliseconds = ms(params.ms); - Logger.log('IssuerServer listening ...'); const CURSOR = this.issuer.config.cursorFile; - const LAST_RECORD_ID = this.issuer.config.lastRecordIdFile; let cursor = await FileSystem.readToJson(CURSOR); const pagination = Objects.isEmpty(cursor) ? {} : { cursor }; - let lastRecordId = await FileSystem.readToString(LAST_RECORD_ID); while (this.server.listening) { - const { records = [], cursor: nextCursor } = await this.issuer.web5.dwn.records.query({ + const { records = [] } = await this.issuer.web5.dwn.records.query({ + from : this.issuer.agent.agentDid.uri, message : { pagination, - filter : { protocol: dcxIssuer.protocol }, + filter : { protocol: issuer.protocol }, }, }); - Logger.log(`Found ${records.length} records`); - if (nextCursor) { - Logger.log(`Next cursor update for next query`, stringifier(nextCursor)); - cursor = nextCursor; - const overwritten = await FileSystem.overwrite(CURSOR, cursor); - Logger.log(`${CURSOR} overwritten ${overwritten}`, cursor); - } else { - Logger.log(`Next cursor not found!`); - } - - if (cursor && !records.length) { - cursor = undefined; - } - const recordIds = records.map((record: Record) => record.id); const recordReads: Record[] = await Promise.all( recordIds.map(async (recordId: string) => { const { record }: { record: Record } = await this.issuer.web5.dwn.records.read({ + from : this.issuer.agent.agentDid.uri, message : { filter : { recordId, @@ -117,8 +109,6 @@ export class IssuerServer implements IServer { }), ); - Logger.log(`Read ${recordReads.length} records`); - if (this.server.testing) { Logger.log('Test Complete! Stopping DCX server ...'); this.stop(); @@ -130,34 +120,31 @@ export class IssuerServer implements IServer { } for (const record of recordReads) { - if (record.id != lastRecordId) { - if (record.protocolPath === 'application') { - const manifest = this.issuer.config.manifests.find( - (manifest: CredentialManifest) => - manifest.presentation_definition.id === record.schema, - ); - - if (manifest) { - const { status } = await this.issuer.processRecord( - { - record, - manifest, - providerId : manifest.output_descriptors[0].id, - } - ); - Logger.debug(`Processed application id ${record.id}`, status); - } else { - Logger.log(`Skipped message with protocol path ${record.protocolPath}`); - } - - lastRecordId = record.id; - const overwritten = await FileSystem.overwrite(LAST_RECORD_ID, lastRecordId); - Logger.log(`Overwritten last record id: ${overwritten}`, lastRecordId); + if (record.protocolPath === 'application') { + const data = await record.data.json(); + Logger.debug(`Found application with id: ${record.id}`, data); + + const manifest = this.issuer.config.manifests.find( + (manifest: CredentialManifest) => + manifest.id.toLowerCase() === data.vpDataModel.credential_application.manifest_id, + ); + + if (!manifest) { + Logger.error(`Manifest not found for application with id: ${record.id}`); + continue; } - } else { - await Time.sleep(milliseconds); + + const { status } = await this.issuer.processApplicationRecord( + { + record, + manifest, + providerId : manifest.issuer.id, + } + ); + Logger.debug(`Processed application with id: ${record.id}`, status); } } + await Time.sleep(20000); } } diff --git a/packages/server/tests/dcx-server.spec.ts b/packages/server/tests/dcx-server.spec.ts index 525aa1e..44f9162 100644 --- a/packages/server/tests/dcx-server.spec.ts +++ b/packages/server/tests/dcx-server.spec.ts @@ -4,8 +4,8 @@ import { DcxIssuer, issuerConfig } from '@dcx-protocol/issuer'; import { Web5 } from '@web5/api'; import { expect } from 'chai'; import { ApplicantServer } from '../src/applicant-server.js'; -import { DcxServer } from '../src/dcx-server.js'; import { IssuerServer } from '../src/issuer-server.js'; +import { DcxServer } from '../src/index.js'; process.env.NODE_ENV = 'test';