Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): add discover features protocol #390

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@types/node v16 was released recently which causes errors with some the types. I'm not sure if it is an error in the types (the error seems weird), so for now we can stick to types of node 15

}
}
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>({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really like this addition

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)
}
}
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
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './DiscoverFeaturesDiscloseMessage'
export * from './DiscoverFeaturesQueryMessage'
Loading