From 547da0a04c28813be1b97ba07055ef583e70ed0d Mon Sep 17 00:00:00 2001 From: Berend Sliedrecht <berend@animo.id> Date: Mon, 4 Dec 2023 12:58:32 +0100 Subject: [PATCH] fix(ssi): do not register did by default but make it an event Signed-off-by: Berend Sliedrecht <berend@animo.id> --- .../src/events/credentialDefinitionEvents.ts | 74 ++++++++++ apps/shared/src/index.ts | 1 + .../credentialDefinitions.controller.spec.ts | 84 +++++++++++ .../credentialDefinitions.controller.ts | 49 +++++++ .../credentialDefinitions.module.ts | 13 ++ .../credentialDefinitions.service.ts | 88 +++++++++++ .../__tests__/schemas.controller.spec.ts | 2 +- .../src/agent/schemas/schemas.service.ts | 8 +- apps/ssi-abstraction/src/app.module.ts | 3 + .../test/credentialDefinitions.e2e-spec.ts | 137 ++++++++++++++++++ apps/ssi-abstraction/test/schemas.e2e-spec.ts | 2 +- 11 files changed, 456 insertions(+), 5 deletions(-) create mode 100644 apps/shared/src/events/credentialDefinitionEvents.ts create mode 100644 apps/ssi-abstraction/src/agent/credentialDefinitions/__tests__/credentialDefinitions.controller.spec.ts create mode 100644 apps/ssi-abstraction/src/agent/credentialDefinitions/credentialDefinitions.controller.ts create mode 100644 apps/ssi-abstraction/src/agent/credentialDefinitions/credentialDefinitions.module.ts create mode 100644 apps/ssi-abstraction/src/agent/credentialDefinitions/credentialDefinitions.service.ts create mode 100644 apps/ssi-abstraction/test/credentialDefinitions.e2e-spec.ts diff --git a/apps/shared/src/events/credentialDefinitionEvents.ts b/apps/shared/src/events/credentialDefinitionEvents.ts new file mode 100644 index 0000000..cc5741a --- /dev/null +++ b/apps/shared/src/events/credentialDefinitionEvents.ts @@ -0,0 +1,74 @@ +import type { BaseEventInput } from './baseEvents.js'; +import type { AnonCredsCredentialDefinition } from '@aries-framework/anoncreds'; + +import { BaseEvent } from './baseEvents.js'; + +export type CredentialDefinitionWithId = AnonCredsCredentialDefinition & { + credentialDefinitionId: string; +}; + +export type EventAnonCredsCredentialDefinitionsGetAllInput = BaseEventInput; +export class EventAnonCredsCredentialDefinitionsGetAll extends BaseEvent< + Array<CredentialDefinitionWithId> +> { + public static token = 'anoncreds.credentialDefinitions.getAll'; + + public get instance() { + return this.data; + } + + public static fromEvent(e: EventAnonCredsCredentialDefinitionsGetAll) { + return new EventAnonCredsCredentialDefinitionsGetAll( + e.data, + e.tenantId, + e.id, + e.type, + e.timestamp, + ); + } +} + +export type EventAnonCredsCredentialDefinitionsGetByIdInput = BaseEventInput<{ + credentialDefinitionId: string; +}>; +export class EventAnonCredsCredentialDefinitionsGetById extends BaseEvent<CredentialDefinitionWithId | null> { + public static token = 'anoncreds.credentialDefinitions.getById'; + + public get instance() { + return this.data; + } + + public static fromEvent(e: EventAnonCredsCredentialDefinitionsGetById) { + return new EventAnonCredsCredentialDefinitionsGetById( + e.data, + e.tenantId, + e.id, + e.type, + e.timestamp, + ); + } +} + +export type EventAnonCredsCredentialDefinitionsRegisterInput = BaseEventInput<{ + schemaId: string; + tag: string; + issuerDid: string; +}>; + +export class EventAnonCredsCredentialDefinitionsRegister extends BaseEvent<CredentialDefinitionWithId> { + public static token = 'anoncreds.credentialDefinitions.register'; + + public get instance() { + return this.data; + } + + public static fromEvent(e: EventAnonCredsCredentialDefinitionsRegister) { + return new EventAnonCredsCredentialDefinitionsRegister( + e.data, + e.tenantId, + e.id, + e.type, + e.timestamp, + ); + } +} diff --git a/apps/shared/src/index.ts b/apps/shared/src/index.ts index 7f8ae5e..8fa24d3 100644 --- a/apps/shared/src/index.ts +++ b/apps/shared/src/index.ts @@ -8,3 +8,4 @@ export * from './events/connectionEvents.js'; export * from './events/didEvents.js'; export * from './events/tenantEvents.js'; export * from './events/schemaEvents.js'; +export * from './events/credentialDefinitionEvents.js'; diff --git a/apps/ssi-abstraction/src/agent/credentialDefinitions/__tests__/credentialDefinitions.controller.spec.ts b/apps/ssi-abstraction/src/agent/credentialDefinitions/__tests__/credentialDefinitions.controller.spec.ts new file mode 100644 index 0000000..b90afbc --- /dev/null +++ b/apps/ssi-abstraction/src/agent/credentialDefinitions/__tests__/credentialDefinitions.controller.spec.ts @@ -0,0 +1,84 @@ +import type { AnonCredsCredentialDefinition } from '@aries-framework/anoncreds'; + +import { Test } from '@nestjs/testing'; + +import { mockConfigModule } from '../../../config/__tests__/mockConfig.js'; +import { AgentModule } from '../../agent.module.js'; +import { CredentialDefinitionsController } from '../credentialDefinitions.controller.js'; +import { CredentialDefinitionsService } from '../credentialDefinitions.service.js'; + +describe('CredentialDefinitionsController', () => { + let credentialDefinitionsController: CredentialDefinitionsController; + let credentialDefinitionsService: CredentialDefinitionsService; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [mockConfigModule(), AgentModule], + controllers: [CredentialDefinitionsController], + providers: [CredentialDefinitionsService], + }).compile(); + + credentialDefinitionsService = moduleRef.get(CredentialDefinitionsService); + credentialDefinitionsController = moduleRef.get( + CredentialDefinitionsController, + ); + }); + + describe('get all', () => { + it('should get all the registered credentialDefinitions of the agent', async () => { + const result: Array<AnonCredsCredentialDefinition> = []; + jest + .spyOn(credentialDefinitionsService, 'getAll') + .mockResolvedValue(result); + + const event = await credentialDefinitionsController.getAll({ + tenantId: 'some-id', + }); + + expect(event.data).toStrictEqual(result); + }); + }); + + describe('get by id', () => { + it('should get a credentialDefinition by id', async () => { + const result: AnonCredsCredentialDefinition | null = null; + jest + .spyOn(credentialDefinitionsService, 'getById') + .mockResolvedValue(result); + + const event = await credentialDefinitionsController.getById({ + credentialDefinitionId: 'id', + tenantId: 'some-id', + }); + + expect(event.data).toStrictEqual(result); + }); + }); + + describe('register credentialDefinition', () => { + it('should register a credentialDefinition on a ledger', async () => { + const result: AnonCredsCredentialDefinition = { + tag: 'some-tag', + type: 'CL', + issuerId: 'did:indy:issuer', + schemaId: 'schemaid:123:default', + value: { + primary: {}, + }, + }; + + jest + .spyOn(credentialDefinitionsService, 'register') + .mockResolvedValue(result); + + const event = await credentialDefinitionsController.register({ + tenantId: 'some-tenant-id', + tag: 'some-tag', + issuerDid: 'did:indy:issuer', + schemaId: 'schemaid:123:default', + }); + + expect(event.data).toStrictEqual(result); + }); + }); +}); diff --git a/apps/ssi-abstraction/src/agent/credentialDefinitions/credentialDefinitions.controller.ts b/apps/ssi-abstraction/src/agent/credentialDefinitions/credentialDefinitions.controller.ts new file mode 100644 index 0000000..2e75b94 --- /dev/null +++ b/apps/ssi-abstraction/src/agent/credentialDefinitions/credentialDefinitions.controller.ts @@ -0,0 +1,49 @@ +import { Controller } from '@nestjs/common'; +import { MessagePattern } from '@nestjs/microservices'; +import { + EventAnonCredsCredentialDefinitionsGetAll, + EventAnonCredsCredentialDefinitionsGetAllInput, + EventAnonCredsCredentialDefinitionsGetById, + EventAnonCredsCredentialDefinitionsGetByIdInput, + EventAnonCredsCredentialDefinitionsRegister, + EventAnonCredsCredentialDefinitionsRegisterInput, +} from '@ocm/shared'; + +import { CredentialDefinitionsService } from './credentialDefinitions.service.js'; + +@Controller('credentialDefinitions') +export class CredentialDefinitionsController { + public constructor( + private credentialDefinitionsService: CredentialDefinitionsService, + ) {} + + @MessagePattern(EventAnonCredsCredentialDefinitionsGetAll.token) + public async getAll( + options: EventAnonCredsCredentialDefinitionsGetAllInput, + ): Promise<EventAnonCredsCredentialDefinitionsGetAll> { + return new EventAnonCredsCredentialDefinitionsGetAll( + await this.credentialDefinitionsService.getAll(options), + options.tenantId, + ); + } + + @MessagePattern(EventAnonCredsCredentialDefinitionsGetById.token) + public async getById( + options: EventAnonCredsCredentialDefinitionsGetByIdInput, + ): Promise<EventAnonCredsCredentialDefinitionsGetById> { + return new EventAnonCredsCredentialDefinitionsGetById( + await this.credentialDefinitionsService.getById(options), + options.tenantId, + ); + } + + @MessagePattern(EventAnonCredsCredentialDefinitionsRegister.token) + public async register( + options: EventAnonCredsCredentialDefinitionsRegisterInput, + ): Promise<EventAnonCredsCredentialDefinitionsRegister> { + return new EventAnonCredsCredentialDefinitionsRegister( + await this.credentialDefinitionsService.register(options), + options.tenantId, + ); + } +} diff --git a/apps/ssi-abstraction/src/agent/credentialDefinitions/credentialDefinitions.module.ts b/apps/ssi-abstraction/src/agent/credentialDefinitions/credentialDefinitions.module.ts new file mode 100644 index 0000000..0bff1d8 --- /dev/null +++ b/apps/ssi-abstraction/src/agent/credentialDefinitions/credentialDefinitions.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; + +import { AgentModule } from '../agent.module.js'; + +import { CredentialDefinitionsController } from './credentialDefinitions.controller.js'; +import { CredentialDefinitionsService } from './credentialDefinitions.service.js'; + +@Module({ + imports: [AgentModule], + providers: [CredentialDefinitionsService], + controllers: [CredentialDefinitionsController], +}) +export class CredentialDefinitionsModule {} diff --git a/apps/ssi-abstraction/src/agent/credentialDefinitions/credentialDefinitions.service.ts b/apps/ssi-abstraction/src/agent/credentialDefinitions/credentialDefinitions.service.ts new file mode 100644 index 0000000..32b2b61 --- /dev/null +++ b/apps/ssi-abstraction/src/agent/credentialDefinitions/credentialDefinitions.service.ts @@ -0,0 +1,88 @@ +import type { AnonCredsCredentialDefinition } from '@aries-framework/anoncreds'; +import type { IndyVdrRegisterCredentialDefinitionOptions } from '@aries-framework/indy-vdr'; +import type { + EventAnonCredsCredentialDefinitionsGetAllInput, + EventAnonCredsCredentialDefinitionsGetByIdInput, + EventAnonCredsCredentialDefinitionsRegisterInput, +} from '@ocm/shared'; + +import { Injectable } from '@nestjs/common'; + +import { WithTenantService } from '../withTenantService.js'; + +@Injectable() +export class CredentialDefinitionsService { + public withTenantService: WithTenantService; + + public constructor(withTenantService: WithTenantService) { + this.withTenantService = withTenantService; + } + + public async getAll({ + tenantId, + }: EventAnonCredsCredentialDefinitionsGetAllInput): Promise< + Array<AnonCredsCredentialDefinition> + > { + return this.withTenantService.invoke(tenantId, async (t) => + (await t.modules.anoncreds.getCreatedCredentialDefinitions({})).map( + (r) => r.credentialDefinition, + ), + ); + } + + public async getById({ + tenantId, + credentialDefinitionId, + }: EventAnonCredsCredentialDefinitionsGetByIdInput): Promise<AnonCredsCredentialDefinition | null> { + return this.withTenantService.invoke(tenantId, async (t) => { + const { credentialDefinition } = + await t.modules.anoncreds.getCredentialDefinition( + credentialDefinitionId, + ); + return credentialDefinition ?? null; + }); + } + + public async register({ + tenantId, + schemaId, + issuerDid, + tag, + }: EventAnonCredsCredentialDefinitionsRegisterInput): Promise< + AnonCredsCredentialDefinition & { credentialDefinitionId: string } + > { + return this.withTenantService.invoke(tenantId, async (t) => { + const { credentialDefinitionState } = + await t.modules.anoncreds.registerCredentialDefinition<IndyVdrRegisterCredentialDefinitionOptions>( + { + credentialDefinition: { + issuerId: issuerDid, + type: 'CL', + schemaId, + tag, + }, + options: { + endorserMode: 'internal', + endorserDid: issuerDid, + }, + }, + ); + + if (credentialDefinitionState.state !== 'finished') { + throw new Error( + `Error registering credentialDefinition: ${ + credentialDefinitionState.state === 'failed' + ? credentialDefinitionState.reason + : 'Not Finished' + }`, + ); + } + + return { + credentialDefinitionId: + credentialDefinitionState.credentialDefinitionId, + ...credentialDefinitionState.credentialDefinition, + }; + }); + } +} diff --git a/apps/ssi-abstraction/src/agent/schemas/__tests__/schemas.controller.spec.ts b/apps/ssi-abstraction/src/agent/schemas/__tests__/schemas.controller.spec.ts index d761a21..95fb19a 100644 --- a/apps/ssi-abstraction/src/agent/schemas/__tests__/schemas.controller.spec.ts +++ b/apps/ssi-abstraction/src/agent/schemas/__tests__/schemas.controller.spec.ts @@ -7,7 +7,7 @@ import { AgentModule } from '../../agent.module.js'; import { SchemasController } from '../schemas.controller.js'; import { SchemasService } from '../schemas.service.js'; -describe('ConnectionsController', () => { +describe('SchemassController', () => { let schemasController: SchemasController; let schemasService: SchemasService; diff --git a/apps/ssi-abstraction/src/agent/schemas/schemas.service.ts b/apps/ssi-abstraction/src/agent/schemas/schemas.service.ts index da3a7dc..e72d2c4 100644 --- a/apps/ssi-abstraction/src/agent/schemas/schemas.service.ts +++ b/apps/ssi-abstraction/src/agent/schemas/schemas.service.ts @@ -42,7 +42,9 @@ export class SchemasService { version, issuerDid, attributeNames, - }: EventAnonCredsSchemasRegisterInput): Promise<AnonCredsSchema> { + }: EventAnonCredsSchemasRegisterInput): Promise< + AnonCredsSchema & { schemaId: string } + > { return this.withTenantService.invoke(tenantId, async (t) => { const { schemaState } = await t.modules.anoncreds.registerSchema<IndyVdrRegisterSchemaOptions>({ @@ -53,7 +55,7 @@ export class SchemasService { attrNames: attributeNames, }, options: { - endorserMode: 'external', + endorserMode: 'internal', endorserDid: issuerDid, }, }); @@ -66,7 +68,7 @@ export class SchemasService { ); } - return schemaState.schema; + return { schemaId: schemaState.schemaId, ...schemaState.schema }; }); } } diff --git a/apps/ssi-abstraction/src/app.module.ts b/apps/ssi-abstraction/src/app.module.ts index 52662f0..876afc5 100644 --- a/apps/ssi-abstraction/src/app.module.ts +++ b/apps/ssi-abstraction/src/app.module.ts @@ -6,6 +6,7 @@ import { HealthController } from '@ocm/shared'; import { AgentModule } from './agent/agent.module.js'; import { ConnectionsModule } from './agent/connections/connections.module.js'; +import { CredentialDefinitionsModule } from './agent/credentialDefinitions/credentialDefinitions.module.js'; import { SchemasModule } from './agent/schemas/schemas.module.js'; import { TenantsModule } from './agent/tenants/tenants.module.js'; import { config } from './config/config.js'; @@ -21,6 +22,8 @@ import { validationSchema } from './config/validation.js'; }), AgentModule, ConnectionsModule, + SchemasModule, + CredentialDefinitionsModule, DidsModule, SchemasModule, TenantsModule, diff --git a/apps/ssi-abstraction/test/credentialDefinitions.e2e-spec.ts b/apps/ssi-abstraction/test/credentialDefinitions.e2e-spec.ts new file mode 100644 index 0000000..2f87be6 --- /dev/null +++ b/apps/ssi-abstraction/test/credentialDefinitions.e2e-spec.ts @@ -0,0 +1,137 @@ +import type { INestApplication } from '@nestjs/common'; +import type { ClientProxy } from '@nestjs/microservices'; +import type { + EventAnonCredsCredentialDefinitionsGetAllInput, + EventAnonCredsCredentialDefinitionsGetByIdInput, + EventAnonCredsCredentialDefinitionsRegisterInput, +} from '@ocm/shared'; + +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { Test } from '@nestjs/testing'; +import { + EventAnonCredsCredentialDefinitionsGetById, + EventAnonCredsCredentialDefinitionsGetAll, + EventAnonCredsCredentialDefinitionsRegister, +} from '@ocm/shared'; +import { firstValueFrom } from 'rxjs'; + +import { AgentModule } from '../src/agent/agent.module.js'; +import { CredentialDefinitionsModule } from '../src/agent/credentialDefinitions/credentialDefinitions.module.js'; +import { DidsModule } from '../src/agent/dids/dids.module.js'; +import { DidsService } from '../src/agent/dids/dids.service.js'; +import { SchemasModule } from '../src/agent/schemas/schemas.module.js'; +import { SchemasService } from '../src/agent/schemas/schemas.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'; + +describe('CredentialDefinitions', () => { + const TOKEN = 'CREDENTIAL_DEFINITIONS_CLIENT_SERVICE'; + let app: INestApplication; + let client: ClientProxy; + let tenantId: string; + + let issuerDid: string; + let schemaId: string; + + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + mockConfigModule(3004, true), + AgentModule, + SchemasModule, + CredentialDefinitionsModule, + TenantsModule, + DidsModule, + 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(); + + const tenantsService = app.get(TenantsService); + const { id } = await tenantsService.create(TOKEN); + tenantId = id; + + const didsService = app.get(DidsService); + const [did] = await didsService.registerDidIndyFromSeed({ + tenantId, + seed: '12312367897123300000000000000000', + }); + issuerDid = did; + + const schemaService = app.get(SchemasService); + const { schemaId: sid } = await schemaService.register({ + issuerDid, + tenantId, + name: 'test-schema-name', + version: `1.${new Date().getTime()}`, + attributeNames: ['none'], + }); + schemaId = sid; + }); + + afterAll(async () => { + await app.close(); + client.close(); + }); + + it(EventAnonCredsCredentialDefinitionsGetAll.token, async () => { + const response$ = client.send< + EventAnonCredsCredentialDefinitionsGetAll, + EventAnonCredsCredentialDefinitionsGetAllInput + >(EventAnonCredsCredentialDefinitionsGetAll.token, { tenantId }); + const response = await firstValueFrom(response$); + const eventInstance = + EventAnonCredsCredentialDefinitionsGetAll.fromEvent(response); + + expect(eventInstance.instance).toEqual(expect.arrayContaining([])); + }); + + it(EventAnonCredsCredentialDefinitionsGetById.token, async () => { + const response$ = client.send< + EventAnonCredsCredentialDefinitionsGetById, + EventAnonCredsCredentialDefinitionsGetByIdInput + >(EventAnonCredsCredentialDefinitionsGetById.token, { + tenantId, + credentialDefinitionId: 'some-id', + }); + const response = await firstValueFrom(response$); + const eventInstance = + EventAnonCredsCredentialDefinitionsGetById.fromEvent(response); + + expect(eventInstance.instance).toEqual(null); + }); + + it(EventAnonCredsCredentialDefinitionsRegister.token, async () => { + const tag = `tag:${new Date().getTime()}`; + const response$ = client.send< + EventAnonCredsCredentialDefinitionsRegister, + EventAnonCredsCredentialDefinitionsRegisterInput + >(EventAnonCredsCredentialDefinitionsRegister.token, { + tenantId, + schemaId, + issuerDid, + tag, + }); + + const response = await firstValueFrom(response$); + const eventInstance = + EventAnonCredsCredentialDefinitionsRegister.fromEvent(response); + + expect(eventInstance.instance).toMatchObject({ + schemaId, + tag, + issuerId: issuerDid, + type: 'CL', + }); + }); +}); diff --git a/apps/ssi-abstraction/test/schemas.e2e-spec.ts b/apps/ssi-abstraction/test/schemas.e2e-spec.ts index acfe669..018b1d3 100644 --- a/apps/ssi-abstraction/test/schemas.e2e-spec.ts +++ b/apps/ssi-abstraction/test/schemas.e2e-spec.ts @@ -92,7 +92,7 @@ describe('Schemas', () => { }); it(EventAnonCredsSchemasRegister.token, async () => { - const version = new Date().getTime().toString(); + const version = `1.${new Date().getTime()}`; const attributeNames = ['names', 'age']; const name = 'my-schema'; const response$ = client.send< -- GitLab