diff --git a/docker-compose.yml b/docker-compose.yml index 8d609b40..2b8ae156 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -48,20 +48,13 @@ services: condition: service_healthy ports: - 3100:3000 - command: --inbound-transport http 5002 ws 5003 --outbound-transport http ws + env_file: + - docker/cloudagent.env environment: - # - AFJ_REST_INBOUND_TRANSPORT="http 5002 ws 5003" - # - AFJ_REST_OUTBOUND_TRANSPORT="http ws" - - AFJ_REST_ENDPOINT=ws://veritable-cloudagent-alice:5003 - - AFJ_REST_ADMIN_PORT=3000 - - AFJ_REST_IPFS_ORIGIN=http://ipfs:5001 - - AFJ_REST_POSTGRES_HOST=postgres-veritable-cloudagent-alice - - AFJ_REST_POSTGRES_PORT=5432 - - AFJ_REST_POSTGRES_USERNAME=postgres - - AFJ_REST_POSTGRES_PASSWORD=postgres - - AFJ_REST_LABEL=vertiable-cloudagent - - AFJ_REST_WALLET_ID=alice - - AFJ_REST_WALLET_KEY=alice-key + - ENDPOINT=ws://veritable-cloudagent-alice:5003 + - POSTGRES_HOST=postgres-veritable-cloudagent-alice + - WALLET_ID=alice + - WALLET_KEY=alice-key # -------------------- bob -------------------------------# veritable-ui-bob: @@ -99,6 +92,9 @@ services: - IDP_INTERNAL_URL_PREFIX=http://keycloak:8080/realms/veritable/protocol/openid-connect - INVITATION_FROM_COMPANY_NUMBER=07964699 - INVITATION_PIN_SECRET=secret + - ISSUANCE_DID_POLICY=EXISTING_OR_NEW + - ISSUANCE_SCHEMA_POLICY=EXISTING_OR_NEW + - ISSUANCE_CRED_DEF_POLICY=EXISTING_OR_NEW postgres-veritable-ui-bob: image: postgres:16.3-alpine container_name: postgres-veritable-ui-bob @@ -127,20 +123,13 @@ services: condition: service_healthy ports: - 3101:3000 - command: --inbound-transport http 5002 ws 5003 --outbound-transport http ws + env_file: + - docker/cloudagent.env environment: - # - AFJ_REST_INBOUND_TRANSPORT="http 5002 ws 5003" - # - AFJ_REST_OUTBOUND_TRANSPORT="http ws" - - AFJ_REST_ENDPOINT=ws://veritable-cloudagent-bob:5003 - - AFJ_REST_ADMIN_PORT=3000 - - AFJ_REST_IPFS_ORIGIN=http://ipfs:5001 - - AFJ_REST_POSTGRES_HOST=postgres-veritable-cloudagent-bob - - AFJ_REST_POSTGRES_PORT=5432 - - AFJ_REST_POSTGRES_USERNAME=postgres - - AFJ_REST_POSTGRES_PASSWORD=postgres - - AFJ_REST_LABEL=vertiable-cloudagent - - AFJ_REST_WALLET_ID=bob - - AFJ_REST_WALLET_KEY=bob-key + - ENDPOINT=ws://veritable-cloudagent-bob:5003 + - POSTGRES_HOST=postgres-veritable-cloudagent-bob + - WALLET_ID=bob + - WALLET_KEY=bob-key volumes: ipfs: diff --git a/docker/cloudagent.env b/docker/cloudagent.env new file mode 100644 index 00000000..68e95b83 --- /dev/null +++ b/docker/cloudagent.env @@ -0,0 +1,8 @@ +INBOUND_TRANSPORT="[{\"transport\": \"http\", \"port\": 5002}, {\"transport\": \"ws\", \"port\": 5003}]" +OUTBOUND_TRANSPORT="http,ws" +ADMIN_PORT=3000 +IPFS_ORIGIN=http://ipfs:5001 +POSTGRES_PORT=5432 +POSTGRES_USERNAME=postgres +POSTGRES_PASSWORD=postgres +LABEL=vertiable-cloudagent \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 6a782d9d..3c848171 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "veritable-ui", - "version": "0.5.5", + "version": "0.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "veritable-ui", - "version": "0.5.5", + "version": "0.6.0", "license": "Apache-2.0", "dependencies": { "@digicatapult/tsoa-oauth-express": "^0.1.10", diff --git a/package.json b/package.json index b706ca96..87724ab9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "veritable-ui", - "version": "0.5.5", + "version": "0.6.0", "description": "UI for Veritable", "main": "src/index.ts", "type": "module", @@ -19,6 +19,8 @@ "tsoa:build": "tsoa spec-and-routes", "tsoa:watch": "node --watch-path=./src ./node_modules/.bin/tsoa -- spec-and-routes", "dev": "npm run tsoa:watch & NODE_ENV=dev node --import=tsimp/import --watch src/index.ts | pino-colada", + "dev:init": "NODE_ENV=dev node --import=tsimp/import src/init.ts | pino-colada", + "init": "node build/init.js", "start": "node build/index.js", "db:cmd": "node --import=tsimp/import ./node_modules/.bin/knex", "db:migrate": "npm run db:cmd -- migrate:latest", diff --git a/src/controllers/__tests__/helpers.ts b/src/controllers/__tests__/helpers.ts index 16bc29a7..5698c2e1 100644 --- a/src/controllers/__tests__/helpers.ts +++ b/src/controllers/__tests__/helpers.ts @@ -2,8 +2,9 @@ import { Readable } from 'node:stream' import { pino } from 'pino' import { Env } from '../../env.js' +import type { ILogger } from '../../logger.js' -export const mockLogger = pino({ level: 'silent' }) +export const mockLogger: ILogger = pino({ level: 'silent' }) export const mockEnv = { get: (name: string) => { diff --git a/src/controllers/connection/__tests__/helpers.ts b/src/controllers/connection/__tests__/helpers.ts index aaad989d..09cbb87d 100644 --- a/src/controllers/connection/__tests__/helpers.ts +++ b/src/controllers/connection/__tests__/helpers.ts @@ -2,6 +2,7 @@ import { Readable } from 'node:stream' import { pino } from 'pino' import { Env } from '../../../env.js' +import type { ILogger } from '../../../logger.js' import CompanyHouseEntity from '../../../models/companyHouseEntity.js' import Database from '../../../models/db/index.js' import { ConnectionRow } from '../../../models/db/types.js' @@ -22,7 +23,7 @@ export const withConnectionMocks = () => { listPage: (connections: ConnectionRow[]) => templateFake('list', connections[0].company_name, connections[0].status), } as ConnectionTemplates - const mockLogger = pino({ level: 'silent' }) + const mockLogger: ILogger = pino({ level: 'silent' }) const dbMock = { get: () => Promise.resolve([{ company_name: 'foo', status: 'verified' }]), } as unknown as Database @@ -35,7 +36,7 @@ export const withConnectionMocks = () => { } export const withNewConnectionMocks = () => { - const mockLogger = pino({ level: 'silent' }) + const mockLogger: ILogger = pino({ level: 'silent' }) const mockTransactionDb = { insert: () => Promise.resolve([{ id: '42' }]), } diff --git a/src/env.ts b/src/env.ts index 1f22d1c1..9d7a6f2b 100644 --- a/src/env.ts +++ b/src/env.ts @@ -2,12 +2,6 @@ import dotenv from 'dotenv' import * as envalid from 'envalid' import { singleton } from 'tsyringe' -if (process.env.NODE_ENV === 'test') { - dotenv.config({ path: 'test/test.env' }) -} else { - dotenv.config() -} - const strArrayValidator = envalid.makeValidator((input) => { const arr = input .split(',') @@ -22,7 +16,30 @@ const strArrayValidator = envalid.makeValidator((input) => { return res }) -const envConfig = { +const issuanceRecordValidator = envalid.makeValidator((input) => { + if (input === 'CREATE_NEW') { + return 'CREATE_NEW' as const + } + + if (input === 'FIND_EXISTING') { + return 'FIND_EXISTING' as const + } + + if (input === 'EXISTING_OR_NEW') { + return 'EXISTING_OR_NEW' as const + } + + if (input.match(/^did:/)) { + return input as `did:${string}` + } + if (input.match(/^ipfs:\/\//)) { + return input as `ipfs://${string}` + } + + throw new Error('must supply a valid issuance policy') +}) + +export const envConfig = { PORT: envalid.port({ default: 3000 }), LOG_LEVEL: envalid.str({ default: 'info', devDefault: 'debug' }), DB_HOST: envalid.host({ devDefault: 'localhost' }), @@ -60,20 +77,33 @@ const envConfig = { CLOUDAGENT_ADMIN_WS_ORIGIN: envalid.url({ devDefault: 'ws://localhost:3100' }), INVITATION_PIN_SECRET: envalid.str({ devDefault: 'secret' }), INVITATION_FROM_COMPANY_NUMBER: envalid.str({ devDefault: '07964699' }), + ISSUANCE_DID_POLICY: issuanceRecordValidator({ devDefault: 'EXISTING_OR_NEW' }), + ISSUANCE_SCHEMA_POLICY: issuanceRecordValidator({ devDefault: 'EXISTING_OR_NEW' }), + ISSUANCE_CRED_DEF_POLICY: issuanceRecordValidator({ devDefault: 'EXISTING_OR_NEW' }), } export type ENV_CONFIG = typeof envConfig export type ENV_KEYS = keyof ENV_CONFIG +export interface PartialEnv { + get(key: K): Pick, KS>[K] +} + @singleton() -export class Env { - private vals: envalid.CleanedEnv +export class Env implements PartialEnv { + private vals: Pick, KS> constructor() { + if (process.env.NODE_ENV === 'test') { + dotenv.config({ path: 'test/test.env' }) + } else { + dotenv.config() + } + this.vals = envalid.cleanEnv(process.env, envConfig) } - get(key: K) { + get(key: K) { return this.vals[key] } } diff --git a/src/index.ts b/src/index.ts index 1f12da06..08390477 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,11 +5,16 @@ import { container } from 'tsyringe' import { Env } from './env.js' import Server from './server.js' -import { logger } from './logger.js' +import { Logger, type ILogger } from './logger.js' +import { CredentialSchema } from './models/credentialSchema.js' ;(async () => { - const { app } = await Server() - const env = container.resolve(Env) + const logger = container.resolve(Logger) + + const schema = container.resolve(CredentialSchema) + await schema.assertIssuanceRecords() + + const { app } = await Server() app.listen(env.get('PORT'), () => { logger.info(`htmx-tsoa listening on ${env.get('PORT')} port`) diff --git a/src/init.ts b/src/init.ts new file mode 100644 index 00000000..d645b352 --- /dev/null +++ b/src/init.ts @@ -0,0 +1,55 @@ +import 'reflect-metadata' + +import dotenv from 'dotenv' +import envalid from 'envalid' +import { pino } from 'pino' + +import { PartialEnv, envConfig } from './env.js' +import { type ILogger } from './logger.js' +import { CredentialSchema } from './models/credentialSchema.js' +import VeritableCloudagent from './models/veritableCloudagent.js' + +type InitConfigKeys = + | 'CLOUDAGENT_ADMIN_ORIGIN' + | 'LOG_LEVEL' + | 'ISSUANCE_DID_POLICY' + | 'ISSUANCE_SCHEMA_POLICY' + | 'ISSUANCE_CRED_DEF_POLICY' + +class InitEnv implements PartialEnv { + private values: Pick, InitConfigKeys> + + constructor() { + if (process.env.NODE_ENV === 'test') { + dotenv.config({ path: 'test/test.env' }) + } else { + dotenv.config() + } + + this.values = envalid.cleanEnv(process.env, { + CLOUDAGENT_ADMIN_ORIGIN: envConfig.CLOUDAGENT_ADMIN_ORIGIN, + LOG_LEVEL: envConfig.LOG_LEVEL, + ISSUANCE_DID_POLICY: envConfig.ISSUANCE_DID_POLICY, + ISSUANCE_SCHEMA_POLICY: envConfig.ISSUANCE_SCHEMA_POLICY, + ISSUANCE_CRED_DEF_POLICY: envConfig.ISSUANCE_CRED_DEF_POLICY, + }) + } + + get(key: K) { + return this.values[key] + } +} +const env = new InitEnv() + +const logger: ILogger = pino( + { + name: 'veritable-ui-init', + timestamp: true, + level: env.get('LOG_LEVEL'), + }, + process.stdout +) +const cloudagent = new VeritableCloudagent(env, logger) +const init = new CredentialSchema(env, logger, cloudagent) +const details = await init.assertIssuanceRecords() +logger.info(details, 'Asserted credential issuance records with: %o', details) diff --git a/src/logger.ts b/src/logger.ts index eed00684..eb6e5513 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -3,20 +3,25 @@ import { container } from 'tsyringe' import { Env } from './env.js' -const env = container.resolve(Env) - -export const logger = pino( - { - name: 'htmx-tsoa', - timestamp: true, - level: env.get('LOG_LEVEL'), - }, - process.stdout -) - export const Logger = Symbol('Logger') -export type ILogger = typeof logger +export type ILogger = ReturnType +let instance: ILogger | null = null container.register(Logger, { - useValue: logger, + useFactory: (container) => { + if (instance) { + return instance + } + + const env = container.resolve(Env) + instance = pino( + { + name: 'veritable-ui', + timestamp: true, + level: env.get('LOG_LEVEL'), + }, + process.stdout + ) + return instance + }, }) diff --git a/src/models/__tests__/credentialSchema.test.ts b/src/models/__tests__/credentialSchema.test.ts new file mode 100644 index 00000000..0364dc74 --- /dev/null +++ b/src/models/__tests__/credentialSchema.test.ts @@ -0,0 +1,482 @@ +import { expect } from 'chai' +import { describe, test } from 'mocha' + +import { CredentialSchema } from '../credentialSchema.js' +import { makeCredentialSchemaMocks } from './helpers/credentialSchemaMocks.js' + +describe('credentialSchema', function () { + describe('assertIssuanceRecords', function () { + describe('did assertions', function () { + test('policy = CREATE_NEW, has existing = false', async function () { + const { + args, + mockCloudagent: { createDid, getCreatedDids }, + } = makeCredentialSchemaMocks({ didPolicy: 'CREATE_NEW', hasDids: false }) + const credentialSchema = new CredentialSchema(...args) + + const result = await credentialSchema.assertIssuanceRecords() + + expect(result).to.deep.equal({ + credentialDefinitionId: { + COMPANY_DETAILS: 'id', + }, + issuerId: 'did-id', + schemaId: { + COMPANY_DETAILS: 'id', + }, + }) + expect(getCreatedDids.callCount).to.equal(0) + expect(createDid.callCount).to.equal(1) + expect(createDid.firstCall.args).deep.equal(['key', { keyType: 'ed25519' }]) + }) + + test('policy = CREATE_NEW, has existing = true', async function () { + const { + args, + mockCloudagent: { createDid, getCreatedDids }, + } = makeCredentialSchemaMocks({ didPolicy: 'CREATE_NEW', hasDids: true }) + const credentialSchema = new CredentialSchema(...args) + + const result = await credentialSchema.assertIssuanceRecords() + + expect(result).to.deep.equal({ + credentialDefinitionId: { + COMPANY_DETAILS: 'id', + }, + issuerId: 'did-id', + schemaId: { + COMPANY_DETAILS: 'id', + }, + }) + expect(getCreatedDids.callCount).to.equal(0) + expect(createDid.callCount).to.equal(1) + expect(createDid.firstCall.args).deep.equal(['key', { keyType: 'ed25519' }]) + }) + + test('policy = FIND_EXISTING, has existing = false', async function () { + const { + args, + mockCloudagent: { createDid, getCreatedDids }, + } = makeCredentialSchemaMocks({ didPolicy: 'FIND_EXISTING', hasDids: false }) + const credentialSchema = new CredentialSchema(...args) + + let error: unknown | null = null + try { + await credentialSchema.assertIssuanceRecords() + } catch (err) { + error = err + } + + expect(error).instanceOf(Error) + expect(getCreatedDids.callCount).to.equal(1) + expect(getCreatedDids.firstCall.args).deep.equal([{ method: 'key' }]) + expect(createDid.callCount).to.equal(0) + }) + + test('policy = FIND_EXISTING, has existing = false', async function () { + const { + args, + mockCloudagent: { createDid, getCreatedDids }, + } = makeCredentialSchemaMocks({ didPolicy: 'FIND_EXISTING', hasDids: true }) + const credentialSchema = new CredentialSchema(...args) + + const result = await credentialSchema.assertIssuanceRecords() + + expect(result).to.deep.equal({ + credentialDefinitionId: { + COMPANY_DETAILS: 'id', + }, + issuerId: 'did-id', + schemaId: { + COMPANY_DETAILS: 'id', + }, + }) + expect(getCreatedDids.callCount).to.equal(1) + expect(getCreatedDids.firstCall.args).deep.equal([{ method: 'key' }]) + expect(createDid.callCount).to.equal(0) + }) + + test('policy = EXISTING_OR_NEW, has existing = true', async function () { + const { + args, + mockCloudagent: { createDid, getCreatedDids }, + } = makeCredentialSchemaMocks({ didPolicy: 'EXISTING_OR_NEW', hasDids: true }) + const credentialSchema = new CredentialSchema(...args) + + const result = await credentialSchema.assertIssuanceRecords() + + expect(result).to.deep.equal({ + credentialDefinitionId: { + COMPANY_DETAILS: 'id', + }, + issuerId: 'did-id', + schemaId: { + COMPANY_DETAILS: 'id', + }, + }) + expect(getCreatedDids.callCount).to.equal(1) + expect(getCreatedDids.firstCall.args).deep.equal([{ method: 'key' }]) + expect(createDid.callCount).to.equal(0) + }) + + test('policy = EXISTING_OR_NEW, has existing = false', async function () { + const { + args, + mockCloudagent: { createDid, getCreatedDids }, + } = makeCredentialSchemaMocks({ didPolicy: 'EXISTING_OR_NEW', hasDids: false }) + const credentialSchema = new CredentialSchema(...args) + + const result = await credentialSchema.assertIssuanceRecords() + + expect(result).to.deep.equal({ + credentialDefinitionId: { + COMPANY_DETAILS: 'id', + }, + issuerId: 'did-id', + schemaId: { + COMPANY_DETAILS: 'id', + }, + }) + expect(getCreatedDids.callCount).to.equal(1) + expect(getCreatedDids.firstCall.args).deep.equal([{ method: 'key' }]) + expect(createDid.callCount).to.equal(1) + expect(createDid.firstCall.args).deep.equal(['key', { keyType: 'ed25519' }]) + }) + }) + + describe('schema assertions', function () { + test('policy = CREATE_NEW, has existing = false', async function () { + const { + args, + mockCloudagent: { createSchema, getCreatedSchemas }, + } = makeCredentialSchemaMocks({ schemaPolicy: 'CREATE_NEW', hasSchema: false }) + const credentialSchema = new CredentialSchema(...args) + + const result = await credentialSchema.assertIssuanceRecords() + + expect(result).to.deep.equal({ + credentialDefinitionId: { + COMPANY_DETAILS: 'id', + }, + issuerId: 'did-id', + schemaId: { + COMPANY_DETAILS: 'id', + }, + }) + expect(getCreatedSchemas.callCount).to.equal(0) + expect(createSchema.callCount).to.equal(1) + expect(createSchema.firstCall.args).deep.equal([ + 'did-id', + 'COMPANY_DETAILS', + '1.0.0', + ['company_number', 'company_name'], + ]) + }) + + test('policy = CREATE_NEW, has existing = true', async function () { + const { + args, + mockCloudagent: { createSchema, getCreatedSchemas }, + } = makeCredentialSchemaMocks({ schemaPolicy: 'CREATE_NEW', hasSchema: true }) + const credentialSchema = new CredentialSchema(...args) + + const result = await credentialSchema.assertIssuanceRecords() + + expect(result).to.deep.equal({ + credentialDefinitionId: { + COMPANY_DETAILS: 'id', + }, + issuerId: 'did-id', + schemaId: { + COMPANY_DETAILS: 'id', + }, + }) + expect(getCreatedSchemas.callCount).to.equal(0) + expect(createSchema.callCount).to.equal(1) + expect(createSchema.firstCall.args).deep.equal([ + 'did-id', + 'COMPANY_DETAILS', + '1.0.0', + ['company_number', 'company_name'], + ]) + }) + + test('policy = FIND_EXISTING, has existing = false', async function () { + const { + args, + mockCloudagent: { createSchema, getCreatedSchemas }, + } = makeCredentialSchemaMocks({ schemaPolicy: 'FIND_EXISTING', hasSchema: false }) + const credentialSchema = new CredentialSchema(...args) + + let error: unknown | null = null + try { + await credentialSchema.assertIssuanceRecords() + } catch (err) { + error = err + } + + expect(error).instanceOf(Error) + expect(getCreatedSchemas.callCount).to.equal(1) + expect(getCreatedSchemas.firstCall.args).deep.equal([ + { + issuerId: 'did-id', + schemaName: 'COMPANY_DETAILS', + schemaVersion: '1.0.0', + }, + ]) + expect(createSchema.callCount).to.equal(0) + }) + + test('policy = FIND_EXISTING, has existing = false', async function () { + const { + args, + mockCloudagent: { createSchema, getCreatedSchemas }, + } = makeCredentialSchemaMocks({ schemaPolicy: 'FIND_EXISTING', hasSchema: true }) + const credentialSchema = new CredentialSchema(...args) + + const result = await credentialSchema.assertIssuanceRecords() + + expect(result).to.deep.equal({ + credentialDefinitionId: { + COMPANY_DETAILS: 'id', + }, + issuerId: 'did-id', + schemaId: { + COMPANY_DETAILS: 'id', + }, + }) + expect(getCreatedSchemas.callCount).to.equal(1) + expect(getCreatedSchemas.firstCall.args).deep.equal([ + { + issuerId: 'did-id', + schemaName: 'COMPANY_DETAILS', + schemaVersion: '1.0.0', + }, + ]) + expect(createSchema.callCount).to.equal(0) + }) + + test('policy = EXISTING_OR_NEW, has existing = true', async function () { + const { + args, + mockCloudagent: { createSchema, getCreatedSchemas }, + } = makeCredentialSchemaMocks({ schemaPolicy: 'EXISTING_OR_NEW', hasSchema: true }) + const credentialSchema = new CredentialSchema(...args) + + const result = await credentialSchema.assertIssuanceRecords() + + expect(result).to.deep.equal({ + credentialDefinitionId: { + COMPANY_DETAILS: 'id', + }, + issuerId: 'did-id', + schemaId: { + COMPANY_DETAILS: 'id', + }, + }) + expect(getCreatedSchemas.callCount).to.equal(1) + expect(getCreatedSchemas.firstCall.args).deep.equal([ + { + issuerId: 'did-id', + schemaName: 'COMPANY_DETAILS', + schemaVersion: '1.0.0', + }, + ]) + expect(createSchema.callCount).to.equal(0) + }) + + test('policy = EXISTING_OR_NEW, has existing = false', async function () { + const { + args, + mockCloudagent: { createSchema, getCreatedSchemas }, + } = makeCredentialSchemaMocks({ schemaPolicy: 'EXISTING_OR_NEW', hasSchema: false }) + const credentialSchema = new CredentialSchema(...args) + + const result = await credentialSchema.assertIssuanceRecords() + + expect(result).to.deep.equal({ + credentialDefinitionId: { + COMPANY_DETAILS: 'id', + }, + issuerId: 'did-id', + schemaId: { + COMPANY_DETAILS: 'id', + }, + }) + expect(getCreatedSchemas.callCount).to.equal(1) + expect(getCreatedSchemas.firstCall.args).deep.equal([ + { + issuerId: 'did-id', + schemaName: 'COMPANY_DETAILS', + schemaVersion: '1.0.0', + }, + ]) + expect(createSchema.callCount).to.equal(1) + expect(createSchema.firstCall.args).deep.equal([ + 'did-id', + 'COMPANY_DETAILS', + '1.0.0', + ['company_number', 'company_name'], + ]) + }) + }) + + describe('credential definition assertions', function () { + test('policy = CREATE_NEW, has existing = false', async function () { + const { + args, + mockCloudagent: { createCredentialDefinition, getCreatedCredentialDefinitions }, + } = makeCredentialSchemaMocks({ credDefPolicy: 'CREATE_NEW', hasCredDef: false }) + const credentialSchema = new CredentialSchema(...args) + + const result = await credentialSchema.assertIssuanceRecords() + + expect(result).to.deep.equal({ + credentialDefinitionId: { + COMPANY_DETAILS: 'id', + }, + issuerId: 'did-id', + schemaId: { + COMPANY_DETAILS: 'id', + }, + }) + expect(getCreatedCredentialDefinitions.callCount).to.equal(0) + expect(createCredentialDefinition.callCount).to.equal(1) + expect(createCredentialDefinition.firstCall.args).deep.equal(['did-id', 'id', 'company_details_v1.0.0']) + }) + + test('policy = CREATE_NEW, has existing = true', async function () { + const { + args, + mockCloudagent: { createCredentialDefinition, getCreatedCredentialDefinitions }, + } = makeCredentialSchemaMocks({ credDefPolicy: 'CREATE_NEW', hasCredDef: true }) + const credentialSchema = new CredentialSchema(...args) + + const result = await credentialSchema.assertIssuanceRecords() + + expect(result).to.deep.equal({ + credentialDefinitionId: { + COMPANY_DETAILS: 'id', + }, + issuerId: 'did-id', + schemaId: { + COMPANY_DETAILS: 'id', + }, + }) + expect(getCreatedCredentialDefinitions.callCount).to.equal(0) + expect(createCredentialDefinition.callCount).to.equal(1) + expect(createCredentialDefinition.firstCall.args).deep.equal(['did-id', 'id', 'company_details_v1.0.0']) + }) + + test('policy = FIND_EXISTING, has existing = false', async function () { + const { + args, + mockCloudagent: { createCredentialDefinition, getCreatedCredentialDefinitions }, + } = makeCredentialSchemaMocks({ credDefPolicy: 'FIND_EXISTING', hasCredDef: false }) + const credentialSchema = new CredentialSchema(...args) + + let error: unknown | null = null + try { + await credentialSchema.assertIssuanceRecords() + } catch (err) { + error = err + } + + expect(error).instanceOf(Error) + expect(getCreatedCredentialDefinitions.callCount).to.equal(1) + expect(getCreatedCredentialDefinitions.firstCall.args).deep.equal([ + { + issuerId: 'did-id', + schemaId: 'id', + }, + ]) + expect(createCredentialDefinition.callCount).to.equal(0) + }) + + test('policy = FIND_EXISTING, has existing = false', async function () { + const { + args, + mockCloudagent: { createCredentialDefinition, getCreatedCredentialDefinitions }, + } = makeCredentialSchemaMocks({ credDefPolicy: 'FIND_EXISTING', hasCredDef: true }) + const credentialSchema = new CredentialSchema(...args) + + const result = await credentialSchema.assertIssuanceRecords() + + expect(result).to.deep.equal({ + credentialDefinitionId: { + COMPANY_DETAILS: 'id', + }, + issuerId: 'did-id', + schemaId: { + COMPANY_DETAILS: 'id', + }, + }) + expect(getCreatedCredentialDefinitions.callCount).to.equal(1) + expect(getCreatedCredentialDefinitions.firstCall.args).deep.equal([ + { + issuerId: 'did-id', + schemaId: 'id', + }, + ]) + expect(createCredentialDefinition.callCount).to.equal(0) + }) + + test('policy = EXISTING_OR_NEW, has existing = true', async function () { + const { + args, + mockCloudagent: { createCredentialDefinition, getCreatedCredentialDefinitions }, + } = makeCredentialSchemaMocks({ credDefPolicy: 'EXISTING_OR_NEW', hasCredDef: true }) + const credentialSchema = new CredentialSchema(...args) + + const result = await credentialSchema.assertIssuanceRecords() + + expect(result).to.deep.equal({ + credentialDefinitionId: { + COMPANY_DETAILS: 'id', + }, + issuerId: 'did-id', + schemaId: { + COMPANY_DETAILS: 'id', + }, + }) + expect(getCreatedCredentialDefinitions.callCount).to.equal(1) + expect(getCreatedCredentialDefinitions.firstCall.args).deep.equal([ + { + issuerId: 'did-id', + schemaId: 'id', + }, + ]) + expect(createCredentialDefinition.callCount).to.equal(0) + }) + + test('policy = EXISTING_OR_NEW, has existing = false', async function () { + const { + args, + mockCloudagent: { createCredentialDefinition, getCreatedCredentialDefinitions }, + } = makeCredentialSchemaMocks({ credDefPolicy: 'EXISTING_OR_NEW', hasCredDef: false }) + const credentialSchema = new CredentialSchema(...args) + + const result = await credentialSchema.assertIssuanceRecords() + + expect(result).to.deep.equal({ + credentialDefinitionId: { + COMPANY_DETAILS: 'id', + }, + issuerId: 'did-id', + schemaId: { + COMPANY_DETAILS: 'id', + }, + }) + expect(getCreatedCredentialDefinitions.callCount).to.equal(1) + expect(getCreatedCredentialDefinitions.firstCall.args).deep.equal([ + { + issuerId: 'did-id', + schemaId: 'id', + }, + ]) + expect(createCredentialDefinition.callCount).to.equal(1) + expect(createCredentialDefinition.firstCall.args).deep.equal(['did-id', 'id', 'company_details_v1.0.0']) + }) + }) + }) +}) diff --git a/src/models/__tests__/fixtures/cloudagentFixtures.ts b/src/models/__tests__/fixtures/cloudagentFixtures.ts index b3a5375c..3861d973 100644 --- a/src/models/__tests__/fixtures/cloudagentFixtures.ts +++ b/src/models/__tests__/fixtures/cloudagentFixtures.ts @@ -1,5 +1,7 @@ import { pino } from 'pino' +import type { ILogger } from '../../../logger.js' + export const createInviteSuccessResponse = { invitationUrl: 'example.com', outOfBandRecord: { id: 'example-id' }, @@ -22,6 +24,26 @@ export const getConnectionsSuccessResponse = [ }, ] +export const createDidResponse = { + didDocument: { + id: 'did-id', + }, +} + +export const createSchemaResponse = { + id: 'id', + issuerId: 'issuerId', + name: 'name', + version: 'version', + attrNames: ['attrName'], +} + +export const createCredentialDefinitionResponse = { + id: 'id', + issuerId: 'issuerId', + schemaId: 'schemaId', +} + export const invalidResponse = {} -export const mockLogger = pino({ level: 'silent' }) +export const mockLogger: ILogger = pino({ level: 'silent' }) diff --git a/src/models/__tests__/helpers/credentialSchemaMocks.ts b/src/models/__tests__/helpers/credentialSchemaMocks.ts new file mode 100644 index 00000000..249022de --- /dev/null +++ b/src/models/__tests__/helpers/credentialSchemaMocks.ts @@ -0,0 +1,65 @@ +import { pino } from 'pino' +import sinon from 'sinon' + +import { Env } from '../../../env.js' +import { ILogger } from '../../../logger.js' +import VeritableCloudagent from '../../veritableCloudagent.js' +import { + createCredentialDefinitionResponse, + createDidResponse, + createSchemaResponse, +} from '../fixtures/cloudagentFixtures.js' + +type MockOptions = { + hasDids: boolean + hasSchema: boolean + hasCredDef: boolean + didPolicy: string + schemaPolicy: string + credDefPolicy: string +} +const defaultMockOptions: MockOptions = { + hasDids: true, + hasSchema: true, + hasCredDef: true, + didPolicy: 'FIND_EXISTING', + schemaPolicy: 'FIND_EXISTING', + credDefPolicy: 'FIND_EXISTING', +} + +export const makeCredentialSchemaMocks = (options: Partial) => { + const mergedOptions = Object.assign({}, defaultMockOptions, options) + + const mockLogger: ILogger = pino({ level: 'silent' }) + const mockEnv = { + get: (name: string) => { + switch (name) { + case 'ISSUANCE_DID_POLICY': + return mergedOptions.didPolicy + case 'ISSUANCE_SCHEMA_POLICY': + return mergedOptions.schemaPolicy + case 'ISSUANCE_CRED_DEF_POLICY': + return mergedOptions.credDefPolicy + default: + throw new Error() + } + }, + } as unknown as Env + const mockCloudagent = { + getCreatedDids: sinon.stub().resolves(mergedOptions.hasDids ? [createDidResponse.didDocument] : []), + createDid: sinon.stub().resolves(createDidResponse.didDocument), + getCreatedSchemas: sinon.stub().resolves(mergedOptions.hasSchema ? [createSchemaResponse] : []), + createSchema: sinon.stub().resolves(createSchemaResponse), + getCreatedCredentialDefinitions: sinon + .stub() + .resolves(mergedOptions.hasCredDef ? [createCredentialDefinitionResponse] : []), + createCredentialDefinition: sinon.stub().resolves(createCredentialDefinitionResponse), + } + + return { + mockEnv, + mockLogger, + mockCloudagent, + args: [mockEnv, mockLogger, mockCloudagent as unknown as VeritableCloudagent] as const, + } +} diff --git a/src/models/__tests__/idpService.test.ts b/src/models/__tests__/idpService.test.ts index d485ea4c..d96a214d 100644 --- a/src/models/__tests__/idpService.test.ts +++ b/src/models/__tests__/idpService.test.ts @@ -5,6 +5,7 @@ import { MockAgent, getGlobalDispatcher, setGlobalDispatcher } from 'undici' import { pino } from 'pino' import { Env } from '../../env.js' import { ForbiddenError } from '../../errors.js' +import type { ILogger } from '../../logger.js' import IDPService from '../idpService.js' const mockEnv: Env = { @@ -20,7 +21,7 @@ const mockEnv: Env = { }, } as Env -const mockLogger = pino({ level: 'silent' }) +const mockLogger: ILogger = pino({ level: 'silent' }) const mockTokenResponse = { access_token: 'access', diff --git a/src/models/__tests__/veritableCloudagent.test.ts b/src/models/__tests__/veritableCloudagent.test.ts index 505d6119..5e34a561 100644 --- a/src/models/__tests__/veritableCloudagent.test.ts +++ b/src/models/__tests__/veritableCloudagent.test.ts @@ -2,7 +2,10 @@ import { describe, it } from 'mocha' import { Env } from '../../env.js' import { + createCredentialDefinitionResponse, + createDidResponse, createInviteSuccessResponse, + createSchemaResponse, getConnectionsSuccessResponse, invalidResponse, mockLogger, @@ -198,4 +201,331 @@ describe('veritableCloudagent', () => { }) }) }) + + describe('createDid', () => { + describe('success', function () { + withCloudagentMock('POST', `/v1/dids/create`, 200, createDidResponse) + + it('should give back did', async () => { + const environment = new Env() + const cloudagent = new VeritableCloudagent(environment, mockLogger) + const response = await cloudagent.createDid('key', { keyType: 'ed25519' }) + expect(response).deep.equal(createDidResponse.didDocument) + }) + }) + + describe('error (response code)', function () { + withCloudagentMock('POST', `/v1/dids/create`, 400, {}) + + it('should throw internal error', async () => { + const environment = new Env() + const cloudagent = new VeritableCloudagent(environment, mockLogger) + + let error: unknown = null + try { + await cloudagent.createDid('key', { keyType: 'ed25519' }) + } catch (err) { + error = err + } + expect(error).instanceOf(InternalError) + }) + }) + + describe('error (response invalid)', function () { + withCloudagentMock('POST', `/v1/dids/create`, 200, invalidResponse) + + it('should throw internal error', async () => { + const environment = new Env() + const cloudagent = new VeritableCloudagent(environment, mockLogger) + + let error: unknown = null + try { + await cloudagent.createDid('key', { keyType: 'ed25519' }) + } catch (err) { + error = err + } + expect(error).instanceOf(InternalError) + }) + }) + }) + + describe('getCreatedDids', () => { + describe('success', function () { + withCloudagentMock('GET', `/v1/dids?createdLocally=true&method=key`, 200, [createDidResponse]) + + it('should give back did', async () => { + const environment = new Env() + const cloudagent = new VeritableCloudagent(environment, mockLogger) + const response = await cloudagent.getCreatedDids({ method: 'key' }) + expect(response).deep.equal([createDidResponse.didDocument]) + }) + }) + + describe('error (response code)', function () { + withCloudagentMock('GET', `/v1/dids?createdLocally=true&method=key`, 400, {}) + + it('should throw internal error', async () => { + const environment = new Env() + const cloudagent = new VeritableCloudagent(environment, mockLogger) + + let error: unknown = null + try { + await cloudagent.getCreatedDids({ method: 'key' }) + } catch (err) { + error = err + } + expect(error).instanceOf(InternalError) + }) + }) + + describe('error (response invalid)', function () { + withCloudagentMock('GET', `/v1/dids?createdLocally=true&method=key`, 200, invalidResponse) + + it('should throw internal error', async () => { + const environment = new Env() + const cloudagent = new VeritableCloudagent(environment, mockLogger) + + let error: unknown = null + try { + await cloudagent.getCreatedDids({ method: 'key' }) + } catch (err) { + error = err + } + expect(error).instanceOf(InternalError) + }) + }) + }) + + describe('createSchema', () => { + describe('success', function () { + withCloudagentMock('POST', `/v1/schemas`, 200, createSchemaResponse) + + it('should give back schema', async () => { + const environment = new Env() + const cloudagent = new VeritableCloudagent(environment, mockLogger) + const response = await cloudagent.createSchema('issuerId', 'name', 'version', []) + expect(response).deep.equal(createSchemaResponse) + }) + }) + + describe('error (response code)', function () { + withCloudagentMock('POST', `/v1/schemas`, 400, {}) + + it('should throw internal error', async () => { + const environment = new Env() + const cloudagent = new VeritableCloudagent(environment, mockLogger) + + let error: unknown = null + try { + await cloudagent.createSchema('issuerId', 'name', 'version', []) + } catch (err) { + error = err + } + expect(error).instanceOf(InternalError) + }) + }) + + describe('error (response invalid)', function () { + withCloudagentMock('POST', `/v1/schemas`, 200, invalidResponse) + + it('should throw internal error', async () => { + const environment = new Env() + const cloudagent = new VeritableCloudagent(environment, mockLogger) + + let error: unknown = null + try { + await cloudagent.createSchema('issuerId', 'name', 'version', []) + } catch (err) { + error = err + } + expect(error).instanceOf(InternalError) + }) + }) + }) + + describe('getCreatedSchemas', () => { + describe('success', function () { + withCloudagentMock( + 'GET', + `/v1/schemas?createdLocally=true&issuerId=issuerId&schemaName=name&schemaVersion=version`, + 200, + [createSchemaResponse] + ) + + it('should give back schema', async () => { + const environment = new Env() + const cloudagent = new VeritableCloudagent(environment, mockLogger) + const response = await cloudagent.getCreatedSchemas({ + issuerId: 'issuerId', + schemaName: 'name', + schemaVersion: 'version', + }) + expect(response).deep.equal([createSchemaResponse]) + }) + }) + + describe('error (response code)', function () { + withCloudagentMock( + 'GET', + `/v1/schemas?createdLocally=true&issuerId=issuerId&schemaName=name&schemaVersion=version`, + 400, + {} + ) + + it('should throw internal error', async () => { + const environment = new Env() + const cloudagent = new VeritableCloudagent(environment, mockLogger) + + let error: unknown = null + try { + await cloudagent.getCreatedSchemas({ issuerId: 'issuerId', schemaName: 'name', schemaVersion: 'version' }) + } catch (err) { + error = err + } + expect(error).instanceOf(InternalError) + }) + }) + + describe('error (response invalid)', function () { + withCloudagentMock( + 'GET', + `/v1/schemas?createdLocally=true&issuerId=issuerId&schemaName=name&schemaVersion=version`, + 200, + invalidResponse + ) + + it('should throw internal error', async () => { + const environment = new Env() + const cloudagent = new VeritableCloudagent(environment, mockLogger) + + let error: unknown = null + try { + await cloudagent.getCreatedSchemas({ issuerId: 'issuerId', schemaName: 'name', schemaVersion: 'version' }) + } catch (err) { + error = err + } + expect(error).instanceOf(InternalError) + }) + }) + }) + + //HERE + + describe('createCredentialDefinition', () => { + describe('success', function () { + withCloudagentMock('POST', `/v1/credential-definitions`, 200, createCredentialDefinitionResponse) + + it('should give back credential definition', async () => { + const environment = new Env() + const cloudagent = new VeritableCloudagent(environment, mockLogger) + const response = await cloudagent.createCredentialDefinition('issuerId', 'schemaId', 'tag') + expect(response).deep.equal(createCredentialDefinitionResponse) + }) + }) + + describe('error (response code)', function () { + withCloudagentMock('POST', `/v1/schemas`, 400, {}) + + it('should throw internal error', async () => { + const environment = new Env() + const cloudagent = new VeritableCloudagent(environment, mockLogger) + + let error: unknown = null + try { + await cloudagent.createCredentialDefinition('issuerId', 'schemaId', 'tag') + } catch (err) { + error = err + } + expect(error).instanceOf(InternalError) + }) + }) + + describe('error (response invalid)', function () { + withCloudagentMock('POST', `/v1/schemas`, 200, invalidResponse) + + it('should throw internal error', async () => { + const environment = new Env() + const cloudagent = new VeritableCloudagent(environment, mockLogger) + + let error: unknown = null + try { + await cloudagent.createCredentialDefinition('issuerId', 'schemaId', 'tag') + } catch (err) { + error = err + } + expect(error).instanceOf(InternalError) + }) + }) + }) + + describe('getCreatedCredentialDefinitions', () => { + describe('success', function () { + withCloudagentMock( + 'GET', + `/v1/credential-definitions?createdLocally=true&issuerId=issuerId&schemaId=schemaId`, + 200, + [createCredentialDefinitionResponse] + ) + + it('should give back credential definition', async () => { + const environment = new Env() + const cloudagent = new VeritableCloudagent(environment, mockLogger) + const response = await cloudagent.getCreatedCredentialDefinitions({ + issuerId: 'issuerId', + schemaId: 'schemaId', + }) + expect(response).deep.equal([createCredentialDefinitionResponse]) + }) + }) + + describe('error (response code)', function () { + withCloudagentMock( + 'GET', + `/v1/credential-definitions?createdLocally=true&issuerId=issuerId&schemaId=schemaId`, + 400, + {} + ) + + it('should throw internal error', async () => { + const environment = new Env() + const cloudagent = new VeritableCloudagent(environment, mockLogger) + + let error: unknown = null + try { + await cloudagent.getCreatedCredentialDefinitions({ + issuerId: 'issuerId', + schemaId: 'schemaId', + }) + } catch (err) { + error = err + } + expect(error).instanceOf(InternalError) + }) + }) + + describe('error (response invalid)', function () { + withCloudagentMock( + 'GET', + `/v1/credential-definitions?createdLocally=true&issuerId=issuerId&schemaId=schemaId`, + 200, + invalidResponse + ) + + it('should throw internal error', async () => { + const environment = new Env() + const cloudagent = new VeritableCloudagent(environment, mockLogger) + + let error: unknown = null + try { + await cloudagent.getCreatedCredentialDefinitions({ + issuerId: 'issuerId', + schemaId: 'schemaId', + }) + } catch (err) { + error = err + } + expect(error).instanceOf(InternalError) + }) + }) + }) }) diff --git a/src/models/credentialSchema.ts b/src/models/credentialSchema.ts new file mode 100644 index 00000000..ea78b2e2 --- /dev/null +++ b/src/models/credentialSchema.ts @@ -0,0 +1,148 @@ +import { inject, injectable, singleton } from 'tsyringe' +import { Env, type PartialEnv } from '../env.js' +import { Logger, type ILogger } from '../logger.js' +import VeritableCloudagent from './veritableCloudagent.js' + +export const schemaMap = { + COMPANY_DETAILS: { version: '1.0.0', attrNames: ['company_number', 'company_name'] }, +} as const + +type SCHEMA_NAMES = keyof typeof schemaMap + +@singleton() +@injectable() +export class CredentialSchema { + private issuerId?: string + private schemaId?: Record + private credentialDefinitionId?: Record + + constructor( + @inject(Env) private env: PartialEnv<'ISSUANCE_DID_POLICY' | 'ISSUANCE_SCHEMA_POLICY' | 'ISSUANCE_CRED_DEF_POLICY'>, + @inject(Logger) private logger: ILogger, + private cloudagent: VeritableCloudagent + ) {} + + private async assertDid(): Promise { + if (this.issuerId) { + return this.issuerId + } + + const policy = this.env.get('ISSUANCE_DID_POLICY') + if (policy !== 'CREATE_NEW' && policy !== 'FIND_EXISTING' && policy !== 'EXISTING_OR_NEW') { + return policy + } + + if (policy === 'FIND_EXISTING' || policy === 'EXISTING_OR_NEW') { + const createdDids = await this.cloudagent.getCreatedDids({ method: 'key' }) + if (createdDids.length !== 0) { + return createdDids[0].id + } + } + + if (policy === 'CREATE_NEW' || policy === 'EXISTING_OR_NEW') { + const result = await this.cloudagent.createDid('key', { keyType: 'ed25519' }) + return result.id + } + + throw new Error('Could not find existing DiD to use for issuing credentials') + } + + private async assertSchema(issuerId: string, schemaName: SCHEMA_NAMES): Promise { + if (this.schemaId) { + return this.schemaId[schemaName] + } + + const policy = this.env.get('ISSUANCE_SCHEMA_POLICY') + if (policy !== 'CREATE_NEW' && policy !== 'FIND_EXISTING' && policy !== 'EXISTING_OR_NEW') { + return policy + } + + const expectedSchema = { + issuerId, + name: schemaName, + version: schemaMap[schemaName].version, + attrNames: schemaMap[schemaName].attrNames, + } + + if (policy === 'EXISTING_OR_NEW' || policy === 'FIND_EXISTING') { + const findSchema = await this.cloudagent.getCreatedSchemas({ + issuerId, + schemaName: expectedSchema.name, + schemaVersion: expectedSchema.version, + }) + if (findSchema.length !== 0) { + return findSchema[0].id + } + } + + if (policy === 'CREATE_NEW' || policy === 'EXISTING_OR_NEW') { + const result = await this.cloudagent.createSchema( + expectedSchema.issuerId, + expectedSchema.name, + expectedSchema.version, + [...expectedSchema.attrNames] + ) + return result.id + } + + throw new Error(`Could not assert schema that matched schema policy ${policy}`) + } + + private async assertCredentialDefinition( + issuerId: string, + schemaId: string, + schemaName: SCHEMA_NAMES + ): Promise { + if (this.credentialDefinitionId) { + return this.credentialDefinitionId[schemaName] + } + + const policy = this.env.get('ISSUANCE_CRED_DEF_POLICY') + if (policy !== 'CREATE_NEW' && policy !== 'FIND_EXISTING' && policy !== 'EXISTING_OR_NEW') { + return policy + } + + if (policy === 'EXISTING_OR_NEW' || policy === 'FIND_EXISTING') { + const findCredDef = await this.cloudagent.getCreatedCredentialDefinitions({ schemaId, issuerId }) + if (findCredDef.length !== 0) { + return findCredDef[0].id + } + } + + if (policy === 'CREATE_NEW' || policy === 'EXISTING_OR_NEW') { + const result = await this.cloudagent.createCredentialDefinition(issuerId, schemaId, 'company_details_v1.0.0') + return result.id + } + + throw new Error(`Could not assert credential definition that matched schema policy ${policy}`) + } + + public async assertIssuanceRecords() { + const issuerId = await this.assertDid() + this.issuerId = issuerId + + const schemaId = await this.assertSchema(issuerId, 'COMPANY_DETAILS') + this.schemaId = { COMPANY_DETAILS: schemaId } + + const credentialDefinitionId = await this.assertCredentialDefinition(issuerId, schemaId, 'COMPANY_DETAILS') + this.credentialDefinitionId = { COMPANY_DETAILS: credentialDefinitionId } + + const records = this.issuanceRecords + this.logger.info( + records, + 'For issuing credentials using:\n\tissuerId:\t%s\n\tschemaId:\t%s\n\tcred-def:\t%s', + records.issuerId, + records.schemaId.COMPANY_DETAILS, + records.credentialDefinitionId.COMPANY_DETAILS + ) + + return records + } + + get issuanceRecords() { + if (!this.issuerId || !this.schemaId || !this.credentialDefinitionId) { + throw new Error('Credential Schema records have not been initialised') + } + return { issuerId: this.issuerId, schemaId: this.schemaId, credentialDefinitionId: this.credentialDefinitionId } + } +} diff --git a/src/models/emailService/__tests__/index.test.ts b/src/models/emailService/__tests__/index.test.ts index 94613f60..0d8f31ae 100644 --- a/src/models/emailService/__tests__/index.test.ts +++ b/src/models/emailService/__tests__/index.test.ts @@ -6,6 +6,7 @@ import sinon from 'sinon' import { Env } from '../../../env.js' +import type { ILogger } from '../../../logger.js' import EmailService from '../index.js' const mockEnv: Env = { @@ -31,7 +32,7 @@ describe('EmailService', () => { describe('sendMail', () => { it('should log message details', async () => { const logger = mkMockLogger() - const emailService = new EmailService(mockEnv, mockTemplates, logger as unknown as pino.Logger) + const emailService = new EmailService(mockEnv, mockTemplates, logger as unknown as ILogger) await emailService.sendMail('connection_invite', { to: 'user@example.com', invite: '1234567890987654321' }) diff --git a/src/models/veritableCloudagent.ts b/src/models/veritableCloudagent.ts index 6a8077a3..ef5b0830 100644 --- a/src/models/veritableCloudagent.ts +++ b/src/models/veritableCloudagent.ts @@ -1,7 +1,7 @@ import { inject, injectable, singleton } from 'tsyringe' import { z } from 'zod' -import { Env } from '../env.js' +import { Env, type PartialEnv } from '../env.js' import { InternalError } from '../errors.js' import { Logger, type ILogger } from '../logger.js' @@ -38,7 +38,35 @@ export const connectionParser = z.object({ }) export type Connection = z.infer +export const didDocumentParser = z.object({ + id: z.string(), +}) +export type DidDocument = z.infer + +export const didCreateParser = z.object({ + didDocument: didDocumentParser, +}) +export const didListParser = z.array(didCreateParser) + +export const schemaParser = z.object({ + id: z.string(), + issuerId: z.string(), + name: z.string(), + version: z.string(), + attrNames: z.array(z.string()), +}) +export type Schema = z.infer + +export const credentialDefinitionParser = z.object({ + id: z.string(), + issuerId: z.string(), + schemaId: z.string(), +}) +export type CredentialDefinition = z.infer + const connectionListParser = z.array(connectionParser) +const schemaListParser = z.array(schemaParser) +const credentialDefinitionListParser = z.array(credentialDefinitionParser) type parserFn = (res: Response) => O | Promise @@ -46,7 +74,7 @@ type parserFn = (res: Response) => O | Promise @injectable() export default class VeritableCloudagent { constructor( - private env: Env, + @inject(Env) private env: PartialEnv<'CLOUDAGENT_ADMIN_ORIGIN'>, @inject(Logger) protected logger: ILogger ) {} @@ -88,6 +116,61 @@ export default class VeritableCloudagent { return this.deleteRequest(`/v1/connections/${id}`, () => {}) } + public async createDid(method: string, options: Record): Promise { + return this.postRequest('/v1/dids/create', { method, options }, this.buildParser(didCreateParser)).then( + (res) => res.didDocument + ) + } + + public async getCreatedDids(filters: Partial<{ method: string }> = {}): Promise { + const params = new URLSearchParams({ + createdLocally: 'true', + ...filters, + }).toString() + + return this.getRequest(`/v1/dids?${params}`, this.buildParser(didListParser)).then((dids) => + dids.map((did) => did.didDocument) + ) + } + + public async createSchema(issuerId: string, name: string, version: string, attrNames: string[]): Promise { + return this.postRequest('/v1/schemas', { issuerId, name, version, attrNames }, this.buildParser(schemaParser)) + } + + public async getCreatedSchemas( + filters: Partial<{ issuerId: string; schemaName: string; schemaVersion: string }> = {} + ): Promise { + const params = new URLSearchParams({ + createdLocally: 'true', + ...filters, + }).toString() + + return this.getRequest(`/v1/schemas?${params}`, this.buildParser(schemaListParser)) + } + + public async createCredentialDefinition( + issuerId: string, + schemaId: string, + tag: string + ): Promise { + return this.postRequest( + '/v1/credential-definitions', + { tag, issuerId, schemaId }, + this.buildParser(credentialDefinitionParser) + ) + } + + public async getCreatedCredentialDefinitions( + filters: Partial<{ schemaId: string; issuerId: string }> = {} + ): Promise { + const params = new URLSearchParams({ + createdLocally: 'true', + ...filters, + }).toString() + + return this.getRequest(`/v1/credential-definitions?${params}`, this.buildParser(credentialDefinitionListParser)) + } + private async getRequest(path: string, parse: parserFn): Promise { return this.noBodyRequest('GET', path, parse) } diff --git a/src/utils/__tests__/fixtures/testIndexedAsyncEventEmitter.ts b/src/utils/__tests__/fixtures/testIndexedAsyncEventEmitter.ts index df327301..c822716a 100644 --- a/src/utils/__tests__/fixtures/testIndexedAsyncEventEmitter.ts +++ b/src/utils/__tests__/fixtures/testIndexedAsyncEventEmitter.ts @@ -1,9 +1,9 @@ import { pino } from 'pino' -import { ILogger } from '../../../logger.js' +import type { ILogger } from '../../../logger.js' import IndexedAsyncEventEmitter from '../../indexedAsyncEventEmitter.js' -const mockLogger = pino({ level: 'silent' }) +const mockLogger: ILogger = pino({ level: 'silent' }) export type EventNames = 'A' | 'B' | 'C' export type EventData = { diff --git a/test/helpers/cloudagent.ts b/test/helpers/cloudagent.ts index 1a5a5f61..95e374fe 100644 --- a/test/helpers/cloudagent.ts +++ b/test/helpers/cloudagent.ts @@ -4,9 +4,10 @@ import { Env } from '../../src/env.js' import VeritableCloudagent from '../../src/models/veritableCloudagent.js' import { container } from 'tsyringe' +import { type ILogger } from '../../src/logger.js' import { validCompanyName, validCompanyNumber } from './fixtures.js' -const mockLogger = pino({ level: 'silent' }) +const mockLogger: ILogger = pino({ level: 'silent' }) const cleanupShared = async function (agent: VeritableCloudagent) { const connections = await agent.getConnections()