From 2b6441a2de5e9940bdf225b1ad9028cdfbf15cd5 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Mon, 4 Apr 2022 18:10:17 -0300 Subject: [PATCH] feat: extension module creation (#688) Signed-off-by: Ariel Gentile --- package.json | 3 +- packages/core/package.json | 4 +- packages/core/src/index.ts | 7 + samples/extension-module/README.md | 80 ++++++++ samples/extension-module/dummy/DummyModule.ts | 80 ++++++++ .../dummy/handlers/DummyRequestHandler.ts | 19 ++ .../dummy/handlers/DummyResponseHandler.ts | 19 ++ .../extension-module/dummy/handlers/index.ts | 2 + samples/extension-module/dummy/index.ts | 5 + .../dummy/messages/DummyRequestMessage.ts | 20 ++ .../dummy/messages/DummyResponseMessage.ts | 24 +++ .../extension-module/dummy/messages/index.ts | 2 + .../dummy/repository/DummyRecord.ts | 51 +++++ .../dummy/repository/DummyRepository.ts | 11 ++ .../dummy/repository/DummyState.ts | 7 + .../dummy/repository/index.ts | 3 + .../dummy/services/DummyEvents.ts | 15 ++ .../dummy/services/DummyService.ts | 176 ++++++++++++++++++ .../extension-module/dummy/services/index.ts | 2 + samples/extension-module/package.json | 29 +++ samples/extension-module/requester.ts | 67 +++++++ samples/extension-module/responder.ts | 71 +++++++ samples/extension-module/tsconfig.json | 6 + yarn.lock | 4 +- 24 files changed, 702 insertions(+), 5 deletions(-) create mode 100644 samples/extension-module/README.md create mode 100644 samples/extension-module/dummy/DummyModule.ts create mode 100644 samples/extension-module/dummy/handlers/DummyRequestHandler.ts create mode 100644 samples/extension-module/dummy/handlers/DummyResponseHandler.ts create mode 100644 samples/extension-module/dummy/handlers/index.ts create mode 100644 samples/extension-module/dummy/index.ts create mode 100644 samples/extension-module/dummy/messages/DummyRequestMessage.ts create mode 100644 samples/extension-module/dummy/messages/DummyResponseMessage.ts create mode 100644 samples/extension-module/dummy/messages/index.ts create mode 100644 samples/extension-module/dummy/repository/DummyRecord.ts create mode 100644 samples/extension-module/dummy/repository/DummyRepository.ts create mode 100644 samples/extension-module/dummy/repository/DummyState.ts create mode 100644 samples/extension-module/dummy/repository/index.ts create mode 100644 samples/extension-module/dummy/services/DummyEvents.ts create mode 100644 samples/extension-module/dummy/services/DummyService.ts create mode 100644 samples/extension-module/dummy/services/index.ts create mode 100644 samples/extension-module/package.json create mode 100644 samples/extension-module/requester.ts create mode 100644 samples/extension-module/responder.ts create mode 100644 samples/extension-module/tsconfig.json diff --git a/package.json b/package.json index 4f8c4c6b93..459785c109 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "license": "Apache-2.0", "workspaces": [ "packages/*", - "demo" + "demo", + "samples/*" ], "repository": { "url": "https://github.com/hyperledger/aries-framework-javascript", diff --git a/packages/core/package.json b/packages/core/package.json index 204f4046be..7dc29699c6 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -28,7 +28,7 @@ "@stablelib/sha256": "^1.0.1", "@types/indy-sdk": "^1.16.16", "@types/node-fetch": "^2.5.10", - "@types/ws": "^7.4.4", + "@types/ws": "^7.4.6", "abort-controller": "^3.0.0", "bn.js": "^5.2.0", "borc": "^3.0.0", @@ -42,7 +42,7 @@ "object-inspect": "^1.10.3", "query-string": "^7.0.1", "reflect-metadata": "^0.1.13", - "rxjs": "^7.1.0", + "rxjs": "^7.2.0", "tsyringe": "^4.5.0", "uuid": "^8.3.2", "varint": "^6.0.0", diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 5c2a0cb75f..9f3979de9e 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -2,6 +2,10 @@ import 'reflect-metadata' export { Agent } from './agent/Agent' +export { BaseEvent } from './agent/Events' +export { EventEmitter } from './agent/EventEmitter' +export { Handler, HandlerInboundMessage } from './agent/Handler' +export { InboundMessageContext } from './agent/models/InboundMessageContext' export { AgentConfig } from './agent/AgentConfig' export { AgentMessage } from './agent/AgentMessage' export { Dispatcher } from './agent/Dispatcher' @@ -10,7 +14,10 @@ export type { AgentDependencies } from './agent/AgentDependencies' export type { InitConfig, OutboundPackage, EncryptedMessage } from './types' export { DidCommMimeType } from './types' export type { FileSystem } from './storage/FileSystem' +export { BaseRecord } from './storage/BaseRecord' export { InMemoryMessageRepository } from './storage/InMemoryMessageRepository' +export { Repository } from './storage/Repository' +export { StorageService } from './storage/StorageService' export { getDirFromFilePath } from './utils/path' export { InjectionSymbols } from './constants' export type { Wallet } from './wallet/Wallet' diff --git a/samples/extension-module/README.md b/samples/extension-module/README.md new file mode 100644 index 0000000000..dcb70074a3 --- /dev/null +++ b/samples/extension-module/README.md @@ -0,0 +1,80 @@ +

Extension module example

+ +This example shows how can an extension module be written and injected to an Aries Framework Javascript `Agent` instance. Its structure is similar to the one of regular modules, although is not strictly needed to follow it to achieve this goal. + +An extension module could be used for different purposes, such as storing data in an Identity Wallet, supporting custom protocols over Didcomm or implementing new [Aries RFCs](https://github.com/hyperledger/aries-rfcs/tree/main/features) without the need of embed them right into AFJ's Core package. Injected modules can access to other core modules and services and trigger events, so in practice they work much in the same way as if they were included statically. + +## Dummy module + +This example consists of a module that implements a very simple request-response protocol called Dummy. In order to do so and be able to be injected into an AFJ instance, some steps were followed: + +- Define Dummy protocol message classes (inherited from `AgentMessage`) +- Create handlers for those messages (inherited from `Handler`) +- Define records (inherited from `BaseRecord`) and a container-scoped repository (inherited from `Repository`) for state persistance +- Define events (inherited from `BaseEvent`) +- Create a container-scoped service class that manages records and repository, and also trigger events using Agent's `EventEmitter` +- Create a container-scoped module class that registers handlers in Agent's `Dispatcher` and provides a simple API to do requests and responses, with the aid of service classes and Agent's `MessageSender` + +## Usage + +In order to use this module, it must be injected into an AFJ instance. This can be done by resolving DummyModule right after agent is instantiated: + +```ts +import { DummyModule } from './dummy' + +const agent = new Agent(/** agent config... */) + +const dummyModule = agent.injectionContainer.resolve(DummyModule) + +await agent.initialize() +``` + +Then, Dummy module API methods can be called, and events listeners can be created: + +```ts +agent.events.on(DummyEventTypes.StateChanged, async (event: DummyStateChangedEvent) => { + if (event.payload.dummyRecord.state === DummyState.RequestReceived) { + await dummyModule.respond(event.payload.dummyRecord) + } +}) + +const record = await dummyModule.request(connection) +``` + +## Run demo + +This repository includes a demonstration of a requester and a responder controller using this module to exchange Dummy protocol messages. For environment set up, make sure you followed instructions for [NodeJS](/docs/setup-nodejs.md). + +These are the steps for running it: + +Clone the AFJ git repository: + +```sh +git clone https://github.com/hyperledger/aries-framework-javascript.git +``` + +Open two different terminals and go to the extension-module directory: + +```sh +cd aries-framework-javascript/samples/extension-module +``` + +Install the project in one of the terminals: + +```sh +yarn install +``` + +In that terminal run the responder: + +```sh +yarn responder +``` + +Wait for it to finish the startup process (i.e. logger showing 'Responder listening to port ...') and run requester in another terminal: + +```sh +yarn requester +``` + +If everything goes right, requester will connect to responder and, as soon as connection protocol is finished, it will send a Dummy request. Responder will answer with a Dummy response and requester will happily exit. diff --git a/samples/extension-module/dummy/DummyModule.ts b/samples/extension-module/dummy/DummyModule.ts new file mode 100644 index 0000000000..f569af05ec --- /dev/null +++ b/samples/extension-module/dummy/DummyModule.ts @@ -0,0 +1,80 @@ +import type { DummyRecord } from './repository/DummyRecord' +import type { ConnectionRecord } from '@aries-framework/core' + +import { ConnectionService, Dispatcher, MessageSender } from '@aries-framework/core' +import { Lifecycle, scoped } from 'tsyringe' + +import { DummyRequestHandler, DummyResponseHandler } from './handlers' +import { DummyState } from './repository' +import { DummyService } from './services' + +@scoped(Lifecycle.ContainerScoped) +export class DummyModule { + private messageSender: MessageSender + private dummyService: DummyService + private connectionService: ConnectionService + + public constructor( + dispatcher: Dispatcher, + messageSender: MessageSender, + dummyService: DummyService, + connectionService: ConnectionService + ) { + this.messageSender = messageSender + this.dummyService = dummyService + this.connectionService = connectionService + this.registerHandlers(dispatcher) + } + + /** + * Send a Dummy Request + * + * @param connection record of the target responder (must be active) + * @returns created Dummy Record + */ + public async request(connection: ConnectionRecord) { + const { record, message: payload } = await this.dummyService.createRequest(connection) + + await this.messageSender.sendMessage({ connection, payload }) + + await this.dummyService.updateState(record, DummyState.RequestSent) + + return record + } + + /** + * Respond a Dummy Request + * + * @param record Dummy record + * @returns Updated dummy record + */ + public async respond(record: DummyRecord) { + if (!record.connectionId) { + throw new Error('Connection not found!') + } + + const connection = await this.connectionService.getById(record.connectionId) + + const payload = await this.dummyService.createResponse(record) + + await this.messageSender.sendMessage({ connection, payload }) + + await this.dummyService.updateState(record, DummyState.ResponseSent) + + return record + } + + /** + * Retrieve all dummy records + * + * @returns List containing all records + */ + public getAll(): Promise { + return this.dummyService.getAll() + } + + private registerHandlers(dispatcher: Dispatcher) { + dispatcher.registerHandler(new DummyRequestHandler(this.dummyService)) + dispatcher.registerHandler(new DummyResponseHandler(this.dummyService)) + } +} diff --git a/samples/extension-module/dummy/handlers/DummyRequestHandler.ts b/samples/extension-module/dummy/handlers/DummyRequestHandler.ts new file mode 100644 index 0000000000..c5b1e047e6 --- /dev/null +++ b/samples/extension-module/dummy/handlers/DummyRequestHandler.ts @@ -0,0 +1,19 @@ +import type { DummyService } from '../services' +import type { Handler, HandlerInboundMessage } from '@aries-framework/core' + +import { DummyRequestMessage } from '../messages' + +export class DummyRequestHandler implements Handler { + public supportedMessages = [DummyRequestMessage] + private dummyService: DummyService + + public constructor(dummyService: DummyService) { + this.dummyService = dummyService + } + + public async handle(inboundMessage: HandlerInboundMessage) { + inboundMessage.assertReadyConnection() + + await this.dummyService.processRequest(inboundMessage) + } +} diff --git a/samples/extension-module/dummy/handlers/DummyResponseHandler.ts b/samples/extension-module/dummy/handlers/DummyResponseHandler.ts new file mode 100644 index 0000000000..faca594166 --- /dev/null +++ b/samples/extension-module/dummy/handlers/DummyResponseHandler.ts @@ -0,0 +1,19 @@ +import type { DummyService } from '../services' +import type { Handler, HandlerInboundMessage } from '@aries-framework/core' + +import { DummyResponseMessage } from '../messages' + +export class DummyResponseHandler implements Handler { + public supportedMessages = [DummyResponseMessage] + private dummyService: DummyService + + public constructor(dummyService: DummyService) { + this.dummyService = dummyService + } + + public async handle(inboundMessage: HandlerInboundMessage) { + inboundMessage.assertReadyConnection() + + await this.dummyService.processResponse(inboundMessage) + } +} diff --git a/samples/extension-module/dummy/handlers/index.ts b/samples/extension-module/dummy/handlers/index.ts new file mode 100644 index 0000000000..1aacc16089 --- /dev/null +++ b/samples/extension-module/dummy/handlers/index.ts @@ -0,0 +1,2 @@ +export * from './DummyRequestHandler' +export * from './DummyResponseHandler' diff --git a/samples/extension-module/dummy/index.ts b/samples/extension-module/dummy/index.ts new file mode 100644 index 0000000000..2ca47f690a --- /dev/null +++ b/samples/extension-module/dummy/index.ts @@ -0,0 +1,5 @@ +export * from './DummyModule' +export * from './handlers' +export * from './messages' +export * from './services' +export * from './repository' diff --git a/samples/extension-module/dummy/messages/DummyRequestMessage.ts b/samples/extension-module/dummy/messages/DummyRequestMessage.ts new file mode 100644 index 0000000000..12902f0504 --- /dev/null +++ b/samples/extension-module/dummy/messages/DummyRequestMessage.ts @@ -0,0 +1,20 @@ +import { AgentMessage } from '@aries-framework/core' +import { Equals } from 'class-validator' + +export interface DummyRequestMessageOptions { + id?: string +} + +export class DummyRequestMessage extends AgentMessage { + public constructor(options: DummyRequestMessageOptions) { + super() + + if (options) { + this.id = options.id ?? this.generateId() + } + } + + @Equals(DummyRequestMessage.type) + public readonly type = DummyRequestMessage.type + public static readonly type = 'https://didcomm.org/dummy/1.0/request' +} diff --git a/samples/extension-module/dummy/messages/DummyResponseMessage.ts b/samples/extension-module/dummy/messages/DummyResponseMessage.ts new file mode 100644 index 0000000000..9cdb931bd7 --- /dev/null +++ b/samples/extension-module/dummy/messages/DummyResponseMessage.ts @@ -0,0 +1,24 @@ +import { AgentMessage } from '@aries-framework/core' +import { Equals } from 'class-validator' + +export interface DummyResponseMessageOptions { + id?: string + threadId: string +} + +export class DummyResponseMessage extends AgentMessage { + public constructor(options: DummyResponseMessageOptions) { + super() + + if (options) { + this.id = options.id ?? this.generateId() + this.setThread({ + threadId: options.threadId, + }) + } + } + + @Equals(DummyResponseMessage.type) + public readonly type = DummyResponseMessage.type + public static readonly type = 'https://2060.io/didcomm/dummy/response' +} diff --git a/samples/extension-module/dummy/messages/index.ts b/samples/extension-module/dummy/messages/index.ts new file mode 100644 index 0000000000..7b11bafe4f --- /dev/null +++ b/samples/extension-module/dummy/messages/index.ts @@ -0,0 +1,2 @@ +export * from './DummyRequestMessage' +export * from './DummyResponseMessage' diff --git a/samples/extension-module/dummy/repository/DummyRecord.ts b/samples/extension-module/dummy/repository/DummyRecord.ts new file mode 100644 index 0000000000..c321e5941a --- /dev/null +++ b/samples/extension-module/dummy/repository/DummyRecord.ts @@ -0,0 +1,51 @@ +import type { DummyState } from './DummyState' + +import { BaseRecord } from '@aries-framework/core' +import { v4 as uuid } from 'uuid' + +export interface DummyStorageProps { + id?: string + createdAt?: Date + connectionId?: string + threadId: string + state: DummyState +} + +export class DummyRecord extends BaseRecord implements DummyStorageProps { + public connectionId?: string + public threadId!: string + public state!: DummyState + + public static readonly type = 'DummyRecord' + public readonly type = DummyRecord.type + + public constructor(props: DummyStorageProps) { + super() + if (props) { + this.id = props.id ?? uuid() + this.createdAt = props.createdAt ?? new Date() + this.state = props.state + this.connectionId = props.connectionId + this.threadId = props.threadId + } + } + + public getTags() { + return { + ...this._tags, + threadId: this.threadId, + connectionId: this.connectionId, + state: this.state, + } + } + + public assertState(expectedStates: DummyState | DummyState[]) { + if (!Array.isArray(expectedStates)) { + expectedStates = [expectedStates] + } + + if (!expectedStates.includes(this.state)) { + throw new Error(`Dummy record is in invalid state ${this.state}. Valid states are: ${expectedStates.join(', ')}.`) + } + } +} diff --git a/samples/extension-module/dummy/repository/DummyRepository.ts b/samples/extension-module/dummy/repository/DummyRepository.ts new file mode 100644 index 0000000000..f9384e0cfc --- /dev/null +++ b/samples/extension-module/dummy/repository/DummyRepository.ts @@ -0,0 +1,11 @@ +import { Repository, StorageService, InjectionSymbols } from '@aries-framework/core' +import { inject, scoped, Lifecycle } from 'tsyringe' + +import { DummyRecord } from './DummyRecord' + +@scoped(Lifecycle.ContainerScoped) +export class DummyRepository extends Repository { + public constructor(@inject(InjectionSymbols.StorageService) storageService: StorageService) { + super(DummyRecord, storageService) + } +} diff --git a/samples/extension-module/dummy/repository/DummyState.ts b/samples/extension-module/dummy/repository/DummyState.ts new file mode 100644 index 0000000000..c5f8f411b1 --- /dev/null +++ b/samples/extension-module/dummy/repository/DummyState.ts @@ -0,0 +1,7 @@ +export enum DummyState { + Init = 'init', + RequestSent = 'request-sent', + RequestReceived = 'request-received', + ResponseSent = 'response-sent', + ResponseReceived = 'response-received', +} diff --git a/samples/extension-module/dummy/repository/index.ts b/samples/extension-module/dummy/repository/index.ts new file mode 100644 index 0000000000..38d0353bd5 --- /dev/null +++ b/samples/extension-module/dummy/repository/index.ts @@ -0,0 +1,3 @@ +export * from './DummyRecord' +export * from './DummyRepository' +export * from './DummyState' diff --git a/samples/extension-module/dummy/services/DummyEvents.ts b/samples/extension-module/dummy/services/DummyEvents.ts new file mode 100644 index 0000000000..981630e0df --- /dev/null +++ b/samples/extension-module/dummy/services/DummyEvents.ts @@ -0,0 +1,15 @@ +import type { DummyRecord } from '../repository/DummyRecord' +import type { DummyState } from '../repository/DummyState' +import type { BaseEvent } from '@aries-framework/core' + +export enum DummyEventTypes { + StateChanged = 'DummyStateChanged', +} + +export interface DummyStateChangedEvent extends BaseEvent { + type: DummyEventTypes.StateChanged + payload: { + dummyRecord: DummyRecord + previousState: DummyState | null + } +} diff --git a/samples/extension-module/dummy/services/DummyService.ts b/samples/extension-module/dummy/services/DummyService.ts new file mode 100644 index 0000000000..d0c3635d33 --- /dev/null +++ b/samples/extension-module/dummy/services/DummyService.ts @@ -0,0 +1,176 @@ +import type { DummyStateChangedEvent } from './DummyEvents' +import type { ConnectionRecord, InboundMessageContext } from '@aries-framework/core' + +import { EventEmitter } from '@aries-framework/core' +import { Lifecycle, scoped } from 'tsyringe' + +import { DummyRequestMessage, DummyResponseMessage } from '../messages' +import { DummyRecord } from '../repository/DummyRecord' +import { DummyRepository } from '../repository/DummyRepository' +import { DummyState } from '../repository/DummyState' + +import { DummyEventTypes } from './DummyEvents' + +@scoped(Lifecycle.ContainerScoped) +export class DummyService { + private dummyRepository: DummyRepository + private eventEmitter: EventEmitter + + public constructor(dummyRepository: DummyRepository, eventEmitter: EventEmitter) { + this.dummyRepository = dummyRepository + this.eventEmitter = eventEmitter + } + + /** + * Create a {@link DummyRequestMessage}. + * + * @param connectionRecord The connection for which to create the dummy request + * @returns Object containing dummy request message and associated dummy record + * + */ + public async createRequest(connectionRecord: ConnectionRecord) { + // Create message + const message = new DummyRequestMessage({}) + + // Create record + const record = new DummyRecord({ + connectionId: connectionRecord.id, + threadId: message.id, + state: DummyState.Init, + }) + + await this.dummyRepository.save(record) + + this.eventEmitter.emit({ + type: DummyEventTypes.StateChanged, + payload: { + dummyRecord: record, + previousState: null, + }, + }) + + return { record, message } + } + + /** + * Create a dummy response message for the specified dummy record. + * + * @param record the dummy record for which to create a dummy response + * @returns outbound message containing dummy response + */ + public async createResponse(record: DummyRecord) { + const responseMessage = new DummyResponseMessage({ + threadId: record.threadId, + }) + + return responseMessage + } + + /** + * Process a received {@link DummyRequestMessage}. + * + * @param messageContext The message context containing a dummy request message + * @returns dummy record associated with the dummy request message + * + */ + public async processRequest(messageContext: InboundMessageContext) { + const connectionRecord = messageContext.connection + + // Create record + const record = new DummyRecord({ + connectionId: connectionRecord?.id, + threadId: messageContext.message.id, + state: DummyState.RequestReceived, + }) + + await this.dummyRepository.save(record) + + this.eventEmitter.emit({ + type: DummyEventTypes.StateChanged, + payload: { + dummyRecord: record, + previousState: null, + }, + }) + + return record + } + + /** + * Process a received {@link DummyResponseMessage}. + * + * @param messageContext The message context containing a dummy response message + * @returns dummy record associated with the dummy response message + * + */ + public async processResponse(messageContext: InboundMessageContext) { + const { connection, message } = messageContext + + // Dummy record already exists + const record = await this.findByThreadAndConnectionId(message.threadId, connection?.id) + + if (record) { + // Check current state + record.assertState(DummyState.RequestSent) + + await this.updateState(record, DummyState.ResponseReceived) + } else { + throw new Error(`Dummy record not found with threadId ${message.threadId}`) + } + + return record + } + + /** + * Retrieve all dummy records + * + * @returns List containing all dummy records + */ + public getAll(): Promise { + return this.dummyRepository.getAll() + } + + /** + * Retrieve a dummy record by id + * + * @param dummyRecordId The dummy record id + * @throws {RecordNotFoundError} If no record is found + * @return The dummy record + * + */ + public getById(dummyRecordId: string): Promise { + return this.dummyRepository.getById(dummyRecordId) + } + + /** + * Retrieve a dummy record by connection id and thread id + * + * @param connectionId The connection id + * @param threadId The thread id + * @throws {RecordNotFoundError} If no record is found + * @throws {RecordDuplicateError} If multiple records are found + * @returns The dummy record + */ + public async findByThreadAndConnectionId(threadId: string, connectionId?: string): Promise { + return this.dummyRepository.findSingleByQuery({ threadId, connectionId }) + } + + /** + * Update the record to a new state and emit an state changed event. Also updates the record + * in storage. + * + * @param dummyRecord The record to update the state for + * @param newState The state to update to + * + */ + public async updateState(dummyRecord: DummyRecord, newState: DummyState) { + const previousState = dummyRecord.state + dummyRecord.state = newState + await this.dummyRepository.update(dummyRecord) + + this.eventEmitter.emit({ + type: DummyEventTypes.StateChanged, + payload: { dummyRecord, previousState: previousState }, + }) + } +} diff --git a/samples/extension-module/dummy/services/index.ts b/samples/extension-module/dummy/services/index.ts new file mode 100644 index 0000000000..05bcbc5d0a --- /dev/null +++ b/samples/extension-module/dummy/services/index.ts @@ -0,0 +1,2 @@ +export * from './DummyService' +export * from './DummyEvents' diff --git a/samples/extension-module/package.json b/samples/extension-module/package.json new file mode 100644 index 0000000000..ae446d5838 --- /dev/null +++ b/samples/extension-module/package.json @@ -0,0 +1,29 @@ +{ + "name": "afj-extension-module-sample", + "version": "1.0.0", + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/hyperledger/aries-framework-javascript", + "directory": "samples/extension-module/" + }, + "license": "Apache-2.0", + "scripts": { + "requester": "ts-node requester.ts", + "responder": "ts-node responder.ts" + }, + "devDependencies": { + "@aries-framework/core": "^0.1.0", + "@aries-framework/node": "^0.1.0", + "ts-node": "^10.4.0" + }, + "dependencies": { + "@types/express": "^4.17.13", + "@types/uuid": "^8.3.1", + "@types/ws": "^7.4.6", + "class-validator": "0.13.1", + "reflect-metadata": "^0.1.13", + "rxjs": "^7.2.0", + "tsyringe": "^4.5.0" + } +} diff --git a/samples/extension-module/requester.ts b/samples/extension-module/requester.ts new file mode 100644 index 0000000000..1ccdb433a5 --- /dev/null +++ b/samples/extension-module/requester.ts @@ -0,0 +1,67 @@ +import type { DummyRecord, DummyStateChangedEvent } from './dummy' + +import { Agent, ConsoleLogger, LogLevel, WsOutboundTransport } from '@aries-framework/core' +import { agentDependencies } from '@aries-framework/node' +import { filter, first, firstValueFrom, map, ReplaySubject, timeout } from 'rxjs' + +import { DummyEventTypes, DummyModule, DummyState } from './dummy' + +const run = async () => { + // Create transports + const port = process.env.RESPONDER_PORT ? Number(process.env.RESPONDER_PORT) : 3002 + const wsOutboundTransport = new WsOutboundTransport() + + // Setup the agent + const agent = new Agent( + { + label: 'Dummy-powered agent - requester', + walletConfig: { + id: 'requester', + key: 'requester', + }, + logger: new ConsoleLogger(LogLevel.test), + autoAcceptConnections: true, + }, + agentDependencies + ) + + // Register transports + agent.registerOutboundTransport(wsOutboundTransport) + + // Inject DummyModule + const dummyModule = agent.injectionContainer.resolve(DummyModule) + + // Now agent will handle messages and events from Dummy protocol + + //Initialize the agent + await agent.initialize() + + // Connect to responder using its invitation endpoint + const invitationUrl = await (await agentDependencies.fetch(`http://localhost:${port}/invitation`)).text() + const connection = await agent.connections.receiveInvitationFromUrl(invitationUrl) + await agent.connections.returnWhenIsConnected(connection.id) + + // Create observable for Response Received event + const observable = agent.events.observable(DummyEventTypes.StateChanged) + const subject = new ReplaySubject(1) + + observable + .pipe( + filter((event: DummyStateChangedEvent) => event.payload.dummyRecord.state === DummyState.ResponseReceived), + map((e) => e.payload.dummyRecord), + first(), + timeout(5000) + ) + .subscribe(subject) + + // Send a dummy request and wait for response + const record = await dummyModule.request(connection) + agent.config.logger.info(`Request received for Dummy Record: ${record.id}`) + + const dummyRecord = await firstValueFrom(subject) + agent.config.logger.info(`Response received for Dummy Record: ${dummyRecord.id}`) + + await agent.shutdown() +} + +void run() diff --git a/samples/extension-module/responder.ts b/samples/extension-module/responder.ts new file mode 100644 index 0000000000..c92b9ed43d --- /dev/null +++ b/samples/extension-module/responder.ts @@ -0,0 +1,71 @@ +import type { DummyStateChangedEvent } from './dummy' +import type { Socket } from 'net' + +import { Agent, ConsoleLogger, LogLevel, WsOutboundTransport } from '@aries-framework/core' +import { agentDependencies, HttpInboundTransport, WsInboundTransport } from '@aries-framework/node' +import express from 'express' +import { Server } from 'ws' + +import { DummyEventTypes, DummyModule, DummyState } from './dummy' + +const run = async () => { + // Create transports + const port = process.env.RESPONDER_PORT ? Number(process.env.RESPONDER_PORT) : 3002 + const app = express() + const socketServer = new Server({ noServer: true }) + + const httpInboundTransport = new HttpInboundTransport({ app, port }) + const wsInboundTransport = new WsInboundTransport({ server: socketServer }) + const wsOutboundTransport = new WsOutboundTransport() + + // Setup the agent + const agent = new Agent( + { + label: 'Dummy-powered agent - responder', + endpoints: [`ws://localhost:${port}`], + walletConfig: { + id: 'responder', + key: 'responder', + }, + logger: new ConsoleLogger(LogLevel.test), + autoAcceptConnections: true, + }, + agentDependencies + ) + + // Register transports + agent.registerInboundTransport(httpInboundTransport) + agent.registerInboundTransport(wsInboundTransport) + agent.registerInboundTransport(wsOutboundTransport) + + // Allow to create invitation, no other way to ask for invitation yet + app.get('/invitation', async (req, res) => { + const { invitation } = await agent.connections.createConnection() + res.send(invitation.toUrl({ domain: `http://localhost:${port}/invitation` })) + }) + + // Inject DummyModule + const dummyModule = agent.injectionContainer.resolve(DummyModule) + + // Now agent will handle messages and events from Dummy protocol + + //Initialize the agent + await agent.initialize() + + httpInboundTransport.server?.on('upgrade', (request, socket, head) => { + socketServer.handleUpgrade(request, socket as Socket, head, (socket) => { + socketServer.emit('connection', socket, request) + }) + }) + + // Subscribe to dummy record events + agent.events.on(DummyEventTypes.StateChanged, async (event: DummyStateChangedEvent) => { + if (event.payload.dummyRecord.state === DummyState.RequestReceived) { + await dummyModule.respond(event.payload.dummyRecord) + } + }) + + agent.config.logger.info(`Responder listening to port ${port}`) +} + +void run() diff --git a/samples/extension-module/tsconfig.json b/samples/extension-module/tsconfig.json new file mode 100644 index 0000000000..2e05131598 --- /dev/null +++ b/samples/extension-module/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["node"] + } +} diff --git a/yarn.lock b/yarn.lock index ef14107d76..fa7a0181c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2420,7 +2420,7 @@ dependencies: "@types/node" "*" -"@types/ws@^7.4.4", "@types/ws@^7.4.6": +"@types/ws@^7.4.6": version "7.4.7" resolved "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz#f7c390a36f7a0679aa69de2d501319f4f8d9b702" integrity sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww== @@ -8916,7 +8916,7 @@ rxjs@^6.6.0: dependencies: tslib "^1.9.0" -rxjs@^7.1.0, rxjs@^7.2.0: +rxjs@^7.2.0: version "7.5.2" resolved "https://registry.npmjs.org/rxjs/-/rxjs-7.5.2.tgz#11e4a3a1dfad85dbf7fb6e33cbba17668497490b" integrity sha512-PwDt186XaL3QN5qXj/H9DGyHhP3/RYYgZZwqBv9Tv8rsAaiwFH1IsJJlcgD37J7UW5a6O67qX0KWKS3/pu0m4w==