From c8472b4a512b169b5161caeaac13b43ea19df017 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Thu, 9 Feb 2023 16:04:11 +0100 Subject: [PATCH] refactor(proofs)!: generalize proofs api and improve consistency with credentials module Signed-off-by: Timo Glastra --- demo/src/Alice.ts | 5 +- demo/src/Faber.ts | 18 +- package.json | 2 - .../tests/bbs-signatures.e2e.test.ts | 3 - ...proof.credentials.propose-offerBbs.test.ts | 2 +- packages/core/src/agent/AgentModules.ts | 4 +- packages/core/src/agent/BaseAgent.ts | 7 +- .../core/src/agent/__tests__/Agent.test.ts | 11 +- .../src/decorators/attachment/Attachment.ts | 4 +- .../connections/services/ConnectionService.ts | 4 +- .../src/modules/credentials/CredentialsApi.ts | 181 +-- .../credentials/CredentialsApiOptions.ts | 38 +- .../modules/credentials/CredentialsModule.ts | 4 +- .../formats/CredentialFormatService.ts | 84 +- .../formats/CredentialFormatServiceOptions.ts | 45 +- .../IndyCredentialFormatService.test.ts | 12 +- .../JsonLdCredentialFormatService.test.ts | 14 +- .../indy/IndyCredentialFormatService.ts | 117 +- .../formats/indy/models/IndyCredentialInfo.ts | 11 +- .../jsonld/JsonLdCredentialFormatService.ts | 98 +- .../models/CredentialFormatSpec.ts | 6 +- .../credentials/models/CredentialState.ts | 1 + .../protocol/BaseCredentialProtocol.ts | 77 +- .../protocol/CredentialProtocol.ts | 64 +- .../CredentialProtocolOptions.ts | 55 +- .../protocol/v1/V1CredentialProtocol.ts | 124 +- .../V1CredentialProtocolCred.test.ts | 110 +- .../V1CredentialProtocolProposeOffer.test.ts | 40 +- .../v1/messages/V1ProposeCredentialMessage.ts | 8 - .../v2/CredentialFormatCoordinator.ts | 2 +- .../protocol/v2/V2CredentialProtocol.ts | 123 +- .../V2CredentialProtocolCred.test.ts | 101 +- .../V2CredentialProtocolOffer.test.ts | 21 +- .../v2/handlers/V2OfferCredentialHandler.ts | 7 +- .../v2/messages/V2IssueCredentialMessage.ts | 4 +- .../v2/messages/V2ProposeCredentialMessage.ts | 23 +- .../credentials/util/composeAutoAccept.ts | 1 - .../proofs/ProofResponseCoordinator.ts | 93 -- .../core/src/modules/proofs/ProofService.ts | 261 ---- packages/core/src/modules/proofs/ProofsApi.ts | 536 ++++---- .../src/modules/proofs/ProofsApiOptions.ts | 204 ++- .../core/src/modules/proofs/ProofsModule.ts | 70 +- .../src/modules/proofs/ProofsModuleConfig.ts | 30 +- .../proofs/__tests__/ProofsModule.test.ts | 51 +- .../__tests__/ProofsModuleConfig.test.ts | 26 + .../src/modules/proofs/__tests__/fixtures.ts | 30 + .../src/modules/proofs/formats/ProofFormat.ts | 47 +- .../proofs/formats/ProofFormatConstants.ts | 4 - .../proofs/formats/ProofFormatService.ts | 125 +- .../formats/ProofFormatServiceOptions.ts | 110 +- .../errors/InvalidEncodedValueError.ts | 3 + .../errors/MissingIndyProofMessageError.ts | 3 + .../core/src/modules/proofs/formats/index.ts | 9 +- .../proofs/formats/indy/IndyProofFormat.ts | 79 +- .../formats/indy/IndyProofFormatService.ts | 763 +++++------ .../indy/IndyProofFormatsServiceOptions.ts | 38 - .../{ => formats/indy}/__tests__/groupKeys.ts | 8 +- .../formats/indy/__tests__/util.test.ts | 493 ++++++++ .../errors/MissingIndyProofMessageError.ts | 3 - .../proofs/formats/indy/errors/index.ts | 1 - .../src/modules/proofs/formats/indy/index.ts | 4 +- .../indy}/models/PartialProof.ts | 0 .../indy}/models/ProofAttribute.ts | 0 .../formats/indy/models/ProofAttributeInfo.ts | 8 +- .../indy}/models/ProofIdentifier.ts | 0 .../formats/indy/models/ProofPredicateInfo.ts | 17 +- .../formats/indy/models/ProofRequest.ts | 33 +- .../formats/indy/models/RequestedAttribute.ts | 14 +- .../indy/models/RequestedCredentials.ts | 26 +- .../formats/indy/models/RequestedPredicate.ts | 13 +- .../indy}/models/RequestedProof.ts | 0 .../models}/__tests__/ProofRequest.test.ts | 8 +- .../src/modules/proofs/formats/indy/util.ts | 252 ++++ .../formats/models/ProofAttachmentFormat.ts | 7 - .../models/ProofFormatServiceOptions.ts | 64 - .../modules/proofs/formats/models/index.ts | 2 - packages/core/src/modules/proofs/index.ts | 7 +- .../proofs/messages/PresentationAckMessage.ts | 10 - .../core/src/modules/proofs/messages/index.ts | 1 - .../models/GetRequestedCredentialsConfig.ts | 19 - .../modules/proofs/models/ModuleOptions.ts | 22 - .../proofs/models/ProofServiceOptions.ts | 85 -- .../modules/proofs/models/SharedOptions.ts | 62 - .../{ => models}/__tests__/ProofState.test.ts | 2 +- .../core/src/modules/proofs/models/index.ts | 1 - .../proofs/protocol/BaseProofProtocol.ts | 286 +++++ .../modules/proofs/protocol/ProofProtocol.ts | 117 ++ .../proofs/protocol/ProofProtocolOptions.ts | 165 +++ .../core/src/modules/proofs/protocol/index.ts | 4 + .../proofs/protocol/v1/V1ProofProtocol.ts | 1111 +++++++++++++++++ .../proofs/protocol/v1/V1ProofService.ts | 1111 ----------------- .../v1/__tests__/V1ProofProtocol.test.ts} | 127 +- .../__tests__/indy-proof-negotiation.test.ts | 61 +- ...ts => indy-proof-presentation.test.e2e.ts} | 20 +- ...est.ts => indy-proof-proposal.test.e2e.ts} | 5 +- .../v1-connectionless-proofs.e2e.test.ts} | 57 +- ...t.ts => v1-indy-proof-request.e2e.test.ts} | 50 +- .../v1/__tests__/v1-indy-proofs.e2e.test.ts} | 158 +-- .../v1-proofs-auto-accept.e2e.test.ts} | 40 +- .../v1/handlers/V1PresentationAckHandler.ts | 10 +- .../v1/handlers/V1PresentationHandler.ts | 84 +- .../V1PresentationProblemReportHandler.ts | 10 +- .../handlers/V1ProposePresentationHandler.ts | 93 +- .../handlers/V1RequestPresentationHandler.ts | 120 +- .../src/modules/proofs/protocol/v1/index.ts | 2 +- .../v1/messages/V1PresentationMessage.ts | 34 +- .../messages/V1ProposePresentationMessage.ts | 20 +- .../messages/V1RequestPresentationMessage.ts | 58 +- .../v1/models/V1PresentationPreview.ts | 44 +- .../proofs/protocol/v1/models/index.ts | 4 - .../protocol/v2/ProofFormatCoordinator.ts | 520 ++++++++ .../proofs/protocol/v2/V2ProofProtocol.ts | 1069 ++++++++++++++++ .../proofs/protocol/v2/V2ProofService.ts | 953 -------------- .../v2/__tests__/V2ProofProtocol.test.ts} | 145 +-- ...v2-indy-connectionless-proofs.e2e.test.ts} | 62 +- ...t.ts => v2-indy-proof-negotiation.test.ts} | 106 +- ...=> v2-indy-proof-presentation.e2e.test.ts} | 37 +- ....ts => v2-indy-proof-proposal.e2e.test.ts} | 18 +- ...t.ts => v2-indy-proof-request.e2e.test.ts} | 24 +- .../v2-indy-proofs-auto-accept.2e.test.ts} | 56 +- .../v2/__tests__/v2-indy-proofs.e2e.test.ts} | 230 ++-- .../v2/handlers/V2PresentationAckHandler.ts | 10 +- .../v2/handlers/V2PresentationHandler.ts | 61 +- .../V2PresentationProblemReportHandler.ts | 6 +- .../handlers/V2ProposePresentationHandler.ts | 93 +- .../handlers/V2RequestPresentationHandler.ts | 94 +- .../src/modules/proofs/protocol/v2/index.ts | 2 +- .../v2/messages/V2PresentationAckMessage.ts | 6 - .../v2/messages/V2PresentationMessage.ts | 43 +- .../V2PresentationProblemReportMessage.ts | 12 - .../messages/V2ProposalPresentationMessage.ts | 100 -- .../messages/V2ProposePresentationMessage.ts | 62 + .../messages/V2RequestPresentationMessage.ts | 70 +- .../proofs/protocol/v2/messages/index.ts | 2 +- .../proofs/repository/ProofExchangeRecord.ts | 8 + .../modules/proofs/utils/composeAutoAccept.ts | 11 + .../src/modules/vc/W3cCredentialService.ts | 11 +- .../vc/__tests__/W3cCredentialService.test.ts | 3 - .../vc/__tests__/contexts/citizenship_v2.ts | 45 + .../modules/vc/__tests__/contexts/index.ts | 2 + .../vc/__tests__/contexts/submission.ts | 15 + .../vc/__tests__/contexts/vaccination_v2.ts | 88 ++ .../modules/vc/__tests__/documentLoader.ts | 14 +- .../core/src/modules/vc/__tests__/fixtures.ts | 30 + packages/core/src/modules/vc/constants.ts | 1 + .../vc/models/W3cCredentialServiceOptions.ts | 2 - .../utils/__tests__/indyProofRequest.test.ts | 81 -- packages/core/src/utils/index.ts | 1 + packages/core/src/utils/indyProofRequest.ts | 2 +- packages/core/tests/helpers.ts | 145 +-- packages/core/tests/logger.ts | 55 +- packages/core/tests/oob.test.ts | 4 +- .../core/tests/proofs-sub-protocol.test.ts | 36 +- packages/openid4vc-client/tests/fixtures.ts | 2 +- .../tests/openid4vc-client.e2e.test.ts | 12 +- tests/e2e-test.ts | 25 +- yarn.lock | 5 - 157 files changed, 6989 insertions(+), 6256 deletions(-) rename packages/core/src/modules/credentials/{ => protocol}/CredentialProtocolOptions.ts (69%) delete mode 100644 packages/core/src/modules/proofs/ProofResponseCoordinator.ts delete mode 100644 packages/core/src/modules/proofs/ProofService.ts create mode 100644 packages/core/src/modules/proofs/__tests__/ProofsModuleConfig.test.ts delete mode 100644 packages/core/src/modules/proofs/formats/ProofFormatConstants.ts create mode 100644 packages/core/src/modules/proofs/formats/errors/InvalidEncodedValueError.ts create mode 100644 packages/core/src/modules/proofs/formats/errors/MissingIndyProofMessageError.ts delete mode 100644 packages/core/src/modules/proofs/formats/indy/IndyProofFormatsServiceOptions.ts rename packages/core/src/modules/proofs/{ => formats/indy}/__tests__/groupKeys.ts (79%) create mode 100644 packages/core/src/modules/proofs/formats/indy/__tests__/util.test.ts delete mode 100644 packages/core/src/modules/proofs/formats/indy/errors/MissingIndyProofMessageError.ts rename packages/core/src/modules/proofs/{protocol/v1 => formats/indy}/models/PartialProof.ts (100%) rename packages/core/src/modules/proofs/{protocol/v1 => formats/indy}/models/ProofAttribute.ts (100%) rename packages/core/src/modules/proofs/{protocol/v1 => formats/indy}/models/ProofIdentifier.ts (100%) rename packages/core/src/modules/proofs/{protocol/v1 => formats/indy}/models/RequestedProof.ts (100%) rename packages/core/src/modules/proofs/{ => formats/indy/models}/__tests__/ProofRequest.test.ts (87%) create mode 100644 packages/core/src/modules/proofs/formats/indy/util.ts delete mode 100644 packages/core/src/modules/proofs/formats/models/ProofAttachmentFormat.ts delete mode 100644 packages/core/src/modules/proofs/formats/models/ProofFormatServiceOptions.ts delete mode 100644 packages/core/src/modules/proofs/formats/models/index.ts delete mode 100644 packages/core/src/modules/proofs/messages/PresentationAckMessage.ts delete mode 100644 packages/core/src/modules/proofs/messages/index.ts delete mode 100644 packages/core/src/modules/proofs/models/GetRequestedCredentialsConfig.ts delete mode 100644 packages/core/src/modules/proofs/models/ModuleOptions.ts delete mode 100644 packages/core/src/modules/proofs/models/ProofServiceOptions.ts delete mode 100644 packages/core/src/modules/proofs/models/SharedOptions.ts rename packages/core/src/modules/proofs/{ => models}/__tests__/ProofState.test.ts (92%) create mode 100644 packages/core/src/modules/proofs/protocol/BaseProofProtocol.ts create mode 100644 packages/core/src/modules/proofs/protocol/ProofProtocol.ts create mode 100644 packages/core/src/modules/proofs/protocol/ProofProtocolOptions.ts create mode 100644 packages/core/src/modules/proofs/protocol/v1/V1ProofProtocol.ts delete mode 100644 packages/core/src/modules/proofs/protocol/v1/V1ProofService.ts rename packages/core/src/modules/proofs/{__tests__/V1ProofService.test.ts => protocol/v1/__tests__/V1ProofProtocol.test.ts} (63%) rename packages/core/src/modules/proofs/protocol/v1/__tests__/{indy-proof-presentation.test.ts => indy-proof-presentation.test.e2e.ts} (93%) rename packages/core/src/modules/proofs/protocol/v1/__tests__/{indy-proof-proposal.test.ts => indy-proof-proposal.test.e2e.ts} (95%) rename packages/core/{tests/v1-connectionless-proofs.test.ts => src/modules/proofs/protocol/v1/__tests__/v1-connectionless-proofs.e2e.test.ts} (89%) rename packages/core/src/modules/proofs/protocol/v1/__tests__/{indy-proof-request.test.ts => v1-indy-proof-request.e2e.test.ts} (70%) rename packages/core/{tests/v1-indy-proofs.test.ts => src/modules/proofs/protocol/v1/__tests__/v1-indy-proofs.e2e.test.ts} (81%) rename packages/core/{tests/v1-proofs-auto-accept.test.ts => src/modules/proofs/protocol/v1/__tests__/v1-proofs-auto-accept.e2e.test.ts} (82%) create mode 100644 packages/core/src/modules/proofs/protocol/v2/ProofFormatCoordinator.ts create mode 100644 packages/core/src/modules/proofs/protocol/v2/V2ProofProtocol.ts delete mode 100644 packages/core/src/modules/proofs/protocol/v2/V2ProofService.ts rename packages/core/src/modules/proofs/{__tests__/V2ProofService.test.ts => protocol/v2/__tests__/V2ProofProtocol.test.ts} (61%) rename packages/core/{tests/v2-connectionless-proofs.test.ts => src/modules/proofs/protocol/v2/__tests__/v2-indy-connectionless-proofs.e2e.test.ts} (88%) rename packages/core/src/modules/proofs/protocol/v2/__tests__/{indy-proof-negotiation.test.ts => v2-indy-proof-negotiation.test.ts} (84%) rename packages/core/src/modules/proofs/protocol/v2/__tests__/{indy-proof-presentation.test.ts => v2-indy-proof-presentation.e2e.test.ts} (89%) rename packages/core/src/modules/proofs/protocol/v2/__tests__/{indy-proof-proposal.test.ts => v2-indy-proof-proposal.e2e.test.ts} (84%) rename packages/core/src/modules/proofs/protocol/v2/__tests__/{indy-proof-request.test.ts => v2-indy-proof-request.e2e.test.ts} (87%) rename packages/core/{tests/v2-proofs-auto-accept.test.ts => src/modules/proofs/protocol/v2/__tests__/v2-indy-proofs-auto-accept.2e.test.ts} (79%) rename packages/core/{tests/v2-indy-proofs.test.ts => src/modules/proofs/protocol/v2/__tests__/v2-indy-proofs.e2e.test.ts} (77%) delete mode 100644 packages/core/src/modules/proofs/protocol/v2/messages/V2ProposalPresentationMessage.ts create mode 100644 packages/core/src/modules/proofs/protocol/v2/messages/V2ProposePresentationMessage.ts create mode 100644 packages/core/src/modules/proofs/utils/composeAutoAccept.ts create mode 100644 packages/core/src/modules/vc/__tests__/contexts/citizenship_v2.ts create mode 100644 packages/core/src/modules/vc/__tests__/contexts/submission.ts create mode 100644 packages/core/src/modules/vc/__tests__/contexts/vaccination_v2.ts delete mode 100644 packages/core/src/utils/__tests__/indyProofRequest.test.ts diff --git a/demo/src/Alice.ts b/demo/src/Alice.ts index 252c04c632..aa705ca7a4 100644 --- a/demo/src/Alice.ts +++ b/demo/src/Alice.ts @@ -52,11 +52,8 @@ export class Alice extends BaseAgent { } public async acceptProofRequest(proofRecord: ProofExchangeRecord) { - const requestedCredentials = await this.agent.proofs.autoSelectCredentialsForProofRequest({ + const requestedCredentials = await this.agent.proofs.selectCredentialsForRequest({ proofRecordId: proofRecord.id, - config: { - filterByPresentationPreview: true, - }, }) await this.agent.proofs.acceptRequest({ diff --git a/demo/src/Faber.ts b/demo/src/Faber.ts index 267349b785..a19906d0fa 100644 --- a/demo/src/Faber.ts +++ b/demo/src/Faber.ts @@ -2,13 +2,7 @@ import type { ConnectionRecord, ConnectionStateChangedEvent } from '@aries-frame import type { CredDef, Schema } from 'indy-sdk' import type BottomBar from 'inquirer/lib/ui/bottom-bar' -import { - AttributeFilter, - ProofAttributeInfo, - utils, - V1CredentialPreview, - ConnectionEventTypes, -} from '@aries-framework/core' +import { utils, V1CredentialPreview, ConnectionEventTypes } from '@aries-framework/core' import { ui } from 'inquirer' import { BaseAgent } from './BaseAgent' @@ -171,15 +165,16 @@ export class Faber extends BaseAgent { private async newProofAttribute() { await this.printProofFlow(greenText(`Creating new proof attribute for 'name' ...\n`)) const proofAttribute = { - name: new ProofAttributeInfo({ + name: { name: 'name', restrictions: [ - new AttributeFilter({ + { credentialDefinitionId: this.credentialDefinition?.id, - }), + }, ], - }), + }, } + return proofAttribute } @@ -195,7 +190,6 @@ export class Faber extends BaseAgent { indy: { name: 'proof-request', version: '1.0', - nonce: '1298236324864', requestedAttributes: proofAttribute, }, }, diff --git a/package.json b/package.json index 24f487b9a2..582f91a77e 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,6 @@ "test": "jest", "lint": "eslint --ignore-path .gitignore .", "validate": "yarn lint && yarn check-types && yarn check-format", - "prepare": "husky install", "run-mediator": "ts-node ./samples/mediator.ts", "next-version-bump": "ts-node ./scripts/get-next-bump.ts" }, @@ -48,7 +47,6 @@ "eslint-plugin-import": "^2.23.4", "eslint-plugin-prettier": "^3.4.0", "express": "^4.17.1", - "husky": "^7.0.1", "indy-sdk": "^1.16.0-dev-1636", "jest": "^27.0.4", "lerna": "^4.0.0", diff --git a/packages/bbs-signatures/tests/bbs-signatures.e2e.test.ts b/packages/bbs-signatures/tests/bbs-signatures.e2e.test.ts index 8e579225fe..936dfbe22e 100644 --- a/packages/bbs-signatures/tests/bbs-signatures.e2e.test.ts +++ b/packages/bbs-signatures/tests/bbs-signatures.e2e.test.ts @@ -252,10 +252,7 @@ describeSkipNode17And18('BBS W3cCredentialService', () => { const result = await w3cCredentialService.verifyPresentation(agentContext, { presentation: vp, - proofType: 'Ed25519Signature2018', challenge: 'e950bfe5-d7ec-4303-ad61-6983fb976ac9', - verificationMethod: - 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', }) expect(result.verified).toBe(true) diff --git a/packages/bbs-signatures/tests/v2.ldproof.credentials.propose-offerBbs.test.ts b/packages/bbs-signatures/tests/v2.ldproof.credentials.propose-offerBbs.test.ts index 997c73c18b..a22408ce87 100644 --- a/packages/bbs-signatures/tests/v2.ldproof.credentials.propose-offerBbs.test.ts +++ b/packages/bbs-signatures/tests/v2.ldproof.credentials.propose-offerBbs.test.ts @@ -87,7 +87,7 @@ describeSkipNode17And18('credentials, BBS+ signature', () => { testLogger.test('Alice sends (v2, Indy) credential proposal to Faber') - const credentialExchangeRecord: CredentialExchangeRecord = await aliceAgent.credentials.proposeCredential({ + const credentialExchangeRecord = await aliceAgent.credentials.proposeCredential({ connectionId: aliceConnection.id, protocolVersion: 'v2', credentialFormats: { diff --git a/packages/core/src/agent/AgentModules.ts b/packages/core/src/agent/AgentModules.ts index 3f9512bdba..20f0d6cbb7 100644 --- a/packages/core/src/agent/AgentModules.ts +++ b/packages/core/src/agent/AgentModules.ts @@ -36,9 +36,11 @@ export type AgentModulesInput = Partial & ModulesMap * Defines the input type for the default agent modules. This is overwritten as we * want the input type to allow for generics to be passed in for the credentials module. */ -export type DefaultAgentModulesInput = Omit & { +export type DefaultAgentModulesInput = Omit & { // eslint-disable-next-line @typescript-eslint/no-explicit-any credentials: CredentialsModule + // eslint-disable-next-line @typescript-eslint/no-explicit-any + proofs: ProofsModule } /** diff --git a/packages/core/src/agent/BaseAgent.ts b/packages/core/src/agent/BaseAgent.ts index 5d47157f59..704b2b5d98 100644 --- a/packages/core/src/agent/BaseAgent.ts +++ b/packages/core/src/agent/BaseAgent.ts @@ -3,6 +3,7 @@ import type { AgentApi, EmptyModuleMap, ModulesMap, WithoutDefaultModules, Custo import type { TransportSession } from './TransportService' import type { Logger } from '../logger' import type { CredentialsModule } from '../modules/credentials' +import type { ProofsModule } from '../modules/proofs' import type { DependencyManager } from '../plugins' import { AriesFrameworkError } from '../error' @@ -14,7 +15,7 @@ import { DiscoverFeaturesApi } from '../modules/discover-features' import { GenericRecordsApi } from '../modules/generic-records' import { LedgerApi } from '../modules/ledger' import { OutOfBandApi } from '../modules/oob' -import { ProofsApi } from '../modules/proofs/ProofsApi' +import { ProofsApi } from '../modules/proofs' import { MediatorApi, RecipientApi } from '../modules/routing' import { StorageUpdateService } from '../storage' import { UpdateAssistant } from '../storage/migration/UpdateAssistant' @@ -44,7 +45,7 @@ export abstract class BaseAgent - public readonly proofs: ProofsApi + public readonly proofs: CustomOrDefaultApi public readonly mediator: MediatorApi public readonly mediationRecipient: RecipientApi public readonly basicMessages: BasicMessagesApi @@ -88,7 +89,7 @@ export abstract class BaseAgent - this.proofs = this.dependencyManager.resolve(ProofsApi) + this.proofs = this.dependencyManager.resolve(ProofsApi) as CustomOrDefaultApi this.mediator = this.dependencyManager.resolve(MediatorApi) this.mediationRecipient = this.dependencyManager.resolve(RecipientApi) this.basicMessages = this.dependencyManager.resolve(BasicMessagesApi) diff --git a/packages/core/src/agent/__tests__/Agent.test.ts b/packages/core/src/agent/__tests__/Agent.test.ts index 2eca3350be..31ff626e32 100644 --- a/packages/core/src/agent/__tests__/Agent.test.ts +++ b/packages/core/src/agent/__tests__/Agent.test.ts @@ -16,8 +16,8 @@ import { IndyLedgerService } from '../../modules/ledger' import { LedgerApi } from '../../modules/ledger/LedgerApi' import { ProofRepository } from '../../modules/proofs' import { ProofsApi } from '../../modules/proofs/ProofsApi' -import { V1ProofService } from '../../modules/proofs/protocol/v1' -import { V2ProofService } from '../../modules/proofs/protocol/v2' +import { V1ProofProtocol } from '../../modules/proofs/protocol/v1' +import { V2ProofProtocol } from '../../modules/proofs/protocol/v2' import { MediationRecipientService, MediationRepository, @@ -161,8 +161,6 @@ describe('Agent', () => { expect(container.resolve(ConnectionRepository)).toBeInstanceOf(ConnectionRepository) expect(container.resolve(TrustPingService)).toBeInstanceOf(TrustPingService) - expect(container.resolve(V1ProofService)).toBeInstanceOf(V1ProofService) - expect(container.resolve(V2ProofService)).toBeInstanceOf(V2ProofService) expect(container.resolve(ProofsApi)).toBeInstanceOf(ProofsApi) expect(container.resolve(ProofRepository)).toBeInstanceOf(ProofRepository) @@ -204,8 +202,6 @@ describe('Agent', () => { expect(container.resolve(ConnectionRepository)).toBe(container.resolve(ConnectionRepository)) expect(container.resolve(TrustPingService)).toBe(container.resolve(TrustPingService)) - expect(container.resolve(V1ProofService)).toBe(container.resolve(V1ProofService)) - expect(container.resolve(V2ProofService)).toBe(container.resolve(V2ProofService)) expect(container.resolve(ProofsApi)).toBe(container.resolve(ProofsApi)) expect(container.resolve(ProofRepository)).toBe(container.resolve(ProofRepository)) @@ -263,10 +259,11 @@ describe('Agent', () => { 'https://didcomm.org/messagepickup/2.0', 'https://didcomm.org/out-of-band/1.1', 'https://didcomm.org/present-proof/1.0', + 'https://didcomm.org/present-proof/2.0', 'https://didcomm.org/revocation_notification/1.0', 'https://didcomm.org/revocation_notification/2.0', ]) ) - expect(protocols.length).toEqual(14) + expect(protocols.length).toEqual(15) }) }) diff --git a/packages/core/src/decorators/attachment/Attachment.ts b/packages/core/src/decorators/attachment/Attachment.ts index b39fa52d8d..50a91e8edb 100644 --- a/packages/core/src/decorators/attachment/Attachment.ts +++ b/packages/core/src/decorators/attachment/Attachment.ts @@ -25,7 +25,7 @@ export interface AttachmentOptions { mimeType?: string lastmodTime?: Date byteCount?: number - data: AttachmentData + data: AttachmentDataOptions } export interface AttachmentDataOptions { @@ -97,7 +97,7 @@ export class Attachment { this.mimeType = options.mimeType this.lastmodTime = options.lastmodTime this.byteCount = options.byteCount - this.data = options.data + this.data = new AttachmentData(options.data) } } diff --git a/packages/core/src/modules/connections/services/ConnectionService.ts b/packages/core/src/modules/connections/services/ConnectionService.ts index f228f4fa64..26e971e393 100644 --- a/packages/core/src/modules/connections/services/ConnectionService.ts +++ b/packages/core/src/modules/connections/services/ConnectionService.ts @@ -447,8 +447,8 @@ export class ConnectionService { previousSentMessage, previousReceivedMessage, }: { - previousSentMessage?: AgentMessage - previousReceivedMessage?: AgentMessage + previousSentMessage?: AgentMessage | null + previousReceivedMessage?: AgentMessage | null } = {} ) { const { connection, message } = messageContext diff --git a/packages/core/src/modules/credentials/CredentialsApi.ts b/packages/core/src/modules/credentials/CredentialsApi.ts index e69fe6c6a5..06e7f6a712 100644 --- a/packages/core/src/modules/credentials/CredentialsApi.ts +++ b/packages/core/src/modules/credentials/CredentialsApi.ts @@ -1,23 +1,23 @@ -import type { CredentialFormatsFromProtocols, DeleteCredentialOptions } from './CredentialProtocolOptions' import type { AcceptCredentialOptions, AcceptCredentialOfferOptions, AcceptCredentialProposalOptions, AcceptCredentialRequestOptions, - CreateOfferOptions, + CreateCredentialOfferOptions, FindCredentialMessageReturn, FindCredentialOfferMessageReturn, FindCredentialProposalMessageReturn, FindCredentialRequestMessageReturn, - GetFormatDataReturn, + GetCredentialFormatDataReturn, NegotiateCredentialOfferOptions, NegotiateCredentialProposalOptions, OfferCredentialOptions, ProposeCredentialOptions, SendCredentialProblemReportOptions, - CredentialProtocolMap, + DeleteCredentialOptions, } from './CredentialsApiOptions' import type { CredentialProtocol } from './protocol/CredentialProtocol' +import type { CredentialFormatsFromProtocols } from './protocol/CredentialProtocolOptions' import type { CredentialExchangeRecord } from './repository/CredentialExchangeRecord' import type { AgentMessage } from '../../agent/AgentMessage' import type { Query } from '../../storage/StorageService' @@ -63,7 +63,7 @@ export interface CredentialsApi { acceptCredential(options: AcceptCredentialOptions): Promise // out of band - createOffer(options: CreateOfferOptions): Promise<{ + createOffer(options: CreateCredentialOfferOptions): Promise<{ message: AgentMessage credentialRecord: CredentialExchangeRecord }> @@ -77,7 +77,7 @@ export interface CredentialsApi { findById(credentialRecordId: string): Promise deleteById(credentialRecordId: string, options?: DeleteCredentialOptions): Promise update(credentialRecord: CredentialExchangeRecord): Promise - getFormatData(credentialRecordId: string): Promise>> + getFormatData(credentialRecordId: string): Promise>> // DidComm Message Records findProposalMessage(credentialExchangeId: string): Promise> @@ -89,7 +89,7 @@ export interface CredentialsApi { @injectable() export class CredentialsApi implements CredentialsApi { /** - * Configuration for the connections module + * Configuration for the credentials module */ public readonly config: CredentialsModuleConfig @@ -100,7 +100,6 @@ export class CredentialsApi implements Credent private didCommMessageRepository: DidCommMessageRepository private routingService: RoutingService private logger: Logger - private credentialProtocolMap: CredentialProtocolMap public constructor( messageSender: MessageSender, @@ -123,58 +122,49 @@ export class CredentialsApi implements Credent this.didCommMessageRepository = didCommMessageRepository this.logger = logger this.config = config - - // Dynamically build service map. This will be extracted once services are registered dynamically - this.credentialProtocolMap = config.credentialProtocols.reduce( - (protocolMap, service) => ({ - ...protocolMap, - [service.version]: service, - }), - {} - ) as CredentialProtocolMap } - private getProtocol>(protocolVersion: PVT): CredentialProtocol { - if (!this.credentialProtocolMap[protocolVersion]) { + private getProtocol(protocolVersion: PVT): CredentialProtocol { + const credentialProtocol = this.config.credentialProtocols.find((protocol) => protocol.version === protocolVersion) + + if (!credentialProtocol) { throw new AriesFrameworkError(`No credential protocol registered for protocol version ${protocolVersion}`) } - return this.credentialProtocolMap[protocolVersion] as CredentialProtocol + return credentialProtocol } /** * Initiate a new credential exchange as holder by sending a credential proposal message - * to the connection with the specified credential options + * to the connection with the specified connection id. * * @param options configuration to use for the proposal * @returns Credential exchange record associated with the sent proposal message */ public async proposeCredential(options: ProposeCredentialOptions): Promise { - const service = this.getProtocol(options.protocolVersion) + const protocol = this.getProtocol(options.protocolVersion) - this.logger.debug(`Got a credentialProtocol object for version ${options.protocolVersion}`) + const connectionRecord = await this.connectionService.getById(this.agentContext, options.connectionId) - const connection = await this.connectionService.getById(this.agentContext, options.connectionId) + // Assert + connectionRecord.assertReady() // will get back a credential record -> map to Credential Exchange Record - const { credentialRecord, message } = await service.createProposal(this.agentContext, { - connection, + const { credentialRecord, message } = await protocol.createProposal(this.agentContext, { + connectionRecord, credentialFormats: options.credentialFormats, comment: options.comment, autoAcceptCredential: options.autoAcceptCredential, }) - this.logger.debug('We have a message (sending outbound): ', message) - // send the message here const outboundMessageContext = new OutboundMessageContext(message, { agentContext: this.agentContext, - connection, + connection: connectionRecord, associatedRecord: credentialRecord, }) - this.logger.debug('In proposeCredential: Send Proposal to Issuer') await this.messageSender.sendMessage(outboundMessageContext) return credentialRecord } @@ -196,11 +186,15 @@ export class CredentialsApi implements Credent ) } - // with version we can get the Service - const service = this.getProtocol(credentialRecord.protocolVersion) + // with version we can get the protocol + const protocol = this.getProtocol(credentialRecord.protocolVersion) + const connectionRecord = await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) + + // Assert + connectionRecord.assertReady() // will get back a credential record -> map to Credential Exchange Record - const { message } = await service.acceptProposal(this.agentContext, { + const { message } = await protocol.acceptProposal(this.agentContext, { credentialRecord, credentialFormats: options.credentialFormats, comment: options.comment, @@ -208,10 +202,9 @@ export class CredentialsApi implements Credent }) // send the message - const connection = await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) const outboundMessageContext = new OutboundMessageContext(message, { agentContext: this.agentContext, - connection, + connection: connectionRecord, associatedRecord: credentialRecord, }) await this.messageSender.sendMessage(outboundMessageContext) @@ -237,9 +230,9 @@ export class CredentialsApi implements Credent } // with version we can get the Service - const service = this.getProtocol(credentialRecord.protocolVersion) + const protocol = this.getProtocol(credentialRecord.protocolVersion) - const { message } = await service.negotiateProposal(this.agentContext, { + const { message } = await protocol.negotiateProposal(this.agentContext, { credentialRecord, credentialFormats: options.credentialFormats, comment: options.comment, @@ -265,22 +258,22 @@ export class CredentialsApi implements Credent * @returns Credential exchange record associated with the sent credential offer message */ public async offerCredential(options: OfferCredentialOptions): Promise { - const connection = await this.connectionService.getById(this.agentContext, options.connectionId) - const service = this.getProtocol(options.protocolVersion) + const connectionRecord = await this.connectionService.getById(this.agentContext, options.connectionId) + const protocol = this.getProtocol(options.protocolVersion) this.logger.debug(`Got a credentialProtocol object for version ${options.protocolVersion}`) - const { message, credentialRecord } = await service.createOffer(this.agentContext, { + const { message, credentialRecord } = await protocol.createOffer(this.agentContext, { credentialFormats: options.credentialFormats, autoAcceptCredential: options.autoAcceptCredential, comment: options.comment, - connection, + connectionRecord, }) this.logger.debug('Offer Message successfully created; message= ', message) const outboundMessageContext = new OutboundMessageContext(message, { agentContext: this.agentContext, - connection, + connection: connectionRecord, associatedRecord: credentialRecord, }) await this.messageSender.sendMessage(outboundMessageContext) @@ -298,16 +291,19 @@ export class CredentialsApi implements Credent public async acceptOffer(options: AcceptCredentialOfferOptions): Promise { const credentialRecord = await this.getById(options.credentialRecordId) - const service = this.getProtocol(credentialRecord.protocolVersion) + const protocol = this.getProtocol(credentialRecord.protocolVersion) - this.logger.debug(`Got a credentialProtocol object for this version; version = ${service.version}`) - const offerMessage = await service.findOfferMessage(this.agentContext, credentialRecord.id) + this.logger.debug(`Got a credentialProtocol object for this version; version = ${protocol.version}`) + const offerMessage = await protocol.findOfferMessage(this.agentContext, credentialRecord.id) // Use connection if present if (credentialRecord.connectionId) { - const connection = await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) + const connectionRecord = await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) - const { message } = await service.acceptOffer(this.agentContext, { + // Assert + connectionRecord.assertReady() + + const { message } = await protocol.acceptOffer(this.agentContext, { credentialRecord, credentialFormats: options.credentialFormats, comment: options.comment, @@ -316,7 +312,7 @@ export class CredentialsApi implements Credent const outboundMessageContext = new OutboundMessageContext(message, { agentContext: this.agentContext, - connection, + connection: connectionRecord, associatedRecord: credentialRecord, }) await this.messageSender.sendMessage(outboundMessageContext) @@ -334,7 +330,7 @@ export class CredentialsApi implements Credent }) const recipientService = offerMessage.service - const { message } = await service.acceptOffer(this.agentContext, { + const { message } = await protocol.acceptOffer(this.agentContext, { credentialRecord, credentialFormats: options.credentialFormats, comment: options.comment, @@ -375,8 +371,8 @@ export class CredentialsApi implements Credent credentialRecord.assertState(CredentialState.OfferReceived) // with version we can get the Service - const service = this.getProtocol(credentialRecord.protocolVersion) - await service.updateState(this.agentContext, credentialRecord, CredentialState.Declined) + const protocol = this.getProtocol(credentialRecord.protocolVersion) + await protocol.updateState(this.agentContext, credentialRecord, CredentialState.Declined) return credentialRecord } @@ -384,24 +380,28 @@ export class CredentialsApi implements Credent public async negotiateOffer(options: NegotiateCredentialOfferOptions): Promise { const credentialRecord = await this.getById(options.credentialRecordId) - const service = this.getProtocol(credentialRecord.protocolVersion) - const { message } = await service.negotiateOffer(this.agentContext, { - credentialFormats: options.credentialFormats, - credentialRecord, - comment: options.comment, - autoAcceptCredential: options.autoAcceptCredential, - }) - if (!credentialRecord.connectionId) { throw new AriesFrameworkError( `No connection id for credential record ${credentialRecord.id} not found. Connection-less issuance does not support negotiation` ) } - const connection = await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) + const connectionRecord = await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) + + // Assert + connectionRecord.assertReady() + + const protocol = this.getProtocol(credentialRecord.protocolVersion) + const { message } = await protocol.negotiateOffer(this.agentContext, { + credentialFormats: options.credentialFormats, + credentialRecord, + comment: options.comment, + autoAcceptCredential: options.autoAcceptCredential, + }) + const outboundMessageContext = new OutboundMessageContext(message, { agentContext: this.agentContext, - connection, + connection: connectionRecord, associatedRecord: credentialRecord, }) await this.messageSender.sendMessage(outboundMessageContext) @@ -415,14 +415,14 @@ export class CredentialsApi implements Credent * @param options The credential options to use for the offer * @returns The credential record and credential offer message */ - public async createOffer(options: CreateOfferOptions): Promise<{ + public async createOffer(options: CreateCredentialOfferOptions): Promise<{ message: AgentMessage credentialRecord: CredentialExchangeRecord }> { - const service = this.getProtocol(options.protocolVersion) + const protocol = this.getProtocol(options.protocolVersion) this.logger.debug(`Got a credentialProtocol object for version ${options.protocolVersion}`) - const { message, credentialRecord } = await service.createOffer(this.agentContext, { + const { message, credentialRecord } = await protocol.createOffer(this.agentContext, { credentialFormats: options.credentialFormats, comment: options.comment, autoAcceptCredential: options.autoAcceptCredential, @@ -444,11 +444,11 @@ export class CredentialsApi implements Credent const credentialRecord = await this.getById(options.credentialRecordId) // with version we can get the Service - const service = this.getProtocol(credentialRecord.protocolVersion) + const protocol = this.getProtocol(credentialRecord.protocolVersion) this.logger.debug(`Got a credentialProtocol object for version ${credentialRecord.protocolVersion}`) - const { message } = await service.acceptRequest(this.agentContext, { + const { message } = await protocol.acceptRequest(this.agentContext, { credentialRecord, credentialFormats: options.credentialFormats, comment: options.comment, @@ -456,8 +456,8 @@ export class CredentialsApi implements Credent }) this.logger.debug('We have a credential message (sending outbound): ', message) - const requestMessage = await service.findRequestMessage(this.agentContext, credentialRecord.id) - const offerMessage = await service.findOfferMessage(this.agentContext, credentialRecord.id) + const requestMessage = await protocol.findRequestMessage(this.agentContext, credentialRecord.id) + const offerMessage = await protocol.findOfferMessage(this.agentContext, credentialRecord.id) // Use connection if present if (credentialRecord.connectionId) { @@ -516,16 +516,16 @@ export class CredentialsApi implements Credent const credentialRecord = await this.getById(options.credentialRecordId) // with version we can get the Service - const service = this.getProtocol(credentialRecord.protocolVersion) + const protocol = this.getProtocol(credentialRecord.protocolVersion) this.logger.debug(`Got a credentialProtocol object for version ${credentialRecord.protocolVersion}`) - const { message } = await service.acceptCredential(this.agentContext, { + const { message } = await protocol.acceptCredential(this.agentContext, { credentialRecord, }) - const requestMessage = await service.findRequestMessage(this.agentContext, credentialRecord.id) - const credentialMessage = await service.findCredentialMessage(this.agentContext, credentialRecord.id) + const requestMessage = await protocol.findRequestMessage(this.agentContext, credentialRecord.id) + const credentialMessage = await protocol.findCredentialMessage(this.agentContext, credentialRecord.id) if (credentialRecord.connectionId) { const connection = await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) @@ -578,12 +578,15 @@ export class CredentialsApi implements Credent } const connection = await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) - const service = this.getProtocol(credentialRecord.protocolVersion) - const problemReportMessage = service.createProblemReport(this.agentContext, { message: options.message }) - problemReportMessage.setThread({ + const protocol = this.getProtocol(credentialRecord.protocolVersion) + const { message } = await protocol.createProblemReport(this.agentContext, { + description: options.description, + credentialRecord, + }) + message.setThread({ threadId: credentialRecord.threadId, }) - const outboundMessageContext = new OutboundMessageContext(problemReportMessage, { + const outboundMessageContext = new OutboundMessageContext(message, { agentContext: this.agentContext, connection, associatedRecord: credentialRecord, @@ -595,11 +598,11 @@ export class CredentialsApi implements Credent public async getFormatData( credentialRecordId: string - ): Promise>> { + ): Promise>> { const credentialRecord = await this.getById(credentialRecordId) - const service = this.getProtocol(credentialRecord.protocolVersion) + const protocol = this.getProtocol(credentialRecord.protocolVersion) - return service.getFormatData(this.agentContext, credentialRecordId) + return protocol.getFormatData(this.agentContext, credentialRecordId) } /** @@ -650,8 +653,8 @@ export class CredentialsApi implements Credent */ public async deleteById(credentialId: string, options?: DeleteCredentialOptions) { const credentialRecord = await this.getById(credentialId) - const service = this.getProtocol(credentialRecord.protocolVersion) - return service.delete(this.agentContext, credentialRecord, options) + const protocol = this.getProtocol(credentialRecord.protocolVersion) + return protocol.delete(this.agentContext, credentialRecord, options) } /** @@ -664,33 +667,33 @@ export class CredentialsApi implements Credent } public async findProposalMessage(credentialExchangeId: string): Promise> { - const service = await this.getServiceForCredentialExchangeId(credentialExchangeId) + const protocol = await this.getServiceForCredentialExchangeId(credentialExchangeId) - return service.findProposalMessage( + return protocol.findProposalMessage( this.agentContext, credentialExchangeId ) as FindCredentialProposalMessageReturn } public async findOfferMessage(credentialExchangeId: string): Promise> { - const service = await this.getServiceForCredentialExchangeId(credentialExchangeId) + const protocol = await this.getServiceForCredentialExchangeId(credentialExchangeId) - return service.findOfferMessage(this.agentContext, credentialExchangeId) as FindCredentialOfferMessageReturn + return protocol.findOfferMessage(this.agentContext, credentialExchangeId) as FindCredentialOfferMessageReturn } public async findRequestMessage(credentialExchangeId: string): Promise> { - const service = await this.getServiceForCredentialExchangeId(credentialExchangeId) + const protocol = await this.getServiceForCredentialExchangeId(credentialExchangeId) - return service.findRequestMessage( + return protocol.findRequestMessage( this.agentContext, credentialExchangeId ) as FindCredentialRequestMessageReturn } public async findCredentialMessage(credentialExchangeId: string): Promise> { - const service = await this.getServiceForCredentialExchangeId(credentialExchangeId) + const protocol = await this.getServiceForCredentialExchangeId(credentialExchangeId) - return service.findCredentialMessage(this.agentContext, credentialExchangeId) as FindCredentialMessageReturn + return protocol.findCredentialMessage(this.agentContext, credentialExchangeId) as FindCredentialMessageReturn } private async getServiceForCredentialExchangeId(credentialExchangeId: string) { diff --git a/packages/core/src/modules/credentials/CredentialsApiOptions.ts b/packages/core/src/modules/credentials/CredentialsApiOptions.ts index 24fb0a86d1..57af50851f 100644 --- a/packages/core/src/modules/credentials/CredentialsApiOptions.ts +++ b/packages/core/src/modules/credentials/CredentialsApiOptions.ts @@ -1,10 +1,14 @@ -import type { CredentialFormatsFromProtocols, GetFormatDataReturn } from './CredentialProtocolOptions' import type { CredentialFormatPayload } from './formats' -import type { AutoAcceptCredential } from './models/CredentialAutoAcceptType' +import type { AutoAcceptCredential } from './models' import type { CredentialProtocol } from './protocol/CredentialProtocol' +import type { + CredentialFormatsFromProtocols, + DeleteCredentialOptions, + GetCredentialFormatDataReturn, +} from './protocol/CredentialProtocolOptions' -// re-export GetFormatDataReturn type from service, as it is also used in the module -export type { GetFormatDataReturn } +// re-export GetFormatDataReturn type from protocol, as it is also used in the api +export type { GetCredentialFormatDataReturn, DeleteCredentialOptions } export type FindCredentialProposalMessageReturn = ReturnType< CPs[number]['findProposalMessage'] @@ -25,23 +29,6 @@ export type FindCredentialMessageReturn = CPs[number]['version'] -/** - * Get the service map for usage in the credentials module. Will return a type mapping of protocol version to service. - * - * @example - * ``` - * type ProtocolMap = CredentialProtocolMap<[IndyCredentialFormatService], [V1CredentialProtocol]> - * - * // equal to - * type ProtocolMap = { - * v1: V1CredentialProtocol - * } - * ``` - */ -export type CredentialProtocolMap = { - [CP in CPs[number] as CP['version']]: CredentialProtocol -} - interface BaseOptions { autoAcceptCredential?: AutoAcceptCredential comment?: string @@ -79,17 +66,18 @@ export interface NegotiateCredentialProposalOptions extends BaseOptions { +export interface CreateCredentialOfferOptions + extends BaseOptions { protocolVersion: CredentialProtocolVersionType credentialFormats: CredentialFormatPayload, 'createOffer'> } /** - * Interface for CredentialsApi.offerCredentials. Extends CreateOfferOptions, will send an offer + * Interface for CredentialsApi.offerCredential. Extends CreateCredentialOfferOptions, will send an offer */ export interface OfferCredentialOptions extends BaseOptions, - CreateOfferOptions { + CreateCredentialOfferOptions { connectionId: string } @@ -138,5 +126,5 @@ export interface AcceptCredentialOptions { */ export interface SendCredentialProblemReportOptions { credentialRecordId: string - message: string + description: string } diff --git a/packages/core/src/modules/credentials/CredentialsModule.ts b/packages/core/src/modules/credentials/CredentialsModule.ts index b141f63f8a..a7d762e248 100644 --- a/packages/core/src/modules/credentials/CredentialsModule.ts +++ b/packages/core/src/modules/credentials/CredentialsModule.ts @@ -20,7 +20,7 @@ import { CredentialRepository } from './repository' */ export type DefaultCredentialProtocols = [V1CredentialProtocol, V2CredentialProtocol] -// CredentialModuleOptions makes the credentialProtocols property optional from the config, as it will set it when not provided. +// CredentialsModuleOptions makes the credentialProtocols property optional from the config, as it will set it when not provided. export type CredentialsModuleOptions = Optional< CredentialsModuleConfigOptions, 'credentialProtocols' @@ -38,7 +38,7 @@ export class CredentialsModule } diff --git a/packages/core/src/modules/credentials/formats/CredentialFormatService.ts b/packages/core/src/modules/credentials/formats/CredentialFormatService.ts index 20d98623d3..ac1ffde0a9 100644 --- a/packages/core/src/modules/credentials/formats/CredentialFormatService.ts +++ b/packages/core/src/modules/credentials/formats/CredentialFormatService.ts @@ -1,23 +1,22 @@ import type { CredentialFormat } from './CredentialFormat' import type { - FormatCreateProposalOptions, - FormatCreateProposalReturn, - FormatProcessOptions, - FormatCreateOfferOptions, - FormatCreateOfferReturn, - FormatCreateRequestOptions, + CredentialFormatCreateProposalOptions, + CredentialFormatCreateProposalReturn, + CredentialFormatProcessOptions, + CredentialFormatCreateOfferOptions, + CredentialFormatCreateOfferReturn, + CredentialFormatCreateRequestOptions, CredentialFormatCreateReturn, - FormatAcceptRequestOptions, - FormatAcceptOfferOptions, - FormatAcceptProposalOptions, - FormatAutoRespondCredentialOptions, - FormatAutoRespondOfferOptions, - FormatAutoRespondProposalOptions, - FormatAutoRespondRequestOptions, - FormatProcessCredentialOptions, + CredentialFormatAcceptRequestOptions, + CredentialFormatAcceptOfferOptions, + CredentialFormatAcceptProposalOptions, + CredentialFormatAutoRespondCredentialOptions, + CredentialFormatAutoRespondOfferOptions, + CredentialFormatAutoRespondProposalOptions, + CredentialFormatAutoRespondRequestOptions, + CredentialFormatProcessCredentialOptions, } from './CredentialFormatServiceOptions' import type { AgentContext } from '../../../agent' -import type { Attachment } from '../../../decorators/attachment/Attachment' export interface CredentialFormatService { formatKey: CF['formatKey'] @@ -26,39 +25,58 @@ export interface CredentialFormatService - ): Promise - processProposal(agentContext: AgentContext, options: FormatProcessOptions): Promise - acceptProposal(agentContext: AgentContext, options: FormatAcceptProposalOptions): Promise + options: CredentialFormatCreateProposalOptions + ): Promise + processProposal(agentContext: AgentContext, options: CredentialFormatProcessOptions): Promise + acceptProposal( + agentContext: AgentContext, + options: CredentialFormatAcceptProposalOptions + ): Promise // offer methods - createOffer(agentContext: AgentContext, options: FormatCreateOfferOptions): Promise - processOffer(agentContext: AgentContext, options: FormatProcessOptions): Promise - acceptOffer(agentContext: AgentContext, options: FormatAcceptOfferOptions): Promise + createOffer( + agentContext: AgentContext, + options: CredentialFormatCreateOfferOptions + ): Promise + processOffer(agentContext: AgentContext, options: CredentialFormatProcessOptions): Promise + acceptOffer( + agentContext: AgentContext, + options: CredentialFormatAcceptOfferOptions + ): Promise // request methods createRequest( agentContext: AgentContext, - options: FormatCreateRequestOptions + options: CredentialFormatCreateRequestOptions ): Promise - processRequest(agentContext: AgentContext, options: FormatProcessOptions): Promise + processRequest(agentContext: AgentContext, options: CredentialFormatProcessOptions): Promise acceptRequest( agentContext: AgentContext, - options: FormatAcceptRequestOptions + options: CredentialFormatAcceptRequestOptions ): Promise // credential methods - processCredential(agentContext: AgentContext, options: FormatProcessCredentialOptions): Promise + processCredential(agentContext: AgentContext, options: CredentialFormatProcessCredentialOptions): Promise // auto accept methods - shouldAutoRespondToProposal(agentContext: AgentContext, options: FormatAutoRespondProposalOptions): boolean - shouldAutoRespondToOffer(agentContext: AgentContext, options: FormatAutoRespondOfferOptions): boolean - shouldAutoRespondToRequest(agentContext: AgentContext, options: FormatAutoRespondRequestOptions): boolean - shouldAutoRespondToCredential(agentContext: AgentContext, options: FormatAutoRespondCredentialOptions): boolean + shouldAutoRespondToProposal( + agentContext: AgentContext, + options: CredentialFormatAutoRespondProposalOptions + ): Promise + shouldAutoRespondToOffer( + agentContext: AgentContext, + options: CredentialFormatAutoRespondOfferOptions + ): Promise + shouldAutoRespondToRequest( + agentContext: AgentContext, + options: CredentialFormatAutoRespondRequestOptions + ): Promise + shouldAutoRespondToCredential( + agentContext: AgentContext, + options: CredentialFormatAutoRespondCredentialOptions + ): Promise deleteCredentialById(agentContext: AgentContext, credentialId: string): Promise - supportsFormat(format: string): boolean - - getFormatData(data: unknown, id: string): Attachment + supportsFormat(formatIdentifier: string): boolean } diff --git a/packages/core/src/modules/credentials/formats/CredentialFormatServiceOptions.ts b/packages/core/src/modules/credentials/formats/CredentialFormatServiceOptions.ts index 2f494da4ae..2d79961ebb 100644 --- a/packages/core/src/modules/credentials/formats/CredentialFormatServiceOptions.ts +++ b/packages/core/src/modules/credentials/formats/CredentialFormatServiceOptions.ts @@ -46,93 +46,88 @@ export interface CredentialFormatCreateReturn { } /** - * Base return type for all process methods. + * Base return type for all credential process methods. */ -export interface FormatProcessOptions { +export interface CredentialFormatProcessOptions { attachment: Attachment credentialRecord: CredentialExchangeRecord } -export interface FormatProcessCredentialOptions extends FormatProcessOptions { +export interface CredentialFormatProcessCredentialOptions extends CredentialFormatProcessOptions { requestAttachment: Attachment } -export interface FormatCreateProposalOptions { +export interface CredentialFormatCreateProposalOptions { credentialRecord: CredentialExchangeRecord credentialFormats: CredentialFormatPayload<[CF], 'createProposal'> + attachmentId?: string } -export interface FormatAcceptProposalOptions { +export interface CredentialFormatAcceptProposalOptions { credentialRecord: CredentialExchangeRecord credentialFormats?: CredentialFormatPayload<[CF], 'acceptProposal'> - attachId?: string + attachmentId?: string proposalAttachment: Attachment } -export interface FormatCreateProposalReturn extends CredentialFormatCreateReturn { +export interface CredentialFormatCreateProposalReturn extends CredentialFormatCreateReturn { previewAttributes?: CredentialPreviewAttribute[] } -export interface FormatCreateOfferOptions { +export interface CredentialFormatCreateOfferOptions { credentialRecord: CredentialExchangeRecord credentialFormats: CredentialFormatPayload<[CF], 'createOffer'> - attachId?: string + attachmentId?: string } -export interface FormatAcceptOfferOptions { +export interface CredentialFormatAcceptOfferOptions { credentialRecord: CredentialExchangeRecord credentialFormats?: CredentialFormatPayload<[CF], 'acceptOffer'> - attachId?: string + attachmentId?: string offerAttachment: Attachment } -export interface FormatCreateOfferReturn extends CredentialFormatCreateReturn { +export interface CredentialFormatCreateOfferReturn extends CredentialFormatCreateReturn { previewAttributes?: CredentialPreviewAttribute[] } -export interface FormatCreateRequestOptions { +export interface CredentialFormatCreateRequestOptions { credentialRecord: CredentialExchangeRecord credentialFormats: CredentialFormatPayload<[CF], 'createRequest'> } -export interface FormatAcceptRequestOptions { +export interface CredentialFormatAcceptRequestOptions { credentialRecord: CredentialExchangeRecord credentialFormats?: CredentialFormatPayload<[CF], 'acceptRequest'> - attachId?: string + attachmentId?: string requestAttachment: Attachment offerAttachment?: Attachment } -export interface FormatAcceptCredentialOptions { - credentialRecord: CredentialExchangeRecord - attachId?: string - requestAttachment: Attachment - offerAttachment?: Attachment -} // Auto accept method interfaces -export interface FormatAutoRespondProposalOptions { +export interface CredentialFormatAutoRespondProposalOptions { credentialRecord: CredentialExchangeRecord proposalAttachment: Attachment offerAttachment: Attachment } -export interface FormatAutoRespondOfferOptions { +export interface CredentialFormatAutoRespondOfferOptions { credentialRecord: CredentialExchangeRecord proposalAttachment: Attachment offerAttachment: Attachment } -export interface FormatAutoRespondRequestOptions { +export interface CredentialFormatAutoRespondRequestOptions { credentialRecord: CredentialExchangeRecord proposalAttachment?: Attachment offerAttachment: Attachment requestAttachment: Attachment } -export interface FormatAutoRespondCredentialOptions { +export interface CredentialFormatAutoRespondCredentialOptions { credentialRecord: CredentialExchangeRecord proposalAttachment?: Attachment offerAttachment?: Attachment diff --git a/packages/core/src/modules/credentials/formats/__tests__/IndyCredentialFormatService.test.ts b/packages/core/src/modules/credentials/formats/__tests__/IndyCredentialFormatService.test.ts index 90b6db58c0..bbe6f379f8 100644 --- a/packages/core/src/modules/credentials/formats/__tests__/IndyCredentialFormatService.test.ts +++ b/packages/core/src/modules/credentials/formats/__tests__/IndyCredentialFormatService.test.ts @@ -136,7 +136,7 @@ const mockCredentialRecord = ({ id: '', formats: [ { - attachId: INDY_CREDENTIAL_OFFER_ATTACHMENT_ID, + attachmentId: INDY_CREDENTIAL_OFFER_ATTACHMENT_ID, format: 'hlindy/cred-abstract@v2.0', }, ], @@ -248,7 +248,7 @@ describe('Indy CredentialFormatService', () => { ]) expect(format).toMatchObject({ - attachId: expect.any(String), + attachmentId: expect.any(String), format: 'hlindy/cred-filter@v2.0', }) }) @@ -299,7 +299,7 @@ describe('Indy CredentialFormatService', () => { ]) expect(format).toMatchObject({ - attachId: expect.any(String), + attachmentId: expect.any(String), format: 'hlindy/cred-abstract@v2.0', }) }) @@ -360,7 +360,7 @@ describe('Indy CredentialFormatService', () => { }, }) expect(format).toMatchObject({ - attachId: expect.any(String), + attachmentId: expect.any(String), format: 'hlindy/cred-req@v2.0', }) @@ -387,7 +387,7 @@ describe('Indy CredentialFormatService', () => { credentialRecord, requestAttachment, offerAttachment, - attachId: INDY_CREDENTIAL_ATTACHMENT_ID, + attachmentId: INDY_CREDENTIAL_ATTACHMENT_ID, }) expect(attachment).toMatchObject({ @@ -407,7 +407,7 @@ describe('Indy CredentialFormatService', () => { }, }) expect(format).toMatchObject({ - attachId: expect.any(String), + attachmentId: expect.any(String), format: 'hlindy/cred@v2.0', }) }) diff --git a/packages/core/src/modules/credentials/formats/__tests__/JsonLdCredentialFormatService.test.ts b/packages/core/src/modules/credentials/formats/__tests__/JsonLdCredentialFormatService.test.ts index b94e44651b..59761f5808 100644 --- a/packages/core/src/modules/credentials/formats/__tests__/JsonLdCredentialFormatService.test.ts +++ b/packages/core/src/modules/credentials/formats/__tests__/JsonLdCredentialFormatService.test.ts @@ -117,7 +117,7 @@ const mockCredentialRecord = ({ id: '', formats: [ { - attachId: INDY_CREDENTIAL_OFFER_ATTACHMENT_ID, + attachmentId: INDY_CREDENTIAL_OFFER_ATTACHMENT_ID, format: 'hlindy/cred-abstract@v2.0', }, ], @@ -220,7 +220,7 @@ describe('JsonLd CredentialFormatService', () => { }) expect(format).toMatchObject({ - attachId: expect.any(String), + attachmentId: expect.any(String), format: 'aries/ld-proof-vc-detail@v1.0', }) }) @@ -255,7 +255,7 @@ describe('JsonLd CredentialFormatService', () => { expect(previewAttributes).toBeUndefined() expect(format).toMatchObject({ - attachId: expect.any(String), + attachmentId: expect.any(String), format: 'aries/ld-proof-vc-detail@v1.0', }) }) @@ -294,7 +294,7 @@ describe('JsonLd CredentialFormatService', () => { }, }) expect(format).toMatchObject({ - attachId: expect.any(String), + attachmentId: expect.any(String), format: 'aries/ld-proof-vc-detail@v1.0', }) }) @@ -362,7 +362,7 @@ describe('JsonLd CredentialFormatService', () => { }, }) expect(format).toMatchObject({ - attachId: expect.any(String), + attachmentId: expect.any(String), format: 'aries/ld-proof-vc@1.0', }) }) @@ -552,7 +552,7 @@ describe('JsonLd CredentialFormatService', () => { }) // indirectly test areCredentialsEqual as black box rather than expose that method in the API - let areCredentialsEqual = jsonLdFormatService.shouldAutoRespondToProposal(agentContext, { + let areCredentialsEqual = await jsonLdFormatService.shouldAutoRespondToProposal(agentContext, { credentialRecord, proposalAttachment: message1, offerAttachment: message2, @@ -570,7 +570,7 @@ describe('JsonLd CredentialFormatService', () => { base64: JsonEncoder.toBase64(inputDoc2), }) - areCredentialsEqual = jsonLdFormatService.shouldAutoRespondToProposal(agentContext, { + areCredentialsEqual = await jsonLdFormatService.shouldAutoRespondToProposal(agentContext, { credentialRecord, proposalAttachment: message1, offerAttachment: message2, diff --git a/packages/core/src/modules/credentials/formats/indy/IndyCredentialFormatService.ts b/packages/core/src/modules/credentials/formats/indy/IndyCredentialFormatService.ts index aca8aec43a..bee27e8983 100644 --- a/packages/core/src/modules/credentials/formats/indy/IndyCredentialFormatService.ts +++ b/packages/core/src/modules/credentials/formats/indy/IndyCredentialFormatService.ts @@ -5,20 +5,20 @@ import type { CredentialPreviewAttributeOptions } from '../../models/CredentialP import type { CredentialExchangeRecord } from '../../repository/CredentialExchangeRecord' import type { CredentialFormatService } from '../CredentialFormatService' import type { - FormatAcceptOfferOptions, - FormatAcceptProposalOptions, - FormatAcceptRequestOptions, - FormatAutoRespondCredentialOptions, - FormatAutoRespondOfferOptions, - FormatAutoRespondProposalOptions, - FormatAutoRespondRequestOptions, - FormatCreateOfferOptions, - FormatCreateOfferReturn, - FormatCreateProposalOptions, - FormatCreateProposalReturn, + CredentialFormatAcceptOfferOptions, + CredentialFormatAcceptProposalOptions, + CredentialFormatAcceptRequestOptions, + CredentialFormatAutoRespondCredentialOptions, + CredentialFormatAutoRespondOfferOptions, + CredentialFormatAutoRespondProposalOptions, + CredentialFormatAutoRespondRequestOptions, + CredentialFormatCreateOfferOptions, + CredentialFormatCreateOfferReturn, + CredentialFormatCreateProposalOptions, + CredentialFormatCreateProposalReturn, CredentialFormatCreateReturn, - FormatProcessOptions, - FormatProcessCredentialOptions, + CredentialFormatProcessOptions, + CredentialFormatProcessCredentialOptions, } from '../CredentialFormatServiceOptions' import type * as Indy from 'indy-sdk' @@ -62,10 +62,11 @@ export class IndyCredentialFormatService implements CredentialFormatService - ): Promise { + { credentialFormats, credentialRecord, attachmentId }: CredentialFormatCreateProposalOptions + ): Promise { const format = new CredentialFormatSpec({ format: INDY_CRED_FILTER, + attachmentId, }) const indyFormat = credentialFormats.indy @@ -86,7 +87,7 @@ export class IndyCredentialFormatService implements CredentialFormatService { + public async processProposal( + agentContext: AgentContext, + { attachment }: CredentialFormatProcessOptions + ): Promise { const proposalJson = attachment.getDataAsJson() // fromJSON also validates @@ -112,12 +116,12 @@ export class IndyCredentialFormatService implements CredentialFormatService - ): Promise { + }: CredentialFormatAcceptProposalOptions + ): Promise { const indyFormat = credentialFormats?.indy const credentialProposal = JsonTransformer.fromJSON(proposalAttachment.getDataAsJson(), IndyCredPropose) @@ -137,7 +141,7 @@ export class IndyCredentialFormatService implements CredentialFormatService - ): Promise { + { credentialFormats, credentialRecord, attachmentId }: CredentialFormatCreateOfferOptions + ): Promise { const indyFormat = credentialFormats.indy if (!indyFormat) { @@ -165,7 +169,7 @@ export class IndyCredentialFormatService implements CredentialFormatService() @@ -188,7 +195,12 @@ export class IndyCredentialFormatService implements CredentialFormatService + { + credentialFormats, + credentialRecord, + attachmentId, + offerAttachment, + }: CredentialFormatAcceptOfferOptions ): Promise { const indyFormat = credentialFormats?.indy @@ -219,11 +231,11 @@ export class IndyCredentialFormatService implements CredentialFormatService { + public async processRequest(agentContext: AgentContext, options: CredentialFormatProcessOptions): Promise { // not needed for Indy } public async acceptRequest( agentContext: AgentContext, - { credentialRecord, attachId, offerAttachment, requestAttachment }: FormatAcceptRequestOptions + { + credentialRecord, + attachmentId, + offerAttachment, + requestAttachment, + }: CredentialFormatAcceptRequestOptions ): Promise { // Assert credential attributes const credentialAttributes = credentialRecord.credentialAttributes @@ -278,11 +295,11 @@ export class IndyCredentialFormatService implements CredentialFormatService { const credentialRequestMetadata = credentialRecord.metadata.get(CredentialMetadataKeys.IndyRequest) @@ -357,15 +374,15 @@ export class IndyCredentialFormatService implements CredentialFormatService this.supportsFormat(f.format)).map((f) => f.attachId) + const supportedAttachmentIds = formats.filter((f) => this.supportsFormat(f.format)).map((f) => f.attachmentId) const supportedAttachments = messageAttachments.filter((attachment) => supportedAttachmentIds.includes(attachment.id) ) @@ -379,9 +396,9 @@ export class IndyCredentialFormatService implements CredentialFormatService() const credentialRequestJson = requestAttachment.getDataAsJson() @@ -419,9 +436,9 @@ export class IndyCredentialFormatService implements CredentialFormatService() const credentialRequestJson = requestAttachment.getDataAsJson() @@ -441,23 +458,23 @@ export class IndyCredentialFormatService implements CredentialFormatService { + ): Promise { const indyIssuerService = agentContext.dependencyManager.resolve(IndyIssuerService) // if the proposal has an attachment Id use that, otherwise the generated id of the formats object const format = new CredentialFormatSpec({ - attachId: attachId, + attachmentId, format: INDY_CRED_ABSTRACT, }) @@ -475,7 +492,7 @@ export class IndyCredentialFormatService implements CredentialFormatService + schemaId: string + credentialDefinitionId: string + revocationRegistryId?: string + credentialRevocationId?: string +} + export class IndyCredentialInfo { - public constructor(options: IndyCredentialInfo) { + public constructor(options: IndyCredentialInfoOptions) { if (options) { this.referent = options.referent this.attributes = options.attributes diff --git a/packages/core/src/modules/credentials/formats/jsonld/JsonLdCredentialFormatService.ts b/packages/core/src/modules/credentials/formats/jsonld/JsonLdCredentialFormatService.ts index 36f88eb4db..52be8f6493 100644 --- a/packages/core/src/modules/credentials/formats/jsonld/JsonLdCredentialFormatService.ts +++ b/packages/core/src/modules/credentials/formats/jsonld/JsonLdCredentialFormatService.ts @@ -7,21 +7,21 @@ import type { import type { AgentContext } from '../../../../agent' import type { CredentialFormatService } from '../CredentialFormatService' import type { - FormatAcceptOfferOptions, - FormatAcceptProposalOptions, - FormatAcceptRequestOptions, - FormatAutoRespondOfferOptions, - FormatAutoRespondProposalOptions, - FormatAutoRespondRequestOptions, - FormatCreateOfferOptions, - FormatCreateOfferReturn, - FormatCreateProposalOptions, - FormatCreateProposalReturn, - FormatCreateRequestOptions, + CredentialFormatAcceptOfferOptions, + CredentialFormatAcceptProposalOptions, + CredentialFormatAcceptRequestOptions, + CredentialFormatAutoRespondOfferOptions, + CredentialFormatAutoRespondProposalOptions, + CredentialFormatAutoRespondRequestOptions, + CredentialFormatCreateOfferOptions, + CredentialFormatCreateOfferReturn, + CredentialFormatCreateProposalOptions, + CredentialFormatCreateProposalReturn, + CredentialFormatCreateRequestOptions, CredentialFormatCreateReturn, - FormatProcessCredentialOptions, - FormatProcessOptions, - FormatAutoRespondCredentialOptions, + CredentialFormatProcessCredentialOptions, + CredentialFormatProcessOptions, + CredentialFormatAutoRespondCredentialOptions, } from '../CredentialFormatServiceOptions' import { Attachment, AttachmentData } from '../../../../decorators/attachment/Attachment' @@ -52,8 +52,8 @@ export class JsonLdCredentialFormatService implements CredentialFormatService - ): Promise { + { credentialFormats }: CredentialFormatCreateProposalOptions + ): Promise { const format = new CredentialFormatSpec({ format: JSONLD_VC_DETAIL, }) @@ -67,7 +67,7 @@ export class JsonLdCredentialFormatService implements CredentialFormatService { + public async processProposal( + agentContext: AgentContext, + { attachment }: CredentialFormatProcessOptions + ): Promise { const credProposalJson = attachment.getDataAsJson() if (!credProposalJson) { @@ -88,11 +91,11 @@ export class JsonLdCredentialFormatService implements CredentialFormatService - ): Promise { + { attachmentId, proposalAttachment }: CredentialFormatAcceptProposalOptions + ): Promise { // if the offer has an attachment Id use that, otherwise the generated id of the formats object const format = new CredentialFormatSpec({ - attachId, + attachmentId, format: JSONLD_VC_DETAIL, }) @@ -101,7 +104,7 @@ export class JsonLdCredentialFormatService implements CredentialFormatService - ): Promise { + { credentialFormats, attachmentId }: CredentialFormatCreateOfferOptions + ): Promise { // if the offer has an attachment Id use that, otherwise the generated id of the formats object const format = new CredentialFormatSpec({ - attachId, + attachmentId, format: JSONLD_VC_DETAIL, }) @@ -131,12 +134,12 @@ export class JsonLdCredentialFormatService implements CredentialFormatService() if (!credentialOfferJson) { @@ -148,7 +151,7 @@ export class JsonLdCredentialFormatService implements CredentialFormatService + { attachmentId, offerAttachment }: CredentialFormatAcceptOfferOptions ): Promise { const credentialOffer = offerAttachment.getDataAsJson() @@ -156,11 +159,11 @@ export class JsonLdCredentialFormatService implements CredentialFormatService + { credentialFormats }: CredentialFormatCreateRequestOptions ): Promise { const jsonLdFormat = credentialFormats?.jsonld @@ -188,12 +191,15 @@ export class JsonLdCredentialFormatService implements CredentialFormatService { + public async processRequest( + agentContext: AgentContext, + { attachment }: CredentialFormatProcessOptions + ): Promise { const requestJson = attachment.getDataAsJson() if (!requestJson) { @@ -206,7 +212,7 @@ export class JsonLdCredentialFormatService implements CredentialFormatService + { credentialFormats, attachmentId, requestAttachment }: CredentialFormatAcceptRequestOptions ): Promise { const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) @@ -222,7 +228,7 @@ export class JsonLdCredentialFormatService implements CredentialFormatService { const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) @@ -385,30 +391,30 @@ export class JsonLdCredentialFormatService implements CredentialFormatService() const w3cCredential = JsonTransformer.fromJSON(credentialJson, W3cVerifiableCredential) @@ -432,7 +438,7 @@ export class JsonLdCredentialFormatService implements CredentialFormatService + options: CreateCredentialProposalOptions ): Promise> public abstract processProposal( messageContext: InboundMessageContext ): Promise public abstract acceptProposal( agentContext: AgentContext, - options: AcceptProposalOptions + options: AcceptCredentialProposalOptions ): Promise> public abstract negotiateProposal( agentContext: AgentContext, - options: NegotiateProposalOptions + options: NegotiateCredentialProposalOptions ): Promise> // methods for offer public abstract createOffer( agentContext: AgentContext, - options: CreateOfferOptions + options: CreateCredentialOfferOptions ): Promise> public abstract processOffer(messageContext: InboundMessageContext): Promise public abstract acceptOffer( agentContext: AgentContext, - options: AcceptOfferOptions + options: AcceptCredentialOfferOptions ): Promise> public abstract negotiateOffer( agentContext: AgentContext, - options: NegotiateOfferOptions + options: NegotiateCredentialOfferOptions ): Promise> // methods for request public abstract createRequest( agentContext: AgentContext, - options: CreateRequestOptions + options: CreateCredentialRequestOptions ): Promise> public abstract processRequest(messageContext: InboundMessageContext): Promise public abstract acceptRequest( agentContext: AgentContext, - options: AcceptRequestOptions + options: AcceptCredentialRequestOptions ): Promise> // methods for issue @@ -101,8 +101,8 @@ export abstract class BaseCredentialProtocol> public abstract findProposalMessage( agentContext: AgentContext, @@ -123,25 +123,10 @@ export abstract class BaseCredentialProtocol>> + ): Promise>> public abstract register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry): void - /** - * Decline a credential offer - * @param credentialRecord The credential to be declined - */ - public async declineOffer( - agentContext: AgentContext, - credentialRecord: CredentialExchangeRecord - ): Promise { - credentialRecord.assertState(CredentialState.OfferReceived) - - await this.updateState(agentContext, credentialRecord, CredentialState.Declined) - - return credentialRecord - } - /** * Process a received credential {@link ProblemReportMessage}. * @@ -155,17 +140,17 @@ export abstract class BaseCredentialProtocol { + public findById(agentContext: AgentContext, proofRecordId: string): Promise { const credentialRepository = agentContext.dependencyManager.resolve(CredentialRepository) - return credentialRepository.findById(agentContext, connectionId) + return credentialRepository.findById(agentContext, proofRecordId) } public async delete( diff --git a/packages/core/src/modules/credentials/protocol/CredentialProtocol.ts b/packages/core/src/modules/credentials/protocol/CredentialProtocol.ts index 77665a8236..b91939bbcf 100644 --- a/packages/core/src/modules/credentials/protocol/CredentialProtocol.ts +++ b/packages/core/src/modules/credentials/protocol/CredentialProtocol.ts @@ -1,3 +1,18 @@ +import type { + CreateCredentialProposalOptions, + CredentialProtocolMsgReturnType, + DeleteCredentialOptions, + AcceptCredentialProposalOptions, + NegotiateCredentialProposalOptions, + CreateCredentialOfferOptions, + NegotiateCredentialOfferOptions, + CreateCredentialRequestOptions, + AcceptCredentialOfferOptions, + AcceptCredentialRequestOptions, + AcceptCredentialOptions, + GetCredentialFormatDataReturn, + CreateCredentialProblemReportOptions, +} from './CredentialProtocolOptions' import type { AgentContext } from '../../../agent' import type { AgentMessage } from '../../../agent/AgentMessage' import type { FeatureRegistry } from '../../../agent/FeatureRegistry' @@ -5,21 +20,6 @@ import type { InboundMessageContext } from '../../../agent/models/InboundMessage import type { DependencyManager } from '../../../plugins' import type { Query } from '../../../storage/StorageService' import type { ProblemReportMessage } from '../../problem-reports' -import type { - CreateProposalOptions, - CredentialProtocolMsgReturnType, - DeleteCredentialOptions, - AcceptProposalOptions, - NegotiateProposalOptions, - CreateOfferOptions, - NegotiateOfferOptions, - CreateRequestOptions, - AcceptOfferOptions, - AcceptRequestOptions, - AcceptCredentialOptions, - GetFormatDataReturn, - CreateProblemReportOptions, -} from '../CredentialProtocolOptions' import type { CredentialFormatService, ExtractCredentialFormats } from '../formats' import type { CredentialState } from '../models/CredentialState' import type { CredentialExchangeRecord } from '../repository' @@ -30,42 +30,42 @@ export interface CredentialProtocol + options: CreateCredentialProposalOptions ): Promise> processProposal(messageContext: InboundMessageContext): Promise acceptProposal( agentContext: AgentContext, - options: AcceptProposalOptions + options: AcceptCredentialProposalOptions ): Promise> negotiateProposal( agentContext: AgentContext, - options: NegotiateProposalOptions + options: NegotiateCredentialProposalOptions ): Promise> // methods for offer createOffer( agentContext: AgentContext, - options: CreateOfferOptions + options: CreateCredentialOfferOptions ): Promise> processOffer(messageContext: InboundMessageContext): Promise acceptOffer( agentContext: AgentContext, - options: AcceptOfferOptions + options: AcceptCredentialOfferOptions ): Promise> negotiateOffer( agentContext: AgentContext, - options: NegotiateOfferOptions + options: NegotiateCredentialOfferOptions ): Promise> // methods for request createRequest( agentContext: AgentContext, - options: CreateRequestOptions + options: CreateCredentialRequestOptions ): Promise> processRequest(messageContext: InboundMessageContext): Promise acceptRequest( agentContext: AgentContext, - options: AcceptRequestOptions + options: AcceptCredentialRequestOptions ): Promise> // methods for issue @@ -79,7 +79,11 @@ export interface CredentialProtocol): Promise // methods for problem-report - createProblemReport(agentContext: AgentContext, options: CreateProblemReportOptions): ProblemReportMessage + createProblemReport( + agentContext: AgentContext, + options: CreateCredentialProblemReportOptions + ): Promise> + processProblemReport(messageContext: InboundMessageContext): Promise findProposalMessage(agentContext: AgentContext, credentialExchangeId: string): Promise findOfferMessage(agentContext: AgentContext, credentialExchangeId: string): Promise @@ -88,13 +92,7 @@ export interface CredentialProtocol>> - - declineOffer( - agentContext: AgentContext, - credentialRecord: CredentialExchangeRecord - ): Promise - processProblemReport(messageContext: InboundMessageContext): Promise + ): Promise>> // Repository methods updateState( @@ -102,13 +100,13 @@ export interface CredentialProtocol - getById(agentContext: AgentContext, credentialRecordId: string): Promise + getById(agentContext: AgentContext, credentialExchangeId: string): Promise getAll(agentContext: AgentContext): Promise findAllByQuery( agentContext: AgentContext, query: Query ): Promise - findById(agentContext: AgentContext, connectionId: string): Promise + findById(agentContext: AgentContext, credentialExchangeId: string): Promise delete( agentContext: AgentContext, credentialRecord: CredentialExchangeRecord, diff --git a/packages/core/src/modules/credentials/CredentialProtocolOptions.ts b/packages/core/src/modules/credentials/protocol/CredentialProtocolOptions.ts similarity index 69% rename from packages/core/src/modules/credentials/CredentialProtocolOptions.ts rename to packages/core/src/modules/credentials/protocol/CredentialProtocolOptions.ts index 5b934bdb0c..e68129f942 100644 --- a/packages/core/src/modules/credentials/CredentialProtocolOptions.ts +++ b/packages/core/src/modules/credentials/protocol/CredentialProtocolOptions.ts @@ -1,15 +1,15 @@ +import type { CredentialProtocol } from './CredentialProtocol' +import type { AgentMessage } from '../../../agent/AgentMessage' +import type { ConnectionRecord } from '../../connections/repository/ConnectionRecord' import type { CredentialFormat, CredentialFormatPayload, CredentialFormatService, ExtractCredentialFormats, -} from './formats' -import type { CredentialPreviewAttributeOptions } from './models' -import type { AutoAcceptCredential } from './models/CredentialAutoAcceptType' -import type { CredentialProtocol } from './protocol/CredentialProtocol' -import type { CredentialExchangeRecord } from './repository/CredentialExchangeRecord' -import type { AgentMessage } from '../../agent/AgentMessage' -import type { ConnectionRecord } from '../connections/repository/ConnectionRecord' +} from '../formats' +import type { CredentialPreviewAttributeOptions } from '../models' +import type { AutoAcceptCredential } from '../models/CredentialAutoAcceptType' +import type { CredentialExchangeRecord } from '../repository/CredentialExchangeRecord' /** * Get the format data payload for a specific message from a list of CredentialFormat interfaces and a message @@ -20,7 +20,7 @@ import type { ConnectionRecord } from '../connections/repository/ConnectionRecor * @example * ``` * - * type OfferFormatData = FormatDataMessagePayload<[IndyCredentialFormat, JsonLdCredentialFormat], 'offer'> + * type OfferFormatData = FormatDataMessagePayload<[IndyCredentialFormat, JsonLdCredentialFormat], 'createOffer'> * * // equal to * type OfferFormatData = { @@ -33,7 +33,7 @@ import type { ConnectionRecord } from '../connections/repository/ConnectionRecor * } * ``` */ -export type FormatDataMessagePayload< +export type CredentialFormatDataMessagePayload< CFs extends CredentialFormat[] = CredentialFormat[], M extends keyof CredentialFormat['formatData'] = keyof CredentialFormat['formatData'] > = { @@ -80,66 +80,66 @@ export type CredentialFormatsFromProtocols = * } * ``` */ -export type GetFormatDataReturn = { +export type GetCredentialFormatDataReturn = { proposalAttributes?: CredentialPreviewAttributeOptions[] - proposal?: FormatDataMessagePayload - offer?: FormatDataMessagePayload + proposal?: CredentialFormatDataMessagePayload + offer?: CredentialFormatDataMessagePayload offerAttributes?: CredentialPreviewAttributeOptions[] - request?: FormatDataMessagePayload - credential?: FormatDataMessagePayload + request?: CredentialFormatDataMessagePayload + credential?: CredentialFormatDataMessagePayload } -export interface CreateProposalOptions { - connection: ConnectionRecord +export interface CreateCredentialProposalOptions { + connectionRecord: ConnectionRecord credentialFormats: CredentialFormatPayload, 'createProposal'> autoAcceptCredential?: AutoAcceptCredential comment?: string } -export interface AcceptProposalOptions { +export interface AcceptCredentialProposalOptions { credentialRecord: CredentialExchangeRecord credentialFormats?: CredentialFormatPayload, 'acceptProposal'> autoAcceptCredential?: AutoAcceptCredential comment?: string } -export interface NegotiateProposalOptions { +export interface NegotiateCredentialProposalOptions { credentialRecord: CredentialExchangeRecord credentialFormats: CredentialFormatPayload, 'createOffer'> autoAcceptCredential?: AutoAcceptCredential comment?: string } -export interface CreateOfferOptions { +export interface CreateCredentialOfferOptions { // Create offer can also be used for connection-less, so connection is optional - connection?: ConnectionRecord + connectionRecord?: ConnectionRecord credentialFormats: CredentialFormatPayload, 'createOffer'> autoAcceptCredential?: AutoAcceptCredential comment?: string } -export interface AcceptOfferOptions { +export interface AcceptCredentialOfferOptions { credentialRecord: CredentialExchangeRecord credentialFormats?: CredentialFormatPayload, 'acceptOffer'> autoAcceptCredential?: AutoAcceptCredential comment?: string } -export interface NegotiateOfferOptions { +export interface NegotiateCredentialOfferOptions { credentialRecord: CredentialExchangeRecord credentialFormats: CredentialFormatPayload, 'createProposal'> autoAcceptCredential?: AutoAcceptCredential comment?: string } -export interface CreateRequestOptions { - connection: ConnectionRecord +export interface CreateCredentialRequestOptions { + connectionRecord: ConnectionRecord credentialFormats: CredentialFormatPayload, 'createRequest'> autoAcceptCredential?: AutoAcceptCredential comment?: string } -export interface AcceptRequestOptions { +export interface AcceptCredentialRequestOptions { credentialRecord: CredentialExchangeRecord credentialFormats?: CredentialFormatPayload, 'acceptRequest'> autoAcceptCredential?: AutoAcceptCredential @@ -150,8 +150,9 @@ export interface AcceptCredentialOptions { credentialRecord: CredentialExchangeRecord } -export interface CreateProblemReportOptions { - message: string +export interface CreateCredentialProblemReportOptions { + credentialRecord: CredentialExchangeRecord + description: string } export interface CredentialProtocolMsgReturnType { diff --git a/packages/core/src/modules/credentials/protocol/v1/V1CredentialProtocol.ts b/packages/core/src/modules/credentials/protocol/v1/V1CredentialProtocol.ts index 879ef75fd3..59338c7835 100644 --- a/packages/core/src/modules/credentials/protocol/v1/V1CredentialProtocol.ts +++ b/packages/core/src/modules/credentials/protocol/v1/V1CredentialProtocol.ts @@ -4,25 +4,25 @@ import type { FeatureRegistry } from '../../../../agent/FeatureRegistry' import type { InboundMessageContext } from '../../../../agent/models/InboundMessageContext' import type { DependencyManager } from '../../../../plugins' import type { ProblemReportMessage } from '../../../problem-reports' +import type { GetCredentialFormatDataReturn } from '../../CredentialsApiOptions' +import type { CredentialFormatService, ExtractCredentialFormats, IndyCredentialFormat } from '../../formats' +import type { CredentialProtocol } from '../CredentialProtocol' import type { AcceptCredentialOptions, - AcceptOfferOptions, - AcceptProposalOptions, - AcceptRequestOptions, - CreateOfferOptions, - CreateProblemReportOptions, - CreateProposalOptions, + AcceptCredentialOfferOptions, + AcceptCredentialProposalOptions, + AcceptCredentialRequestOptions, + CreateCredentialOfferOptions, + CreateCredentialProblemReportOptions, + CreateCredentialProposalOptions, CredentialProtocolMsgReturnType, - NegotiateOfferOptions, - NegotiateProposalOptions, -} from '../../CredentialProtocolOptions' -import type { GetFormatDataReturn } from '../../CredentialsApiOptions' -import type { CredentialFormatService, ExtractCredentialFormats, IndyCredentialFormat } from '../../formats' + NegotiateCredentialOfferOptions, + NegotiateCredentialProposalOptions, +} from '../CredentialProtocolOptions' import { Protocol } from '../../../../agent/models/features' import { Attachment, AttachmentData } from '../../../../decorators/attachment/Attachment' import { AriesFrameworkError } from '../../../../error' -import { injectable } from '../../../../plugins' import { DidCommMessageRepository, DidCommMessageRole } from '../../../../storage' import { JsonTransformer } from '../../../../utils' import { isLinkedAttachment } from '../../../../utils/attachment' @@ -60,15 +60,19 @@ import { } from './messages' import { V1CredentialPreview } from './messages/V1CredentialPreview' +type IndyCredentialFormatServiceLike = CredentialFormatService + export interface V1CredentialProtocolConfig { // indyCredentialFormat must be a service that implements the `IndyCredentialFormat` interface, however it doesn't // have to be the IndyCredentialFormatService implementation per se. - indyCredentialFormat: CredentialFormatService + indyCredentialFormat: IndyCredentialFormatServiceLike } -@injectable() -export class V1CredentialProtocol extends BaseCredentialProtocol<[CredentialFormatService]> { - private indyCredentialFormat: CredentialFormatService +export class V1CredentialProtocol + extends BaseCredentialProtocol<[IndyCredentialFormatServiceLike]> + implements CredentialProtocol<[IndyCredentialFormatServiceLike]> +{ + private indyCredentialFormat: IndyCredentialFormatServiceLike public constructor({ indyCredentialFormat }: V1CredentialProtocolConfig) { super() @@ -77,7 +81,7 @@ export class V1CredentialProtocol extends BaseCredentialProtocol<[CredentialForm } /** - * The version of the issue credential protocol this service supports + * The version of the issue credential protocol this protocol supports */ public readonly version = 'v1' @@ -115,11 +119,11 @@ export class V1CredentialProtocol extends BaseCredentialProtocol<[CredentialForm public async createProposal( agentContext: AgentContext, { - connection, + connectionRecord, credentialFormats, comment, autoAcceptCredential, - }: CreateProposalOptions<[CredentialFormatService]> + }: CreateCredentialProposalOptions<[IndyCredentialFormatServiceLike]> ): Promise> { this.assertOnlyIndyFormat(credentialFormats) @@ -136,11 +140,11 @@ export class V1CredentialProtocol extends BaseCredentialProtocol<[CredentialForm // Create record const credentialRecord = new CredentialExchangeRecord({ - connectionId: connection.id, + connectionId: connectionRecord.id, threadId: uuid(), state: CredentialState.ProposalSent, linkedAttachments: linkedAttachments?.map((linkedAttachment) => linkedAttachment.attachment), - autoAcceptCredential: autoAcceptCredential, + autoAcceptCredential, protocolVersion: 'v1', }) @@ -218,18 +222,18 @@ export class V1CredentialProtocol extends BaseCredentialProtocol<[CredentialForm credentialRecord.assertProtocolVersion('v1') credentialRecord.assertState(CredentialState.OfferSent) - const proposalCredentialMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + const previousReceivedMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { associatedRecordId: credentialRecord.id, messageClass: V1ProposeCredentialMessage, }) - const offerCredentialMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + const previousSentMessage = await didCommMessageRepository.getAgentMessage(messageContext.agentContext, { associatedRecordId: credentialRecord.id, messageClass: V1OfferCredentialMessage, }) connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage: proposalCredentialMessage ?? undefined, - previousSentMessage: offerCredentialMessage ?? undefined, + previousReceivedMessage, + previousSentMessage, }) await this.indyCredentialFormat.processProposal(messageContext.agentContext, { @@ -287,7 +291,7 @@ export class V1CredentialProtocol extends BaseCredentialProtocol<[CredentialForm credentialFormats, comment, autoAcceptCredential, - }: AcceptProposalOptions<[CredentialFormatService]> + }: AcceptCredentialProposalOptions<[IndyCredentialFormatServiceLike]> ): Promise> { // Assert credentialRecord.assertProtocolVersion('v1') @@ -307,7 +311,7 @@ export class V1CredentialProtocol extends BaseCredentialProtocol<[CredentialForm credentialRecord.credentialAttributes = proposalMessage.credentialPreview?.attributes const { attachment, previewAttributes } = await this.indyCredentialFormat.acceptProposal(agentContext, { - attachId: INDY_CREDENTIAL_OFFER_ATTACHMENT_ID, + attachmentId: INDY_CREDENTIAL_OFFER_ATTACHMENT_ID, credentialFormats, credentialRecord, proposalAttachment: new Attachment({ @@ -349,7 +353,7 @@ export class V1CredentialProtocol extends BaseCredentialProtocol<[CredentialForm * Negotiate a credential proposal as issuer (by sending a credential offer message) to the connection * associated with the credential record. * - * @param options configuration for the offer see {@link NegotiateProposalOptions} + * @param options configuration for the offer see {@link NegotiateCredentialProposalOptions} * @returns Credential record associated with the credential offer and the corresponding new offer message * */ @@ -360,17 +364,17 @@ export class V1CredentialProtocol extends BaseCredentialProtocol<[CredentialForm credentialRecord, comment, autoAcceptCredential, - }: NegotiateProposalOptions<[CredentialFormatService]> + }: NegotiateCredentialProposalOptions<[IndyCredentialFormatServiceLike]> ): Promise> { // Assert credentialRecord.assertProtocolVersion('v1') credentialRecord.assertState(CredentialState.ProposalReceived) - if (credentialFormats) this.assertOnlyIndyFormat(credentialFormats) + this.assertOnlyIndyFormat(credentialFormats) const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) const { attachment, previewAttributes } = await this.indyCredentialFormat.createOffer(agentContext, { - attachId: INDY_CREDENTIAL_OFFER_ATTACHMENT_ID, + attachmentId: INDY_CREDENTIAL_OFFER_ATTACHMENT_ID, credentialFormats, credentialRecord, }) @@ -416,11 +420,11 @@ export class V1CredentialProtocol extends BaseCredentialProtocol<[CredentialForm credentialFormats, autoAcceptCredential, comment, - connection, - }: CreateOfferOptions<[CredentialFormatService]> + connectionRecord, + }: CreateCredentialOfferOptions<[IndyCredentialFormatServiceLike]> ): Promise> { // Assert - if (credentialFormats) this.assertOnlyIndyFormat(credentialFormats) + this.assertOnlyIndyFormat(credentialFormats) const credentialRepository = agentContext.dependencyManager.resolve(CredentialRepository) const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) @@ -431,7 +435,7 @@ export class V1CredentialProtocol extends BaseCredentialProtocol<[CredentialForm // Create record const credentialRecord = new CredentialExchangeRecord({ - connectionId: connection?.id, + connectionId: connectionRecord?.id, threadId: uuid(), linkedAttachments: credentialFormats.indy.linkedAttachments?.map( (linkedAttachments) => linkedAttachments.attachment @@ -442,7 +446,7 @@ export class V1CredentialProtocol extends BaseCredentialProtocol<[CredentialForm }) const { attachment, previewAttributes } = await this.indyCredentialFormat.createOffer(agentContext, { - attachId: INDY_CREDENTIAL_OFFER_ATTACHMENT_ID, + attachmentId: INDY_CREDENTIAL_OFFER_ATTACHMENT_ID, credentialFormats, credentialRecord, }) @@ -499,11 +503,7 @@ export class V1CredentialProtocol extends BaseCredentialProtocol<[CredentialForm agentContext.config.logger.debug(`Processing credential offer with id ${offerMessage.id}`) - let credentialRecord = await this.findByThreadAndConnectionId( - messageContext.agentContext, - offerMessage.threadId, - connection?.id - ) + let credentialRecord = await this.findByThreadAndConnectionId(agentContext, offerMessage.threadId, connection?.id) const offerAttachment = offerMessage.getOfferAttachmentById(INDY_CREDENTIAL_OFFER_ATTACHMENT_ID) if (!offerAttachment) { @@ -513,11 +513,11 @@ export class V1CredentialProtocol extends BaseCredentialProtocol<[CredentialForm } if (credentialRecord) { - const proposalCredentialMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + const previousSentMessage = await didCommMessageRepository.getAgentMessage(messageContext.agentContext, { associatedRecordId: credentialRecord.id, messageClass: V1ProposeCredentialMessage, }) - const offerCredentialMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + const previousReceivedMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { associatedRecordId: credentialRecord.id, messageClass: V1OfferCredentialMessage, }) @@ -526,8 +526,8 @@ export class V1CredentialProtocol extends BaseCredentialProtocol<[CredentialForm credentialRecord.assertProtocolVersion('v1') credentialRecord.assertState(CredentialState.ProposalSent) connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage: offerCredentialMessage ?? undefined, - previousSentMessage: proposalCredentialMessage ?? undefined, + previousReceivedMessage, + previousSentMessage, }) await this.indyCredentialFormat.processOffer(messageContext.agentContext, { @@ -587,7 +587,7 @@ export class V1CredentialProtocol extends BaseCredentialProtocol<[CredentialForm credentialFormats, comment, autoAcceptCredential, - }: AcceptOfferOptions<[CredentialFormatService]> + }: AcceptCredentialOfferOptions<[IndyCredentialFormatServiceLike]> ): Promise> { // Assert credential credentialRecord.assertProtocolVersion('v1') @@ -610,7 +610,7 @@ export class V1CredentialProtocol extends BaseCredentialProtocol<[CredentialForm const { attachment } = await this.indyCredentialFormat.acceptOffer(agentContext, { credentialRecord, credentialFormats, - attachId: INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID, + attachmentId: INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID, offerAttachment, }) @@ -654,7 +654,7 @@ export class V1CredentialProtocol extends BaseCredentialProtocol<[CredentialForm credentialRecord, autoAcceptCredential, comment, - }: NegotiateOfferOptions<[CredentialFormatService]> + }: NegotiateCredentialOfferOptions<[IndyCredentialFormatServiceLike]> ): Promise> { // Assert credentialRecord.assertProtocolVersion('v1') @@ -670,7 +670,7 @@ export class V1CredentialProtocol extends BaseCredentialProtocol<[CredentialForm } if (!credentialFormats.indy) { - throw new AriesFrameworkError('Missing indy credential format in v1 create proposal call.') + throw new AriesFrameworkError('Missing indy credential format in v1 negotiate proposal call.') } const { linkedAttachments } = credentialFormats.indy @@ -808,11 +808,12 @@ export class V1CredentialProtocol extends BaseCredentialProtocol<[CredentialForm credentialFormats, comment, autoAcceptCredential, - }: AcceptRequestOptions<[CredentialFormatService]> + }: AcceptCredentialRequestOptions<[IndyCredentialFormatServiceLike]> ): Promise> { // Assert credentialRecord.assertProtocolVersion('v1') credentialRecord.assertState(CredentialState.RequestReceived) + if (credentialFormats) this.assertOnlyIndyFormat(credentialFormats) const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) @@ -834,17 +835,17 @@ export class V1CredentialProtocol extends BaseCredentialProtocol<[CredentialForm ) } - const { attachment: credentialsAttach } = await this.indyCredentialFormat.acceptRequest(agentContext, { + const { attachment } = await this.indyCredentialFormat.acceptRequest(agentContext, { credentialRecord, requestAttachment, offerAttachment, - attachId: INDY_CREDENTIAL_ATTACHMENT_ID, + attachmentId: INDY_CREDENTIAL_ATTACHMENT_ID, credentialFormats, }) const issueMessage = new V1IssueCredentialMessage({ comment, - credentialAttachments: [credentialsAttach], + credentialAttachments: [attachment], attachments: credentialRecord.linkedAttachments, }) @@ -902,8 +903,8 @@ export class V1CredentialProtocol extends BaseCredentialProtocol<[CredentialForm credentialRecord.assertProtocolVersion('v1') credentialRecord.assertState(CredentialState.RequestSent) connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage: offerCredentialMessage ?? undefined, - previousSentMessage: requestCredentialMessage ?? undefined, + previousReceivedMessage: offerCredentialMessage, + previousSentMessage: requestCredentialMessage, }) const issueAttachment = issueMessage.getCredentialAttachmentById(INDY_CREDENTIAL_ATTACHMENT_ID) @@ -1014,13 +1015,18 @@ export class V1CredentialProtocol extends BaseCredentialProtocol<[CredentialForm * @returns a {@link V1CredentialProblemReportMessage} * */ - public createProblemReport(agentContext: AgentContext, options: CreateProblemReportOptions): ProblemReportMessage { - return new V1CredentialProblemReportMessage({ + public async createProblemReport( + agentContext: AgentContext, + { credentialRecord, description }: CreateCredentialProblemReportOptions + ): Promise> { + const message = new V1CredentialProblemReportMessage({ description: { - en: options.message, + en: description, code: CredentialProblemReportReason.IssuanceAbandoned, }, }) + + return { message, credentialRecord } } // AUTO RESPOND METHODS @@ -1215,7 +1221,7 @@ export class V1CredentialProtocol extends BaseCredentialProtocol<[CredentialForm public async getFormatData( agentContext: AgentContext, credentialExchangeId: string - ): Promise]>>> { + ): Promise>> { // TODO: we could looking at fetching all record using a single query and then filtering based on the type of the message. const [proposalMessage, offerMessage, requestMessage, credentialMessage] = await Promise.all([ this.findProposalMessage(agentContext, credentialExchangeId), diff --git a/packages/core/src/modules/credentials/protocol/v1/__tests__/V1CredentialProtocolCred.test.ts b/packages/core/src/modules/credentials/protocol/v1/__tests__/V1CredentialProtocolCred.test.ts index f6ba0a6c22..d563555bd5 100644 --- a/packages/core/src/modules/credentials/protocol/v1/__tests__/V1CredentialProtocolCred.test.ts +++ b/packages/core/src/modules/credentials/protocol/v1/__tests__/V1CredentialProtocolCred.test.ts @@ -9,7 +9,6 @@ import type { CustomCredentialTags } from '../../../repository/CredentialExchang import { Subject } from 'rxjs' import { getAgentConfig, getAgentContext, getMockConnection, mockFunction } from '../../../../../../tests/helpers' -import { Dispatcher } from '../../../../../agent/Dispatcher' import { EventEmitter } from '../../../../../agent/EventEmitter' import { InboundMessageContext } from '../../../../../agent/models/InboundMessageContext' import { Attachment, AttachmentData } from '../../../../../decorators/attachment/Attachment' @@ -22,7 +21,6 @@ import { uuid } from '../../../../../utils/uuid' import { AckStatus } from '../../../../common' import { DidExchangeState } from '../../../../connections' import { ConnectionService } from '../../../../connections/services/ConnectionService' -import { RoutingService } from '../../../../routing/services/RoutingService' import { CredentialEventTypes } from '../../../CredentialEvents' import { credDef, credReq } from '../../../__tests__/fixtures' import { CredentialProblemReportReason } from '../../../errors/CredentialProblemReportReason' @@ -52,23 +50,17 @@ import { jest.mock('../../../repository/CredentialRepository') jest.mock('../../../formats/indy/IndyCredentialFormatService') jest.mock('../../../../../storage/didcomm/DidCommMessageRepository') -jest.mock('../../../../routing/services/RoutingService') jest.mock('../../../../connections/services/ConnectionService') -jest.mock('../../../../../agent/Dispatcher') // Mock typed object const CredentialRepositoryMock = CredentialRepository as jest.Mock const IndyCredentialFormatServiceMock = IndyCredentialFormatService as jest.Mock const DidCommMessageRepositoryMock = DidCommMessageRepository as jest.Mock -const RoutingServiceMock = RoutingService as jest.Mock const ConnectionServiceMock = ConnectionService as jest.Mock -const DispatcherMock = Dispatcher as jest.Mock const credentialRepository = new CredentialRepositoryMock() const didCommMessageRepository = new DidCommMessageRepositoryMock() -const routingService = new RoutingServiceMock() const indyCredentialFormatService = new IndyCredentialFormatServiceMock() -const dispatcher = new DispatcherMock() const connectionService = new ConnectionServiceMock() // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -235,10 +227,8 @@ describe('V1CredentialProtocol', () => { registerInstances: [ [CredentialRepository, credentialRepository], [DidCommMessageRepository, didCommMessageRepository], - [RoutingService, routingService], - [Dispatcher, dispatcher], - [ConnectionService, connectionService], [EventEmitter, eventEmitter], + [ConnectionService, connectionService], ], agentConfig, }) @@ -280,7 +270,7 @@ describe('V1CredentialProtocol', () => { attachment: requestAttachment, format: new CredentialFormatSpec({ format: 'indy', - attachId: INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID, + attachmentId: INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID, }), }) @@ -310,7 +300,7 @@ describe('V1CredentialProtocol', () => { expect(credentialRepository.update).toHaveBeenCalledTimes(1) expect(indyCredentialFormatService.acceptOffer).toHaveBeenCalledWith(agentContext, { credentialRecord, - attachId: INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID, + attachmentId: INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID, offerAttachment, credentialFormats: { indy: { @@ -337,7 +327,7 @@ describe('V1CredentialProtocol', () => { attachment: requestAttachment, format: new CredentialFormatSpec({ format: 'indy', - attachId: INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID, + attachmentId: INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID, }), }) @@ -443,7 +433,7 @@ describe('V1CredentialProtocol', () => { attachment: credentialAttachment, format: new CredentialFormatSpec({ format: 'the-format', - attachId: 'the-attach-id', + attachmentId: 'the-attach-id', }), }) @@ -473,7 +463,7 @@ describe('V1CredentialProtocol', () => { attachment: credentialAttachment, format: new CredentialFormatSpec({ format: 'the-format', - attachId: 'the-attach-id', + attachmentId: 'the-attach-id', }), }) @@ -513,7 +503,7 @@ describe('V1CredentialProtocol', () => { attachment: credentialAttachment, format: new CredentialFormatSpec({ format: 'the-format', - attachId: 'the-attach-id', + attachmentId: 'the-attach-id', }), }) @@ -536,7 +526,7 @@ describe('V1CredentialProtocol', () => { credentialRecord, requestAttachment, offerAttachment, - attachId: INDY_CREDENTIAL_ATTACHMENT_ID, + attachmentId: INDY_CREDENTIAL_ATTACHMENT_ID, }) }) }) @@ -708,7 +698,6 @@ describe('V1CredentialProtocol', () => { describe('createProblemReport', () => { const threadId = 'fd9c5ddb-ec11-4acd-bc32-540736249746' - const message = 'Indy error' let credential: CredentialExchangeRecord beforeEach(() => { @@ -719,16 +708,19 @@ describe('V1CredentialProtocol', () => { }) }) - test('returns problem report message base once get error', () => { + test('returns problem report message base once get error', async () => { // given mockFunction(credentialRepository.getById).mockReturnValue(Promise.resolve(credential)) // when - const credentialProblemReportMessage = credentialProtocol.createProblemReport(agentContext, { message }) + const { message } = await credentialProtocol.createProblemReport(agentContext, { + description: 'Indy error', + credentialRecord: credential, + }) - credentialProblemReportMessage.setThread({ threadId }) + message.setThread({ threadId }) // then - expect(credentialProblemReportMessage.toJSON()).toMatchObject({ + expect(message.toJSON()).toMatchObject({ '@id': expect.any(String), '@type': 'https://didcomm.org/issue-credential/1.0/problem-report', '~thread': { @@ -736,7 +728,7 @@ describe('V1CredentialProtocol', () => { }, description: { code: CredentialProblemReportReason.IssuanceAbandoned, - en: message, + en: 'Indy error', }, }) }) @@ -909,74 +901,4 @@ describe('V1CredentialProtocol', () => { expect(didCommMessageRepository.delete).toHaveBeenCalledTimes(3) }) }) - - describe('declineOffer', () => { - const threadId = 'fd9c5ddb-ec11-4acd-bc32-540736249754' - let credential: CredentialExchangeRecord - - beforeEach(() => { - credential = mockCredentialRecord({ - state: CredentialState.OfferReceived, - tags: { threadId }, - }) - }) - - test(`updates state to ${CredentialState.Declined}`, async () => { - // given - const repositoryUpdateSpy = jest.spyOn(credentialRepository, 'update') - - // when - await credentialProtocol.declineOffer(agentContext, credential) - - // then - const expectedCredentialState = { - state: CredentialState.Declined, - } - expect(repositoryUpdateSpy).toHaveBeenCalledTimes(1) - expect(repositoryUpdateSpy).toHaveBeenNthCalledWith( - 1, - agentContext, - expect.objectContaining(expectedCredentialState) - ) - }) - - test(`emits stateChange event from ${CredentialState.OfferReceived} to ${CredentialState.Declined}`, async () => { - const eventListenerMock = jest.fn() - eventEmitter.on(CredentialEventTypes.CredentialStateChanged, eventListenerMock) - - // given - mockFunction(credentialRepository.getSingleByQuery).mockReturnValue(Promise.resolve(credential)) - - // when - await credentialProtocol.declineOffer(agentContext, credential) - - // then - expect(eventListenerMock).toHaveBeenCalledTimes(1) - const [[event]] = eventListenerMock.mock.calls - expect(event).toMatchObject({ - type: 'CredentialStateChanged', - metadata: { - contextCorrelationId: 'mock', - }, - payload: { - previousState: CredentialState.OfferReceived, - credentialRecord: expect.objectContaining({ - state: CredentialState.Declined, - }), - }, - }) - }) - - const validState = CredentialState.OfferReceived - const invalidCredentialStates = Object.values(CredentialState).filter((state) => state !== validState) - test(`throws an error when state transition is invalid`, async () => { - await Promise.all( - invalidCredentialStates.map(async (state) => { - await expect( - credentialProtocol.declineOffer(agentContext, mockCredentialRecord({ state, tags: { threadId } })) - ).rejects.toThrowError(`Credential record is in invalid state ${state}. Valid states are: ${validState}.`) - }) - ) - }) - }) }) diff --git a/packages/core/src/modules/credentials/protocol/v1/__tests__/V1CredentialProtocolProposeOffer.test.ts b/packages/core/src/modules/credentials/protocol/v1/__tests__/V1CredentialProtocolProposeOffer.test.ts index 78841df7fe..d1c27861b2 100644 --- a/packages/core/src/modules/credentials/protocol/v1/__tests__/V1CredentialProtocolProposeOffer.test.ts +++ b/packages/core/src/modules/credentials/protocol/v1/__tests__/V1CredentialProtocolProposeOffer.test.ts @@ -1,5 +1,5 @@ import type { CredentialStateChangedEvent } from '../../../CredentialEvents' -import type { CreateOfferOptions, CreateProposalOptions } from '../../../CredentialProtocolOptions' +import type { CreateCredentialOfferOptions, CreateCredentialProposalOptions } from '../../CredentialProtocolOptions' import { Subject } from 'rxjs' @@ -70,7 +70,7 @@ const agentContext = getAgentContext({ // @ts-ignore indyCredentialFormatService.credentialRecordType = 'indy' -const connection = getMockConnection({ +const connectionRecord = getMockConnection({ id: '123', state: DidExchangeState.Completed, }) @@ -107,7 +107,7 @@ describe('V1CredentialProtocolProposeOffer', () => { beforeEach(async () => { // mock function implementations - mockFunction(connectionService.getById).mockResolvedValue(connection) + mockFunction(connectionService.getById).mockResolvedValue(connectionRecord) mockFunction(indyLedgerService.getCredentialDefinition).mockResolvedValue(credDef) mockFunction(indyLedgerService.getSchema).mockResolvedValue(schema) @@ -121,8 +121,8 @@ describe('V1CredentialProtocolProposeOffer', () => { }) describe('createProposal', () => { - const proposeOptions: CreateProposalOptions<[IndyCredentialFormatService]> = { - connection, + const proposeOptions: CreateCredentialProposalOptions<[IndyCredentialFormatService]> = { + connectionRecord: connectionRecord, credentialFormats: { indy: { credentialDefinitionId: 'Th7MpTaRZVRYnPiabds81Y:3:CL:17:TAG', @@ -136,6 +136,7 @@ describe('V1CredentialProtocolProposeOffer', () => { }, comment: 'v1 propose credential test', } + test(`creates credential record in ${CredentialState.OfferSent} state with offer, thread id`, async () => { const repositorySaveSpy = jest.spyOn(credentialRepository, 'save') @@ -143,7 +144,7 @@ describe('V1CredentialProtocolProposeOffer', () => { attachment: proposalAttachment, format: new CredentialFormatSpec({ format: 'indy', - attachId: 'indy-proposal', + attachmentId: 'indy-proposal', }), }) @@ -157,7 +158,7 @@ describe('V1CredentialProtocolProposeOffer', () => { type: CredentialExchangeRecord.type, id: expect.any(String), createdAt: expect.any(Date), - connectionId: connection.id, + connectionId: connectionRecord.id, state: CredentialState.ProposalSent, }) ) @@ -171,7 +172,7 @@ describe('V1CredentialProtocolProposeOffer', () => { attachment: proposalAttachment, format: new CredentialFormatSpec({ format: 'indy', - attachId: 'indy-proposal', + attachmentId: 'indy-proposal', }), }) @@ -196,7 +197,7 @@ describe('V1CredentialProtocolProposeOffer', () => { attachment: proposalAttachment, format: new CredentialFormatSpec({ format: 'indy', - attachId: 'indy-proposal', + attachmentId: 'indy-proposal', }), previewAttributes: credentialPreview.attributes, }) @@ -233,9 +234,9 @@ describe('V1CredentialProtocolProposeOffer', () => { }) describe('createOffer', () => { - const offerOptions: CreateOfferOptions<[IndyCredentialFormatService]> = { + const offerOptions: CreateCredentialOfferOptions<[IndyCredentialFormatService]> = { comment: 'some comment', - connection, + connectionRecord, credentialFormats: { indy: { attributes: credentialPreview.attributes, @@ -249,7 +250,7 @@ describe('V1CredentialProtocolProposeOffer', () => { attachment: offerAttachment, format: new CredentialFormatSpec({ format: 'indy', - attachId: 'indy-offer', + attachmentId: 'indy-offer', }), previewAttributes: credentialPreview.attributes, }) @@ -267,7 +268,7 @@ describe('V1CredentialProtocolProposeOffer', () => { id: expect.any(String), createdAt: expect.any(Date), threadId: createdCredentialRecord.threadId, - connectionId: connection.id, + connectionId: connectionRecord.id, state: CredentialState.OfferSent, }) }) @@ -280,7 +281,7 @@ describe('V1CredentialProtocolProposeOffer', () => { attachment: offerAttachment, format: new CredentialFormatSpec({ format: 'indy', - attachId: 'indy-offer', + attachmentId: 'indy-offer', }), previewAttributes: credentialPreview.attributes, }) @@ -306,7 +307,7 @@ describe('V1CredentialProtocolProposeOffer', () => { attachment: offerAttachment, format: new CredentialFormatSpec({ format: 'indy', - attachId: 'indy-offer', + attachmentId: 'indy-offer', }), }) @@ -320,7 +321,7 @@ describe('V1CredentialProtocolProposeOffer', () => { attachment: offerAttachment, format: new CredentialFormatSpec({ format: 'indy', - attachId: 'indy-offer', + attachmentId: 'indy-offer', }), previewAttributes: credentialPreview.attributes, }) @@ -356,7 +357,10 @@ describe('V1CredentialProtocolProposeOffer', () => { credentialPreview: credentialPreview, offerAttachments: [offerAttachment], }) - const messageContext = new InboundMessageContext(credentialOfferMessage, { agentContext, connection }) + const messageContext = new InboundMessageContext(credentialOfferMessage, { + agentContext, + connection: connectionRecord, + }) test(`creates and return credential record in ${CredentialState.OfferReceived} state with offer, thread ID`, async () => { // when @@ -371,7 +375,7 @@ describe('V1CredentialProtocolProposeOffer', () => { id: expect.any(String), createdAt: expect.any(Date), threadId: credentialOfferMessage.id, - connectionId: connection.id, + connectionId: connectionRecord.id, state: CredentialState.OfferReceived, credentialAttributes: undefined, }) diff --git a/packages/core/src/modules/credentials/protocol/v1/messages/V1ProposeCredentialMessage.ts b/packages/core/src/modules/credentials/protocol/v1/messages/V1ProposeCredentialMessage.ts index 3e2f46e8c6..2772595d33 100644 --- a/packages/core/src/modules/credentials/protocol/v1/messages/V1ProposeCredentialMessage.ts +++ b/packages/core/src/modules/credentials/protocol/v1/messages/V1ProposeCredentialMessage.ts @@ -121,12 +121,4 @@ export class V1ProposeCredentialMessage extends AgentMessage { @IsOptional() @Matches(indyDidRegex) public issuerDid?: string - - public getAttachment(): Attachment | undefined { - if (this.appendedAttachments) { - return this.appendedAttachments[0] - } else { - return undefined - } - } } diff --git a/packages/core/src/modules/credentials/protocol/v2/CredentialFormatCoordinator.ts b/packages/core/src/modules/credentials/protocol/v2/CredentialFormatCoordinator.ts index 97e5489378..1def0ac9f4 100644 --- a/packages/core/src/modules/credentials/protocol/v2/CredentialFormatCoordinator.ts +++ b/packages/core/src/modules/credentials/protocol/v2/CredentialFormatCoordinator.ts @@ -568,6 +568,6 @@ export class CredentialFormatCoordinator if (!format) throw new AriesFrameworkError(`No attachment found for service ${credentialFormatService.formatKey}`) - return format.attachId + return format.attachmentId } } diff --git a/packages/core/src/modules/credentials/protocol/v2/V2CredentialProtocol.ts b/packages/core/src/modules/credentials/protocol/v2/V2CredentialProtocol.ts index 7fac43d7e0..df39187210 100644 --- a/packages/core/src/modules/credentials/protocol/v2/V2CredentialProtocol.ts +++ b/packages/core/src/modules/credentials/protocol/v2/V2CredentialProtocol.ts @@ -5,21 +5,6 @@ import type { MessageHandlerInboundMessage } from '../../../../agent/MessageHand import type { InboundMessageContext } from '../../../../agent/models/InboundMessageContext' import type { DependencyManager } from '../../../../plugins' import type { ProblemReportMessage } from '../../../problem-reports' -import type { - AcceptCredentialOptions, - AcceptOfferOptions, - AcceptProposalOptions, - AcceptRequestOptions, - CreateOfferOptions, - CreateProposalOptions, - CreateRequestOptions, - CredentialProtocolMsgReturnType, - FormatDataMessagePayload, - CreateProblemReportOptions, - GetFormatDataReturn, - NegotiateOfferOptions, - NegotiateProposalOptions, -} from '../../CredentialProtocolOptions' import type { CredentialFormat, CredentialFormatPayload, @@ -27,6 +12,22 @@ import type { ExtractCredentialFormats, } from '../../formats' import type { CredentialFormatSpec } from '../../models/CredentialFormatSpec' +import type { CredentialProtocol } from '../CredentialProtocol' +import type { + AcceptCredentialOptions, + AcceptCredentialOfferOptions, + AcceptCredentialProposalOptions, + AcceptCredentialRequestOptions, + CreateCredentialOfferOptions, + CreateCredentialProposalOptions, + CreateCredentialRequestOptions, + CredentialProtocolMsgReturnType, + CredentialFormatDataMessagePayload, + CreateCredentialProblemReportOptions, + GetCredentialFormatDataReturn, + NegotiateCredentialOfferOptions, + NegotiateCredentialProposalOptions, +} from '../CredentialProtocolOptions' import { Protocol } from '../../../../agent/models/features/Protocol' import { AriesFrameworkError } from '../../../../error' @@ -64,9 +65,10 @@ export interface V2CredentialProtocolConfig extends BaseCredentialProtocol { +export class V2CredentialProtocol + extends BaseCredentialProtocol + implements CredentialProtocol +{ private credentialFormatCoordinator = new CredentialFormatCoordinator() private credentialFormats: CFs @@ -95,7 +97,7 @@ export class V2CredentialProtocol< new V2CredentialProblemReportHandler(this), ]) - // Register Issue Credential V1 in feature registry, with supported roles + // Register Issue Credential V2 in feature registry, with supported roles featureRegistry.register( new Protocol({ id: 'https://didcomm.org/issue-credential/2.0', @@ -113,7 +115,7 @@ export class V2CredentialProtocol< */ public async createProposal( agentContext: AgentContext, - { connection, credentialFormats, comment, autoAcceptCredential }: CreateProposalOptions + { connectionRecord, credentialFormats, comment, autoAcceptCredential }: CreateCredentialProposalOptions ): Promise> { agentContext.config.logger.debug('Get the Format Service and Create Proposal Message') @@ -125,7 +127,7 @@ export class V2CredentialProtocol< } const credentialRecord = new CredentialExchangeRecord({ - connectionId: connection.id, + connectionId: connectionRecord.id, threadId: uuid(), state: CredentialState.ProposalSent, autoAcceptCredential, @@ -169,7 +171,7 @@ export class V2CredentialProtocol< connection?.id ) - const formatServices = this.getFormatServicesFromMessage(agentContext, proposalMessage.formats) + const formatServices = this.getFormatServicesFromMessage(proposalMessage.formats) if (formatServices.length === 0) { throw new AriesFrameworkError(`Unable to process proposal. No supported formats`) } @@ -230,7 +232,7 @@ export class V2CredentialProtocol< public async acceptProposal( agentContext: AgentContext, - { credentialRecord, credentialFormats, autoAcceptCredential, comment }: AcceptProposalOptions + { credentialRecord, credentialFormats, autoAcceptCredential, comment }: AcceptCredentialProposalOptions ): Promise> { // Assert credentialRecord.assertProtocolVersion('v2') @@ -249,7 +251,7 @@ export class V2CredentialProtocol< messageClass: V2ProposeCredentialMessage, }) - formatServices = this.getFormatServicesFromMessage(agentContext, proposalMessage.formats) + formatServices = this.getFormatServicesFromMessage(proposalMessage.formats) } // If the format services list is still empty, throw an error as we don't support any @@ -277,13 +279,13 @@ export class V2CredentialProtocol< * Negotiate a credential proposal as issuer (by sending a credential offer message) to the connection * associated with the credential record. * - * @param options configuration for the offer see {@link NegotiateProposalOptions} + * @param options configuration for the offer see {@link NegotiateCredentialProposalOptions} * @returns Credential exchange record associated with the credential offer * */ public async negotiateProposal( agentContext: AgentContext, - { credentialRecord, credentialFormats, autoAcceptCredential, comment }: NegotiateProposalOptions + { credentialRecord, credentialFormats, autoAcceptCredential, comment }: NegotiateCredentialProposalOptions ): Promise> { // Assert credentialRecord.assertProtocolVersion('v2') @@ -324,7 +326,7 @@ export class V2CredentialProtocol< */ public async createOffer( agentContext: AgentContext, - { credentialFormats, autoAcceptCredential, comment, connection }: CreateOfferOptions + { credentialFormats, autoAcceptCredential, comment, connectionRecord }: CreateCredentialOfferOptions ): Promise> { const credentialRepository = agentContext.dependencyManager.resolve(CredentialRepository) @@ -334,7 +336,7 @@ export class V2CredentialProtocol< } const credentialRecord = new CredentialExchangeRecord({ - connectionId: connection?.id, + connectionId: connectionRecord?.id, threadId: uuid(), state: CredentialState.OfferSent, autoAcceptCredential, @@ -380,7 +382,7 @@ export class V2CredentialProtocol< connection?.id ) - const formatServices = this.getFormatServicesFromMessage(agentContext, offerMessage.formats) + const formatServices = this.getFormatServicesFromMessage(offerMessage.formats) if (formatServices.length === 0) { throw new AriesFrameworkError(`Unable to process offer. No supported formats`) } @@ -441,7 +443,7 @@ export class V2CredentialProtocol< public async acceptOffer( agentContext: AgentContext, - { credentialRecord, autoAcceptCredential, comment, credentialFormats }: AcceptOfferOptions + { credentialRecord, autoAcceptCredential, comment, credentialFormats }: AcceptCredentialOfferOptions ) { const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) @@ -460,7 +462,7 @@ export class V2CredentialProtocol< messageClass: V2OfferCredentialMessage, }) - formatServices = this.getFormatServicesFromMessage(agentContext, offerMessage.formats) + formatServices = this.getFormatServicesFromMessage(offerMessage.formats) } // If the format services list is still empty, throw an error as we don't support any @@ -494,7 +496,7 @@ export class V2CredentialProtocol< */ public async negotiateOffer( agentContext: AgentContext, - { credentialRecord, credentialFormats, autoAcceptCredential, comment }: NegotiateOfferOptions + { credentialRecord, credentialFormats, autoAcceptCredential, comment }: NegotiateCredentialOfferOptions ): Promise> { // Assert credentialRecord.assertProtocolVersion('v2') @@ -531,7 +533,7 @@ export class V2CredentialProtocol< */ public async createRequest( agentContext: AgentContext, - { credentialFormats, autoAcceptCredential, comment, connection }: CreateRequestOptions + { credentialFormats, autoAcceptCredential, comment, connectionRecord }: CreateCredentialRequestOptions ): Promise> { const credentialRepository = agentContext.dependencyManager.resolve(CredentialRepository) @@ -541,7 +543,7 @@ export class V2CredentialProtocol< } const credentialRecord = new CredentialExchangeRecord({ - connectionId: connection.id, + connectionId: connectionRecord.id, threadId: uuid(), state: CredentialState.RequestSent, autoAcceptCredential, @@ -591,7 +593,7 @@ export class V2CredentialProtocol< connection?.id ) - const formatServices = this.getFormatServicesFromMessage(agentContext, requestMessage.formats) + const formatServices = this.getFormatServicesFromMessage(requestMessage.formats) if (formatServices.length === 0) { throw new AriesFrameworkError(`Unable to process request. No supported formats`) } @@ -654,7 +656,7 @@ export class V2CredentialProtocol< public async acceptRequest( agentContext: AgentContext, - { credentialRecord, autoAcceptCredential, comment, credentialFormats }: AcceptRequestOptions + { credentialRecord, autoAcceptCredential, comment, credentialFormats }: AcceptCredentialRequestOptions ) { const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) @@ -673,7 +675,7 @@ export class V2CredentialProtocol< messageClass: V2RequestCredentialMessage, }) - formatServices = this.getFormatServicesFromMessage(agentContext, requestMessage.formats) + formatServices = this.getFormatServicesFromMessage(requestMessage.formats) } // If the format services list is still empty, throw an error as we don't support any @@ -740,7 +742,7 @@ export class V2CredentialProtocol< previousSentMessage: requestMessage, }) - const formatServices = this.getFormatServicesFromMessage(agentContext, requestMessage.formats) + const formatServices = this.getFormatServicesFromMessage(credentialMessage.formats) if (formatServices.length === 0) { throw new AriesFrameworkError(`Unable to process credential. No supported formats`) } @@ -837,13 +839,20 @@ export class V2CredentialProtocol< * @returns a {@link V2CredentialProblemReportMessage} * */ - public createProblemReport(agentContext: AgentContext, options: CreateProblemReportOptions): ProblemReportMessage { - return new V2CredentialProblemReportMessage({ + public async createProblemReport( + agentContext: AgentContext, + { credentialRecord, description }: CreateCredentialProblemReportOptions + ): Promise> { + const message = new V2CredentialProblemReportMessage({ description: { - en: options.message, + en: description, code: CredentialProblemReportReason.IssuanceAbandoned, }, }) + + message.setThread({ threadId: credentialRecord.threadId }) + + return { credentialRecord, message } } // AUTO ACCEPT METHODS @@ -872,7 +881,7 @@ export class V2CredentialProtocol< // NOTE: we take the formats from the offerMessage so we always check all services that we last sent // Otherwise we'll only check the formats from the proposal, which could be different from the formats // we use. - const formatServices = this.getFormatServicesFromMessage(agentContext, offerMessage.formats) + const formatServices = this.getFormatServicesFromMessage(offerMessage.formats) for (const formatService of formatServices) { const offerAttachment = this.credentialFormatCoordinator.getAttachmentForService( @@ -887,7 +896,7 @@ export class V2CredentialProtocol< proposalMessage.proposalAttachments ) - const shouldAutoRespondToFormat = formatService.shouldAutoRespondToProposal(agentContext, { + const shouldAutoRespondToFormat = await formatService.shouldAutoRespondToProposal(agentContext, { credentialRecord, offerAttachment, proposalAttachment, @@ -926,7 +935,6 @@ export class V2CredentialProtocol< credentialRecord.autoAcceptCredential, credentialsModuleConfig.autoAcceptCredentials ) - // Handle always / never cases if (autoAccept === AutoAcceptCredential.Always) return true if (autoAccept === AutoAcceptCredential.Never) return false @@ -937,7 +945,7 @@ export class V2CredentialProtocol< // NOTE: we take the formats from the proposalMessage so we always check all services that we last sent // Otherwise we'll only check the formats from the offer, which could be different from the formats // we use. - const formatServices = this.getFormatServicesFromMessage(agentContext, proposalMessage.formats) + const formatServices = this.getFormatServicesFromMessage(proposalMessage.formats) for (const formatService of formatServices) { const offerAttachment = this.credentialFormatCoordinator.getAttachmentForService( @@ -952,7 +960,7 @@ export class V2CredentialProtocol< proposalMessage.proposalAttachments ) - const shouldAutoRespondToFormat = formatService.shouldAutoRespondToOffer(agentContext, { + const shouldAutoRespondToFormat = await formatService.shouldAutoRespondToOffer(agentContext, { credentialRecord, offerAttachment, proposalAttachment, @@ -1001,7 +1009,7 @@ export class V2CredentialProtocol< // NOTE: we take the formats from the offerMessage so we always check all services that we last sent // Otherwise we'll only check the formats from the request, which could be different from the formats // we use. - const formatServices = this.getFormatServicesFromMessage(agentContext, offerMessage.formats) + const formatServices = this.getFormatServicesFromMessage(offerMessage.formats) for (const formatService of formatServices) { const offerAttachment = this.credentialFormatCoordinator.getAttachmentForService( @@ -1024,7 +1032,7 @@ export class V2CredentialProtocol< requestMessage.requestAttachments ) - const shouldAutoRespondToFormat = formatService.shouldAutoRespondToRequest(agentContext, { + const shouldAutoRespondToFormat = await formatService.shouldAutoRespondToRequest(agentContext, { credentialRecord, offerAttachment, requestAttachment, @@ -1066,7 +1074,7 @@ export class V2CredentialProtocol< // NOTE: we take the formats from the requestMessage so we always check all services that we last sent // Otherwise we'll only check the formats from the credential, which could be different from the formats // we use. - const formatServices = this.getFormatServicesFromMessage(agentContext, requestMessage.formats) + const formatServices = this.getFormatServicesFromMessage(requestMessage.formats) for (const formatService of formatServices) { const offerAttachment = offerMessage @@ -1097,7 +1105,7 @@ export class V2CredentialProtocol< credentialMessage.credentialAttachments ) - const shouldAutoRespondToFormat = formatService.shouldAutoRespondToCredential(agentContext, { + const shouldAutoRespondToFormat = await formatService.shouldAutoRespondToCredential(agentContext, { credentialRecord, offerAttachment, credentialAttachment, @@ -1150,7 +1158,7 @@ export class V2CredentialProtocol< public async getFormatData( agentContext: AgentContext, credentialExchangeId: string - ): Promise>> { + ): Promise>> { // TODO: we could looking at fetching all record using a single query and then filtering based on the type of the message. const [proposalMessage, offerMessage, requestMessage, credentialMessage] = await Promise.all([ this.findProposalMessage(agentContext, credentialExchangeId), @@ -1168,7 +1176,7 @@ export class V2CredentialProtocol< credential: [credentialMessage?.formats, credentialMessage?.credentialAttachments], } as const - const formatData: GetFormatDataReturn = { + const formatData: GetCredentialFormatDataReturn = { proposalAttributes: proposalMessage?.credentialPreview?.attributes, offerAttributes: offerMessage?.credentialPreview?.attributes, } @@ -1179,8 +1187,8 @@ export class V2CredentialProtocol< if (!formats || !attachments) continue // Find all format services associated with the message - const formatServices = this.getFormatServicesFromMessage(agentContext, formats) - const messageFormatData: FormatDataMessagePayload = {} + const formatServices = this.getFormatServicesFromMessage(formats) + const messageFormatData: CredentialFormatDataMessagePayload = {} // Loop through all of the format services, for each we will extract the attachment data and assign this to the object // using the unique format key (e.g. indy) @@ -1190,7 +1198,7 @@ export class V2CredentialProtocol< messageFormatData[formatService.formatKey] = attachment.getDataAsJson() } - formatData[messageKey as Exclude] = + formatData[messageKey as Exclude] = messageFormatData } @@ -1202,10 +1210,7 @@ export class V2CredentialProtocol< * @param messageFormats the format objects containing the format name (eg indy) * @return the credential format service objects in an array - derived from format object keys */ - private getFormatServicesFromMessage( - agentContext: AgentContext, - messageFormats: CredentialFormatSpec[] - ): CredentialFormatService[] { + private getFormatServicesFromMessage(messageFormats: CredentialFormatSpec[]): CredentialFormatService[] { const formatServices = new Set() for (const msg of messageFormats) { diff --git a/packages/core/src/modules/credentials/protocol/v2/__tests__/V2CredentialProtocolCred.test.ts b/packages/core/src/modules/credentials/protocol/v2/__tests__/V2CredentialProtocolCred.test.ts index 5939cb70a5..8cddb48b7d 100644 --- a/packages/core/src/modules/credentials/protocol/v2/__tests__/V2CredentialProtocolCred.test.ts +++ b/packages/core/src/modules/credentials/protocol/v2/__tests__/V2CredentialProtocolCred.test.ts @@ -9,7 +9,6 @@ import { Subject } from 'rxjs' import { AriesFrameworkError, CredentialFormatSpec } from '../../../../..' import { getAgentConfig, getAgentContext, getMockConnection, mockFunction } from '../../../../../../tests/helpers' -import { Dispatcher } from '../../../../../agent/Dispatcher' import { EventEmitter } from '../../../../../agent/EventEmitter' import { InboundMessageContext } from '../../../../../agent/models/InboundMessageContext' import { Attachment, AttachmentData } from '../../../../../decorators/attachment/Attachment' @@ -19,7 +18,6 @@ import { JsonEncoder } from '../../../../../utils/JsonEncoder' import { AckStatus } from '../../../../common/messages/AckMessage' import { DidExchangeState } from '../../../../connections' import { ConnectionService } from '../../../../connections/services/ConnectionService' -import { RoutingService } from '../../../../routing/services/RoutingService' import { CredentialEventTypes } from '../../../CredentialEvents' import { credReq } from '../../../__tests__/fixtures' import { CredentialProblemReportReason } from '../../../errors/CredentialProblemReportReason' @@ -53,16 +51,12 @@ const CredentialRepositoryMock = CredentialRepository as jest.Mock const JsonLdCredentialFormatServiceMock = JsonLdCredentialFormatService as jest.Mock const DidCommMessageRepositoryMock = DidCommMessageRepository as jest.Mock -const RoutingServiceMock = RoutingService as jest.Mock const ConnectionServiceMock = ConnectionService as jest.Mock -const DispatcherMock = Dispatcher as jest.Mock const credentialRepository = new CredentialRepositoryMock() const didCommMessageRepository = new DidCommMessageRepositoryMock() -const routingService = new RoutingServiceMock() const indyCredentialFormatService = new IndyCredentialFormatServiceMock() const jsonLdCredentialFormatService = new JsonLdCredentialFormatServiceMock() -const dispatcher = new DispatcherMock() const connectionService = new ConnectionServiceMock() // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -83,8 +77,6 @@ const agentContext = getAgentContext({ registerInstances: [ [CredentialRepository, credentialRepository], [DidCommMessageRepository, didCommMessageRepository], - [RoutingService, routingService], - [Dispatcher, dispatcher], [ConnectionService, connectionService], [EventEmitter, eventEmitter], ], @@ -129,7 +121,7 @@ const credentialAttachment = new Attachment({ }) const requestFormat = new CredentialFormatSpec({ - attachId: 'request-attachment-id', + attachmentId: 'request-attachment-id', format: 'hlindy/cred-filter@v2.0', }) @@ -143,17 +135,17 @@ const proposalAttachment = new Attachment({ }) const offerFormat = new CredentialFormatSpec({ - attachId: 'offer-attachment-id', + attachmentId: 'offer-attachment-id', format: 'hlindy/cred-abstract@v2.0', }) const proposalFormat = new CredentialFormatSpec({ - attachId: 'proposal-attachment-id', + attachmentId: 'proposal-attachment-id', format: 'hlindy/cred-abstract@v2.0', }) const credentialFormat = new CredentialFormatSpec({ - attachId: 'credential-attachment-id', + attachmentId: 'credential-attachment-id', format: 'hlindy/cred@v2.0', }) @@ -687,22 +679,25 @@ describe('credentialProtocol', () => { }) describe('createProblemReport', () => { - test('returns problem report message base once get error', () => { + test('returns problem report message base once get error', async () => { // given const credentialRecord = mockCredentialRecord({ state: CredentialState.OfferReceived, threadId: 'somethreadid', connectionId: 'b1e2f039-aa39-40be-8643-6ce2797b5190', }) - const message = 'Indy error' + const description = 'Indy error' mockFunction(credentialRepository.getById).mockResolvedValue(credentialRecord) // when - const credentialProblemReportMessage = credentialProtocol.createProblemReport(agentContext, { message }) + const { message } = await credentialProtocol.createProblemReport(agentContext, { + description, + credentialRecord, + }) - credentialProblemReportMessage.setThread({ threadId: 'somethreadid' }) + message.setThread({ threadId: 'somethreadid' }) // then - expect(credentialProblemReportMessage.toJSON()).toMatchObject({ + expect(message.toJSON()).toMatchObject({ '@id': expect.any(String), '@type': 'https://didcomm.org/issue-credential/2.0/problem-report', '~thread': { @@ -710,21 +705,21 @@ describe('credentialProtocol', () => { }, description: { code: CredentialProblemReportReason.IssuanceAbandoned, - en: message, + en: description, }, }) }) }) describe('processProblemReport', () => { - const credentialProblemReportMessage = new V2CredentialProblemReportMessage({ + const message = new V2CredentialProblemReportMessage({ description: { en: 'Indy error', code: CredentialProblemReportReason.IssuanceAbandoned, }, }) - credentialProblemReportMessage.setThread({ threadId: 'somethreadid' }) - const messageContext = new InboundMessageContext(credentialProblemReportMessage, { + message.setThread({ threadId: 'somethreadid' }) + const messageContext = new InboundMessageContext(message, { connection, agentContext, }) @@ -875,68 +870,4 @@ describe('credentialProtocol', () => { expect(didCommMessageRepository.delete).toHaveBeenCalledTimes(3) }) }) - - describe('declineOffer', () => { - test(`updates state to ${CredentialState.Declined}`, async () => { - const credentialRecord = mockCredentialRecord({ - state: CredentialState.OfferReceived, - }) - - // when - await credentialProtocol.declineOffer(agentContext, credentialRecord) - - // then - - expect(credentialRepository.update).toHaveBeenNthCalledWith( - 1, - agentContext, - expect.objectContaining({ - state: CredentialState.Declined, - }) - ) - }) - - test(`emits stateChange event from ${CredentialState.OfferReceived} to ${CredentialState.Declined}`, async () => { - const credentialRecord = mockCredentialRecord({ - state: CredentialState.OfferReceived, - }) - - const eventListenerMock = jest.fn() - eventEmitter.on(CredentialEventTypes.CredentialStateChanged, eventListenerMock) - - // given - mockFunction(credentialRepository.getSingleByQuery).mockResolvedValue(credentialRecord) - - // when - await credentialProtocol.declineOffer(agentContext, credentialRecord) - - // then - expect(eventListenerMock).toHaveBeenCalledTimes(1) - const [[event]] = eventListenerMock.mock.calls - expect(event).toMatchObject({ - type: 'CredentialStateChanged', - metadata: { - contextCorrelationId: 'mock', - }, - payload: { - previousState: CredentialState.OfferReceived, - credentialRecord: expect.objectContaining({ - state: CredentialState.Declined, - }), - }, - }) - }) - - const validState = CredentialState.OfferReceived - const invalidCredentialStates = Object.values(CredentialState).filter((state) => state !== validState) - test(`throws an error when state transition is invalid`, async () => { - await Promise.all( - invalidCredentialStates.map(async (state) => { - await expect( - credentialProtocol.declineOffer(agentContext, mockCredentialRecord({ state })) - ).rejects.toThrowError(`Credential record is in invalid state ${state}. Valid states are: ${validState}.`) - }) - ) - }) - }) }) diff --git a/packages/core/src/modules/credentials/protocol/v2/__tests__/V2CredentialProtocolOffer.test.ts b/packages/core/src/modules/credentials/protocol/v2/__tests__/V2CredentialProtocolOffer.test.ts index 7b76103178..5ae8e56632 100644 --- a/packages/core/src/modules/credentials/protocol/v2/__tests__/V2CredentialProtocolOffer.test.ts +++ b/packages/core/src/modules/credentials/protocol/v2/__tests__/V2CredentialProtocolOffer.test.ts @@ -1,5 +1,5 @@ import type { CredentialStateChangedEvent } from '../../../CredentialEvents' -import type { CreateOfferOptions } from '../../../CredentialProtocolOptions' +import type { CreateCredentialOfferOptions } from '../../CredentialProtocolOptions' import { Subject } from 'rxjs' @@ -78,7 +78,7 @@ const agentContext = getAgentContext({ agentConfig, }) -const connection = getMockConnection({ +const connectionRecord = getMockConnection({ id: '123', state: DidExchangeState.Completed, }) @@ -88,7 +88,7 @@ const credentialPreview = V1CredentialPreview.fromRecord({ age: '99', }) const offerFormat = new CredentialFormatSpec({ - attachId: 'offer-attachment-id', + attachmentId: 'offer-attachment-id', format: 'hlindy/cred-abstract@v2.0', }) @@ -106,7 +106,7 @@ describe('V2CredentialProtocolOffer', () => { beforeEach(async () => { // mock function implementations - mockFunction(connectionService.getById).mockResolvedValue(connection) + mockFunction(connectionService.getById).mockResolvedValue(connectionRecord) mockFunction(indyLedgerService.getCredentialDefinition).mockResolvedValue(credDef) mockFunction(indyLedgerService.getSchema).mockResolvedValue(schema) @@ -120,9 +120,9 @@ describe('V2CredentialProtocolOffer', () => { }) describe('createOffer', () => { - const offerOptions: CreateOfferOptions<[IndyCredentialFormatService]> = { + const offerOptions: CreateCredentialOfferOptions<[IndyCredentialFormatService]> = { comment: 'some comment', - connection, + connectionRecord, credentialFormats: { indy: { attributes: credentialPreview.attributes, @@ -150,7 +150,7 @@ describe('V2CredentialProtocolOffer', () => { id: expect.any(String), createdAt: expect.any(Date), state: CredentialState.OfferSent, - connectionId: connection.id, + connectionId: connectionRecord.id, }) ) }) @@ -224,7 +224,10 @@ describe('V2CredentialProtocolOffer', () => { offerAttachments: [offerAttachment], }) - const messageContext = new InboundMessageContext(credentialOfferMessage, { agentContext, connection }) + const messageContext = new InboundMessageContext(credentialOfferMessage, { + agentContext, + connection: connectionRecord, + }) test(`creates and return credential record in ${CredentialState.OfferReceived} state with offer, thread ID`, async () => { mockFunction(indyCredentialFormatService.supportsFormat).mockReturnValue(true) @@ -241,7 +244,7 @@ describe('V2CredentialProtocolOffer', () => { id: expect.any(String), createdAt: expect.any(Date), threadId: credentialOfferMessage.id, - connectionId: connection.id, + connectionId: connectionRecord.id, state: CredentialState.OfferReceived, }) ) diff --git a/packages/core/src/modules/credentials/protocol/v2/handlers/V2OfferCredentialHandler.ts b/packages/core/src/modules/credentials/protocol/v2/handlers/V2OfferCredentialHandler.ts index f23f53d2c0..8320314f0a 100644 --- a/packages/core/src/modules/credentials/protocol/v2/handlers/V2OfferCredentialHandler.ts +++ b/packages/core/src/modules/credentials/protocol/v2/handlers/V2OfferCredentialHandler.ts @@ -32,8 +32,7 @@ export class V2OfferCredentialHandler implements MessageHandler { private async acceptOffer( credentialRecord: CredentialExchangeRecord, - messageContext: MessageHandlerInboundMessage, - offerMessage?: V2OfferCredentialMessage + messageContext: MessageHandlerInboundMessage ) { messageContext.agentContext.config.logger.info(`Automatically sending request with autoAccept`) @@ -46,7 +45,7 @@ export class V2OfferCredentialHandler implements MessageHandler { connection: messageContext.connection, associatedRecord: credentialRecord, }) - } else if (offerMessage?.service) { + } else if (messageContext.message?.service) { const routingService = messageContext.agentContext.dependencyManager.resolve(RoutingService) const routing = await routingService.getRouting(messageContext.agentContext) const ourService = new ServiceDecorator({ @@ -54,7 +53,7 @@ export class V2OfferCredentialHandler implements MessageHandler { recipientKeys: [routing.recipientKey.publicKeyBase58], routingKeys: routing.routingKeys.map((key) => key.publicKeyBase58), }) - const recipientService = offerMessage.service + const recipientService = messageContext.message.service const { message } = await this.credentialProtocol.acceptOffer(messageContext.agentContext, { credentialRecord, diff --git a/packages/core/src/modules/credentials/protocol/v2/messages/V2IssueCredentialMessage.ts b/packages/core/src/modules/credentials/protocol/v2/messages/V2IssueCredentialMessage.ts index 95bbc29663..45bfec36c0 100644 --- a/packages/core/src/modules/credentials/protocol/v2/messages/V2IssueCredentialMessage.ts +++ b/packages/core/src/modules/credentials/protocol/v2/messages/V2IssueCredentialMessage.ts @@ -6,7 +6,7 @@ import { Attachment } from '../../../../../decorators/attachment/Attachment' import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' import { CredentialFormatSpec } from '../../../models' -export interface V2IssueCredentialMessageProps { +export interface V2IssueCredentialMessageOptions { id?: string comment?: string formats: CredentialFormatSpec[] @@ -14,7 +14,7 @@ export interface V2IssueCredentialMessageProps { } export class V2IssueCredentialMessage extends AgentMessage { - public constructor(options: V2IssueCredentialMessageProps) { + public constructor(options: V2IssueCredentialMessageOptions) { super() if (options) { diff --git a/packages/core/src/modules/credentials/protocol/v2/messages/V2ProposeCredentialMessage.ts b/packages/core/src/modules/credentials/protocol/v2/messages/V2ProposeCredentialMessage.ts index b4cbbc5b01..cc7873d505 100644 --- a/packages/core/src/modules/credentials/protocol/v2/messages/V2ProposeCredentialMessage.ts +++ b/packages/core/src/modules/credentials/protocol/v2/messages/V2ProposeCredentialMessage.ts @@ -8,7 +8,7 @@ import { CredentialFormatSpec } from '../../../models' import { V2CredentialPreview } from './V2CredentialPreview' -export interface V2ProposeCredentialMessageProps { +export interface V2ProposeCredentialMessageOptions { id?: string formats: CredentialFormatSpec[] proposalAttachments: Attachment[] @@ -18,21 +18,22 @@ export interface V2ProposeCredentialMessageProps { } export class V2ProposeCredentialMessage extends AgentMessage { - public constructor(props: V2ProposeCredentialMessageProps) { + public constructor(options: V2ProposeCredentialMessageOptions) { super() - if (props) { - this.id = props.id ?? this.generateId() - this.comment = props.comment - this.credentialPreview = props.credentialPreview - this.formats = props.formats - this.proposalAttachments = props.proposalAttachments - this.appendedAttachments = props.attachments + if (options) { + this.id = options.id ?? this.generateId() + this.comment = options.comment + this.credentialPreview = options.credentialPreview + this.formats = options.formats + this.proposalAttachments = options.proposalAttachments + this.appendedAttachments = options.attachments } } @Type(() => CredentialFormatSpec) - @ValidateNested() + @ValidateNested({ each: true }) @IsArray() + @IsInstance(CredentialFormatSpec, { each: true }) public formats!: CredentialFormatSpec[] @IsValidMessageType(V2ProposeCredentialMessage.type) @@ -64,6 +65,6 @@ export class V2ProposeCredentialMessage extends AgentMessage { public comment?: string public getProposalAttachmentById(id: string): Attachment | undefined { - return this.proposalAttachments.find((attachment) => attachment.id == id) + return this.proposalAttachments.find((attachment) => attachment.id === id) } } diff --git a/packages/core/src/modules/credentials/util/composeAutoAccept.ts b/packages/core/src/modules/credentials/util/composeAutoAccept.ts index 55b3e70362..ace6fdf80c 100644 --- a/packages/core/src/modules/credentials/util/composeAutoAccept.ts +++ b/packages/core/src/modules/credentials/util/composeAutoAccept.ts @@ -6,7 +6,6 @@ import { AutoAcceptCredential } from '../models/CredentialAutoAcceptType' * - Otherwise the agent config * - Otherwise {@link AutoAcceptCredential.Never} is returned */ - export function composeAutoAccept( recordConfig: AutoAcceptCredential | undefined, agentConfig: AutoAcceptCredential | undefined diff --git a/packages/core/src/modules/proofs/ProofResponseCoordinator.ts b/packages/core/src/modules/proofs/ProofResponseCoordinator.ts deleted file mode 100644 index db625f71e0..0000000000 --- a/packages/core/src/modules/proofs/ProofResponseCoordinator.ts +++ /dev/null @@ -1,93 +0,0 @@ -import type { ProofExchangeRecord } from './repository' -import type { AgentContext } from '../../agent/context/AgentContext' - -import { injectable } from '../../plugins' - -import { ProofService } from './ProofService' -import { AutoAcceptProof } from './models/ProofAutoAcceptType' - -/** - * This class handles all the automation with all the messages in the present proof protocol - * Every function returns `true` if it should automate the flow and `false` if not - */ -@injectable() -export class ProofResponseCoordinator { - private proofService: ProofService - - public constructor(proofService: ProofService) { - this.proofService = proofService - } - - /** - * Returns the proof auto accept config based on priority: - * - The record config takes first priority - * - Otherwise the agent config - * - Otherwise {@link AutoAcceptProof.Never} is returned - */ - private static composeAutoAccept( - recordConfig: AutoAcceptProof | undefined, - agentConfig: AutoAcceptProof | undefined - ) { - return recordConfig ?? agentConfig ?? AutoAcceptProof.Never - } - - /** - * Checks whether it should automatically respond to a proposal - */ - public async shouldAutoRespondToProposal(agentContext: AgentContext, proofRecord: ProofExchangeRecord) { - const autoAccept = ProofResponseCoordinator.composeAutoAccept( - proofRecord.autoAcceptProof, - agentContext.config.autoAcceptProofs - ) - - if (autoAccept === AutoAcceptProof.Always) { - return true - } - - if (autoAccept === AutoAcceptProof.ContentApproved) { - return this.proofService.shouldAutoRespondToProposal(agentContext, proofRecord) - } - - return false - } - - /** - * Checks whether it should automatically respond to a request - */ - public async shouldAutoRespondToRequest(agentContext: AgentContext, proofRecord: ProofExchangeRecord) { - const autoAccept = ProofResponseCoordinator.composeAutoAccept( - proofRecord.autoAcceptProof, - agentContext.config.autoAcceptProofs - ) - - if (autoAccept === AutoAcceptProof.Always) { - return true - } - - if (autoAccept === AutoAcceptProof.ContentApproved) { - return this.proofService.shouldAutoRespondToRequest(agentContext, proofRecord) - } - - return false - } - - /** - * Checks whether it should automatically respond to a presentation of proof - */ - public async shouldAutoRespondToPresentation(agentContext: AgentContext, proofRecord: ProofExchangeRecord) { - const autoAccept = ProofResponseCoordinator.composeAutoAccept( - proofRecord.autoAcceptProof, - agentContext.config.autoAcceptProofs - ) - - if (autoAccept === AutoAcceptProof.Always) { - return true - } - - if (autoAccept === AutoAcceptProof.ContentApproved) { - return this.proofService.shouldAutoRespondToPresentation(agentContext, proofRecord) - } - - return false - } -} diff --git a/packages/core/src/modules/proofs/ProofService.ts b/packages/core/src/modules/proofs/ProofService.ts deleted file mode 100644 index 2fcbe509b1..0000000000 --- a/packages/core/src/modules/proofs/ProofService.ts +++ /dev/null @@ -1,261 +0,0 @@ -import type { ProofStateChangedEvent } from './ProofEvents' -import type { ProofResponseCoordinator } from './ProofResponseCoordinator' -import type { ProofFormat } from './formats/ProofFormat' -import type { CreateProblemReportOptions } from './formats/models/ProofFormatServiceOptions' -import type { - CreateAckOptions, - CreatePresentationOptions, - CreateProofRequestFromProposalOptions, - CreateProposalAsResponseOptions, - CreateProposalOptions, - CreateRequestAsResponseOptions, - CreateRequestOptions, - DeleteProofOptions, - FormatRequestedCredentialReturn, - FormatRetrievedCredentialOptions, - GetFormatDataReturn, - GetRequestedCredentialsForProofRequestOptions, - ProofRequestFromProposalOptions, -} from './models/ProofServiceOptions' -import type { ProofState } from './models/ProofState' -import type { ProofExchangeRecord, ProofRepository } from './repository' -import type { AgentConfig } from '../../agent/AgentConfig' -import type { AgentMessage } from '../../agent/AgentMessage' -import type { Dispatcher } from '../../agent/Dispatcher' -import type { EventEmitter } from '../../agent/EventEmitter' -import type { AgentContext } from '../../agent/context/AgentContext' -import type { InboundMessageContext } from '../../agent/models/InboundMessageContext' -import type { Logger } from '../../logger' -import type { DidCommMessageRepository, DidCommMessageRole } from '../../storage' -import type { Wallet } from '../../wallet/Wallet' -import type { ConnectionService } from '../connections/services' -import type { MediationRecipientService, RoutingService } from '../routing' - -import { JsonTransformer } from '../../utils/JsonTransformer' - -import { ProofEventTypes } from './ProofEvents' - -export abstract class ProofService { - protected proofRepository: ProofRepository - protected didCommMessageRepository: DidCommMessageRepository - protected eventEmitter: EventEmitter - protected connectionService: ConnectionService - protected wallet: Wallet - protected logger: Logger - - public constructor( - agentConfig: AgentConfig, - proofRepository: ProofRepository, - connectionService: ConnectionService, - didCommMessageRepository: DidCommMessageRepository, - wallet: Wallet, - eventEmitter: EventEmitter - ) { - this.proofRepository = proofRepository - this.connectionService = connectionService - this.didCommMessageRepository = didCommMessageRepository - this.eventEmitter = eventEmitter - this.wallet = wallet - this.logger = agentConfig.logger - } - public abstract readonly version: string - - public emitStateChangedEvent( - agentContext: AgentContext, - proofRecord: ProofExchangeRecord, - previousState: ProofState | null - ) { - const clonedProof = JsonTransformer.clone(proofRecord) - - this.eventEmitter.emit(agentContext, { - type: ProofEventTypes.ProofStateChanged, - payload: { - proofRecord: clonedProof, - previousState: previousState, - }, - }) - } - - /** - * Update the record to a new state and emit an state changed event. Also updates the record - * in storage. - * - * @param proofRecord The proof record to update the state for - * @param newState The state to update to - * - */ - public async updateState(agentContext: AgentContext, proofRecord: ProofExchangeRecord, newState: ProofState) { - const previousState = proofRecord.state - proofRecord.state = newState - await this.proofRepository.update(agentContext, proofRecord) - - this.emitStateChangedEvent(agentContext, proofRecord, previousState) - } - - public update(agentContext: AgentContext, proofRecord: ProofExchangeRecord) { - return this.proofRepository.update(agentContext, proofRecord) - } - - /** - * 1. Assert (connection ready, record state) - * 2. Create proposal message - * 3. loop through all formats from ProposeProofOptions and call format service - * 4. Create and store proof record - * 5. Store proposal message - * 6. Return proposal message + proof record - */ - public abstract createProposal( - agentContext: AgentContext, - options: CreateProposalOptions - ): Promise<{ proofRecord: ProofExchangeRecord; message: AgentMessage }> - - /** - * Create a proposal message in response to a received proof request message - * - * 1. assert record state - * 2. Create proposal message - * 3. loop through all formats from ProposeProofOptions and call format service - * 4. Update proof record - * 5. Create or update proposal message - * 6. Return proposal message + proof record - */ - public abstract createProposalAsResponse( - agentContext: AgentContext, - options: CreateProposalAsResponseOptions - ): Promise<{ proofRecord: ProofExchangeRecord; message: AgentMessage }> - - /** - * Process a received proposal message (does not accept yet) - * - * 1. Find proof record by thread and connection id - * - * Two flows possible: - * - Proof record already exist - * 2. Assert state - * 3. Save or update proposal message in storage (didcomm message record) - * 4. Loop through all format services to process proposal message - * 5. Update & return record - * - * - Proof record does not exist yet - * 2. Create record - * 3. Save proposal message - * 4. Loop through all format services to process proposal message - * 5. Save & return record - */ - public abstract processProposal(messageContext: InboundMessageContext): Promise - - public abstract createRequest( - agentContext: AgentContext, - options: CreateRequestOptions - ): Promise<{ proofRecord: ProofExchangeRecord; message: AgentMessage }> - - public abstract createRequestAsResponse( - agentContext: AgentContext, - options: CreateRequestAsResponseOptions - ): Promise<{ proofRecord: ProofExchangeRecord; message: AgentMessage }> - - public abstract processRequest(messageContext: InboundMessageContext): Promise - - public abstract createPresentation( - agentContext: AgentContext, - options: CreatePresentationOptions - ): Promise<{ proofRecord: ProofExchangeRecord; message: AgentMessage }> - - public abstract processPresentation(messageContext: InboundMessageContext): Promise - - public abstract createAck( - agentContext: AgentContext, - options: CreateAckOptions - ): Promise<{ proofRecord: ProofExchangeRecord; message: AgentMessage }> - - public abstract processAck(messageContext: InboundMessageContext): Promise - - public abstract createProblemReport( - agentContext: AgentContext, - options: CreateProblemReportOptions - ): Promise<{ proofRecord: ProofExchangeRecord; message: AgentMessage }> - - public abstract processProblemReport( - messageContext: InboundMessageContext - ): Promise - - public abstract shouldAutoRespondToProposal( - agentContext: AgentContext, - proofRecord: ProofExchangeRecord - ): Promise - - public abstract shouldAutoRespondToRequest( - agentContext: AgentContext, - proofRecord: ProofExchangeRecord - ): Promise - - public abstract shouldAutoRespondToPresentation( - agentContext: AgentContext, - proofRecord: ProofExchangeRecord - ): Promise - - public abstract registerMessageHandlers( - dispatcher: Dispatcher, - agentConfig: AgentConfig, - proofResponseCoordinator: ProofResponseCoordinator, - mediationRecipientService: MediationRecipientService, - routingService: RoutingService - ): void - - public abstract findProposalMessage(agentContext: AgentContext, proofRecordId: string): Promise - public abstract findRequestMessage(agentContext: AgentContext, proofRecordId: string): Promise - public abstract findPresentationMessage( - agentContext: AgentContext, - proofRecordId: string - ): Promise - - public async saveOrUpdatePresentationMessage( - agentContext: AgentContext, - options: { - proofRecord: ProofExchangeRecord - message: AgentMessage - role: DidCommMessageRole - } - ): Promise { - await this.didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { - associatedRecordId: options.proofRecord.id, - agentMessage: options.message, - role: options.role, - }) - } - - public async delete( - agentContext: AgentContext, - proofRecord: ProofExchangeRecord, - options?: DeleteProofOptions - ): Promise { - await this.proofRepository.delete(agentContext, proofRecord) - - const deleteAssociatedDidCommMessages = options?.deleteAssociatedDidCommMessages ?? true - - if (deleteAssociatedDidCommMessages) { - const didCommMessages = await this.didCommMessageRepository.findByQuery(agentContext, { - associatedRecordId: proofRecord.id, - }) - for (const didCommMessage of didCommMessages) { - await this.didCommMessageRepository.delete(agentContext, didCommMessage) - } - } - } - - public abstract getRequestedCredentialsForProofRequest( - agentContext: AgentContext, - options: GetRequestedCredentialsForProofRequestOptions - ): Promise> - - public abstract autoSelectCredentialsForProofRequest( - options: FormatRetrievedCredentialOptions - ): Promise> - - public abstract createProofRequestFromProposal( - agentContext: AgentContext, - options: CreateProofRequestFromProposalOptions - ): Promise> - - public abstract getFormatData(agentContext: AgentContext, proofRecordId: string): Promise> -} diff --git a/packages/core/src/modules/proofs/ProofsApi.ts b/packages/core/src/modules/proofs/ProofsApi.ts index 38f5e642bd..286637a66b 100644 --- a/packages/core/src/modules/proofs/ProofsApi.ts +++ b/packages/core/src/modules/proofs/ProofsApi.ts @@ -1,93 +1,77 @@ -import type { ProofService } from './ProofService' import type { - AcceptProofPresentationOptions, + AcceptProofOptions, AcceptProofProposalOptions, + AcceptProofRequestOptions, CreateProofRequestOptions, + DeleteProofOptions, FindProofPresentationMessageReturn, FindProofProposalMessageReturn, FindProofRequestMessageReturn, + GetCredentialsForProofRequestOptions, + GetCredentialsForProofRequestReturn, + GetProofFormatDataReturn, + NegotiateProofProposalOptions, + NegotiateProofRequestOptions, ProposeProofOptions, RequestProofOptions, - ProofServiceMap, - NegotiateRequestOptions, - NegotiateProposalOptions, + SelectCredentialsForProofRequestOptions, + SelectCredentialsForProofRequestReturn, + SendProofProblemReportOptions, } from './ProofsApiOptions' -import type { ProofFormat } from './formats/ProofFormat' -import type { IndyProofFormat } from './formats/indy/IndyProofFormat' -import type { - AutoSelectCredentialsForProofRequestOptions, - GetRequestedCredentialsForProofRequest, -} from './models/ModuleOptions' -import type { - CreatePresentationOptions, - CreateProposalOptions, - CreateRequestOptions, - CreateRequestAsResponseOptions, - CreateProofRequestFromProposalOptions, - FormatRequestedCredentialReturn, - FormatRetrievedCredentialOptions, - DeleteProofOptions, - GetFormatDataReturn, - CreateProposalAsResponseOptions, -} from './models/ProofServiceOptions' +import type { ProofProtocol } from './protocol/ProofProtocol' +import type { ProofFormatsFromProtocols } from './protocol/ProofProtocolOptions' import type { ProofExchangeRecord } from './repository/ProofExchangeRecord' import type { AgentMessage } from '../../agent/AgentMessage' import type { Query } from '../../storage/StorageService' -import { inject, injectable } from 'tsyringe' +import { injectable } from 'tsyringe' -import { AgentConfig } from '../../agent/AgentConfig' -import { Dispatcher } from '../../agent/Dispatcher' import { MessageSender } from '../../agent/MessageSender' import { AgentContext } from '../../agent/context/AgentContext' import { OutboundMessageContext } from '../../agent/models' -import { InjectionSymbols } from '../../constants' import { ServiceDecorator } from '../../decorators/service/ServiceDecorator' import { AriesFrameworkError } from '../../error' -import { Logger } from '../../logger' +import { DidCommMessageRepository } from '../../storage' import { DidCommMessageRole } from '../../storage/didcomm/DidCommMessageRole' import { ConnectionService } from '../connections/services/ConnectionService' -import { MediationRecipientService } from '../routing/services/MediationRecipientService' import { RoutingService } from '../routing/services/RoutingService' -import { ProofResponseCoordinator } from './ProofResponseCoordinator' +import { ProofsModuleConfig } from './ProofsModuleConfig' import { ProofState } from './models/ProofState' -import { V1ProofService } from './protocol/v1/V1ProofService' -import { V2ProofService } from './protocol/v2/V2ProofService' import { ProofRepository } from './repository/ProofRepository' -export interface ProofsApi[]> { +export interface ProofsApi { // Proposal methods - proposeProof(options: ProposeProofOptions): Promise - acceptProposal(options: AcceptProofProposalOptions): Promise - negotiateProposal(options: NegotiateProposalOptions): Promise + proposeProof(options: ProposeProofOptions): Promise + acceptProposal(options: AcceptProofProposalOptions): Promise + negotiateProposal(options: NegotiateProofProposalOptions): Promise // Request methods - requestProof(options: RequestProofOptions): Promise - acceptRequest(options: AcceptProofPresentationOptions): Promise + requestProof(options: RequestProofOptions): Promise + acceptRequest(options: AcceptProofRequestOptions): Promise declineRequest(proofRecordId: string): Promise - negotiateRequest(options: NegotiateRequestOptions): Promise + negotiateRequest(options: NegotiateProofRequestOptions): Promise // Present - acceptPresentation(proofRecordId: string): Promise + acceptPresentation(options: AcceptProofOptions): Promise // out of band - createRequest(options: CreateProofRequestOptions): Promise<{ + createRequest(options: CreateProofRequestOptions): Promise<{ message: AgentMessage proofRecord: ProofExchangeRecord }> // Auto Select - autoSelectCredentialsForProofRequest( - options: AutoSelectCredentialsForProofRequestOptions - ): Promise> + selectCredentialsForRequest( + options: SelectCredentialsForProofRequestOptions + ): Promise> - // Get Requested Credentials - getRequestedCredentialsForProofRequest( - options: AutoSelectCredentialsForProofRequestOptions - ): Promise> + // Get credentials for request + getCredentialsForRequest( + options: GetCredentialsForProofRequestOptions + ): Promise> - sendProblemReport(proofRecordId: string, message: string): Promise + sendProblemReport(options: SendProofProblemReportOptions): Promise // Record Methods getAll(): Promise @@ -96,107 +80,87 @@ export interface ProofsApi deleteById(proofId: string, options?: DeleteProofOptions): Promise update(proofRecord: ProofExchangeRecord): Promise - getFormatData(proofRecordId: string): Promise> + getFormatData(proofRecordId: string): Promise>> // DidComm Message Records - findProposalMessage(proofRecordId: string): Promise> - findRequestMessage(proofRecordId: string): Promise> - findPresentationMessage(proofRecordId: string): Promise> + findProposalMessage(proofRecordId: string): Promise> + findRequestMessage(proofRecordId: string): Promise> + findPresentationMessage(proofRecordId: string): Promise> } @injectable() -export class ProofsApi< - PFs extends ProofFormat[] = [IndyProofFormat], - PSs extends ProofService[] = [V1ProofService, V2ProofService] -> implements ProofsApi -{ +export class ProofsApi implements ProofsApi { + /** + * Configuration for the credentials module + */ + public readonly config: ProofsModuleConfig + private connectionService: ConnectionService private messageSender: MessageSender private routingService: RoutingService private proofRepository: ProofRepository + private didCommMessageRepository: DidCommMessageRepository private agentContext: AgentContext - private agentConfig: AgentConfig - private logger: Logger - private serviceMap: ProofServiceMap public constructor( - dispatcher: Dispatcher, - mediationRecipientService: MediationRecipientService, messageSender: MessageSender, connectionService: ConnectionService, agentContext: AgentContext, - agentConfig: AgentConfig, - routingService: RoutingService, - @inject(InjectionSymbols.Logger) logger: Logger, proofRepository: ProofRepository, - v1Service: V1ProofService, - v2Service: V2ProofService + routingService: RoutingService, + didCommMessageRepository: DidCommMessageRepository, + config: ProofsModuleConfig ) { this.messageSender = messageSender this.connectionService = connectionService this.proofRepository = proofRepository this.agentContext = agentContext - this.agentConfig = agentConfig this.routingService = routingService - this.logger = logger - // Dynamically build service map. This will be extracted once services are registered dynamically - this.serviceMap = [v1Service, v2Service].reduce( - (serviceMap, service) => ({ - ...serviceMap, - [service.version]: service, - }), - {} - ) as ProofServiceMap - - this.logger.debug(`Initializing Proofs Module for agent ${this.agentContext.config.label}`) - - this.registerMessageHandlers(dispatcher, mediationRecipientService) + this.didCommMessageRepository = didCommMessageRepository + this.config = config } - public getService(protocolVersion: PVT): ProofService { - if (!this.serviceMap[protocolVersion]) { - throw new AriesFrameworkError(`No proof service registered for protocol version ${protocolVersion}`) + private getProtocol(protocolVersion: PVT): ProofProtocol { + const proofProtocol = this.config.proofProtocols.find((protocol) => protocol.version === protocolVersion) + + if (!proofProtocol) { + throw new AriesFrameworkError(`No proof protocol registered for protocol version ${protocolVersion}`) } - return this.serviceMap[protocolVersion] as ProofService + return proofProtocol } /** * Initiate a new presentation exchange as prover by sending a presentation proposal message * to the connection with the specified connection id. * - * @param options multiple properties like protocol version, connection id, proof format (indy/ presentation exchange) - * to include in the message - * @returns Proof record associated with the sent proposal message + * @param options configuration to use for the proposal + * @returns Proof exchange record associated with the sent proposal message */ - public async proposeProof(options: ProposeProofOptions): Promise { - const service = this.getService(options.protocolVersion) + public async proposeProof(options: ProposeProofOptions): Promise { + const protocol = this.getProtocol(options.protocolVersion) - const { connectionId } = options - - const connection = await this.connectionService.getById(this.agentContext, connectionId) + const connectionRecord = await this.connectionService.getById(this.agentContext, options.connectionId) // Assert - connection.assertReady() + connectionRecord.assertReady() - const proposalOptions: CreateProposalOptions = { - connectionRecord: connection, + const { message, proofRecord } = await protocol.createProposal(this.agentContext, { + connectionRecord, proofFormats: options.proofFormats, autoAcceptProof: options.autoAcceptProof, goalCode: options.goalCode, comment: options.comment, parentThreadId: options.parentThreadId, - } - - const { message, proofRecord } = await service.createProposal(this.agentContext, proposalOptions) + }) const outboundMessageContext = new OutboundMessageContext(message, { agentContext: this.agentContext, - connection, + connection: connectionRecord, associatedRecord: proofRecord, }) - await this.messageSender.sendMessage(outboundMessageContext) + await this.messageSender.sendMessage(outboundMessageContext) return proofRecord } @@ -204,53 +168,42 @@ export class ProofsApi< * Accept a presentation proposal as verifier (by sending a presentation request message) to the connection * associated with the proof record. * - * @param options multiple properties like proof record id, additional configuration for creating the request - * @returns Proof record associated with the presentation request + * @param options config object for accepting the proposal + * @returns Proof exchange record associated with the presentation request */ - public async acceptProposal(options: AcceptProofProposalOptions): Promise { - const { proofRecordId } = options - - const proofRecord = await this.getById(proofRecordId) - - const service = this.getService(proofRecord.protocolVersion) + public async acceptProposal(options: AcceptProofProposalOptions): Promise { + const proofRecord = await this.getById(options.proofRecordId) if (!proofRecord.connectionId) { throw new AriesFrameworkError( - `No connectionId found for proof record '${proofRecord.id}'. Connection-less issuance does not support presentation proposal or negotiation.` + `No connectionId found for proof record '${proofRecord.id}'. Connection-less verification does not support presentation proposal or negotiation.` ) } - const connection = await this.connectionService.getById(this.agentContext, proofRecord.connectionId) + // with version we can get the protocol + const protocol = this.getProtocol(proofRecord.protocolVersion) + const connectionRecord = await this.connectionService.getById(this.agentContext, proofRecord.connectionId) // Assert - connection.assertReady() + connectionRecord.assertReady() - const proofRequestFromProposalOptions: CreateProofRequestFromProposalOptions = { + const { message } = await protocol.acceptProposal(this.agentContext, { proofRecord, - } - - const proofRequest = await service.createProofRequestFromProposal( - this.agentContext, - proofRequestFromProposalOptions - ) - - const requestOptions: CreateRequestAsResponseOptions = { - proofRecord: proofRecord, - proofFormats: proofRequest.proofFormats, + proofFormats: options.proofFormats, goalCode: options.goalCode, - willConfirm: options.willConfirm ?? true, + willConfirm: options.willConfirm, comment: options.comment, - } - - const { message } = await service.createRequestAsResponse(this.agentContext, requestOptions) + autoAcceptProof: options.autoAcceptProof, + }) + // send the message const outboundMessageContext = new OutboundMessageContext(message, { agentContext: this.agentContext, - connection, + connection: connectionRecord, associatedRecord: proofRecord, }) - await this.messageSender.sendMessage(outboundMessageContext) + await this.messageSender.sendMessage(outboundMessageContext) return proofRecord } @@ -262,35 +215,33 @@ export class ProofsApi< * specifying which credentials to use for the proof * @returns Proof record associated with the sent request message */ - public async negotiateProposal(options: NegotiateProposalOptions): Promise { - const { proofRecordId } = options - - const proofRecord = await this.getById(proofRecordId) - - const service = this.getService(proofRecord.protocolVersion) + public async negotiateProposal(options: NegotiateProofProposalOptions): Promise { + const proofRecord = await this.getById(options.proofRecordId) if (!proofRecord.connectionId) { throw new AriesFrameworkError( - `No connectionId found for proof record '${proofRecord.id}'. Connection-less issuance does not support negotiation.` + `No connectionId found for proof record '${proofRecord.id}'. Connection-less verification does not support negotiation.` ) } - const connection = await this.connectionService.getById(this.agentContext, proofRecord.connectionId) + const protocol = this.getProtocol(proofRecord.protocolVersion) + const connectionRecord = await this.connectionService.getById(this.agentContext, proofRecord.connectionId) // Assert - connection.assertReady() + connectionRecord.assertReady() - const requestOptions: CreateRequestAsResponseOptions = { + const { message } = await protocol.negotiateProposal(this.agentContext, { proofRecord, proofFormats: options.proofFormats, autoAcceptProof: options.autoAcceptProof, comment: options.comment, - } - const { message } = await service.createRequestAsResponse(this.agentContext, requestOptions) + goalCode: options.goalCode, + willConfirm: options.willConfirm, + }) const outboundMessageContext = new OutboundMessageContext(message, { agentContext: this.agentContext, - connection, + connection: connectionRecord, associatedRecord: proofRecord, }) await this.messageSender.sendMessage(outboundMessageContext) @@ -305,30 +256,30 @@ export class ProofsApi< * @param options multiple properties like connection id, protocol version, proof Formats to build the proof request * @returns Proof record associated with the sent request message */ - public async requestProof(options: RequestProofOptions): Promise { - const service = this.getService(options.protocolVersion) - - const connection = await this.connectionService.getById(this.agentContext, options.connectionId) + public async requestProof(options: RequestProofOptions): Promise { + const connectionRecord = await this.connectionService.getById(this.agentContext, options.connectionId) + const protocol = this.getProtocol(options.protocolVersion) // Assert - connection.assertReady() + connectionRecord.assertReady() - const createProofRequest: CreateRequestOptions = { - connectionRecord: connection, + const { message, proofRecord } = await protocol.createRequest(this.agentContext, { + connectionRecord, proofFormats: options.proofFormats, autoAcceptProof: options.autoAcceptProof, parentThreadId: options.parentThreadId, comment: options.comment, - } - const { message, proofRecord } = await service.createRequest(this.agentContext, createProofRequest) + goalCode: options.goalCode, + willConfirm: options.willConfirm, + }) const outboundMessageContext = new OutboundMessageContext(message, { agentContext: this.agentContext, - connection, + connection: connectionRecord, associatedRecord: proofRecord, }) - await this.messageSender.sendMessage(outboundMessageContext) + await this.messageSender.sendMessage(outboundMessageContext) return proofRecord } @@ -340,32 +291,31 @@ export class ProofsApi< * specifying which credentials to use for the proof * @returns Proof record associated with the sent presentation message */ - public async acceptRequest(options: AcceptProofPresentationOptions): Promise { - const { proofRecordId, proofFormats, comment } = options - - const record = await this.getById(proofRecordId) - - const service = this.getService(record.protocolVersion) + public async acceptRequest(options: AcceptProofRequestOptions): Promise { + const proofRecord = await this.getById(options.proofRecordId) - const presentationOptions: CreatePresentationOptions = { - proofFormats, - proofRecord: record, - comment, - } - const { message, proofRecord } = await service.createPresentation(this.agentContext, presentationOptions) + const protocol = this.getProtocol(proofRecord.protocolVersion) - const requestMessage = await service.findRequestMessage(this.agentContext, proofRecord.id) + const requestMessage = await protocol.findRequestMessage(this.agentContext, proofRecord.id) // Use connection if present if (proofRecord.connectionId) { - const connection = await this.connectionService.getById(this.agentContext, proofRecord.connectionId) + const connectionRecord = await this.connectionService.getById(this.agentContext, proofRecord.connectionId) // Assert - connection.assertReady() + connectionRecord.assertReady() + + const { message } = await protocol.acceptRequest(this.agentContext, { + proofFormats: options.proofFormats, + proofRecord, + comment: options.comment, + autoAcceptProof: options.autoAcceptProof, + goalCode: options.goalCode, + }) const outboundMessageContext = new OutboundMessageContext(message, { agentContext: this.agentContext, - connection, + connection: connectionRecord, associatedRecord: proofRecord, }) await this.messageSender.sendMessage(outboundMessageContext) @@ -377,20 +327,27 @@ export class ProofsApi< else if (requestMessage?.service) { // Create ~service decorator const routing = await this.routingService.getRouting(this.agentContext) - message.service = new ServiceDecorator({ + const ourService = new ServiceDecorator({ serviceEndpoint: routing.endpoints[0], recipientKeys: [routing.recipientKey.publicKeyBase58], routingKeys: routing.routingKeys.map((key) => key.publicKeyBase58), }) - const recipientService = requestMessage.service - // Set and save ~service decorator to record (to remember our verkey) + const { message } = await protocol.acceptRequest(this.agentContext, { + proofFormats: options.proofFormats, + proofRecord, + comment: options.comment, + autoAcceptProof: options.autoAcceptProof, + goalCode: options.goalCode, + }) - await service.saveOrUpdatePresentationMessage(this.agentContext, { - proofRecord: proofRecord, - message: message, + // Set and save ~service decorator to record (to remember our verkey) + message.service = ourService + await this.didCommMessageRepository.saveOrUpdateAgentMessage(this.agentContext, { + agentMessage: message, role: DidCommMessageRole.Sender, + associatedRecordId: proofRecord.id, }) await this.messageSender.sendMessageToService( @@ -398,7 +355,7 @@ export class ProofsApi< agentContext: this.agentContext, serviceParams: { service: recipientService.resolvedDidCommService, - senderKey: message.service.resolvedDidCommService.recipientKeys[0], + senderKey: ourService.resolvedDidCommService.recipientKeys[0], returnRoute: true, }, }) @@ -414,36 +371,12 @@ export class ProofsApi< } } - /** - * Initiate a new presentation exchange as verifier by sending an out of band presentation - * request message - * - * @param options multiple properties like protocol version, proof Formats to build the proof request - * @returns the message itself and the proof record associated with the sent request message - */ - public async createRequest(options: CreateProofRequestOptions): Promise<{ - message: AgentMessage - proofRecord: ProofExchangeRecord - }> { - const service = this.getService(options.protocolVersion) - - const createProofRequest: CreateRequestOptions = { - proofFormats: options.proofFormats, - autoAcceptProof: options.autoAcceptProof, - comment: options.comment, - parentThreadId: options.parentThreadId, - } - - return await service.createRequest(this.agentContext, createProofRequest) - } - public async declineRequest(proofRecordId: string): Promise { const proofRecord = await this.getById(proofRecordId) - const service = this.getService(proofRecord.protocolVersion) - proofRecord.assertState(ProofState.RequestReceived) - await service.updateState(this.agentContext, proofRecord, ProofState.Declined) + const protocol = this.getProtocol(proofRecord.protocolVersion) + await protocol.updateState(this.agentContext, proofRecord, ProofState.Declined) return proofRecord } @@ -456,43 +389,62 @@ export class ProofsApi< * to include in the message * @returns Proof record associated with the sent proposal message */ - public async negotiateRequest(options: NegotiateRequestOptions): Promise { - const { proofRecordId } = options - const proofRecord = await this.getById(proofRecordId) - - const service = this.getService(proofRecord.protocolVersion) + public async negotiateRequest(options: NegotiateProofRequestOptions): Promise { + const proofRecord = await this.getById(options.proofRecordId) if (!proofRecord.connectionId) { throw new AriesFrameworkError( - `No connectionId found for proof record '${proofRecord.id}'. Connection-less issuance does not support presentation proposal or negotiation.` + `No connectionId found for proof record '${proofRecord.id}'. Connection-less verification does not support presentation proposal or negotiation.` ) } - const connection = await this.connectionService.getById(this.agentContext, proofRecord.connectionId) + const connectionRecord = await this.connectionService.getById(this.agentContext, proofRecord.connectionId) // Assert - connection.assertReady() + connectionRecord.assertReady() - const proposalOptions: CreateProposalAsResponseOptions = { + const protocol = this.getProtocol(proofRecord.protocolVersion) + const { message } = await protocol.negotiateRequest(this.agentContext, { proofRecord, proofFormats: options.proofFormats, autoAcceptProof: options.autoAcceptProof, goalCode: options.goalCode, comment: options.comment, - } - - const { message } = await service.createProposalAsResponse(this.agentContext, proposalOptions) + }) const outboundMessageContext = new OutboundMessageContext(message, { agentContext: this.agentContext, - connection, + connection: connectionRecord, associatedRecord: proofRecord, }) - await this.messageSender.sendMessage(outboundMessageContext) + await this.messageSender.sendMessage(outboundMessageContext) return proofRecord } + /** + * Initiate a new presentation exchange as verifier by sending an out of band presentation + * request message + * + * @param options multiple properties like protocol version, proof Formats to build the proof request + * @returns the message itself and the proof record associated with the sent request message + */ + public async createRequest(options: CreateProofRequestOptions): Promise<{ + message: AgentMessage + proofRecord: ProofExchangeRecord + }> { + const protocol = this.getProtocol(options.protocolVersion) + + return await protocol.createRequest(this.agentContext, { + proofFormats: options.proofFormats, + autoAcceptProof: options.autoAcceptProof, + comment: options.comment, + parentThreadId: options.parentThreadId, + goalCode: options.goalCode, + willConfirm: options.willConfirm, + }) + } + /** * Accept a presentation as prover (by sending a presentation acknowledgement message) to the connection * associated with the proof record. @@ -501,37 +453,42 @@ export class ProofsApi< * @returns Proof record associated with the sent presentation acknowledgement message * */ - public async acceptPresentation(proofRecordId: string): Promise { - const record = await this.getById(proofRecordId) - const service = this.getService(record.protocolVersion) - - const { message, proofRecord } = await service.createAck(this.agentContext, { - proofRecord: record, - }) - - const requestMessage = await service.findRequestMessage(this.agentContext, record.id) + public async acceptPresentation(options: AcceptProofOptions): Promise { + const proofRecord = await this.getById(options.proofRecordId) + const protocol = this.getProtocol(proofRecord.protocolVersion) - const presentationMessage = await service.findPresentationMessage(this.agentContext, record.id) + const requestMessage = await protocol.findRequestMessage(this.agentContext, proofRecord.id) + const presentationMessage = await protocol.findPresentationMessage(this.agentContext, proofRecord.id) // Use connection if present if (proofRecord.connectionId) { - const connection = await this.connectionService.getById(this.agentContext, proofRecord.connectionId) + const connectionRecord = await this.connectionService.getById(this.agentContext, proofRecord.connectionId) // Assert - connection.assertReady() + connectionRecord.assertReady() + + const { message } = await protocol.acceptPresentation(this.agentContext, { + proofRecord, + }) const outboundMessageContext = new OutboundMessageContext(message, { agentContext: this.agentContext, - connection, + connection: connectionRecord, associatedRecord: proofRecord, }) await this.messageSender.sendMessage(outboundMessageContext) + + return proofRecord } // Use ~service decorator otherwise else if (requestMessage?.service && presentationMessage?.service) { - const recipientService = presentationMessage?.service + const recipientService = presentationMessage.service const ourService = requestMessage.service + const { message } = await protocol.acceptPresentation(this.agentContext, { + proofRecord, + }) + await this.messageSender.sendMessageToService( new OutboundMessageContext(message, { agentContext: this.agentContext, @@ -542,6 +499,8 @@ export class ProofsApi< }, }) ) + + return proofRecord } // Cannot send message without credentialId or ~service decorator else { @@ -549,8 +508,6 @@ export class ProofsApi< `Cannot accept presentation without connectionId or ~service decorator on presentation message.` ) } - - return record } /** @@ -561,39 +518,34 @@ export class ProofsApi< * @param options multiple properties like proof record id and optional configuration * @returns RequestedCredentials */ - public async autoSelectCredentialsForProofRequest( - options: AutoSelectCredentialsForProofRequestOptions - ): Promise> { + public async selectCredentialsForRequest( + options: SelectCredentialsForProofRequestOptions + ): Promise> { const proofRecord = await this.getById(options.proofRecordId) - const service = this.getService(proofRecord.protocolVersion) + const protocol = this.getProtocol(proofRecord.protocolVersion) - const retrievedCredentials: FormatRetrievedCredentialOptions = - await service.getRequestedCredentialsForProofRequest(this.agentContext, { - proofRecord: proofRecord, - config: options.config, - }) - return await service.autoSelectCredentialsForProofRequest(retrievedCredentials) + return protocol.selectCredentialsForRequest(this.agentContext, { + proofFormats: options.proofFormats, + proofRecord, + }) } /** - * Create a {@link RetrievedCredentials} object. Given input proof request and presentation proposal, - * use credentials in the wallet to build indy requested credentials object for input to proof creation. - * - * If restrictions allow, self attested attributes will be used. + * Get credentials in the wallet for a received proof request. * * @param options multiple properties like proof record id and optional configuration - * @returns RetrievedCredentials object */ - public async getRequestedCredentialsForProofRequest( - options: GetRequestedCredentialsForProofRequest - ): Promise> { - const record = await this.getById(options.proofRecordId) - const service = this.getService(record.protocolVersion) - - return await service.getRequestedCredentialsForProofRequest(this.agentContext, { - proofRecord: record, - config: options.config, + public async getCredentialsForRequest( + options: GetCredentialsForProofRequestOptions + ): Promise> { + const proofRecord = await this.getById(options.proofRecordId) + + const protocol = this.getProtocol(proofRecord.protocolVersion) + + return protocol.getCredentialsForRequest(this.agentContext, { + proofRecord, + proofFormats: options.proofFormats, }) } @@ -604,37 +556,38 @@ export class ProofsApi< * @param message message to send * @returns proof record associated with the proof problem report message */ - public async sendProblemReport(proofRecordId: string, message: string): Promise { - const record = await this.getById(proofRecordId) - const service = this.getService(record.protocolVersion) - if (!record.connectionId) { - throw new AriesFrameworkError(`No connectionId found for proof record '${record.id}'.`) + public async sendProblemReport(options: SendProofProblemReportOptions): Promise { + const proofRecord = await this.getById(options.proofRecordId) + if (!proofRecord.connectionId) { + throw new AriesFrameworkError(`No connectionId found for proof record '${proofRecord.id}'.`) } - const connection = await this.connectionService.getById(this.agentContext, record.connectionId) + + const protocol = this.getProtocol(proofRecord.protocolVersion) + const connectionRecord = await this.connectionService.getById(this.agentContext, proofRecord.connectionId) // Assert - connection.assertReady() + connectionRecord.assertReady() - const { message: problemReport } = await service.createProblemReport(this.agentContext, { - proofRecord: record, - description: message, + const { message: problemReport } = await protocol.createProblemReport(this.agentContext, { + proofRecord, + description: options.description, }) const outboundMessageContext = new OutboundMessageContext(problemReport, { agentContext: this.agentContext, - connection, - associatedRecord: record, + connection: connectionRecord, + associatedRecord: proofRecord, }) - await this.messageSender.sendMessage(outboundMessageContext) - return record + await this.messageSender.sendMessage(outboundMessageContext) + return proofRecord } - public async getFormatData(proofRecordId: string): Promise> { + public async getFormatData(proofRecordId: string): Promise>> { const proofRecord = await this.getById(proofRecordId) - const service = this.getService(proofRecord.protocolVersion) + const protocol = this.getProtocol(proofRecord.protocolVersion) - return service.getFormatData(this.agentContext, proofRecordId) + return protocol.getFormatData(this.agentContext, proofRecordId) } /** @@ -685,8 +638,8 @@ export class ProofsApi< */ public async deleteById(proofId: string, options?: DeleteProofOptions) { const proofRecord = await this.getById(proofId) - const service = this.getService(proofRecord.protocolVersion) - return service.delete(this.agentContext, proofRecord, options) + const protocol = this.getProtocol(proofRecord.protocolVersion) + return protocol.delete(this.agentContext, proofRecord, options) } /** @@ -725,34 +678,21 @@ export class ProofsApi< await this.proofRepository.update(this.agentContext, proofRecord) } - public async findProposalMessage(proofRecordId: string): Promise> { + public async findProposalMessage(proofRecordId: string): Promise> { const record = await this.getById(proofRecordId) - const service = this.getService(record.protocolVersion) - return service.findProposalMessage(this.agentContext, proofRecordId) as FindProofProposalMessageReturn + const protocol = this.getProtocol(record.protocolVersion) + return protocol.findProposalMessage(this.agentContext, proofRecordId) as FindProofProposalMessageReturn } - public async findRequestMessage(proofRecordId: string): Promise> { + public async findRequestMessage(proofRecordId: string): Promise> { const record = await this.getById(proofRecordId) - const service = this.getService(record.protocolVersion) - return service.findRequestMessage(this.agentContext, proofRecordId) as FindProofRequestMessageReturn + const protocol = this.getProtocol(record.protocolVersion) + return protocol.findRequestMessage(this.agentContext, proofRecordId) as FindProofRequestMessageReturn } - public async findPresentationMessage(proofRecordId: string): Promise> { + public async findPresentationMessage(proofRecordId: string): Promise> { const record = await this.getById(proofRecordId) - const service = this.getService(record.protocolVersion) - return service.findPresentationMessage(this.agentContext, proofRecordId) as FindProofPresentationMessageReturn - } - - private registerMessageHandlers(dispatcher: Dispatcher, mediationRecipientService: MediationRecipientService) { - for (const service of Object.values(this.serviceMap)) { - const proofService = service as ProofService - proofService.registerMessageHandlers( - dispatcher, - this.agentConfig, - new ProofResponseCoordinator(proofService), - mediationRecipientService, - this.routingService - ) - } + const protocol = this.getProtocol(record.protocolVersion) + return protocol.findPresentationMessage(this.agentContext, proofRecordId) as FindProofPresentationMessageReturn } } diff --git a/packages/core/src/modules/proofs/ProofsApiOptions.ts b/packages/core/src/modules/proofs/ProofsApiOptions.ts index 5d23a7b131..97bfca04eb 100644 --- a/packages/core/src/modules/proofs/ProofsApiOptions.ts +++ b/packages/core/src/modules/proofs/ProofsApiOptions.ts @@ -1,97 +1,169 @@ -import type { ProofService } from './ProofService' -import type { ProofFormat, ProofFormatPayload } from './formats/ProofFormat' +import type { ProofFormatCredentialForRequestPayload, ProofFormatPayload } from './formats' import type { AutoAcceptProof } from './models' -import type { ProofConfig } from './models/ModuleOptions' +import type { ProofProtocol } from './protocol/ProofProtocol' +import type { + DeleteProofOptions, + GetProofFormatDataReturn, + ProofFormatsFromProtocols, +} from './protocol/ProofProtocolOptions' -/** - * Get the supported protocol versions based on the provided proof services. - */ -export type ProofsProtocolVersionType = PSs[number]['version'] -export type FindProofProposalMessageReturn = ReturnType -export type FindProofRequestMessageReturn = ReturnType -export type FindProofPresentationMessageReturn = ReturnType< - PSs[number]['findPresentationMessage'] +// re-export GetFormatDataReturn type from protocol, as it is also used in the api +export type { GetProofFormatDataReturn, DeleteProofOptions } + +export type FindProofProposalMessageReturn = ReturnType +export type FindProofRequestMessageReturn = ReturnType +export type FindProofPresentationMessageReturn = ReturnType< + PPs[number]['findPresentationMessage'] > /** - * Get the service map for usage in the proofs module. Will return a type mapping of protocol version to service. - * - * @example - * ``` - * type ServiceMap = ProofServiceMap<[IndyProofFormat], [V1ProofService]> - * - * // equal to - * type ServiceMap = { - * v1: V1ProofService - * } - * ``` + * Get the supported protocol versions based on the provided proof protocols. */ -export type ProofServiceMap[]> = { - [PS in PSs[number] as PS['version']]: ProofService +export type ProofsProtocolVersionType = PPs[number]['version'] + +interface BaseOptions { + autoAcceptProof?: AutoAcceptProof + comment?: string } -export interface ProposeProofOptions< - PFs extends ProofFormat[] = ProofFormat[], - PSs extends ProofService[] = ProofService[] -> { +/** + * Interface for ProofsApi.proposeProof. Will send a proposal. + */ +export interface ProposeProofOptions extends BaseOptions { connectionId: string - protocolVersion: ProofsProtocolVersionType - proofFormats: ProofFormatPayload - comment?: string + protocolVersion: ProofsProtocolVersionType + proofFormats: ProofFormatPayload, 'createProposal'> + goalCode?: string - autoAcceptProof?: AutoAcceptProof parentThreadId?: string } -export interface NegotiateRequestOptions { +/** + * Interface for ProofsApi.acceptProposal. Will send a request + * + * proofFormats is optional because this is an accept method + */ +export interface AcceptProofProposalOptions extends BaseOptions { proofRecordId: string - proofFormats: ProofFormatPayload - comment?: string + proofFormats?: ProofFormatPayload, 'acceptProposal'> + goalCode?: string - autoAcceptProof?: AutoAcceptProof + + /** @default true */ + willConfirm?: boolean } -export interface AcceptProofPresentationOptions { +/** + * Interface for ProofsApi.negotiateProposal. Will send a request + */ +export interface NegotiateProofProposalOptions extends BaseOptions { proofRecordId: string - comment?: string - proofFormats: ProofFormatPayload + proofFormats: ProofFormatPayload, 'createRequest'> + + goalCode?: string + + /** @default true */ + willConfirm?: boolean } -export interface AcceptProofProposalOptions { - proofRecordId: string - config?: ProofConfig +/** + * Interface for ProofsApi.createRequest. Will create an out of band request + */ +export interface CreateProofRequestOptions extends BaseOptions { + protocolVersion: ProofsProtocolVersionType + proofFormats: ProofFormatPayload, 'createRequest'> + goalCode?: string + parentThreadId?: string + + /** @default true */ willConfirm?: boolean - comment?: string } -export interface RequestProofOptions< - PFs extends ProofFormat[] = ProofFormat[], - PSs extends ProofService[] = ProofService[] -> { - protocolVersion: ProofsProtocolVersionType +/** + * Interface for ProofsApi.requestCredential. Extends CreateProofRequestOptions, will send a request + */ +export interface RequestProofOptions + extends BaseOptions, + CreateProofRequestOptions { connectionId: string - proofFormats: ProofFormatPayload - comment?: string - autoAcceptProof?: AutoAcceptProof - parentThreadId?: string } -export interface NegotiateProposalOptions { +/** + * Interface for ProofsApi.acceptRequest. Will send a presentation + */ +export interface AcceptProofRequestOptions extends BaseOptions { proofRecordId: string - proofFormats: ProofFormatPayload - comment?: string - autoAcceptProof?: AutoAcceptProof - parentThreadId?: string + proofFormats?: ProofFormatPayload, 'acceptRequest'> + + goalCode?: string + + /** @default true */ + willConfirm?: boolean } -export interface CreateProofRequestOptions< - PFs extends ProofFormat[] = ProofFormat[], - PSs extends ProofService[] = ProofService[] -> { - protocolVersion: ProofsProtocolVersionType - proofFormats: ProofFormatPayload - comment?: string - autoAcceptProof?: AutoAcceptProof - parentThreadId?: string +/** + * Interface for ProofsApi.negotiateRequest. Will send a proposal + */ +export interface NegotiateProofRequestOptions extends BaseOptions { + proofRecordId: string + proofFormats: ProofFormatPayload, 'createProposal'> + + goalCode?: string +} + +/** + * Interface for ProofsApi.acceptPresentation. Will send an ack message + */ +export interface AcceptProofOptions { + proofRecordId: string +} + +/** + * Interface for ProofsApi.getCredentialsForRequest. Will return the credentials that match the proof request + */ +export interface GetCredentialsForProofRequestOptions { + proofRecordId: string + proofFormats?: ProofFormatCredentialForRequestPayload< + ProofFormatsFromProtocols, + 'getCredentialsForRequest', + 'input' + > +} + +export interface GetCredentialsForProofRequestReturn { + proofFormats: ProofFormatCredentialForRequestPayload< + ProofFormatsFromProtocols, + 'getCredentialsForRequest', + 'output' + > +} + +/** + * Interface for ProofsApi.selectCredentialsForRequest. Will automatically select return the first/best + * credentials that match the proof request + */ +export interface SelectCredentialsForProofRequestOptions { + proofRecordId: string + proofFormats?: ProofFormatCredentialForRequestPayload< + ProofFormatsFromProtocols, + 'getCredentialsForRequest', + 'input' + > +} + +export interface SelectCredentialsForProofRequestReturn { + proofFormats: ProofFormatCredentialForRequestPayload< + ProofFormatsFromProtocols, + 'selectCredentialsForRequest', + 'output' + > +} + +/** + * Interface for ProofsApi.sendProblemReport. Will send a problem-report message + */ +export interface SendProofProblemReportOptions { + proofRecordId: string + description: string } diff --git a/packages/core/src/modules/proofs/ProofsModule.ts b/packages/core/src/modules/proofs/ProofsModule.ts index 339ecd0c41..675e7a3355 100644 --- a/packages/core/src/modules/proofs/ProofsModule.ts +++ b/packages/core/src/modules/proofs/ProofsModule.ts @@ -1,22 +1,55 @@ import type { ProofsModuleConfigOptions } from './ProofsModuleConfig' +import type { ProofProtocol } from './protocol/ProofProtocol' import type { FeatureRegistry } from '../../agent/FeatureRegistry' -import type { DependencyManager, Module } from '../../plugins' - -import { Protocol } from '../../agent/models' +import type { ApiModule, DependencyManager } from '../../plugins' +import type { Optional } from '../../utils' +import type { Constructor } from '../../utils/mixins' import { ProofsApi } from './ProofsApi' import { ProofsModuleConfig } from './ProofsModuleConfig' import { IndyProofFormatService } from './formats/indy/IndyProofFormatService' -import { V1ProofService } from './protocol/v1' -import { V2ProofService } from './protocol/v2' +import { V2ProofProtocol, V1ProofProtocol } from './protocol' import { ProofRepository } from './repository' -export class ProofsModule implements Module { - public readonly config: ProofsModuleConfig - public readonly api = ProofsApi +/** + * Default proofProtocols that will be registered if the `proofProtocols` property is not configured. + */ +export type DefaultProofProtocols = [V1ProofProtocol, V2ProofProtocol] + +// ProofsModuleOptions makes the proofProtocols property optional from the config, as it will set it when not provided. +export type ProofsModuleOptions = Optional< + ProofsModuleConfigOptions, + 'proofProtocols' +> + +export class ProofsModule implements ApiModule { + public readonly config: ProofsModuleConfig + + public readonly api: Constructor> = ProofsApi + + public constructor(config?: ProofsModuleOptions) { + this.config = new ProofsModuleConfig({ + ...config, + // NOTE: the proofProtocols defaults are set in the ProofsModule rather than the ProofsModuleConfig to + // avoid dependency cycles. + proofProtocols: config?.proofProtocols ?? this.getDefaultProofProtocols(), + }) as ProofsModuleConfig + } + + /** + * Get the default proof protocols that will be registered if the `proofProtocols` property is not configured. + */ + private getDefaultProofProtocols(): DefaultProofProtocols { + // Instantiate proof formats + const indyProofFormat = new IndyProofFormatService() + + // Instantiate proof protocols + const v1ProofProtocol = new V1ProofProtocol({ indyProofFormat }) + const v2ProofProtocol = new V2ProofProtocol({ + proofFormats: [indyProofFormat], + }) - public constructor(config?: ProofsModuleConfigOptions) { - this.config = new ProofsModuleConfig(config) + return [v1ProofProtocol, v2ProofProtocol] } /** @@ -29,22 +62,11 @@ export class ProofsModule implements Module { // Config dependencyManager.registerInstance(ProofsModuleConfig, this.config) - // Services - dependencyManager.registerSingleton(V1ProofService) - dependencyManager.registerSingleton(V2ProofService) - // Repositories dependencyManager.registerSingleton(ProofRepository) - // Proof Formats - dependencyManager.registerSingleton(IndyProofFormatService) - - // Features - featureRegistry.register( - new Protocol({ - id: 'https://didcomm.org/present-proof/1.0', - roles: ['verifier', 'prover'], - }) - ) + for (const proofProtocol of this.config.proofProtocols) { + proofProtocol.register(dependencyManager, featureRegistry) + } } } diff --git a/packages/core/src/modules/proofs/ProofsModuleConfig.ts b/packages/core/src/modules/proofs/ProofsModuleConfig.ts index e0b12449e2..3526e4eb7d 100644 --- a/packages/core/src/modules/proofs/ProofsModuleConfig.ts +++ b/packages/core/src/modules/proofs/ProofsModuleConfig.ts @@ -1,27 +1,47 @@ +import type { ProofProtocol } from './protocol/ProofProtocol' + import { AutoAcceptProof } from './models/ProofAutoAcceptType' /** * ProofsModuleConfigOptions defines the interface for the options of the ProofsModuleConfig class. * This can contain optional parameters that have default values in the config class itself. */ -export interface ProofsModuleConfigOptions { +export interface ProofsModuleConfigOptions { /** * Whether to automatically accept proof messages. Applies to all present proof protocol versions. * * @default {@link AutoAcceptProof.Never} */ autoAcceptProofs?: AutoAcceptProof + + /** + * Proof protocols to make available to the proofs module. Only one proof protocol should be registered for each proof + * protocol version. + * + * When not provided, the `V1ProofProtocol` and `V2ProofProtocol` are registered by default. + * + * @default + * ``` + * [V1ProofProtocol, V2ProofProtocol] + * ``` + */ + proofProtocols: ProofProtocols } -export class ProofsModuleConfig { - private options: ProofsModuleConfigOptions +export class ProofsModuleConfig { + private options: ProofsModuleConfigOptions - public constructor(options?: ProofsModuleConfigOptions) { - this.options = options ?? {} + public constructor(options: ProofsModuleConfigOptions) { + this.options = options } /** See {@link ProofsModuleConfigOptions.autoAcceptProofs} */ public get autoAcceptProofs() { return this.options.autoAcceptProofs ?? AutoAcceptProof.Never } + + /** See {@link CredentialsModuleConfigOptions.proofProtocols} */ + public get proofProtocols() { + return this.options.proofProtocols + } } diff --git a/packages/core/src/modules/proofs/__tests__/ProofsModule.test.ts b/packages/core/src/modules/proofs/__tests__/ProofsModule.test.ts index 8af9a5b2c2..c2012ed566 100644 --- a/packages/core/src/modules/proofs/__tests__/ProofsModule.test.ts +++ b/packages/core/src/modules/proofs/__tests__/ProofsModule.test.ts @@ -1,33 +1,60 @@ +import type { ProofProtocol } from '../protocol/ProofProtocol' + import { FeatureRegistry } from '../../../agent/FeatureRegistry' import { DependencyManager } from '../../../plugins/DependencyManager' import { ProofsApi } from '../ProofsApi' import { ProofsModule } from '../ProofsModule' -import { IndyProofFormatService } from '../formats/indy/IndyProofFormatService' -import { V1ProofService } from '../protocol/v1/V1ProofService' -import { V2ProofService } from '../protocol/v2/V2ProofService' +import { ProofsModuleConfig } from '../ProofsModuleConfig' +import { V1ProofProtocol } from '../protocol/v1/V1ProofProtocol' +import { V2ProofProtocol } from '../protocol/v2/V2ProofProtocol' import { ProofRepository } from '../repository' jest.mock('../../../plugins/DependencyManager') -const DependencyManagerMock = DependencyManager as jest.Mock +jest.mock('../../../agent/FeatureRegistry') +const DependencyManagerMock = DependencyManager as jest.Mock const dependencyManager = new DependencyManagerMock() - -jest.mock('../../../agent/FeatureRegistry') const FeatureRegistryMock = FeatureRegistry as jest.Mock - const featureRegistry = new FeatureRegistryMock() describe('ProofsModule', () => { test('registers dependencies on the dependency manager', () => { - new ProofsModule().register(dependencyManager, featureRegistry) + const proofsModule = new ProofsModule({ + proofProtocols: [], + }) + proofsModule.register(dependencyManager, featureRegistry) expect(dependencyManager.registerContextScoped).toHaveBeenCalledTimes(1) expect(dependencyManager.registerContextScoped).toHaveBeenCalledWith(ProofsApi) - expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(4) - expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(V1ProofService) - expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(V2ProofService) + expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(1) + expect(dependencyManager.registerInstance).toHaveBeenCalledWith(ProofsModuleConfig, proofsModule.config) + + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(1) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(ProofRepository) - expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(IndyProofFormatService) + }) + + test('registers V1ProofProtocol and V2ProofProtocol if no proofProtocols are configured', () => { + const proofsModule = new ProofsModule() + + expect(proofsModule.config.proofProtocols).toEqual([expect.any(V1ProofProtocol), expect.any(V2ProofProtocol)]) + }) + + test('calls register on the provided ProofProtocols', () => { + const registerMock = jest.fn() + const proofProtocol = { + register: registerMock, + } as unknown as ProofProtocol + + const proofsModule = new ProofsModule({ + proofProtocols: [proofProtocol], + }) + + expect(proofsModule.config.proofProtocols).toEqual([proofProtocol]) + + proofsModule.register(dependencyManager, featureRegistry) + + expect(registerMock).toHaveBeenCalledTimes(1) + expect(registerMock).toHaveBeenCalledWith(dependencyManager, featureRegistry) }) }) diff --git a/packages/core/src/modules/proofs/__tests__/ProofsModuleConfig.test.ts b/packages/core/src/modules/proofs/__tests__/ProofsModuleConfig.test.ts new file mode 100644 index 0000000000..35920a4f48 --- /dev/null +++ b/packages/core/src/modules/proofs/__tests__/ProofsModuleConfig.test.ts @@ -0,0 +1,26 @@ +import type { ProofProtocol } from '../protocol/ProofProtocol' + +import { ProofsModuleConfig } from '../ProofsModuleConfig' +import { AutoAcceptProof } from '../models' + +describe('ProofsModuleConfig', () => { + test('sets default values', () => { + const config = new ProofsModuleConfig({ + proofProtocols: [], + }) + + expect(config.autoAcceptProofs).toBe(AutoAcceptProof.Never) + expect(config.proofProtocols).toEqual([]) + }) + + test('sets values', () => { + const proofProtocol = jest.fn() as unknown as ProofProtocol + const config = new ProofsModuleConfig({ + autoAcceptProofs: AutoAcceptProof.Always, + proofProtocols: [proofProtocol], + }) + + expect(config.autoAcceptProofs).toBe(AutoAcceptProof.Always) + expect(config.proofProtocols).toEqual([proofProtocol]) + }) +}) diff --git a/packages/core/src/modules/proofs/__tests__/fixtures.ts b/packages/core/src/modules/proofs/__tests__/fixtures.ts index 10606073b8..2045f6f0f8 100644 --- a/packages/core/src/modules/proofs/__tests__/fixtures.ts +++ b/packages/core/src/modules/proofs/__tests__/fixtures.ts @@ -15,3 +15,33 @@ export const credDef = { }, }, } + +export const TEST_INPUT_DESCRIPTORS_CITIZENSHIP = { + constraints: { + fields: [ + { + path: ['$.credentialSubject.familyName'], + purpose: 'The claim must be from one of the specified issuers', + id: '1f44d55f-f161-4938-a659-f8026467f126', + }, + { + path: ['$.credentialSubject.givenName'], + purpose: 'The claim must be from one of the specified issuers', + }, + ], + }, + schema: [ + { + uri: 'https://www.w3.org/2018/credentials#VerifiableCredential', + }, + { + uri: 'https://w3id.org/citizenship#PermanentResident', + }, + { + uri: 'https://w3id.org/citizenship/v1', + }, + ], + name: "EU Driver's License", + group: ['A'], + id: 'citizenship_input_1', +} diff --git a/packages/core/src/modules/proofs/formats/ProofFormat.ts b/packages/core/src/modules/proofs/formats/ProofFormat.ts index 18fbba278d..c573c12579 100644 --- a/packages/core/src/modules/proofs/formats/ProofFormat.ts +++ b/packages/core/src/modules/proofs/formats/ProofFormat.ts @@ -21,21 +21,50 @@ export type ProofFormatPayload + * + * // equal to + * type SelectedCredentialsForRequest = { + * indy: { + * // ... return value for indy selected credentials ... + * }, + * presentationExchange: { + * // ... return value for presentation exchange selected credentials ... + * } + * } + * ``` + */ +export type ProofFormatCredentialForRequestPayload< + PFs extends ProofFormat[], + M extends 'selectCredentialsForRequest' | 'getCredentialsForRequest', + IO extends 'input' | 'output' +> = { + [ProofFormat in PFs[number] as ProofFormat['formatKey']]?: ProofFormat['proofFormats'][M][IO] +} + export interface ProofFormat { - formatKey: string // e.g. 'ProofManifest', cannot be shared between different formats + formatKey: string // e.g. 'presentationExchange', cannot be shared between different formats + proofFormats: { createProposal: unknown acceptProposal: unknown createRequest: unknown acceptRequest: unknown - createPresentation: unknown - acceptPresentation: unknown - createProposalAsResponse: unknown - createOutOfBandRequest: unknown - createRequestAsResponse: unknown - createProofRequestFromProposal: unknown - requestCredentials: unknown - retrieveCredentials: unknown + + getCredentialsForRequest: { + input: unknown + output: unknown + } + selectCredentialsForRequest: { + input: unknown + output: unknown + } } formatData: { proposal: unknown diff --git a/packages/core/src/modules/proofs/formats/ProofFormatConstants.ts b/packages/core/src/modules/proofs/formats/ProofFormatConstants.ts deleted file mode 100644 index 35e1ce33ab..0000000000 --- a/packages/core/src/modules/proofs/formats/ProofFormatConstants.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const INDY_ATTACH_ID = 'indy' -export const V2_INDY_PRESENTATION_PROPOSAL = 'hlindy/proof-req@v2.0' -export const V2_INDY_PRESENTATION_REQUEST = 'hlindy/proof-req@v2.0' -export const V2_INDY_PRESENTATION = 'hlindy/proof@v2.0' diff --git a/packages/core/src/modules/proofs/formats/ProofFormatService.ts b/packages/core/src/modules/proofs/formats/ProofFormatService.ts index 931d4c886f..f48db80624 100644 --- a/packages/core/src/modules/proofs/formats/ProofFormatService.ts +++ b/packages/core/src/modules/proofs/formats/ProofFormatService.ts @@ -1,81 +1,70 @@ import type { ProofFormat } from './ProofFormat' -import type { IndyProofFormat } from './indy/IndyProofFormat' -import type { GetRequestedCredentialsFormat } from './indy/IndyProofFormatsServiceOptions' -import type { ProofAttachmentFormat } from './models/ProofAttachmentFormat' import type { - CreatePresentationFormatsOptions, - FormatCreateProofProposalOptions, - CreateRequestOptions, - FormatCreatePresentationOptions, - ProcessPresentationOptions, - ProcessProposalOptions, - ProcessRequestOptions, -} from './models/ProofFormatServiceOptions' + ProofFormatAcceptProposalOptions, + ProofFormatAcceptRequestOptions, + ProofFormatCreateProposalOptions, + FormatCreateRequestOptions, + ProofFormatProcessPresentationOptions, + ProofFormatCreateReturn, + ProofFormatProcessOptions, + ProofFormatGetCredentialsForRequestOptions, + ProofFormatGetCredentialsForRequestReturn, + ProofFormatSelectCredentialsForRequestOptions, + ProofFormatSelectCredentialsForRequestReturn, + ProofFormatAutoRespondProposalOptions, + ProofFormatAutoRespondRequestOptions, + ProofFormatAutoRespondPresentationOptions, +} from './ProofFormatServiceOptions' import type { AgentContext } from '../../../agent' -import type { AgentConfig } from '../../../agent/AgentConfig' -import type { DidCommMessageRepository } from '../../../storage' -import type { - CreateRequestAsResponseOptions, - FormatRequestedCredentialReturn, - FormatRetrievedCredentialOptions, -} from '../models/ProofServiceOptions' -import type { ProofRequestFormats } from '../models/SharedOptions' - -/** - * This abstract class is the base class for any proof format - * specific service. - * - * @export - * @abstract - * @class ProofFormatService - */ -export abstract class ProofFormatService { - protected didCommMessageRepository: DidCommMessageRepository - protected agentConfig: AgentConfig - - public abstract readonly formatKey: PF['formatKey'] - - public constructor(didCommMessageRepository: DidCommMessageRepository, agentConfig: AgentConfig) { - this.didCommMessageRepository = didCommMessageRepository - this.agentConfig = agentConfig - } - public abstract createProposal(options: FormatCreateProofProposalOptions): Promise +export interface ProofFormatService { + formatKey: PF['formatKey'] - public abstract processProposal(options: ProcessProposalOptions): Promise - - public abstract createRequest(options: CreateRequestOptions): Promise - - public abstract processRequest(options: ProcessRequestOptions): Promise - - public abstract createPresentation( + // proposal methods + createProposal( agentContext: AgentContext, - options: FormatCreatePresentationOptions - ): Promise - - public abstract processPresentation(agentContext: AgentContext, options: ProcessPresentationOptions): Promise - - public abstract createProofRequestFromProposal( - options: CreatePresentationFormatsOptions - ): Promise + options: ProofFormatCreateProposalOptions + ): Promise + processProposal(agentContext: AgentContext, options: ProofFormatProcessOptions): Promise + acceptProposal( + agentContext: AgentContext, + options: ProofFormatAcceptProposalOptions + ): Promise - public abstract getRequestedCredentialsForProofRequest( + // request methods + createRequest(agentContext: AgentContext, options: FormatCreateRequestOptions): Promise + processRequest(agentContext: AgentContext, options: ProofFormatProcessOptions): Promise + acceptRequest( agentContext: AgentContext, - options: GetRequestedCredentialsFormat - ): Promise> + options: ProofFormatAcceptRequestOptions + ): Promise - public abstract autoSelectCredentialsForProofRequest( - options: FormatRetrievedCredentialOptions<[PF]> - ): Promise> + // presentation methods + processPresentation(agentContext: AgentContext, options: ProofFormatProcessPresentationOptions): Promise - public abstract proposalAndRequestAreEqual( - proposalAttachments: ProofAttachmentFormat[], - requestAttachments: ProofAttachmentFormat[] - ): boolean + // credentials for request + getCredentialsForRequest( + agentContext: AgentContext, + options: ProofFormatGetCredentialsForRequestOptions + ): Promise> + selectCredentialsForRequest( + agentContext: AgentContext, + options: ProofFormatSelectCredentialsForRequestOptions + ): Promise> - public abstract supportsFormat(formatIdentifier: string): boolean + // auto accept methods + shouldAutoRespondToProposal( + agentContext: AgentContext, + options: ProofFormatAutoRespondProposalOptions + ): Promise + shouldAutoRespondToRequest( + agentContext: AgentContext, + options: ProofFormatAutoRespondRequestOptions + ): Promise + shouldAutoRespondToPresentation( + agentContext: AgentContext, + options: ProofFormatAutoRespondPresentationOptions + ): Promise - public abstract createRequestAsResponse( - options: CreateRequestAsResponseOptions<[IndyProofFormat]> - ): Promise + supportsFormat(formatIdentifier: string): boolean } diff --git a/packages/core/src/modules/proofs/formats/ProofFormatServiceOptions.ts b/packages/core/src/modules/proofs/formats/ProofFormatServiceOptions.ts index 25731e43c8..db7923bafc 100644 --- a/packages/core/src/modules/proofs/formats/ProofFormatServiceOptions.ts +++ b/packages/core/src/modules/proofs/formats/ProofFormatServiceOptions.ts @@ -1,23 +1,35 @@ -import type { ProofFormat } from './ProofFormat' +import type { ProofFormat, ProofFormatCredentialForRequestPayload, ProofFormatPayload } from './ProofFormat' import type { ProofFormatService } from './ProofFormatService' import type { Attachment } from '../../../decorators/attachment/Attachment' import type { ProofFormatSpec } from '../models/ProofFormatSpec' +import type { ProofExchangeRecord } from '../repository/ProofExchangeRecord' /** - * Get the service map for usage in the proofs module. Will return a type mapping of protocol version to service. + * Infer the {@link ProofFormat} based on a {@link ProofFormatService}. + * + * It does this by extracting the `ProofFormat` generic from the `ProofFormatService`. * * @example * ``` - * type FormatServiceMap = ProofFormatServiceMap<[IndyProofFormat]> + * // TheProofFormat is now equal to IndyProofFormat + * type TheProofFormat = ExtractProofFormat + * ``` * - * // equal to - * type FormatServiceMap = { - * indy: ProofFormatServiceMap + * Because the `IndyProofFormatService` is defined as follows: + * ``` + * class IndyProofFormatService implements ProofFormatService { * } * ``` */ -export type ProofFormatServiceMap = { - [PF in PFs[number] as PF['formatKey']]: ProofFormatService +export type ExtractProofFormat = Type extends ProofFormatService ? ProofFormat : never + +/** + * Infer an array of {@link ProofFormat} types based on an array of {@link ProofFormatService} types. + * + * This is based on {@link ExtractProofFormat}, but allows to handle arrays. + */ +export type ExtractProofFormats = { + [PF in keyof PFs]: ExtractProofFormat } /** @@ -29,3 +41,85 @@ export interface ProofFormatCreateReturn { format: ProofFormatSpec attachment: Attachment } + +/** + * Base type for all proof process methods. + */ +export interface ProofFormatProcessOptions { + attachment: Attachment + proofRecord: ProofExchangeRecord +} + +export interface ProofFormatProcessPresentationOptions extends ProofFormatProcessOptions { + requestAttachment: Attachment +} + +export interface ProofFormatCreateProposalOptions { + proofRecord: ProofExchangeRecord + proofFormats: ProofFormatPayload<[PF], 'createProposal'> + attachmentId?: string +} + +export interface ProofFormatAcceptProposalOptions { + proofRecord: ProofExchangeRecord + proofFormats?: ProofFormatPayload<[PF], 'acceptProposal'> + attachmentId?: string + + proposalAttachment: Attachment +} + +export interface FormatCreateRequestOptions { + proofRecord: ProofExchangeRecord + proofFormats: ProofFormatPayload<[PF], 'createRequest'> + attachmentId?: string +} + +export interface ProofFormatAcceptRequestOptions { + proofRecord: ProofExchangeRecord + proofFormats?: ProofFormatPayload<[PF], 'acceptRequest'> + attachmentId?: string + + requestAttachment: Attachment + proposalAttachment?: Attachment +} + +export interface ProofFormatGetCredentialsForRequestOptions { + proofRecord: ProofExchangeRecord + proofFormats?: ProofFormatCredentialForRequestPayload<[PF], 'getCredentialsForRequest', 'input'> + + requestAttachment: Attachment + proposalAttachment?: Attachment +} + +export type ProofFormatGetCredentialsForRequestReturn = + PF['proofFormats']['getCredentialsForRequest']['output'] + +export interface ProofFormatSelectCredentialsForRequestOptions { + proofRecord: ProofExchangeRecord + proofFormats?: ProofFormatCredentialForRequestPayload<[PF], 'selectCredentialsForRequest', 'input'> + + requestAttachment: Attachment + proposalAttachment?: Attachment +} + +export type ProofFormatSelectCredentialsForRequestReturn = + PF['proofFormats']['selectCredentialsForRequest']['output'] + +export interface ProofFormatAutoRespondProposalOptions { + proofRecord: ProofExchangeRecord + proposalAttachment: Attachment + requestAttachment: Attachment +} + +export interface ProofFormatAutoRespondRequestOptions { + proofRecord: ProofExchangeRecord + requestAttachment: Attachment + proposalAttachment: Attachment +} + +export interface ProofFormatAutoRespondPresentationOptions { + proofRecord: ProofExchangeRecord + proposalAttachment?: Attachment + requestAttachment: Attachment + presentationAttachment: Attachment +} diff --git a/packages/core/src/modules/proofs/formats/errors/InvalidEncodedValueError.ts b/packages/core/src/modules/proofs/formats/errors/InvalidEncodedValueError.ts new file mode 100644 index 0000000000..a81e4e5553 --- /dev/null +++ b/packages/core/src/modules/proofs/formats/errors/InvalidEncodedValueError.ts @@ -0,0 +1,3 @@ +import { AriesFrameworkError } from '../../../../error/AriesFrameworkError' + +export class InvalidEncodedValueError extends AriesFrameworkError {} diff --git a/packages/core/src/modules/proofs/formats/errors/MissingIndyProofMessageError.ts b/packages/core/src/modules/proofs/formats/errors/MissingIndyProofMessageError.ts new file mode 100644 index 0000000000..a00abc40cb --- /dev/null +++ b/packages/core/src/modules/proofs/formats/errors/MissingIndyProofMessageError.ts @@ -0,0 +1,3 @@ +import { AriesFrameworkError } from '../../../../error/AriesFrameworkError' + +export class MissingIndyProofMessageError extends AriesFrameworkError {} diff --git a/packages/core/src/modules/proofs/formats/index.ts b/packages/core/src/modules/proofs/formats/index.ts index efb4e8a6ab..08ece3aa21 100644 --- a/packages/core/src/modules/proofs/formats/index.ts +++ b/packages/core/src/modules/proofs/formats/index.ts @@ -1,6 +1,9 @@ -export * from './indy' -export * from './models' export * from './ProofFormat' -export * from './ProofFormatConstants' export * from './ProofFormatService' export * from './ProofFormatServiceOptions' + +export * from './indy' + +import * as ProofFormatServiceOptions from './ProofFormatServiceOptions' + +export { ProofFormatServiceOptions } diff --git a/packages/core/src/modules/proofs/formats/indy/IndyProofFormat.ts b/packages/core/src/modules/proofs/formats/indy/IndyProofFormat.ts index ae58b2db75..a6afce160a 100644 --- a/packages/core/src/modules/proofs/formats/indy/IndyProofFormat.ts +++ b/packages/core/src/modules/proofs/formats/indy/IndyProofFormat.ts @@ -1,46 +1,75 @@ -import type { RequestedAttribute } from './models/RequestedAttribute' -import type { IndyRequestedCredentialsOptions } from './models/RequestedCredentials' -import type { RequestedPredicate } from './models/RequestedPredicate' -import type { PresentationPreviewAttribute, PresentationPreviewPredicate } from '../../protocol/v1' +import type { ProofAttributeInfoOptions, ProofPredicateInfoOptions } from './models' +import type { RequestedAttributeOptions } from './models/RequestedAttribute' +import type { RequestedPredicateOptions } from './models/RequestedPredicate' +import type { V1PresentationPreviewAttributeOptions, V1PresentationPreviewPredicateOptions } from '../../protocol/v1' import type { ProofFormat } from '../ProofFormat' -import type { IndyRequestProofFormat } from '../indy/IndyProofFormatsServiceOptions' import type { IndyProof, IndyProofRequest } from 'indy-sdk' +/** + * Interface for creating an indy proof proposal. + */ export interface IndyProposeProofFormat { - attributes?: PresentationPreviewAttribute[] - predicates?: PresentationPreviewPredicate[] - nonce?: string name?: string version?: string + attributes?: V1PresentationPreviewAttributeOptions[] + predicates?: V1PresentationPreviewPredicateOptions[] } -export interface IndyRequestedCredentialsFormat { - requestedAttributes: Record - requestedPredicates: Record +/** + * Interface for creating an indy proof request. + */ +export interface IndyRequestProofFormat { + name: string + version: string + // TODO: update to AnonCredsNonRevokedInterval when moving to AnonCreds package + nonRevoked?: { from?: number; to?: number } + requestedAttributes?: Record + requestedPredicates?: Record +} + +/** + * Interface for accepting an indy proof request. + */ +export type IndyAcceptProofRequestFormat = Partial + +export interface IndySelectedCredentialsForProofRequest { + requestedAttributes: Record + requestedPredicates: Record selfAttestedAttributes: Record } -export interface IndyRetrievedCredentialsFormat { - requestedAttributes: Record - requestedPredicates: Record +/** + * Interface for getting credentials for an indy proof request. + */ +export interface IndyCredentialsForProofRequest { + attributes: Record + predicates: Record +} + +export interface IndyGetCredentialsForProofRequestOptions { + filterByNonRevocationRequirements?: boolean } export interface IndyProofFormat extends ProofFormat { formatKey: 'indy' - proofRecordType: 'indy' + proofFormats: { createProposal: IndyProposeProofFormat - acceptProposal: unknown + acceptProposal: { + name?: string + version?: string + } createRequest: IndyRequestProofFormat - acceptRequest: unknown - createPresentation: IndyRequestedCredentialsOptions - acceptPresentation: unknown - createProposalAsResponse: IndyProposeProofFormat - createOutOfBandRequest: unknown - createRequestAsResponse: IndyRequestProofFormat - createProofRequestFromProposal: IndyRequestProofFormat - requestCredentials: IndyRequestedCredentialsFormat - retrieveCredentials: IndyRetrievedCredentialsFormat + acceptRequest: IndyAcceptProofRequestFormat + + getCredentialsForRequest: { + input: IndyGetCredentialsForProofRequestOptions + output: IndyCredentialsForProofRequest + } + selectCredentialsForRequest: { + input: IndyGetCredentialsForProofRequestOptions + output: IndySelectedCredentialsForProofRequest + } } formatData: { diff --git a/packages/core/src/modules/proofs/formats/indy/IndyProofFormatService.ts b/packages/core/src/modules/proofs/formats/indy/IndyProofFormatService.ts index 9730e6aa8d..924f9dcb62 100644 --- a/packages/core/src/modules/proofs/formats/indy/IndyProofFormatService.ts +++ b/packages/core/src/modules/proofs/formats/indy/IndyProofFormatService.ts @@ -1,293 +1,211 @@ -import type { IndyProofFormat, IndyProposeProofFormat } from './IndyProofFormat' -import type { GetRequestedCredentialsFormat } from './IndyProofFormatsServiceOptions' -import type { AgentContext } from '../../../../agent' -import type { Logger } from '../../../../logger' import type { - CreateRequestAsResponseOptions, - FormatRequestedCredentialReturn, - FormatRetrievedCredentialOptions, -} from '../../models/ProofServiceOptions' -import type { ProofRequestFormats } from '../../models/SharedOptions' -import type { PresentationPreviewAttribute } from '../../protocol/v1/models' -import type { ProofAttachmentFormat } from '../models/ProofAttachmentFormat' + IndyCredentialsForProofRequest, + IndyGetCredentialsForProofRequestOptions, + IndyProofFormat, + IndySelectedCredentialsForProofRequest, +} from './IndyProofFormat' +import type { ProofAttributeInfo, ProofPredicateInfo } from './models' +import type { AgentContext } from '../../../../agent' +import type { ProofFormatService } from '../ProofFormatService' import type { - CreatePresentationFormatsOptions, - CreateProofAttachmentOptions, - FormatCreateProofProposalOptions, - CreateRequestAttachmentOptions, - CreateRequestOptions, - FormatCreatePresentationOptions, - ProcessPresentationOptions, - ProcessProposalOptions, - ProcessRequestOptions, - VerifyProofOptions, -} from '../models/ProofFormatServiceOptions' -import type { CredDef, IndyProof, Schema } from 'indy-sdk' - -import { Lifecycle, scoped } from 'tsyringe' - -import { AgentConfig } from '../../../../agent/AgentConfig' + ProofFormatCreateProposalOptions, + ProofFormatCreateReturn, + ProofFormatAcceptProposalOptions, + ProofFormatAcceptRequestOptions, + ProofFormatAutoRespondProposalOptions, + ProofFormatAutoRespondRequestOptions, + ProofFormatGetCredentialsForRequestOptions, + ProofFormatGetCredentialsForRequestReturn, + ProofFormatSelectCredentialsForRequestOptions, + ProofFormatSelectCredentialsForRequestReturn, + ProofFormatProcessOptions, + FormatCreateRequestOptions, + ProofFormatProcessPresentationOptions, +} from '../ProofFormatServiceOptions' +import type { CredDef, IndyProof, IndyProofRequest, Schema } from 'indy-sdk' + import { Attachment, AttachmentData } from '../../../../decorators/attachment/Attachment' import { AriesFrameworkError } from '../../../../error/AriesFrameworkError' -import { ConsoleLogger, LogLevel } from '../../../../logger' -import { DidCommMessageRepository } from '../../../../storage/didcomm/DidCommMessageRepository' -import { checkProofRequestForDuplicates, deepEquality } from '../../../../utils' import { JsonEncoder } from '../../../../utils/JsonEncoder' import { JsonTransformer } from '../../../../utils/JsonTransformer' import { MessageValidator } from '../../../../utils/MessageValidator' -import { uuid } from '../../../../utils/uuid' -import { IndyWallet } from '../../../../wallet/IndyWallet' import { IndyCredential, IndyCredentialInfo } from '../../../credentials' import { IndyCredentialUtils } from '../../../credentials/formats/indy/IndyCredentialUtils' -import { IndyHolderService, IndyVerifierService, IndyRevocationService } from '../../../indy' +import { IndyVerifierService, IndyHolderService, IndyRevocationService } from '../../../indy' import { IndyLedgerService } from '../../../ledger' import { ProofFormatSpec } from '../../models/ProofFormatSpec' -import { PartialProof, PresentationPreview } from '../../protocol/v1/models' -import { - V2_INDY_PRESENTATION_REQUEST, - V2_INDY_PRESENTATION_PROPOSAL, - V2_INDY_PRESENTATION, -} from '../ProofFormatConstants' -import { ProofFormatService } from '../ProofFormatService' import { InvalidEncodedValueError } from './errors/InvalidEncodedValueError' -import { MissingIndyProofMessageError } from './errors/MissingIndyProofMessageError' -import { - AttributeFilter, - ProofAttributeInfo, - ProofPredicateInfo, - RequestedAttribute, - RequestedPredicate, -} from './models' +import { RequestedAttribute, RequestedPredicate } from './models' +import { PartialProof } from './models/PartialProof' import { ProofRequest } from './models/ProofRequest' import { RequestedCredentials } from './models/RequestedCredentials' -import { RetrievedCredentials } from './models/RetrievedCredentials' +import { areIndyProofRequestsEqual, assertNoDuplicateGroupsNamesInProofRequest, createRequestFromPreview } from './util' import { sortRequestedCredentials } from './util/sortRequestedCredentials' -@scoped(Lifecycle.ContainerScoped) -export class IndyProofFormatService extends ProofFormatService { - private indyHolderService: IndyHolderService - private indyVerifierService: IndyVerifierService - private indyRevocationService: IndyRevocationService - private ledgerService: IndyLedgerService - private logger: Logger - private wallet: IndyWallet - - public constructor( - agentConfig: AgentConfig, - indyHolderService: IndyHolderService, - indyVerifierService: IndyVerifierService, - indyRevocationService: IndyRevocationService, - ledgerService: IndyLedgerService, - didCommMessageRepository: DidCommMessageRepository, - wallet: IndyWallet - ) { - super(didCommMessageRepository, agentConfig) - this.indyHolderService = indyHolderService - this.indyVerifierService = indyVerifierService - this.indyRevocationService = indyRevocationService - this.ledgerService = ledgerService - this.wallet = wallet - this.logger = new ConsoleLogger(LogLevel.off) - } +const V2_INDY_PRESENTATION_PROPOSAL = 'hlindy/proof-req@v2.0' +const V2_INDY_PRESENTATION_REQUEST = 'hlindy/proof-req@v2.0' +const V2_INDY_PRESENTATION = 'hlindy/proof@v2.0' + +export class IndyProofFormatService implements ProofFormatService { public readonly formatKey = 'indy' as const - public readonly proofRecordType = 'indy' as const - private createRequestAttachment(options: CreateRequestAttachmentOptions): ProofAttachmentFormat { + public async createProposal( + agentContext: AgentContext, + { attachmentId, proofFormats }: ProofFormatCreateProposalOptions + ): Promise { const format = new ProofFormatSpec({ - attachmentId: options.id, - format: V2_INDY_PRESENTATION_REQUEST, + format: V2_INDY_PRESENTATION_PROPOSAL, + attachmentId, }) - const request = new ProofRequest(options.proofRequestOptions) - - // Assert attribute and predicate (group) names do not match - checkProofRequestForDuplicates(request) + const indyFormat = proofFormats.indy + if (!indyFormat) { + throw Error('Missing indy format to create proposal attachment format') + } - const attachment = new Attachment({ - id: options.id, - mimeType: 'application/json', - data: new AttachmentData({ - base64: JsonEncoder.toBase64(request), - }), + const proofRequest = createRequestFromPreview({ + attributes: indyFormat.attributes ?? [], + predicates: indyFormat.predicates ?? [], + name: indyFormat.name ?? 'Proof request', + version: indyFormat.version ?? '1.0', + nonce: await agentContext.wallet.generateNonce(), }) - return { format, attachment } + const attachment = this.getFormatData(proofRequest.toJSON(), format.attachmentId) + + return { attachment, format } } - private async createProofAttachment(options: CreateProofAttachmentOptions): Promise { - const format = new ProofFormatSpec({ - attachmentId: options.id, - format: V2_INDY_PRESENTATION_PROPOSAL, - }) + public async processProposal(agentContext: AgentContext, { attachment }: ProofFormatProcessOptions): Promise { + const proposalJson = attachment.getDataAsJson() - const request = new ProofRequest(options.proofProposalOptions) - MessageValidator.validateSync(request) + // fromJSON also validates + const proposal = JsonTransformer.fromJSON(proposalJson, ProofRequest) - const attachment = new Attachment({ - id: options.id, - mimeType: 'application/json', - data: new AttachmentData({ - base64: JsonEncoder.toBase64(JsonTransformer.toJSON(request)), - }), - }) - return { format, attachment } + // Assert attribute and predicate (group) names do not match + assertNoDuplicateGroupsNamesInProofRequest(proposal) } - public async createProposal(options: FormatCreateProofProposalOptions): Promise { - if (!options.formats.indy) { - throw Error('Missing indy format to create proposal attachment format') - } - const proofRequest = await this.createRequestFromPreview(options.formats.indy) - - return await this.createProofAttachment({ - id: options.id ?? uuid(), - proofProposalOptions: proofRequest, + public async acceptProposal( + agentContext: AgentContext, + { proposalAttachment, attachmentId }: ProofFormatAcceptProposalOptions + ): Promise { + const format = new ProofFormatSpec({ + format: V2_INDY_PRESENTATION_REQUEST, + attachmentId, }) - } - public async processProposal(options: ProcessProposalOptions): Promise { - const proofProposalJson = options.proposal.attachment.getDataAsJson() + const proposalJson = proposalAttachment.getDataAsJson() - // Assert attachment - if (!proofProposalJson) { - throw new AriesFrameworkError( - `Missing required base64 or json encoded attachment data for presentation proposal with thread id ${options.record?.threadId}` - ) - } + // The proposal and request formats are the same, so we can just use the proposal + const request = JsonTransformer.fromJSON(proposalJson, ProofRequest) - const proposalMessage = JsonTransformer.fromJSON(proofProposalJson, ProofRequest) + // We never want to reuse the nonce from the proposal, as this will allow replay attacks + request.nonce = await agentContext.wallet.generateNonce() - MessageValidator.validateSync(proposalMessage) - } - - public async createRequestAsResponse( - options: CreateRequestAsResponseOptions<[IndyProofFormat]> - ): Promise { - if (!options.proofFormats.indy) { - throw Error('Missing indy format to create proposal attachment format') - } + const attachment = this.getFormatData(request.toJSON(), format.attachmentId) - const id = options.id ?? uuid() + return { attachment, format } + } + public async createRequest( + agentContext: AgentContext, + { attachmentId, proofFormats }: FormatCreateRequestOptions + ): Promise { const format = new ProofFormatSpec({ - attachmentId: id, format: V2_INDY_PRESENTATION_REQUEST, + attachmentId, }) - const attachment = new Attachment({ - id: id, - mimeType: 'application/json', - data: new AttachmentData({ - base64: JsonEncoder.toBase64(options.proofFormats.indy), - }), + const indyFormat = proofFormats.indy + if (!indyFormat) { + throw Error('Missing indy format in create request attachment format') + } + + const request = new ProofRequest({ + name: indyFormat.name, + version: indyFormat.version, + nonce: await agentContext.wallet.generateNonce(), + requestedAttributes: indyFormat.requestedAttributes, + requestedPredicates: indyFormat.requestedPredicates, + nonRevoked: indyFormat.nonRevoked, }) - return { format, attachment } - } - public async createRequest(options: CreateRequestOptions): Promise { - if (!options.formats.indy) { - throw new AriesFrameworkError('Missing indy format to create proof request attachment format.') - } + // Validate to make sure user provided correct input + MessageValidator.validateSync(request) + assertNoDuplicateGroupsNamesInProofRequest(request) - const indyFormat = options.formats.indy + const attachment = this.getFormatData(request.toJSON(), format.attachmentId) - return this.createRequestAttachment({ - id: options.id ?? uuid(), - proofRequestOptions: { - ...indyFormat, - name: indyFormat.name ?? 'proof-request', - version: indyFormat.version ?? '1.0', - nonce: indyFormat.nonce ?? (await this.wallet.generateNonce()), - }, - }) + return { attachment, format } } - public async processRequest(options: ProcessRequestOptions): Promise { - const proofRequestJson = options.requestAttachment.attachment.getDataAsJson() + public async processRequest(agentContext: AgentContext, { attachment }: ProofFormatProcessOptions): Promise { + const requestJson = attachment.getDataAsJson() - const proofRequest = JsonTransformer.fromJSON(proofRequestJson, ProofRequest) - - // Assert attachment - if (!proofRequest) { - throw new AriesFrameworkError( - `Missing required base64 or json encoded attachment data for presentation request with thread id ${options.record?.threadId}` - ) - } - MessageValidator.validateSync(proofRequest) + // fromJSON also validates + const proofRequest = JsonTransformer.fromJSON(requestJson, ProofRequest) // Assert attribute and predicate (group) names do not match - checkProofRequestForDuplicates(proofRequest) + assertNoDuplicateGroupsNamesInProofRequest(proofRequest) } - public async createPresentation( + public async acceptRequest( agentContext: AgentContext, - options: FormatCreatePresentationOptions - ): Promise { - // Extract proof request from attachment - const proofRequestJson = options.attachment.getDataAsJson() ?? null - const proofRequest = JsonTransformer.fromJSON(proofRequestJson, ProofRequest) - - // verify everything is there - if (!options.proofFormats.indy) { - throw new AriesFrameworkError('Missing indy format to create proof presentation attachment format.') - } - - const requestedCredentials = new RequestedCredentials({ - requestedAttributes: options.proofFormats.indy.requestedAttributes, - requestedPredicates: options.proofFormats.indy.requestedPredicates, - selfAttestedAttributes: options.proofFormats.indy.selfAttestedAttributes, + { proofFormats, requestAttachment, attachmentId }: ProofFormatAcceptRequestOptions + ): Promise { + const format = new ProofFormatSpec({ + format: V2_INDY_PRESENTATION, + attachmentId, }) - const proof = await this.createProof(agentContext, proofRequest, requestedCredentials) + const indyFormat = proofFormats?.indy - const attachmentId = options.id ?? uuid() + const requestJson = requestAttachment.getDataAsJson() + const proofRequest = JsonTransformer.fromJSON(requestJson, ProofRequest) - const format = new ProofFormatSpec({ - attachmentId, - format: V2_INDY_PRESENTATION, - }) + let requestedCredentials: RequestedCredentials - const attachment = new Attachment({ - id: attachmentId, - mimeType: 'application/json', - data: new AttachmentData({ - base64: JsonEncoder.toBase64(proof), - }), - }) - return { format, attachment } - } + if (indyFormat) { + requestedCredentials = new RequestedCredentials({ + requestedAttributes: indyFormat.requestedAttributes, + requestedPredicates: indyFormat.requestedPredicates, + selfAttestedAttributes: indyFormat.selfAttestedAttributes, + }) - public async processPresentation(agentContext: AgentContext, options: ProcessPresentationOptions): Promise { - const requestFormat = options.formatAttachments.request.find( - (x) => x.format.format === V2_INDY_PRESENTATION_REQUEST - ) + // Validate to make sure user provided correct input + MessageValidator.validateSync(requestedCredentials) + } else { + const selectedCredentials = await this._selectCredentialsForRequest(agentContext, proofRequest, { + filterByNonRevocationRequirements: true, + }) - if (!requestFormat) { - throw new MissingIndyProofMessageError( - 'Missing Indy Proof Request format while trying to process an Indy proof presentation.' - ) + requestedCredentials = new RequestedCredentials({ + requestedAttributes: selectedCredentials.requestedAttributes, + requestedPredicates: selectedCredentials.requestedPredicates, + selfAttestedAttributes: selectedCredentials.selfAttestedAttributes, + }) } - const proofFormat = options.formatAttachments.presentation.find((x) => x.format.format === V2_INDY_PRESENTATION) + const proof = await this.createProof(agentContext, proofRequest, requestedCredentials) + const attachment = this.getFormatData(proof, format.attachmentId) - if (!proofFormat) { - throw new MissingIndyProofMessageError( - 'Missing Indy Proof Presentation format while trying to process an Indy proof presentation.' - ) + return { + attachment, + format, } - - return await this.verifyProof(agentContext, { request: requestFormat.attachment, proof: proofFormat.attachment }) } - public async verifyProof(agentContext: AgentContext, options: VerifyProofOptions): Promise { - if (!options) { - throw new AriesFrameworkError('No Indy proof was provided.') - } - const proofRequestJson = options.request.getDataAsJson() ?? null - const proofRequest = JsonTransformer.fromJSON(proofRequestJson, ProofRequest) + public async processPresentation( + agentContext: AgentContext, + { requestAttachment, attachment }: ProofFormatProcessPresentationOptions + ): Promise { + const indyVerifierService = agentContext.dependencyManager.resolve(IndyVerifierService) - const proofJson = options.proof.getDataAsJson() ?? null + const proofRequestJson = requestAttachment.getDataAsJson() + const proofRequest = JsonTransformer.fromJSON(proofRequestJson, ProofRequest) + const proofJson = attachment.getDataAsJson() const proof = JsonTransformer.fromJSON(proofJson, PartialProof) for (const [referent, attribute] of proof.requestedProof.revealedAttributes.entries()) { @@ -310,7 +228,7 @@ export class IndyProofFormatService extends ProofFormatService { new Set(proof.identifiers.map((i) => i.credentialDefinitionId)) ) - return await this.indyVerifierService.verifyProof(agentContext, { + return await indyVerifierService.verifyProof(agentContext, { proofRequest: proofRequest.toJSON(), proof: proofJson, schemas, @@ -318,116 +236,95 @@ export class IndyProofFormatService extends ProofFormatService { }) } - public supportsFormat(formatIdentifier: string): boolean { - const supportedFormats = [V2_INDY_PRESENTATION_PROPOSAL, V2_INDY_PRESENTATION_REQUEST, V2_INDY_PRESENTATION] - return supportedFormats.includes(formatIdentifier) + public async getCredentialsForRequest( + agentContext: AgentContext, + { requestAttachment, proofFormats }: ProofFormatGetCredentialsForRequestOptions + ): Promise> { + const proofRequestJson = requestAttachment.getDataAsJson() + const proofRequest = JsonTransformer.fromJSON(proofRequestJson, ProofRequest) + + // Set default values + const { filterByNonRevocationRequirements = true } = proofFormats?.indy ?? {} + + const credentialsForRequest = await this._getCredentialsForRequest(agentContext, proofRequest, { + filterByNonRevocationRequirements, + }) + + return credentialsForRequest } - /** - * Compare presentation attrs with request/proposal attrs (auto-accept) - * - * @param proposalAttachments attachment data from the proposal - * @param requestAttachments attachment data from the request - * @returns boolean value - */ - public proposalAndRequestAreEqual( - proposalAttachments: ProofAttachmentFormat[], - requestAttachments: ProofAttachmentFormat[] - ) { - const proposalAttachment = proposalAttachments.find( - (x) => x.format.format === V2_INDY_PRESENTATION_PROPOSAL - )?.attachment - const requestAttachment = requestAttachments.find( - (x) => x.format.format === V2_INDY_PRESENTATION_REQUEST - )?.attachment - - if (!proposalAttachment) { - throw new AriesFrameworkError('Proposal message has no attachment linked to it') - } + public async selectCredentialsForRequest( + agentContext: AgentContext, + { requestAttachment, proofFormats }: ProofFormatSelectCredentialsForRequestOptions + ): Promise> { + const proofRequestJson = requestAttachment.getDataAsJson() + const proofRequest = JsonTransformer.fromJSON(proofRequestJson, ProofRequest) - if (!requestAttachment) { - throw new AriesFrameworkError('Request message has no attachment linked to it') - } + // Set default values + const { filterByNonRevocationRequirements = true } = proofFormats?.indy ?? {} - const proposalAttachmentJson = proposalAttachment.getDataAsJson() - const proposalAttachmentData = JsonTransformer.fromJSON(proposalAttachmentJson, ProofRequest) + const selectedCredentials = this._selectCredentialsForRequest(agentContext, proofRequest, { + filterByNonRevocationRequirements, + }) - const requestAttachmentJson = requestAttachment.getDataAsJson() - const requestAttachmentData = JsonTransformer.fromJSON(requestAttachmentJson, ProofRequest) + return selectedCredentials + } - if ( - deepEquality(proposalAttachmentData.requestedAttributes, requestAttachmentData.requestedAttributes) && - deepEquality(proposalAttachmentData.requestedPredicates, requestAttachmentData.requestedPredicates) - ) { - return true - } + public async shouldAutoRespondToProposal( + agentContext: AgentContext, + { proposalAttachment, requestAttachment }: ProofFormatAutoRespondProposalOptions + ): Promise { + const proposalJson = proposalAttachment.getDataAsJson() + const requestJson = requestAttachment.getDataAsJson() + + const areRequestsEqual = areIndyProofRequestsEqual(proposalJson, requestJson) + agentContext.config.logger.debug(`Indy request and proposal are are equal: ${areRequestsEqual}`, { + proposalJson, + requestJson, + }) - return false + return areRequestsEqual } - /** - * Build credential definitions object needed to create and verify proof objects. - * - * Creates object with `{ credentialDefinitionId: CredentialDefinition }` mapping - * - * @param credentialDefinitionIds List of credential definition ids - * @returns Object containing credential definitions for specified credential definition ids - * - */ - private async getCredentialDefinitions(agentContext: AgentContext, credentialDefinitionIds: Set) { - const credentialDefinitions: { [key: string]: CredDef } = {} + public async shouldAutoRespondToRequest( + agentContext: AgentContext, + { proposalAttachment, requestAttachment }: ProofFormatAutoRespondRequestOptions + ): Promise { + const proposalJson = proposalAttachment.getDataAsJson() + const requestJson = requestAttachment.getDataAsJson() - for (const credDefId of credentialDefinitionIds) { - const credDef = await this.ledgerService.getCredentialDefinition(agentContext, credDefId) - credentialDefinitions[credDefId] = credDef - } + return areIndyProofRequestsEqual(proposalJson, requestJson) + } - return credentialDefinitions + public async shouldAutoRespondToPresentation(): Promise { + // The presentation is already verified in processPresentation, so we can just return true here. + // It's only an ack, so it's just that we received the presentation. + return true } - public async getRequestedCredentialsForProofRequest( + public supportsFormat(formatIdentifier: string): boolean { + const supportedFormats = [V2_INDY_PRESENTATION_PROPOSAL, V2_INDY_PRESENTATION_REQUEST, V2_INDY_PRESENTATION] + return supportedFormats.includes(formatIdentifier) + } + + private async _getCredentialsForRequest( agentContext: AgentContext, - options: GetRequestedCredentialsFormat - ): Promise> { - const retrievedCredentials = new RetrievedCredentials({}) - const { attachment, presentationProposal } = options - const filterByNonRevocationRequirements = options.config?.filterByNonRevocationRequirements + proofRequest: ProofRequest, + options: IndyGetCredentialsForProofRequestOptions + ): Promise { + const credentialsForProofRequest: IndyCredentialsForProofRequest = { + attributes: {}, + predicates: {}, + } - const proofRequestJson = attachment.getDataAsJson() ?? null - const proofRequest = JsonTransformer.fromJSON(proofRequestJson, ProofRequest) + const proofRequestJson = proofRequest.toJSON() for (const [referent, requestedAttribute] of proofRequest.requestedAttributes.entries()) { - let credentialMatch: IndyCredential[] = [] - const credentials = await this.getCredentialsForProofRequest(agentContext, proofRequest, referent) + const credentials = await this.getCredentialsForProofRequestReferent(agentContext, proofRequestJson, referent) - // If we have exactly one credential, or no proposal to pick preferences - // on the credentials to use, we will use the first one - if (credentials.length === 1 || !presentationProposal) { - credentialMatch = credentials - } - // If we have a proposal we will use that to determine the credentials to use - else { - const names = requestedAttribute.names ?? [requestedAttribute.name] - - // Find credentials that matches all parameters from the proposal - credentialMatch = credentials.filter((credential) => { - const { attributes, credentialDefinitionId } = credential.credentialInfo - - // Check if credentials matches all parameters from proposal - return names.every((name) => - presentationProposal.attributes.find( - (a) => - a.name === name && - a.credentialDefinitionId === credentialDefinitionId && - (!a.value || a.value === attributes[name]) - ) - ) - }) - } - - retrievedCredentials.requestedAttributes[referent] = sortRequestedCredentials( + credentialsForProofRequest.attributes[referent] = sortRequestedCredentials( await Promise.all( - credentialMatch.map(async (credential: IndyCredential) => { + credentials.map(async (credential: IndyCredential) => { const { revoked, deltaTimestamp } = await this.getRevocationStatusForRequestedItem(agentContext, { proofRequest, requestedItem: requestedAttribute, @@ -447,17 +344,17 @@ export class IndyProofFormatService extends ProofFormatService { // We only attach revoked state if non-revocation is requested. So if revoked is true it means // the credential is not applicable to the proof request - if (filterByNonRevocationRequirements) { - retrievedCredentials.requestedAttributes[referent] = retrievedCredentials.requestedAttributes[referent].filter( + if (options.filterByNonRevocationRequirements) { + credentialsForProofRequest.attributes[referent] = credentialsForProofRequest.attributes[referent].filter( (r) => !r.revoked ) } } for (const [referent, requestedPredicate] of proofRequest.requestedPredicates.entries()) { - const credentials = await this.getCredentialsForProofRequest(agentContext, proofRequest, referent) + const credentials = await this.getCredentialsForProofRequestReferent(agentContext, proofRequestJson, referent) - retrievedCredentials.requestedPredicates[referent] = sortRequestedCredentials( + credentialsForProofRequest.predicates[referent] = sortRequestedCredentials( await Promise.all( credentials.map(async (credential) => { const { revoked, deltaTimestamp } = await this.getRevocationStatusForRequestedItem(agentContext, { @@ -478,68 +375,64 @@ export class IndyProofFormatService extends ProofFormatService { // We only attach revoked state if non-revocation is requested. So if revoked is true it means // the credential is not applicable to the proof request - if (filterByNonRevocationRequirements) { - retrievedCredentials.requestedPredicates[referent] = retrievedCredentials.requestedPredicates[referent].filter( + if (options.filterByNonRevocationRequirements) { + credentialsForProofRequest.predicates[referent] = credentialsForProofRequest.predicates[referent].filter( (r) => !r.revoked ) } } - return { - proofFormats: { - indy: retrievedCredentials, - }, - } + return credentialsForProofRequest } - private async getCredentialsForProofRequest( + private async _selectCredentialsForRequest( agentContext: AgentContext, proofRequest: ProofRequest, - attributeReferent: string - ): Promise { - const credentialsJson = await this.indyHolderService.getCredentialsForProofRequest(agentContext, { - proofRequest: proofRequest.toJSON(), - attributeReferent, - }) - - return JsonTransformer.fromJSON(credentialsJson, IndyCredential) as unknown as IndyCredential[] - } - - public async autoSelectCredentialsForProofRequest( - options: FormatRetrievedCredentialOptions<[IndyProofFormat]> - ): Promise> { - const { proofFormats } = options - const indy = proofFormats.indy - - if (!indy) { - throw new AriesFrameworkError('No indy options provided') + options: IndyGetCredentialsForProofRequestOptions + ): Promise { + const credentialsForRequest = await this._getCredentialsForRequest(agentContext, proofRequest, options) + + const selectedCredentials: IndySelectedCredentialsForProofRequest = { + requestedAttributes: {}, + requestedPredicates: {}, + selfAttestedAttributes: {}, } - const requestedCredentials = new RequestedCredentials({}) - - Object.keys(indy.requestedAttributes).forEach((attributeName) => { - const attributeArray = indy.requestedAttributes[attributeName] + Object.keys(credentialsForRequest.attributes).forEach((attributeName) => { + const attributeArray = credentialsForRequest.attributes[attributeName] if (attributeArray.length === 0) { throw new AriesFrameworkError('Unable to automatically select requested attributes.') - } else { - requestedCredentials.requestedAttributes[attributeName] = attributeArray[0] } + + selectedCredentials.requestedAttributes[attributeName] = attributeArray[0] }) - Object.keys(indy.requestedPredicates).forEach((attributeName) => { - if (indy.requestedPredicates[attributeName].length === 0) { + Object.keys(credentialsForRequest.predicates).forEach((attributeName) => { + if (credentialsForRequest.predicates[attributeName].length === 0) { throw new AriesFrameworkError('Unable to automatically select requested predicates.') } else { - requestedCredentials.requestedPredicates[attributeName] = indy.requestedPredicates[attributeName][0] + selectedCredentials.requestedPredicates[attributeName] = credentialsForRequest.predicates[attributeName][0] } }) - return { - proofFormats: { - indy: requestedCredentials, - }, - } + return selectedCredentials + } + + private async getCredentialsForProofRequestReferent( + agentContext: AgentContext, + // pass as json to prevent having to transform to json on every call + proofRequestJson: IndyProofRequest, + attributeReferent: string + ): Promise { + const holderService = agentContext.dependencyManager.resolve(IndyHolderService) + + const credentialsJson = await holderService.getCredentialsForProofRequest(agentContext, { + proofRequest: proofRequestJson, + attributeReferent, + }) + + return JsonTransformer.fromJSON(credentialsJson, IndyCredential) as unknown as IndyCredential[] } /** @@ -552,16 +445,40 @@ export class IndyProofFormatService extends ProofFormatService { * */ private async getSchemas(agentContext: AgentContext, schemaIds: Set) { + const ledgerService = agentContext.dependencyManager.resolve(IndyLedgerService) + const schemas: { [key: string]: Schema } = {} for (const schemaId of schemaIds) { - const schema = await this.ledgerService.getSchema(agentContext, schemaId) + const schema = await ledgerService.getSchema(agentContext, schemaId) schemas[schemaId] = schema } return schemas } + /** + * Build credential definitions object needed to create and verify proof objects. + * + * Creates object with `{ credentialDefinitionId: CredentialDefinition }` mapping + * + * @param credentialDefinitionIds List of credential definition ids + * @returns Object containing credential definitions for specified credential definition ids + * + */ + private async getCredentialDefinitions(agentContext: AgentContext, credentialDefinitionIds: Set) { + const ledgerService = agentContext.dependencyManager.resolve(IndyLedgerService) + + const credentialDefinitions: { [key: string]: CredDef } = {} + + for (const credDefId of credentialDefinitionIds) { + const credDef = await ledgerService.getCredentialDefinition(agentContext, credDefId) + credentialDefinitions[credDefId] = credDef + } + + return credentialDefinitions + } + /** * Create indy proof from a given proof request and requested credential object. * @@ -574,6 +491,8 @@ export class IndyProofFormatService extends ProofFormatService { proofRequest: ProofRequest, requestedCredentials: RequestedCredentials ): Promise { + const indyHolderService = agentContext.dependencyManager.resolve(IndyHolderService) + const credentialObjects = await Promise.all( [ ...Object.values(requestedCredentials.requestedAttributes), @@ -582,7 +501,7 @@ export class IndyProofFormatService extends ProofFormatService { if (c.credentialInfo) { return c.credentialInfo } - const credentialInfo = await this.indyHolderService.getCredential(agentContext, c.credentialId) + const credentialInfo = await indyHolderService.getCredential(agentContext, c.credentialId) return JsonTransformer.fromJSON(credentialInfo, IndyCredentialInfo) }) ) @@ -593,7 +512,7 @@ export class IndyProofFormatService extends ProofFormatService { new Set(credentialObjects.map((c) => c.credentialDefinitionId)) ) - return await this.indyHolderService.createProof(agentContext, { + return await indyHolderService.createProof(agentContext, { proofRequest: proofRequest.toJSON(), requestedCredentials: requestedCredentials, schemas, @@ -601,25 +520,6 @@ export class IndyProofFormatService extends ProofFormatService { }) } - public async createProofRequestFromProposal(options: CreatePresentationFormatsOptions): Promise { - const proofRequestJson = options.presentationAttachment.getDataAsJson() - - const proofRequest = JsonTransformer.fromJSON(proofRequestJson, ProofRequest) - - // Assert attachment - if (!proofRequest) { - throw new AriesFrameworkError(`Missing required base64 or json encoded attachment data for presentation request.`) - } - MessageValidator.validateSync(proofRequest) - - // Assert attribute and predicate (group) names do not match - checkProofRequestForDuplicates(proofRequest) - - return { - indy: proofRequest, - } - } - private async getRevocationStatusForRequestedItem( agentContext: AgentContext, { @@ -632,13 +532,15 @@ export class IndyProofFormatService extends ProofFormatService { credential: IndyCredential } ) { + const indyRevocationService = agentContext.dependencyManager.resolve(IndyRevocationService) + const requestNonRevoked = requestedItem.nonRevoked ?? proofRequest.nonRevoked const credentialRevocationId = credential.credentialInfo.credentialRevocationId const revocationRegistryId = credential.credentialInfo.revocationRegistryId // If revocation interval is present and the credential is revocable then fetch the revocation status of credentials for display if (requestNonRevoked && credentialRevocationId && revocationRegistryId) { - this.logger.trace( + agentContext.config.logger.trace( `Presentation is requesting proof of non revocation, getting revocation status for credential`, { requestNonRevoked, @@ -648,7 +550,7 @@ export class IndyProofFormatService extends ProofFormatService { ) // Note presentation from-to's vs ledger from-to's: https://github.com/hyperledger/indy-hipe/blob/master/text/0011-cred-revocation/README.md#indy-node-revocation-registry-intervals - const status = await this.indyRevocationService.getRevocationStatus( + const status = await indyRevocationService.getRevocationStatus( agentContext, credentialRevocationId, revocationRegistryId, @@ -661,89 +563,22 @@ export class IndyProofFormatService extends ProofFormatService { return { revoked: undefined, deltaTimestamp: undefined } } - public async createRequestFromPreview(indyFormat: IndyProposeProofFormat): Promise { - const preview = new PresentationPreview({ - attributes: indyFormat.attributes, - predicates: indyFormat.predicates, - }) - - const proofRequest = await this.createReferentForProofRequest(indyFormat, preview) - - return proofRequest - } - - public async createReferentForProofRequest( - indyFormat: IndyProposeProofFormat, - preview: PresentationPreview - ): Promise { - const proofRequest = new ProofRequest({ - name: indyFormat.name ?? 'proof-request', - version: indyFormat.version ?? '1.0', - nonce: indyFormat.nonce ?? (await this.wallet.generateNonce()), + /** + * Returns an object of type {@link Attachment} for use in credential exchange messages. + * It looks up the correct format identifier and encodes the data as a base64 attachment. + * + * @param data The data to include in the attach object + * @param id the attach id from the formats component of the message + */ + private getFormatData(data: unknown, id: string): Attachment { + const attachment = new Attachment({ + id, + mimeType: 'application/json', + data: new AttachmentData({ + base64: JsonEncoder.toBase64(data), + }), }) - /** - * Create mapping of attributes by referent. This required the - * attributes to come from the same credential. - * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0037-present-proof/README.md#referent - * - * { - * "referent1": [Attribute1, Attribute2], - * "referent2": [Attribute3] - * } - */ - const attributesByReferent: Record = {} - for (const proposedAttributes of preview.attributes) { - if (!proposedAttributes.referent) proposedAttributes.referent = uuid() - - const referentAttributes = attributesByReferent[proposedAttributes.referent] - - // Referent key already exist, add to list - if (referentAttributes) { - referentAttributes.push(proposedAttributes) - } - - // Referent key does not exist yet, create new entry - else { - attributesByReferent[proposedAttributes.referent] = [proposedAttributes] - } - } - - // Transform attributes by referent to requested attributes - for (const [referent, proposedAttributes] of Object.entries(attributesByReferent)) { - // Either attributeName or attributeNames will be undefined - const attributeName = proposedAttributes.length == 1 ? proposedAttributes[0].name : undefined - const attributeNames = proposedAttributes.length > 1 ? proposedAttributes.map((a) => a.name) : undefined - - const requestedAttribute = new ProofAttributeInfo({ - name: attributeName, - names: attributeNames, - restrictions: [ - new AttributeFilter({ - credentialDefinitionId: proposedAttributes[0].credentialDefinitionId, - }), - ], - }) - - proofRequest.requestedAttributes.set(referent, requestedAttribute) - } - - // Transform proposed predicates to requested predicates - for (const proposedPredicate of preview.predicates) { - const requestedPredicate = new ProofPredicateInfo({ - name: proposedPredicate.name, - predicateType: proposedPredicate.predicate, - predicateValue: proposedPredicate.threshold, - restrictions: [ - new AttributeFilter({ - credentialDefinitionId: proposedPredicate.credentialDefinitionId, - }), - ], - }) - - proofRequest.requestedPredicates.set(uuid(), requestedPredicate) - } - - return proofRequest + return attachment } } diff --git a/packages/core/src/modules/proofs/formats/indy/IndyProofFormatsServiceOptions.ts b/packages/core/src/modules/proofs/formats/indy/IndyProofFormatsServiceOptions.ts deleted file mode 100644 index bb78139bb7..0000000000 --- a/packages/core/src/modules/proofs/formats/indy/IndyProofFormatsServiceOptions.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { IndyRequestedCredentialsFormat } from './IndyProofFormat' -import type { ProofAttributeInfo } from '.././indy/models/ProofAttributeInfo' -import type { ProofPredicateInfo } from '.././indy/models/ProofPredicateInfo' -import type { Attachment } from '../../../../decorators/attachment/Attachment' -import type { IndyRevocationInterval } from '../../../credentials' -import type { GetRequestedCredentialsConfig } from '../../models/GetRequestedCredentialsConfig' -import type { PresentationPreview } from '../../protocol/v1/models/V1PresentationPreview' -import type { ProofExchangeRecord } from '../../repository/ProofExchangeRecord' - -export type IndyPresentationProofFormat = IndyRequestedCredentialsFormat - -export interface IndyRequestProofFormat { - name?: string - version?: string - nonce?: string - nonRevoked?: IndyRevocationInterval - ver?: '1.0' | '2.0' - requestedAttributes?: Record | Map - requestedPredicates?: Record | Map -} - -export interface IndyVerifyProofFormat { - proofJson: Attachment - proofRequest: Attachment -} - -export interface GetRequestedCredentialsFormat { - attachment: Attachment - presentationProposal?: PresentationPreview - config?: GetRequestedCredentialsConfig -} - -export interface IndyProofRequestFromProposalOptions { - proofRecord: ProofExchangeRecord - name?: string - version?: string - nonce?: string -} diff --git a/packages/core/src/modules/proofs/__tests__/groupKeys.ts b/packages/core/src/modules/proofs/formats/indy/__tests__/groupKeys.ts similarity index 79% rename from packages/core/src/modules/proofs/__tests__/groupKeys.ts rename to packages/core/src/modules/proofs/formats/indy/__tests__/groupKeys.ts index e20144792f..3d62914aca 100644 --- a/packages/core/src/modules/proofs/__tests__/groupKeys.ts +++ b/packages/core/src/modules/proofs/formats/indy/__tests__/groupKeys.ts @@ -1,9 +1,9 @@ -import type { IndyProofFormat } from '../formats/indy/IndyProofFormat' -import type { GetFormatDataReturn } from '../models/ProofServiceOptions' +import type { GetProofFormatDataReturn } from '../../../protocol/ProofProtocolOptions' +import type { IndyProofFormat } from '../IndyProofFormat' -import { AriesFrameworkError } from '../../../error' +import { AriesFrameworkError } from '../../../../../error' -export function getGroupKeysFromIndyProofFormatData(formatData: GetFormatDataReturn<[IndyProofFormat]>): { +export function getGroupKeysFromIndyProofFormatData(formatData: GetProofFormatDataReturn<[IndyProofFormat]>): { proposeKey1: string proposeKey2: string requestKey1: string diff --git a/packages/core/src/modules/proofs/formats/indy/__tests__/util.test.ts b/packages/core/src/modules/proofs/formats/indy/__tests__/util.test.ts new file mode 100644 index 0000000000..f4165bdb46 --- /dev/null +++ b/packages/core/src/modules/proofs/formats/indy/__tests__/util.test.ts @@ -0,0 +1,493 @@ +import type { default as Indy } from 'indy-sdk' + +import { AriesFrameworkError } from '../../../../../error' +import { AttributeFilter, PredicateType, ProofAttributeInfo, ProofPredicateInfo, ProofRequest } from '../models' +import { areIndyProofRequestsEqual, assertNoDuplicateGroupsNamesInProofRequest } from '../util' + +const proofRequest = { + name: 'Proof Request', + version: '1.0.0', + nonce: 'nonce', + ver: '1.0', + non_revoked: {}, + requested_attributes: { + a: { + names: ['name1', 'name2'], + restrictions: [ + { + cred_def_id: 'cred_def_id1', + }, + { + schema_id: 'schema_id', + }, + ], + }, + }, + requested_predicates: { + p: { + name: 'Hello', + p_type: '<', + p_value: 10, + restrictions: [ + { + cred_def_id: 'string2', + }, + { + cred_def_id: 'string', + }, + ], + }, + }, +} satisfies Indy.IndyProofRequest + +const credDefId = '9vPXgSpQJPkJEALbLXueBp:3:CL:57753:tag1' +const nonce = 'testtesttest12345' + +describe('IndyProofFormat | util', () => { + describe('assertNoDuplicateGroupsNamesInProofRequest', () => { + test('attribute names match', () => { + const attributes = { + age1: new ProofAttributeInfo({ + name: 'age', + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: credDefId, + }), + ], + }), + age2: new ProofAttributeInfo({ + name: 'age', + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: credDefId, + }), + ], + }), + } + + const proofRequest = new ProofRequest({ + name: 'proof-request', + version: '1.0', + nonce, + requestedAttributes: attributes, + }) + + expect(() => assertNoDuplicateGroupsNamesInProofRequest(proofRequest)).not.toThrow() + }) + + test('attribute names match with predicates name', () => { + const attributes = { + attrib: new ProofAttributeInfo({ + name: 'age', + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: credDefId, + }), + ], + }), + } + + const predicates = { + predicate: new ProofPredicateInfo({ + name: 'age', + predicateType: PredicateType.GreaterThanOrEqualTo, + predicateValue: 50, + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: credDefId, + }), + ], + }), + } + + const proofRequest = new ProofRequest({ + name: 'proof-request', + version: '1.0', + nonce, + requestedAttributes: attributes, + requestedPredicates: predicates, + }) + + expect(() => assertNoDuplicateGroupsNamesInProofRequest(proofRequest)).toThrowError(AriesFrameworkError) + }) + }) + describe('areIndyProofRequestsEqual', () => { + test('does not compare name, ver, version and nonce', () => { + expect( + areIndyProofRequestsEqual(proofRequest, { + ...proofRequest, + name: 'Proof Request 2', + version: '2.0.0', + nonce: 'nonce2', + ver: '2.0', + }) + ).toBe(true) + }) + + test('check top level non_revocation interval', () => { + // empty object is semantically equal to undefined + expect( + areIndyProofRequestsEqual(proofRequest, { + ...proofRequest, + non_revoked: {}, + }) + ).toBe(true) + + // properties inside object are different + expect( + areIndyProofRequestsEqual( + { + ...proofRequest, + non_revoked: { + to: 5, + }, + }, + { + ...proofRequest, + non_revoked: { + from: 5, + }, + } + ) + ).toBe(false) + + // One has non_revoked, other doesn't + expect( + areIndyProofRequestsEqual(proofRequest, { + ...proofRequest, + non_revoked: { + from: 5, + }, + }) + ).toBe(false) + }) + + test('ignores attribute group name differences', () => { + expect( + areIndyProofRequestsEqual(proofRequest, { + ...proofRequest, + requested_attributes: { + b: proofRequest.requested_attributes.a, + }, + }) + ).toBe(true) + }) + + test('ignores attribute restriction order', () => { + expect( + areIndyProofRequestsEqual(proofRequest, { + ...proofRequest, + requested_attributes: { + a: { + ...proofRequest.requested_attributes.a, + restrictions: [...proofRequest.requested_attributes.a.restrictions].reverse(), + }, + }, + }) + ).toBe(true) + }) + + test('ignores attribute restriction undefined vs empty array', () => { + expect( + areIndyProofRequestsEqual( + { + ...proofRequest, + requested_attributes: { + a: { + ...proofRequest.requested_attributes.a, + restrictions: undefined, + }, + }, + }, + { + ...proofRequest, + requested_attributes: { + a: { + ...proofRequest.requested_attributes.a, + restrictions: [], + }, + }, + } + ) + ).toBe(true) + }) + + test('ignores attribute names order', () => { + expect( + areIndyProofRequestsEqual(proofRequest, { + ...proofRequest, + requested_attributes: { + a: { + ...proofRequest.requested_attributes.a, + names: ['name2', 'name1'], + }, + }, + }) + ).toBe(true) + }) + + test('checks attribute non_revocation interval', () => { + // empty object is semantically equal to undefined + expect( + areIndyProofRequestsEqual(proofRequest, { + ...proofRequest, + requested_attributes: { + a: { + ...proofRequest.requested_attributes.a, + non_revoked: {}, + }, + }, + }) + ).toBe(true) + + // properties inside object are different + expect( + areIndyProofRequestsEqual( + { + ...proofRequest, + requested_attributes: { + a: { + ...proofRequest.requested_attributes.a, + non_revoked: { + to: 5, + }, + }, + }, + }, + { + ...proofRequest, + requested_attributes: { + a: { + ...proofRequest.requested_attributes.a, + non_revoked: { + from: 5, + }, + }, + }, + } + ) + ).toBe(false) + + // One has non_revoked, other doesn't + expect( + areIndyProofRequestsEqual(proofRequest, { + ...proofRequest, + requested_attributes: { + a: { + ...proofRequest.requested_attributes.a, + non_revoked: { + from: 5, + }, + }, + }, + }) + ).toBe(false) + }) + + test('checks attribute restriction differences', () => { + expect( + areIndyProofRequestsEqual(proofRequest, { + ...proofRequest, + requested_attributes: { + a: { + ...proofRequest.requested_attributes.a, + restrictions: [ + { + cred_def_id: 'cred_def_id1', + }, + { + cred_def_id: 'cred_def_id2', + }, + ], + }, + }, + }) + ).toBe(false) + }) + + test('checks attribute name differences', () => { + expect( + areIndyProofRequestsEqual(proofRequest, { + ...proofRequest, + requested_attributes: { + a: { + ...proofRequest.requested_attributes.a, + names: ['name3'], + }, + }, + }) + ).toBe(false) + + expect( + areIndyProofRequestsEqual(proofRequest, { + ...proofRequest, + requested_attributes: { + a: { + ...proofRequest.requested_attributes.a, + name: 'name3', + names: undefined, + }, + }, + }) + ).toBe(false) + }) + + test('ignores predicate group name differences', () => { + expect( + areIndyProofRequestsEqual(proofRequest, { + ...proofRequest, + requested_predicates: { + a: proofRequest.requested_predicates.p, + }, + }) + ).toBe(true) + }) + + test('ignores predicate restriction order', () => { + expect( + areIndyProofRequestsEqual(proofRequest, { + ...proofRequest, + requested_predicates: { + p: { + ...proofRequest.requested_predicates.p, + restrictions: [...proofRequest.requested_predicates.p.restrictions].reverse(), + }, + }, + }) + ).toBe(true) + }) + + test('ignores predicate restriction undefined vs empty array', () => { + expect( + areIndyProofRequestsEqual( + { + ...proofRequest, + requested_predicates: { + p: { + ...proofRequest.requested_predicates.p, + restrictions: undefined, + }, + }, + }, + { + ...proofRequest, + requested_predicates: { + p: { + ...proofRequest.requested_predicates.p, + restrictions: [], + }, + }, + } + ) + ).toBe(true) + }) + + test('checks predicate restriction differences', () => { + expect( + areIndyProofRequestsEqual(proofRequest, { + ...proofRequest, + requested_attributes: { + p: { + ...proofRequest.requested_predicates.p, + restrictions: [ + { + cred_def_id: 'cred_def_id1', + }, + { + cred_def_id: 'cred_def_id2', + }, + ], + }, + }, + }) + ).toBe(false) + }) + + test('checks predicate name differences', () => { + expect( + areIndyProofRequestsEqual(proofRequest, { + ...proofRequest, + requested_predicates: { + p: { + ...proofRequest.requested_predicates.p, + name: 'name3', + }, + }, + }) + ).toBe(false) + }) + + test('checks predicate non_revocation interval', () => { + // empty object is semantically equal to undefined + expect( + areIndyProofRequestsEqual(proofRequest, { + ...proofRequest, + requested_predicates: { + p: { + ...proofRequest.requested_predicates.p, + non_revoked: {}, + }, + }, + }) + ).toBe(true) + + // properties inside object are different + expect( + areIndyProofRequestsEqual( + { + ...proofRequest, + requested_predicates: { + p: { + ...proofRequest.requested_predicates.p, + non_revoked: { + to: 5, + }, + }, + }, + }, + { + ...proofRequest, + requested_predicates: { + p: { + ...proofRequest.requested_predicates.p, + non_revoked: { + from: 5, + }, + }, + }, + } + ) + ).toBe(false) + + // One has non_revoked, other doesn't + expect( + areIndyProofRequestsEqual(proofRequest, { + ...proofRequest, + requested_predicates: { + p: { + ...proofRequest.requested_predicates.p, + non_revoked: { + from: 5, + }, + }, + }, + }) + ).toBe(false) + }) + + test('checks predicate p_type and p_value', () => { + expect( + areIndyProofRequestsEqual(proofRequest, { + ...proofRequest, + requested_predicates: { + p: { + ...proofRequest.requested_predicates.p, + p_type: '<', + p_value: 134134, + }, + }, + }) + ).toBe(false) + }) + }) +}) diff --git a/packages/core/src/modules/proofs/formats/indy/errors/MissingIndyProofMessageError.ts b/packages/core/src/modules/proofs/formats/indy/errors/MissingIndyProofMessageError.ts deleted file mode 100644 index 2ab9c3f15e..0000000000 --- a/packages/core/src/modules/proofs/formats/indy/errors/MissingIndyProofMessageError.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { AriesFrameworkError } from '../../../../../error/AriesFrameworkError' - -export class MissingIndyProofMessageError extends AriesFrameworkError {} diff --git a/packages/core/src/modules/proofs/formats/indy/errors/index.ts b/packages/core/src/modules/proofs/formats/indy/errors/index.ts index 0f0b302726..7b2373bb66 100644 --- a/packages/core/src/modules/proofs/formats/indy/errors/index.ts +++ b/packages/core/src/modules/proofs/formats/indy/errors/index.ts @@ -1,2 +1 @@ export * from './InvalidEncodedValueError' -export * from './MissingIndyProofMessageError' diff --git a/packages/core/src/modules/proofs/formats/indy/index.ts b/packages/core/src/modules/proofs/formats/indy/index.ts index c94afb8629..185c2f8afc 100644 --- a/packages/core/src/modules/proofs/formats/indy/index.ts +++ b/packages/core/src/modules/proofs/formats/indy/index.ts @@ -1,4 +1,2 @@ -export * from './errors' -export * from './models' export * from './IndyProofFormat' -export * from './IndyProofFormatsServiceOptions' +export * from './IndyProofFormatService' diff --git a/packages/core/src/modules/proofs/protocol/v1/models/PartialProof.ts b/packages/core/src/modules/proofs/formats/indy/models/PartialProof.ts similarity index 100% rename from packages/core/src/modules/proofs/protocol/v1/models/PartialProof.ts rename to packages/core/src/modules/proofs/formats/indy/models/PartialProof.ts diff --git a/packages/core/src/modules/proofs/protocol/v1/models/ProofAttribute.ts b/packages/core/src/modules/proofs/formats/indy/models/ProofAttribute.ts similarity index 100% rename from packages/core/src/modules/proofs/protocol/v1/models/ProofAttribute.ts rename to packages/core/src/modules/proofs/formats/indy/models/ProofAttribute.ts diff --git a/packages/core/src/modules/proofs/formats/indy/models/ProofAttributeInfo.ts b/packages/core/src/modules/proofs/formats/indy/models/ProofAttributeInfo.ts index 4bf1f136b0..a67c8425ae 100644 --- a/packages/core/src/modules/proofs/formats/indy/models/ProofAttributeInfo.ts +++ b/packages/core/src/modules/proofs/formats/indy/models/ProofAttributeInfo.ts @@ -5,13 +5,15 @@ import { IndyRevocationInterval } from '../../../../credentials' import { AttributeFilter } from './AttributeFilter' +export type ProofAttributeInfoOptions = ProofAttributeInfo + export class ProofAttributeInfo { - public constructor(options: ProofAttributeInfo) { + public constructor(options: ProofAttributeInfoOptions) { if (options) { this.name = options.name this.names = options.names - this.nonRevoked = options.nonRevoked - this.restrictions = options.restrictions + this.nonRevoked = options.nonRevoked ? new IndyRevocationInterval(options.nonRevoked) : undefined + this.restrictions = options.restrictions?.map((r) => new AttributeFilter(r)) } } diff --git a/packages/core/src/modules/proofs/protocol/v1/models/ProofIdentifier.ts b/packages/core/src/modules/proofs/formats/indy/models/ProofIdentifier.ts similarity index 100% rename from packages/core/src/modules/proofs/protocol/v1/models/ProofIdentifier.ts rename to packages/core/src/modules/proofs/formats/indy/models/ProofIdentifier.ts diff --git a/packages/core/src/modules/proofs/formats/indy/models/ProofPredicateInfo.ts b/packages/core/src/modules/proofs/formats/indy/models/ProofPredicateInfo.ts index 8f246746bf..48083fc54d 100644 --- a/packages/core/src/modules/proofs/formats/indy/models/ProofPredicateInfo.ts +++ b/packages/core/src/modules/proofs/formats/indy/models/ProofPredicateInfo.ts @@ -6,13 +6,22 @@ import { IndyRevocationInterval } from '../../../../credentials' import { AttributeFilter } from './AttributeFilter' import { PredicateType } from './PredicateType' +export interface ProofPredicateInfoOptions { + name: string + // Also allow string value of the enum as input, to make it easier to use in the API + predicateType: PredicateType | `${PredicateType}` + predicateValue: number + nonRevoked?: IndyRevocationInterval + restrictions?: AttributeFilter[] +} + export class ProofPredicateInfo { - public constructor(options: ProofPredicateInfo) { + public constructor(options: ProofPredicateInfoOptions) { if (options) { this.name = options.name - this.nonRevoked = options.nonRevoked - this.restrictions = options.restrictions - this.predicateType = options.predicateType + this.nonRevoked = options.nonRevoked ? new IndyRevocationInterval(options.nonRevoked) : undefined + this.restrictions = options.restrictions?.map((r) => new AttributeFilter(r)) + this.predicateType = options.predicateType as PredicateType this.predicateValue = options.predicateValue } } diff --git a/packages/core/src/modules/proofs/formats/indy/models/ProofRequest.ts b/packages/core/src/modules/proofs/formats/indy/models/ProofRequest.ts index 224169c864..b2d5cf83cc 100644 --- a/packages/core/src/modules/proofs/formats/indy/models/ProofRequest.ts +++ b/packages/core/src/modules/proofs/formats/indy/models/ProofRequest.ts @@ -1,3 +1,5 @@ +import type { ProofAttributeInfoOptions } from './ProofAttributeInfo' +import type { ProofPredicateInfoOptions } from './ProofPredicateInfo' import type { IndyProofRequest } from 'indy-sdk' import { Expose, Type } from 'class-transformer' @@ -16,8 +18,8 @@ export interface ProofRequestOptions { nonce: string nonRevoked?: IndyRevocationInterval ver?: '1.0' | '2.0' - requestedAttributes?: Record | Map - requestedPredicates?: Record | Map + requestedAttributes?: Record + requestedPredicates?: Record } /** @@ -31,17 +33,22 @@ export class ProofRequest { this.name = options.name this.version = options.version this.nonce = options.nonce - this.requestedAttributes = options.requestedAttributes - ? options.requestedAttributes instanceof Map - ? options.requestedAttributes - : new Map(Object.entries(options.requestedAttributes)) - : new Map() - this.requestedPredicates = options.requestedPredicates - ? options.requestedPredicates instanceof Map - ? options.requestedPredicates - : new Map(Object.entries(options.requestedPredicates)) - : new Map() - this.nonRevoked = options.nonRevoked + + this.requestedAttributes = new Map( + Object.entries(options.requestedAttributes ?? {}).map(([key, attribute]) => [ + key, + new ProofAttributeInfo(attribute), + ]) + ) + + this.requestedPredicates = new Map( + Object.entries(options.requestedPredicates ?? {}).map(([key, predicate]) => [ + key, + new ProofPredicateInfo(predicate), + ]) + ) + + this.nonRevoked = options.nonRevoked ? new IndyRevocationInterval(options.nonRevoked) : undefined this.ver = options.ver } } diff --git a/packages/core/src/modules/proofs/formats/indy/models/RequestedAttribute.ts b/packages/core/src/modules/proofs/formats/indy/models/RequestedAttribute.ts index 048a89cf82..21a1e9a1c3 100644 --- a/packages/core/src/modules/proofs/formats/indy/models/RequestedAttribute.ts +++ b/packages/core/src/modules/proofs/formats/indy/models/RequestedAttribute.ts @@ -1,18 +1,28 @@ +import type { IndyCredentialInfoOptions } from '../../../../credentials/formats/indy/models/IndyCredentialInfo' + import { Exclude, Expose } from 'class-transformer' import { IsBoolean, IsInt, IsOptional, IsString } from 'class-validator' import { IndyCredentialInfo } from '../../../../credentials/formats/indy/models/IndyCredentialInfo' +export interface RequestedAttributeOptions { + credentialId: string + timestamp?: number + revealed: boolean + credentialInfo?: IndyCredentialInfoOptions + revoked?: boolean +} + /** * Requested Attribute for Indy proof creation */ export class RequestedAttribute { - public constructor(options: RequestedAttribute) { + public constructor(options: RequestedAttributeOptions) { if (options) { this.credentialId = options.credentialId this.timestamp = options.timestamp this.revealed = options.revealed - this.credentialInfo = options.credentialInfo + this.credentialInfo = options.credentialInfo ? new IndyCredentialInfo(options.credentialInfo) : undefined this.revoked = options.revoked } } diff --git a/packages/core/src/modules/proofs/formats/indy/models/RequestedCredentials.ts b/packages/core/src/modules/proofs/formats/indy/models/RequestedCredentials.ts index b2824bf7bd..f515a82dee 100644 --- a/packages/core/src/modules/proofs/formats/indy/models/RequestedCredentials.ts +++ b/packages/core/src/modules/proofs/formats/indy/models/RequestedCredentials.ts @@ -1,3 +1,5 @@ +import type { RequestedAttributeOptions } from './RequestedAttribute' +import type { RequestedPredicateOptions } from './RequestedPredicate' import type { IndyRequestedCredentials } from 'indy-sdk' import { Expose } from 'class-transformer' @@ -10,8 +12,8 @@ import { RequestedAttribute } from './RequestedAttribute' import { RequestedPredicate } from './RequestedPredicate' export interface IndyRequestedCredentialsOptions { - requestedAttributes?: Record - requestedPredicates?: Record + requestedAttributes?: Record + requestedPredicates?: Record selfAttestedAttributes?: Record } @@ -23,8 +25,24 @@ export interface IndyRequestedCredentialsOptions { export class RequestedCredentials { public constructor(options: IndyRequestedCredentialsOptions = {}) { if (options) { - this.requestedAttributes = options.requestedAttributes ?? {} - this.requestedPredicates = options.requestedPredicates ?? {} + const { requestedAttributes, requestedPredicates } = options + + // Create RequestedAttribute objects from options + this.requestedAttributes = {} + if (requestedAttributes) { + Object.keys(requestedAttributes).forEach((key) => { + this.requestedAttributes[key] = new RequestedAttribute(requestedAttributes[key]) + }) + } + + // Create RequestedPredicate objects from options + this.requestedPredicates = {} + if (requestedPredicates) { + Object.keys(requestedPredicates).forEach((key) => { + this.requestedPredicates[key] = new RequestedPredicate(requestedPredicates[key]) + }) + } + this.selfAttestedAttributes = options.selfAttestedAttributes ?? {} } } diff --git a/packages/core/src/modules/proofs/formats/indy/models/RequestedPredicate.ts b/packages/core/src/modules/proofs/formats/indy/models/RequestedPredicate.ts index 9109b51a4d..d8f5e2d9d2 100644 --- a/packages/core/src/modules/proofs/formats/indy/models/RequestedPredicate.ts +++ b/packages/core/src/modules/proofs/formats/indy/models/RequestedPredicate.ts @@ -1,17 +1,26 @@ +import type { IndyCredentialInfoOptions } from '../../../../credentials' + import { Exclude, Expose } from 'class-transformer' import { IsInt, IsOptional, IsString } from 'class-validator' import { IndyCredentialInfo } from '../../../../credentials' +export interface RequestedPredicateOptions { + credentialId: string + timestamp?: number + credentialInfo?: IndyCredentialInfoOptions + revoked?: boolean +} + /** * Requested Predicate for Indy proof creation */ export class RequestedPredicate { - public constructor(options: RequestedPredicate) { + public constructor(options: RequestedPredicateOptions) { if (options) { this.credentialId = options.credentialId this.timestamp = options.timestamp - this.credentialInfo = options.credentialInfo + this.credentialInfo = options.credentialInfo ? new IndyCredentialInfo(options.credentialInfo) : undefined this.revoked = options.revoked } } diff --git a/packages/core/src/modules/proofs/protocol/v1/models/RequestedProof.ts b/packages/core/src/modules/proofs/formats/indy/models/RequestedProof.ts similarity index 100% rename from packages/core/src/modules/proofs/protocol/v1/models/RequestedProof.ts rename to packages/core/src/modules/proofs/formats/indy/models/RequestedProof.ts diff --git a/packages/core/src/modules/proofs/__tests__/ProofRequest.test.ts b/packages/core/src/modules/proofs/formats/indy/models/__tests__/ProofRequest.test.ts similarity index 87% rename from packages/core/src/modules/proofs/__tests__/ProofRequest.test.ts rename to packages/core/src/modules/proofs/formats/indy/models/__tests__/ProofRequest.test.ts index fbfab93e5f..9d52625ece 100644 --- a/packages/core/src/modules/proofs/__tests__/ProofRequest.test.ts +++ b/packages/core/src/modules/proofs/formats/indy/models/__tests__/ProofRequest.test.ts @@ -1,7 +1,7 @@ -import { ClassValidationError } from '../../../error/ClassValidationError' -import { JsonTransformer } from '../../../utils/JsonTransformer' -import { MessageValidator } from '../../../utils/MessageValidator' -import { ProofRequest } from '../formats/indy/models/ProofRequest' +import { ClassValidationError } from '../../../../../../error/ClassValidationError' +import { JsonTransformer } from '../../../../../../utils/JsonTransformer' +import { MessageValidator } from '../../../../../../utils/MessageValidator' +import { ProofRequest } from '../ProofRequest' describe('ProofRequest', () => { it('should successfully validate if the proof request JSON contains a valid structure', async () => { diff --git a/packages/core/src/modules/proofs/formats/indy/util.ts b/packages/core/src/modules/proofs/formats/indy/util.ts new file mode 100644 index 0000000000..b68e50a6b4 --- /dev/null +++ b/packages/core/src/modules/proofs/formats/indy/util.ts @@ -0,0 +1,252 @@ +import type { V1PresentationPreviewAttributeOptions, V1PresentationPreviewPredicateOptions } from '../../protocol' +import type { default as Indy } from 'indy-sdk' + +import { AriesFrameworkError } from '../../../../error' +import { areObjectsEqual } from '../../../../utils' +import { uuid } from '../../../../utils/uuid' + +import { ProofAttributeInfo, ProofPredicateInfo, ProofRequest } from './models' + +export function createRequestFromPreview({ + name, + version, + nonce, + attributes, + predicates, +}: { + name: string + version: string + nonce: string + attributes: V1PresentationPreviewAttributeOptions[] + predicates: V1PresentationPreviewPredicateOptions[] +}): ProofRequest { + const proofRequest = new ProofRequest({ + name, + version, + nonce, + }) + + /** + * Create mapping of attributes by referent. This required the + * attributes to come from the same credential. + * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0037-present-proof/README.md#referent + * + * { + * "referent1": [Attribute1, Attribute2], + * "referent2": [Attribute3] + * } + */ + const attributesByReferent: Record = {} + for (const proposedAttributes of attributes ?? []) { + if (!proposedAttributes.referent) proposedAttributes.referent = uuid() + + const referentAttributes = attributesByReferent[proposedAttributes.referent] + + // Referent key already exist, add to list + if (referentAttributes) { + referentAttributes.push(proposedAttributes) + } + + // Referent key does not exist yet, create new entry + else { + attributesByReferent[proposedAttributes.referent] = [proposedAttributes] + } + } + + // Transform attributes by referent to requested attributes + for (const [referent, proposedAttributes] of Object.entries(attributesByReferent)) { + // Either attributeName or attributeNames will be undefined + const attributeName = proposedAttributes.length == 1 ? proposedAttributes[0].name : undefined + const attributeNames = proposedAttributes.length > 1 ? proposedAttributes.map((a) => a.name) : undefined + + const requestedAttribute = new ProofAttributeInfo({ + name: attributeName, + names: attributeNames, + restrictions: [ + { + credentialDefinitionId: proposedAttributes[0].credentialDefinitionId, + }, + ], + }) + + proofRequest.requestedAttributes.set(referent, requestedAttribute) + } + + // Transform proposed predicates to requested predicates + for (const proposedPredicate of predicates ?? []) { + const requestedPredicate = new ProofPredicateInfo({ + name: proposedPredicate.name, + predicateType: proposedPredicate.predicate, + predicateValue: proposedPredicate.threshold, + restrictions: [ + { + credentialDefinitionId: proposedPredicate.credentialDefinitionId, + }, + ], + }) + + proofRequest.requestedPredicates.set(uuid(), requestedPredicate) + } + + return proofRequest +} + +/** + * Checks whether two `names` arrays are equal. The order of the names doesn't matter. + */ +function areNamesEqual(namesA: string[] | undefined, namesB: string[] | undefined) { + if (namesA === undefined) return namesB === undefined || namesB.length === 0 + if (namesB === undefined) return namesA.length === 0 + + // Check if there are any duplicates + if (new Set(namesA).size !== namesA.length || new Set(namesB).size !== namesB.length) return false + + // Check if the number of names is equal between A & B + if (namesA.length !== namesB.length) return false + + return namesA.every((a) => namesB.includes(a)) +} + +/** + * Checks whether two proof requests are semantically equal. The `name`, `version` and `nonce`, `ver` fields are ignored. + * In addition the group names don't have to be the same between the different requests. + */ +export function areIndyProofRequestsEqual(requestA: Indy.IndyProofRequest, requestB: Indy.IndyProofRequest): boolean { + // Check if the top-level non-revocation interval is equal + if (!isNonRevokedEqual(requestA.non_revoked, requestB.non_revoked)) return false + + const attributeAList = Object.values(requestA.requested_attributes) + const attributeBList = Object.values(requestB.requested_attributes) + + // Check if the number of attribute groups is equal in both requests + if (attributeAList.length !== attributeBList.length) return false + + // Check if all attribute groups in A are also in B + const attributesMatch = attributeAList.every((a) => { + // find an attribute in B that matches this attribute + const bIndex = attributeBList.findIndex((b) => { + return ( + b.name === a.name && + areNamesEqual(a.names, b.names) && + isNonRevokedEqual(a.non_revoked, b.non_revoked) && + areRestrictionsEqual(a.restrictions, b.restrictions) + ) + }) + + // Match found + if (bIndex !== -1) { + attributeBList.splice(bIndex, 1) + return true + } + + // Match not found + return false + }) + + if (!attributesMatch) return false + + const predicatesA = Object.values(requestA.requested_predicates) + const predicatesB = Object.values(requestB.requested_predicates) + + if (predicatesA.length !== predicatesB.length) return false + const predicatesMatch = predicatesA.every((a) => { + // find a predicate in B that matches this predicate + const bIndex = predicatesB.findIndex((b) => { + return ( + a.name === b.name && + a.p_type === b.p_type && + a.p_value === b.p_value && + isNonRevokedEqual(a.non_revoked, b.non_revoked) && + areRestrictionsEqual(a.restrictions, b.restrictions) + ) + }) + + if (bIndex !== -1) { + predicatesB.splice(bIndex, 1) + return true + } + + return false + }) + + if (!predicatesMatch) return false + + return true +} + +/** + * Checks whether two non-revocation intervals are semantically equal. They are considered equal if: + * - Both are undefined + * - Both are empty objects + * - One if undefined and the other is an empty object + * - Both have the same from and to values + */ +function isNonRevokedEqual( + nonRevokedA: Indy.NonRevokedInterval | undefined, + nonRevokedB: Indy.NonRevokedInterval | undefined +) { + // Having an empty non-revoked object is the same as not having one + if (nonRevokedA === undefined) + return nonRevokedB === undefined || (nonRevokedB.from === undefined && nonRevokedB.to === undefined) + if (nonRevokedB === undefined) return nonRevokedA.from === undefined && nonRevokedA.to === undefined + + return nonRevokedA.from === nonRevokedB.from && nonRevokedA.to === nonRevokedB.to +} + +/** + * Check if two restriction lists are equal. The order of the restrictions does not matter. + */ +function areRestrictionsEqual( + restrictionsA: Indy.WalletQuery[] | undefined, + restrictionsB: Indy.WalletQuery[] | undefined +) { + // Having an undefined restrictions property or an empty array is the same + if (restrictionsA === undefined) return restrictionsB === undefined || restrictionsB.length === 0 + if (restrictionsB === undefined) return restrictionsA.length === 0 + + // Clone array to not modify input object + const bList = [...restrictionsB] + + // Check if all restrictions in A are also in B + return restrictionsA.every((a) => { + const bIndex = restrictionsB.findIndex((b) => areObjectsEqual(a, b)) + + // Match found + if (bIndex !== -1) { + bList.splice(bIndex, 1) + return true + } + + // Match not found + return false + }) +} + +function attributeNamesToArray(proofRequest: ProofRequest) { + // Attributes can contain either a `name` string value or an `names` string array. We reduce it to a single array + // containing all attribute names from the requested attributes. + return Array.from(proofRequest.requestedAttributes.values()).reduce( + (names, a) => [...names, ...(a.name ? [a.name] : a.names ? a.names : [])], + [] + ) +} + +function predicateNamesToArray(proofRequest: ProofRequest) { + return Array.from(new Set(Array.from(proofRequest.requestedPredicates.values()).map((a) => a.name))) +} + +function assertNoDuplicates(predicates: string[], attributeNames: string[]) { + const duplicates = predicates.filter((item) => attributeNames.indexOf(item) !== -1) + if (duplicates.length > 0) { + throw new AriesFrameworkError( + `The proof request contains duplicate predicates and attributes: ${duplicates.toString()}` + ) + } +} + +// TODO: This is still not ideal. The requested groups can specify different credentials using restrictions. +export function assertNoDuplicateGroupsNamesInProofRequest(proofRequest: ProofRequest) { + const attributes = attributeNamesToArray(proofRequest) + const predicates = predicateNamesToArray(proofRequest) + assertNoDuplicates(predicates, attributes) +} diff --git a/packages/core/src/modules/proofs/formats/models/ProofAttachmentFormat.ts b/packages/core/src/modules/proofs/formats/models/ProofAttachmentFormat.ts deleted file mode 100644 index 5bc2fc881b..0000000000 --- a/packages/core/src/modules/proofs/formats/models/ProofAttachmentFormat.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Attachment } from '../../../../decorators/attachment/Attachment' -import type { ProofFormatSpec } from '../../models/ProofFormatSpec' - -export interface ProofAttachmentFormat { - format: ProofFormatSpec - attachment: Attachment -} diff --git a/packages/core/src/modules/proofs/formats/models/ProofFormatServiceOptions.ts b/packages/core/src/modules/proofs/formats/models/ProofFormatServiceOptions.ts deleted file mode 100644 index 8859c48b64..0000000000 --- a/packages/core/src/modules/proofs/formats/models/ProofFormatServiceOptions.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { ProofAttachmentFormat } from './ProofAttachmentFormat' -import type { Attachment } from '../../../../decorators/attachment/Attachment' -import type { ProposeProofFormats } from '../../models/SharedOptions' -import type { ProofExchangeRecord } from '../../repository' -import type { ProofFormat, ProofFormatPayload } from '../ProofFormat' -import type { ProofRequestOptions } from '../indy/models/ProofRequest' - -export interface CreateRequestAttachmentOptions { - id?: string - proofRequestOptions: ProofRequestOptions -} - -export interface CreateProofAttachmentOptions { - id?: string - proofProposalOptions: ProofRequestOptions -} - -export interface FormatCreateProofProposalOptions { - id?: string - formats: ProposeProofFormats -} - -export interface ProcessProposalOptions { - proposal: ProofAttachmentFormat - record?: ProofExchangeRecord -} - -export interface CreateRequestOptions { - id?: string - formats: ProposeProofFormats -} - -export interface ProcessRequestOptions { - requestAttachment: ProofAttachmentFormat - record?: ProofExchangeRecord -} - -export interface FormatCreatePresentationOptions { - id?: string - attachment: Attachment - proofFormats: ProofFormatPayload<[PF], 'createPresentation'> -} - -export interface ProcessPresentationOptions { - record: ProofExchangeRecord - formatAttachments: { - request: ProofAttachmentFormat[] - presentation: ProofAttachmentFormat[] - } -} - -export interface VerifyProofOptions { - request: Attachment - proof: Attachment -} - -export interface CreateProblemReportOptions { - proofRecord: ProofExchangeRecord - description: string -} - -export interface CreatePresentationFormatsOptions { - presentationAttachment: Attachment -} diff --git a/packages/core/src/modules/proofs/formats/models/index.ts b/packages/core/src/modules/proofs/formats/models/index.ts deleted file mode 100644 index 968a6b53ee..0000000000 --- a/packages/core/src/modules/proofs/formats/models/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './ProofAttachmentFormat' -export * from './ProofFormatServiceOptions' diff --git a/packages/core/src/modules/proofs/index.ts b/packages/core/src/modules/proofs/index.ts index 5d4f5f16c7..30eb44ba0f 100644 --- a/packages/core/src/modules/proofs/index.ts +++ b/packages/core/src/modules/proofs/index.ts @@ -1,13 +1,14 @@ export * from './errors' export * from './formats' -export * from './messages' export * from './models' export * from './protocol' export * from './repository' export * from './ProofEvents' -export * from './ProofResponseCoordinator' + +// Api export * from './ProofsApi' export * from './ProofsApiOptions' -export * from './ProofService' + +// Module export * from './ProofsModule' export * from './ProofsModuleConfig' diff --git a/packages/core/src/modules/proofs/messages/PresentationAckMessage.ts b/packages/core/src/modules/proofs/messages/PresentationAckMessage.ts deleted file mode 100644 index 64e60f56b2..0000000000 --- a/packages/core/src/modules/proofs/messages/PresentationAckMessage.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { ProtocolVersion } from '../../../types' -import type { AckMessageOptions } from '../../common' - -export type PresentationAckMessageOptions = AckMessageOptions - -type PresentationAckMessageType = `https://didcomm.org/present-proof/${ProtocolVersion}/ack` - -export interface PresentationAckMessage { - type: PresentationAckMessageType -} diff --git a/packages/core/src/modules/proofs/messages/index.ts b/packages/core/src/modules/proofs/messages/index.ts deleted file mode 100644 index 1f395b2d57..0000000000 --- a/packages/core/src/modules/proofs/messages/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './PresentationAckMessage' diff --git a/packages/core/src/modules/proofs/models/GetRequestedCredentialsConfig.ts b/packages/core/src/modules/proofs/models/GetRequestedCredentialsConfig.ts deleted file mode 100644 index 9041bbabe3..0000000000 --- a/packages/core/src/modules/proofs/models/GetRequestedCredentialsConfig.ts +++ /dev/null @@ -1,19 +0,0 @@ -export interface GetRequestedCredentialsConfig { - /** - * Whether to filter the retrieved credentials using the presentation preview. - * This configuration will only have effect if a presentation proposal message is available - * containing a presentation preview. - * - * @default false - */ - filterByPresentationPreview?: boolean - - /** - * Whether to filter the retrieved credentials using the non-revocation request in the proof request. - * This configuration will only have effect if the proof request requires proof on non-revocation of any kind. - * Default to true - * - * @default true - */ - filterByNonRevocationRequirements?: boolean -} diff --git a/packages/core/src/modules/proofs/models/ModuleOptions.ts b/packages/core/src/modules/proofs/models/ModuleOptions.ts deleted file mode 100644 index e471a243db..0000000000 --- a/packages/core/src/modules/proofs/models/ModuleOptions.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { GetRequestedCredentialsConfig } from './GetRequestedCredentialsConfig' -import type { AutoAcceptProof } from './ProofAutoAcceptType' -import type { ProposeProofFormats } from './SharedOptions' - -export interface ProofConfig { - name: string - version: string -} - -export interface NegotiateRequestOptions { - proofRecordId: string - proofFormats: ProposeProofFormats - comment?: string - autoAcceptProof?: AutoAcceptProof -} - -export interface AutoSelectCredentialsForProofRequestOptions { - proofRecordId: string - config?: GetRequestedCredentialsConfig -} - -export type GetRequestedCredentialsForProofRequest = AutoSelectCredentialsForProofRequestOptions diff --git a/packages/core/src/modules/proofs/models/ProofServiceOptions.ts b/packages/core/src/modules/proofs/models/ProofServiceOptions.ts deleted file mode 100644 index 1a978404eb..0000000000 --- a/packages/core/src/modules/proofs/models/ProofServiceOptions.ts +++ /dev/null @@ -1,85 +0,0 @@ -import type { GetRequestedCredentialsConfig } from './GetRequestedCredentialsConfig' -import type { AutoAcceptProof } from './ProofAutoAcceptType' -import type { ConnectionRecord } from '../../connections' -import type { ProofFormat, ProofFormatPayload } from '../formats/ProofFormat' -import type { ProofExchangeRecord } from '../repository' - -export type FormatDataMessagePayload< - CFs extends ProofFormat[] = ProofFormat[], - M extends keyof ProofFormat['formatData'] = keyof ProofFormat['formatData'] -> = { - [ProofFormat in CFs[number] as ProofFormat['formatKey']]?: ProofFormat['formatData'][M] -} - -interface BaseOptions { - willConfirm?: boolean - goalCode?: string - comment?: string - autoAcceptProof?: AutoAcceptProof -} - -export interface CreateProposalOptions extends BaseOptions { - connectionRecord: ConnectionRecord - proofFormats: ProofFormatPayload - parentThreadId?: string -} - -export interface CreateProposalAsResponseOptions extends BaseOptions { - proofRecord: ProofExchangeRecord - proofFormats: ProofFormatPayload -} - -export interface CreateRequestAsResponseOptions extends BaseOptions { - id?: string - proofRecord: ProofExchangeRecord - proofFormats: ProofFormatPayload -} - -export interface CreateRequestOptions extends BaseOptions { - connectionRecord?: ConnectionRecord - proofFormats: ProofFormatPayload - parentThreadId?: string -} - -export interface CreateProofRequestFromProposalOptions extends BaseOptions { - id?: string - proofRecord: ProofExchangeRecord -} - -export interface FormatRetrievedCredentialOptions { - proofFormats: ProofFormatPayload -} - -export interface FormatRequestedCredentialReturn { - proofFormats: ProofFormatPayload -} - -export interface CreatePresentationOptions extends BaseOptions { - proofRecord: ProofExchangeRecord - proofFormats: ProofFormatPayload // - lastPresentation?: boolean -} - -export interface CreateAckOptions { - proofRecord: ProofExchangeRecord -} - -export interface GetRequestedCredentialsForProofRequestOptions { - proofRecord: ProofExchangeRecord - config?: GetRequestedCredentialsConfig -} - -export interface ProofRequestFromProposalOptions { - proofRecord: ProofExchangeRecord - proofFormats: ProofFormatPayload -} - -export interface DeleteProofOptions { - deleteAssociatedDidCommMessages?: boolean -} - -export type GetFormatDataReturn = { - proposal?: FormatDataMessagePayload - request?: FormatDataMessagePayload - presentation?: FormatDataMessagePayload -} diff --git a/packages/core/src/modules/proofs/models/SharedOptions.ts b/packages/core/src/modules/proofs/models/SharedOptions.ts deleted file mode 100644 index 18fe5ef7f3..0000000000 --- a/packages/core/src/modules/proofs/models/SharedOptions.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { GetRequestedCredentialsConfig } from './GetRequestedCredentialsConfig' -import type { IndyProposeProofFormat } from '../formats/indy/IndyProofFormat' -import type { IndyRequestProofFormat, IndyVerifyProofFormat } from '../formats/indy/IndyProofFormatsServiceOptions' -import type { ProofRequest } from '../formats/indy/models/ProofRequest' -import type { IndyRequestedCredentialsOptions } from '../formats/indy/models/RequestedCredentials' -import type { RetrievedCredentials } from '../formats/indy/models/RetrievedCredentials' - -export interface ProposeProofFormats { - // If you want to propose an indy proof without attributes or - // any of the other properties you should pass an empty object - indy?: IndyProposeProofFormat - presentationExchange?: never -} - -export interface RequestProofFormats { - indy?: IndyRequestProofFormat - presentationExchange?: never -} - -export interface CreatePresentationFormats { - indy?: IndyRequestedCredentialsOptions - presentationExchange?: never -} - -export interface AcceptProposalFormats { - indy?: IndyAcceptProposalOptions - presentationExchange?: never -} - -export interface VerifyProofFormats { - indy?: IndyVerifyProofFormat - presentationExchange?: never -} - -export interface RequestedCredentialConfigOptions { - indy?: GetRequestedCredentialsConfig - presentationExchange?: never -} - -// export interface RetrievedCredentialOptions { -// indy?: RetrievedCredentials -// presentationExchange?: undefined -// } - -export interface ProofRequestFormats { - indy?: ProofRequest - presentationExchange?: undefined -} - -// export interface RequestedCredentialsFormats { -// indy?: RequestedCredentials -// presentationExchange?: undefined -// } - -interface IndyAcceptProposalOptions { - request: ProofRequest -} - -export interface AutoSelectCredentialOptions { - indy?: RetrievedCredentials - presentationExchange?: undefined -} diff --git a/packages/core/src/modules/proofs/__tests__/ProofState.test.ts b/packages/core/src/modules/proofs/models/__tests__/ProofState.test.ts similarity index 92% rename from packages/core/src/modules/proofs/__tests__/ProofState.test.ts rename to packages/core/src/modules/proofs/models/__tests__/ProofState.test.ts index 4b67ed11d0..9cabafd183 100644 --- a/packages/core/src/modules/proofs/__tests__/ProofState.test.ts +++ b/packages/core/src/modules/proofs/models/__tests__/ProofState.test.ts @@ -1,4 +1,4 @@ -import { ProofState } from '../models/ProofState' +import { ProofState } from '../ProofState' describe('ProofState', () => { test('state matches Present Proof 1.0 (RFC 0037) state value', () => { diff --git a/packages/core/src/modules/proofs/models/index.ts b/packages/core/src/modules/proofs/models/index.ts index a092a0ae7e..9e20094e5e 100644 --- a/packages/core/src/modules/proofs/models/index.ts +++ b/packages/core/src/modules/proofs/models/index.ts @@ -1,3 +1,2 @@ -export * from './GetRequestedCredentialsConfig' export * from './ProofAutoAcceptType' export * from './ProofState' diff --git a/packages/core/src/modules/proofs/protocol/BaseProofProtocol.ts b/packages/core/src/modules/proofs/protocol/BaseProofProtocol.ts new file mode 100644 index 0000000000..9e4e9e8b1c --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/BaseProofProtocol.ts @@ -0,0 +1,286 @@ +import type { ProofProtocol } from './ProofProtocol' +import type { + CreateProofProposalOptions, + CreateProofRequestOptions, + DeleteProofOptions, + GetProofFormatDataReturn, + CreateProofProblemReportOptions, + ProofProtocolMsgReturnType, + AcceptPresentationOptions, + AcceptProofProposalOptions, + AcceptProofRequestOptions, + GetCredentialsForRequestOptions, + GetCredentialsForRequestReturn, + NegotiateProofProposalOptions, + NegotiateProofRequestOptions, + SelectCredentialsForRequestOptions, + SelectCredentialsForRequestReturn, +} from './ProofProtocolOptions' +import type { AgentMessage } from '../../../agent/AgentMessage' +import type { FeatureRegistry } from '../../../agent/FeatureRegistry' +import type { AgentContext } from '../../../agent/context/AgentContext' +import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext' +import type { DependencyManager } from '../../../plugins' +import type { Query } from '../../../storage/StorageService' +import type { ProblemReportMessage } from '../../problem-reports' +import type { ProofStateChangedEvent } from '../ProofEvents' +import type { ExtractProofFormats, ProofFormatService } from '../formats' +import type { ProofExchangeRecord } from '../repository' + +import { EventEmitter } from '../../../agent/EventEmitter' +import { DidCommMessageRepository } from '../../../storage' +import { JsonTransformer } from '../../../utils/JsonTransformer' +import { ProofEventTypes } from '../ProofEvents' +import { ProofState } from '../models/ProofState' +import { ProofRepository } from '../repository' + +export abstract class BaseProofProtocol + implements ProofProtocol +{ + public abstract readonly version: string + + public abstract register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry): void + + // methods for proposal + public abstract createProposal( + agentContext: AgentContext, + options: CreateProofProposalOptions + ): Promise> + public abstract processProposal(messageContext: InboundMessageContext): Promise + public abstract acceptProposal( + agentContext: AgentContext, + options: AcceptProofProposalOptions + ): Promise> + public abstract negotiateProposal( + agentContext: AgentContext, + options: NegotiateProofProposalOptions + ): Promise> + + // methods for request + public abstract createRequest( + agentContext: AgentContext, + options: CreateProofRequestOptions + ): Promise> + public abstract processRequest(messageContext: InboundMessageContext): Promise + public abstract acceptRequest( + agentContext: AgentContext, + options: AcceptProofRequestOptions + ): Promise> + public abstract negotiateRequest( + agentContext: AgentContext, + options: NegotiateProofRequestOptions + ): Promise> + + // retrieving credentials for request + public abstract getCredentialsForRequest( + agentContext: AgentContext, + options: GetCredentialsForRequestOptions + ): Promise> + public abstract selectCredentialsForRequest( + agentContext: AgentContext, + options: SelectCredentialsForRequestOptions + ): Promise> + + // methods for presentation + public abstract processPresentation(messageContext: InboundMessageContext): Promise + public abstract acceptPresentation( + agentContext: AgentContext, + options: AcceptPresentationOptions + ): Promise> + + // methods for ack + public abstract processAck(messageContext: InboundMessageContext): Promise + // method for problem report + public abstract createProblemReport( + agentContext: AgentContext, + options: CreateProofProblemReportOptions + ): Promise> + + public abstract findProposalMessage(agentContext: AgentContext, proofExchangeId: string): Promise + public abstract findRequestMessage(agentContext: AgentContext, proofExchangeId: string): Promise + public abstract findPresentationMessage( + agentContext: AgentContext, + proofExchangeId: string + ): Promise + public abstract getFormatData( + agentContext: AgentContext, + proofExchangeId: string + ): Promise>> + + public async processProblemReport( + messageContext: InboundMessageContext + ): Promise { + const { message: proofProblemReportMessage, agentContext } = messageContext + + const connection = messageContext.assertReadyConnection() + + agentContext.config.logger.debug(`Processing problem report with message id ${proofProblemReportMessage.id}`) + + const proofRecord = await this.getByThreadAndConnectionId( + agentContext, + proofProblemReportMessage.threadId, + connection.id + ) + + // Update record + proofRecord.errorMessage = `${proofProblemReportMessage.description.code}: ${proofProblemReportMessage.description.en}` + await this.updateState(agentContext, proofRecord, ProofState.Abandoned) + return proofRecord + } + + /** + * Update the record to a new state and emit an state changed event. Also updates the record + * in storage. + * + * @param proofRecord The proof record to update the state for + * @param newState The state to update to + * + */ + public async updateState(agentContext: AgentContext, proofRecord: ProofExchangeRecord, newState: ProofState) { + const proofRepository = agentContext.dependencyManager.resolve(ProofRepository) + + agentContext.config.logger.debug( + `Updating proof record ${proofRecord.id} to state ${newState} (previous=${proofRecord.state})` + ) + + const previousState = proofRecord.state + proofRecord.state = newState + await proofRepository.update(agentContext, proofRecord) + + this.emitStateChangedEvent(agentContext, proofRecord, previousState) + } + + protected emitStateChangedEvent( + agentContext: AgentContext, + proofRecord: ProofExchangeRecord, + previousState: ProofState | null + ) { + const eventEmitter = agentContext.dependencyManager.resolve(EventEmitter) + + const clonedProof = JsonTransformer.clone(proofRecord) + + eventEmitter.emit(agentContext, { + type: ProofEventTypes.ProofStateChanged, + payload: { + proofRecord: clonedProof, + previousState: previousState, + }, + }) + } + + /** + * Retrieve a proof record by id + * + * @param proofRecordId The proof record id + * @throws {RecordNotFoundError} If no record is found + * @return The proof record + * + */ + public getById(agentContext: AgentContext, proofRecordId: string): Promise { + const proofRepository = agentContext.dependencyManager.resolve(ProofRepository) + + return proofRepository.getById(agentContext, proofRecordId) + } + + /** + * Retrieve all proof records + * + * @returns List containing all proof records + */ + public getAll(agentContext: AgentContext): Promise { + const proofRepository = agentContext.dependencyManager.resolve(ProofRepository) + + return proofRepository.getAll(agentContext) + } + + public async findAllByQuery( + agentContext: AgentContext, + query: Query + ): Promise { + const proofRepository = agentContext.dependencyManager.resolve(ProofRepository) + + return proofRepository.findByQuery(agentContext, query) + } + + /** + * Find a proof record by id + * + * @param proofRecordId the proof record id + * @returns The proof record or null if not found + */ + public findById(agentContext: AgentContext, proofRecordId: string): Promise { + const proofRepository = agentContext.dependencyManager.resolve(ProofRepository) + + return proofRepository.findById(agentContext, proofRecordId) + } + + public async delete( + agentContext: AgentContext, + proofRecord: ProofExchangeRecord, + options?: DeleteProofOptions + ): Promise { + const proofRepository = agentContext.dependencyManager.resolve(ProofRepository) + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + await proofRepository.delete(agentContext, proofRecord) + + const deleteAssociatedDidCommMessages = options?.deleteAssociatedDidCommMessages ?? true + + if (deleteAssociatedDidCommMessages) { + const didCommMessages = await didCommMessageRepository.findByQuery(agentContext, { + associatedRecordId: proofRecord.id, + }) + for (const didCommMessage of didCommMessages) { + await didCommMessageRepository.delete(agentContext, didCommMessage) + } + } + } + + /** + * Retrieve a proof record by connection id and thread id + * + * @param connectionId The connection id + * @param threadId The thread id + * @throws {RecordNotFoundError} If no record is found + * @throws {RecordDuplicateError} If multiple records are found + * @returns The proof record + */ + public getByThreadAndConnectionId( + agentContext: AgentContext, + threadId: string, + connectionId?: string + ): Promise { + const proofRepository = agentContext.dependencyManager.resolve(ProofRepository) + + return proofRepository.getSingleByQuery(agentContext, { + connectionId, + threadId, + }) + } + + /** + * Find a proof record by connection id and thread id, returns null if not found + * + * @param connectionId The connection id + * @param threadId The thread id + * @returns The proof record + */ + public findByThreadAndConnectionId( + agentContext: AgentContext, + threadId: string, + connectionId?: string + ): Promise { + const proofRepository = agentContext.dependencyManager.resolve(ProofRepository) + + return proofRepository.findSingleByQuery(agentContext, { + connectionId, + threadId, + }) + } + + public async update(agentContext: AgentContext, proofRecord: ProofExchangeRecord) { + const proofRepository = agentContext.dependencyManager.resolve(ProofRepository) + + return await proofRepository.update(agentContext, proofRecord) + } +} diff --git a/packages/core/src/modules/proofs/protocol/ProofProtocol.ts b/packages/core/src/modules/proofs/protocol/ProofProtocol.ts new file mode 100644 index 0000000000..2065d97b35 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/ProofProtocol.ts @@ -0,0 +1,117 @@ +import type { + CreateProofProposalOptions, + CreateProofRequestOptions, + DeleteProofOptions, + GetProofFormatDataReturn, + CreateProofProblemReportOptions, + ProofProtocolMsgReturnType, + AcceptProofProposalOptions, + NegotiateProofProposalOptions, + AcceptProofRequestOptions, + NegotiateProofRequestOptions, + AcceptPresentationOptions, + GetCredentialsForRequestOptions, + GetCredentialsForRequestReturn, + SelectCredentialsForRequestOptions, + SelectCredentialsForRequestReturn, +} from './ProofProtocolOptions' +import type { AgentMessage } from '../../../agent/AgentMessage' +import type { FeatureRegistry } from '../../../agent/FeatureRegistry' +import type { AgentContext } from '../../../agent/context/AgentContext' +import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext' +import type { DependencyManager } from '../../../plugins' +import type { Query } from '../../../storage/StorageService' +import type { ProblemReportMessage } from '../../problem-reports' +import type { ExtractProofFormats, ProofFormatService } from '../formats' +import type { ProofState } from '../models/ProofState' +import type { ProofExchangeRecord } from '../repository' + +export interface ProofProtocol { + readonly version: string + + // methods for proposal + createProposal( + agentContext: AgentContext, + options: CreateProofProposalOptions + ): Promise> + processProposal(messageContext: InboundMessageContext): Promise + acceptProposal( + agentContext: AgentContext, + options: AcceptProofProposalOptions + ): Promise> + negotiateProposal( + agentContext: AgentContext, + options: NegotiateProofProposalOptions + ): Promise> + + // methods for request + createRequest( + agentContext: AgentContext, + options: CreateProofRequestOptions + ): Promise> + processRequest(messageContext: InboundMessageContext): Promise + acceptRequest( + agentContext: AgentContext, + options: AcceptProofRequestOptions + ): Promise> + negotiateRequest( + agentContext: AgentContext, + options: NegotiateProofRequestOptions + ): Promise> + + // retrieving credentials for request + getCredentialsForRequest( + agentContext: AgentContext, + options: GetCredentialsForRequestOptions + ): Promise> + selectCredentialsForRequest( + agentContext: AgentContext, + options: SelectCredentialsForRequestOptions + ): Promise> + + // methods for presentation + processPresentation(messageContext: InboundMessageContext): Promise + acceptPresentation( + agentContext: AgentContext, + options: AcceptPresentationOptions + ): Promise> + + // methods for ack + processAck(messageContext: InboundMessageContext): Promise + + // method for problem report + createProblemReport( + agentContext: AgentContext, + options: CreateProofProblemReportOptions + ): Promise> + processProblemReport(messageContext: InboundMessageContext): Promise + + findProposalMessage(agentContext: AgentContext, proofExchangeId: string): Promise + findRequestMessage(agentContext: AgentContext, proofExchangeId: string): Promise + findPresentationMessage(agentContext: AgentContext, proofExchangeId: string): Promise + getFormatData( + agentContext: AgentContext, + proofExchangeId: string + ): Promise>> + + // repository methods + updateState(agentContext: AgentContext, proofRecord: ProofExchangeRecord, newState: ProofState): Promise + getById(agentContext: AgentContext, proofExchangeId: string): Promise + getAll(agentContext: AgentContext): Promise + findAllByQuery(agentContext: AgentContext, query: Query): Promise + findById(agentContext: AgentContext, proofExchangeId: string): Promise + delete(agentContext: AgentContext, proofRecord: ProofExchangeRecord, options?: DeleteProofOptions): Promise + getByThreadAndConnectionId( + agentContext: AgentContext, + threadId: string, + connectionId?: string + ): Promise + findByThreadAndConnectionId( + agentContext: AgentContext, + threadId: string, + connectionId?: string + ): Promise + update(agentContext: AgentContext, proofRecord: ProofExchangeRecord): Promise + + register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry): void +} diff --git a/packages/core/src/modules/proofs/protocol/ProofProtocolOptions.ts b/packages/core/src/modules/proofs/protocol/ProofProtocolOptions.ts new file mode 100644 index 0000000000..43276bfeee --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/ProofProtocolOptions.ts @@ -0,0 +1,165 @@ +import type { ProofProtocol } from './ProofProtocol' +import type { AgentMessage } from '../../../agent/AgentMessage' +import type { ConnectionRecord } from '../../connections' +import type { + ExtractProofFormats, + ProofFormat, + ProofFormatCredentialForRequestPayload, + ProofFormatPayload, + ProofFormatService, +} from '../formats' +import type { AutoAcceptProof } from '../models' +import type { ProofExchangeRecord } from '../repository' + +/** + * Get the format data payload for a specific message from a list of ProofFormat interfaces and a message + * + * For an indy offer, this resolves to the proof request format as defined here: + * https://github.com/hyperledger/aries-rfcs/tree/b3a3942ef052039e73cd23d847f42947f8287da2/features/0592-indy-attachments#proof-request-format + * + * @example + * ``` + * + * type RequestFormatData = FormatDataMessagePayload<[IndyProofFormat, PresentationExchangeProofFormat], 'createRequest'> + * + * // equal to + * type RequestFormatData = { + * indy: { + * // ... payload for indy proof request attachment as defined in RFC 0592 ... + * }, + * presentationExchange: { + * // ... payload for presentation exchange request attachment as defined in RFC 0510 ... + * } + * } + * ``` + */ +export type ProofFormatDataMessagePayload< + CFs extends ProofFormat[] = ProofFormat[], + M extends keyof ProofFormat['formatData'] = keyof ProofFormat['formatData'] +> = { + [ProofFormat in CFs[number] as ProofFormat['formatKey']]?: ProofFormat['formatData'][M] +} + +/** + * Infer the {@link ProofFormat} types based on an array of {@link ProofProtocol} types. + * + * It does this by extracting the `ProofFormatServices` generic from the `ProofProtocol`, and + * then extracting the `ProofFormat` generic from each of the `ProofFormatService` types. + * + * @example + * ``` + * // TheProofFormatServices is now equal to [IndyProofFormatService] + * type TheProofFormatServices = ProofFormatsFromProtocols<[V1ProofProtocol]> + * ``` + * + * Because the `V1ProofProtocol` is defined as follows: + * ``` + * class V1ProofProtocol implements ProofProtocol<[IndyProofFormatService]> { + * } + * ``` + */ +export type ProofFormatsFromProtocols = Type[number] extends ProofProtocol< + infer ProofFormatServices +> + ? ProofFormatServices extends ProofFormatService[] + ? ExtractProofFormats + : never + : never + +export type GetProofFormatDataReturn = { + proposal?: ProofFormatDataMessagePayload + request?: ProofFormatDataMessagePayload + presentation?: ProofFormatDataMessagePayload +} + +interface BaseOptions { + goalCode?: string + comment?: string + autoAcceptProof?: AutoAcceptProof +} + +export interface CreateProofProposalOptions extends BaseOptions { + connectionRecord: ConnectionRecord + proofFormats: ProofFormatPayload, 'createProposal'> + parentThreadId?: string +} + +export interface AcceptProofProposalOptions extends BaseOptions { + proofRecord: ProofExchangeRecord + proofFormats?: ProofFormatPayload, 'acceptProposal'> + + /** @default true */ + willConfirm?: boolean +} + +export interface NegotiateProofProposalOptions extends BaseOptions { + proofRecord: ProofExchangeRecord + proofFormats: ProofFormatPayload, 'createRequest'> + + /** @default true */ + willConfirm?: boolean +} + +export interface CreateProofRequestOptions extends BaseOptions { + // Create request can also be used for connection-less, so connection is optional + connectionRecord?: ConnectionRecord + proofFormats: ProofFormatPayload, 'createRequest'> + parentThreadId?: string + + /** @default true */ + willConfirm?: boolean +} + +export interface AcceptProofRequestOptions extends BaseOptions { + proofRecord: ProofExchangeRecord + proofFormats?: ProofFormatPayload, 'acceptRequest'> +} + +export interface NegotiateProofRequestOptions extends BaseOptions { + proofRecord: ProofExchangeRecord + proofFormats: ProofFormatPayload, 'createProposal'> +} + +export interface GetCredentialsForRequestOptions { + proofRecord: ProofExchangeRecord + proofFormats?: ProofFormatCredentialForRequestPayload, 'getCredentialsForRequest', 'input'> +} + +export interface GetCredentialsForRequestReturn { + proofFormats: ProofFormatCredentialForRequestPayload, 'getCredentialsForRequest', 'output'> +} + +export interface SelectCredentialsForRequestOptions { + proofRecord: ProofExchangeRecord + proofFormats?: ProofFormatCredentialForRequestPayload< + ExtractProofFormats, + 'selectCredentialsForRequest', + 'input' + > +} + +export interface SelectCredentialsForRequestReturn { + proofFormats: ProofFormatCredentialForRequestPayload< + ExtractProofFormats, + 'selectCredentialsForRequest', + 'output' + > +} + +export interface AcceptPresentationOptions { + proofRecord: ProofExchangeRecord +} + +export interface CreateProofProblemReportOptions { + proofRecord: ProofExchangeRecord + description: string +} + +export interface ProofProtocolMsgReturnType { + message: MessageType + proofRecord: ProofExchangeRecord +} + +export interface DeleteProofOptions { + deleteAssociatedDidCommMessages?: boolean +} diff --git a/packages/core/src/modules/proofs/protocol/index.ts b/packages/core/src/modules/proofs/protocol/index.ts index 4d9da63573..db72d7287c 100644 --- a/packages/core/src/modules/proofs/protocol/index.ts +++ b/packages/core/src/modules/proofs/protocol/index.ts @@ -1,2 +1,6 @@ export * from './v1' export * from './v2' +export { ProofProtocol } from './ProofProtocol' +import * as ProofProtocolOptions from './ProofProtocolOptions' + +export { ProofProtocolOptions } diff --git a/packages/core/src/modules/proofs/protocol/v1/V1ProofProtocol.ts b/packages/core/src/modules/proofs/protocol/v1/V1ProofProtocol.ts new file mode 100644 index 0000000000..5461aa8ebc --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v1/V1ProofProtocol.ts @@ -0,0 +1,1111 @@ +import type { AgentContext } from '../../../../agent' +import type { AgentMessage } from '../../../../agent/AgentMessage' +import type { FeatureRegistry } from '../../../../agent/FeatureRegistry' +import type { InboundMessageContext } from '../../../../agent/models/InboundMessageContext' +import type { DependencyManager } from '../../../../plugins' +import type { ProblemReportMessage } from '../../../problem-reports' +import type { ProofFormatService } from '../../formats' +import type { ProofFormat } from '../../formats/ProofFormat' +import type { IndyProofFormat } from '../../formats/indy/IndyProofFormat' +import type { ProofProtocol } from '../ProofProtocol' +import type { + AcceptPresentationOptions, + AcceptProofProposalOptions, + AcceptProofRequestOptions, + CreateProofProblemReportOptions, + CreateProofProposalOptions, + CreateProofRequestOptions, + GetCredentialsForRequestOptions, + GetCredentialsForRequestReturn, + GetProofFormatDataReturn, + NegotiateProofProposalOptions, + NegotiateProofRequestOptions, + ProofProtocolMsgReturnType, + SelectCredentialsForRequestOptions, + SelectCredentialsForRequestReturn, +} from '../ProofProtocolOptions' + +import { Protocol } from '../../../../agent/models' +import { Attachment } from '../../../../decorators/attachment/Attachment' +import { AriesFrameworkError } from '../../../../error/AriesFrameworkError' +import { DidCommMessageRepository, DidCommMessageRole } from '../../../../storage' +import { JsonEncoder } from '../../../../utils' +import { JsonTransformer } from '../../../../utils/JsonTransformer' +import { MessageValidator } from '../../../../utils/MessageValidator' +import { uuid } from '../../../../utils/uuid' +import { AckStatus } from '../../../common/messages/AckMessage' +import { ConnectionService } from '../../../connections' +import { ProofsModuleConfig } from '../../ProofsModuleConfig' +import { PresentationProblemReportReason } from '../../errors/PresentationProblemReportReason' +import { createRequestFromPreview } from '../../formats/indy/util' +import { AutoAcceptProof } from '../../models' +import { ProofState } from '../../models/ProofState' +import { ProofRepository } from '../../repository' +import { ProofExchangeRecord } from '../../repository/ProofExchangeRecord' +import { composeAutoAccept } from '../../utils/composeAutoAccept' +import { BaseProofProtocol } from '../BaseProofProtocol' + +import { V1PresentationProblemReportError } from './errors' +import { + V1PresentationAckHandler, + V1PresentationHandler, + V1PresentationProblemReportHandler, + V1ProposePresentationHandler, + V1RequestPresentationHandler, +} from './handlers' +import { + INDY_PROOF_ATTACHMENT_ID, + INDY_PROOF_REQUEST_ATTACHMENT_ID, + V1PresentationAckMessage, + V1PresentationMessage, + V1ProposePresentationMessage, + V1RequestPresentationMessage, +} from './messages' +import { V1PresentationProblemReportMessage } from './messages/V1PresentationProblemReportMessage' +import { V1PresentationPreview } from './models/V1PresentationPreview' + +type IndyProofFormatServiceLike = ProofFormatService + +export interface V1ProofProtocolConfig { + // indyCredentialFormat must be a service that implements the `IndyProofFormat` interface, however it doesn't + // have to be the IndyProofFormatService implementation per se. + indyProofFormat: ProofFormatService +} + +export class V1ProofProtocol extends BaseProofProtocol implements ProofProtocol<[IndyProofFormatServiceLike]> { + private indyProofFormat: ProofFormatService + + public constructor({ indyProofFormat }: V1ProofProtocolConfig) { + super() + + this.indyProofFormat = indyProofFormat + } + + /** + * The version of the present proof protocol this protocol supports + */ + public readonly version = 'v1' as const + + /** + * Registers the protocol implementation (handlers, feature registry) on the agent. + */ + public register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry) { + // Register message handlers for the Issue Credential V1 Protocol + dependencyManager.registerMessageHandlers([ + new V1ProposePresentationHandler(this), + new V1RequestPresentationHandler(this), + new V1PresentationHandler(this), + new V1PresentationAckHandler(this), + new V1PresentationProblemReportHandler(this), + ]) + + // Register Present Proof V1 in feature registry, with supported roles + featureRegistry.register( + new Protocol({ + id: 'https://didcomm.org/present-proof/1.0', + roles: ['prover', 'verifier'], + }) + ) + } + + public async createProposal( + agentContext: AgentContext, + { + proofFormats, + connectionRecord, + comment, + parentThreadId, + autoAcceptProof, + }: CreateProofProposalOptions<[IndyProofFormatServiceLike]> + ): Promise> { + this.assertOnlyIndyFormat(proofFormats) + + const proofRepository = agentContext.dependencyManager.resolve(ProofRepository) + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + if (!proofFormats.indy) { + throw new AriesFrameworkError('Missing indy proof format in v1 create proposal call.') + } + + const presentationProposal = new V1PresentationPreview({ + attributes: proofFormats.indy?.attributes, + predicates: proofFormats.indy?.predicates, + }) + + // validate input data from user + MessageValidator.validateSync(presentationProposal) + + // Create message + const message = new V1ProposePresentationMessage({ + presentationProposal, + comment, + }) + + if (parentThreadId) + message.setThread({ + parentThreadId, + }) + + // Create record + const proofRecord = new ProofExchangeRecord({ + connectionId: connectionRecord.id, + threadId: message.threadId, + parentThreadId: message.thread?.parentThreadId, + state: ProofState.ProposalSent, + autoAcceptProof, + protocolVersion: 'v1', + }) + + await didCommMessageRepository.saveAgentMessage(agentContext, { + agentMessage: message, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Sender, + }) + + await proofRepository.save(agentContext, proofRecord) + this.emitStateChangedEvent(agentContext, proofRecord, null) + + return { proofRecord, message } + } + + public async processProposal( + messageContext: InboundMessageContext + ): Promise { + const { message: proposalMessage, connection, agentContext } = messageContext + + const proofRepository = agentContext.dependencyManager.resolve(ProofRepository) + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // TODO: with this method, we should update the credential protocol to use the ConnectionApi, so it + // only depends on the public api, rather than the internal API (this helps with breaking changes) + const connectionService = agentContext.dependencyManager.resolve(ConnectionService) + + agentContext.config.logger.debug(`Processing presentation proposal with message id ${proposalMessage.id}`) + + let proofRecord = await this.findByThreadAndConnectionId(agentContext, proposalMessage.threadId, connection?.id) + + // Proof record already exists, this is a response to an earlier message sent by us + if (proofRecord) { + agentContext.config.logger.debug('Proof record already exists for incoming proposal') + + // Assert + proofRecord.assertState(ProofState.RequestSent) + proofRecord.assertProtocolVersion('v1') + + const previousReceivedMessage = await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V1ProposePresentationMessage, + }) + const previousSentMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V1RequestPresentationMessage, + }) + connectionService.assertConnectionOrServiceDecorator(messageContext, { + previousReceivedMessage, + previousSentMessage, + }) + + // Update record + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: proposalMessage, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Receiver, + }) + await this.updateState(agentContext, proofRecord, ProofState.ProposalReceived) + } else { + agentContext.config.logger.debug('Proof record does not exists yet for incoming proposal') + + // No proof record exists with thread id + proofRecord = new ProofExchangeRecord({ + connectionId: connection?.id, + threadId: proposalMessage.threadId, + parentThreadId: proposalMessage.thread?.parentThreadId, + state: ProofState.ProposalReceived, + protocolVersion: 'v1', + }) + + // Assert + connectionService.assertConnectionOrServiceDecorator(messageContext) + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: proposalMessage, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Sender, + }) + + // Save record + await proofRepository.save(agentContext, proofRecord) + this.emitStateChangedEvent(agentContext, proofRecord, null) + } + + return proofRecord + } + + public async acceptProposal( + agentContext: AgentContext, + { proofRecord, proofFormats, comment, autoAcceptProof }: AcceptProofProposalOptions<[IndyProofFormatServiceLike]> + ): Promise> { + // Assert + proofRecord.assertProtocolVersion('v1') + proofRecord.assertState(ProofState.ProposalReceived) + if (proofFormats) this.assertOnlyIndyFormat(proofFormats) + + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + const proposalMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V1ProposePresentationMessage, + }) + + const indyFormat = proofFormats?.indy + + // Create a proof request from the preview, so we can let the messages + // be handled using the indy proof format which supports RFC0592 + const requestFromPreview = createRequestFromPreview({ + attributes: proposalMessage.presentationProposal.attributes, + predicates: proposalMessage.presentationProposal.predicates, + name: indyFormat?.name ?? 'Proof Request', + version: indyFormat?.version ?? '1.0', + nonce: await agentContext.wallet.generateNonce(), + }) + + const proposalAttachment = new Attachment({ + data: { + json: JsonTransformer.toJSON(requestFromPreview), + }, + }) + + // Create message + const { attachment } = await this.indyProofFormat.acceptProposal(agentContext, { + attachmentId: INDY_PROOF_REQUEST_ATTACHMENT_ID, + proofRecord, + proposalAttachment, + }) + + const requestPresentationMessage = new V1RequestPresentationMessage({ + comment, + requestAttachments: [attachment], + }) + + requestPresentationMessage.setThread({ + threadId: proofRecord.threadId, + parentThreadId: proofRecord.parentThreadId, + }) + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: requestPresentationMessage, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Sender, + }) + + // Update record + proofRecord.autoAcceptProof = autoAcceptProof ?? proofRecord.autoAcceptProof + await this.updateState(agentContext, proofRecord, ProofState.RequestSent) + + return { message: requestPresentationMessage, proofRecord } + } + + public async negotiateProposal( + agentContext: AgentContext, + { proofFormats, proofRecord, comment, autoAcceptProof }: NegotiateProofProposalOptions<[IndyProofFormatServiceLike]> + ): Promise> { + // Assert + proofRecord.assertProtocolVersion('v1') + proofRecord.assertState(ProofState.ProposalReceived) + this.assertOnlyIndyFormat(proofFormats) + + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // Create message + const { attachment } = await this.indyProofFormat.createRequest(agentContext, { + attachmentId: INDY_PROOF_REQUEST_ATTACHMENT_ID, + proofFormats, + proofRecord, + }) + + const requestPresentationMessage = new V1RequestPresentationMessage({ + comment, + requestAttachments: [attachment], + }) + requestPresentationMessage.setThread({ + threadId: proofRecord.threadId, + }) + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: requestPresentationMessage, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Sender, + }) + + proofRecord.autoAcceptProof = autoAcceptProof ?? proofRecord.autoAcceptProof + await this.updateState(agentContext, proofRecord, ProofState.RequestSent) + + return { message: requestPresentationMessage, proofRecord } + } + + public async createRequest( + agentContext: AgentContext, + { + proofFormats, + connectionRecord, + comment, + parentThreadId, + autoAcceptProof, + }: CreateProofRequestOptions<[IndyProofFormatServiceLike]> + ): Promise> { + this.assertOnlyIndyFormat(proofFormats) + + const proofRepository = agentContext.dependencyManager.resolve(ProofRepository) + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + if (!proofFormats.indy) { + throw new AriesFrameworkError('Missing indy proof request data for v1 create request') + } + + // Create record + const proofRecord = new ProofExchangeRecord({ + connectionId: connectionRecord?.id, + threadId: uuid(), + parentThreadId, + state: ProofState.RequestSent, + autoAcceptProof, + protocolVersion: 'v1', + }) + + // Create message + const { attachment } = await this.indyProofFormat.createRequest(agentContext, { + attachmentId: INDY_PROOF_REQUEST_ATTACHMENT_ID, + proofFormats, + proofRecord, + }) + + // Construct request message + const message = new V1RequestPresentationMessage({ + id: proofRecord.threadId, + comment, + requestAttachments: [attachment], + }) + + message.setThread({ + threadId: proofRecord.threadId, + parentThreadId: proofRecord.parentThreadId, + }) + + await didCommMessageRepository.saveAgentMessage(agentContext, { + agentMessage: message, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Sender, + }) + + await proofRepository.save(agentContext, proofRecord) + this.emitStateChangedEvent(agentContext, proofRecord, null) + + return { message, proofRecord } + } + + public async processRequest( + messageContext: InboundMessageContext + ): Promise { + const { message: proofRequestMessage, connection, agentContext } = messageContext + + const proofRepository = agentContext.dependencyManager.resolve(ProofRepository) + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // TODO: with this method, we should update the credential protocol to use the ConnectionApi, so it + // only depends on the public api, rather than the internal API (this helps with breaking changes) + const connectionService = agentContext.dependencyManager.resolve(ConnectionService) + + agentContext.config.logger.debug(`Processing presentation request with id ${proofRequestMessage.id}`) + + let proofRecord = await this.findByThreadAndConnectionId(agentContext, proofRequestMessage.threadId, connection?.id) + + const requestAttachment = proofRequestMessage.getRequestAttachmentById(INDY_PROOF_REQUEST_ATTACHMENT_ID) + if (!requestAttachment) { + throw new AriesFrameworkError( + `Indy attachment with id ${INDY_PROOF_REQUEST_ATTACHMENT_ID} not found in request message` + ) + } + + // proof record already exists, this means we are the message is sent as reply to a proposal we sent + if (proofRecord) { + const previousReceivedMessage = await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V1RequestPresentationMessage, + }) + const previousSentMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V1ProposePresentationMessage, + }) + + // Assert + proofRecord.assertProtocolVersion('v1') + proofRecord.assertState(ProofState.ProposalSent) + connectionService.assertConnectionOrServiceDecorator(messageContext, { + previousReceivedMessage, + previousSentMessage, + }) + + await this.indyProofFormat.processRequest(agentContext, { + attachment: requestAttachment, + proofRecord, + }) + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: proofRequestMessage, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Receiver, + }) + await this.updateState(agentContext, proofRecord, ProofState.RequestReceived) + } else { + // No proof record exists with thread id + proofRecord = new ProofExchangeRecord({ + connectionId: connection?.id, + threadId: proofRequestMessage.threadId, + parentThreadId: proofRequestMessage.thread?.parentThreadId, + state: ProofState.RequestReceived, + protocolVersion: 'v1', + }) + + await this.indyProofFormat.processRequest(agentContext, { + attachment: requestAttachment, + proofRecord, + }) + + await didCommMessageRepository.saveAgentMessage(agentContext, { + agentMessage: proofRequestMessage, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Receiver, + }) + + // Assert + connectionService.assertConnectionOrServiceDecorator(messageContext) + + // Save in repository + await proofRepository.save(agentContext, proofRecord) + this.emitStateChangedEvent(agentContext, proofRecord, null) + } + + return proofRecord + } + + public async negotiateRequest( + agentContext: AgentContext, + { proofFormats, proofRecord, comment, autoAcceptProof }: NegotiateProofRequestOptions<[IndyProofFormatServiceLike]> + ): Promise> { + // Assert + proofRecord.assertProtocolVersion('v1') + proofRecord.assertState(ProofState.RequestReceived) + this.assertOnlyIndyFormat(proofFormats) + + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + if (!proofRecord.connectionId) { + throw new AriesFrameworkError( + `No connectionId found for proof record '${proofRecord.id}'. Connection-less verification does not support negotiation.` + ) + } + + if (!proofFormats.indy) { + throw new AriesFrameworkError('Missing indy proof format in v1 negotiate request call.') + } + + const presentationProposal = new V1PresentationPreview({ + attributes: proofFormats.indy?.attributes, + predicates: proofFormats.indy?.predicates, + }) + + // validate input data from user + MessageValidator.validateSync(presentationProposal) + + const message = new V1ProposePresentationMessage({ + comment, + presentationProposal, + }) + message.setThread({ threadId: proofRecord.threadId }) + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Sender, + }) + + // Update record + proofRecord.autoAcceptProof = autoAcceptProof ?? proofRecord.autoAcceptProof + await this.updateState(agentContext, proofRecord, ProofState.ProposalSent) + + return { proofRecord, message: message } + } + + public async acceptRequest( + agentContext: AgentContext, + { proofRecord, proofFormats, autoAcceptProof, comment }: AcceptProofRequestOptions<[IndyProofFormatServiceLike]> + ): Promise> { + // Assert + proofRecord.assertProtocolVersion('v1') + proofRecord.assertState(ProofState.RequestReceived) + + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + const requestMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V1RequestPresentationMessage, + }) + const proposalMessage = await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V1ProposePresentationMessage, + }) + + const requestAttachment = requestMessage.getRequestAttachmentById(INDY_PROOF_REQUEST_ATTACHMENT_ID) + const indyProofRequest = requestMessage.indyProofRequest + + if (!requestAttachment || !indyProofRequest) { + throw new V1PresentationProblemReportError( + `Missing indy attachment in request message for presentation with thread id ${proofRecord.threadId}`, + { problemCode: PresentationProblemReportReason.Abandoned } + ) + } + + const proposalAttachment = proposalMessage + ? new Attachment({ + data: { + json: JsonTransformer.toJSON( + createRequestFromPreview({ + attributes: proposalMessage.presentationProposal?.attributes, + predicates: proposalMessage.presentationProposal?.predicates, + name: indyProofRequest.name, + nonce: indyProofRequest.nonce, + version: indyProofRequest.nonce, + }) + ), + }, + }) + : undefined + + const { attachment } = await this.indyProofFormat.acceptRequest(agentContext, { + attachmentId: INDY_PROOF_ATTACHMENT_ID, + requestAttachment, + proposalAttachment, + proofFormats, + proofRecord, + }) + + const message = new V1PresentationMessage({ + comment, + presentationAttachments: [attachment], + }) + message.setThread({ threadId: proofRecord.threadId }) + + await didCommMessageRepository.saveAgentMessage(agentContext, { + agentMessage: message, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Sender, + }) + + // Update record + proofRecord.autoAcceptProof = autoAcceptProof ?? proofRecord.autoAcceptProof + await this.updateState(agentContext, proofRecord, ProofState.PresentationSent) + + return { message, proofRecord } + } + + public async getCredentialsForRequest( + agentContext: AgentContext, + { proofRecord, proofFormats }: GetCredentialsForRequestOptions<[IndyProofFormatServiceLike]> + ): Promise> { + if (proofFormats) this.assertOnlyIndyFormat(proofFormats) + + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + const requestMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V1RequestPresentationMessage, + }) + + const proposalMessage = await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V1ProposePresentationMessage, + }) + + const requestAttachment = requestMessage.getRequestAttachmentById(INDY_PROOF_REQUEST_ATTACHMENT_ID) + const indyProofRequest = requestMessage.indyProofRequest + + if (!requestAttachment || !indyProofRequest) { + throw new AriesFrameworkError( + `Missing indy attachment in request message for presentation with thread id ${proofRecord.threadId}` + ) + } + + const proposalAttachment = proposalMessage + ? new Attachment({ + data: { + json: JsonTransformer.toJSON( + createRequestFromPreview({ + attributes: proposalMessage.presentationProposal?.attributes, + predicates: proposalMessage.presentationProposal?.predicates, + name: indyProofRequest.name, + nonce: indyProofRequest.nonce, + version: indyProofRequest.nonce, + }) + ), + }, + }) + : undefined + + const credentialForRequest = await this.indyProofFormat.getCredentialsForRequest(agentContext, { + proofRecord, + requestAttachment, + proofFormats, + proposalAttachment, + }) + + return { + proofFormats: { + indy: credentialForRequest, + }, + } + } + + public async selectCredentialsForRequest( + agentContext: AgentContext, + { proofRecord, proofFormats }: SelectCredentialsForRequestOptions<[IndyProofFormatServiceLike]> + ): Promise> { + if (proofFormats) this.assertOnlyIndyFormat(proofFormats) + + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + const requestMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V1RequestPresentationMessage, + }) + + const proposalMessage = await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V1ProposePresentationMessage, + }) + + const requestAttachment = requestMessage.getRequestAttachmentById(INDY_PROOF_REQUEST_ATTACHMENT_ID) + const indyProofRequest = requestMessage.indyProofRequest + + if (!requestAttachment || !indyProofRequest) { + throw new AriesFrameworkError( + `Missing indy attachment in request message for presentation with thread id ${proofRecord.threadId}` + ) + } + + const proposalAttachment = proposalMessage + ? new Attachment({ + data: { + json: JsonTransformer.toJSON( + createRequestFromPreview({ + attributes: proposalMessage.presentationProposal?.attributes, + predicates: proposalMessage.presentationProposal?.predicates, + name: indyProofRequest.name, + nonce: indyProofRequest.nonce, + version: indyProofRequest.nonce, + }) + ), + }, + }) + : undefined + + const selectedCredentials = await this.indyProofFormat.selectCredentialsForRequest(agentContext, { + proofFormats, + proofRecord, + requestAttachment, + proposalAttachment, + }) + + return { + proofFormats: { + indy: selectedCredentials, + }, + } + } + + public async processPresentation( + messageContext: InboundMessageContext + ): Promise { + const { message: presentationMessage, connection, agentContext } = messageContext + + agentContext.config.logger.debug(`Processing presentation with message id ${presentationMessage.id}`) + + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // TODO: with this method, we should update the credential protocol to use the ConnectionApi, so it + // only depends on the public api, rather than the internal API (this helps with breaking changes) + const connectionService = agentContext.dependencyManager.resolve(ConnectionService) + + const proofRecord = await this.getByThreadAndConnectionId( + agentContext, + presentationMessage.threadId, + connection?.id + ) + + const proposalMessage = await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V1ProposePresentationMessage, + }) + + const requestMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V1RequestPresentationMessage, + }) + + // Assert + proofRecord.assertState(ProofState.RequestSent) + proofRecord.assertProtocolVersion('v1') + connectionService.assertConnectionOrServiceDecorator(messageContext, { + previousReceivedMessage: proposalMessage, + previousSentMessage: requestMessage, + }) + + const presentationAttachment = presentationMessage.getPresentationAttachmentById(INDY_PROOF_ATTACHMENT_ID) + if (!presentationAttachment) { + throw new AriesFrameworkError('Missing indy proof attachment in processPresentation') + } + + const requestAttachment = requestMessage.getRequestAttachmentById(INDY_PROOF_REQUEST_ATTACHMENT_ID) + if (!requestAttachment) { + throw new AriesFrameworkError('Missing indy proof request attachment in processPresentation') + } + + const isValid = await this.indyProofFormat.processPresentation(agentContext, { + proofRecord, + attachment: presentationAttachment, + requestAttachment, + }) + + await didCommMessageRepository.saveAgentMessage(agentContext, { + agentMessage: presentationMessage, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Receiver, + }) + + // Update record + proofRecord.isVerified = isValid + await this.updateState(agentContext, proofRecord, ProofState.PresentationReceived) + + return proofRecord + } + + public async acceptPresentation( + agentContext: AgentContext, + { proofRecord }: AcceptPresentationOptions + ): Promise> { + agentContext.config.logger.debug(`Creating presentation ack for proof record with id ${proofRecord.id}`) + + // Assert + proofRecord.assertProtocolVersion('v1') + proofRecord.assertState(ProofState.PresentationReceived) + + // Create message + const ackMessage = new V1PresentationAckMessage({ + status: AckStatus.OK, + threadId: proofRecord.threadId, + }) + + // Update record + await this.updateState(agentContext, proofRecord, ProofState.Done) + + return { message: ackMessage, proofRecord } + } + + public async processAck( + messageContext: InboundMessageContext + ): Promise { + const { message: presentationAckMessage, connection, agentContext } = messageContext + + agentContext.config.logger.debug(`Processing presentation ack with message id ${presentationAckMessage.id}`) + + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // TODO: with this method, we should update the credential protocol to use the ConnectionApi, so it + // only depends on the public api, rather than the internal API (this helps with breaking changes) + const connectionService = agentContext.dependencyManager.resolve(ConnectionService) + + const proofRecord = await this.getByThreadAndConnectionId( + agentContext, + presentationAckMessage.threadId, + connection?.id + ) + + const previousReceivedMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V1RequestPresentationMessage, + }) + + const previousSentMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V1PresentationMessage, + }) + + // Assert + proofRecord.assertProtocolVersion('v1') + proofRecord.assertState(ProofState.PresentationSent) + connectionService.assertConnectionOrServiceDecorator(messageContext, { + previousReceivedMessage, + previousSentMessage, + }) + + // Update record + await this.updateState(agentContext, proofRecord, ProofState.Done) + + return proofRecord + } + + public async createProblemReport( + agentContext: AgentContext, + { proofRecord, description }: CreateProofProblemReportOptions + ): Promise> { + const message = new V1PresentationProblemReportMessage({ + description: { + code: PresentationProblemReportReason.Abandoned, + en: description, + }, + }) + + message.setThread({ + threadId: proofRecord.threadId, + parentThreadId: proofRecord.parentThreadId, + }) + + return { + proofRecord, + message, + } + } + + public async shouldAutoRespondToProposal( + agentContext: AgentContext, + options: { + proofRecord: ProofExchangeRecord + proposalMessage: V1ProposePresentationMessage + } + ): Promise { + const { proofRecord, proposalMessage } = options + + const proofsModuleConfig = agentContext.dependencyManager.resolve(ProofsModuleConfig) + + const autoAccept = composeAutoAccept(proofRecord.autoAcceptProof, proofsModuleConfig.autoAcceptProofs) + + // Handle always / never cases + if (autoAccept === AutoAcceptProof.Always) return true + if (autoAccept === AutoAcceptProof.Never) return false + + // We are in the ContentApproved case. We need to make sure we've sent a request, and it matches the proposal + const requestMessage = await this.findRequestMessage(agentContext, proofRecord.id) + const requestAttachment = requestMessage?.getRequestAttachmentById(INDY_PROOF_REQUEST_ATTACHMENT_ID) + if (!requestAttachment) return false + + const rfc0592Proposal = JsonTransformer.toJSON( + createRequestFromPreview({ + name: 'Proof Request', + nonce: await agentContext.wallet.generateNonce(), + version: '1.0', + attributes: proposalMessage.presentationProposal.attributes, + predicates: proposalMessage.presentationProposal.predicates, + }) + ) + + return this.indyProofFormat.shouldAutoRespondToProposal(agentContext, { + proofRecord, + proposalAttachment: new Attachment({ + data: { + json: rfc0592Proposal, + }, + }), + requestAttachment, + }) + } + + public async shouldAutoRespondToRequest( + agentContext: AgentContext, + options: { + proofRecord: ProofExchangeRecord + requestMessage: V1RequestPresentationMessage + } + ): Promise { + const { proofRecord, requestMessage } = options + + const proofsModuleConfig = agentContext.dependencyManager.resolve(ProofsModuleConfig) + + const autoAccept = composeAutoAccept(proofRecord.autoAcceptProof, proofsModuleConfig.autoAcceptProofs) + + // Handle always / never cases + if (autoAccept === AutoAcceptProof.Always) return true + if (autoAccept === AutoAcceptProof.Never) return false + + const requestAttachment = requestMessage.getRequestAttachmentById(INDY_PROOF_REQUEST_ATTACHMENT_ID) + if (!requestAttachment) return false + + // We are in the ContentApproved case. We need to make sure we've sent a proposal, and it matches the request + const proposalMessage = await this.findProposalMessage(agentContext, proofRecord.id) + if (!proposalMessage) return false + + const rfc0592Proposal = createRequestFromPreview({ + name: 'Proof Request', + nonce: await agentContext.wallet.generateNonce(), + version: '1.0', + attributes: proposalMessage.presentationProposal.attributes, + predicates: proposalMessage.presentationProposal.predicates, + }).toJSON() + + return this.indyProofFormat.shouldAutoRespondToRequest(agentContext, { + proofRecord, + proposalAttachment: new Attachment({ + data: { + base64: JsonEncoder.toBase64(rfc0592Proposal), + }, + }), + requestAttachment, + }) + } + + public async shouldAutoRespondToPresentation( + agentContext: AgentContext, + options: { + proofRecord: ProofExchangeRecord + presentationMessage: V1PresentationMessage + } + ): Promise { + const { proofRecord, presentationMessage } = options + + const proofsModuleConfig = agentContext.dependencyManager.resolve(ProofsModuleConfig) + + const autoAccept = composeAutoAccept(proofRecord.autoAcceptProof, proofsModuleConfig.autoAcceptProofs) + + // Handle always / never cases + if (autoAccept === AutoAcceptProof.Always) return true + if (autoAccept === AutoAcceptProof.Never) return false + + const presentationAttachment = presentationMessage.getPresentationAttachmentById(INDY_PROOF_ATTACHMENT_ID) + if (!presentationAttachment) return false + + // We are in the ContentApproved case. We need to make sure we've sent a request, and it matches the presentation + const requestMessage = await this.findRequestMessage(agentContext, proofRecord.id) + const requestAttachment = requestMessage?.getRequestAttachmentById(INDY_PROOF_REQUEST_ATTACHMENT_ID) + if (!requestAttachment) return false + + // We are in the ContentApproved case. We need to make sure we've sent a proposal, and it matches the request + const proposalMessage = await this.findProposalMessage(agentContext, proofRecord.id) + + const rfc0592Proposal = proposalMessage + ? JsonTransformer.toJSON( + createRequestFromPreview({ + name: 'Proof Request', + nonce: await agentContext.wallet.generateNonce(), + version: '1.0', + attributes: proposalMessage.presentationProposal.attributes, + predicates: proposalMessage.presentationProposal.predicates, + }) + ) + : undefined + + return this.indyProofFormat.shouldAutoRespondToPresentation(agentContext, { + proofRecord, + requestAttachment, + presentationAttachment, + proposalAttachment: new Attachment({ + data: { + json: rfc0592Proposal, + }, + }), + }) + } + + public async findProposalMessage( + agentContext: AgentContext, + proofRecordId: string + ): Promise { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + return await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: proofRecordId, + messageClass: V1ProposePresentationMessage, + }) + } + + public async findRequestMessage( + agentContext: AgentContext, + proofRecordId: string + ): Promise { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + return await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: proofRecordId, + messageClass: V1RequestPresentationMessage, + }) + } + + public async findPresentationMessage( + agentContext: AgentContext, + proofRecordId: string + ): Promise { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + return await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: proofRecordId, + messageClass: V1PresentationMessage, + }) + } + + public async getFormatData( + agentContext: AgentContext, + proofRecordId: string + ): Promise> { + // TODO: we could looking at fetching all record using a single query and then filtering based on the type of the message. + const [proposalMessage, requestMessage, presentationMessage] = await Promise.all([ + this.findProposalMessage(agentContext, proofRecordId), + this.findRequestMessage(agentContext, proofRecordId), + this.findPresentationMessage(agentContext, proofRecordId), + ]) + + let indyProposeProof = undefined + const indyRequestProof = requestMessage?.indyProofRequest ?? undefined + const indyPresentProof = presentationMessage?.indyProof ?? undefined + + if (proposalMessage && indyRequestProof) { + indyProposeProof = createRequestFromPreview({ + name: indyRequestProof.name, + version: indyRequestProof.version, + nonce: indyRequestProof.nonce, + attributes: proposalMessage.presentationProposal.attributes, + predicates: proposalMessage.presentationProposal.predicates, + }) + } else if (proposalMessage) { + indyProposeProof = createRequestFromPreview({ + name: 'Proof Request', + version: '1.0', + nonce: await agentContext.wallet.generateNonce(), + attributes: proposalMessage.presentationProposal.attributes, + predicates: proposalMessage.presentationProposal.predicates, + }) + } + + return { + proposal: proposalMessage + ? { + indy: indyProposeProof?.toJSON(), + } + : undefined, + request: requestMessage + ? { + indy: indyRequestProof, + } + : undefined, + presentation: presentationMessage + ? { + indy: indyPresentProof, + } + : undefined, + } + } + + private assertOnlyIndyFormat(proofFormats: Record) { + const formatKeys = Object.keys(proofFormats) + + // It's fine to not have any formats in some cases, if indy is required the method that calls this should check for this + if (formatKeys.length === 0) return + + if (formatKeys.length !== 1 || !formatKeys.includes('indy')) { + throw new AriesFrameworkError('Only indy proof format is supported for present proof v1 protocol') + } + } +} diff --git a/packages/core/src/modules/proofs/protocol/v1/V1ProofService.ts b/packages/core/src/modules/proofs/protocol/v1/V1ProofService.ts deleted file mode 100644 index 22b43cdc93..0000000000 --- a/packages/core/src/modules/proofs/protocol/v1/V1ProofService.ts +++ /dev/null @@ -1,1111 +0,0 @@ -import type { AgentContext } from '../../../../agent' -import type { AgentMessage } from '../../../../agent/AgentMessage' -import type { Dispatcher } from '../../../../agent/Dispatcher' -import type { InboundMessageContext } from '../../../../agent/models/InboundMessageContext' -import type { Attachment } from '../../../../decorators/attachment/Attachment' -import type { MediationRecipientService } from '../../../routing/services/MediationRecipientService' -import type { RoutingService } from '../../../routing/services/RoutingService' -import type { ProofResponseCoordinator } from '../../ProofResponseCoordinator' -import type { ProofFormat } from '../../formats/ProofFormat' -import type { IndyProofFormat, IndyProposeProofFormat } from '../../formats/indy/IndyProofFormat' -import type { ProofAttributeInfo } from '../../formats/indy/models' -import type { - CreateProblemReportOptions, - FormatCreatePresentationOptions, -} from '../../formats/models/ProofFormatServiceOptions' -import type { - CreateAckOptions, - CreatePresentationOptions, - CreateProofRequestFromProposalOptions, - CreateProposalAsResponseOptions, - CreateProposalOptions, - CreateRequestAsResponseOptions, - CreateRequestOptions, - FormatRequestedCredentialReturn, - FormatRetrievedCredentialOptions, - GetFormatDataReturn, - GetRequestedCredentialsForProofRequestOptions, - ProofRequestFromProposalOptions, -} from '../../models/ProofServiceOptions' - -import { validateOrReject } from 'class-validator' -import { inject, Lifecycle, scoped } from 'tsyringe' - -import { AgentConfig } from '../../../../agent/AgentConfig' -import { EventEmitter } from '../../../../agent/EventEmitter' -import { InjectionSymbols } from '../../../../constants' -import { AriesFrameworkError } from '../../../../error/AriesFrameworkError' -import { DidCommMessageRole } from '../../../../storage' -import { DidCommMessageRepository } from '../../../../storage/didcomm/DidCommMessageRepository' -import { checkProofRequestForDuplicates } from '../../../../utils' -import { JsonTransformer } from '../../../../utils/JsonTransformer' -import { MessageValidator } from '../../../../utils/MessageValidator' -import { Wallet } from '../../../../wallet' -import { AckStatus } from '../../../common/messages/AckMessage' -import { ConnectionService } from '../../../connections' -import { CredentialRepository } from '../../../credentials' -import { IndyCredentialInfo } from '../../../credentials/formats/indy/models/IndyCredentialInfo' -import { IndyHolderService, IndyRevocationService } from '../../../indy' -import { IndyLedgerService } from '../../../ledger/services/IndyLedgerService' -import { ProofService } from '../../ProofService' -import { PresentationProblemReportReason } from '../../errors/PresentationProblemReportReason' -import { IndyProofFormatService } from '../../formats/indy/IndyProofFormatService' -import { ProofRequest } from '../../formats/indy/models/ProofRequest' -import { RequestedCredentials } from '../../formats/indy/models/RequestedCredentials' -import { ProofState } from '../../models/ProofState' -import { ProofExchangeRecord } from '../../repository/ProofExchangeRecord' -import { ProofRepository } from '../../repository/ProofRepository' - -import { V1PresentationProblemReportError } from './errors' -import { - V1PresentationAckHandler, - V1PresentationHandler, - V1PresentationProblemReportHandler, - V1ProposePresentationHandler, - V1RequestPresentationHandler, -} from './handlers' -import { - INDY_PROOF_ATTACHMENT_ID, - INDY_PROOF_REQUEST_ATTACHMENT_ID, - V1PresentationAckMessage, - V1PresentationMessage, - V1ProposePresentationMessage, - V1RequestPresentationMessage, -} from './messages' -import { V1PresentationProblemReportMessage } from './messages/V1PresentationProblemReportMessage' -import { PresentationPreview } from './models/V1PresentationPreview' - -/** - * @todo add method to check if request matches proposal. Useful to see if a request I received is the same as the proposal I sent. - * @todo add method to reject / revoke messages - * @todo validate attachments / messages - */ -@scoped(Lifecycle.ContainerScoped) -export class V1ProofService extends ProofService<[IndyProofFormat]> { - private credentialRepository: CredentialRepository - private ledgerService: IndyLedgerService - private indyHolderService: IndyHolderService - private indyRevocationService: IndyRevocationService - private indyProofFormatService: IndyProofFormatService - - public constructor( - proofRepository: ProofRepository, - didCommMessageRepository: DidCommMessageRepository, - ledgerService: IndyLedgerService, - @inject(InjectionSymbols.Wallet) wallet: Wallet, - agentConfig: AgentConfig, - connectionService: ConnectionService, - eventEmitter: EventEmitter, - credentialRepository: CredentialRepository, - formatService: IndyProofFormatService, - indyHolderService: IndyHolderService, - indyRevocationService: IndyRevocationService - ) { - super(agentConfig, proofRepository, connectionService, didCommMessageRepository, wallet, eventEmitter) - this.credentialRepository = credentialRepository - this.ledgerService = ledgerService - this.wallet = wallet - this.indyProofFormatService = formatService - this.indyHolderService = indyHolderService - this.indyRevocationService = indyRevocationService - } - - /** - * The version of the present proof protocol this service supports - */ - public readonly version = 'v1' as const - - public async createProposal( - agentContext: AgentContext, - options: CreateProposalOptions<[IndyProofFormat]> - ): Promise<{ proofRecord: ProofExchangeRecord; message: AgentMessage }> { - const { connectionRecord, proofFormats } = options - - // Assert - connectionRecord.assertReady() - - if (!proofFormats.indy || Object.keys(proofFormats).length !== 1) { - throw new AriesFrameworkError('Only indy proof format is supported for present proof protocol v1') - } - - const presentationProposal = new PresentationPreview({ - attributes: proofFormats.indy?.attributes, - predicates: proofFormats.indy?.predicates, - }) - - // Create message - const proposalMessage = new V1ProposePresentationMessage({ - comment: options?.comment, - presentationProposal, - parentThreadId: options.parentThreadId, - }) - - // Create record - const proofRecord = new ProofExchangeRecord({ - connectionId: connectionRecord.id, - threadId: proposalMessage.threadId, - parentThreadId: proposalMessage.thread?.parentThreadId, - state: ProofState.ProposalSent, - autoAcceptProof: options?.autoAcceptProof, - protocolVersion: 'v1', - }) - - await this.didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { - agentMessage: proposalMessage, - associatedRecordId: proofRecord.id, - role: DidCommMessageRole.Sender, - }) - - await this.proofRepository.save(agentContext, proofRecord) - this.emitStateChangedEvent(agentContext, proofRecord, null) - - return { proofRecord, message: proposalMessage } - } - - public async createProposalAsResponse( - agentContext: AgentContext, - options: CreateProposalAsResponseOptions<[IndyProofFormat]> - ): Promise<{ proofRecord: ProofExchangeRecord; message: AgentMessage }> { - const { proofRecord, proofFormats, comment } = options - - // Assert - proofRecord.assertState(ProofState.RequestReceived) - - if (!proofFormats.indy || Object.keys(proofFormats).length !== 1) { - throw new AriesFrameworkError('Only indy proof format is supported for present proof protocol v1') - } - - // Create message - const presentationPreview = new PresentationPreview({ - attributes: proofFormats.indy?.attributes, - predicates: proofFormats.indy?.predicates, - }) - - const proposalMessage: V1ProposePresentationMessage = new V1ProposePresentationMessage({ - comment, - presentationProposal: presentationPreview, - }) - - proposalMessage.setThread({ threadId: proofRecord.threadId }) - - await this.didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { - agentMessage: proposalMessage, - associatedRecordId: proofRecord.id, - role: DidCommMessageRole.Sender, - }) - - // Update record - await this.updateState(agentContext, proofRecord, ProofState.ProposalSent) - - return { proofRecord, message: proposalMessage } - } - - public async processProposal( - messageContext: InboundMessageContext - ): Promise { - let proofRecord: ProofExchangeRecord - const { message: proposalMessage, connection } = messageContext - - this.logger.debug(`Processing presentation proposal with id ${proposalMessage.id}`) - - try { - // Proof record already exists - proofRecord = await this.getByThreadAndConnectionId( - messageContext.agentContext, - proposalMessage.threadId, - connection?.id - ) - - const requestMessage = await this.didCommMessageRepository.findAgentMessage(messageContext.agentContext, { - associatedRecordId: proofRecord.id, - messageClass: V1RequestPresentationMessage, - }) - - // Assert - proofRecord.assertState(ProofState.RequestSent) - this.connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage: proposalMessage, - previousSentMessage: requestMessage ?? undefined, - }) - - await this.didCommMessageRepository.saveOrUpdateAgentMessage(messageContext.agentContext, { - agentMessage: proposalMessage, - associatedRecordId: proofRecord.id, - role: DidCommMessageRole.Receiver, - }) - - // Update record - await this.updateState(messageContext.agentContext, proofRecord, ProofState.ProposalReceived) - } catch { - // No proof record exists with thread id - proofRecord = new ProofExchangeRecord({ - connectionId: connection?.id, - threadId: proposalMessage.threadId, - parentThreadId: proposalMessage.thread?.parentThreadId, - state: ProofState.ProposalReceived, - protocolVersion: 'v1', - }) - - // Assert - this.connectionService.assertConnectionOrServiceDecorator(messageContext) - - // Save record - await this.didCommMessageRepository.saveOrUpdateAgentMessage(messageContext.agentContext, { - agentMessage: proposalMessage, - associatedRecordId: proofRecord.id, - role: DidCommMessageRole.Sender, - }) - - await this.proofRepository.save(messageContext.agentContext, proofRecord) - - this.emitStateChangedEvent(messageContext.agentContext, proofRecord, null) - } - - return proofRecord - } - - public async createRequestAsResponse( - agentContext: AgentContext, - options: CreateRequestAsResponseOptions<[IndyProofFormat]> - ): Promise<{ proofRecord: ProofExchangeRecord; message: AgentMessage }> { - const { proofRecord, comment, proofFormats } = options - if (!proofFormats.indy) { - throw new AriesFrameworkError('Only indy proof format is supported for present proof protocol v1') - } - - // Assert - proofRecord.assertState(ProofState.ProposalReceived) - - // Create message - const { attachment } = await this.indyProofFormatService.createRequest({ - id: INDY_PROOF_REQUEST_ATTACHMENT_ID, - formats: proofFormats, - }) - - const requestPresentationMessage = new V1RequestPresentationMessage({ - comment, - requestPresentationAttachments: [attachment], - }) - requestPresentationMessage.setThread({ - threadId: proofRecord.threadId, - }) - - await this.didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { - agentMessage: requestPresentationMessage, - associatedRecordId: proofRecord.id, - role: DidCommMessageRole.Sender, - }) - - // Update record - await this.updateState(agentContext, proofRecord, ProofState.RequestSent) - - return { message: requestPresentationMessage, proofRecord } - } - - public async createRequest( - agentContext: AgentContext, - options: CreateRequestOptions<[IndyProofFormat]> - ): Promise<{ proofRecord: ProofExchangeRecord; message: AgentMessage }> { - this.logger.debug(`Creating proof request`) - - // Assert - if (options.connectionRecord) { - options.connectionRecord.assertReady() - } - - if (!options.proofFormats.indy || Object.keys(options.proofFormats).length !== 1) { - throw new AriesFrameworkError('Only indy proof format is supported for present proof protocol v1') - } - - // Create message - const { attachment } = await this.indyProofFormatService.createRequest({ - id: INDY_PROOF_REQUEST_ATTACHMENT_ID, - formats: options.proofFormats, - }) - - const requestPresentationMessage = new V1RequestPresentationMessage({ - comment: options?.comment, - requestPresentationAttachments: [attachment], - parentThreadId: options.parentThreadId, - }) - - // Create record - const proofRecord = new ProofExchangeRecord({ - connectionId: options.connectionRecord?.id, - threadId: requestPresentationMessage.threadId, - parentThreadId: requestPresentationMessage.thread?.parentThreadId, - state: ProofState.RequestSent, - autoAcceptProof: options?.autoAcceptProof, - protocolVersion: 'v1', - }) - - await this.didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { - agentMessage: requestPresentationMessage, - associatedRecordId: proofRecord.id, - role: DidCommMessageRole.Sender, - }) - - await this.proofRepository.save(agentContext, proofRecord) - this.emitStateChangedEvent(agentContext, proofRecord, null) - - return { message: requestPresentationMessage, proofRecord } - } - - public async processRequest( - messageContext: InboundMessageContext - ): Promise { - let proofRecord: ProofExchangeRecord - const { message: proofRequestMessage, connection } = messageContext - - this.logger.debug(`Processing presentation request with id ${proofRequestMessage.id}`) - - const requestAttachments = proofRequestMessage.getAttachmentFormats() - - for (const attachmentFormat of requestAttachments) { - await this.indyProofFormatService.processRequest({ - requestAttachment: attachmentFormat, - }) - } - - const proofRequest = proofRequestMessage.indyProofRequest - - // Assert attachment - if (!proofRequest) { - throw new V1PresentationProblemReportError( - `Missing required base64 or json encoded attachment data for presentation request with thread id ${proofRequestMessage.threadId}`, - { problemCode: PresentationProblemReportReason.Abandoned } - ) - } - await validateOrReject(proofRequest) - - // Assert attribute and predicate (group) names do not match - checkProofRequestForDuplicates(proofRequest) - - this.logger.debug('received proof request', proofRequest) - - try { - // Proof record already exists - proofRecord = await this.getByThreadAndConnectionId( - messageContext.agentContext, - proofRequestMessage.threadId, - connection?.id - ) - - const requestMessage = await this.didCommMessageRepository.findAgentMessage(messageContext.agentContext, { - associatedRecordId: proofRecord.id, - messageClass: V1RequestPresentationMessage, - }) - - const proposalMessage = await this.didCommMessageRepository.findAgentMessage(messageContext.agentContext, { - associatedRecordId: proofRecord.id, - messageClass: V1ProposePresentationMessage, - }) - - // Assert - proofRecord.assertState(ProofState.ProposalSent) - this.connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage: requestMessage ?? undefined, - previousSentMessage: proposalMessage ?? undefined, - }) - - await this.didCommMessageRepository.saveOrUpdateAgentMessage(messageContext.agentContext, { - agentMessage: proofRequestMessage, - associatedRecordId: proofRecord.id, - role: DidCommMessageRole.Receiver, - }) - - // Update record - await this.updateState(messageContext.agentContext, proofRecord, ProofState.RequestReceived) - } catch { - // No proof record exists with thread id - proofRecord = new ProofExchangeRecord({ - connectionId: connection?.id, - threadId: proofRequestMessage.threadId, - parentThreadId: proofRequestMessage.thread?.parentThreadId, - state: ProofState.RequestReceived, - protocolVersion: 'v1', - }) - - await this.didCommMessageRepository.saveOrUpdateAgentMessage(messageContext.agentContext, { - agentMessage: proofRequestMessage, - associatedRecordId: proofRecord.id, - role: DidCommMessageRole.Receiver, - }) - - // Assert - this.connectionService.assertConnectionOrServiceDecorator(messageContext) - - // Save in repository - await this.proofRepository.save(messageContext.agentContext, proofRecord) - this.emitStateChangedEvent(messageContext.agentContext, proofRecord, null) - } - - return proofRecord - } - - public async createPresentation( - agentContext: AgentContext, - options: CreatePresentationOptions<[IndyProofFormat]> - ): Promise<{ proofRecord: ProofExchangeRecord; message: AgentMessage }> { - const { proofRecord, proofFormats } = options - - this.logger.debug(`Creating presentation for proof record with id ${proofRecord.id}`) - - if (!proofFormats.indy || Object.keys(proofFormats).length !== 1) { - throw new AriesFrameworkError('Only indy proof format is supported for present proof protocol v1') - } - - // Assert - proofRecord.assertState(ProofState.RequestReceived) - - const requestMessage = await this.didCommMessageRepository.findAgentMessage(agentContext, { - associatedRecordId: proofRecord.id, - messageClass: V1RequestPresentationMessage, - }) - - const requestAttachment = requestMessage?.indyAttachment - - if (!requestAttachment) { - throw new V1PresentationProblemReportError( - `Missing required base64 or json encoded attachment data for presentation with thread id ${proofRecord.threadId}`, - { problemCode: PresentationProblemReportReason.Abandoned } - ) - } - - const presentationOptions: FormatCreatePresentationOptions = { - id: INDY_PROOF_ATTACHMENT_ID, - attachment: requestAttachment, - proofFormats: proofFormats, - } - - const proof = await this.indyProofFormatService.createPresentation(agentContext, presentationOptions) - - // Extract proof request from attachment - const proofRequestJson = requestAttachment.getDataAsJson() ?? null - const proofRequest = JsonTransformer.fromJSON(proofRequestJson, ProofRequest) - - const requestedCredentials = new RequestedCredentials({ - requestedAttributes: proofFormats.indy?.requestedAttributes, - requestedPredicates: proofFormats.indy?.requestedPredicates, - selfAttestedAttributes: proofFormats.indy?.selfAttestedAttributes, - }) - - // Get the matching attachments to the requested credentials - const linkedAttachments = await this.getRequestedAttachmentsForRequestedCredentials( - agentContext, - proofRequest, - requestedCredentials - ) - - const presentationMessage = new V1PresentationMessage({ - comment: options?.comment, - presentationAttachments: [proof.attachment], - attachments: linkedAttachments, - }) - presentationMessage.setThread({ threadId: proofRecord.threadId }) - - await this.didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { - agentMessage: presentationMessage, - associatedRecordId: proofRecord.id, - role: DidCommMessageRole.Sender, - }) - - // Update record - await this.updateState(agentContext, proofRecord, ProofState.PresentationSent) - - return { message: presentationMessage, proofRecord } - } - - public async processPresentation( - messageContext: InboundMessageContext - ): Promise { - const { message: presentationMessage, connection } = messageContext - - this.logger.debug(`Processing presentation with id ${presentationMessage.id}`) - - const proofRecord = await this.getByThreadAndConnectionId( - messageContext.agentContext, - presentationMessage.threadId, - connection?.id - ) - - const proposalMessage = await this.didCommMessageRepository.findAgentMessage(messageContext.agentContext, { - associatedRecordId: proofRecord.id, - messageClass: V1ProposePresentationMessage, - }) - - const requestMessage = await this.didCommMessageRepository.getAgentMessage(messageContext.agentContext, { - associatedRecordId: proofRecord.id, - messageClass: V1RequestPresentationMessage, - }) - - // Assert - proofRecord.assertState(ProofState.RequestSent) - this.connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage: proposalMessage ?? undefined, - previousSentMessage: requestMessage ?? undefined, - }) - - try { - const isValid = await this.indyProofFormatService.processPresentation(messageContext.agentContext, { - record: proofRecord, - formatAttachments: { - presentation: presentationMessage.getAttachmentFormats(), - request: requestMessage.getAttachmentFormats(), - }, - }) - await this.didCommMessageRepository.saveOrUpdateAgentMessage(messageContext.agentContext, { - agentMessage: presentationMessage, - associatedRecordId: proofRecord.id, - role: DidCommMessageRole.Receiver, - }) - - // Update record - proofRecord.isVerified = isValid - await this.updateState(messageContext.agentContext, proofRecord, ProofState.PresentationReceived) - } catch (e) { - if (e instanceof AriesFrameworkError) { - throw new V1PresentationProblemReportError(e.message, { - problemCode: PresentationProblemReportReason.Abandoned, - }) - } - throw e - } - - return proofRecord - } - - public async processAck( - messageContext: InboundMessageContext - ): Promise { - const { message: presentationAckMessage, connection } = messageContext - - this.logger.debug(`Processing presentation ack with id ${presentationAckMessage.id}`) - - const proofRecord = await this.getByThreadAndConnectionId( - messageContext.agentContext, - presentationAckMessage.threadId, - connection?.id - ) - - const requestMessage = await this.didCommMessageRepository.findAgentMessage(messageContext.agentContext, { - associatedRecordId: proofRecord.id, - messageClass: V1RequestPresentationMessage, - }) - - const presentationMessage = await this.didCommMessageRepository.findAgentMessage(messageContext.agentContext, { - associatedRecordId: proofRecord.id, - messageClass: V1PresentationMessage, - }) - - // Assert - proofRecord.assertState(ProofState.PresentationSent) - this.connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage: requestMessage ?? undefined, - previousSentMessage: presentationMessage ?? undefined, - }) - - // Update record - await this.updateState(messageContext.agentContext, proofRecord, ProofState.Done) - - return proofRecord - } - - public async createProblemReport( - agentContext: AgentContext, - options: CreateProblemReportOptions - ): Promise<{ proofRecord: ProofExchangeRecord; message: AgentMessage }> { - const msg = new V1PresentationProblemReportMessage({ - description: { - code: PresentationProblemReportReason.Abandoned, - en: options.description, - }, - }) - - msg.setThread({ - threadId: options.proofRecord.threadId, - parentThreadId: options.proofRecord.parentThreadId, - }) - - return { - proofRecord: options.proofRecord, - message: msg, - } - } - - public async processProblemReport( - messageContext: InboundMessageContext - ): Promise { - const { message: presentationProblemReportMessage } = messageContext - - const connection = messageContext.assertReadyConnection() - - this.logger.debug(`Processing problem report with id ${presentationProblemReportMessage.id}`) - - const proofRecord = await this.getByThreadAndConnectionId( - messageContext.agentContext, - presentationProblemReportMessage.threadId, - connection?.id - ) - - proofRecord.errorMessage = `${presentationProblemReportMessage.description.code}: ${presentationProblemReportMessage.description.en}` - await this.updateState(messageContext.agentContext, proofRecord, ProofState.Abandoned) - return proofRecord - } - - public async createProofRequestFromProposal( - agentContext: AgentContext, - options: CreateProofRequestFromProposalOptions - ): Promise> { - const proofRecordId = options.proofRecord.id - const proposalMessage = await this.didCommMessageRepository.findAgentMessage(agentContext, { - associatedRecordId: proofRecordId, - messageClass: V1ProposePresentationMessage, - }) - - if (!proposalMessage) { - throw new AriesFrameworkError(`Proof record with id ${proofRecordId} is missing required presentation proposal`) - } - - const indyProposeProofFormat: IndyProposeProofFormat = { - name: 'Proof Request', - version: '1.0', - nonce: await this.wallet.generateNonce(), - } - - const proofRequest: ProofRequest = await this.indyProofFormatService.createReferentForProofRequest( - indyProposeProofFormat, - proposalMessage.presentationProposal - ) - - return { - proofRecord: options.proofRecord, - proofFormats: { - indy: proofRequest, - }, - } - } - - /** - * Retrieves the linked attachments for an {@link indyProofRequest} - * @param indyProofRequest The proof request for which the linked attachments have to be found - * @param requestedCredentials The requested credentials - * @returns a list of attachments that are linked to the requested credentials - */ - public async getRequestedAttachmentsForRequestedCredentials( - agentContext: AgentContext, - indyProofRequest: ProofRequest, - requestedCredentials: RequestedCredentials - ): Promise { - const attachments: Attachment[] = [] - const credentialIds = new Set() - const requestedAttributesNames: (string | undefined)[] = [] - - // Get the credentialIds if it contains a hashlink - for (const [referent, requestedAttribute] of Object.entries(requestedCredentials.requestedAttributes)) { - // Find the requested Attributes - const requestedAttributes = indyProofRequest.requestedAttributes.get(referent) as ProofAttributeInfo - - // List the requested attributes - requestedAttributesNames.push(...(requestedAttributes.names ?? [requestedAttributes.name])) - - //Get credentialInfo - if (!requestedAttribute.credentialInfo) { - const indyCredentialInfo = await this.indyHolderService.getCredential( - agentContext, - requestedAttribute.credentialId - ) - requestedAttribute.credentialInfo = JsonTransformer.fromJSON(indyCredentialInfo, IndyCredentialInfo) - } - - // Find the attributes that have a hashlink as a value - for (const attribute of Object.values(requestedAttribute.credentialInfo.attributes)) { - if (attribute.toLowerCase().startsWith('hl:')) { - credentialIds.add(requestedAttribute.credentialId) - } - } - } - - // Only continues if there is an attribute value that contains a hashlink - for (const credentialId of credentialIds) { - // Get the credentialRecord that matches the ID - - const credentialRecord = await this.credentialRepository.getSingleByQuery(agentContext, { - credentialIds: [credentialId], - }) - - if (credentialRecord.linkedAttachments) { - // Get the credentials that have a hashlink as value and are requested - const requestedCredentials = credentialRecord.credentialAttributes?.filter( - (credential) => - credential.value.toLowerCase().startsWith('hl:') && requestedAttributesNames.includes(credential.name) - ) - - // Get the linked attachments that match the requestedCredentials - const linkedAttachments = credentialRecord.linkedAttachments.filter((attachment) => - requestedCredentials?.map((credential) => credential.value.split(':')[1]).includes(attachment.id) - ) - - if (linkedAttachments) { - attachments.push(...linkedAttachments) - } - } - } - - return attachments.length ? attachments : undefined - } - - public async shouldAutoRespondToProposal( - agentContext: AgentContext, - proofRecord: ProofExchangeRecord - ): Promise { - const proposal = await this.didCommMessageRepository.findAgentMessage(agentContext, { - associatedRecordId: proofRecord.id, - messageClass: V1ProposePresentationMessage, - }) - - if (!proposal) return false - MessageValidator.validateSync(proposal) - - // check the proposal against a possible previous request - const request = await this.didCommMessageRepository.findAgentMessage(agentContext, { - associatedRecordId: proofRecord.id, - messageClass: V1RequestPresentationMessage, - }) - - if (!request) return false - - const proofRequest = request.indyProofRequest - - if (!proofRequest) { - throw new V1PresentationProblemReportError( - `Missing required base64 or json encoded attachment data for presentation request with thread id ${request.threadId}`, - { problemCode: PresentationProblemReportReason.Abandoned } - ) - } - await validateOrReject(proofRequest) - - // Assert attribute and predicate (group) names do not match - checkProofRequestForDuplicates(proofRequest) - - const proposalAttributes = proposal.presentationProposal.attributes - const requestedAttributes = proofRequest.requestedAttributes - - const proposedAttributeNames = proposalAttributes.map((x) => x.name) - let requestedAttributeNames: string[] = [] - - const requestedAttributeList = Array.from(requestedAttributes.values()) - - requestedAttributeList.forEach((x) => { - if (x.name) { - requestedAttributeNames.push(x.name) - } else if (x.names) { - requestedAttributeNames = requestedAttributeNames.concat(x.names) - } - }) - - if (requestedAttributeNames.length > proposedAttributeNames.length) { - // more attributes are requested than have been proposed - return false - } - - requestedAttributeNames.forEach((x) => { - if (!proposedAttributeNames.includes(x)) { - this.logger.debug(`Attribute ${x} was requested but wasn't proposed.`) - return false - } - }) - - // assert that all requested attributes are provided - const providedPredicateNames = proposal.presentationProposal.predicates.map((x) => x.name) - proofRequest.requestedPredicates.forEach((x) => { - if (!providedPredicateNames.includes(x.name)) { - return false - } - }) - return true - } - - public async shouldAutoRespondToRequest( - agentContext: AgentContext, - proofRecord: ProofExchangeRecord - ): Promise { - const proposal = await this.didCommMessageRepository.findAgentMessage(agentContext, { - associatedRecordId: proofRecord.id, - messageClass: V1ProposePresentationMessage, - }) - - if (!proposal) { - return false - } - - const request = await this.didCommMessageRepository.findAgentMessage(agentContext, { - associatedRecordId: proofRecord.id, - messageClass: V1RequestPresentationMessage, - }) - - if (!request) { - throw new AriesFrameworkError( - `Expected to find a request message for ProofExchangeRecord with id ${proofRecord.id}` - ) - } - - const proofRequest = request.indyProofRequest - - // Assert attachment - if (!proofRequest) { - throw new V1PresentationProblemReportError( - `Missing required base64 or json encoded attachment data for presentation request with thread id ${request.threadId}`, - { problemCode: PresentationProblemReportReason.Abandoned } - ) - } - await validateOrReject(proofRequest) - - // Assert attribute and predicate (group) names do not match - checkProofRequestForDuplicates(proofRequest) - - const proposalAttributes = proposal.presentationProposal.attributes - const requestedAttributes = proofRequest.requestedAttributes - - const proposedAttributeNames = proposalAttributes.map((x) => x.name) - let requestedAttributeNames: string[] = [] - - const requestedAttributeList = Array.from(requestedAttributes.values()) - - requestedAttributeList.forEach((x) => { - if (x.name) { - requestedAttributeNames.push(x.name) - } else if (x.names) { - requestedAttributeNames = requestedAttributeNames.concat(x.names) - } - }) - - if (requestedAttributeNames.length > proposedAttributeNames.length) { - // more attributes are requested than have been proposed - return false - } - - requestedAttributeNames.forEach((x) => { - if (!proposedAttributeNames.includes(x)) { - this.logger.debug(`Attribute ${x} was requested but wasn't proposed.`) - return false - } - }) - - // assert that all requested attributes are provided - const providedPredicateNames = proposal.presentationProposal.predicates.map((x) => x.name) - proofRequest.requestedPredicates.forEach((x) => { - if (!providedPredicateNames.includes(x.name)) { - return false - } - }) - - return true - } - - public async shouldAutoRespondToPresentation( - agentContext: AgentContext, - proofRecord: ProofExchangeRecord - ): Promise { - this.logger.debug(`Should auto respond to presentation for proof record id: ${proofRecord.id}`) - return true - } - - public async getRequestedCredentialsForProofRequest( - agentContext: AgentContext, - options: GetRequestedCredentialsForProofRequestOptions - ): Promise> { - const requestMessage = await this.didCommMessageRepository.findAgentMessage(agentContext, { - associatedRecordId: options.proofRecord.id, - messageClass: V1RequestPresentationMessage, - }) - - const proposalMessage = await this.didCommMessageRepository.findAgentMessage(agentContext, { - associatedRecordId: options.proofRecord.id, - messageClass: V1ProposePresentationMessage, - }) - - const indyProofRequest = requestMessage?.requestPresentationAttachments - - if (!indyProofRequest) { - throw new AriesFrameworkError('Could not find proof request') - } - - const requestedCredentials: FormatRetrievedCredentialOptions<[IndyProofFormat]> = - await this.indyProofFormatService.getRequestedCredentialsForProofRequest(agentContext, { - attachment: indyProofRequest[0], - presentationProposal: proposalMessage?.presentationProposal, - config: options.config ?? undefined, - }) - return requestedCredentials - } - - public async autoSelectCredentialsForProofRequest( - options: FormatRetrievedCredentialOptions - ): Promise> { - return await this.indyProofFormatService.autoSelectCredentialsForProofRequest(options) - } - - public registerMessageHandlers( - dispatcher: Dispatcher, - agentConfig: AgentConfig, - proofResponseCoordinator: ProofResponseCoordinator, - mediationRecipientService: MediationRecipientService, - routingService: RoutingService - ): void { - dispatcher.registerMessageHandler( - new V1ProposePresentationHandler(this, agentConfig, proofResponseCoordinator, this.didCommMessageRepository) - ) - - dispatcher.registerMessageHandler( - new V1RequestPresentationHandler( - this, - agentConfig, - proofResponseCoordinator, - mediationRecipientService, - this.didCommMessageRepository, - routingService - ) - ) - - dispatcher.registerMessageHandler( - new V1PresentationHandler(this, agentConfig, proofResponseCoordinator, this.didCommMessageRepository) - ) - dispatcher.registerMessageHandler(new V1PresentationAckHandler(this)) - dispatcher.registerMessageHandler(new V1PresentationProblemReportHandler(this)) - } - - public async findRequestMessage( - agentContext: AgentContext, - proofRecordId: string - ): Promise { - return await this.didCommMessageRepository.findAgentMessage(agentContext, { - associatedRecordId: proofRecordId, - messageClass: V1RequestPresentationMessage, - }) - } - public async findPresentationMessage( - agentContext: AgentContext, - proofRecordId: string - ): Promise { - return await this.didCommMessageRepository.findAgentMessage(agentContext, { - associatedRecordId: proofRecordId, - messageClass: V1PresentationMessage, - }) - } - - public async findProposalMessage( - agentContext: AgentContext, - proofRecordId: string - ): Promise { - return await this.didCommMessageRepository.findAgentMessage(agentContext, { - associatedRecordId: proofRecordId, - messageClass: V1ProposePresentationMessage, - }) - } - - public async getFormatData( - agentContext: AgentContext, - proofRecordId: string - ): Promise> { - const [proposalMessage, requestMessage, presentationMessage] = await Promise.all([ - this.findProposalMessage(agentContext, proofRecordId), - this.findRequestMessage(agentContext, proofRecordId), - this.findPresentationMessage(agentContext, proofRecordId), - ]) - - const indyProposeProof = proposalMessage - ? JsonTransformer.toJSON(await this.rfc0592ProposalFromV1ProposeMessage(proposalMessage)) - : undefined - const indyRequestProof = requestMessage?.indyProofRequestJson ?? undefined - const indyPresentProof = presentationMessage?.indyProof ?? undefined - - return { - proposal: proposalMessage - ? { - indy: indyProposeProof, - } - : undefined, - request: requestMessage - ? { - indy: indyRequestProof, - } - : undefined, - presentation: presentationMessage - ? { - indy: indyPresentProof, - } - : undefined, - } - } - - private async rfc0592ProposalFromV1ProposeMessage( - proposalMessage: V1ProposePresentationMessage - ): Promise { - const indyFormat: IndyProposeProofFormat = { - name: 'Proof Request', - version: '1.0', - nonce: await this.wallet.generateNonce(), - attributes: proposalMessage.presentationProposal.attributes, - predicates: proposalMessage.presentationProposal.predicates, - } - - if (!indyFormat) { - throw new AriesFrameworkError('No Indy format found.') - } - - const preview = new PresentationPreview({ - attributes: indyFormat.attributes, - predicates: indyFormat.predicates, - }) - - return this.indyProofFormatService.createReferentForProofRequest(indyFormat, preview) - } - /** - * Retrieve all proof records - * - * @returns List containing all proof records - */ - public async getAll(agentContext: AgentContext): Promise { - return this.proofRepository.getAll(agentContext) - } - - /** - * Retrieve a proof record by connection id and thread id - * - * @param connectionId The connection id - * @param threadId The thread id - * @throws {RecordNotFoundError} If no record is found - * @throws {RecordDuplicateError} If multiple records are found - * @returns The proof record - */ - public async getByThreadAndConnectionId( - agentContext: AgentContext, - threadId: string, - connectionId?: string - ): Promise { - return this.proofRepository.getSingleByQuery(agentContext, { threadId, connectionId }) - } - - public async createAck( - gentContext: AgentContext, - options: CreateAckOptions - ): Promise<{ proofRecord: ProofExchangeRecord; message: AgentMessage }> { - const { proofRecord } = options - this.logger.debug(`Creating presentation ack for proof record with id ${proofRecord.id}`) - - // Assert - proofRecord.assertState(ProofState.PresentationReceived) - - // Create message - const ackMessage = new V1PresentationAckMessage({ - status: AckStatus.OK, - threadId: proofRecord.threadId, - }) - - // Update record - await this.updateState(gentContext, proofRecord, ProofState.Done) - - return { message: ackMessage, proofRecord } - } -} diff --git a/packages/core/src/modules/proofs/__tests__/V1ProofService.test.ts b/packages/core/src/modules/proofs/protocol/v1/__tests__/V1ProofProtocol.test.ts similarity index 63% rename from packages/core/src/modules/proofs/__tests__/V1ProofService.test.ts rename to packages/core/src/modules/proofs/protocol/v1/__tests__/V1ProofProtocol.test.ts index 3da57e27c1..0b1febb680 100644 --- a/packages/core/src/modules/proofs/__tests__/V1ProofService.test.ts +++ b/packages/core/src/modules/proofs/protocol/v1/__tests__/V1ProofProtocol.test.ts @@ -1,51 +1,43 @@ -import type { CustomProofTags } from './../repository/ProofExchangeRecord' -import type { AgentContext } from '../../../agent' -import type { Wallet } from '../../../wallet/Wallet' -import type { CredentialRepository } from '../../credentials/repository' -import type { ProofStateChangedEvent } from '../ProofEvents' +import type { AgentContext } from '../../../../../agent' +import type { AgentConfig } from '../../../../../agent/AgentConfig' +import type { ProofStateChangedEvent } from '../../../ProofEvents' +import type { CustomProofTags } from '../../../repository/ProofExchangeRecord' import { Subject } from 'rxjs' -import { getAgentConfig, getAgentContext, getMockConnection, mockFunction } from '../../../../tests/helpers' -import { EventEmitter } from '../../../agent/EventEmitter' -import { InboundMessageContext } from '../../../agent/models/InboundMessageContext' -import { Attachment, AttachmentData } from '../../../decorators/attachment/Attachment' -import { DidCommMessageRepository } from '../../../storage' -import { ConnectionService, DidExchangeState } from '../../connections' -import { IndyHolderService } from '../../indy/services/IndyHolderService' -import { IndyRevocationService } from '../../indy/services/IndyRevocationService' -import { IndyLedgerService } from '../../ledger/services' -import { ProofEventTypes } from '../ProofEvents' -import { PresentationProblemReportReason } from '../errors/PresentationProblemReportReason' -import { IndyProofFormatService } from '../formats/indy/IndyProofFormatService' -import { ProofState } from '../models/ProofState' -import { V1ProofService } from '../protocol/v1' -import { INDY_PROOF_REQUEST_ATTACHMENT_ID, V1RequestPresentationMessage } from '../protocol/v1/messages' -import { V1PresentationProblemReportMessage } from '../protocol/v1/messages/V1PresentationProblemReportMessage' -import { ProofExchangeRecord } from '../repository/ProofExchangeRecord' -import { ProofRepository } from '../repository/ProofRepository' - -import { credDef } from './fixtures' +import { getAgentConfig, getAgentContext, getMockConnection, mockFunction } from '../../../../../../tests/helpers' +import { EventEmitter } from '../../../../../agent/EventEmitter' +import { InboundMessageContext } from '../../../../../agent/models/InboundMessageContext' +import { Attachment, AttachmentData } from '../../../../../decorators/attachment/Attachment' +import { DidCommMessageRepository } from '../../../../../storage' +import { ConnectionService, DidExchangeState } from '../../../../connections' +import { ProofEventTypes } from '../../../ProofEvents' +import { PresentationProblemReportReason } from '../../../errors/PresentationProblemReportReason' +import { IndyProofFormatService } from '../../../formats/indy/IndyProofFormatService' +import { ProofState } from '../../../models/ProofState' +import { ProofExchangeRecord } from '../../../repository/ProofExchangeRecord' +import { ProofRepository } from '../../../repository/ProofRepository' +import { V1ProofProtocol } from '../V1ProofProtocol' +import { INDY_PROOF_REQUEST_ATTACHMENT_ID, V1RequestPresentationMessage } from '../messages' +import { V1PresentationProblemReportMessage } from '../messages/V1PresentationProblemReportMessage' // Mock classes -jest.mock('../repository/ProofRepository') -jest.mock('../../../modules/ledger/services/IndyLedgerService') -jest.mock('../../indy/services/IndyHolderService') -jest.mock('../../indy/services/IndyIssuerService') -jest.mock('../../indy/services/IndyVerifierService') -jest.mock('../../indy/services/IndyRevocationService') -jest.mock('../../connections/services/ConnectionService') -jest.mock('../../../storage/Repository') +jest.mock('../../../repository/ProofRepository') +jest.mock('../../../../../storage/didcomm/DidCommMessageRepository') +jest.mock('../../../../connections/services/ConnectionService') +jest.mock('../../../../../storage/Repository') // Mock typed object const ProofRepositoryMock = ProofRepository as jest.Mock -const IndyLedgerServiceMock = IndyLedgerService as jest.Mock -const IndyHolderServiceMock = IndyHolderService as jest.Mock -const IndyRevocationServiceMock = IndyRevocationService as jest.Mock const connectionServiceMock = ConnectionService as jest.Mock const didCommMessageRepositoryMock = DidCommMessageRepository as jest.Mock const indyProofFormatServiceMock = IndyProofFormatService as jest.Mock +const proofRepository = new ProofRepositoryMock() +const connectionService = new connectionServiceMock() +const didCommMessageRepository = new didCommMessageRepositoryMock() +const indyProofFormatService = new indyProofFormatServiceMock() + const connection = getMockConnection({ id: '123', state: DidExchangeState.Completed, @@ -78,7 +70,7 @@ const mockProofExchangeRecord = ({ } = {}) => { const requestPresentationMessage = new V1RequestPresentationMessage({ comment: 'some comment', - requestPresentationAttachments: [requestAttachment], + requestAttachments: [requestAttachment], }) const proofRecord = new ProofExchangeRecord({ @@ -93,58 +85,37 @@ const mockProofExchangeRecord = ({ return proofRecord } -describe('V1ProofService', () => { - let proofRepository: ProofRepository - let proofService: V1ProofService - let ledgerService: IndyLedgerService - let wallet: Wallet - let indyHolderService: IndyHolderService - let indyRevocationService: IndyRevocationService +describe('V1ProofProtocol', () => { let eventEmitter: EventEmitter - let credentialRepository: CredentialRepository - let connectionService: ConnectionService - let didCommMessageRepository: DidCommMessageRepository - let indyProofFormatService: IndyProofFormatService + let agentConfig: AgentConfig let agentContext: AgentContext + let proofProtocol: V1ProofProtocol beforeEach(() => { - const agentConfig = getAgentConfig('V1ProofServiceTest') - agentContext = getAgentContext() - proofRepository = new ProofRepositoryMock() - indyHolderService = new IndyHolderServiceMock() - indyRevocationService = new IndyRevocationServiceMock() - ledgerService = new IndyLedgerServiceMock() + // real objects + agentConfig = getAgentConfig('V1ProofProtocolTest') eventEmitter = new EventEmitter(agentConfig.agentDependencies, new Subject()) - connectionService = new connectionServiceMock() - didCommMessageRepository = new didCommMessageRepositoryMock() - indyProofFormatService = new indyProofFormatServiceMock() - agentContext = getAgentContext() - - proofService = new V1ProofService( - proofRepository, - didCommMessageRepository, - ledgerService, - wallet, + + agentContext = getAgentContext({ + registerInstances: [ + [ProofRepository, proofRepository], + [DidCommMessageRepository, didCommMessageRepository], + [EventEmitter, eventEmitter], + [ConnectionService, connectionService], + ], agentConfig, - connectionService, - eventEmitter, - credentialRepository, - indyProofFormatService, - indyHolderService, - indyRevocationService - ) - - mockFunction(ledgerService.getCredentialDefinition).mockReturnValue(Promise.resolve(credDef)) + }) + proofProtocol = new V1ProofProtocol({ indyProofFormat: indyProofFormatService }) }) - describe('processProofRequest', () => { + describe('processRequest', () => { let presentationRequest: V1RequestPresentationMessage let messageContext: InboundMessageContext beforeEach(() => { presentationRequest = new V1RequestPresentationMessage({ comment: 'abcd', - requestPresentationAttachments: [requestAttachment], + requestAttachments: [requestAttachment], }) messageContext = new InboundMessageContext(presentationRequest, { connection, @@ -156,7 +127,7 @@ describe('V1ProofService', () => { const repositorySaveSpy = jest.spyOn(proofRepository, 'save') // when - const returnedProofExchangeRecord = await proofService.processRequest(messageContext) + const returnedProofExchangeRecord = await proofProtocol.processRequest(messageContext) // then const expectedProofExchangeRecord = { @@ -178,7 +149,7 @@ describe('V1ProofService', () => { eventEmitter.on(ProofEventTypes.ProofStateChanged, eventListenerMock) // when - await proofService.processRequest(messageContext) + await proofProtocol.processRequest(messageContext) // then expect(eventListenerMock).toHaveBeenCalledWith({ @@ -261,7 +232,7 @@ describe('V1ProofService', () => { mockFunction(proofRepository.getSingleByQuery).mockReturnValue(Promise.resolve(proof)) // when - const returnedCredentialRecord = await proofService.processProblemReport(messageContext) + const returnedCredentialRecord = await proofProtocol.processProblemReport(messageContext) // then const expectedCredentialRecord = { diff --git a/packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-negotiation.test.ts b/packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-negotiation.test.ts index ee7b481cbb..e7f7a6f5f1 100644 --- a/packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-negotiation.test.ts +++ b/packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-negotiation.test.ts @@ -1,8 +1,8 @@ import type { Agent } from '../../../../../agent/Agent' import type { ConnectionRecord } from '../../../../connections/repository/ConnectionRecord' -import type { AcceptProofProposalOptions, NegotiateProposalOptions } from '../../../ProofsApiOptions' +import type { AcceptProofProposalOptions, NegotiateProofProposalOptions } from '../../../ProofsApiOptions' import type { ProofExchangeRecord } from '../../../repository/ProofExchangeRecord' -import type { PresentationPreview } from '../models/V1PresentationPreview' +import type { V1PresentationPreview } from '../models/V1PresentationPreview' import type { CredDefId } from 'indy-sdk' import { setupProofsTest, waitForProofExchangeRecord } from '../../../../../../tests/helpers' @@ -20,7 +20,7 @@ describe('Present Proof', () => { let aliceAgent: Agent let credDefId: CredDefId let aliceConnection: ConnectionRecord - let presentationPreview: PresentationPreview + let presentationPreview: V1PresentationPreview let faberProofExchangeRecord: ProofExchangeRecord let aliceProofExchangeRecord: ProofExchangeRecord let didCommMessageRepository: DidCommMessageRepository @@ -54,7 +54,6 @@ describe('Present Proof', () => { proofFormats: { indy: { name: 'proof-request', - nonce: '58d223e5-fc4d-4448-b74c-5eb11c6b558f', version: '1.0', attributes: presentationPreview.attributes.filter((attribute) => attribute.name !== 'name'), predicates: presentationPreview.predicates, @@ -135,7 +134,7 @@ describe('Present Proof', () => { }), } - const requestProofAsResponseOptions: NegotiateProposalOptions = { + const requestProofAsResponseOptions: NegotiateProofProposalOptions = { proofRecordId: faberProofExchangeRecord.id, proofFormats: { indy: { @@ -169,7 +168,7 @@ describe('Present Proof', () => { expect(request).toMatchObject({ type: 'https://didcomm.org/present-proof/1.0/request-presentation', id: expect.any(String), - requestPresentationAttachments: [ + requestAttachments: [ { id: 'libindy-request-presentation-0', mimeType: 'application/json', @@ -200,7 +199,6 @@ describe('Present Proof', () => { proofFormats: { indy: { name: 'proof-request', - nonce: '58d223e5-fc4d-4448-b74c-5eb11c6b558f', version: '1.0', attributes: presentationPreview.attributes.filter((attribute) => attribute.name === 'name'), predicates: presentationPreview.predicates, @@ -276,7 +274,7 @@ describe('Present Proof', () => { expect(request).toMatchObject({ type: 'https://didcomm.org/present-proof/1.0/request-presentation', id: expect.any(String), - requestPresentationAttachments: [ + requestAttachments: [ { id: 'libindy-request-presentation-0', mimeType: 'application/json', @@ -296,9 +294,8 @@ describe('Present Proof', () => { protocolVersion: 'v1', }) - const presentationProposalMessage = await aliceAgent.proofs.findProposalMessage(aliceProofExchangeRecord.id) - - expect(presentationProposalMessage).toMatchObject({ + const proposalMessage = await aliceAgent.proofs.findProposalMessage(aliceProofExchangeRecord.id) + expect(proposalMessage).toMatchObject({ type: 'https://didcomm.org/present-proof/1.0/propose-presentation', id: expect.any(String), comment: 'V1 propose proof test 2', @@ -327,29 +324,31 @@ describe('Present Proof', () => { aliceProofExchangeRecord.id )) as V1RequestPresentationMessage - const predicateKey = proofRequestMessage.indyProofRequest?.requestedPredicates?.keys().next().value - const predicate = Object.values(predicates)[0] - + const predicateKey = Object.keys(proofRequestMessage.indyProofRequest?.requested_predicates ?? {})[0] expect(proofRequestMessage.indyProofRequest).toMatchObject({ name: 'Proof Request', version: '1.0', - requestedAttributes: new Map( - Object.entries({ - '0': new ProofAttributeInfo({ - name: 'name', - restrictions: [ - new AttributeFilter({ - credentialDefinitionId: credDefId, - }), - ], - }), - }) - ), - requestedPredicates: new Map( - Object.entries({ - [predicateKey]: predicate, - }) - ), + requested_attributes: { + '0': { + name: 'name', + restrictions: [ + { + cred_def_id: credDefId, + }, + ], + }, + }, + requested_predicates: { + [predicateKey]: { + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credDefId, + }, + ], + }, + }, }) }) }) diff --git a/packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-presentation.test.ts b/packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-presentation.test.e2e.ts similarity index 93% rename from packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-presentation.test.ts rename to packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-presentation.test.e2e.ts index 8b32bbe14c..86095b8f01 100644 --- a/packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-presentation.test.ts +++ b/packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-presentation.test.e2e.ts @@ -1,19 +1,19 @@ import type { Agent } from '../../../../../agent/Agent' import type { ConnectionRecord } from '../../../../connections/repository/ConnectionRecord' -import type { PresentationPreview } from '../models/V1PresentationPreview' +import type { V1PresentationPreview } from '../models/V1PresentationPreview' import { setupProofsTest, waitForProofExchangeRecord } from '../../../../../../tests/helpers' import testLogger from '../../../../../../tests/logger' import { DidCommMessageRepository } from '../../../../../storage/didcomm' import { ProofState } from '../../../models/ProofState' -import { ProofExchangeRecord } from '../../../repository/ProofExchangeRecord' +import { ProofExchangeRecord } from '../../../repository' import { V1PresentationMessage, V1ProposePresentationMessage, V1RequestPresentationMessage } from '../messages' describe('Present Proof', () => { let faberAgent: Agent let aliceAgent: Agent let aliceConnection: ConnectionRecord - let presentationPreview: PresentationPreview + let presentationPreview: V1PresentationPreview let faberProofExchangeRecord: ProofExchangeRecord let aliceProofExchangeRecord: ProofExchangeRecord let didCommMessageRepository: DidCommMessageRepository @@ -21,8 +21,8 @@ describe('Present Proof', () => { beforeAll(async () => { testLogger.test('Initializing the agents') ;({ faberAgent, aliceAgent, aliceConnection, presentationPreview } = await setupProofsTest( - 'Faber agent', - 'Alice agent' + 'Faber Agent Proofs', + 'Alice Agent Proofs' )) }) @@ -47,7 +47,6 @@ describe('Present Proof', () => { proofFormats: { indy: { name: 'ProofRequest', - nonce: '58d223e5-fc4d-4448-b74c-5eb11c6b558f', version: '1.0', attributes: presentationPreview.attributes, predicates: presentationPreview.predicates, @@ -128,7 +127,7 @@ describe('Present Proof', () => { expect(request).toMatchObject({ type: 'https://didcomm.org/present-proof/1.0/request-presentation', id: expect.any(String), - requestPresentationAttachments: [ + requestAttachments: [ { id: 'libindy-request-presentation-0', mimeType: 'application/json', @@ -150,11 +149,8 @@ describe('Present Proof', () => { }) test(`Alice accepts presentation request from Faber`, async () => { - const requestedCredentials = await aliceAgent.proofs.autoSelectCredentialsForProofRequest({ + const requestedCredentials = await aliceAgent.proofs.selectCredentialsForRequest({ proofRecordId: aliceProofExchangeRecord.id, - config: { - filterByPresentationPreview: true, - }, }) const faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { @@ -217,7 +213,7 @@ describe('Present Proof', () => { }) // Faber accepts the presentation provided by Alice - await faberAgent.proofs.acceptPresentation(faberProofExchangeRecord.id) + await faberAgent.proofs.acceptPresentation({ proofRecordId: faberProofExchangeRecord.id }) // Alice waits until she received a presentation acknowledgement testLogger.test('Alice waits until she receives a presentation acknowledgement') diff --git a/packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-proposal.test.ts b/packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-proposal.test.e2e.ts similarity index 95% rename from packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-proposal.test.ts rename to packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-proposal.test.e2e.ts index 606f1e7ff8..f69048fece 100644 --- a/packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-proposal.test.ts +++ b/packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-proposal.test.e2e.ts @@ -1,7 +1,7 @@ import type { Agent } from '../../../../../agent/Agent' import type { ConnectionRecord } from '../../../../connections/repository/ConnectionRecord' import type { ProofExchangeRecord } from '../../../repository/ProofExchangeRecord' -import type { PresentationPreview } from '../models/V1PresentationPreview' +import type { V1PresentationPreview } from '../models/V1PresentationPreview' import { setupProofsTest, waitForProofExchangeRecord } from '../../../../../../tests/helpers' import testLogger from '../../../../../../tests/logger' @@ -13,7 +13,7 @@ describe('Present Proof', () => { let faberAgent: Agent let aliceAgent: Agent let aliceConnection: ConnectionRecord - let presentationPreview: PresentationPreview + let presentationPreview: V1PresentationPreview let faberProofExchangeRecord: ProofExchangeRecord let didCommMessageRepository: DidCommMessageRepository @@ -46,7 +46,6 @@ describe('Present Proof', () => { proofFormats: { indy: { name: 'ProofRequest', - nonce: '58d223e5-fc4d-4448-b74c-5eb11c6b558f', version: '1.0', attributes: presentationPreview.attributes, predicates: presentationPreview.predicates, diff --git a/packages/core/tests/v1-connectionless-proofs.test.ts b/packages/core/src/modules/proofs/protocol/v1/__tests__/v1-connectionless-proofs.e2e.test.ts similarity index 89% rename from packages/core/tests/v1-connectionless-proofs.test.ts rename to packages/core/src/modules/proofs/protocol/v1/__tests__/v1-connectionless-proofs.e2e.test.ts index d2fe8af3c3..fcfaaaebf1 100644 --- a/packages/core/tests/v1-connectionless-proofs.test.ts +++ b/packages/core/src/modules/proofs/protocol/v1/__tests__/v1-connectionless-proofs.e2e.test.ts @@ -1,36 +1,29 @@ -import type { SubjectMessage } from '../../../tests/transport/SubjectInboundTransport' -import type { ProofStateChangedEvent } from '../src/modules/proofs' +import type { SubjectMessage } from '../../../../../../../../tests/transport/SubjectInboundTransport' +import type { ProofStateChangedEvent } from '../../../ProofEvents' import { Subject, ReplaySubject } from 'rxjs' -import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport' -import { SubjectOutboundTransport } from '../../../tests/transport/SubjectOutboundTransport' -import { Agent } from '../src/agent/Agent' -import { Attachment, AttachmentData } from '../src/decorators/attachment/Attachment' -import { HandshakeProtocol } from '../src/modules/connections' -import { V1CredentialPreview } from '../src/modules/credentials' +import { SubjectInboundTransport } from '../../../../../../../../tests/transport/SubjectInboundTransport' +import { SubjectOutboundTransport } from '../../../../../../../../tests/transport/SubjectOutboundTransport' import { - PredicateType, - ProofState, - ProofAttributeInfo, - AttributeFilter, - ProofPredicateInfo, - AutoAcceptProof, - ProofEventTypes, -} from '../src/modules/proofs' -import { MediatorPickupStrategy } from '../src/modules/routing' -import { LinkedAttachment } from '../src/utils/LinkedAttachment' -import { uuid } from '../src/utils/uuid' - -import { - getAgentOptions, - issueCredential, - makeConnection, - prepareForIssuance, setupProofsTest, waitForProofExchangeRecordSubject, -} from './helpers' -import testLogger from './logger' + getAgentOptions, + prepareForIssuance, + makeConnection, + issueCredential, +} from '../../../../../../tests/helpers' +import testLogger from '../../../../../../tests/logger' +import { Agent } from '../../../../../agent/Agent' +import { Attachment, AttachmentData } from '../../../../../decorators/attachment/Attachment' +import { LinkedAttachment } from '../../../../../utils/LinkedAttachment' +import { uuid } from '../../../../../utils/uuid' +import { HandshakeProtocol } from '../../../../connections' +import { V1CredentialPreview } from '../../../../credentials' +import { MediatorPickupStrategy } from '../../../../routing' +import { ProofEventTypes } from '../../../ProofEvents' +import { ProofAttributeInfo, AttributeFilter, ProofPredicateInfo, PredicateType } from '../../../formats/indy/models' +import { AutoAcceptProof, ProofState } from '../../../models' describe('Present Proof', () => { let agents: Agent[] @@ -86,7 +79,6 @@ describe('Present Proof', () => { indy: { name: 'test-proof-request', version: '1.0', - nonce: '12345678901', requestedAttributes: attributes, requestedPredicates: predicates, }, @@ -104,11 +96,8 @@ describe('Present Proof', () => { let aliceProofExchangeRecord = await aliceProofExchangeRecordPromise testLogger.test('Alice accepts presentation request from Faber') - const requestedCredentials = await aliceAgent.proofs.autoSelectCredentialsForProofRequest({ + const requestedCredentials = await aliceAgent.proofs.selectCredentialsForRequest({ proofRecordId: aliceProofExchangeRecord.id, - config: { - filterByPresentationPreview: true, - }, }) const faberProofExchangeRecordPromise = waitForProofExchangeRecordSubject(faberReplay, { @@ -133,7 +122,7 @@ describe('Present Proof', () => { }) // Faber accepts presentation - await faberAgent.proofs.acceptPresentation(faberProofExchangeRecord.id) + await faberAgent.proofs.acceptPresentation({ proofRecordId: faberProofExchangeRecord.id }) // Alice waits till it receives presentation ack aliceProofExchangeRecord = await aliceProofExchangeRecordPromise @@ -189,7 +178,6 @@ describe('Present Proof', () => { indy: { name: 'test-proof-request', version: '1.0', - nonce: '12345678901', requestedAttributes: attributes, requestedPredicates: predicates, }, @@ -353,7 +341,6 @@ describe('Present Proof', () => { indy: { name: 'test-proof-request', version: '1.0', - nonce: '12345678901', requestedAttributes: attributes, requestedPredicates: predicates, }, diff --git a/packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-request.test.ts b/packages/core/src/modules/proofs/protocol/v1/__tests__/v1-indy-proof-request.e2e.test.ts similarity index 70% rename from packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-request.test.ts rename to packages/core/src/modules/proofs/protocol/v1/__tests__/v1-indy-proof-request.e2e.test.ts index c3bccd9f9b..8c9278b879 100644 --- a/packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-request.test.ts +++ b/packages/core/src/modules/proofs/protocol/v1/__tests__/v1-indy-proof-request.e2e.test.ts @@ -1,23 +1,19 @@ import type { Agent } from '../../../../../agent/Agent' -import type { ConnectionRecord } from '../../../../connections/repository/ConnectionRecord' -import type { AcceptProofProposalOptions } from '../../../ProofsApiOptions' -import type { ProofExchangeRecord } from '../../../repository/ProofExchangeRecord' -import type { PresentationPreview } from '../models/V1PresentationPreview' +import type { ConnectionRecord } from '../../../../connections' +import type { ProofExchangeRecord } from '../../../repository' +import type { V1PresentationPreview } from '../models' import { setupProofsTest, waitForProofExchangeRecord } from '../../../../../../tests/helpers' import testLogger from '../../../../../../tests/logger' -import { DidCommMessageRepository } from '../../../../../storage/didcomm' -import { ProofState } from '../../../models/ProofState' -import { V1ProposePresentationMessage, V1RequestPresentationMessage } from '../messages' +import { ProofState } from '../../../models' -describe('Present Proof', () => { +describe('Present Proof | V1ProofProtocol', () => { let faberAgent: Agent let aliceAgent: Agent let aliceConnection: ConnectionRecord - let presentationPreview: PresentationPreview + let presentationPreview: V1PresentationPreview let faberProofExchangeRecord: ProofExchangeRecord let aliceProofExchangeRecord: ProofExchangeRecord - let didCommMessageRepository: DidCommMessageRepository beforeAll(async () => { testLogger.test('Initializing the agents') @@ -47,8 +43,7 @@ describe('Present Proof', () => { protocolVersion: 'v1', proofFormats: { indy: { - name: 'ProofRequest', - nonce: '58d223e5-fc4d-4448-b74c-5eb11c6b558f', + name: 'Proof Request', version: '1.0', attributes: presentationPreview.attributes, predicates: presentationPreview.predicates, @@ -60,13 +55,7 @@ describe('Present Proof', () => { testLogger.test('Faber waits for presentation from Alice') faberProofExchangeRecord = await faberProofExchangeRecordPromise - didCommMessageRepository = faberAgent.dependencyManager.resolve(DidCommMessageRepository) - - const proposal = await didCommMessageRepository.findAgentMessage(faberAgent.context, { - associatedRecordId: faberProofExchangeRecord.id, - messageClass: V1ProposePresentationMessage, - }) - + const proposal = await faberAgent.proofs.findProposalMessage(faberProofExchangeRecord.id) expect(proposal).toMatchObject({ type: 'https://didcomm.org/present-proof/1.0/propose-presentation', id: expect.any(String), @@ -103,34 +92,26 @@ describe('Present Proof', () => { }) }) - test(`Faber accepts the Proposal send by Alice and Creates Proof Request`, async () => { - // Accept Proposal - const acceptProposalOptions: AcceptProofProposalOptions = { - proofRecordId: faberProofExchangeRecord.id, - } - + test(`Faber accepts the Proposal sent by Alice and Creates Proof Request`, async () => { const aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { threadId: faberProofExchangeRecord.threadId, state: ProofState.RequestReceived, }) + // Accept Proposal testLogger.test('Faber accepts presentation proposal from Alice') - faberProofExchangeRecord = await faberAgent.proofs.acceptProposal(acceptProposalOptions) + faberProofExchangeRecord = await faberAgent.proofs.acceptProposal({ + proofRecordId: faberProofExchangeRecord.id, + }) testLogger.test('Alice waits for proof request from Faber') aliceProofExchangeRecord = await aliceProofExchangeRecordPromise - didCommMessageRepository = faberAgent.dependencyManager.resolve(DidCommMessageRepository) - - const request = await didCommMessageRepository.findAgentMessage(faberAgent.context, { - associatedRecordId: faberProofExchangeRecord.id, - messageClass: V1RequestPresentationMessage, - }) - + const request = await faberAgent.proofs.findRequestMessage(faberProofExchangeRecord.id) expect(request).toMatchObject({ type: 'https://didcomm.org/present-proof/1.0/request-presentation', id: expect.any(String), - requestPresentationAttachments: [ + requestAttachments: [ { id: 'libindy-request-presentation-0', mimeType: 'application/json', @@ -143,6 +124,7 @@ describe('Present Proof', () => { threadId: faberProofExchangeRecord.threadId, }, }) + expect(aliceProofExchangeRecord).toMatchObject({ id: expect.anything(), threadId: faberProofExchangeRecord.threadId, diff --git a/packages/core/tests/v1-indy-proofs.test.ts b/packages/core/src/modules/proofs/protocol/v1/__tests__/v1-indy-proofs.e2e.test.ts similarity index 81% rename from packages/core/tests/v1-indy-proofs.test.ts rename to packages/core/src/modules/proofs/protocol/v1/__tests__/v1-indy-proofs.e2e.test.ts index 440da6a5d4..918673b0b3 100644 --- a/packages/core/tests/v1-indy-proofs.test.ts +++ b/packages/core/src/modules/proofs/protocol/v1/__tests__/v1-indy-proofs.e2e.test.ts @@ -1,42 +1,29 @@ -import type { Agent, ConnectionRecord } from '../src' -import type { AcceptProofProposalOptions } from '../src/modules/proofs/ProofsApiOptions' -import type { PresentationPreview } from '../src/modules/proofs/protocol/v1/models/V1PresentationPreview' -import type { CredDefId } from 'indy-sdk' - -import { ProofExchangeRecord } from '../src' -import { getGroupKeysFromIndyProofFormatData } from '../src/modules/proofs/__tests__/groupKeys' -import { - ProofAttributeInfo, - AttributeFilter, - ProofPredicateInfo, - PredicateType, -} from '../src/modules/proofs/formats/indy/models' -import { ProofState } from '../src/modules/proofs/models/ProofState' -import { - V1ProposePresentationMessage, - V1RequestPresentationMessage, - V1PresentationMessage, -} from '../src/modules/proofs/protocol/v1/messages' -import { DidCommMessageRepository } from '../src/storage/didcomm' - -import { setupProofsTest, waitForProofExchangeRecord } from './helpers' -import testLogger from './logger' +import type { Agent } from '../../../../../agent/Agent' +import type { ConnectionRecord } from '../../../../connections' +import type { V1PresentationPreview } from '../models' + +import { setupProofsTest, waitForProofExchangeRecord } from '../../../../../../tests/helpers' +import testLogger from '../../../../../../tests/logger' +import { getGroupKeysFromIndyProofFormatData } from '../../../formats/indy/__tests__/groupKeys' +import { ProofAttributeInfo, AttributeFilter, ProofPredicateInfo, PredicateType } from '../../../formats/indy/models' +import { ProofState } from '../../../models' +import { ProofExchangeRecord } from '../../../repository' +import { V1ProposePresentationMessage, V1RequestPresentationMessage, V1PresentationMessage } from '../messages' describe('Present Proof', () => { let faberAgent: Agent let aliceAgent: Agent - let credDefId: CredDefId + let credDefId: string let faberConnection: ConnectionRecord let aliceConnection: ConnectionRecord let faberProofExchangeRecord: ProofExchangeRecord let aliceProofExchangeRecord: ProofExchangeRecord - let presentationPreview: PresentationPreview - let didCommMessageRepository: DidCommMessageRepository + let presentationPreview: V1PresentationPreview beforeAll(async () => { testLogger.test('Initializing the agents') ;({ faberAgent, aliceAgent, credDefId, faberConnection, aliceConnection, presentationPreview } = - await setupProofsTest('Faber agent', 'Alice agent')) + await setupProofsTest('Faber agent v1', 'Alice agent v1')) testLogger.test('Issuing second credential') }) @@ -70,14 +57,7 @@ describe('Present Proof', () => { // Faber waits for a presentation proposal from Alice testLogger.test('Faber waits for a presentation proposal from Alice') faberProofExchangeRecord = await faberProofExchangeRecordPromise - - didCommMessageRepository = faberAgent.dependencyManager.resolve(DidCommMessageRepository) - - const proposal = await didCommMessageRepository.findAgentMessage(faberAgent.context, { - associatedRecordId: faberProofExchangeRecord.id, - messageClass: V1ProposePresentationMessage, - }) - + const proposal = await faberAgent.proofs.findProposalMessage(faberProofExchangeRecord.id) expect(proposal).toMatchObject({ type: 'https://didcomm.org/present-proof/1.0/propose-presentation', id: expect.any(String), @@ -112,10 +92,6 @@ describe('Present Proof', () => { protocolVersion: 'v1', }) - const acceptProposalOptions: AcceptProofProposalOptions = { - proofRecordId: faberProofExchangeRecord.id, - } - let aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { threadId: aliceProofExchangeRecord.threadId, state: ProofState.RequestReceived, @@ -123,21 +99,19 @@ describe('Present Proof', () => { // Faber accepts the presentation proposal from Alice testLogger.test('Faber accepts presentation proposal from Alice') - faberProofExchangeRecord = await faberAgent.proofs.acceptProposal(acceptProposalOptions) + faberProofExchangeRecord = await faberAgent.proofs.acceptProposal({ + proofRecordId: faberProofExchangeRecord.id, + }) // Alice waits for presentation request from Faber testLogger.test('Alice waits for presentation request from Faber') aliceProofExchangeRecord = await aliceProofExchangeRecordPromise - const request = await didCommMessageRepository.findAgentMessage(faberAgent.context, { - associatedRecordId: faberProofExchangeRecord.id, - messageClass: V1RequestPresentationMessage, - }) - + const request = await faberAgent.proofs.findRequestMessage(faberProofExchangeRecord.id) expect(request).toMatchObject({ type: 'https://didcomm.org/present-proof/1.0/request-presentation', id: expect.any(String), - requestPresentationAttachments: [ + requestAttachments: [ { id: 'libindy-request-presentation-0', mimeType: 'application/json', @@ -154,11 +128,8 @@ describe('Present Proof', () => { // Alice retrieves the requested credentials and accepts the presentation request testLogger.test('Alice accepts presentation request from Faber') - const requestedCredentials = await aliceAgent.proofs.autoSelectCredentialsForProofRequest({ + const requestedCredentials = await aliceAgent.proofs.selectCredentialsForRequest({ proofRecordId: aliceProofExchangeRecord.id, - config: { - filterByPresentationPreview: true, - }, }) faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { @@ -175,11 +146,7 @@ describe('Present Proof', () => { testLogger.test('Faber waits for presentation from Alice') faberProofExchangeRecord = await faberProofExchangeRecordPromise - const presentation = await didCommMessageRepository.findAgentMessage(faberAgent.context, { - associatedRecordId: faberProofExchangeRecord.id, - messageClass: V1PresentationMessage, - }) - + const presentation = await faberAgent.proofs.findPresentationMessage(faberProofExchangeRecord.id) expect(presentation).toMatchObject({ type: 'https://didcomm.org/present-proof/1.0/presentation', id: expect.any(String), @@ -192,15 +159,15 @@ describe('Present Proof', () => { }, }, ], - appendedAttachments: [ - { - id: expect.any(String), - filename: expect.any(String), - data: { - base64: expect.any(String), - }, - }, - ], + // appendedAttachments: [ + // { + // id: expect.any(String), + // filename: expect.any(String), + // data: { + // base64: expect.any(String), + // }, + // }, + // ], thread: { threadId: expect.any(String), }, @@ -220,7 +187,7 @@ describe('Present Proof', () => { // Faber accepts the presentation provided by Alice testLogger.test('Faber accepts the presentation provided by Alice') - await faberAgent.proofs.acceptPresentation(faberProofExchangeRecord.id) + await faberAgent.proofs.acceptPresentation({ proofRecordId: faberProofExchangeRecord.id }) // Alice waits until she received a presentation acknowledgement testLogger.test('Alice waits until she receives a presentation acknowledgement') @@ -390,17 +357,11 @@ describe('Present Proof', () => { testLogger.test('Alice waits for presentation request from Faber') aliceProofExchangeRecord = await aliceProofExchangeRecordPromise - didCommMessageRepository = faberAgent.dependencyManager.resolve(DidCommMessageRepository) - - const request = await didCommMessageRepository.findAgentMessage(faberAgent.context, { - associatedRecordId: faberProofExchangeRecord.id, - messageClass: V1RequestPresentationMessage, - }) - + const request = await faberAgent.proofs.findRequestMessage(faberProofExchangeRecord.id) expect(request).toMatchObject({ type: 'https://didcomm.org/present-proof/1.0/request-presentation', id: expect.any(String), - requestPresentationAttachments: [ + requestAttachments: [ { id: 'libindy-request-presentation-0', mimeType: 'application/json', @@ -421,11 +382,8 @@ describe('Present Proof', () => { // Alice retrieves the requested credentials and accepts the presentation request testLogger.test('Alice accepts presentation request from Faber') - const requestedCredentials = await aliceAgent.proofs.autoSelectCredentialsForProofRequest({ + const requestedCredentials = await aliceAgent.proofs.selectCredentialsForRequest({ proofRecordId: aliceProofExchangeRecord.id, - config: { - filterByPresentationPreview: true, - }, }) const faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { @@ -442,11 +400,7 @@ describe('Present Proof', () => { testLogger.test('Faber waits for presentation from Alice') faberProofExchangeRecord = await faberProofExchangeRecordPromise - const presentation = await didCommMessageRepository.findAgentMessage(faberAgent.context, { - associatedRecordId: faberProofExchangeRecord.id, - messageClass: V1PresentationMessage, - }) - + const presentation = await faberAgent.proofs.findPresentationMessage(faberProofExchangeRecord.id) expect(presentation).toMatchObject({ type: 'https://didcomm.org/present-proof/1.0/presentation', id: expect.any(String), @@ -459,15 +413,15 @@ describe('Present Proof', () => { }, }, ], - appendedAttachments: [ - { - id: expect.any(String), - filename: expect.any(String), - data: { - base64: expect.any(String), - }, - }, - ], + // appendedAttachments: [ + // { + // id: expect.any(String), + // filename: expect.any(String), + // data: { + // base64: expect.any(String), + // }, + // }, + // ], thread: { threadId: expect.any(String), }, @@ -487,7 +441,7 @@ describe('Present Proof', () => { // Faber accepts the presentation testLogger.test('Faber accept the presentation from Alice') - await faberAgent.proofs.acceptPresentation(faberProofExchangeRecord.id) + await faberAgent.proofs.acceptPresentation({ proofRecordId: faberProofExchangeRecord.id }) // Alice waits until she receives a presentation acknowledgement testLogger.test('Alice waits for acceptance by Faber') @@ -548,7 +502,6 @@ describe('Present Proof', () => { indy: { name: 'proof-request', version: '1.0', - nonce: '1298236324864', requestedAttributes: attributes, requestedPredicates: predicates, }, @@ -604,7 +557,6 @@ describe('Present Proof', () => { indy: { name: 'proof-request', version: '1.0', - nonce: '1298236324864', requestedAttributes: attributes, requestedPredicates: predicates, }, @@ -615,17 +567,11 @@ describe('Present Proof', () => { testLogger.test('Alice waits for presentation request from Faber') aliceProofExchangeRecord = await aliceProofExchangeRecordPromise - didCommMessageRepository = faberAgent.dependencyManager.resolve(DidCommMessageRepository) - - const request = await didCommMessageRepository.findAgentMessage(faberAgent.context, { - associatedRecordId: faberProofExchangeRecord.id, - messageClass: V1RequestPresentationMessage, - }) - + const request = await faberAgent.proofs.findRequestMessage(faberProofExchangeRecord.id) expect(request).toMatchObject({ type: 'https://didcomm.org/present-proof/1.0/request-presentation', id: expect.any(String), - requestPresentationAttachments: [ + requestAttachments: [ { id: 'libindy-request-presentation-0', mimeType: 'application/json', @@ -648,10 +594,10 @@ describe('Present Proof', () => { state: ProofState.Abandoned, }) - aliceProofExchangeRecord = await aliceAgent.proofs.sendProblemReport( - aliceProofExchangeRecord.id, - 'Problem inside proof request' - ) + aliceProofExchangeRecord = await aliceAgent.proofs.sendProblemReport({ + proofRecordId: aliceProofExchangeRecord.id, + description: 'Problem inside proof request', + }) faberProofExchangeRecord = await faberProofExchangeRecordPromise diff --git a/packages/core/tests/v1-proofs-auto-accept.test.ts b/packages/core/src/modules/proofs/protocol/v1/__tests__/v1-proofs-auto-accept.e2e.test.ts similarity index 82% rename from packages/core/tests/v1-proofs-auto-accept.test.ts rename to packages/core/src/modules/proofs/protocol/v1/__tests__/v1-proofs-auto-accept.e2e.test.ts index 6c222e70e9..c8a116e8ed 100644 --- a/packages/core/tests/v1-proofs-auto-accept.test.ts +++ b/packages/core/src/modules/proofs/protocol/v1/__tests__/v1-proofs-auto-accept.e2e.test.ts @@ -1,17 +1,11 @@ -import type { Agent, ConnectionRecord } from '../src' -import type { PresentationPreview } from '../src/modules/proofs/protocol/v1/models/V1PresentationPreview' +import type { Agent } from '../../../../../agent/Agent' +import type { ConnectionRecord } from '../../../../connections' +import type { V1PresentationPreview } from '../models' -import { - AutoAcceptProof, - ProofState, - ProofAttributeInfo, - AttributeFilter, - ProofPredicateInfo, - PredicateType, -} from '../src' - -import { setupProofsTest, waitForProofExchangeRecord } from './helpers' -import testLogger from './logger' +import { setupProofsTest, waitForProofExchangeRecord } from '../../../../../../tests/helpers' +import testLogger from '../../../../../../tests/logger' +import { ProofAttributeInfo, AttributeFilter, ProofPredicateInfo, PredicateType } from '../../../formats/indy/models' +import { AutoAcceptProof, ProofState } from '../../../models' describe('Auto accept present proof', () => { let faberAgent: Agent @@ -19,9 +13,9 @@ describe('Auto accept present proof', () => { let credDefId: string let faberConnection: ConnectionRecord let aliceConnection: ConnectionRecord - let presentationPreview: PresentationPreview + let presentationPreview: V1PresentationPreview - describe('Auto accept on `always`', () => { + describe("Auto accept on 'always'", () => { beforeAll(async () => { ;({ faberAgent, aliceAgent, credDefId, faberConnection, aliceConnection, presentationPreview } = await setupProofsTest( @@ -37,7 +31,7 @@ describe('Auto accept present proof', () => { await aliceAgent.wallet.delete() }) - test('Alice starts with proof proposal to Faber, both with autoAcceptProof on `always`', async () => { + test("Alice starts with proof proposal to Faber, both with autoAcceptProof on 'always'", async () => { testLogger.test('Alice sends presentation proposal to Faber') await aliceAgent.proofs.proposeProof({ @@ -45,7 +39,6 @@ describe('Auto accept present proof', () => { protocolVersion: 'v1', proofFormats: { indy: { - nonce: '58d223e5-fc4d-4448-b74c-5eb11c6b558f', name: 'abc', version: '1.0', attributes: presentationPreview.attributes, @@ -62,7 +55,7 @@ describe('Auto accept present proof', () => { ]) }) - test('Faber starts with proof requests to Alice, both with autoAcceptProof on `always`', async () => { + test("Faber starts with proof requests to Alice, both with autoAcceptProof on 'always'", async () => { testLogger.test('Faber sends presentation request to Alice') const attributes = { name: new ProofAttributeInfo({ @@ -94,7 +87,6 @@ describe('Auto accept present proof', () => { indy: { name: 'proof-request', version: '1.0', - nonce: '1298236324864', requestedAttributes: attributes, requestedPredicates: predicates, }, @@ -109,7 +101,7 @@ describe('Auto accept present proof', () => { }) }) - describe('Auto accept on `contentApproved`', () => { + describe("Auto accept on 'contentApproved'", () => { beforeAll(async () => { testLogger.test('Initializing the agents') ;({ faberAgent, aliceAgent, credDefId, faberConnection, aliceConnection, presentationPreview } = @@ -127,7 +119,7 @@ describe('Auto accept present proof', () => { await aliceAgent.wallet.delete() }) - test('Alice starts with proof proposal to Faber, both with autoacceptproof on `contentApproved`', async () => { + test("Alice starts with proof proposal to Faber, both with autoAcceptProof on 'contentApproved'", async () => { testLogger.test('Alice sends presentation proposal to Faber') const aliceProofExchangeRecord = await aliceAgent.proofs.proposeProof({ @@ -135,7 +127,6 @@ describe('Auto accept present proof', () => { protocolVersion: 'v1', proofFormats: { indy: { - nonce: '1298236324864', name: 'abc', version: '1.0', attributes: presentationPreview.attributes, @@ -159,7 +150,7 @@ describe('Auto accept present proof', () => { ]) }) - test('Faber starts with proof requests to Alice, both with autoacceptproof on `contentApproved`', async () => { + test("Faber starts with proof requests to Alice, both with autoAcceptProof on 'contentApproved'", async () => { testLogger.test('Faber sends presentation request to Alice') const attributes = { name: new ProofAttributeInfo({ @@ -191,7 +182,6 @@ describe('Auto accept present proof', () => { indy: { name: 'proof-request', version: '1.0', - nonce: '1298236324866', requestedAttributes: attributes, requestedPredicates: predicates, }, @@ -203,7 +193,7 @@ describe('Auto accept present proof', () => { state: ProofState.RequestReceived, }) - const { proofFormats } = await aliceAgent.proofs.autoSelectCredentialsForProofRequest({ proofRecordId }) + const { proofFormats } = await aliceAgent.proofs.selectCredentialsForRequest({ proofRecordId }) await aliceAgent.proofs.acceptRequest({ proofRecordId, proofFormats }) await Promise.all([ diff --git a/packages/core/src/modules/proofs/protocol/v1/handlers/V1PresentationAckHandler.ts b/packages/core/src/modules/proofs/protocol/v1/handlers/V1PresentationAckHandler.ts index cdc9f6d797..c8f331c3a8 100644 --- a/packages/core/src/modules/proofs/protocol/v1/handlers/V1PresentationAckHandler.ts +++ b/packages/core/src/modules/proofs/protocol/v1/handlers/V1PresentationAckHandler.ts @@ -1,17 +1,17 @@ import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' -import type { V1ProofService } from '../V1ProofService' +import type { V1ProofProtocol } from '../V1ProofProtocol' import { V1PresentationAckMessage } from '../messages' export class V1PresentationAckHandler implements MessageHandler { - private proofService: V1ProofService + private proofProtocol: V1ProofProtocol public supportedMessages = [V1PresentationAckMessage] - public constructor(proofService: V1ProofService) { - this.proofService = proofService + public constructor(proofProtocol: V1ProofProtocol) { + this.proofProtocol = proofProtocol } public async handle(messageContext: MessageHandlerInboundMessage) { - await this.proofService.processAck(messageContext) + await this.proofProtocol.processAck(messageContext) } } diff --git a/packages/core/src/modules/proofs/protocol/v1/handlers/V1PresentationHandler.ts b/packages/core/src/modules/proofs/protocol/v1/handlers/V1PresentationHandler.ts index 6918979829..e5553b1283 100644 --- a/packages/core/src/modules/proofs/protocol/v1/handlers/V1PresentationHandler.ts +++ b/packages/core/src/modules/proofs/protocol/v1/handlers/V1PresentationHandler.ts @@ -1,77 +1,69 @@ -import type { AgentConfig } from '../../../../../agent/AgentConfig' import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' -import type { DidCommMessageRepository } from '../../../../../storage' -import type { ProofResponseCoordinator } from '../../../ProofResponseCoordinator' import type { ProofExchangeRecord } from '../../../repository' -import type { V1ProofService } from '../V1ProofService' +import type { V1ProofProtocol } from '../V1ProofProtocol' import { OutboundMessageContext } from '../../../../../agent/models' +import { DidCommMessageRepository } from '../../../../../storage' import { V1PresentationMessage, V1RequestPresentationMessage } from '../messages' export class V1PresentationHandler implements MessageHandler { - private proofService: V1ProofService - private agentConfig: AgentConfig - private proofResponseCoordinator: ProofResponseCoordinator - private didCommMessageRepository: DidCommMessageRepository + private proofProtocol: V1ProofProtocol public supportedMessages = [V1PresentationMessage] - public constructor( - proofService: V1ProofService, - agentConfig: AgentConfig, - proofResponseCoordinator: ProofResponseCoordinator, - didCommMessageRepository: DidCommMessageRepository - ) { - this.proofService = proofService - this.agentConfig = agentConfig - this.proofResponseCoordinator = proofResponseCoordinator - this.didCommMessageRepository = didCommMessageRepository + public constructor(proofProtocol: V1ProofProtocol) { + this.proofProtocol = proofProtocol } public async handle(messageContext: MessageHandlerInboundMessage) { - const proofRecord = await this.proofService.processPresentation(messageContext) + const proofRecord = await this.proofProtocol.processPresentation(messageContext) - const shouldAutoRespond = await this.proofResponseCoordinator.shouldAutoRespondToPresentation( - messageContext.agentContext, - proofRecord - ) + const shouldAutoRespond = await this.proofProtocol.shouldAutoRespondToPresentation(messageContext.agentContext, { + presentationMessage: messageContext.message, + proofRecord, + }) if (shouldAutoRespond) { - return await this.createAck(proofRecord, messageContext) + return await this.acceptPresentation(proofRecord, messageContext) } } - private async createAck( - record: ProofExchangeRecord, + private async acceptPresentation( + proofRecord: ProofExchangeRecord, messageContext: MessageHandlerInboundMessage ) { - this.agentConfig.logger.info( - `Automatically sending acknowledgement with autoAccept on ${this.agentConfig.autoAcceptProofs}` - ) - - const { message, proofRecord } = await this.proofService.createAck(messageContext.agentContext, { - proofRecord: record, - }) - - const requestMessage = await this.didCommMessageRepository.findAgentMessage(messageContext.agentContext, { - associatedRecordId: proofRecord.id, - messageClass: V1RequestPresentationMessage, - }) - - const presentationMessage = await this.didCommMessageRepository.findAgentMessage(messageContext.agentContext, { - associatedRecordId: proofRecord.id, - messageClass: V1PresentationMessage, - }) + messageContext.agentContext.config.logger.info(`Automatically sending acknowledgement with autoAccept`) if (messageContext.connection) { + const { message } = await this.proofProtocol.acceptPresentation(messageContext.agentContext, { + proofRecord, + }) + return new OutboundMessageContext(message, { agentContext: messageContext.agentContext, connection: messageContext.connection, associatedRecord: proofRecord, }) - } else if (requestMessage?.service && presentationMessage?.service) { - const recipientService = presentationMessage?.service + } else if (messageContext.message.service) { + const { message } = await this.proofProtocol.acceptPresentation(messageContext.agentContext, { + proofRecord, + }) + + const didCommMessageRepository = messageContext.agentContext.dependencyManager.resolve(DidCommMessageRepository) + const requestMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V1RequestPresentationMessage, + }) + + const recipientService = messageContext.message.service const ourService = requestMessage?.service + if (!ourService) { + messageContext.agentContext.config.logger.error( + `Could not automatically create presentation ack. Missing ourService on request message` + ) + return + } + return new OutboundMessageContext(message, { agentContext: messageContext.agentContext, serviceParams: { @@ -81,6 +73,6 @@ export class V1PresentationHandler implements MessageHandler { }) } - this.agentConfig.logger.error(`Could not automatically create presentation ack`) + messageContext.agentContext.config.logger.error(`Could not automatically create presentation ack`) } } diff --git a/packages/core/src/modules/proofs/protocol/v1/handlers/V1PresentationProblemReportHandler.ts b/packages/core/src/modules/proofs/protocol/v1/handlers/V1PresentationProblemReportHandler.ts index e3c7f97410..eee0266a68 100644 --- a/packages/core/src/modules/proofs/protocol/v1/handlers/V1PresentationProblemReportHandler.ts +++ b/packages/core/src/modules/proofs/protocol/v1/handlers/V1PresentationProblemReportHandler.ts @@ -1,17 +1,17 @@ import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' -import type { V1ProofService } from '../V1ProofService' +import type { V1ProofProtocol } from '../V1ProofProtocol' import { V1PresentationProblemReportMessage } from '../messages/V1PresentationProblemReportMessage' export class V1PresentationProblemReportHandler implements MessageHandler { - private proofService: V1ProofService + private proofProtocol: V1ProofProtocol public supportedMessages = [V1PresentationProblemReportMessage] - public constructor(proofService: V1ProofService) { - this.proofService = proofService + public constructor(proofProtocol: V1ProofProtocol) { + this.proofProtocol = proofProtocol } public async handle(messageContext: MessageHandlerInboundMessage) { - await this.proofService.processProblemReport(messageContext) + await this.proofProtocol.processProblemReport(messageContext) } } diff --git a/packages/core/src/modules/proofs/protocol/v1/handlers/V1ProposePresentationHandler.ts b/packages/core/src/modules/proofs/protocol/v1/handlers/V1ProposePresentationHandler.ts index e69764d3d5..113c695c98 100644 --- a/packages/core/src/modules/proofs/protocol/v1/handlers/V1ProposePresentationHandler.ts +++ b/packages/core/src/modules/proofs/protocol/v1/handlers/V1ProposePresentationHandler.ts @@ -1,107 +1,44 @@ -import type { AgentConfig } from '../../../../../agent/AgentConfig' import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' -import type { DidCommMessageRepository } from '../../../../../storage/didcomm/DidCommMessageRepository' -import type { ProofResponseCoordinator } from '../../../ProofResponseCoordinator' -import type { IndyProofFormat } from '../../../formats/indy/IndyProofFormat' -import type { IndyProofRequestFromProposalOptions } from '../../../formats/indy/IndyProofFormatsServiceOptions' -import type { ProofRequestFromProposalOptions } from '../../../models/ProofServiceOptions' import type { ProofExchangeRecord } from '../../../repository/ProofExchangeRecord' -import type { V1ProofService } from '../V1ProofService' +import type { V1ProofProtocol } from '../V1ProofProtocol' import { OutboundMessageContext } from '../../../../../agent/models' -import { AriesFrameworkError } from '../../../../../error' import { V1ProposePresentationMessage } from '../messages' export class V1ProposePresentationHandler implements MessageHandler { - private proofService: V1ProofService - private agentConfig: AgentConfig - private didCommMessageRepository: DidCommMessageRepository - private proofResponseCoordinator: ProofResponseCoordinator + private proofProtocol: V1ProofProtocol public supportedMessages = [V1ProposePresentationMessage] - public constructor( - proofService: V1ProofService, - agentConfig: AgentConfig, - proofResponseCoordinator: ProofResponseCoordinator, - didCommMessageRepository: DidCommMessageRepository - ) { - this.proofService = proofService - this.agentConfig = agentConfig - this.proofResponseCoordinator = proofResponseCoordinator - this.didCommMessageRepository = didCommMessageRepository + public constructor(proofProtocol: V1ProofProtocol) { + this.proofProtocol = proofProtocol } public async handle(messageContext: MessageHandlerInboundMessage) { - const proofRecord = await this.proofService.processProposal(messageContext) + const proofRecord = await this.proofProtocol.processProposal(messageContext) - const shouldAutoRespond = await this.proofResponseCoordinator.shouldAutoRespondToProposal( - messageContext.agentContext, - proofRecord - ) + const shouldAutoRespond = await this.proofProtocol.shouldAutoRespondToProposal(messageContext.agentContext, { + proofRecord, + proposalMessage: messageContext.message, + }) if (shouldAutoRespond) { - return await this.createRequest(proofRecord, messageContext) + return await this.acceptProposal(proofRecord, messageContext) } } - private async createRequest( + private async acceptProposal( proofRecord: ProofExchangeRecord, messageContext: MessageHandlerInboundMessage ) { - this.agentConfig.logger.info( - `Automatically sending request with autoAccept on ${this.agentConfig.autoAcceptProofs}` - ) + messageContext.agentContext.config.logger.info(`Automatically sending request with autoAccept`) if (!messageContext.connection) { - this.agentConfig.logger.error('No connection on the messageContext') - throw new AriesFrameworkError('No connection on the messageContext') - } - - const proposalMessage = await this.didCommMessageRepository.getAgentMessage(messageContext.agentContext, { - associatedRecordId: proofRecord.id, - messageClass: V1ProposePresentationMessage, - }) - - if (!proposalMessage) { - this.agentConfig.logger.error(`Proof record with id ${proofRecord.id} is missing required credential proposal`) - throw new AriesFrameworkError(`Proof record with id ${proofRecord.id} is missing required credential proposal`) + messageContext.agentContext.config.logger.error('No connection on the messageContext, aborting auto accept') + return } - const proofRequestFromProposalOptions: IndyProofRequestFromProposalOptions = { - name: 'proof-request', - version: '1.0', - nonce: await messageContext.agentContext.wallet.generateNonce(), + const { message } = await this.proofProtocol.acceptProposal(messageContext.agentContext, { proofRecord, - } - - const proofRequest: ProofRequestFromProposalOptions<[IndyProofFormat]> = - await this.proofService.createProofRequestFromProposal( - messageContext.agentContext, - proofRequestFromProposalOptions - ) - - const indyProofRequest = proofRequest.proofFormats - - if (!indyProofRequest || !indyProofRequest.indy) { - this.agentConfig.logger.error(`No Indy proof request was found`) - throw new AriesFrameworkError('No Indy proof request was found') - } - - const { message } = await this.proofService.createRequestAsResponse(messageContext.agentContext, { - proofFormats: { - indy: { - name: indyProofRequest.indy?.name, - version: indyProofRequest.indy?.version, - nonRevoked: indyProofRequest.indy?.nonRevoked, - requestedAttributes: indyProofRequest.indy?.requestedAttributes, - requestedPredicates: indyProofRequest.indy?.requestedPredicates, - ver: indyProofRequest.indy?.ver, - nonce: indyProofRequest.indy?.nonce, - }, - }, - proofRecord: proofRecord, - autoAcceptProof: proofRecord.autoAcceptProof, - willConfirm: true, }) return new OutboundMessageContext(message, { diff --git a/packages/core/src/modules/proofs/protocol/v1/handlers/V1RequestPresentationHandler.ts b/packages/core/src/modules/proofs/protocol/v1/handlers/V1RequestPresentationHandler.ts index d460bbe122..340307e50a 100644 --- a/packages/core/src/modules/proofs/protocol/v1/handlers/V1RequestPresentationHandler.ts +++ b/packages/core/src/modules/proofs/protocol/v1/handlers/V1RequestPresentationHandler.ts @@ -1,126 +1,74 @@ -import type { AgentConfig } from '../../../../../agent/AgentConfig' import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' -import type { DidCommMessageRepository } from '../../../../../storage/didcomm/DidCommMessageRepository' -import type { MediationRecipientService, RoutingService } from '../../../../routing' -import type { ProofResponseCoordinator } from '../../../ProofResponseCoordinator' -import type { IndyProofFormat } from '../../../formats/indy/IndyProofFormat' -import type { - FormatRequestedCredentialReturn, - FormatRetrievedCredentialOptions, -} from '../../../models/ProofServiceOptions' import type { ProofExchangeRecord } from '../../../repository/ProofExchangeRecord' -import type { V1ProofService } from '../V1ProofService' +import type { V1ProofProtocol } from '../V1ProofProtocol' import { OutboundMessageContext } from '../../../../../agent/models' import { ServiceDecorator } from '../../../../../decorators/service/ServiceDecorator' -import { AriesFrameworkError } from '../../../../../error' -import { DidCommMessageRole } from '../../../../../storage' +import { DidCommMessageRepository, DidCommMessageRole } from '../../../../../storage' +import { RoutingService } from '../../../../routing' import { V1RequestPresentationMessage } from '../messages' export class V1RequestPresentationHandler implements MessageHandler { - private proofService: V1ProofService - private agentConfig: AgentConfig - private proofResponseCoordinator: ProofResponseCoordinator - private mediationRecipientService: MediationRecipientService - private didCommMessageRepository: DidCommMessageRepository - private routingService: RoutingService + private proofProtocol: V1ProofProtocol public supportedMessages = [V1RequestPresentationMessage] - public constructor( - proofService: V1ProofService, - agentConfig: AgentConfig, - proofResponseCoordinator: ProofResponseCoordinator, - mediationRecipientService: MediationRecipientService, - didCommMessageRepository: DidCommMessageRepository, - routingService: RoutingService - ) { - this.proofService = proofService - this.agentConfig = agentConfig - this.proofResponseCoordinator = proofResponseCoordinator - this.mediationRecipientService = mediationRecipientService - this.didCommMessageRepository = didCommMessageRepository - this.routingService = routingService + public constructor(proofProtocol: V1ProofProtocol) { + this.proofProtocol = proofProtocol } public async handle(messageContext: MessageHandlerInboundMessage) { - const proofRecord = await this.proofService.processRequest(messageContext) + const proofRecord = await this.proofProtocol.processRequest(messageContext) - const shouldAutoRespond = await this.proofResponseCoordinator.shouldAutoRespondToRequest( - messageContext.agentContext, - proofRecord - ) + const shouldAutoRespond = await this.proofProtocol.shouldAutoRespondToRequest(messageContext.agentContext, { + proofRecord, + requestMessage: messageContext.message, + }) if (shouldAutoRespond) { - return await this.createPresentation(proofRecord, messageContext) + return await this.acceptRequest(proofRecord, messageContext) } } - private async createPresentation( - record: ProofExchangeRecord, + private async acceptRequest( + proofRecord: ProofExchangeRecord, messageContext: MessageHandlerInboundMessage ) { - const requestMessage = await this.didCommMessageRepository.getAgentMessage(messageContext.agentContext, { - associatedRecordId: record.id, - messageClass: V1RequestPresentationMessage, - }) - - const indyProofRequest = requestMessage.indyProofRequest - - this.agentConfig.logger.info( - `Automatically sending presentation with autoAccept on ${this.agentConfig.autoAcceptProofs}` - ) + messageContext.agentContext.config.logger.info(`Automatically sending presentation with autoAccept on`) - if (!indyProofRequest) { - this.agentConfig.logger.error('Proof request is undefined.') - throw new AriesFrameworkError('No proof request found.') - } - - const retrievedCredentials: FormatRetrievedCredentialOptions<[IndyProofFormat]> = - await this.proofService.getRequestedCredentialsForProofRequest(messageContext.agentContext, { - proofRecord: record, - config: { - filterByPresentationPreview: true, - }, + if (messageContext.connection) { + const { message } = await this.proofProtocol.acceptRequest(messageContext.agentContext, { + proofRecord, }) - if (!retrievedCredentials.proofFormats.indy) { - this.agentConfig.logger.error('No matching Indy credentials could be retrieved.') - throw new AriesFrameworkError('No matching Indy credentials could be retrieved.') - } - const options: FormatRetrievedCredentialOptions<[IndyProofFormat]> = { - proofFormats: retrievedCredentials.proofFormats, - } - const requestedCredentials: FormatRequestedCredentialReturn<[IndyProofFormat]> = - await this.proofService.autoSelectCredentialsForProofRequest(options) - - const { message, proofRecord } = await this.proofService.createPresentation(messageContext.agentContext, { - proofRecord: record, - proofFormats: { - indy: requestedCredentials.proofFormats.indy, - }, - willConfirm: true, - }) - - if (messageContext.connection) { return new OutboundMessageContext(message, { agentContext: messageContext.agentContext, connection: messageContext.connection, associatedRecord: proofRecord, }) - } else if (requestMessage.service) { - const routing = await this.routingService.getRouting(messageContext.agentContext) - message.service = new ServiceDecorator({ + } else if (messageContext.message.service) { + const { message } = await this.proofProtocol.acceptRequest(messageContext.agentContext, { + proofRecord, + }) + + const routingService = messageContext.agentContext.dependencyManager.resolve(RoutingService) + const routing = await routingService.getRouting(messageContext.agentContext) + const ourService = new ServiceDecorator({ serviceEndpoint: routing.endpoints[0], recipientKeys: [routing.recipientKey.publicKeyBase58], routingKeys: routing.routingKeys.map((key) => key.publicKeyBase58), }) - const recipientService = requestMessage.service + const recipientService = messageContext.message.service - await this.didCommMessageRepository.saveOrUpdateAgentMessage(messageContext.agentContext, { + // Set and save ~service decorator to record (to remember our verkey) + message.service = ourService + + const didCommMessageRepository = messageContext.agentContext.dependencyManager.resolve(DidCommMessageRepository) + await didCommMessageRepository.saveOrUpdateAgentMessage(messageContext.agentContext, { agentMessage: message, associatedRecordId: proofRecord.id, role: DidCommMessageRole.Sender, }) + return new OutboundMessageContext(message, { agentContext: messageContext.agentContext, serviceParams: { @@ -130,6 +78,6 @@ export class V1RequestPresentationHandler implements MessageHandler { }) } - this.agentConfig.logger.error(`Could not automatically create presentation`) + messageContext.agentContext.config.logger.error(`Could not automatically create presentation`) } } diff --git a/packages/core/src/modules/proofs/protocol/v1/index.ts b/packages/core/src/modules/proofs/protocol/v1/index.ts index a7e92f64d6..e698bc7140 100644 --- a/packages/core/src/modules/proofs/protocol/v1/index.ts +++ b/packages/core/src/modules/proofs/protocol/v1/index.ts @@ -1,4 +1,4 @@ export * from './errors' export * from './messages' export * from './models' -export * from './V1ProofService' +export * from './V1ProofProtocol' diff --git a/packages/core/src/modules/proofs/protocol/v1/messages/V1PresentationMessage.ts b/packages/core/src/modules/proofs/protocol/v1/messages/V1PresentationMessage.ts index d66360d0e5..04db86e9a5 100644 --- a/packages/core/src/modules/proofs/protocol/v1/messages/V1PresentationMessage.ts +++ b/packages/core/src/modules/proofs/protocol/v1/messages/V1PresentationMessage.ts @@ -1,4 +1,3 @@ -import type { ProofAttachmentFormat } from '../../../formats/models/ProofAttachmentFormat' import type { IndyProof } from 'indy-sdk' import { Expose, Type } from 'class-transformer' @@ -6,14 +5,11 @@ import { IsArray, IsString, ValidateNested, IsOptional, IsInstance } from 'class import { AgentMessage } from '../../../../../agent/AgentMessage' import { Attachment } from '../../../../../decorators/attachment/Attachment' -import { AriesFrameworkError } from '../../../../../error/AriesFrameworkError' import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' -import { V2_INDY_PRESENTATION } from '../../../formats/ProofFormatConstants' -import { ProofFormatSpec } from '../../../models/ProofFormatSpec' export const INDY_PROOF_ATTACHMENT_ID = 'libindy-presentation-0' -export interface PresentationOptions { +export interface V1PresentationMessageOptions { id?: string comment?: string presentationAttachments: Attachment[] @@ -27,7 +23,7 @@ export interface PresentationOptions { * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0037-present-proof/README.md#presentation */ export class V1PresentationMessage extends AgentMessage { - public constructor(options: PresentationOptions) { + public constructor(options: V1PresentationMessageOptions) { super() if (options) { @@ -61,30 +57,16 @@ export class V1PresentationMessage extends AgentMessage { @IsInstance(Attachment, { each: true }) public presentationAttachments!: Attachment[] - public getAttachmentFormats(): ProofAttachmentFormat[] { - const attachment = this.indyAttachment - - if (!attachment) { - throw new AriesFrameworkError(`Could not find a presentation attachment`) - } - - return [ - { - format: new ProofFormatSpec({ format: V2_INDY_PRESENTATION }), - attachment: attachment, - }, - ] - } - - public get indyAttachment(): Attachment | null { - return this.presentationAttachments.find((attachment) => attachment.id === INDY_PROOF_ATTACHMENT_ID) ?? null - } - public get indyProof(): IndyProof | null { - const attachment = this.indyAttachment + const attachment = + this.presentationAttachments.find((attachment) => attachment.id === INDY_PROOF_ATTACHMENT_ID) ?? null const proofJson = attachment?.getDataAsJson() ?? null return proofJson } + + public getPresentationAttachmentById(id: string): Attachment | undefined { + return this.presentationAttachments.find((attachment) => attachment.id == id) + } } diff --git a/packages/core/src/modules/proofs/protocol/v1/messages/V1ProposePresentationMessage.ts b/packages/core/src/modules/proofs/protocol/v1/messages/V1ProposePresentationMessage.ts index fab2d86765..bef0a44c7a 100644 --- a/packages/core/src/modules/proofs/protocol/v1/messages/V1ProposePresentationMessage.ts +++ b/packages/core/src/modules/proofs/protocol/v1/messages/V1ProposePresentationMessage.ts @@ -3,13 +3,12 @@ import { IsInstance, IsOptional, IsString, ValidateNested } from 'class-validato import { AgentMessage } from '../../../../../agent/AgentMessage' import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' -import { PresentationPreview } from '../models/V1PresentationPreview' +import { V1PresentationPreview } from '../models/V1PresentationPreview' -export interface ProposePresentationMessageOptions { +export interface V1ProposePresentationMessageOptions { id?: string comment?: string - parentThreadId?: string - presentationProposal: PresentationPreview + presentationProposal: V1PresentationPreview } /** @@ -18,17 +17,12 @@ export interface ProposePresentationMessageOptions { * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0037-present-proof/README.md#propose-presentation */ export class V1ProposePresentationMessage extends AgentMessage { - public constructor(options: ProposePresentationMessageOptions) { + public constructor(options: V1ProposePresentationMessageOptions) { super() if (options) { this.id = options.id ?? this.generateId() this.comment = options.comment - if (options.parentThreadId) { - this.setThread({ - parentThreadId: options.parentThreadId, - }) - } this.presentationProposal = options.presentationProposal } } @@ -48,8 +42,8 @@ export class V1ProposePresentationMessage extends AgentMessage { * Represents the presentation example that prover wants to provide. */ @Expose({ name: 'presentation_proposal' }) - @Type(() => PresentationPreview) + @Type(() => V1PresentationPreview) @ValidateNested() - @IsInstance(PresentationPreview) - public presentationProposal!: PresentationPreview + @IsInstance(V1PresentationPreview) + public presentationProposal!: V1PresentationPreview } diff --git a/packages/core/src/modules/proofs/protocol/v1/messages/V1RequestPresentationMessage.ts b/packages/core/src/modules/proofs/protocol/v1/messages/V1RequestPresentationMessage.ts index a65dae533f..5ac5fd6798 100644 --- a/packages/core/src/modules/proofs/protocol/v1/messages/V1RequestPresentationMessage.ts +++ b/packages/core/src/modules/proofs/protocol/v1/messages/V1RequestPresentationMessage.ts @@ -1,4 +1,3 @@ -import type { ProofAttachmentFormat } from '../../../formats/models/ProofAttachmentFormat' import type { IndyProofRequest } from 'indy-sdk' import { Expose, Type } from 'class-transformer' @@ -6,18 +5,12 @@ import { IsArray, IsString, ValidateNested, IsOptional, IsInstance } from 'class import { AgentMessage } from '../../../../../agent/AgentMessage' import { Attachment } from '../../../../../decorators/attachment/Attachment' -import { AriesFrameworkError } from '../../../../../error/AriesFrameworkError' -import { JsonTransformer } from '../../../../../utils/JsonTransformer' import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' -import { V2_INDY_PRESENTATION_REQUEST } from '../../../formats/ProofFormatConstants' -import { ProofRequest } from '../../../formats/indy/models/ProofRequest' -import { ProofFormatSpec } from '../../../models/ProofFormatSpec' -export interface RequestPresentationOptions { +export interface V1RequestPresentationMessageOptions { id?: string comment?: string - parentThreadId?: string - requestPresentationAttachments: Attachment[] + requestAttachments: Attachment[] } export const INDY_PROOF_REQUEST_ATTACHMENT_ID = 'libindy-request-presentation-0' @@ -28,18 +21,13 @@ export const INDY_PROOF_REQUEST_ATTACHMENT_ID = 'libindy-request-presentation-0' * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0037-present-proof/README.md#request-presentation */ export class V1RequestPresentationMessage extends AgentMessage { - public constructor(options: RequestPresentationOptions) { + public constructor(options: V1RequestPresentationMessageOptions) { super() if (options) { this.id = options.id ?? this.generateId() this.comment = options.comment - this.requestPresentationAttachments = options.requestPresentationAttachments - if (options.parentThreadId) { - this.setThread({ - parentThreadId: options.parentThreadId, - }) - } + this.requestAttachments = options.requestAttachments } } @@ -64,43 +52,15 @@ export class V1RequestPresentationMessage extends AgentMessage { each: true, }) @IsInstance(Attachment, { each: true }) - public requestPresentationAttachments!: Attachment[] + public requestAttachments!: Attachment[] - public get indyProofRequest(): ProofRequest | null { - // Extract proof request from attachment - const proofRequestJson = this.indyProofRequestJson - const proofRequest = JsonTransformer.fromJSON(proofRequestJson, ProofRequest) - - return proofRequest - } - - public get indyProofRequestJson(): IndyProofRequest | null { - const attachment = this.requestPresentationAttachments.find( - (attachment) => attachment.id === INDY_PROOF_REQUEST_ATTACHMENT_ID - ) + public get indyProofRequest(): IndyProofRequest | null { + const attachment = this.requestAttachments.find((attachment) => attachment.id === INDY_PROOF_REQUEST_ATTACHMENT_ID) // Extract proof request from attachment return attachment?.getDataAsJson() ?? null } - public getAttachmentFormats(): ProofAttachmentFormat[] { - const attachment = this.indyAttachment - - if (!attachment) { - throw new AriesFrameworkError(`Could not find a request presentation attachment`) - } - - return [ - { - format: new ProofFormatSpec({ format: V2_INDY_PRESENTATION_REQUEST }), - attachment: attachment, - }, - ] - } - - public get indyAttachment(): Attachment | null { - return ( - this.requestPresentationAttachments.find((attachment) => attachment.id === INDY_PROOF_REQUEST_ATTACHMENT_ID) ?? - null - ) + public getRequestAttachmentById(id: string): Attachment | undefined { + return this.requestAttachments.find((attachment) => attachment.id === id) } } diff --git a/packages/core/src/modules/proofs/protocol/v1/models/V1PresentationPreview.ts b/packages/core/src/modules/proofs/protocol/v1/models/V1PresentationPreview.ts index 2ac71e903e..0de67b3a00 100644 --- a/packages/core/src/modules/proofs/protocol/v1/models/V1PresentationPreview.ts +++ b/packages/core/src/modules/proofs/protocol/v1/models/V1PresentationPreview.ts @@ -16,7 +16,7 @@ import { JsonTransformer } from '../../../../../utils/JsonTransformer' import { IsValidMessageType, parseMessageType, replaceLegacyDidSovPrefix } from '../../../../../utils/messageType' import { PredicateType } from '../../../formats/indy/models/PredicateType' -export interface PresentationPreviewAttributeOptions { +export interface V1PresentationPreviewAttributeOptions { name: string credentialDefinitionId?: string mimeType?: string @@ -24,8 +24,8 @@ export interface PresentationPreviewAttributeOptions { referent?: string } -export class PresentationPreviewAttribute { - public constructor(options: PresentationPreviewAttributeOptions) { +export class V1PresentationPreviewAttribute { + public constructor(options: V1PresentationPreviewAttributeOptions) { if (options) { this.name = options.name this.credentialDefinitionId = options.credentialDefinitionId @@ -39,7 +39,7 @@ export class PresentationPreviewAttribute { @Expose({ name: 'cred_def_id' }) @IsString() - @ValidateIf((o: PresentationPreviewAttribute) => o.referent !== undefined) + @ValidateIf((o: V1PresentationPreviewAttribute) => o.referent !== undefined) @Matches(credDefIdRegex) public credentialDefinitionId?: string @@ -61,15 +61,15 @@ export class PresentationPreviewAttribute { } } -export interface PresentationPreviewPredicateOptions { +export interface V1PresentationPreviewPredicateOptions { name: string credentialDefinitionId: string predicate: PredicateType threshold: number } -export class PresentationPreviewPredicate { - public constructor(options: PresentationPreviewPredicateOptions) { +export class V1PresentationPreviewPredicate { + public constructor(options: V1PresentationPreviewPredicateOptions) { if (options) { this.name = options.name this.credentialDefinitionId = options.credentialDefinitionId @@ -97,9 +97,9 @@ export class PresentationPreviewPredicate { } } -export interface PresentationPreviewOptions { - attributes?: PresentationPreviewAttribute[] - predicates?: PresentationPreviewPredicate[] +export interface V1PresentationPreviewOptions { + attributes?: V1PresentationPreviewAttributeOptions[] + predicates?: V1PresentationPreviewPredicateOptions[] } /** @@ -109,31 +109,31 @@ export interface PresentationPreviewOptions { * * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0037-present-proof/README.md#presentation-preview */ -export class PresentationPreview { - public constructor(options: PresentationPreviewOptions) { +export class V1PresentationPreview { + public constructor(options: V1PresentationPreviewOptions) { if (options) { - this.attributes = options.attributes ?? [] - this.predicates = options.predicates ?? [] + this.attributes = options.attributes?.map((a) => new V1PresentationPreviewAttribute(a)) ?? [] + this.predicates = options.predicates?.map((p) => new V1PresentationPreviewPredicate(p)) ?? [] } } @Expose({ name: '@type' }) - @IsValidMessageType(PresentationPreview.type) + @IsValidMessageType(V1PresentationPreview.type) @Transform(({ value }) => replaceLegacyDidSovPrefix(value), { toClassOnly: true, }) - public readonly type = PresentationPreview.type.messageTypeUri + public readonly type = V1PresentationPreview.type.messageTypeUri public static readonly type = parseMessageType('https://didcomm.org/present-proof/1.0/presentation-preview') - @Type(() => PresentationPreviewAttribute) + @Type(() => V1PresentationPreviewAttribute) @ValidateNested({ each: true }) - @IsInstance(PresentationPreviewAttribute, { each: true }) - public attributes!: PresentationPreviewAttribute[] + @IsInstance(V1PresentationPreviewAttribute, { each: true }) + public attributes!: V1PresentationPreviewAttribute[] - @Type(() => PresentationPreviewPredicate) + @Type(() => V1PresentationPreviewPredicate) @ValidateNested({ each: true }) - @IsInstance(PresentationPreviewPredicate, { each: true }) - public predicates!: PresentationPreviewPredicate[] + @IsInstance(V1PresentationPreviewPredicate, { each: true }) + public predicates!: V1PresentationPreviewPredicate[] public toJSON(): Record { return JsonTransformer.toJSON(this) diff --git a/packages/core/src/modules/proofs/protocol/v1/models/index.ts b/packages/core/src/modules/proofs/protocol/v1/models/index.ts index 35ec9d0545..56c1a4fde0 100644 --- a/packages/core/src/modules/proofs/protocol/v1/models/index.ts +++ b/packages/core/src/modules/proofs/protocol/v1/models/index.ts @@ -1,5 +1 @@ -export * from './PartialProof' -export * from './ProofAttribute' -export * from './ProofIdentifier' -export * from './RequestedProof' export * from './V1PresentationPreview' diff --git a/packages/core/src/modules/proofs/protocol/v2/ProofFormatCoordinator.ts b/packages/core/src/modules/proofs/protocol/v2/ProofFormatCoordinator.ts new file mode 100644 index 0000000000..efc6a02aad --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/ProofFormatCoordinator.ts @@ -0,0 +1,520 @@ +import type { AgentContext } from '../../../../agent' +import type { Attachment } from '../../../../decorators/attachment/Attachment' +import type { + ExtractProofFormats, + IndyProofFormat, + ProofFormatCredentialForRequestPayload, + ProofFormatPayload, + ProofFormatService, +} from '../../formats' +import type { ProofFormatSpec } from '../../models/ProofFormatSpec' +import type { ProofExchangeRecord } from '../../repository' + +import { AriesFrameworkError } from '../../../../error' +import { DidCommMessageRepository, DidCommMessageRole } from '../../../../storage' + +import { V2PresentationMessage, V2ProposePresentationMessage, V2RequestPresentationMessage } from './messages' + +export class ProofFormatCoordinator { + /** + * Create a {@link V2ProposePresentationMessage}. + * + * @param options + * @returns The created {@link V2ProposePresentationMessage} + * + */ + public async createProposal( + agentContext: AgentContext, + { + proofFormats, + formatServices, + proofRecord, + comment, + goalCode, + }: { + formatServices: ProofFormatService[] + proofFormats: ProofFormatPayload, 'createProposal'> + proofRecord: ProofExchangeRecord + comment?: string + goalCode?: string + } + ): Promise { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // create message. there are two arrays in each message, one for formats the other for attachments + const formats: ProofFormatSpec[] = [] + const proposalAttachments: Attachment[] = [] + + for (const formatService of formatServices) { + const { format, attachment } = await formatService.createProposal(agentContext, { + proofFormats, + proofRecord, + }) + + proposalAttachments.push(attachment) + formats.push(format) + } + + const message = new V2ProposePresentationMessage({ + id: proofRecord.threadId, + formats, + proposalAttachments, + comment: comment, + goalCode, + }) + + message.setThread({ threadId: proofRecord.threadId, parentThreadId: proofRecord.parentThreadId }) + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + role: DidCommMessageRole.Sender, + associatedRecordId: proofRecord.id, + }) + + return message + } + + public async processProposal( + agentContext: AgentContext, + { + proofRecord, + message, + formatServices, + }: { + proofRecord: ProofExchangeRecord + message: V2ProposePresentationMessage + formatServices: ProofFormatService[] + } + ) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + for (const formatService of formatServices) { + const attachment = this.getAttachmentForService(formatService, message.formats, message.proposalAttachments) + + await formatService.processProposal(agentContext, { + attachment, + proofRecord, + }) + } + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + role: DidCommMessageRole.Receiver, + associatedRecordId: proofRecord.id, + }) + } + + public async acceptProposal( + agentContext: AgentContext, + { + proofRecord, + proofFormats, + formatServices, + comment, + goalCode, + presentMultiple, + willConfirm, + }: { + proofRecord: ProofExchangeRecord + proofFormats?: ProofFormatPayload, 'acceptProposal'> + formatServices: ProofFormatService[] + comment?: string + goalCode?: string + presentMultiple?: boolean + willConfirm?: boolean + } + ) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // create message. there are two arrays in each message, one for formats the other for attachments + const formats: ProofFormatSpec[] = [] + const requestAttachments: Attachment[] = [] + + const proposalMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2ProposePresentationMessage, + }) + + for (const formatService of formatServices) { + const proposalAttachment = this.getAttachmentForService( + formatService, + proposalMessage.formats, + proposalMessage.proposalAttachments + ) + + const { attachment, format } = await formatService.acceptProposal(agentContext, { + proofRecord, + proofFormats, + proposalAttachment, + }) + + requestAttachments.push(attachment) + formats.push(format) + } + + const message = new V2RequestPresentationMessage({ + formats, + requestAttachments, + comment, + goalCode, + presentMultiple, + willConfirm, + }) + + message.setThread({ threadId: proofRecord.threadId, parentThreadId: proofRecord.parentThreadId }) + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Sender, + }) + + return message + } + + /** + * Create a {@link V2RequestPresentationMessage}. + * + * @param options + * @returns The created {@link V2RequestPresentationMessage} + * + */ + public async createRequest( + agentContext: AgentContext, + { + proofFormats, + formatServices, + proofRecord, + comment, + goalCode, + presentMultiple, + willConfirm, + }: { + formatServices: ProofFormatService[] + proofFormats: ProofFormatPayload, 'createRequest'> + proofRecord: ProofExchangeRecord + comment?: string + goalCode?: string + presentMultiple?: boolean + willConfirm?: boolean + } + ): Promise { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // create message. there are two arrays in each message, one for formats the other for attachments + const formats: ProofFormatSpec[] = [] + const requestAttachments: Attachment[] = [] + + for (const formatService of formatServices) { + const { format, attachment } = await formatService.createRequest(agentContext, { + proofFormats, + proofRecord, + }) + + requestAttachments.push(attachment) + formats.push(format) + } + + const message = new V2RequestPresentationMessage({ + formats, + comment, + requestAttachments, + goalCode, + presentMultiple, + willConfirm, + }) + + message.setThread({ threadId: proofRecord.threadId, parentThreadId: proofRecord.parentThreadId }) + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + role: DidCommMessageRole.Sender, + associatedRecordId: proofRecord.id, + }) + + return message + } + + public async processRequest( + agentContext: AgentContext, + { + proofRecord, + message, + formatServices, + }: { + proofRecord: ProofExchangeRecord + message: V2RequestPresentationMessage + formatServices: ProofFormatService[] + } + ) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + for (const formatService of formatServices) { + const attachment = this.getAttachmentForService(formatService, message.formats, message.requestAttachments) + + await formatService.processRequest(agentContext, { + attachment, + proofRecord, + }) + } + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + role: DidCommMessageRole.Receiver, + associatedRecordId: proofRecord.id, + }) + } + + public async acceptRequest( + agentContext: AgentContext, + { + proofRecord, + proofFormats, + formatServices, + comment, + lastPresentation, + goalCode, + }: { + proofRecord: ProofExchangeRecord + proofFormats?: ProofFormatPayload, 'acceptRequest'> + formatServices: ProofFormatService[] + comment?: string + lastPresentation?: boolean + goalCode?: string + } + ) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + const requestMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2RequestPresentationMessage, + }) + + const proposalMessage = await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2ProposePresentationMessage, + }) + + // create message. there are two arrays in each message, one for formats the other for attachments + const formats: ProofFormatSpec[] = [] + const presentationAttachments: Attachment[] = [] + + for (const formatService of formatServices) { + const requestAttachment = this.getAttachmentForService( + formatService, + requestMessage.formats, + requestMessage.requestAttachments + ) + + const proposalAttachment = proposalMessage + ? this.getAttachmentForService(formatService, proposalMessage.formats, proposalMessage.proposalAttachments) + : undefined + + const { attachment, format } = await formatService.acceptRequest(agentContext, { + requestAttachment, + proposalAttachment, + proofRecord, + proofFormats, + }) + + presentationAttachments.push(attachment) + formats.push(format) + } + + const message = new V2PresentationMessage({ + formats, + presentationAttachments, + comment, + lastPresentation, + goalCode, + }) + + message.setThread({ threadId: proofRecord.threadId }) + message.setPleaseAck() + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Sender, + }) + + return message + } + + public async getCredentialsForRequest( + agentContext: AgentContext, + { + proofRecord, + proofFormats, + formatServices, + }: { + proofRecord: ProofExchangeRecord + proofFormats?: ProofFormatCredentialForRequestPayload< + ExtractProofFormats, + 'getCredentialsForRequest', + 'input' + > + formatServices: ProofFormatService[] + } + ): Promise, 'getCredentialsForRequest', 'output'>> { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + const requestMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2RequestPresentationMessage, + }) + + const proposalMessage = await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2ProposePresentationMessage, + }) + + const credentialsForRequest: Record = {} + + for (const formatService of formatServices) { + const requestAttachment = this.getAttachmentForService( + formatService, + requestMessage.formats, + requestMessage.requestAttachments + ) + + const proposalAttachment = proposalMessage + ? this.getAttachmentForService(formatService, proposalMessage.formats, proposalMessage.proposalAttachments) + : undefined + + const credentialsForFormat = await formatService.getCredentialsForRequest(agentContext, { + requestAttachment, + proposalAttachment, + proofRecord, + proofFormats, + }) + + credentialsForRequest[formatService.formatKey] = credentialsForFormat + } + + return credentialsForRequest + } + + public async selectCredentialsForRequest( + agentContext: AgentContext, + { + proofRecord, + proofFormats, + formatServices, + }: { + proofRecord: ProofExchangeRecord + proofFormats?: ProofFormatCredentialForRequestPayload< + ExtractProofFormats, + 'selectCredentialsForRequest', + 'input' + > + formatServices: ProofFormatService[] + } + ): Promise< + ProofFormatCredentialForRequestPayload, 'selectCredentialsForRequest', 'output'> + > { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + const requestMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2RequestPresentationMessage, + }) + + const proposalMessage = await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2ProposePresentationMessage, + }) + + const credentialsForRequest: Record = {} + + for (const formatService of formatServices) { + const requestAttachment = this.getAttachmentForService( + formatService, + requestMessage.formats, + requestMessage.requestAttachments + ) + + const proposalAttachment = proposalMessage + ? this.getAttachmentForService(formatService, proposalMessage.formats, proposalMessage.proposalAttachments) + : undefined + + const credentialsForFormat = await formatService.selectCredentialsForRequest(agentContext, { + requestAttachment, + proposalAttachment, + proofRecord, + proofFormats, + }) + + credentialsForRequest[formatService.formatKey] = credentialsForFormat + } + + return credentialsForRequest + } + + public async processPresentation( + agentContext: AgentContext, + { + proofRecord, + message, + requestMessage, + formatServices, + }: { + proofRecord: ProofExchangeRecord + message: V2PresentationMessage + requestMessage: V2RequestPresentationMessage + formatServices: ProofFormatService[] + } + ) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + const formatVerificationResults: boolean[] = [] + + for (const formatService of formatServices) { + const attachment = this.getAttachmentForService(formatService, message.formats, message.presentationAttachments) + const requestAttachment = this.getAttachmentForService( + formatService, + requestMessage.formats, + requestMessage.requestAttachments + ) + + const isValid = await formatService.processPresentation(agentContext, { + attachment, + requestAttachment, + proofRecord, + }) + + formatVerificationResults.push(isValid) + } + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + role: DidCommMessageRole.Receiver, + associatedRecordId: proofRecord.id, + }) + + return formatVerificationResults.every((isValid) => isValid === true) + } + + public getAttachmentForService( + credentialFormatService: ProofFormatService, + formats: ProofFormatSpec[], + attachments: Attachment[] + ) { + const attachmentId = this.getAttachmentIdForService(credentialFormatService, formats) + const attachment = attachments.find((attachment) => attachment.id === attachmentId) + + if (!attachment) { + throw new AriesFrameworkError(`Attachment with id ${attachmentId} not found in attachments.`) + } + + return attachment + } + + private getAttachmentIdForService(credentialFormatService: ProofFormatService, formats: ProofFormatSpec[]) { + const format = formats.find((format) => credentialFormatService.supportsFormat(format.format)) + + if (!format) throw new AriesFrameworkError(`No attachment found for service ${credentialFormatService.formatKey}`) + + return format.attachmentId + } +} diff --git a/packages/core/src/modules/proofs/protocol/v2/V2ProofProtocol.ts b/packages/core/src/modules/proofs/protocol/v2/V2ProofProtocol.ts new file mode 100644 index 0000000000..7c05da4e49 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/V2ProofProtocol.ts @@ -0,0 +1,1069 @@ +import type { AgentContext } from '../../../../agent' +import type { AgentMessage } from '../../../../agent/AgentMessage' +import type { FeatureRegistry } from '../../../../agent/FeatureRegistry' +import type { InboundMessageContext } from '../../../../agent/models/InboundMessageContext' +import type { DependencyManager } from '../../../../plugins' +import type { ProblemReportMessage } from '../../../problem-reports' +import type { + ExtractProofFormats, + ProofFormat, + ProofFormatCredentialForRequestPayload, + ProofFormatPayload, +} from '../../formats' +import type { ProofFormatService } from '../../formats/ProofFormatService' +import type { ProofFormatSpec } from '../../models/ProofFormatSpec' +import type { ProofProtocol } from '../ProofProtocol' +import type { + AcceptPresentationOptions, + AcceptProofProposalOptions, + AcceptProofRequestOptions, + CreateProofProblemReportOptions, + CreateProofProposalOptions, + CreateProofRequestOptions, + ProofFormatDataMessagePayload, + GetCredentialsForRequestOptions, + GetCredentialsForRequestReturn, + GetProofFormatDataReturn, + NegotiateProofProposalOptions, + NegotiateProofRequestOptions, + ProofProtocolMsgReturnType, + SelectCredentialsForRequestOptions, + SelectCredentialsForRequestReturn, +} from '../ProofProtocolOptions' + +import { Protocol } from '../../../../agent/models' +import { AriesFrameworkError } from '../../../../error' +import { DidCommMessageRepository } from '../../../../storage' +import { uuid } from '../../../../utils/uuid' +import { AckStatus } from '../../../common' +import { ConnectionService } from '../../../connections' +import { V2ProposeCredentialMessage } from '../../../credentials' +import { ProofsModuleConfig } from '../../ProofsModuleConfig' +import { PresentationProblemReportReason } from '../../errors/PresentationProblemReportReason' +import { AutoAcceptProof, ProofState } from '../../models' +import { ProofExchangeRecord, ProofRepository } from '../../repository' +import { composeAutoAccept } from '../../utils/composeAutoAccept' +import { BaseProofProtocol } from '../BaseProofProtocol' + +import { ProofFormatCoordinator } from './ProofFormatCoordinator' +import { V2PresentationAckHandler } from './handlers/V2PresentationAckHandler' +import { V2PresentationHandler } from './handlers/V2PresentationHandler' +import { V2PresentationProblemReportHandler } from './handlers/V2PresentationProblemReportHandler' +import { V2ProposePresentationHandler } from './handlers/V2ProposePresentationHandler' +import { V2RequestPresentationHandler } from './handlers/V2RequestPresentationHandler' +import { V2PresentationAckMessage, V2RequestPresentationMessage } from './messages' +import { V2PresentationMessage } from './messages/V2PresentationMessage' +import { V2PresentationProblemReportMessage } from './messages/V2PresentationProblemReportMessage' +import { V2ProposePresentationMessage } from './messages/V2ProposePresentationMessage' + +export interface V2ProofProtocolConfig { + proofFormats: ProofFormatServices +} + +export class V2ProofProtocol + extends BaseProofProtocol + implements ProofProtocol +{ + private proofFormatCoordinator = new ProofFormatCoordinator() + private proofFormats: PFs + + public constructor({ proofFormats }: V2ProofProtocolConfig) { + super() + + this.proofFormats = proofFormats + } + + /** + * The version of the present proof protocol this service supports + */ + public readonly version = 'v2' as const + + public register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry) { + // Register message handlers for the Present Proof V2 Protocol + dependencyManager.registerMessageHandlers([ + new V2ProposePresentationHandler(this), + new V2RequestPresentationHandler(this), + new V2PresentationHandler(this), + new V2PresentationAckHandler(this), + new V2PresentationProblemReportHandler(this), + ]) + + // Register Present Proof V2 in feature registry, with supported roles + featureRegistry.register( + new Protocol({ + id: 'https://didcomm.org/present-proof/2.0', + roles: ['prover', 'verifier'], + }) + ) + } + + public async createProposal( + agentContext: AgentContext, + { + connectionRecord, + proofFormats, + comment, + autoAcceptProof, + goalCode, + parentThreadId, + }: CreateProofProposalOptions + ): Promise<{ proofRecord: ProofExchangeRecord; message: AgentMessage }> { + const proofRepository = agentContext.dependencyManager.resolve(ProofRepository) + + const formatServices = this.getFormatServices(proofFormats) + if (formatServices.length === 0) { + throw new AriesFrameworkError(`Unable to create proposal. No supported formats`) + } + + const proofRecord = new ProofExchangeRecord({ + connectionId: connectionRecord.id, + threadId: uuid(), + parentThreadId, + state: ProofState.ProposalSent, + protocolVersion: 'v2', + autoAcceptProof, + }) + + const proposalMessage = await this.proofFormatCoordinator.createProposal(agentContext, { + proofFormats, + proofRecord, + formatServices, + comment, + goalCode, + }) + + agentContext.config.logger.debug('Save record and emit state change event') + await proofRepository.save(agentContext, proofRecord) + this.emitStateChangedEvent(agentContext, proofRecord, null) + + return { + proofRecord, + message: proposalMessage, + } + } + + /** + * Method called by {@link V2ProposeCredentialHandler} on reception of a propose presentation message + * We do the necessary processing here to accept the proposal and do the state change, emit event etc. + * @param messageContext the inbound propose presentation message + * @returns proof record appropriate for this incoming message (once accepted) + */ + public async processProposal( + messageContext: InboundMessageContext + ): Promise { + const { message: proposalMessage, connection, agentContext } = messageContext + + agentContext.config.logger.debug(`Processing presentation proposal with id ${proposalMessage.id}`) + + const proofRepository = agentContext.dependencyManager.resolve(ProofRepository) + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + const connectionService = agentContext.dependencyManager.resolve(ConnectionService) + + let proofRecord = await this.findByThreadAndConnectionId( + messageContext.agentContext, + proposalMessage.threadId, + connection?.id + ) + + const formatServices = this.getFormatServicesFromMessage(proposalMessage.formats) + if (formatServices.length === 0) { + throw new AriesFrameworkError(`Unable to process proposal. No supported formats`) + } + + // credential record already exists + if (proofRecord) { + const previousReceivedMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2ProposePresentationMessage, + }) + const previousSentMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2RequestPresentationMessage, + }) + + // Assert + proofRecord.assertProtocolVersion('v2') + proofRecord.assertState(ProofState.RequestSent) + connectionService.assertConnectionOrServiceDecorator(messageContext, { + previousReceivedMessage, + previousSentMessage, + }) + + await this.proofFormatCoordinator.processProposal(messageContext.agentContext, { + proofRecord, + formatServices, + message: proposalMessage, + }) + + await this.updateState(messageContext.agentContext, proofRecord, ProofState.ProposalReceived) + + return proofRecord + } else { + // Assert + connectionService.assertConnectionOrServiceDecorator(messageContext) + + // No credential record exists with thread id + proofRecord = new ProofExchangeRecord({ + connectionId: connection?.id, + threadId: proposalMessage.threadId, + state: ProofState.ProposalReceived, + protocolVersion: 'v2', + parentThreadId: proposalMessage.thread?.parentThreadId, + }) + + await this.proofFormatCoordinator.processProposal(messageContext.agentContext, { + proofRecord, + formatServices, + message: proposalMessage, + }) + + // Save record and emit event + await proofRepository.save(messageContext.agentContext, proofRecord) + this.emitStateChangedEvent(messageContext.agentContext, proofRecord, null) + + return proofRecord + } + } + + public async acceptProposal( + agentContext: AgentContext, + { proofRecord, proofFormats, autoAcceptProof, comment, goalCode, willConfirm }: AcceptProofProposalOptions + ): Promise> { + // Assert + proofRecord.assertProtocolVersion('v2') + proofRecord.assertState(ProofState.ProposalReceived) + + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // Use empty proofFormats if not provided to denote all formats should be accepted + let formatServices = this.getFormatServices(proofFormats ?? {}) + + // if no format services could be extracted from the proofFormats + // take all available format services from the proposal message + if (formatServices.length === 0) { + const proposalMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2ProposePresentationMessage, + }) + + formatServices = this.getFormatServicesFromMessage(proposalMessage.formats) + } + + // If the format services list is still empty, throw an error as we don't support any + // of the formats + if (formatServices.length === 0) { + throw new AriesFrameworkError( + `Unable to accept proposal. No supported formats provided as input or in proposal message` + ) + } + + const requestMessage = await this.proofFormatCoordinator.acceptProposal(agentContext, { + proofRecord, + formatServices, + comment, + proofFormats, + goalCode, + willConfirm, + // Not supported at the moment + presentMultiple: false, + }) + + proofRecord.autoAcceptProof = autoAcceptProof ?? proofRecord.autoAcceptProof + await this.updateState(agentContext, proofRecord, ProofState.RequestSent) + + return { proofRecord, message: requestMessage } + } + + /** + * Negotiate a proof proposal as verifier (by sending a proof request message) to the connection + * associated with the proof record. + * + * @param options configuration for the request see {@link NegotiateProofProposalOptions} + * @returns Proof exchange record associated with the proof request + * + */ + public async negotiateProposal( + agentContext: AgentContext, + { proofRecord, proofFormats, autoAcceptProof, comment, goalCode, willConfirm }: NegotiateProofProposalOptions + ): Promise> { + // Assert + proofRecord.assertProtocolVersion('v2') + proofRecord.assertState(ProofState.ProposalReceived) + + if (!proofRecord.connectionId) { + throw new AriesFrameworkError( + `No connectionId found for proof record '${proofRecord.id}'. Connection-less verification does not support negotiation.` + ) + } + + const formatServices = this.getFormatServices(proofFormats) + if (formatServices.length === 0) { + throw new AriesFrameworkError(`Unable to create request. No supported formats`) + } + + const requestMessage = await this.proofFormatCoordinator.createRequest(agentContext, { + formatServices, + proofFormats, + proofRecord, + comment, + goalCode, + willConfirm, + // Not supported at the moment + presentMultiple: false, + }) + + proofRecord.autoAcceptProof = autoAcceptProof ?? proofRecord.autoAcceptProof + await this.updateState(agentContext, proofRecord, ProofState.RequestSent) + + return { proofRecord, message: requestMessage } + } + + /** + * Create a {@link V2RequestPresentationMessage} as beginning of protocol process. + * @returns Object containing request message and associated credential record + * + */ + public async createRequest( + agentContext: AgentContext, + { + proofFormats, + autoAcceptProof, + comment, + connectionRecord, + parentThreadId, + goalCode, + willConfirm, + }: CreateProofRequestOptions + ): Promise> { + const proofRepository = agentContext.dependencyManager.resolve(ProofRepository) + + const formatServices = this.getFormatServices(proofFormats) + if (formatServices.length === 0) { + throw new AriesFrameworkError(`Unable to create request. No supported formats`) + } + + const proofRecord = new ProofExchangeRecord({ + connectionId: connectionRecord?.id, + threadId: uuid(), + state: ProofState.RequestSent, + autoAcceptProof, + protocolVersion: 'v2', + parentThreadId, + }) + + const requestMessage = await this.proofFormatCoordinator.createRequest(agentContext, { + formatServices, + proofFormats, + proofRecord, + comment, + goalCode, + willConfirm, + }) + + agentContext.config.logger.debug( + `Saving record and emitting state changed for proof exchange record ${proofRecord.id}` + ) + await proofRepository.save(agentContext, proofRecord) + this.emitStateChangedEvent(agentContext, proofRecord, null) + + return { proofRecord, message: requestMessage } + } + + /** + * Process a received {@link V2RequestPresentationMessage}. This will not accept the proof request + * or send a proof. It will only update the existing proof record with + * the information from the proof request message. Use {@link createCredential} + * after calling this method to create a proof. + *z + * @param messageContext The message context containing a v2 proof request message + * @returns proof record associated with the proof request message + * + */ + public async processRequest( + messageContext: InboundMessageContext + ): Promise { + const { message: requestMessage, connection, agentContext } = messageContext + + const proofRepository = agentContext.dependencyManager.resolve(ProofRepository) + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + const connectionService = agentContext.dependencyManager.resolve(ConnectionService) + + agentContext.config.logger.debug(`Processing proof request with id ${requestMessage.id}`) + + let proofRecord = await this.findByThreadAndConnectionId( + messageContext.agentContext, + requestMessage.threadId, + connection?.id + ) + + const formatServices = this.getFormatServicesFromMessage(requestMessage.formats) + if (formatServices.length === 0) { + throw new AriesFrameworkError(`Unable to process request. No supported formats`) + } + + // proof record already exists + if (proofRecord) { + const previousSentMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2ProposeCredentialMessage, + }) + + const previousReceivedMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2RequestPresentationMessage, + }) + + // Assert + proofRecord.assertProtocolVersion('v2') + proofRecord.assertState(ProofState.ProposalSent) + connectionService.assertConnectionOrServiceDecorator(messageContext, { + previousReceivedMessage, + previousSentMessage, + }) + + await this.proofFormatCoordinator.processRequest(messageContext.agentContext, { + proofRecord, + formatServices, + message: requestMessage, + }) + + await this.updateState(messageContext.agentContext, proofRecord, ProofState.RequestReceived) + return proofRecord + } else { + // Assert + connectionService.assertConnectionOrServiceDecorator(messageContext) + + // No proof record exists with thread id + agentContext.config.logger.debug('No proof record found for request, creating a new one') + proofRecord = new ProofExchangeRecord({ + connectionId: connection?.id, + threadId: requestMessage.threadId, + state: ProofState.RequestReceived, + protocolVersion: 'v2', + parentThreadId: requestMessage.thread?.parentThreadId, + }) + + await this.proofFormatCoordinator.processRequest(messageContext.agentContext, { + proofRecord, + formatServices, + message: requestMessage, + }) + + // Save in repository + agentContext.config.logger.debug('Saving proof record and emit request-received event') + await proofRepository.save(messageContext.agentContext, proofRecord) + + this.emitStateChangedEvent(messageContext.agentContext, proofRecord, null) + return proofRecord + } + } + + public async acceptRequest( + agentContext: AgentContext, + { proofRecord, autoAcceptProof, comment, proofFormats, goalCode }: AcceptProofRequestOptions + ) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // Assert + proofRecord.assertProtocolVersion('v2') + proofRecord.assertState(ProofState.RequestReceived) + + // Use empty proofFormats if not provided to denote all formats should be accepted + let formatServices = this.getFormatServices(proofFormats ?? {}) + + // if no format services could be extracted from the proofFormats + // take all available format services from the request message + if (formatServices.length === 0) { + const requestMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2RequestPresentationMessage, + }) + + formatServices = this.getFormatServicesFromMessage(requestMessage.formats) + } + + // If the format services list is still empty, throw an error as we don't support any + // of the formats + if (formatServices.length === 0) { + throw new AriesFrameworkError( + `Unable to accept request. No supported formats provided as input or in request message` + ) + } + const message = await this.proofFormatCoordinator.acceptRequest(agentContext, { + proofRecord, + formatServices, + comment, + proofFormats, + goalCode, + // Sending multiple presentation messages not supported at the moment + lastPresentation: true, + }) + + proofRecord.autoAcceptProof = autoAcceptProof ?? proofRecord.autoAcceptProof + await this.updateState(agentContext, proofRecord, ProofState.PresentationSent) + + return { proofRecord, message } + } + + /** + * Create a {@link V2ProposePresentationMessage} as response to a received credential request. + * To create a proposal not bound to an existing proof exchange, use {@link createProposal}. + * + * @param options configuration to use for the proposal + * @returns Object containing proposal message and associated proof record + * + */ + public async negotiateRequest( + agentContext: AgentContext, + { proofRecord, proofFormats, autoAcceptProof, comment, goalCode }: NegotiateProofRequestOptions + ): Promise> { + // Assert + proofRecord.assertProtocolVersion('v2') + proofRecord.assertState(ProofState.RequestReceived) + + if (!proofRecord.connectionId) { + throw new AriesFrameworkError( + `No connectionId found for proof record '${proofRecord.id}'. Connection-less verification does not support negotiation.` + ) + } + + const formatServices = this.getFormatServices(proofFormats) + if (formatServices.length === 0) { + throw new AriesFrameworkError(`Unable to create proposal. No supported formats`) + } + + const proposalMessage = await this.proofFormatCoordinator.createProposal(agentContext, { + formatServices, + proofFormats, + proofRecord, + comment, + goalCode, + }) + + proofRecord.autoAcceptProof = autoAcceptProof ?? proofRecord.autoAcceptProof + await this.updateState(agentContext, proofRecord, ProofState.ProposalSent) + + return { proofRecord, message: proposalMessage } + } + + public async getCredentialsForRequest( + agentContext: AgentContext, + { proofRecord, proofFormats }: GetCredentialsForRequestOptions + ): Promise> { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // Assert + proofRecord.assertProtocolVersion('v2') + proofRecord.assertState(ProofState.RequestReceived) + + // Use empty proofFormats if not provided to denote all formats should be accepted + let formatServices = this.getFormatServices(proofFormats ?? {}) + + // if no format services could be extracted from the proofFormats + // take all available format services from the request message + if (formatServices.length === 0) { + const requestMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2RequestPresentationMessage, + }) + + formatServices = this.getFormatServicesFromMessage(requestMessage.formats) + } + + // If the format services list is still empty, throw an error as we don't support any + // of the formats + if (formatServices.length === 0) { + throw new AriesFrameworkError( + `Unable to get credentials for request. No supported formats provided as input or in request message` + ) + } + + const result = await this.proofFormatCoordinator.getCredentialsForRequest(agentContext, { + formatServices, + proofFormats, + proofRecord, + }) + + return { + proofFormats: result, + } + } + + public async selectCredentialsForRequest( + agentContext: AgentContext, + { proofRecord, proofFormats }: SelectCredentialsForRequestOptions + ): Promise> { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // Assert + proofRecord.assertProtocolVersion('v2') + proofRecord.assertState(ProofState.RequestReceived) + + // Use empty proofFormats if not provided to denote all formats should be accepted + let formatServices = this.getFormatServices(proofFormats ?? {}) + + // if no format services could be extracted from the proofFormats + // take all available format services from the request message + if (formatServices.length === 0) { + const requestMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2RequestPresentationMessage, + }) + + formatServices = this.getFormatServicesFromMessage(requestMessage.formats) + } + + // If the format services list is still empty, throw an error as we don't support any + // of the formats + if (formatServices.length === 0) { + throw new AriesFrameworkError( + `Unable to get credentials for request. No supported formats provided as input or in request message` + ) + } + + const result = await this.proofFormatCoordinator.selectCredentialsForRequest(agentContext, { + formatServices, + proofFormats, + proofRecord, + }) + + return { + proofFormats: result, + } + } + + public async processPresentation( + messageContext: InboundMessageContext + ): Promise { + const { message: presentationMessage, connection, agentContext } = messageContext + + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + const connectionService = agentContext.dependencyManager.resolve(ConnectionService) + + agentContext.config.logger.debug(`Processing presentation with id ${presentationMessage.id}`) + + const proofRecord = await this.getByThreadAndConnectionId( + messageContext.agentContext, + presentationMessage.threadId, + connection?.id + ) + + const previousSentMessage = await didCommMessageRepository.getAgentMessage(messageContext.agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2RequestPresentationMessage, + }) + + const previousReceivedMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2ProposePresentationMessage, + }) + + // Assert + proofRecord.assertProtocolVersion('v2') + proofRecord.assertState(ProofState.RequestSent) + connectionService.assertConnectionOrServiceDecorator(messageContext, { + previousReceivedMessage, + previousSentMessage, + }) + + const formatServices = this.getFormatServicesFromMessage(presentationMessage.formats) + if (formatServices.length === 0) { + throw new AriesFrameworkError(`Unable to process presentation. No supported formats`) + } + + const isValid = await this.proofFormatCoordinator.processPresentation(messageContext.agentContext, { + proofRecord, + formatServices, + requestMessage: previousSentMessage, + message: presentationMessage, + }) + + proofRecord.isVerified = isValid + await this.updateState(messageContext.agentContext, proofRecord, ProofState.PresentationReceived) + + return proofRecord + } + + public async acceptPresentation( + agentContext: AgentContext, + { proofRecord }: AcceptPresentationOptions + ): Promise> { + proofRecord.assertProtocolVersion('v2') + proofRecord.assertState(ProofState.PresentationReceived) + + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // assert we've received the final presentation + const presentation = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2PresentationMessage, + }) + + if (!presentation.lastPresentation) { + throw new AriesFrameworkError( + `Trying to send an ack message while presentation with id ${presentation.id} indicates this is not the last presentation (presentation.last_presentation is set to false)` + ) + } + + const message = new V2PresentationAckMessage({ + threadId: proofRecord.threadId, + status: AckStatus.OK, + }) + + await this.updateState(agentContext, proofRecord, ProofState.Done) + + return { + message, + proofRecord, + } + } + + public async processAck( + messageContext: InboundMessageContext + ): Promise { + const { message: ackMessage, connection, agentContext } = messageContext + + agentContext.config.logger.debug(`Processing proof ack with id ${ackMessage.id}`) + + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + const connectionService = agentContext.dependencyManager.resolve(ConnectionService) + + const proofRecord = await this.getByThreadAndConnectionId( + messageContext.agentContext, + ackMessage.threadId, + connection?.id + ) + proofRecord.connectionId = connection?.id + + const previousReceivedMessage = await didCommMessageRepository.getAgentMessage(messageContext.agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2RequestPresentationMessage, + }) + + const previousSentMessage = await didCommMessageRepository.getAgentMessage(messageContext.agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V2PresentationMessage, + }) + + // Assert + proofRecord.assertProtocolVersion('v2') + proofRecord.assertState(ProofState.PresentationSent) + connectionService.assertConnectionOrServiceDecorator(messageContext, { + previousReceivedMessage, + previousSentMessage, + }) + + // Update record + await this.updateState(messageContext.agentContext, proofRecord, ProofState.Done) + + return proofRecord + } + + public async createProblemReport( + agentContext: AgentContext, + { description, proofRecord }: CreateProofProblemReportOptions + ): Promise> { + const message = new V2PresentationProblemReportMessage({ + description: { + en: description, + code: PresentationProblemReportReason.Abandoned, + }, + }) + + message.setThread({ + threadId: proofRecord.threadId, + parentThreadId: proofRecord.parentThreadId, + }) + + return { + proofRecord, + message, + } + } + + public async shouldAutoRespondToProposal( + agentContext: AgentContext, + options: { + proofRecord: ProofExchangeRecord + proposalMessage: V2ProposePresentationMessage + } + ): Promise { + const { proofRecord, proposalMessage } = options + const proofsModuleConfig = agentContext.dependencyManager.resolve(ProofsModuleConfig) + + const autoAccept = composeAutoAccept(proofRecord.autoAcceptProof, proofsModuleConfig.autoAcceptProofs) + + // Handle always / never cases + if (autoAccept === AutoAcceptProof.Always) return true + if (autoAccept === AutoAcceptProof.Never) return false + + const requestMessage = await this.findRequestMessage(agentContext, proofRecord.id) + if (!requestMessage) return false + + // NOTE: we take the formats from the requestMessage so we always check all services that we last sent + // Otherwise we'll only check the formats from the proposal, which could be different from the formats + // we use. + const formatServices = this.getFormatServicesFromMessage(requestMessage.formats) + + for (const formatService of formatServices) { + const requestAttachment = this.proofFormatCoordinator.getAttachmentForService( + formatService, + requestMessage.formats, + requestMessage.requestAttachments + ) + + const proposalAttachment = this.proofFormatCoordinator.getAttachmentForService( + formatService, + proposalMessage.formats, + proposalMessage.proposalAttachments + ) + + const shouldAutoRespondToFormat = await formatService.shouldAutoRespondToProposal(agentContext, { + proofRecord, + requestAttachment, + proposalAttachment, + }) + // If any of the formats return false, we should not auto accept + if (!shouldAutoRespondToFormat) return false + } + + return true + } + + public async shouldAutoRespondToRequest( + agentContext: AgentContext, + options: { + proofRecord: ProofExchangeRecord + requestMessage: V2RequestPresentationMessage + } + ): Promise { + const { proofRecord, requestMessage } = options + const proofsModuleConfig = agentContext.dependencyManager.resolve(ProofsModuleConfig) + + const autoAccept = composeAutoAccept(proofRecord.autoAcceptProof, proofsModuleConfig.autoAcceptProofs) + + // Handle always / never cases + if (autoAccept === AutoAcceptProof.Always) return true + if (autoAccept === AutoAcceptProof.Never) return false + + const proposalMessage = await this.findProposalMessage(agentContext, proofRecord.id) + if (!proposalMessage) return false + + // NOTE: we take the formats from the proposalMessage so we always check all services that we last sent + // Otherwise we'll only check the formats from the request, which could be different from the formats + // we use. + const formatServices = this.getFormatServicesFromMessage(proposalMessage.formats) + + for (const formatService of formatServices) { + const proposalAttachment = this.proofFormatCoordinator.getAttachmentForService( + formatService, + proposalMessage.formats, + proposalMessage.proposalAttachments + ) + + const requestAttachment = this.proofFormatCoordinator.getAttachmentForService( + formatService, + requestMessage.formats, + requestMessage.requestAttachments + ) + + const shouldAutoRespondToFormat = await formatService.shouldAutoRespondToRequest(agentContext, { + proofRecord, + requestAttachment, + proposalAttachment, + }) + + // If any of the formats return false, we should not auto accept + if (!shouldAutoRespondToFormat) return false + } + + return true + } + + public async shouldAutoRespondToPresentation( + agentContext: AgentContext, + options: { proofRecord: ProofExchangeRecord; presentationMessage: V2PresentationMessage } + ): Promise { + const { proofRecord, presentationMessage } = options + const proofsModuleConfig = agentContext.dependencyManager.resolve(ProofsModuleConfig) + + // If this isn't the last presentation yet, we should not auto accept + if (!presentationMessage.lastPresentation) return false + + const autoAccept = composeAutoAccept(proofRecord.autoAcceptProof, proofsModuleConfig.autoAcceptProofs) + + // Handle always / never cases + if (autoAccept === AutoAcceptProof.Always) return true + if (autoAccept === AutoAcceptProof.Never) return false + + const proposalMessage = await this.findProposalMessage(agentContext, proofRecord.id) + + const requestMessage = await this.findRequestMessage(agentContext, proofRecord.id) + if (!requestMessage) return false + if (!requestMessage.willConfirm) return false + + // NOTE: we take the formats from the requestMessage so we always check all services that we last sent + // Otherwise we'll only check the formats from the credential, which could be different from the formats + // we use. + const formatServices = this.getFormatServicesFromMessage(requestMessage.formats) + + for (const formatService of formatServices) { + const proposalAttachment = proposalMessage + ? this.proofFormatCoordinator.getAttachmentForService( + formatService, + proposalMessage.formats, + proposalMessage.proposalAttachments + ) + : undefined + + const requestAttachment = this.proofFormatCoordinator.getAttachmentForService( + formatService, + requestMessage.formats, + requestMessage.requestAttachments + ) + + const presentationAttachment = this.proofFormatCoordinator.getAttachmentForService( + formatService, + presentationMessage.formats, + presentationMessage.presentationAttachments + ) + + const shouldAutoRespondToFormat = await formatService.shouldAutoRespondToPresentation(agentContext, { + proofRecord, + presentationAttachment, + requestAttachment, + proposalAttachment, + }) + + // If any of the formats return false, we should not auto accept + if (!shouldAutoRespondToFormat) return false + } + return true + } + + public async findRequestMessage( + agentContext: AgentContext, + proofRecordId: string + ): Promise { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + return await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: proofRecordId, + messageClass: V2RequestPresentationMessage, + }) + } + + public async findPresentationMessage( + agentContext: AgentContext, + proofRecordId: string + ): Promise { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + return await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: proofRecordId, + messageClass: V2PresentationMessage, + }) + } + + public async findProposalMessage( + agentContext: AgentContext, + proofRecordId: string + ): Promise { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + return await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: proofRecordId, + messageClass: V2ProposePresentationMessage, + }) + } + + public async getFormatData(agentContext: AgentContext, proofRecordId: string): Promise { + // TODO: we could looking at fetching all record using a single query and then filtering based on the type of the message. + const [proposalMessage, requestMessage, presentationMessage] = await Promise.all([ + this.findProposalMessage(agentContext, proofRecordId), + this.findRequestMessage(agentContext, proofRecordId), + this.findPresentationMessage(agentContext, proofRecordId), + ]) + + // Create object with the keys and the message formats/attachments. We can then loop over this in a generic + // way so we don't have to add the same operation code four times + const messages = { + proposal: [proposalMessage?.formats, proposalMessage?.proposalAttachments], + request: [requestMessage?.formats, requestMessage?.requestAttachments], + presentation: [presentationMessage?.formats, presentationMessage?.presentationAttachments], + } as const + + const formatData: GetProofFormatDataReturn = {} + + // We loop through all of the message keys as defined above + for (const [messageKey, [formats, attachments]] of Object.entries(messages)) { + // Message can be undefined, so we continue if it is not defined + if (!formats || !attachments) continue + + // Find all format services associated with the message + const formatServices = this.getFormatServicesFromMessage(formats) + + const messageFormatData: ProofFormatDataMessagePayload = {} + + // Loop through all of the format services, for each we will extract the attachment data and assign this to the object + // using the unique format key (e.g. indy) + for (const formatService of formatServices) { + const attachment = this.proofFormatCoordinator.getAttachmentForService(formatService, formats, attachments) + messageFormatData[formatService.formatKey] = attachment.getDataAsJson() + } + + formatData[messageKey as keyof GetProofFormatDataReturn] = messageFormatData + } + + return formatData + } + + /** + * Get all the format service objects for a given proof format from an incoming message + * @param messageFormats the format objects containing the format name (eg indy) + * @return the proof format service objects in an array - derived from format object keys + */ + private getFormatServicesFromMessage(messageFormats: ProofFormatSpec[]): ProofFormatService[] { + const formatServices = new Set() + + for (const msg of messageFormats) { + const service = this.getFormatServiceForFormat(msg.format) + if (service) formatServices.add(service) + } + + return Array.from(formatServices) + } + + /** + * Get all the format service objects for a given proof format + * @param proofFormats the format object containing various optional parameters + * @return the proof format service objects in an array - derived from format object keys + */ + private getFormatServices( + proofFormats: M extends 'selectCredentialsForRequest' | 'getCredentialsForRequest' + ? ProofFormatCredentialForRequestPayload, M, 'input'> + : ProofFormatPayload, M> + ): ProofFormatService[] { + const formats = new Set() + + for (const formatKey of Object.keys(proofFormats)) { + const formatService = this.getFormatServiceForFormatKey(formatKey) + + if (formatService) formats.add(formatService) + } + + return Array.from(formats) + } + + private getFormatServiceForFormatKey(formatKey: string): ProofFormatService | null { + const formatService = this.proofFormats.find((proofFormats) => proofFormats.formatKey === formatKey) + + return formatService ?? null + } + + private getFormatServiceForFormat(format: string): ProofFormatService | null { + const formatService = this.proofFormats.find((proofFormats) => proofFormats.supportsFormat(format)) + + return formatService ?? null + } +} diff --git a/packages/core/src/modules/proofs/protocol/v2/V2ProofService.ts b/packages/core/src/modules/proofs/protocol/v2/V2ProofService.ts deleted file mode 100644 index cfe6ae8e43..0000000000 --- a/packages/core/src/modules/proofs/protocol/v2/V2ProofService.ts +++ /dev/null @@ -1,953 +0,0 @@ -import type { AgentContext } from '../../../../agent' -import type { AgentMessage } from '../../../../agent/AgentMessage' -import type { Dispatcher } from '../../../../agent/Dispatcher' -import type { InboundMessageContext } from '../../../../agent/models/InboundMessageContext' -import type { Attachment } from '../../../../decorators/attachment/Attachment' -import type { MediationRecipientService } from '../../../routing/services/MediationRecipientService' -import type { RoutingService } from '../../../routing/services/RoutingService' -import type { ProofResponseCoordinator } from '../../ProofResponseCoordinator' -import type { ProofFormatServiceMap } from '../../formats' -import type { ProofFormat } from '../../formats/ProofFormat' -import type { ProofFormatService } from '../../formats/ProofFormatService' -import type { CreateProblemReportOptions } from '../../formats/models/ProofFormatServiceOptions' -import type { ProofFormatSpec } from '../../models/ProofFormatSpec' -import type { - CreateAckOptions, - CreatePresentationOptions, - CreateProofRequestFromProposalOptions, - CreateProposalAsResponseOptions, - CreateProposalOptions, - CreateRequestAsResponseOptions, - CreateRequestOptions, - FormatDataMessagePayload, - FormatRequestedCredentialReturn, - FormatRetrievedCredentialOptions, - GetFormatDataReturn, - GetRequestedCredentialsForProofRequestOptions, - ProofRequestFromProposalOptions, -} from '../../models/ProofServiceOptions' - -import { inject, Lifecycle, scoped } from 'tsyringe' - -import { AgentConfig } from '../../../../agent/AgentConfig' -import { EventEmitter } from '../../../../agent/EventEmitter' -import { InjectionSymbols } from '../../../../constants' -import { AriesFrameworkError } from '../../../../error' -import { DidCommMessageRepository, DidCommMessageRole } from '../../../../storage' -import { MessageValidator } from '../../../../utils/MessageValidator' -import { Wallet } from '../../../../wallet/Wallet' -import { AckStatus } from '../../../common' -import { ConnectionService } from '../../../connections' -import { ProofService } from '../../ProofService' -import { PresentationProblemReportReason } from '../../errors/PresentationProblemReportReason' -import { V2_INDY_PRESENTATION_REQUEST } from '../../formats/ProofFormatConstants' -import { IndyProofFormatService } from '../../formats/indy/IndyProofFormatService' -import { ProofState } from '../../models/ProofState' -import { ProofExchangeRecord, ProofRepository } from '../../repository' - -import { V2PresentationProblemReportError } from './errors' -import { V2PresentationAckHandler } from './handlers/V2PresentationAckHandler' -import { V2PresentationHandler } from './handlers/V2PresentationHandler' -import { V2PresentationProblemReportHandler } from './handlers/V2PresentationProblemReportHandler' -import { V2ProposePresentationHandler } from './handlers/V2ProposePresentationHandler' -import { V2RequestPresentationHandler } from './handlers/V2RequestPresentationHandler' -import { V2PresentationAckMessage } from './messages' -import { V2PresentationMessage } from './messages/V2PresentationMessage' -import { V2PresentationProblemReportMessage } from './messages/V2PresentationProblemReportMessage' -import { V2ProposalPresentationMessage } from './messages/V2ProposalPresentationMessage' -import { V2RequestPresentationMessage } from './messages/V2RequestPresentationMessage' - -@scoped(Lifecycle.ContainerScoped) -export class V2ProofService extends ProofService { - private formatServiceMap: { [key: string]: ProofFormatService } - - public constructor( - agentConfig: AgentConfig, - connectionService: ConnectionService, - proofRepository: ProofRepository, - didCommMessageRepository: DidCommMessageRepository, - eventEmitter: EventEmitter, - indyProofFormatService: IndyProofFormatService, - @inject(InjectionSymbols.Wallet) wallet: Wallet - ) { - super(agentConfig, proofRepository, connectionService, didCommMessageRepository, wallet, eventEmitter) - this.wallet = wallet - // Dynamically build format service map. This will be extracted once services are registered dynamically - this.formatServiceMap = [indyProofFormatService].reduce( - (formatServiceMap, formatService) => ({ - ...formatServiceMap, - [formatService.formatKey]: formatService, - }), - {} - ) as ProofFormatServiceMap - } - - /** - * The version of the present proof protocol this service supports - */ - public readonly version = 'v2' as const - - public async createProposal( - agentContext: AgentContext, - options: CreateProposalOptions - ): Promise<{ proofRecord: ProofExchangeRecord; message: AgentMessage }> { - const formats = [] - for (const key of Object.keys(options.proofFormats)) { - const service = this.formatServiceMap[key] - formats.push(await service.createProposal({ formats: options.proofFormats })) - } - - const proposalMessage = new V2ProposalPresentationMessage({ - attachmentInfo: formats, - comment: options.comment, - willConfirm: options.willConfirm, - goalCode: options.goalCode, - parentThreadId: options.parentThreadId, - }) - - const proofRecord = new ProofExchangeRecord({ - connectionId: options.connectionRecord.id, - threadId: proposalMessage.threadId, - parentThreadId: proposalMessage.thread?.parentThreadId, - state: ProofState.ProposalSent, - protocolVersion: 'v2', - }) - - await this.proofRepository.save(agentContext, proofRecord) - - await this.didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { - agentMessage: proposalMessage, - role: DidCommMessageRole.Sender, - associatedRecordId: proofRecord.id, - }) - - this.emitStateChangedEvent(agentContext, proofRecord, null) - - return { - proofRecord: proofRecord, - message: proposalMessage, - } - } - - public async createProposalAsResponse( - agentContext: AgentContext, - options: CreateProposalAsResponseOptions - ): Promise<{ proofRecord: ProofExchangeRecord; message: AgentMessage }> { - options.proofRecord.assertState(ProofState.RequestReceived) - - const formats = [] - for (const key of Object.keys(options.proofFormats)) { - const service = this.formatServiceMap[key] - formats.push( - await service.createProposal({ - formats: options.proofFormats, - }) - ) - } - - const proposalMessage = new V2ProposalPresentationMessage({ - attachmentInfo: formats, - comment: options.comment, - goalCode: options.goalCode, - willConfirm: options.willConfirm, - }) - - proposalMessage.setThread({ threadId: options.proofRecord.threadId }) - - await this.didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { - agentMessage: proposalMessage, - role: DidCommMessageRole.Sender, - associatedRecordId: options.proofRecord.id, - }) - - await this.updateState(agentContext, options.proofRecord, ProofState.ProposalSent) - - return { message: proposalMessage, proofRecord: options.proofRecord } - } - - public async processProposal( - messageContext: InboundMessageContext - ): Promise { - const { message: proposalMessage, connection: connectionRecord } = messageContext - let proofRecord: ProofExchangeRecord - - const proposalAttachments = proposalMessage.getAttachmentFormats() - - for (const attachmentFormat of proposalAttachments) { - const service = this.getFormatServiceForFormat(attachmentFormat.format) - await service?.processProposal({ - proposal: attachmentFormat, - }) - } - - try { - proofRecord = await this.proofRepository.getSingleByQuery(messageContext.agentContext, { - threadId: proposalMessage.threadId, - connectionId: connectionRecord?.id, - }) - - const requestMessage = await this.didCommMessageRepository.findAgentMessage(messageContext.agentContext, { - associatedRecordId: proofRecord.id, - messageClass: V2RequestPresentationMessage, - }) - - // Assert - proofRecord.assertState(ProofState.RequestSent) - this.connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage: proposalMessage, - previousSentMessage: requestMessage ?? undefined, - }) - - await this.didCommMessageRepository.saveOrUpdateAgentMessage(messageContext.agentContext, { - agentMessage: proposalMessage, - associatedRecordId: proofRecord.id, - role: DidCommMessageRole.Receiver, - }) - - await this.updateState(messageContext.agentContext, proofRecord, ProofState.ProposalReceived) - } catch { - // No proof record exists with thread id - proofRecord = new ProofExchangeRecord({ - connectionId: connectionRecord?.id, - threadId: proposalMessage.threadId, - parentThreadId: proposalMessage.thread?.parentThreadId, - state: ProofState.ProposalReceived, - protocolVersion: 'v2', - }) - - // Assert - this.connectionService.assertConnectionOrServiceDecorator(messageContext) - - // Save record - await this.didCommMessageRepository.saveOrUpdateAgentMessage(messageContext.agentContext, { - agentMessage: proposalMessage, - associatedRecordId: proofRecord.id, - role: DidCommMessageRole.Receiver, - }) - - await this.proofRepository.save(messageContext.agentContext, proofRecord) - this.emitStateChangedEvent(messageContext.agentContext, proofRecord, null) - } - - return proofRecord - } - - public async createRequest( - agentContext: AgentContext, - options: CreateRequestOptions - ): Promise<{ proofRecord: ProofExchangeRecord; message: AgentMessage }> { - // create attachment formats - const formats = [] - for (const key of Object.keys(options.proofFormats)) { - const service = this.formatServiceMap[key] - formats.push( - await service.createRequest({ - formats: options.proofFormats, - }) - ) - } - - // create request message - const requestMessage = new V2RequestPresentationMessage({ - attachmentInfo: formats, - comment: options.comment, - willConfirm: options.willConfirm, - goalCode: options.goalCode, - parentThreadId: options.parentThreadId, - }) - - // create & store proof record - const proofRecord = new ProofExchangeRecord({ - connectionId: options.connectionRecord?.id, - threadId: requestMessage.threadId, - parentThreadId: requestMessage.thread?.parentThreadId, - state: ProofState.RequestSent, - protocolVersion: 'v2', - }) - - await this.proofRepository.save(agentContext, proofRecord) - - // create DIDComm message - await this.didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { - agentMessage: requestMessage, - role: DidCommMessageRole.Sender, - associatedRecordId: proofRecord.id, - }) - - this.emitStateChangedEvent(agentContext, proofRecord, null) - - return { - proofRecord: proofRecord, - message: requestMessage, - } - } - - public async createRequestAsResponse( - agentContext: AgentContext, - options: CreateRequestAsResponseOptions - ): Promise<{ proofRecord: ProofExchangeRecord; message: AgentMessage }> { - options.proofRecord.assertState(ProofState.ProposalReceived) - - const proposal = await this.didCommMessageRepository.getAgentMessage(agentContext, { - associatedRecordId: options.proofRecord.id, - messageClass: V2ProposalPresentationMessage, - }) - - if (!proposal) { - throw new AriesFrameworkError( - `Proof record with id ${options.proofRecord.id} is missing required presentation proposal` - ) - } - - // create attachment formats - const formats = [] - - for (const key of Object.keys(options.proofFormats)) { - const service = this.formatServiceMap[key] - const requestOptions: CreateRequestAsResponseOptions = { - proofFormats: options.proofFormats, - proofRecord: options.proofRecord, - } - formats.push(await service.createRequestAsResponse(requestOptions)) - } - - // create request message - const requestMessage = new V2RequestPresentationMessage({ - attachmentInfo: formats, - comment: options.comment, - willConfirm: options.willConfirm, - goalCode: options.goalCode, - }) - requestMessage.setThread({ threadId: options.proofRecord.threadId }) - - await this.didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { - agentMessage: requestMessage, - role: DidCommMessageRole.Sender, - associatedRecordId: options.proofRecord.id, - }) - - await this.updateState(agentContext, options.proofRecord, ProofState.RequestSent) - - return { message: requestMessage, proofRecord: options.proofRecord } - } - - public async processRequest( - messageContext: InboundMessageContext - ): Promise { - const { message: proofRequestMessage, connection: connectionRecord } = messageContext - - const requestAttachments = proofRequestMessage.getAttachmentFormats() - - for (const attachmentFormat of requestAttachments) { - const service = this.getFormatServiceForFormat(attachmentFormat.format) - await service?.processRequest({ - requestAttachment: attachmentFormat, - }) - } - - // assert - if (proofRequestMessage.requestPresentationsAttach.length === 0) { - throw new V2PresentationProblemReportError( - `Missing required base64 or json encoded attachment data for presentation request with thread id ${proofRequestMessage.threadId}`, - { problemCode: PresentationProblemReportReason.Abandoned } - ) - } - - this.logger.debug(`Received proof request`, proofRequestMessage) - - let proofRecord: ProofExchangeRecord - - try { - proofRecord = await this.proofRepository.getSingleByQuery(messageContext.agentContext, { - threadId: proofRequestMessage.threadId, - connectionId: connectionRecord?.id, - }) - - const requestMessage = await this.didCommMessageRepository.findAgentMessage(messageContext.agentContext, { - associatedRecordId: proofRecord.id, - messageClass: V2RequestPresentationMessage, - }) - - const proposalMessage = await this.didCommMessageRepository.findAgentMessage(messageContext.agentContext, { - associatedRecordId: proofRecord.id, - messageClass: V2ProposalPresentationMessage, - }) - - // Assert - proofRecord.assertState(ProofState.ProposalSent) - this.connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage: requestMessage ?? undefined, - previousSentMessage: proposalMessage ?? undefined, - }) - - await this.didCommMessageRepository.saveOrUpdateAgentMessage(messageContext.agentContext, { - agentMessage: proofRequestMessage, - associatedRecordId: proofRecord.id, - role: DidCommMessageRole.Receiver, - }) - - // Update record - await this.updateState(messageContext.agentContext, proofRecord, ProofState.RequestReceived) - } catch { - // No proof record exists with thread id - proofRecord = new ProofExchangeRecord({ - connectionId: connectionRecord?.id, - threadId: proofRequestMessage.threadId, - parentThreadId: proofRequestMessage.thread?.parentThreadId, - state: ProofState.RequestReceived, - protocolVersion: 'v2', - }) - - await this.didCommMessageRepository.saveOrUpdateAgentMessage(messageContext.agentContext, { - agentMessage: proofRequestMessage, - associatedRecordId: proofRecord.id, - role: DidCommMessageRole.Receiver, - }) - - // Assert - this.connectionService.assertConnectionOrServiceDecorator(messageContext) - - // Save in repository - await this.proofRepository.save(messageContext.agentContext, proofRecord) - this.emitStateChangedEvent(messageContext.agentContext, proofRecord, null) - } - - return proofRecord - } - - public async createPresentation( - agentContext: AgentContext, - options: CreatePresentationOptions - ): Promise<{ proofRecord: ProofExchangeRecord; message: AgentMessage }> { - // assert state - options.proofRecord.assertState(ProofState.RequestReceived) - - const proofRequest = await this.didCommMessageRepository.getAgentMessage(agentContext, { - associatedRecordId: options.proofRecord.id, - messageClass: V2RequestPresentationMessage, - }) - - const formats = [] - for (const key of Object.keys(options.proofFormats)) { - const service = this.formatServiceMap[key] - formats.push( - await service.createPresentation(agentContext, { - attachment: proofRequest.getAttachmentByFormatIdentifier(V2_INDY_PRESENTATION_REQUEST), - proofFormats: options.proofFormats, - }) - ) - } - - const presentationMessage = new V2PresentationMessage({ - comment: options.comment, - attachmentInfo: formats, - goalCode: options.goalCode, - lastPresentation: options.lastPresentation, - }) - presentationMessage.setThread({ threadId: options.proofRecord.threadId }) - - await this.didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { - agentMessage: presentationMessage, - associatedRecordId: options.proofRecord.id, - role: DidCommMessageRole.Sender, - }) - - await this.updateState(agentContext, options.proofRecord, ProofState.PresentationSent) - - return { message: presentationMessage, proofRecord: options.proofRecord } - } - - public async processPresentation( - messageContext: InboundMessageContext - ): Promise { - const { message: presentationMessage, connection: connectionRecord } = messageContext - - this.logger.debug(`Processing presentation with id ${presentationMessage.id}`) - - const proofRecord = await this.proofRepository.getSingleByQuery(messageContext.agentContext, { - threadId: presentationMessage.threadId, - connectionId: connectionRecord?.id, - }) - - const proposalMessage = await this.didCommMessageRepository.findAgentMessage(messageContext.agentContext, { - associatedRecordId: proofRecord.id, - messageClass: V2ProposalPresentationMessage, - }) - - const requestMessage = await this.didCommMessageRepository.getAgentMessage(messageContext.agentContext, { - associatedRecordId: proofRecord.id, - messageClass: V2RequestPresentationMessage, - }) - - // Assert - proofRecord.assertState(ProofState.RequestSent) - this.connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage: proposalMessage ?? undefined, - previousSentMessage: requestMessage ?? undefined, - }) - - const formatVerificationResults = [] - for (const attachmentFormat of presentationMessage.getAttachmentFormats()) { - const service = this.getFormatServiceForFormat(attachmentFormat.format) - if (service) { - try { - formatVerificationResults.push( - await service.processPresentation(messageContext.agentContext, { - record: proofRecord, - formatAttachments: { - request: requestMessage?.getAttachmentFormats(), - presentation: presentationMessage.getAttachmentFormats(), - }, - }) - ) - } catch (e) { - if (e instanceof AriesFrameworkError) { - throw new V2PresentationProblemReportError(e.message, { - problemCode: PresentationProblemReportReason.Abandoned, - }) - } - throw e - } - } - } - if (formatVerificationResults.length === 0) { - throw new V2PresentationProblemReportError('None of the received formats are supported.', { - problemCode: PresentationProblemReportReason.Abandoned, - }) - } - - const isValid = formatVerificationResults.every((x) => x === true) - - await this.didCommMessageRepository.saveOrUpdateAgentMessage(messageContext.agentContext, { - agentMessage: presentationMessage, - associatedRecordId: proofRecord.id, - role: DidCommMessageRole.Receiver, - }) - - // Update record - proofRecord.isVerified = isValid - await this.updateState(messageContext.agentContext, proofRecord, ProofState.PresentationReceived) - - return proofRecord - } - - public async createAck( - agentContext: AgentContext, - options: CreateAckOptions - ): Promise<{ proofRecord: ProofExchangeRecord; message: AgentMessage }> { - // assert we've received the final presentation - const presentation = await this.didCommMessageRepository.getAgentMessage(agentContext, { - associatedRecordId: options.proofRecord.id, - messageClass: V2PresentationMessage, - }) - - if (!presentation.lastPresentation) { - throw new AriesFrameworkError( - `Trying to send an ack message while presentation with id ${presentation.id} indicates this is not the last presentation (presentation.lastPresentation is set to false)` - ) - } - - const message = new V2PresentationAckMessage({ - threadId: options.proofRecord.threadId, - status: AckStatus.OK, - }) - - await this.updateState(agentContext, options.proofRecord, ProofState.Done) - - return { - message, - proofRecord: options.proofRecord, - } - } - - public async processAck( - messageContext: InboundMessageContext - ): Promise { - const { message: ackMessage, connection: connectionRecord } = messageContext - - const proofRecord = await this.proofRepository.getSingleByQuery(messageContext.agentContext, { - threadId: ackMessage.threadId, - connectionId: connectionRecord?.id, - }) - - const requestMessage = await this.didCommMessageRepository.findAgentMessage(messageContext.agentContext, { - associatedRecordId: proofRecord.id, - messageClass: V2RequestPresentationMessage, - }) - - const presentationMessage = await this.didCommMessageRepository.findAgentMessage(messageContext.agentContext, { - associatedRecordId: proofRecord.id, - messageClass: V2PresentationMessage, - }) - - // Assert - proofRecord.assertState(ProofState.PresentationSent) - this.connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage: requestMessage ?? undefined, - previousSentMessage: presentationMessage ?? undefined, - }) - - // Update record - await this.updateState(messageContext.agentContext, proofRecord, ProofState.Done) - - return proofRecord - } - - public async createProblemReport( - agentContext: AgentContext, - options: CreateProblemReportOptions - ): Promise<{ proofRecord: ProofExchangeRecord; message: AgentMessage }> { - const msg = new V2PresentationProblemReportMessage({ - description: { - code: PresentationProblemReportReason.Abandoned, - en: options.description, - }, - }) - - msg.setThread({ - threadId: options.proofRecord.threadId, - parentThreadId: options.proofRecord.threadId, - }) - - return { - proofRecord: options.proofRecord, - message: msg, - } - } - - public async processProblemReport( - messageContext: InboundMessageContext - ): Promise { - const { message: presentationProblemReportMessage } = messageContext - - const connectionRecord = messageContext.assertReadyConnection() - - this.logger.debug(`Processing problem report with id ${presentationProblemReportMessage.id}`) - - const proofRecord = await this.proofRepository.getSingleByQuery(messageContext.agentContext, { - threadId: presentationProblemReportMessage.threadId, - connectionId: connectionRecord?.id, - }) - - proofRecord.errorMessage = `${presentationProblemReportMessage.description.code}: ${presentationProblemReportMessage.description.en}` - await this.updateState(messageContext.agentContext, proofRecord, ProofState.Abandoned) - return proofRecord - } - - public async createProofRequestFromProposal( - agentContext: AgentContext, - options: CreateProofRequestFromProposalOptions - ): Promise> { - const proofRecordId = options.proofRecord.id - const proposalMessage = await this.didCommMessageRepository.findAgentMessage(agentContext, { - associatedRecordId: proofRecordId, - messageClass: V2ProposalPresentationMessage, - }) - - if (!proposalMessage) { - throw new AriesFrameworkError(`Proof record with id ${proofRecordId} is missing required presentation proposal`) - } - - const proposalAttachments = proposalMessage.getAttachmentFormats() - - let result = {} - - for (const attachmentFormat of proposalAttachments) { - const service = this.getFormatServiceForFormat(attachmentFormat.format) - - if (!service) { - throw new AriesFrameworkError('No format service found for getting requested.') - } - - result = { - ...result, - ...(await service.createProofRequestFromProposal({ - presentationAttachment: attachmentFormat.attachment, - })), - } - } - - const retVal: ProofRequestFromProposalOptions = { - proofRecord: options.proofRecord, - proofFormats: result, - } - return retVal - } - - public async shouldAutoRespondToProposal( - agentContext: AgentContext, - proofRecord: ProofExchangeRecord - ): Promise { - const proposal = await this.didCommMessageRepository.findAgentMessage(agentContext, { - associatedRecordId: proofRecord.id, - messageClass: V2ProposalPresentationMessage, - }) - - if (!proposal) return false - - const request = await this.didCommMessageRepository.findAgentMessage(agentContext, { - associatedRecordId: proofRecord.id, - messageClass: V2RequestPresentationMessage, - }) - - if (!request) return false - - MessageValidator.validateSync(proposal) - - const proposalAttachments = proposal.getAttachmentFormats() - const requestAttachments = request.getAttachmentFormats() - - const equalityResults = [] - for (const attachmentFormat of proposalAttachments) { - const service = this.getFormatServiceForFormat(attachmentFormat.format) - equalityResults.push(service?.proposalAndRequestAreEqual(proposalAttachments, requestAttachments)) - } - return true - } - - public async shouldAutoRespondToRequest( - agentContext: AgentContext, - proofRecord: ProofExchangeRecord - ): Promise { - const proposal = await this.didCommMessageRepository.findAgentMessage(agentContext, { - associatedRecordId: proofRecord.id, - messageClass: V2ProposalPresentationMessage, - }) - - if (!proposal) { - return false - } - - const request = await this.didCommMessageRepository.findAgentMessage(agentContext, { - associatedRecordId: proofRecord.id, - messageClass: V2RequestPresentationMessage, - }) - - if (!request) { - throw new AriesFrameworkError( - `Expected to find a request message for ProofExchangeRecord with id ${proofRecord.id}` - ) - } - - const proposalAttachments = proposal.getAttachmentFormats() - const requestAttachments = request.getAttachmentFormats() - - const equalityResults = [] - for (const attachmentFormat of proposalAttachments) { - const service = this.getFormatServiceForFormat(attachmentFormat.format) - equalityResults.push(service?.proposalAndRequestAreEqual(proposalAttachments, requestAttachments)) - } - - return equalityResults.every((x) => x === true) - } - - public async shouldAutoRespondToPresentation( - agentContext: AgentContext, - proofRecord: ProofExchangeRecord - ): Promise { - const request = await this.didCommMessageRepository.getAgentMessage(agentContext, { - associatedRecordId: proofRecord.id, - messageClass: V2RequestPresentationMessage, - }) - - return request.willConfirm - } - - public async findRequestMessage( - agentContext: AgentContext, - proofRecordId: string - ): Promise { - return await this.didCommMessageRepository.findAgentMessage(agentContext, { - associatedRecordId: proofRecordId, - messageClass: V2RequestPresentationMessage, - }) - } - - public async findPresentationMessage( - agentContext: AgentContext, - proofRecordId: string - ): Promise { - return await this.didCommMessageRepository.findAgentMessage(agentContext, { - associatedRecordId: proofRecordId, - messageClass: V2PresentationMessage, - }) - } - - public async findProposalMessage( - agentContext: AgentContext, - proofRecordId: string - ): Promise { - return await this.didCommMessageRepository.findAgentMessage(agentContext, { - associatedRecordId: proofRecordId, - messageClass: V2ProposalPresentationMessage, - }) - } - - public async getFormatData(agentContext: AgentContext, proofRecordId: string): Promise { - // TODO: we could looking at fetching all record using a single query and then filtering based on the type of the message. - const [proposalMessage, requestMessage, presentationMessage] = await Promise.all([ - this.findProposalMessage(agentContext, proofRecordId), - this.findRequestMessage(agentContext, proofRecordId), - this.findPresentationMessage(agentContext, proofRecordId), - ]) - - // Create object with the keys and the message formats/attachments. We can then loop over this in a generic - // way so we don't have to add the same operation code four times - const messages = { - proposal: [proposalMessage?.formats, proposalMessage?.proposalsAttach], - request: [requestMessage?.formats, requestMessage?.requestPresentationsAttach], - presentation: [presentationMessage?.formats, presentationMessage?.presentationsAttach], - } as const - - const formatData: GetFormatDataReturn = {} - - // We loop through all of the message keys as defined above - for (const [messageKey, [formats, attachments]] of Object.entries(messages)) { - // Message can be undefined, so we continue if it is not defined - if (!formats || !attachments) continue - - // Find all format services associated with the message - const formatServices = this.getFormatServicesFromMessage(formats) - - const messageFormatData: FormatDataMessagePayload = {} - - // Loop through all of the format services, for each we will extract the attachment data and assign this to the object - // using the unique format key (e.g. indy) - for (const formatService of formatServices) { - const attachment = this.getAttachmentForService(formatService, formats, attachments) - messageFormatData[formatService.formatKey] = attachment.getDataAsJson() - } - - formatData[messageKey as Exclude] = - messageFormatData - } - - return formatData - } - - private getFormatServicesFromMessage(messageFormats: ProofFormatSpec[]): ProofFormatService[] { - const formatServices = new Set() - - for (const msg of messageFormats) { - const service = this.getFormatServiceForFormat(msg) - if (service) formatServices.add(service) - } - - return Array.from(formatServices) - } - - private getAttachmentForService( - proofFormatService: ProofFormatService, - formats: ProofFormatSpec[], - attachments: Attachment[] - ) { - const attachmentId = this.getAttachmentIdForService(proofFormatService, formats) - const attachment = attachments.find((attachment) => attachment.id === attachmentId) - - if (!attachment) { - throw new AriesFrameworkError(`Attachment with id ${attachmentId} not found in attachments.`) - } - - return attachment - } - - private getAttachmentIdForService(proofFormatService: ProofFormatService, formats: ProofFormatSpec[]) { - const format = formats.find((format) => proofFormatService.supportsFormat(format.format)) - - if (!format) throw new AriesFrameworkError(`No attachment found for service ${proofFormatService.formatKey}`) - - return format.attachmentId - } - - public async getRequestedCredentialsForProofRequest( - agentContext: AgentContext, - options: GetRequestedCredentialsForProofRequestOptions - ): Promise> { - const requestMessage = await this.didCommMessageRepository.findAgentMessage(agentContext, { - associatedRecordId: options.proofRecord.id, - messageClass: V2RequestPresentationMessage, - }) - - if (!requestMessage) { - throw new AriesFrameworkError('No proof request found.') - } - - const requestAttachments = requestMessage.getAttachmentFormats() - - let result = { - proofFormats: {}, - } - for (const attachmentFormat of requestAttachments) { - const service = this.getFormatServiceForFormat(attachmentFormat.format) - - if (!service) { - throw new AriesFrameworkError('No format service found for getting requested.') - } - - result = { - ...result, - ...(await service.getRequestedCredentialsForProofRequest(agentContext, { - attachment: attachmentFormat.attachment, - presentationProposal: undefined, - config: options.config, - })), - } - } - - return result - } - - public async autoSelectCredentialsForProofRequest( - options: FormatRetrievedCredentialOptions - ): Promise> { - let returnValue = { - proofFormats: {}, - } - - for (const [id] of Object.entries(options.proofFormats)) { - const service = this.formatServiceMap[id] - const credentials = await service.autoSelectCredentialsForProofRequest(options) - returnValue = { ...returnValue, ...credentials } - } - - return returnValue - } - - public registerMessageHandlers( - dispatcher: Dispatcher, - agentConfig: AgentConfig, - proofResponseCoordinator: ProofResponseCoordinator, - mediationRecipientService: MediationRecipientService, - routingService: RoutingService - ): void { - dispatcher.registerMessageHandler( - new V2ProposePresentationHandler(this, agentConfig, this.didCommMessageRepository, proofResponseCoordinator) - ) - - dispatcher.registerMessageHandler( - new V2RequestPresentationHandler( - this, - agentConfig, - proofResponseCoordinator, - mediationRecipientService, - this.didCommMessageRepository, - routingService - ) - ) - - dispatcher.registerMessageHandler( - new V2PresentationHandler(this, agentConfig, proofResponseCoordinator, this.didCommMessageRepository) - ) - dispatcher.registerMessageHandler(new V2PresentationAckHandler(this)) - dispatcher.registerMessageHandler(new V2PresentationProblemReportHandler(this)) - } - - private getFormatServiceForFormat(format: ProofFormatSpec) { - for (const service of Object.values(this.formatServiceMap)) { - if (service.supportsFormat(format.format)) { - return service - } - } - return null - } -} diff --git a/packages/core/src/modules/proofs/__tests__/V2ProofService.test.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/V2ProofProtocol.test.ts similarity index 61% rename from packages/core/src/modules/proofs/__tests__/V2ProofService.test.ts rename to packages/core/src/modules/proofs/protocol/v2/__tests__/V2ProofProtocol.test.ts index 255c97a92b..397e0b8866 100644 --- a/packages/core/src/modules/proofs/__tests__/V2ProofService.test.ts +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/V2ProofProtocol.test.ts @@ -1,44 +1,55 @@ -import type { AgentContext } from '../../../agent' -import type { Wallet } from '../../../wallet/Wallet' -import type { ProofStateChangedEvent } from '../ProofEvents' -import type { CustomProofTags } from '../repository/ProofExchangeRecord' +import type { ProofStateChangedEvent } from '../../../ProofEvents' +import type { CustomProofTags } from '../../../repository/ProofExchangeRecord' import { Subject } from 'rxjs' -import { getAgentConfig, getAgentContext, getMockConnection, mockFunction } from '../../../../tests/helpers' -import { EventEmitter } from '../../../agent/EventEmitter' -import { InboundMessageContext } from '../../../agent/models/InboundMessageContext' -import { Attachment, AttachmentData } from '../../../decorators/attachment/Attachment' -import { DidCommMessageRepository } from '../../../storage' -import { ConnectionService, DidExchangeState } from '../../connections' -import { IndyLedgerService } from '../../ledger/services/IndyLedgerService' -import { ProofEventTypes } from '../ProofEvents' -import { PresentationProblemReportReason } from '../errors/PresentationProblemReportReason' -import { V2_INDY_PRESENTATION, V2_INDY_PRESENTATION_REQUEST } from '../formats/ProofFormatConstants' -import { IndyProofFormatService } from '../formats/indy/IndyProofFormatService' -import { ProofState } from '../models/ProofState' -import { V2ProofService } from '../protocol/v2/V2ProofService' -import { V2PresentationProblemReportMessage, V2RequestPresentationMessage } from '../protocol/v2/messages' -import { ProofExchangeRecord } from '../repository/ProofExchangeRecord' -import { ProofRepository } from '../repository/ProofRepository' - -import { credDef } from './fixtures' +import { getAgentConfig, getAgentContext, getMockConnection, mockFunction } from '../../../../../../tests/helpers' +import { EventEmitter } from '../../../../../agent/EventEmitter' +import { InboundMessageContext } from '../../../../../agent/models/InboundMessageContext' +import { Attachment, AttachmentData } from '../../../../../decorators/attachment/Attachment' +import { DidCommMessageRepository } from '../../../../../storage' +import { uuid } from '../../../../../utils/uuid' +import { ConnectionService, DidExchangeState } from '../../../../connections' +import { ProofEventTypes } from '../../../ProofEvents' +import { PresentationProblemReportReason } from '../../../errors/PresentationProblemReportReason' +import { IndyProofFormatService } from '../../../formats/indy/IndyProofFormatService' +import { ProofFormatSpec } from '../../../models/ProofFormatSpec' +import { ProofState } from '../../../models/ProofState' +import { ProofExchangeRecord } from '../../../repository/ProofExchangeRecord' +import { ProofRepository } from '../../../repository/ProofRepository' +import { V2ProofProtocol } from '../V2ProofProtocol' +import { V2PresentationProblemReportMessage, V2RequestPresentationMessage } from '../messages' // Mock classes -jest.mock('../repository/ProofRepository') -jest.mock('../../../modules/ledger/services/IndyLedgerService') -jest.mock('../../indy/services/IndyHolderService') -jest.mock('../../indy/services/IndyIssuerService') -jest.mock('../../indy/services/IndyVerifierService') -jest.mock('../../connections/services/ConnectionService') -jest.mock('../../../storage/Repository') +jest.mock('../../../repository/ProofRepository') +jest.mock('../../../../connections/services/ConnectionService') +jest.mock('../../../../../storage/Repository') // Mock typed object const ProofRepositoryMock = ProofRepository as jest.Mock -const IndyLedgerServiceMock = IndyLedgerService as jest.Mock const connectionServiceMock = ConnectionService as jest.Mock const didCommMessageRepositoryMock = DidCommMessageRepository as jest.Mock -const indyProofFormatServiceMock = IndyProofFormatService as jest.Mock +const IndyProofFormatServiceMock = IndyProofFormatService as jest.Mock + +const proofRepository = new ProofRepositoryMock() +const connectionService = new connectionServiceMock() +const didCommMessageRepository = new didCommMessageRepositoryMock() +const indyProofFormatService = new IndyProofFormatServiceMock() + +const agentConfig = getAgentConfig('V2ProofProtocolTest') +const eventEmitter = new EventEmitter(agentConfig.agentDependencies, new Subject()) + +const agentContext = getAgentContext({ + registerInstances: [ + [ProofRepository, proofRepository], + [DidCommMessageRepository, didCommMessageRepository], + [ConnectionService, connectionService], + [EventEmitter, eventEmitter], + ], + agentConfig, +}) + +const proofProtocol = new V2ProofProtocol({ proofFormats: [indyProofFormatService] }) const connection = getMockConnection({ id: '123', @@ -64,30 +75,16 @@ const mockProofExchangeRecord = ({ id, }: { state?: ProofState - requestMessage?: V2RequestPresentationMessage tags?: CustomProofTags threadId?: string connectionId?: string id?: string } = {}) => { - const requestPresentationMessage = new V2RequestPresentationMessage({ - attachmentInfo: [ - { - format: { - attachmentId: 'abdc8b63-29c6-49ad-9e10-98f9d85db9a2', - format: V2_INDY_PRESENTATION, - }, - attachment: requestAttachment, - }, - ], - comment: 'some comment', - }) - const proofRecord = new ProofExchangeRecord({ protocolVersion: 'v2', id, state: state || ProofState.RequestSent, - threadId: threadId ?? requestPresentationMessage.id, + threadId: threadId ?? uuid(), connectionId: connectionId ?? '123', tags, }) @@ -95,57 +92,23 @@ const mockProofExchangeRecord = ({ return proofRecord } -describe('V2ProofService', () => { - let proofRepository: ProofRepository - let proofService: V2ProofService - let ledgerService: IndyLedgerService - let wallet: Wallet - let eventEmitter: EventEmitter - let connectionService: ConnectionService - let didCommMessageRepository: DidCommMessageRepository - let indyProofFormatService: IndyProofFormatService - let agentContext: AgentContext - - beforeEach(() => { - agentContext = getAgentContext() - const agentConfig = getAgentConfig('V2ProofServiceTest') - proofRepository = new ProofRepositoryMock() - ledgerService = new IndyLedgerServiceMock() - eventEmitter = new EventEmitter(agentConfig.agentDependencies, new Subject()) - connectionService = new connectionServiceMock() - didCommMessageRepository = new didCommMessageRepositoryMock() - indyProofFormatService = new indyProofFormatServiceMock() - - proofService = new V2ProofService( - agentConfig, - connectionService, - proofRepository, - didCommMessageRepository, - eventEmitter, - indyProofFormatService, - wallet - ) - - mockFunction(ledgerService.getCredentialDefinition).mockReturnValue(Promise.resolve(credDef)) - }) - +describe('V2ProofProtocol', () => { describe('processProofRequest', () => { let presentationRequest: V2RequestPresentationMessage let messageContext: InboundMessageContext beforeEach(() => { presentationRequest = new V2RequestPresentationMessage({ - attachmentInfo: [ - { - format: { - attachmentId: 'abdc8b63-29c6-49ad-9e10-98f9d85db9a2', - format: V2_INDY_PRESENTATION_REQUEST, - }, - attachment: requestAttachment, - }, + formats: [ + new ProofFormatSpec({ + attachmentId: 'abdc8b63-29c6-49ad-9e10-98f9d85db9a2', + format: 'hlindy/proof-req@v2.0', + }), ], + requestAttachments: [requestAttachment], comment: 'Proof Request', }) + messageContext = new InboundMessageContext(presentationRequest, { agentContext, connection }) }) @@ -153,7 +116,7 @@ describe('V2ProofService', () => { const repositorySaveSpy = jest.spyOn(proofRepository, 'save') // when - const returnedProofExchangeRecord = await proofService.processRequest(messageContext) + const returnedProofExchangeRecord = await proofProtocol.processRequest(messageContext) // then const expectedProofExchangeRecord = { @@ -175,7 +138,7 @@ describe('V2ProofService', () => { eventEmitter.on(ProofEventTypes.ProofStateChanged, eventListenerMock) // when - await proofService.processRequest(messageContext) + await proofProtocol.processRequest(messageContext) // then expect(eventListenerMock).toHaveBeenCalledWith({ @@ -254,7 +217,7 @@ describe('V2ProofService', () => { mockFunction(proofRepository.getSingleByQuery).mockReturnValue(Promise.resolve(proof)) // when - const returnedCredentialRecord = await proofService.processProblemReport(messageContext) + const returnedCredentialRecord = await proofProtocol.processProblemReport(messageContext) // then const expectedCredentialRecord = { diff --git a/packages/core/tests/v2-connectionless-proofs.test.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-connectionless-proofs.e2e.test.ts similarity index 88% rename from packages/core/tests/v2-connectionless-proofs.test.ts rename to packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-connectionless-proofs.e2e.test.ts index 921f6c3127..f924869f3f 100644 --- a/packages/core/tests/v2-connectionless-proofs.test.ts +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-connectionless-proofs.e2e.test.ts @@ -1,36 +1,29 @@ -import type { SubjectMessage } from '../../../tests/transport/SubjectInboundTransport' -import type { ProofStateChangedEvent } from '../src/modules/proofs' +import type { SubjectMessage } from '../../../../../../../../tests/transport/SubjectInboundTransport' +import type { ProofStateChangedEvent } from '../../../ProofEvents' import { Subject, ReplaySubject } from 'rxjs' -import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport' -import { SubjectOutboundTransport } from '../../../tests/transport/SubjectOutboundTransport' -import { V1CredentialPreview } from '../src' -import { Agent } from '../src/agent/Agent' -import { Attachment, AttachmentData } from '../src/decorators/attachment/Attachment' -import { HandshakeProtocol } from '../src/modules/connections/models/HandshakeProtocol' +import { SubjectInboundTransport } from '../../../../../../../../tests/transport/SubjectInboundTransport' +import { SubjectOutboundTransport } from '../../../../../../../../tests/transport/SubjectOutboundTransport' import { - PredicateType, - ProofState, - ProofAttributeInfo, - AttributeFilter, - ProofPredicateInfo, - AutoAcceptProof, - ProofEventTypes, -} from '../src/modules/proofs' -import { MediatorPickupStrategy } from '../src/modules/routing/MediatorPickupStrategy' -import { LinkedAttachment } from '../src/utils/LinkedAttachment' -import { uuid } from '../src/utils/uuid' - -import { - getAgentOptions, - issueCredential, - makeConnection, - prepareForIssuance, setupProofsTest, waitForProofExchangeRecordSubject, -} from './helpers' -import testLogger from './logger' + getAgentOptions, + prepareForIssuance, + makeConnection, + issueCredential, +} from '../../../../../../tests/helpers' +import testLogger from '../../../../../../tests/logger' +import { Agent } from '../../../../../agent/Agent' +import { Attachment, AttachmentData } from '../../../../../decorators/attachment/Attachment' +import { LinkedAttachment } from '../../../../../utils/LinkedAttachment' +import { uuid } from '../../../../../utils/uuid' +import { HandshakeProtocol } from '../../../../connections' +import { V1CredentialPreview } from '../../../../credentials' +import { MediatorPickupStrategy } from '../../../../routing' +import { ProofEventTypes } from '../../../ProofEvents' +import { ProofAttributeInfo, AttributeFilter, ProofPredicateInfo, PredicateType } from '../../../formats/indy/models' +import { AutoAcceptProof, ProofState } from '../../../models' describe('Present Proof', () => { let agents: Agent[] @@ -44,8 +37,8 @@ describe('Present Proof', () => { test('Faber starts with connection-less proof requests to Alice', async () => { const { aliceAgent, faberAgent, aliceReplay, credDefId, faberReplay } = await setupProofsTest( - 'Faber connection-less Proofs', - 'Alice connection-less Proofs', + 'Faber connection-less Proofs v2', + 'Alice connection-less Proofs v2', AutoAcceptProof.Never ) agents = [aliceAgent, faberAgent] @@ -86,7 +79,6 @@ describe('Present Proof', () => { indy: { name: 'test-proof-request', version: '1.0', - nonce: '12345678901', requestedAttributes: attributes, requestedPredicates: predicates, }, @@ -106,11 +98,8 @@ describe('Present Proof', () => { testLogger.test('Alice accepts presentation request from Faber') - const requestedCredentials = await aliceAgent.proofs.autoSelectCredentialsForProofRequest({ + const requestedCredentials = await aliceAgent.proofs.selectCredentialsForRequest({ proofRecordId: aliceProofExchangeRecord.id, - config: { - filterByPresentationPreview: true, - }, }) const faberProofExchangeRecordPromise = waitForProofExchangeRecordSubject(faberReplay, { @@ -135,7 +124,7 @@ describe('Present Proof', () => { }) // Faber accepts presentation - await faberAgent.proofs.acceptPresentation(faberProofExchangeRecord.id) + await faberAgent.proofs.acceptPresentation({ proofRecordId: faberProofExchangeRecord.id }) // Alice waits till it receives presentation ack aliceProofExchangeRecord = await aliceProofExchangeRecordPromise @@ -191,7 +180,6 @@ describe('Present Proof', () => { indy: { name: 'test-proof-request', version: '1.0', - nonce: '12345678901', requestedAttributes: attributes, requestedPredicates: predicates, }, @@ -260,7 +248,6 @@ describe('Present Proof', () => { const aliceOptions = getAgentOptions(`Connectionless proofs with mediator Alice-${unique}`, { autoAcceptProofs: AutoAcceptProof.Always, - // logger: new TestLogger(LogLevel.test), mediatorConnectionsInvite: aliceMediationOutOfBandRecord.outOfBandInvitation.toUrl({ domain: 'https://example.com', }), @@ -356,7 +343,6 @@ describe('Present Proof', () => { indy: { name: 'test-proof-request', version: '1.0', - nonce: '12345678901', requestedAttributes: attributes, requestedPredicates: predicates, }, diff --git a/packages/core/src/modules/proofs/protocol/v2/__tests__/indy-proof-negotiation.test.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proof-negotiation.test.ts similarity index 84% rename from packages/core/src/modules/proofs/protocol/v2/__tests__/indy-proof-negotiation.test.ts rename to packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proof-negotiation.test.ts index a337eca475..16a4808612 100644 --- a/packages/core/src/modules/proofs/protocol/v2/__tests__/indy-proof-negotiation.test.ts +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proof-negotiation.test.ts @@ -1,29 +1,28 @@ import type { Agent } from '../../../../../agent/Agent' import type { ConnectionRecord } from '../../../../connections/repository/ConnectionRecord' -import type { AcceptProofProposalOptions, NegotiateProposalOptions } from '../../../ProofsApiOptions' +import type { AcceptProofProposalOptions, NegotiateProofProposalOptions } from '../../../ProofsApiOptions' import type { ProofExchangeRecord } from '../../../repository/ProofExchangeRecord' -import type { PresentationPreview } from '../../v1/models/V1PresentationPreview' +import type { V1PresentationPreview } from '../../v1/models/V1PresentationPreview' import type { CredDefId } from 'indy-sdk' import { setupProofsTest, waitForProofExchangeRecord } from '../../../../../../tests/helpers' import testLogger from '../../../../../../tests/logger' import { DidCommMessageRepository } from '../../../../../storage/didcomm' import { JsonTransformer } from '../../../../../utils/JsonTransformer' -import { V2_INDY_PRESENTATION_PROPOSAL, V2_INDY_PRESENTATION_REQUEST } from '../../../formats/ProofFormatConstants' import { AttributeFilter } from '../../../formats/indy/models/AttributeFilter' import { PredicateType } from '../../../formats/indy/models/PredicateType' import { ProofAttributeInfo } from '../../../formats/indy/models/ProofAttributeInfo' import { ProofPredicateInfo } from '../../../formats/indy/models/ProofPredicateInfo' import { ProofRequest } from '../../../formats/indy/models/ProofRequest' import { ProofState } from '../../../models/ProofState' -import { V2ProposalPresentationMessage, V2RequestPresentationMessage } from '../messages' +import { V2ProposePresentationMessage, V2RequestPresentationMessage } from '../messages' describe('Present Proof', () => { let faberAgent: Agent let aliceAgent: Agent let credDefId: CredDefId let aliceConnection: ConnectionRecord - let presentationPreview: PresentationPreview + let presentationPreview: V1PresentationPreview let faberProofExchangeRecord: ProofExchangeRecord let aliceProofExchangeRecord: ProofExchangeRecord let didCommMessageRepository: DidCommMessageRepository @@ -57,7 +56,6 @@ describe('Present Proof', () => { proofFormats: { indy: { name: 'proof-request', - nonce: '58d223e5-fc4d-4448-b74c-5eb11c6b558f', version: '1.0', attributes: presentationPreview.attributes.filter((attribute) => attribute.name !== 'name'), predicates: presentationPreview.predicates, @@ -73,7 +71,7 @@ describe('Present Proof', () => { let proposal = await didCommMessageRepository.findAgentMessage(faberAgent.context, { associatedRecordId: faberProofExchangeRecord.id, - messageClass: V2ProposalPresentationMessage, + messageClass: V2ProposePresentationMessage, }) expect(proposal).toMatchObject({ @@ -81,10 +79,10 @@ describe('Present Proof', () => { formats: [ { attachmentId: expect.any(String), - format: V2_INDY_PRESENTATION_PROPOSAL, + format: 'hlindy/proof-req@v2.0', }, ], - proposalsAttach: [ + proposalAttachments: [ { id: expect.any(String), mimeType: 'application/json', @@ -97,7 +95,7 @@ describe('Present Proof', () => { comment: 'V2 propose proof test 1', }) // eslint-disable-next-line @typescript-eslint/no-explicit-any - let proposalAttach = proposal?.proposalsAttach[0].getDataAsJson() as any + let proposalAttach = proposal?.proposalAttachments[0].getDataAsJson() as any let attributesGroup = Object.keys(proposalAttach.requested_attributes ?? {})[0] let predicatesGroup = Object.keys(proposalAttach.requested_predicates ?? {})[0] expect(proposalAttach).toMatchObject({ @@ -164,27 +162,24 @@ describe('Present Proof', () => { }), } - const requestProofAsResponseOptions: NegotiateProposalOptions = { + let aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + threadId: faberProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + }) + + testLogger.test('Faber sends new proof request to Alice') + faberProofExchangeRecord = await faberAgent.proofs.negotiateProposal({ proofRecordId: faberProofExchangeRecord.id, proofFormats: { indy: { name: 'proof-request', - nonce: '58d223e5-fc4d-4448-b74c-5eb11c6b558f', version: '1.0', requestedAttributes: attributes, requestedPredicates: predicates, }, }, - } - - let aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { - threadId: faberProofExchangeRecord.threadId, - state: ProofState.RequestReceived, }) - testLogger.test('Faber sends new proof request to Alice') - faberProofExchangeRecord = await faberAgent.proofs.negotiateProposal(requestProofAsResponseOptions) - testLogger.test('Alice waits for proof request from Faber') aliceProofExchangeRecord = await aliceProofExchangeRecordPromise @@ -198,7 +193,7 @@ describe('Present Proof', () => { expect(request).toMatchObject({ type: 'https://didcomm.org/present-proof/2.0/request-presentation', id: expect.any(String), - requestPresentationsAttach: [ + requestAttachments: [ { id: expect.any(String), mimeType: 'application/json', @@ -229,7 +224,6 @@ describe('Present Proof', () => { proofFormats: { indy: { name: 'proof-request', - nonce: '58d223e5-fc4d-4448-b74c-5eb11c6b558f', version: '1.0', attributes: presentationPreview.attributes.filter((attribute) => attribute.name === 'name'), predicates: presentationPreview.predicates, @@ -245,7 +239,7 @@ describe('Present Proof', () => { proposal = await didCommMessageRepository.findAgentMessage(faberAgent.context, { associatedRecordId: faberProofExchangeRecord.id, - messageClass: V2ProposalPresentationMessage, + messageClass: V2ProposePresentationMessage, }) expect(proposal).toMatchObject({ @@ -253,10 +247,10 @@ describe('Present Proof', () => { formats: [ { attachmentId: expect.any(String), - format: V2_INDY_PRESENTATION_PROPOSAL, + format: 'hlindy/proof-req@v2.0', }, ], - proposalsAttach: [ + proposalAttachments: [ { id: expect.any(String), mimeType: 'application/json', @@ -269,7 +263,7 @@ describe('Present Proof', () => { comment: 'V2 propose proof test 2', }) // eslint-disable-next-line @typescript-eslint/no-explicit-any - proposalAttach = proposal?.proposalsAttach[0].getDataAsJson() as any + proposalAttach = proposal?.proposalAttachments[0].getDataAsJson() as any attributesGroup = Object.keys(proposalAttach.requested_attributes ?? {})[0] predicatesGroup = Object.keys(proposalAttach.requested_predicates ?? {})[0] expect(proposalAttach).toMatchObject({ @@ -331,10 +325,10 @@ describe('Present Proof', () => { formats: [ { attachmentId: expect.any(String), - format: V2_INDY_PRESENTATION_REQUEST, + format: 'hlindy/proof-req@v2.0', }, ], - requestPresentationsAttach: [ + requestAttachments: [ { id: expect.any(String), mimeType: 'application/json', @@ -355,17 +349,17 @@ describe('Present Proof', () => { protocolVersion: 'v2', }) - const presentationProposalMessage = await aliceAgent.proofs.findProposalMessage(aliceProofExchangeRecord.id) + const proposalMessage = await aliceAgent.proofs.findProposalMessage(aliceProofExchangeRecord.id) - expect(presentationProposalMessage).toMatchObject({ + expect(proposalMessage).toMatchObject({ type: 'https://didcomm.org/present-proof/2.0/propose-presentation', formats: [ { attachmentId: expect.any(String), - format: V2_INDY_PRESENTATION_PROPOSAL, + format: 'hlindy/proof-req@v2.0', }, ], - proposalsAttach: [ + proposalAttachments: [ { id: expect.any(String), mimeType: 'application/json', @@ -379,7 +373,7 @@ describe('Present Proof', () => { }) // eslint-disable-next-line @typescript-eslint/no-explicit-any - proposalAttach = proposal?.proposalsAttach[0].getDataAsJson() as any + proposalAttach = proposal?.proposalAttachments[0].getDataAsJson() as any attributesGroup = Object.keys(proposalAttach.requested_attributes ?? {})[0] predicatesGroup = Object.keys(proposalAttach.requested_predicates ?? {})[0] expect(proposalAttach).toMatchObject({ @@ -412,33 +406,37 @@ describe('Present Proof', () => { )) as V2RequestPresentationMessage const proofRequest = JsonTransformer.fromJSON( - proofRequestMessage.requestPresentationsAttach[0].getDataAsJson(), + proofRequestMessage.requestAttachments[0].getDataAsJson(), ProofRequest ) const predicateKey = proofRequest.requestedPredicates?.keys().next().value - const predicate = Object.values(predicates)[0] - expect(proofRequest).toMatchObject({ + expect(proofRequest.toJSON()).toMatchObject({ name: 'proof-request', - nonce: '58d223e5-fc4d-4448-b74c-5eb11c6b558f', + nonce: expect.any(String), version: '1.0', - requestedAttributes: new Map( - Object.entries({ - '0': new ProofAttributeInfo({ - name: 'name', - restrictions: [ - new AttributeFilter({ - credentialDefinitionId: credDefId, - }), - ], - }), - }) - ), - requestedPredicates: new Map( - Object.entries({ - [predicateKey]: predicate, - }) - ), + requested_attributes: { + '0': { + name: 'name', + restrictions: [ + { + cred_def_id: credDefId, + }, + ], + }, + }, + requested_predicates: { + [predicateKey]: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credDefId, + }, + ], + }, + }, }) }) }) diff --git a/packages/core/src/modules/proofs/protocol/v2/__tests__/indy-proof-presentation.test.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proof-presentation.e2e.test.ts similarity index 89% rename from packages/core/src/modules/proofs/protocol/v2/__tests__/indy-proof-presentation.test.ts rename to packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proof-presentation.e2e.test.ts index a8f2d6a531..2fccef1f98 100644 --- a/packages/core/src/modules/proofs/protocol/v2/__tests__/indy-proof-presentation.test.ts +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proof-presentation.e2e.test.ts @@ -1,26 +1,21 @@ import type { Agent } from '../../../../../agent/Agent' import type { ConnectionRecord } from '../../../../connections/repository/ConnectionRecord' import type { AcceptProofProposalOptions } from '../../../ProofsApiOptions' -import type { PresentationPreview } from '../../v1/models/V1PresentationPreview' +import type { V1PresentationPreview } from '../../v1/models/V1PresentationPreview' import { setupProofsTest, waitForProofExchangeRecord } from '../../../../../../tests/helpers' import testLogger from '../../../../../../tests/logger' import { DidCommMessageRepository } from '../../../../../storage' -import { - V2_INDY_PRESENTATION_PROPOSAL, - V2_INDY_PRESENTATION_REQUEST, - V2_INDY_PRESENTATION, -} from '../../../formats/ProofFormatConstants' import { ProofState } from '../../../models/ProofState' import { ProofExchangeRecord } from '../../../repository/ProofExchangeRecord' import { V2PresentationMessage, V2RequestPresentationMessage } from '../messages' -import { V2ProposalPresentationMessage } from '../messages/V2ProposalPresentationMessage' +import { V2ProposePresentationMessage } from '../messages/V2ProposePresentationMessage' describe('Present Proof', () => { let faberAgent: Agent let aliceAgent: Agent let aliceConnection: ConnectionRecord - let presentationPreview: PresentationPreview + let presentationPreview: V1PresentationPreview let faberProofExchangeRecord: ProofExchangeRecord let aliceProofExchangeRecord: ProofExchangeRecord let didCommMessageRepository: DidCommMessageRepository @@ -28,8 +23,8 @@ describe('Present Proof', () => { beforeAll(async () => { testLogger.test('Initializing the agents') ;({ faberAgent, aliceAgent, aliceConnection, presentationPreview } = await setupProofsTest( - 'Faber agent', - 'Alice agent' + 'Faber agent v2 present proof', + 'Alice agent v2 present proof' )) }) @@ -54,7 +49,6 @@ describe('Present Proof', () => { proofFormats: { indy: { name: 'ProofRequest', - nonce: '947121108704767252195126', version: '1.0', attributes: presentationPreview.attributes, predicates: presentationPreview.predicates, @@ -70,7 +64,7 @@ describe('Present Proof', () => { const proposal = await didCommMessageRepository.findAgentMessage(faberAgent.context, { associatedRecordId: faberProofExchangeRecord.id, - messageClass: V2ProposalPresentationMessage, + messageClass: V2ProposePresentationMessage, }) expect(proposal).toMatchObject({ @@ -78,10 +72,10 @@ describe('Present Proof', () => { formats: [ { attachmentId: expect.any(String), - format: V2_INDY_PRESENTATION_PROPOSAL, + format: 'hlindy/proof-req@v2.0', }, ], - proposalsAttach: [ + proposalAttachments: [ { id: expect.any(String), mimeType: 'application/json', @@ -130,10 +124,10 @@ describe('Present Proof', () => { formats: [ { attachmentId: expect.any(String), - format: V2_INDY_PRESENTATION_REQUEST, + format: 'hlindy/proof-req@v2.0', }, ], - requestPresentationsAttach: [ + requestAttachments: [ { id: expect.any(String), mimeType: 'application/json', @@ -159,11 +153,8 @@ describe('Present Proof', () => { // Alice retrieves the requested credentials and accepts the presentation request testLogger.test('Alice accepts presentation request from Faber') - const requestedCredentials = await aliceAgent.proofs.autoSelectCredentialsForProofRequest({ + const requestedCredentials = await aliceAgent.proofs.selectCredentialsForRequest({ proofRecordId: aliceProofExchangeRecord.id, - config: { - filterByPresentationPreview: true, - }, }) const faberPresentationRecordPromise = waitForProofExchangeRecord(faberAgent, { @@ -190,10 +181,10 @@ describe('Present Proof', () => { formats: [ { attachmentId: expect.any(String), - format: V2_INDY_PRESENTATION, + format: 'hlindy/proof@v2.0', }, ], - presentationsAttach: [ + presentationAttachments: [ { id: expect.any(String), mimeType: 'application/json', @@ -223,7 +214,7 @@ describe('Present Proof', () => { // Faber accepts the presentation provided by Alice testLogger.test('Faber accepts the presentation provided by Alice') - await faberAgent.proofs.acceptPresentation(faberProofExchangeRecord.id) + await faberAgent.proofs.acceptPresentation({ proofRecordId: faberProofExchangeRecord.id }) // Alice waits until she received a presentation acknowledgement testLogger.test('Alice waits until she receives a presentation acknowledgement') diff --git a/packages/core/src/modules/proofs/protocol/v2/__tests__/indy-proof-proposal.test.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proof-proposal.e2e.test.ts similarity index 84% rename from packages/core/src/modules/proofs/protocol/v2/__tests__/indy-proof-proposal.test.ts rename to packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proof-proposal.e2e.test.ts index 39a29df125..68c09d5717 100644 --- a/packages/core/src/modules/proofs/protocol/v2/__tests__/indy-proof-proposal.test.ts +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proof-proposal.e2e.test.ts @@ -1,28 +1,27 @@ import type { Agent } from '../../../../../agent/Agent' import type { ConnectionRecord } from '../../../../connections/repository/ConnectionRecord' import type { ProofExchangeRecord } from '../../../repository' -import type { PresentationPreview } from '../../v1/models/V1PresentationPreview' +import type { V1PresentationPreview } from '../../v1/models/V1PresentationPreview' import { setupProofsTest, waitForProofExchangeRecord } from '../../../../../../tests/helpers' import testLogger from '../../../../../../tests/logger' import { DidCommMessageRepository } from '../../../../../storage' -import { V2_INDY_PRESENTATION_PROPOSAL } from '../../../formats/ProofFormatConstants' import { ProofState } from '../../../models/ProofState' -import { V2ProposalPresentationMessage } from '../messages/V2ProposalPresentationMessage' +import { V2ProposePresentationMessage } from '../messages/V2ProposePresentationMessage' describe('Present Proof', () => { let faberAgent: Agent let aliceAgent: Agent let aliceConnection: ConnectionRecord - let presentationPreview: PresentationPreview + let presentationPreview: V1PresentationPreview let faberPresentationRecord: ProofExchangeRecord let didCommMessageRepository: DidCommMessageRepository beforeAll(async () => { testLogger.test('Initializing the agents') ;({ faberAgent, aliceAgent, aliceConnection, presentationPreview } = await setupProofsTest( - 'Faber agent', - 'Alice agent' + 'Faber agent v2', + 'Alice agent v2' )) }) @@ -47,7 +46,6 @@ describe('Present Proof', () => { proofFormats: { indy: { name: 'ProofRequest', - nonce: '58d223e5-fc4d-4448-b74c-5eb11c6b558f', version: '1.0', attributes: presentationPreview.attributes, predicates: presentationPreview.predicates, @@ -63,7 +61,7 @@ describe('Present Proof', () => { const proposal = await didCommMessageRepository.findAgentMessage(faberAgent.context, { associatedRecordId: faberPresentationRecord.id, - messageClass: V2ProposalPresentationMessage, + messageClass: V2ProposePresentationMessage, }) expect(proposal).toMatchObject({ @@ -71,10 +69,10 @@ describe('Present Proof', () => { formats: [ { attachmentId: expect.any(String), - format: V2_INDY_PRESENTATION_PROPOSAL, + format: 'hlindy/proof-req@v2.0', }, ], - proposalsAttach: [ + proposalAttachments: [ { id: expect.any(String), mimeType: 'application/json', diff --git a/packages/core/src/modules/proofs/protocol/v2/__tests__/indy-proof-request.test.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proof-request.e2e.test.ts similarity index 87% rename from packages/core/src/modules/proofs/protocol/v2/__tests__/indy-proof-request.test.ts rename to packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proof-request.e2e.test.ts index 47a697821f..827555ec65 100644 --- a/packages/core/src/modules/proofs/protocol/v2/__tests__/indy-proof-request.test.ts +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proof-request.e2e.test.ts @@ -2,21 +2,20 @@ import type { Agent } from '../../../../../agent/Agent' import type { ConnectionRecord } from '../../../../connections/repository/ConnectionRecord' import type { AcceptProofProposalOptions } from '../../../ProofsApiOptions' import type { ProofExchangeRecord } from '../../../repository/ProofExchangeRecord' -import type { PresentationPreview } from '../../v1/models/V1PresentationPreview' +import type { V1PresentationPreview } from '../../v1/models/V1PresentationPreview' import { setupProofsTest, waitForProofExchangeRecord } from '../../../../../../tests/helpers' import testLogger from '../../../../../../tests/logger' import { DidCommMessageRepository } from '../../../../../storage' -import { V2_INDY_PRESENTATION_PROPOSAL, V2_INDY_PRESENTATION_REQUEST } from '../../../formats/ProofFormatConstants' import { ProofState } from '../../../models/ProofState' import { V2RequestPresentationMessage } from '../messages' -import { V2ProposalPresentationMessage } from '../messages/V2ProposalPresentationMessage' +import { V2ProposePresentationMessage } from '../messages/V2ProposePresentationMessage' describe('Present Proof', () => { let faberAgent: Agent let aliceAgent: Agent let aliceConnection: ConnectionRecord - let presentationPreview: PresentationPreview + let presentationPreview: V1PresentationPreview let faberProofExchangeRecord: ProofExchangeRecord let aliceProofExchangeRecord: ProofExchangeRecord let didCommMessageRepository: DidCommMessageRepository @@ -24,8 +23,8 @@ describe('Present Proof', () => { beforeAll(async () => { testLogger.test('Initializing the agents') ;({ faberAgent, aliceAgent, aliceConnection, presentationPreview } = await setupProofsTest( - 'Faber agent', - 'Alice agent' + 'Faber agent v2', + 'Alice agent v2' )) }) @@ -50,7 +49,6 @@ describe('Present Proof', () => { proofFormats: { indy: { name: 'ProofRequest', - nonce: '58d223e5-fc4d-4448-b74c-5eb11c6b558f', version: '1.0', attributes: presentationPreview.attributes, predicates: presentationPreview.predicates, @@ -66,7 +64,7 @@ describe('Present Proof', () => { const proposal = await didCommMessageRepository.findAgentMessage(faberAgent.context, { associatedRecordId: faberProofExchangeRecord.id, - messageClass: V2ProposalPresentationMessage, + messageClass: V2ProposePresentationMessage, }) expect(proposal).toMatchObject({ @@ -74,10 +72,10 @@ describe('Present Proof', () => { formats: [ { attachmentId: expect.any(String), - format: V2_INDY_PRESENTATION_PROPOSAL, + format: 'hlindy/proof-req@v2.0', }, ], - proposalsAttach: [ + proposalAttachments: [ { id: expect.any(String), mimeType: 'application/json', @@ -97,7 +95,7 @@ describe('Present Proof', () => { }) }) - test(`Faber accepts the Proposal send by Alice`, async () => { + test(`Faber accepts the Proposal sent by Alice`, async () => { // Accept Proposal const acceptProposalOptions: AcceptProofProposalOptions = { proofRecordId: faberProofExchangeRecord.id, @@ -126,10 +124,10 @@ describe('Present Proof', () => { formats: [ { attachmentId: expect.any(String), - format: V2_INDY_PRESENTATION_REQUEST, + format: 'hlindy/proof-req@v2.0', }, ], - requestPresentationsAttach: [ + requestAttachments: [ { id: expect.any(String), mimeType: 'application/json', diff --git a/packages/core/tests/v2-proofs-auto-accept.test.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proofs-auto-accept.2e.test.ts similarity index 79% rename from packages/core/tests/v2-proofs-auto-accept.test.ts rename to packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proofs-auto-accept.2e.test.ts index 0ab3d81e13..e3a9841613 100644 --- a/packages/core/tests/v2-proofs-auto-accept.test.ts +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proofs-auto-accept.2e.test.ts @@ -1,17 +1,11 @@ -import type { Agent, ConnectionRecord } from '../src' -import type { PresentationPreview } from '../src/modules/proofs/protocol/v1/models/V1PresentationPreview' +import type { Agent } from '../../../../../agent/Agent' +import type { ConnectionRecord } from '../../../../connections' +import type { V1PresentationPreview } from '../../v1' -import { - AutoAcceptProof, - ProofState, - ProofAttributeInfo, - AttributeFilter, - ProofPredicateInfo, - PredicateType, -} from '../src' - -import { setupProofsTest, waitForProofExchangeRecord } from './helpers' -import testLogger from './logger' +import { setupProofsTest, waitForProofExchangeRecord } from '../../../../../../tests/helpers' +import testLogger from '../../../../../../tests/logger' +import { ProofAttributeInfo, AttributeFilter, ProofPredicateInfo, PredicateType } from '../../../formats/indy/models' +import { AutoAcceptProof, ProofState } from '../../../models' describe('Auto accept present proof', () => { let faberAgent: Agent @@ -19,7 +13,7 @@ describe('Auto accept present proof', () => { let credDefId: string let faberConnection: ConnectionRecord let aliceConnection: ConnectionRecord - let presentationPreview: PresentationPreview + let presentationPreview: V1PresentationPreview describe('Auto accept on `always`', () => { beforeAll(async () => { @@ -45,7 +39,6 @@ describe('Auto accept present proof', () => { protocolVersion: 'v2', proofFormats: { indy: { - nonce: '1298236324864', name: 'abc', version: '1.0', attributes: presentationPreview.attributes, @@ -94,7 +87,6 @@ describe('Auto accept present proof', () => { indy: { name: 'proof-request', version: '1.0', - nonce: '1298236324864', requestedAttributes: attributes, requestedPredicates: predicates, }, @@ -110,7 +102,7 @@ describe('Auto accept present proof', () => { }) }) - describe('Auto accept on `contentApproved`', () => { + describe("Auto accept on 'contentApproved'", () => { beforeAll(async () => { testLogger.test('Initializing the agents') ;({ faberAgent, aliceAgent, credDefId, faberConnection, aliceConnection, presentationPreview } = @@ -128,15 +120,18 @@ describe('Auto accept present proof', () => { await aliceAgent.wallet.delete() }) - test('Alice starts with proof proposal to Faber, both with autoAcceptProof on `contentApproved`', async () => { + test("Alice starts with proof proposal to Faber, both with autoAcceptProof on 'contentApproved'", async () => { testLogger.test('Alice sends presentation proposal to Faber') + const faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { + state: ProofState.ProposalReceived, + }) + await aliceAgent.proofs.proposeProof({ connectionId: aliceConnection.id, protocolVersion: 'v2', proofFormats: { indy: { - nonce: '1298236324864', attributes: presentationPreview.attributes, predicates: presentationPreview.predicates, name: 'abc', @@ -145,20 +140,18 @@ describe('Auto accept present proof', () => { }, }) - const { id: proofRecordId } = await waitForProofExchangeRecord(faberAgent, { - state: ProofState.ProposalReceived, + const faberProofExchangeRecord = await faberProofExchangeRecordPromise + await faberAgent.proofs.acceptProposal({ + proofRecordId: faberProofExchangeRecord.id, }) - testLogger.test('Faber accepts presentation proposal from Alice') - await faberAgent.proofs.acceptProposal({ proofRecordId }) - await Promise.all([ waitForProofExchangeRecord(aliceAgent, { state: ProofState.Done }), waitForProofExchangeRecord(faberAgent, { state: ProofState.Done }), ]) }) - test('Faber starts with proof requests to Alice, both with autoAcceptProof on `contentApproved`', async () => { + test("Faber starts with proof requests to Alice, both with autoAcceptProof on 'contentApproved'", async () => { testLogger.test('Faber sends presentation request to Alice') const attributes = { name: new ProofAttributeInfo({ @@ -183,6 +176,10 @@ describe('Auto accept present proof', () => { }), } + const aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + state: ProofState.RequestReceived, + }) + await faberAgent.proofs.requestProof({ protocolVersion: 'v2', connectionId: faberConnection.id, @@ -190,19 +187,16 @@ describe('Auto accept present proof', () => { indy: { name: 'proof-request', version: '1.0', - nonce: '1298236324866', requestedAttributes: attributes, requestedPredicates: predicates, }, }, }) - testLogger.test('Alice waits for request from Faber') - const { id: proofRecordId } = await waitForProofExchangeRecord(aliceAgent, { - state: ProofState.RequestReceived, + const aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + await aliceAgent.proofs.acceptRequest({ + proofRecordId: aliceProofExchangeRecord.id, }) - const { proofFormats } = await aliceAgent.proofs.autoSelectCredentialsForProofRequest({ proofRecordId }) - await aliceAgent.proofs.acceptRequest({ proofRecordId, proofFormats }) await Promise.all([ waitForProofExchangeRecord(faberAgent, { state: ProofState.Done }), diff --git a/packages/core/tests/v2-indy-proofs.test.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proofs.e2e.test.ts similarity index 77% rename from packages/core/tests/v2-indy-proofs.test.ts rename to packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proofs.e2e.test.ts index f54f6da15e..815188bf69 100644 --- a/packages/core/tests/v2-indy-proofs.test.ts +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proofs.e2e.test.ts @@ -1,47 +1,29 @@ -import type { Agent, ConnectionRecord } from '../src' -import type { AcceptProofProposalOptions } from '../src/modules/proofs/ProofsApiOptions' -import type { PresentationPreview } from '../src/modules/proofs/protocol/v1/models/V1PresentationPreview' -import type { CredDefId } from 'indy-sdk' - -import { - ProofExchangeRecord, - AttributeFilter, - PredicateType, - ProofAttributeInfo, - ProofPredicateInfo, - ProofState, -} from '../src' -import { getGroupKeysFromIndyProofFormatData } from '../src/modules/proofs/__tests__/groupKeys' -import { - V2_INDY_PRESENTATION_PROPOSAL, - V2_INDY_PRESENTATION_REQUEST, - V2_INDY_PRESENTATION, -} from '../src/modules/proofs/formats/ProofFormatConstants' -import { - V2PresentationMessage, - V2ProposalPresentationMessage, - V2RequestPresentationMessage, -} from '../src/modules/proofs/protocol/v2/messages' -import { DidCommMessageRepository } from '../src/storage/didcomm' - -import { setupProofsTest, waitForProofExchangeRecord } from './helpers' -import testLogger from './logger' +import type { Agent } from '../../../../../agent/Agent' +import type { ConnectionRecord } from '../../../../connections' +import type { V1PresentationPreview } from '../../v1' + +import { setupProofsTest, waitForProofExchangeRecord } from '../../../../../../tests/helpers' +import testLogger from '../../../../../../tests/logger' +import { getGroupKeysFromIndyProofFormatData } from '../../../formats/indy/__tests__/groupKeys' +import { ProofAttributeInfo, AttributeFilter, ProofPredicateInfo, PredicateType } from '../../../formats/indy/models' +import { ProofState } from '../../../models' +import { ProofExchangeRecord } from '../../../repository' +import { V2ProposePresentationMessage, V2RequestPresentationMessage, V2PresentationMessage } from '../messages' describe('Present Proof', () => { let faberAgent: Agent let aliceAgent: Agent - let credDefId: CredDefId + let credDefId: string let aliceConnection: ConnectionRecord let faberConnection: ConnectionRecord let faberProofExchangeRecord: ProofExchangeRecord let aliceProofExchangeRecord: ProofExchangeRecord - let presentationPreview: PresentationPreview - let didCommMessageRepository: DidCommMessageRepository + let presentationPreview: V1PresentationPreview beforeAll(async () => { testLogger.test('Initializing the agents') ;({ faberAgent, aliceAgent, credDefId, faberConnection, aliceConnection, presentationPreview } = - await setupProofsTest('Faber agent', 'Alice agent')) + await setupProofsTest('Faber agent indy proofs', 'Alice agent indy proofs')) }) afterAll(async () => { @@ -67,7 +49,6 @@ describe('Present Proof', () => { indy: { name: 'abc', version: '1.0', - nonce: '947121108704767252195126', attributes: presentationPreview.attributes, predicates: presentationPreview.predicates, }, @@ -78,22 +59,16 @@ describe('Present Proof', () => { testLogger.test('Faber waits for a presentation proposal from Alice') faberProofExchangeRecord = await faberProofExchangeRecordPromise - didCommMessageRepository = faberAgent.dependencyManager.resolve(DidCommMessageRepository) - - const proposal = await didCommMessageRepository.findAgentMessage(faberAgent.context, { - associatedRecordId: faberProofExchangeRecord.id, - messageClass: V2ProposalPresentationMessage, - }) - + const proposal = await faberAgent.proofs.findProposalMessage(faberProofExchangeRecord.id) expect(proposal).toMatchObject({ type: 'https://didcomm.org/present-proof/2.0/propose-presentation', formats: [ { attachmentId: expect.any(String), - format: V2_INDY_PRESENTATION_PROPOSAL, + format: 'hlindy/proof-req@v2.0', }, ], - proposalsAttach: [ + proposalAttachments: [ { id: expect.any(String), mimeType: 'application/json', @@ -111,36 +86,30 @@ describe('Present Proof', () => { protocolVersion: 'v2', }) - const acceptProposalOptions: AcceptProofProposalOptions = { - proofRecordId: faberProofExchangeRecord.id, - } - let aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { state: ProofState.RequestReceived, }) // Faber accepts the presentation proposal from Alice testLogger.test('Faber accepts presentation proposal from Alice') - faberProofExchangeRecord = await faberAgent.proofs.acceptProposal(acceptProposalOptions) + faberProofExchangeRecord = await faberAgent.proofs.acceptProposal({ + proofRecordId: faberProofExchangeRecord.id, + }) // Alice waits for presentation request from Faber testLogger.test('Alice waits for presentation request from Faber') aliceProofExchangeRecord = await aliceProofExchangeRecordPromise - const request = await didCommMessageRepository.findAgentMessage(faberAgent.context, { - associatedRecordId: faberProofExchangeRecord.id, - messageClass: V2RequestPresentationMessage, - }) - + const request = await faberAgent.proofs.findRequestMessage(faberProofExchangeRecord.id) expect(request).toMatchObject({ type: 'https://didcomm.org/present-proof/2.0/request-presentation', formats: [ { attachmentId: expect.any(String), - format: V2_INDY_PRESENTATION_REQUEST, + format: 'hlindy/proof-req@v2.0', }, ], - requestPresentationsAttach: [ + requestAttachments: [ { id: expect.any(String), mimeType: 'application/json', @@ -158,11 +127,8 @@ describe('Present Proof', () => { // Alice retrieves the requested credentials and accepts the presentation request testLogger.test('Alice accepts presentation request from Faber') - const requestedCredentials = await aliceAgent.proofs.autoSelectCredentialsForProofRequest({ + const requestedCredentials = await aliceAgent.proofs.selectCredentialsForRequest({ proofRecordId: aliceProofExchangeRecord.id, - config: { - filterByPresentationPreview: true, - }, }) faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { @@ -179,20 +145,16 @@ describe('Present Proof', () => { testLogger.test('Faber waits for presentation from Alice') faberProofExchangeRecord = await faberProofExchangeRecordPromise - const presentation = await didCommMessageRepository.findAgentMessage(faberAgent.context, { - associatedRecordId: faberProofExchangeRecord.id, - messageClass: V2PresentationMessage, - }) - + const presentation = await faberAgent.proofs.findPresentationMessage(faberProofExchangeRecord.id) expect(presentation).toMatchObject({ type: 'https://didcomm.org/present-proof/2.0/presentation', formats: [ { attachmentId: expect.any(String), - format: V2_INDY_PRESENTATION, + format: 'hlindy/proof@v2.0', }, ], - presentationsAttach: [ + presentationAttachments: [ { id: expect.any(String), mimeType: 'application/json', @@ -220,7 +182,7 @@ describe('Present Proof', () => { // Faber accepts the presentation provided by Alice testLogger.test('Faber accepts the presentation provided by Alice') - await faberAgent.proofs.acceptPresentation(faberProofExchangeRecord.id) + await faberAgent.proofs.acceptPresentation({ proofRecordId: faberProofExchangeRecord.id }) // Alice waits until she received a presentation acknowledgement testLogger.test('Alice waits until she receives a presentation acknowledgement') @@ -249,7 +211,7 @@ describe('Present Proof', () => { const requestMessage = await aliceAgent.proofs.findRequestMessage(aliceProofExchangeRecord.id) const presentationMessage = await aliceAgent.proofs.findPresentationMessage(aliceProofExchangeRecord.id) - expect(proposalMessage).toBeInstanceOf(V2ProposalPresentationMessage) + expect(proposalMessage).toBeInstanceOf(V2ProposePresentationMessage) expect(requestMessage).toBeInstanceOf(V2RequestPresentationMessage) expect(presentationMessage).toBeInstanceOf(V2PresentationMessage) @@ -263,7 +225,7 @@ describe('Present Proof', () => { indy: { name: 'abc', version: '1.0', - nonce: '947121108704767252195126', + nonce: expect.any(String), requested_attributes: { 0: { name: 'name', @@ -295,7 +257,7 @@ describe('Present Proof', () => { indy: { name: 'abc', version: '1.0', - nonce: '947121108704767252195126', + nonce: expect.any(String), requested_attributes: { 0: { name: 'name', @@ -389,6 +351,8 @@ describe('Present Proof', () => { connectionId: faberConnection.id, proofFormats: { indy: { + name: 'Proof Request', + version: '1.0.0', requestedAttributes: attributes, requestedPredicates: predicates, }, @@ -399,20 +363,16 @@ describe('Present Proof', () => { testLogger.test('Alice waits for presentation request from Faber') aliceProofExchangeRecord = await aliceProofExchangeRecordPromise - const request = await didCommMessageRepository.findAgentMessage(faberAgent.context, { - associatedRecordId: faberProofExchangeRecord.id, - messageClass: V2RequestPresentationMessage, - }) - + const request = await faberAgent.proofs.findRequestMessage(faberProofExchangeRecord.id) expect(request).toMatchObject({ type: 'https://didcomm.org/present-proof/2.0/request-presentation', formats: [ { attachmentId: expect.any(String), - format: V2_INDY_PRESENTATION_REQUEST, + format: 'hlindy/proof-req@v2.0', }, ], - requestPresentationsAttach: [ + requestAttachments: [ { id: expect.any(String), mimeType: 'application/json', @@ -434,11 +394,8 @@ describe('Present Proof', () => { // Alice retrieves the requested credentials and accepts the presentation request testLogger.test('Alice accepts presentation request from Faber') - const requestedCredentials = await aliceAgent.proofs.autoSelectCredentialsForProofRequest({ + const requestedCredentials = await aliceAgent.proofs.selectCredentialsForRequest({ proofRecordId: aliceProofExchangeRecord.id, - config: { - filterByPresentationPreview: true, - }, }) const faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { @@ -455,20 +412,16 @@ describe('Present Proof', () => { testLogger.test('Faber waits for presentation from Alice') faberProofExchangeRecord = await faberProofExchangeRecordPromise - const presentation = await didCommMessageRepository.findAgentMessage(faberAgent.context, { - associatedRecordId: faberProofExchangeRecord.id, - messageClass: V2PresentationMessage, - }) - + const presentation = await faberAgent.proofs.findPresentationMessage(faberProofExchangeRecord.id) expect(presentation).toMatchObject({ type: 'https://didcomm.org/present-proof/2.0/presentation', formats: [ { attachmentId: expect.any(String), - format: V2_INDY_PRESENTATION, + format: 'hlindy/proof@v2.0', }, ], - presentationsAttach: [ + presentationAttachments: [ { id: expect.any(String), mimeType: 'application/json', @@ -496,7 +449,7 @@ describe('Present Proof', () => { // Faber accepts the presentation testLogger.test('Faber accept the presentation from Alice') - await faberAgent.proofs.acceptPresentation(faberProofExchangeRecord.id) + await faberAgent.proofs.acceptPresentation({ proofRecordId: faberProofExchangeRecord.id }) // Alice waits until she receives a presentation acknowledgement testLogger.test('Alice waits for acceptance by Faber') @@ -567,9 +520,8 @@ describe('Present Proof', () => { connectionId: faberConnection.id, proofFormats: { indy: { - name: 'proof-request', - version: '1.0', - nonce: '1298236324864', + name: 'Proof Request', + version: '1.0.0', requestedAttributes: attributes, requestedPredicates: predicates, }, @@ -580,18 +532,76 @@ describe('Present Proof', () => { testLogger.test('Alice waits for presentation request from Faber') aliceProofExchangeRecord = await aliceProofExchangeRecordPromise - const retrievedCredentials = await faberAgent.proofs.getRequestedCredentialsForProofRequest({ - proofRecordId: faberProofExchangeRecord.id, - config: {}, + const retrievedCredentials = await aliceAgent.proofs.getCredentialsForRequest({ + proofRecordId: aliceProofExchangeRecord.id, }) - if (retrievedCredentials.proofFormats.indy) { - const keys = Object.keys(retrievedCredentials.proofFormats.indy?.requestedAttributes) - expect(keys).toContain('name') - expect(keys).toContain('image_0') - } else { - fail() - } + expect(retrievedCredentials).toMatchObject({ + proofFormats: { + indy: { + attributes: { + name: [ + { + credentialId: expect.any(String), + revealed: true, + credentialInfo: { + referent: expect.any(String), + attributes: { + image_0: 'hl:zQmfDXo7T3J43j3CTkEZaz7qdHuABhWktksZ7JEBueZ5zUS', + image_1: 'hl:zQmRHBT9rDs5QhsnYuPY3mNpXxgLcnNXkhjWJvTSAPMmcVd', + name: 'John', + age: '99', + }, + schemaId: expect.any(String), + credentialDefinitionId: expect.any(String), + revocationRegistryId: null, + credentialRevocationId: null, + }, + }, + ], + image_0: [ + { + credentialId: expect.any(String), + revealed: true, + credentialInfo: { + referent: expect.any(String), + attributes: { + age: '99', + image_0: 'hl:zQmfDXo7T3J43j3CTkEZaz7qdHuABhWktksZ7JEBueZ5zUS', + image_1: 'hl:zQmRHBT9rDs5QhsnYuPY3mNpXxgLcnNXkhjWJvTSAPMmcVd', + name: 'John', + }, + schemaId: expect.any(String), + credentialDefinitionId: expect.any(String), + revocationRegistryId: null, + credentialRevocationId: null, + }, + }, + ], + }, + predicates: { + age: [ + { + credentialId: expect.any(String), + credentialInfo: { + referent: expect.any(String), + attributes: { + image_1: 'hl:zQmRHBT9rDs5QhsnYuPY3mNpXxgLcnNXkhjWJvTSAPMmcVd', + image_0: 'hl:zQmfDXo7T3J43j3CTkEZaz7qdHuABhWktksZ7JEBueZ5zUS', + name: 'John', + age: '99', + }, + schemaId: expect.any(String), + credentialDefinitionId: expect.any(String), + revocationRegistryId: null, + credentialRevocationId: null, + }, + }, + ], + }, + }, + }, + }) }) test('Faber starts with proof request to Alice but gets Problem Reported', async () => { @@ -641,7 +651,6 @@ describe('Present Proof', () => { indy: { name: 'proof-request', version: '1.0', - nonce: '1298236324864', requestedAttributes: attributes, requestedPredicates: predicates, }, @@ -652,20 +661,17 @@ describe('Present Proof', () => { testLogger.test('Alice waits for presentation request from Faber') aliceProofExchangeRecord = await aliceProofExchangeRecordPromise - const request = await didCommMessageRepository.findAgentMessage(faberAgent.context, { - associatedRecordId: faberProofExchangeRecord.id, - messageClass: V2RequestPresentationMessage, - }) + const request = await faberAgent.proofs.findRequestMessage(faberProofExchangeRecord.id) expect(request).toMatchObject({ type: 'https://didcomm.org/present-proof/2.0/request-presentation', formats: [ { attachmentId: expect.any(String), - format: V2_INDY_PRESENTATION_REQUEST, + format: 'hlindy/proof-req@v2.0', }, ], - requestPresentationsAttach: [ + requestAttachments: [ { id: expect.any(String), mimeType: 'application/json', @@ -689,10 +695,10 @@ describe('Present Proof', () => { state: ProofState.Abandoned, }) - aliceProofExchangeRecord = await aliceAgent.proofs.sendProblemReport( - aliceProofExchangeRecord.id, - 'Problem inside proof request' - ) + aliceProofExchangeRecord = await aliceAgent.proofs.sendProblemReport({ + description: 'Problem inside proof request', + proofRecordId: aliceProofExchangeRecord.id, + }) faberProofExchangeRecord = await faberProofExchangeRecordPromise diff --git a/packages/core/src/modules/proofs/protocol/v2/handlers/V2PresentationAckHandler.ts b/packages/core/src/modules/proofs/protocol/v2/handlers/V2PresentationAckHandler.ts index 3d28970a6e..43b9e15a69 100644 --- a/packages/core/src/modules/proofs/protocol/v2/handlers/V2PresentationAckHandler.ts +++ b/packages/core/src/modules/proofs/protocol/v2/handlers/V2PresentationAckHandler.ts @@ -1,17 +1,17 @@ import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' -import type { ProofService } from '../../../ProofService' +import type { ProofProtocol } from '../../ProofProtocol' import { V2PresentationAckMessage } from '../messages' export class V2PresentationAckHandler implements MessageHandler { - private proofService: ProofService + private proofProtocol: ProofProtocol public supportedMessages = [V2PresentationAckMessage] - public constructor(proofService: ProofService) { - this.proofService = proofService + public constructor(proofProtocol: ProofProtocol) { + this.proofProtocol = proofProtocol } public async handle(messageContext: MessageHandlerInboundMessage) { - await this.proofService.processAck(messageContext) + await this.proofProtocol.processAck(messageContext) } } diff --git a/packages/core/src/modules/proofs/protocol/v2/handlers/V2PresentationHandler.ts b/packages/core/src/modules/proofs/protocol/v2/handlers/V2PresentationHandler.ts index 9f2d074d67..e3c68c1d84 100644 --- a/packages/core/src/modules/proofs/protocol/v2/handlers/V2PresentationHandler.ts +++ b/packages/core/src/modules/proofs/protocol/v2/handlers/V2PresentationHandler.ts @@ -1,75 +1,56 @@ -import type { AgentConfig } from '../../../../../agent/AgentConfig' import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' -import type { DidCommMessageRepository } from '../../../../../storage' -import type { ProofResponseCoordinator } from '../../../ProofResponseCoordinator' import type { ProofExchangeRecord } from '../../../repository' -import type { V2ProofService } from '../V2ProofService' +import type { V2ProofProtocol } from '../V2ProofProtocol' import { OutboundMessageContext } from '../../../../../agent/models' +import { DidCommMessageRepository } from '../../../../../storage' import { V2PresentationMessage, V2RequestPresentationMessage } from '../messages' export class V2PresentationHandler implements MessageHandler { - private proofService: V2ProofService - private agentConfig: AgentConfig - private proofResponseCoordinator: ProofResponseCoordinator - private didCommMessageRepository: DidCommMessageRepository + private proofProtocol: V2ProofProtocol public supportedMessages = [V2PresentationMessage] - public constructor( - proofService: V2ProofService, - agentConfig: AgentConfig, - proofResponseCoordinator: ProofResponseCoordinator, - didCommMessageRepository: DidCommMessageRepository - ) { - this.proofService = proofService - this.agentConfig = agentConfig - this.proofResponseCoordinator = proofResponseCoordinator - this.didCommMessageRepository = didCommMessageRepository + public constructor(proofProtocol: V2ProofProtocol) { + this.proofProtocol = proofProtocol } public async handle(messageContext: MessageHandlerInboundMessage) { - const proofRecord = await this.proofService.processPresentation(messageContext) + const proofRecord = await this.proofProtocol.processPresentation(messageContext) - const shouldAutoRespond = await this.proofResponseCoordinator.shouldAutoRespondToPresentation( - messageContext.agentContext, - proofRecord - ) + const shouldAutoRespond = await this.proofProtocol.shouldAutoRespondToPresentation(messageContext.agentContext, { + proofRecord, + presentationMessage: messageContext.message, + }) if (shouldAutoRespond) { - return await this.createAck(proofRecord, messageContext) + return await this.acceptPresentation(proofRecord, messageContext) } } - private async createAck( - record: ProofExchangeRecord, + private async acceptPresentation( + proofRecord: ProofExchangeRecord, messageContext: MessageHandlerInboundMessage ) { - this.agentConfig.logger.info( - `Automatically sending acknowledgement with autoAccept on ${this.agentConfig.autoAcceptProofs}` - ) + messageContext.agentContext.config.logger.info(`Automatically sending acknowledgement with autoAccept`) - const { message, proofRecord } = await this.proofService.createAck(messageContext.agentContext, { - proofRecord: record, + const { message } = await this.proofProtocol.acceptPresentation(messageContext.agentContext, { + proofRecord, }) - const requestMessage = await this.didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + const didCommMessageRepository = messageContext.agentContext.dependencyManager.resolve(DidCommMessageRepository) + const requestMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { associatedRecordId: proofRecord.id, messageClass: V2RequestPresentationMessage, }) - const presentationMessage = await this.didCommMessageRepository.findAgentMessage(messageContext.agentContext, { - associatedRecordId: proofRecord.id, - messageClass: V2PresentationMessage, - }) - if (messageContext.connection) { return new OutboundMessageContext(message, { agentContext: messageContext.agentContext, connection: messageContext.connection, associatedRecord: proofRecord, }) - } else if (requestMessage?.service && presentationMessage?.service) { - const recipientService = presentationMessage?.service + } else if (requestMessage?.service && messageContext.message?.service) { + const recipientService = messageContext.message?.service const ourService = requestMessage?.service return new OutboundMessageContext(message, { @@ -81,6 +62,6 @@ export class V2PresentationHandler implements MessageHandler { }) } - this.agentConfig.logger.error(`Could not automatically create presentation ack`) + messageContext.agentContext.config.logger.error(`Could not automatically create presentation ack`) } } diff --git a/packages/core/src/modules/proofs/protocol/v2/handlers/V2PresentationProblemReportHandler.ts b/packages/core/src/modules/proofs/protocol/v2/handlers/V2PresentationProblemReportHandler.ts index 947a8c6c44..5d9512d824 100644 --- a/packages/core/src/modules/proofs/protocol/v2/handlers/V2PresentationProblemReportHandler.ts +++ b/packages/core/src/modules/proofs/protocol/v2/handlers/V2PresentationProblemReportHandler.ts @@ -1,13 +1,13 @@ import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' -import type { V2ProofService } from '../V2ProofService' +import type { V2ProofProtocol } from '../V2ProofProtocol' import { V2PresentationProblemReportMessage } from '../messages' export class V2PresentationProblemReportHandler implements MessageHandler { - private proofService: V2ProofService + private proofService: V2ProofProtocol public supportedMessages = [V2PresentationProblemReportMessage] - public constructor(proofService: V2ProofService) { + public constructor(proofService: V2ProofProtocol) { this.proofService = proofService } diff --git a/packages/core/src/modules/proofs/protocol/v2/handlers/V2ProposePresentationHandler.ts b/packages/core/src/modules/proofs/protocol/v2/handlers/V2ProposePresentationHandler.ts index 9432a3ca56..589ff6db3e 100644 --- a/packages/core/src/modules/proofs/protocol/v2/handlers/V2ProposePresentationHandler.ts +++ b/packages/core/src/modules/proofs/protocol/v2/handlers/V2ProposePresentationHandler.ts @@ -1,99 +1,42 @@ -import type { AgentConfig } from '../../../../../agent/AgentConfig' import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' -import type { DidCommMessageRepository } from '../../../../../storage' -import type { ProofResponseCoordinator } from '../../../ProofResponseCoordinator' -import type { ProofFormat } from '../../../formats/ProofFormat' -import type { - CreateProofRequestFromProposalOptions, - CreateRequestAsResponseOptions, - ProofRequestFromProposalOptions, -} from '../../../models/ProofServiceOptions' import type { ProofExchangeRecord } from '../../../repository/ProofExchangeRecord' -import type { V2ProofService } from '../V2ProofService' +import type { V2ProofProtocol } from '../V2ProofProtocol' import { OutboundMessageContext } from '../../../../../agent/models' -import { AriesFrameworkError } from '../../../../../error/AriesFrameworkError' -import { V2ProposalPresentationMessage } from '../messages/V2ProposalPresentationMessage' +import { V2ProposePresentationMessage } from '../messages/V2ProposePresentationMessage' -export class V2ProposePresentationHandler implements MessageHandler { - private proofService: V2ProofService - private agentConfig: AgentConfig - private didCommMessageRepository: DidCommMessageRepository - private proofResponseCoordinator: ProofResponseCoordinator - public supportedMessages = [V2ProposalPresentationMessage] +export class V2ProposePresentationHandler implements MessageHandler { + private proofProtocol: V2ProofProtocol + public supportedMessages = [V2ProposePresentationMessage] - public constructor( - proofService: V2ProofService, - agentConfig: AgentConfig, - didCommMessageRepository: DidCommMessageRepository, - proofResponseCoordinator: ProofResponseCoordinator - ) { - this.proofService = proofService - this.agentConfig = agentConfig - this.didCommMessageRepository = didCommMessageRepository - this.proofResponseCoordinator = proofResponseCoordinator + public constructor(proofProtocol: V2ProofProtocol) { + this.proofProtocol = proofProtocol } public async handle(messageContext: MessageHandlerInboundMessage) { - const proofRecord = await this.proofService.processProposal(messageContext) + const proofRecord = await this.proofProtocol.processProposal(messageContext) - const shouldAutoRespond = await this.proofResponseCoordinator.shouldAutoRespondToProposal( - messageContext.agentContext, - proofRecord - ) + const shouldAutoRespond = await this.proofProtocol.shouldAutoRespondToProposal(messageContext.agentContext, { + proofRecord, + proposalMessage: messageContext.message, + }) if (shouldAutoRespond) { - return this.createRequest(proofRecord, messageContext) + return this.acceptProposal(proofRecord, messageContext) } } - - private async createRequest( + private async acceptProposal( proofRecord: ProofExchangeRecord, messageContext: MessageHandlerInboundMessage ) { - this.agentConfig.logger.info( - `Automatically sending request with autoAccept on ${this.agentConfig.autoAcceptProofs}` - ) + messageContext.agentContext.config.logger.info(`Automatically sending request with autoAccept`) if (!messageContext.connection) { - this.agentConfig.logger.error('No connection on the messageContext') - throw new AriesFrameworkError('No connection on the messageContext') - } - - const proposalMessage = await this.didCommMessageRepository.findAgentMessage(messageContext.agentContext, { - associatedRecordId: proofRecord.id, - messageClass: V2ProposalPresentationMessage, - }) - - if (!proposalMessage) { - this.agentConfig.logger.error(`Proof record with id ${proofRecord.id} is missing required credential proposal`) - throw new AriesFrameworkError(`Proof record with id ${proofRecord.id} is missing required credential proposal`) - } - - const proofRequestFromProposalOptions: CreateProofRequestFromProposalOptions = { - proofRecord, - } - - const proofRequest: ProofRequestFromProposalOptions = await this.proofService.createProofRequestFromProposal( - messageContext.agentContext, - proofRequestFromProposalOptions - ) - - const indyProofRequest = proofRequest.proofFormats - - if (!indyProofRequest) { - this.agentConfig.logger.error('Failed to create proof request') - throw new AriesFrameworkError('Failed to create proof request.') - } - - const options: CreateRequestAsResponseOptions = { - proofRecord: proofRecord, - autoAcceptProof: proofRecord.autoAcceptProof, - proofFormats: indyProofRequest, - willConfirm: true, + messageContext.agentContext.config.logger.error('No connection on the messageContext, aborting auto accept') + return } - const { message } = await this.proofService.createRequestAsResponse(messageContext.agentContext, options) + const { message } = await this.proofProtocol.acceptProposal(messageContext.agentContext, { proofRecord }) return new OutboundMessageContext(message, { agentContext: messageContext.agentContext, diff --git a/packages/core/src/modules/proofs/protocol/v2/handlers/V2RequestPresentationHandler.ts b/packages/core/src/modules/proofs/protocol/v2/handlers/V2RequestPresentationHandler.ts index 65edcd85c5..e43a60df7e 100644 --- a/packages/core/src/modules/proofs/protocol/v2/handlers/V2RequestPresentationHandler.ts +++ b/packages/core/src/modules/proofs/protocol/v2/handlers/V2RequestPresentationHandler.ts @@ -1,86 +1,45 @@ -import type { AgentConfig } from '../../../../../agent/AgentConfig' import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' -import type { DidCommMessageRepository } from '../../../../../storage/didcomm/DidCommMessageRepository' -import type { MediationRecipientService, RoutingService } from '../../../../routing' -import type { ProofResponseCoordinator } from '../../../ProofResponseCoordinator' -import type { ProofFormat } from '../../../formats/ProofFormat' -import type { - FormatRequestedCredentialReturn, - FormatRetrievedCredentialOptions, -} from '../../../models/ProofServiceOptions' import type { ProofExchangeRecord } from '../../../repository/ProofExchangeRecord' -import type { V2ProofService } from '../V2ProofService' +import type { V2ProofProtocol } from '../V2ProofProtocol' import { OutboundMessageContext } from '../../../../../agent/models' import { ServiceDecorator } from '../../../../../decorators/service/ServiceDecorator' import { DidCommMessageRole } from '../../../../../storage' +import { DidCommMessageRepository } from '../../../../../storage/didcomm/DidCommMessageRepository' +import { RoutingService } from '../../../../routing' import { V2RequestPresentationMessage } from '../messages/V2RequestPresentationMessage' -export class V2RequestPresentationHandler implements MessageHandler { - private proofService: V2ProofService - private agentConfig: AgentConfig - private proofResponseCoordinator: ProofResponseCoordinator - private mediationRecipientService: MediationRecipientService - private didCommMessageRepository: DidCommMessageRepository - private routingService: RoutingService +export class V2RequestPresentationHandler implements MessageHandler { + private proofProtocol: V2ProofProtocol public supportedMessages = [V2RequestPresentationMessage] - public constructor( - proofService: V2ProofService, - agentConfig: AgentConfig, - proofResponseCoordinator: ProofResponseCoordinator, - mediationRecipientService: MediationRecipientService, - didCommMessageRepository: DidCommMessageRepository, - routingService: RoutingService - ) { - this.proofService = proofService - this.agentConfig = agentConfig - this.proofResponseCoordinator = proofResponseCoordinator - this.mediationRecipientService = mediationRecipientService - this.didCommMessageRepository = didCommMessageRepository - this.routingService = routingService + public constructor(proofProtocol: V2ProofProtocol) { + this.proofProtocol = proofProtocol } public async handle(messageContext: MessageHandlerInboundMessage) { - const proofRecord = await this.proofService.processRequest(messageContext) + const proofRecord = await this.proofProtocol.processRequest(messageContext) + + const shouldAutoRespond = await this.proofProtocol.shouldAutoRespondToRequest(messageContext.agentContext, { + proofRecord, + requestMessage: messageContext.message, + }) - const shouldAutoRespond = await this.proofResponseCoordinator.shouldAutoRespondToRequest( - messageContext.agentContext, - proofRecord - ) + messageContext.agentContext.config.logger.debug(`Should auto respond to request: ${shouldAutoRespond}`) if (shouldAutoRespond) { - return await this.createPresentation(proofRecord, messageContext) + return await this.acceptRequest(proofRecord, messageContext) } } - private async createPresentation( - record: ProofExchangeRecord, + private async acceptRequest( + proofRecord: ProofExchangeRecord, messageContext: MessageHandlerInboundMessage ) { - const requestMessage = await this.didCommMessageRepository.getAgentMessage(messageContext.agentContext, { - associatedRecordId: record.id, - messageClass: V2RequestPresentationMessage, - }) - - this.agentConfig.logger.info( - `Automatically sending presentation with autoAccept on ${this.agentConfig.autoAcceptProofs}` - ) + messageContext.agentContext.config.logger.info(`Automatically sending presentation with autoAccept`) - const retrievedCredentials: FormatRetrievedCredentialOptions = - await this.proofService.getRequestedCredentialsForProofRequest(messageContext.agentContext, { - proofRecord: record, - config: { - filterByPresentationPreview: false, - }, - }) - - const requestedCredentials: FormatRequestedCredentialReturn = - await this.proofService.autoSelectCredentialsForProofRequest(retrievedCredentials) - - const { message, proofRecord } = await this.proofService.createPresentation(messageContext.agentContext, { - proofRecord: record, - proofFormats: requestedCredentials.proofFormats, + const { message } = await this.proofProtocol.acceptRequest(messageContext.agentContext, { + proofRecord, }) if (messageContext.connection) { @@ -89,16 +48,19 @@ export class V2RequestPresentationHandler(RoutingService) + const didCommMessageRepository = messageContext.agentContext.dependencyManager.resolve(DidCommMessageRepository) + + const routing = await routingService.getRouting(messageContext.agentContext) message.service = new ServiceDecorator({ serviceEndpoint: routing.endpoints[0], recipientKeys: [routing.recipientKey.publicKeyBase58], routingKeys: routing.routingKeys.map((key) => key.publicKeyBase58), }) - const recipientService = requestMessage.service + const recipientService = messageContext.message.service - await this.didCommMessageRepository.saveOrUpdateAgentMessage(messageContext.agentContext, { + await didCommMessageRepository.saveOrUpdateAgentMessage(messageContext.agentContext, { agentMessage: message, associatedRecordId: proofRecord.id, role: DidCommMessageRole.Sender, @@ -113,6 +75,6 @@ export class V2RequestPresentationHandler { - const attachment = this.presentationsAttach.find((attachment) => attachment.id === format.attachmentId) - - if (!attachment) { - throw new AriesFrameworkError(`Could not find a matching attachment with attachmentId: ${format.attachmentId}`) - } - - attachmentFormats.push({ format, attachment }) - }) - return attachmentFormats - } - @IsValidMessageType(V2PresentationMessage.type) public readonly type = V2PresentationMessage.type.messageTypeUri public static readonly type = parseMessageType('https://didcomm.org/present-proof/2.0/presentation') @@ -89,5 +62,9 @@ export class V2PresentationMessage extends AgentMessage { @IsArray() @ValidateNested({ each: true }) @IsInstance(Attachment, { each: true }) - public presentationsAttach!: Attachment[] + public presentationAttachments!: Attachment[] + + public getPresentationAttachmentById(id: string): Attachment | undefined { + return this.presentationAttachments.find((attachment) => attachment.id == id) + } } diff --git a/packages/core/src/modules/proofs/protocol/v2/messages/V2PresentationProblemReportMessage.ts b/packages/core/src/modules/proofs/protocol/v2/messages/V2PresentationProblemReportMessage.ts index b36a69a7fe..ed97f72319 100644 --- a/packages/core/src/modules/proofs/protocol/v2/messages/V2PresentationProblemReportMessage.ts +++ b/packages/core/src/modules/proofs/protocol/v2/messages/V2PresentationProblemReportMessage.ts @@ -1,22 +1,10 @@ -import type { ProblemReportMessageOptions } from '../../../../problem-reports/messages/ProblemReportMessage' - import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' import { ProblemReportMessage } from '../../../../problem-reports/messages/ProblemReportMessage' -export type V2PresentationProblemReportMessageOptions = ProblemReportMessageOptions - /** * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0035-report-problem/README.md */ export class V2PresentationProblemReportMessage extends ProblemReportMessage { - /** - * Create new PresentationProblemReportMessage instance. - * @param options - */ - public constructor(options: V2PresentationProblemReportMessageOptions) { - super(options) - } - @IsValidMessageType(V2PresentationProblemReportMessage.type) public readonly type = V2PresentationProblemReportMessage.type.messageTypeUri public static readonly type = parseMessageType('https://didcomm.org/present-proof/2.0/problem-report') diff --git a/packages/core/src/modules/proofs/protocol/v2/messages/V2ProposalPresentationMessage.ts b/packages/core/src/modules/proofs/protocol/v2/messages/V2ProposalPresentationMessage.ts deleted file mode 100644 index 265ed8ae7e..0000000000 --- a/packages/core/src/modules/proofs/protocol/v2/messages/V2ProposalPresentationMessage.ts +++ /dev/null @@ -1,100 +0,0 @@ -import type { ProofAttachmentFormat } from '../../../formats/models/ProofAttachmentFormat' - -import { Expose, Type } from 'class-transformer' -import { IsArray, IsBoolean, IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' - -import { AgentMessage } from '../../../../../agent/AgentMessage' -import { Attachment } from '../../../../../decorators/attachment/Attachment' -import { AriesFrameworkError } from '../../../../../error/AriesFrameworkError' -import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' -import { uuid } from '../../../../../utils/uuid' -import { ProofFormatSpec } from '../../../models/ProofFormatSpec' - -export interface V2ProposePresentationMessageOptions { - id?: string - comment?: string - goalCode?: string - willConfirm?: boolean - parentThreadId?: string - attachmentInfo: ProofAttachmentFormat[] -} - -export class V2ProposalPresentationMessage extends AgentMessage { - public constructor(options: V2ProposePresentationMessageOptions) { - super() - - if (options) { - this.formats = [] - this.proposalsAttach = [] - this.id = options.id ?? uuid() - this.comment = options.comment - this.goalCode = options.goalCode - this.willConfirm = options.willConfirm ?? false - - if (options.parentThreadId) { - this.setThread({ - parentThreadId: options.parentThreadId, - }) - } - - for (const entry of options.attachmentInfo) { - this.addProposalsAttachment(entry) - } - } - } - - public addProposalsAttachment(attachment: ProofAttachmentFormat) { - this.formats.push(attachment.format) - this.proposalsAttach.push(attachment.attachment) - } - - /** - * Every attachment has a corresponding entry in the formats array. - * This method pairs those together in a {@link ProofAttachmentFormat} object. - */ - public getAttachmentFormats(): ProofAttachmentFormat[] { - const attachmentFormats: ProofAttachmentFormat[] = [] - - this.formats.forEach((format) => { - const attachment = this.proposalsAttach.find((attachment) => attachment.id === format.attachmentId) - - if (!attachment) { - throw new AriesFrameworkError(`Could not find a matching attachment with attachmentId: ${format.attachmentId}`) - } - - attachmentFormats.push({ format, attachment }) - }) - return attachmentFormats - } - - @IsValidMessageType(V2ProposalPresentationMessage.type) - public readonly type = V2ProposalPresentationMessage.type.messageTypeUri - public static readonly type = parseMessageType(`https://didcomm.org/present-proof/2.0/propose-presentation`) - - @IsString() - @IsOptional() - public comment?: string - - @Expose({ name: 'goal_code' }) - @IsString() - @IsOptional() - public goalCode?: string - - @Expose({ name: 'will_confirm' }) - @IsBoolean() - public willConfirm = false - - @Expose({ name: 'formats' }) - @Type(() => ProofFormatSpec) - @IsArray() - @ValidateNested({ each: true }) - @IsInstance(ProofFormatSpec, { each: true }) - public formats!: ProofFormatSpec[] - - @Expose({ name: 'proposals~attach' }) - @Type(() => Attachment) - @IsArray() - @ValidateNested({ each: true }) - @IsInstance(Attachment, { each: true }) - public proposalsAttach!: Attachment[] -} diff --git a/packages/core/src/modules/proofs/protocol/v2/messages/V2ProposePresentationMessage.ts b/packages/core/src/modules/proofs/protocol/v2/messages/V2ProposePresentationMessage.ts new file mode 100644 index 0000000000..385925a5d0 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/messages/V2ProposePresentationMessage.ts @@ -0,0 +1,62 @@ +import { Expose, Type } from 'class-transformer' +import { IsArray, IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' + +import { AgentMessage } from '../../../../../agent/AgentMessage' +import { Attachment } from '../../../../../decorators/attachment/Attachment' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { uuid } from '../../../../../utils/uuid' +import { ProofFormatSpec } from '../../../models/ProofFormatSpec' + +export interface V2ProposePresentationMessageOptions { + id?: string + comment?: string + goalCode?: string + proposalAttachments: Attachment[] + formats: ProofFormatSpec[] +} + +export class V2ProposePresentationMessage extends AgentMessage { + public constructor(options: V2ProposePresentationMessageOptions) { + super() + + if (options) { + this.formats = [] + this.proposalAttachments = [] + this.id = options.id ?? uuid() + this.comment = options.comment + this.goalCode = options.goalCode + this.formats = options.formats + this.proposalAttachments = options.proposalAttachments + } + } + + @IsValidMessageType(V2ProposePresentationMessage.type) + public readonly type = V2ProposePresentationMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/present-proof/2.0/propose-presentation') + + @IsString() + @IsOptional() + public comment?: string + + @Expose({ name: 'goal_code' }) + @IsString() + @IsOptional() + public goalCode?: string + + @Type(() => ProofFormatSpec) + @IsArray() + @ValidateNested({ each: true }) + @IsInstance(ProofFormatSpec, { each: true }) + public formats!: ProofFormatSpec[] + + @Expose({ name: 'proposals~attach' }) + @Type(() => Attachment) + @IsArray() + @ValidateNested({ each: true }) + @IsInstance(Attachment, { each: true }) + public proposalAttachments!: Attachment[] + + public getProposalAttachmentById(id: string): Attachment | undefined { + return this.proposalAttachments.find((attachment) => attachment.id === id) + } +} diff --git a/packages/core/src/modules/proofs/protocol/v2/messages/V2RequestPresentationMessage.ts b/packages/core/src/modules/proofs/protocol/v2/messages/V2RequestPresentationMessage.ts index 060badc050..0723d3d792 100644 --- a/packages/core/src/modules/proofs/protocol/v2/messages/V2RequestPresentationMessage.ts +++ b/packages/core/src/modules/proofs/protocol/v2/messages/V2RequestPresentationMessage.ts @@ -1,11 +1,8 @@ -import type { ProofAttachmentFormat } from '../../../formats/models/ProofAttachmentFormat' - import { Expose, Type } from 'class-transformer' import { IsArray, IsBoolean, IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' import { AgentMessage } from '../../../../../agent/AgentMessage' import { Attachment } from '../../../../../decorators/attachment/Attachment' -import { AriesFrameworkError } from '../../../../../error' import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' import { uuid } from '../../../../../utils/uuid' import { ProofFormatSpec } from '../../../models/ProofFormatSpec' @@ -16,8 +13,8 @@ export interface V2RequestPresentationMessageOptions { goalCode?: string presentMultiple?: boolean willConfirm?: boolean - parentThreadId?: string - attachmentInfo: ProofAttachmentFormat[] + formats: ProofFormatSpec[] + requestAttachments: Attachment[] } export class V2RequestPresentationMessage extends AgentMessage { @@ -26,68 +23,17 @@ export class V2RequestPresentationMessage extends AgentMessage { if (options) { this.formats = [] - this.requestPresentationsAttach = [] + this.requestAttachments = [] this.id = options.id ?? uuid() this.comment = options.comment this.goalCode = options.goalCode this.willConfirm = options.willConfirm ?? true this.presentMultiple = options.presentMultiple ?? false - - if (options.parentThreadId) { - this.setThread({ - parentThreadId: options.parentThreadId, - }) - } - - for (const entry of options.attachmentInfo) { - this.addRequestPresentationsAttachment(entry) - } + this.requestAttachments = options.requestAttachments + this.formats = options.formats } } - public addRequestPresentationsAttachment(attachment: ProofAttachmentFormat) { - this.formats.push(attachment.format) - this.requestPresentationsAttach.push(attachment.attachment) - } - - public getAttachmentByFormatIdentifier(formatIdentifier: string) { - const format = this.formats.find((x) => x.format === formatIdentifier) - if (!format) { - throw new AriesFrameworkError( - `Expected to find a format entry of type: ${formatIdentifier}, but none could be found.` - ) - } - - const attachment = this.requestPresentationsAttach.find((x) => x.id === format.attachmentId) - - if (!attachment) { - throw new AriesFrameworkError( - `Expected to find an attachment entry with id: ${format.attachmentId}, but none could be found.` - ) - } - - return attachment - } - - /** - * Every attachment has a corresponding entry in the formats array. - * This method pairs those together in a {@link ProofAttachmentFormat} object. - */ - public getAttachmentFormats(): ProofAttachmentFormat[] { - const attachmentFormats: ProofAttachmentFormat[] = [] - - this.formats.forEach((format) => { - const attachment = this.requestPresentationsAttach.find((attachment) => attachment.id === format.attachmentId) - - if (!attachment) { - throw new AriesFrameworkError(`Could not find a matching attachment with attachmentId: ${format.attachmentId}`) - } - - attachmentFormats.push({ format, attachment }) - }) - return attachmentFormats - } - @IsValidMessageType(V2RequestPresentationMessage.type) public readonly type = V2RequestPresentationMessage.type.messageTypeUri public static readonly type = parseMessageType('https://didcomm.org/present-proof/2.0/request-presentation') @@ -121,5 +67,9 @@ export class V2RequestPresentationMessage extends AgentMessage { @IsArray() @ValidateNested({ each: true }) @IsInstance(Attachment, { each: true }) - public requestPresentationsAttach!: Attachment[] + public requestAttachments!: Attachment[] + + public getRequestAttachmentById(id: string): Attachment | undefined { + return this.requestAttachments.find((attachment) => attachment.id == id) + } } diff --git a/packages/core/src/modules/proofs/protocol/v2/messages/index.ts b/packages/core/src/modules/proofs/protocol/v2/messages/index.ts index 8b0c4a005d..515b0afb9c 100644 --- a/packages/core/src/modules/proofs/protocol/v2/messages/index.ts +++ b/packages/core/src/modules/proofs/protocol/v2/messages/index.ts @@ -1,5 +1,5 @@ export * from './V2PresentationAckMessage' export * from './V2PresentationMessage' export * from './V2PresentationProblemReportMessage' -export * from './V2ProposalPresentationMessage' +export * from './V2ProposePresentationMessage' export * from './V2RequestPresentationMessage' diff --git a/packages/core/src/modules/proofs/repository/ProofExchangeRecord.ts b/packages/core/src/modules/proofs/repository/ProofExchangeRecord.ts index f145703dff..e8d6784f12 100644 --- a/packages/core/src/modules/proofs/repository/ProofExchangeRecord.ts +++ b/packages/core/src/modules/proofs/repository/ProofExchangeRecord.ts @@ -82,6 +82,14 @@ export class ProofExchangeRecord extends BaseRecord ): Promise { @@ -367,6 +367,15 @@ export class W3cCredentialService { const result = await this.w3cCredentialRepository.findSingleByQuery(agentContext, query) return result?.credential } + public getProofTypeByVerificationMethodType(verificationMethodType: string): string { + const suite = this.signatureSuiteRegistry.getByVerificationMethodType(verificationMethodType) + + if (!suite) { + throw new AriesFrameworkError(`No suite found for verification method type ${verificationMethodType}}`) + } + + return suite.proofType + } private getSignatureSuitesForCredential(agentContext: AgentContext, credential: W3cVerifiableCredential) { const WalletKeyPair = createWalletKeyPairClass(agentContext.wallet) diff --git a/packages/core/src/modules/vc/__tests__/W3cCredentialService.test.ts b/packages/core/src/modules/vc/__tests__/W3cCredentialService.test.ts index c339fbfb4e..d6f3eb266f 100644 --- a/packages/core/src/modules/vc/__tests__/W3cCredentialService.test.ts +++ b/packages/core/src/modules/vc/__tests__/W3cCredentialService.test.ts @@ -297,10 +297,7 @@ describe('W3cCredentialService', () => { const result = await w3cCredentialService.verifyPresentation(agentContext, { presentation: vp, - proofType: 'Ed25519Signature2018', challenge: '7bf32d0b-39d4-41f3-96b6-45de52988e4c', - verificationMethod: - 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', }) expect(result.verified).toBe(true) diff --git a/packages/core/src/modules/vc/__tests__/contexts/citizenship_v2.ts b/packages/core/src/modules/vc/__tests__/contexts/citizenship_v2.ts new file mode 100644 index 0000000000..667571bea2 --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/contexts/citizenship_v2.ts @@ -0,0 +1,45 @@ +export const CITIZENSHIP_V2 = { + '@context': { + '@version': 1.1, + '@protected': true, + name: 'http://schema.org/name', + description: 'http://schema.org/description', + identifier: 'http://schema.org/identifier', + image: { '@id': 'http://schema.org/image', '@type': '@id' }, + PermanentResidentCard: { + '@id': 'https://w3id.org/citizenship#PermanentResidentCard', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + description: 'http://schema.org/description', + name: 'http://schema.org/name', + identifier: 'http://schema.org/identifier', + image: { '@id': 'http://schema.org/image', '@type': '@id' }, + }, + }, + PermanentResident: { + '@id': 'https://w3id.org/citizenship#PermanentResident', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + ctzn: 'https://w3id.org/citizenship#', + schema: 'http://schema.org/', + xsd: 'http://www.w3.org/2001/XMLSchema#', + birthCountry: 'ctzn:birthCountry', + birthDate: { '@id': 'schema:birthDate', '@type': 'xsd:dateTime' }, + commuterClassification: 'ctzn:commuterClassification', + familyName: 'schema:familyName', + gender: 'schema:gender', + givenName: 'schema:givenName', + lprCategory: 'ctzn:lprCategory', + lprNumber: 'ctzn:lprNumber', + residentSince: { '@id': 'ctzn:residentSince', '@type': 'xsd:dateTime' }, + }, + }, + Person: 'http://schema.org/Person', + }, +} diff --git a/packages/core/src/modules/vc/__tests__/contexts/index.ts b/packages/core/src/modules/vc/__tests__/contexts/index.ts index c66801c24a..0d5bfff11a 100644 --- a/packages/core/src/modules/vc/__tests__/contexts/index.ts +++ b/packages/core/src/modules/vc/__tests__/contexts/index.ts @@ -8,4 +8,6 @@ export * from './schema_org' export * from './security_v1' export * from './security_v2' export * from './security_v3_unstable' +export * from './submission' export * from './vaccination_v1' +export * from './vaccination_v2' diff --git a/packages/core/src/modules/vc/__tests__/contexts/submission.ts b/packages/core/src/modules/vc/__tests__/contexts/submission.ts new file mode 100644 index 0000000000..4df5ca9b4f --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/contexts/submission.ts @@ -0,0 +1,15 @@ +export const PRESENTATION_SUBMISSION = { + '@context': { + '@version': 1.1, + PresentationSubmission: { + '@id': 'https://identity.foundation/presentation-exchange/#presentation-submission', + '@context': { + '@version': 1.1, + presentation_submission: { + '@id': 'https://identity.foundation/presentation-exchange/#presentation-submission', + '@type': '@json', + }, + }, + }, + }, +} diff --git a/packages/core/src/modules/vc/__tests__/contexts/vaccination_v2.ts b/packages/core/src/modules/vc/__tests__/contexts/vaccination_v2.ts new file mode 100644 index 0000000000..483c87134b --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/contexts/vaccination_v2.ts @@ -0,0 +1,88 @@ +export const VACCINATION_V2 = { + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + description: 'http://schema.org/description', + identifier: 'http://schema.org/identifier', + name: 'http://schema.org/name', + image: 'http://schema.org/image', + VaccinationCertificate: { + '@id': 'https://w3id.org/vaccination#VaccinationCertificate', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + description: 'http://schema.org/description', + identifier: 'http://schema.org/identifier', + name: 'http://schema.org/name', + image: 'http://schema.org/image', + }, + }, + VaccinationEvent: { + '@id': 'https://w3id.org/vaccination#VaccinationEvent', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + administeringCentre: 'https://w3id.org/vaccination#administeringCentre', + batchNumber: 'https://w3id.org/vaccination#batchNumber', + countryOfVaccination: 'https://w3id.org/vaccination#countryOfVaccination', + dateOfVaccination: { + '@id': 'https://w3id.org/vaccination#dateOfVaccination', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + healthProfessional: 'https://w3id.org/vaccination#healthProfessional', + nextVaccinationDate: { + '@id': 'https://w3id.org/vaccination#nextVaccinationDate', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + order: 'https://w3id.org/vaccination#order', + recipient: { + '@id': 'https://w3id.org/vaccination#recipient', + '@type': 'https://w3id.org/vaccination#VaccineRecipient', + }, + vaccine: { + '@id': 'https://w3id.org/vaccination#VaccineEventVaccine', + '@type': 'https://w3id.org/vaccination#Vaccine', + }, + }, + }, + VaccineRecipient: { + '@id': 'https://w3id.org/vaccination#VaccineRecipient', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + birthDate: { + '@id': 'http://schema.org/birthDate', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + familyName: 'http://schema.org/familyName', + gender: 'http://schema.org/gender', + givenName: 'http://schema.org/givenName', + }, + }, + Vaccine: { + '@id': 'https://w3id.org/vaccination#Vaccine', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + atcCode: 'https://w3id.org/vaccination#atc-code', + disease: 'https://w3id.org/vaccination#disease', + event: { + '@id': 'https://w3id.org/vaccination#VaccineRecipientVaccineEvent', + '@type': 'https://w3id.org/vaccination#VaccineEvent', + }, + marketingAuthorizationHolder: 'https://w3id.org/vaccination#marketingAuthorizationHolder', + medicinalProductName: 'https://w3id.org/vaccination#medicinalProductName', + }, + }, + }, +} diff --git a/packages/core/src/modules/vc/__tests__/documentLoader.ts b/packages/core/src/modules/vc/__tests__/documentLoader.ts index 4d7aa89f0d..adf72dba7f 100644 --- a/packages/core/src/modules/vc/__tests__/documentLoader.ts +++ b/packages/core/src/modules/vc/__tests__/documentLoader.ts @@ -4,9 +4,18 @@ import type { DocumentLoaderResult } from '../libraries/jsonld' import jsonld from '../libraries/jsonld' -import { BBS_V1, EXAMPLES_V1, ODRL, SCHEMA_ORG, VACCINATION_V1 } from './contexts' +import { + BBS_V1, + EXAMPLES_V1, + ODRL, + PRESENTATION_SUBMISSION, + SCHEMA_ORG, + VACCINATION_V1, + VACCINATION_V2, +} from './contexts' import { X25519_V1 } from './contexts/X25519_v1' import { CITIZENSHIP_V1 } from './contexts/citizenship_v1' +import { CITIZENSHIP_V2 } from './contexts/citizenship_v2' import { CREDENTIALS_V1 } from './contexts/credentials_v1' import { DID_V1 } from './contexts/did_v1' import { ED25519_V1 } from './contexts/ed25519_v1' @@ -95,9 +104,12 @@ export const DOCUMENTS = { 'https://www.w3.org/ns/did/v1': DID_V1, 'https://w3.org/ns/did/v1': DID_V1, 'https://w3id.org/citizenship/v1': CITIZENSHIP_V1, + 'https://w3id.org/citizenship/v2': CITIZENSHIP_V2, 'https://www.w3.org/ns/odrl.jsonld': ODRL, 'http://schema.org/': SCHEMA_ORG, 'https://w3id.org/vaccination/v1': VACCINATION_V1, + 'https://w3id.org/vaccination/v2': VACCINATION_V2, + 'https://identity.foundation/presentation-exchange/submission/v1': PRESENTATION_SUBMISSION, 'https://mattr.global/contexts/vc-extensions/v1': MATTR_VC_EXTENSION_V1, 'https://purl.imsglobal.org/spec/ob/v3p0/context.json': PURL_OB_V3P0, 'https://w3c-ccg.github.io/vc-status-rl-2020/contexts/vc-revocation-list-2020/v1.jsonld': VC_REVOCATION_LIST_2020, diff --git a/packages/core/src/modules/vc/__tests__/fixtures.ts b/packages/core/src/modules/vc/__tests__/fixtures.ts index 491a388f98..9e8a6caa16 100644 --- a/packages/core/src/modules/vc/__tests__/fixtures.ts +++ b/packages/core/src/modules/vc/__tests__/fixtures.ts @@ -13,6 +13,36 @@ export const Ed25519Signature2018Fixtures = { }, }, }, + TEST_LD_DOCUMENT_2: { + '@context': [ + 'https://www.w3.org/2018/credentials/v1', + 'https://w3id.org/security/suites/ed25519-2020/v1', + 'https://w3id.org/citizenship/v1', + ], + id: 'https://issuer.oidp.uscis.gov/credentials/83627465', + type: ['VerifiableCredential', 'PermanentResidentCard'], + issuer: '', + identifier: '83627465', + name: 'Permanent Resident Card', + description: 'Government of Example Permanent Resident Card.', + issuanceDate: '2019-12-03T12:19:52Z', + expirationDate: '2029-12-03T12:19:52Z', + credentialSubject: { + id: 'did:example:b34ca6cd37bbf23', + type: ['PermanentResident', 'Person'], + givenName: 'JOHN', + familyName: 'SMITH', + gender: 'Male', + image: 'data:image/png;base64,iVBORw0KGgokJggg==', + residentSince: '2015-01-01', + lprCategory: 'C09', + lprNumber: '999-999-999', + commuterClassification: 'C1', + birthCountry: 'Bahamas', + birthDate: '1958-07-17', + }, + }, + TEST_LD_DOCUMENT_SIGNED: { '@context': [CREDENTIALS_CONTEXT_V1_URL, 'https://www.w3.org/2018/credentials/examples/v1'], type: ['VerifiableCredential', 'UniversityDegreeCredential'], diff --git a/packages/core/src/modules/vc/constants.ts b/packages/core/src/modules/vc/constants.ts index 6b298625df..b166244ebf 100644 --- a/packages/core/src/modules/vc/constants.ts +++ b/packages/core/src/modules/vc/constants.ts @@ -5,6 +5,7 @@ export const SECURITY_CONTEXT_URL = SECURITY_CONTEXT_V2_URL export const SECURITY_X25519_CONTEXT_URL = 'https://w3id.org/security/suites/x25519-2019/v1' export const DID_V1_CONTEXT_URL = 'https://www.w3.org/ns/did/v1' export const CREDENTIALS_CONTEXT_V1_URL = 'https://www.w3.org/2018/credentials/v1' +export const BANKACCOUNT_CONTEXT_V1_URL = 'https://www.w3.org/2018/bankaccount/v1' export const SECURITY_CONTEXT_BBS_URL = 'https://w3id.org/security/bbs/v1' export const CREDENTIALS_ISSUER_URL = 'https://www.w3.org/2018/credentials#issuer' export const SECURITY_PROOF_URL = 'https://w3id.org/security#proof' diff --git a/packages/core/src/modules/vc/models/W3cCredentialServiceOptions.ts b/packages/core/src/modules/vc/models/W3cCredentialServiceOptions.ts index 042550fd8e..7851859949 100644 --- a/packages/core/src/modules/vc/models/W3cCredentialServiceOptions.ts +++ b/packages/core/src/modules/vc/models/W3cCredentialServiceOptions.ts @@ -39,8 +39,6 @@ export interface SignPresentationOptions { export interface VerifyPresentationOptions { presentation: W3cVerifiablePresentation - proofType: string - verificationMethod: string purpose?: ProofPurpose challenge?: string } diff --git a/packages/core/src/utils/__tests__/indyProofRequest.test.ts b/packages/core/src/utils/__tests__/indyProofRequest.test.ts deleted file mode 100644 index ef15b80f40..0000000000 --- a/packages/core/src/utils/__tests__/indyProofRequest.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { checkProofRequestForDuplicates } from '../indyProofRequest' - -import { - AriesFrameworkError, - AttributeFilter, - PredicateType, - ProofAttributeInfo, - ProofPredicateInfo, - ProofRequest, -} from '@aries-framework/core' - -describe('Present Proof', () => { - const credDefId = '9vPXgSpQJPkJEALbLXueBp:3:CL:57753:tag1' - const nonce = 'testtesttest12345' - - test('attribute names match', () => { - const attributes = { - age1: new ProofAttributeInfo({ - name: 'age', - restrictions: [ - new AttributeFilter({ - credentialDefinitionId: credDefId, - }), - ], - }), - age2: new ProofAttributeInfo({ - name: 'age', - restrictions: [ - new AttributeFilter({ - credentialDefinitionId: credDefId, - }), - ], - }), - } - - const proofRequest = new ProofRequest({ - name: 'proof-request', - version: '1.0', - nonce, - requestedAttributes: attributes, - }) - - expect(() => checkProofRequestForDuplicates(proofRequest)).not.toThrow() - }) - - test('attribute names match with predicates name', () => { - const attributes = { - attrib: new ProofAttributeInfo({ - name: 'age', - restrictions: [ - new AttributeFilter({ - credentialDefinitionId: credDefId, - }), - ], - }), - } - - const predicates = { - predicate: new ProofPredicateInfo({ - name: 'age', - predicateType: PredicateType.GreaterThanOrEqualTo, - predicateValue: 50, - restrictions: [ - new AttributeFilter({ - credentialDefinitionId: credDefId, - }), - ], - }), - } - - const proofRequest = new ProofRequest({ - name: 'proof-request', - version: '1.0', - nonce, - requestedAttributes: attributes, - requestedPredicates: predicates, - }) - - expect(() => checkProofRequestForDuplicates(proofRequest)).toThrowError(AriesFrameworkError) - }) -}) diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index c0dfd135df..8875930e15 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -14,3 +14,4 @@ export * from './type' export * from './indyIdentifiers' export * from './deepEquality' export * from './objectEquality' +export * from './MessageValidator' diff --git a/packages/core/src/utils/indyProofRequest.ts b/packages/core/src/utils/indyProofRequest.ts index 853bf4f742..df52b72cdc 100644 --- a/packages/core/src/utils/indyProofRequest.ts +++ b/packages/core/src/utils/indyProofRequest.ts @@ -25,7 +25,7 @@ function assertNoDuplicates(predicates: string[], attributeNames: string[]) { } // TODO: This is still not ideal. The requested groups can specify different credentials using restrictions. -export function checkProofRequestForDuplicates(proofRequest: ProofRequest) { +export function assertNoDuplicateGroupsNamesInProofRequest(proofRequest: ProofRequest) { const attributes = attributeNamesToArray(proofRequest) const predicates = predicateNamesToArray(proofRequest) assertNoDuplicates(predicates, attributes) diff --git a/packages/core/tests/helpers.ts b/packages/core/tests/helpers.ts index 597cc6bda7..e8fd1f8a29 100644 --- a/packages/core/tests/helpers.ts +++ b/packages/core/tests/helpers.ts @@ -17,7 +17,7 @@ import type { import type { AgentModulesInput, EmptyModuleMap } from '../src/agent/AgentModules' import type { TrustPingReceivedEvent, TrustPingResponseReceivedEvent } from '../src/modules/connections/TrustPingEvents' import type { IndyOfferCredentialFormat } from '../src/modules/credentials/formats/indy/IndyCredentialFormat' -import type { ProofAttributeInfo, ProofPredicateInfo } from '../src/modules/proofs/formats/indy/models' +import type { ProofAttributeInfo, ProofPredicateInfoOptions } from '../src/modules/proofs/formats/indy/models' import type { AutoAcceptProof } from '../src/modules/proofs/models/ProofAutoAcceptType' import type { Awaited, WalletConfig } from '../src/types' import type { CredDef, Schema } from 'indy-sdk' @@ -53,7 +53,6 @@ import { DidExchangeState, HandshakeProtocol, InjectionSymbols, - LogLevel, ProofEventTypes, } from '../src' import { Key, KeyType } from '../src/crypto' @@ -68,11 +67,7 @@ import { OutOfBandInvitation } from '../src/modules/oob/messages' import { OutOfBandRecord } from '../src/modules/oob/repository' import { PredicateType } from '../src/modules/proofs/formats/indy/models' import { ProofState } from '../src/modules/proofs/models/ProofState' -import { - PresentationPreview, - PresentationPreviewAttribute, - PresentationPreviewPredicate, -} from '../src/modules/proofs/protocol/v1/models/V1PresentationPreview' +import { V1PresentationPreview } from '../src/modules/proofs/protocol/v1/models/V1PresentationPreview' import { customDocumentLoader } from '../src/modules/vc/__tests__/documentLoader' import { KeyDerivationMethod } from '../src/types' import { LinkedAttachment } from '../src/utils/LinkedAttachment' @@ -117,7 +112,7 @@ export function getAgentOptions = + subject instanceof ReplaySubject ? subject.asObservable() : subject return firstValueFrom( observable.pipe( filter((e) => previousState === undefined || e.payload.previousState === previousState), @@ -605,71 +601,24 @@ export async function issueCredential({ } } -export async function issueConnectionLessCredential({ - issuerAgent, - holderAgent, - credentialTemplate, -}: { - issuerAgent: Agent - holderAgent: Agent - credentialTemplate: IndyOfferCredentialFormat -}) { - const issuerReplay = new ReplaySubject() - const holderReplay = new ReplaySubject() - - issuerAgent.events - .observable(CredentialEventTypes.CredentialStateChanged) - .subscribe(issuerReplay) - holderAgent.events - .observable(CredentialEventTypes.CredentialStateChanged) - .subscribe(holderReplay) - - // eslint-disable-next-line prefer-const - let { credentialRecord: issuerCredentialRecord, message } = await issuerAgent.credentials.createOffer({ - comment: 'V1 Out of Band offer', - protocolVersion: 'v1', - credentialFormats: { - indy: { - attributes: credentialTemplate.attributes, - credentialDefinitionId: credentialTemplate.credentialDefinitionId, - }, - }, - autoAcceptCredential: AutoAcceptCredential.ContentApproved, - }) - - const { message: offerMessage } = await issuerAgent.oob.createLegacyConnectionlessInvitation({ - recordId: issuerCredentialRecord.id, - domain: 'https://example.org', - message, - }) - - await holderAgent.receiveMessage(offerMessage.toJSON()) - - let holderCredentialRecord = await waitForCredentialRecordSubject(holderReplay, { - threadId: issuerCredentialRecord.threadId, - state: CredentialState.OfferReceived, - }) - const acceptOfferOptions: AcceptCredentialOfferOptions = { - credentialRecordId: holderCredentialRecord.id, - autoAcceptCredential: AutoAcceptCredential.ContentApproved, - } - - await holderAgent.credentials.acceptOffer(acceptOfferOptions) - - holderCredentialRecord = await waitForCredentialRecordSubject(holderReplay, { - threadId: issuerCredentialRecord.threadId, - state: CredentialState.Done, - }) - - issuerCredentialRecord = await waitForCredentialRecordSubject(issuerReplay, { - threadId: issuerCredentialRecord.threadId, - state: CredentialState.Done, - }) +/** + * Returns mock of function with correct type annotations according to original function `fn`. + * It can be used also for class methods. + * + * @param fn function you want to mock + * @returns mock function with type annotations + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function mockFunction any>(fn: T): jest.MockedFunction { + return fn as jest.MockedFunction +} - return { - issuerCredential: issuerCredentialRecord, - holderCredential: holderCredentialRecord, - } +/** + * Set a property using a getter value on a mocked oject. + */ +// eslint-disable-next-line @typescript-eslint/ban-types +export function mockProperty(object: T, property: K, value: T[K]) { + Object.defineProperty(object, property, { get: () => value }) } export async function presentProof({ @@ -683,7 +632,7 @@ export async function presentProof({ holderAgent: Agent presentationTemplate: { attributes?: Record - predicates?: Record + predicates?: Record } }) { const verifierReplay = new ReplaySubject() @@ -704,7 +653,6 @@ export async function presentProof({ requestedAttributes: attributes, requestedPredicates: predicates, version: '1.0', - nonce: '947121108704767252195123', }, }, protocolVersion: 'v2', @@ -712,11 +660,8 @@ export async function presentProof({ let holderRecord = await holderProofExchangeRecordPromise - const requestedCredentials = await holderAgent.proofs.autoSelectCredentialsForProofRequest({ + const requestedCredentials = await holderAgent.proofs.selectCredentialsForRequest({ proofRecordId: holderRecord.id, - config: { - filterByPresentationPreview: true, - }, }) const verifierProofExchangeRecordPromise = waitForProofExchangeRecordSubject(verifierReplay, { @@ -739,7 +684,7 @@ export async function presentProof({ state: ProofState.Done, }) - verifierRecord = await verifierAgent.proofs.acceptPresentation(verifierRecord.id) + verifierRecord = await verifierAgent.proofs.acceptPresentation({ proofRecordId: verifierRecord.id }) holderRecord = await holderProofExchangeRecordPromise return { @@ -748,26 +693,6 @@ export async function presentProof({ } } -/** - * Returns mock of function with correct type annotations according to original function `fn`. - * It can be used also for class methods. - * - * @param fn function you want to mock - * @returns mock function with type annotations - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function mockFunction any>(fn: T): jest.MockedFunction { - return fn as jest.MockedFunction -} - -/** - * Set a property using a getter value on a mocked oject. - */ -// eslint-disable-next-line @typescript-eslint/ban-types -export function mockProperty(object: T, property: K, value: T[K]) { - Object.defineProperty(object, property, { get: () => value }) -} - // Helper type to get the type of the agents (with the custom modules) for the credential tests export type CredentialTestsAgent = Awaited>['aliceAgent'] export async function setupCredentialTests( @@ -857,12 +782,12 @@ export async function setupProofsTest(faberName: string, aliceName: string, auto const unique = uuid().substring(0, 4) - const faberAgentOptions = getAgentOptions(`${faberName}-${unique}`, { + const faberAgentOptions = getAgentOptions(`${faberName} - ${unique}`, { autoAcceptProofs, endpoints: ['rxjs:faber'], }) - const aliceAgentOptions = getAgentOptions(`${aliceName}-${unique}`, { + const aliceAgentOptions = getAgentOptions(`${aliceName} - ${unique}`, { autoAcceptProofs, endpoints: ['rxjs:alice'], }) @@ -893,26 +818,26 @@ export async function setupProofsTest(faberName: string, aliceName: string, auto const faberConnection = agentAConnection const aliceConnection = agentBConnection - const presentationPreview = new PresentationPreview({ + const presentationPreview = new V1PresentationPreview({ attributes: [ - new PresentationPreviewAttribute({ + { name: 'name', credentialDefinitionId: definition.id, referent: '0', value: 'John', - }), - new PresentationPreviewAttribute({ + }, + { name: 'image_0', credentialDefinitionId: definition.id, - }), + }, ], predicates: [ - new PresentationPreviewPredicate({ + { name: 'age', credentialDefinitionId: definition.id, predicate: PredicateType.GreaterThanOrEqualTo, threshold: 50, - }), + }, ], }) diff --git a/packages/core/tests/logger.ts b/packages/core/tests/logger.ts index 46c7066acc..cff2e39586 100644 --- a/packages/core/tests/logger.ts +++ b/packages/core/tests/logger.ts @@ -14,7 +14,7 @@ function logToTransport(logObject: ILogObject) { } export class TestLogger extends BaseLogger { - private logger: Logger + public readonly logger: Logger // Map our log levels to tslog levels private tsLogLevelMap = { @@ -27,29 +27,40 @@ export class TestLogger extends BaseLogger { [LogLevel.fatal]: 'fatal', } as const - public constructor(logLevel: LogLevel, name?: string) { + public static fromLogger(logger: TestLogger, name?: string) { + return new TestLogger(logger.logLevel, name, logger.logger) + } + + public constructor(logLevel: LogLevel, name?: string, logger?: Logger) { super(logLevel) - this.logger = new Logger({ - name, - minLevel: this.logLevel == LogLevel.off ? undefined : this.tsLogLevelMap[this.logLevel], - ignoreStackLevels: 5, - attachedTransports: [ - { - transportLogger: { - silly: logToTransport, - debug: logToTransport, - trace: logToTransport, - info: logToTransport, - warn: logToTransport, - error: logToTransport, - fatal: logToTransport, + if (logger) { + this.logger = logger.getChildLogger({ + name, + minLevel: this.logLevel == LogLevel.off ? undefined : this.tsLogLevelMap[this.logLevel], + }) + } else { + this.logger = new Logger({ + name, + minLevel: this.logLevel == LogLevel.off ? undefined : this.tsLogLevelMap[this.logLevel], + ignoreStackLevels: 5, + attachedTransports: [ + { + transportLogger: { + silly: logToTransport, + debug: logToTransport, + trace: logToTransport, + info: logToTransport, + warn: logToTransport, + error: logToTransport, + fatal: logToTransport, + }, + // always log to file + minLevel: 'silly', }, - // always log to file - minLevel: 'silly', - }, - ], - }) + ], + }) + } } private log(level: Exclude, message: string, data?: Record): void { @@ -93,6 +104,6 @@ export class TestLogger extends BaseLogger { } } -const testLogger = new TestLogger(LogLevel.error) +const testLogger = new TestLogger(LogLevel.off) export default testLogger diff --git a/packages/core/tests/oob.test.ts b/packages/core/tests/oob.test.ts index 4df883e972..94161fd838 100644 --- a/packages/core/tests/oob.test.ts +++ b/packages/core/tests/oob.test.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import type { SubjectMessage } from '../../../tests/transport/SubjectInboundTransport' -import type { CreateOfferOptions, DefaultCredentialProtocols } from '../src/modules/credentials' +import type { CreateCredentialOfferOptions, DefaultCredentialProtocols } from '../src/modules/credentials' import type { AgentMessage, AgentMessageReceivedEvent } from '@aries-framework/core' import { Subject } from 'rxjs' @@ -58,7 +58,7 @@ describe('out of band', () => { let faberAgent: Agent let aliceAgent: Agent - let credentialTemplate: CreateOfferOptions + let credentialTemplate: CreateCredentialOfferOptions beforeAll(async () => { const faberMessages = new Subject() diff --git a/packages/core/tests/proofs-sub-protocol.test.ts b/packages/core/tests/proofs-sub-protocol.test.ts index 271bd089c3..45fa30e713 100644 --- a/packages/core/tests/proofs-sub-protocol.test.ts +++ b/packages/core/tests/proofs-sub-protocol.test.ts @@ -1,5 +1,5 @@ import type { Agent, ConnectionRecord, ProofExchangeRecord } from '../src' -import type { PresentationPreview } from '../src/modules/proofs/protocol/v1/models/V1PresentationPreview' +import type { V1PresentationPreview } from '../src/modules/proofs/protocol/v1/models/V1PresentationPreview' import type { CredDefId } from 'indy-sdk' import { @@ -21,7 +21,7 @@ describe('Present Proof Subprotocol', () => { let faberConnection: ConnectionRecord let aliceConnection: ConnectionRecord let aliceProofExchangeRecord: ProofExchangeRecord - let presentationPreview: PresentationPreview + let presentationPreview: V1PresentationPreview beforeAll(async () => { testLogger.test('Initializing the agents') @@ -57,7 +57,6 @@ describe('Present Proof Subprotocol', () => { indy: { name: 'abc', version: '1.0', - nonce: '947121108704767252195126', attributes: presentationPreview.attributes, predicates: presentationPreview.predicates, }, @@ -87,11 +86,8 @@ describe('Present Proof Subprotocol', () => { // Alice retrieves the requested credentials and accepts the presentation request testLogger.test('Alice accepts presentation request from Faber') - const requestedCredentials = await aliceAgent.proofs.autoSelectCredentialsForProofRequest({ + const requestedCredentials = await aliceAgent.proofs.selectCredentialsForRequest({ proofRecordId: aliceProofExchangeRecord.id, - config: { - filterByPresentationPreview: true, - }, }) await aliceAgent.proofs.acceptRequest({ proofRecordId: aliceProofExchangeRecord.id, @@ -107,7 +103,7 @@ describe('Present Proof Subprotocol', () => { // Faber accepts the presentation provided by Alice testLogger.test('Faber accepts the presentation provided by Alice') - await faberAgent.proofs.acceptPresentation(faberProofExchangeRecord.id) + await faberAgent.proofs.acceptPresentation({ proofRecordId: faberProofExchangeRecord.id }) // Alice waits until she received a presentation acknowledgement testLogger.test('Alice waits until she receives a presentation acknowledgement') @@ -161,7 +157,6 @@ describe('Present Proof Subprotocol', () => { indy: { name: 'proof-request', version: '1.0', - nonce: '1298236324864', requestedAttributes: attributes, requestedPredicates: predicates, }, @@ -181,11 +176,8 @@ describe('Present Proof Subprotocol', () => { // Alice retrieves the requested credentials and accepts the presentation request testLogger.test('Alice accepts presentation request from Faber') - const requestedCredentials = await aliceAgent.proofs.autoSelectCredentialsForProofRequest({ + const requestedCredentials = await aliceAgent.proofs.selectCredentialsForRequest({ proofRecordId: aliceProofExchangeRecord.id, - config: { - filterByPresentationPreview: true, - }, }) await aliceAgent.proofs.acceptRequest({ proofRecordId: aliceProofExchangeRecord.id, @@ -202,7 +194,7 @@ describe('Present Proof Subprotocol', () => { // Faber accepts the presentation testLogger.test('Faber accept the presentation from Alice') - await faberAgent.proofs.acceptPresentation(faberProofExchangeRecord.id) + await faberAgent.proofs.acceptPresentation({ proofRecordId: faberProofExchangeRecord.id }) // Alice waits until she receives a presentation acknowledgement testLogger.test('Alice waits for acceptance by Faber') @@ -232,7 +224,6 @@ describe('Present Proof Subprotocol', () => { indy: { name: 'abc', version: '1.0', - nonce: '947121108704767252195126', attributes: presentationPreview.attributes, predicates: presentationPreview.predicates, }, @@ -262,11 +253,8 @@ describe('Present Proof Subprotocol', () => { // Alice retrieves the requested credentials and accepts the presentation request testLogger.test('Alice accepts presentation request from Faber') - const requestedCredentials = await aliceAgent.proofs.autoSelectCredentialsForProofRequest({ + const requestedCredentials = await aliceAgent.proofs.selectCredentialsForRequest({ proofRecordId: aliceProofExchangeRecord.id, - config: { - filterByPresentationPreview: true, - }, }) await aliceAgent.proofs.acceptRequest({ proofRecordId: aliceProofExchangeRecord.id, @@ -282,7 +270,7 @@ describe('Present Proof Subprotocol', () => { // Faber accepts the presentation provided by Alice testLogger.test('Faber accepts the presentation provided by Alice') - await faberAgent.proofs.acceptPresentation(faberProofExchangeRecord.id) + await faberAgent.proofs.acceptPresentation({ proofRecordId: faberProofExchangeRecord.id }) // Alice waits until she received a presentation acknowledgement testLogger.test('Alice waits until she receives a presentation acknowledgement') @@ -336,7 +324,6 @@ describe('Present Proof Subprotocol', () => { indy: { name: 'proof-request', version: '1.0', - nonce: '1298236324864', requestedAttributes: attributes, requestedPredicates: predicates, }, @@ -356,11 +343,8 @@ describe('Present Proof Subprotocol', () => { // Alice retrieves the requested credentials and accepts the presentation request testLogger.test('Alice accepts presentation request from Faber') - const requestedCredentials = await aliceAgent.proofs.autoSelectCredentialsForProofRequest({ + const requestedCredentials = await aliceAgent.proofs.selectCredentialsForRequest({ proofRecordId: aliceProofExchangeRecord.id, - config: { - filterByPresentationPreview: true, - }, }) await aliceAgent.proofs.acceptRequest({ proofRecordId: aliceProofExchangeRecord.id, @@ -377,7 +361,7 @@ describe('Present Proof Subprotocol', () => { // Faber accepts the presentation testLogger.test('Faber accept the presentation from Alice') - await faberAgent.proofs.acceptPresentation(faberProofExchangeRecord.id) + await faberAgent.proofs.acceptPresentation({ proofRecordId: faberProofExchangeRecord.id }) // Alice waits until she receives a presentation acknowledgement testLogger.test('Alice waits for acceptance by Faber') diff --git a/packages/openid4vc-client/tests/fixtures.ts b/packages/openid4vc-client/tests/fixtures.ts index 44936b9169..e739859415 100644 --- a/packages/openid4vc-client/tests/fixtures.ts +++ b/packages/openid4vc-client/tests/fixtures.ts @@ -64,7 +64,7 @@ export const getMetadataResponse = { }, } -export const aquireAccessTokenResponse = { +export const acquireAccessTokenResponse = { access_token: '7nikUotMQefxn7oRX56R7MDNE7KJTGfwGjOkHzGaUIG', expires_in: 3600, scope: 'OpenBadgeCredential', diff --git a/packages/openid4vc-client/tests/openid4vc-client.e2e.test.ts b/packages/openid4vc-client/tests/openid4vc-client.e2e.test.ts index fe2d5b1dbc..8b01c14c17 100644 --- a/packages/openid4vc-client/tests/openid4vc-client.e2e.test.ts +++ b/packages/openid4vc-client/tests/openid4vc-client.e2e.test.ts @@ -1,6 +1,6 @@ import type { KeyDidCreateOptions } from '@aries-framework/core' -import { Agent, KeyType, LogLevel, W3cCredentialRecord, W3cVcModule } from '@aries-framework/core' +import { Agent, KeyType, W3cCredentialRecord, W3cVcModule } from '@aries-framework/core' import nock, { cleanAll, enableNetConnect } from 'nock' import { didKeyToInstanceOfKey } from '../../core/src/modules/dids/helpers' @@ -9,9 +9,7 @@ import { getAgentOptions } from '../../core/tests/helpers' import { OpenId4VcClientModule } from '@aries-framework/openid4vc-client' -import { TestLogger } from '../../core/tests/logger' - -import { aquireAccessTokenResponse, credentialRequestResponse, getMetadataResponse } from './fixtures' +import { acquireAccessTokenResponse, credentialRequestResponse, getMetadataResponse } from './fixtures' describe('OpenId4VcClient', () => { let agent: Agent<{ @@ -22,9 +20,7 @@ describe('OpenId4VcClient', () => { beforeEach(async () => { const agentOptions = getAgentOptions( 'OpenId4VcClient Agent', - { - logger: new TestLogger(LogLevel.test), - }, + {}, { openId4VcClient: new OpenId4VcClientModule(), w3cVc: new W3cVcModule({ @@ -62,7 +58,7 @@ describe('OpenId4VcClient', () => { .reply(200, getMetadataResponse) // setup access token response - httpMock.post('/oidc/v1/auth/token').reply(200, aquireAccessTokenResponse) + httpMock.post('/oidc/v1/auth/token').reply(200, acquireAccessTokenResponse) // setup credential request response httpMock.post('/oidc/v1/auth/credential').reply(200, credentialRequestResponse) diff --git a/tests/e2e-test.ts b/tests/e2e-test.ts index 86fb6dfe83..0f4c07e6da 100644 --- a/tests/e2e-test.ts +++ b/tests/e2e-test.ts @@ -3,16 +3,7 @@ import type { Agent } from '@aries-framework/core' import { sleep } from '../packages/core/src/utils/sleep' import { issueCredential, makeConnection, prepareForIssuance, presentProof } from '../packages/core/tests/helpers' -import { - V1CredentialPreview, - AttributeFilter, - CredentialState, - MediationState, - PredicateType, - ProofAttributeInfo, - ProofPredicateInfo, - ProofState, -} from '@aries-framework/core' +import { V1CredentialPreview, CredentialState, MediationState, ProofState } from '@aries-framework/core' export async function e2eTest({ mediatorAgent, @@ -63,9 +54,9 @@ export async function e2eTest({ // Present Proof from recipient to sender const definitionRestriction = [ - new AttributeFilter({ + { credentialDefinitionId: definition.id, - }), + }, ] const { holderProof, verifierProof } = await presentProof({ verifierAgent: senderAgent, @@ -73,18 +64,18 @@ export async function e2eTest({ verifierConnectionId: senderRecipientConnection.id, presentationTemplate: { attributes: { - name: new ProofAttributeInfo({ + name: { name: 'name', restrictions: definitionRestriction, - }), + }, }, predicates: { - olderThan21: new ProofPredicateInfo({ + olderThan21: { name: 'age', restrictions: definitionRestriction, - predicateType: PredicateType.LessThan, + predicateType: '<=', predicateValue: 20000712, - }), + }, }, }, }) diff --git a/yarn.lock b/yarn.lock index 4a8447c5fe..63404b7c30 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6085,11 +6085,6 @@ humanize-ms@^1.2.1: dependencies: ms "^2.0.0" -husky@^7.0.1: - version "7.0.4" - resolved "https://registry.yarnpkg.com/husky/-/husky-7.0.4.tgz#242048245dc49c8fb1bf0cc7cfb98dd722531535" - integrity sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ== - iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"