diff --git a/packages/core/package.json b/packages/core/package.json index 96ea4432ae..fdfb75f182 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -49,5 +49,8 @@ "rimraf": "~3.0.2", "tslog": "^3.2.0", "typescript": "~4.3.0" + }, + "resolutions": { + "@types/node": "^15.14.1" } } diff --git a/packages/core/src/agent/Agent.ts b/packages/core/src/agent/Agent.ts index d72010be6f..d34dd1fcd5 100644 --- a/packages/core/src/agent/Agent.ts +++ b/packages/core/src/agent/Agent.ts @@ -17,6 +17,7 @@ import { AriesFrameworkError } from '../error' import { BasicMessagesModule } from '../modules/basic-messages/BasicMessagesModule' import { ConnectionsModule } from '../modules/connections/ConnectionsModule' import { CredentialsModule } from '../modules/credentials/CredentialsModule' +import { DiscoverFeaturesModule } from '../modules/discover-features' import { LedgerModule } from '../modules/ledger/LedgerModule' import { ProofsModule } from '../modules/proofs/ProofsModule' import { MediatorModule } from '../modules/routing/MediatorModule' @@ -53,6 +54,7 @@ export class Agent { public readonly credentials!: CredentialsModule public readonly mediationRecipient!: RecipientModule public readonly mediator!: MediatorModule + public readonly discovery!: DiscoverFeaturesModule public constructor(initialConfig: InitConfig, dependencies: AgentDependencies) { // Create child container so we don't interfere with anything outside of this agent @@ -100,6 +102,7 @@ export class Agent { this.mediationRecipient = this.container.resolve(RecipientModule) this.basicMessages = this.container.resolve(BasicMessagesModule) this.ledger = this.container.resolve(LedgerModule) + this.discovery = this.container.resolve(DiscoverFeaturesModule) // Listen for new messages (either from transports or somewhere else in the framework / extensions) this.messageSubscription = this.eventEmitter diff --git a/packages/core/src/agent/Dispatcher.ts b/packages/core/src/agent/Dispatcher.ts index 5f9014c8a6..b4122c1322 100644 --- a/packages/core/src/agent/Dispatcher.ts +++ b/packages/core/src/agent/Dispatcher.ts @@ -1,4 +1,5 @@ import type { AgentMessage } from './AgentMessage' +import type { AgentMessageProcessedEvent } from './Events' import type { Handler } from './Handler' import type { InboundMessageContext } from './models/InboundMessageContext' @@ -6,18 +7,19 @@ import { Lifecycle, scoped } from 'tsyringe' import { AriesFrameworkError } from '../error/AriesFrameworkError' +import { EventEmitter } from './EventEmitter' +import { AgentEventTypes } from './Events' import { MessageSender } from './MessageSender' -import { TransportService } from './TransportService' @scoped(Lifecycle.ContainerScoped) class Dispatcher { private handlers: Handler[] = [] private messageSender: MessageSender - private transportService: TransportService + private eventEmitter: EventEmitter - public constructor(messageSender: MessageSender, transportService: TransportService) { + public constructor(messageSender: MessageSender, eventEmitter: EventEmitter) { this.messageSender = messageSender - this.transportService = transportService + this.eventEmitter = eventEmitter } public registerHandler(handler: Handler) { @@ -34,6 +36,15 @@ class Dispatcher { const outboundMessage = await handler.handle(messageContext) + // Emit event that allows to hook into received messages + this.eventEmitter.emit({ + type: AgentEventTypes.AgentMessageProcessed, + payload: { + message: messageContext.message, + connection: messageContext.connection, + }, + }) + if (outboundMessage) { await this.messageSender.sendMessage(outboundMessage) } @@ -54,6 +65,12 @@ class Dispatcher { } } } + + public get supportedMessageTypes() { + return this.handlers + .reduce((all, cur) => [...all, ...cur.supportedMessages], []) + .map((m) => m.type) + } } export { Dispatcher } diff --git a/packages/core/src/agent/Events.ts b/packages/core/src/agent/Events.ts index 2f5e37812a..cb700d57de 100644 --- a/packages/core/src/agent/Events.ts +++ b/packages/core/src/agent/Events.ts @@ -1,5 +1,9 @@ +import type { ConnectionRecord } from '../modules/connections' +import type { AgentMessage } from './AgentMessage' + export enum AgentEventTypes { AgentMessageReceived = 'AgentMessageReceived', + AgentMessageProcessed = 'AgentMessageProcessed', } export interface BaseEvent { @@ -13,3 +17,11 @@ export interface AgentMessageReceivedEvent extends BaseEvent { message: unknown } } + +export interface AgentMessageProcessedEvent extends BaseEvent { + type: typeof AgentEventTypes.AgentMessageProcessed + payload: { + message: AgentMessage + connection?: ConnectionRecord + } +} diff --git a/packages/core/src/modules/discover-features/DiscoverFeaturesModule.ts b/packages/core/src/modules/discover-features/DiscoverFeaturesModule.ts new file mode 100644 index 0000000000..1b498f9adc --- /dev/null +++ b/packages/core/src/modules/discover-features/DiscoverFeaturesModule.ts @@ -0,0 +1,42 @@ +import { Lifecycle, scoped } from 'tsyringe' + +import { Dispatcher } from '../../agent/Dispatcher' +import { MessageSender } from '../../agent/MessageSender' +import { createOutboundMessage } from '../../agent/helpers' +import { ConnectionService } from '../connections/services' + +import { DiscloseMessageHandler, QueryMessageHandler } from './handlers' +import { DiscoverFeaturesService } from './services' + +@scoped(Lifecycle.ContainerScoped) +export class DiscoverFeaturesModule { + private connectionService: ConnectionService + private messageSender: MessageSender + private discoverFeaturesService: DiscoverFeaturesService + + public constructor( + dispatcher: Dispatcher, + connectionService: ConnectionService, + messageSender: MessageSender, + discoverFeaturesService: DiscoverFeaturesService + ) { + this.connectionService = connectionService + this.messageSender = messageSender + this.discoverFeaturesService = discoverFeaturesService + this.registerHandlers(dispatcher) + } + + public async queryFeatures(connectionId: string, options: { query: string; comment?: string }) { + const connection = await this.connectionService.getById(connectionId) + + const queryMessage = await this.discoverFeaturesService.createQuery(options) + + const outbound = createOutboundMessage(connection, queryMessage) + await this.messageSender.sendMessage(outbound) + } + + private registerHandlers(dispatcher: Dispatcher) { + dispatcher.registerHandler(new DiscloseMessageHandler()) + dispatcher.registerHandler(new QueryMessageHandler(this.discoverFeaturesService)) + } +} diff --git a/packages/core/src/modules/discover-features/__tests__/DiscoverFeaturesService.test.ts b/packages/core/src/modules/discover-features/__tests__/DiscoverFeaturesService.test.ts new file mode 100644 index 0000000000..a26cbcceee --- /dev/null +++ b/packages/core/src/modules/discover-features/__tests__/DiscoverFeaturesService.test.ts @@ -0,0 +1,64 @@ +import type { Dispatcher } from '../../../agent/Dispatcher' + +import { DiscoverFeaturesQueryMessage } from '../messages' +import { DiscoverFeaturesService } from '../services/DiscoverFeaturesService' + +const supportedMessageTypes = [ + 'https://didcomm.org/connections/1.0/invitation', + 'https://didcomm.org/connections/1.0/request', + 'https://didcomm.org/connections/1.0/response', + 'https://didcomm.org/notification/1.0/ack', + 'https://didcomm.org/issue-credential/1.0/credential-proposal', +] + +describe('DiscoverFeaturesService', () => { + const discoverFeaturesService = new DiscoverFeaturesService({ supportedMessageTypes } as Dispatcher) + + describe('createDisclose', () => { + it('should return all protocols when query is *', async () => { + const queryMessage = new DiscoverFeaturesQueryMessage({ + query: '*', + }) + + const message = await discoverFeaturesService.createDisclose(queryMessage) + + expect(message.protocols.map((p) => p.protocolId)).toStrictEqual([ + 'https://didcomm.org/connections/1.0/', + 'https://didcomm.org/notification/1.0/', + 'https://didcomm.org/issue-credential/1.0/', + ]) + }) + + it('should return only one protocol if the query specifies a specific protocol', async () => { + const queryMessage = new DiscoverFeaturesQueryMessage({ + query: 'https://didcomm.org/connections/1.0/', + }) + + const message = await discoverFeaturesService.createDisclose(queryMessage) + + expect(message.protocols.map((p) => p.protocolId)).toStrictEqual(['https://didcomm.org/connections/1.0/']) + }) + + it('should respect a wild card at the end of the query', async () => { + const queryMessage = new DiscoverFeaturesQueryMessage({ + query: 'https://didcomm.org/connections/*', + }) + + const message = await discoverFeaturesService.createDisclose(queryMessage) + + expect(message.protocols.map((p) => p.protocolId)).toStrictEqual(['https://didcomm.org/connections/1.0/']) + }) + }) + + describe('createQuery', () => { + it('should return a query message with the query and comment', async () => { + const message = await discoverFeaturesService.createQuery({ + query: '*', + comment: 'Hello', + }) + + expect(message.query).toBe('*') + expect(message.comment).toBe('Hello') + }) + }) +}) diff --git a/packages/core/src/modules/discover-features/handlers/DiscloseMessageHandler.ts b/packages/core/src/modules/discover-features/handlers/DiscloseMessageHandler.ts new file mode 100644 index 0000000000..0ada211a9a --- /dev/null +++ b/packages/core/src/modules/discover-features/handlers/DiscloseMessageHandler.ts @@ -0,0 +1,13 @@ +import type { Handler, HandlerInboundMessage } from '../../../agent/Handler' + +import { DiscoverFeaturesDiscloseMessage } from '../messages' + +export class DiscloseMessageHandler implements Handler { + public supportedMessages = [DiscoverFeaturesDiscloseMessage] + + public async handle(inboundMessage: HandlerInboundMessage) { + // We don't really need to do anything with this at the moment + // The result can be hooked into through the generic message processed event + inboundMessage.assertReadyConnection() + } +} diff --git a/packages/core/src/modules/discover-features/handlers/QueryMessageHandler.ts b/packages/core/src/modules/discover-features/handlers/QueryMessageHandler.ts new file mode 100644 index 0000000000..7e15f01451 --- /dev/null +++ b/packages/core/src/modules/discover-features/handlers/QueryMessageHandler.ts @@ -0,0 +1,22 @@ +import type { Handler, HandlerInboundMessage } from '../../../agent/Handler' +import type { DiscoverFeaturesService } from '../services/DiscoverFeaturesService' + +import { createOutboundMessage } from '../../../agent/helpers' +import { DiscoverFeaturesQueryMessage } from '../messages' + +export class QueryMessageHandler implements Handler { + private discoverFeaturesService: DiscoverFeaturesService + public supportedMessages = [DiscoverFeaturesQueryMessage] + + public constructor(discoverFeaturesService: DiscoverFeaturesService) { + this.discoverFeaturesService = discoverFeaturesService + } + + public async handle(inboundMessage: HandlerInboundMessage) { + const connection = inboundMessage.assertReadyConnection() + + const discloseMessage = await this.discoverFeaturesService.createDisclose(inboundMessage.message) + + return createOutboundMessage(connection, discloseMessage) + } +} diff --git a/packages/core/src/modules/discover-features/handlers/index.ts b/packages/core/src/modules/discover-features/handlers/index.ts new file mode 100644 index 0000000000..6ae21a8989 --- /dev/null +++ b/packages/core/src/modules/discover-features/handlers/index.ts @@ -0,0 +1,2 @@ +export * from './DiscloseMessageHandler' +export * from './QueryMessageHandler' diff --git a/packages/core/src/modules/discover-features/index.ts b/packages/core/src/modules/discover-features/index.ts new file mode 100644 index 0000000000..b4bbee6b99 --- /dev/null +++ b/packages/core/src/modules/discover-features/index.ts @@ -0,0 +1,4 @@ +export * from './DiscoverFeaturesModule' +export * from './handlers' +export * from './messages' +export * from './services' diff --git a/packages/core/src/modules/discover-features/messages/DiscoverFeaturesDiscloseMessage.ts b/packages/core/src/modules/discover-features/messages/DiscoverFeaturesDiscloseMessage.ts new file mode 100644 index 0000000000..a097824cc0 --- /dev/null +++ b/packages/core/src/modules/discover-features/messages/DiscoverFeaturesDiscloseMessage.ts @@ -0,0 +1,54 @@ +import { Expose, Type } from 'class-transformer' +import { Equals, IsInstance, IsOptional, IsString } from 'class-validator' + +import { AgentMessage } from '../../../agent/AgentMessage' + +export interface DiscloseProtocolOptions { + protocolId: string + roles?: string[] +} + +export class DiscloseProtocol { + public constructor(options: DiscloseProtocolOptions) { + if (options) { + this.protocolId = options.protocolId + this.roles = options.roles + } + } + + @Expose({ name: 'pid' }) + @IsString() + public protocolId!: string + + @IsString({ each: true }) + @IsOptional() + public roles?: string[] +} + +export interface DiscoverFeaturesDiscloseMessageOptions { + id?: string + threadId: string + protocols: DiscloseProtocolOptions[] +} + +export class DiscoverFeaturesDiscloseMessage extends AgentMessage { + public constructor(options: DiscoverFeaturesDiscloseMessageOptions) { + super() + + if (options) { + this.id = options.id ?? this.generateId() + this.protocols = options.protocols.map((p) => new DiscloseProtocol(p)) + this.setThread({ + threadId: options.threadId, + }) + } + } + + @Equals(DiscoverFeaturesDiscloseMessage.type) + public readonly type = DiscoverFeaturesDiscloseMessage.type + public static readonly type = 'https://didcomm.org/discover-features/1.0/disclose' + + @IsInstance(DiscloseProtocol, { each: true }) + @Type(() => DiscloseProtocol) + public protocols!: DiscloseProtocol[] +} diff --git a/packages/core/src/modules/discover-features/messages/DiscoverFeaturesQueryMessage.ts b/packages/core/src/modules/discover-features/messages/DiscoverFeaturesQueryMessage.ts new file mode 100644 index 0000000000..cf0310654c --- /dev/null +++ b/packages/core/src/modules/discover-features/messages/DiscoverFeaturesQueryMessage.ts @@ -0,0 +1,32 @@ +import { Equals, IsOptional, IsString } from 'class-validator' + +import { AgentMessage } from '../../../agent/AgentMessage' + +export interface DiscoverFeaturesQueryMessageOptions { + id?: string + query: string + comment?: string +} + +export class DiscoverFeaturesQueryMessage extends AgentMessage { + public constructor(options: DiscoverFeaturesQueryMessageOptions) { + super() + + if (options) { + this.id = options.id ?? this.generateId() + this.query = options.query + this.comment = options.comment + } + } + + @Equals(DiscoverFeaturesQueryMessage.type) + public readonly type = DiscoverFeaturesQueryMessage.type + public static readonly type = 'https://didcomm.org/discover-features/1.0/query' + + @IsString() + public query!: string + + @IsString() + @IsOptional() + public comment?: string +} diff --git a/packages/core/src/modules/discover-features/messages/index.ts b/packages/core/src/modules/discover-features/messages/index.ts new file mode 100644 index 0000000000..6d64705e8e --- /dev/null +++ b/packages/core/src/modules/discover-features/messages/index.ts @@ -0,0 +1,2 @@ +export * from './DiscoverFeaturesDiscloseMessage' +export * from './DiscoverFeaturesQueryMessage' diff --git a/packages/core/src/modules/discover-features/services/DiscoverFeaturesService.ts b/packages/core/src/modules/discover-features/services/DiscoverFeaturesService.ts new file mode 100644 index 0000000000..ac284a9c37 --- /dev/null +++ b/packages/core/src/modules/discover-features/services/DiscoverFeaturesService.ts @@ -0,0 +1,43 @@ +import { Lifecycle, scoped } from 'tsyringe' + +import { Dispatcher } from '../../../agent/Dispatcher' +import { DiscoverFeaturesQueryMessage, DiscoverFeaturesDiscloseMessage } from '../messages' + +@scoped(Lifecycle.ContainerScoped) +export class DiscoverFeaturesService { + private dispatcher: Dispatcher + + public constructor(dispatcher: Dispatcher) { + this.dispatcher = dispatcher + } + + public async createQuery(options: { query: string; comment?: string }) { + const queryMessage = new DiscoverFeaturesQueryMessage(options) + + return queryMessage + } + + public async createDisclose(queryMessage: DiscoverFeaturesQueryMessage) { + const { query } = queryMessage + + const messageTypes = this.dispatcher.supportedMessageTypes + const messageFamilies = Array.from(new Set(messageTypes.map((m) => m.substring(0, m.lastIndexOf('/') + 1)))) + let protocols: string[] = [] + + if (query === '*') { + protocols = messageFamilies + } else if (query.endsWith('*')) { + const match = query.slice(0, -1) + protocols = messageFamilies.filter((m) => m.startsWith(match)) + } else if (messageFamilies.includes(query)) { + protocols = [query] + } + + const discloseMessage = new DiscoverFeaturesDiscloseMessage({ + threadId: queryMessage.threadId, + protocols: protocols.map((protocolId) => ({ protocolId })), + }) + + return discloseMessage + } +} diff --git a/packages/core/src/modules/discover-features/services/index.ts b/packages/core/src/modules/discover-features/services/index.ts new file mode 100644 index 0000000000..2a245ed256 --- /dev/null +++ b/packages/core/src/modules/discover-features/services/index.ts @@ -0,0 +1 @@ +export * from './DiscoverFeaturesService'