From 250db507d709843e19cc7c7d3f7966bdd85e6171 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Wed, 16 Aug 2023 22:19:03 -0300 Subject: [PATCH 01/22] feat: Basic Messages V2 for DIDComm V2 Signed-off-by: Ariel Gentile --- demo/src/Listener.ts | 5 +- packages/core/src/agent/BaseAgent.ts | 8 +- .../core/src/agent/__tests__/Agent.test.ts | 6 +- .../versions/v2/DidCommV2BaseMessage.ts | 6 + .../basic-messages/BasicMessageEvents.ts | 4 +- .../basic-messages/BasicMessagesApi.ts | 73 ++++++++---- .../basic-messages/BasicMessagesModule.ts | 56 ++++++--- .../BasicMessagesModuleConfig.ts | 32 ++++++ .../__tests__/BasicMessageService.test.ts | 15 ++- .../__tests__/BasicMessagesModule.test.ts | 7 +- .../__tests__/basic-messages.e2e.test.ts | 10 +- .../handlers/BasicMessageHandler.ts | 18 --- .../modules/basic-messages/handlers/index.ts | 1 - .../core/src/modules/basic-messages/index.ts | 3 +- .../modules/basic-messages/messages/index.ts | 1 - .../protocols/BaseBasicMessageProtocol.ts | 56 +++++++++ .../protocols/BasicMessageProtocol.ts | 20 ++++ .../protocols/BasicMessageProtocolOptions.ts | 14 +++ .../modules/basic-messages/protocols/index.ts | 4 + .../protocols/v1/V1BasicMessageProtocol.ts | 99 ++++++++++++++++ .../v1/handlers/V1BasicMessageHandler.ts | 18 +++ .../protocols/v1/handlers/index.ts | 1 + .../basic-messages/protocols/v1/index.ts | 3 + .../v1/messages/V1BasicMessage.ts} | 12 +- .../protocols/v1/messages/index.ts | 1 + .../protocols/v2/V2BasicMessageProtocol.ts | 108 ++++++++++++++++++ .../v2/handlers/V2BasicMessageHandler.ts | 18 +++ .../protocols/v2/handlers/index.ts | 1 + .../basic-messages/protocols/v2/index.ts | 3 + .../protocols/v2/messages/V2BasicMessage.ts | 60 ++++++++++ .../protocols/v2/messages/index.ts | 1 + .../services/BasicMessageService.ts | 100 ---------------- .../modules/basic-messages/services/index.ts | 1 - packages/core/tests/helpers.ts | 16 ++- 34 files changed, 586 insertions(+), 195 deletions(-) create mode 100644 packages/core/src/modules/basic-messages/BasicMessagesModuleConfig.ts delete mode 100644 packages/core/src/modules/basic-messages/handlers/BasicMessageHandler.ts delete mode 100644 packages/core/src/modules/basic-messages/handlers/index.ts delete mode 100644 packages/core/src/modules/basic-messages/messages/index.ts create mode 100644 packages/core/src/modules/basic-messages/protocols/BaseBasicMessageProtocol.ts create mode 100644 packages/core/src/modules/basic-messages/protocols/BasicMessageProtocol.ts create mode 100644 packages/core/src/modules/basic-messages/protocols/BasicMessageProtocolOptions.ts create mode 100644 packages/core/src/modules/basic-messages/protocols/index.ts create mode 100644 packages/core/src/modules/basic-messages/protocols/v1/V1BasicMessageProtocol.ts create mode 100644 packages/core/src/modules/basic-messages/protocols/v1/handlers/V1BasicMessageHandler.ts create mode 100644 packages/core/src/modules/basic-messages/protocols/v1/handlers/index.ts create mode 100644 packages/core/src/modules/basic-messages/protocols/v1/index.ts rename packages/core/src/modules/basic-messages/{messages/BasicMessage.ts => protocols/v1/messages/V1BasicMessage.ts} (71%) create mode 100644 packages/core/src/modules/basic-messages/protocols/v1/messages/index.ts create mode 100644 packages/core/src/modules/basic-messages/protocols/v2/V2BasicMessageProtocol.ts create mode 100644 packages/core/src/modules/basic-messages/protocols/v2/handlers/V2BasicMessageHandler.ts create mode 100644 packages/core/src/modules/basic-messages/protocols/v2/handlers/index.ts create mode 100644 packages/core/src/modules/basic-messages/protocols/v2/index.ts create mode 100644 packages/core/src/modules/basic-messages/protocols/v2/messages/V2BasicMessage.ts create mode 100644 packages/core/src/modules/basic-messages/protocols/v2/messages/index.ts delete mode 100644 packages/core/src/modules/basic-messages/services/BasicMessageService.ts delete mode 100644 packages/core/src/modules/basic-messages/services/index.ts diff --git a/demo/src/Listener.ts b/demo/src/Listener.ts index 4a4c7e383a..dad670a652 100644 --- a/demo/src/Listener.ts +++ b/demo/src/Listener.ts @@ -24,6 +24,7 @@ import { ProofEventTypes, ProofState, TrustPingEventTypes, + V1BasicMessage, } from '@aries-framework/core' import { ui } from 'inquirer' @@ -78,7 +79,9 @@ export class Listener { public messageListener(agent: Agent, name: string) { agent.events.on(BasicMessageEventTypes.BasicMessageStateChanged, async (event: BasicMessageStateChangedEvent) => { if (event.payload.basicMessageRecord.role === BasicMessageRole.Receiver) { - this.ui.updateBottomBar(purpleText(`\n${name} received a message: ${event.payload.message.content}\n`)) + const message = event.payload.message + const content = message instanceof V1BasicMessage ? message.content : message.body.content + this.ui.updateBottomBar(purpleText(`\n${name} received a message: ${content}\n`)) } }) } diff --git a/packages/core/src/agent/BaseAgent.ts b/packages/core/src/agent/BaseAgent.ts index 39ea8d521d..bf57ad5fd6 100644 --- a/packages/core/src/agent/BaseAgent.ts +++ b/packages/core/src/agent/BaseAgent.ts @@ -2,6 +2,7 @@ import type { AgentConfig } from './AgentConfig' import type { AgentApi, CustomOrDefaultApi, EmptyModuleMap, ModulesMap, WithoutDefaultModules } from './AgentModules' import type { TransportSession } from './TransportService' import type { Logger } from '../logger' +import type { BasicMessagesModule } from '../modules/basic-messages' import type { CredentialsModule } from '../modules/credentials' import type { MessagePickupModule } from '../modules/message-pìckup' import type { ProofsModule } from '../modules/proofs' @@ -51,7 +52,7 @@ export abstract class BaseAgent - public readonly basicMessages: BasicMessagesApi + public readonly basicMessages: CustomOrDefaultApi public readonly genericRecords: GenericRecordsApi public readonly discovery: DiscoverFeaturesApi public readonly dids: DidsApi @@ -99,7 +100,10 @@ export abstract class BaseAgent - this.basicMessages = this.dependencyManager.resolve(BasicMessagesApi) + this.basicMessages = this.dependencyManager.resolve(BasicMessagesApi) as CustomOrDefaultApi< + AgentModules['basicMessages'], + BasicMessagesModule + > this.genericRecords = this.dependencyManager.resolve(GenericRecordsApi) this.discovery = this.dependencyManager.resolve(DiscoverFeaturesApi) this.dids = this.dependencyManager.resolve(DidsApi) diff --git a/packages/core/src/agent/__tests__/Agent.test.ts b/packages/core/src/agent/__tests__/Agent.test.ts index ed3154f8fd..65729dd6c5 100644 --- a/packages/core/src/agent/__tests__/Agent.test.ts +++ b/packages/core/src/agent/__tests__/Agent.test.ts @@ -5,7 +5,7 @@ import { injectable } from 'tsyringe' import { getIndySdkModules } from '../../../../indy-sdk/tests/setupIndySdkModule' import { getAgentOptions } from '../../../tests/helpers' import { InjectionSymbols } from '../../constants' -import { BasicMessageRepository, BasicMessageService } from '../../modules/basic-messages' +import { BasicMessageRepository, V1BasicMessageProtocol } from '../../modules/basic-messages' import { BasicMessagesApi } from '../../modules/basic-messages/BasicMessagesApi' import { ConnectionsApi } from '../../modules/connections/ConnectionsApi' import { V1TrustPingService } from '../../modules/connections/protocols/trust-ping/v1/V1TrustPingService' @@ -167,7 +167,7 @@ describe('Agent', () => { expect(container.resolve(CredentialRepository)).toBeInstanceOf(CredentialRepository) expect(container.resolve(BasicMessagesApi)).toBeInstanceOf(BasicMessagesApi) - expect(container.resolve(BasicMessageService)).toBeInstanceOf(BasicMessageService) + expect(container.resolve(V1BasicMessageProtocol)).toBeInstanceOf(V1BasicMessageProtocol) expect(container.resolve(BasicMessageRepository)).toBeInstanceOf(BasicMessageRepository) expect(container.resolve(MediatorApi)).toBeInstanceOf(MediatorApi) @@ -205,7 +205,7 @@ describe('Agent', () => { expect(container.resolve(CredentialRepository)).toBe(container.resolve(CredentialRepository)) expect(container.resolve(BasicMessagesApi)).toBe(container.resolve(BasicMessagesApi)) - expect(container.resolve(BasicMessageService)).toBe(container.resolve(BasicMessageService)) + expect(container.resolve(V1BasicMessageProtocol)).toBe(container.resolve(V1BasicMessageProtocol)) expect(container.resolve(BasicMessageRepository)).toBe(container.resolve(BasicMessageRepository)) expect(container.resolve(MediatorApi)).toBe(container.resolve(MediatorApi)) diff --git a/packages/core/src/didcomm/versions/v2/DidCommV2BaseMessage.ts b/packages/core/src/didcomm/versions/v2/DidCommV2BaseMessage.ts index 4e17749e91..dd67574eac 100644 --- a/packages/core/src/didcomm/versions/v2/DidCommV2BaseMessage.ts +++ b/packages/core/src/didcomm/versions/v2/DidCommV2BaseMessage.ts @@ -20,6 +20,7 @@ export type DidCommV2MessageParams = { createdTime?: number expiresTime?: number fromPrior?: string + language?: string attachments?: Array body?: unknown } @@ -68,6 +69,11 @@ export class DidCommV2BaseMessage { @IsOptional() public fromPrior?: string + @Expose({ name: 'lang' }) + @IsString() + @IsOptional() + public language?: string + public body!: unknown @IsOptional() diff --git a/packages/core/src/modules/basic-messages/BasicMessageEvents.ts b/packages/core/src/modules/basic-messages/BasicMessageEvents.ts index f05873f5de..745decb916 100644 --- a/packages/core/src/modules/basic-messages/BasicMessageEvents.ts +++ b/packages/core/src/modules/basic-messages/BasicMessageEvents.ts @@ -1,4 +1,4 @@ -import type { BasicMessage } from './messages' +import type { V1BasicMessage, V2BasicMessage } from './protocols' import type { BasicMessageRecord } from './repository' import type { BaseEvent } from '../../agent/Events' @@ -8,7 +8,7 @@ export enum BasicMessageEventTypes { export interface BasicMessageStateChangedEvent extends BaseEvent { type: typeof BasicMessageEventTypes.BasicMessageStateChanged payload: { - message: BasicMessage + message: V1BasicMessage | V2BasicMessage basicMessageRecord: BasicMessageRecord } } diff --git a/packages/core/src/modules/basic-messages/BasicMessagesApi.ts b/packages/core/src/modules/basic-messages/BasicMessagesApi.ts index 82e94cf9e4..d50da6da62 100644 --- a/packages/core/src/modules/basic-messages/BasicMessagesApi.ts +++ b/packages/core/src/modules/basic-messages/BasicMessagesApi.ts @@ -1,35 +1,59 @@ +import type { BasicMessageProtocol, V2BasicMessage } from './protocols' import type { BasicMessageRecord } from './repository/BasicMessageRecord' import type { Query } from '../../storage/StorageService' import { AgentContext } from '../../agent' -import { MessageHandlerRegistry } from '../../agent/MessageHandlerRegistry' import { MessageSender } from '../../agent/MessageSender' import { OutboundMessageContext } from '../../agent/models' +import { AriesFrameworkError } from '../../error' import { injectable } from '../../plugins' import { ConnectionService } from '../connections' -import { BasicMessageHandler } from './handlers' -import { BasicMessageService } from './services' +import { BasicMessagesModuleConfig } from './BasicMessagesModuleConfig' +import { BasicMessageRepository } from './repository' + +export interface BasicMessagesApi { + sendMessage(connectionId: string, message: string, parentThreadId?: string): Promise + + findAllByQuery(query: Query): Promise + getById(basicMessageRecordId: string): Promise + getByThreadId(threadId: string): Promise + deleteById(basicMessageRecordId: string): Promise +} @injectable() -export class BasicMessagesApi { - private basicMessageService: BasicMessageService +export class BasicMessagesApi implements BasicMessagesApi { + public readonly config: BasicMessagesModuleConfig + + private basicMessageRepository: BasicMessageRepository private messageSender: MessageSender private connectionService: ConnectionService private agentContext: AgentContext public constructor( - messageHandlerRegistry: MessageHandlerRegistry, - basicMessageService: BasicMessageService, + basicMessageRepository: BasicMessageRepository, messageSender: MessageSender, connectionService: ConnectionService, - agentContext: AgentContext + agentContext: AgentContext, + config: BasicMessagesModuleConfig ) { - this.basicMessageService = basicMessageService + this.basicMessageRepository = basicMessageRepository this.messageSender = messageSender this.connectionService = connectionService this.agentContext = agentContext - this.registerMessageHandlers(messageHandlerRegistry) + this.config = config + } + + private getProtocol(protocolVersion: PVT): BasicMessageProtocol { + const basicMessageProtocol = this.config.basicMessageProtocols.find( + (protocol) => protocol.version === protocolVersion + ) + + if (!basicMessageProtocol) { + throw new AriesFrameworkError(`No basic message protocol registered for protocol version ${protocolVersion}`) + } + + return basicMessageProtocol } /** @@ -44,13 +68,18 @@ export class BasicMessagesApi { public async sendMessage(connectionId: string, message: string, parentThreadId?: string) { const connection = await this.connectionService.getById(this.agentContext, connectionId) - const { message: basicMessage, record: basicMessageRecord } = await this.basicMessageService.createMessage( + // TODO: Parameterize in API + const basicMessageProtocol = this.getProtocol(connection.isDidCommV1Connection ? 'v1' : 'v2') + + const { message: basicMessage, record: basicMessageRecord } = await basicMessageProtocol.createMessage( this.agentContext, - message, - connection, - parentThreadId + { + content: message, + connectionRecord: connection, + parentThreadId, + } ) - const outboundMessageContext = new OutboundMessageContext(basicMessage, { + const outboundMessageContext = new OutboundMessageContext(basicMessage as V2BasicMessage, { agentContext: this.agentContext, connection, associatedRecord: basicMessageRecord, @@ -67,7 +96,7 @@ export class BasicMessagesApi { * @returns array containing all matching records */ public async findAllByQuery(query: Query) { - return this.basicMessageService.findAllByQuery(this.agentContext, query) + return this.basicMessageRepository.findByQuery(this.agentContext, query) } /** @@ -79,7 +108,7 @@ export class BasicMessagesApi { * */ public async getById(basicMessageRecordId: string) { - return this.basicMessageService.getById(this.agentContext, basicMessageRecordId) + return this.basicMessageRepository.getById(this.agentContext, basicMessageRecordId) } /** @@ -90,8 +119,8 @@ export class BasicMessagesApi { * @throws {RecordDuplicateError} If multiple records are found * @returns The connection record */ - public async getByThreadId(basicMessageRecordId: string) { - return this.basicMessageService.getByThreadId(this.agentContext, basicMessageRecordId) + public async getByThreadId(threadId: string) { + return this.basicMessageRepository.getSingleByQuery(this.agentContext, { threadId }) } /** @@ -101,10 +130,6 @@ export class BasicMessagesApi { * @throws {RecordNotFoundError} If no record is found */ public async deleteById(basicMessageRecordId: string) { - await this.basicMessageService.deleteById(this.agentContext, basicMessageRecordId) - } - - private registerMessageHandlers(messageHandlerRegistry: MessageHandlerRegistry) { - messageHandlerRegistry.registerMessageHandler(new BasicMessageHandler(this.basicMessageService)) + await this.basicMessageRepository.deleteById(this.agentContext, basicMessageRecordId) } } diff --git a/packages/core/src/modules/basic-messages/BasicMessagesModule.ts b/packages/core/src/modules/basic-messages/BasicMessagesModule.ts index fd1fd77f6c..95a9bb1210 100644 --- a/packages/core/src/modules/basic-messages/BasicMessagesModule.ts +++ b/packages/core/src/modules/basic-messages/BasicMessagesModule.ts @@ -1,15 +1,42 @@ +import type { BasicMessagesModuleConfigOptions } from './BasicMessagesModuleConfig' +import type { BasicMessageProtocol } from './protocols/BasicMessageProtocol' import type { FeatureRegistry } from '../../agent/FeatureRegistry' -import type { DependencyManager, Module } from '../../plugins' +import type { ApiModule, DependencyManager } from '../../plugins' +import type { Optional } from '../../utils' +import type { Constructor } from '../../utils/mixins' -import { Protocol } from '../../agent/models' - -import { BasicMessageRole } from './BasicMessageRole' import { BasicMessagesApi } from './BasicMessagesApi' +import { BasicMessagesModuleConfig } from './BasicMessagesModuleConfig' +import { V1BasicMessageProtocol, V2BasicMessageProtocol } from './protocols' import { BasicMessageRepository } from './repository' -import { BasicMessageService } from './services' -export class BasicMessagesModule implements Module { - public readonly api = BasicMessagesApi +/** + * Default basicMessageProtocols that will be registered if the `basicMessageProtocols` property is not configured. + */ +export type DefaultBasicMessageProtocols = [] + +// BasicMessagesModuleOptions makes the credentialProtocols property optional from the config, as it will set it when not provided. +export type BasicMessagesModuleOptions = Optional< + BasicMessagesModuleConfigOptions, + 'basicMessageProtocols' +> + +export class BasicMessagesModule + implements ApiModule +{ + public readonly config: BasicMessagesModuleConfig + + public readonly api: Constructor> = BasicMessagesApi + + public constructor(config?: BasicMessagesModuleConfig) { + this.config = new BasicMessagesModuleConfig({ + ...config, + basicMessageProtocols: config?.basicMessageProtocols ?? [ + new V1BasicMessageProtocol(), + new V2BasicMessageProtocol(), + ], + } as BasicMessagesModuleConfig) + } /** * Registers the dependencies of the basic message module on the dependency manager. @@ -18,18 +45,15 @@ export class BasicMessagesModule implements Module { // Api dependencyManager.registerContextScoped(BasicMessagesApi) - // Services - dependencyManager.registerSingleton(BasicMessageService) + // Config + dependencyManager.registerInstance(BasicMessagesModuleConfig, this.config) // Repositories dependencyManager.registerSingleton(BasicMessageRepository) - // Features - featureRegistry.register( - new Protocol({ - id: 'https://didcomm.org/basicmessage/1.0', - roles: [BasicMessageRole.Sender, BasicMessageRole.Receiver], - }) - ) + // Protocol needs to register feature registry items and handlers + for (const basicMessageProtocols of this.config.basicMessageProtocols) { + basicMessageProtocols.register(dependencyManager, featureRegistry) + } } } diff --git a/packages/core/src/modules/basic-messages/BasicMessagesModuleConfig.ts b/packages/core/src/modules/basic-messages/BasicMessagesModuleConfig.ts new file mode 100644 index 0000000000..d87f2d9854 --- /dev/null +++ b/packages/core/src/modules/basic-messages/BasicMessagesModuleConfig.ts @@ -0,0 +1,32 @@ +import type { BasicMessageProtocol } from './protocols/BasicMessageProtocol' + +/** + * CredentialsModuleConfigOptions defines the interface for the options of the CredentialsModuleConfig class. + * This can contain optional parameters that have default values in the config class itself. + */ +export interface BasicMessagesModuleConfigOptions { + /** + * Protocols to make available to the module. + * + * When not provided, both `V1BasicMessageProtocol` and `V2BasicMessageProtocol` are registered by default. + * + * @default + * ``` + * [V1BasicMessageProtocol, V2BasicMessageProtocol] + * ``` + */ + basicMessageProtocols: BasicMessageProtocols +} + +export class BasicMessagesModuleConfig { + private options: BasicMessagesModuleConfigOptions + + public constructor(options: BasicMessagesModuleConfigOptions) { + this.options = options + } + + /** See {@link BasicMessagesModuleConfigOptions.basicMessageProtocols} */ + public get basicMessageProtocols() { + return this.options.basicMessageProtocols + } +} diff --git a/packages/core/src/modules/basic-messages/__tests__/BasicMessageService.test.ts b/packages/core/src/modules/basic-messages/__tests__/BasicMessageService.test.ts index 83dd0c4c01..bd3783c14a 100644 --- a/packages/core/src/modules/basic-messages/__tests__/BasicMessageService.test.ts +++ b/packages/core/src/modules/basic-messages/__tests__/BasicMessageService.test.ts @@ -2,10 +2,10 @@ import { getAgentContext, getMockConnection } from '../../../../tests/helpers' import { EventEmitter } from '../../../agent/EventEmitter' import { InboundMessageContext } from '../../../agent/models/InboundMessageContext' import { BasicMessageRole } from '../BasicMessageRole' -import { BasicMessage } from '../messages' +import { V1BasicMessageProtocol } from '../protocols' +import { V1BasicMessage } from '../protocols/v1/messages' import { BasicMessageRecord } from '../repository/BasicMessageRecord' import { BasicMessageRepository } from '../repository/BasicMessageRepository' -import { BasicMessageService } from '../services' jest.mock('../repository/BasicMessageRepository') const BasicMessageRepositoryMock = BasicMessageRepository as jest.Mock @@ -18,19 +18,22 @@ const eventEmitter = new EventEmitterMock() const agentContext = getAgentContext() describe('BasicMessageService', () => { - let basicMessageService: BasicMessageService + let basicMessageService: V1BasicMessageProtocol const mockConnectionRecord = getMockConnection({ id: 'd3849ac3-c981-455b-a1aa-a10bea6cead8', did: 'did:sov:C2SsBf5QUQpqSAQfhu3sd2', }) beforeEach(() => { - basicMessageService = new BasicMessageService(basicMessageRepository, eventEmitter) + basicMessageService = new V1BasicMessageProtocol() }) describe('createMessage', () => { it(`creates message and record, and emits message and basic message record`, async () => { - const { message } = await basicMessageService.createMessage(agentContext, 'hello', mockConnectionRecord) + const { message } = await basicMessageService.createMessage(agentContext, { + content: 'hello', + connectionRecord: mockConnectionRecord, + }) expect(message.content).toBe('hello') @@ -53,7 +56,7 @@ describe('BasicMessageService', () => { describe('save', () => { it(`stores record and emits message and basic message record`, async () => { - const basicMessage = new BasicMessage({ + const basicMessage = new V1BasicMessage({ id: '123', content: 'message', }) diff --git a/packages/core/src/modules/basic-messages/__tests__/BasicMessagesModule.test.ts b/packages/core/src/modules/basic-messages/__tests__/BasicMessagesModule.test.ts index 4a9f106810..b0643bf3cd 100644 --- a/packages/core/src/modules/basic-messages/__tests__/BasicMessagesModule.test.ts +++ b/packages/core/src/modules/basic-messages/__tests__/BasicMessagesModule.test.ts @@ -2,8 +2,8 @@ import { FeatureRegistry } from '../../../agent/FeatureRegistry' import { DependencyManager } from '../../../plugins/DependencyManager' import { BasicMessagesApi } from '../BasicMessagesApi' import { BasicMessagesModule } from '../BasicMessagesModule' +import { V1BasicMessageProtocol, V2BasicMessageProtocol } from '../protocols' import { BasicMessageRepository } from '../repository' -import { BasicMessageService } from '../services' jest.mock('../../../plugins/DependencyManager') const DependencyManagerMock = DependencyManager as jest.Mock @@ -22,8 +22,9 @@ describe('BasicMessagesModule', () => { expect(dependencyManager.registerContextScoped).toHaveBeenCalledTimes(1) expect(dependencyManager.registerContextScoped).toHaveBeenCalledWith(BasicMessagesApi) - expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(2) - expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(BasicMessageService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(3) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(V1BasicMessageProtocol) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(V2BasicMessageProtocol) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(BasicMessageRepository) }) }) diff --git a/packages/core/src/modules/basic-messages/__tests__/basic-messages.e2e.test.ts b/packages/core/src/modules/basic-messages/__tests__/basic-messages.e2e.test.ts index a18aa98754..4afb412f78 100644 --- a/packages/core/src/modules/basic-messages/__tests__/basic-messages.e2e.test.ts +++ b/packages/core/src/modules/basic-messages/__tests__/basic-messages.e2e.test.ts @@ -11,7 +11,7 @@ import { getAgentOptions, makeConnection, waitForBasicMessage } from '../../../. import testLogger from '../../../../tests/logger' import { Agent } from '../../../agent/Agent' import { MessageSendingError, RecordNotFoundError } from '../../../error' -import { BasicMessage } from '../messages' +import { V1BasicMessage } from '../protocols' import { BasicMessageRecord } from '../repository' const faberConfig = getAgentOptions( @@ -101,9 +101,9 @@ describe('Basic Messages E2E', () => { expect(replyRecord.parentThreadId).toBe(helloMessage.id) testLogger.test('Alice waits until she receives message from faber') - const replyMessage = await waitForBasicMessage(aliceAgent, { + const replyMessage = (await waitForBasicMessage(aliceAgent, { content: 'How are you?', - }) + })) as V1BasicMessage expect(replyMessage.content).toBe('How are you?') expect(replyMessage.thread?.parentThreadId).toBe(helloMessage.id) @@ -151,8 +151,8 @@ describe('Basic Messages E2E', () => { testLogger.test('Error thrown includes the outbound message and recently created record id') expect(thrownError.outboundMessageContext.associatedRecord).toBeInstanceOf(BasicMessageRecord) - expect(thrownError.outboundMessageContext.message).toBeInstanceOf(BasicMessage) - expect((thrownError.outboundMessageContext.message as BasicMessage).content).toBe('Hello undeliverable') + expect(thrownError.outboundMessageContext.message).toBeInstanceOf(V1BasicMessage) + expect((thrownError.outboundMessageContext.message as V1BasicMessage).content).toBe('Hello undeliverable') testLogger.test('Created record can be found and deleted by id') const storedRecord = await aliceAgent.basicMessages.getById( diff --git a/packages/core/src/modules/basic-messages/handlers/BasicMessageHandler.ts b/packages/core/src/modules/basic-messages/handlers/BasicMessageHandler.ts deleted file mode 100644 index cec6931983..0000000000 --- a/packages/core/src/modules/basic-messages/handlers/BasicMessageHandler.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { MessageHandler, MessageHandlerInboundMessage } from '../../../agent/MessageHandler' -import type { BasicMessageService } from '../services/BasicMessageService' - -import { BasicMessage } from '../messages' - -export class BasicMessageHandler implements MessageHandler { - private basicMessageService: BasicMessageService - public supportedMessages = [BasicMessage] - - public constructor(basicMessageService: BasicMessageService) { - this.basicMessageService = basicMessageService - } - - public async handle(messageContext: MessageHandlerInboundMessage) { - const connection = messageContext.assertReadyConnection() - await this.basicMessageService.save(messageContext, connection) - } -} diff --git a/packages/core/src/modules/basic-messages/handlers/index.ts b/packages/core/src/modules/basic-messages/handlers/index.ts deleted file mode 100644 index 64f421dd88..0000000000 --- a/packages/core/src/modules/basic-messages/handlers/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './BasicMessageHandler' diff --git a/packages/core/src/modules/basic-messages/index.ts b/packages/core/src/modules/basic-messages/index.ts index e0ca5207d1..70590c4eb1 100644 --- a/packages/core/src/modules/basic-messages/index.ts +++ b/packages/core/src/modules/basic-messages/index.ts @@ -1,5 +1,4 @@ -export * from './messages' -export * from './services' +export * from './protocols' export * from './repository' export * from './BasicMessageEvents' export * from './BasicMessagesApi' diff --git a/packages/core/src/modules/basic-messages/messages/index.ts b/packages/core/src/modules/basic-messages/messages/index.ts deleted file mode 100644 index 40d57b1840..0000000000 --- a/packages/core/src/modules/basic-messages/messages/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './BasicMessage' diff --git a/packages/core/src/modules/basic-messages/protocols/BaseBasicMessageProtocol.ts b/packages/core/src/modules/basic-messages/protocols/BaseBasicMessageProtocol.ts new file mode 100644 index 0000000000..5ddbc08d08 --- /dev/null +++ b/packages/core/src/modules/basic-messages/protocols/BaseBasicMessageProtocol.ts @@ -0,0 +1,56 @@ +import type { BasicMessageProtocol } from './BasicMessageProtocol' +import type { BasicMessageProtocolMsgReturnType, CreateMessageOptions } from './BasicMessageProtocolOptions' +import type { AgentContext } from '../../../agent' +import type { AgentBaseMessage } from '../../../agent/AgentBaseMessage' +import type { FeatureRegistry } from '../../../agent/FeatureRegistry' +import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext' +import type { DependencyManager } from '../../../plugins' +import type { Query } from '../../../storage/StorageService' +import type { ConnectionRecord } from '../../connections/repository/ConnectionRecord' +import type { BasicMessageRecord } from '../repository' + +import { BasicMessageRepository } from '../repository' + +export abstract class BaseBasicMessageProtocol implements BasicMessageProtocol { + public abstract readonly version: string + + public abstract createMessage( + agentContext: AgentContext, + options: CreateMessageOptions + ): Promise> + + /** + * @todo use connection from message context + */ + public abstract save( + { message, agentContext }: InboundMessageContext, + connection: ConnectionRecord + ): Promise + + public async findAllByQuery(agentContext: AgentContext, query: Query) { + const basicMessageRepository = agentContext.dependencyManager.resolve(BasicMessageRepository) + return basicMessageRepository.findByQuery(agentContext, query) + } + + public async getById(agentContext: AgentContext, basicMessageRecordId: string) { + const basicMessageRepository = agentContext.dependencyManager.resolve(BasicMessageRepository) + return basicMessageRepository.getById(agentContext, basicMessageRecordId) + } + + public async getByThreadId(agentContext: AgentContext, threadId: string) { + const basicMessageRepository = agentContext.dependencyManager.resolve(BasicMessageRepository) + return basicMessageRepository.getSingleByQuery(agentContext, { threadId }) + } + + public async findAllByParentThreadId(agentContext: AgentContext, parentThreadId: string) { + const basicMessageRepository = agentContext.dependencyManager.resolve(BasicMessageRepository) + return basicMessageRepository.findByQuery(agentContext, { parentThreadId }) + } + + public async deleteById(agentContext: AgentContext, basicMessageRecordId: string) { + const basicMessageRepository = agentContext.dependencyManager.resolve(BasicMessageRepository) + return basicMessageRepository.deleteById(agentContext, basicMessageRecordId) + } + + public abstract register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry): void +} diff --git a/packages/core/src/modules/basic-messages/protocols/BasicMessageProtocol.ts b/packages/core/src/modules/basic-messages/protocols/BasicMessageProtocol.ts new file mode 100644 index 0000000000..d34024e7b0 --- /dev/null +++ b/packages/core/src/modules/basic-messages/protocols/BasicMessageProtocol.ts @@ -0,0 +1,20 @@ +import type { BasicMessageProtocolMsgReturnType, CreateMessageOptions } from './BasicMessageProtocolOptions' +import type { AgentContext } from '../../../agent' +import type { AgentBaseMessage } from '../../../agent/AgentBaseMessage' +import type { FeatureRegistry } from '../../../agent/FeatureRegistry' +import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext' +import type { DependencyManager } from '../../../plugins' +import type { ConnectionRecord } from '../../connections/repository/ConnectionRecord' + +export interface BasicMessageProtocol { + readonly version: string + + createMessage( + agentContext: AgentContext, + options: CreateMessageOptions + ): Promise> + + save({ message, agentContext }: InboundMessageContext, connection: ConnectionRecord): Promise + + register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry): void +} diff --git a/packages/core/src/modules/basic-messages/protocols/BasicMessageProtocolOptions.ts b/packages/core/src/modules/basic-messages/protocols/BasicMessageProtocolOptions.ts new file mode 100644 index 0000000000..b71d942346 --- /dev/null +++ b/packages/core/src/modules/basic-messages/protocols/BasicMessageProtocolOptions.ts @@ -0,0 +1,14 @@ +import type { AgentBaseMessage } from '../../../agent/AgentBaseMessage' +import type { ConnectionRecord } from '../../connections' +import type { BasicMessageRecord } from '../repository' + +export interface CreateMessageOptions { + connectionRecord: ConnectionRecord + content: string + parentThreadId?: string +} + +export interface BasicMessageProtocolMsgReturnType { + message: MessageType + record: BasicMessageRecord +} diff --git a/packages/core/src/modules/basic-messages/protocols/index.ts b/packages/core/src/modules/basic-messages/protocols/index.ts new file mode 100644 index 0000000000..4806c2a7b2 --- /dev/null +++ b/packages/core/src/modules/basic-messages/protocols/index.ts @@ -0,0 +1,4 @@ +export * from './v1' +export * from './v2' +export * from './BasicMessageProtocol' +export * from './BasicMessageProtocolOptions' diff --git a/packages/core/src/modules/basic-messages/protocols/v1/V1BasicMessageProtocol.ts b/packages/core/src/modules/basic-messages/protocols/v1/V1BasicMessageProtocol.ts new file mode 100644 index 0000000000..8ad13e3bfa --- /dev/null +++ b/packages/core/src/modules/basic-messages/protocols/v1/V1BasicMessageProtocol.ts @@ -0,0 +1,99 @@ +import type { AgentContext } from '../../../../agent' +import type { FeatureRegistry } from '../../../../agent/FeatureRegistry' +import type { InboundMessageContext } from '../../../../agent/models/InboundMessageContext' +import type { DependencyManager } from '../../../../plugins' +import type { ConnectionRecord } from '../../../connections/repository/ConnectionRecord' +import type { BasicMessageStateChangedEvent } from '../../BasicMessageEvents' +import type { CreateMessageOptions } from '../BasicMessageProtocolOptions' + +import { EventEmitter } from '../../../../agent/EventEmitter' +import { Protocol } from '../../../../agent/models' +import { injectable } from '../../../../plugins' +import { BasicMessageEventTypes } from '../../BasicMessageEvents' +import { BasicMessageRole } from '../../BasicMessageRole' +import { BasicMessageRecord, BasicMessageRepository } from '../../repository' +import { BaseBasicMessageProtocol } from '../BaseBasicMessageProtocol' + +import { V1BasicMessageHandler } from './handlers' +import { V1BasicMessage } from './messages' + +@injectable() +export class V1BasicMessageProtocol extends BaseBasicMessageProtocol { + /** + * The version of Basic Messages this class 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 Basic Message V1 Protocol + dependencyManager.registerMessageHandlers([new V1BasicMessageHandler(this)]) + + // Register in feature registry, with supported roles + featureRegistry.register( + new Protocol({ + id: 'https://didcomm.org/basicmessage/1.0', + roles: [BasicMessageRole.Sender, BasicMessageRole.Receiver], + }) + ) + } + + public async createMessage(agentContext: AgentContext, options: CreateMessageOptions) { + const { content, parentThreadId, connectionRecord } = options + const basicMessage = new V1BasicMessage({ content }) + + const basicMessageRepository = agentContext.dependencyManager.resolve(BasicMessageRepository) + + // If no parentThreadid is defined, there is no need to explicitly send a thread decorator + if (parentThreadId) { + basicMessage.setThread({ parentThreadId }) + } + + const basicMessageRecord = new BasicMessageRecord({ + sentTime: basicMessage.sentTime.toISOString(), + content: basicMessage.content, + connectionId: connectionRecord.id, + role: BasicMessageRole.Sender, + threadId: basicMessage.threadId, + parentThreadId, + }) + + await basicMessageRepository.save(agentContext, basicMessageRecord) + this.emitStateChangedEvent(agentContext, basicMessageRecord, basicMessage) + + return { message: basicMessage, record: basicMessageRecord } + } + + /** + * @todo use connection from message context + */ + public async save({ message, agentContext }: InboundMessageContext, connection: ConnectionRecord) { + const basicMessageRepository = agentContext.dependencyManager.resolve(BasicMessageRepository) + + const basicMessageRecord = new BasicMessageRecord({ + sentTime: message.sentTime.toISOString(), + content: message.content, + connectionId: connection.id, + role: BasicMessageRole.Receiver, + threadId: message.threadId, + parentThreadId: message.thread?.parentThreadId, + }) + + await basicMessageRepository.save(agentContext, basicMessageRecord) + this.emitStateChangedEvent(agentContext, basicMessageRecord, message) + } + + protected emitStateChangedEvent( + agentContext: AgentContext, + basicMessageRecord: BasicMessageRecord, + basicMessage: V1BasicMessage + ) { + const eventEmitter = agentContext.dependencyManager.resolve(EventEmitter) + eventEmitter.emit(agentContext, { + type: BasicMessageEventTypes.BasicMessageStateChanged, + payload: { message: basicMessage, basicMessageRecord: basicMessageRecord.clone() }, + }) + } +} diff --git a/packages/core/src/modules/basic-messages/protocols/v1/handlers/V1BasicMessageHandler.ts b/packages/core/src/modules/basic-messages/protocols/v1/handlers/V1BasicMessageHandler.ts new file mode 100644 index 0000000000..d91428c3ef --- /dev/null +++ b/packages/core/src/modules/basic-messages/protocols/v1/handlers/V1BasicMessageHandler.ts @@ -0,0 +1,18 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' +import type { V1BasicMessageProtocol } from '../V1BasicMessageProtocol' + +import { V1BasicMessage } from '../messages' + +export class V1BasicMessageHandler implements MessageHandler { + private basicMessageProtocol: V1BasicMessageProtocol + public supportedMessages = [V1BasicMessage] + + public constructor(basicMessageProtocol: V1BasicMessageProtocol) { + this.basicMessageProtocol = basicMessageProtocol + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + const connection = messageContext.assertReadyConnection() + await this.basicMessageProtocol.save(messageContext, connection) + } +} diff --git a/packages/core/src/modules/basic-messages/protocols/v1/handlers/index.ts b/packages/core/src/modules/basic-messages/protocols/v1/handlers/index.ts new file mode 100644 index 0000000000..b7fa7fb3f2 --- /dev/null +++ b/packages/core/src/modules/basic-messages/protocols/v1/handlers/index.ts @@ -0,0 +1 @@ +export * from './V1BasicMessageHandler' diff --git a/packages/core/src/modules/basic-messages/protocols/v1/index.ts b/packages/core/src/modules/basic-messages/protocols/v1/index.ts new file mode 100644 index 0000000000..9e581eae0d --- /dev/null +++ b/packages/core/src/modules/basic-messages/protocols/v1/index.ts @@ -0,0 +1,3 @@ +export * from './V1BasicMessageProtocol' +export * from './handlers' +export * from './messages' diff --git a/packages/core/src/modules/basic-messages/messages/BasicMessage.ts b/packages/core/src/modules/basic-messages/protocols/v1/messages/V1BasicMessage.ts similarity index 71% rename from packages/core/src/modules/basic-messages/messages/BasicMessage.ts rename to packages/core/src/modules/basic-messages/protocols/v1/messages/V1BasicMessage.ts index 1b720dceec..cef62224a3 100644 --- a/packages/core/src/modules/basic-messages/messages/BasicMessage.ts +++ b/packages/core/src/modules/basic-messages/protocols/v1/messages/V1BasicMessage.ts @@ -1,11 +1,11 @@ import { Expose, Transform } from 'class-transformer' import { IsDate, IsString } from 'class-validator' -import { DidCommV1Message } from '../../../didcomm' -import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' -import { DateParser } from '../../../utils/transformers' +import { DidCommV1Message } from '../../../../../didcomm' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { DateParser } from '../../../../../utils/transformers' -export class BasicMessage extends DidCommV1Message { +export class V1BasicMessage extends DidCommV1Message { public readonly allowDidSovPrefix = true /** @@ -24,8 +24,8 @@ export class BasicMessage extends DidCommV1Message { } } - @IsValidMessageType(BasicMessage.type) - public readonly type = BasicMessage.type.messageTypeUri + @IsValidMessageType(V1BasicMessage.type) + public readonly type = V1BasicMessage.type.messageTypeUri public static readonly type = parseMessageType('https://didcomm.org/basicmessage/1.0/message') @Expose({ name: 'sent_time' }) diff --git a/packages/core/src/modules/basic-messages/protocols/v1/messages/index.ts b/packages/core/src/modules/basic-messages/protocols/v1/messages/index.ts new file mode 100644 index 0000000000..0e2b67bf45 --- /dev/null +++ b/packages/core/src/modules/basic-messages/protocols/v1/messages/index.ts @@ -0,0 +1 @@ +export * from './V1BasicMessage' diff --git a/packages/core/src/modules/basic-messages/protocols/v2/V2BasicMessageProtocol.ts b/packages/core/src/modules/basic-messages/protocols/v2/V2BasicMessageProtocol.ts new file mode 100644 index 0000000000..074a93dafe --- /dev/null +++ b/packages/core/src/modules/basic-messages/protocols/v2/V2BasicMessageProtocol.ts @@ -0,0 +1,108 @@ +import type { AgentContext } from '../../../../agent' +import type { FeatureRegistry } from '../../../../agent/FeatureRegistry' +import type { InboundMessageContext } from '../../../../agent/models/InboundMessageContext' +import type { DependencyManager } from '../../../../plugins' +import type { ConnectionRecord } from '../../../connections/repository/ConnectionRecord' +import type { BasicMessageStateChangedEvent } from '../../BasicMessageEvents' +import type { CreateMessageOptions } from '../BasicMessageProtocolOptions' + +import { EventEmitter } from '../../../../agent/EventEmitter' +import { Protocol } from '../../../../agent/models' +import { AriesFrameworkError } from '../../../../error' +import { injectable } from '../../../../plugins' +import { BasicMessageEventTypes } from '../../BasicMessageEvents' +import { BasicMessageRole } from '../../BasicMessageRole' +import { BasicMessageRecord, BasicMessageRepository } from '../../repository' +import { BaseBasicMessageProtocol } from '../BaseBasicMessageProtocol' + +import { V2BasicMessageHandler } from './handlers' +import { V2BasicMessage } from './messages' + +@injectable() +export class V2BasicMessageProtocol extends BaseBasicMessageProtocol { + /** + * The version of Basic Messages this class supports + */ + public readonly version = 'v2' as const + + /** + * Registers the protocol implementation (handlers, feature registry) on the agent. + */ + public register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry) { + // Register message handlers for Basic Message V2 Protocol + dependencyManager.registerMessageHandlers([new V2BasicMessageHandler(this)]) + + // Register in feature registry, with supported roles + featureRegistry.register( + new Protocol({ + id: 'https://didcomm.org/basicmessage/2.0', + roles: [BasicMessageRole.Sender, BasicMessageRole.Receiver], + }) + ) + } + + public async createMessage(agentContext: AgentContext, options: CreateMessageOptions) { + const { content, parentThreadId, connectionRecord } = options + if (!connectionRecord.did || !connectionRecord.theirDid) { + throw new AriesFrameworkError('Connection Record must have both our and their did') + } + + const basicMessage = new V2BasicMessage({ + from: connectionRecord.did, + to: connectionRecord.theirDid, + content, + }) + + const basicMessageRepository = agentContext.dependencyManager.resolve(BasicMessageRepository) + + // If no parentThreadid is defined, there is no need to explicitly send a thread decorator + if (parentThreadId) { + basicMessage.parentThreadId = parentThreadId + } + + const basicMessageRecord = new BasicMessageRecord({ + sentTime: new Date(basicMessage.createdTime).toISOString(), + content: basicMessage.body.content, + connectionId: connectionRecord.id, + role: BasicMessageRole.Sender, + threadId: basicMessage.threadId, + parentThreadId, + }) + + await basicMessageRepository.save(agentContext, basicMessageRecord) + this.emitStateChangedEvent(agentContext, basicMessageRecord, basicMessage) + + return { message: basicMessage, record: basicMessageRecord } + } + + /** + * @todo use connection from message context + */ + public async save({ message, agentContext }: InboundMessageContext, connection: ConnectionRecord) { + const basicMessageRepository = agentContext.dependencyManager.resolve(BasicMessageRepository) + + const basicMessageRecord = new BasicMessageRecord({ + sentTime: new Date(message.createdTime).toISOString(), + content: message.body.content, + connectionId: connection.id, + role: BasicMessageRole.Receiver, + threadId: message.threadId, + parentThreadId: message.parentThreadId, + }) + + await basicMessageRepository.save(agentContext, basicMessageRecord) + this.emitStateChangedEvent(agentContext, basicMessageRecord, message) + } + + protected emitStateChangedEvent( + agentContext: AgentContext, + basicMessageRecord: BasicMessageRecord, + basicMessage: V2BasicMessage + ) { + const eventEmitter = agentContext.dependencyManager.resolve(EventEmitter) + eventEmitter.emit(agentContext, { + type: BasicMessageEventTypes.BasicMessageStateChanged, + payload: { message: basicMessage, basicMessageRecord: basicMessageRecord.clone() }, + }) + } +} diff --git a/packages/core/src/modules/basic-messages/protocols/v2/handlers/V2BasicMessageHandler.ts b/packages/core/src/modules/basic-messages/protocols/v2/handlers/V2BasicMessageHandler.ts new file mode 100644 index 0000000000..2995d643b5 --- /dev/null +++ b/packages/core/src/modules/basic-messages/protocols/v2/handlers/V2BasicMessageHandler.ts @@ -0,0 +1,18 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' +import type { V2BasicMessageProtocol } from '../V2BasicMessageProtocol' + +import { V2BasicMessage } from '../messages' + +export class V2BasicMessageHandler implements MessageHandler { + private basicMessageProtocol: V2BasicMessageProtocol + public supportedMessages = [V2BasicMessage] + + public constructor(basicMessageProtocol: V2BasicMessageProtocol) { + this.basicMessageProtocol = basicMessageProtocol + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + const connection = messageContext.assertReadyConnection() + await this.basicMessageProtocol.save(messageContext, connection) + } +} diff --git a/packages/core/src/modules/basic-messages/protocols/v2/handlers/index.ts b/packages/core/src/modules/basic-messages/protocols/v2/handlers/index.ts new file mode 100644 index 0000000000..a35412c989 --- /dev/null +++ b/packages/core/src/modules/basic-messages/protocols/v2/handlers/index.ts @@ -0,0 +1 @@ +export * from './V2BasicMessageHandler' diff --git a/packages/core/src/modules/basic-messages/protocols/v2/index.ts b/packages/core/src/modules/basic-messages/protocols/v2/index.ts new file mode 100644 index 0000000000..ca75f6a6e8 --- /dev/null +++ b/packages/core/src/modules/basic-messages/protocols/v2/index.ts @@ -0,0 +1,3 @@ +export * from './V2BasicMessageProtocol' +export * from './handlers' +export * from './messages' diff --git a/packages/core/src/modules/basic-messages/protocols/v2/messages/V2BasicMessage.ts b/packages/core/src/modules/basic-messages/protocols/v2/messages/V2BasicMessage.ts new file mode 100644 index 0000000000..cb42d4d1df --- /dev/null +++ b/packages/core/src/modules/basic-messages/protocols/v2/messages/V2BasicMessage.ts @@ -0,0 +1,60 @@ +import { Type } from 'class-transformer' +import { IsNotEmpty, IsObject, IsString, ValidateNested } from 'class-validator' + +import { DidCommV2Message } from '../../../../../didcomm' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' + +class V2BasicMessageBody { + public constructor(options: { content: string }) { + if (options) { + this.content = options.content + } + } + @IsString() + public content!: string +} + +export class V2BasicMessage extends DidCommV2Message { + public readonly allowDidSovPrefix = false + + @IsNotEmpty() + public createdTime!: number + + @IsString() + @IsNotEmpty() + public from!: string + + @IsObject() + @ValidateNested() + @Type(() => V2BasicMessageBody) + public body!: V2BasicMessageBody + + /** + * Create new BasicMessage instance. + * sentTime will be assigned to new Date if not passed, id will be assigned to uuid/v4 if not passed + * @param options + */ + public constructor(options: { + from: string + to: string + content: string + sentTime?: Date + id?: string + locale?: string + }) { + super() + + if (options) { + this.id = options.id || this.generateId() + this.createdTime = options.sentTime?.getTime() || new Date().getTime() + this.from = options.from + this.to = [options.to] + this.body = new V2BasicMessageBody({ content: options.content }) + this.language = options.locale || 'en' + } + } + + @IsValidMessageType(V2BasicMessage.type) + public readonly type = V2BasicMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/basicmessage/2.0/message') +} diff --git a/packages/core/src/modules/basic-messages/protocols/v2/messages/index.ts b/packages/core/src/modules/basic-messages/protocols/v2/messages/index.ts new file mode 100644 index 0000000000..171b735eb1 --- /dev/null +++ b/packages/core/src/modules/basic-messages/protocols/v2/messages/index.ts @@ -0,0 +1 @@ +export * from './V2BasicMessage' diff --git a/packages/core/src/modules/basic-messages/services/BasicMessageService.ts b/packages/core/src/modules/basic-messages/services/BasicMessageService.ts deleted file mode 100644 index d58ec06564..0000000000 --- a/packages/core/src/modules/basic-messages/services/BasicMessageService.ts +++ /dev/null @@ -1,100 +0,0 @@ -import type { AgentContext } from '../../../agent' -import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext' -import type { Query } from '../../../storage/StorageService' -import type { ConnectionRecord } from '../../connections/repository/ConnectionRecord' -import type { BasicMessageStateChangedEvent } from '../BasicMessageEvents' - -import { EventEmitter } from '../../../agent/EventEmitter' -import { injectable } from '../../../plugins' -import { BasicMessageEventTypes } from '../BasicMessageEvents' -import { BasicMessageRole } from '../BasicMessageRole' -import { BasicMessage } from '../messages' -import { BasicMessageRecord, BasicMessageRepository } from '../repository' - -@injectable() -export class BasicMessageService { - private basicMessageRepository: BasicMessageRepository - private eventEmitter: EventEmitter - - public constructor(basicMessageRepository: BasicMessageRepository, eventEmitter: EventEmitter) { - this.basicMessageRepository = basicMessageRepository - this.eventEmitter = eventEmitter - } - - public async createMessage( - agentContext: AgentContext, - message: string, - connectionRecord: ConnectionRecord, - parentThreadId?: string - ) { - const basicMessage = new BasicMessage({ content: message }) - - // If no parentThreadid is defined, there is no need to explicitly send a thread decorator - if (parentThreadId) { - basicMessage.setThread({ parentThreadId }) - } - - const basicMessageRecord = new BasicMessageRecord({ - sentTime: basicMessage.sentTime.toISOString(), - content: basicMessage.content, - connectionId: connectionRecord.id, - role: BasicMessageRole.Sender, - threadId: basicMessage.threadId, - parentThreadId, - }) - - await this.basicMessageRepository.save(agentContext, basicMessageRecord) - this.emitStateChangedEvent(agentContext, basicMessageRecord, basicMessage) - - return { message: basicMessage, record: basicMessageRecord } - } - - /** - * @todo use connection from message context - */ - public async save({ message, agentContext }: InboundMessageContext, connection: ConnectionRecord) { - const basicMessageRecord = new BasicMessageRecord({ - sentTime: message.sentTime.toISOString(), - content: message.content, - connectionId: connection.id, - role: BasicMessageRole.Receiver, - threadId: message.threadId, - parentThreadId: message.thread?.parentThreadId, - }) - - await this.basicMessageRepository.save(agentContext, basicMessageRecord) - this.emitStateChangedEvent(agentContext, basicMessageRecord, message) - } - - private emitStateChangedEvent( - agentContext: AgentContext, - basicMessageRecord: BasicMessageRecord, - basicMessage: BasicMessage - ) { - this.eventEmitter.emit(agentContext, { - type: BasicMessageEventTypes.BasicMessageStateChanged, - payload: { message: basicMessage, basicMessageRecord: basicMessageRecord.clone() }, - }) - } - - public async findAllByQuery(agentContext: AgentContext, query: Query) { - return this.basicMessageRepository.findByQuery(agentContext, query) - } - - public async getById(agentContext: AgentContext, basicMessageRecordId: string) { - return this.basicMessageRepository.getById(agentContext, basicMessageRecordId) - } - - public async getByThreadId(agentContext: AgentContext, threadId: string) { - return this.basicMessageRepository.getSingleByQuery(agentContext, { threadId }) - } - - public async findAllByParentThreadId(agentContext: AgentContext, parentThreadId: string) { - return this.basicMessageRepository.findByQuery(agentContext, { parentThreadId }) - } - - public async deleteById(agentContext: AgentContext, basicMessageRecordId: string) { - const basicMessageRecord = await this.getById(agentContext, basicMessageRecordId) - return this.basicMessageRepository.delete(agentContext, basicMessageRecord) - } -} diff --git a/packages/core/src/modules/basic-messages/services/index.ts b/packages/core/src/modules/basic-messages/services/index.ts deleted file mode 100644 index a48826839a..0000000000 --- a/packages/core/src/modules/basic-messages/services/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './BasicMessageService' diff --git a/packages/core/tests/helpers.ts b/packages/core/tests/helpers.ts index 4fe9732007..8aedbc6bba 100644 --- a/packages/core/tests/helpers.ts +++ b/packages/core/tests/helpers.ts @@ -2,7 +2,6 @@ import type { AgentDependencies, BaseEvent, - BasicMessage, BasicMessageStateChangedEvent, ConnectionRecordProps, CredentialStateChangedEvent, @@ -31,6 +30,8 @@ import { catchError, filter, map, take, timeout } from 'rxjs/operators' import { agentDependencies, IndySdkPostgresWalletScheme } from '../../node/src' import { + V1BasicMessage, + V2BasicMessage, OutOfBandDidCommService, ConnectionsModule, ConnectionEventTypes, @@ -442,15 +443,22 @@ export async function waitForConnectionRecord( return waitForConnectionRecordSubject(observable, options) } -export async function waitForBasicMessage(agent: Agent, { content }: { content?: string }): Promise { +export async function waitForBasicMessage( + agent: Agent, + { content }: { content?: string } +): Promise { return new Promise((resolve) => { const listener = (event: BasicMessageStateChangedEvent) => { - const contentMatches = content === undefined || event.payload.message.content === content + const message = event.payload.message + const contentMatches = + content === undefined || + (message instanceof V1BasicMessage && message.content === content) || + (message instanceof V2BasicMessage && message.body.content === content) if (contentMatches) { agent.events.off(BasicMessageEventTypes.BasicMessageStateChanged, listener) - resolve(event.payload.message) + resolve(message) } } From 191ad5b584f51a2c5fdfa5646b2cea968c34875e Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Thu, 17 Aug 2023 11:46:34 -0300 Subject: [PATCH 02/22] feat: initial work on IC V3 (wip) Signed-off-by: Ariel Gentile --- .../didcomm/versions/v2/DidCommV2Message.ts | 4 +- .../connections/services/ConnectionService.ts | 12 +- .../formats/CredentialFormatServiceOptions.ts | 38 +- .../protocol/BaseCredentialProtocol.ts | 50 +- .../protocol/CredentialProtocol.ts | 46 +- .../protocol/CredentialProtocolOptions.ts | 4 +- .../v3/CredentialFormatCoordinator.ts | 573 ++++++++ .../protocol/v3/V3CredentialProtocol.ts | 1266 +++++++++++++++++ .../errors/V3CredentialProblemReportError.ts | 25 + .../credentials/protocol/v3/errors/index.ts | 1 + .../v3/handlers/V2IssueCredentialHandler.ts | 56 + .../v3/handlers/V2OfferCredentialHandler.ts | 45 + .../v3/handlers/V2ProposeCredentialHandler.ts | 50 + .../v3/handlers/V2RequestCredentialHandler.ts | 58 + .../v3/handlers/V3CredentialAckHandler.ts | 17 + .../V3CredentialProblemReportHandler.ts | 17 + .../credentials/protocol/v3/handlers/index.ts | 6 + .../modules/credentials/protocol/v3/index.ts | 3 + .../v3/messages/V3CredentialAckMessage.ts | 23 + .../v3/messages/V3CredentialPreview.ts | 64 + .../V3CredentialProblemReportMessage.ts | 23 + .../v3/messages/V3IssueCredentialMessage.ts | 53 + .../v3/messages/V3OfferCredentialMessage.ts | 69 + .../v3/messages/V3ProposeCredentialMessage.ts | 70 + .../v3/messages/V3RequestCredentialMessage.ts | 57 + .../credentials/protocol/v3/messages/index.ts | 7 + .../errors/V2ProblemReportError.ts | 22 + .../problem-reports/versions/v2/helpers.ts | 6 +- .../problem-reports/versions/v2/index.ts | 2 +- .../v2/messages/ProblemReportMessage.ts | 6 +- 30 files changed, 2593 insertions(+), 80 deletions(-) create mode 100644 packages/core/src/modules/credentials/protocol/v3/CredentialFormatCoordinator.ts create mode 100644 packages/core/src/modules/credentials/protocol/v3/V3CredentialProtocol.ts create mode 100644 packages/core/src/modules/credentials/protocol/v3/errors/V3CredentialProblemReportError.ts create mode 100644 packages/core/src/modules/credentials/protocol/v3/errors/index.ts create mode 100644 packages/core/src/modules/credentials/protocol/v3/handlers/V2IssueCredentialHandler.ts create mode 100644 packages/core/src/modules/credentials/protocol/v3/handlers/V2OfferCredentialHandler.ts create mode 100644 packages/core/src/modules/credentials/protocol/v3/handlers/V2ProposeCredentialHandler.ts create mode 100644 packages/core/src/modules/credentials/protocol/v3/handlers/V2RequestCredentialHandler.ts create mode 100644 packages/core/src/modules/credentials/protocol/v3/handlers/V3CredentialAckHandler.ts create mode 100644 packages/core/src/modules/credentials/protocol/v3/handlers/V3CredentialProblemReportHandler.ts create mode 100644 packages/core/src/modules/credentials/protocol/v3/handlers/index.ts create mode 100644 packages/core/src/modules/credentials/protocol/v3/index.ts create mode 100644 packages/core/src/modules/credentials/protocol/v3/messages/V3CredentialAckMessage.ts create mode 100644 packages/core/src/modules/credentials/protocol/v3/messages/V3CredentialPreview.ts create mode 100644 packages/core/src/modules/credentials/protocol/v3/messages/V3CredentialProblemReportMessage.ts create mode 100644 packages/core/src/modules/credentials/protocol/v3/messages/V3IssueCredentialMessage.ts create mode 100644 packages/core/src/modules/credentials/protocol/v3/messages/V3OfferCredentialMessage.ts create mode 100644 packages/core/src/modules/credentials/protocol/v3/messages/V3ProposeCredentialMessage.ts create mode 100644 packages/core/src/modules/credentials/protocol/v3/messages/V3RequestCredentialMessage.ts create mode 100644 packages/core/src/modules/credentials/protocol/v3/messages/index.ts create mode 100644 packages/core/src/modules/problem-reports/errors/V2ProblemReportError.ts diff --git a/packages/core/src/didcomm/versions/v2/DidCommV2Message.ts b/packages/core/src/didcomm/versions/v2/DidCommV2Message.ts index a45f45da6f..2803eb9c4e 100644 --- a/packages/core/src/didcomm/versions/v2/DidCommV2Message.ts +++ b/packages/core/src/didcomm/versions/v2/DidCommV2Message.ts @@ -22,8 +22,8 @@ export class DidCommV2Message extends DidCommV2BaseMessage implements AgentBaseM return undefined } - public get threadId(): string | undefined { - return this.thid + public get threadId(): string { + return this.thid ?? this.id } public hasAnyReturnRoute() { diff --git a/packages/core/src/modules/connections/services/ConnectionService.ts b/packages/core/src/modules/connections/services/ConnectionService.ts index 5292eb8ed4..8b47534040 100644 --- a/packages/core/src/modules/connections/services/ConnectionService.ts +++ b/packages/core/src/modules/connections/services/ConnectionService.ts @@ -1,6 +1,6 @@ import type { AgentContext } from '../../../agent' import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext' -import type { DidCommV1Message } from '../../../didcomm' +import type { DidCommV2Message } from '../../../didcomm' import type { Query } from '../../../storage/StorageService' import type { AckMessage } from '../../common' import type { OutOfBandDidCommService } from '../../oob/domain/OutOfBandDidCommService' @@ -18,6 +18,7 @@ import { filterContextCorrelationId } from '../../../agent/Events' import { InjectionSymbols } from '../../../constants' import { Key } from '../../../crypto' import { signData, unpackAndVerifySignatureDecorator } from '../../../decorators/signature/SignatureDecoratorUtils' +import { DidCommV1Message } from '../../../didcomm' import { AriesFrameworkError } from '../../../error' import { Logger } from '../../../logger' import { inject, injectable } from '../../../plugins' @@ -448,13 +449,13 @@ export class ConnectionService { * @param messageContext - the inbound message context */ public async assertConnectionOrOutOfBandExchange( - messageContext: InboundMessageContext, + messageContext: InboundMessageContext, { lastSentMessage, lastReceivedMessage, }: { - lastSentMessage?: DidCommV1Message | null - lastReceivedMessage?: DidCommV1Message | null + lastSentMessage?: DidCommV1Message | DidCommV2Message | null + lastReceivedMessage?: DidCommV1Message | DidCommV2Message | null } = {} ) { const { connection, message } = messageContext @@ -475,7 +476,8 @@ export class ConnectionService { // set theirService to the value of lastReceivedMessage.service let theirService = - messageContext.message?.service?.resolvedDidCommService ?? lastReceivedMessage?.service?.resolvedDidCommService + (message instanceof DidCommV1Message && message.service?.resolvedDidCommService) ?? + (lastReceivedMessage instanceof DidCommV1Message && lastReceivedMessage?.service?.resolvedDidCommService) let ourService = lastSentMessage?.service?.resolvedDidCommService // 1. check if there's an oob record associated. diff --git a/packages/core/src/modules/credentials/formats/CredentialFormatServiceOptions.ts b/packages/core/src/modules/credentials/formats/CredentialFormatServiceOptions.ts index 21ade782cc..4f3aa0072c 100644 --- a/packages/core/src/modules/credentials/formats/CredentialFormatServiceOptions.ts +++ b/packages/core/src/modules/credentials/formats/CredentialFormatServiceOptions.ts @@ -1,6 +1,6 @@ import type { CredentialFormat, CredentialFormatPayload } from './CredentialFormat' import type { CredentialFormatService } from './CredentialFormatService' -import type { Attachment } from '../../../decorators/attachment' +import type { Attachment, V2Attachment } from '../../../decorators/attachment' import type { CredentialFormatSpec } from '../models/CredentialFormatSpec' import type { CredentialPreviewAttributeOptions } from '../models/CredentialPreviewAttribute' import type { CredentialExchangeRecord } from '../repository/CredentialExchangeRecord' @@ -42,19 +42,19 @@ export type ExtractCredentialFormats = { */ export interface CredentialFormatCreateReturn { format: CredentialFormatSpec - attachment: Attachment + attachment: Attachment | V2Attachment } /** * Base return type for all credential process methods. */ export interface CredentialFormatProcessOptions { - attachment: Attachment + attachment: Attachment | V2Attachment credentialRecord: CredentialExchangeRecord } export interface CredentialFormatProcessCredentialOptions extends CredentialFormatProcessOptions { - requestAttachment: Attachment + requestAttachment: Attachment | V2Attachment } export interface CredentialFormatCreateProposalOptions { @@ -68,7 +68,7 @@ export interface CredentialFormatAcceptProposalOptions attachmentId?: string - proposalAttachment: Attachment + proposalAttachment: Attachment | V2Attachment } export interface CredentialFormatCreateProposalReturn extends CredentialFormatCreateReturn { @@ -86,7 +86,7 @@ export interface CredentialFormatAcceptOfferOptions credentialFormats?: CredentialFormatPayload<[CF], 'acceptOffer'> attachmentId?: string - offerAttachment: Attachment + offerAttachment: Attachment | V2Attachment } export interface CredentialFormatCreateOfferReturn extends CredentialFormatCreateReturn { @@ -103,34 +103,34 @@ export interface CredentialFormatAcceptRequestOptions attachmentId?: string - requestAttachment: Attachment - offerAttachment?: Attachment + requestAttachment: Attachment | V2Attachment + offerAttachment?: Attachment | V2Attachment } // Auto accept method interfaces export interface CredentialFormatAutoRespondProposalOptions { credentialRecord: CredentialExchangeRecord - proposalAttachment: Attachment - offerAttachment: Attachment + proposalAttachment: Attachment | V2Attachment + offerAttachment: Attachment | V2Attachment } export interface CredentialFormatAutoRespondOfferOptions { credentialRecord: CredentialExchangeRecord - proposalAttachment: Attachment - offerAttachment: Attachment + proposalAttachment: Attachment | V2Attachment + offerAttachment: Attachment | V2Attachment } export interface CredentialFormatAutoRespondRequestOptions { credentialRecord: CredentialExchangeRecord - proposalAttachment?: Attachment - offerAttachment: Attachment - requestAttachment: Attachment + proposalAttachment?: Attachment | V2Attachment + offerAttachment: Attachment | V2Attachment + requestAttachment: Attachment | V2Attachment } export interface CredentialFormatAutoRespondCredentialOptions { credentialRecord: CredentialExchangeRecord - proposalAttachment?: Attachment - offerAttachment?: Attachment - requestAttachment: Attachment - credentialAttachment: Attachment + proposalAttachment?: Attachment | V2Attachment + offerAttachment?: Attachment | V2Attachment + requestAttachment: Attachment | V2Attachment + credentialAttachment: Attachment | V2Attachment } diff --git a/packages/core/src/modules/credentials/protocol/BaseCredentialProtocol.ts b/packages/core/src/modules/credentials/protocol/BaseCredentialProtocol.ts index 92692107fd..1a7a7d6da6 100644 --- a/packages/core/src/modules/credentials/protocol/BaseCredentialProtocol.ts +++ b/packages/core/src/modules/credentials/protocol/BaseCredentialProtocol.ts @@ -15,17 +15,18 @@ import type { CreateCredentialProblemReportOptions, } from './CredentialProtocolOptions' import type { AgentContext } from '../../../agent' +import type { AgentBaseMessage } from '../../../agent/AgentBaseMessage' import type { FeatureRegistry } from '../../../agent/FeatureRegistry' import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext' -import type { DidCommV1Message } from '../../../didcomm' import type { DependencyManager } from '../../../plugins' import type { Query } from '../../../storage/StorageService' -import type { ProblemReportMessage } from '../../problem-reports' +import type { ProblemReportMessage, V2ProblemReportMessage } from '../../problem-reports' import type { CredentialStateChangedEvent } from '../CredentialEvents' import type { CredentialFormatService, ExtractCredentialFormats } from '../formats' import type { CredentialExchangeRecord } from '../repository' import { EventEmitter } from '../../../agent/EventEmitter' +import { DidCommV1Message } from '../../../didcomm' import { DidCommMessageRepository } from '../../../storage' import { CredentialEventTypes } from '../CredentialEvents' import { CredentialState } from '../models/CredentialState' @@ -46,83 +47,83 @@ export abstract class BaseCredentialProtocol - ): Promise> + ): Promise> public abstract processProposal( - messageContext: InboundMessageContext + messageContext: InboundMessageContext ): Promise public abstract acceptProposal( agentContext: AgentContext, options: AcceptCredentialProposalOptions - ): Promise> + ): Promise> public abstract negotiateProposal( agentContext: AgentContext, options: NegotiateCredentialProposalOptions - ): Promise> + ): Promise> // methods for offer public abstract createOffer( agentContext: AgentContext, options: CreateCredentialOfferOptions - ): Promise> + ): Promise> public abstract processOffer( - messageContext: InboundMessageContext + messageContext: InboundMessageContext ): Promise public abstract acceptOffer( agentContext: AgentContext, options: AcceptCredentialOfferOptions - ): Promise> + ): Promise> public abstract negotiateOffer( agentContext: AgentContext, options: NegotiateCredentialOfferOptions - ): Promise> + ): Promise> // methods for request public abstract createRequest( agentContext: AgentContext, options: CreateCredentialRequestOptions - ): Promise> + ): Promise> public abstract processRequest( - messageContext: InboundMessageContext + messageContext: InboundMessageContext ): Promise public abstract acceptRequest( agentContext: AgentContext, options: AcceptCredentialRequestOptions - ): Promise> + ): Promise> // methods for issue public abstract processCredential( - messageContext: InboundMessageContext + messageContext: InboundMessageContext ): Promise public abstract acceptCredential( agentContext: AgentContext, options: AcceptCredentialOptions - ): Promise> + ): Promise> // methods for ack - public abstract processAck(messageContext: InboundMessageContext): Promise + public abstract processAck(messageContext: InboundMessageContext): Promise // methods for problem-report public abstract createProblemReport( agentContext: AgentContext, options: CreateCredentialProblemReportOptions - ): Promise> + ): Promise> public abstract findProposalMessage( agentContext: AgentContext, credentialExchangeId: string - ): Promise + ): Promise public abstract findOfferMessage( agentContext: AgentContext, credentialExchangeId: string - ): Promise + ): Promise public abstract findRequestMessage( agentContext: AgentContext, credentialExchangeId: string - ): Promise + ): Promise public abstract findCredentialMessage( agentContext: AgentContext, credentialExchangeId: string - ): Promise + ): Promise public abstract getFormatData( agentContext: AgentContext, credentialExchangeId: string @@ -137,7 +138,7 @@ export abstract class BaseCredentialProtocol + messageContext: InboundMessageContext ): Promise { const { message: credentialProblemReportMessage, agentContext } = messageContext @@ -152,7 +153,10 @@ export abstract class BaseCredentialProtocol - ): Promise> - processProposal(messageContext: InboundMessageContext): Promise + ): Promise> + processProposal(messageContext: InboundMessageContext): Promise acceptProposal( agentContext: AgentContext, options: AcceptCredentialProposalOptions - ): Promise> + ): Promise> negotiateProposal( agentContext: AgentContext, options: NegotiateCredentialProposalOptions - ): Promise> + ): Promise> // methods for offer createOffer( agentContext: AgentContext, options: CreateCredentialOfferOptions - ): Promise> - processOffer(messageContext: InboundMessageContext): Promise + ): Promise> + processOffer(messageContext: InboundMessageContext): Promise acceptOffer( agentContext: AgentContext, options: AcceptCredentialOfferOptions - ): Promise> + ): Promise> negotiateOffer( agentContext: AgentContext, options: NegotiateCredentialOfferOptions - ): Promise> + ): Promise> // methods for request createRequest( agentContext: AgentContext, options: CreateCredentialRequestOptions - ): Promise> - processRequest(messageContext: InboundMessageContext): Promise + ): Promise> + processRequest(messageContext: InboundMessageContext): Promise acceptRequest( agentContext: AgentContext, options: AcceptCredentialRequestOptions - ): Promise> + ): Promise> // methods for issue - processCredential(messageContext: InboundMessageContext): Promise + processCredential(messageContext: InboundMessageContext): Promise acceptCredential( agentContext: AgentContext, options: AcceptCredentialOptions - ): Promise> + ): Promise> // methods for ack - processAck(messageContext: InboundMessageContext): Promise + processAck(messageContext: InboundMessageContext): Promise // methods for problem-report createProblemReport( agentContext: AgentContext, options: CreateCredentialProblemReportOptions - ): Promise> - processProblemReport(messageContext: InboundMessageContext): Promise + ): Promise> + processProblemReport( + messageContext: InboundMessageContext + ): Promise - findProposalMessage(agentContext: AgentContext, credentialExchangeId: string): Promise - findOfferMessage(agentContext: AgentContext, credentialExchangeId: string): Promise - findRequestMessage(agentContext: AgentContext, credentialExchangeId: string): Promise - findCredentialMessage(agentContext: AgentContext, credentialExchangeId: string): Promise + findProposalMessage(agentContext: AgentContext, credentialExchangeId: string): Promise + findOfferMessage(agentContext: AgentContext, credentialExchangeId: string): Promise + findRequestMessage(agentContext: AgentContext, credentialExchangeId: string): Promise + findCredentialMessage(agentContext: AgentContext, credentialExchangeId: string): Promise getFormatData( agentContext: AgentContext, credentialExchangeId: string diff --git a/packages/core/src/modules/credentials/protocol/CredentialProtocolOptions.ts b/packages/core/src/modules/credentials/protocol/CredentialProtocolOptions.ts index b8ba5b9761..63791679f6 100644 --- a/packages/core/src/modules/credentials/protocol/CredentialProtocolOptions.ts +++ b/packages/core/src/modules/credentials/protocol/CredentialProtocolOptions.ts @@ -1,5 +1,5 @@ import type { CredentialProtocol } from './CredentialProtocol' -import type { DidCommV1Message } from '../../../didcomm' +import type { AgentBaseMessage } from '../../../agent/AgentBaseMessage' import type { ConnectionRecord } from '../../connections/repository/ConnectionRecord' import type { CredentialFormat, @@ -155,7 +155,7 @@ export interface CreateCredentialProblemReportOptions { description: string } -export interface CredentialProtocolMsgReturnType { +export interface CredentialProtocolMsgReturnType { message: MessageType credentialRecord: CredentialExchangeRecord } diff --git a/packages/core/src/modules/credentials/protocol/v3/CredentialFormatCoordinator.ts b/packages/core/src/modules/credentials/protocol/v3/CredentialFormatCoordinator.ts new file mode 100644 index 0000000000..58b2bdfda4 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v3/CredentialFormatCoordinator.ts @@ -0,0 +1,573 @@ +import type { AgentContext } from '../../../../agent' +import type { Attachment, V2Attachment } from '../../../../decorators/attachment' +import type { CredentialFormatPayload, CredentialFormatService, ExtractCredentialFormats } from '../../formats' +import type { CredentialFormatSpec } from '../../models' +import type { CredentialExchangeRecord } from '../../repository/CredentialExchangeRecord' + +import { AriesFrameworkError } from '../../../../error/AriesFrameworkError' +import { DidCommMessageRepository, DidCommMessageRole } from '../../../../storage' + +import { + V3IssueCredentialMessage, + V3OfferCredentialMessage, + V3ProposeCredentialMessage, + V3RequestCredentialMessage, + V3CredentialPreview, +} from './messages' + +export class CredentialFormatCoordinator { + /** + * Create a {@link V3ProposeCredentialMessage}. + * + * @param options + * @returns The created {@link V3ProposeCredentialMessage} + * + */ + public async createProposal( + agentContext: AgentContext, + { + credentialFormats, + formatServices, + credentialRecord, + comment, + }: { + formatServices: CredentialFormatService[] + credentialFormats: CredentialFormatPayload, 'createProposal'> + credentialRecord: CredentialExchangeRecord + comment?: 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: CredentialFormatSpec[] = [] + const proposalAttachments: V2Attachment[] = [] + let credentialPreview: V3CredentialPreview | undefined + + for (const formatService of formatServices) { + const { format, attachment, previewAttributes } = await formatService.createProposal(agentContext, { + credentialFormats, + credentialRecord, + }) + + if (previewAttributes) { + credentialPreview = new V3CredentialPreview({ + attributes: previewAttributes, + }) + } + + proposalAttachments.push(attachment as V2Attachment) + formats.push(format) + } + + credentialRecord.credentialAttributes = credentialPreview?.attributes + + const message = new V3ProposeCredentialMessage({ + id: credentialRecord.threadId, + formats, + proposalAttachments, + comment: comment, + credentialPreview, + }) + + message.thid = credentialRecord.threadId + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + role: DidCommMessageRole.Sender, + associatedRecordId: credentialRecord.id, + }) + + return message + } + + public async processProposal( + agentContext: AgentContext, + { + credentialRecord, + message, + formatServices, + }: { + credentialRecord: CredentialExchangeRecord + message: V3ProposeCredentialMessage + formatServices: CredentialFormatService[] + } + ) { + 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, + credentialRecord, + }) + } + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + role: DidCommMessageRole.Receiver, + associatedRecordId: credentialRecord.id, + }) + } + + public async acceptProposal( + agentContext: AgentContext, + { + credentialRecord, + credentialFormats, + formatServices, + comment, + }: { + credentialRecord: CredentialExchangeRecord + credentialFormats?: CredentialFormatPayload, 'acceptProposal'> + formatServices: CredentialFormatService[] + comment?: string + } + ) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // create message. there are two arrays in each message, one for formats the other for attachments + const formats: CredentialFormatSpec[] = [] + const offerAttachments: V2Attachment[] = [] + let credentialPreview: V3CredentialPreview | undefined + + const proposalMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V3ProposeCredentialMessage, + }) + + // NOTE: We set the credential attributes from the proposal on the record as we've 'accepted' them + // and can now use them to create the offer in the format services. It may be overwritten later on + // if the user provided other attributes in the credentialFormats array. + credentialRecord.credentialAttributes = proposalMessage.credentialPreview?.attributes + + for (const formatService of formatServices) { + const proposalAttachment = this.getAttachmentForService( + formatService, + proposalMessage.formats, + proposalMessage.proposalAttachments + ) + + const { attachment, format, previewAttributes } = await formatService.acceptProposal(agentContext, { + credentialRecord, + credentialFormats, + proposalAttachment, + }) + + if (previewAttributes) { + credentialPreview = new V3CredentialPreview({ + attributes: previewAttributes, + }) + } + + offerAttachments.push(attachment as V2Attachment) + formats.push(format) + } + + credentialRecord.credentialAttributes = credentialPreview?.attributes + + if (!credentialPreview) { + // If no preview attributes were provided, use a blank preview. Not all formats use this object + // but it is required by the protocol + credentialPreview = new V3CredentialPreview({ + attributes: [], + }) + } + + const message = new V3OfferCredentialMessage({ + formats, + credentialPreview, + offerAttachments, + comment, + }) + + message.thid = credentialRecord.threadId + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + associatedRecordId: credentialRecord.id, + role: DidCommMessageRole.Sender, + }) + + return message + } + + /** + * Create a {@link V3OfferCredentialMessage}. + * + * @param options + * @returns The created {@link V3OfferCredentialMessage} + * + */ + public async createOffer( + agentContext: AgentContext, + { + credentialFormats, + formatServices, + credentialRecord, + comment, + }: { + formatServices: CredentialFormatService[] + credentialFormats: CredentialFormatPayload, 'createOffer'> + credentialRecord: CredentialExchangeRecord + comment?: 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: CredentialFormatSpec[] = [] + const offerAttachments: V2Attachment[] = [] + let credentialPreview: V3CredentialPreview | undefined + + for (const formatService of formatServices) { + const { format, attachment, previewAttributes } = await formatService.createOffer(agentContext, { + credentialFormats, + credentialRecord, + }) + + if (previewAttributes) { + credentialPreview = new V3CredentialPreview({ + attributes: previewAttributes, + }) + } + + offerAttachments.push(attachment as V2Attachment) + formats.push(format) + } + + credentialRecord.credentialAttributes = credentialPreview?.attributes + + if (!credentialPreview) { + // If no preview attributes were provided, use a blank preview. Not all formats use this object + // but it is required by the protocol + credentialPreview = new V3CredentialPreview({ + attributes: [], + }) + } + + const message = new V3OfferCredentialMessage({ + formats, + comment, + offerAttachments, + credentialPreview, + }) + + message.thid = credentialRecord.threadId + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + role: DidCommMessageRole.Sender, + associatedRecordId: credentialRecord.id, + }) + + return message + } + + public async processOffer( + agentContext: AgentContext, + { + credentialRecord, + message, + formatServices, + }: { + credentialRecord: CredentialExchangeRecord + message: V3OfferCredentialMessage + formatServices: CredentialFormatService[] + } + ) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + for (const formatService of formatServices) { + const attachment = this.getAttachmentForService(formatService, message.formats, message.offerAttachments) + + await formatService.processOffer(agentContext, { + attachment, + credentialRecord, + }) + } + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + role: DidCommMessageRole.Receiver, + associatedRecordId: credentialRecord.id, + }) + } + + public async acceptOffer( + agentContext: AgentContext, + { + credentialRecord, + credentialFormats, + formatServices, + comment, + }: { + credentialRecord: CredentialExchangeRecord + credentialFormats?: CredentialFormatPayload, 'acceptOffer'> + formatServices: CredentialFormatService[] + comment?: string + } + ) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + const offerMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V3OfferCredentialMessage, + }) + + // create message. there are two arrays in each message, one for formats the other for attachments + const formats: CredentialFormatSpec[] = [] + const requestAttachments: V2Attachment[] = [] + + for (const formatService of formatServices) { + const offerAttachment = this.getAttachmentForService( + formatService, + offerMessage.formats, + offerMessage.offerAttachments + ) + + const { attachment, format } = await formatService.acceptOffer(agentContext, { + offerAttachment, + credentialRecord, + credentialFormats, + }) + + requestAttachments.push(attachment as V2Attachment) + formats.push(format) + } + + credentialRecord.credentialAttributes = offerMessage.credentialPreview?.attributes + + const message = new V3RequestCredentialMessage({ + formats, + requestAttachments: requestAttachments, + comment, + }) + + message.thid = credentialRecord.threadId + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + associatedRecordId: credentialRecord.id, + role: DidCommMessageRole.Sender, + }) + + return message + } + + /** + * Create a {@link V3RequestCredentialMessage}. + * + * @param options + * @returns The created {@link V3RequestCredentialMessage} + * + */ + public async createRequest( + agentContext: AgentContext, + { + credentialFormats, + formatServices, + credentialRecord, + comment, + }: { + formatServices: CredentialFormatService[] + credentialFormats: CredentialFormatPayload, 'createRequest'> + credentialRecord: CredentialExchangeRecord + comment?: 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: CredentialFormatSpec[] = [] + const requestAttachments: V2Attachment[] = [] + + for (const formatService of formatServices) { + const { format, attachment } = await formatService.createRequest(agentContext, { + credentialFormats, + credentialRecord, + }) + + requestAttachments.push(attachment as V2Attachment) + formats.push(format) + } + + const message = new V3RequestCredentialMessage({ + formats, + comment, + requestAttachments: requestAttachments, + }) + + message.thid = credentialRecord.threadId + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + role: DidCommMessageRole.Sender, + associatedRecordId: credentialRecord.id, + }) + + return message + } + + public async processRequest( + agentContext: AgentContext, + { + credentialRecord, + message, + formatServices, + }: { + credentialRecord: CredentialExchangeRecord + message: V3RequestCredentialMessage + formatServices: CredentialFormatService[] + } + ) { + 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, + credentialRecord, + }) + } + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + role: DidCommMessageRole.Receiver, + associatedRecordId: credentialRecord.id, + }) + } + + public async acceptRequest( + agentContext: AgentContext, + { + credentialRecord, + credentialFormats, + formatServices, + comment, + }: { + credentialRecord: CredentialExchangeRecord + credentialFormats?: CredentialFormatPayload, 'acceptRequest'> + formatServices: CredentialFormatService[] + comment?: string + } + ) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + const requestMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V3RequestCredentialMessage, + }) + + const offerMessage = await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V3OfferCredentialMessage, + }) + + // create message. there are two arrays in each message, one for formats the other for attachments + const formats: CredentialFormatSpec[] = [] + const credentialAttachments: V2Attachment[] = [] + + for (const formatService of formatServices) { + const requestAttachment = this.getAttachmentForService( + formatService, + requestMessage.formats, + requestMessage.requestAttachments + ) + + const offerAttachment = offerMessage + ? this.getAttachmentForService(formatService, offerMessage.formats, offerMessage.offerAttachments) + : undefined + + const { attachment, format } = await formatService.acceptRequest(agentContext, { + requestAttachment, + offerAttachment, + credentialRecord, + credentialFormats, + }) + + credentialAttachments.push(attachment as V2Attachment) + formats.push(format) + } + + const message = new V3IssueCredentialMessage({ + formats, + credentialAttachments: credentialAttachments, + comment, + }) + + message.thid = credentialRecord.threadId + // TODO: message.setPleaseAck() + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + associatedRecordId: credentialRecord.id, + role: DidCommMessageRole.Sender, + }) + + return message + } + + public async processCredential( + agentContext: AgentContext, + { + credentialRecord, + message, + requestMessage, + formatServices, + }: { + credentialRecord: CredentialExchangeRecord + message: V3IssueCredentialMessage + requestMessage: V3RequestCredentialMessage + formatServices: CredentialFormatService[] + } + ) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + for (const formatService of formatServices) { + const attachment = this.getAttachmentForService(formatService, message.formats, message.credentialAttachments) + const requestAttachment = this.getAttachmentForService( + formatService, + requestMessage.formats, + requestMessage.requestAttachments + ) + + await formatService.processCredential(agentContext, { + attachment, + requestAttachment, + credentialRecord, + }) + } + + await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + role: DidCommMessageRole.Receiver, + associatedRecordId: credentialRecord.id, + }) + } + + public getAttachmentForService( + credentialFormatService: CredentialFormatService, + formats: CredentialFormatSpec[], + attachments: V2Attachment[] + ) { + 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: CredentialFormatService, formats: CredentialFormatSpec[]) { + 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/credentials/protocol/v3/V3CredentialProtocol.ts b/packages/core/src/modules/credentials/protocol/v3/V3CredentialProtocol.ts new file mode 100644 index 0000000000..2d59a02955 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v3/V3CredentialProtocol.ts @@ -0,0 +1,1266 @@ +import type { AgentContext } from '../../../../agent' +import type { FeatureRegistry } from '../../../../agent/FeatureRegistry' +import type { MessageHandlerInboundMessage } from '../../../../agent/MessageHandler' +import type { InboundMessageContext } from '../../../../agent/models/InboundMessageContext' +import type { DidCommV2Message } from '../../../../didcomm' +import type { DependencyManager } from '../../../../plugins' +import type { ProblemReportMessage, V2ProblemReportMessage } from '../../../problem-reports' +import type { + CredentialFormat, + CredentialFormatPayload, + CredentialFormatService, + 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' +import { DidCommMessageRepository } from '../../../../storage' +import { uuid } from '../../../../utils/uuid' +import { AckStatus } from '../../../common' +import { ConnectionService } from '../../../connections' +import { CredentialsModuleConfig } from '../../CredentialsModuleConfig' +import { AutoAcceptCredential, CredentialProblemReportReason, CredentialState } from '../../models' +import { CredentialExchangeRecord, CredentialRepository } from '../../repository' +import { composeAutoAccept } from '../../util/composeAutoAccept' +import { arePreviewAttributesEqual } from '../../util/previewAttributes' +import { BaseCredentialProtocol } from '../BaseCredentialProtocol' + +import { CredentialFormatCoordinator } from './CredentialFormatCoordinator' +import { + V3OfferCredentialHandler, + V3CredentialAckHandler, + V3IssueCredentialHandler, + V3ProposeCredentialHandler, + V3RequestCredentialHandler, +} from './handlers' +import { V3CredentialProblemReportHandler } from './handlers/V3CredentialProblemReportHandler' +import { + V3CredentialAckMessage, + V3CredentialProblemReportMessage, + V3IssueCredentialMessage, + V3OfferCredentialMessage, + V3ProposeCredentialMessage, + V3RequestCredentialMessage, +} from './messages' + +export interface V3CredentialProtocolConfig { + credentialFormats: CredentialFormatServices +} + +export class V3CredentialProtocol + extends BaseCredentialProtocol + implements CredentialProtocol +{ + private credentialFormatCoordinator = new CredentialFormatCoordinator() + private credentialFormats: CFs + + public constructor({ credentialFormats }: V3CredentialProtocolConfig) { + super() + + this.credentialFormats = credentialFormats + } + + /** + * The version of the issue credential protocol this service supports + */ + public readonly version = 'v3' 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 V3 Protocol + dependencyManager.registerMessageHandlers([ + new V3ProposeCredentialHandler(this), + new V3OfferCredentialHandler(this), + new V3RequestCredentialHandler(this), + new V3IssueCredentialHandler(this), + new V3CredentialAckHandler(this), + new V3CredentialProblemReportHandler(this), + ]) + + // Register Issue Credential V3 in feature registry, with supported roles + featureRegistry.register( + new Protocol({ + id: 'https://didcomm.org/issue-credential/2.0', + roles: ['holder', 'issuer'], + }) + ) + } + + /** + * Create a {@link V3ProposeCredentialMessage} not bound to an existing credential exchange. + * + * @param proposal The ProposeCredentialOptions object containing the important fields for the credential message + * @returns Object containing proposal message and associated credential record + * + */ + public async createProposal( + agentContext: AgentContext, + { connectionRecord, credentialFormats, comment, autoAcceptCredential }: CreateCredentialProposalOptions + ): Promise> { + agentContext.config.logger.debug('Get the Format Service and Create Proposal Message') + + const credentialRepository = agentContext.dependencyManager.resolve(CredentialRepository) + + const formatServices = this.getFormatServices(credentialFormats) + if (formatServices.length === 0) { + throw new AriesFrameworkError(`Unable to create proposal. No supported formats`) + } + + const credentialRecord = new CredentialExchangeRecord({ + connectionId: connectionRecord.id, + threadId: uuid(), + state: CredentialState.ProposalSent, + autoAcceptCredential, + protocolVersion: 'v3', + }) + + const proposalMessage = await this.credentialFormatCoordinator.createProposal(agentContext, { + credentialFormats, + credentialRecord, + formatServices, + comment, + }) + + agentContext.config.logger.debug('Save record and emit state change event') + await credentialRepository.save(agentContext, credentialRecord) + this.emitStateChangedEvent(agentContext, credentialRecord, null) + + return { credentialRecord, message: proposalMessage } + } + + /** + * Method called by {@link V3ProposeCredentialHandler} on reception of a propose credential message + * We do the necessary processing here to accept the proposal and do the state change, emit event etc. + * @param messageContext the inbound propose credential message + * @returns credential 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 credential proposal with id ${proposalMessage.id}`) + + const credentialRepository = agentContext.dependencyManager.resolve(CredentialRepository) + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + const connectionService = agentContext.dependencyManager.resolve(ConnectionService) + + let credentialRecord = 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 (credentialRecord) { + const proposalCredentialMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V3ProposeCredentialMessage, + }) + const offerCredentialMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V3OfferCredentialMessage, + }) + + // Assert + credentialRecord.assertProtocolVersion('v3') + credentialRecord.assertState(CredentialState.OfferSent) + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage: proposalCredentialMessage ?? undefined, + lastSentMessage: offerCredentialMessage ?? undefined, + }) + + await this.credentialFormatCoordinator.processProposal(messageContext.agentContext, { + credentialRecord, + formatServices, + message: proposalMessage, + }) + + await this.updateState(messageContext.agentContext, credentialRecord, CredentialState.ProposalReceived) + + return credentialRecord + } else { + // Assert + await connectionService.assertConnectionOrOutOfBandExchange(messageContext) + + // No credential record exists with thread id + credentialRecord = new CredentialExchangeRecord({ + connectionId: connection?.id, + threadId: proposalMessage.threadId, + state: CredentialState.ProposalReceived, + protocolVersion: 'v3', + }) + + await this.credentialFormatCoordinator.processProposal(messageContext.agentContext, { + credentialRecord, + formatServices, + message: proposalMessage, + }) + + // Save record and emit event + await credentialRepository.save(messageContext.agentContext, credentialRecord) + this.emitStateChangedEvent(messageContext.agentContext, credentialRecord, null) + + return credentialRecord + } + } + + public async acceptProposal( + agentContext: AgentContext, + { credentialRecord, credentialFormats, autoAcceptCredential, comment }: AcceptCredentialProposalOptions + ): Promise> { + // Assert + credentialRecord.assertProtocolVersion('v3') + credentialRecord.assertState(CredentialState.ProposalReceived) + + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // Use empty credentialFormats if not provided to denote all formats should be accepted + let formatServices = this.getFormatServices(credentialFormats ?? {}) + + // if no format services could be extracted from the credentialFormats + // take all available format services from the proposal message + if (formatServices.length === 0) { + const proposalMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V3ProposeCredentialMessage, + }) + + 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 offerMessage = await this.credentialFormatCoordinator.acceptProposal(agentContext, { + credentialRecord, + formatServices, + comment, + credentialFormats, + }) + + credentialRecord.autoAcceptCredential = autoAcceptCredential ?? credentialRecord.autoAcceptCredential + await this.updateState(agentContext, credentialRecord, CredentialState.OfferSent) + + return { credentialRecord, message: offerMessage } + } + + /** + * 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 NegotiateCredentialProposalOptions} + * @returns Credential exchange record associated with the credential offer + * + */ + public async negotiateProposal( + agentContext: AgentContext, + { credentialRecord, credentialFormats, autoAcceptCredential, comment }: NegotiateCredentialProposalOptions + ): Promise> { + // Assert + credentialRecord.assertProtocolVersion('v3') + credentialRecord.assertState(CredentialState.ProposalReceived) + + if (!credentialRecord.connectionId) { + throw new AriesFrameworkError( + `No connectionId found for credential record '${credentialRecord.id}'. Connection-less issuance does not support negotiation.` + ) + } + + const formatServices = this.getFormatServices(credentialFormats) + if (formatServices.length === 0) { + throw new AriesFrameworkError(`Unable to create offer. No supported formats`) + } + + const offerMessage = await this.credentialFormatCoordinator.createOffer(agentContext, { + formatServices, + credentialFormats, + credentialRecord, + comment, + }) + + credentialRecord.autoAcceptCredential = autoAcceptCredential ?? credentialRecord.autoAcceptCredential + await this.updateState(agentContext, credentialRecord, CredentialState.OfferSent) + + return { credentialRecord, message: offerMessage } + } + + /** + * Create a {@link V3OfferCredentialMessage} as beginning of protocol process. If no connectionId is provided, the + * exchange will be created without a connection for usage in oob and connection-less issuance. + * + * @param formatService {@link CredentialFormatService} the format service object containing format-specific logic + * @param options attributes of the original offer + * @returns Object containing offer message and associated credential record + * + */ + public async createOffer( + agentContext: AgentContext, + { credentialFormats, autoAcceptCredential, comment, connectionRecord }: CreateCredentialOfferOptions + ): Promise> { + const credentialRepository = agentContext.dependencyManager.resolve(CredentialRepository) + + const formatServices = this.getFormatServices(credentialFormats) + if (formatServices.length === 0) { + throw new AriesFrameworkError(`Unable to create offer. No supported formats`) + } + + const credentialRecord = new CredentialExchangeRecord({ + connectionId: connectionRecord?.id, + threadId: uuid(), + state: CredentialState.OfferSent, + autoAcceptCredential, + protocolVersion: 'v3', + }) + + const offerMessage = await this.credentialFormatCoordinator.createOffer(agentContext, { + formatServices, + credentialFormats, + credentialRecord, + comment, + }) + + agentContext.config.logger.debug( + `Saving record and emitting state changed for credential exchange record ${credentialRecord.id}` + ) + await credentialRepository.save(agentContext, credentialRecord) + this.emitStateChangedEvent(agentContext, credentialRecord, null) + + return { credentialRecord, message: offerMessage } + } + + /** + * Method called by {@link V3OfferCredentialHandler} on reception of a offer credential message + * We do the necessary processing here to accept the offer and do the state change, emit event etc. + * @param messageContext the inbound offer credential message + * @returns credential record appropriate for this incoming message (once accepted) + */ + public async processOffer( + messageContext: MessageHandlerInboundMessage + ): Promise { + const { message: offerMessage, connection, agentContext } = messageContext + + agentContext.config.logger.debug(`Processing credential offer with id ${offerMessage.id}`) + + const credentialRepository = agentContext.dependencyManager.resolve(CredentialRepository) + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + const connectionService = agentContext.dependencyManager.resolve(ConnectionService) + + let credentialRecord = await this.findByThreadAndConnectionId( + messageContext.agentContext, + offerMessage.threadId, + connection?.id + ) + + const formatServices = this.getFormatServicesFromMessage(offerMessage.formats) + if (formatServices.length === 0) { + throw new AriesFrameworkError(`Unable to process offer. No supported formats`) + } + + // credential record already exists + if (credentialRecord) { + const proposeCredentialMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V3ProposeCredentialMessage, + }) + const offerCredentialMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V3OfferCredentialMessage, + }) + + credentialRecord.assertProtocolVersion('v3') + credentialRecord.assertState(CredentialState.ProposalSent) + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage: offerCredentialMessage ?? undefined, + lastSentMessage: proposeCredentialMessage ?? undefined, + }) + + await this.credentialFormatCoordinator.processOffer(messageContext.agentContext, { + credentialRecord, + formatServices, + message: offerMessage, + }) + + await this.updateState(messageContext.agentContext, credentialRecord, CredentialState.OfferReceived) + return credentialRecord + } else { + // Assert + await connectionService.assertConnectionOrOutOfBandExchange(messageContext) + + // No credential record exists with thread id + agentContext.config.logger.debug('No credential record found for offer, creating a new one') + credentialRecord = new CredentialExchangeRecord({ + connectionId: connection?.id, + threadId: offerMessage.threadId, + state: CredentialState.OfferReceived, + protocolVersion: 'v3', + }) + + await this.credentialFormatCoordinator.processOffer(messageContext.agentContext, { + credentialRecord, + formatServices, + message: offerMessage, + }) + + // Save in repository + agentContext.config.logger.debug('Saving credential record and emit offer-received event') + await credentialRepository.save(messageContext.agentContext, credentialRecord) + + this.emitStateChangedEvent(messageContext.agentContext, credentialRecord, null) + return credentialRecord + } + } + + public async acceptOffer( + agentContext: AgentContext, + { credentialRecord, autoAcceptCredential, comment, credentialFormats }: AcceptCredentialOfferOptions + ) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // Assert + credentialRecord.assertProtocolVersion('v3') + credentialRecord.assertState(CredentialState.OfferReceived) + + // Use empty credentialFormats if not provided to denote all formats should be accepted + let formatServices = this.getFormatServices(credentialFormats ?? {}) + + // if no format services could be extracted from the credentialFormats + // take all available format services from the offer message + if (formatServices.length === 0) { + const offerMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V3OfferCredentialMessage, + }) + + formatServices = this.getFormatServicesFromMessage(offerMessage.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 offer. No supported formats provided as input or in offer message` + ) + } + + const message = await this.credentialFormatCoordinator.acceptOffer(agentContext, { + credentialRecord, + formatServices, + comment, + credentialFormats, + }) + + credentialRecord.autoAcceptCredential = autoAcceptCredential ?? credentialRecord.autoAcceptCredential + await this.updateState(agentContext, credentialRecord, CredentialState.RequestSent) + + return { credentialRecord, message } + } + + /** + * Create a {@link ProposePresentationMessage} as response to a received credential offer. + * To create a proposal not bound to an existing credential exchange, use {@link createProposal}. + * + * @param options configuration to use for the proposal + * @returns Object containing proposal message and associated credential record + * + */ + public async negotiateOffer( + agentContext: AgentContext, + { credentialRecord, credentialFormats, autoAcceptCredential, comment }: NegotiateCredentialOfferOptions + ): Promise> { + // Assert + credentialRecord.assertProtocolVersion('v3') + credentialRecord.assertState(CredentialState.OfferReceived) + + if (!credentialRecord.connectionId) { + throw new AriesFrameworkError( + `No connectionId found for credential record '${credentialRecord.id}'. Connection-less issuance does not support negotiation.` + ) + } + + const formatServices = this.getFormatServices(credentialFormats) + if (formatServices.length === 0) { + throw new AriesFrameworkError(`Unable to create proposal. No supported formats`) + } + + const proposalMessage = await this.credentialFormatCoordinator.createProposal(agentContext, { + formatServices, + credentialFormats, + credentialRecord, + comment, + }) + + credentialRecord.autoAcceptCredential = autoAcceptCredential ?? credentialRecord.autoAcceptCredential + await this.updateState(agentContext, credentialRecord, CredentialState.ProposalSent) + + return { credentialRecord, message: proposalMessage } + } + + /** + * Create a {@link V3RequestCredentialMessage} as beginning of protocol process. + * @returns Object containing offer message and associated credential record + * + */ + public async createRequest( + agentContext: AgentContext, + { credentialFormats, autoAcceptCredential, comment, connectionRecord }: CreateCredentialRequestOptions + ): Promise> { + const credentialRepository = agentContext.dependencyManager.resolve(CredentialRepository) + + const formatServices = this.getFormatServices(credentialFormats) + if (formatServices.length === 0) { + throw new AriesFrameworkError(`Unable to create request. No supported formats`) + } + + const credentialRecord = new CredentialExchangeRecord({ + connectionId: connectionRecord.id, + threadId: uuid(), + state: CredentialState.RequestSent, + autoAcceptCredential, + protocolVersion: 'v3', + }) + + const requestMessage = await this.credentialFormatCoordinator.createRequest(agentContext, { + formatServices, + credentialFormats, + credentialRecord, + comment, + }) + + agentContext.config.logger.debug( + `Saving record and emitting state changed for credential exchange record ${credentialRecord.id}` + ) + await credentialRepository.save(agentContext, credentialRecord) + this.emitStateChangedEvent(agentContext, credentialRecord, null) + + return { credentialRecord, message: requestMessage } + } + + /** + * Process a received {@link RequestCredentialMessage}. This will not accept the credential request + * or send a credential. It will only update the existing credential record with + * the information from the credential request message. Use {@link createCredential} + * after calling this method to create a credential. + *z + * @param messageContext The message context containing a v3 credential request message + * @returns credential record associated with the credential request message + * + */ + public async processRequest( + messageContext: InboundMessageContext + ): Promise { + const { message: requestMessage, connection, agentContext } = messageContext + + const credentialRepository = agentContext.dependencyManager.resolve(CredentialRepository) + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + const connectionService = agentContext.dependencyManager.resolve(ConnectionService) + + agentContext.config.logger.debug(`Processing credential request with id ${requestMessage.id}`) + + let credentialRecord = 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`) + } + + // credential record already exists + if (credentialRecord) { + const proposalMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V3ProposeCredentialMessage, + }) + + const offerMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V3OfferCredentialMessage, + }) + + // Assert + credentialRecord.assertProtocolVersion('v3') + credentialRecord.assertState(CredentialState.OfferSent) + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage: proposalMessage ?? undefined, + lastSentMessage: offerMessage ?? undefined, + }) + + await this.credentialFormatCoordinator.processRequest(messageContext.agentContext, { + credentialRecord, + formatServices, + message: requestMessage, + }) + + await this.updateState(messageContext.agentContext, credentialRecord, CredentialState.RequestReceived) + return credentialRecord + } else { + // Assert + await connectionService.assertConnectionOrOutOfBandExchange(messageContext) + + // No credential record exists with thread id + agentContext.config.logger.debug('No credential record found for request, creating a new one') + credentialRecord = new CredentialExchangeRecord({ + connectionId: connection?.id, + threadId: requestMessage.threadId, + state: CredentialState.RequestReceived, + protocolVersion: 'v3', + }) + + await this.credentialFormatCoordinator.processRequest(messageContext.agentContext, { + credentialRecord, + formatServices, + message: requestMessage, + }) + + // Save in repository + agentContext.config.logger.debug('Saving credential record and emit request-received event') + await credentialRepository.save(messageContext.agentContext, credentialRecord) + + this.emitStateChangedEvent(messageContext.agentContext, credentialRecord, null) + return credentialRecord + } + } + + public async acceptRequest( + agentContext: AgentContext, + { credentialRecord, autoAcceptCredential, comment, credentialFormats }: AcceptCredentialRequestOptions + ) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + // Assert + credentialRecord.assertProtocolVersion('v3') + credentialRecord.assertState(CredentialState.RequestReceived) + + // Use empty credentialFormats if not provided to denote all formats should be accepted + let formatServices = this.getFormatServices(credentialFormats ?? {}) + + // if no format services could be extracted from the credentialFormats + // take all available format services from the request message + if (formatServices.length === 0) { + const requestMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V3RequestCredentialMessage, + }) + + 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.credentialFormatCoordinator.acceptRequest(agentContext, { + credentialRecord, + formatServices, + comment, + credentialFormats, + }) + + credentialRecord.autoAcceptCredential = autoAcceptCredential ?? credentialRecord.autoAcceptCredential + await this.updateState(agentContext, credentialRecord, CredentialState.CredentialIssued) + + return { credentialRecord, message } + } + + /** + * Process a received {@link V3IssueCredentialMessage}. This will not accept the credential + * or send a credential acknowledgement. It will only update the existing credential record with + * the information from the issue credential message. Use {@link createAck} + * after calling this method to create a credential acknowledgement. + * + * @param messageContext The message context containing an issue credential message + * + * @returns credential record associated with the issue credential message + * + */ + public async processCredential( + messageContext: InboundMessageContext + ): Promise { + const { message: credentialMessage, connection, agentContext } = messageContext + + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + const connectionService = agentContext.dependencyManager.resolve(ConnectionService) + + agentContext.config.logger.debug(`Processing credential with id ${credentialMessage.id}`) + + const credentialRecord = await this.getByThreadAndConnectionId( + messageContext.agentContext, + credentialMessage.threadId, + connection?.id + ) + + const requestMessage = await didCommMessageRepository.getAgentMessage(messageContext.agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V3RequestCredentialMessage, + }) + const offerMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V3OfferCredentialMessage, + }) + + // Assert + credentialRecord.assertProtocolVersion('v3') + credentialRecord.assertState(CredentialState.RequestSent) + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage: offerMessage ?? undefined, + lastSentMessage: requestMessage, + }) + + const formatServices = this.getFormatServicesFromMessage(credentialMessage.formats) + if (formatServices.length === 0) { + throw new AriesFrameworkError(`Unable to process credential. No supported formats`) + } + + await this.credentialFormatCoordinator.processCredential(messageContext.agentContext, { + credentialRecord, + formatServices, + requestMessage: requestMessage, + message: credentialMessage, + }) + + await this.updateState(messageContext.agentContext, credentialRecord, CredentialState.CredentialReceived) + + return credentialRecord + } + + /** + * Create a {@link V3CredentialAckMessage} as response to a received credential. + * + * @param credentialRecord The credential record for which to create the credential acknowledgement + * @returns Object containing credential acknowledgement message and associated credential record + * + */ + public async acceptCredential( + agentContext: AgentContext, + { credentialRecord }: AcceptCredentialOptions + ): Promise> { + credentialRecord.assertProtocolVersion('v3') + credentialRecord.assertState(CredentialState.CredentialReceived) + + // Create message + const ackMessage = new V3CredentialAckMessage({ + status: AckStatus.OK, + threadId: credentialRecord.threadId, + }) + + await this.updateState(agentContext, credentialRecord, CredentialState.Done) + + return { message: ackMessage, credentialRecord } + } + + /** + * Process a received {@link CredentialAckMessage}. + * + * @param messageContext The message context containing a credential acknowledgement message + * @returns credential record associated with the credential acknowledgement message + * + */ + public async processAck( + messageContext: InboundMessageContext + ): Promise { + const { message: ackMessage, connection, agentContext } = messageContext + + agentContext.config.logger.debug(`Processing credential ack with id ${ackMessage.id}`) + + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + const connectionService = agentContext.dependencyManager.resolve(ConnectionService) + + const credentialRecord = await this.getByThreadAndConnectionId( + messageContext.agentContext, + ackMessage.threadId, + connection?.id + ) + credentialRecord.connectionId = connection?.id + + const requestMessage = await didCommMessageRepository.getAgentMessage(messageContext.agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V3RequestCredentialMessage, + }) + + const credentialMessage = await didCommMessageRepository.getAgentMessage(messageContext.agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V3IssueCredentialMessage, + }) + + // Assert + credentialRecord.assertProtocolVersion('v3') + credentialRecord.assertState(CredentialState.CredentialIssued) + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage: requestMessage, + lastSentMessage: credentialMessage, + }) + + // Update record + await this.updateState(messageContext.agentContext, credentialRecord, CredentialState.Done) + + return credentialRecord + } + + /** + * Create a {@link V3CredentialProblemReportMessage} to be sent. + * + * @param message message to send + * @returns a {@link V3CredentialProblemReportMessage} + * + */ + public async createProblemReport( + agentContext: AgentContext, + { credentialRecord, description }: CreateCredentialProblemReportOptions + ): Promise> { + const message = new V3CredentialProblemReportMessage({ + parentThreadId: credentialRecord.threadId, + body: { + comment: description, + code: CredentialProblemReportReason.IssuanceAbandoned, + }, + }) + + return { credentialRecord, message } + } + + // AUTO ACCEPT METHODS + public async shouldAutoRespondToProposal( + agentContext: AgentContext, + options: { + credentialRecord: CredentialExchangeRecord + proposalMessage: V3ProposeCredentialMessage + } + ): Promise { + const { credentialRecord, proposalMessage } = options + const credentialsModuleConfig = agentContext.dependencyManager.resolve(CredentialsModuleConfig) + + const autoAccept = composeAutoAccept( + credentialRecord.autoAcceptCredential, + credentialsModuleConfig.autoAcceptCredentials + ) + + // Handle always / never cases + if (autoAccept === AutoAcceptCredential.Always) return true + if (autoAccept === AutoAcceptCredential.Never) return false + + const offerMessage = await this.findOfferMessage(agentContext, credentialRecord.id) + if (!offerMessage) return false + + // 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(offerMessage.formats) + + for (const formatService of formatServices) { + const offerAttachment = this.credentialFormatCoordinator.getAttachmentForService( + formatService, + offerMessage.formats, + offerMessage.offerAttachments + ) + + const proposalAttachment = this.credentialFormatCoordinator.getAttachmentForService( + formatService, + proposalMessage.formats, + proposalMessage.proposalAttachments + ) + + const shouldAutoRespondToFormat = await formatService.shouldAutoRespondToProposal(agentContext, { + credentialRecord, + offerAttachment, + proposalAttachment, + }) + // If any of the formats return false, we should not auto accept + if (!shouldAutoRespondToFormat) return false + } + + // not all formats use the proposal and preview, we only check if they're present on + // either or both of the messages + if (proposalMessage.credentialPreview || offerMessage.credentialPreview) { + // if one of the message doesn't have a preview, we should not auto accept + if (!proposalMessage.credentialPreview || !offerMessage.credentialPreview) return false + + // Check if preview values match + return arePreviewAttributesEqual( + proposalMessage.credentialPreview.attributes, + offerMessage.credentialPreview.attributes + ) + } + + return true + } + + public async shouldAutoRespondToOffer( + agentContext: AgentContext, + options: { + credentialRecord: CredentialExchangeRecord + offerMessage: V3OfferCredentialMessage + } + ): Promise { + const { credentialRecord, offerMessage } = options + const credentialsModuleConfig = agentContext.dependencyManager.resolve(CredentialsModuleConfig) + + const autoAccept = composeAutoAccept( + credentialRecord.autoAcceptCredential, + credentialsModuleConfig.autoAcceptCredentials + ) + // Handle always / never cases + if (autoAccept === AutoAcceptCredential.Always) return true + if (autoAccept === AutoAcceptCredential.Never) return false + + const proposalMessage = await this.findProposalMessage(agentContext, credentialRecord.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 offer, which could be different from the formats + // we use. + const formatServices = this.getFormatServicesFromMessage(proposalMessage.formats) + + for (const formatService of formatServices) { + const offerAttachment = this.credentialFormatCoordinator.getAttachmentForService( + formatService, + offerMessage.formats, + offerMessage.offerAttachments + ) + + const proposalAttachment = this.credentialFormatCoordinator.getAttachmentForService( + formatService, + proposalMessage.formats, + proposalMessage.proposalAttachments + ) + + const shouldAutoRespondToFormat = await formatService.shouldAutoRespondToOffer(agentContext, { + credentialRecord, + offerAttachment, + proposalAttachment, + }) + + // If any of the formats return false, we should not auto accept + + if (!shouldAutoRespondToFormat) return false + } + + // if one of the message doesn't have a preview, we should not auto accept + if (proposalMessage.credentialPreview || offerMessage.credentialPreview) { + // Check if preview values match + return arePreviewAttributesEqual( + proposalMessage.credentialPreview?.attributes ?? [], + offerMessage.credentialPreview?.attributes ?? [] + ) + } + return true + } + + public async shouldAutoRespondToRequest( + agentContext: AgentContext, + options: { + credentialRecord: CredentialExchangeRecord + requestMessage: V3RequestCredentialMessage + } + ): Promise { + const { credentialRecord, requestMessage } = options + const credentialsModuleConfig = agentContext.dependencyManager.resolve(CredentialsModuleConfig) + + const autoAccept = composeAutoAccept( + credentialRecord.autoAcceptCredential, + credentialsModuleConfig.autoAcceptCredentials + ) + + // Handle always / never cases + if (autoAccept === AutoAcceptCredential.Always) return true + if (autoAccept === AutoAcceptCredential.Never) return false + + const proposalMessage = await this.findProposalMessage(agentContext, credentialRecord.id) + + const offerMessage = await this.findOfferMessage(agentContext, credentialRecord.id) + if (!offerMessage) return false + + // 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(offerMessage.formats) + + for (const formatService of formatServices) { + const offerAttachment = this.credentialFormatCoordinator.getAttachmentForService( + formatService, + offerMessage.formats, + offerMessage.offerAttachments + ) + + const proposalAttachment = proposalMessage + ? this.credentialFormatCoordinator.getAttachmentForService( + formatService, + proposalMessage.formats, + proposalMessage.proposalAttachments + ) + : undefined + + const requestAttachment = this.credentialFormatCoordinator.getAttachmentForService( + formatService, + requestMessage.formats, + requestMessage.requestAttachments + ) + + const shouldAutoRespondToFormat = await formatService.shouldAutoRespondToRequest(agentContext, { + credentialRecord, + offerAttachment, + requestAttachment, + proposalAttachment, + }) + + // If any of the formats return false, we should not auto accept + if (!shouldAutoRespondToFormat) return false + } + + return true + } + + public async shouldAutoRespondToCredential( + agentContext: AgentContext, + options: { + credentialRecord: CredentialExchangeRecord + credentialMessage: V3IssueCredentialMessage + } + ): Promise { + const { credentialRecord, credentialMessage } = options + const credentialsModuleConfig = agentContext.dependencyManager.resolve(CredentialsModuleConfig) + + const autoAccept = composeAutoAccept( + credentialRecord.autoAcceptCredential, + credentialsModuleConfig.autoAcceptCredentials + ) + + // Handle always / never cases + if (autoAccept === AutoAcceptCredential.Always) return true + if (autoAccept === AutoAcceptCredential.Never) return false + + const proposalMessage = await this.findProposalMessage(agentContext, credentialRecord.id) + const offerMessage = await this.findOfferMessage(agentContext, credentialRecord.id) + + const requestMessage = await this.findRequestMessage(agentContext, credentialRecord.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 credential, which could be different from the formats + // we use. + const formatServices = this.getFormatServicesFromMessage(requestMessage.formats) + + for (const formatService of formatServices) { + const offerAttachment = offerMessage + ? this.credentialFormatCoordinator.getAttachmentForService( + formatService, + offerMessage.formats, + offerMessage.offerAttachments + ) + : undefined + + const proposalAttachment = proposalMessage + ? this.credentialFormatCoordinator.getAttachmentForService( + formatService, + proposalMessage.formats, + proposalMessage.proposalAttachments + ) + : undefined + + const requestAttachment = this.credentialFormatCoordinator.getAttachmentForService( + formatService, + requestMessage.formats, + requestMessage.requestAttachments + ) + + const credentialAttachment = this.credentialFormatCoordinator.getAttachmentForService( + formatService, + credentialMessage.formats, + credentialMessage.credentialAttachments + ) + + const shouldAutoRespondToFormat = await formatService.shouldAutoRespondToCredential(agentContext, { + credentialRecord, + offerAttachment, + credentialAttachment, + requestAttachment, + proposalAttachment, + }) + + // If any of the formats return false, we should not auto accept + if (!shouldAutoRespondToFormat) return false + } + return true + } + + public async findProposalMessage(agentContext: AgentContext, credentialExchangeId: string) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + return didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: credentialExchangeId, + messageClass: V3ProposeCredentialMessage, + }) + } + + public async findOfferMessage(agentContext: AgentContext, credentialExchangeId: string) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + return await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: credentialExchangeId, + messageClass: V3OfferCredentialMessage, + }) + } + + public async findRequestMessage(agentContext: AgentContext, credentialExchangeId: string) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + return await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: credentialExchangeId, + messageClass: V3RequestCredentialMessage, + }) + } + + public async findCredentialMessage(agentContext: AgentContext, credentialExchangeId: string) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + return await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: credentialExchangeId, + messageClass: V3IssueCredentialMessage, + }) + } + + public async getFormatData( + agentContext: AgentContext, + credentialExchangeId: 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, offerMessage, requestMessage, credentialMessage] = await Promise.all([ + this.findProposalMessage(agentContext, credentialExchangeId), + this.findOfferMessage(agentContext, credentialExchangeId), + this.findRequestMessage(agentContext, credentialExchangeId), + this.findCredentialMessage(agentContext, credentialExchangeId), + ]) + + // 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], + offer: [offerMessage?.formats, offerMessage?.offerAttachments], + request: [requestMessage?.formats, requestMessage?.requestAttachments], + credential: [credentialMessage?.formats, credentialMessage?.credentialAttachments], + } as const + + const formatData: GetCredentialFormatDataReturn = { + proposalAttributes: proposalMessage?.credentialPreview?.attributes, + offerAttributes: offerMessage?.credentialPreview?.attributes, + } + + // 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: 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) + for (const formatService of formatServices) { + const attachment = this.credentialFormatCoordinator.getAttachmentForService(formatService, formats, attachments) + + messageFormatData[formatService.formatKey] = attachment.getDataAsJson() + } + + formatData[messageKey as Exclude] = + messageFormatData + } + + return formatData + } + + /** + * Get all the format service objects for a given credential format from an incoming message + * @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(messageFormats: CredentialFormatSpec[]): CredentialFormatService[] { + 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 credential format + * @param credentialFormats the format object containing various optional parameters + * @return the credential format service objects in an array - derived from format object keys + */ + private getFormatServices( + credentialFormats: CredentialFormatPayload, M> + ): CredentialFormatService[] { + const formats = new Set() + + for (const formatKey of Object.keys(credentialFormats)) { + const formatService = this.getFormatServiceForFormatKey(formatKey) + + if (formatService) formats.add(formatService) + } + + return Array.from(formats) + } + + private getFormatServiceForFormatKey(formatKey: string): CredentialFormatService | null { + const formatService = this.credentialFormats.find((credentialFormat) => credentialFormat.formatKey === formatKey) + + return formatService ?? null + } + + private getFormatServiceForFormat(format: string): CredentialFormatService | null { + const formatService = this.credentialFormats.find((credentialFormat) => credentialFormat.supportsFormat(format)) + + return formatService ?? null + } + + protected getFormatServiceForRecordType(credentialRecordType: string) { + const formatService = this.credentialFormats.find( + (credentialFormat) => credentialFormat.credentialRecordType === credentialRecordType + ) + + if (!formatService) { + throw new AriesFrameworkError( + `No format service found for credential record type ${credentialRecordType} in v3 credential protocol` + ) + } + + return formatService + } +} diff --git a/packages/core/src/modules/credentials/protocol/v3/errors/V3CredentialProblemReportError.ts b/packages/core/src/modules/credentials/protocol/v3/errors/V3CredentialProblemReportError.ts new file mode 100644 index 0000000000..1c593fc158 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v3/errors/V3CredentialProblemReportError.ts @@ -0,0 +1,25 @@ +import type { ProblemReportErrorOptions } from '../../../../problem-reports' +import type { CredentialProblemReportReason } from '../../../models/CredentialProblemReportReason' + +import { V2ProblemReportError } from '../../../../problem-reports/errors/V2ProblemReportError' +import { V3CredentialProblemReportMessage } from '../messages/V3CredentialProblemReportMessage' + +export interface V3CredentialProblemReportErrorOptions extends ProblemReportErrorOptions { + problemCode: CredentialProblemReportReason + parentThreadId: string +} + +export class V3CredentialProblemReportError extends V2ProblemReportError { + public problemReport: V3CredentialProblemReportMessage + + public constructor(message: string, { problemCode, parentThreadId }: V3CredentialProblemReportErrorOptions) { + super(message, { problemCode, parentThreadId }) + this.problemReport = new V3CredentialProblemReportMessage({ + parentThreadId, + body: { + code: problemCode, + comment: message, + }, + }) + } +} diff --git a/packages/core/src/modules/credentials/protocol/v3/errors/index.ts b/packages/core/src/modules/credentials/protocol/v3/errors/index.ts new file mode 100644 index 0000000000..498d0e065c --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v3/errors/index.ts @@ -0,0 +1 @@ +export { V3CredentialProblemReportError, V3CredentialProblemReportErrorOptions } from './V3CredentialProblemReportError' diff --git a/packages/core/src/modules/credentials/protocol/v3/handlers/V2IssueCredentialHandler.ts b/packages/core/src/modules/credentials/protocol/v3/handlers/V2IssueCredentialHandler.ts new file mode 100644 index 0000000000..82f43cc478 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v3/handlers/V2IssueCredentialHandler.ts @@ -0,0 +1,56 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' +import type { InboundMessageContext } from '../../../../../agent/models/InboundMessageContext' +import type { CredentialExchangeRecord } from '../../../repository/CredentialExchangeRecord' +import type { V3CredentialProtocol } from '../V3CredentialProtocol' + +import { getOutboundMessageContext } from '../../../../../agent/getOutboundMessageContext' +import { AriesFrameworkError } from '../../../../../error' +import { V3IssueCredentialMessage } from '../messages/V3IssueCredentialMessage' + +export class V3IssueCredentialHandler implements MessageHandler { + private credentialProtocol: V3CredentialProtocol + public supportedMessages = [V3IssueCredentialMessage] + + public constructor(credentialProtocol: V3CredentialProtocol) { + this.credentialProtocol = credentialProtocol + } + + public async handle(messageContext: InboundMessageContext) { + const credentialRecord = await this.credentialProtocol.processCredential(messageContext) + + const shouldAutoRespond = await this.credentialProtocol.shouldAutoRespondToCredential(messageContext.agentContext, { + credentialRecord, + credentialMessage: messageContext.message, + }) + + if (shouldAutoRespond) { + return await this.acceptCredential(credentialRecord, messageContext) + } + } + + private async acceptCredential( + credentialRecord: CredentialExchangeRecord, + messageContext: MessageHandlerInboundMessage + ) { + messageContext.agentContext.config.logger.info(`Automatically sending acknowledgement with autoAccept`) + const { message } = await this.credentialProtocol.acceptCredential(messageContext.agentContext, { + credentialRecord, + }) + + const requestMessage = await this.credentialProtocol.findRequestMessage( + messageContext.agentContext, + credentialRecord.id + ) + if (!requestMessage) { + throw new AriesFrameworkError(`No request message found for credential record with id '${credentialRecord.id}'`) + } + + return getOutboundMessageContext(messageContext.agentContext, { + connectionRecord: messageContext.connection, + message, + associatedRecord: credentialRecord, + lastReceivedMessage: messageContext.message, + lastSentMessage: requestMessage, + }) + } +} diff --git a/packages/core/src/modules/credentials/protocol/v3/handlers/V2OfferCredentialHandler.ts b/packages/core/src/modules/credentials/protocol/v3/handlers/V2OfferCredentialHandler.ts new file mode 100644 index 0000000000..b00dbebd52 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v3/handlers/V2OfferCredentialHandler.ts @@ -0,0 +1,45 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' +import type { InboundMessageContext } from '../../../../../agent/models/InboundMessageContext' +import type { CredentialExchangeRecord } from '../../../repository/CredentialExchangeRecord' +import type { V2CredentialProtocol } from '../V3CredentialProtocol' + +import { getOutboundMessageContext } from '../../../../../agent/getOutboundMessageContext' +import { V3OfferCredentialMessage } from '../messages/V3OfferCredentialMessage' + +export class V2OfferCredentialHandler implements MessageHandler { + private credentialProtocol: V2CredentialProtocol + + public supportedMessages = [V3OfferCredentialMessage] + + public constructor(credentialProtocol: V2CredentialProtocol) { + this.credentialProtocol = credentialProtocol + } + + public async handle(messageContext: InboundMessageContext) { + const credentialRecord = await this.credentialProtocol.processOffer(messageContext) + + const shouldAutoRespond = await this.credentialProtocol.shouldAutoRespondToOffer(messageContext.agentContext, { + credentialRecord, + offerMessage: messageContext.message, + }) + if (shouldAutoRespond) { + return await this.acceptOffer(credentialRecord, messageContext) + } + } + + private async acceptOffer( + credentialRecord: CredentialExchangeRecord, + messageContext: MessageHandlerInboundMessage + ) { + messageContext.agentContext.config.logger.info(`Automatically sending request with autoAccept`) + + const { message } = await this.credentialProtocol.acceptOffer(messageContext.agentContext, { credentialRecord }) + + return getOutboundMessageContext(messageContext.agentContext, { + connectionRecord: messageContext.connection, + message, + associatedRecord: credentialRecord, + lastReceivedMessage: messageContext.message, + }) + } +} diff --git a/packages/core/src/modules/credentials/protocol/v3/handlers/V2ProposeCredentialHandler.ts b/packages/core/src/modules/credentials/protocol/v3/handlers/V2ProposeCredentialHandler.ts new file mode 100644 index 0000000000..ddb3a09943 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v3/handlers/V2ProposeCredentialHandler.ts @@ -0,0 +1,50 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' +import type { InboundMessageContext } from '../../../../../agent/models/InboundMessageContext' +import type { CredentialExchangeRecord } from '../../../repository/CredentialExchangeRecord' +import type { V2CredentialProtocol } from '../V3CredentialProtocol' + +import { OutboundMessageContext } from '../../../../../agent/models' +import { V3ProposeCredentialMessage } from '../messages/V3ProposeCredentialMessage' + +export class V2ProposeCredentialHandler implements MessageHandler { + private credentialProtocol: V2CredentialProtocol + + public supportedMessages = [V3ProposeCredentialMessage] + + public constructor(credentialProtocol: V2CredentialProtocol) { + this.credentialProtocol = credentialProtocol + } + + public async handle(messageContext: InboundMessageContext) { + const credentialRecord = await this.credentialProtocol.processProposal(messageContext) + + const shouldAutoRespond = await this.credentialProtocol.shouldAutoRespondToProposal(messageContext.agentContext, { + credentialRecord, + proposalMessage: messageContext.message, + }) + + if (shouldAutoRespond) { + return await this.acceptProposal(credentialRecord, messageContext) + } + } + + private async acceptProposal( + credentialRecord: CredentialExchangeRecord, + messageContext: MessageHandlerInboundMessage + ) { + messageContext.agentContext.config.logger.info(`Automatically sending offer with autoAccept`) + + if (!messageContext.connection) { + messageContext.agentContext.config.logger.error('No connection on the messageContext, aborting auto accept') + return + } + + const { message } = await this.credentialProtocol.acceptProposal(messageContext.agentContext, { credentialRecord }) + + return new OutboundMessageContext(message, { + agentContext: messageContext.agentContext, + connection: messageContext.connection, + associatedRecord: credentialRecord, + }) + } +} diff --git a/packages/core/src/modules/credentials/protocol/v3/handlers/V2RequestCredentialHandler.ts b/packages/core/src/modules/credentials/protocol/v3/handlers/V2RequestCredentialHandler.ts new file mode 100644 index 0000000000..1abb188ef4 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v3/handlers/V2RequestCredentialHandler.ts @@ -0,0 +1,58 @@ +import type { MessageHandler } from '../../../../../agent/MessageHandler' +import type { InboundMessageContext } from '../../../../../agent/models/InboundMessageContext' +import type { CredentialExchangeRecord } from '../../../repository' +import type { V2CredentialProtocol } from '../V3CredentialProtocol' + +import { getOutboundMessageContext } from '../../../../../agent/getOutboundMessageContext' +import { AriesFrameworkError } from '../../../../../error' +import { V3RequestCredentialMessage } from '../messages/V3RequestCredentialMessage' + +export class V2RequestCredentialHandler implements MessageHandler { + private credentialProtocol: V2CredentialProtocol + + public supportedMessages = [V3RequestCredentialMessage] + + public constructor(credentialProtocol: V2CredentialProtocol) { + this.credentialProtocol = credentialProtocol + } + + public async handle(messageContext: InboundMessageContext) { + const credentialRecord = await this.credentialProtocol.processRequest(messageContext) + + const shouldAutoRespond = await this.credentialProtocol.shouldAutoRespondToRequest(messageContext.agentContext, { + credentialRecord, + requestMessage: messageContext.message, + }) + + if (shouldAutoRespond) { + return await this.acceptRequest(credentialRecord, messageContext) + } + } + + private async acceptRequest( + credentialRecord: CredentialExchangeRecord, + messageContext: InboundMessageContext + ) { + messageContext.agentContext.config.logger.info(`Automatically sending credential with autoAccept`) + + const offerMessage = await this.credentialProtocol.findOfferMessage( + messageContext.agentContext, + credentialRecord.id + ) + if (!offerMessage) { + throw new AriesFrameworkError(`Could not find offer message for credential record with id ${credentialRecord.id}`) + } + + const { message } = await this.credentialProtocol.acceptRequest(messageContext.agentContext, { + credentialRecord, + }) + + return getOutboundMessageContext(messageContext.agentContext, { + connectionRecord: messageContext.connection, + message, + associatedRecord: credentialRecord, + lastReceivedMessage: messageContext.message, + lastSentMessage: offerMessage, + }) + } +} diff --git a/packages/core/src/modules/credentials/protocol/v3/handlers/V3CredentialAckHandler.ts b/packages/core/src/modules/credentials/protocol/v3/handlers/V3CredentialAckHandler.ts new file mode 100644 index 0000000000..758d886e5d --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v3/handlers/V3CredentialAckHandler.ts @@ -0,0 +1,17 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' +import type { V3CredentialProtocol } from '../V3CredentialProtocol' + +import { V3CredentialAckMessage } from '../messages/V3CredentialAckMessage' + +export class V3CredentialAckHandler implements MessageHandler { + private credentialProtocol: V3CredentialProtocol + public supportedMessages = [V3CredentialAckMessage] + + public constructor(credentialProtocol: V3CredentialProtocol) { + this.credentialProtocol = credentialProtocol + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + await this.credentialProtocol.processAck(messageContext) + } +} diff --git a/packages/core/src/modules/credentials/protocol/v3/handlers/V3CredentialProblemReportHandler.ts b/packages/core/src/modules/credentials/protocol/v3/handlers/V3CredentialProblemReportHandler.ts new file mode 100644 index 0000000000..06daeacab3 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v3/handlers/V3CredentialProblemReportHandler.ts @@ -0,0 +1,17 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' +import type { V3CredentialProtocol } from '../V3CredentialProtocol' + +import { V3CredentialProblemReportMessage } from '../messages/V3CredentialProblemReportMessage' + +export class V3CredentialProblemReportHandler implements MessageHandler { + private credentialProtocol: V3CredentialProtocol + public supportedMessages = [V3CredentialProblemReportMessage] + + public constructor(credentialProtocol: V3CredentialProtocol) { + this.credentialProtocol = credentialProtocol + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + await this.credentialProtocol.processProblemReport(messageContext) + } +} diff --git a/packages/core/src/modules/credentials/protocol/v3/handlers/index.ts b/packages/core/src/modules/credentials/protocol/v3/handlers/index.ts new file mode 100644 index 0000000000..7151e92f26 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v3/handlers/index.ts @@ -0,0 +1,6 @@ +export * from './V3CredentialAckHandler' +export * from './V2IssueCredentialHandler' +export * from './V2OfferCredentialHandler' +export * from './V2ProposeCredentialHandler' +export * from './V2RequestCredentialHandler' +export * from './V3CredentialProblemReportHandler' diff --git a/packages/core/src/modules/credentials/protocol/v3/index.ts b/packages/core/src/modules/credentials/protocol/v3/index.ts new file mode 100644 index 0000000000..b48358825d --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v3/index.ts @@ -0,0 +1,3 @@ +export * from './V3CredentialProtocol' +export * from './messages' +export * from './errors' diff --git a/packages/core/src/modules/credentials/protocol/v3/messages/V3CredentialAckMessage.ts b/packages/core/src/modules/credentials/protocol/v3/messages/V3CredentialAckMessage.ts new file mode 100644 index 0000000000..4360ff8985 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v3/messages/V3CredentialAckMessage.ts @@ -0,0 +1,23 @@ +import type { AckMessageOptions } from '../../../../common' + +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { AckMessage } from '../../../../common' + +export type V3CredentialAckMessageOptions = AckMessageOptions + +/** + * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0015-acks/README.md#explicit-acks + */ +export class V3CredentialAckMessage extends AckMessage { + /** + * Create new CredentialAckMessage instance. + * @param options + */ + public constructor(options: V3CredentialAckMessageOptions) { + super(options) + } + + @IsValidMessageType(V3CredentialAckMessage.type) + public readonly type = V3CredentialAckMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/issue-credential/3.0/ack') +} diff --git a/packages/core/src/modules/credentials/protocol/v3/messages/V3CredentialPreview.ts b/packages/core/src/modules/credentials/protocol/v3/messages/V3CredentialPreview.ts new file mode 100644 index 0000000000..3c4aaf1725 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v3/messages/V3CredentialPreview.ts @@ -0,0 +1,64 @@ +import type { CredentialPreviewOptions } from '../../../models/CredentialPreviewAttribute' + +import { Expose, Transform, Type } from 'class-transformer' +import { IsInstance, ValidateNested } from 'class-validator' + +import { JsonTransformer } from '../../../../../utils/JsonTransformer' +import { IsValidMessageType, parseMessageType, replaceLegacyDidSovPrefix } from '../../../../../utils/messageType' +import { CredentialPreviewAttribute } from '../../../models/CredentialPreviewAttribute' + +/** + * Credential preview inner message class. + * + * This is not a message but an inner object for other messages in this protocol. It is used construct a preview of the data for the credential. + * + * @see https://github.com/hyperledger/aries-rfcs/tree/main/features/0453-issue-credential-v2#preview-credential + */ +export class V3CredentialPreview { + public constructor(options: CredentialPreviewOptions) { + if (options) { + this.attributes = options.attributes.map((a) => new CredentialPreviewAttribute(a)) + } + } + + @Expose({ name: 'type' }) + @IsValidMessageType(V3CredentialPreview.type) + @Transform(({ value }) => replaceLegacyDidSovPrefix(value), { + toClassOnly: true, + }) + public readonly type = V3CredentialPreview.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/issue-credential/3.0/credential-preview') + + @Type(() => CredentialPreviewAttribute) + @ValidateNested({ each: true }) + @IsInstance(CredentialPreviewAttribute, { each: true }) + public attributes!: CredentialPreviewAttribute[] + + public toJSON(): Record { + return JsonTransformer.toJSON(this) + } + + /** + * Create a credential preview from a record with name and value entries. + * + * @example + * const preview = CredentialPreview.fromRecord({ + * name: "Bob", + * age: "20" + * }) + */ + public static fromRecord(record: Record) { + const attributes = Object.entries(record).map( + ([name, value]) => + new CredentialPreviewAttribute({ + name, + mimeType: 'text/plain', + value, + }) + ) + + return new V3CredentialPreview({ + attributes, + }) + } +} diff --git a/packages/core/src/modules/credentials/protocol/v3/messages/V3CredentialProblemReportMessage.ts b/packages/core/src/modules/credentials/protocol/v3/messages/V3CredentialProblemReportMessage.ts new file mode 100644 index 0000000000..6f2dd834d4 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v3/messages/V3CredentialProblemReportMessage.ts @@ -0,0 +1,23 @@ +import type { V2ProblemReportMessageOptions } from '../../../../problem-reports/versions/v2/messages' + +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { V2ProblemReportMessage } from '../../../../problem-reports/versions/v2' + +export type V3CredentialProblemReportMessageOptions = V2ProblemReportMessageOptions + +/** + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0035-report-problem/README.md + */ +export class V3CredentialProblemReportMessage extends V2ProblemReportMessage { + /** + * Create new CredentialProblemReportMessage instance. + * @param options + */ + public constructor(options: V3CredentialProblemReportMessageOptions) { + super(options) + } + + @IsValidMessageType(V3CredentialProblemReportMessage.type) + public readonly type = V3CredentialProblemReportMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/issue-credential/3.0/problem-report') +} diff --git a/packages/core/src/modules/credentials/protocol/v3/messages/V3IssueCredentialMessage.ts b/packages/core/src/modules/credentials/protocol/v3/messages/V3IssueCredentialMessage.ts new file mode 100644 index 0000000000..1baf5d3489 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v3/messages/V3IssueCredentialMessage.ts @@ -0,0 +1,53 @@ +import { Expose, Type } from 'class-transformer' +import { IsArray, IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' + +import { V2Attachment } from '../../../../../decorators/attachment' +import { DidCommV2Message } from '../../../../../didcomm' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { CredentialFormatSpec } from '../../../models' + +export interface V3IssueCredentialMessageOptions { + id?: string + comment?: string + formats: CredentialFormatSpec[] + credentialAttachments: V2Attachment[] +} + +export class V3IssueCredentialMessage extends DidCommV2Message { + public constructor(options: V3IssueCredentialMessageOptions) { + super() + + if (options) { + this.id = options.id ?? this.generateId() + this.comment = options.comment + this.formats = options.formats + this.credentialAttachments = options.credentialAttachments + } + } + @Type(() => CredentialFormatSpec) + @ValidateNested() + @IsArray() + @IsInstance(CredentialFormatSpec, { each: true }) + public formats!: CredentialFormatSpec[] + + @IsValidMessageType(V3IssueCredentialMessage.type) + public readonly type = V3IssueCredentialMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/issue-credential/3.0/issue-credential') + + @IsString() + @IsOptional() + public comment?: string + + @Expose({ name: 'credentials~attach' }) + @Type(() => V2Attachment) + @IsArray() + @ValidateNested({ + each: true, + }) + @IsInstance(V2Attachment, { each: true }) + public credentialAttachments!: V2Attachment[] + + public getCredentialAttachmentById(id: string): V2Attachment | undefined { + return this.credentialAttachments.find((attachment) => attachment.id === id) + } +} diff --git a/packages/core/src/modules/credentials/protocol/v3/messages/V3OfferCredentialMessage.ts b/packages/core/src/modules/credentials/protocol/v3/messages/V3OfferCredentialMessage.ts new file mode 100644 index 0000000000..0178c4e6c7 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v3/messages/V3OfferCredentialMessage.ts @@ -0,0 +1,69 @@ +import { Expose, Type } from 'class-transformer' +import { IsArray, IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' + +import { V2Attachment } from '../../../../../decorators/attachment' +import { DidCommV2Message } from '../../../../../didcomm' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { CredentialFormatSpec } from '../../../models' + +import { V3CredentialPreview } from './V3CredentialPreview' + +export interface V3OfferCredentialMessageOptions { + id?: string + formats: CredentialFormatSpec[] + offerAttachments: V2Attachment[] + credentialPreview: V3CredentialPreview + replacementId?: string + comment?: string +} + +export class V3OfferCredentialMessage extends DidCommV2Message { + public constructor(options: V3OfferCredentialMessageOptions) { + super() + if (options) { + this.id = options.id ?? this.generateId() + this.comment = options.comment + this.formats = options.formats + this.credentialPreview = options.credentialPreview + this.offerAttachments = options.offerAttachments + } + } + + @Type(() => CredentialFormatSpec) + @ValidateNested() + @IsArray() + @IsInstance(CredentialFormatSpec, { each: true }) + public formats!: CredentialFormatSpec[] + + @IsValidMessageType(V3OfferCredentialMessage.type) + public readonly type = V3OfferCredentialMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/issue-credential/3.0/offer-credential') + + @IsString() + @IsOptional() + public comment?: string + + @Expose({ name: 'credential_preview' }) + @Type(() => V3CredentialPreview) + @ValidateNested() + @IsInstance(V3CredentialPreview) + public credentialPreview?: V3CredentialPreview + + @Expose({ name: 'offers~attach' }) + @Type(() => V2Attachment) + @IsArray() + @ValidateNested({ + each: true, + }) + @IsInstance(V2Attachment, { each: true }) + public offerAttachments!: V2Attachment[] + + @Expose({ name: 'replacement_id' }) + @IsString() + @IsOptional() + public replacementId?: string + + public getOfferAttachmentById(id: string): V2Attachment | undefined { + return this.offerAttachments.find((attachment) => attachment.id === id) + } +} diff --git a/packages/core/src/modules/credentials/protocol/v3/messages/V3ProposeCredentialMessage.ts b/packages/core/src/modules/credentials/protocol/v3/messages/V3ProposeCredentialMessage.ts new file mode 100644 index 0000000000..a2b2ae07ca --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v3/messages/V3ProposeCredentialMessage.ts @@ -0,0 +1,70 @@ +import { Expose, Type } from 'class-transformer' +import { IsArray, IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' + +import { V2Attachment } from '../../../../../decorators/attachment' +import { DidCommV2Message } from '../../../../../didcomm' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { CredentialFormatSpec } from '../../../models' + +import { V3CredentialPreview } from './V3CredentialPreview' + +export interface V3ProposeCredentialMessageOptions { + id?: string + formats: CredentialFormatSpec[] + proposalAttachments: V2Attachment[] + comment?: string + credentialPreview?: V3CredentialPreview + attachments?: V2Attachment[] +} + +export class V3ProposeCredentialMessage extends DidCommV2Message { + public constructor(options: V3ProposeCredentialMessageOptions) { + super() + 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.attachments = options.attachments + } + } + + @Type(() => CredentialFormatSpec) + @ValidateNested({ each: true }) + @IsArray() + @IsInstance(CredentialFormatSpec, { each: true }) + public formats!: CredentialFormatSpec[] + + @IsValidMessageType(V3ProposeCredentialMessage.type) + public readonly type = V3ProposeCredentialMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/issue-credential/3.0/propose-credential') + + @Expose({ name: 'credential_preview' }) + @Type(() => V3CredentialPreview) + @ValidateNested() + @IsOptional() + @IsInstance(V3CredentialPreview) + public credentialPreview?: V3CredentialPreview + + @Expose({ name: 'filters~attach' }) + @Type(() => V2Attachment) + @IsArray() + @ValidateNested({ + each: true, + }) + @IsInstance(V2Attachment, { each: true }) + public proposalAttachments!: V2Attachment[] + + /** + * Human readable information about this Credential Proposal, + * so the proposal can be evaluated by human judgment. + */ + @IsOptional() + @IsString() + public comment?: string + + public getProposalAttachmentById(id: string): V2Attachment | undefined { + return this.proposalAttachments.find((attachment) => attachment.id === id) + } +} diff --git a/packages/core/src/modules/credentials/protocol/v3/messages/V3RequestCredentialMessage.ts b/packages/core/src/modules/credentials/protocol/v3/messages/V3RequestCredentialMessage.ts new file mode 100644 index 0000000000..f3d87a5083 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v3/messages/V3RequestCredentialMessage.ts @@ -0,0 +1,57 @@ +import { Expose, Type } from 'class-transformer' +import { IsArray, IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' + +import { V2Attachment } from '../../../../../decorators/attachment' +import { DidCommV2Message } from '../../../../../didcomm' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { CredentialFormatSpec } from '../../../models' + +export interface V3RequestCredentialMessageOptions { + id?: string + formats: CredentialFormatSpec[] + requestAttachments: V2Attachment[] + comment?: string +} + +export class V3RequestCredentialMessage extends DidCommV2Message { + public constructor(options: V3RequestCredentialMessageOptions) { + super() + if (options) { + this.id = options.id ?? this.generateId() + this.comment = options.comment + this.formats = options.formats + this.requestAttachments = options.requestAttachments + } + } + + @Type(() => CredentialFormatSpec) + @ValidateNested() + @IsArray() + @IsInstance(CredentialFormatSpec, { each: true }) + public formats!: CredentialFormatSpec[] + + @IsValidMessageType(V3RequestCredentialMessage.type) + public readonly type = V3RequestCredentialMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/issue-credential/3.0/request-credential') + + @Expose({ name: 'requests~attach' }) + @Type(() => V2Attachment) + @IsArray() + @ValidateNested({ + each: true, + }) + @IsInstance(V2Attachment, { each: true }) + public requestAttachments!: V2Attachment[] + + /** + * Human readable information about this Credential Request, + * so the proposal can be evaluated by human judgment. + */ + @IsOptional() + @IsString() + public comment?: string + + public getRequestAttachmentById(id: string): V2Attachment | undefined { + return this.requestAttachments.find((attachment) => attachment.id === id) + } +} diff --git a/packages/core/src/modules/credentials/protocol/v3/messages/index.ts b/packages/core/src/modules/credentials/protocol/v3/messages/index.ts new file mode 100644 index 0000000000..a9fa9c6fcc --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v3/messages/index.ts @@ -0,0 +1,7 @@ +export * from './V3CredentialAckMessage' +export * from './V3CredentialProblemReportMessage' +export * from './V3IssueCredentialMessage' +export * from './V3OfferCredentialMessage' +export * from './V3ProposeCredentialMessage' +export * from './V3RequestCredentialMessage' +export * from './V3CredentialPreview' diff --git a/packages/core/src/modules/problem-reports/errors/V2ProblemReportError.ts b/packages/core/src/modules/problem-reports/errors/V2ProblemReportError.ts new file mode 100644 index 0000000000..cc107376b7 --- /dev/null +++ b/packages/core/src/modules/problem-reports/errors/V2ProblemReportError.ts @@ -0,0 +1,22 @@ +import { AriesFrameworkError } from '../../../error/AriesFrameworkError' +import { V2ProblemReportMessage } from '../versions/v2/messages' + +export interface V2ProblemReportErrorOptions { + problemCode: string + parentThreadId: string +} + +export class V2ProblemReportError extends AriesFrameworkError { + public problemReport: V2ProblemReportMessage + + public constructor(message: string, { problemCode, parentThreadId }: V2ProblemReportErrorOptions) { + super(message) + this.problemReport = new V2ProblemReportMessage({ + parentThreadId, + body: { + comment: message, + code: problemCode, + }, + }) + } +} diff --git a/packages/core/src/modules/problem-reports/versions/v2/helpers.ts b/packages/core/src/modules/problem-reports/versions/v2/helpers.ts index 1440efd8fb..72d0a3ee6a 100644 --- a/packages/core/src/modules/problem-reports/versions/v2/helpers.ts +++ b/packages/core/src/modules/problem-reports/versions/v2/helpers.ts @@ -2,7 +2,7 @@ import type { PlaintextDidCommV2Message } from '../../../../didcomm' import { ProblemReportReason } from '../../models/ProblemReportReason' -import { ProblemReportMessage } from './messages' +import { V2ProblemReportMessage } from './messages' /** * Build the v2 problem report message to the recipient. @@ -12,11 +12,11 @@ import { ProblemReportMessage } from './messages' export const buildProblemReportV2Message = ( plaintextMessage: PlaintextDidCommV2Message, errorMessage: string -): ProblemReportMessage | undefined => { +): V2ProblemReportMessage | undefined => { // Cannot send problem report for message with unknown sender or recipient if (!plaintextMessage.from || !plaintextMessage.to?.length) return - return new ProblemReportMessage({ + return new V2ProblemReportMessage({ parentThreadId: plaintextMessage.id, from: plaintextMessage.to.length ? plaintextMessage.to[0] : undefined, to: plaintextMessage.from, diff --git a/packages/core/src/modules/problem-reports/versions/v2/index.ts b/packages/core/src/modules/problem-reports/versions/v2/index.ts index f9d434a2f5..3c4007f660 100644 --- a/packages/core/src/modules/problem-reports/versions/v2/index.ts +++ b/packages/core/src/modules/problem-reports/versions/v2/index.ts @@ -1,2 +1,2 @@ -export { ProblemReportMessage as V2ProblemReportMessage } from './messages' +export { V2ProblemReportMessage as V2ProblemReportMessage } from './messages' export { buildProblemReportV2Message } from './helpers' diff --git a/packages/core/src/modules/problem-reports/versions/v2/messages/ProblemReportMessage.ts b/packages/core/src/modules/problem-reports/versions/v2/messages/ProblemReportMessage.ts index 299066f047..3469ff4b3f 100644 --- a/packages/core/src/modules/problem-reports/versions/v2/messages/ProblemReportMessage.ts +++ b/packages/core/src/modules/problem-reports/versions/v2/messages/ProblemReportMessage.ts @@ -27,7 +27,7 @@ export class ProblemReportBody { public args?: string[] } -export class ProblemReportMessage extends DidCommV2Message { +export class V2ProblemReportMessage extends DidCommV2Message { public constructor(options?: V2ProblemReportMessageOptions) { super(options) if (options) { @@ -36,8 +36,8 @@ export class ProblemReportMessage extends DidCommV2Message { } } - @IsValidMessageType(ProblemReportMessage.type) - public readonly type = ProblemReportMessage.type.messageTypeUri + @IsValidMessageType(V2ProblemReportMessage.type) + public readonly type = V2ProblemReportMessage.type.messageTypeUri public static readonly type = parseMessageType('https://didcomm.org/report-problem/2.0/problem-report') @Expose({ name: 'body' }) From 39d53234bc2d0a346e4233e76d27d4a93102afd1 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Sat, 19 Aug 2023 19:10:12 -0300 Subject: [PATCH 03/22] test: adapt to V2 Basic Messages and some DIDComm V2 stuff Signed-off-by: Ariel Gentile --- .../src/wallet/__tests__/packing.test.ts | 8 +- .../core/src/agent/__tests__/Agent.test.ts | 7 +- .../thread/ThreadDecoratorExtension.ts | 4 + .../didcomm/versions/v2/DidCommV2Message.ts | 2 +- .../__tests__/BasicMessagesModule.test.ts | 12 ++- .../__tests__/basic-messages.e2e.test.ts | 93 +++++++++++++++++-- .../__tests__/V1BasicMessageProtocol.test.ts} | 37 ++++---- .../__tests__/V2BasicMessageProtocol.test.ts | 91 ++++++++++++++++++ .../protocols/v2/messages/V2BasicMessage.ts | 4 + packages/core/tests/helpers.ts | 48 +++++++--- 10 files changed, 256 insertions(+), 50 deletions(-) rename packages/core/src/modules/basic-messages/{__tests__/BasicMessageService.test.ts => protocols/v1/__tests__/V1BasicMessageProtocol.test.ts} (67%) create mode 100644 packages/core/src/modules/basic-messages/protocols/v2/__tests__/V2BasicMessageProtocol.test.ts diff --git a/packages/askar/src/wallet/__tests__/packing.test.ts b/packages/askar/src/wallet/__tests__/packing.test.ts index 282b614c1c..ce9f80d633 100644 --- a/packages/askar/src/wallet/__tests__/packing.test.ts +++ b/packages/askar/src/wallet/__tests__/packing.test.ts @@ -2,7 +2,7 @@ import type { WalletConfig, WalletPackOptions, WalletUnpackOptions } from '@arie import type { JwkProps } from '@hyperledger/aries-askar-shared' import { - BasicMessage, + V1BasicMessage, DidCommMessageVersion, JsonTransformer, KeyDerivationMethod, @@ -71,7 +71,7 @@ describeRunInNodeVersion([18], 'askarWallet packing', () => { }) describe('DIDComm V1 packing and unpacking', () => { - const message = new BasicMessage({ content: 'hello' }) + const message = new V1BasicMessage({ content: 'hello' }) test('Authcrypt', async () => { // Create both sender and recipient keys @@ -86,7 +86,7 @@ describeRunInNodeVersion([18], 'askarWallet packing', () => { const encryptedMessage = await askarWallet.pack(message.toJSON(), params) const plainTextMessage = await askarWallet.unpack(encryptedMessage) - expect(JsonTransformer.fromJSON(plainTextMessage.plaintextMessage, BasicMessage)).toEqual(message) + expect(JsonTransformer.fromJSON(plainTextMessage.plaintextMessage, V1BasicMessage)).toEqual(message) }) test('Anoncrypt', async () => { @@ -101,7 +101,7 @@ describeRunInNodeVersion([18], 'askarWallet packing', () => { const encryptedMessage = await askarWallet.pack(message.toJSON(), params) const plainTextMessage = await askarWallet.unpack(encryptedMessage) - expect(JsonTransformer.fromJSON(plainTextMessage.plaintextMessage, BasicMessage)).toEqual(message) + expect(JsonTransformer.fromJSON(plainTextMessage.plaintextMessage, V1BasicMessage)).toEqual(message) }) }) diff --git a/packages/core/src/agent/__tests__/Agent.test.ts b/packages/core/src/agent/__tests__/Agent.test.ts index 65729dd6c5..dfbf4700bd 100644 --- a/packages/core/src/agent/__tests__/Agent.test.ts +++ b/packages/core/src/agent/__tests__/Agent.test.ts @@ -5,7 +5,7 @@ import { injectable } from 'tsyringe' import { getIndySdkModules } from '../../../../indy-sdk/tests/setupIndySdkModule' import { getAgentOptions } from '../../../tests/helpers' import { InjectionSymbols } from '../../constants' -import { BasicMessageRepository, V1BasicMessageProtocol } from '../../modules/basic-messages' +import { BasicMessageRepository } from '../../modules/basic-messages' import { BasicMessagesApi } from '../../modules/basic-messages/BasicMessagesApi' import { ConnectionsApi } from '../../modules/connections/ConnectionsApi' import { V1TrustPingService } from '../../modules/connections/protocols/trust-ping/v1/V1TrustPingService' @@ -167,7 +167,6 @@ describe('Agent', () => { expect(container.resolve(CredentialRepository)).toBeInstanceOf(CredentialRepository) expect(container.resolve(BasicMessagesApi)).toBeInstanceOf(BasicMessagesApi) - expect(container.resolve(V1BasicMessageProtocol)).toBeInstanceOf(V1BasicMessageProtocol) expect(container.resolve(BasicMessageRepository)).toBeInstanceOf(BasicMessageRepository) expect(container.resolve(MediatorApi)).toBeInstanceOf(MediatorApi) @@ -205,7 +204,6 @@ describe('Agent', () => { expect(container.resolve(CredentialRepository)).toBe(container.resolve(CredentialRepository)) expect(container.resolve(BasicMessagesApi)).toBe(container.resolve(BasicMessagesApi)) - expect(container.resolve(V1BasicMessageProtocol)).toBe(container.resolve(V1BasicMessageProtocol)) expect(container.resolve(BasicMessageRepository)).toBe(container.resolve(BasicMessageRepository)) expect(container.resolve(MediatorApi)).toBe(container.resolve(MediatorApi)) @@ -242,6 +240,7 @@ describe('Agent', () => { expect(protocols).toEqual( expect.arrayContaining([ 'https://didcomm.org/basicmessage/1.0', + 'https://didcomm.org/basicmessage/2.0', 'https://didcomm.org/connections/1.0', 'https://didcomm.org/coordinate-mediation/1.0', 'https://didcomm.org/issue-credential/2.0', @@ -256,6 +255,6 @@ describe('Agent', () => { 'https://didcomm.org/revocation_notification/2.0', ]) ) - expect(protocols.length).toEqual(13) + expect(protocols.length).toEqual(14) }) }) diff --git a/packages/core/src/decorators/thread/ThreadDecoratorExtension.ts b/packages/core/src/decorators/thread/ThreadDecoratorExtension.ts index 5141e5a051..a7cad5f7a0 100644 --- a/packages/core/src/decorators/thread/ThreadDecoratorExtension.ts +++ b/packages/core/src/decorators/thread/ThreadDecoratorExtension.ts @@ -21,6 +21,10 @@ export function ThreadDecorated(Base: return this.thread?.threadId ?? this.id } + public get parentThreadId(): string | undefined { + return this.thread?.parentThreadId + } + public setThread(options: Partial) { this.thread = new ThreadDecorator(options) } diff --git a/packages/core/src/didcomm/versions/v2/DidCommV2Message.ts b/packages/core/src/didcomm/versions/v2/DidCommV2Message.ts index a45f45da6f..0b517c10d9 100644 --- a/packages/core/src/didcomm/versions/v2/DidCommV2Message.ts +++ b/packages/core/src/didcomm/versions/v2/DidCommV2Message.ts @@ -23,7 +23,7 @@ export class DidCommV2Message extends DidCommV2BaseMessage implements AgentBaseM } public get threadId(): string | undefined { - return this.thid + return this.thid ?? this.id } public hasAnyReturnRoute() { diff --git a/packages/core/src/modules/basic-messages/__tests__/BasicMessagesModule.test.ts b/packages/core/src/modules/basic-messages/__tests__/BasicMessagesModule.test.ts index b0643bf3cd..8fd849e156 100644 --- a/packages/core/src/modules/basic-messages/__tests__/BasicMessagesModule.test.ts +++ b/packages/core/src/modules/basic-messages/__tests__/BasicMessagesModule.test.ts @@ -2,7 +2,7 @@ import { FeatureRegistry } from '../../../agent/FeatureRegistry' import { DependencyManager } from '../../../plugins/DependencyManager' import { BasicMessagesApi } from '../BasicMessagesApi' import { BasicMessagesModule } from '../BasicMessagesModule' -import { V1BasicMessageProtocol, V2BasicMessageProtocol } from '../protocols' +import { BasicMessagesModuleConfig } from '../BasicMessagesModuleConfig' import { BasicMessageRepository } from '../repository' jest.mock('../../../plugins/DependencyManager') @@ -17,14 +17,16 @@ const featureRegistry = new FeatureRegistryMock() describe('BasicMessagesModule', () => { test('registers dependencies on the dependency manager', () => { - new BasicMessagesModule().register(dependencyManager, featureRegistry) + const module = new BasicMessagesModule() + module.register(dependencyManager, featureRegistry) expect(dependencyManager.registerContextScoped).toHaveBeenCalledTimes(1) expect(dependencyManager.registerContextScoped).toHaveBeenCalledWith(BasicMessagesApi) - expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(3) - expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(V1BasicMessageProtocol) - expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(V2BasicMessageProtocol) + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(1) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(BasicMessageRepository) + + expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(1) + expect(dependencyManager.registerInstance).toHaveBeenCalledWith(BasicMessagesModuleConfig, module.config) }) }) diff --git a/packages/core/src/modules/basic-messages/__tests__/basic-messages.e2e.test.ts b/packages/core/src/modules/basic-messages/__tests__/basic-messages.e2e.test.ts index 4afb412f78..2f14ca7353 100644 --- a/packages/core/src/modules/basic-messages/__tests__/basic-messages.e2e.test.ts +++ b/packages/core/src/modules/basic-messages/__tests__/basic-messages.e2e.test.ts @@ -1,16 +1,19 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import type { SubjectMessage } from '../../../../../../tests/transport/SubjectInboundTransport' import type { ConnectionRecord } from '../../../modules/connections' +import type { V2BasicMessage } from '../protocols' import { Subject } from 'rxjs' +import { describeRunInNodeVersion } from '../../../../../../tests/runInVersion' import { SubjectInboundTransport } from '../../../../../../tests/transport/SubjectInboundTransport' import { SubjectOutboundTransport } from '../../../../../../tests/transport/SubjectOutboundTransport' -import { getIndySdkModules } from '../../../../../indy-sdk/tests/setupIndySdkModule' +import { getAskarAnonCredsIndyModules } from '../../../../../anoncreds/tests/legacyAnonCredsSetup' import { getAgentOptions, makeConnection, waitForBasicMessage } from '../../../../tests/helpers' import testLogger from '../../../../tests/logger' import { Agent } from '../../../agent/Agent' import { MessageSendingError, RecordNotFoundError } from '../../../error' +import { OutOfBandVersion } from '../../oob' import { V1BasicMessage } from '../protocols' import { BasicMessageRecord } from '../repository' @@ -18,8 +21,9 @@ const faberConfig = getAgentOptions( 'Faber Basic Messages', { endpoints: ['rxjs:faber'], + logger: testLogger, }, - getIndySdkModules() + getAskarAnonCredsIndyModules() ) const aliceConfig = getAgentOptions( @@ -27,21 +31,35 @@ const aliceConfig = getAgentOptions( { endpoints: ['rxjs:alice'], }, - getIndySdkModules() + getAskarAnonCredsIndyModules() ) -describe('Basic Messages E2E', () => { +const bobConfig = getAgentOptions( + 'Bob Basic Messages', + { + endpoints: ['rxjs:bob'], + logger: testLogger, + }, + getAskarAnonCredsIndyModules() +) + +describeRunInNodeVersion([18], 'Basic Messages E2E', () => { let faberAgent: Agent let aliceAgent: Agent + let bobAgent: Agent let faberConnection: ConnectionRecord let aliceConnection: ConnectionRecord + let bobConnection: ConnectionRecord + let faberBobConnection: ConnectionRecord beforeEach(async () => { const faberMessages = new Subject() const aliceMessages = new Subject() + const bobMessages = new Subject() const subjectMap = { 'rxjs:faber': faberMessages, 'rxjs:alice': aliceMessages, + 'rxjs:bob': bobMessages, } faberAgent = new Agent(faberConfig) @@ -54,6 +72,12 @@ describe('Basic Messages E2E', () => { aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) await aliceAgent.initialize() ;[aliceConnection, faberConnection] = await makeConnection(aliceAgent, faberAgent) + + bobAgent = new Agent(bobConfig) + bobAgent.registerInboundTransport(new SubjectInboundTransport(bobMessages)) + bobAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await bobAgent.initialize() + ;[bobConnection, faberBobConnection] = await makeConnection(bobAgent, faberAgent, OutOfBandVersion.V2) }) afterEach(async () => { @@ -61,6 +85,8 @@ describe('Basic Messages E2E', () => { await faberAgent.wallet.delete() await aliceAgent.shutdown() await aliceAgent.wallet.delete() + await bobAgent.shutdown() + await bobAgent.wallet.delete() }) test('Alice and Faber exchange messages', async () => { @@ -101,11 +127,11 @@ describe('Basic Messages E2E', () => { expect(replyRecord.parentThreadId).toBe(helloMessage.id) testLogger.test('Alice waits until she receives message from faber') - const replyMessage = (await waitForBasicMessage(aliceAgent, { + const replyMessage = await waitForBasicMessage(aliceAgent, { content: 'How are you?', - })) as V1BasicMessage + }) expect(replyMessage.content).toBe('How are you?') - expect(replyMessage.thread?.parentThreadId).toBe(helloMessage.id) + expect(replyMessage.parentThreadId).toBe(helloMessage.id) // Both sender and recipient shall be able to find the threaded messages // Hello message @@ -133,6 +159,59 @@ describe('Basic Messages E2E', () => { expect(faberReplyMessages[0]).toMatchObject(replyRecord) }) + test('Bob and Faber exchange messages using V2 protocol', async () => { + testLogger.test('Bob sends message to Faber') + const helloRecord = await bobAgent.basicMessages.sendMessage(bobConnection.id, 'Hello') + + expect(helloRecord.content).toBe('Hello') + + testLogger.test('Faber waits for message from Bob') + const helloMessage = await waitForBasicMessage(faberAgent, { + content: 'Hello', + }) + + testLogger.test('Faber sends message to Bob') + const replyRecord = await faberAgent.basicMessages.sendMessage( + faberBobConnection.id, + 'How are you?', + helloMessage.id + ) + expect(replyRecord.content).toBe('How are you?') + expect(replyRecord.parentThreadId).toBe(helloMessage.id) + + testLogger.test('Bob waits until he receives message from faber') + const replyMessage = (await waitForBasicMessage(bobAgent, { + content: 'How are you?', + })) as V2BasicMessage + expect(replyMessage.body.content).toBe('How are you?') + expect(replyMessage.parentThreadId).toBe(helloMessage.id) + + // Both sender and recipient shall be able to find the threaded messages + // Hello message + const bobHelloMessage = await bobAgent.basicMessages.getByThreadId(helloMessage.id) + const faberHelloMessage = await faberAgent.basicMessages.getByThreadId(helloMessage.id) + expect(bobHelloMessage).toMatchObject({ + content: helloRecord.content, + threadId: helloRecord.threadId, + }) + expect(faberHelloMessage).toMatchObject({ + content: helloRecord.content, + threadId: helloRecord.threadId, + }) + + // Reply message + const bobReplyMessages = await bobAgent.basicMessages.findAllByQuery({ parentThreadId: helloMessage.id }) + const faberReplyMessages = await faberAgent.basicMessages.findAllByQuery({ parentThreadId: helloMessage.id }) + expect(bobReplyMessages.length).toBe(1) + expect(bobReplyMessages[0]).toMatchObject({ + content: replyRecord.content, + parentThreadId: replyRecord.parentThreadId, + threadId: replyRecord.threadId, + }) + expect(faberReplyMessages.length).toBe(1) + expect(faberReplyMessages[0]).toMatchObject(replyRecord) + }) + test('Alice is unable to send a message', async () => { testLogger.test('Alice sends message to Faber that is undeliverable') diff --git a/packages/core/src/modules/basic-messages/__tests__/BasicMessageService.test.ts b/packages/core/src/modules/basic-messages/protocols/v1/__tests__/V1BasicMessageProtocol.test.ts similarity index 67% rename from packages/core/src/modules/basic-messages/__tests__/BasicMessageService.test.ts rename to packages/core/src/modules/basic-messages/protocols/v1/__tests__/V1BasicMessageProtocol.test.ts index bd3783c14a..a62a23aca9 100644 --- a/packages/core/src/modules/basic-messages/__tests__/BasicMessageService.test.ts +++ b/packages/core/src/modules/basic-messages/protocols/v1/__tests__/V1BasicMessageProtocol.test.ts @@ -1,36 +1,41 @@ -import { getAgentContext, getMockConnection } from '../../../../tests/helpers' -import { EventEmitter } from '../../../agent/EventEmitter' -import { InboundMessageContext } from '../../../agent/models/InboundMessageContext' -import { BasicMessageRole } from '../BasicMessageRole' -import { V1BasicMessageProtocol } from '../protocols' -import { V1BasicMessage } from '../protocols/v1/messages' -import { BasicMessageRecord } from '../repository/BasicMessageRecord' -import { BasicMessageRepository } from '../repository/BasicMessageRepository' +import { getAgentContext, getMockConnection } from '../../../../../../tests/helpers' +import { EventEmitter } from '../../../../../agent/EventEmitter' +import { InboundMessageContext } from '../../../../../agent/models/InboundMessageContext' +import { BasicMessageRole } from '../../../BasicMessageRole' +import { BasicMessageRecord } from '../../../repository/BasicMessageRecord' +import { BasicMessageRepository } from '../../../repository/BasicMessageRepository' +import { V1BasicMessageProtocol } from '../V1BasicMessageProtocol' +import { V1BasicMessage } from '../messages' -jest.mock('../repository/BasicMessageRepository') +jest.mock('../../../repository/BasicMessageRepository') const BasicMessageRepositoryMock = BasicMessageRepository as jest.Mock const basicMessageRepository = new BasicMessageRepositoryMock() -jest.mock('../../../agent/EventEmitter') +jest.mock('../../../../../agent/EventEmitter') const EventEmitterMock = EventEmitter as jest.Mock const eventEmitter = new EventEmitterMock() -const agentContext = getAgentContext() +const agentContext = getAgentContext({ + registerInstances: [ + [BasicMessageRepository, basicMessageRepository], + [EventEmitter, eventEmitter], + ], +}) -describe('BasicMessageService', () => { - let basicMessageService: V1BasicMessageProtocol +describe('V1BasicMessageProtocol', () => { + let basicMessageProtocol: V1BasicMessageProtocol const mockConnectionRecord = getMockConnection({ id: 'd3849ac3-c981-455b-a1aa-a10bea6cead8', did: 'did:sov:C2SsBf5QUQpqSAQfhu3sd2', }) beforeEach(() => { - basicMessageService = new V1BasicMessageProtocol() + basicMessageProtocol = new V1BasicMessageProtocol() }) describe('createMessage', () => { it(`creates message and record, and emits message and basic message record`, async () => { - const { message } = await basicMessageService.createMessage(agentContext, { + const { message } = await basicMessageProtocol.createMessage(agentContext, { content: 'hello', connectionRecord: mockConnectionRecord, }) @@ -63,7 +68,7 @@ describe('BasicMessageService', () => { const messageContext = new InboundMessageContext(basicMessage, { agentContext }) - await basicMessageService.save(messageContext, mockConnectionRecord) + await basicMessageProtocol.save(messageContext, mockConnectionRecord) expect(basicMessageRepository.save).toHaveBeenCalledWith(agentContext, expect.any(BasicMessageRecord)) expect(eventEmitter.emit).toHaveBeenCalledWith(agentContext, { diff --git a/packages/core/src/modules/basic-messages/protocols/v2/__tests__/V2BasicMessageProtocol.test.ts b/packages/core/src/modules/basic-messages/protocols/v2/__tests__/V2BasicMessageProtocol.test.ts new file mode 100644 index 0000000000..6118ac1ced --- /dev/null +++ b/packages/core/src/modules/basic-messages/protocols/v2/__tests__/V2BasicMessageProtocol.test.ts @@ -0,0 +1,91 @@ +import { getAgentContext, getMockConnection } from '../../../../../../tests/helpers' +import { EventEmitter } from '../../../../../agent/EventEmitter' +import { InboundMessageContext } from '../../../../../agent/models/InboundMessageContext' +import { BasicMessageRole } from '../../../BasicMessageRole' +import { BasicMessageRecord } from '../../../repository/BasicMessageRecord' +import { BasicMessageRepository } from '../../../repository/BasicMessageRepository' +import { V2BasicMessageProtocol } from '../V2BasicMessageProtocol' +import { V2BasicMessage } from '../messages' + +jest.mock('../../../repository/BasicMessageRepository') +const BasicMessageRepositoryMock = BasicMessageRepository as jest.Mock +const basicMessageRepository = new BasicMessageRepositoryMock() + +jest.mock('../../../../../agent/EventEmitter') +const EventEmitterMock = EventEmitter as jest.Mock +const eventEmitter = new EventEmitterMock() + +const agentContext = getAgentContext({ + registerInstances: [ + [BasicMessageRepository, basicMessageRepository], + [EventEmitter, eventEmitter], + ], +}) + +describe('V2BasicMessageProtocol', () => { + let basicMessageProtocol: V2BasicMessageProtocol + const mockConnectionRecord = getMockConnection({ + id: 'd3849ac3-c981-455b-a1aa-a10bea6cead8', + did: 'did:sov:C2SsBf5QUQpqSAQfhu3sd2', + }) + + beforeEach(() => { + basicMessageProtocol = new V2BasicMessageProtocol() + }) + + describe('createMessage', () => { + it(`creates message and record, and emits message and basic message record`, async () => { + const { message } = await basicMessageProtocol.createMessage(agentContext, { + content: 'hello', + connectionRecord: mockConnectionRecord, + }) + + expect(message.body.content).toBe('hello') + + expect(basicMessageRepository.save).toHaveBeenCalledWith(agentContext, expect.any(BasicMessageRecord)) + expect(eventEmitter.emit).toHaveBeenCalledWith(agentContext, { + type: 'BasicMessageStateChanged', + payload: { + basicMessageRecord: expect.objectContaining({ + connectionId: mockConnectionRecord.id, + id: expect.any(String), + sentTime: expect.any(String), + content: 'hello', + role: BasicMessageRole.Sender, + }), + message, + }, + }) + }) + }) + + describe('save', () => { + it(`stores record and emits message and basic message record`, async () => { + const basicMessage = new V2BasicMessage({ + id: '123', + from: 'did:from', + to: 'did:to', + content: 'message', + }) + + const messageContext = new InboundMessageContext(basicMessage, { agentContext }) + + await basicMessageProtocol.save(messageContext, mockConnectionRecord) + + expect(basicMessageRepository.save).toHaveBeenCalledWith(agentContext, expect.any(BasicMessageRecord)) + expect(eventEmitter.emit).toHaveBeenCalledWith(agentContext, { + type: 'BasicMessageStateChanged', + payload: { + basicMessageRecord: expect.objectContaining({ + connectionId: mockConnectionRecord.id, + id: expect.any(String), + sentTime: new Date(basicMessage.createdTime).toISOString(), + content: basicMessage.body.content, + role: BasicMessageRole.Receiver, + }), + message: messageContext.message, + }, + }) + }) + }) +}) diff --git a/packages/core/src/modules/basic-messages/protocols/v2/messages/V2BasicMessage.ts b/packages/core/src/modules/basic-messages/protocols/v2/messages/V2BasicMessage.ts index cb42d4d1df..a8e57f0439 100644 --- a/packages/core/src/modules/basic-messages/protocols/v2/messages/V2BasicMessage.ts +++ b/packages/core/src/modules/basic-messages/protocols/v2/messages/V2BasicMessage.ts @@ -54,6 +54,10 @@ export class V2BasicMessage extends DidCommV2Message { } } + public get content() { + return this.body.content + } + @IsValidMessageType(V2BasicMessage.type) public readonly type = V2BasicMessage.type.messageTypeUri public static readonly type = parseMessageType('https://didcomm.org/basicmessage/2.0/message') diff --git a/packages/core/tests/helpers.ts b/packages/core/tests/helpers.ts index 8aedbc6bba..df0a1ffe26 100644 --- a/packages/core/tests/helpers.ts +++ b/packages/core/tests/helpers.ts @@ -30,6 +30,7 @@ import { catchError, filter, map, take, timeout } from 'rxjs/operators' import { agentDependencies, IndySdkPostgresWalletScheme } from '../../node/src' import { + OutOfBandVersion, V1BasicMessage, V2BasicMessage, OutOfBandDidCommService, @@ -218,7 +219,7 @@ const isCredentialStateChangedEvent = (e: BaseEvent): e is CredentialStateChange const isConnectionStateChangedEvent = (e: BaseEvent): e is ConnectionStateChangedEvent => e.type === ConnectionEventTypes.ConnectionStateChanged const isTrustPingReceivedEvent = (e: BaseEvent): e is TrustPingReceivedEvent => - e.type === TrustPingEventTypes.TrustPingReceivedEvent + e.type === TrustPingEventTypes.TrustPingReceivedEvent || e.type === TrustPingEventTypes.V2TrustPingReceivedEvent const isTrustPingResponseReceivedEvent = (e: BaseEvent): e is TrustPingResponseReceivedEvent => e.type === TrustPingEventTypes.TrustPingResponseReceivedEvent @@ -268,11 +269,16 @@ export function waitForProofExchangeRecordSubject( export async function waitForTrustPingReceivedEvent( agent: Agent, options: { + protocolVersion?: 'v1' | 'v2' threadId?: string timeoutMs?: number } ) { - const observable = agent.events.observable(TrustPingEventTypes.TrustPingReceivedEvent) + const observable = agent.events.observable( + options.protocolVersion === 'v2' + ? TrustPingEventTypes.V2TrustPingReceivedEvent + : TrustPingEventTypes.TrustPingReceivedEvent + ) return waitForTrustPingReceivedEventSubject(observable, options) } @@ -540,20 +546,36 @@ export function getMockOutOfBand({ return outOfBandRecord } -export async function makeConnection(agentA: Agent, agentB: Agent) { - const agentAOutOfBand = await agentA.oob.createInvitation({ - handshakeProtocols: [HandshakeProtocol.Connections], - }) +export async function makeConnection(agentA: Agent, agentB: Agent, version?: OutOfBandVersion) { + if (version === OutOfBandVersion.V2) { + const agentAOutOfBand = await agentA.oob.createInvitation({ + version, + }) - let { connectionRecord: agentBConnection } = await agentB.oob.receiveInvitation( - agentAOutOfBand.getOutOfBandInvitation() - ) + const { connectionRecord: agentBConnection } = await agentB.oob.receiveInvitation( + agentAOutOfBand.v2OutOfBandInvitation! + ) + if (!agentBConnection) throw new Error('No connection for receiver') + await agentB.connections.sendPing(agentBConnection.id, {}) + await waitForTrustPingReceivedEvent(agentA, { protocolVersion: 'v2', timeoutMs: 4000 }) + const [agentAConnection] = await agentA.connections.findAllByOutOfBandId(agentAOutOfBand.id) + if (!agentAConnection) throw new Error('No connection for inviter') + return [agentAConnection, agentBConnection] + } else { + const agentAOutOfBand = await agentA.oob.createInvitation({ + handshakeProtocols: [HandshakeProtocol.Connections], + }) + + let { connectionRecord: agentBConnection } = await agentB.oob.receiveInvitation( + agentAOutOfBand.getOutOfBandInvitation() + ) - agentBConnection = await agentB.connections.returnWhenIsConnected(agentBConnection!.id) - let [agentAConnection] = await agentA.connections.findAllByOutOfBandId(agentAOutOfBand.id) - agentAConnection = await agentA.connections.returnWhenIsConnected(agentAConnection!.id) + agentBConnection = await agentB.connections.returnWhenIsConnected(agentBConnection!.id) + let [agentAConnection] = await agentA.connections.findAllByOutOfBandId(agentAOutOfBand.id) + agentAConnection = await agentA.connections.returnWhenIsConnected(agentAConnection!.id) - return [agentAConnection, agentBConnection] + return [agentAConnection, agentBConnection] + } } /** From 3b0795d9ab850ee7ca4b96b0198c41718dacab0c Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Mon, 21 Aug 2023 09:58:00 -0300 Subject: [PATCH 04/22] fix: expose V1BasicMessage as BasicMessage Signed-off-by: Ariel Gentile --- demo/src/Listener.ts | 5 +---- packages/askar/src/wallet/__tests__/packing.test.ts | 8 ++++---- packages/core/src/modules/basic-messages/index.ts | 2 ++ packages/core/tests/helpers.ts | 12 ++++-------- 4 files changed, 11 insertions(+), 16 deletions(-) diff --git a/demo/src/Listener.ts b/demo/src/Listener.ts index dad670a652..4a4c7e383a 100644 --- a/demo/src/Listener.ts +++ b/demo/src/Listener.ts @@ -24,7 +24,6 @@ import { ProofEventTypes, ProofState, TrustPingEventTypes, - V1BasicMessage, } from '@aries-framework/core' import { ui } from 'inquirer' @@ -79,9 +78,7 @@ export class Listener { public messageListener(agent: Agent, name: string) { agent.events.on(BasicMessageEventTypes.BasicMessageStateChanged, async (event: BasicMessageStateChangedEvent) => { if (event.payload.basicMessageRecord.role === BasicMessageRole.Receiver) { - const message = event.payload.message - const content = message instanceof V1BasicMessage ? message.content : message.body.content - this.ui.updateBottomBar(purpleText(`\n${name} received a message: ${content}\n`)) + this.ui.updateBottomBar(purpleText(`\n${name} received a message: ${event.payload.message.content}\n`)) } }) } diff --git a/packages/askar/src/wallet/__tests__/packing.test.ts b/packages/askar/src/wallet/__tests__/packing.test.ts index ce9f80d633..282b614c1c 100644 --- a/packages/askar/src/wallet/__tests__/packing.test.ts +++ b/packages/askar/src/wallet/__tests__/packing.test.ts @@ -2,7 +2,7 @@ import type { WalletConfig, WalletPackOptions, WalletUnpackOptions } from '@arie import type { JwkProps } from '@hyperledger/aries-askar-shared' import { - V1BasicMessage, + BasicMessage, DidCommMessageVersion, JsonTransformer, KeyDerivationMethod, @@ -71,7 +71,7 @@ describeRunInNodeVersion([18], 'askarWallet packing', () => { }) describe('DIDComm V1 packing and unpacking', () => { - const message = new V1BasicMessage({ content: 'hello' }) + const message = new BasicMessage({ content: 'hello' }) test('Authcrypt', async () => { // Create both sender and recipient keys @@ -86,7 +86,7 @@ describeRunInNodeVersion([18], 'askarWallet packing', () => { const encryptedMessage = await askarWallet.pack(message.toJSON(), params) const plainTextMessage = await askarWallet.unpack(encryptedMessage) - expect(JsonTransformer.fromJSON(plainTextMessage.plaintextMessage, V1BasicMessage)).toEqual(message) + expect(JsonTransformer.fromJSON(plainTextMessage.plaintextMessage, BasicMessage)).toEqual(message) }) test('Anoncrypt', async () => { @@ -101,7 +101,7 @@ describeRunInNodeVersion([18], 'askarWallet packing', () => { const encryptedMessage = await askarWallet.pack(message.toJSON(), params) const plainTextMessage = await askarWallet.unpack(encryptedMessage) - expect(JsonTransformer.fromJSON(plainTextMessage.plaintextMessage, V1BasicMessage)).toEqual(message) + expect(JsonTransformer.fromJSON(plainTextMessage.plaintextMessage, BasicMessage)).toEqual(message) }) }) diff --git a/packages/core/src/modules/basic-messages/index.ts b/packages/core/src/modules/basic-messages/index.ts index 70590c4eb1..123c503837 100644 --- a/packages/core/src/modules/basic-messages/index.ts +++ b/packages/core/src/modules/basic-messages/index.ts @@ -4,3 +4,5 @@ export * from './BasicMessageEvents' export * from './BasicMessagesApi' export * from './BasicMessageRole' export * from './BasicMessagesModule' + +export { V1BasicMessage as BasicMessage } from './protocols' diff --git a/packages/core/tests/helpers.ts b/packages/core/tests/helpers.ts index df0a1ffe26..a09c6a8f8c 100644 --- a/packages/core/tests/helpers.ts +++ b/packages/core/tests/helpers.ts @@ -13,6 +13,8 @@ import type { CredentialState, ConnectionStateChangedEvent, Buffer, + V1BasicMessage, + V2BasicMessage, } from '../src' import type { AgentModulesInput, EmptyModuleMap } from '../src/agent/AgentModules' import type { @@ -31,8 +33,6 @@ import { catchError, filter, map, take, timeout } from 'rxjs/operators' import { agentDependencies, IndySdkPostgresWalletScheme } from '../../node/src' import { OutOfBandVersion, - V1BasicMessage, - V2BasicMessage, OutOfBandDidCommService, ConnectionsModule, ConnectionEventTypes, @@ -455,16 +455,12 @@ export async function waitForBasicMessage( ): Promise { return new Promise((resolve) => { const listener = (event: BasicMessageStateChangedEvent) => { - const message = event.payload.message - const contentMatches = - content === undefined || - (message instanceof V1BasicMessage && message.content === content) || - (message instanceof V2BasicMessage && message.body.content === content) + const contentMatches = content === undefined || event.payload.message.content === content if (contentMatches) { agent.events.off(BasicMessageEventTypes.BasicMessageStateChanged, listener) - resolve(message) + resolve(event.payload.message) } } From 919dc3b5179ad937bab1688bdcbdafbe0673e9de Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Tue, 22 Aug 2023 18:21:41 -0300 Subject: [PATCH 05/22] fix: v3 handlers and types Signed-off-by: Ariel Gentile --- .../credentials/v1/V1CredentialProtocol.ts | 10 +-- .../connections/services/ConnectionService.ts | 13 ++- .../src/modules/credentials/CredentialsApi.ts | 6 +- .../jsonld/JsonLdCredentialFormatService.ts | 3 +- .../v2/CredentialFormatCoordinator.ts | 12 +-- .../protocol/v3/V3CredentialProtocol.ts | 86 +------------------ ...Handler.ts => V3IssueCredentialHandler.ts} | 0 ...Handler.ts => V3OfferCredentialHandler.ts} | 10 +-- ...ndler.ts => V3ProposeCredentialHandler.ts} | 10 +-- ...ndler.ts => V3RequestCredentialHandler.ts} | 10 +-- .../credentials/protocol/v3/handlers/index.ts | 8 +- 11 files changed, 44 insertions(+), 124 deletions(-) rename packages/core/src/modules/credentials/protocol/v3/handlers/{V2IssueCredentialHandler.ts => V3IssueCredentialHandler.ts} (100%) rename packages/core/src/modules/credentials/protocol/v3/handlers/{V2OfferCredentialHandler.ts => V3OfferCredentialHandler.ts} (84%) rename packages/core/src/modules/credentials/protocol/v3/handlers/{V2ProposeCredentialHandler.ts => V3ProposeCredentialHandler.ts} (85%) rename packages/core/src/modules/credentials/protocol/v3/handlers/{V2RequestCredentialHandler.ts => V3RequestCredentialHandler.ts} (85%) diff --git a/packages/anoncreds/src/protocols/credentials/v1/V1CredentialProtocol.ts b/packages/anoncreds/src/protocols/credentials/v1/V1CredentialProtocol.ts index cc45da3cb5..b3ce5063ca 100644 --- a/packages/anoncreds/src/protocols/credentials/v1/V1CredentialProtocol.ts +++ b/packages/anoncreds/src/protocols/credentials/v1/V1CredentialProtocol.ts @@ -320,7 +320,7 @@ export class V1CredentialProtocol const message = new V1OfferCredentialMessage({ comment, - offerAttachments: [attachment], + offerAttachments: [attachment as Attachment], credentialPreview: new V1CredentialPreview({ attributes: previewAttributes, }), @@ -378,7 +378,7 @@ export class V1CredentialProtocol const message = new V1OfferCredentialMessage({ comment, - offerAttachments: [attachment], + offerAttachments: [attachment as Attachment], credentialPreview: new V1CredentialPreview({ attributes: previewAttributes, }), @@ -455,7 +455,7 @@ export class V1CredentialProtocol attributes: previewAttributes, }), comment, - offerAttachments: [attachment], + offerAttachments: [attachment as Attachment], attachments: credentialFormats.indy.linkedAttachments?.map((linkedAttachments) => linkedAttachments.attachment), }) @@ -609,7 +609,7 @@ export class V1CredentialProtocol const requestMessage = new V1RequestCredentialMessage({ comment, - requestAttachments: [attachment], + requestAttachments: [attachment as Attachment], attachments: offerMessage.appendedAttachments?.filter((attachment) => isLinkedAttachment(attachment)), }) requestMessage.setThread({ threadId: credentialRecord.threadId }) @@ -829,7 +829,7 @@ export class V1CredentialProtocol const issueMessage = new V1IssueCredentialMessage({ comment, - credentialAttachments: [attachment], + credentialAttachments: [attachment as Attachment], attachments: credentialRecord.linkedAttachments, }) diff --git a/packages/core/src/modules/connections/services/ConnectionService.ts b/packages/core/src/modules/connections/services/ConnectionService.ts index 8b47534040..ca7a1ef527 100644 --- a/packages/core/src/modules/connections/services/ConnectionService.ts +++ b/packages/core/src/modules/connections/services/ConnectionService.ts @@ -1,6 +1,6 @@ import type { AgentContext } from '../../../agent' import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext' -import type { DidCommV2Message } from '../../../didcomm' +import type { DidCommV1Message } from '../../../didcomm' import type { Query } from '../../../storage/StorageService' import type { AckMessage } from '../../common' import type { OutOfBandDidCommService } from '../../oob/domain/OutOfBandDidCommService' @@ -18,7 +18,6 @@ import { filterContextCorrelationId } from '../../../agent/Events' import { InjectionSymbols } from '../../../constants' import { Key } from '../../../crypto' import { signData, unpackAndVerifySignatureDecorator } from '../../../decorators/signature/SignatureDecoratorUtils' -import { DidCommV1Message } from '../../../didcomm' import { AriesFrameworkError } from '../../../error' import { Logger } from '../../../logger' import { inject, injectable } from '../../../plugins' @@ -449,13 +448,13 @@ export class ConnectionService { * @param messageContext - the inbound message context */ public async assertConnectionOrOutOfBandExchange( - messageContext: InboundMessageContext, + messageContext: InboundMessageContext, { lastSentMessage, lastReceivedMessage, }: { - lastSentMessage?: DidCommV1Message | DidCommV2Message | null - lastReceivedMessage?: DidCommV1Message | DidCommV2Message | null + lastSentMessage?: DidCommV1Message | null + lastReceivedMessage?: DidCommV1Message | null } = {} ) { const { connection, message } = messageContext @@ -475,9 +474,7 @@ export class ConnectionService { const senderKey = messageContext.senderKey && messageContext.senderKey.publicKeyBase58 // set theirService to the value of lastReceivedMessage.service - let theirService = - (message instanceof DidCommV1Message && message.service?.resolvedDidCommService) ?? - (lastReceivedMessage instanceof DidCommV1Message && lastReceivedMessage?.service?.resolvedDidCommService) + let theirService = message.service?.resolvedDidCommService ?? lastReceivedMessage?.service?.resolvedDidCommService let ourService = lastSentMessage?.service?.resolvedDidCommService // 1. check if there's an oob record associated. diff --git a/packages/core/src/modules/credentials/CredentialsApi.ts b/packages/core/src/modules/credentials/CredentialsApi.ts index d061eb4f14..7f53c4231a 100644 --- a/packages/core/src/modules/credentials/CredentialsApi.ts +++ b/packages/core/src/modules/credentials/CredentialsApi.ts @@ -19,13 +19,13 @@ import type { import type { CredentialProtocol } from './protocol/CredentialProtocol' import type { CredentialFormatsFromProtocols } from './protocol/CredentialProtocolOptions' import type { CredentialExchangeRecord } from './repository/CredentialExchangeRecord' -import type { DidCommV1Message } from '../../didcomm/versions/v1' import type { Query } from '../../storage/StorageService' import { AgentContext } from '../../agent' import { MessageSender } from '../../agent/MessageSender' import { getOutboundMessageContext } from '../../agent/getOutboundMessageContext' import { InjectionSymbols } from '../../constants' +import { isDidCommV1Message, type DidCommV1Message } from '../../didcomm/versions/v1' import { AriesFrameworkError } from '../../error' import { Logger } from '../../logger' import { inject, injectable } from '../../plugins' @@ -384,6 +384,10 @@ export class CredentialsApi implements Credent this.logger.debug('Offer Message successfully created', { message }) + if (!isDidCommV1Message(message)) { + throw new AriesFrameworkError('out-of-band credential offer is only supported for DIDComm V1') + } + return { message, credentialRecord } } diff --git a/packages/core/src/modules/credentials/formats/jsonld/JsonLdCredentialFormatService.ts b/packages/core/src/modules/credentials/formats/jsonld/JsonLdCredentialFormatService.ts index dc3e9b9646..ba934048c2 100644 --- a/packages/core/src/modules/credentials/formats/jsonld/JsonLdCredentialFormatService.ts +++ b/packages/core/src/modules/credentials/formats/jsonld/JsonLdCredentialFormatService.ts @@ -5,6 +5,7 @@ import type { JsonLdFormatDataVerifiableCredential, } from './JsonLdCredentialFormat' import type { AgentContext } from '../../../../agent' +import type { V2Attachment } from '../../../../decorators/attachment' import type { CredentialFormatService } from '../CredentialFormatService' import type { CredentialFormatAcceptOfferOptions, @@ -385,7 +386,7 @@ export class JsonLdCredentialFormatService implements CredentialFormatService { + public areCredentialsEqual = (message1: Attachment | V2Attachment, message2: Attachment | V2Attachment): boolean => { const obj1 = message1.getDataAsJson() const obj2 = message2.getDataAsJson() diff --git a/packages/core/src/modules/credentials/protocol/v2/CredentialFormatCoordinator.ts b/packages/core/src/modules/credentials/protocol/v2/CredentialFormatCoordinator.ts index 25d106250c..4ee23b1657 100644 --- a/packages/core/src/modules/credentials/protocol/v2/CredentialFormatCoordinator.ts +++ b/packages/core/src/modules/credentials/protocol/v2/CredentialFormatCoordinator.ts @@ -56,7 +56,7 @@ export class CredentialFormatCoordinator }) } - proposalAttachments.push(attachment) + proposalAttachments.push(attachment as Attachment) formats.push(format) } @@ -161,7 +161,7 @@ export class CredentialFormatCoordinator }) } - offerAttachments.push(attachment) + offerAttachments.push(attachment as Attachment) formats.push(format) } @@ -233,7 +233,7 @@ export class CredentialFormatCoordinator }) } - offerAttachments.push(attachment) + offerAttachments.push(attachment as Attachment) formats.push(format) } @@ -333,7 +333,7 @@ export class CredentialFormatCoordinator credentialFormats, }) - requestAttachments.push(attachment) + requestAttachments.push(attachment as Attachment) formats.push(format) } @@ -389,7 +389,7 @@ export class CredentialFormatCoordinator credentialRecord, }) - requestAttachments.push(attachment) + requestAttachments.push(attachment as Attachment) formats.push(format) } @@ -488,7 +488,7 @@ export class CredentialFormatCoordinator credentialFormats, }) - credentialAttachments.push(attachment) + credentialAttachments.push(attachment as Attachment) formats.push(format) } diff --git a/packages/core/src/modules/credentials/protocol/v3/V3CredentialProtocol.ts b/packages/core/src/modules/credentials/protocol/v3/V3CredentialProtocol.ts index 2d59a02955..87d2b55ee7 100644 --- a/packages/core/src/modules/credentials/protocol/v3/V3CredentialProtocol.ts +++ b/packages/core/src/modules/credentials/protocol/v3/V3CredentialProtocol.ts @@ -4,7 +4,7 @@ import type { MessageHandlerInboundMessage } from '../../../../agent/MessageHand import type { InboundMessageContext } from '../../../../agent/models/InboundMessageContext' import type { DidCommV2Message } from '../../../../didcomm' import type { DependencyManager } from '../../../../plugins' -import type { ProblemReportMessage, V2ProblemReportMessage } from '../../../problem-reports' +import type { V2ProblemReportMessage } from '../../../problem-reports' import type { CredentialFormat, CredentialFormatPayload, @@ -34,7 +34,6 @@ import { AriesFrameworkError } from '../../../../error' import { DidCommMessageRepository } from '../../../../storage' import { uuid } from '../../../../utils/uuid' import { AckStatus } from '../../../common' -import { ConnectionService } from '../../../connections' import { CredentialsModuleConfig } from '../../CredentialsModuleConfig' import { AutoAcceptCredential, CredentialProblemReportReason, CredentialState } from '../../models' import { CredentialExchangeRecord, CredentialRepository } from '../../repository' @@ -49,8 +48,8 @@ import { V3IssueCredentialHandler, V3ProposeCredentialHandler, V3RequestCredentialHandler, + V3CredentialProblemReportHandler, } from './handlers' -import { V3CredentialProblemReportHandler } from './handlers/V3CredentialProblemReportHandler' import { V3CredentialAckMessage, V3CredentialProblemReportMessage, @@ -99,7 +98,7 @@ export class V3CredentialProtocol + messageContext: MessageHandlerInboundMessage ) { messageContext.agentContext.config.logger.info(`Automatically sending request with autoAccept`) diff --git a/packages/core/src/modules/credentials/protocol/v3/handlers/V2ProposeCredentialHandler.ts b/packages/core/src/modules/credentials/protocol/v3/handlers/V3ProposeCredentialHandler.ts similarity index 85% rename from packages/core/src/modules/credentials/protocol/v3/handlers/V2ProposeCredentialHandler.ts rename to packages/core/src/modules/credentials/protocol/v3/handlers/V3ProposeCredentialHandler.ts index ddb3a09943..38d0c1c35b 100644 --- a/packages/core/src/modules/credentials/protocol/v3/handlers/V2ProposeCredentialHandler.ts +++ b/packages/core/src/modules/credentials/protocol/v3/handlers/V3ProposeCredentialHandler.ts @@ -1,17 +1,17 @@ import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' import type { InboundMessageContext } from '../../../../../agent/models/InboundMessageContext' import type { CredentialExchangeRecord } from '../../../repository/CredentialExchangeRecord' -import type { V2CredentialProtocol } from '../V3CredentialProtocol' +import type { V3CredentialProtocol } from '../V3CredentialProtocol' import { OutboundMessageContext } from '../../../../../agent/models' import { V3ProposeCredentialMessage } from '../messages/V3ProposeCredentialMessage' -export class V2ProposeCredentialHandler implements MessageHandler { - private credentialProtocol: V2CredentialProtocol +export class V3ProposeCredentialHandler implements MessageHandler { + private credentialProtocol: V3CredentialProtocol public supportedMessages = [V3ProposeCredentialMessage] - public constructor(credentialProtocol: V2CredentialProtocol) { + public constructor(credentialProtocol: V3CredentialProtocol) { this.credentialProtocol = credentialProtocol } @@ -30,7 +30,7 @@ export class V2ProposeCredentialHandler implements MessageHandler { private async acceptProposal( credentialRecord: CredentialExchangeRecord, - messageContext: MessageHandlerInboundMessage + messageContext: MessageHandlerInboundMessage ) { messageContext.agentContext.config.logger.info(`Automatically sending offer with autoAccept`) diff --git a/packages/core/src/modules/credentials/protocol/v3/handlers/V2RequestCredentialHandler.ts b/packages/core/src/modules/credentials/protocol/v3/handlers/V3RequestCredentialHandler.ts similarity index 85% rename from packages/core/src/modules/credentials/protocol/v3/handlers/V2RequestCredentialHandler.ts rename to packages/core/src/modules/credentials/protocol/v3/handlers/V3RequestCredentialHandler.ts index 1abb188ef4..acec50762b 100644 --- a/packages/core/src/modules/credentials/protocol/v3/handlers/V2RequestCredentialHandler.ts +++ b/packages/core/src/modules/credentials/protocol/v3/handlers/V3RequestCredentialHandler.ts @@ -1,18 +1,18 @@ import type { MessageHandler } from '../../../../../agent/MessageHandler' import type { InboundMessageContext } from '../../../../../agent/models/InboundMessageContext' import type { CredentialExchangeRecord } from '../../../repository' -import type { V2CredentialProtocol } from '../V3CredentialProtocol' +import type { V3CredentialProtocol } from '../V3CredentialProtocol' import { getOutboundMessageContext } from '../../../../../agent/getOutboundMessageContext' import { AriesFrameworkError } from '../../../../../error' import { V3RequestCredentialMessage } from '../messages/V3RequestCredentialMessage' -export class V2RequestCredentialHandler implements MessageHandler { - private credentialProtocol: V2CredentialProtocol +export class V3RequestCredentialHandler implements MessageHandler { + private credentialProtocol: V3CredentialProtocol public supportedMessages = [V3RequestCredentialMessage] - public constructor(credentialProtocol: V2CredentialProtocol) { + public constructor(credentialProtocol: V3CredentialProtocol) { this.credentialProtocol = credentialProtocol } @@ -51,8 +51,6 @@ export class V2RequestCredentialHandler implements MessageHandler { connectionRecord: messageContext.connection, message, associatedRecord: credentialRecord, - lastReceivedMessage: messageContext.message, - lastSentMessage: offerMessage, }) } } diff --git a/packages/core/src/modules/credentials/protocol/v3/handlers/index.ts b/packages/core/src/modules/credentials/protocol/v3/handlers/index.ts index 7151e92f26..7b55ec4a2a 100644 --- a/packages/core/src/modules/credentials/protocol/v3/handlers/index.ts +++ b/packages/core/src/modules/credentials/protocol/v3/handlers/index.ts @@ -1,6 +1,6 @@ export * from './V3CredentialAckHandler' -export * from './V2IssueCredentialHandler' -export * from './V2OfferCredentialHandler' -export * from './V2ProposeCredentialHandler' -export * from './V2RequestCredentialHandler' +export * from './V3IssueCredentialHandler' +export * from './V3OfferCredentialHandler' +export * from './V3ProposeCredentialHandler' +export * from './V3RequestCredentialHandler' export * from './V3CredentialProblemReportHandler' From 35f75f421daa78cf7059fdeccba293bfe3a33540 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Tue, 22 Aug 2023 18:40:55 -0300 Subject: [PATCH 06/22] feat: generalize getOutboundMessageContext Signed-off-by: Ariel Gentile --- .../src/agent/getOutboundMessageContext.ts | 28 ++++++++++++++++--- .../versions/v2/DidCommV2BaseMessage.ts | 24 ++++++++++++++++ .../didcomm/versions/v2/DidCommV2Message.ts | 12 ++++++++ .../storage/didcomm/DidCommMessageRecord.ts | 8 +++--- 4 files changed, 64 insertions(+), 8 deletions(-) diff --git a/packages/core/src/agent/getOutboundMessageContext.ts b/packages/core/src/agent/getOutboundMessageContext.ts index ea57257da8..7b2b0c0648 100644 --- a/packages/core/src/agent/getOutboundMessageContext.ts +++ b/packages/core/src/agent/getOutboundMessageContext.ts @@ -1,5 +1,5 @@ +import type { AgentBaseMessage } from './AgentBaseMessage' import type { AgentContext } from './context' -import type { DidCommV1Message } from '../didcomm/versions/v1' import type { ConnectionRecord, Routing } from '../modules/connections' import type { ResolvedDidCommService } from '../modules/didcomm' import type { OutOfBandRecord } from '../modules/oob' @@ -7,6 +7,7 @@ import type { BaseRecordAny } from '../storage/BaseRecord' import { Key } from '../crypto' import { ServiceDecorator } from '../decorators/service/ServiceDecorator' +import { DidCommV1Message, DidCommV2Message } from '../didcomm' import { AriesFrameworkError } from '../error' import { OutOfBandService, OutOfBandRole, OutOfBandRepository } from '../modules/oob' import { OutOfBandRecordMetadataKeys } from '../modules/oob/repository/outOfBandRecordMetadataTypes' @@ -37,9 +38,9 @@ export async function getOutboundMessageContext( }: { connectionRecord?: ConnectionRecord associatedRecord?: BaseRecordAny - message: DidCommV1Message - lastReceivedMessage?: DidCommV1Message - lastSentMessage?: DidCommV1Message + message: AgentBaseMessage + lastReceivedMessage?: AgentBaseMessage + lastSentMessage?: AgentBaseMessage } ) { // TODO: even if using a connection record, we should check if there's an oob record associated and this @@ -48,6 +49,17 @@ export async function getOutboundMessageContext( agentContext.config.logger.debug( `Creating outbound message context for message ${message.id} with connection ${connectionRecord.id}` ) + + // Attach 'from' and 'to' fields according to connection record (unless they are previously defined) + if (message instanceof DidCommV2Message) { + message.from = message.from ?? connectionRecord.did + const recipients = message.to ?? (connectionRecord.theirDid ? [connectionRecord.theirDid] : undefined) + if (!recipients) { + throw new AriesFrameworkError('Cannot find recipient did for message') + } + message.to = recipients + } + return new OutboundMessageContext(message, { agentContext, associatedRecord, @@ -67,6 +79,14 @@ export async function getOutboundMessageContext( ) } + if ( + !(message instanceof DidCommV1Message) || + !(lastReceivedMessage instanceof DidCommV1Message) || + !(lastSentMessage instanceof DidCommV1Message) + ) { + throw new AriesFrameworkError('No connection record associated with DIDComm V2 messages exchange') + } + // Connectionless return getConnectionlessOutboundMessageContext(agentContext, { message, diff --git a/packages/core/src/didcomm/versions/v2/DidCommV2BaseMessage.ts b/packages/core/src/didcomm/versions/v2/DidCommV2BaseMessage.ts index dd67574eac..dd3927ca93 100644 --- a/packages/core/src/didcomm/versions/v2/DidCommV2BaseMessage.ts +++ b/packages/core/src/didcomm/versions/v2/DidCommV2BaseMessage.ts @@ -17,6 +17,8 @@ export type DidCommV2MessageParams = { to?: string | string[] thid?: string parentThreadId?: string + senderOrder?: number + receivedOrders?: { [key: string]: number } // TODO: Update to DIDComm V2 format createdTime?: number expiresTime?: number fromPrior?: string @@ -25,6 +27,12 @@ export type DidCommV2MessageParams = { body?: unknown } +type DidCommV2ReceiverOrder = { + id: string + last: number + gaps: number[] +} + export class DidCommV2BaseMessage { @Matches(MessageIdRegExp) public id!: string @@ -64,6 +72,16 @@ export class DidCommV2BaseMessage { @IsOptional() public parentThreadId?: string + @Expose({ name: 'sender_order' }) + @IsNumber() + @IsOptional() + public senderOrder?: number + + @Expose({ name: 'received_orders' }) + @IsOptional() + @IsArray() + public receivedOrders?: DidCommV2ReceiverOrder[] + @Expose({ name: 'from_prior' }) @IsString() @IsOptional() @@ -92,6 +110,12 @@ export class DidCommV2BaseMessage { this.to = typeof options.to === 'string' ? [options.to] : options.to this.thid = options.thid this.parentThreadId = options.parentThreadId + this.senderOrder = options.senderOrder + this.receivedOrders = Object.entries(options.receivedOrders ?? {}).map(([id, last]) => ({ + id, + last, + gaps: [], + })) this.createdTime = options.createdTime this.expiresTime = options.expiresTime this.fromPrior = options.fromPrior diff --git a/packages/core/src/didcomm/versions/v2/DidCommV2Message.ts b/packages/core/src/didcomm/versions/v2/DidCommV2Message.ts index 2803eb9c4e..d4a3596180 100644 --- a/packages/core/src/didcomm/versions/v2/DidCommV2Message.ts +++ b/packages/core/src/didcomm/versions/v2/DidCommV2Message.ts @@ -1,6 +1,7 @@ import type { PlaintextDidCommV2Message } from './types' import type { AgentBaseMessage } from '../../../agent/AgentBaseMessage' import type { ServiceDecorator } from '../../../decorators/service/ServiceDecorator' +import type { ThreadDecorator } from '../../../decorators/thread/ThreadDecorator' import type { PlaintextMessage } from '../../types' import { AriesFrameworkError } from '../../../error' @@ -26,6 +27,17 @@ export class DidCommV2Message extends DidCommV2BaseMessage implements AgentBaseM return this.thid ?? this.id } + public setThread(options: Partial) { + this.thid = options.threadId + this.parentThreadId = options.parentThreadId + this.senderOrder = options.senderOrder + this.receivedOrders = Object.entries(options.receivedOrders ?? {}).map(([id, last]) => ({ + id, + last, + gaps: [], + })) + } + public hasAnyReturnRoute() { return false } diff --git a/packages/core/src/storage/didcomm/DidCommMessageRecord.ts b/packages/core/src/storage/didcomm/DidCommMessageRecord.ts index 6e5a60b3b0..7d0a88c555 100644 --- a/packages/core/src/storage/didcomm/DidCommMessageRecord.ts +++ b/packages/core/src/storage/didcomm/DidCommMessageRecord.ts @@ -58,13 +58,13 @@ export class DidCommMessageRecord extends BaseRecord } public getTags() { - const messageId = this.message['@id'] as string - const messageType = this.message['@type'] as string + const messageId = (this.message['@id'] ?? this.message['id']) as string + const messageType = (this.message['@type'] ?? this.message['type']) as string const { protocolName, protocolMajorVersion, protocolMinorVersion, messageName } = parseMessageType(messageType) const thread = this.message['~thread'] - let threadId = messageId + let threadId = (this.message['thid'] ?? messageId) as string if (isJsonObject(thread) && typeof thread.thid === 'string') { threadId = thread.thid @@ -89,7 +89,7 @@ export class DidCommMessageRecord extends BaseRecord public getMessageInstance( messageClass: MessageClass ): InstanceType { - const messageType = parseMessageType(this.message['@type'] as string) + const messageType = parseMessageType((this.message['@type'] ?? this.message['type']) as string) if (!canHandleMessageType(messageClass, messageType)) { throw new AriesFrameworkError('Provided message class type does not match type of stored message') From d7a5fa57c0d5bf738da55548a755a91f40ec961f Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Tue, 22 Aug 2023 18:41:13 -0300 Subject: [PATCH 07/22] feat: use v3 in demo Signed-off-by: Ariel Gentile --- demo/src/BaseAgent.ts | 4 ++++ demo/src/Faber.ts | 2 +- packages/core/src/modules/credentials/CredentialsModule.ts | 7 +++++-- .../src/modules/credentials/CredentialsModuleConfig.ts | 4 ++-- packages/core/src/modules/credentials/protocol/index.ts | 1 + 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/demo/src/BaseAgent.ts b/demo/src/BaseAgent.ts index c7b9902686..f6f0bd638e 100644 --- a/demo/src/BaseAgent.ts +++ b/demo/src/BaseAgent.ts @@ -23,6 +23,7 @@ import { DidsModule, V2ProofProtocol, V2CredentialProtocol, + V3CredentialProtocol, ProofsModule, AutoAcceptProof, AutoAcceptCredential, @@ -122,6 +123,9 @@ function getAskarAnonCredsIndyModules() { new V2CredentialProtocol({ credentialFormats: [legacyIndyCredentialFormatService, new AnonCredsCredentialFormatService()], }), + new V3CredentialProtocol({ + credentialFormats: [legacyIndyCredentialFormatService, new AnonCredsCredentialFormatService()], + }), ], }), proofs: new ProofsModule({ diff --git a/demo/src/Faber.ts b/demo/src/Faber.ts index 1b66998405..caadc51576 100644 --- a/demo/src/Faber.ts +++ b/demo/src/Faber.ts @@ -203,7 +203,7 @@ export class Faber extends BaseAgent { await this.agent.credentials.offerCredential({ connectionId: connectionRecord.id, - protocolVersion: 'v2', + protocolVersion: connectionRecord.isDidCommV1Connection ? 'v2' : 'v3', credentialFormats: { anoncreds: { attributes: [ diff --git a/packages/core/src/modules/credentials/CredentialsModule.ts b/packages/core/src/modules/credentials/CredentialsModule.ts index 9cae75eb28..c36b799999 100644 --- a/packages/core/src/modules/credentials/CredentialsModule.ts +++ b/packages/core/src/modules/credentials/CredentialsModule.ts @@ -9,8 +9,8 @@ import { Protocol } from '../../agent/models' import { CredentialsApi } from './CredentialsApi' import { CredentialsModuleConfig } from './CredentialsModuleConfig' +import { V2CredentialProtocol, V3CredentialProtocol } from './protocol' import { RevocationNotificationService } from './protocol/revocation-notification/services' -import { V2CredentialProtocol } from './protocol/v2' import { CredentialRepository } from './repository' /** @@ -37,7 +37,10 @@ export class CredentialsModule } diff --git a/packages/core/src/modules/credentials/CredentialsModuleConfig.ts b/packages/core/src/modules/credentials/CredentialsModuleConfig.ts index e6d23909ed..61df19ea1e 100644 --- a/packages/core/src/modules/credentials/CredentialsModuleConfig.ts +++ b/packages/core/src/modules/credentials/CredentialsModuleConfig.ts @@ -18,11 +18,11 @@ export interface CredentialsModuleConfigOptions Date: Tue, 22 Aug 2023 19:16:10 -0300 Subject: [PATCH 08/22] feat: set from/to in getOutboundContext Signed-off-by: Ariel Gentile --- .../modules/basic-messages/BasicMessagesApi.ts | 10 +++++----- .../protocols/v2/V2BasicMessageProtocol.ts | 10 +--------- .../protocols/v2/messages/V2BasicMessage.ts | 15 +-------------- 3 files changed, 7 insertions(+), 28 deletions(-) diff --git a/packages/core/src/modules/basic-messages/BasicMessagesApi.ts b/packages/core/src/modules/basic-messages/BasicMessagesApi.ts index d50da6da62..df1328b1f3 100644 --- a/packages/core/src/modules/basic-messages/BasicMessagesApi.ts +++ b/packages/core/src/modules/basic-messages/BasicMessagesApi.ts @@ -1,10 +1,10 @@ -import type { BasicMessageProtocol, V2BasicMessage } from './protocols' +import type { BasicMessageProtocol } from './protocols' import type { BasicMessageRecord } from './repository/BasicMessageRecord' import type { Query } from '../../storage/StorageService' import { AgentContext } from '../../agent' import { MessageSender } from '../../agent/MessageSender' -import { OutboundMessageContext } from '../../agent/models' +import { getOutboundMessageContext } from '../../agent/getOutboundMessageContext' import { AriesFrameworkError } from '../../error' import { injectable } from '../../plugins' import { ConnectionService } from '../connections' @@ -79,9 +79,9 @@ export class BasicMessagesApi implements Ba parentThreadId, } ) - const outboundMessageContext = new OutboundMessageContext(basicMessage as V2BasicMessage, { - agentContext: this.agentContext, - connection, + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message: basicMessage, + connectionRecord: connection, associatedRecord: basicMessageRecord, }) diff --git a/packages/core/src/modules/basic-messages/protocols/v2/V2BasicMessageProtocol.ts b/packages/core/src/modules/basic-messages/protocols/v2/V2BasicMessageProtocol.ts index 074a93dafe..d101227e63 100644 --- a/packages/core/src/modules/basic-messages/protocols/v2/V2BasicMessageProtocol.ts +++ b/packages/core/src/modules/basic-messages/protocols/v2/V2BasicMessageProtocol.ts @@ -43,15 +43,7 @@ export class V2BasicMessageProtocol extends BaseBasicMessageProtocol { public async createMessage(agentContext: AgentContext, options: CreateMessageOptions) { const { content, parentThreadId, connectionRecord } = options - if (!connectionRecord.did || !connectionRecord.theirDid) { - throw new AriesFrameworkError('Connection Record must have both our and their did') - } - - const basicMessage = new V2BasicMessage({ - from: connectionRecord.did, - to: connectionRecord.theirDid, - content, - }) + const basicMessage = new V2BasicMessage({ content }) const basicMessageRepository = agentContext.dependencyManager.resolve(BasicMessageRepository) diff --git a/packages/core/src/modules/basic-messages/protocols/v2/messages/V2BasicMessage.ts b/packages/core/src/modules/basic-messages/protocols/v2/messages/V2BasicMessage.ts index a8e57f0439..7429e3dee0 100644 --- a/packages/core/src/modules/basic-messages/protocols/v2/messages/V2BasicMessage.ts +++ b/packages/core/src/modules/basic-messages/protocols/v2/messages/V2BasicMessage.ts @@ -20,10 +20,6 @@ export class V2BasicMessage extends DidCommV2Message { @IsNotEmpty() public createdTime!: number - @IsString() - @IsNotEmpty() - public from!: string - @IsObject() @ValidateNested() @Type(() => V2BasicMessageBody) @@ -34,21 +30,12 @@ export class V2BasicMessage extends DidCommV2Message { * sentTime will be assigned to new Date if not passed, id will be assigned to uuid/v4 if not passed * @param options */ - public constructor(options: { - from: string - to: string - content: string - sentTime?: Date - id?: string - locale?: string - }) { + public constructor(options: { content: string; sentTime?: Date; id?: string; locale?: string }) { super() if (options) { this.id = options.id || this.generateId() this.createdTime = options.sentTime?.getTime() || new Date().getTime() - this.from = options.from - this.to = [options.to] this.body = new V2BasicMessageBody({ content: options.content }) this.language = options.locale || 'en' } From 35c15c47615843dcea38fcec04a1cba60d1b969e Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Tue, 22 Aug 2023 20:16:27 -0300 Subject: [PATCH 09/22] feat: transform V1/V2 attachments Signed-off-by: Ariel Gentile --- .../decorators/attachment/v2/V2Attachment.ts | 38 ++++++++++++++++-- packages/core/src/didcomm/index.ts | 1 + packages/core/src/didcomm/transformers.ts | 39 +++++++++++++++++++ .../formats/CredentialFormatServiceOptions.ts | 38 +++++++++--------- .../v3/CredentialFormatCoordinator.ts | 17 ++++---- 5 files changed, 102 insertions(+), 31 deletions(-) create mode 100644 packages/core/src/didcomm/transformers.ts diff --git a/packages/core/src/decorators/attachment/v2/V2Attachment.ts b/packages/core/src/decorators/attachment/v2/V2Attachment.ts index e46aad845c..98dd398761 100644 --- a/packages/core/src/decorators/attachment/v2/V2Attachment.ts +++ b/packages/core/src/decorators/attachment/v2/V2Attachment.ts @@ -1,7 +1,8 @@ +import type { JwsDetachedFormat, JwsFlattenedDetachedFormat } from '../../../crypto/JwsTypes' + import { Expose, Type } from 'class-transformer' -import { IsBase64, IsInstance, IsMimeType, IsOptional, IsString, ValidateNested } from 'class-validator' +import { IsBase64, IsDate, IsInstance, IsInt, IsMimeType, IsOptional, IsString, ValidateNested } from 'class-validator' -import { Jws } from '../../../crypto/JwsTypes' import { AriesFrameworkError } from '../../../error' import { JsonEncoder } from '../../../utils/JsonEncoder' import { uuid } from '../../../utils/uuid' @@ -12,6 +13,7 @@ export interface V2AttachmentOptions { id?: string description?: string filename?: string + lastmodTime?: Date mediaType?: string byteCount?: number data: V2AttachmentData @@ -21,7 +23,8 @@ export interface V2AttachmentDataOptions { base64?: string json?: Record links?: string[] - jws?: Jws + jws?: JwsDetachedFormat | JwsFlattenedDetachedFormat + hash?: string } /** @@ -52,7 +55,14 @@ export class V2AttachmentData { * A JSON Web Signature over the content of the attachment. Optional. */ @IsOptional() - public jws?: Jws + public jws?: JwsDetachedFormat | JwsFlattenedDetachedFormat + + /** + * The hash of the content encoded in multi-hash format. Used as an integrity check for the attachment, and MUST be used if the data is referenced via the links data attribute. + */ + @IsOptional() + @IsString() + public hash?: string public constructor(options: V2AttachmentDataOptions) { if (options) { @@ -60,6 +70,7 @@ export class V2AttachmentData { this.json = options.json this.links = options.links this.jws = options.jws + this.hash = options.hash } } } @@ -73,7 +84,9 @@ export class V2Attachment { if (options) { this.id = options.id ?? uuid() this.description = options.description + this.byteCount = options.byteCount this.filename = options.filename + this.lastmodTime = options.lastmodTime this.mediaType = options.mediaType this.data = options.data } @@ -110,6 +123,23 @@ export class V2Attachment { @IsMimeType() public mediaType?: string + /** + * A hint about when the content in this attachment was last modified. + */ + @Expose({ name: 'lastmod_time' }) + @Type(() => Date) + @IsOptional() + @IsDate() + public lastmodTime?: Date + + /** + * Optional, and mostly relevant when content is included by reference instead of by value. Lets the receiver guess how expensive it will be, in time, bandwidth, and storage, to fully fetch the attachment. + */ + @Expose({ name: 'byte_count' }) + @IsOptional() + @IsInt() + public byteCount?: number + @Type(() => V2AttachmentData) @ValidateNested() @IsInstance(V2AttachmentData) diff --git a/packages/core/src/didcomm/index.ts b/packages/core/src/didcomm/index.ts index e74379142a..3b11531e8b 100644 --- a/packages/core/src/didcomm/index.ts +++ b/packages/core/src/didcomm/index.ts @@ -6,6 +6,7 @@ import type { Constructor } from '../utils/mixins' export * from './versions/v1' export * from './versions/v2' export * from './types' +export * from './transformers' export * from './helpers' export * from './JweEnvelope' diff --git a/packages/core/src/didcomm/transformers.ts b/packages/core/src/didcomm/transformers.ts new file mode 100644 index 0000000000..7e29a728af --- /dev/null +++ b/packages/core/src/didcomm/transformers.ts @@ -0,0 +1,39 @@ +import { Attachment, AttachmentData, V2Attachment, V2AttachmentData } from '../decorators/attachment' + +export function toV2Attachment(v1Attachment: Attachment): V2Attachment { + const { id, description, byteCount, filename, lastmodTime, mimeType, data } = v1Attachment + return new V2Attachment({ + id, + description, + byteCount, + filename, + lastmodTime, + mediaType: mimeType, + data: new V2AttachmentData({ + base64: data.base64, + json: data.json, + jws: data.jws, + links: data.links, + hash: data.sha256, + }), + }) +} + +export function toV1Attachment(v2Attachment: V2Attachment): Attachment { + const { id, description, byteCount, filename, lastmodTime, mediaType, data } = v2Attachment + return new Attachment({ + id, + description, + byteCount, + filename, + lastmodTime, + mimeType: mediaType, + data: new AttachmentData({ + base64: data.base64, + json: data.json, + jws: data.jws, + links: data.links, + sha256: data.hash, + }), + }) +} diff --git a/packages/core/src/modules/credentials/formats/CredentialFormatServiceOptions.ts b/packages/core/src/modules/credentials/formats/CredentialFormatServiceOptions.ts index 4f3aa0072c..21ade782cc 100644 --- a/packages/core/src/modules/credentials/formats/CredentialFormatServiceOptions.ts +++ b/packages/core/src/modules/credentials/formats/CredentialFormatServiceOptions.ts @@ -1,6 +1,6 @@ import type { CredentialFormat, CredentialFormatPayload } from './CredentialFormat' import type { CredentialFormatService } from './CredentialFormatService' -import type { Attachment, V2Attachment } from '../../../decorators/attachment' +import type { Attachment } from '../../../decorators/attachment' import type { CredentialFormatSpec } from '../models/CredentialFormatSpec' import type { CredentialPreviewAttributeOptions } from '../models/CredentialPreviewAttribute' import type { CredentialExchangeRecord } from '../repository/CredentialExchangeRecord' @@ -42,19 +42,19 @@ export type ExtractCredentialFormats = { */ export interface CredentialFormatCreateReturn { format: CredentialFormatSpec - attachment: Attachment | V2Attachment + attachment: Attachment } /** * Base return type for all credential process methods. */ export interface CredentialFormatProcessOptions { - attachment: Attachment | V2Attachment + attachment: Attachment credentialRecord: CredentialExchangeRecord } export interface CredentialFormatProcessCredentialOptions extends CredentialFormatProcessOptions { - requestAttachment: Attachment | V2Attachment + requestAttachment: Attachment } export interface CredentialFormatCreateProposalOptions { @@ -68,7 +68,7 @@ export interface CredentialFormatAcceptProposalOptions attachmentId?: string - proposalAttachment: Attachment | V2Attachment + proposalAttachment: Attachment } export interface CredentialFormatCreateProposalReturn extends CredentialFormatCreateReturn { @@ -86,7 +86,7 @@ export interface CredentialFormatAcceptOfferOptions credentialFormats?: CredentialFormatPayload<[CF], 'acceptOffer'> attachmentId?: string - offerAttachment: Attachment | V2Attachment + offerAttachment: Attachment } export interface CredentialFormatCreateOfferReturn extends CredentialFormatCreateReturn { @@ -103,34 +103,34 @@ export interface CredentialFormatAcceptRequestOptions attachmentId?: string - requestAttachment: Attachment | V2Attachment - offerAttachment?: Attachment | V2Attachment + requestAttachment: Attachment + offerAttachment?: Attachment } // Auto accept method interfaces export interface CredentialFormatAutoRespondProposalOptions { credentialRecord: CredentialExchangeRecord - proposalAttachment: Attachment | V2Attachment - offerAttachment: Attachment | V2Attachment + proposalAttachment: Attachment + offerAttachment: Attachment } export interface CredentialFormatAutoRespondOfferOptions { credentialRecord: CredentialExchangeRecord - proposalAttachment: Attachment | V2Attachment - offerAttachment: Attachment | V2Attachment + proposalAttachment: Attachment + offerAttachment: Attachment } export interface CredentialFormatAutoRespondRequestOptions { credentialRecord: CredentialExchangeRecord - proposalAttachment?: Attachment | V2Attachment - offerAttachment: Attachment | V2Attachment - requestAttachment: Attachment | V2Attachment + proposalAttachment?: Attachment + offerAttachment: Attachment + requestAttachment: Attachment } export interface CredentialFormatAutoRespondCredentialOptions { credentialRecord: CredentialExchangeRecord - proposalAttachment?: Attachment | V2Attachment - offerAttachment?: Attachment | V2Attachment - requestAttachment: Attachment | V2Attachment - credentialAttachment: Attachment | V2Attachment + proposalAttachment?: Attachment + offerAttachment?: Attachment + requestAttachment: Attachment + credentialAttachment: Attachment } diff --git a/packages/core/src/modules/credentials/protocol/v3/CredentialFormatCoordinator.ts b/packages/core/src/modules/credentials/protocol/v3/CredentialFormatCoordinator.ts index 58b2bdfda4..5b2b4191fc 100644 --- a/packages/core/src/modules/credentials/protocol/v3/CredentialFormatCoordinator.ts +++ b/packages/core/src/modules/credentials/protocol/v3/CredentialFormatCoordinator.ts @@ -1,9 +1,10 @@ import type { AgentContext } from '../../../../agent' -import type { Attachment, V2Attachment } from '../../../../decorators/attachment' +import type { V2Attachment } from '../../../../decorators/attachment' import type { CredentialFormatPayload, CredentialFormatService, ExtractCredentialFormats } from '../../formats' import type { CredentialFormatSpec } from '../../models' import type { CredentialExchangeRecord } from '../../repository/CredentialExchangeRecord' +import { toV1Attachment, toV2Attachment } from '../../../../didcomm' import { AriesFrameworkError } from '../../../../error/AriesFrameworkError' import { DidCommMessageRepository, DidCommMessageRole } from '../../../../storage' @@ -56,7 +57,7 @@ export class CredentialFormatCoordinator }) } - proposalAttachments.push(attachment as V2Attachment) + proposalAttachments.push(toV2Attachment(attachment)) formats.push(format) } @@ -161,7 +162,7 @@ export class CredentialFormatCoordinator }) } - offerAttachments.push(attachment as V2Attachment) + offerAttachments.push(toV2Attachment(attachment)) formats.push(format) } @@ -233,7 +234,7 @@ export class CredentialFormatCoordinator }) } - offerAttachments.push(attachment as V2Attachment) + offerAttachments.push(toV2Attachment(attachment)) formats.push(format) } @@ -333,7 +334,7 @@ export class CredentialFormatCoordinator credentialFormats, }) - requestAttachments.push(attachment as V2Attachment) + requestAttachments.push(toV2Attachment(attachment)) formats.push(format) } @@ -389,7 +390,7 @@ export class CredentialFormatCoordinator credentialRecord, }) - requestAttachments.push(attachment as V2Attachment) + requestAttachments.push(toV2Attachment(attachment)) formats.push(format) } @@ -488,7 +489,7 @@ export class CredentialFormatCoordinator credentialFormats, }) - credentialAttachments.push(attachment as V2Attachment) + credentialAttachments.push(toV2Attachment(attachment)) formats.push(format) } @@ -560,7 +561,7 @@ export class CredentialFormatCoordinator throw new AriesFrameworkError(`Attachment with id ${attachmentId} not found in attachments.`) } - return attachment + return toV1Attachment(attachment) } private getAttachmentIdForService(credentialFormatService: CredentialFormatService, formats: CredentialFormatSpec[]) { From 3b850d99fa308e4095d1f24743f08924816d863b Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Tue, 22 Aug 2023 20:16:42 -0300 Subject: [PATCH 10/22] fix: basic message types Signed-off-by: Ariel Gentile --- .../protocols/v2/__tests__/V2BasicMessageProtocol.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/core/src/modules/basic-messages/protocols/v2/__tests__/V2BasicMessageProtocol.test.ts b/packages/core/src/modules/basic-messages/protocols/v2/__tests__/V2BasicMessageProtocol.test.ts index 6118ac1ced..6aa93a20bb 100644 --- a/packages/core/src/modules/basic-messages/protocols/v2/__tests__/V2BasicMessageProtocol.test.ts +++ b/packages/core/src/modules/basic-messages/protocols/v2/__tests__/V2BasicMessageProtocol.test.ts @@ -63,8 +63,6 @@ describe('V2BasicMessageProtocol', () => { it(`stores record and emits message and basic message record`, async () => { const basicMessage = new V2BasicMessage({ id: '123', - from: 'did:from', - to: 'did:to', content: 'message', }) From 1b493fb6ab789d8f92935d95ec5924411fcc3f6e Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Tue, 22 Aug 2023 21:08:34 -0300 Subject: [PATCH 11/22] fix: use DIDComm v2 for ICV3 Ack message Signed-off-by: Ariel Gentile --- .../protocol/v3/V3CredentialProtocol.ts | 2 -- .../v3/messages/V3CredentialAckMessage.ts | 20 ++++++++++--------- .../v2/handlers/V2PresentationAckHandler.ts | 6 +++--- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/core/src/modules/credentials/protocol/v3/V3CredentialProtocol.ts b/packages/core/src/modules/credentials/protocol/v3/V3CredentialProtocol.ts index 87d2b55ee7..6b7005cdfa 100644 --- a/packages/core/src/modules/credentials/protocol/v3/V3CredentialProtocol.ts +++ b/packages/core/src/modules/credentials/protocol/v3/V3CredentialProtocol.ts @@ -33,7 +33,6 @@ import { Protocol } from '../../../../agent/models/features/Protocol' import { AriesFrameworkError } from '../../../../error' import { DidCommMessageRepository } from '../../../../storage' import { uuid } from '../../../../utils/uuid' -import { AckStatus } from '../../../common' import { CredentialsModuleConfig } from '../../CredentialsModuleConfig' import { AutoAcceptCredential, CredentialProblemReportReason, CredentialState } from '../../models' import { CredentialExchangeRecord, CredentialRepository } from '../../repository' @@ -711,7 +710,6 @@ export class V3CredentialProtocol Date: Tue, 22 Aug 2023 18:40:55 -0300 Subject: [PATCH 12/22] feat: generalize getOutboundMessageContext Signed-off-by: Ariel Gentile --- .../src/agent/getOutboundMessageContext.ts | 28 ++++++++++++++++--- .../versions/v2/DidCommV2BaseMessage.ts | 24 ++++++++++++++++ .../didcomm/versions/v2/DidCommV2Message.ts | 12 ++++++++ .../storage/didcomm/DidCommMessageRecord.ts | 8 +++--- 4 files changed, 64 insertions(+), 8 deletions(-) diff --git a/packages/core/src/agent/getOutboundMessageContext.ts b/packages/core/src/agent/getOutboundMessageContext.ts index ea57257da8..7b2b0c0648 100644 --- a/packages/core/src/agent/getOutboundMessageContext.ts +++ b/packages/core/src/agent/getOutboundMessageContext.ts @@ -1,5 +1,5 @@ +import type { AgentBaseMessage } from './AgentBaseMessage' import type { AgentContext } from './context' -import type { DidCommV1Message } from '../didcomm/versions/v1' import type { ConnectionRecord, Routing } from '../modules/connections' import type { ResolvedDidCommService } from '../modules/didcomm' import type { OutOfBandRecord } from '../modules/oob' @@ -7,6 +7,7 @@ import type { BaseRecordAny } from '../storage/BaseRecord' import { Key } from '../crypto' import { ServiceDecorator } from '../decorators/service/ServiceDecorator' +import { DidCommV1Message, DidCommV2Message } from '../didcomm' import { AriesFrameworkError } from '../error' import { OutOfBandService, OutOfBandRole, OutOfBandRepository } from '../modules/oob' import { OutOfBandRecordMetadataKeys } from '../modules/oob/repository/outOfBandRecordMetadataTypes' @@ -37,9 +38,9 @@ export async function getOutboundMessageContext( }: { connectionRecord?: ConnectionRecord associatedRecord?: BaseRecordAny - message: DidCommV1Message - lastReceivedMessage?: DidCommV1Message - lastSentMessage?: DidCommV1Message + message: AgentBaseMessage + lastReceivedMessage?: AgentBaseMessage + lastSentMessage?: AgentBaseMessage } ) { // TODO: even if using a connection record, we should check if there's an oob record associated and this @@ -48,6 +49,17 @@ export async function getOutboundMessageContext( agentContext.config.logger.debug( `Creating outbound message context for message ${message.id} with connection ${connectionRecord.id}` ) + + // Attach 'from' and 'to' fields according to connection record (unless they are previously defined) + if (message instanceof DidCommV2Message) { + message.from = message.from ?? connectionRecord.did + const recipients = message.to ?? (connectionRecord.theirDid ? [connectionRecord.theirDid] : undefined) + if (!recipients) { + throw new AriesFrameworkError('Cannot find recipient did for message') + } + message.to = recipients + } + return new OutboundMessageContext(message, { agentContext, associatedRecord, @@ -67,6 +79,14 @@ export async function getOutboundMessageContext( ) } + if ( + !(message instanceof DidCommV1Message) || + !(lastReceivedMessage instanceof DidCommV1Message) || + !(lastSentMessage instanceof DidCommV1Message) + ) { + throw new AriesFrameworkError('No connection record associated with DIDComm V2 messages exchange') + } + // Connectionless return getConnectionlessOutboundMessageContext(agentContext, { message, diff --git a/packages/core/src/didcomm/versions/v2/DidCommV2BaseMessage.ts b/packages/core/src/didcomm/versions/v2/DidCommV2BaseMessage.ts index dd67574eac..dd3927ca93 100644 --- a/packages/core/src/didcomm/versions/v2/DidCommV2BaseMessage.ts +++ b/packages/core/src/didcomm/versions/v2/DidCommV2BaseMessage.ts @@ -17,6 +17,8 @@ export type DidCommV2MessageParams = { to?: string | string[] thid?: string parentThreadId?: string + senderOrder?: number + receivedOrders?: { [key: string]: number } // TODO: Update to DIDComm V2 format createdTime?: number expiresTime?: number fromPrior?: string @@ -25,6 +27,12 @@ export type DidCommV2MessageParams = { body?: unknown } +type DidCommV2ReceiverOrder = { + id: string + last: number + gaps: number[] +} + export class DidCommV2BaseMessage { @Matches(MessageIdRegExp) public id!: string @@ -64,6 +72,16 @@ export class DidCommV2BaseMessage { @IsOptional() public parentThreadId?: string + @Expose({ name: 'sender_order' }) + @IsNumber() + @IsOptional() + public senderOrder?: number + + @Expose({ name: 'received_orders' }) + @IsOptional() + @IsArray() + public receivedOrders?: DidCommV2ReceiverOrder[] + @Expose({ name: 'from_prior' }) @IsString() @IsOptional() @@ -92,6 +110,12 @@ export class DidCommV2BaseMessage { this.to = typeof options.to === 'string' ? [options.to] : options.to this.thid = options.thid this.parentThreadId = options.parentThreadId + this.senderOrder = options.senderOrder + this.receivedOrders = Object.entries(options.receivedOrders ?? {}).map(([id, last]) => ({ + id, + last, + gaps: [], + })) this.createdTime = options.createdTime this.expiresTime = options.expiresTime this.fromPrior = options.fromPrior diff --git a/packages/core/src/didcomm/versions/v2/DidCommV2Message.ts b/packages/core/src/didcomm/versions/v2/DidCommV2Message.ts index 0b517c10d9..1dea0dcfd4 100644 --- a/packages/core/src/didcomm/versions/v2/DidCommV2Message.ts +++ b/packages/core/src/didcomm/versions/v2/DidCommV2Message.ts @@ -1,6 +1,7 @@ import type { PlaintextDidCommV2Message } from './types' import type { AgentBaseMessage } from '../../../agent/AgentBaseMessage' import type { ServiceDecorator } from '../../../decorators/service/ServiceDecorator' +import type { ThreadDecorator } from '../../../decorators/thread/ThreadDecorator' import type { PlaintextMessage } from '../../types' import { AriesFrameworkError } from '../../../error' @@ -26,6 +27,17 @@ export class DidCommV2Message extends DidCommV2BaseMessage implements AgentBaseM return this.thid ?? this.id } + public setThread(options: Partial) { + this.thid = options.threadId + this.parentThreadId = options.parentThreadId + this.senderOrder = options.senderOrder + this.receivedOrders = Object.entries(options.receivedOrders ?? {}).map(([id, last]) => ({ + id, + last, + gaps: [], + })) + } + public hasAnyReturnRoute() { return false } diff --git a/packages/core/src/storage/didcomm/DidCommMessageRecord.ts b/packages/core/src/storage/didcomm/DidCommMessageRecord.ts index 6e5a60b3b0..7d0a88c555 100644 --- a/packages/core/src/storage/didcomm/DidCommMessageRecord.ts +++ b/packages/core/src/storage/didcomm/DidCommMessageRecord.ts @@ -58,13 +58,13 @@ export class DidCommMessageRecord extends BaseRecord } public getTags() { - const messageId = this.message['@id'] as string - const messageType = this.message['@type'] as string + const messageId = (this.message['@id'] ?? this.message['id']) as string + const messageType = (this.message['@type'] ?? this.message['type']) as string const { protocolName, protocolMajorVersion, protocolMinorVersion, messageName } = parseMessageType(messageType) const thread = this.message['~thread'] - let threadId = messageId + let threadId = (this.message['thid'] ?? messageId) as string if (isJsonObject(thread) && typeof thread.thid === 'string') { threadId = thread.thid @@ -89,7 +89,7 @@ export class DidCommMessageRecord extends BaseRecord public getMessageInstance( messageClass: MessageClass ): InstanceType { - const messageType = parseMessageType(this.message['@type'] as string) + const messageType = parseMessageType((this.message['@type'] ?? this.message['type']) as string) if (!canHandleMessageType(messageClass, messageType)) { throw new AriesFrameworkError('Provided message class type does not match type of stored message') From 30eaf5b47548e9fcf72d7416d4e6b1b767550e4a Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Tue, 22 Aug 2023 20:16:42 -0300 Subject: [PATCH 13/22] fix: basic message types Signed-off-by: Ariel Gentile --- .../protocols/v2/__tests__/V2BasicMessageProtocol.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/core/src/modules/basic-messages/protocols/v2/__tests__/V2BasicMessageProtocol.test.ts b/packages/core/src/modules/basic-messages/protocols/v2/__tests__/V2BasicMessageProtocol.test.ts index 6118ac1ced..6aa93a20bb 100644 --- a/packages/core/src/modules/basic-messages/protocols/v2/__tests__/V2BasicMessageProtocol.test.ts +++ b/packages/core/src/modules/basic-messages/protocols/v2/__tests__/V2BasicMessageProtocol.test.ts @@ -63,8 +63,6 @@ describe('V2BasicMessageProtocol', () => { it(`stores record and emits message and basic message record`, async () => { const basicMessage = new V2BasicMessage({ id: '123', - from: 'did:from', - to: 'did:to', content: 'message', }) From 821ea021e6798e6b142442404326430e4ed21e29 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Tue, 22 Aug 2023 19:16:10 -0300 Subject: [PATCH 14/22] feat: set from/to in getOutboundContext Signed-off-by: Ariel Gentile --- .../modules/basic-messages/BasicMessagesApi.ts | 10 +++++----- .../protocols/v2/V2BasicMessageProtocol.ts | 10 +--------- .../protocols/v2/messages/V2BasicMessage.ts | 15 +-------------- 3 files changed, 7 insertions(+), 28 deletions(-) diff --git a/packages/core/src/modules/basic-messages/BasicMessagesApi.ts b/packages/core/src/modules/basic-messages/BasicMessagesApi.ts index d50da6da62..df1328b1f3 100644 --- a/packages/core/src/modules/basic-messages/BasicMessagesApi.ts +++ b/packages/core/src/modules/basic-messages/BasicMessagesApi.ts @@ -1,10 +1,10 @@ -import type { BasicMessageProtocol, V2BasicMessage } from './protocols' +import type { BasicMessageProtocol } from './protocols' import type { BasicMessageRecord } from './repository/BasicMessageRecord' import type { Query } from '../../storage/StorageService' import { AgentContext } from '../../agent' import { MessageSender } from '../../agent/MessageSender' -import { OutboundMessageContext } from '../../agent/models' +import { getOutboundMessageContext } from '../../agent/getOutboundMessageContext' import { AriesFrameworkError } from '../../error' import { injectable } from '../../plugins' import { ConnectionService } from '../connections' @@ -79,9 +79,9 @@ export class BasicMessagesApi implements Ba parentThreadId, } ) - const outboundMessageContext = new OutboundMessageContext(basicMessage as V2BasicMessage, { - agentContext: this.agentContext, - connection, + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message: basicMessage, + connectionRecord: connection, associatedRecord: basicMessageRecord, }) diff --git a/packages/core/src/modules/basic-messages/protocols/v2/V2BasicMessageProtocol.ts b/packages/core/src/modules/basic-messages/protocols/v2/V2BasicMessageProtocol.ts index 074a93dafe..d101227e63 100644 --- a/packages/core/src/modules/basic-messages/protocols/v2/V2BasicMessageProtocol.ts +++ b/packages/core/src/modules/basic-messages/protocols/v2/V2BasicMessageProtocol.ts @@ -43,15 +43,7 @@ export class V2BasicMessageProtocol extends BaseBasicMessageProtocol { public async createMessage(agentContext: AgentContext, options: CreateMessageOptions) { const { content, parentThreadId, connectionRecord } = options - if (!connectionRecord.did || !connectionRecord.theirDid) { - throw new AriesFrameworkError('Connection Record must have both our and their did') - } - - const basicMessage = new V2BasicMessage({ - from: connectionRecord.did, - to: connectionRecord.theirDid, - content, - }) + const basicMessage = new V2BasicMessage({ content }) const basicMessageRepository = agentContext.dependencyManager.resolve(BasicMessageRepository) diff --git a/packages/core/src/modules/basic-messages/protocols/v2/messages/V2BasicMessage.ts b/packages/core/src/modules/basic-messages/protocols/v2/messages/V2BasicMessage.ts index a8e57f0439..7429e3dee0 100644 --- a/packages/core/src/modules/basic-messages/protocols/v2/messages/V2BasicMessage.ts +++ b/packages/core/src/modules/basic-messages/protocols/v2/messages/V2BasicMessage.ts @@ -20,10 +20,6 @@ export class V2BasicMessage extends DidCommV2Message { @IsNotEmpty() public createdTime!: number - @IsString() - @IsNotEmpty() - public from!: string - @IsObject() @ValidateNested() @Type(() => V2BasicMessageBody) @@ -34,21 +30,12 @@ export class V2BasicMessage extends DidCommV2Message { * sentTime will be assigned to new Date if not passed, id will be assigned to uuid/v4 if not passed * @param options */ - public constructor(options: { - from: string - to: string - content: string - sentTime?: Date - id?: string - locale?: string - }) { + public constructor(options: { content: string; sentTime?: Date; id?: string; locale?: string }) { super() if (options) { this.id = options.id || this.generateId() this.createdTime = options.sentTime?.getTime() || new Date().getTime() - this.from = options.from - this.to = [options.to] this.body = new V2BasicMessageBody({ content: options.content }) this.language = options.locale || 'en' } From 318e5a078962aa3a48ce285b12b2b32c3aa23bfb Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Mon, 28 Aug 2023 12:29:34 -0300 Subject: [PATCH 15/22] fix: revert changes in BasicMessageStateChanged event Signed-off-by: Ariel Gentile --- demo/src/Listener.ts | 19 ++++--- .../basic-messages/BasicMessageEvents.ts | 4 +- .../protocols/v2/V2BasicMessageProtocol.ts | 20 +------- .../__tests__/V2BasicMessageProtocol.test.ts | 4 +- packages/core/tests/helpers.ts | 51 +++++++++++++------ 5 files changed, 54 insertions(+), 44 deletions(-) diff --git a/demo/src/Listener.ts b/demo/src/Listener.ts index 4a4c7e383a..3b98345569 100644 --- a/demo/src/Listener.ts +++ b/demo/src/Listener.ts @@ -4,7 +4,6 @@ import type { Faber } from './Faber' import type { FaberInquirer } from './FaberInquirer' import type { Agent, - BasicMessageStateChangedEvent, CredentialExchangeRecord, CredentialStateChangedEvent, TrustPingReceivedEvent, @@ -13,12 +12,15 @@ import type { V2TrustPingResponseReceivedEvent, ProofExchangeRecord, ProofStateChangedEvent, + AgentMessageProcessedEvent, + AgentBaseMessage, } from '@aries-framework/core' import type BottomBar from 'inquirer/lib/ui/bottom-bar' import { - BasicMessageEventTypes, - BasicMessageRole, + V1BasicMessage, + V2BasicMessage, + AgentEventTypes, CredentialEventTypes, CredentialState, ProofEventTypes, @@ -76,9 +78,14 @@ export class Listener { } public messageListener(agent: Agent, name: string) { - agent.events.on(BasicMessageEventTypes.BasicMessageStateChanged, async (event: BasicMessageStateChangedEvent) => { - if (event.payload.basicMessageRecord.role === BasicMessageRole.Receiver) { - this.ui.updateBottomBar(purpleText(`\n${name} received a message: ${event.payload.message.content}\n`)) + const isBasicMessage = (message: AgentBaseMessage): message is V1BasicMessage | V2BasicMessage => + [V1BasicMessage.type.messageTypeUri, V2BasicMessage.type.messageTypeUri].includes(message.type) + + agent.events.on(AgentEventTypes.AgentMessageProcessed, async (event: AgentMessageProcessedEvent) => { + const message = event.payload.message + + if (isBasicMessage(message)) { + this.ui.updateBottomBar(purpleText(`\n${name} received a message: ${message.content}\n`)) } }) } diff --git a/packages/core/src/modules/basic-messages/BasicMessageEvents.ts b/packages/core/src/modules/basic-messages/BasicMessageEvents.ts index 745decb916..c02bf4832e 100644 --- a/packages/core/src/modules/basic-messages/BasicMessageEvents.ts +++ b/packages/core/src/modules/basic-messages/BasicMessageEvents.ts @@ -1,4 +1,4 @@ -import type { V1BasicMessage, V2BasicMessage } from './protocols' +import type { V1BasicMessage } from './protocols' import type { BasicMessageRecord } from './repository' import type { BaseEvent } from '../../agent/Events' @@ -8,7 +8,7 @@ export enum BasicMessageEventTypes { export interface BasicMessageStateChangedEvent extends BaseEvent { type: typeof BasicMessageEventTypes.BasicMessageStateChanged payload: { - message: V1BasicMessage | V2BasicMessage + message: V1BasicMessage basicMessageRecord: BasicMessageRecord } } diff --git a/packages/core/src/modules/basic-messages/protocols/v2/V2BasicMessageProtocol.ts b/packages/core/src/modules/basic-messages/protocols/v2/V2BasicMessageProtocol.ts index d101227e63..a2dcc38855 100644 --- a/packages/core/src/modules/basic-messages/protocols/v2/V2BasicMessageProtocol.ts +++ b/packages/core/src/modules/basic-messages/protocols/v2/V2BasicMessageProtocol.ts @@ -3,14 +3,10 @@ import type { FeatureRegistry } from '../../../../agent/FeatureRegistry' import type { InboundMessageContext } from '../../../../agent/models/InboundMessageContext' import type { DependencyManager } from '../../../../plugins' import type { ConnectionRecord } from '../../../connections/repository/ConnectionRecord' -import type { BasicMessageStateChangedEvent } from '../../BasicMessageEvents' import type { CreateMessageOptions } from '../BasicMessageProtocolOptions' -import { EventEmitter } from '../../../../agent/EventEmitter' import { Protocol } from '../../../../agent/models' -import { AriesFrameworkError } from '../../../../error' import { injectable } from '../../../../plugins' -import { BasicMessageEventTypes } from '../../BasicMessageEvents' import { BasicMessageRole } from '../../BasicMessageRole' import { BasicMessageRecord, BasicMessageRepository } from '../../repository' import { BaseBasicMessageProtocol } from '../BaseBasicMessageProtocol' @@ -62,7 +58,7 @@ export class V2BasicMessageProtocol extends BaseBasicMessageProtocol { }) await basicMessageRepository.save(agentContext, basicMessageRecord) - this.emitStateChangedEvent(agentContext, basicMessageRecord, basicMessage) + // TODO: Emit BasicStateChangedEvent when it is updated to accept V2 Basic Messages return { message: basicMessage, record: basicMessageRecord } } @@ -83,18 +79,6 @@ export class V2BasicMessageProtocol extends BaseBasicMessageProtocol { }) await basicMessageRepository.save(agentContext, basicMessageRecord) - this.emitStateChangedEvent(agentContext, basicMessageRecord, message) - } - - protected emitStateChangedEvent( - agentContext: AgentContext, - basicMessageRecord: BasicMessageRecord, - basicMessage: V2BasicMessage - ) { - const eventEmitter = agentContext.dependencyManager.resolve(EventEmitter) - eventEmitter.emit(agentContext, { - type: BasicMessageEventTypes.BasicMessageStateChanged, - payload: { message: basicMessage, basicMessageRecord: basicMessageRecord.clone() }, - }) + // TODO: Emit BasicStateChangedEvent when it is updated to accept V2 Basic Messages } } diff --git a/packages/core/src/modules/basic-messages/protocols/v2/__tests__/V2BasicMessageProtocol.test.ts b/packages/core/src/modules/basic-messages/protocols/v2/__tests__/V2BasicMessageProtocol.test.ts index 6aa93a20bb..2420a61884 100644 --- a/packages/core/src/modules/basic-messages/protocols/v2/__tests__/V2BasicMessageProtocol.test.ts +++ b/packages/core/src/modules/basic-messages/protocols/v2/__tests__/V2BasicMessageProtocol.test.ts @@ -43,7 +43,7 @@ describe('V2BasicMessageProtocol', () => { expect(message.body.content).toBe('hello') expect(basicMessageRepository.save).toHaveBeenCalledWith(agentContext, expect.any(BasicMessageRecord)) - expect(eventEmitter.emit).toHaveBeenCalledWith(agentContext, { + expect(eventEmitter.emit).not.toHaveBeenCalledWith(agentContext, { type: 'BasicMessageStateChanged', payload: { basicMessageRecord: expect.objectContaining({ @@ -71,7 +71,7 @@ describe('V2BasicMessageProtocol', () => { await basicMessageProtocol.save(messageContext, mockConnectionRecord) expect(basicMessageRepository.save).toHaveBeenCalledWith(agentContext, expect.any(BasicMessageRecord)) - expect(eventEmitter.emit).toHaveBeenCalledWith(agentContext, { + expect(eventEmitter.emit).not.toHaveBeenCalledWith(agentContext, { type: 'BasicMessageStateChanged', payload: { basicMessageRecord: expect.objectContaining({ diff --git a/packages/core/tests/helpers.ts b/packages/core/tests/helpers.ts index a09c6a8f8c..a907165bd9 100644 --- a/packages/core/tests/helpers.ts +++ b/packages/core/tests/helpers.ts @@ -2,7 +2,6 @@ import type { AgentDependencies, BaseEvent, - BasicMessageStateChangedEvent, ConnectionRecordProps, CredentialStateChangedEvent, InitConfig, @@ -13,8 +12,7 @@ import type { CredentialState, ConnectionStateChangedEvent, Buffer, - V1BasicMessage, - V2BasicMessage, + AgentMessageProcessedEvent, } from '../src' import type { AgentModulesInput, EmptyModuleMap } from '../src/agent/AgentModules' import type { @@ -32,6 +30,8 @@ import { catchError, filter, map, take, timeout } from 'rxjs/operators' import { agentDependencies, IndySdkPostgresWalletScheme } from '../../node/src' import { + V1BasicMessage, + V2BasicMessage, OutOfBandVersion, OutOfBandDidCommService, ConnectionsModule, @@ -39,7 +39,6 @@ import { TypedArrayEncoder, AgentConfig, AgentContext, - BasicMessageEventTypes, ConnectionRecord, CredentialEventTypes, DependencyManager, @@ -49,6 +48,7 @@ import { InjectionSymbols, ProofEventTypes, TrustPingEventTypes, + AgentEventTypes, } from '../src' import { Key, KeyType } from '../src/crypto' import { DidKey } from '../src/modules/dids/methods/key' @@ -222,6 +222,8 @@ const isTrustPingReceivedEvent = (e: BaseEvent): e is TrustPingReceivedEvent => e.type === TrustPingEventTypes.TrustPingReceivedEvent || e.type === TrustPingEventTypes.V2TrustPingReceivedEvent const isTrustPingResponseReceivedEvent = (e: BaseEvent): e is TrustPingResponseReceivedEvent => e.type === TrustPingEventTypes.TrustPingResponseReceivedEvent +const isAgentMessageProcessedEvent = (e: BaseEvent): e is AgentMessageProcessedEvent => + e.type === AgentEventTypes.AgentMessageProcessed export function waitForProofExchangeRecordSubject( subject: ReplaySubject | Observable, @@ -451,21 +453,38 @@ export async function waitForConnectionRecord( export async function waitForBasicMessage( agent: Agent, - { content }: { content?: string } + { content, timeoutMs }: { content?: string; timeoutMs?: number } ): Promise { - return new Promise((resolve) => { - const listener = (event: BasicMessageStateChangedEvent) => { - const contentMatches = content === undefined || event.payload.message.content === content - - if (contentMatches) { - agent.events.off(BasicMessageEventTypes.BasicMessageStateChanged, listener) + const observable = agent.events.observable(AgentEventTypes.AgentMessageProcessed) + return waitForBasicMessageSubject(observable, { content, timeoutMs }) +} - resolve(event.payload.message) - } - } +export function waitForBasicMessageSubject( + subject: ReplaySubject | Observable, + { + content, + timeoutMs = 5000, + }: { + content?: string + timeoutMs?: number + } +) { + const observable = subject instanceof ReplaySubject ? subject.asObservable() : subject - agent.events.on(BasicMessageEventTypes.BasicMessageStateChanged, listener) - }) + return firstValueFrom( + observable.pipe( + filter(isAgentMessageProcessedEvent), + map((e) => e.payload.message), + filter((e): e is V1BasicMessage | V2BasicMessage => + [V1BasicMessage.type.messageTypeUri, V2BasicMessage.type.messageTypeUri].includes(e.type) + ), + filter((e) => content === undefined || e.content === content), + timeout(timeoutMs), + catchError(() => { + throw new Error(`Basic Message not received within specified timeout: { content: ${content} }`) + }) + ) + ) } export function getMockConnection({ From 2bcaf3faa15c27a2bde5f2001d300a882e0ef754 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Mon, 28 Aug 2023 12:36:55 -0300 Subject: [PATCH 16/22] fix: remove unneeded casting Signed-off-by: Ariel Gentile --- .../protocols/credentials/v1/V1CredentialProtocol.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/anoncreds/src/protocols/credentials/v1/V1CredentialProtocol.ts b/packages/anoncreds/src/protocols/credentials/v1/V1CredentialProtocol.ts index b3ce5063ca..cc45da3cb5 100644 --- a/packages/anoncreds/src/protocols/credentials/v1/V1CredentialProtocol.ts +++ b/packages/anoncreds/src/protocols/credentials/v1/V1CredentialProtocol.ts @@ -320,7 +320,7 @@ export class V1CredentialProtocol const message = new V1OfferCredentialMessage({ comment, - offerAttachments: [attachment as Attachment], + offerAttachments: [attachment], credentialPreview: new V1CredentialPreview({ attributes: previewAttributes, }), @@ -378,7 +378,7 @@ export class V1CredentialProtocol const message = new V1OfferCredentialMessage({ comment, - offerAttachments: [attachment as Attachment], + offerAttachments: [attachment], credentialPreview: new V1CredentialPreview({ attributes: previewAttributes, }), @@ -455,7 +455,7 @@ export class V1CredentialProtocol attributes: previewAttributes, }), comment, - offerAttachments: [attachment as Attachment], + offerAttachments: [attachment], attachments: credentialFormats.indy.linkedAttachments?.map((linkedAttachments) => linkedAttachments.attachment), }) @@ -609,7 +609,7 @@ export class V1CredentialProtocol const requestMessage = new V1RequestCredentialMessage({ comment, - requestAttachments: [attachment as Attachment], + requestAttachments: [attachment], attachments: offerMessage.appendedAttachments?.filter((attachment) => isLinkedAttachment(attachment)), }) requestMessage.setThread({ threadId: credentialRecord.threadId }) @@ -829,7 +829,7 @@ export class V1CredentialProtocol const issueMessage = new V1IssueCredentialMessage({ comment, - credentialAttachments: [attachment as Attachment], + credentialAttachments: [attachment], attachments: credentialRecord.linkedAttachments, }) From 8cb618be3fdff4b4edc305b3037d546d51a5b124 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Mon, 28 Aug 2023 12:43:23 -0300 Subject: [PATCH 17/22] fix: remove unneeded cast Signed-off-by: Ariel Gentile --- .../protocol/v2/CredentialFormatCoordinator.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/core/src/modules/credentials/protocol/v2/CredentialFormatCoordinator.ts b/packages/core/src/modules/credentials/protocol/v2/CredentialFormatCoordinator.ts index 4ee23b1657..25d106250c 100644 --- a/packages/core/src/modules/credentials/protocol/v2/CredentialFormatCoordinator.ts +++ b/packages/core/src/modules/credentials/protocol/v2/CredentialFormatCoordinator.ts @@ -56,7 +56,7 @@ export class CredentialFormatCoordinator }) } - proposalAttachments.push(attachment as Attachment) + proposalAttachments.push(attachment) formats.push(format) } @@ -161,7 +161,7 @@ export class CredentialFormatCoordinator }) } - offerAttachments.push(attachment as Attachment) + offerAttachments.push(attachment) formats.push(format) } @@ -233,7 +233,7 @@ export class CredentialFormatCoordinator }) } - offerAttachments.push(attachment as Attachment) + offerAttachments.push(attachment) formats.push(format) } @@ -333,7 +333,7 @@ export class CredentialFormatCoordinator credentialFormats, }) - requestAttachments.push(attachment as Attachment) + requestAttachments.push(attachment) formats.push(format) } @@ -389,7 +389,7 @@ export class CredentialFormatCoordinator credentialRecord, }) - requestAttachments.push(attachment as Attachment) + requestAttachments.push(attachment) formats.push(format) } @@ -488,7 +488,7 @@ export class CredentialFormatCoordinator credentialFormats, }) - credentialAttachments.push(attachment as Attachment) + credentialAttachments.push(attachment) formats.push(format) } From da9a00e1d6088543a0b0eec3d6308a1798af2c17 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Mon, 28 Aug 2023 20:18:39 -0300 Subject: [PATCH 18/22] feat: initial Present Proof V3 implementation Signed-off-by: Ariel Gentile --- demo/src/BaseAgent.ts | 4 + demo/src/Faber.ts | 2 +- .../core/src/agent/__tests__/Agent.test.ts | 4 +- .../decorators/attachment/v2/V2Attachment.ts | 2 + .../v3/messages/V3CredentialAckMessage.ts | 2 +- packages/core/src/modules/proofs/ProofsApi.ts | 9 +- .../core/src/modules/proofs/ProofsModule.ts | 9 +- .../src/modules/proofs/ProofsModuleConfig.ts | 4 +- .../proofs/protocol/BaseProofProtocol.ts | 42 +- .../modules/proofs/protocol/ProofProtocol.ts | 38 +- .../proofs/protocol/ProofProtocolOptions.ts | 4 +- .../core/src/modules/proofs/protocol/index.ts | 1 + .../protocol/v3/ProofFormatCoordinator.ts | 470 +++++++++ .../proofs/protocol/v3/V3ProofProtocol.ts | 972 ++++++++++++++++++ .../V3PresentationProblemReportError.ts | 25 + .../proofs/protocol/v3/errors/index.ts | 1 + .../v3/handlers/V3PresentationAckHandler.ts | 17 + .../v3/handlers/V3PresentationHandler.ts | 54 + .../V3PresentationProblemReportHandler.ts | 17 + .../handlers/V3ProposePresentationHandler.ts | 47 + .../handlers/V3RequestPresentationHandler.ts | 48 + .../src/modules/proofs/protocol/v3/index.ts | 3 + .../v3/messages/V3PresentationAckMessage.ts | 25 + .../v3/messages/V3PresentationMessage.ts | 60 ++ .../V3PresentationProblemReportMessage.ts | 23 + .../messages/V3ProposePresentationMessage.ts | 60 ++ .../messages/V3RequestPresentationMessage.ts | 66 ++ .../proofs/protocol/v3/messages/index.ts | 5 + 28 files changed, 1966 insertions(+), 48 deletions(-) create mode 100644 packages/core/src/modules/proofs/protocol/v3/ProofFormatCoordinator.ts create mode 100644 packages/core/src/modules/proofs/protocol/v3/V3ProofProtocol.ts create mode 100644 packages/core/src/modules/proofs/protocol/v3/errors/V3PresentationProblemReportError.ts create mode 100644 packages/core/src/modules/proofs/protocol/v3/errors/index.ts create mode 100644 packages/core/src/modules/proofs/protocol/v3/handlers/V3PresentationAckHandler.ts create mode 100644 packages/core/src/modules/proofs/protocol/v3/handlers/V3PresentationHandler.ts create mode 100644 packages/core/src/modules/proofs/protocol/v3/handlers/V3PresentationProblemReportHandler.ts create mode 100644 packages/core/src/modules/proofs/protocol/v3/handlers/V3ProposePresentationHandler.ts create mode 100644 packages/core/src/modules/proofs/protocol/v3/handlers/V3RequestPresentationHandler.ts create mode 100644 packages/core/src/modules/proofs/protocol/v3/index.ts create mode 100644 packages/core/src/modules/proofs/protocol/v3/messages/V3PresentationAckMessage.ts create mode 100644 packages/core/src/modules/proofs/protocol/v3/messages/V3PresentationMessage.ts create mode 100644 packages/core/src/modules/proofs/protocol/v3/messages/V3PresentationProblemReportMessage.ts create mode 100644 packages/core/src/modules/proofs/protocol/v3/messages/V3ProposePresentationMessage.ts create mode 100644 packages/core/src/modules/proofs/protocol/v3/messages/V3RequestPresentationMessage.ts create mode 100644 packages/core/src/modules/proofs/protocol/v3/messages/index.ts diff --git a/demo/src/BaseAgent.ts b/demo/src/BaseAgent.ts index f6f0bd638e..170ead2012 100644 --- a/demo/src/BaseAgent.ts +++ b/demo/src/BaseAgent.ts @@ -30,6 +30,7 @@ import { CredentialsModule, Agent, HttpOutboundTransport, + V3ProofProtocol, } from '@aries-framework/core' import { IndySdkAnonCredsRegistry, IndySdkModule, IndySdkSovDidResolver } from '@aries-framework/indy-sdk' import { IndyVdrIndyDidResolver, IndyVdrAnonCredsRegistry, IndyVdrModule } from '@aries-framework/indy-vdr' @@ -137,6 +138,9 @@ function getAskarAnonCredsIndyModules() { new V2ProofProtocol({ proofFormats: [legacyIndyProofFormatService, new AnonCredsProofFormatService()], }), + new V3ProofProtocol({ + proofFormats: [legacyIndyProofFormatService, new AnonCredsProofFormatService()], + }), ], }), anoncreds: new AnonCredsModule({ diff --git a/demo/src/Faber.ts b/demo/src/Faber.ts index caadc51576..813f553602 100644 --- a/demo/src/Faber.ts +++ b/demo/src/Faber.ts @@ -256,7 +256,7 @@ export class Faber extends BaseAgent { await this.printProofFlow(greenText('\nRequesting proof...\n', false)) await this.agent.proofs.requestProof({ - protocolVersion: 'v2', + protocolVersion: connectionRecord.isDidCommV1Connection ? 'v2' : 'v3', connectionId: connectionRecord.id, proofFormats: { anoncreds: { diff --git a/packages/core/src/agent/__tests__/Agent.test.ts b/packages/core/src/agent/__tests__/Agent.test.ts index dfbf4700bd..60c1344b31 100644 --- a/packages/core/src/agent/__tests__/Agent.test.ts +++ b/packages/core/src/agent/__tests__/Agent.test.ts @@ -244,7 +244,9 @@ describe('Agent', () => { 'https://didcomm.org/connections/1.0', 'https://didcomm.org/coordinate-mediation/1.0', 'https://didcomm.org/issue-credential/2.0', + 'https://didcomm.org/issue-credential/3.0', 'https://didcomm.org/present-proof/2.0', + 'https://didcomm.org/present-proof/3.0', 'https://didcomm.org/didexchange/1.0', 'https://didcomm.org/discover-features/1.0', 'https://didcomm.org/discover-features/2.0', @@ -255,6 +257,6 @@ describe('Agent', () => { 'https://didcomm.org/revocation_notification/2.0', ]) ) - expect(protocols.length).toEqual(14) + expect(protocols.length).toEqual(16) }) }) diff --git a/packages/core/src/decorators/attachment/v2/V2Attachment.ts b/packages/core/src/decorators/attachment/v2/V2Attachment.ts index 98dd398761..6b4e4aa89f 100644 --- a/packages/core/src/decorators/attachment/v2/V2Attachment.ts +++ b/packages/core/src/decorators/attachment/v2/V2Attachment.ts @@ -13,6 +13,7 @@ export interface V2AttachmentOptions { id?: string description?: string filename?: string + format?: string lastmodTime?: Date mediaType?: string byteCount?: number @@ -86,6 +87,7 @@ export class V2Attachment { this.description = options.description this.byteCount = options.byteCount this.filename = options.filename + this.format = options.format this.lastmodTime = options.lastmodTime this.mediaType = options.mediaType this.data = options.data diff --git a/packages/core/src/modules/credentials/protocol/v3/messages/V3CredentialAckMessage.ts b/packages/core/src/modules/credentials/protocol/v3/messages/V3CredentialAckMessage.ts index 459dea2928..755455c53c 100644 --- a/packages/core/src/modules/credentials/protocol/v3/messages/V3CredentialAckMessage.ts +++ b/packages/core/src/modules/credentials/protocol/v3/messages/V3CredentialAckMessage.ts @@ -8,7 +8,7 @@ export type V3CredentialAckMessageOptions = { export class V3CredentialAckMessage extends DidCommV2Message { /** - * Create new CredentialAckMessage instance. + * Create new V3CredentialAckMessage instance. * @param options */ public constructor(options: V3CredentialAckMessageOptions) { diff --git a/packages/core/src/modules/proofs/ProofsApi.ts b/packages/core/src/modules/proofs/ProofsApi.ts index 69ff9af772..02bfd0d1e5 100644 --- a/packages/core/src/modules/proofs/ProofsApi.ts +++ b/packages/core/src/modules/proofs/ProofsApi.ts @@ -30,6 +30,7 @@ import { injectable } from 'tsyringe' import { MessageSender } from '../../agent/MessageSender' import { AgentContext } from '../../agent/context/AgentContext' import { getOutboundMessageContext } from '../../agent/getOutboundMessageContext' +import { isDidCommV1Message } from '../../didcomm' import { AriesFrameworkError } from '../../error' import { ConnectionService } from '../connections/services/ConnectionService' @@ -385,7 +386,7 @@ export class ProofsApi implements ProofsApi { }> { const protocol = this.getProtocol(options.protocolVersion) - return await protocol.createRequest(this.agentContext, { + const { message, proofRecord } = await protocol.createRequest(this.agentContext, { proofFormats: options.proofFormats, autoAcceptProof: options.autoAcceptProof, comment: options.comment, @@ -393,6 +394,12 @@ export class ProofsApi implements ProofsApi { goalCode: options.goalCode, willConfirm: options.willConfirm, }) + + if (!isDidCommV1Message(message)) { + throw new AriesFrameworkError('out-of-band credential offer is only supported for DIDComm V1') + } + + return { message, proofRecord } } /** diff --git a/packages/core/src/modules/proofs/ProofsModule.ts b/packages/core/src/modules/proofs/ProofsModule.ts index b154e6bef3..84e9343338 100644 --- a/packages/core/src/modules/proofs/ProofsModule.ts +++ b/packages/core/src/modules/proofs/ProofsModule.ts @@ -7,13 +7,13 @@ import type { Constructor } from '../../utils/mixins' import { ProofsApi } from './ProofsApi' import { ProofsModuleConfig } from './ProofsModuleConfig' -import { V2ProofProtocol } from './protocol' +import { V2ProofProtocol, V3ProofProtocol } from './protocol' import { ProofRepository } from './repository' /** * Default proofProtocols that will be registered if the `proofProtocols` property is not configured. */ -export type DefaultProofProtocols = [V2ProofProtocol<[]>] +export type DefaultProofProtocols = [V2ProofProtocol<[]>, V3ProofProtocol<[]>] // ProofsModuleOptions makes the proofProtocols property optional from the config, as it will set it when not provided. export type ProofsModuleOptions = Optional< @@ -31,7 +31,10 @@ export class ProofsModule } diff --git a/packages/core/src/modules/proofs/ProofsModuleConfig.ts b/packages/core/src/modules/proofs/ProofsModuleConfig.ts index e87966ef27..8a412b8935 100644 --- a/packages/core/src/modules/proofs/ProofsModuleConfig.ts +++ b/packages/core/src/modules/proofs/ProofsModuleConfig.ts @@ -18,11 +18,11 @@ export interface ProofsModuleConfigOptions - ): Promise> - public abstract processProposal(messageContext: InboundMessageContext): Promise + ): Promise> + public abstract processProposal(messageContext: InboundMessageContext): Promise public abstract acceptProposal( agentContext: AgentContext, options: AcceptProofProposalOptions - ): Promise> + ): Promise> public abstract negotiateProposal( agentContext: AgentContext, options: NegotiateProofProposalOptions - ): Promise> + ): Promise> // methods for request public abstract createRequest( agentContext: AgentContext, options: CreateProofRequestOptions - ): Promise> - public abstract processRequest(messageContext: InboundMessageContext): Promise + ): Promise> + public abstract processRequest(messageContext: InboundMessageContext): Promise public abstract acceptRequest( agentContext: AgentContext, options: AcceptProofRequestOptions - ): Promise> + ): Promise> public abstract negotiateRequest( agentContext: AgentContext, options: NegotiateProofRequestOptions - ): Promise> + ): Promise> // retrieving credentials for request public abstract getCredentialsForRequest( @@ -82,40 +83,40 @@ export abstract class BaseProofProtocol + messageContext: InboundMessageContext ): Promise public abstract acceptPresentation( agentContext: AgentContext, options: AcceptPresentationOptions - ): Promise> + ): Promise> // methods for ack - public abstract processAck(messageContext: InboundMessageContext): Promise + public abstract processAck(messageContext: InboundMessageContext): Promise // method for problem report public abstract createProblemReport( agentContext: AgentContext, options: CreateProofProblemReportOptions - ): Promise> + ): Promise> public abstract findProposalMessage( agentContext: AgentContext, proofExchangeId: string - ): Promise + ): Promise public abstract findRequestMessage( agentContext: AgentContext, proofExchangeId: string - ): Promise + ): Promise public abstract findPresentationMessage( agentContext: AgentContext, proofExchangeId: string - ): Promise + ): Promise public abstract getFormatData( agentContext: AgentContext, proofExchangeId: string ): Promise>> public async processProblemReport( - messageContext: InboundMessageContext + messageContext: InboundMessageContext ): Promise { const { message: proofProblemReportMessage, agentContext, connection } = messageContext @@ -128,7 +129,10 @@ export abstract class BaseProofProtocol - ): Promise> - processProposal(messageContext: InboundMessageContext): Promise + ): Promise> + processProposal(messageContext: InboundMessageContext): Promise acceptProposal( agentContext: AgentContext, options: AcceptProofProposalOptions - ): Promise> + ): Promise> negotiateProposal( agentContext: AgentContext, options: NegotiateProofProposalOptions - ): Promise> + ): Promise> // methods for request createRequest( agentContext: AgentContext, options: CreateProofRequestOptions - ): Promise> - processRequest(messageContext: InboundMessageContext): Promise + ): Promise> + processRequest(messageContext: InboundMessageContext): Promise acceptRequest( agentContext: AgentContext, options: AcceptProofRequestOptions - ): Promise> + ): Promise> negotiateRequest( agentContext: AgentContext, options: NegotiateProofRequestOptions - ): Promise> + ): Promise> // retrieving credentials for request getCredentialsForRequest( @@ -70,25 +70,27 @@ export interface ProofProtocol> // methods for presentation - processPresentation(messageContext: InboundMessageContext): Promise + processPresentation(messageContext: InboundMessageContext): Promise acceptPresentation( agentContext: AgentContext, options: AcceptPresentationOptions - ): Promise> + ): Promise> // methods for ack - processAck(messageContext: InboundMessageContext): Promise + processAck(messageContext: InboundMessageContext): Promise // method for problem report createProblemReport( agentContext: AgentContext, options: CreateProofProblemReportOptions - ): Promise> - processProblemReport(messageContext: InboundMessageContext): Promise + ): Promise> + processProblemReport( + messageContext: InboundMessageContext + ): Promise - findProposalMessage(agentContext: AgentContext, proofExchangeId: string): Promise - findRequestMessage(agentContext: AgentContext, proofExchangeId: string): Promise - findPresentationMessage(agentContext: AgentContext, proofExchangeId: string): Promise + findProposalMessage(agentContext: AgentContext, proofExchangeId: string): Promise + findRequestMessage(agentContext: AgentContext, proofExchangeId: string): Promise + findPresentationMessage(agentContext: AgentContext, proofExchangeId: string): Promise getFormatData( agentContext: AgentContext, proofExchangeId: string diff --git a/packages/core/src/modules/proofs/protocol/ProofProtocolOptions.ts b/packages/core/src/modules/proofs/protocol/ProofProtocolOptions.ts index e84881f32a..a87ddd55cb 100644 --- a/packages/core/src/modules/proofs/protocol/ProofProtocolOptions.ts +++ b/packages/core/src/modules/proofs/protocol/ProofProtocolOptions.ts @@ -1,5 +1,5 @@ import type { ProofProtocol } from './ProofProtocol' -import type { DidCommV1Message } from '../../../didcomm' +import type { AgentBaseMessage } from '../../../agent/AgentBaseMessage' import type { ConnectionRecord } from '../../connections' import type { ExtractProofFormats, @@ -155,7 +155,7 @@ export interface CreateProofProblemReportOptions { description: string } -export interface ProofProtocolMsgReturnType { +export interface ProofProtocolMsgReturnType { message: MessageType proofRecord: ProofExchangeRecord } diff --git a/packages/core/src/modules/proofs/protocol/index.ts b/packages/core/src/modules/proofs/protocol/index.ts index 71799a5c45..9c2242ad5d 100644 --- a/packages/core/src/modules/proofs/protocol/index.ts +++ b/packages/core/src/modules/proofs/protocol/index.ts @@ -1,4 +1,5 @@ export * from './v2' +export * from './v3' import * as ProofProtocolOptions from './ProofProtocolOptions' export { ProofProtocol } from './ProofProtocol' diff --git a/packages/core/src/modules/proofs/protocol/v3/ProofFormatCoordinator.ts b/packages/core/src/modules/proofs/protocol/v3/ProofFormatCoordinator.ts new file mode 100644 index 0000000000..8cac6b1fdf --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v3/ProofFormatCoordinator.ts @@ -0,0 +1,470 @@ +import type { AgentContext } from '../../../../agent' +import type { V2Attachment } from '../../../../decorators/attachment' +import type { + ExtractProofFormats, + ProofFormatCredentialForRequestPayload, + ProofFormatPayload, + ProofFormatService, +} from '../../formats' +import type { ProofExchangeRecord } from '../../repository' + +import { toV1Attachment, toV2Attachment } from '../../../../didcomm' +import { AriesFrameworkError } from '../../../../error' +import { DidCommMessageRepository, DidCommMessageRole } from '../../../../storage' + +import { V3PresentationMessage, V3ProposePresentationMessage, V3RequestPresentationMessage } from './messages' + +export class ProofFormatCoordinator { + /** + * Create a {@link V3ProposePresentationMessage}. + * + * @param options + * @returns The created {@link V3ProposePresentationMessage} + * + */ + 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) + + const proposalAttachments: V2Attachment[] = [] + + for (const formatService of formatServices) { + const { format, attachment } = await formatService.createProposal(agentContext, { + proofFormats, + proofRecord, + }) + + const v2Attachment = toV2Attachment(attachment) + v2Attachment.format = format.format + proposalAttachments.push(v2Attachment) + } + + const message = new V3ProposePresentationMessage({ + id: proofRecord.threadId, + attachments: 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: V3ProposePresentationMessage + formatServices: ProofFormatService[] + } + ) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + for (const formatService of formatServices) { + const attachment = this.getAttachmentForService(formatService, message.attachments) + + 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, + willConfirm, + }: { + proofRecord: ProofExchangeRecord + proofFormats?: ProofFormatPayload, 'acceptProposal'> + formatServices: ProofFormatService[] + comment?: string + goalCode?: string + willConfirm?: boolean + } + ) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + const requestAttachments: V2Attachment[] = [] + + const proposalMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V3ProposePresentationMessage, + }) + + for (const formatService of formatServices) { + const proposalAttachment = this.getAttachmentForService(formatService, proposalMessage.attachments) + + const { attachment, format } = await formatService.acceptProposal(agentContext, { + proofRecord, + proofFormats, + proposalAttachment, + }) + const v2Attachment = toV2Attachment(attachment) + v2Attachment.format = format.format + requestAttachments.push(v2Attachment) + } + + const message = new V3RequestPresentationMessage({ + attachments: requestAttachments, + comment, + goalCode, + 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 V3RequestPresentationMessage}. + * + * @param options + * @returns The created {@link V3RequestPresentationMessage} + * + */ + public async createRequest( + agentContext: AgentContext, + { + proofFormats, + formatServices, + proofRecord, + comment, + goalCode, + willConfirm, + }: { + formatServices: ProofFormatService[] + proofFormats: ProofFormatPayload, 'createRequest'> + proofRecord: ProofExchangeRecord + comment?: string + goalCode?: string + willConfirm?: boolean + } + ): Promise { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + const requestAttachments: V2Attachment[] = [] + + for (const formatService of formatServices) { + const { format, attachment } = await formatService.createRequest(agentContext, { + proofFormats, + proofRecord, + }) + + const v2Attachment = toV2Attachment(attachment) + v2Attachment.format = format.format + requestAttachments.push(v2Attachment) + } + + const message = new V3RequestPresentationMessage({ + comment, + attachments: requestAttachments, + goalCode, + 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: V3RequestPresentationMessage + formatServices: ProofFormatService[] + } + ) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + for (const formatService of formatServices) { + const attachment = this.getAttachmentForService(formatService, message.attachments) + + 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, + goalCode, + }: { + proofRecord: ProofExchangeRecord + proofFormats?: ProofFormatPayload, 'acceptRequest'> + formatServices: ProofFormatService[] + comment?: string + goalCode?: string + } + ) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + const requestMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V3RequestPresentationMessage, + }) + + const proposalMessage = await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V3ProposePresentationMessage, + }) + + const presentationAttachments: V2Attachment[] = [] + + for (const formatService of formatServices) { + const requestAttachment = this.getAttachmentForService(formatService, requestMessage.attachments) + + const proposalAttachment = proposalMessage + ? this.getAttachmentForService(formatService, proposalMessage.attachments) + : undefined + + const { attachment, format } = await formatService.acceptRequest(agentContext, { + requestAttachment, + proposalAttachment, + proofRecord, + proofFormats, + }) + + const v2Attachment = toV2Attachment(attachment) + v2Attachment.format = format.format + presentationAttachments.push(v2Attachment) + } + + const message = new V3PresentationMessage({ + attachments: presentationAttachments, + comment, + goalCode, + }) + + message.setThread({ threadId: proofRecord.threadId }) + //TODO: 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: V3RequestPresentationMessage, + }) + + const proposalMessage = await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V3ProposePresentationMessage, + }) + + const credentialsForRequest: Record = {} + + for (const formatService of formatServices) { + const requestAttachment = this.getAttachmentForService(formatService, requestMessage.attachments) + + const proposalAttachment = proposalMessage + ? this.getAttachmentForService(formatService, proposalMessage.attachments) + : 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: V3RequestPresentationMessage, + }) + + const proposalMessage = await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V3ProposePresentationMessage, + }) + + const credentialsForRequest: Record = {} + + for (const formatService of formatServices) { + const requestAttachment = this.getAttachmentForService(formatService, requestMessage.attachments) + + const proposalAttachment = proposalMessage + ? this.getAttachmentForService(formatService, proposalMessage.attachments) + : 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: V3PresentationMessage + requestMessage: V3RequestPresentationMessage + formatServices: ProofFormatService[] + } + ) { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + const formatVerificationResults: boolean[] = [] + + for (const formatService of formatServices) { + const attachment = this.getAttachmentForService(formatService, message.attachments) + const requestAttachment = this.getAttachmentForService(formatService, requestMessage.attachments) + + 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(proofFormatService: ProofFormatService, attachments: V2Attachment[]) { + const attachment = attachments.find( + (attachment) => attachment.format && proofFormatService.supportsFormat(attachment.format) + ) + + if (!attachment) { + throw new AriesFrameworkError(`Attachment with format ${proofFormatService.formatKey} not found in attachments.`) + } + + return toV1Attachment(attachment) + } +} diff --git a/packages/core/src/modules/proofs/protocol/v3/V3ProofProtocol.ts b/packages/core/src/modules/proofs/protocol/v3/V3ProofProtocol.ts new file mode 100644 index 0000000000..4d2a6d711c --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v3/V3ProofProtocol.ts @@ -0,0 +1,972 @@ +import type { AgentContext } from '../../../../agent' +import type { FeatureRegistry } from '../../../../agent/FeatureRegistry' +import type { InboundMessageContext } from '../../../../agent/models/InboundMessageContext' +import type { V2Attachment } from '../../../../decorators/attachment' +import type { DidCommV2Message } from '../../../../didcomm' +import type { DependencyManager } from '../../../../plugins' +import type { V2ProblemReportMessage } from '../../../problem-reports' +import type { + ExtractProofFormats, + ProofFormat, + ProofFormatCredentialForRequestPayload, + ProofFormatPayload, +} from '../../formats' +import type { ProofFormatService } from '../../formats/ProofFormatService' +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 { 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 { V3PresentationAckHandler } from './handlers/V3PresentationAckHandler' +import { V3PresentationHandler } from './handlers/V3PresentationHandler' +import { V3PresentationProblemReportHandler } from './handlers/V3PresentationProblemReportHandler' +import { V3ProposePresentationHandler } from './handlers/V3ProposePresentationHandler' +import { V3RequestPresentationHandler } from './handlers/V3RequestPresentationHandler' +import { + V3ProposePresentationMessage, + V3PresentationProblemReportMessage, + V3PresentationMessage, + V3PresentationAckMessage, + V3RequestPresentationMessage, +} from './messages' + +export interface V3ProofProtocolConfig { + proofFormats: ProofFormatServices +} + +export class V3ProofProtocol + extends BaseProofProtocol + implements ProofProtocol +{ + private proofFormatCoordinator = new ProofFormatCoordinator() + private proofFormats: PFs + + public constructor({ proofFormats }: V3ProofProtocolConfig) { + super() + + this.proofFormats = proofFormats + } + + /** + * The version of the present proof protocol this service supports + */ + public readonly version = 'v3' as const + + public register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry) { + // Register message handlers for the Present Proof V3 Protocol + dependencyManager.registerMessageHandlers([ + new V3ProposePresentationHandler(this), + new V3RequestPresentationHandler(this), + new V3PresentationHandler(this), + new V3PresentationAckHandler(this), + new V3PresentationProblemReportHandler(this), + ]) + + // Register Present Proof V2 in feature registry, with supported roles + featureRegistry.register( + new Protocol({ + id: 'https://didcomm.org/present-proof/3.0', + roles: ['prover', 'verifier'], + }) + ) + } + + public async createProposal( + agentContext: AgentContext, + { + connectionRecord, + proofFormats, + comment, + autoAcceptProof, + goalCode, + parentThreadId, + }: CreateProofProposalOptions + ): Promise> { + 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: 'v3', + 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) + + let proofRecord = await this.findByThreadAndConnectionId( + messageContext.agentContext, + proposalMessage.threadId, + connection?.id + ) + + const formatServices = this.getFormatServicesFromAttachments(proposalMessage.attachments) + + if (formatServices.length === 0) { + throw new AriesFrameworkError(`Unable to process proposal. No supported formats`) + } + + // credential record already exists + if (proofRecord) { + // Assert + proofRecord.assertProtocolVersion('v3') + proofRecord.assertState(ProofState.RequestSent) + + await this.proofFormatCoordinator.processProposal(messageContext.agentContext, { + proofRecord, + formatServices, + message: proposalMessage, + }) + + await this.updateState(messageContext.agentContext, proofRecord, ProofState.ProposalReceived) + + return proofRecord + } else { + // No proof record exists with thread id + proofRecord = new ProofExchangeRecord({ + connectionId: connection?.id, + threadId: proposalMessage.threadId, + state: ProofState.ProposalReceived, + protocolVersion: 'v3', + parentThreadId: proposalMessage.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('v3') + 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: V3ProposePresentationMessage, + }) + + formatServices = this.getFormatServicesFromAttachments(proposalMessage.attachments) + } + + // 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, + }) + + 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('v3') + 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, + }) + + proofRecord.autoAcceptProof = autoAcceptProof ?? proofRecord.autoAcceptProof + await this.updateState(agentContext, proofRecord, ProofState.RequestSent) + + return { proofRecord, message: requestMessage } + } + + /** + * Create a {@link V3RequestPresentationMessage} 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: 'v3', + 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 V3RequestPresentationMessage}. 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) + + 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.getFormatServicesFromAttachments(requestMessage.attachments) + if (formatServices.length === 0) { + throw new AriesFrameworkError(`Unable to process request. No supported formats`) + } + + // proof record already exists + if (proofRecord) { + // Assert + proofRecord.assertProtocolVersion('v3') + proofRecord.assertState(ProofState.ProposalSent) + await this.proofFormatCoordinator.processRequest(messageContext.agentContext, { + proofRecord, + formatServices, + message: requestMessage, + }) + + await this.updateState(messageContext.agentContext, proofRecord, ProofState.RequestReceived) + return proofRecord + } else { + // 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: 'v3', + parentThreadId: requestMessage.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('v3') + 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: V3RequestPresentationMessage, + }) + + formatServices = this.getFormatServicesFromAttachments(requestMessage.attachments) + } + + // 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, + }) + + proofRecord.autoAcceptProof = autoAcceptProof ?? proofRecord.autoAcceptProof + await this.updateState(agentContext, proofRecord, ProofState.PresentationSent) + + return { proofRecord, message } + } + + /** + * Create a {@link V3ProposePresentationMessage} 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('v3') + 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('v3') + 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: V3RequestPresentationMessage, + }) + + formatServices = this.getFormatServicesFromAttachments(requestMessage.attachments) + } + + // 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('v3') + 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: V3RequestPresentationMessage, + }) + + formatServices = this.getFormatServicesFromAttachments(requestMessage.attachments) + } + + // 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) + + agentContext.config.logger.debug(`Processing presentation with id ${presentationMessage.id}`) + + const proofRecord = await this.getByThreadAndConnectionId( + messageContext.agentContext, + presentationMessage.threadId, + connection?.id + ) + + const requestMessage = await didCommMessageRepository.getAgentMessage(messageContext.agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V3RequestPresentationMessage, + }) + + // Assert + proofRecord.assertProtocolVersion('v3') + proofRecord.assertState(ProofState.RequestSent) + + const formatServices = this.getFormatServicesFromAttachments(presentationMessage.attachments) + 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, + 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('v3') + proofRecord.assertState(ProofState.PresentationReceived) + + const message = new V3PresentationAckMessage({ + threadId: proofRecord.threadId, + }) + + 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 proofRecord = await this.getByThreadAndConnectionId( + messageContext.agentContext, + ackMessage.threadId, + connection?.id + ) + proofRecord.connectionId = connection?.id + + // Assert + proofRecord.assertProtocolVersion('v3') + proofRecord.assertState(ProofState.PresentationSent) + + // Update record + await this.updateState(messageContext.agentContext, proofRecord, ProofState.Done) + + return proofRecord + } + + public async createProblemReport( + agentContext: AgentContext, + { description, proofRecord }: CreateProofProblemReportOptions + ): Promise> { + const message = new V3PresentationProblemReportMessage({ + parentThreadId: proofRecord.threadId, + body: { + comment: description, + code: PresentationProblemReportReason.Abandoned, + }, + }) + + message.setThread({ + threadId: proofRecord.threadId, + parentThreadId: proofRecord.parentThreadId, + }) + + return { + proofRecord, + message, + } + } + + public async shouldAutoRespondToProposal( + agentContext: AgentContext, + options: { + proofRecord: ProofExchangeRecord + proposalMessage: V3ProposePresentationMessage + } + ): 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.getFormatServicesFromAttachments(requestMessage.attachments) + + for (const formatService of formatServices) { + const requestAttachment = this.proofFormatCoordinator.getAttachmentForService( + formatService, + requestMessage.attachments + ) + + const proposalAttachment = this.proofFormatCoordinator.getAttachmentForService( + formatService, + proposalMessage.attachments + ) + + 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: V3RequestPresentationMessage + } + ): 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.getFormatServicesFromAttachments(proposalMessage.attachments) + + for (const formatService of formatServices) { + const proposalAttachment = this.proofFormatCoordinator.getAttachmentForService( + formatService, + proposalMessage.attachments + ) + + const requestAttachment = this.proofFormatCoordinator.getAttachmentForService( + formatService, + requestMessage.attachments + ) + + 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: V3PresentationMessage } + ): 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 proposalMessage = await this.findProposalMessage(agentContext, proofRecord.id) + + const requestMessage = await this.findRequestMessage(agentContext, proofRecord.id) + if (!requestMessage) return false + if (!requestMessage.body.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.getFormatServicesFromAttachments(requestMessage.attachments) + + for (const formatService of formatServices) { + const proposalAttachment = proposalMessage + ? this.proofFormatCoordinator.getAttachmentForService(formatService, proposalMessage.attachments) + : undefined + + const requestAttachment = this.proofFormatCoordinator.getAttachmentForService( + formatService, + requestMessage.attachments + ) + + const presentationAttachment = this.proofFormatCoordinator.getAttachmentForService( + formatService, + presentationMessage.attachments + ) + + 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: V3RequestPresentationMessage, + }) + } + + public async findPresentationMessage( + agentContext: AgentContext, + proofRecordId: string + ): Promise { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + return await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: proofRecordId, + messageClass: V3PresentationMessage, + }) + } + + public async findProposalMessage( + agentContext: AgentContext, + proofRecordId: string + ): Promise { + const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + + return await didCommMessageRepository.findAgentMessage(agentContext, { + associatedRecordId: proofRecordId, + messageClass: V3ProposePresentationMessage, + }) + } + + 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?.attachments, + request: requestMessage?.attachments, + presentation: presentationMessage?.attachments, + } as const + + const formatData: GetProofFormatDataReturn = {} + + // We loop through all of the message keys as defined above + for (const [messageKey, attachments] of Object.entries(messages)) { + // Message can be undefined, so we continue if it is not defined + if (!attachments) continue + + // Find all format services associated with the message + const formatServices = this.getFormatServicesFromAttachments(attachments) + + 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, 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 getFormatServicesFromAttachments(attachments: V2Attachment[]): ProofFormatService[] { + const formatServices = new Set() + + for (const attachment of attachments) { + const service = attachment.format ? this.getFormatServiceForFormat(attachment.format) : undefined + 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/v3/errors/V3PresentationProblemReportError.ts b/packages/core/src/modules/proofs/protocol/v3/errors/V3PresentationProblemReportError.ts new file mode 100644 index 0000000000..c8fba69429 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v3/errors/V3PresentationProblemReportError.ts @@ -0,0 +1,25 @@ +import type { ProblemReportErrorOptions } from '../../../../problem-reports' +import type { PresentationProblemReportReason } from '../../../errors/PresentationProblemReportReason' + +import { V2ProblemReportError } from '../../../../problem-reports/errors/V2ProblemReportError' +import { V3PresentationProblemReportMessage as V3PresentationProblemReportMessage } from '../messages' + +interface V3PresentationProblemReportErrorOptions extends ProblemReportErrorOptions { + problemCode: PresentationProblemReportReason + parentThreadId: string +} + +export class V3PresentationProblemReportError extends V2ProblemReportError { + public problemReport: V3PresentationProblemReportMessage + + public constructor(public message: string, { problemCode, parentThreadId }: V3PresentationProblemReportErrorOptions) { + super(message, { problemCode, parentThreadId }) + this.problemReport = new V3PresentationProblemReportMessage({ + parentThreadId, + body: { + code: problemCode, + comment: message, + }, + }) + } +} diff --git a/packages/core/src/modules/proofs/protocol/v3/errors/index.ts b/packages/core/src/modules/proofs/protocol/v3/errors/index.ts new file mode 100644 index 0000000000..0db271f358 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v3/errors/index.ts @@ -0,0 +1 @@ +export * from './V3PresentationProblemReportError' diff --git a/packages/core/src/modules/proofs/protocol/v3/handlers/V3PresentationAckHandler.ts b/packages/core/src/modules/proofs/protocol/v3/handlers/V3PresentationAckHandler.ts new file mode 100644 index 0000000000..b7e0476ee8 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v3/handlers/V3PresentationAckHandler.ts @@ -0,0 +1,17 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' +import type { V3ProofProtocol } from '../V3ProofProtocol' + +import { V3PresentationAckMessage } from '../messages' + +export class V3PresentationAckHandler implements MessageHandler { + private proofProtocol: V3ProofProtocol + public supportedMessages = [V3PresentationAckMessage] + + public constructor(proofProtocol: V3ProofProtocol) { + this.proofProtocol = proofProtocol + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + await this.proofProtocol.processAck(messageContext) + } +} diff --git a/packages/core/src/modules/proofs/protocol/v3/handlers/V3PresentationHandler.ts b/packages/core/src/modules/proofs/protocol/v3/handlers/V3PresentationHandler.ts new file mode 100644 index 0000000000..9b1ebe53f1 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v3/handlers/V3PresentationHandler.ts @@ -0,0 +1,54 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' +import type { ProofExchangeRecord } from '../../../repository' +import type { V3ProofProtocol } from '../V3ProofProtocol' + +import { getOutboundMessageContext } from '../../../../../agent/getOutboundMessageContext' +import { DidCommMessageRepository } from '../../../../../storage' +import { V3PresentationMessage, V3RequestPresentationMessage } from '../messages' + +export class V3PresentationHandler implements MessageHandler { + private proofProtocol: V3ProofProtocol + public supportedMessages = [V3PresentationMessage] + + public constructor(proofProtocol: V3ProofProtocol) { + this.proofProtocol = proofProtocol + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + const proofRecord = await this.proofProtocol.processPresentation(messageContext) + + const shouldAutoRespond = await this.proofProtocol.shouldAutoRespondToPresentation(messageContext.agentContext, { + proofRecord, + presentationMessage: messageContext.message, + }) + + if (shouldAutoRespond) { + return await this.acceptPresentation(proofRecord, messageContext) + } + } + + private async acceptPresentation( + proofRecord: ProofExchangeRecord, + messageContext: MessageHandlerInboundMessage + ) { + messageContext.agentContext.config.logger.info(`Automatically sending acknowledgement with autoAccept`) + + const { message } = await this.proofProtocol.acceptPresentation(messageContext.agentContext, { + proofRecord, + }) + + const didCommMessageRepository = messageContext.agentContext.dependencyManager.resolve(DidCommMessageRepository) + const requestMessage = await didCommMessageRepository.getAgentMessage(messageContext.agentContext, { + associatedRecordId: proofRecord.id, + messageClass: V3RequestPresentationMessage, + }) + + return getOutboundMessageContext(messageContext.agentContext, { + connectionRecord: messageContext.connection, + message, + associatedRecord: proofRecord, + lastReceivedMessage: messageContext.message, + lastSentMessage: requestMessage, + }) + } +} diff --git a/packages/core/src/modules/proofs/protocol/v3/handlers/V3PresentationProblemReportHandler.ts b/packages/core/src/modules/proofs/protocol/v3/handlers/V3PresentationProblemReportHandler.ts new file mode 100644 index 0000000000..4ea3835ac3 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v3/handlers/V3PresentationProblemReportHandler.ts @@ -0,0 +1,17 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' +import type { V3ProofProtocol } from '../V3ProofProtocol' + +import { V3PresentationProblemReportMessage } from '../messages' + +export class V3PresentationProblemReportHandler implements MessageHandler { + private proofService: V3ProofProtocol + public supportedMessages = [V3PresentationProblemReportMessage] + + public constructor(proofService: V3ProofProtocol) { + this.proofService = proofService + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + await this.proofService.processProblemReport(messageContext) + } +} diff --git a/packages/core/src/modules/proofs/protocol/v3/handlers/V3ProposePresentationHandler.ts b/packages/core/src/modules/proofs/protocol/v3/handlers/V3ProposePresentationHandler.ts new file mode 100644 index 0000000000..c71265a3ea --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v3/handlers/V3ProposePresentationHandler.ts @@ -0,0 +1,47 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' +import type { ProofExchangeRecord } from '../../../repository/ProofExchangeRecord' +import type { V3ProofProtocol } from '../V3ProofProtocol' + +import { OutboundMessageContext } from '../../../../../agent/models' +import { V3ProposePresentationMessage } from '../messages/V3ProposePresentationMessage' + +export class V3ProposePresentationHandler implements MessageHandler { + private proofProtocol: V3ProofProtocol + public supportedMessages = [V3ProposePresentationMessage] + + public constructor(proofProtocol: V3ProofProtocol) { + this.proofProtocol = proofProtocol + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + const proofRecord = await this.proofProtocol.processProposal(messageContext) + + const shouldAutoRespond = await this.proofProtocol.shouldAutoRespondToProposal(messageContext.agentContext, { + proofRecord, + proposalMessage: messageContext.message, + }) + + if (shouldAutoRespond) { + return this.acceptProposal(proofRecord, messageContext) + } + } + private async acceptProposal( + proofRecord: ProofExchangeRecord, + messageContext: MessageHandlerInboundMessage + ) { + messageContext.agentContext.config.logger.info(`Automatically sending request with autoAccept`) + + if (!messageContext.connection) { + messageContext.agentContext.config.logger.error('No connection on the messageContext, aborting auto accept') + return + } + + const { message } = await this.proofProtocol.acceptProposal(messageContext.agentContext, { proofRecord }) + + return new OutboundMessageContext(message, { + agentContext: messageContext.agentContext, + connection: messageContext.connection, + associatedRecord: proofRecord, + }) + } +} diff --git a/packages/core/src/modules/proofs/protocol/v3/handlers/V3RequestPresentationHandler.ts b/packages/core/src/modules/proofs/protocol/v3/handlers/V3RequestPresentationHandler.ts new file mode 100644 index 0000000000..59a2ce8640 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v3/handlers/V3RequestPresentationHandler.ts @@ -0,0 +1,48 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' +import type { ProofExchangeRecord } from '../../../repository/ProofExchangeRecord' +import type { V3ProofProtocol } from '../V3ProofProtocol' + +import { getOutboundMessageContext } from '../../../../../agent/getOutboundMessageContext' +import { V3RequestPresentationMessage } from '../messages/V3RequestPresentationMessage' + +export class V3RequestPresentationHandler implements MessageHandler { + private proofProtocol: V3ProofProtocol + public supportedMessages = [V3RequestPresentationMessage] + + public constructor(proofProtocol: V3ProofProtocol) { + this.proofProtocol = proofProtocol + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + const proofRecord = await this.proofProtocol.processRequest(messageContext) + + const shouldAutoRespond = await this.proofProtocol.shouldAutoRespondToRequest(messageContext.agentContext, { + proofRecord, + requestMessage: messageContext.message, + }) + + messageContext.agentContext.config.logger.debug(`Should auto respond to request: ${shouldAutoRespond}`) + + if (shouldAutoRespond) { + return await this.acceptRequest(proofRecord, messageContext) + } + } + + private async acceptRequest( + proofRecord: ProofExchangeRecord, + messageContext: MessageHandlerInboundMessage + ) { + messageContext.agentContext.config.logger.info(`Automatically sending presentation with autoAccept`) + + const { message } = await this.proofProtocol.acceptRequest(messageContext.agentContext, { + proofRecord, + }) + + return getOutboundMessageContext(messageContext.agentContext, { + message, + lastReceivedMessage: messageContext.message, + associatedRecord: proofRecord, + connectionRecord: messageContext.connection, + }) + } +} diff --git a/packages/core/src/modules/proofs/protocol/v3/index.ts b/packages/core/src/modules/proofs/protocol/v3/index.ts new file mode 100644 index 0000000000..c0aebb1bf7 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v3/index.ts @@ -0,0 +1,3 @@ +export * from './errors' +export * from './messages' +export * from './V3ProofProtocol' diff --git a/packages/core/src/modules/proofs/protocol/v3/messages/V3PresentationAckMessage.ts b/packages/core/src/modules/proofs/protocol/v3/messages/V3PresentationAckMessage.ts new file mode 100644 index 0000000000..527dff1b66 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v3/messages/V3PresentationAckMessage.ts @@ -0,0 +1,25 @@ +import { DidCommV2Message } from '../../../../../didcomm' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' + +export type V3PresentationAckMessageOptions = { + id?: string + threadId: string +} + +export class V3PresentationAckMessage extends DidCommV2Message { + /** + * Create new V3PresentationAckMessage instance. + * @param options + */ + public constructor(options: V3PresentationAckMessageOptions) { + super() + if (options) { + this.id = options.id ?? this.generateId() + this.thid = options.threadId + } + } + + @IsValidMessageType(V3PresentationAckMessage.type) + public readonly type = V3PresentationAckMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/present-proof/3.0/ack') +} diff --git a/packages/core/src/modules/proofs/protocol/v3/messages/V3PresentationMessage.ts b/packages/core/src/modules/proofs/protocol/v3/messages/V3PresentationMessage.ts new file mode 100644 index 0000000000..14f5f654d0 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v3/messages/V3PresentationMessage.ts @@ -0,0 +1,60 @@ +import { Expose, Type } from 'class-transformer' +import { IsArray, IsObject, IsOptional, IsString, ValidateNested } from 'class-validator' + +import { V2Attachment } from '../../../../../decorators/attachment' +import { DidCommV2Message } from '../../../../../didcomm' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { uuid } from '../../../../../utils/uuid' + +export interface V3PresentationMessageOptions { + id?: string + goalCode?: string + comment?: string + attachments: V2Attachment[] +} + +class V3PresentationMessageBody { + public constructor(options: { goalCode?: string; comment?: string }) { + if (options) { + this.comment = options.comment + this.goalCode = options.goalCode + } + } + + @IsString() + @IsOptional() + public comment?: string + + @Expose({ name: 'goal_code' }) + @IsString() + @IsOptional() + public goalCode?: string +} + +export class V3PresentationMessage extends DidCommV2Message { + public constructor(options: V3PresentationMessageOptions) { + super() + + if (options) { + this.id = options.id ?? uuid() + this.body = new V3PresentationMessageBody(options) + this.attachments = options.attachments + } + } + + @IsValidMessageType(V3PresentationMessage.type) + public readonly type = V3PresentationMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/present-proof/3.0/presentation') + + @IsObject() + @ValidateNested() + @Type(() => V3PresentationMessageBody) + public body!: V3PresentationMessageBody + + @Type(() => V2Attachment) + @IsArray() + @ValidateNested({ + each: true, + }) + public attachments!: Array +} diff --git a/packages/core/src/modules/proofs/protocol/v3/messages/V3PresentationProblemReportMessage.ts b/packages/core/src/modules/proofs/protocol/v3/messages/V3PresentationProblemReportMessage.ts new file mode 100644 index 0000000000..911fb05d22 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v3/messages/V3PresentationProblemReportMessage.ts @@ -0,0 +1,23 @@ +import type { V2ProblemReportMessageOptions } from '../../../../problem-reports/versions/v2/messages' + +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { V2ProblemReportMessage } from '../../../../problem-reports/versions/v2' + +export type V3PresentationProblemReportMessageOptions = V2ProblemReportMessageOptions + +/** + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0035-report-problem/README.md + */ +export class V3PresentationProblemReportMessage extends V2ProblemReportMessage { + /** + * Create new V3PresentationProblemReportMessage instance. + * @param options + */ + public constructor(options: V3PresentationProblemReportMessageOptions) { + super(options) + } + + @IsValidMessageType(V3PresentationProblemReportMessage.type) + public readonly type = V3PresentationProblemReportMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/present-proof/3.0/problem-report') +} diff --git a/packages/core/src/modules/proofs/protocol/v3/messages/V3ProposePresentationMessage.ts b/packages/core/src/modules/proofs/protocol/v3/messages/V3ProposePresentationMessage.ts new file mode 100644 index 0000000000..61f7cb511e --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v3/messages/V3ProposePresentationMessage.ts @@ -0,0 +1,60 @@ +import { Expose, Type } from 'class-transformer' +import { IsArray, IsObject, IsOptional, IsString, ValidateNested } from 'class-validator' + +import { V2Attachment } from '../../../../../decorators/attachment' +import { DidCommV2Message } from '../../../../../didcomm' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { uuid } from '../../../../../utils/uuid' + +export interface V3ProposePresentationMessageOptions { + id?: string + comment?: string + goalCode?: string + attachments: V2Attachment[] +} + +class V3ProposePresentationMessageBody { + public constructor(options: { goalCode?: string; comment?: string }) { + if (options) { + this.comment = options.comment + this.goalCode = options.goalCode + } + } + + @IsString() + @IsOptional() + public comment?: string + + @Expose({ name: 'goal_code' }) + @IsString() + @IsOptional() + public goalCode?: string +} + +export class V3ProposePresentationMessage extends DidCommV2Message { + public constructor(options: V3ProposePresentationMessageOptions) { + super() + + if (options) { + this.id = options.id ?? uuid() + this.body = new V3ProposePresentationMessageBody(options) + this.attachments = options.attachments + } + } + + @IsValidMessageType(V3ProposePresentationMessage.type) + public readonly type = V3ProposePresentationMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/present-proof/3.0/propose-presentation') + + @IsObject() + @ValidateNested() + @Type(() => V3ProposePresentationMessageBody) + public body!: V3ProposePresentationMessageBody + + @Type(() => V2Attachment) + @IsArray() + @ValidateNested({ + each: true, + }) + public attachments!: Array +} diff --git a/packages/core/src/modules/proofs/protocol/v3/messages/V3RequestPresentationMessage.ts b/packages/core/src/modules/proofs/protocol/v3/messages/V3RequestPresentationMessage.ts new file mode 100644 index 0000000000..493c66b82d --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v3/messages/V3RequestPresentationMessage.ts @@ -0,0 +1,66 @@ +import { Expose, Type } from 'class-transformer' +import { IsArray, IsBoolean, IsObject, IsOptional, IsString, ValidateNested } from 'class-validator' + +import { V2Attachment } from '../../../../../decorators/attachment' +import { DidCommV2Message } from '../../../../../didcomm' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { uuid } from '../../../../../utils/uuid' + +export interface V3RequestPresentationMessageOptions { + id?: string + comment?: string + goalCode?: string + willConfirm?: boolean + attachments: V2Attachment[] +} + +class V3RequestPresentationMessageBody { + public constructor(options: { goalCode?: string; comment?: string; willConfirm?: boolean }) { + if (options) { + this.comment = options.comment + this.goalCode = options.goalCode + this.willConfirm = options.willConfirm ?? true + } + } + + @IsString() + @IsOptional() + public comment?: string + + @Expose({ name: 'goal_code' }) + @IsString() + @IsOptional() + public goalCode?: string + + @Expose({ name: 'will_confirm' }) + @IsBoolean() + public willConfirm!: boolean +} + +export class V3RequestPresentationMessage extends DidCommV2Message { + public constructor(options: V3RequestPresentationMessageOptions) { + super() + + if (options) { + this.id = options.id ?? uuid() + this.body = new V3RequestPresentationMessageBody(options) + this.attachments = options.attachments + } + } + + @IsValidMessageType(V3RequestPresentationMessage.type) + public readonly type = V3RequestPresentationMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/present-proof/3.0/request-presentation') + + @IsObject() + @ValidateNested() + @Type(() => V3RequestPresentationMessageBody) + public body!: V3RequestPresentationMessageBody + + @Type(() => V2Attachment) + @IsArray() + @ValidateNested({ + each: true, + }) + public attachments!: Array +} diff --git a/packages/core/src/modules/proofs/protocol/v3/messages/index.ts b/packages/core/src/modules/proofs/protocol/v3/messages/index.ts new file mode 100644 index 0000000000..be3579f324 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v3/messages/index.ts @@ -0,0 +1,5 @@ +export * from './V3PresentationAckMessage' +export * from './V3PresentationMessage' +export * from './V3PresentationProblemReportMessage' +export * from './V3ProposePresentationMessage' +export * from './V3RequestPresentationMessage' From f12938d915070f50a847d54acc9116270b95cb91 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Mon, 28 Aug 2023 22:09:27 -0300 Subject: [PATCH 19/22] fix: body and attachments in ICV3 Signed-off-by: Ariel Gentile --- .../v3/CredentialFormatCoordinator.ts | 122 ++++++------------ .../protocol/v3/V3CredentialProtocol.ts | 108 +++++++--------- .../v3/messages/V3IssueCredentialMessage.ts | 57 ++++---- .../v3/messages/V3OfferCredentialMessage.ts | 73 ++++++----- .../v3/messages/V3ProposeCredentialMessage.ts | 67 +++++----- .../v3/messages/V3RequestCredentialMessage.ts | 56 ++++---- .../proofs/protocol/v3/V3ProofProtocol.ts | 2 +- 7 files changed, 226 insertions(+), 259 deletions(-) diff --git a/packages/core/src/modules/credentials/protocol/v3/CredentialFormatCoordinator.ts b/packages/core/src/modules/credentials/protocol/v3/CredentialFormatCoordinator.ts index 5b2b4191fc..c9285cca4c 100644 --- a/packages/core/src/modules/credentials/protocol/v3/CredentialFormatCoordinator.ts +++ b/packages/core/src/modules/credentials/protocol/v3/CredentialFormatCoordinator.ts @@ -1,7 +1,6 @@ import type { AgentContext } from '../../../../agent' import type { V2Attachment } from '../../../../decorators/attachment' import type { CredentialFormatPayload, CredentialFormatService, ExtractCredentialFormats } from '../../formats' -import type { CredentialFormatSpec } from '../../models' import type { CredentialExchangeRecord } from '../../repository/CredentialExchangeRecord' import { toV1Attachment, toV2Attachment } from '../../../../didcomm' @@ -40,8 +39,6 @@ export class CredentialFormatCoordinator ): 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: CredentialFormatSpec[] = [] const proposalAttachments: V2Attachment[] = [] let credentialPreview: V3CredentialPreview | undefined @@ -57,16 +54,16 @@ export class CredentialFormatCoordinator }) } - proposalAttachments.push(toV2Attachment(attachment)) - formats.push(format) + const v2Attachment = toV2Attachment(attachment) + v2Attachment.format = format.format + proposalAttachments.push(v2Attachment) } credentialRecord.credentialAttributes = credentialPreview?.attributes const message = new V3ProposeCredentialMessage({ id: credentialRecord.threadId, - formats, - proposalAttachments, + attachments: proposalAttachments, comment: comment, credentialPreview, }) @@ -97,7 +94,7 @@ export class CredentialFormatCoordinator const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) for (const formatService of formatServices) { - const attachment = this.getAttachmentForService(formatService, message.formats, message.proposalAttachments) + const attachment = this.getAttachmentForService(formatService, message.attachments) await formatService.processProposal(agentContext, { attachment, @@ -128,8 +125,6 @@ export class CredentialFormatCoordinator ) { const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) - // create message. there are two arrays in each message, one for formats the other for attachments - const formats: CredentialFormatSpec[] = [] const offerAttachments: V2Attachment[] = [] let credentialPreview: V3CredentialPreview | undefined @@ -141,14 +136,10 @@ export class CredentialFormatCoordinator // NOTE: We set the credential attributes from the proposal on the record as we've 'accepted' them // and can now use them to create the offer in the format services. It may be overwritten later on // if the user provided other attributes in the credentialFormats array. - credentialRecord.credentialAttributes = proposalMessage.credentialPreview?.attributes + credentialRecord.credentialAttributes = proposalMessage.body.credentialPreview?.attributes for (const formatService of formatServices) { - const proposalAttachment = this.getAttachmentForService( - formatService, - proposalMessage.formats, - proposalMessage.proposalAttachments - ) + const proposalAttachment = this.getAttachmentForService(formatService, proposalMessage.attachments) const { attachment, format, previewAttributes } = await formatService.acceptProposal(agentContext, { credentialRecord, @@ -162,8 +153,9 @@ export class CredentialFormatCoordinator }) } - offerAttachments.push(toV2Attachment(attachment)) - formats.push(format) + const v2Attachment = toV2Attachment(attachment) + v2Attachment.format = format.format + offerAttachments.push(v2Attachment) } credentialRecord.credentialAttributes = credentialPreview?.attributes @@ -177,9 +169,8 @@ export class CredentialFormatCoordinator } const message = new V3OfferCredentialMessage({ - formats, credentialPreview, - offerAttachments, + attachments: offerAttachments, comment, }) @@ -217,8 +208,6 @@ export class CredentialFormatCoordinator ): 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: CredentialFormatSpec[] = [] const offerAttachments: V2Attachment[] = [] let credentialPreview: V3CredentialPreview | undefined @@ -234,8 +223,9 @@ export class CredentialFormatCoordinator }) } - offerAttachments.push(toV2Attachment(attachment)) - formats.push(format) + const v2Attachment = toV2Attachment(attachment) + v2Attachment.format = format.format + offerAttachments.push(v2Attachment) } credentialRecord.credentialAttributes = credentialPreview?.attributes @@ -249,9 +239,8 @@ export class CredentialFormatCoordinator } const message = new V3OfferCredentialMessage({ - formats, comment, - offerAttachments, + attachments: offerAttachments, credentialPreview, }) @@ -281,7 +270,7 @@ export class CredentialFormatCoordinator const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) for (const formatService of formatServices) { - const attachment = this.getAttachmentForService(formatService, message.formats, message.offerAttachments) + const attachment = this.getAttachmentForService(formatService, message.attachments) await formatService.processOffer(agentContext, { attachment, @@ -317,16 +306,10 @@ export class CredentialFormatCoordinator messageClass: V3OfferCredentialMessage, }) - // create message. there are two arrays in each message, one for formats the other for attachments - const formats: CredentialFormatSpec[] = [] const requestAttachments: V2Attachment[] = [] for (const formatService of formatServices) { - const offerAttachment = this.getAttachmentForService( - formatService, - offerMessage.formats, - offerMessage.offerAttachments - ) + const offerAttachment = this.getAttachmentForService(formatService, offerMessage.attachments) const { attachment, format } = await formatService.acceptOffer(agentContext, { offerAttachment, @@ -334,15 +317,15 @@ export class CredentialFormatCoordinator credentialFormats, }) - requestAttachments.push(toV2Attachment(attachment)) - formats.push(format) + const v2Attachment = toV2Attachment(attachment) + v2Attachment.format = format.format + requestAttachments.push(v2Attachment) } - credentialRecord.credentialAttributes = offerMessage.credentialPreview?.attributes + credentialRecord.credentialAttributes = offerMessage.body.credentialPreview?.attributes const message = new V3RequestCredentialMessage({ - formats, - requestAttachments: requestAttachments, + attachments: requestAttachments, comment, }) @@ -380,8 +363,6 @@ export class CredentialFormatCoordinator ): 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: CredentialFormatSpec[] = [] const requestAttachments: V2Attachment[] = [] for (const formatService of formatServices) { @@ -390,14 +371,14 @@ export class CredentialFormatCoordinator credentialRecord, }) - requestAttachments.push(toV2Attachment(attachment)) - formats.push(format) + const v2Attachment = toV2Attachment(attachment) + v2Attachment.format = format.format + requestAttachments.push(v2Attachment) } const message = new V3RequestCredentialMessage({ - formats, comment, - requestAttachments: requestAttachments, + attachments: requestAttachments, }) message.thid = credentialRecord.threadId @@ -426,7 +407,7 @@ export class CredentialFormatCoordinator const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) for (const formatService of formatServices) { - const attachment = this.getAttachmentForService(formatService, message.formats, message.requestAttachments) + const attachment = this.getAttachmentForService(formatService, message.attachments) await formatService.processRequest(agentContext, { attachment, @@ -467,19 +448,13 @@ export class CredentialFormatCoordinator messageClass: V3OfferCredentialMessage, }) - // create message. there are two arrays in each message, one for formats the other for attachments - const formats: CredentialFormatSpec[] = [] const credentialAttachments: V2Attachment[] = [] for (const formatService of formatServices) { - const requestAttachment = this.getAttachmentForService( - formatService, - requestMessage.formats, - requestMessage.requestAttachments - ) + const requestAttachment = this.getAttachmentForService(formatService, requestMessage.attachments) const offerAttachment = offerMessage - ? this.getAttachmentForService(formatService, offerMessage.formats, offerMessage.offerAttachments) + ? this.getAttachmentForService(formatService, offerMessage.attachments) : undefined const { attachment, format } = await formatService.acceptRequest(agentContext, { @@ -489,13 +464,13 @@ export class CredentialFormatCoordinator credentialFormats, }) - credentialAttachments.push(toV2Attachment(attachment)) - formats.push(format) + const v2Attachment = toV2Attachment(attachment) + v2Attachment.format = format.format + credentialAttachments.push(v2Attachment) } const message = new V3IssueCredentialMessage({ - formats, - credentialAttachments: credentialAttachments, + attachments: credentialAttachments, comment, }) @@ -528,12 +503,8 @@ export class CredentialFormatCoordinator const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) for (const formatService of formatServices) { - const attachment = this.getAttachmentForService(formatService, message.formats, message.credentialAttachments) - const requestAttachment = this.getAttachmentForService( - formatService, - requestMessage.formats, - requestMessage.requestAttachments - ) + const attachment = this.getAttachmentForService(formatService, message.attachments) + const requestAttachment = this.getAttachmentForService(formatService, requestMessage.attachments) await formatService.processCredential(agentContext, { attachment, @@ -549,26 +520,17 @@ export class CredentialFormatCoordinator }) } - public getAttachmentForService( - credentialFormatService: CredentialFormatService, - formats: CredentialFormatSpec[], - attachments: V2Attachment[] - ) { - const attachmentId = this.getAttachmentIdForService(credentialFormatService, formats) - const attachment = attachments.find((attachment) => attachment.id === attachmentId) + public getAttachmentForService(credentialFormatService: CredentialFormatService, attachments: V2Attachment[]) { + const attachment = attachments.find( + (attachment) => attachment.format && credentialFormatService.supportsFormat(attachment.format) + ) if (!attachment) { - throw new AriesFrameworkError(`Attachment with id ${attachmentId} not found in attachments.`) + throw new AriesFrameworkError( + `Attachment with format ${credentialFormatService.formatKey} not found in attachments.` + ) } return toV1Attachment(attachment) } - - private getAttachmentIdForService(credentialFormatService: CredentialFormatService, formats: CredentialFormatSpec[]) { - 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/credentials/protocol/v3/V3CredentialProtocol.ts b/packages/core/src/modules/credentials/protocol/v3/V3CredentialProtocol.ts index 6b7005cdfa..0242e9bfdf 100644 --- a/packages/core/src/modules/credentials/protocol/v3/V3CredentialProtocol.ts +++ b/packages/core/src/modules/credentials/protocol/v3/V3CredentialProtocol.ts @@ -2,6 +2,7 @@ import type { AgentContext } from '../../../../agent' import type { FeatureRegistry } from '../../../../agent/FeatureRegistry' import type { MessageHandlerInboundMessage } from '../../../../agent/MessageHandler' import type { InboundMessageContext } from '../../../../agent/models/InboundMessageContext' +import type { V2Attachment } from '../../../../decorators/attachment' import type { DidCommV2Message } from '../../../../didcomm' import type { DependencyManager } from '../../../../plugins' import type { V2ProblemReportMessage } from '../../../problem-reports' @@ -11,7 +12,6 @@ import type { CredentialFormatService, ExtractCredentialFormats, } from '../../formats' -import type { CredentialFormatSpec } from '../../models/CredentialFormatSpec' import type { CredentialProtocol } from '../CredentialProtocol' import type { AcceptCredentialOptions, @@ -166,7 +166,7 @@ export class V3CredentialProtocol() - for (const msg of messageFormats) { - const service = this.getFormatServiceForFormat(msg.format) + for (const attachment of attachments) { + const service = attachment.format ? this.getFormatServiceForFormat(attachment.format) : undefined if (service) formatServices.add(service) } diff --git a/packages/core/src/modules/credentials/protocol/v3/messages/V3IssueCredentialMessage.ts b/packages/core/src/modules/credentials/protocol/v3/messages/V3IssueCredentialMessage.ts index 1baf5d3489..74787aa63d 100644 --- a/packages/core/src/modules/credentials/protocol/v3/messages/V3IssueCredentialMessage.ts +++ b/packages/core/src/modules/credentials/protocol/v3/messages/V3IssueCredentialMessage.ts @@ -1,16 +1,40 @@ import { Expose, Type } from 'class-transformer' -import { IsArray, IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' +import { IsArray, IsInstance, IsObject, IsOptional, IsString, ValidateNested } from 'class-validator' import { V2Attachment } from '../../../../../decorators/attachment' import { DidCommV2Message } from '../../../../../didcomm' import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' -import { CredentialFormatSpec } from '../../../models' export interface V3IssueCredentialMessageOptions { id?: string comment?: string - formats: CredentialFormatSpec[] - credentialAttachments: V2Attachment[] + goalCode?: string + replacementId?: string + attachments: V2Attachment[] +} + +class V3IssueCredentialMessageBody { + public constructor(options: { goalCode?: string; comment?: string; replacementId?: string }) { + if (options) { + this.comment = options.comment + this.goalCode = options.goalCode + this.replacementId = options.replacementId + } + } + + @IsString() + @IsOptional() + public comment?: string + + @Expose({ name: 'goal_code' }) + @IsString() + @IsOptional() + public goalCode?: string + + @Expose({ name: 'replacement_id' }) + @IsString() + @IsOptional() + public replacementId?: string } export class V3IssueCredentialMessage extends DidCommV2Message { @@ -19,35 +43,24 @@ export class V3IssueCredentialMessage extends DidCommV2Message { if (options) { this.id = options.id ?? this.generateId() - this.comment = options.comment - this.formats = options.formats - this.credentialAttachments = options.credentialAttachments + this.body = new V3IssueCredentialMessageBody(options) + this.attachments = options.attachments } } - @Type(() => CredentialFormatSpec) - @ValidateNested() - @IsArray() - @IsInstance(CredentialFormatSpec, { each: true }) - public formats!: CredentialFormatSpec[] - @IsValidMessageType(V3IssueCredentialMessage.type) public readonly type = V3IssueCredentialMessage.type.messageTypeUri public static readonly type = parseMessageType('https://didcomm.org/issue-credential/3.0/issue-credential') - @IsString() - @IsOptional() - public comment?: string + @IsObject() + @ValidateNested() + @Type(() => V3IssueCredentialMessageBody) + public body!: V3IssueCredentialMessageBody - @Expose({ name: 'credentials~attach' }) @Type(() => V2Attachment) @IsArray() @ValidateNested({ each: true, }) @IsInstance(V2Attachment, { each: true }) - public credentialAttachments!: V2Attachment[] - - public getCredentialAttachmentById(id: string): V2Attachment | undefined { - return this.credentialAttachments.find((attachment) => attachment.id === id) - } + public attachments!: V2Attachment[] } diff --git a/packages/core/src/modules/credentials/protocol/v3/messages/V3OfferCredentialMessage.ts b/packages/core/src/modules/credentials/protocol/v3/messages/V3OfferCredentialMessage.ts index 0178c4e6c7..61de226033 100644 --- a/packages/core/src/modules/credentials/protocol/v3/messages/V3OfferCredentialMessage.ts +++ b/packages/core/src/modules/credentials/protocol/v3/messages/V3OfferCredentialMessage.ts @@ -1,69 +1,80 @@ import { Expose, Type } from 'class-transformer' -import { IsArray, IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' +import { IsArray, IsInstance, IsObject, IsOptional, IsString, ValidateNested } from 'class-validator' import { V2Attachment } from '../../../../../decorators/attachment' import { DidCommV2Message } from '../../../../../didcomm' import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' -import { CredentialFormatSpec } from '../../../models' import { V3CredentialPreview } from './V3CredentialPreview' export interface V3OfferCredentialMessageOptions { id?: string - formats: CredentialFormatSpec[] - offerAttachments: V2Attachment[] + attachments: V2Attachment[] credentialPreview: V3CredentialPreview replacementId?: string comment?: string } -export class V3OfferCredentialMessage extends DidCommV2Message { - public constructor(options: V3OfferCredentialMessageOptions) { - super() +class V3OfferCredentialMessageBody { + public constructor(options: { + goalCode?: string + comment?: string + credentialPreview?: V3CredentialPreview + replacementId?: string + }) { if (options) { - this.id = options.id ?? this.generateId() this.comment = options.comment - this.formats = options.formats this.credentialPreview = options.credentialPreview - this.offerAttachments = options.offerAttachments + this.goalCode = options.goalCode + this.replacementId = options.replacementId } } - @Type(() => CredentialFormatSpec) - @ValidateNested() - @IsArray() - @IsInstance(CredentialFormatSpec, { each: true }) - public formats!: CredentialFormatSpec[] - - @IsValidMessageType(V3OfferCredentialMessage.type) - public readonly type = V3OfferCredentialMessage.type.messageTypeUri - public static readonly type = parseMessageType('https://didcomm.org/issue-credential/3.0/offer-credential') - @IsString() @IsOptional() public comment?: string + @Expose({ name: 'goal_code' }) + @IsString() + @IsOptional() + public goalCode?: string + @Expose({ name: 'credential_preview' }) @Type(() => V3CredentialPreview) @ValidateNested() @IsInstance(V3CredentialPreview) public credentialPreview?: V3CredentialPreview - @Expose({ name: 'offers~attach' }) - @Type(() => V2Attachment) - @IsArray() - @ValidateNested({ - each: true, - }) - @IsInstance(V2Attachment, { each: true }) - public offerAttachments!: V2Attachment[] - @Expose({ name: 'replacement_id' }) @IsString() @IsOptional() public replacementId?: string +} - public getOfferAttachmentById(id: string): V2Attachment | undefined { - return this.offerAttachments.find((attachment) => attachment.id === id) +export class V3OfferCredentialMessage extends DidCommV2Message { + public constructor(options: V3OfferCredentialMessageOptions) { + super() + if (options) { + this.id = options.id ?? this.generateId() + this.body = new V3OfferCredentialMessageBody(options) + this.attachments = options.attachments + } } + + @IsValidMessageType(V3OfferCredentialMessage.type) + public readonly type = V3OfferCredentialMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/issue-credential/3.0/offer-credential') + + @IsObject() + @ValidateNested() + @Type(() => V3OfferCredentialMessageBody) + public body!: V3OfferCredentialMessageBody + + @Type(() => V2Attachment) + @IsArray() + @ValidateNested({ + each: true, + }) + @IsInstance(V2Attachment, { each: true }) + public attachments!: V2Attachment[] } diff --git a/packages/core/src/modules/credentials/protocol/v3/messages/V3ProposeCredentialMessage.ts b/packages/core/src/modules/credentials/protocol/v3/messages/V3ProposeCredentialMessage.ts index a2b2ae07ca..93baacc1fc 100644 --- a/packages/core/src/modules/credentials/protocol/v3/messages/V3ProposeCredentialMessage.ts +++ b/packages/core/src/modules/credentials/protocol/v3/messages/V3ProposeCredentialMessage.ts @@ -1,44 +1,36 @@ import { Expose, Type } from 'class-transformer' -import { IsArray, IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' +import { IsArray, IsInstance, IsObject, IsOptional, IsString, ValidateNested } from 'class-validator' import { V2Attachment } from '../../../../../decorators/attachment' import { DidCommV2Message } from '../../../../../didcomm' import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' -import { CredentialFormatSpec } from '../../../models' import { V3CredentialPreview } from './V3CredentialPreview' export interface V3ProposeCredentialMessageOptions { id?: string - formats: CredentialFormatSpec[] - proposalAttachments: V2Attachment[] comment?: string credentialPreview?: V3CredentialPreview - attachments?: V2Attachment[] + attachments: V2Attachment[] } -export class V3ProposeCredentialMessage extends DidCommV2Message { - public constructor(options: V3ProposeCredentialMessageOptions) { - super() +class V3ProposeCredentialMessageBody { + public constructor(options: { goalCode?: string; comment?: string; credentialPreview?: V3CredentialPreview }) { 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.attachments = options.attachments + this.goalCode = options.goalCode } } - @Type(() => CredentialFormatSpec) - @ValidateNested({ each: true }) - @IsArray() - @IsInstance(CredentialFormatSpec, { each: true }) - public formats!: CredentialFormatSpec[] + @IsString() + @IsOptional() + public comment?: string - @IsValidMessageType(V3ProposeCredentialMessage.type) - public readonly type = V3ProposeCredentialMessage.type.messageTypeUri - public static readonly type = parseMessageType('https://didcomm.org/issue-credential/3.0/propose-credential') + @Expose({ name: 'goal_code' }) + @IsString() + @IsOptional() + public goalCode?: string @Expose({ name: 'credential_preview' }) @Type(() => V3CredentialPreview) @@ -46,25 +38,32 @@ export class V3ProposeCredentialMessage extends DidCommV2Message { @IsOptional() @IsInstance(V3CredentialPreview) public credentialPreview?: V3CredentialPreview +} + +export class V3ProposeCredentialMessage extends DidCommV2Message { + public constructor(options: V3ProposeCredentialMessageOptions) { + super() + if (options) { + this.id = options.id ?? this.generateId() + this.body = new V3ProposeCredentialMessageBody(options) + this.attachments = options.attachments + } + } + + @IsValidMessageType(V3ProposeCredentialMessage.type) + public readonly type = V3ProposeCredentialMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/issue-credential/3.0/propose-credential') + + @IsObject() + @ValidateNested() + @Type(() => V3ProposeCredentialMessageBody) + public body!: V3ProposeCredentialMessageBody - @Expose({ name: 'filters~attach' }) @Type(() => V2Attachment) @IsArray() @ValidateNested({ each: true, }) @IsInstance(V2Attachment, { each: true }) - public proposalAttachments!: V2Attachment[] - - /** - * Human readable information about this Credential Proposal, - * so the proposal can be evaluated by human judgment. - */ - @IsOptional() - @IsString() - public comment?: string - - public getProposalAttachmentById(id: string): V2Attachment | undefined { - return this.proposalAttachments.find((attachment) => attachment.id === id) - } + public attachments!: V2Attachment[] } diff --git a/packages/core/src/modules/credentials/protocol/v3/messages/V3RequestCredentialMessage.ts b/packages/core/src/modules/credentials/protocol/v3/messages/V3RequestCredentialMessage.ts index f3d87a5083..4aba0c7482 100644 --- a/packages/core/src/modules/credentials/protocol/v3/messages/V3RequestCredentialMessage.ts +++ b/packages/core/src/modules/credentials/protocol/v3/messages/V3RequestCredentialMessage.ts @@ -1,16 +1,33 @@ import { Expose, Type } from 'class-transformer' -import { IsArray, IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' +import { IsArray, IsInstance, IsObject, IsOptional, IsString, ValidateNested } from 'class-validator' import { V2Attachment } from '../../../../../decorators/attachment' import { DidCommV2Message } from '../../../../../didcomm' import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' -import { CredentialFormatSpec } from '../../../models' export interface V3RequestCredentialMessageOptions { id?: string - formats: CredentialFormatSpec[] - requestAttachments: V2Attachment[] + attachments: V2Attachment[] comment?: string + goalCode?: string +} + +class V3RequestCredentialMessageBody { + public constructor(options: { goalCode?: string; comment?: string }) { + if (options) { + this.comment = options.comment + this.goalCode = options.goalCode + } + } + + @IsString() + @IsOptional() + public comment?: string + + @Expose({ name: 'goal_code' }) + @IsString() + @IsOptional() + public goalCode?: string } export class V3RequestCredentialMessage extends DidCommV2Message { @@ -18,40 +35,25 @@ export class V3RequestCredentialMessage extends DidCommV2Message { super() if (options) { this.id = options.id ?? this.generateId() - this.comment = options.comment - this.formats = options.formats - this.requestAttachments = options.requestAttachments + this.body = new V3RequestCredentialMessageBody(options) + this.attachments = options.attachments } } - @Type(() => CredentialFormatSpec) - @ValidateNested() - @IsArray() - @IsInstance(CredentialFormatSpec, { each: true }) - public formats!: CredentialFormatSpec[] - @IsValidMessageType(V3RequestCredentialMessage.type) public readonly type = V3RequestCredentialMessage.type.messageTypeUri public static readonly type = parseMessageType('https://didcomm.org/issue-credential/3.0/request-credential') - @Expose({ name: 'requests~attach' }) + @IsObject() + @ValidateNested() + @Type(() => V3RequestCredentialMessageBody) + public body!: V3RequestCredentialMessageBody + @Type(() => V2Attachment) @IsArray() @ValidateNested({ each: true, }) @IsInstance(V2Attachment, { each: true }) - public requestAttachments!: V2Attachment[] - - /** - * Human readable information about this Credential Request, - * so the proposal can be evaluated by human judgment. - */ - @IsOptional() - @IsString() - public comment?: string - - public getRequestAttachmentById(id: string): V2Attachment | undefined { - return this.requestAttachments.find((attachment) => attachment.id === id) - } + public attachments!: V2Attachment[] } diff --git a/packages/core/src/modules/proofs/protocol/v3/V3ProofProtocol.ts b/packages/core/src/modules/proofs/protocol/v3/V3ProofProtocol.ts index 4d2a6d711c..47e6e4e79d 100644 --- a/packages/core/src/modules/proofs/protocol/v3/V3ProofProtocol.ts +++ b/packages/core/src/modules/proofs/protocol/v3/V3ProofProtocol.ts @@ -923,7 +923,7 @@ export class V3ProofProtocol Date: Mon, 28 Aug 2023 22:21:03 -0300 Subject: [PATCH 20/22] fix: OOB in outbound message context Signed-off-by: Ariel Gentile --- packages/core/src/agent/getOutboundMessageContext.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/agent/getOutboundMessageContext.ts b/packages/core/src/agent/getOutboundMessageContext.ts index 7b2b0c0648..d6f4101b44 100644 --- a/packages/core/src/agent/getOutboundMessageContext.ts +++ b/packages/core/src/agent/getOutboundMessageContext.ts @@ -81,8 +81,8 @@ export async function getOutboundMessageContext( if ( !(message instanceof DidCommV1Message) || - !(lastReceivedMessage instanceof DidCommV1Message) || - !(lastSentMessage instanceof DidCommV1Message) + (lastReceivedMessage !== undefined && !(lastReceivedMessage instanceof DidCommV1Message)) || + (lastSentMessage !== undefined && !(lastSentMessage instanceof DidCommV1Message)) ) { throw new AriesFrameworkError('No connection record associated with DIDComm V2 messages exchange') } From 13291924f9c8574381b65a94323fd5f757d6c3c4 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Mon, 28 Aug 2023 22:27:36 -0300 Subject: [PATCH 21/22] test: fix module tests Signed-off-by: Ariel Gentile --- .../credentials/__tests__/CredentialsModule.test.ts | 7 +++++-- .../core/src/modules/proofs/__tests__/ProofsModule.test.ts | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/core/src/modules/credentials/__tests__/CredentialsModule.test.ts b/packages/core/src/modules/credentials/__tests__/CredentialsModule.test.ts index 49a9028262..a0cd849acb 100644 --- a/packages/core/src/modules/credentials/__tests__/CredentialsModule.test.ts +++ b/packages/core/src/modules/credentials/__tests__/CredentialsModule.test.ts @@ -6,7 +6,7 @@ import { DependencyManager } from '../../../plugins/DependencyManager' import { CredentialsApi } from '../CredentialsApi' import { CredentialsModule } from '../CredentialsModule' import { CredentialsModuleConfig } from '../CredentialsModuleConfig' -import { V2CredentialProtocol } from '../protocol' +import { V2CredentialProtocol, V3CredentialProtocol } from '../protocol' import { RevocationNotificationService } from '../protocol/revocation-notification/services' import { CredentialRepository } from '../repository' @@ -54,7 +54,10 @@ describe('CredentialsModule', () => { test('registers V2CredentialProtocol if no credentialProtocols are configured', () => { const credentialsModule = new CredentialsModule() - expect(credentialsModule.config.credentialProtocols).toEqual([expect.any(V2CredentialProtocol)]) + expect(credentialsModule.config.credentialProtocols).toEqual([ + expect.any(V2CredentialProtocol), + expect.any(V3CredentialProtocol), + ]) }) test('calls register on the provided CredentialProtocols', () => { diff --git a/packages/core/src/modules/proofs/__tests__/ProofsModule.test.ts b/packages/core/src/modules/proofs/__tests__/ProofsModule.test.ts index 1126aeda52..645324d5aa 100644 --- a/packages/core/src/modules/proofs/__tests__/ProofsModule.test.ts +++ b/packages/core/src/modules/proofs/__tests__/ProofsModule.test.ts @@ -5,7 +5,7 @@ import { DependencyManager } from '../../../plugins/DependencyManager' import { ProofsApi } from '../ProofsApi' import { ProofsModule } from '../ProofsModule' import { ProofsModuleConfig } from '../ProofsModuleConfig' -import { V2ProofProtocol } from '../protocol/v2/V2ProofProtocol' +import { V2ProofProtocol, V3ProofProtocol } from '../protocol' import { ProofRepository } from '../repository' jest.mock('../../../plugins/DependencyManager') @@ -36,7 +36,7 @@ describe('ProofsModule', () => { test('registers V2ProofProtocol if no proofProtocols are configured', () => { const proofsModule = new ProofsModule() - expect(proofsModule.config.proofProtocols).toEqual([expect.any(V2ProofProtocol)]) + expect(proofsModule.config.proofProtocols).toEqual([expect.any(V2ProofProtocol), expect.any(V3ProofProtocol)]) }) test('calls register on the provided ProofProtocols', () => { From 323032ec511eef74e709daa165b9421f560b2a09 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Mon, 28 Aug 2023 22:21:03 -0300 Subject: [PATCH 22/22] fix: OOB in outbound message context Signed-off-by: Ariel Gentile --- packages/core/src/agent/getOutboundMessageContext.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/agent/getOutboundMessageContext.ts b/packages/core/src/agent/getOutboundMessageContext.ts index 7b2b0c0648..d6f4101b44 100644 --- a/packages/core/src/agent/getOutboundMessageContext.ts +++ b/packages/core/src/agent/getOutboundMessageContext.ts @@ -81,8 +81,8 @@ export async function getOutboundMessageContext( if ( !(message instanceof DidCommV1Message) || - !(lastReceivedMessage instanceof DidCommV1Message) || - !(lastSentMessage instanceof DidCommV1Message) + (lastReceivedMessage !== undefined && !(lastReceivedMessage instanceof DidCommV1Message)) || + (lastSentMessage !== undefined && !(lastSentMessage instanceof DidCommV1Message)) ) { throw new AriesFrameworkError('No connection record associated with DIDComm V2 messages exchange') }