diff --git a/packages/did-comm/src/__tests__/messagepickup-message-handler.test.ts b/packages/did-comm/src/__tests__/messagepickup-message-handler.test.ts index 2a3b9c299..82457b56e 100644 --- a/packages/did-comm/src/__tests__/messagepickup-message-handler.test.ts +++ b/packages/did-comm/src/__tests__/messagepickup-message-handler.test.ts @@ -21,6 +21,7 @@ import { IDIDCommMessage, DIDCommMessageMediaType, IPackedDIDCommMessage } from import { QUEUE_MESSAGE_TYPE } from '../protocols/routing-message-handler' import { PickupMediatorMessageHandler, + PickupRecipientMessageHandler, STATUS_REQUEST_MESSAGE_TYPE, STATUS_MESSAGE_TYPE, DELIVERY_MESSAGE_TYPE, @@ -51,191 +52,191 @@ const DIDCommEventSniffer: IEventListener = { const databaseFile = `./tmp/local-database2-${Math.random().toPrecision(5)}.sqlite` describe('messagepickup-message-handler', () => { - let recipient: IIdentifier - let recipient2: IIdentifier - let mediator: IIdentifier - let agent: TAgent - let didCommEndpointServer: Server - let listeningPort = Math.round(Math.random() * 32000 + 2048) - let dbConnection: DataSource - - let messageToQueue: Message - let messageToQueue1: Message - let innerMessage: IPackedDIDCommMessage - - beforeAll(async () => { - dbConnection = new DataSource({ - name: 'test', - type: 'sqlite', - database: databaseFile, - synchronize: false, - migrations: migrations, - migrationsRun: true, - logging: false, - entities: Entities, - }) - agent = createAgent({ - plugins: [ - new KeyManager({ - store: new MemoryKeyStore(), - kms: { - // @ts-ignore - local: new KeyManagementSystem(new MemoryPrivateKeyStore()), + describe('PickupMediatorMessageHandler', () => { + let recipient: IIdentifier + let recipient2: IIdentifier + let mediator: IIdentifier + let agent: TAgent + let didCommEndpointServer: Server + let listeningPort = Math.round(Math.random() * 32000 + 2048) + let dbConnection: DataSource + + let messageToQueue: Message + let messageToQueue1: Message + let innerMessage: IPackedDIDCommMessage + + beforeAll(async () => { + dbConnection = new DataSource({ + name: 'test', + type: 'sqlite', + database: databaseFile, + synchronize: false, + migrations: migrations, + migrationsRun: true, + logging: false, + entities: Entities, + }) + agent = createAgent({ + plugins: [ + new KeyManager({ + store: new MemoryKeyStore(), + kms: { + // @ts-ignore + local: new KeyManagementSystem(new MemoryPrivateKeyStore()), + }, + }), + new DIDManager({ + providers: { + 'did:fake': new FakeDidProvider(), + // 'did:web': new WebDIDProvider({ defaultKms: 'local' }) + }, + store: new MemoryDIDStore(), + defaultProvider: 'did:fake', + }), + new DIDResolverPlugin({ + resolver: new Resolver({ + ...new FakeDidResolver(() => agent).getDidFakeResolver(), + }), + }), + // @ts-ignore + new DIDComm([new DIDCommHttpTransport()]), + new MessageHandler({ + messageHandlers: [ + // @ts-ignore + new DIDCommMessageHandler(), + new PickupMediatorMessageHandler(), + ], + }), + new DataStore(dbConnection), + new DataStoreORM(dbConnection), + DIDCommEventSniffer, + ], + }) + + recipient = await agent.didManagerImport({ + did: 'did:fake:z6MkgbqNU4uF9NKSz5BqJQ4XKVHuQZYcUZP8pXGsJC8nTHwo', + keys: [ + { + type: 'Ed25519', + kid: 'didcomm-senderKey-1', + publicKeyHex: '1fe9b397c196ab33549041b29cf93be29b9f2bdd27322f05844112fad97ff92a', + privateKeyHex: + 'b57103882f7c66512dc96777cbafbeb2d48eca1e7a867f5a17a84e9a6740f7dc1fe9b397c196ab33549041b29cf93be29b9f2bdd27322f05844112fad97ff92a', + kms: 'local', }, - }), - new DIDManager({ - providers: { - 'did:fake': new FakeDidProvider(), - // 'did:web': new WebDIDProvider({ defaultKms: 'local' }) + ], + services: [ + { + id: 'msg1', + type: 'DIDCommMessaging', + serviceEndpoint: `http://localhost:${listeningPort}/messaging`, }, - store: new MemoryDIDStore(), - defaultProvider: 'did:fake', - }), - new DIDResolverPlugin({ - resolver: new Resolver({ - ...new FakeDidResolver(() => agent).getDidFakeResolver(), - }), - }), - // @ts-ignore - new DIDComm([new DIDCommHttpTransport()]), - new MessageHandler({ - messageHandlers: [ - // @ts-ignore - new DIDCommMessageHandler(), - new PickupMediatorMessageHandler(), - ], - }), - new DataStore(dbConnection), - new DataStoreORM(dbConnection), - DIDCommEventSniffer, - ], - }) + ], + provider: 'did:fake', + alias: 'sender', + }) - recipient = await agent.didManagerImport({ - did: 'did:fake:z6MkgbqNU4uF9NKSz5BqJQ4XKVHuQZYcUZP8pXGsJC8nTHwo', - keys: [ - { - type: 'Ed25519', - kid: 'didcomm-senderKey-1', - publicKeyHex: '1fe9b397c196ab33549041b29cf93be29b9f2bdd27322f05844112fad97ff92a', - privateKeyHex: - 'b57103882f7c66512dc96777cbafbeb2d48eca1e7a867f5a17a84e9a6740f7dc1fe9b397c196ab33549041b29cf93be29b9f2bdd27322f05844112fad97ff92a', - kms: 'local', - }, - ], - services: [ - { - id: 'msg1', - type: 'DIDCommMessaging', - serviceEndpoint: `http://localhost:${listeningPort}/messaging`, - }, - ], - provider: 'did:fake', - alias: 'sender', - }) + recipient2 = await agent.didManagerImport({ + did: 'did:fake:recipient2', + keys: [ + { + type: 'Ed25519', + kid: 'didcomm-senderKey-1', + publicKeyHex: '1fe9b397c196ab33549041b29cf93be29b9f2bdd27322f05844112fad97ff92a', + privateKeyHex: + 'b57103882f7c66512dc96777cbafbeb2d48eca1e7a867f5a17a84e9a6740f7dc1fe9b397c196ab33549041b29cf93be29b9f2bdd27322f05844112fad97ff92a', + kms: 'local', + }, + ], + services: [ + { + id: 'msg1', + type: 'DIDCommMessaging', + serviceEndpoint: `http://localhost:${listeningPort}/messaging`, + }, + ], + provider: 'did:fake', + alias: 'recipient2', + }) - recipient2 = await agent.didManagerImport({ - did: 'did:fake:recipient2', - keys: [ - { - type: 'Ed25519', - kid: 'didcomm-senderKey-1', - publicKeyHex: '1fe9b397c196ab33549041b29cf93be29b9f2bdd27322f05844112fad97ff92a', - privateKeyHex: - 'b57103882f7c66512dc96777cbafbeb2d48eca1e7a867f5a17a84e9a6740f7dc1fe9b397c196ab33549041b29cf93be29b9f2bdd27322f05844112fad97ff92a', - kms: 'local', - }, - ], - services: [ - { - id: 'msg1', - type: 'DIDCommMessaging', - serviceEndpoint: `http://localhost:${listeningPort}/messaging`, - }, - ], - provider: 'did:fake', - alias: 'recipient2', - }) + mediator = await agent.didManagerImport({ + did: 'did:fake:z6MkrPhffVLBZpxH7xvKNyD4sRVZeZsNTWJkLdHdgWbfgNu3', + keys: [ + { + type: 'Ed25519', + kid: 'didcomm-receiverKey-1', + publicKeyHex: 'b162e405b6485eff8a57932429b192ec4de13c06813e9028a7cdadf0e2703636', + privateKeyHex: + '19ed9b6949cfd0f9a57e30f0927839a985fa699491886ebcdda6a954d869732ab162e405b6485eff8a57932429b192ec4de13c06813e9028a7cdadf0e2703636', + kms: 'local', + }, + ], + services: [ + { + id: 'msg2', + type: 'DIDCommMessaging', + serviceEndpoint: `http://localhost:${listeningPort}/messaging`, + }, + ], + provider: 'did:fake', + alias: 'receiver', + }) + // console.log('sender: ', sender) + // console.log('recipient: ', recipient) - mediator = await agent.didManagerImport({ - did: 'did:fake:z6MkrPhffVLBZpxH7xvKNyD4sRVZeZsNTWJkLdHdgWbfgNu3', - keys: [ - { - type: 'Ed25519', - kid: 'didcomm-receiverKey-1', - publicKeyHex: 'b162e405b6485eff8a57932429b192ec4de13c06813e9028a7cdadf0e2703636', - privateKeyHex: - '19ed9b6949cfd0f9a57e30f0927839a985fa699491886ebcdda6a954d869732ab162e405b6485eff8a57932429b192ec4de13c06813e9028a7cdadf0e2703636', - kms: 'local', - }, - ], - services: [ - { - id: 'msg2', - type: 'DIDCommMessaging', - serviceEndpoint: `http://localhost:${listeningPort}/messaging`, + // Save messages in queue + innerMessage = await agent.packDIDCommMessage({ + packing: 'authcrypt', + message: { + type: 'test', + to: recipient.did, + from: mediator.did, + id: 'test', + body: { hello: 'world' }, }, - ], - provider: 'did:fake', - alias: 'receiver', - }) - // console.log('sender: ', sender) - // console.log('recipient: ', recipient) - - // Save messages in queue - innerMessage = await agent.packDIDCommMessage({ - packing: 'authcrypt', - message: { - type: 'test', - to: recipient.did, - from: mediator.did, - id: 'test', - body: { hello: 'world' }, - }, - }) + }) - messageToQueue = new Message({ raw: innerMessage.message }) - messageToQueue.id = 'test1' - messageToQueue.type = QUEUE_MESSAGE_TYPE - messageToQueue.to = `${recipient.did}#${recipient.keys[0].kid}` - messageToQueue.createdAt = new Date().toISOString() - await agent.dataStoreSaveMessage({ message: messageToQueue }) - - messageToQueue1 = new Message({ raw: innerMessage.message }) - messageToQueue1.id = 'test2' - messageToQueue1.type = QUEUE_MESSAGE_TYPE - messageToQueue1.to = `${recipient.did}#some-other-key` - messageToQueue1.createdAt = new Date().toISOString() - await agent.dataStoreSaveMessage({ message: messageToQueue1 }) - - const requestWithAgent = RequestWithAgentRouter({ agent }) - - await new Promise((resolve) => { - //setup a server to receive HTTP messages and forward them to this agent to be processed as DIDComm messages - const app = express() - // app.use(requestWithAgent) - app.use( - '/messaging', - requestWithAgent, - MessagingRouter({ - metaData: { type: 'DIDComm', value: 'integration test' }, - }), - ) - didCommEndpointServer = app.listen(listeningPort, () => { - resolve(true) + messageToQueue = new Message({ raw: innerMessage.message }) + messageToQueue.id = 'test1' + messageToQueue.type = QUEUE_MESSAGE_TYPE + messageToQueue.to = `${recipient.did}#${recipient.keys[0].kid}` + messageToQueue.createdAt = new Date().toISOString() + await agent.dataStoreSaveMessage({ message: messageToQueue }) + + messageToQueue1 = new Message({ raw: innerMessage.message }) + messageToQueue1.id = 'test2' + messageToQueue1.type = QUEUE_MESSAGE_TYPE + messageToQueue1.to = `${recipient.did}#some-other-key` + messageToQueue1.createdAt = new Date().toISOString() + await agent.dataStoreSaveMessage({ message: messageToQueue1 }) + + const requestWithAgent = RequestWithAgentRouter({ agent }) + + await new Promise((resolve) => { + //setup a server to receive HTTP messages and forward them to this agent to be processed as DIDComm messages + const app = express() + // app.use(requestWithAgent) + app.use( + '/messaging', + requestWithAgent, + MessagingRouter({ + metaData: { type: 'DIDComm', value: 'integration test' }, + }), + ) + didCommEndpointServer = app.listen(listeningPort, () => { + resolve(true) + }) }) }) - }) - afterAll(async () => { - try { - await new Promise((resolve, reject) => didCommEndpointServer?.close(resolve)) - } catch (e) { - //nop - } - }) + afterAll(async () => { + try { + await new Promise((resolve, reject) => didCommEndpointServer?.close(resolve)) + } catch (e) { + //nop + } + }) - describe('PickupMediatorMessageHandler', () => { it('should respond to StatusRequest with no recipient_key', async () => { expect.assertions(1) @@ -765,4 +766,270 @@ describe('messagepickup-message-handler', () => { await agent.dataStoreDeleteMessage({ id: messageToQueue2.id }) }) }) + + describe('PickupRecipientMessageHandler', () => { + let recipient: IIdentifier + let recipient2: IIdentifier + let mediator: IIdentifier + let agent: TAgent + let didCommEndpointServer: Server + let listeningPort = Math.round(Math.random() * 32000 + 2048) + let dbConnection: DataSource + + let messageToQueue: Message + let messageToQueue1: Message + let innerMessage: IPackedDIDCommMessage + + beforeAll(async () => { + dbConnection = new DataSource({ + name: 'test', + type: 'sqlite', + database: databaseFile, + synchronize: false, + migrations: migrations, + migrationsRun: true, + logging: false, + entities: Entities, + }) + agent = createAgent({ + plugins: [ + new KeyManager({ + store: new MemoryKeyStore(), + kms: { + // @ts-ignore + local: new KeyManagementSystem(new MemoryPrivateKeyStore()), + }, + }), + new DIDManager({ + providers: { + 'did:fake': new FakeDidProvider(), + // 'did:web': new WebDIDProvider({ defaultKms: 'local' }) + }, + store: new MemoryDIDStore(), + defaultProvider: 'did:fake', + }), + new DIDResolverPlugin({ + resolver: new Resolver({ + ...new FakeDidResolver(() => agent).getDidFakeResolver(), + }), + }), + // @ts-ignore + new DIDComm([new DIDCommHttpTransport()]), + new MessageHandler({ + messageHandlers: [ + // @ts-ignore + new DIDCommMessageHandler(), + new PickupRecipientMessageHandler(), + ], + }), + new DataStore(dbConnection), + new DataStoreORM(dbConnection), + DIDCommEventSniffer, + ], + }) + + recipient = await agent.didManagerImport({ + did: 'did:fake:z6MkgbqNU4uF9NKSz5BqJQ4XKVHuQZYcUZP8pXGsJC8nTHwo', + keys: [ + { + type: 'Ed25519', + kid: 'didcomm-senderKey-1', + publicKeyHex: '1fe9b397c196ab33549041b29cf93be29b9f2bdd27322f05844112fad97ff92a', + privateKeyHex: + 'b57103882f7c66512dc96777cbafbeb2d48eca1e7a867f5a17a84e9a6740f7dc1fe9b397c196ab33549041b29cf93be29b9f2bdd27322f05844112fad97ff92a', + kms: 'local', + }, + ], + services: [ + { + id: 'msg1', + type: 'DIDCommMessaging', + serviceEndpoint: `http://localhost:${listeningPort}/messaging`, + }, + ], + provider: 'did:fake', + alias: 'sender', + }) + + recipient2 = await agent.didManagerImport({ + did: 'did:fake:recipient2', + keys: [ + { + type: 'Ed25519', + kid: 'didcomm-senderKey-1', + publicKeyHex: '1fe9b397c196ab33549041b29cf93be29b9f2bdd27322f05844112fad97ff92a', + privateKeyHex: + 'b57103882f7c66512dc96777cbafbeb2d48eca1e7a867f5a17a84e9a6740f7dc1fe9b397c196ab33549041b29cf93be29b9f2bdd27322f05844112fad97ff92a', + kms: 'local', + }, + ], + services: [ + { + id: 'msg1', + type: 'DIDCommMessaging', + serviceEndpoint: `http://localhost:${listeningPort}/messaging`, + }, + ], + provider: 'did:fake', + alias: 'recipient2', + }) + + mediator = await agent.didManagerImport({ + did: 'did:fake:z6MkrPhffVLBZpxH7xvKNyD4sRVZeZsNTWJkLdHdgWbfgNu3', + keys: [ + { + type: 'Ed25519', + kid: 'didcomm-receiverKey-1', + publicKeyHex: 'b162e405b6485eff8a57932429b192ec4de13c06813e9028a7cdadf0e2703636', + privateKeyHex: + '19ed9b6949cfd0f9a57e30f0927839a985fa699491886ebcdda6a954d869732ab162e405b6485eff8a57932429b192ec4de13c06813e9028a7cdadf0e2703636', + kms: 'local', + }, + ], + services: [ + { + id: 'msg2', + type: 'DIDCommMessaging', + serviceEndpoint: `http://localhost:${listeningPort}/messaging`, + }, + ], + provider: 'did:fake', + alias: 'receiver', + }) + // console.log('sender: ', sender) + // console.log('recipient: ', recipient) + + // Save messages in queue + innerMessage = await agent.packDIDCommMessage({ + packing: 'authcrypt', + message: { + type: 'test', + to: recipient.did, + from: mediator.did, + id: 'test', + body: { hello: 'world' }, + }, + }) + + messageToQueue = new Message({ raw: innerMessage.message }) + messageToQueue.id = 'test1' + messageToQueue.type = QUEUE_MESSAGE_TYPE + messageToQueue.to = `${recipient.did}#${recipient.keys[0].kid}` + messageToQueue.createdAt = new Date().toISOString() + await agent.dataStoreSaveMessage({ message: messageToQueue }) + + messageToQueue1 = new Message({ raw: innerMessage.message }) + messageToQueue1.id = 'test2' + messageToQueue1.type = QUEUE_MESSAGE_TYPE + messageToQueue1.to = `${recipient.did}#some-other-key` + messageToQueue1.createdAt = new Date().toISOString() + await agent.dataStoreSaveMessage({ message: messageToQueue1 }) + + const requestWithAgent = RequestWithAgentRouter({ agent }) + + await new Promise((resolve) => { + //setup a server to receive HTTP messages and forward them to this agent to be processed as DIDComm messages + const app = express() + // app.use(requestWithAgent) + app.use( + '/messaging', + requestWithAgent, + MessagingRouter({ + metaData: { type: 'DIDComm', value: 'integration test' }, + }), + ) + didCommEndpointServer = app.listen(listeningPort, () => { + resolve(true) + }) + }) + }) + + afterAll(async () => { + try { + await new Promise((resolve, reject) => didCommEndpointServer?.close(resolve)) + } catch (e) { + //nop + } + }) + + it('should handle messages from MessageDelivery batch', async () => { + expect.assertions(3) + + // Send MessageDelivery + const msgId = v4() + const messageDeliveryMessage: IDIDCommMessage = { + body: {}, + id: v4(), + created_time: new Date().toISOString(), + to: recipient.did, + from: mediator.did, + type: DELIVERY_MESSAGE_TYPE, + attachments: [ + { + id: msgId, + data: { + json: JSON.parse(innerMessage.message), + }, + }, + ], + } + const packedMessage = await agent.packDIDCommMessage({ + packing: 'authcrypt', + message: messageDeliveryMessage, + }) + await agent.sendDIDCommMessage({ + messageId: messageDeliveryMessage.id, + packedMessage, + recipientDidUrl: recipient.did, + }) + + expect(DIDCommEventSniffer.onEvent).toHaveBeenCalledWith( + { + data: { + message: messageDeliveryMessage, + metaData: { packing: 'authcrypt' }, + }, + type: 'DIDCommV2Message-received', + }, + expect.anything(), + ) + + expect(DIDCommEventSniffer.onEvent).toHaveBeenCalledWith( + { + data: { + message: { + type: 'test', + to: recipient.did, + from: mediator.did, + id: 'test', + body: { hello: 'world' }, + }, + metaData: { packing: 'authcrypt' }, + }, + type: 'DIDCommV2Message-received', + }, + expect.anything(), + ) + + expect(DIDCommEventSniffer.onEvent).toHaveBeenCalledWith( + { + data: { + message: { + id: expect.anything(), + type: MESSAGES_RECEIVED_MESSAGE_TYPE, + to: mediator.did, + from: recipient.did, + created_time: expect.anything(), + thid: messageDeliveryMessage.id, + return_route: 'all', + body: { message_id_list: [msgId] }, + }, + metaData: { packing: 'authcrypt' }, + }, + type: 'DIDCommV2Message-received', + }, + expect.anything(), + ) + }) + }) }) diff --git a/packages/did-comm/src/protocols/messagepickup-message-handler.ts b/packages/did-comm/src/protocols/messagepickup-message-handler.ts index eba6fe294..1b69696b4 100644 --- a/packages/did-comm/src/protocols/messagepickup-message-handler.ts +++ b/packages/did-comm/src/protocols/messagepickup-message-handler.ts @@ -1,4 +1,11 @@ -import { IAgentContext, IDIDManager, IKeyManager, IDataStore, IDataStoreORM } from '@veramo/core' +import { + IAgentContext, + IDIDManager, + IKeyManager, + IDataStore, + IDataStoreORM, + IMessageHandler, +} from '@veramo/core' import { AbstractMessageHandler, Message } from '@veramo/message-handler' import Debug from 'debug' import { v4 } from 'uuid' @@ -8,7 +15,9 @@ import { IDIDCommMessage, DIDCommMessageMediaType, IDIDCommMessageAttachment } f import { Where, TMessageColumns } from '@veramo/core' const debug = Debug('veramo:did-comm:messagepickup-message-handler') -type IContext = IAgentContext +type IContext = IAgentContext< + IDIDManager & IKeyManager & IDIDComm & IDataStore & IDataStoreORM & IMessageHandler +> export const STATUS_REQUEST_MESSAGE_TYPE = 'https://didcomm.org/messagepickup/3.0/status-request' export const STATUS_MESSAGE_TYPE = 'https://didcomm.org/messagepickup/3.0/status' @@ -36,6 +45,7 @@ function generateGetMessagesWhereQuery(from: string, recipientKey?: string): Whe }, ] } + /** * A plugin for the {@link @veramo/message-handler#MessageHandler} that handles Pickup messages for the mediator role. * @beta This API may change without a BREAKING CHANGE notice. @@ -138,7 +148,7 @@ export class PickupMediatorMessageHandler extends AbstractMessageHandler { await Promise.all( data.message_id_list.map(async (messageId: string) => { const message = await context.agent.dataStoreGetMessage({ id: messageId }) - + // Delete message if meant for recipient if (message.to?.startsWith(`${from}#`)) { await context.agent.dataStoreDeleteMessage({ id: messageId }) @@ -200,3 +210,76 @@ export class PickupMediatorMessageHandler extends AbstractMessageHandler { } } } + +/** + * A plugin for the {@link @veramo/message-handler#MessageHandler} that handles Pickup messages for the mediator role. + * @beta This API may change without a BREAKING CHANGE notice. + */ +export class PickupRecipientMessageHandler extends AbstractMessageHandler { + constructor() { + super() + } + + /** + * Handles messages for Pickup protocol and recipient role + * https://didcomm.org/pickup/3.0/ + */ + public async handle(message: Message, context: IContext): Promise { + if (message.type === DELIVERY_MESSAGE_TYPE) { + debug('Message Delivery batch Received') + try { + const { attachments, to, from } = message + + if (!to) { + throw new Error('invalid_argument: StatusRequest received without `to` set') + } + if (!from) { + throw new Error('invalid_argument: StatusRequest received without `from` set') + } + + if (!attachments) { + throw new Error('invalid_argument: MessagesDelivery received without `attachments` set') + } + + // 1. Handle batch of messages + const messageIds = await Promise.all( + attachments.map(async (attachment) => { + await context.agent.handleMessage({ + raw: JSON.stringify(attachment.data.json), + metaData: [{ type: 'didCommMsgFromMediator', value: attachment.id }], + }) + return attachment.id + }), + ) + + // 2. Reply with messages-received + const replyMessage: IDIDCommMessage = { + type: MESSAGES_RECEIVED_MESSAGE_TYPE, + from: to, + to: from, + id: v4(), + thid: message.threadId ?? message.id, + created_time: new Date().toISOString(), + return_route: 'all', + body: { + message_id_list: messageIds, + }, + } + const packedResponse = await context.agent.packDIDCommMessage({ + message: replyMessage, + packing: 'authcrypt', + }) + await context.agent.sendDIDCommMessage({ + packedMessage: packedResponse, + messageId: replyMessage.id, + recipientDidUrl: from, + }) + } catch (ex) { + debug(ex) + } + return message + } + + return super.handle(message, context) + } +}