diff --git a/__tests__/shared/didCommWithFakeDidFlow.ts b/__tests__/shared/didCommWithFakeDidFlow.ts index d30b1e532..bbb1b1177 100644 --- a/__tests__/shared/didCommWithFakeDidFlow.ts +++ b/__tests__/shared/didCommWithFakeDidFlow.ts @@ -15,7 +15,7 @@ import { jest } from '@jest/globals' type ConfiguredAgent = TAgent const DIDCommEventSniffer: IEventListener = { - eventTypes: ['DIDCommV2Message-sent', 'DIDCommV2Message-received'], + eventTypes: ['DIDCommV2Message-sent', 'DIDCommV2Message-received', 'DIDCommV2Message-forwarded'], onEvent: jest.fn(() => Promise.resolve()), } @@ -27,7 +27,14 @@ export default (testContext: { describe('DID comm using did:fake flow', () => { let agent: ConfiguredAgent let sender: IIdentifier + let mediator: IIdentifier + let mediator2: IIdentifier let receiver: IIdentifier + let receiverWithMediation: IIdentifier + let receiverWithMediation2: IIdentifier + let receiverWithMediation3: IIdentifier + let receiverWithMediation4: IIdentifier + let receiverWithMediation5: IIdentifier beforeAll(async () => { await testContext.setup({ plugins: [DIDCommEventSniffer] }) @@ -56,6 +63,52 @@ export default (testContext: { alias: 'sender', }) + mediator = await agent.didManagerImport({ + did: 'did:fake:mediator', + keys: [ + { + type: 'Ed25519', + kid: 'didcomm-mediatorKey-1', + publicKeyHex: '1fe9b397c196ab33549041b29cf93be29b9f2bdd27322f05844112fad97ff92a', + privateKeyHex: + 'b57103882f7c66512dc96777cbafbeb2d48eca1e7a867f5a17a84e9a6740f7dc1fe9b397c196ab33549041b29cf93be29b9f2bdd27322f05844112fad97ff92a', + kms: 'local', + }, + ], + services: [ + { + id: 'msgM1', + type: 'DIDCommMessaging', + serviceEndpoint: 'http://localhost:3002/messaging', + }, + ], + provider: 'did:fake', + alias: 'mediator', + }) + + mediator2 = await agent.didManagerImport({ + did: 'did:fake:mediator2', + keys: [ + { + type: 'Ed25519', + kid: 'didcomm-mediator2Key-1', + publicKeyHex: '1fe9b397c196ab33549041b29cf93be29b9f2bdd27322f05844112fad97ff92a', + privateKeyHex: + 'b57103882f7c66512dc96777cbafbeb2d48eca1e7a867f5a17a84e9a6740f7dc1fe9b397c196ab33549041b29cf93be29b9f2bdd27322f05844112fad97ff92a', + kms: 'local', + }, + ], + services: [ + { + id: 'msgM2', + type: 'DIDCommMessaging', + serviceEndpoint: 'http://localhost:3002/messaging', + }, + ], + provider: 'did:fake', + alias: 'mediator2', + }) + receiver = await agent.didManagerImport({ did: 'did:fake:z6MkrPhffVLBZpxH7xvKNyD4sRVZeZsNTWJkLdHdgWbfgNu3', keys: [ @@ -78,6 +131,140 @@ export default (testContext: { provider: 'did:fake', alias: 'receiver', }) + + receiverWithMediation = await agent.didManagerImport({ + did: 'did:fake:receiverWithMediation', + keys: [ + { + type: 'Ed25519', + kid: 'didcomm-receiverWithMediationKey-1', + publicKeyHex: 'b162e405b6485eff8a57932429b192ec4de13c06813e9028a7cdadf0e2703636', + privateKeyHex: + '19ed9b6949cfd0f9a57e30f0927839a985fa699491886ebcdda6a954d869732ab162e405b6485eff8a57932429b192ec4de13c06813e9028a7cdadf0e2703636', + kms: 'local', + }, + ], + services: [ + { + id: 'msg3', + type: 'DIDCommMessaging', + serviceEndpoint: [ + { + uri: 'http://localhost:3002/messaging', + routingKeys: [`${mediator.did}#${mediator.keys[0].kid}`], + }, + ], + }, + ], + provider: 'did:fake', + alias: 'receiverWithMediation', + }) + + receiverWithMediation2 = await agent.didManagerImport({ + did: 'did:fake:receiverWithMediation2', + keys: [ + { + type: 'Ed25519', + kid: 'didcomm-receiverWithMediation2Key-1', + publicKeyHex: 'b162e405b6485eff8a57932429b192ec4de13c06813e9028a7cdadf0e2703636', + privateKeyHex: + '19ed9b6949cfd0f9a57e30f0927839a985fa699491886ebcdda6a954d869732ab162e405b6485eff8a57932429b192ec4de13c06813e9028a7cdadf0e2703636', + kms: 'local', + }, + ], + services: [ + { + id: 'msg4', + type: 'DIDCommMessaging', + serviceEndpoint: [ + { + uri: 'http://localhost:3002/messaging', + routingKeys: [ + `${mediator2.did}#${mediator2.keys[0].kid}`, + `${mediator.did}#${mediator.keys[0].kid}`, + ], + }, + ], + }, + ], + provider: 'did:fake', + alias: 'receiverWithMediation2', + }) + + receiverWithMediation3 = await agent.didManagerImport({ + did: 'did:fake:receiverWithMediation3', + keys: [ + { + type: 'Ed25519', + kid: 'didcomm-receiverWithMediation3Key-1', + publicKeyHex: 'b162e405b6485eff8a57932429b192ec4de13c06813e9028a7cdadf0e2703636', + privateKeyHex: + '19ed9b6949cfd0f9a57e30f0927839a985fa699491886ebcdda6a954d869732ab162e405b6485eff8a57932429b192ec4de13c06813e9028a7cdadf0e2703636', + kms: 'local', + }, + ], + services: [ + { + id: 'msg5', + type: 'DIDCommMessaging', + serviceEndpoint: [{ uri: mediator.did }], + }, + ], + provider: 'did:fake', + alias: 'receiverWithMediation3', + }) + + receiverWithMediation4 = await agent.didManagerImport({ + did: 'did:fake:receiverWithMediation4', + keys: [ + { + type: 'Ed25519', + kid: 'didcomm-receiverWithMediation4Key-1', + publicKeyHex: 'b162e405b6485eff8a57932429b192ec4de13c06813e9028a7cdadf0e2703636', + privateKeyHex: + '19ed9b6949cfd0f9a57e30f0927839a985fa699491886ebcdda6a954d869732ab162e405b6485eff8a57932429b192ec4de13c06813e9028a7cdadf0e2703636', + kms: 'local', + }, + ], + services: [ + { + id: 'msg6', + type: 'DIDCommMessaging', + serviceEndpoint: [ + { uri: mediator2.did, routingKeys: [`${mediator.did}#${mediator.keys[0].kid}`] }, + ], + }, + ], + provider: 'did:fake', + alias: 'receiverWithMediation4', + }) + + receiverWithMediation5 = await agent.didManagerImport({ + did: 'did:fake:receiverWithMediation5', + keys: [ + { + type: 'Ed25519', + kid: 'didcomm-receiverWithMediation5Key-1', + publicKeyHex: 'b162e405b6485eff8a57932429b192ec4de13c06813e9028a7cdadf0e2703636', + privateKeyHex: + '19ed9b6949cfd0f9a57e30f0927839a985fa699491886ebcdda6a954d869732ab162e405b6485eff8a57932429b192ec4de13c06813e9028a7cdadf0e2703636', + kms: 'local', + }, + ], + services: [ + { + id: 'msg7', + type: 'DIDCommMessaging', + serviceEndpoint: { + uri: 'http://localhost:3002/messaging', + routingKeys: [`${mediator.did}#${mediator.keys[0].kid}`], + }, + }, + ], + provider: 'did:fake', + alias: 'receiverWithMediation5', + }) + return true }) afterAll(testContext.tearDown) @@ -125,5 +312,302 @@ export default (testContext: { expect.anything(), ) }) + + it('should wrap and forward a message to single mediator via routingKeys', async () => { + expect.assertions(4) + + const message = { + type: 'test', + to: receiverWithMediation.did, + from: sender.did, + id: 'test', + body: { hello: 'world' }, + } + const packedMessage = await agent.packDIDCommMessage({ + packing: 'authcrypt', + message, + }) + const result = await agent.sendDIDCommMessage({ + messageId: '123', + packedMessage, + recipientDidUrl: receiverWithMediation.did, + }) + + expect(result).toBeTruthy() + expect(DIDCommEventSniffer.onEvent).toHaveBeenCalledWith( + { + data: { + messageId: '123', + next: receiverWithMediation.did, + routingKey: 'did:fake:mediator#didcomm-mediatorKey-1', + }, + type: 'DIDCommV2Message-forwarded', + }, + expect.anything(), + ) + expect(DIDCommEventSniffer.onEvent).toHaveBeenCalledWith( + { data: '123', type: 'DIDCommV2Message-sent' }, + expect.anything(), + ) + // in our case, it is the same agent that is receiving the messages + expect(DIDCommEventSniffer.onEvent).toHaveBeenCalledWith( + { + data: { + message: { + body: { next: receiverWithMediation.did }, + id: expect.anything(), + to: mediator.did, + type: 'https://didcomm.org/routing/2.0/forward', + attachments: expect.anything(), + }, + metaData: { packing: 'anoncrypt' }, + }, + type: 'DIDCommV2Message-received', + }, + expect.anything(), + ) + }) + + it('should wrap and forward a message to single mediator via routingKeys in single service endpoint', async () => { + expect.assertions(4) + + const message = { + type: 'test', + to: receiverWithMediation5.did, + from: sender.did, + id: 'test', + body: { hello: 'world' }, + } + const packedMessage = await agent.packDIDCommMessage({ + packing: 'authcrypt', + message, + }) + const result = await agent.sendDIDCommMessage({ + messageId: '123', + packedMessage, + recipientDidUrl: receiverWithMediation5.did, + }) + + expect(result).toBeTruthy() + expect(DIDCommEventSniffer.onEvent).toHaveBeenCalledWith( + { + data: { + messageId: '123', + next: receiverWithMediation5.did, + routingKey: 'did:fake:mediator#didcomm-mediatorKey-1', + }, + type: 'DIDCommV2Message-forwarded', + }, + expect.anything(), + ) + expect(DIDCommEventSniffer.onEvent).toHaveBeenCalledWith( + { data: '123', type: 'DIDCommV2Message-sent' }, + expect.anything(), + ) + // in our case, it is the same agent that is receiving the messages + expect(DIDCommEventSniffer.onEvent).toHaveBeenCalledWith( + { + data: { + message: { + body: { next: receiverWithMediation5.did }, + id: expect.anything(), + to: mediator.did, + type: 'https://didcomm.org/routing/2.0/forward', + attachments: expect.anything(), + }, + metaData: { packing: 'anoncrypt' }, + }, + type: 'DIDCommV2Message-received', + }, + expect.anything(), + ) + }) + + it('should wrap and forward a message to multiple mediators via routingKeys', async () => { + expect.assertions(5) + + const message = { + type: 'test', + to: receiverWithMediation2.did, + from: sender.did, + id: 'test', + body: { hello: 'world' }, + } + const packedMessage = await agent.packDIDCommMessage({ + packing: 'authcrypt', + message, + }) + const result = await agent.sendDIDCommMessage({ + messageId: '123', + packedMessage, + recipientDidUrl: receiverWithMediation2.did, + }) + + expect(result).toBeTruthy() + expect(DIDCommEventSniffer.onEvent).toHaveBeenCalledWith( + { + data: { + messageId: '123', + next: receiverWithMediation2.did, + routingKey: 'did:fake:mediator#didcomm-mediatorKey-1', + }, + type: 'DIDCommV2Message-forwarded', + }, + expect.anything(), + ) + expect(DIDCommEventSniffer.onEvent).toHaveBeenCalledWith( + { + data: { + messageId: '123', + next: mediator.did, + routingKey: 'did:fake:mediator2#didcomm-mediator2Key-1', + }, + type: 'DIDCommV2Message-forwarded', + }, + expect.anything(), + ) + expect(DIDCommEventSniffer.onEvent).toHaveBeenCalledWith( + { data: '123', type: 'DIDCommV2Message-sent' }, + expect.anything(), + ) + // in our case, it is the same agent that is receiving the messages + expect(DIDCommEventSniffer.onEvent).toHaveBeenCalledWith( + { + data: { + message: { + body: { next: mediator.did }, + id: expect.anything(), + to: mediator2.did, + type: 'https://didcomm.org/routing/2.0/forward', + attachments: expect.anything(), + }, + metaData: { packing: 'anoncrypt' }, + }, + type: 'DIDCommV2Message-received', + }, + expect.anything(), + ) + }) + + it('should wrap and forward a message to single mediator via DID as URI', async () => { + expect.assertions(4) + + const message = { + type: 'test', + to: receiverWithMediation3.did, + from: sender.did, + id: 'test', + body: { hello: 'world' }, + } + const packedMessage = await agent.packDIDCommMessage({ + packing: 'authcrypt', + message, + }) + const result = await agent.sendDIDCommMessage({ + messageId: '123', + packedMessage, + recipientDidUrl: receiverWithMediation3.did, + }) + + expect(result).toBeTruthy() + expect(DIDCommEventSniffer.onEvent).toHaveBeenCalledWith( + { + data: { + messageId: '123', + next: receiverWithMediation3.did, + routingKey: 'did:fake:mediator', + }, + type: 'DIDCommV2Message-forwarded', + }, + expect.anything(), + ) + expect(DIDCommEventSniffer.onEvent).toHaveBeenCalledWith( + { data: '123', type: 'DIDCommV2Message-sent' }, + expect.anything(), + ) + // in our case, it is the same agent that is receiving the messages + expect(DIDCommEventSniffer.onEvent).toHaveBeenCalledWith( + { + data: { + message: { + body: { next: receiverWithMediation3.did }, + id: expect.anything(), + to: mediator.did, + type: 'https://didcomm.org/routing/2.0/forward', + attachments: expect.anything(), + }, + metaData: { packing: 'anoncrypt' }, + }, + type: 'DIDCommV2Message-received', + }, + expect.anything(), + ) + }) + + it('should wrap and forward a message to multiple mediators via DID as URI and routingKeys', async () => { + expect.assertions(5) + + const message = { + type: 'test', + to: receiverWithMediation4.did, + from: sender.did, + id: 'test', + body: { hello: 'world' }, + } + const packedMessage = await agent.packDIDCommMessage({ + packing: 'authcrypt', + message, + }) + const result = await agent.sendDIDCommMessage({ + messageId: '123', + packedMessage, + recipientDidUrl: receiverWithMediation4.did, + }) + + expect(result).toBeTruthy() + expect(DIDCommEventSniffer.onEvent).toHaveBeenCalledWith( + { + data: { + messageId: '123', + next: receiverWithMediation4.did, + routingKey: 'did:fake:mediator#didcomm-mediatorKey-1', + }, + type: 'DIDCommV2Message-forwarded', + }, + expect.anything(), + ) + expect(DIDCommEventSniffer.onEvent).toHaveBeenCalledWith( + { + data: { + messageId: '123', + next: mediator.did, + routingKey: 'did:fake:mediator2', + }, + type: 'DIDCommV2Message-forwarded', + }, + expect.anything(), + ) + expect(DIDCommEventSniffer.onEvent).toHaveBeenCalledWith( + { data: '123', type: 'DIDCommV2Message-sent' }, + expect.anything(), + ) + // in our case, it is the same agent that is receiving the messages + expect(DIDCommEventSniffer.onEvent).toHaveBeenCalledWith( + { + data: { + message: { + body: { next: mediator.did }, + id: expect.anything(), + to: mediator2.did, + type: 'https://didcomm.org/routing/2.0/forward', + attachments: expect.anything(), + }, + metaData: { packing: 'anoncrypt' }, + }, + type: 'DIDCommV2Message-received', + }, + expect.anything(), + ) + }) }) } diff --git a/packages/core-types/src/plugin.schema.json b/packages/core-types/src/plugin.schema.json index 967895137..911bedd10 100644 --- a/packages/core-types/src/plugin.schema.json +++ b/packages/core-types/src/plugin.schema.json @@ -1922,6 +1922,19 @@ "IDataStore": { "components": { "schemas": { + "IDataStoreDeleteMessageArgs": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Required. Message ID" + } + }, + "required": [ + "id" + ], + "description": "Input arguments for {@link IDataStore.dataStoreDeleteMessage | dataStoreDeleteMessage }" + }, "IDataStoreDeleteVerifiableCredentialArgs": { "type": "object", "properties": { @@ -2039,6 +2052,10 @@ "$ref": "#/components/schemas/IMessageAttachment" }, "description": "Optional. Array of generic attachments" + }, + "returnRoute": { + "type": "string", + "description": "Optional. Signal how to reuse transport for return messages" } }, "required": [ @@ -2376,6 +2393,15 @@ } }, "methods": { + "dataStoreDeleteMessage": { + "description": "Deletes message from the data store", + "arguments": { + "$ref": "#/components/schemas/IDataStoreDeleteMessageArgs" + }, + "returnType": { + "type": "boolean" + } + }, "dataStoreDeleteVerifiableCredential": { "description": "Deletes verifiable credential from the data store", "arguments": { @@ -2889,6 +2915,10 @@ "$ref": "#/components/schemas/IMessageAttachment" }, "description": "Optional. Array of generic attachments" + }, + "returnRoute": { + "type": "string", + "description": "Optional. Signal how to reuse transport for return messages" } }, "required": [ @@ -3743,6 +3773,10 @@ "$ref": "#/components/schemas/IMessageAttachment" }, "description": "Optional. Array of generic attachments" + }, + "returnRoute": { + "type": "string", + "description": "Optional. Signal how to reuse transport for return messages" } }, "required": [ diff --git a/packages/core-types/src/types/IDataStore.ts b/packages/core-types/src/types/IDataStore.ts index 99f4bbb20..c66f4841d 100644 --- a/packages/core-types/src/types/IDataStore.ts +++ b/packages/core-types/src/types/IDataStore.ts @@ -24,6 +24,17 @@ export interface IDataStoreGetMessageArgs { id: string } +/** + * Input arguments for {@link IDataStore.dataStoreDeleteMessage | dataStoreDeleteMessage} + * @public + */ +export interface IDataStoreDeleteMessageArgs { + /** + * Required. Message ID + */ + id: string +} + /** * Input arguments for {@link IDataStore.dataStoreSaveVerifiableCredential | dataStoreSaveVerifiableCredential} * @public @@ -98,6 +109,13 @@ export interface IDataStore extends IPluginMethodMap { */ dataStoreGetMessage(args: IDataStoreGetMessageArgs): Promise + /** + * Deletes message from the data store + * @param args - arguments for deleting message + * @returns a promise that resolves to a boolean + */ + dataStoreDeleteMessage(args: IDataStoreDeleteMessageArgs): Promise + /** * Saves verifiable credential to the data store * @param args - verifiable credential diff --git a/packages/core-types/src/types/IMessage.ts b/packages/core-types/src/types/IMessage.ts index fa5c0ab67..78afa96a6 100644 --- a/packages/core-types/src/types/IMessage.ts +++ b/packages/core-types/src/types/IMessage.ts @@ -124,4 +124,9 @@ export interface IMessage { * Optional. Array of generic attachments */ attachments?: IMessageAttachment[] + + /** + * Optional. Signal how to reuse transport for return messages + */ + returnRoute?: string } diff --git a/packages/data-store-json/src/data-store-json.ts b/packages/data-store-json/src/data-store-json.ts index a4053897b..55afbad52 100644 --- a/packages/data-store-json/src/data-store-json.ts +++ b/packages/data-store-json/src/data-store-json.ts @@ -5,6 +5,7 @@ import { IDataStore, IDataStoreDeleteVerifiableCredentialArgs, IDataStoreGetMessageArgs, + IDataStoreDeleteMessageArgs, IDataStoreGetVerifiableCredentialArgs, IDataStoreGetVerifiablePresentationArgs, IDataStoreORM, @@ -76,7 +77,7 @@ export class DataStoreJson implements IAgentPlugin { // IDataStore methods dataStoreSaveMessage: this.dataStoreSaveMessage.bind(this), dataStoreGetMessage: this.dataStoreGetMessage.bind(this), - //dataStoreDeleteMessage: this.dataStoreDeleteMessage.bind(this), + dataStoreDeleteMessage: this.dataStoreDeleteMessage.bind(this), dataStoreSaveVerifiableCredential: this.dataStoreSaveVerifiableCredential.bind(this), dataStoreGetVerifiableCredential: this.dataStoreGetVerifiableCredential.bind(this), dataStoreDeleteVerifiableCredential: this.dataStoreDeleteVerifiableCredential.bind(this), @@ -137,6 +138,18 @@ export class DataStoreJson implements IAgentPlugin { } } + async dataStoreDeleteMessage(args: IDataStoreDeleteMessageArgs): Promise { + const message = this.cacheTree.messages[args.id] + if (message) { + const oldTree = deserialize(serialize(this.cacheTree, { lossy: true })) + delete this.cacheTree.messages[args.id] + await this.notifyUpdate(oldTree, this.cacheTree) + return true + } else { + return false + } + } + private async _dataStoreSaveVerifiableCredential( args: IDataStoreSaveVerifiableCredentialArgs, postUpdates: boolean = true, @@ -393,15 +406,17 @@ export class DataStoreJson implements IAgentPlugin { filteredCredentials.add(this.cacheTree.credentials[claim.credentialHash]) }) - return deserialize(serialize( - Array.from(filteredCredentials).map((cred) => { - const { hash, parsedCredential } = cred - return { - hash, - verifiableCredential: parsedCredential, - } - }), - )) + return deserialize( + serialize( + Array.from(filteredCredentials).map((cred) => { + const { hash, parsedCredential } = cred + return { + hash, + verifiableCredential: parsedCredential, + } + }), + ), + ) } async dataStoreORMGetVerifiableCredentialsByClaimsCount( @@ -422,15 +437,17 @@ export class DataStoreJson implements IAgentPlugin { context.authorizedDID, ) - return deserialize(serialize( - credentials.map((cred: any) => { - const { hash, parsedCredential } = cred - return { - hash, - verifiableCredential: parsedCredential, - } - }), - )) + return deserialize( + serialize( + credentials.map((cred: any) => { + const { hash, parsedCredential } = cred + return { + hash, + verifiableCredential: parsedCredential, + } + }), + ), + ) } async dataStoreORMGetVerifiableCredentialsCount( @@ -451,15 +468,17 @@ export class DataStoreJson implements IAgentPlugin { context.authorizedDID, ) - return deserialize(serialize( - presentations.map((pres: any) => { - const { hash, parsedPresentation } = pres - return { - hash, - verifiablePresentation: parsedPresentation, - } - }), - )) + return deserialize( + serialize( + presentations.map((pres: any) => { + const { hash, parsedPresentation } = pres + return { + hash, + verifiablePresentation: parsedPresentation, + } + }), + ), + ) } async dataStoreORMGetVerifiablePresentationsCount( diff --git a/packages/data-store/src/data-store.ts b/packages/data-store/src/data-store.ts index 7250249ef..e8cdbe1cf 100644 --- a/packages/data-store/src/data-store.ts +++ b/packages/data-store/src/data-store.ts @@ -3,6 +3,7 @@ import { IDataStore, IDataStoreDeleteVerifiableCredentialArgs, IDataStoreGetMessageArgs, + IDataStoreDeleteMessageArgs, IDataStoreGetVerifiableCredentialArgs, IDataStoreGetVerifiablePresentationArgs, IDataStoreSaveMessageArgs, @@ -45,6 +46,7 @@ export class DataStore implements IAgentPlugin { this.methods = { dataStoreSaveMessage: this.dataStoreSaveMessage.bind(this), dataStoreGetMessage: this.dataStoreGetMessage.bind(this), + dataStoreDeleteMessage: this.dataStoreDeleteMessage.bind(this), dataStoreDeleteVerifiableCredential: this.dataStoreDeleteVerifiableCredential.bind(this), dataStoreSaveVerifiableCredential: this.dataStoreSaveVerifiableCredential.bind(this), dataStoreGetVerifiableCredential: this.dataStoreGetVerifiableCredential.bind(this), @@ -70,6 +72,20 @@ export class DataStore implements IAgentPlugin { return createMessage(messageEntity) } + async dataStoreDeleteMessage(args: IDataStoreDeleteMessageArgs): Promise { + const messageEntity = await (await getConnectedDb(this.dbConnection)).getRepository(Message).findOne({ + where: { id: args.id }, + relations: ['credentials', 'presentations'], + }) + if (!messageEntity) { + return false + } + + await (await getConnectedDb(this.dbConnection)).getRepository(Message).remove(messageEntity) + + return true + } + async dataStoreDeleteVerifiableCredential( args: IDataStoreDeleteVerifiableCredentialArgs, ): Promise { diff --git a/packages/did-comm/src/__tests__/coordinate-mediation-message-handler.test.ts b/packages/did-comm/src/__tests__/coordinate-mediation-message-handler.test.ts new file mode 100644 index 000000000..e4c9d7f7b --- /dev/null +++ b/packages/did-comm/src/__tests__/coordinate-mediation-message-handler.test.ts @@ -0,0 +1,353 @@ +import { DIDComm } from '../didcomm' +import { + createAgent, + IDIDManager, + IEventListener, + IIdentifier, + IKeyManager, + IMessageHandler, + IResolver, + TAgent, +} from '../../../core/src' +import { DIDManager, MemoryDIDStore } from '../../../did-manager/src' +import { KeyManager, MemoryKeyStore, MemoryPrivateKeyStore } from '../../../key-manager/src' +import { KeyManagementSystem } from '../../../kms-local/src' +import { DIDResolverPlugin } from '../../../did-resolver/src' +import { Resolver } from 'did-resolver' +import { DIDCommHttpTransport } from '../transports/transports' +import { IDIDComm } from '../types/IDIDComm' +import { MessageHandler } from '../../../message-handler/src' +import { + CoordinateMediationMediatorMessageHandler, + CoordinateMediationRecipientMessageHandler, + createMediateRequestMessage, + createMediateGrantMessage, +} from '../protocols/coordinate-mediation-message-handler' +import { FakeDidProvider, FakeDidResolver } from '../../../test-utils/src' +import { MessagingRouter, RequestWithAgentRouter } from '../../../remote-server/src' +import { Entities, IDataStore, migrations } from '../../../data-store/src' +import express from 'express' +import { Server } from 'http' +import { DIDCommMessageHandler } from '../message-handler' +import { DataStore, DataStoreORM } from '../../../data-store/src' +import { DataSource } from 'typeorm' +import { v4 } from 'uuid' +import { jest } from '@jest/globals' + +const DIDCommEventSniffer: IEventListener = { + eventTypes: ['DIDCommV2Message-sent', 'DIDCommV2Message-received'], + onEvent: jest.fn(() => Promise.resolve()), +} + +const databaseFile = `./tmp/local-database2-${Math.random().toPrecision(5)}.sqlite` + +describe('coordinate-mediation-message-handler', () => { + let recipient: IIdentifier + let mediator: IIdentifier + let agent: TAgent + let didCommEndpointServer: Server + let listeningPort = Math.round(Math.random() * 32000 + 2048) + let dbConnection: DataSource + + 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 CoordinateMediationMediatorMessageHandler(), + new CoordinateMediationRecipientMessageHandler(), + ], + }), + 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', + }) + + 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) + + 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 + } + }) + + const expectMsg = (msgid: string) => { + expect(DIDCommEventSniffer.onEvent).toHaveBeenCalledWith( + { + data: msgid, + type: 'DIDCommV2Message-sent', + }, + expect.anything(), + ) + } + + const expectReceiveRequest = (msgid: string) => { + // mediator receives request + expect(DIDCommEventSniffer.onEvent).toHaveBeenCalledWith( + { + data: { + message: { + body: {}, + from: recipient.did, + return_route: 'all', + id: msgid, + to: mediator.did, + created_time: expect.anything(), + type: 'https://didcomm.org/coordinate-mediation/2.0/mediate-request', + }, + metaData: { packing: 'authcrypt' }, + }, + type: 'DIDCommV2Message-received', + }, + expect.anything(), + ) + } + + const expectGrantRequest = (msgid: string) => { + // mediator receives request + expect(DIDCommEventSniffer.onEvent).toHaveBeenCalledWith( + { + data: { + message: { + body: { + routing_did: [mediator.did], + }, + from: mediator.did, + id: expect.anything(), + thid: msgid, + to: recipient.did, + created_time: expect.anything(), + type: 'https://didcomm.org/coordinate-mediation/2.0/mediate-grant', + }, + metaData: { packing: 'authcrypt' }, + }, + type: 'DIDCommV2Message-received', + }, + expect.anything(), + ) + } + + describe('mediator', () => { + it('should grant mediation to valid request via return_route', async () => { + expect.assertions(4) + + const mediateRequestMessage = createMediateRequestMessage(recipient.did, mediator.did) + const packedMessage = await agent.packDIDCommMessage({ + packing: 'authcrypt', + message: mediateRequestMessage, + }) + await agent.sendDIDCommMessage({ + messageId: mediateRequestMessage.id, + packedMessage, + recipientDidUrl: mediator.did, + }) + expectMsg(mediateRequestMessage.id) + expectReceiveRequest(mediateRequestMessage.id) + expect(DIDCommEventSniffer.onEvent).toHaveBeenCalledWith( + { + data: expect.anything(), + type: 'DIDCommV2Message-sent', + }, + expect.anything(), + ) + expectGrantRequest(mediateRequestMessage.id) + }) + }) + + describe('recipient', () => { + it('should save new service on mediate grant', async () => { + const mediateRequestMessage = createMediateRequestMessage(recipient.did, mediator.did) + const packedMessage = await agent.packDIDCommMessage({ + packing: 'authcrypt', + message: mediateRequestMessage, + }) + await agent.sendDIDCommMessage({ + messageId: mediateRequestMessage.id, + packedMessage, + recipientDidUrl: mediator.did, + }) + + const didDoc = (await agent.resolveDid({ didUrl: recipient.did })).didDocument + const service = didDoc?.service?.find((s) => s.id === `${recipient.did}#didcomm-mediator`) + expect(service?.serviceEndpoint).toEqual([{ uri: mediator.did }]) + }) + + it('should remove service on mediate deny', async () => { + const mediateRequestMessage = createMediateRequestMessage(recipient.did, mediator.did) + const packedMessage = await agent.packDIDCommMessage({ + packing: 'authcrypt', + message: mediateRequestMessage, + }) + await agent.sendDIDCommMessage({ + messageId: mediateRequestMessage.id, + packedMessage, + recipientDidUrl: mediator.did, + }) + + const msgid = v4() + const packedDenyMessage = await agent.packDIDCommMessage({ + packing: 'authcrypt', + message: { + type: 'https://didcomm.org/coordinate-mediation/2.0/mediate-deny', + from: mediator.did, + to: recipient.did, + id: msgid, + thid: '', + body: {}, + }, + }) + await agent.sendDIDCommMessage({ + messageId: msgid, + packedMessage: packedDenyMessage, + recipientDidUrl: recipient.did, + }) + + const didDoc = (await agent.resolveDid({ didUrl: recipient.did })).didDocument + const service = didDoc?.service?.find((s) => s.id === `${recipient.did}#didcomm-mediator`) + expect(service).toBeUndefined() + }) + + it('should not save service if mediate request cannot be found', async () => { + const mediateGrantMessage = createMediateGrantMessage(recipient.did, mediator.did, '') + const packedMessage = await agent.packDIDCommMessage({ + packing: 'authcrypt', + message: mediateGrantMessage, + }) + await agent.sendDIDCommMessage({ + messageId: mediateGrantMessage.id, + packedMessage, + recipientDidUrl: recipient.did, + }) + + const didDoc = (await agent.resolveDid({ didUrl: recipient.did })).didDocument + const service = didDoc?.service?.find((s) => s.id === `${recipient.did}#didcomm-mediator`) + expect(service).toBeUndefined() + }) + + it('should not save service if mediate grant message has bad routing_did', async () => { + const msgid = v4() + const packedMessage = await agent.packDIDCommMessage({ + packing: 'authcrypt', + message: { + type: 'https://didcomm.org/coordinate-mediation/2.0/mediate-grant', + from: mediator.did, + to: recipient.did, + id: msgid, + thid: '', + body: {}, + }, + }) + await agent.sendDIDCommMessage({ + messageId: msgid, + packedMessage, + recipientDidUrl: recipient.did, + }) + + const didDoc = (await agent.resolveDid({ didUrl: recipient.did })).didDocument + const service = didDoc?.service?.find((s) => s.id === `${recipient.did}#didcomm-mediator`) + expect(service).toBeUndefined() + }) + }) +}) diff --git a/packages/did-comm/src/__tests__/messagepickup-message-handler.test.ts b/packages/did-comm/src/__tests__/messagepickup-message-handler.test.ts new file mode 100644 index 000000000..a0cf0a134 --- /dev/null +++ b/packages/did-comm/src/__tests__/messagepickup-message-handler.test.ts @@ -0,0 +1,1036 @@ +import { DIDComm } from '../didcomm' +import { + createAgent, + IDIDManager, + IEventListener, + IIdentifier, + IKeyManager, + IMessageHandler, + IResolver, + TAgent, +} from '../../../core/src' +import { DIDManager, MemoryDIDStore } from '../../../did-manager/src' +import { KeyManager, MemoryKeyStore, MemoryPrivateKeyStore } from '../../../key-manager/src' +import { KeyManagementSystem } from '../../../kms-local/src' +import { DIDResolverPlugin } from '../../../did-resolver/src' +import { Resolver } from 'did-resolver' +import { DIDCommHttpTransport } from '../transports/transports' +import { IDIDComm } from '../types/IDIDComm' +import { MessageHandler } from '../../../message-handler/src' +import { IDIDCommMessage, DIDCommMessageMediaType, IPackedDIDCommMessage } from '../types/message-types' +import { QUEUE_MESSAGE_TYPE } from '../protocols/routing-message-handler' +import { + PickupMediatorMessageHandler, + PickupRecipientMessageHandler, + STATUS_REQUEST_MESSAGE_TYPE, + STATUS_MESSAGE_TYPE, + DELIVERY_MESSAGE_TYPE, + DELIVERY_REQUEST_MESSAGE_TYPE, + MESSAGES_RECEIVED_MESSAGE_TYPE, +} from '../protocols/messagepickup-message-handler' +import { FakeDidProvider, FakeDidResolver } from '../../../test-utils/src' +import { MessagingRouter, RequestWithAgentRouter } from '../../../remote-server/src' +import { Entities, IDataStore, migrations } from '../../../data-store/src' +import express from 'express' +import { Server } from 'http' +import { DIDCommMessageHandler } from '../message-handler' +import { DataStore, DataStoreORM } from '../../../data-store/src' +import { DataSource } from 'typeorm' +import { v4 } from 'uuid' +import { Message } from '@veramo/message-handler' +import { jest } from '@jest/globals' + +const DIDCommEventSniffer: IEventListener = { + eventTypes: [ + 'DIDCommV2Message-sent', + 'DIDCommV2Message-received', + 'DIDCommV2Message-forwardMessageQueued', + 'DIDCommV2Message-forwardMessageDequeued', + ], + onEvent: jest.fn(() => Promise.resolve()), +} + +const databaseFile = `./tmp/local-database2-${Math.random().toPrecision(5)}.sqlite` + +describe('messagepickup-message-handler', () => { + 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', + }, + ], + 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 respond to StatusRequest with no recipient_key', async () => { + expect.assertions(1) + + // Send StatusRequest + const statusRequestMessage: IDIDCommMessage = { + id: v4(), + type: STATUS_REQUEST_MESSAGE_TYPE, + to: mediator.did, + from: recipient.did, + return_route: 'all', + body: {}, + } + const packedMessage = await agent.packDIDCommMessage({ + packing: 'authcrypt', + message: statusRequestMessage, + }) + await agent.sendDIDCommMessage({ + messageId: statusRequestMessage.id, + packedMessage, + recipientDidUrl: mediator.did, + }) + + expect(DIDCommEventSniffer.onEvent).toHaveBeenCalledWith( + { + data: { + message: { + body: { message_count: 2, live_delivery: false }, + id: expect.anything(), + created_time: expect.anything(), + thid: statusRequestMessage.id, + to: recipient.did, + from: mediator.did, + type: STATUS_MESSAGE_TYPE, + }, + metaData: { packing: 'authcrypt' }, + }, + type: 'DIDCommV2Message-received', + }, + expect.anything(), + ) + }) + + it('should respond to StatusRequest with recipient_key', async () => { + expect.assertions(1) + + // Send StatusRequest + const statusRequestMessage: IDIDCommMessage = { + id: v4(), + type: STATUS_REQUEST_MESSAGE_TYPE, + to: mediator.did, + from: recipient.did, + return_route: 'all', + body: { + recipient_key: `${recipient.did}#${recipient.keys[0].kid}`, + }, + } + const packedMessage = await agent.packDIDCommMessage({ + packing: 'authcrypt', + message: statusRequestMessage, + }) + await agent.sendDIDCommMessage({ + messageId: statusRequestMessage.id, + packedMessage, + recipientDidUrl: mediator.did, + }) + + expect(DIDCommEventSniffer.onEvent).toHaveBeenCalledWith( + { + data: { + message: { + body: { + message_count: 1, + live_delivery: false, + recipient_key: `${recipient.did}#${recipient.keys[0].kid}`, + }, + id: expect.anything(), + created_time: expect.anything(), + thid: statusRequestMessage.id, + to: recipient.did, + from: mediator.did, + type: STATUS_MESSAGE_TYPE, + }, + metaData: { packing: 'authcrypt' }, + }, + type: 'DIDCommV2Message-received', + }, + expect.anything(), + ) + }) + + it('should not respond to StatusRequest with no return_route', async () => { + expect.assertions(1) + const statusRequestMessage: IDIDCommMessage = { + id: v4(), + type: STATUS_REQUEST_MESSAGE_TYPE, + to: mediator.did, + from: recipient.did, + body: {}, + } + const packedMessage = await agent.packDIDCommMessage({ + packing: 'authcrypt', + message: statusRequestMessage, + }) + await agent.sendDIDCommMessage({ + messageId: statusRequestMessage.id, + packedMessage, + recipientDidUrl: mediator.did, + }) + + expect(DIDCommEventSniffer.onEvent).not.toHaveBeenCalledWith( + { + data: { + message: { + body: expect.anything(), + id: expect.anything(), + created_time: expect.anything(), + thid: statusRequestMessage.id, + to: recipient.did, + from: mediator.did, + type: STATUS_MESSAGE_TYPE, + }, + metaData: { packing: 'authcrypt' }, + }, + type: 'DIDCommV2Message-received', + }, + expect.anything(), + ) + }) + + it('should respond to DeliveryRequest with no recipient_key', async () => { + expect.assertions(1) + + // Send DeliveryRequest + const deliveryRequestMessage: IDIDCommMessage = { + id: v4(), + type: DELIVERY_REQUEST_MESSAGE_TYPE, + to: mediator.did, + from: recipient.did, + return_route: 'all', + body: { limit: 2 }, + } + const packedMessage = await agent.packDIDCommMessage({ + packing: 'authcrypt', + message: deliveryRequestMessage, + }) + await agent.sendDIDCommMessage({ + messageId: deliveryRequestMessage.id, + packedMessage, + recipientDidUrl: mediator.did, + }) + + expect(DIDCommEventSniffer.onEvent).toHaveBeenCalledWith( + { + data: { + message: { + body: {}, + id: expect.anything(), + created_time: expect.anything(), + thid: deliveryRequestMessage.id, + to: recipient.did, + from: mediator.did, + type: DELIVERY_MESSAGE_TYPE, + attachments: [ + { + id: messageToQueue.id, + media_type: DIDCommMessageMediaType.ENCRYPTED, + data: { + json: JSON.parse(innerMessage.message), + }, + }, + { + id: messageToQueue1.id, + media_type: DIDCommMessageMediaType.ENCRYPTED, + data: { + json: JSON.parse(innerMessage.message), + }, + }, + ], + }, + metaData: { packing: 'authcrypt' }, + }, + type: 'DIDCommV2Message-received', + }, + expect.anything(), + ) + }) + + it('should respond to DeliveryRequest with recipient_key', async () => { + expect.assertions(1) + + // Send DeliveryRequest + const deliveryRequestMessage: IDIDCommMessage = { + id: v4(), + type: DELIVERY_REQUEST_MESSAGE_TYPE, + to: mediator.did, + from: recipient.did, + return_route: 'all', + body: { limit: 2, recipient_key: `${recipient.did}#${recipient.keys[0].kid}` }, + } + const packedMessage = await agent.packDIDCommMessage({ + packing: 'authcrypt', + message: deliveryRequestMessage, + }) + await agent.sendDIDCommMessage({ + messageId: deliveryRequestMessage.id, + packedMessage, + recipientDidUrl: mediator.did, + }) + + expect(DIDCommEventSniffer.onEvent).toHaveBeenCalledWith( + { + data: { + message: { + body: { recipient_key: `${recipient.did}#${recipient.keys[0].kid}` }, + id: expect.anything(), + created_time: expect.anything(), + thid: deliveryRequestMessage.id, + to: recipient.did, + from: mediator.did, + type: DELIVERY_MESSAGE_TYPE, + attachments: [ + { + id: messageToQueue.id, + media_type: DIDCommMessageMediaType.ENCRYPTED, + data: { + json: JSON.parse(innerMessage.message), + }, + }, + ], + }, + metaData: { packing: 'authcrypt' }, + }, + type: 'DIDCommV2Message-received', + }, + expect.anything(), + ) + }) + + it('should not respond to DeliveryRequest with no return_route', async () => { + expect.assertions(1) + const deliveryRequestMessage: IDIDCommMessage = { + id: v4(), + type: DELIVERY_REQUEST_MESSAGE_TYPE, + to: mediator.did, + from: recipient.did, + body: { limit: 2 }, + } + const packedMessage = await agent.packDIDCommMessage({ + packing: 'authcrypt', + message: deliveryRequestMessage, + }) + await agent.sendDIDCommMessage({ + messageId: deliveryRequestMessage.id, + packedMessage, + recipientDidUrl: mediator.did, + }) + + expect(DIDCommEventSniffer.onEvent).not.toHaveBeenCalledWith( + { + data: { + message: { + attachments: expect.anything(), + body: expect.anything(), + id: expect.anything(), + created_time: expect.anything(), + thid: deliveryRequestMessage.id, + to: recipient.did, + from: mediator.did, + type: DELIVERY_MESSAGE_TYPE, + }, + metaData: { packing: 'authcrypt' }, + }, + type: 'DIDCommV2Message-received', + }, + expect.anything(), + ) + }) + + it('should not respond to DeliveryRequest with no limit', async () => { + expect.assertions(1) + const deliveryRequestMessage: IDIDCommMessage = { + id: v4(), + type: DELIVERY_REQUEST_MESSAGE_TYPE, + to: mediator.did, + from: recipient.did, + return_route: 'all', + body: {}, + } + const packedMessage = await agent.packDIDCommMessage({ + packing: 'authcrypt', + message: deliveryRequestMessage, + }) + await agent.sendDIDCommMessage({ + messageId: deliveryRequestMessage.id, + packedMessage, + recipientDidUrl: mediator.did, + }) + + expect(DIDCommEventSniffer.onEvent).not.toHaveBeenCalledWith( + { + data: { + message: { + attachments: expect.anything(), + body: expect.anything(), + id: expect.anything(), + created_time: expect.anything(), + thid: deliveryRequestMessage.id, + to: recipient.did, + from: mediator.did, + type: DELIVERY_MESSAGE_TYPE, + }, + metaData: { packing: 'authcrypt' }, + }, + type: 'DIDCommV2Message-received', + }, + expect.anything(), + ) + }) + + it('should not respond to DeliveryRequest with bad limit', async () => { + expect.assertions(1) + const deliveryRequestMessage: IDIDCommMessage = { + id: v4(), + type: DELIVERY_REQUEST_MESSAGE_TYPE, + to: mediator.did, + from: recipient.did, + return_route: 'all', + body: { limit: 'not a number' }, + } + const packedMessage = await agent.packDIDCommMessage({ + packing: 'authcrypt', + message: deliveryRequestMessage, + }) + await agent.sendDIDCommMessage({ + messageId: deliveryRequestMessage.id, + packedMessage, + recipientDidUrl: mediator.did, + }) + + expect(DIDCommEventSniffer.onEvent).not.toHaveBeenCalledWith( + { + data: { + message: { + attachments: expect.anything(), + body: expect.anything(), + id: expect.anything(), + created_time: expect.anything(), + thid: deliveryRequestMessage.id, + to: recipient.did, + from: mediator.did, + type: DELIVERY_MESSAGE_TYPE, + }, + metaData: { packing: 'authcrypt' }, + }, + type: 'DIDCommV2Message-received', + }, + expect.anything(), + ) + }) + + it('should clear message on MessagesReceived', async () => { + expect.assertions(2) + + // Save message + const messageToQueue2 = new Message({ raw: innerMessage.message }) + messageToQueue2.id = 'test3' + messageToQueue2.type = QUEUE_MESSAGE_TYPE + messageToQueue2.to = `${recipient.did}#${recipient.keys[0].kid}` + messageToQueue2.createdAt = new Date().toISOString() + await agent.dataStoreSaveMessage({ message: messageToQueue2 }) + + // Send MessagesRequest + const messagesRequestMessage: IDIDCommMessage = { + id: v4(), + type: MESSAGES_RECEIVED_MESSAGE_TYPE, + to: mediator.did, + from: recipient.did, + return_route: 'all', + body: { message_id_list: [messageToQueue2.id] }, + } + const packedMessage = await agent.packDIDCommMessage({ + packing: 'authcrypt', + message: messagesRequestMessage, + }) + await agent.sendDIDCommMessage({ + messageId: messagesRequestMessage.id, + packedMessage, + recipientDidUrl: mediator.did, + }) + + expect(DIDCommEventSniffer.onEvent).toHaveBeenCalledWith( + { + data: messageToQueue2.id, + type: 'DIDCommV2Message-forwardMessageDequeued', + }, + expect.anything(), + ) + expect(DIDCommEventSniffer.onEvent).toHaveBeenCalledWith( + { + data: { + message: { + body: { message_count: 2, live_delivery: false }, + id: expect.anything(), + created_time: expect.anything(), + thid: messagesRequestMessage.id, + to: recipient.did, + from: mediator.did, + type: STATUS_MESSAGE_TYPE, + }, + metaData: { packing: 'authcrypt' }, + }, + type: 'DIDCommV2Message-received', + }, + expect.anything(), + ) + }) + + it('should clear multiple messages on MessagesReceived', async () => { + expect.assertions(3) + + // Save messages + const messageToQueue2 = new Message({ raw: innerMessage.message }) + messageToQueue2.id = v4() + messageToQueue2.type = QUEUE_MESSAGE_TYPE + messageToQueue2.to = `${recipient.did}#${recipient.keys[0].kid}` + messageToQueue2.createdAt = new Date().toISOString() + await agent.dataStoreSaveMessage({ message: messageToQueue2 }) + + const messageToQueue3 = new Message({ raw: innerMessage.message }) + messageToQueue3.id = v4() + messageToQueue3.type = QUEUE_MESSAGE_TYPE + messageToQueue3.to = `${recipient.did}#${recipient.keys[0].kid}` + messageToQueue3.createdAt = new Date().toISOString() + await agent.dataStoreSaveMessage({ message: messageToQueue3 }) + + // Send MessagesRequest + const messagesRequestMessage: IDIDCommMessage = { + id: v4(), + type: MESSAGES_RECEIVED_MESSAGE_TYPE, + to: mediator.did, + from: recipient.did, + return_route: 'all', + body: { message_id_list: [messageToQueue2.id, messageToQueue3.id] }, + } + const packedMessage = await agent.packDIDCommMessage({ + packing: 'authcrypt', + message: messagesRequestMessage, + }) + await agent.sendDIDCommMessage({ + messageId: messagesRequestMessage.id, + packedMessage, + recipientDidUrl: mediator.did, + }) + + expect(DIDCommEventSniffer.onEvent).toHaveBeenCalledWith( + { + data: messageToQueue2.id, + type: 'DIDCommV2Message-forwardMessageDequeued', + }, + expect.anything(), + ) + expect(DIDCommEventSniffer.onEvent).toHaveBeenCalledWith( + { + data: messageToQueue3.id, + type: 'DIDCommV2Message-forwardMessageDequeued', + }, + expect.anything(), + ) + expect(DIDCommEventSniffer.onEvent).toHaveBeenCalledWith( + { + data: { + message: { + body: { message_count: 2, live_delivery: false }, + id: expect.anything(), + created_time: expect.anything(), + thid: messagesRequestMessage.id, + to: recipient.did, + from: mediator.did, + type: STATUS_MESSAGE_TYPE, + }, + metaData: { packing: 'authcrypt' }, + }, + type: 'DIDCommV2Message-received', + }, + expect.anything(), + ) + }) + + it('should not clear messages on MessagesReceived for another recipient', async () => { + // Save message + const messageToQueue2 = new Message({ raw: innerMessage.message }) + messageToQueue2.id = v4() + messageToQueue2.type = QUEUE_MESSAGE_TYPE + messageToQueue2.to = `${recipient.did}#${recipient.keys[0].kid}` + messageToQueue2.createdAt = new Date().toISOString() + await agent.dataStoreSaveMessage({ message: messageToQueue2 }) + + // Send MessagesRequest + const messagesRequestMessage: IDIDCommMessage = { + id: v4(), + type: MESSAGES_RECEIVED_MESSAGE_TYPE, + to: mediator.did, + from: recipient2.did, + return_route: 'all', + body: { message_id_list: [messageToQueue2.id] }, + } + const packedMessage = await agent.packDIDCommMessage({ + packing: 'authcrypt', + message: messagesRequestMessage, + }) + await agent.sendDIDCommMessage({ + messageId: messagesRequestMessage.id, + packedMessage, + recipientDidUrl: mediator.did, + }) + + expect(DIDCommEventSniffer.onEvent).not.toHaveBeenCalledWith( + { + data: messageToQueue2.id, + type: 'DIDCommV2Message-forwardMessageDequeued', + }, + expect.anything(), + ) + expect(await agent.dataStoreGetMessage({ id: messageToQueue2.id })).toBeDefined() + + // Clean up + 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/__tests__/routing-message-handler.test.ts b/packages/did-comm/src/__tests__/routing-message-handler.test.ts new file mode 100644 index 000000000..5f217b7db --- /dev/null +++ b/packages/did-comm/src/__tests__/routing-message-handler.test.ts @@ -0,0 +1,413 @@ +import { DIDComm } from '../didcomm' +import { + createAgent, + IDIDManager, + IEventListener, + IIdentifier, + IKeyManager, + IMessageHandler, + IResolver, + TAgent, +} from '../../../core/src' +import { DIDManager, MemoryDIDStore } from '../../../did-manager/src' +import { KeyManager, MemoryKeyStore, MemoryPrivateKeyStore } from '../../../key-manager/src' +import { KeyManagementSystem } from '../../../kms-local/src' +import { DIDResolverPlugin } from '../../../did-resolver/src' +import { Resolver } from 'did-resolver' +import { DIDCommHttpTransport } from '../transports/transports' +import { IDIDComm } from '../types/IDIDComm' +import { MessageHandler } from '../../../message-handler/src' +import { + CoordinateMediationMediatorMessageHandler, + CoordinateMediationRecipientMessageHandler, + createMediateRequestMessage, + MEDIATE_DENY_MESSAGE_TYPE, +} from '../protocols/coordinate-mediation-message-handler' +import { DIDCommMessageMediaType } from '../types/message-types' +import { + RoutingMessageHandler, + FORWARD_MESSAGE_TYPE, + QUEUE_MESSAGE_TYPE, +} from '../protocols/routing-message-handler' +import { FakeDidProvider, FakeDidResolver } from '../../../test-utils/src' +import { MessagingRouter, RequestWithAgentRouter } from '../../../remote-server/src' +import { Entities, IDataStore, migrations } from '../../../data-store/src' +import express from 'express' +import { Server } from 'http' +import { DIDCommMessageHandler } from '../message-handler' +import { DataStore, DataStoreORM } from '../../../data-store/src' +import { DataSource } from 'typeorm' +import { v4 } from 'uuid' +import { jest } from '@jest/globals' + +const DIDCommEventSniffer: IEventListener = { + eventTypes: ['DIDCommV2Message-sent', 'DIDCommV2Message-received', 'DIDCommV2Message-forwardMessageQueued'], + onEvent: jest.fn(() => Promise.resolve()), +} + +const databaseFile = `./tmp/local-database2-${Math.random().toPrecision(5)}.sqlite` + +describe('routing-message-handler', () => { + let recipient: IIdentifier + let mediator: IIdentifier + let agent: TAgent + let didCommEndpointServer: Server + let listeningPort = Math.round(Math.random() * 32000 + 2048) + let dbConnection: DataSource + + 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 CoordinateMediationMediatorMessageHandler(), + new CoordinateMediationRecipientMessageHandler(), + new RoutingMessageHandler(), + ], + }), + 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', + }) + + 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) + + 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 save forward message in queue for recipient', async () => { + expect.assertions(2) + + // 1. Coordinate mediation + const mediateRequestMessage = createMediateRequestMessage(recipient.did, mediator.did) + const packedMessage = await agent.packDIDCommMessage({ + packing: 'authcrypt', + message: mediateRequestMessage, + }) + await agent.sendDIDCommMessage({ + messageId: mediateRequestMessage.id, + packedMessage, + recipientDidUrl: mediator.did, + }) + + // 2. Forward message + const innerMessage = await agent.packDIDCommMessage({ + packing: 'authcrypt', + message: { + type: 'test', + to: recipient.did, + from: mediator.did, + id: 'test', + body: { hello: 'world' }, + }, + }) + const msgId = v4() + const packedForwardMessage = await agent.packDIDCommMessage({ + packing: 'anoncrypt', + message: { + type: FORWARD_MESSAGE_TYPE, + to: mediator.did, + id: msgId, + body: { + next: recipient.did, + }, + attachments: [{ media_type: DIDCommMessageMediaType.ENCRYPTED, data: { json: JSON.parse(innerMessage.message) } }], + }, + }) + await agent.sendDIDCommMessage({ + messageId: msgId, + packedMessage: packedForwardMessage, + recipientDidUrl: mediator.did, + }) + + expect(DIDCommEventSniffer.onEvent).toHaveBeenCalledWith( + { + data: { + message: { + body: { next: recipient.did }, + id: msgId, + to: mediator.did, + type: FORWARD_MESSAGE_TYPE, + attachments: [{ media_type: DIDCommMessageMediaType.ENCRYPTED, data: { json: JSON.parse(innerMessage.message) } }], + }, + metaData: { packing: 'anoncrypt' }, + }, + type: 'DIDCommV2Message-received', + }, + expect.anything(), + ) + + expect(DIDCommEventSniffer.onEvent).toHaveBeenCalledWith( + { + data: { + id: expect.anything(), + to: `${recipient.did}#${recipient.keys[0].kid}`, + type: QUEUE_MESSAGE_TYPE, + raw: innerMessage.message, + createdAt: expect.anything(), + metaData: [{ type: 'didCommForwardMsgId', value: msgId }], + }, + type: 'DIDCommV2Message-forwardMessageQueued', + }, + expect.anything(), + ) + }) + + it('should save forward message in queue for recipient previously denied', async () => { + expect.assertions(1) + + // 1. Coordinate mediation + const mediateRequestMessage = createMediateRequestMessage(recipient.did, mediator.did) + const packedMessage = await agent.packDIDCommMessage({ + packing: 'authcrypt', + message: mediateRequestMessage, + }) + await agent.sendDIDCommMessage({ + messageId: mediateRequestMessage.id, + packedMessage, + recipientDidUrl: mediator.did, + }) + + // 2. Save deny message + await agent.dataStoreSaveMessage({ + message: { + type: MEDIATE_DENY_MESSAGE_TYPE, + from: mediator.did, + to: recipient.did, + id: v4(), + createdAt: new Date().toISOString(), + data: {}, + }, + }) + + // 3. Request again + await agent.sendDIDCommMessage({ + messageId: mediateRequestMessage.id, + packedMessage, + recipientDidUrl: mediator.did, + }) + + // 4. Forward message + const innerMessage = await agent.packDIDCommMessage({ + packing: 'authcrypt', + message: { + type: 'test', + to: recipient.did, + from: mediator.did, + id: 'test', + body: { hello: 'world' }, + }, + }) + const msgId = v4() + const packedForwardMessage = await agent.packDIDCommMessage({ + packing: 'anoncrypt', + message: { + type: FORWARD_MESSAGE_TYPE, + to: mediator.did, + id: msgId, + body: { + next: recipient.did, + }, + attachments: [{ media_type: DIDCommMessageMediaType.ENCRYPTED, data: { json: JSON.parse(innerMessage.message) } }], + }, + }) + await agent.sendDIDCommMessage({ + messageId: msgId, + packedMessage: packedForwardMessage, + recipientDidUrl: mediator.did, + }) + + expect(DIDCommEventSniffer.onEvent).toHaveBeenCalledWith( + { + data: { + id: expect.anything(), + to: `${recipient.did}#${recipient.keys[0].kid}`, + type: QUEUE_MESSAGE_TYPE, + raw: innerMessage.message, + createdAt: expect.anything(), + metaData: [{ type: 'didCommForwardMsgId', value: msgId }], + }, + type: 'DIDCommV2Message-forwardMessageQueued', + }, + expect.anything(), + ) + }) + + it('should not save forward message in queue for recipient denied', async () => { + expect.assertions(1) + + // 1. Coordinate mediation + const mediateRequestMessage = createMediateRequestMessage(recipient.did, mediator.did) + const packedMessage = await agent.packDIDCommMessage({ + packing: 'authcrypt', + message: mediateRequestMessage, + }) + await agent.sendDIDCommMessage({ + messageId: mediateRequestMessage.id, + packedMessage, + recipientDidUrl: mediator.did, + }) + + // 2. Save deny message + await agent.dataStoreSaveMessage({ + message: { + type: MEDIATE_DENY_MESSAGE_TYPE, + from: mediator.did, + to: recipient.did, + id: v4(), + createdAt: new Date().toISOString(), + data: {}, + }, + }) + + // 3. Forward message + const innerMessage = await agent.packDIDCommMessage({ + packing: 'authcrypt', + message: { + type: 'test', + to: recipient.did, + from: mediator.did, + id: 'test', + body: { hello: 'world' }, + }, + }) + const msgId = v4() + const packedForwardMessage = await agent.packDIDCommMessage({ + packing: 'anoncrypt', + message: { + type: FORWARD_MESSAGE_TYPE, + to: mediator.did, + id: msgId, + body: { + next: recipient.did, + }, + attachments: [{ media_type: DIDCommMessageMediaType.ENCRYPTED, data: { json: JSON.parse(innerMessage.message) } }], + }, + }) + await agent.sendDIDCommMessage({ + messageId: msgId, + packedMessage: packedForwardMessage, + recipientDidUrl: mediator.did, + }) + + expect(DIDCommEventSniffer.onEvent).not.toHaveBeenCalledWith( + { + data: { + id: expect.anything(), + to: `${recipient.did}#${recipient.keys[0].kid}`, + type: QUEUE_MESSAGE_TYPE, + raw: innerMessage, + createdAt: expect.anything(), + metaData: [{ type: 'didCommForwardMsgId', value: msgId }], + }, + type: 'DIDCommV2Message-forwardMessageQueued', + }, + expect.anything(), + ) + }) +}) diff --git a/packages/did-comm/src/didcomm.ts b/packages/did-comm/src/didcomm.ts index 2a977f6d5..d260da4d5 100644 --- a/packages/did-comm/src/didcomm.ts +++ b/packages/did-comm/src/didcomm.ts @@ -21,7 +21,7 @@ import { Encrypter, verifyJWS, } from 'did-jwt' -import { DIDDocument, parse as parseDidUrl, ServiceEndpoint, VerificationMethod } from 'did-resolver' +import { DIDDocument, parse as parseDidUrl, ServiceEndpoint, VerificationMethod, Service } from 'did-resolver' import schema from "./plugin.schema.json" assert { type: 'json' } @@ -295,7 +295,9 @@ export class DIDComm implements IAgentPlugin { // 2.2 extract all recipient key agreement keys and normalize them const keyAgreementKeys: _NormalizedVerificationMethod[] = ( await dereferenceDidKeys(didDocument, 'keyAgreement', context) - ).filter((k) => k.publicKeyHex?.length! > 0) + ) + .filter((k) => k.publicKeyHex?.length! > 0) + .filter((k) => (args.options?.recipientKids ? args.options?.recipientKids.includes(k.id) : true)) if (keyAgreementKeys.length === 0) { throw new Error(`key_not_found: no key agreement keys found for recipient ${to}`) @@ -495,15 +497,60 @@ export class DIDComm implements IAgentPlugin { return { msgObj, mediaType } } - private findPreferredDIDCommService(services: any) { + private findPreferredDIDCommService(services: Service[]) { // FIXME: TODO: get preferred service endpoint according to configuration; now defaulting to first service return services[0] } + private async wrapDIDCommForwardMessage( + recipientDidUrl: string, + messageId: string, + packedMessageToForward: IPackedDIDCommMessage, + routingKey: string, + context: IAgentContext, + ): Promise { + const splitKey = routingKey.split('#') + const shouldUseSpecificKid = splitKey.length > 1 + const mediatorDidUrl = splitKey[0] + // 1. Create forward message + const forwardMessage: IDIDCommMessage = { + id: uuidv4(), + type: 'https://didcomm.org/routing/2.0/forward', + to: mediatorDidUrl, + body: { + next: recipientDidUrl, + }, + attachments: [ + { + media_type: DIDCommMessageMediaType.ENCRYPTED, + data: { + json: JSON.parse(packedMessageToForward.message), + }, + }, + ], + } + + context.agent.emit('DIDCommV2Message-forwarded', { + messageId, + next: recipientDidUrl, + routingKey: routingKey, + }) + + // 2. Pack message for routingKey with anoncrypt + if (shouldUseSpecificKid) { + return this.packDIDCommMessageJWE( + { message: forwardMessage, packing: 'anoncrypt', options: { recipientKids: [routingKey] } }, + context, + ) + } else { + return this.packDIDCommMessageJWE({ message: forwardMessage, packing: 'anoncrypt' }, context) + } + } + /** {@inheritdoc IDIDComm.sendDIDCommMessage} */ async sendDIDCommMessage( args: ISendDIDCommMessageArgs, - context: IAgentContext, + context: IAgentContext, ): Promise { const { packedMessage, returnTransportId, recipientDidUrl, messageId } = args @@ -534,7 +581,72 @@ export class DIDComm implements IAgentPlugin { ) } - // FIXME: TODO: wrap forward messages based on service entry + // serviceEndpoint can be a string, a ServiceEndpoint object, or an array of strings or ServiceEndpoint objects + let routingKeys: string[] = [] + let serviceEndpointUrl = '' + if (typeof service.serviceEndpoint === 'string') { + serviceEndpointUrl = service.serviceEndpoint + } else if ((service.serviceEndpoint as any).uri) { + serviceEndpointUrl = (service.serviceEndpoint as any).uri + } else if (Array.isArray(service.serviceEndpoint) && service.serviceEndpoint.length > 0) { + if (typeof service.serviceEndpoint[0] === 'string') { + serviceEndpointUrl = service.serviceEndpoint[0] + } else if (service.serviceEndpoint[0].uri) { + serviceEndpointUrl = service.serviceEndpoint[0].uri + } + } + + if (typeof service.serviceEndpoint !== 'string') { + if ( + Array.isArray(service.serviceEndpoint) && + service.serviceEndpoint.length > 0 && + service.serviceEndpoint[0].routingKeys + ) { + routingKeys = service.serviceEndpoint[0].routingKeys + } else if (service.serviceEndpoint.routingKeys) { + routingKeys = service.serviceEndpoint.routingKeys + } + } + + if (routingKeys.length > 0) { + // routingKeys found, wrap forward messages + let wrappedMessage: IPackedDIDCommMessage = packedMessage + for (let i = routingKeys.length - 1; i >= 0; i--) { + const recipient = i >= routingKeys.length - 1 ? recipientDidUrl : routingKeys[i + 1].split('#')[0] + wrappedMessage = await this.wrapDIDCommForwardMessage( + recipient, + messageId, + wrappedMessage, + routingKeys[i], + context, + ) + } + packedMessage.message = wrappedMessage.message + } + + // Check for DID as URI + let isServiceEndpointDid = false + try { + await resolveDidOrThrow(serviceEndpointUrl, context) + isServiceEndpointDid = true + } catch (e) {} + + if (isServiceEndpointDid) { + // Final wrapping and send to mediator DID + const recipient = + routingKeys.length > 0 ? routingKeys[routingKeys.length - 1].split('#')[0] : recipientDidUrl + const wrappedMessage = await this.wrapDIDCommForwardMessage( + recipient, + messageId, + packedMessage, + serviceEndpointUrl, + context, + ) + return this.sendDIDCommMessage( + { packedMessage: wrappedMessage, recipientDidUrl: serviceEndpointUrl, messageId }, + context, + ) + } const transports = this.transports.filter( (t) => t.isServiceSupported(service) && (!returnTransportId || t.id === returnTransportId), @@ -545,9 +657,10 @@ export class DIDComm implements IAgentPlugin { // TODO: better strategy for selecting the transport if multiple transports apply const transport = transports[0] - + + let response try { - const response = await transport.send(service, packedMessage.message) + response = await transport.send(service, packedMessage.message) if (response.error) { throw new Error( `Error when sending DIDComm message through transport with id: '${transport.id}': ${response.error}`, @@ -558,6 +671,13 @@ export class DIDComm implements IAgentPlugin { } context.agent.emit('DIDCommV2Message-sent', messageId) + + if (response.returnMessage) { + // Handle return message + await context.agent.handleMessage({ + raw: response.returnMessage, + }) + } return transport.id } diff --git a/packages/did-comm/src/message-handler.ts b/packages/did-comm/src/message-handler.ts index bbb9ee9b4..4c76975d2 100644 --- a/packages/did-comm/src/message-handler.ts +++ b/packages/did-comm/src/message-handler.ts @@ -108,6 +108,7 @@ export class DIDCommMessageHandler extends AbstractMessageHandler { expires_time: expiresAt, body: data, attachments, + return_route } = unpackedMessage.message message.type = type @@ -119,6 +120,7 @@ export class DIDCommMessageHandler extends AbstractMessageHandler { message.expiresAt = expiresAt message.data = data message.attachments = attachments + message.returnRoute = return_route message.addMetaData({ type: 'didCommMetaData', value: JSON.stringify(unpackedMessage.metaData) }) context.agent.emit('DIDCommV2Message-received', unpackedMessage) diff --git a/packages/did-comm/src/plugin.schema.json b/packages/did-comm/src/plugin.schema.json index 107af9a6f..664667ba5 100644 --- a/packages/did-comm/src/plugin.schema.json +++ b/packages/did-comm/src/plugin.schema.json @@ -84,6 +84,9 @@ "items": { "$ref": "#/components/schemas/IDIDCommMessageAttachment" } + }, + "return_route": { + "type": "string" } }, "required": [ @@ -168,6 +171,13 @@ "type": "string" }, "description": "Add extra recipients for the packed message." + }, + "recipientKids": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Restrict to a set of kids for recipient" } }, "description": "Extra options when packing a DIDComm message." @@ -340,6 +350,10 @@ "$ref": "#/components/schemas/IMessageAttachment" }, "description": "Optional. Array of generic attachments" + }, + "returnRoute": { + "type": "string", + "description": "Optional. Signal how to reuse transport for return messages" } }, "required": [ diff --git a/packages/did-comm/src/protocols/coordinate-mediation-message-handler.ts b/packages/did-comm/src/protocols/coordinate-mediation-message-handler.ts new file mode 100644 index 000000000..7c93bf117 --- /dev/null +++ b/packages/did-comm/src/protocols/coordinate-mediation-message-handler.ts @@ -0,0 +1,193 @@ +import { IAgentContext, IDIDManager, IKeyManager, IDataStore } from '@veramo/core-types' +import { AbstractMessageHandler, Message } from '@veramo/message-handler' +import Debug from 'debug' +import { v4 } from 'uuid' +import { IDIDComm } from '../types/IDIDComm.js' +import { IDIDCommMessage, DIDCommMessageMediaType } from '../types/message-types.js' + +const debug = Debug('veramo:did-comm:coordinate-mediation-message-handler') + +type IContext = IAgentContext + +export const MEDIATE_REQUEST_MESSAGE_TYPE = 'https://didcomm.org/coordinate-mediation/2.0/mediate-request' +export const MEDIATE_GRANT_MESSAGE_TYPE = 'https://didcomm.org/coordinate-mediation/2.0/mediate-grant' +export const MEDIATE_DENY_MESSAGE_TYPE = 'https://didcomm.org/coordinate-mediation/2.0/mediate-deny' + +export function createMediateRequestMessage( + recipientDidUrl: string, + mediatorDidUrl: string, +): IDIDCommMessage { + return { + type: MEDIATE_REQUEST_MESSAGE_TYPE, + from: recipientDidUrl, + to: mediatorDidUrl, + id: v4(), + return_route: 'all', + created_time: (new Date()).toISOString(), + body: {}, + } +} + +export function createMediateGrantMessage( + recipientDidUrl: string, + mediatorDidUrl: string, + thid: string, +): IDIDCommMessage { + return { + type: MEDIATE_GRANT_MESSAGE_TYPE, + from: mediatorDidUrl, + to: recipientDidUrl, + id: v4(), + thid: thid, + created_time: (new Date()).toISOString(), + body: { + routing_did: [mediatorDidUrl], + }, + } +} + +/** + * A plugin for the {@link @veramo/message-handler#MessageHandler} that handles Mediator Coordinator messages for the mediator role. + * @beta This API may change without a BREAKING CHANGE notice. + */ +export class CoordinateMediationMediatorMessageHandler extends AbstractMessageHandler { + constructor() { + super() + } + + /** + * Handles a Mediator Coordinator messages for the mediator role + * https://didcomm.org/mediator-coordination/2.0/ + */ + public async handle(message: Message, context: IContext): Promise { + if (message.type === MEDIATE_REQUEST_MESSAGE_TYPE) { + debug('MediateRequest Message Received') + try { + const { from, to, returnRoute } = message + if (!from) { + throw new Error('invalid_argument: MediateRequest received without `from` set') + } + if (!to) { + throw new Error('invalid_argument: MediateRequest received without `to` set') + } + if (returnRoute === 'all') { + // Grant requests to all recipients + // TODO: Come up with another method for approving and rejecting recipients + const response = createMediateGrantMessage(from, to, message.id) + const packedResponse = await context.agent.packDIDCommMessage({ + message: response, + packing: 'authcrypt', + }) + const returnResponse = { + id: response.id, + message: packedResponse.message, + contentType: DIDCommMessageMediaType.ENCRYPTED, + } + message.addMetaData({ type: 'ReturnRouteResponse', value: JSON.stringify(returnResponse) }) + + // Save message to track recipients + await context.agent.dataStoreSaveMessage({ + message: { + type: response.type, + from: response.from, + to: response.to, + id: response.id, + threadId: response.thid, + data: response.body, + createdAt: response.created_time + }, + }) + } + } catch (ex) { + debug(ex) + } + return message + } + + return super.handle(message, context) + } +} + +/** + * A plugin for the {@link @veramo/message-handler#MessageHandler} that handles Mediator Coordinator messages for the recipient role. + * @beta This API may change without a BREAKING CHANGE notice. + */ +export class CoordinateMediationRecipientMessageHandler extends AbstractMessageHandler { + constructor() { + super() + } + + /** + * Handles a Mediator Coordinator messages for the recipient role + * https://didcomm.org/mediator-coordination/2.0/ + */ + public async handle(message: Message, context: IContext): Promise { + if (message.type === MEDIATE_GRANT_MESSAGE_TYPE) { + debug('MediateGrant Message Received') + try { + const { from, to, data, threadId } = message + if (!from) { + throw new Error('invalid_argument: MediateGrant received without `from` set') + } + if (!to) { + throw new Error('invalid_argument: MediateGrant received without `to` set') + } + if (!threadId) { + throw new Error('invalid_argument: MediateGrant received without `thid` set') + } + if (!data.routing_did || data.routing_did.length === 0) { + throw new Error('invalid_argument: MediateGrant received with invalid routing_did') + } + // If mediate request was previously sent, add service to DID document + const prevRequestMsg = await context.agent.dataStoreGetMessage({ id: threadId }) + if (prevRequestMsg.from === to && prevRequestMsg.to === from) { + const service = { + id: 'didcomm-mediator', + type: 'DIDCommMessaging', + serviceEndpoint: [ + { + uri: data.routing_did[0], + }, + ], + } + await context.agent.didManagerAddService({ + did: to, + service: service, + }) + message.addMetaData({ type: 'DIDCommMessagingServiceAdded', value: JSON.stringify(service) }) + } + } catch (ex) { + debug(ex) + } + return message + } else if (message.type === MEDIATE_DENY_MESSAGE_TYPE) { + debug('MediateDeny Message Received') + try { + const { from, to } = message + if (!from) { + throw new Error('invalid_argument: MediateGrant received without `from` set') + } + if (!to) { + throw new Error('invalid_argument: MediateGrant received without `to` set') + } + + // Delete service if it exists + const did = await context.agent.didManagerGet({ + did: to, + }) + const existingService = did.services.find( + (s) => + s.serviceEndpoint === from || + (Array.isArray(s.serviceEndpoint) && s.serviceEndpoint.includes(from)), + ) + if (existingService) { + await context.agent.didManagerRemoveService({ did: to, id: existingService.id }) + } + } catch (ex) { + debug(ex) + } + } + + return super.handle(message, context) + } +} diff --git a/packages/did-comm/src/protocols/index.ts b/packages/did-comm/src/protocols/index.ts index 57316cfd4..3782ebbfa 100644 --- a/packages/did-comm/src/protocols/index.ts +++ b/packages/did-comm/src/protocols/index.ts @@ -1 +1,3 @@ -export { TrustPingMessageHandler } from './trust-ping-message-handler.js' \ No newline at end of file +export { TrustPingMessageHandler } from './trust-ping-message-handler.js' +export { CoordinateMediationMediatorMessageHandler, CoordinateMediationRecipientMessageHandler } from "./coordinate-mediation-message-handler.js" +export { RoutingMessageHandler } from "./routing-message-handler.js" \ No newline at end of file diff --git a/packages/did-comm/src/protocols/messagepickup-message-handler.ts b/packages/did-comm/src/protocols/messagepickup-message-handler.ts new file mode 100644 index 000000000..3ee732b9b --- /dev/null +++ b/packages/did-comm/src/protocols/messagepickup-message-handler.ts @@ -0,0 +1,286 @@ +import { + IAgentContext, + IDIDManager, + IKeyManager, + IDataStore, + IDataStoreORM, + IMessageHandler, + Where, + TMessageColumns, +} from '@veramo/core-types' +import { AbstractMessageHandler, Message } from '@veramo/message-handler' +import Debug from 'debug' +import { v4 } from 'uuid' +import { IDIDComm } from '../types/IDIDComm.js' +import { QUEUE_MESSAGE_TYPE } from './routing-message-handler.js' +import { IDIDCommMessage, DIDCommMessageMediaType, IDIDCommMessageAttachment } from '../types/message-types.js' +const debug = Debug('veramo:did-comm:messagepickup-message-handler') + +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' +export const DELIVERY_REQUEST_MESSAGE_TYPE = 'https://didcomm.org/messagepickup/3.0/delivery-request' +export const DELIVERY_MESSAGE_TYPE = 'https://didcomm.org/messagepickup/3.0/delivery' +export const MESSAGES_RECEIVED_MESSAGE_TYPE = 'https://didcomm.org/messagepickup/3.0/messages-received' + +function generateGetMessagesWhereQuery(from: string, recipientKey?: string): Where[] { + return [ + { + column: 'type', + value: [QUEUE_MESSAGE_TYPE], + op: 'In', + }, + recipientKey + ? { + column: 'to', + value: [recipientKey], + op: 'In', + } + : { + column: 'to', + value: [`${from}%`], + op: 'Like', + }, + ] +} + +/** + * 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 PickupMediatorMessageHandler extends AbstractMessageHandler { + constructor() { + super() + } + + /** + * Handles messages for Pickup protocol and mediator role + * https://didcomm.org/pickup/3.0/ + */ + public async handle(message: Message, context: IContext): Promise { + if (message.type === STATUS_REQUEST_MESSAGE_TYPE) { + debug('Status Request Message Received') + try { + await this.replyWithStatusMessage(message, context) + } catch (ex) { + debug(ex) + } + return message + } else if (message.type === DELIVERY_REQUEST_MESSAGE_TYPE) { + debug('Delivery Request Message Received') + try { + const { returnRoute, data, from, to } = message + + if (!to) { + throw new Error('invalid_argument: DeliveryRequest received without `to` set') + } + if (!from) { + throw new Error('invalid_argument: DeliveryRequest received without `from` set') + } + if (!data.limit || Number.isNaN(data.limit)) { + throw new Error('invalid_argument: DeliveryRequest received without `body.limit` set') + } + + if (returnRoute === 'all') { + const queuedMessages = await context.agent.dataStoreORMGetMessages({ + where: generateGetMessagesWhereQuery(from, data.recipient_key), + take: data.limit, + }) + + if (queuedMessages.length == 0) { + await this.replyWithStatusMessage(message, context) + return message + } + + const attachments: IDIDCommMessageAttachment[] = queuedMessages.map((message) => { + return { + id: message.id, + media_type: DIDCommMessageMediaType.ENCRYPTED, + data: { + json: JSON.parse(message.raw!), + }, + } + }) + const replyRecipientKey = data.recipient_key ? { recipient_key: data.recipient_key } : {} + const replyMessage: IDIDCommMessage = { + type: DELIVERY_MESSAGE_TYPE, + from: to, + to: from, + id: v4(), + thid: message.threadId ?? message.id, + created_time: new Date().toISOString(), + body: { + ...replyRecipientKey, + }, + attachments, + } + const packedResponse = await context.agent.packDIDCommMessage({ + message: replyMessage, + packing: 'authcrypt', + }) + const returnResponse = { + id: replyMessage.id, + message: packedResponse.message, + contentType: DIDCommMessageMediaType.ENCRYPTED, + } + message.addMetaData({ type: 'ReturnRouteResponse', value: JSON.stringify(returnResponse) }) + } else { + throw new Error('No return_route found for DeliveryRequest') + } + } catch (ex) { + debug(ex) + } + return message + } else if (message.type === MESSAGES_RECEIVED_MESSAGE_TYPE) { + debug('MessagesReceived Message Received') + try { + const { data, from } = message + + if (!from) { + throw new Error('invalid_argument: MessagesReceived received without `from` set') + } + if (!data.message_id_list || !Array.isArray(data.message_id_list)) { + throw new Error('invalid_argument: MessagesReceived received without `body.message_id_list` set') + } + + 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 }) + context.agent.emit('DIDCommV2Message-forwardMessageDequeued', messageId) + } + }), + ) + + await this.replyWithStatusMessage(message, context) + } catch (ex) { + debug(ex) + } + } + + return super.handle(message, context) + } + + private async replyWithStatusMessage(message: Message, context: IContext) { + const { returnRoute, data, from, to } = 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 (returnRoute === 'all') { + const queuedMessageCount = await context.agent.dataStoreORMGetMessagesCount({ + where: generateGetMessagesWhereQuery(from, data.recipient_key), + }) + + const replyRecipientKey = data.recipient_key ? { recipient_key: data.recipient_key } : {} + const replyMessage: IDIDCommMessage = { + type: STATUS_MESSAGE_TYPE, + from: to, + to: from, + id: v4(), + thid: message.threadId ?? message.id, + created_time: new Date().toISOString(), + body: { + message_count: queuedMessageCount, + live_delivery: false, + ...replyRecipientKey, + }, + } + const packedResponse = await context.agent.packDIDCommMessage({ + message: replyMessage, + packing: 'authcrypt', + }) + const returnResponse = { + id: replyMessage.id, + message: packedResponse.message, + contentType: DIDCommMessageMediaType.ENCRYPTED, + } + message.addMetaData({ type: 'ReturnRouteResponse', value: JSON.stringify(returnResponse) }) + } else { + throw new Error('No return_route found for StatusRequest') + } + } +} + +/** + * 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) + } +} diff --git a/packages/did-comm/src/protocols/routing-message-handler.ts b/packages/did-comm/src/protocols/routing-message-handler.ts new file mode 100644 index 000000000..298271f45 --- /dev/null +++ b/packages/did-comm/src/protocols/routing-message-handler.ts @@ -0,0 +1,87 @@ +import { IAgentContext, IDIDManager, IKeyManager, IDataStore, IDataStoreORM } from '@veramo/core-types' +import { AbstractMessageHandler, Message } from '@veramo/message-handler' +import Debug from 'debug' +import { v4 } from 'uuid' +import { IDIDComm } from '../types/IDIDComm.js' +import { MEDIATE_GRANT_MESSAGE_TYPE, MEDIATE_DENY_MESSAGE_TYPE } from './coordinate-mediation-message-handler.js' + +const debug = Debug('veramo:did-comm:routing-message-handler') + +type IContext = IAgentContext + +export const FORWARD_MESSAGE_TYPE = 'https://didcomm.org/routing/2.0/forward' +export const QUEUE_MESSAGE_TYPE = 'https://didcomm.org/routing/2.0/forward/queue-message' + +/** + * A plugin for the {@link @veramo/message-handler#MessageHandler} that handles forward messages for the Routing protocol. + * @beta This API may change without a BREAKING CHANGE notice. + */ +export class RoutingMessageHandler extends AbstractMessageHandler { + constructor() { + super() + } + + /** + * Handles forward messages for Routing protocol + * https://didcomm.org/routing/2.0/ + */ + public async handle(message: Message, context: IContext): Promise { + if (message.type === FORWARD_MESSAGE_TYPE) { + debug('Forward Message Received') + try { + const { attachments, data } = message + if (!attachments) { + throw new Error('invalid_argument: Forward received without `attachments` set') + } + if (!data.next) { + throw new Error('invalid_argument: Forward received without `body.next` set') + } + + if (attachments.length > 0) { + // Check if receiver has been granted mediation + const mediationResponses = await context.agent.dataStoreORMGetMessages({ + where: [ + { + column: 'type', + value: [MEDIATE_GRANT_MESSAGE_TYPE, MEDIATE_DENY_MESSAGE_TYPE], + op: 'In', + }, + { + column: 'to', + value: [data.next], + op: 'In', + }, + ], + order: [{ column: 'createdAt', direction: 'DESC' }], + }) + + // If last mediation response was a grant (not deny) + if (mediationResponses.length > 0 && mediationResponses[0].type === MEDIATE_GRANT_MESSAGE_TYPE) { + const recipients = attachments[0].data.json.recipients + for (let i = 0; i < recipients.length; i++) { + const recipient = recipients[i].header.kid + + // Save message for queue + const messageToQueue = new Message({ raw: JSON.stringify(attachments[0].data.json) }) + messageToQueue.id = v4() + messageToQueue.type = QUEUE_MESSAGE_TYPE + messageToQueue.to = recipient + messageToQueue.createdAt = new Date().toISOString() + messageToQueue.addMetaData({ type: 'didCommForwardMsgId', value: message.id }) + + await context.agent.dataStoreSaveMessage({ message: messageToQueue }) + context.agent.emit('DIDCommV2Message-forwardMessageQueued', messageToQueue) + } + } else { + debug('Forward received for DID without granting mediation') + } + } + } catch (ex) { + debug(ex) + } + return message + } + + return super.handle(message, context) + } +} diff --git a/packages/did-comm/src/transports/transports.ts b/packages/did-comm/src/transports/transports.ts index 2e47f77ed..8e88a80e7 100644 --- a/packages/did-comm/src/transports/transports.ts +++ b/packages/did-comm/src/transports/transports.ts @@ -9,6 +9,7 @@ import { v4 as uuidv4 } from 'uuid' export interface IDIDCommTransportResult { result?: string error?: string + returnMessage?: string } /** @@ -142,8 +143,15 @@ export class DIDCommHttpTransport extends AbstractDIDCommTransport { let result if (response.ok) { + let returnMessage + + // Check if response is a DIDComm message + if (response.headers.get('Content-Type')?.startsWith('application/didcomm')) { + returnMessage = await response.json() + } result = { result: 'successfully sent message: ' + response.statusText, + returnMessage: returnMessage, } } else { result = { diff --git a/packages/did-comm/src/types/message-types.ts b/packages/did-comm/src/types/message-types.ts index d840f4a98..7af35cdff 100644 --- a/packages/did-comm/src/types/message-types.ts +++ b/packages/did-comm/src/types/message-types.ts @@ -17,6 +17,7 @@ export interface IDIDCommMessage { from_prior?: string body: any attachments?: IDIDCommMessageAttachment[] + return_route?: string } /** @@ -29,6 +30,11 @@ export interface IDIDCommOptions { * Add extra recipients for the packed message. */ bcc?: string[] + + /** + * Restrict to a set of kids for recipient + */ + recipientKids?: string[] } /** diff --git a/packages/message-handler/src/message.ts b/packages/message-handler/src/message.ts index 4e06a802d..be8ebb4a9 100644 --- a/packages/message-handler/src/message.ts +++ b/packages/message-handler/src/message.ts @@ -46,6 +46,8 @@ export class Message implements IMessage { from?: string to?: string + + returnRoute?: string metaData?: IMetaData[] diff --git a/packages/remote-server/src/messaging-router.ts b/packages/remote-server/src/messaging-router.ts index 38621d2d3..bbf2e0d6e 100644 --- a/packages/remote-server/src/messaging-router.ts +++ b/packages/remote-server/src/messaging-router.ts @@ -44,7 +44,12 @@ export const MessagingRouter = (options: MessagingRouterOptions): Router => { save: typeof options.save === 'undefined' ? true : options.save, }) - if (message) { + const returnRouteResponse = message?.metaData?.find((v) => v.type === 'ReturnRouteResponse') + if (returnRouteResponse && returnRouteResponse.value) { + const returnMessage = JSON.parse(returnRouteResponse.value) + res.contentType(returnMessage.contentType).json(returnMessage.message) + req.agent?.emit('DIDCommV2Message-sent', returnMessage.id) + } else if (message) { res.json({ id: message.id }) } } catch (e: any) {