From ef35b4a51bfe8ccff9531bc15936276f0450f770 Mon Sep 17 00:00:00 2001
From: Berend Sliedrecht <berend@animo.id>
Date: Sun, 3 Dec 2023 21:43:19 +0100
Subject: [PATCH] feat(ssi): finished tenants module with e2e tests

Signed-off-by: Berend Sliedrecht <berend@animo.id>
---
 apps/shared/package.json                      |   5 +-
 .../src/events/__tests__/baseEvents.spec.ts   |   2 +-
 .../events/__tests__/connectionEvents.spec.ts |   6 +-
 .../src/events/__tests__/didEvents.spec.ts    |  29 ++++
 .../src/events/__tests__/tenantEvents.spec.ts |  24 ++++
 apps/shared/src/events/baseEvents.ts          |  20 ++-
 apps/shared/src/events/connectionEvents.ts    |  28 +++-
 apps/shared/src/events/didEvents.ts           |  23 ++-
 apps/shared/src/events/tenantEvents.ts        |  22 +++
 apps/shared/src/index.ts                      |   1 +
 apps/ssi-abstraction/package.json             |   7 +-
 .../agent/__tests__/agent.controller.spec.ts  |  32 -----
 .../src/agent/agent.controller.ts             |  17 ---
 .../ssi-abstraction/src/agent/agent.module.ts |   7 +-
 .../src/agent/agent.service.ts                |  30 ++--
 .../__tests__/connections.controller.spec.ts  |   9 +-
 .../connections/connections.controller.ts     |  40 +++---
 .../agent/connections/connections.service.ts  | 135 +++++++++++-------
 .../dids/__tests__/dids.controller.spec.ts    |   1 +
 .../src/agent/dids/dids.controller.ts         |  24 +++-
 .../src/agent/dids/dids.module.ts             |   2 +-
 .../src/agent/dids/dids.service.ts            |  68 ++++++---
 .../__tests__/tenants.controller.spec.ts      |  41 ++++++
 .../src/agent/tenants/tenants.controller.ts   |  20 +++
 .../src/agent/tenants/tenants.module.ts       |  13 ++
 .../src/agent/tenants/tenants.service.ts      |  18 +++
 .../src/agent/withTenantService.ts            |  34 +++++
 apps/ssi-abstraction/src/app.module.ts        |   4 +
 apps/ssi-abstraction/test/agent.e2e-spec.ts   |  75 ----------
 .../test/connections.e2e-spec.ts              |  53 +++++--
 apps/ssi-abstraction/test/dids.e2e-spec.ts    |  51 ++++++-
 apps/ssi-abstraction/test/jest.config.js      |   2 +-
 apps/ssi-abstraction/test/tenants.e2e-spec.ts |  65 +++++++++
 pnpm-lock.yaml                                |  29 +++-
 34 files changed, 650 insertions(+), 287 deletions(-)
 create mode 100644 apps/shared/src/events/__tests__/didEvents.spec.ts
 create mode 100644 apps/shared/src/events/__tests__/tenantEvents.spec.ts
 create mode 100644 apps/shared/src/events/tenantEvents.ts
 delete mode 100644 apps/ssi-abstraction/src/agent/__tests__/agent.controller.spec.ts
 delete mode 100644 apps/ssi-abstraction/src/agent/agent.controller.ts
 create mode 100644 apps/ssi-abstraction/src/agent/tenants/__tests__/tenants.controller.spec.ts
 create mode 100644 apps/ssi-abstraction/src/agent/tenants/tenants.controller.ts
 create mode 100644 apps/ssi-abstraction/src/agent/tenants/tenants.module.ts
 create mode 100644 apps/ssi-abstraction/src/agent/tenants/tenants.service.ts
 create mode 100644 apps/ssi-abstraction/src/agent/withTenantService.ts
 delete mode 100644 apps/ssi-abstraction/test/agent.e2e-spec.ts
 create mode 100644 apps/ssi-abstraction/test/tenants.e2e-spec.ts

diff --git a/apps/shared/package.json b/apps/shared/package.json
index 7e03331..8bd91ab 100644
--- a/apps/shared/package.json
+++ b/apps/shared/package.json
@@ -22,6 +22,7 @@
   },
   "dependencies": {
     "@aries-framework/core": "0.4.2",
+    "@aries-framework/tenants": "^0.4.2",
     "@elastic/ecs-winston-format": "^1.5.0",
     "@nestjs/common": "^10.2.10",
     "@nestjs/microservices": "^10.2.10",
@@ -32,10 +33,10 @@
     "winston": "^3.11.0"
   },
   "devDependencies": {
-    "@types/jest": "^29.5.9",
-    "@types/node": "^20.9.3",
     "@nestjs/cli": "^10.2.1",
     "@nestjs/testing": "^10.2.10",
+    "@types/jest": "^29.5.9",
+    "@types/node": "^20.9.3",
     "rimraf": "^5.0.5",
     "supertest": "^6.1.3",
     "ts-jest": "^29.1.1",
diff --git a/apps/shared/src/events/__tests__/baseEvents.spec.ts b/apps/shared/src/events/__tests__/baseEvents.spec.ts
index a037c79..fac31bb 100644
--- a/apps/shared/src/events/__tests__/baseEvents.spec.ts
+++ b/apps/shared/src/events/__tests__/baseEvents.spec.ts
@@ -6,7 +6,7 @@ describe('Base Events', () => {
   });
 
   it('should create a new base event', () => {
-    const baseEvent = new BaseEvent({ some: 'data' });
+    const baseEvent = new BaseEvent({ some: 'data' }, 'tenantId');
 
     expect(typeof baseEvent.id).toStrictEqual('string');
     expect(baseEvent.type).toStrictEqual('BaseEvent');
diff --git a/apps/shared/src/events/__tests__/connectionEvents.spec.ts b/apps/shared/src/events/__tests__/connectionEvents.spec.ts
index 70f97f0..ea43996 100644
--- a/apps/shared/src/events/__tests__/connectionEvents.spec.ts
+++ b/apps/shared/src/events/__tests__/connectionEvents.spec.ts
@@ -17,7 +17,7 @@ describe('Connection Events', () => {
   });
 
   it('should create a new connections get all event', () => {
-    const event = new EventDidcommConnectionsGetAll([]);
+    const event = new EventDidcommConnectionsGetAll([], 'tenantId');
 
     expect(typeof event.id).toStrictEqual('string');
     expect(event.type).toStrictEqual('EventDidcommConnectionsGetAll');
@@ -26,7 +26,7 @@ describe('Connection Events', () => {
   });
 
   it('should create a new connections get by id event', () => {
-    const event = new EventDidcommConnectionsGetById(null);
+    const event = new EventDidcommConnectionsGetById(null, 'tenantId');
 
     expect(typeof event.id).toStrictEqual('string');
     expect(event.type).toStrictEqual('EventDidcommConnectionsGetById');
@@ -40,6 +40,7 @@ describe('Connection Events', () => {
         role: DidExchangeRole.Requester,
         state: DidExchangeState.Completed,
       }),
+      'tenantId',
     );
 
     expect(typeof event.id).toStrictEqual('string');
@@ -57,6 +58,7 @@ describe('Connection Events', () => {
         role: DidExchangeRole.Requester,
         state: DidExchangeState.Completed,
       }),
+      'tenantId',
     );
 
     expect(typeof event.id).toStrictEqual('string');
diff --git a/apps/shared/src/events/__tests__/didEvents.spec.ts b/apps/shared/src/events/__tests__/didEvents.spec.ts
new file mode 100644
index 0000000..1acd0fa
--- /dev/null
+++ b/apps/shared/src/events/__tests__/didEvents.spec.ts
@@ -0,0 +1,29 @@
+import { DidDocument } from '@aries-framework/core';
+
+import { EventDidsPublicDid, EventDidsResolve } from '../didEvents.js';
+
+describe('Did Events', () => {
+  it('should return module', () => {
+    jest.requireActual('../didEvents');
+  });
+
+  it('should create get public did event', () => {
+    const doc = new DidDocument({ id: 'did:web:123.com' });
+    const event = new EventDidsPublicDid(doc, 'tenantId');
+
+    expect(typeof event.id).toStrictEqual('string');
+    expect(event.type).toStrictEqual('EventDidsPublicDid');
+    expect(event.timestamp).toBeInstanceOf(Date);
+    expect(event.instance).toMatchObject(doc);
+  });
+
+  it('should create did resolve event', () => {
+    const doc = new DidDocument({ id: 'did:my:id' });
+    const event = new EventDidsResolve(doc, 'tenantId');
+
+    expect(typeof event.id).toStrictEqual('string');
+    expect(event.type).toStrictEqual('EventDidsResolve');
+    expect(event.timestamp).toBeInstanceOf(Date);
+    expect(event.instance).toMatchObject(doc);
+  });
+});
diff --git a/apps/shared/src/events/__tests__/tenantEvents.spec.ts b/apps/shared/src/events/__tests__/tenantEvents.spec.ts
new file mode 100644
index 0000000..3eda953
--- /dev/null
+++ b/apps/shared/src/events/__tests__/tenantEvents.spec.ts
@@ -0,0 +1,24 @@
+import { TenantRecord } from '@aries-framework/tenants';
+
+import { EventTenantsCreate } from '../tenantEvents.js';
+
+describe('Tenant Events', () => {
+  it('should return module', () => {
+    jest.requireActual('../tenantEvents');
+  });
+
+  it('should create a create tenant event', () => {
+    const tenantRecord = new TenantRecord({
+      config: {
+        label: 'my-label',
+        walletConfig: { id: 'some-id', key: 'some-key' },
+      },
+    });
+    const event = new EventTenantsCreate(tenantRecord, undefined);
+
+    expect(typeof event.id).toStrictEqual('string');
+    expect(event.type).toStrictEqual('EventTenantsCreate');
+    expect(event.timestamp).toBeInstanceOf(Date);
+    expect(event.instance).toMatchObject(tenantRecord);
+  });
+});
diff --git a/apps/shared/src/events/baseEvents.ts b/apps/shared/src/events/baseEvents.ts
index 7be7e86..04cebaa 100644
--- a/apps/shared/src/events/baseEvents.ts
+++ b/apps/shared/src/events/baseEvents.ts
@@ -1,15 +1,29 @@
 import { utils } from '@aries-framework/core';
 
-export class BaseEvent<T = Record<string, unknown>> {
+export class BaseEvent<T = Record<string, unknown>, TenantIdType = string> {
   public readonly id: string;
   public readonly type: string;
   public readonly timestamp: Date;
   public readonly data: T;
+  public readonly tenantId: TenantIdType;
+
+  public constructor(
+    data: T,
+    tenantId: TenantIdType,
+    id?: string,
+    type?: string,
+    timestamp?: Date,
+  ) {
+    this.data = data;
+    this.tenantId = tenantId;
 
-  public constructor(data: T, id?: string, type?: string, timestamp?: Date) {
     this.id = id ?? utils.uuid();
     this.type = type ?? this.constructor.name;
     this.timestamp = timestamp ?? new Date();
-    this.data = data;
   }
 }
+
+export type BaseEventInput<
+  T extends Record<string, unknown> = Record<string, unknown>,
+  TenantIdType extends undefined | string = string,
+> = TenantIdType extends string ? { tenantId: string } & T : T;
diff --git a/apps/shared/src/events/connectionEvents.ts b/apps/shared/src/events/connectionEvents.ts
index 1ec09a5..a9ae32a 100644
--- a/apps/shared/src/events/connectionEvents.ts
+++ b/apps/shared/src/events/connectionEvents.ts
@@ -1,7 +1,10 @@
+import type { BaseEventInput } from './baseEvents.js';
+
 import { ConnectionRecord, JsonTransformer } from '@aries-framework/core';
 
 import { BaseEvent } from './baseEvents.js';
 
+export type EventDidcommConnectionsGetAllInput = BaseEventInput;
 export class EventDidcommConnectionsGetAll extends BaseEvent<
   Array<ConnectionRecord>
 > {
@@ -12,10 +15,19 @@ export class EventDidcommConnectionsGetAll extends BaseEvent<
   }
 
   public static fromEvent(e: EventDidcommConnectionsGetAll) {
-    return new EventDidcommConnectionsGetAll(e.data, e.id, e.type, e.timestamp);
+    return new EventDidcommConnectionsGetAll(
+      e.data,
+      e.tenantId,
+      e.id,
+      e.type,
+      e.timestamp,
+    );
   }
 }
 
+export type EventDidcommConnectionsGetByIdInput = BaseEventInput<{
+  id: string;
+}>;
 export class EventDidcommConnectionsGetById extends BaseEvent<ConnectionRecord | null> {
   public static token = 'didcomm.connections.getById';
 
@@ -28,6 +40,7 @@ export class EventDidcommConnectionsGetById extends BaseEvent<ConnectionRecord |
   public static fromEvent(e: EventDidcommConnectionsGetById) {
     return new EventDidcommConnectionsGetById(
       e.data,
+      e.tenantId,
       e.id,
       e.type,
       e.timestamp,
@@ -35,6 +48,7 @@ export class EventDidcommConnectionsGetById extends BaseEvent<ConnectionRecord |
   }
 }
 
+export type EventDidcommConnectionsCreateWithSelfInput = BaseEventInput;
 export class EventDidcommConnectionsCreateWithSelf extends BaseEvent<ConnectionRecord> {
   public static token = 'didcomm.connections.createWithSelf';
 
@@ -45,6 +59,7 @@ export class EventDidcommConnectionsCreateWithSelf extends BaseEvent<ConnectionR
   public static fromEvent(e: EventDidcommConnectionsCreateWithSelf) {
     return new EventDidcommConnectionsCreateWithSelf(
       e.data,
+      e.tenantId,
       e.id,
       e.type,
       e.timestamp,
@@ -52,6 +67,9 @@ export class EventDidcommConnectionsCreateWithSelf extends BaseEvent<ConnectionR
   }
 }
 
+export type EventDidcommConnectionsBlockInput = BaseEventInput<{
+  idOrDid: string;
+}>;
 export class EventDidcommConnectionsBlock extends BaseEvent<ConnectionRecord | null> {
   public static token = 'didcomm.connections.block';
 
@@ -62,6 +80,12 @@ export class EventDidcommConnectionsBlock extends BaseEvent<ConnectionRecord | n
   }
 
   public static fromEvent(e: EventDidcommConnectionsBlock) {
-    return new EventDidcommConnectionsBlock(e.data, e.id, e.type, e.timestamp);
+    return new EventDidcommConnectionsBlock(
+      e.data,
+      e.tenantId,
+      e.id,
+      e.type,
+      e.timestamp,
+    );
   }
 }
diff --git a/apps/shared/src/events/didEvents.ts b/apps/shared/src/events/didEvents.ts
index c6a2d68..60f5c68 100644
--- a/apps/shared/src/events/didEvents.ts
+++ b/apps/shared/src/events/didEvents.ts
@@ -1,3 +1,5 @@
+import type { BaseEventInput } from './baseEvents.js';
+
 import { DidDocument, JsonTransformer } from '@aries-framework/core';
 
 import { BaseEvent } from './baseEvents.js';
@@ -7,18 +9,31 @@ import { BaseEvent } from './baseEvents.js';
  * @todo: this should be removed as it is a weird event that should not be needed
  *
  */
-export class EventInfoPublicDid extends BaseEvent<DidDocument> {
+export type EventDidsPublicDidInput = BaseEventInput;
+/**
+ *
+ * @todo: this should be removed as it is a weird event that should not be needed
+ *
+ */
+export class EventDidsPublicDid extends BaseEvent<DidDocument> {
   public static token = 'dids.publicDid';
 
   public get instance() {
     return JsonTransformer.fromJSON(this.data, DidDocument);
   }
 
-  public static fromEvent(e: EventInfoPublicDid) {
-    return new EventInfoPublicDid(e.data, e.id, e.type, e.timestamp);
+  public static fromEvent(e: EventDidsPublicDid) {
+    return new EventDidsPublicDid(
+      e.data,
+      e.tenantId,
+      e.id,
+      e.type,
+      e.timestamp,
+    );
   }
 }
 
+export type EventDidsResolveInput = BaseEventInput<{ did: string }>;
 export class EventDidsResolve extends BaseEvent<DidDocument> {
   public static token = 'dids.resolve';
 
@@ -27,6 +42,6 @@ export class EventDidsResolve extends BaseEvent<DidDocument> {
   }
 
   public static fromEvent(e: EventDidsResolve) {
-    return new EventDidsResolve(e.data, e.id, e.type, e.timestamp);
+    return new EventDidsResolve(e.data, e.tenantId, e.id, e.type, e.timestamp);
   }
 }
diff --git a/apps/shared/src/events/tenantEvents.ts b/apps/shared/src/events/tenantEvents.ts
new file mode 100644
index 0000000..c3552d1
--- /dev/null
+++ b/apps/shared/src/events/tenantEvents.ts
@@ -0,0 +1,22 @@
+import type { BaseEventInput } from './baseEvents.js';
+
+import { JsonTransformer } from '@aries-framework/core';
+import { TenantRecord } from '@aries-framework/tenants';
+
+import { BaseEvent } from './baseEvents.js';
+
+export type EventTenantsCreateInput = BaseEventInput<
+  { label: string },
+  undefined
+>;
+export class EventTenantsCreate extends BaseEvent<TenantRecord, undefined> {
+  public static token = 'tenants.create';
+
+  public get instance() {
+    return JsonTransformer.fromJSON(this.data, TenantRecord);
+  }
+
+  public static fromEvent(e: EventTenantsCreate) {
+    return new EventTenantsCreate(e.data, undefined, e.id, e.type, e.timestamp);
+  }
+}
diff --git a/apps/shared/src/index.ts b/apps/shared/src/index.ts
index 8a7c542..42fe800 100644
--- a/apps/shared/src/index.ts
+++ b/apps/shared/src/index.ts
@@ -6,3 +6,4 @@ export * from './logging/logAxiosError.js';
 
 export * from './events/connectionEvents.js';
 export * from './events/didEvents.js';
+export * from './events/tenantEvents.js';
diff --git a/apps/ssi-abstraction/package.json b/apps/ssi-abstraction/package.json
index 0d6e996..f7e81bf 100644
--- a/apps/ssi-abstraction/package.json
+++ b/apps/ssi-abstraction/package.json
@@ -26,6 +26,7 @@
     "@aries-framework/core": "0.4.2",
     "@aries-framework/indy-vdr": "0.4.2",
     "@aries-framework/node": "0.4.2",
+    "@aries-framework/tenants": "^0.4.2",
     "@elastic/ecs-winston-format": "^1.5.0",
     "@hyperledger/anoncreds-nodejs": "^0.1.0",
     "@hyperledger/aries-askar-nodejs": "^0.1.0",
@@ -45,13 +46,13 @@
     "winston": "^3.11.0"
   },
   "devDependencies": {
+    "@nestjs/cli": "^10.2.1",
+    "@nestjs/schematics": "^10.0.3",
+    "@nestjs/testing": "^10.2.10",
     "@types/express": "^4.17.21",
     "@types/jest": "^29.5.9",
     "@types/node": "^20.9.3",
     "@types/supertest": "^2.0.16",
-    "@nestjs/cli": "^10.2.1",
-    "@nestjs/schematics": "^10.0.3",
-    "@nestjs/testing": "^10.2.10",
     "@typescript-eslint/eslint-plugin": "^6.12.0",
     "@typescript-eslint/parser": "^6.12.0",
     "eslint": "^8.54.0",
diff --git a/apps/ssi-abstraction/src/agent/__tests__/agent.controller.spec.ts b/apps/ssi-abstraction/src/agent/__tests__/agent.controller.spec.ts
deleted file mode 100644
index 5de798e..0000000
--- a/apps/ssi-abstraction/src/agent/__tests__/agent.controller.spec.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { DidDocument } from '@aries-framework/core';
-import { Test } from '@nestjs/testing';
-
-import { mockConfigModule } from '../../config/__tests__/mockConfig.js';
-import { AgentController } from '../agent.controller.js';
-import { AgentService } from '../agent.service.js';
-
-describe('AgentController', () => {
-  let agentController: AgentController;
-  let agentService: AgentService;
-
-  beforeEach(async () => {
-    const moduleRef = await Test.createTestingModule({
-      imports: [mockConfigModule()],
-      controllers: [AgentController],
-      providers: [AgentService],
-    }).compile();
-
-    agentService = moduleRef.get(AgentService);
-    agentController = moduleRef.get(AgentController);
-  });
-
-  describe('public did', () => {
-    it('should get the public did information of the agent', async () => {
-      const result = new DidDocument({ id: 'did:key:123' });
-      jest.spyOn(agentService, 'getPublicDid').mockResolvedValue(result);
-
-      const event = await agentController.publicDid();
-      expect(event.data).toMatchObject(result);
-    });
-  });
-});
diff --git a/apps/ssi-abstraction/src/agent/agent.controller.ts b/apps/ssi-abstraction/src/agent/agent.controller.ts
deleted file mode 100644
index 5469aec..0000000
--- a/apps/ssi-abstraction/src/agent/agent.controller.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { Controller } from '@nestjs/common';
-import { MessagePattern } from '@nestjs/microservices';
-import { EventInfoPublicDid } from '@ocm/shared';
-
-import { AgentService } from './agent.service.js';
-
-@Controller('agent')
-export class AgentController {
-  public constructor(private agent: AgentService) {}
-
-  @MessagePattern(EventInfoPublicDid.token)
-  public async publicDid() {
-    const didDocument = await this.agent.getPublicDid();
-
-    return new EventInfoPublicDid(didDocument);
-  }
-}
diff --git a/apps/ssi-abstraction/src/agent/agent.module.ts b/apps/ssi-abstraction/src/agent/agent.module.ts
index 23b4de0..82fefd5 100644
--- a/apps/ssi-abstraction/src/agent/agent.module.ts
+++ b/apps/ssi-abstraction/src/agent/agent.module.ts
@@ -1,13 +1,12 @@
 import { Module } from '@nestjs/common';
 import { ConfigModule } from '@nestjs/config';
 
-import { AgentController } from './agent.controller.js';
 import { AgentService } from './agent.service.js';
+import { WithTenantService } from './withTenantService.js';
 
 @Module({
   imports: [ConfigModule],
-  providers: [AgentService],
-  controllers: [AgentController],
-  exports: [AgentService],
+  providers: [AgentService, WithTenantService],
+  exports: [AgentService, WithTenantService],
 })
 export class AgentModule {}
diff --git a/apps/ssi-abstraction/src/agent/agent.service.ts b/apps/ssi-abstraction/src/agent/agent.service.ts
index ef1ee73..76b0bad 100644
--- a/apps/ssi-abstraction/src/agent/agent.service.ts
+++ b/apps/ssi-abstraction/src/agent/agent.service.ts
@@ -30,6 +30,7 @@ import {
   IndyVdrSovDidResolver,
 } from '@aries-framework/indy-vdr';
 import { agentDependencies, HttpInboundTransport } from '@aries-framework/node';
+import { TenantsModule } from '@aries-framework/tenants';
 import { anoncreds } from '@hyperledger/anoncreds-nodejs';
 import { ariesAskar } from '@hyperledger/aries-askar-nodejs';
 import { indyVdr } from '@hyperledger/indy-vdr-nodejs';
@@ -113,10 +114,16 @@ export class AgentService implements OnApplicationShutdown {
           new JwkDidResolver(),
           new WebDidResolver(),
         ],
-        registrars: [new PeerDidRegistrar(), new KeyDidRegistrar(), new JwkDidRegistrar()],
+        registrars: [
+          new PeerDidRegistrar(),
+          new KeyDidRegistrar(),
+          new JwkDidRegistrar(),
+        ],
       }),
 
       askar: new AskarModule({ ariesAskar }),
+
+      tenants: new TenantsModule(),
     };
   }
 
@@ -178,27 +185,6 @@ export class AgentService implements OnApplicationShutdown {
     }
   }
 
-  public async getPublicDid() {
-    const dids = await this.agent.dids.getCreatedDids({ method: 'indy' });
-    if (dids.length === 0) {
-      throw new Error('No registered public DIDs');
-    }
-
-    if (dids.length > 1) {
-      throw new Error('Multiple public DIDs found');
-    }
-
-    const didRecord = dids[0];
-
-    if (!didRecord.didDocument) {
-      throw new Error(
-        'A public DID was found, but did not include a DID Document',
-      );
-    }
-
-    return didRecord.didDocument;
-  }
-
   public async onModuleInit() {
     await this.agent.initialize();
     await this.registerPublicDid();
diff --git a/apps/ssi-abstraction/src/agent/connections/__tests__/connections.controller.spec.ts b/apps/ssi-abstraction/src/agent/connections/__tests__/connections.controller.spec.ts
index 2bcb88b..235511f 100644
--- a/apps/ssi-abstraction/src/agent/connections/__tests__/connections.controller.spec.ts
+++ b/apps/ssi-abstraction/src/agent/connections/__tests__/connections.controller.spec.ts
@@ -30,7 +30,9 @@ describe('ConnectionsController', () => {
       const result: Array<ConnectionRecord> = [];
       jest.spyOn(connectionsService, 'getAll').mockResolvedValue(result);
 
-      const connectionsEvent = await connectionsController.getAll();
+      const connectionsEvent = await connectionsController.getAll({
+        tenantId: 'some-id',
+      });
 
       expect(connectionsEvent.data).toStrictEqual(result);
     });
@@ -43,6 +45,7 @@ describe('ConnectionsController', () => {
 
       const connectionsEvent = await connectionsController.getById({
         id: 'id',
+        tenantId: 'some-id',
       });
 
       expect(connectionsEvent.data).toStrictEqual(result);
@@ -61,7 +64,9 @@ describe('ConnectionsController', () => {
         .mockResolvedValue(result);
 
       const connectionsEvent =
-        await connectionsController.createConnectionWithSelf();
+        await connectionsController.createConnectionWithSelf({
+          tenantId: 'some-id',
+        });
 
       expect(connectionsEvent.data).toStrictEqual(result);
     });
diff --git a/apps/ssi-abstraction/src/agent/connections/connections.controller.ts b/apps/ssi-abstraction/src/agent/connections/connections.controller.ts
index 0eb6bc7..4f04f50 100644
--- a/apps/ssi-abstraction/src/agent/connections/connections.controller.ts
+++ b/apps/ssi-abstraction/src/agent/connections/connections.controller.ts
@@ -5,6 +5,10 @@ import {
   EventDidcommConnectionsGetAll,
   EventDidcommConnectionsCreateWithSelf,
   EventDidcommConnectionsBlock,
+  EventDidcommConnectionsGetAllInput,
+  EventDidcommConnectionsGetByIdInput,
+  EventDidcommConnectionsCreateWithSelfInput,
+  EventDidcommConnectionsBlockInput,
 } from '@ocm/shared';
 
 import { ConnectionsService } from './connections.service.js';
@@ -14,38 +18,42 @@ export class ConnectionsController {
   public constructor(private connectionsService: ConnectionsService) {}
 
   @MessagePattern(EventDidcommConnectionsGetAll.token)
-  public async getAll(): Promise<EventDidcommConnectionsGetAll> {
+  public async getAll(
+    options: EventDidcommConnectionsGetAllInput,
+  ): Promise<EventDidcommConnectionsGetAll> {
     return new EventDidcommConnectionsGetAll(
-      await this.connectionsService.getAll(),
+      await this.connectionsService.getAll(options),
+      options.tenantId,
     );
   }
 
   @MessagePattern(EventDidcommConnectionsGetById.token)
-  public async getById({
-    id,
-  }: {
-    id: string;
-  }): Promise<EventDidcommConnectionsGetById> {
+  public async getById(
+    options: EventDidcommConnectionsGetByIdInput,
+  ): Promise<EventDidcommConnectionsGetById> {
     return new EventDidcommConnectionsGetById(
-      await this.connectionsService.getById(id),
+      await this.connectionsService.getById(options),
+      options.tenantId,
     );
   }
 
   @MessagePattern(EventDidcommConnectionsCreateWithSelf.token)
-  public async createConnectionWithSelf(): Promise<EventDidcommConnectionsCreateWithSelf> {
+  public async createConnectionWithSelf(
+    options: EventDidcommConnectionsCreateWithSelfInput,
+  ): Promise<EventDidcommConnectionsCreateWithSelf> {
     return new EventDidcommConnectionsCreateWithSelf(
-      await this.connectionsService.createConnectionWithSelf(),
+      await this.connectionsService.createConnectionWithSelf(options),
+      options.tenantId,
     );
   }
 
   @MessagePattern(EventDidcommConnectionsBlock.token)
-  public async blockConnection({
-    idOrDid,
-  }: {
-    idOrDid: string;
-  }): Promise<EventDidcommConnectionsBlock> {
+  public async blockConnection(
+    options: EventDidcommConnectionsBlockInput,
+  ): Promise<EventDidcommConnectionsBlock> {
     return new EventDidcommConnectionsBlock(
-      await this.connectionsService.blockByIdOrDid(idOrDid),
+      await this.connectionsService.blockByIdOrDid(options),
+      options.tenantId,
     );
   }
 }
diff --git a/apps/ssi-abstraction/src/agent/connections/connections.service.ts b/apps/ssi-abstraction/src/agent/connections/connections.service.ts
index ba72b2c..027474a 100644
--- a/apps/ssi-abstraction/src/agent/connections/connections.service.ts
+++ b/apps/ssi-abstraction/src/agent/connections/connections.service.ts
@@ -3,6 +3,12 @@ import type {
   ConnectionRecord,
   ConnectionStateChangedEvent,
 } from '@aries-framework/core';
+import type {
+  EventDidcommConnectionsBlockInput,
+  EventDidcommConnectionsCreateWithSelfInput,
+  EventDidcommConnectionsGetAllInput,
+  EventDidcommConnectionsGetByIdInput,
+} from '@ocm/shared';
 
 import {
   ConnectionEventTypes,
@@ -14,79 +20,100 @@ import { Injectable } from '@nestjs/common';
 
 import { MetadataTokens } from '../../common/constants.js';
 import { AgentService } from '../agent.service.js';
+import { WithTenantService } from '../withTenantService.js';
 
 @Injectable()
 export class ConnectionsService {
   public agent: AppAgent;
+  public withTenantService: WithTenantService;
 
-  public constructor(agentService: AgentService) {
+  public constructor(
+    agentService: AgentService,
+    withTenantService: WithTenantService,
+  ) {
     this.agent = agentService.agent;
+    this.withTenantService = withTenantService;
   }
 
-  public async getAll(): Promise<Array<ConnectionRecord>> {
-    return await this.agent.connections.getAll();
+  public async getAll({
+    tenantId,
+  }: EventDidcommConnectionsGetAllInput): Promise<Array<ConnectionRecord>> {
+    return this.withTenantService.invoke(tenantId, (t) =>
+      t.connections.getAll(),
+    );
   }
 
-  public async getById(id: string): Promise<ConnectionRecord | null> {
-    return await this.agent.connections.findById(id);
+  public async getById({
+    tenantId,
+    id,
+  }: EventDidcommConnectionsGetByIdInput): Promise<ConnectionRecord | null> {
+    return this.withTenantService.invoke(tenantId, (t) =>
+      t.connections.findById(id),
+    );
   }
 
-  public async blockByIdOrDid(
-    idOrDid: string,
-  ): Promise<ConnectionRecord | null> {
-    if (isDid(idOrDid)) {
-      const records = await this.agent.connections.findAllByQuery({
-        theirDid: idOrDid,
-      });
+  public async blockByIdOrDid({
+    tenantId,
+    idOrDid,
+  }: EventDidcommConnectionsBlockInput): Promise<ConnectionRecord | null> {
+    return this.withTenantService.invoke(tenantId, async (t) => {
+      if (isDid(idOrDid)) {
+        const records = await t.connections.findAllByQuery({
+          theirDid: idOrDid,
+        });
+
+        if (records.length === 0) {
+          return null;
+        }
+
+        if (records.length > 1) {
+          throw new Error(
+            'Found multiple records with the same DID. This should not be possible',
+          );
+        }
 
-      if (records.length === 0) {
-        return null;
-      }
+        await t.connections.deleteById(records[0].id);
 
-      if (records.length > 1) {
-        throw new Error(
-          'Found multiple records with the same DID. This should not be possible',
-        );
+        return records[0];
       }
 
-      await this.agent.connections.deleteById(records[0].id);
-
-      return records[0];
-    }
+      const record = await t.connections.findById(idOrDid);
+      if (!record) return null;
 
-    const record = await this.agent.connections.findById(idOrDid);
-    if (!record) return null;
+      await t.connections.deleteById(record.id);
 
-    await this.agent.connections.deleteById(record.id);
-
-    return record;
+      return record;
+    });
   }
 
-  public async createConnectionWithSelf(): Promise<ConnectionRecord> {
-    const outOfBandRecord = await this.agent.oob.createInvitation();
-    const invitation = outOfBandRecord.outOfBandInvitation;
-
-    void this.agent.oob.receiveInvitation(invitation);
-
-    return new Promise((resolve) =>
-      this.agent.events.on<ConnectionStateChangedEvent>(
-        ConnectionEventTypes.ConnectionStateChanged,
-        async ({ payload: { connectionRecord } }) => {
-          if (connectionRecord.state !== DidExchangeState.Completed) return;
-          connectionRecord.metadata.set(
-            MetadataTokens.GAIA_X_CONNECTION_METADATA_KEY,
-            {
-              trusted: true,
-            },
-          );
-
-          const connRepo =
-            this.agent.dependencyManager.resolve(ConnectionRepository);
-          await connRepo.update(this.agent.context, connectionRecord);
-
-          resolve(connectionRecord);
-        },
-      ),
-    );
+  public async createConnectionWithSelf({
+    tenantId,
+  }: EventDidcommConnectionsCreateWithSelfInput): Promise<ConnectionRecord> {
+    return this.withTenantService.invoke(tenantId, async (t) => {
+      const outOfBandRecord = await t.oob.createInvitation();
+      const invitation = outOfBandRecord.outOfBandInvitation;
+
+      void t.oob.receiveInvitation(invitation);
+
+      return new Promise((resolve) =>
+        this.agent.events.on<ConnectionStateChangedEvent>(
+          ConnectionEventTypes.ConnectionStateChanged,
+          async ({ payload: { connectionRecord } }) => {
+            if (connectionRecord.state !== DidExchangeState.Completed) return;
+            connectionRecord.metadata.set(
+              MetadataTokens.GAIA_X_CONNECTION_METADATA_KEY,
+              {
+                trusted: true,
+              },
+            );
+
+            const connRepo = t.dependencyManager.resolve(ConnectionRepository);
+            await connRepo.update(t.context, connectionRecord);
+
+            resolve(connectionRecord);
+          },
+        ),
+      );
+    });
   }
 }
diff --git a/apps/ssi-abstraction/src/agent/dids/__tests__/dids.controller.spec.ts b/apps/ssi-abstraction/src/agent/dids/__tests__/dids.controller.spec.ts
index c6507d0..eb513d9 100644
--- a/apps/ssi-abstraction/src/agent/dids/__tests__/dids.controller.spec.ts
+++ b/apps/ssi-abstraction/src/agent/dids/__tests__/dids.controller.spec.ts
@@ -28,6 +28,7 @@ describe('DidsController', () => {
 
       const event = await didsController.resolve({
         did: 'did:key:foo',
+        tenantId: 'some-id',
       });
 
       expect(event.data).toStrictEqual(result);
diff --git a/apps/ssi-abstraction/src/agent/dids/dids.controller.ts b/apps/ssi-abstraction/src/agent/dids/dids.controller.ts
index 70074c7..c869d0d 100644
--- a/apps/ssi-abstraction/src/agent/dids/dids.controller.ts
+++ b/apps/ssi-abstraction/src/agent/dids/dids.controller.ts
@@ -1,6 +1,11 @@
 import { Controller } from '@nestjs/common';
 import { MessagePattern } from '@nestjs/microservices';
-import { EventDidsResolve } from '@ocm/shared';
+import {
+  EventDidsPublicDid,
+  EventDidsPublicDidInput,
+  EventDidsResolve,
+  EventDidsResolveInput,
+} from '@ocm/shared';
 
 import { DidsService } from './dids.service.js';
 
@@ -8,8 +13,19 @@ import { DidsService } from './dids.service.js';
 export class DidsController {
   public constructor(private didsService: DidsService) {}
 
-  @MessagePattern('dids.resolve')
-  public async resolve({ did }: { did: string }) {
-    return new EventDidsResolve(await this.didsService.resolve(did));
+  @MessagePattern(EventDidsPublicDid.token)
+  public async publicDid(options: EventDidsPublicDidInput) {
+    return new EventDidsPublicDid(
+      await this.didsService.getPublicDid(options),
+      options.tenantId,
+    );
+  }
+
+  @MessagePattern(EventDidsResolve.token)
+  public async resolve(options: EventDidsResolveInput) {
+    return new EventDidsResolve(
+      await this.didsService.resolve(options),
+      options.tenantId,
+    );
   }
 }
diff --git a/apps/ssi-abstraction/src/agent/dids/dids.module.ts b/apps/ssi-abstraction/src/agent/dids/dids.module.ts
index d4428a9..18668fe 100644
--- a/apps/ssi-abstraction/src/agent/dids/dids.module.ts
+++ b/apps/ssi-abstraction/src/agent/dids/dids.module.ts
@@ -6,7 +6,7 @@ import { DidsController } from './dids.controller.js';
 import { DidsService } from './dids.service.js';
 
 @Module({
-  imports: [AgentModule],
+  imports: [AgentModule ],
   providers: [DidsService],
   controllers: [DidsController],
 })
diff --git a/apps/ssi-abstraction/src/agent/dids/dids.service.ts b/apps/ssi-abstraction/src/agent/dids/dids.service.ts
index d1c4e86..2adf4c4 100644
--- a/apps/ssi-abstraction/src/agent/dids/dids.service.ts
+++ b/apps/ssi-abstraction/src/agent/dids/dids.service.ts
@@ -1,31 +1,59 @@
-import type { AppAgent } from '../agent.service.js';
+import type {
+  EventDidsPublicDidInput,
+  EventDidsResolveInput,
+} from '@ocm/shared';
 
 import { Injectable } from '@nestjs/common';
 
-import { AgentService } from '../agent.service.js';
+import { WithTenantService } from '../withTenantService.js';
 
 @Injectable()
 export class DidsService {
-  public agent: AppAgent;
+  public withTenantService: WithTenantService;
 
-  public constructor(agentService: AgentService) {
-    this.agent = agentService.agent;
+  public constructor(withTenantService: WithTenantService) {
+    this.withTenantService = withTenantService;
   }
 
-  public async resolve(did: string) {
-    const {
-      didDocument,
-      didResolutionMetadata: { message, error },
-    } = await this.agent.dids.resolve(did);
-
-    if (!didDocument) {
-      throw new Error(
-        `Could not resolve did: '${did}'. Error: ${error ?? 'None'} Message: ${
-          message ?? 'None'
-        }`,
-      );
-    }
-
-    return didDocument;
+  public async resolve({ did, tenantId }: EventDidsResolveInput) {
+    return this.withTenantService.invoke(tenantId, async (t) => {
+      const {
+        didDocument,
+        didResolutionMetadata: { message, error },
+      } = await t.dids.resolve(did);
+
+      if (!didDocument) {
+        throw new Error(
+          `Could not resolve did: '${did}'. Error: ${
+            error ?? 'None'
+          } Message: ${message ?? 'None'}`,
+        );
+      }
+
+      return didDocument;
+    });
+  }
+
+  public async getPublicDid({ tenantId }: EventDidsPublicDidInput) {
+    return this.withTenantService.invoke(tenantId, async (t) => {
+      const dids = await t.dids.getCreatedDids({ method: 'indy' });
+      if (dids.length === 0) {
+        throw new Error('No registered public DIDs');
+      }
+
+      if (dids.length > 1) {
+        throw new Error('Multiple public DIDs found');
+      }
+
+      const didRecord = dids[0];
+
+      if (!didRecord.didDocument) {
+        throw new Error(
+          'A public DID was found, but did not include a DID Document',
+        );
+      }
+
+      return didRecord.didDocument;
+    });
   }
 }
diff --git a/apps/ssi-abstraction/src/agent/tenants/__tests__/tenants.controller.spec.ts b/apps/ssi-abstraction/src/agent/tenants/__tests__/tenants.controller.spec.ts
new file mode 100644
index 0000000..b9c8f97
--- /dev/null
+++ b/apps/ssi-abstraction/src/agent/tenants/__tests__/tenants.controller.spec.ts
@@ -0,0 +1,41 @@
+import { TenantRecord } from '@aries-framework/tenants';
+import { Test } from '@nestjs/testing';
+
+import { mockConfigModule } from '../../../config/__tests__/mockConfig.js';
+import { AgentModule } from '../../agent.module.js';
+import { TenantsController } from '../tenants.controller.js';
+import { TenantsService } from '../tenants.service.js';
+
+describe('TenantsController', () => {
+  let tenantsController: TenantsController;
+  let tenantsService: TenantsService;
+
+  beforeEach(async () => {
+    const moduleRef = await Test.createTestingModule({
+      imports: [mockConfigModule(), AgentModule],
+      controllers: [TenantsController],
+      providers: [TenantsService],
+    }).compile();
+
+    tenantsService = moduleRef.get(TenantsService);
+    tenantsController = moduleRef.get(TenantsController);
+  });
+
+  describe('resolve', () => {
+    it('should resolve a basic did', async () => {
+      const result = new TenantRecord({
+        config: {
+          label: 'my-label',
+          walletConfig: { key: 'some-key', id: 'some-id' },
+        },
+      });
+      jest.spyOn(tenantsService, 'create').mockResolvedValue(result);
+
+      const event = await tenantsController.create({
+        label: 'my-label',
+      });
+
+      expect(event.data).toStrictEqual(result);
+    });
+  });
+});
diff --git a/apps/ssi-abstraction/src/agent/tenants/tenants.controller.ts b/apps/ssi-abstraction/src/agent/tenants/tenants.controller.ts
new file mode 100644
index 0000000..89dc001
--- /dev/null
+++ b/apps/ssi-abstraction/src/agent/tenants/tenants.controller.ts
@@ -0,0 +1,20 @@
+import { Controller } from '@nestjs/common';
+import { MessagePattern } from '@nestjs/microservices';
+import { EventTenantsCreate, EventTenantsCreateInput } from '@ocm/shared';
+
+import { TenantsService } from './tenants.service.js';
+
+@Controller('tenants')
+export class TenantsController {
+  public constructor(private tenantsService: TenantsService) {}
+
+  @MessagePattern(EventTenantsCreate.token)
+  public async create({
+    label,
+  }: EventTenantsCreateInput): Promise<EventTenantsCreate> {
+    return new EventTenantsCreate(
+      await this.tenantsService.create(label),
+      undefined,
+    );
+  }
+}
diff --git a/apps/ssi-abstraction/src/agent/tenants/tenants.module.ts b/apps/ssi-abstraction/src/agent/tenants/tenants.module.ts
new file mode 100644
index 0000000..78df866
--- /dev/null
+++ b/apps/ssi-abstraction/src/agent/tenants/tenants.module.ts
@@ -0,0 +1,13 @@
+import { Module } from '@nestjs/common';
+
+import { AgentModule } from '../agent.module.js';
+
+import { TenantsController } from './tenants.controller.js';
+import { TenantsService } from './tenants.service.js';
+
+@Module({
+  imports: [AgentModule],
+  providers: [TenantsService],
+  controllers: [TenantsController],
+})
+export class TenantsModule {}
diff --git a/apps/ssi-abstraction/src/agent/tenants/tenants.service.ts b/apps/ssi-abstraction/src/agent/tenants/tenants.service.ts
new file mode 100644
index 0000000..784e1cf
--- /dev/null
+++ b/apps/ssi-abstraction/src/agent/tenants/tenants.service.ts
@@ -0,0 +1,18 @@
+import type { AppAgent } from '../agent.service.js';
+
+import { Injectable } from '@nestjs/common';
+
+import { AgentService } from '../agent.service.js';
+
+@Injectable()
+export class TenantsService {
+  public agent: AppAgent;
+
+  public constructor(agentService: AgentService) {
+    this.agent = agentService.agent;
+  }
+
+  public async create(label: string) {
+    return await this.agent.modules.tenants.createTenant({ config: { label } });
+  }
+}
diff --git a/apps/ssi-abstraction/src/agent/withTenantService.ts b/apps/ssi-abstraction/src/agent/withTenantService.ts
new file mode 100644
index 0000000..eacdb08
--- /dev/null
+++ b/apps/ssi-abstraction/src/agent/withTenantService.ts
@@ -0,0 +1,34 @@
+import type { AppAgent } from './agent.service.js';
+
+import { Injectable } from '@nestjs/common';
+
+import { AgentService } from './agent.service.js';
+
+@Injectable()
+export class WithTenantService {
+  private agent: AppAgent;
+
+  public constructor(agentService: AgentService) {
+    this.agent = agentService.agent;
+  }
+
+  public invoke<T>(
+    tenantId: string,
+    cb: (tenant: AppAgent) => Promise<T>,
+  ): Promise<T> {
+    // eslint-disable-next-line no-async-promise-executor
+    return new Promise<T>(async (resolve, reject) => {
+      await this.agent.modules.tenants.withTenantAgent(
+        { tenantId },
+        async (tenant) => {
+          try {
+            const ret = await cb(tenant as unknown as AppAgent);
+            resolve(ret);
+          } catch (e) {
+            reject(e);
+          }
+        },
+      );
+    });
+  }
+}
diff --git a/apps/ssi-abstraction/src/app.module.ts b/apps/ssi-abstraction/src/app.module.ts
index 42d9931..44a25ae 100644
--- a/apps/ssi-abstraction/src/app.module.ts
+++ b/apps/ssi-abstraction/src/app.module.ts
@@ -1,3 +1,4 @@
+import { DidsModule } from '@aries-framework/core';
 import { Module } from '@nestjs/common';
 import { ConfigModule } from '@nestjs/config';
 import { TerminusModule } from '@nestjs/terminus';
@@ -5,6 +6,7 @@ import { HealthController } from '@ocm/shared';
 
 import { AgentModule } from './agent/agent.module.js';
 import { ConnectionsModule } from './agent/connections/connections.module.js';
+import { TenantsModule } from './agent/tenants/tenants.module.js';
 import { config } from './config/config.js';
 import { validationSchema } from './config/validation.js';
 
@@ -18,6 +20,8 @@ import { validationSchema } from './config/validation.js';
     }),
     AgentModule,
     ConnectionsModule,
+    DidsModule,
+    TenantsModule,
   ],
   controllers: [HealthController],
 })
diff --git a/apps/ssi-abstraction/test/agent.e2e-spec.ts b/apps/ssi-abstraction/test/agent.e2e-spec.ts
deleted file mode 100644
index b66afcd..0000000
--- a/apps/ssi-abstraction/test/agent.e2e-spec.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import './setEnvVars.js';
-
-import type { INestApplication } from '@nestjs/common';
-import type { ClientProxy } from '@nestjs/microservices';
-
-import { DidDocument } from '@aries-framework/core';
-import { ClientsModule, Transport } from '@nestjs/microservices';
-import { Test } from '@nestjs/testing';
-import { EventInfoPublicDid } from '@ocm/shared';
-import { firstValueFrom, type Observable } from 'rxjs';
-
-import { AgentModule } from '../src/agent/agent.module.js';
-import { AgentService } from '../src/agent/agent.service.js';
-import { mockConfigModule } from '../src/config/__tests__/mockConfig.js';
-
-const mockDidDocument = {
-  context: ['https://w3id.org/did/v1'],
-  id: 'did:indy:bcovrin:test:7KuDTpQh3GJ7Gp6kErpWvM',
-  verificationMethod: [
-    {
-      id: 'did:indy:bcovrin:test:7KuDTpQh3GJ7Gp6kErpWvM#verkey',
-      type: 'Ed25519VerificationKey2018',
-      controller: 'did:indy:bcovrin:test:7KuDTpQh3GJ7Gp6kErpWvM',
-      publicKeyBase58: '4SySYXQUtuK26zfC7RpQpWYMThfbXphUf8LWyXXmxyTX',
-    },
-  ],
-  authentication: ['did:indy:bcovrin:test:7KuDTpQh3GJ7Gp6kErpWvM#verkey'],
-};
-
-describe('Agent', () => {
-  const TOKEN = 'AGENT_CLIENT_SERVICE';
-  let app: INestApplication;
-  let client: ClientProxy;
-
-  beforeAll(async () => {
-    jest
-      .spyOn(AgentService.prototype, 'getPublicDid')
-      .mockResolvedValue(new DidDocument(mockDidDocument));
-
-    const moduleRef = await Test.createTestingModule({
-      imports: [
-        mockConfigModule(3000),
-        AgentModule,
-        ClientsModule.register([{ name: TOKEN, transport: Transport.NATS }]),
-      ],
-    }).compile();
-
-    app = moduleRef.createNestApplication();
-
-    app.connectMicroservice({ transport: Transport.NATS });
-
-    await app.startAllMicroservices();
-    await app.init();
-
-    client = app.get(TOKEN);
-    await client.connect();
-  });
-
-  it(EventInfoPublicDid.token, async () => {
-    const response$: Observable<EventInfoPublicDid> = client.send(
-      EventInfoPublicDid.token,
-      {},
-    );
-
-    const response = await firstValueFrom(response$);
-    const eventInstance = EventInfoPublicDid.fromEvent(response);
-
-    expect(eventInstance.instance).toMatchObject(mockDidDocument);
-  });
-
-  afterAll(async () => {
-    await app.close();
-    client.close();
-  });
-});
diff --git a/apps/ssi-abstraction/test/connections.e2e-spec.ts b/apps/ssi-abstraction/test/connections.e2e-spec.ts
index 3f52769..ff9a2e4 100644
--- a/apps/ssi-abstraction/test/connections.e2e-spec.ts
+++ b/apps/ssi-abstraction/test/connections.e2e-spec.ts
@@ -1,6 +1,11 @@
 import type { INestApplication } from '@nestjs/common';
 import type { ClientProxy } from '@nestjs/microservices';
-import type { Observable } from 'rxjs';
+import type {
+  EventDidcommConnectionsGetAllInput,
+  EventDidcommConnectionsCreateWithSelfInput,
+  EventDidcommConnectionsGetByIdInput,
+  EventDidcommConnectionsBlockInput,
+} from '@ocm/shared';
 
 import { ClientsModule, Transport } from '@nestjs/microservices';
 import { Test } from '@nestjs/testing';
@@ -14,6 +19,8 @@ import { firstValueFrom } from 'rxjs';
 
 import { AgentModule } from '../src/agent/agent.module.js';
 import { ConnectionsModule } from '../src/agent/connections/connections.module.js';
+import { TenantsModule } from '../src/agent/tenants/tenants.module.js';
+import { TenantsService } from '../src/agent/tenants/tenants.service.js';
 import { MetadataTokens } from '../src/common/constants.js';
 import { mockConfigModule } from '../src/config/__tests__/mockConfig.js';
 
@@ -21,6 +28,7 @@ describe('Connections', () => {
   const TOKEN = 'CONNECTIONS_CLIENT_SERVICE';
   let app: INestApplication;
   let client: ClientProxy;
+  let tenantId: string;
 
   beforeAll(async () => {
     const moduleRef = await Test.createTestingModule({
@@ -28,6 +36,7 @@ describe('Connections', () => {
         mockConfigModule(3004),
         AgentModule,
         ConnectionsModule,
+        TenantsModule,
         ClientsModule.register([{ name: TOKEN, transport: Transport.NATS }]),
       ],
     }).compile();
@@ -41,6 +50,10 @@ describe('Connections', () => {
 
     client = app.get(TOKEN);
     await client.connect();
+
+    const ts = app.get(TenantsService);
+    const { id } = await ts.create(TOKEN);
+    tenantId = id;
   });
 
   afterAll(async () => {
@@ -49,10 +62,10 @@ describe('Connections', () => {
   });
 
   it(EventDidcommConnectionsGetAll.token, async () => {
-    const response$: Observable<EventDidcommConnectionsGetAll> = client.send(
-      EventDidcommConnectionsGetAll.token,
-      {},
-    );
+    const response$ = client.send<
+      EventDidcommConnectionsGetAll,
+      EventDidcommConnectionsGetAllInput
+    >(EventDidcommConnectionsGetAll.token, { tenantId });
     const response = await firstValueFrom(response$);
     const eventInstance = EventDidcommConnectionsGetAll.fromEvent(response);
 
@@ -60,10 +73,13 @@ describe('Connections', () => {
   });
 
   it(EventDidcommConnectionsGetById.token, async () => {
-    const response$: Observable<EventDidcommConnectionsGetById> = client.send(
-      EventDidcommConnectionsGetById.token,
-      { id: 'some-id' },
-    );
+    const response$ = client.send<
+      EventDidcommConnectionsGetById,
+      EventDidcommConnectionsGetByIdInput
+    >(EventDidcommConnectionsGetById.token, {
+      id: 'some-id',
+      tenantId,
+    });
     const response = await firstValueFrom(response$);
     const eventInstance = EventDidcommConnectionsGetById.fromEvent(response);
 
@@ -71,8 +87,12 @@ describe('Connections', () => {
   });
 
   it(EventDidcommConnectionsCreateWithSelf.token, async () => {
-    const response$: Observable<EventDidcommConnectionsCreateWithSelf> =
-      client.send(EventDidcommConnectionsCreateWithSelf.token, {});
+    const response$ = client.send<
+      EventDidcommConnectionsCreateWithSelf,
+      EventDidcommConnectionsCreateWithSelfInput
+    >(EventDidcommConnectionsCreateWithSelf.token, {
+      tenantId,
+    });
 
     const response = await firstValueFrom(response$);
     const eventInstance =
@@ -86,10 +106,13 @@ describe('Connections', () => {
   });
 
   it(EventDidcommConnectionsBlock.token, async () => {
-    const response$: Observable<EventDidcommConnectionsBlock> = client.send(
-      EventDidcommConnectionsBlock.token,
-      { idOrDid: 'some-id' },
-    );
+    const response$ = client.send<
+      EventDidcommConnectionsBlock,
+      EventDidcommConnectionsBlockInput
+    >(EventDidcommConnectionsBlock.token, {
+      idOrDid: 'some-id',
+      tenantId,
+    });
 
     const response = await firstValueFrom(response$);
     const eventInstance = EventDidcommConnectionsBlock.fromEvent(response);
diff --git a/apps/ssi-abstraction/test/dids.e2e-spec.ts b/apps/ssi-abstraction/test/dids.e2e-spec.ts
index e276f36..8f3bafe 100644
--- a/apps/ssi-abstraction/test/dids.e2e-spec.ts
+++ b/apps/ssi-abstraction/test/dids.e2e-spec.ts
@@ -1,29 +1,53 @@
 import type { INestApplication } from '@nestjs/common';
 import type { ClientProxy } from '@nestjs/microservices';
-import type { Observable } from 'rxjs';
+import type {
+  EventDidsResolveInput,
+  EventDidsPublicDidInput,
+} from '@ocm/shared';
 
+import { DidDocument } from '@aries-framework/core';
 import { ClientsModule, Transport } from '@nestjs/microservices';
 import { Test } from '@nestjs/testing';
-import {
-  EventDidsResolve,
-} from '@ocm/shared';
+import { EventDidsResolve, EventDidsPublicDid } from '@ocm/shared';
 import { firstValueFrom } from 'rxjs';
 
 import { AgentModule } from '../src/agent/agent.module.js';
 import { DidsModule } from '../src/agent/dids/dids.module.js';
+import { DidsService } from '../src/agent/dids/dids.service.js';
+import { TenantsModule } from '../src/agent/tenants/tenants.module.js';
+import { TenantsService } from '../src/agent/tenants/tenants.service.js';
 import { mockConfigModule } from '../src/config/__tests__/mockConfig.js';
 
+const mockDidDocument = {
+  context: ['https://w3id.org/did/v1'],
+  id: 'did:indy:bcovrin:test:7KuDTpQh3GJ7Gp6kErpWvM',
+  verificationMethod: [
+    {
+      id: 'did:indy:bcovrin:test:7KuDTpQh3GJ7Gp6kErpWvM#verkey',
+      type: 'Ed25519VerificationKey2018',
+      controller: 'did:indy:bcovrin:test:7KuDTpQh3GJ7Gp6kErpWvM',
+      publicKeyBase58: '4SySYXQUtuK26zfC7RpQpWYMThfbXphUf8LWyXXmxyTX',
+    },
+  ],
+  authentication: ['did:indy:bcovrin:test:7KuDTpQh3GJ7Gp6kErpWvM#verkey'],
+};
+
 describe('Dids', () => {
   const TOKEN = 'DIDS_CLIENT_SERVICE';
   let app: INestApplication;
   let client: ClientProxy;
+  let tenantId: string;
 
   beforeAll(async () => {
+    jest
+      .spyOn(DidsService.prototype, 'getPublicDid')
+      .mockResolvedValue(new DidDocument(mockDidDocument));
     const moduleRef = await Test.createTestingModule({
       imports: [
         mockConfigModule(3005),
         AgentModule,
         DidsModule,
+        TenantsModule,
         ClientsModule.register([{ name: TOKEN, transport: Transport.NATS }]),
       ],
     }).compile();
@@ -37,6 +61,10 @@ describe('Dids', () => {
 
     client = app.get(TOKEN);
     await client.connect();
+
+    const ts = app.get(TenantsService);
+    const { id } = await ts.create(TOKEN);
+    tenantId = id;
   });
 
   afterAll(async () => {
@@ -44,11 +72,24 @@ describe('Dids', () => {
     client.close();
   });
 
+  it(EventDidsPublicDid.token, async () => {
+    const response$ = client.send<EventDidsPublicDid, EventDidsPublicDidInput>(
+      EventDidsPublicDid.token,
+      { tenantId },
+    );
+
+    const response = await firstValueFrom(response$);
+    const eventInstance = EventDidsPublicDid.fromEvent(response);
+
+    expect(eventInstance.instance).toMatchObject(mockDidDocument);
+  });
+
   it(EventDidsResolve.token, async () => {
-    const response$: Observable<EventDidsResolve> = client.send(
+    const response$ = client.send<EventDidsResolve, EventDidsResolveInput>(
       EventDidsResolve.token,
       {
         did: 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH',
+        tenantId,
       },
     );
 
diff --git a/apps/ssi-abstraction/test/jest.config.js b/apps/ssi-abstraction/test/jest.config.js
index c03e51b..fb2852f 100644
--- a/apps/ssi-abstraction/test/jest.config.js
+++ b/apps/ssi-abstraction/test/jest.config.js
@@ -3,7 +3,7 @@ import config from '../jest.config.js';
 /** @type {import('jest').Config} */
 export default {
   ...config,
-  testTimeout: 12000,
+  testTimeout: 24000,
   rootDir: '.',
   testRegex: '.*\\.e2e-spec\\.ts$',
 };
diff --git a/apps/ssi-abstraction/test/tenants.e2e-spec.ts b/apps/ssi-abstraction/test/tenants.e2e-spec.ts
new file mode 100644
index 0000000..a0d372f
--- /dev/null
+++ b/apps/ssi-abstraction/test/tenants.e2e-spec.ts
@@ -0,0 +1,65 @@
+import type { INestApplication } from '@nestjs/common';
+import type { ClientProxy } from '@nestjs/microservices';
+import type { EventTenantsCreateInput } from '@ocm/shared';
+
+import { ClientsModule, Transport } from '@nestjs/microservices';
+import { Test } from '@nestjs/testing';
+import { EventTenantsCreate } from '@ocm/shared';
+import { firstValueFrom } from 'rxjs';
+
+import { AgentModule } from '../src/agent/agent.module.js';
+import { TenantsModule } from '../src/agent/tenants/tenants.module.js';
+import { mockConfigModule } from '../src/config/__tests__/mockConfig.js';
+
+describe('Tenants', () => {
+  const TOKEN = 'TENANTS_CLIENT_SERVICE';
+  let app: INestApplication;
+  let client: ClientProxy;
+
+  beforeAll(async () => {
+    const moduleRef = await Test.createTestingModule({
+      imports: [
+        mockConfigModule(3005),
+        AgentModule,
+        TenantsModule,
+        ClientsModule.register([{ name: TOKEN, transport: Transport.NATS }]),
+      ],
+    }).compile();
+
+    app = moduleRef.createNestApplication();
+
+    app.connectMicroservice({ transport: Transport.NATS });
+
+    await app.startAllMicroservices();
+    await app.init();
+
+    client = app.get(TOKEN);
+    await client.connect();
+  });
+
+  afterAll(async () => {
+    await app.close();
+    client.close();
+  });
+
+  it(EventTenantsCreate.token, async () => {
+    const response$ = client.send<EventTenantsCreate, EventTenantsCreateInput>(
+      EventTenantsCreate.token,
+      {
+        label: 'my-new-tenant',
+      },
+    );
+
+    const response = await firstValueFrom(response$);
+    const eventInstance = EventTenantsCreate.fromEvent(response);
+
+    expect(eventInstance.instance.toJSON()).toMatchObject({
+      config: {
+        label: 'my-new-tenant',
+        walletConfig: {
+          keyDerivationMethod: 'RAW',
+        },
+      },
+    });
+  });
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 24b76e3..89f27d6 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -482,6 +482,9 @@ importers:
       '@aries-framework/core':
         specifier: 0.4.2
         version: 0.4.2(expo@49.0.18)(react-native@0.72.7)
+      '@aries-framework/tenants':
+        specifier: ^0.4.2
+        version: 0.4.2(expo@49.0.18)(react-native@0.72.7)
       '@elastic/ecs-winston-format':
         specifier: ^1.5.0
         version: 1.5.0
@@ -558,6 +561,9 @@ importers:
       '@aries-framework/node':
         specifier: 0.4.2
         version: 0.4.2(expo@49.0.18)(react-native@0.72.7)
+      '@aries-framework/tenants':
+        specifier: ^0.4.2
+        version: 0.4.2(expo@49.0.18)(react-native@0.72.7)
       '@elastic/ecs-winston-format':
         specifier: ^1.5.0
         version: 1.5.0
@@ -862,6 +868,19 @@ packages:
       - web-streams-polyfill
     dev: false
 
+  /@aries-framework/tenants@0.4.2(expo@49.0.18)(react-native@0.72.7):
+    resolution: {integrity: sha512-dRgneBY4z6YAn9ieNSeLEqhW+H03aFZwnxcnWhJfSGeHKUl0kMPmjCqvpP3NFhdB/rX92U9OOZDruIv2efM2ig==}
+    dependencies:
+      '@aries-framework/core': 0.4.2(expo@49.0.18)(react-native@0.72.7)
+      async-mutex: 0.4.0
+    transitivePeerDependencies:
+      - domexception
+      - encoding
+      - expo
+      - react-native
+      - web-streams-polyfill
+    dev: false
+
   /@babel/code-frame@7.10.4:
     resolution: {integrity: sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==}
     dependencies:
@@ -5876,6 +5895,12 @@ packages:
     dev: false
     optional: true
 
+  /async-mutex@0.4.0:
+    resolution: {integrity: sha512-eJFZ1YhRR8UN8eBLoNzcDPcy/jqjsg6I1AP+KvWQX80BqOSW1oJPJXDylPUEeMr2ZQvHgnQ//Lp6f3RQ1zI7HA==}
+    dependencies:
+      tslib: 2.6.2
+    dev: false
+
   /async-value-promise@1.1.1:
     resolution: {integrity: sha512-c2RFDKjJle1rHa0YxN9Ysu97/QBu3Wa+NOejJxsX+1qVDJrkD3JL/GN1B3gaILAEXJXbu/4Z1lcoCHFESe/APA==}
     requiresBuild: true
@@ -8705,7 +8730,7 @@ packages:
       semver: 7.5.4
       tapable: 2.2.1
       typescript: 5.2.2
-      webpack: 5.89.0(@swc/core@1.3.96)
+      webpack: 5.89.0
     dev: true
 
   /form-data@3.0.1:
@@ -10044,7 +10069,7 @@ packages:
       pretty-format: 29.7.0
       slash: 3.0.0
       strip-json-comments: 3.1.1
-      ts-node: 10.9.1(@swc/core@1.3.96)(@types/node@20.9.0)(typescript@5.2.2)
+      ts-node: 10.9.1(@types/node@20.9.4)(typescript@5.3.2)
     transitivePeerDependencies:
       - babel-plugin-macros
       - supports-color
-- 
GitLab