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(tenants): initial tenants module #932

11 changes: 10 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,16 @@ module.exports = {
},
},
{
files: ['*.test.ts', '**/__tests__/**', '**/tests/**', 'jest.*.ts', 'samples/**', 'demo/**', 'scripts/**'],
files: [
'*.test.ts',
'**/__tests__/**',
'**/tests/**',
'jest.*.ts',
'samples/**',
'demo/**',
'scripts/**',
'**/tests/**',
],
env: {
jest: true,
node: false,
Expand Down
18 changes: 9 additions & 9 deletions packages/core/src/agent/Agent.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import type { DependencyManager } from '../plugins'
import type { InboundTransport } from '../transport/InboundTransport'
import type { OutboundTransport } from '../transport/OutboundTransport'
import type { InitConfig } from '../types'
import type { AgentDependencies } from './AgentDependencies'
import type { AgentMessageReceivedEvent } from './Events'
import type { Subscription } from 'rxjs'
import type { DependencyContainer } from 'tsyringe'

import { Subject } from 'rxjs'
import { concatMap, takeUntil } from 'rxjs/operators'
import { container as baseContainer } from 'tsyringe'

import { CacheRepository } from '../cache'
import { InjectionSymbols } from '../constants'
Expand All @@ -29,6 +26,7 @@ import { QuestionAnswerModule } from '../modules/question-answer/QuestionAnswerM
import { MediatorModule } from '../modules/routing/MediatorModule'
import { RecipientModule } from '../modules/routing/RecipientModule'
import { W3cVcModule } from '../modules/vc/module'
import { DependencyManager } from '../plugins'
import { DidCommMessageRepository, StorageUpdateService, StorageVersionRepository } from '../storage'
import { InMemoryMessageRepository } from '../storage/InMemoryMessageRepository'
import { IndyStorageService } from '../storage/IndyStorageService'
Expand All @@ -52,11 +50,11 @@ export class Agent extends BaseAgent {
public constructor(
initialConfig: InitConfig,
dependencies: AgentDependencies,
injectionContainer?: DependencyContainer
dependencyManager?: DependencyManager
) {
// NOTE: we can't create variables before calling super as TS will complain that the super call must be the
// the first statement in the constructor.
super(new AgentConfig(initialConfig, dependencies), injectionContainer ?? baseContainer.createChildContainer())
super(new AgentConfig(initialConfig, dependencies), dependencyManager ?? new DependencyManager())

const stop$ = this.dependencyManager.resolve<Subject<boolean>>(InjectionSymbols.Stop$)

Expand Down Expand Up @@ -95,10 +93,6 @@ export class Agent extends BaseAgent {
return this.eventEmitter
}

public get isInitialized() {
return this._isInitialized && this.wallet.isInitialized
}

public async initialize() {
const { connectToIndyLedgersOnStartup, mediatorConnectionsInvite } = this.agentConfig

Expand Down Expand Up @@ -146,7 +140,13 @@ export class Agent extends BaseAgent {
const transportPromises = allTransports.map((transport) => transport.stop())
await Promise.all(transportPromises)

// close wallet if still initialized
if (this.wallet.isInitialized) {
await this.wallet.close()
}

await super.shutdown()
this._isInitialized = false
}

protected registerDependencies(dependencyManager: DependencyManager) {
Expand Down
6 changes: 2 additions & 4 deletions packages/core/src/agent/AgentConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,14 +120,12 @@ export class AgentConfig {
)
}

public toString() {
const config = {
public toJSON() {
return {
...this.initConfig,
logger: this.logger !== undefined,
agentDependencies: this.agentDependencies != undefined,
label: this.label,
}

return JSON.stringify(config, null, 2)
}
}
18 changes: 5 additions & 13 deletions packages/core/src/agent/BaseAgent.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Logger } from '../logger'
import type { DependencyManager } from '../plugins'
import type { AgentConfig } from './AgentConfig'
import type { TransportSession } from './TransportService'
import type { DependencyContainer } from 'tsyringe'

import { AriesFrameworkError } from '../error'
import { BasicMessagesModule } from '../modules/basic-messages/BasicMessagesModule'
Expand All @@ -16,7 +16,6 @@ import { ProofsModule } from '../modules/proofs/ProofsModule'
import { QuestionAnswerModule } from '../modules/question-answer/QuestionAnswerModule'
import { MediatorModule } from '../modules/routing/MediatorModule'
import { RecipientModule } from '../modules/routing/RecipientModule'
import { DependencyManager } from '../plugins'
import { StorageUpdateService } from '../storage'
import { UpdateAssistant } from '../storage/migration/UpdateAssistant'
import { DEFAULT_UPDATE_CONFIG } from '../storage/migration/updates'
Expand Down Expand Up @@ -54,17 +53,14 @@ export abstract class BaseAgent {
public readonly wallet: WalletModule
public readonly oob: OutOfBandModule

public constructor(agentConfig: AgentConfig, container: DependencyContainer) {
this.dependencyManager = new DependencyManager(container)
public constructor(agentConfig: AgentConfig, dependencyManager: DependencyManager) {
this.dependencyManager = dependencyManager

this.agentConfig = agentConfig
this.logger = this.agentConfig.logger

this.logger.info('Creating agent with config', {
...agentConfig,
// Prevent large object being logged.
// Will display true/false to indicate if value is present in config
logger: agentConfig.logger != undefined,
agentConfig: agentConfig.toJSON(),
})

if (!this.agentConfig.walletConfig) {
Expand Down Expand Up @@ -153,11 +149,7 @@ export abstract class BaseAgent {
}

public async shutdown() {
// close wallet if still initialized
if (this.wallet.isInitialized) {
await this.wallet.close()
}
this._isInitialized = false
// No logic required at the moment
}

public get publicDid() {
Expand Down
11 changes: 9 additions & 2 deletions packages/core/src/agent/EventEmitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { injectable, inject } from '../plugins'

import { AgentDependencies } from './AgentDependencies'

type EmitEvent<T extends BaseEvent> = Omit<T, 'metadata'>

@injectable()
export class EventEmitter {
private eventEmitter: NativeEventEmitter
Expand All @@ -24,8 +26,13 @@ export class EventEmitter {
}

// agentContext is currently not used, but already making required as it will be used soon
public emit<T extends BaseEvent>(agentContext: AgentContext, data: T) {
this.eventEmitter.emit(data.type, data)
public emit<T extends BaseEvent>(agentContext: AgentContext, data: EmitEvent<T>) {
this.eventEmitter.emit(data.type, {
...data,
metadata: {
contextCorrelationId: agentContext.contextCorrelationId,
},
})
}

public on<T extends BaseEvent>(event: T['type'], listener: (data: T) => void | Promise<void>) {
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/agent/Events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@ export enum AgentEventTypes {
AgentMessageProcessed = 'AgentMessageProcessed',
}

export interface EventMetadata {
contextCorrelationId: string
}

export interface BaseEvent {
type: string
payload: Record<string, unknown>
metadata: EventMetadata
}

export interface AgentMessageReceivedEvent extends BaseEvent {
Expand Down
59 changes: 59 additions & 0 deletions packages/core/src/agent/__tests__/EventEmitter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { EventEmitter as NativeEventEmitter } from 'events'

import { Subject } from 'rxjs'

import { agentDependencies, getAgentContext } from '../../../tests/helpers'
import { EventEmitter } from '../EventEmitter'

const mockEmit = jest.fn()
const mockOn = jest.fn()
const mockOff = jest.fn()
const mock = jest.fn().mockImplementation(() => {
return { emit: mockEmit, on: mockOn, off: mockOff }
}) as jest.Mock<NativeEventEmitter>

const eventEmitter = new EventEmitter(
{ ...agentDependencies, EventEmitterClass: mock as unknown as typeof NativeEventEmitter },
new Subject()
)
const agentContext = getAgentContext({})

describe('EventEmitter', () => {
afterEach(() => {
jest.clearAllMocks()
})

describe('emit', () => {
test("calls 'emit' on native event emitter instance", () => {
eventEmitter.emit(agentContext, {
payload: { some: 'payload' },
type: 'some-event',
})

expect(mockEmit).toHaveBeenCalledWith('some-event', {
payload: { some: 'payload' },
type: 'some-event',
metadata: {
contextCorrelationId: agentContext.contextCorrelationId,
},
})
})
})

describe('on', () => {
test("calls 'on' on native event emitter instance", () => {
const listener = jest.fn()
eventEmitter.on('some-event', listener)

expect(mockOn).toHaveBeenCalledWith('some-event', listener)
})
})
describe('off', () => {
test("calls 'off' on native event emitter instance", () => {
const listener = jest.fn()
eventEmitter.off('some-event', listener)

expect(mockOff).toHaveBeenCalledWith('some-event', listener)
})
})
})
6 changes: 6 additions & 0 deletions packages/core/src/agent/context/AgentContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,10 @@ export class AgentContext {
public get wallet() {
return this.dependencyManager.resolve<Wallet>(InjectionSymbols.Wallet)
}

public toJSON() {
return {
contextCorrelationId: this.contextCorrelationId,
}
}
}
8 changes: 7 additions & 1 deletion packages/core/src/agent/context/AgentContextProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { AgentContext } from './AgentContext'

export interface AgentContextProvider {
/**
* Find the agent context based for an inbound message. It's possible to provide a contextCorrelationId to make it
* Get the agent context for an inbound message. It's possible to provide a contextCorrelationId to make it
* easier for the context provider implementation to correlate inbound messages to the correct context. This can be useful if
* a plaintext message is passed and the context provider can't determine the context based on the recipient public keys
* of the inbound message.
Expand All @@ -14,4 +14,10 @@ export interface AgentContextProvider {
inboundMessage: unknown,
options?: { contextCorrelationId?: string }
): Promise<AgentContext>

/**
* Get the agent context for a context correlation id. Will throw an error if no AgentContext could be retrieved
* for the specified contextCorrelationId.
*/
getAgentContextForContextCorrelationId(contextCorrelationId: string): Promise<AgentContext>
}
22 changes: 21 additions & 1 deletion packages/core/src/agent/context/DefaultAgentContextProvider.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { AgentContextProvider } from './AgentContextProvider'

import { AriesFrameworkError } from '../../error'
import { injectable } from '../../plugins'

import { AgentContext } from './AgentContext'
Expand All @@ -18,7 +19,26 @@ export class DefaultAgentContextProvider implements AgentContextProvider {
this.agentContext = agentContext
}

public async getContextForInboundMessage(): Promise<AgentContext> {
public async getAgentContextForContextCorrelationId(contextCorrelationId: string): Promise<AgentContext> {
if (contextCorrelationId !== this.agentContext.contextCorrelationId) {
throw new AriesFrameworkError(
`Could not get agent context for contextCorrelationId '${contextCorrelationId}'. Only contextCorrelationId '${this.agentContext.contextCorrelationId}' is supported.`
)
}

return this.agentContext
}

public async getContextForInboundMessage(
// We don't need to look at the message as we always use the same context in the default agent context provider
_: unknown,
options?: { contextCorrelationId?: string }
): Promise<AgentContext> {
// This will throw an error if the contextCorrelationId does not match with the contextCorrelationId of the agent context property of this class.
if (options?.contextCorrelationId) {
return this.getAgentContextForContextCorrelationId(options.contextCorrelationId)
}

return this.agentContext
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,33 @@ describe('DefaultAgentContextProvider', () => {

await expect(agentContextProvider.getContextForInboundMessage(message)).resolves.toBe(agentContext)
})

test('throws an error if the provided contextCorrelationId does not match with the contextCorrelationId from the constructor agent context', async () => {
const agentContextProvider: AgentContextProvider = new DefaultAgentContextProvider(agentContext)

const message = {}

await expect(
agentContextProvider.getContextForInboundMessage(message, { contextCorrelationId: 'wrong' })
).rejects.toThrowError(
`Could not get agent context for contextCorrelationId 'wrong'. Only contextCorrelationId 'mock' is supported.`
)
})
})

describe('getAgentContextForContextCorrelationId()', () => {
test('returns the agent context provided in the constructor if contextCorrelationId matches', async () => {
const agentContextProvider: AgentContextProvider = new DefaultAgentContextProvider(agentContext)

await expect(agentContextProvider.getAgentContextForContextCorrelationId('mock')).resolves.toBe(agentContext)
})

test('throws an error if the contextCorrelationId does not match with the contextCorrelationId from the constructor agent context', async () => {
const agentContextProvider: AgentContextProvider = new DefaultAgentContextProvider(agentContext)

await expect(agentContextProvider.getAgentContextForContextCorrelationId('wrong')).rejects.toThrowError(
`Could not get agent context for contextCorrelationId 'wrong'. Only contextCorrelationId 'mock' is supported.`
)
})
})
})
10 changes: 10 additions & 0 deletions packages/core/src/agent/models/InboundMessageContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,14 @@ export class InboundMessageContext<T extends AgentMessage = AgentMessage> {

return this.connection
}

public toJSON() {
return {
message: this.message,
recipientKey: this.recipientKey?.fingerprint,
senderKey: this.senderKey?.fingerprint,
sessionId: this.sessionId,
agentContext: this.agentContext.toJSON(),
}
}
}
7 changes: 5 additions & 2 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
// reflect-metadata used for class-transformer + class-validator
import 'reflect-metadata'

export { AgentContext } from './agent'
export { MessageReceiver } from './agent/MessageReceiver'
export { Agent } from './agent/Agent'
export { BaseAgent } from './agent/BaseAgent'
export * from './agent'
export { EventEmitter } from './agent/EventEmitter'
export { Handler, HandlerInboundMessage } from './agent/Handler'
export { InboundMessageContext } from './agent/models/InboundMessageContext'
Expand Down Expand Up @@ -41,11 +42,13 @@ export * from './modules/ledger'
export * from './modules/routing'
export * from './modules/question-answer'
export * from './modules/oob'
export * from './wallet/WalletModule'
export * from './modules/dids'
export * from './utils/JsonTransformer'
export { JsonEncoder, JsonTransformer, isJsonObject, isValidJweStructure } from './utils'
export * from './logger'
export * from './error'
export * from './wallet/error'
export { Key, KeyType } from './crypto'
export { parseMessageType, IsValidMessageType } from './utils/messageType'

export * from './agent/Events'
Expand Down
1 change: 0 additions & 1 deletion packages/core/src/modules/connections/ConnectionsModule.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { Key } from '../../crypto'
import type { DependencyManager } from '../../plugins'
import type { OutOfBandRecord } from '../oob/repository'
import type { ConnectionRecord } from './repository/ConnectionRecord'
Expand Down
Loading