From c0d6281af33277b996cb46dcb964c155fb0552ad Mon Sep 17 00:00:00 2001
From: Berend Sliedrecht <berend@animo.id>
Date: Wed, 20 Dec 2023 17:23:03 +0100
Subject: [PATCH] feat(ssi-abstraction): create and receive invitation

Signed-off-by: Berend Sliedrecht <berend@animo.id>
---
 .../src/events/__tests__/didEvents.spec.ts    | 12 +----
 apps/shared/src/events/connectionEvents.ts    | 45 ++++++++++++++++
 .../connections/connections.controller.ts     | 24 +++++++++
 .../agent/connections/connections.module.ts   |  3 +-
 .../agent/connections/connections.service.ts  | 45 ++++++++++++++++
 .../test/connections.e2e-spec.ts              | 52 +++++++++++++++++++
 6 files changed, 169 insertions(+), 12 deletions(-)

diff --git a/apps/shared/src/events/__tests__/didEvents.spec.ts b/apps/shared/src/events/__tests__/didEvents.spec.ts
index 1acd0fa..abd6787 100644
--- a/apps/shared/src/events/__tests__/didEvents.spec.ts
+++ b/apps/shared/src/events/__tests__/didEvents.spec.ts
@@ -1,22 +1,12 @@
 import { DidDocument } from '@aries-framework/core';
 
-import { EventDidsPublicDid, EventDidsResolve } from '../didEvents.js';
+import { 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');
diff --git a/apps/shared/src/events/connectionEvents.ts b/apps/shared/src/events/connectionEvents.ts
index a9ae32a..166f6cd 100644
--- a/apps/shared/src/events/connectionEvents.ts
+++ b/apps/shared/src/events/connectionEvents.ts
@@ -48,6 +48,51 @@ export class EventDidcommConnectionsGetById extends BaseEvent<ConnectionRecord |
   }
 }
 
+export type EventDidcommConnectionsCreateInvitationInput = BaseEventInput;
+export class EventDidcommConnectionsCreateInvitation extends BaseEvent<{
+  invitationUrl: string;
+}> {
+  public static token = 'didcomm.connections.createInvitation';
+
+  public get instance() {
+    return this.data;
+  }
+
+  public static fromEvent(e: EventDidcommConnectionsCreateInvitation) {
+    return new EventDidcommConnectionsCreateInvitation(
+      e.data,
+      e.tenantId,
+      e.id,
+      e.type,
+      e.timestamp,
+    );
+  }
+}
+
+export type EventDidcommConnectionsReceiveInvitationFromUrlInput =
+  BaseEventInput<{
+    invitationUrl: string;
+  }>;
+export class EventDidcommConnectionsReceiveInvitationFromUrl extends BaseEvent<ConnectionRecord | null> {
+  public static token = 'didcomm.connections.receiveInvitationFromUrl';
+
+  public get instance() {
+    return this.data
+      ? JsonTransformer.fromJSON(this.data, ConnectionRecord)
+      : null;
+  }
+
+  public static fromEvent(e: EventDidcommConnectionsReceiveInvitationFromUrl) {
+    return new EventDidcommConnectionsReceiveInvitationFromUrl(
+      e.data,
+      e.tenantId,
+      e.id,
+      e.type,
+      e.timestamp,
+    );
+  }
+}
+
 export type EventDidcommConnectionsCreateWithSelfInput = BaseEventInput;
 export class EventDidcommConnectionsCreateWithSelf extends BaseEvent<ConnectionRecord> {
   public static token = 'didcomm.connections.createWithSelf';
diff --git a/apps/ssi-abstraction/src/agent/connections/connections.controller.ts b/apps/ssi-abstraction/src/agent/connections/connections.controller.ts
index 4f04f50..8b59f12 100644
--- a/apps/ssi-abstraction/src/agent/connections/connections.controller.ts
+++ b/apps/ssi-abstraction/src/agent/connections/connections.controller.ts
@@ -9,6 +9,10 @@ import {
   EventDidcommConnectionsGetByIdInput,
   EventDidcommConnectionsCreateWithSelfInput,
   EventDidcommConnectionsBlockInput,
+  EventDidcommConnectionsCreateInvitation,
+  EventDidcommConnectionsCreateInvitationInput,
+  EventDidcommConnectionsReceiveInvitationFromUrl,
+  EventDidcommConnectionsReceiveInvitationFromUrlInput,
 } from '@ocm/shared';
 
 import { ConnectionsService } from './connections.service.js';
@@ -37,6 +41,26 @@ export class ConnectionsController {
     );
   }
 
+  @MessagePattern(EventDidcommConnectionsCreateInvitation.token)
+  public async createInvitation(
+    options: EventDidcommConnectionsCreateInvitationInput,
+  ): Promise<EventDidcommConnectionsCreateInvitation> {
+    return new EventDidcommConnectionsCreateInvitation(
+      await this.connectionsService.createInvitation(options),
+      options.tenantId,
+    );
+  }
+
+  @MessagePattern(EventDidcommConnectionsReceiveInvitationFromUrl.token)
+  public async receiveInvitationFromUrl(
+    options: EventDidcommConnectionsReceiveInvitationFromUrlInput,
+  ): Promise<EventDidcommConnectionsReceiveInvitationFromUrl> {
+    return new EventDidcommConnectionsReceiveInvitationFromUrl(
+      await this.connectionsService.receiveInvitationFromUrl(options),
+      options.tenantId,
+    );
+  }
+
   @MessagePattern(EventDidcommConnectionsCreateWithSelf.token)
   public async createConnectionWithSelf(
     options: EventDidcommConnectionsCreateWithSelfInput,
diff --git a/apps/ssi-abstraction/src/agent/connections/connections.module.ts b/apps/ssi-abstraction/src/agent/connections/connections.module.ts
index edee4f4..0689f1a 100644
--- a/apps/ssi-abstraction/src/agent/connections/connections.module.ts
+++ b/apps/ssi-abstraction/src/agent/connections/connections.module.ts
@@ -1,4 +1,5 @@
 import { Module } from '@nestjs/common';
+import { ConfigModule } from '@nestjs/config';
 
 import { AgentModule } from '../agent.module.js';
 
@@ -6,7 +7,7 @@ import { ConnectionsController } from './connections.controller.js';
 import { ConnectionsService } from './connections.service.js';
 
 @Module({
-  imports: [AgentModule],
+  imports: [AgentModule, ConfigModule],
   providers: [ConnectionsService],
   controllers: [ConnectionsController],
 })
diff --git a/apps/ssi-abstraction/src/agent/connections/connections.service.ts b/apps/ssi-abstraction/src/agent/connections/connections.service.ts
index 2e2b3c9..620b916 100644
--- a/apps/ssi-abstraction/src/agent/connections/connections.service.ts
+++ b/apps/ssi-abstraction/src/agent/connections/connections.service.ts
@@ -5,9 +5,11 @@ import type {
 } from '@aries-framework/core';
 import type {
   EventDidcommConnectionsBlockInput,
+  EventDidcommConnectionsCreateInvitationInput,
   EventDidcommConnectionsCreateWithSelfInput,
   EventDidcommConnectionsGetAllInput,
   EventDidcommConnectionsGetByIdInput,
+  EventDidcommConnectionsReceiveInvitationFromUrlInput,
 } from '@ocm/shared';
 
 import {
@@ -17,6 +19,7 @@ import {
 } from '@aries-framework/core';
 import { isDid } from '@aries-framework/core/build/utils/did.js';
 import { Injectable } from '@nestjs/common';
+import { ConfigService } from '@nestjs/config';
 
 import { MetadataTokens } from '../../common/constants.js';
 import { AgentService } from '../agent.service.js';
@@ -29,6 +32,7 @@ export class ConnectionsService {
   public constructor(
     agentService: AgentService,
     private withTenantService: WithTenantService,
+    private configService: ConfigService,
   ) {
     this.agent = agentService.agent;
   }
@@ -84,6 +88,47 @@ export class ConnectionsService {
     });
   }
 
+  public async createInvitation({
+    tenantId,
+  }: EventDidcommConnectionsCreateInvitationInput): Promise<{
+    invitationUrl: string;
+  }> {
+    const host = this.configService.get<string>('agent.host');
+    if (!host) {
+      throw new Error(
+        'Could not get the `agentHost` from the config. This is required to create an invitation',
+      );
+    }
+
+    return this.withTenantService.invoke(tenantId, async (t) => {
+      const { outOfBandInvitation } = await t.oob.createInvitation();
+
+      return {
+        invitationUrl: outOfBandInvitation.toUrl({
+          domain: host,
+        }),
+      };
+    });
+  }
+
+  public async receiveInvitationFromUrl({
+    tenantId,
+    invitationUrl,
+  }: EventDidcommConnectionsReceiveInvitationFromUrlInput): Promise<ConnectionRecord> {
+    return this.withTenantService.invoke(tenantId, async (t) => {
+      const { connectionRecord } =
+        await t.oob.receiveInvitationFromUrl(invitationUrl);
+
+      if (!connectionRecord) {
+        throw new Error(
+          'Invitation did not establish a connection. Is it a connection invitation?',
+        );
+      }
+
+      return connectionRecord;
+    });
+  }
+
   public async createConnectionWithSelf({
     tenantId,
   }: EventDidcommConnectionsCreateWithSelfInput): Promise<ConnectionRecord> {
diff --git a/apps/ssi-abstraction/test/connections.e2e-spec.ts b/apps/ssi-abstraction/test/connections.e2e-spec.ts
index ff9a2e4..cbda3ca 100644
--- a/apps/ssi-abstraction/test/connections.e2e-spec.ts
+++ b/apps/ssi-abstraction/test/connections.e2e-spec.ts
@@ -5,8 +5,11 @@ import type {
   EventDidcommConnectionsCreateWithSelfInput,
   EventDidcommConnectionsGetByIdInput,
   EventDidcommConnectionsBlockInput,
+  EventDidcommConnectionsReceiveInvitationFromUrlInput,
+  EventDidcommConnectionsCreateInvitationInput,
 } from '@ocm/shared';
 
+import { ConnectionRecord } from '@aries-framework/core';
 import { ClientsModule, Transport } from '@nestjs/microservices';
 import { Test } from '@nestjs/testing';
 import {
@@ -14,6 +17,8 @@ import {
   EventDidcommConnectionsGetAll,
   EventDidcommConnectionsCreateWithSelf,
   EventDidcommConnectionsBlock,
+  EventDidcommConnectionsReceiveInvitationFromUrl,
+  EventDidcommConnectionsCreateInvitation,
 } from '@ocm/shared';
 import { firstValueFrom } from 'rxjs';
 
@@ -86,6 +91,53 @@ describe('Connections', () => {
     expect(eventInstance.instance).toBeNull();
   });
 
+  it(EventDidcommConnectionsCreateInvitation.token, async () => {
+    const response$ = client.send<
+      EventDidcommConnectionsCreateInvitation,
+      EventDidcommConnectionsCreateInvitationInput
+    >(EventDidcommConnectionsCreateInvitation.token, {
+      tenantId,
+    });
+    const response = await firstValueFrom(response$);
+    const eventInstance =
+      EventDidcommConnectionsCreateInvitation.fromEvent(response);
+
+    expect(eventInstance.instance).toMatchObject({
+      invitationUrl: expect.any(String),
+    });
+  });
+
+  it(EventDidcommConnectionsReceiveInvitationFromUrl.token, async () => {
+    const createInvitationResponse$ = client.send<
+      EventDidcommConnectionsCreateInvitation,
+      EventDidcommConnectionsCreateInvitationInput
+    >(EventDidcommConnectionsCreateInvitation.token, {
+      tenantId,
+    });
+    const createInvitationResponse = await firstValueFrom(
+      createInvitationResponse$,
+    );
+    const createInvitationEventInstance =
+      EventDidcommConnectionsCreateInvitation.fromEvent(
+        createInvitationResponse,
+      );
+
+    const { invitationUrl } = createInvitationEventInstance.instance;
+
+    const response$ = client.send<
+      EventDidcommConnectionsReceiveInvitationFromUrl,
+      EventDidcommConnectionsReceiveInvitationFromUrlInput
+    >(EventDidcommConnectionsReceiveInvitationFromUrl.token, {
+      invitationUrl,
+      tenantId,
+    });
+    const response = await firstValueFrom(response$);
+    const eventInstance =
+      EventDidcommConnectionsReceiveInvitationFromUrl.fromEvent(response);
+
+    expect(eventInstance.instance).toBeInstanceOf(ConnectionRecord);
+  });
+
   it(EventDidcommConnectionsCreateWithSelf.token, async () => {
     const response$ = client.send<
       EventDidcommConnectionsCreateWithSelf,
-- 
GitLab