Skip to content

Commit

Permalink
feat(core): add discover features protocol (#390)
Browse files Browse the repository at this point in the history
  • Loading branch information
TimoGlastra authored Jul 20, 2021
1 parent 8e83c03 commit 3347424
Show file tree
Hide file tree
Showing 15 changed files with 318 additions and 4 deletions.
3 changes: 3 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,8 @@
"rimraf": "~3.0.2",
"tslog": "^3.2.0",
"typescript": "~4.3.0"
},
"resolutions": {
"@types/node": "^15.14.1"
}
}
3 changes: 3 additions & 0 deletions packages/core/src/agent/Agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
25 changes: 21 additions & 4 deletions packages/core/src/agent/Dispatcher.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
import type { AgentMessage } from './AgentMessage'
import type { AgentMessageProcessedEvent } from './Events'
import type { Handler } from './Handler'
import type { InboundMessageContext } from './models/InboundMessageContext'

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) {
Expand All @@ -34,6 +36,15 @@ class Dispatcher {

const outboundMessage = await handler.handle(messageContext)

// Emit event that allows to hook into received messages
this.eventEmitter.emit<AgentMessageProcessedEvent>({
type: AgentEventTypes.AgentMessageProcessed,
payload: {
message: messageContext.message,
connection: messageContext.connection,
},
})

if (outboundMessage) {
await this.messageSender.sendMessage(outboundMessage)
}
Expand All @@ -54,6 +65,12 @@ class Dispatcher {
}
}
}

public get supportedMessageTypes() {
return this.handlers
.reduce<typeof AgentMessage[]>((all, cur) => [...all, ...cur.supportedMessages], [])
.map((m) => m.type)
}
}

export { Dispatcher }
12 changes: 12 additions & 0 deletions packages/core/src/agent/Events.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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))
}
}
Original file line number Diff line number Diff line change
@@ -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')
})
})
})
Original file line number Diff line number Diff line change
@@ -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<DiscloseMessageHandler>) {
// 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()
}
}
Original file line number Diff line number Diff line change
@@ -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<QueryMessageHandler>) {
const connection = inboundMessage.assertReadyConnection()

const discloseMessage = await this.discoverFeaturesService.createDisclose(inboundMessage.message)

return createOutboundMessage(connection, discloseMessage)
}
}
2 changes: 2 additions & 0 deletions packages/core/src/modules/discover-features/handlers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './DiscloseMessageHandler'
export * from './QueryMessageHandler'
4 changes: 4 additions & 0 deletions packages/core/src/modules/discover-features/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './DiscoverFeaturesModule'
export * from './handlers'
export * from './messages'
export * from './services'
Original file line number Diff line number Diff line change
@@ -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[]
}
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 2 additions & 0 deletions packages/core/src/modules/discover-features/messages/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './DiscoverFeaturesDiscloseMessage'
export * from './DiscoverFeaturesQueryMessage'
Loading

0 comments on commit 3347424

Please sign in to comment.