Skip to content
Snippets Groups Projects
Commit fdcce1c0 authored by Berend Sliedrecht's avatar Berend Sliedrecht
Browse files

feat(ssi): establish a trusted connection with yourself


Signed-off-by: default avatarBerend Sliedrecht <berend@animo.id>
parent f9e23977
No related branches found
No related tags found
No related merge requests found
Showing
with 304 additions and 92 deletions
import { BaseEvent, EventDidcommConnectionsGetAll } from './events.js';
import { BaseEvent } from '../baseEvents.js';
describe('check logger', () => {
describe('Base Events', () => {
it('should return module', () => {
jest.requireActual('./events');
jest.requireActual('../baseEvents');
});
it('should create a new base event', () => {
......@@ -13,17 +13,4 @@ describe('check logger', () => {
expect(baseEvent.timestamp).toBeInstanceOf(Date);
expect(baseEvent.data).toMatchObject({ some: 'data' });
});
it('should create a new connections get all event', () => {
const getAllConnectionsEvent = new EventDidcommConnectionsGetAll({
connections: [],
});
expect(typeof getAllConnectionsEvent.id).toStrictEqual('string');
expect(getAllConnectionsEvent.type).toStrictEqual(
'EventDidcommConnectionsGetAll',
);
expect(getAllConnectionsEvent.timestamp).toBeInstanceOf(Date);
expect(getAllConnectionsEvent.data).toMatchObject({ connections: [] });
});
});
import {
ConnectionRecord,
DidExchangeRole,
DidExchangeState,
} from '@aries-framework/core';
import {
EventDidcommConnectionsCreateWithSelf,
EventDidcommConnectionsGetAll,
EventDidcommConnectionsGetById,
} from '../connectionEvents.js';
describe('Connection Events', () => {
it('should return module', () => {
jest.requireActual('../connectionEvents');
});
it('should create a new connections get all event', () => {
const event = new EventDidcommConnectionsGetAll([]);
expect(typeof event.id).toStrictEqual('string');
expect(event.type).toStrictEqual('EventDidcommConnectionsGetAll');
expect(event.timestamp).toBeInstanceOf(Date);
expect(event.instance).toMatchObject([]);
});
it('should create a new connections get by id event', () => {
const event = new EventDidcommConnectionsGetById(null);
expect(typeof event.id).toStrictEqual('string');
expect(event.type).toStrictEqual('EventDidcommConnectionsGetById');
expect(event.timestamp).toBeInstanceOf(Date);
expect(event.instance).toBeNull();
});
it('should create a new connections create with self event', () => {
const event = new EventDidcommConnectionsCreateWithSelf(
new ConnectionRecord({
role: DidExchangeRole.Requester,
state: DidExchangeState.Completed,
}),
);
expect(typeof event.id).toStrictEqual('string');
expect(event.type).toStrictEqual('EventDidcommConnectionsCreateWithSelf');
expect(event.timestamp).toBeInstanceOf(Date);
expect(event.instance).toMatchObject({
role: DidExchangeRole.Requester,
state: DidExchangeState.Completed,
});
});
});
import { utils } from '@aries-framework/core';
export class BaseEvent<T = Record<string, unknown>> {
public readonly id: string;
public readonly type: string;
public readonly timestamp: Date;
public readonly data: T;
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;
}
}
import {
ConnectionRecord,
DidDocument,
JsonTransformer,
} from '@aries-framework/core';
import { BaseEvent } from './baseEvents.js';
export class EventInfoPublicDid extends BaseEvent<DidDocument> {
public static token = 'didcomm.info.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);
}
}
export class EventDidcommConnectionsGetAll extends BaseEvent<
Array<ConnectionRecord>
> {
public static token = 'didcomm.connections.getAll';
public get instance() {
return this.data.map((d) => JsonTransformer.fromJSON(d, ConnectionRecord));
}
public static fromEvent(e: EventDidcommConnectionsGetAll) {
return new EventDidcommConnectionsGetAll(e.data, e.id, e.type, e.timestamp);
}
}
export class EventDidcommConnectionsGetById extends BaseEvent<ConnectionRecord | null> {
public static token = 'didcomm.connections.getById';
public get instance() {
return this.data
? JsonTransformer.fromJSON(this.data, ConnectionRecord)
: null;
}
public static fromEvent(e: EventDidcommConnectionsGetById) {
return new EventDidcommConnectionsGetById(
e.data,
e.id,
e.type,
e.timestamp,
);
}
}
export class EventDidcommConnectionsCreateWithSelf extends BaseEvent<ConnectionRecord> {
public static token = 'didcomm.connections.createWithSelf';
public get instance() {
return JsonTransformer.fromJSON(this.data, ConnectionRecord, {
validate: true,
});
}
public static fromEvent(e: EventDidcommConnectionsCreateWithSelf) {
return new EventDidcommConnectionsCreateWithSelf(
e.data,
e.id,
e.type,
e.timestamp,
);
}
}
import type { DidDocument, ConnectionRecord } from '@aries-framework/core';
import { utils } from '@aries-framework/core';
export class BaseEvent<
T extends Record<string, unknown> = Record<string, unknown>,
> {
public id: string;
public type: string;
public timestamp: Date;
public data: T;
public constructor(data: T) {
this.id = utils.uuid();
this.type = this.constructor.name;
this.timestamp = new Date();
this.data = data;
}
}
export class EventInfoPublicDid extends BaseEvent<{
didDocument: DidDocument;
}> {}
export class EventDidcommConnectionsGetAll extends BaseEvent<{
connections: Array<ConnectionRecord>;
}> {}
export class EventDidcommConnectionsGetById extends BaseEvent<{
connection: ConnectionRecord | null;
}> {}
......@@ -4,4 +4,4 @@ export * from './health/health.controller.js';
export * from './logging/logger.js';
export * from './logging/logAxiosError.js';
export * from './events/events.js';
export * from './events/connectionEvents.js';
describe('check logger', () => {
it('should return module', () => {
jest.requireActual('./logger');
jest.requireActual('../logger');
});
});
......@@ -17,7 +17,7 @@
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest.config.js"
"test:e2e": "jest --config ./test/jest.config.js --runInBand"
},
"dependencies": {
"@aries-framework/anoncreds": "0.4.2",
......
......@@ -26,7 +26,7 @@ describe('AgentController', () => {
jest.spyOn(agentService, 'getPublicDid').mockResolvedValue(result);
const event = await agentController.publicDid();
expect(event.data).toMatchObject({ didDocument: result });
expect(event.data).toMatchObject(result);
});
});
});
......@@ -8,10 +8,10 @@ import { AgentService } from './agent.service.js';
export class AgentController {
public constructor(private agent: AgentService) {}
@MessagePattern('info.publicDid')
@MessagePattern(EventInfoPublicDid.token)
public async publicDid() {
const didDocument = await this.agent.getPublicDid();
return new EventInfoPublicDid({ didDocument });
return new EventInfoPublicDid(didDocument);
}
}
import type { LedgerIds } from '../config/ledger.js';
import type { InitConfig } from '@aries-framework/core';
import type { IndyVdrPoolConfig } from '@aries-framework/indy-vdr';
import type { OnApplicationShutdown } from '@nestjs/common';
import { AnonCredsModule } from '@aries-framework/anoncreds';
import { AnonCredsRsModule } from '@aries-framework/anoncreds-rs';
......@@ -41,7 +42,7 @@ import { AgentLogger } from './logger.js';
export type AppAgent = Agent<AgentService['modules']>;
@Injectable()
export class AgentService {
export class AgentService implements OnApplicationShutdown {
public agent: AppAgent;
private configService: ConfigService;
......@@ -50,7 +51,6 @@ export class AgentService {
this.configService = configService;
const inboundPort = this.configService.get('agent.inboundPort');
this.agent = new Agent({
config: this.config,
modules: this.modules,
......@@ -70,7 +70,7 @@ export class AgentService {
const { name, walletId, walletKey, host, inboundPort, path } =
this.configService.get('agent');
const endpoints = [`${host}${inboundPort}${path}`];
const endpoints = [`${host}:${inboundPort}${path}`];
return {
label: name,
......@@ -200,7 +200,9 @@ export class AgentService {
logger.info('Agent initialized');
}
public async onModuleDestory() {
public async onApplicationShutdown() {
if (!this.agent.isInitialized) return;
await this.agent.shutdown();
}
}
import type { ConnectionRecord } from '@aries-framework/core';
import {
ConnectionRecord,
DidExchangeRole,
DidExchangeState,
} from '@aries-framework/core';
import { Test } from '@nestjs/testing';
import { mockConfigModule } from '../../../config/__tests__/mockConfig.js';
......@@ -25,28 +28,42 @@ describe('ConnectionsController', () => {
describe('get all', () => {
it('should get all the connection records of the agent', async () => {
const result: Array<ConnectionRecord> = [];
jest
.spyOn(connectionsService, 'getAll')
.mockImplementation(() => Promise.resolve(result));
jest.spyOn(connectionsService, 'getAll').mockResolvedValue(result);
const connectionsEvent = await connectionsController.getAll();
expect(connectionsEvent.data).toStrictEqual({ connections: result });
expect(connectionsEvent.data).toStrictEqual(result);
});
});
describe('get by id', () => {
it('should get a connection record by id', async () => {
const result: ConnectionRecord | null = null;
jest
.spyOn(connectionsService, 'getById')
.mockImplementation(() => Promise.resolve(result));
jest.spyOn(connectionsService, 'getById').mockResolvedValue(result);
const connectionsEvent = await connectionsController.getById({
id: 'id',
});
expect(connectionsEvent.data).toStrictEqual({ connection: result });
expect(connectionsEvent.data).toStrictEqual(result);
});
});
describe('create connection with self', () => {
it('should create a connection with itself', async () => {
const result: ConnectionRecord = new ConnectionRecord({
state: DidExchangeState.Completed,
role: DidExchangeRole.Requester,
});
jest
.spyOn(connectionsService, 'createConnectionWithSelf')
.mockResolvedValue(result);
const connectionsEvent =
await connectionsController.createConnectionWithSelf();
expect(connectionsEvent.data).toStrictEqual(result);
});
});
});
......@@ -3,6 +3,7 @@ import { MessagePattern } from '@nestjs/microservices';
import {
EventDidcommConnectionsGetById,
EventDidcommConnectionsGetAll,
EventDidcommConnectionsCreateWithSelf,
} from '@ocm/shared';
import { ConnectionsService } from './connections.service.js';
......@@ -11,21 +12,28 @@ import { ConnectionsService } from './connections.service.js';
export class ConnectionsController {
public constructor(private connectionsService: ConnectionsService) {}
@MessagePattern('didcomm.connections.getAll')
@MessagePattern(EventDidcommConnectionsGetAll.token)
public async getAll(): Promise<EventDidcommConnectionsGetAll> {
return new EventDidcommConnectionsGetAll({
connections: await this.connectionsService.getAll(),
});
return new EventDidcommConnectionsGetAll(
await this.connectionsService.getAll(),
);
}
@MessagePattern('didcomm.connections.getById')
@MessagePattern(EventDidcommConnectionsGetById.token)
public async getById({
id,
}: {
id: string;
}): Promise<EventDidcommConnectionsGetById> {
return new EventDidcommConnectionsGetById({
connection: await this.connectionsService.getById(id),
});
return new EventDidcommConnectionsGetById(
await this.connectionsService.getById(id),
);
}
@MessagePattern(EventDidcommConnectionsCreateWithSelf.token)
public async createConnectionWithSelf(): Promise<EventDidcommConnectionsCreateWithSelf> {
return new EventDidcommConnectionsCreateWithSelf(
await this.connectionsService.createConnectionWithSelf(),
);
}
}
import type { AppAgent } from '../agent.service.js';
import type { ConnectionRecord } from '@aries-framework/core';
import type {
ConnectionRecord,
ConnectionStateChangedEvent,
} from '@aries-framework/core';
import {
ConnectionEventTypes,
ConnectionRepository,
DidExchangeState,
} from '@aries-framework/core';
import { Injectable } from '@nestjs/common';
import { MetadataTokens } from '../../common/constants.js';
import { AgentService } from '../agent.service.js';
@Injectable()
......@@ -20,4 +29,32 @@ export class ConnectionsService {
public async getById(id: string): Promise<ConnectionRecord | null> {
return await this.agent.connections.findById(id);
}
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);
},
),
);
}
}
export enum NATSServices {
SERVICE_NAME = 'SSI_ABSTRACTION_SERVICE',
}
export enum MetadataTokens {
GAIA_X_CONNECTION_METADATA_KEY = 'gaia_x_connection_metadata_key',
}
import type { AppConfig } from '../config.js';
import { AutoAcceptCredential } from '@aries-framework/core';
import { AutoAcceptCredential, utils } from '@aries-framework/core';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { validationSchema } from '../validation.js';
const mockConfig = (port: number = 3001): AppConfig => ({
agentHost: '',
port:3000,
port: 3000,
jwtSecret: '',
nats: {
url: 'localhost',
},
agent: {
name: 'my-test-agent',
walletId: 'some-id',
walletId: utils.uuid(),
walletKey: 'some-key',
ledgerIds: [],
host: '3000',
host: 'http://localhost',
inboundPort: port,
path: '',
publicDidSeed: '',
autoAcceptConnection: false,
autoAcceptConnection: true,
autoAcceptCredential: AutoAcceptCredential.ContentApproved,
},
});
......@@ -50,7 +50,7 @@ describe('configuration', () => {
it('should be able to extract root value as object', () => {
const configuration = new ConfigService(mockConfig());
expect(configuration.get('agent')).toMatchObject(mockedConfig.agent);
expect(configuration.get('agent')).toHaveProperty('name');
});
it('should be able to extract nested values', () => {
......
......@@ -38,7 +38,7 @@ export const config = (): AppConfig => ({
walletKey: process.env.AGENT_WALLET_KEY || '',
ledgerIds: process.env.AGENT_LEDGER_ID?.split(','),
host: process.env.AGENT_HOST || '',
inboundPort: parseInt(process.env.AGENT_INBOUND_PORT || '3001'),
inboundPort: Number(process.env.AGENT_INBOUND_PORT || '3001'),
path: process.env.AGENT_URL_PATH || '',
publicDidSeed: process.env.AGENT_PUBLIC_DID_SEED || '',
autoAcceptConnection: process.env.AGENT_AUTO_ACCEPT_CONNECTION === 'true',
......
import './setEnvVars.js';
import type { INestApplication } from '@nestjs/common';
import type { ClientProxy } from '@nestjs/microservices';
import type { EventInfoPublicDid } from '@ocm/shared';
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';
......@@ -12,7 +14,7 @@ import { AgentService } from '../src/agent/agent.service.js';
import { mockConfigModule } from '../src/config/__tests__/mockConfig.js';
const mockDidDocument = {
'@context': ['https://w3id.org/did/v1'],
context: ['https://w3id.org/did/v1'],
id: 'did:indy:bcovrin:test:7KuDTpQh3GJ7Gp6kErpWvM',
verificationMethod: [
{
......@@ -33,9 +35,7 @@ describe('Agent', () => {
beforeAll(async () => {
jest
.spyOn(AgentService.prototype, 'getPublicDid')
.mockImplementation(() =>
Promise.resolve(new DidDocument(mockDidDocument)),
);
.mockResolvedValue(new DidDocument(mockDidDocument));
const moduleRef = await Test.createTestingModule({
imports: [
......@@ -56,17 +56,16 @@ describe('Agent', () => {
await client.connect();
});
it('info.publicDid', async () => {
it(EventInfoPublicDid.token, async () => {
const response$: Observable<EventInfoPublicDid> = client.send(
'info.publicDid',
EventInfoPublicDid.token,
{},
);
const response = await firstValueFrom(response$);
const eventInstance = EventInfoPublicDid.fromEvent(response);
expect(response.data).toMatchObject({
didDocument: mockDidDocument,
});
expect(eventInstance.instance).toMatchObject(mockDidDocument);
});
afterAll(async () => {
......
import type { INestApplication } from '@nestjs/common';
import type { ClientProxy } from '@nestjs/microservices';
import type {
EventDidcommConnectionsGetById,
EventDidcommConnectionsGetAll,
} from '@ocm/shared';
import type { Observable } from 'rxjs';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { Test } from '@nestjs/testing';
import { firstValueFrom, type Observable } from 'rxjs';
import {
EventDidcommConnectionsGetById,
EventDidcommConnectionsGetAll,
EventDidcommConnectionsCreateWithSelf,
} from '@ocm/shared';
import { firstValueFrom } from 'rxjs';
import { AgentModule } from '../src/agent/agent.module.js';
import { ConnectionsModule } from '../src/agent/connections/connections.module.js';
import { MetadataTokens } from '../src/common/constants.js';
import { mockConfigModule } from '../src/config/__tests__/mockConfig.js';
describe('Connections', () => {
......@@ -39,26 +42,45 @@ describe('Connections', () => {
await client.connect();
});
it('didcomm.connections.getAll', async () => {
afterAll(async () => {
await app.close();
client.close();
});
it(EventDidcommConnectionsGetAll.token, async () => {
const response$: Observable<EventDidcommConnectionsGetAll> = client.send(
'didcomm.connections.getAll',
EventDidcommConnectionsGetAll.token,
{},
);
const response = await firstValueFrom(response$);
expect(response.data).toMatchObject({ connections: [] });
const eventInstance = EventDidcommConnectionsGetAll.fromEvent(response);
expect(eventInstance.instance).toEqual(expect.arrayContaining([]));
});
it('didcomm.connections.getById', async () => {
it(EventDidcommConnectionsGetById.token, async () => {
const response$: Observable<EventDidcommConnectionsGetById> = client.send(
'didcomm.connections.getById',
EventDidcommConnectionsGetById.token,
{ id: 'some-id' },
);
const response = await firstValueFrom(response$);
expect(response.data).toMatchObject({ connection: null });
const eventInstance = EventDidcommConnectionsGetById.fromEvent(response);
expect(eventInstance.instance).toBeNull();
});
afterAll(async () => {
await app.close();
client.close();
it(EventDidcommConnectionsCreateWithSelf.token, async () => {
const response$: Observable<EventDidcommConnectionsCreateWithSelf> =
client.send(EventDidcommConnectionsCreateWithSelf.token, {});
const response = await firstValueFrom(response$);
const eventInstance =
EventDidcommConnectionsCreateWithSelf.fromEvent(response);
expect(eventInstance.instance).toHaveProperty('id');
const metadata = eventInstance.instance.metadata.get(
MetadataTokens.GAIA_X_CONNECTION_METADATA_KEY,
);
expect(metadata).toMatchObject({ trusted: true });
});
});
......@@ -5,7 +5,7 @@ process.env.NATS_URL = 'nats://localhost:4222';
process.env.ECSURL = 'http://localhost:9200/';
process.env.AGENT_HOST = 'http://localhost';
process.env.AGENT_NAME = 'ssi-abstraction-agent';
process.env.AGENT_INBOUND_PORT = ':4000';
process.env.AGENT_INBOUND_PORT = 4000;
process.env.AGENT_URL_PATH = '/ocm/abstraction';
process.env.AGENT_PUBLIC_DID_SEED = '6b8b882e2618fa5d45ee7229ca880083';
process.env.AGENT_AUTO_ACCEPT_CONNECTION = true;
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment