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