Skip to content

Commit

Permalink
feat(tenants): initial tenants module (#932)
Browse files Browse the repository at this point in the history
Signed-off-by: Timo Glastra <[email protected]>
  • Loading branch information
TimoGlastra committed Aug 26, 2022
1 parent 7c34f8b commit e88a9cf
Show file tree
Hide file tree
Showing 66 changed files with 2,012 additions and 64 deletions.
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
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export class DidExchangeRequestHandler implements Handler {

const connectionRecord = await this.didExchangeProtocol.processRequest(messageContext, outOfBandRecord)

if (connectionRecord?.autoAcceptConnection ?? messageContext.agentContext.config.autoAcceptConnections) {
if (connectionRecord.autoAcceptConnection ?? messageContext.agentContext.config.autoAcceptConnections) {
// TODO We should add an option to not pass routing and therefore do not rotate keys and use the keys from the invitation
// TODO: Allow rotation of keys used in the invitation for new ones not only when out-of-band is reusable
const routing = outOfBandRecord.reusable
Expand Down
Loading

0 comments on commit e88a9cf

Please sign in to comment.