From 4dd777cf48a6d16f65d0407c6be0b798b1a5dea6 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Tue, 10 Dec 2024 17:18:28 -0500 Subject: [PATCH 001/132] feat: update model supporting revocation --- .../credentials/CredentialTypeController.ts | 2 +- .../src/controllers/message/MessageService.ts | 2 ++ .../src/messages/CredentialIssuanceMessage.ts | 16 +++++++++++++++- packages/model/src/types.ts | 1 + yarn.lock | 2 +- 5 files changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/main/src/controllers/credentials/CredentialTypeController.ts b/packages/main/src/controllers/credentials/CredentialTypeController.ts index b9a9302..14306c5 100644 --- a/packages/main/src/controllers/credentials/CredentialTypeController.ts +++ b/packages/main/src/controllers/credentials/CredentialTypeController.ts @@ -131,7 +131,7 @@ export class CredentialTypesController { const registrationResult = await agent.modules.anoncreds.registerCredentialDefinition({ credentialDefinition: { issuerId, schemaId, tag: `${options.name}.${options.version}` }, - options: { supportRevocation: false }, + options: { supportRevocation: true }, }) const credentialDefinitionId = registrationResult.credentialDefinitionState.credentialDefinitionId diff --git a/packages/main/src/controllers/message/MessageService.ts b/packages/main/src/controllers/message/MessageService.ts index 1dc143f..d7c44d3 100644 --- a/packages/main/src/controllers/message/MessageService.ts +++ b/packages/main/src/controllers/message/MessageService.ts @@ -223,6 +223,8 @@ export class MessageService { return { name: item.name, mimeType: item.mimeType, value: item.value } }), credentialDefinitionId: msg.credentialDefinitionId, + revocationRegistryDefinitionId: msg.revocationDefinitionId, + revocationRegistryIndex: msg.revocationRegistryIndex, }, }, protocolVersion: 'v2', diff --git a/packages/model/src/messages/CredentialIssuanceMessage.ts b/packages/model/src/messages/CredentialIssuanceMessage.ts index d4b4fba..0a6d18b 100644 --- a/packages/model/src/messages/CredentialIssuanceMessage.ts +++ b/packages/model/src/messages/CredentialIssuanceMessage.ts @@ -1,7 +1,7 @@ import 'reflect-metadata' import { Expose, Type } from 'class-transformer' -import { IsOptional, IsString, IsArray, IsInstance, ValidateNested } from 'class-validator' +import { IsOptional, IsString, IsArray, IsInstance, ValidateNested, IsNumber } from 'class-validator' import { BaseMessage, BaseMessageOptions } from './BaseMessage' import { MessageType } from './MessageType' @@ -37,6 +37,8 @@ export class Claim { export interface CredentialIssuanceMessageOptions extends BaseMessageOptions { credentialDefinitionId: string + revocationDefinitionId?: string + revocationRegistryIndex?: number claims?: Claim[] } @@ -50,6 +52,8 @@ export class CredentialIssuanceMessage extends BaseMessage { this.timestamp = options.timestamp ?? new Date() this.connectionId = options.connectionId this.credentialDefinitionId = options.credentialDefinitionId + this.revocationDefinitionId = options.revocationDefinitionId + this.revocationRegistryIndex = options.revocationRegistryIndex this.claims = options.claims?.map(item => new Claim(item)) } } @@ -60,6 +64,16 @@ export class CredentialIssuanceMessage extends BaseMessage { @Expose() @IsString() public credentialDefinitionId?: string + + @Expose() + @IsString() + @IsOptional() + public revocationDefinitionId?: string + + @Expose() + @IsNumber() + @IsOptional() + public revocationRegistryIndex?: number @Expose() @Type(() => Claim) diff --git a/packages/model/src/types.ts b/packages/model/src/types.ts index abcb4fc..36be6d6 100644 --- a/packages/model/src/types.ts +++ b/packages/model/src/types.ts @@ -25,6 +25,7 @@ export interface CreateCredentialTypeOptions { version: string attributes: string[] schemaId?: string + revocationId?: string } type JsonObject = { diff --git a/yarn.lock b/yarn.lock index e3e7421..2809295 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1986,7 +1986,7 @@ dependencies: "@types/node" "*" -"@types/node@*", "@types/node@^20.11.19": +"@types/node@*", "@types/node@20.11.19", "@types/node@^20.11.19": version "20.11.19" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.19.tgz#b466de054e9cb5b3831bee38938de64ac7f81195" integrity sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ== From f576635311e6b392bbe48a21313a2f257159205a Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Tue, 10 Dec 2024 23:57:57 -0500 Subject: [PATCH 002/132] feat: create FullTailsFileService and add server for tails --- packages/main/src/didWebServer.ts | 117 ++++++++++++++++++ .../main/src/services/FullTailsFileService.ts | 41 ++++++ packages/main/src/utils/ServiceAgent.ts | 6 + packages/main/src/utils/setupAgent.ts | 1 + .../src/messages/CredentialIssuanceMessage.ts | 4 +- 5 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 packages/main/src/services/FullTailsFileService.ts diff --git a/packages/main/src/didWebServer.ts b/packages/main/src/didWebServer.ts index ba43910..4221153 100644 --- a/packages/main/src/didWebServer.ts +++ b/packages/main/src/didWebServer.ts @@ -4,7 +4,10 @@ import type { DidWebServerConfig } from './utils/ServerConfig' import { AnonCredsCredentialDefinitionRepository, AnonCredsSchemaRepository } from '@credo-ts/anoncreds' import cors from 'cors' +import { createHash } from 'crypto' import express from 'express' +import fs from 'fs' +import multer, { diskStorage } from 'multer' import { ServiceAgent } from './utils/ServiceAgent' @@ -27,6 +30,41 @@ export const startDidWebServer = async (agent: ServiceAgent, config: DidWebServe return server } +const baseFilePath = './tails' +const indexFilePath = `./${baseFilePath}/index.json` + +if (!fs.existsSync(baseFilePath)) { + fs.mkdirSync(baseFilePath, { recursive: true }) +} +const tailsIndex = ( + fs.existsSync(indexFilePath) ? JSON.parse(fs.readFileSync(indexFilePath, { encoding: 'utf-8' })) : {} +) as Record + +function fileHash(filePath: string, algorithm = 'sha256') { + return new Promise((resolve, reject) => { + const shasum = createHash(algorithm) + try { + const s = fs.createReadStream(filePath) + s.on('data', function (data) { + shasum.update(data) + }) + // making digest + s.on('end', function () { + const hash = shasum.digest('hex') + return resolve(hash) + }) + } catch (error) { + return reject('error in calculation') + } + }) +} + +const fileStorage = diskStorage({ + filename: (req: any, file: { originalname: string }, cb: (arg0: null, arg1: string) => void) => { + cb(null, file.originalname + '-' + new Date().toISOString()) + }, +}) + export const addDidWebRoutes = async ( app: express.Express, agent: ServiceAgent, @@ -89,5 +127,84 @@ export const addDidWebRoutes = async ( res.send(404) }) + + // Allow to create invitation, no other way to ask for invitation yet + app.get('/:tailsFileId', async (req, res) => { + agent.config.logger.debug(`requested file`) + + const tailsFileId = req.params.tailsFileId + if (!tailsFileId) { + res.status(409).end() + return + } + + const fileName = tailsIndex[tailsFileId] + + if (!fileName) { + agent.config.logger.debug(`no entry found for tailsFileId: ${tailsFileId}`) + res.status(404).end() + return + } + + const path = `${baseFilePath}/${fileName}` + try { + agent.config.logger.debug(`reading file: ${path}`) + + if (!fs.existsSync(path)) { + agent.config.logger.debug(`file not found: ${path}`) + res.status(404).end() + return + } + + const file = fs.createReadStream(path) + res.setHeader('Content-Disposition', `attachment: filename="${fileName}"`) + file.pipe(res) + } catch (error) { + agent.config.logger.debug(`error reading file: ${path}`) + res.status(500).end() + } + }) + + app.put('/:tailsFileId', multer({ storage: fileStorage }).single('file'), async (req, res) => { + agent.config.logger.info(`tails file upload: ${req.params.tailsFileId}`) + + const file = req.file + + if (!file) { + agent.config.logger.info(`No file found: ${JSON.stringify(req.headers)}`) + return res.status(400).send('No files were uploaded.') + } + + const tailsFileId = req.params.tailsFileId + if (!tailsFileId) { + // Clean up temporary file + fs.rmSync(file.path) + return res.status(409).send('Missing tailsFileId') + } + + const item = tailsIndex[tailsFileId] + + if (item) { + agent.config.logger.debug(`there is already an entry for: ${tailsFileId}`) + res.status(409).end() + return + } + + const hash = await fileHash(file.path) + const destinationPath = `${baseFilePath}/${hash}` + + if (fs.existsSync(destinationPath)) { + agent.config.logger.warn('tails file already exists') + } else { + fs.copyFileSync(file.path, destinationPath) + fs.rmSync(file.path) + } + + // Store filename in index + tailsIndex[tailsFileId] = hash + fs.writeFileSync(indexFilePath, JSON.stringify(tailsIndex)) + + res.status(200).end() + }) } } diff --git a/packages/main/src/services/FullTailsFileService.ts b/packages/main/src/services/FullTailsFileService.ts new file mode 100644 index 0000000..05d766a --- /dev/null +++ b/packages/main/src/services/FullTailsFileService.ts @@ -0,0 +1,41 @@ +import type { AnonCredsRevocationRegistryDefinition } from '@credo-ts/anoncreds' +import type { AgentContext } from '@credo-ts/core' + +import { BasicTailsFileService } from '@credo-ts/anoncreds' +import { utils } from '@credo-ts/core' +import FormData from 'form-data' +import fs from 'fs' + +export class FullTailsFileService extends BasicTailsFileService { + private tailsServerBaseUrl?: string + public constructor(options?: { tailsDirectoryPath?: string; tailsServerBaseUrl?: string }) { + super(options) + this.tailsServerBaseUrl = options?.tailsServerBaseUrl + } + + public async uploadTailsFile( + agentContext: AgentContext, + options: { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + }, + ) { + const revocationRegistryDefinition = options.revocationRegistryDefinition + const localTailsFilePath = revocationRegistryDefinition.value.tailsLocation + + const tailsFileId = utils.uuid() + const data = new FormData() + const readStream = fs.createReadStream(localTailsFilePath) + data.append('file', readStream) + const response = await agentContext.config.agentDependencies.fetch( + `${this.tailsServerBaseUrl}/${encodeURIComponent(tailsFileId)}`, + { + method: 'PUT', + body: data, + }, + ) + if (response.status !== 200) { + throw new Error('Cannot upload tails file') + } + return { tailsFileUrl: `${this.tailsServerBaseUrl}/${encodeURIComponent(tailsFileId)}` } + } +} diff --git a/packages/main/src/utils/ServiceAgent.ts b/packages/main/src/utils/ServiceAgent.ts index ac976f1..bb66b2f 100644 --- a/packages/main/src/utils/ServiceAgent.ts +++ b/packages/main/src/utils/ServiceAgent.ts @@ -30,6 +30,8 @@ import { DidWebAnonCredsRegistry } from 'credo-ts-didweb-anoncreds' import { MediaSharingModule } from 'credo-ts-media-sharing' import { ReceiptsModule } from 'credo-ts-receipts' +import { FullTailsFileService } from '../services/FullTailsFileService' + import { CachedWebDidResolver } from './CachedWebDidResolver' type ServiceAgentModules = { @@ -69,6 +71,7 @@ export interface ServiceAgentOptions { config: InitConfig did?: string dependencies: AgentDependencies + anoncredsServiceBaseUrl?: string } export const createServiceAgent = (options: ServiceAgentOptions): ServiceAgent => { @@ -80,6 +83,9 @@ export const createServiceAgent = (options: ServiceAgentOptions): ServiceAgent = askar: new AskarModule({ ariesAskar }), anoncreds: new AnonCredsModule({ anoncreds, + tailsFileService: new FullTailsFileService({ + tailsServerBaseUrl: options.anoncredsServiceBaseUrl, + }), registries: [ new DidWebAnonCredsRegistry({ cacheOptions: { allowCaching: true, cacheDurationInSeconds: 24 * 60 * 60 }, diff --git a/packages/main/src/utils/setupAgent.ts b/packages/main/src/utils/setupAgent.ts index db86313..e760cdb 100644 --- a/packages/main/src/utils/setupAgent.ts +++ b/packages/main/src/utils/setupAgent.ts @@ -73,6 +73,7 @@ export const setupAgent = async ({ }, did: publicDid, dependencies: agentDependencies, + anoncredsServiceBaseUrl, }) const app = express() diff --git a/packages/model/src/messages/CredentialIssuanceMessage.ts b/packages/model/src/messages/CredentialIssuanceMessage.ts index 0a6d18b..af9a209 100644 --- a/packages/model/src/messages/CredentialIssuanceMessage.ts +++ b/packages/model/src/messages/CredentialIssuanceMessage.ts @@ -64,12 +64,12 @@ export class CredentialIssuanceMessage extends BaseMessage { @Expose() @IsString() public credentialDefinitionId?: string - + @Expose() @IsString() @IsOptional() public revocationDefinitionId?: string - + @Expose() @IsNumber() @IsOptional() From da3ade92d3c9f01b39a69808cc15e878c83aabd2 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Wed, 11 Dec 2024 08:05:38 -0500 Subject: [PATCH 003/132] fix: create FullTailsFileService and add server for tails --- packages/main/src/services/FullTailsFileService.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/main/src/services/FullTailsFileService.ts b/packages/main/src/services/FullTailsFileService.ts index 05d766a..eaae4ab 100644 --- a/packages/main/src/services/FullTailsFileService.ts +++ b/packages/main/src/services/FullTailsFileService.ts @@ -3,7 +3,7 @@ import type { AgentContext } from '@credo-ts/core' import { BasicTailsFileService } from '@credo-ts/anoncreds' import { utils } from '@credo-ts/core' -import FormData from 'form-data' +import { FormData } from 'undici' import fs from 'fs' export class FullTailsFileService extends BasicTailsFileService { @@ -30,6 +30,7 @@ export class FullTailsFileService extends BasicTailsFileService { `${this.tailsServerBaseUrl}/${encodeURIComponent(tailsFileId)}`, { method: 'PUT', + headers: {}, body: data, }, ) From bb061dc7284540e231d3fe13bbb312d366516d63 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Wed, 11 Dec 2024 09:06:36 -0500 Subject: [PATCH 004/132] feat: update controller credential type --- .../credentials/CredentialTypeController.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/packages/main/src/controllers/credentials/CredentialTypeController.ts b/packages/main/src/controllers/credentials/CredentialTypeController.ts index 14306c5..f5f1554 100644 --- a/packages/main/src/controllers/credentials/CredentialTypeController.ts +++ b/packages/main/src/controllers/credentials/CredentialTypeController.ts @@ -144,7 +144,38 @@ export class CredentialTypesController { `Cannot create credential definition: ${JSON.stringify(registrationResult.registrationMetadata)}`, ) } + + const revocationResult = await agent.modules.anoncreds.registerRevocationRegistryDefinition({ + revocationRegistryDefinition: { + credentialDefinitionId, + tag: 'default', + maximumCredentialNumber: 10, + issuerId, + }, + options: {}, + }) + const revocationDefinitionId = + revocationResult.revocationRegistryDefinitionState.revocationRegistryDefinitionId + this.logger.debug!( + `revocationRegistryDefinitionState: ${JSON.stringify(revocationResult.revocationRegistryDefinitionState)}`, + ) + + if (!revocationDefinitionId) { + throw new Error( + `Cannot create credential revocations: ${JSON.stringify(registrationResult.registrationMetadata)}`, + ) + } + + await agent.modules.anoncreds.registerRevocationStatusList({ + revocationStatusList: { + issuerId, + revocationRegistryDefinitionId: revocationDefinitionId, + }, + options: {}, + }) + this.logger.log(`Credential Definition Id: ${credentialDefinitionId}`) + this.logger.log(`Revocation Definition Id: ${revocationDefinitionId}`) // Apply name and version as tags const credentialDefinitionRepository = agent.dependencyManager.resolve( @@ -164,6 +195,7 @@ export class CredentialTypesController { name: options.name, version: options.version, schemaId, + revocationId: revocationDefinitionId, } } catch (error) { throw new HttpException( From 42736a09287a3eb1389034e0a7b76dfd244f73f4 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Wed, 11 Dec 2024 09:17:22 -0500 Subject: [PATCH 005/132] fix: test --- .../pull_request_template.md => PULL_REQUEST_TEMPLATE.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/{PULL_REQUEST_TEMPLATE/pull_request_template.md => PULL_REQUEST_TEMPLATE.md} (100%) diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE.md similarity index 100% rename from .github/PULL_REQUEST_TEMPLATE/pull_request_template.md rename to .github/PULL_REQUEST_TEMPLATE.md From 1ac0989b91cd6bd3a8491a879898e7d8f53acf8b Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Wed, 11 Dec 2024 09:23:12 -0500 Subject: [PATCH 006/132] fix: test --- .github/{PULL_REQUEST_TEMPLATE.md => pull_request_template.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/{PULL_REQUEST_TEMPLATE.md => pull_request_template.md} (100%) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/pull_request_template.md similarity index 100% rename from .github/PULL_REQUEST_TEMPLATE.md rename to .github/pull_request_template.md From efc7e834aeb70862afc24e04932d0b4f1def06da Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Wed, 11 Dec 2024 09:25:35 -0500 Subject: [PATCH 007/132] fix: test --- .github/{ => PULL_REQUEST_TEMPLATE}/pull_request_template.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/{ => PULL_REQUEST_TEMPLATE}/pull_request_template.md (100%) diff --git a/.github/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md similarity index 100% rename from .github/pull_request_template.md rename to .github/PULL_REQUEST_TEMPLATE/pull_request_template.md From a66bdcc93a55d27e239b61e5a48ecdf77a35c3e5 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Wed, 11 Dec 2024 15:09:10 -0500 Subject: [PATCH 008/132] feat: add axios to tales service --- packages/main/package.json | 1 + .../main/src/services/FullTailsFileService.ts | 14 ++++++-------- yarn.lock | 19 +++++++++++++++++++ 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/packages/main/package.json b/packages/main/package.json index 5c94c68..70f4be7 100644 --- a/packages/main/package.json +++ b/packages/main/package.json @@ -72,6 +72,7 @@ "@nestjs/platform-express": "^10.0.0", "@nestjs/swagger": "^7.3.0", "@types/qrcode": "^1.5.0", + "axios": "^1.7.9", "body-parser": "^1.20.0", "bull": "^4.16.2", "cors": "^2.8.5", diff --git a/packages/main/src/services/FullTailsFileService.ts b/packages/main/src/services/FullTailsFileService.ts index eaae4ab..063d2a9 100644 --- a/packages/main/src/services/FullTailsFileService.ts +++ b/packages/main/src/services/FullTailsFileService.ts @@ -3,7 +3,8 @@ import type { AgentContext } from '@credo-ts/core' import { BasicTailsFileService } from '@credo-ts/anoncreds' import { utils } from '@credo-ts/core' -import { FormData } from 'undici' +import axios from 'axios' +import FormData from 'form-data' import fs from 'fs' export class FullTailsFileService extends BasicTailsFileService { @@ -26,14 +27,11 @@ export class FullTailsFileService extends BasicTailsFileService { const data = new FormData() const readStream = fs.createReadStream(localTailsFilePath) data.append('file', readStream) - const response = await agentContext.config.agentDependencies.fetch( - `${this.tailsServerBaseUrl}/${encodeURIComponent(tailsFileId)}`, - { - method: 'PUT', - headers: {}, - body: data, + const response = await axios.put(`${this.tailsServerBaseUrl}/${encodeURIComponent(tailsFileId)}`, data, { + headers: { + ...data.getHeaders(), }, - ) + }) if (response.status !== 200) { throw new Error('Cannot upload tails file') } diff --git a/yarn.lock b/yarn.lock index 2809295..ee59c6d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2663,6 +2663,15 @@ available-typed-arrays@^1.0.7: dependencies: possible-typed-array-names "^1.0.0" +axios@^1.7.9: + version "1.7.9" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.9.tgz#d7d071380c132a24accda1b2cfc1535b79ec650a" + integrity sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + b64-lite@^1.3.1, b64-lite@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/b64-lite/-/b64-lite-1.4.0.tgz#e62442de11f1f21c60e38b74f111ac0242283d3d" @@ -4350,6 +4359,11 @@ flatted@^3.2.9: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== +follow-redirects@^1.15.6: + version "1.15.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" + integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -6614,6 +6628,11 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + punycode@^2.1.0: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" From a6a327d0df9e3a9fae528490aa49538fe6f0e945 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Wed, 11 Dec 2024 17:43:50 -0500 Subject: [PATCH 009/132] feat: add revocation to get credential --- .../credentials/CredentialTypeController.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/main/src/controllers/credentials/CredentialTypeController.ts b/packages/main/src/controllers/credentials/CredentialTypeController.ts index f5f1554..f3e0a5f 100644 --- a/packages/main/src/controllers/credentials/CredentialTypeController.ts +++ b/packages/main/src/controllers/credentials/CredentialTypeController.ts @@ -57,11 +57,21 @@ export class CredentialTypesController { const schema = schemaResult.schema + const revocationDefinitionRepository = agent.dependencyManager.resolve( + AnonCredsRevocationRegistryDefinitionRepository, + ) + + const revocationDefinition = await revocationDefinitionRepository.findAllByCredentialDefinitionId( + agent.context, + record.credentialDefinitionId, + ) + return { id: record.credentialDefinitionId, name: (record.getTag('name') as string) ?? schema?.name, version: (record.getTag('version') as string) ?? schema?.version, attributes: schema?.attrNames || [], + revocationId: revocationDefinition[0].revocationRegistryDefinitionId, } }), ) @@ -149,7 +159,7 @@ export class CredentialTypesController { revocationRegistryDefinition: { credentialDefinitionId, tag: 'default', - maximumCredentialNumber: 10, + maximumCredentialNumber: 1000, issuerId, }, options: {}, From a1277e0261bd497231a8b9d2f3bbd6c8007d641e Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Thu, 12 Dec 2024 00:51:56 -0500 Subject: [PATCH 010/132] feat: update did server for revcredential --- .../credentials/CredentialTypeController.ts | 16 +++-------- packages/main/src/didWebServer.ts | 28 ++++++++++++++++++- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/packages/main/src/controllers/credentials/CredentialTypeController.ts b/packages/main/src/controllers/credentials/CredentialTypeController.ts index f3e0a5f..17b38cd 100644 --- a/packages/main/src/controllers/credentials/CredentialTypeController.ts +++ b/packages/main/src/controllers/credentials/CredentialTypeController.ts @@ -57,21 +57,12 @@ export class CredentialTypesController { const schema = schemaResult.schema - const revocationDefinitionRepository = agent.dependencyManager.resolve( - AnonCredsRevocationRegistryDefinitionRepository, - ) - - const revocationDefinition = await revocationDefinitionRepository.findAllByCredentialDefinitionId( - agent.context, - record.credentialDefinitionId, - ) - return { id: record.credentialDefinitionId, name: (record.getTag('name') as string) ?? schema?.name, - version: (record.getTag('version') as string) ?? schema?.version, + version: (record.getTag('revocationDefinitionId') as string) ?? schema?.version, attributes: schema?.attrNames || [], - revocationId: revocationDefinition[0].revocationRegistryDefinitionId, + revocationId: record.getTag('revocationDefinitionId') as string, } }), ) @@ -159,7 +150,7 @@ export class CredentialTypesController { revocationRegistryDefinition: { credentialDefinitionId, tag: 'default', - maximumCredentialNumber: 1000, + maximumCredentialNumber: 10, issuerId, }, options: {}, @@ -197,6 +188,7 @@ export class CredentialTypesController { ) credentialDefinitionRecord.setTag('name', options.name) credentialDefinitionRecord.setTag('version', options.version) + credentialDefinitionRecord.setTag('revocationDefinitionId', revocationDefinitionId) await credentialDefinitionRepository.update(agent.context, credentialDefinitionRecord) return { diff --git a/packages/main/src/didWebServer.ts b/packages/main/src/didWebServer.ts index 4221153..7a79fae 100644 --- a/packages/main/src/didWebServer.ts +++ b/packages/main/src/didWebServer.ts @@ -2,7 +2,11 @@ import 'reflect-metadata' import type { DidWebServerConfig } from './utils/ServerConfig' -import { AnonCredsCredentialDefinitionRepository, AnonCredsSchemaRepository } from '@credo-ts/anoncreds' +import { + AnonCredsCredentialDefinitionRepository, + AnonCredsRevocationRegistryDefinitionRepository, + AnonCredsSchemaRepository, +} from '@credo-ts/anoncreds' import cors from 'cors' import { createHash } from 'crypto' import express from 'express' @@ -128,6 +132,28 @@ export const addDidWebRoutes = async ( res.send(404) }) + app.get('/anoncreds/v1/revRegDef/:revocationDefinitionId', async (req, res) => { + const revocationDefinitionId = req.params.revocationDefinitionId + + agent.config.logger.debug(`revocate definition requested: ${revocationDefinitionId}`) + const revocationDefinitionRepository = agent.dependencyManager.resolve( + AnonCredsRevocationRegistryDefinitionRepository, + ) + + const revocationDefinitionRecord = + await revocationDefinitionRepository.findByRevocationRegistryDefinitionId( + agent.context, + `${agent.did}?service=anoncreds&relativeRef=/revRegDef/${revocationDefinitionId}`, + ) + + if (revocationDefinitionRecord) { + res.send({ resource: revocationDefinitionRecord.revocationRegistryDefinition, resourceMetadata: {} }) + return + } + + res.send(404) + }) + // Allow to create invitation, no other way to ask for invitation yet app.get('/:tailsFileId', async (req, res) => { agent.config.logger.debug(`requested file`) From 206ab4c60df4915a1e5db0c02881560f89dab5e2 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Thu, 12 Dec 2024 08:44:02 -0500 Subject: [PATCH 011/132] feat: improve credential request message --- .../src/messages/CredentialRequestMessage.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/model/src/messages/CredentialRequestMessage.ts b/packages/model/src/messages/CredentialRequestMessage.ts index 0e543bd..837dcf9 100644 --- a/packages/model/src/messages/CredentialRequestMessage.ts +++ b/packages/model/src/messages/CredentialRequestMessage.ts @@ -1,5 +1,5 @@ import { Expose, Type } from 'class-transformer' -import { IsOptional, IsString, IsArray, IsInstance, ValidateNested } from 'class-validator' +import { IsOptional, IsString, IsArray, IsInstance, ValidateNested, IsNumber } from 'class-validator' import { BaseMessage, BaseMessageOptions } from './BaseMessage' import { Claim, ClaimOptions } from './CredentialIssuanceMessage' @@ -7,6 +7,8 @@ import { MessageType } from './MessageType' export interface CredentialRequestMessageOptions extends BaseMessageOptions { credentialDefinitionId: string + revocationDefinitionId?: string + revocationRegistryIndex?: number claims: ClaimOptions[] } @@ -20,6 +22,8 @@ export class CredentialRequestMessage extends BaseMessage { this.timestamp = options.timestamp ?? new Date() this.connectionId = options.connectionId this.credentialDefinitionId = options.credentialDefinitionId + this.revocationDefinitionId = options.revocationDefinitionId + this.revocationRegistryIndex = options.revocationRegistryIndex this.claims = options.claims.map(item => new Claim(item)) } } @@ -30,6 +34,16 @@ export class CredentialRequestMessage extends BaseMessage { @Expose() @IsString() public credentialDefinitionId!: string + + @Expose() + @IsString() + @IsOptional() + public revocationDefinitionId?: string + + @Expose() + @IsNumber() + @IsOptional() + public revocationRegistryIndex?: number @Expose() @Type(() => Claim) From e9d70d9b839772c95058e6cc182495e332214a0f Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Thu, 12 Dec 2024 16:00:30 -0500 Subject: [PATCH 012/132] feat: add update revocation status list --- packages/main/.gitignore | 1 + packages/main/src/didWebServer.ts | 36 ++++++++++++++++++- .../src/messages/CredentialRequestMessage.ts | 2 +- .../messages/calls/CallOfferRequestMessage.ts | 2 -- 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/packages/main/.gitignore b/packages/main/.gitignore index 43aeb53..0027e20 100644 --- a/packages/main/.gitignore +++ b/packages/main/.gitignore @@ -5,3 +5,4 @@ build *.txn logs.txt examples/**/afj +tails/ diff --git a/packages/main/src/didWebServer.ts b/packages/main/src/didWebServer.ts index 7a79fae..7432301 100644 --- a/packages/main/src/didWebServer.ts +++ b/packages/main/src/didWebServer.ts @@ -147,7 +147,41 @@ export const addDidWebRoutes = async ( ) if (revocationDefinitionRecord) { - res.send({ resource: revocationDefinitionRecord.revocationRegistryDefinition, resourceMetadata: {} }) + res.send({ + resource: revocationDefinitionRecord.revocationRegistryDefinition, + resourceMetadata: { + revocationStatusListEndpoint: `${anoncredsBaseUrl}/anoncreds/v1/revStatus/${revocationDefinitionId}` + } + }) + return + } + + res.send(404) + }) + + app.get('/anoncreds/v1/revStatus/:revocationDefinitionId', async (req, res) => { + const revocationDefinitionId = req.params.revocationDefinitionId + + agent.config.logger.debug(`revocate definition requested: ${revocationDefinitionId}`) + const revocationDefinitionRepository = agent.dependencyManager.resolve( + AnonCredsRevocationRegistryDefinitionRepository, + ) + + const revocationDefinitionRecord = + await revocationDefinitionRepository.findByRevocationRegistryDefinitionId( + agent.context, + `${agent.did}?service=anoncreds&relativeRef=/revRegDef/${revocationDefinitionId}`, + ) + + if (revocationDefinitionRecord) { + const revStatusList = revocationDefinitionRecord.metadata.get('revStatusList') + res.send({ + resource: revStatusList, + resourceMetadata: { + previousVersionId: "", + nextVersionId: "", + } + }) return } diff --git a/packages/model/src/messages/CredentialRequestMessage.ts b/packages/model/src/messages/CredentialRequestMessage.ts index 837dcf9..bd07194 100644 --- a/packages/model/src/messages/CredentialRequestMessage.ts +++ b/packages/model/src/messages/CredentialRequestMessage.ts @@ -34,7 +34,7 @@ export class CredentialRequestMessage extends BaseMessage { @Expose() @IsString() public credentialDefinitionId!: string - + @Expose() @IsString() @IsOptional() diff --git a/packages/model/src/messages/calls/CallOfferRequestMessage.ts b/packages/model/src/messages/calls/CallOfferRequestMessage.ts index 32a3442..c739912 100644 --- a/packages/model/src/messages/calls/CallOfferRequestMessage.ts +++ b/packages/model/src/messages/calls/CallOfferRequestMessage.ts @@ -32,11 +32,9 @@ export class CallOfferRequestMessage extends BaseMessage { public static readonly type = MessageType.CallOfferRequestMessage @IsOptional() - @Transform(({ value }) => DateParser(value)) public offerExpirationTime?: Date @IsOptional() - @Transform(({ value }) => DateParser(value)) public offerStartTime?: Date @Expose() From a83ec160063c5bc217d072f8dff92468cd0ff97b Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Thu, 12 Dec 2024 17:02:40 -0500 Subject: [PATCH 013/132] fix: add update revocation status list --- .../controllers/credentials/CredentialTypeController.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/main/src/controllers/credentials/CredentialTypeController.ts b/packages/main/src/controllers/credentials/CredentialTypeController.ts index 17b38cd..3e96dc8 100644 --- a/packages/main/src/controllers/credentials/CredentialTypeController.ts +++ b/packages/main/src/controllers/credentials/CredentialTypeController.ts @@ -186,10 +186,19 @@ export class CredentialTypesController { agent.context, credentialDefinitionId, ) + const revocationDefinitionRepository = agent.dependencyManager.resolve( + AnonCredsRevocationRegistryDefinitionRepository, + ) + const revocationDefinitionRecord = await revocationDefinitionRepository.getByRevocationRegistryDefinitionId( + agent.context, + revocationDefinitionId, + ) credentialDefinitionRecord.setTag('name', options.name) credentialDefinitionRecord.setTag('version', options.version) credentialDefinitionRecord.setTag('revocationDefinitionId', revocationDefinitionId) await credentialDefinitionRepository.update(agent.context, credentialDefinitionRecord) + revocationDefinitionRecord.metadata.add('revStatusList', revStatusListResult.revocationStatusListState.revocationStatusList!) + await revocationDefinitionRepository.update(agent.context, revocationDefinitionRecord) return { id: credentialDefinitionId, From 27b9a33d6cb638ef9e011d4f7af7eaf9f6db1e51 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Thu, 12 Dec 2024 17:08:15 -0500 Subject: [PATCH 014/132] fix: add update revocation status list --- .../main/src/controllers/credentials/CredentialTypeController.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/main/src/controllers/credentials/CredentialTypeController.ts b/packages/main/src/controllers/credentials/CredentialTypeController.ts index 3e96dc8..3c130bd 100644 --- a/packages/main/src/controllers/credentials/CredentialTypeController.ts +++ b/packages/main/src/controllers/credentials/CredentialTypeController.ts @@ -7,6 +7,7 @@ import { AnonCredsCredentialDefinitionRepository, AnonCredsKeyCorrectnessProofRecord, AnonCredsKeyCorrectnessProofRepository, + AnonCredsRevocationRegistryDefinitionRepository, AnonCredsSchema, AnonCredsSchemaRecord, AnonCredsSchemaRepository, From 4dcf175aaa15638c37e87dc7c57d27a8ce9235c4 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Thu, 12 Dec 2024 17:08:52 -0500 Subject: [PATCH 015/132] fix: add update revocation status list --- .../src/controllers/credentials/CredentialTypeController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/main/src/controllers/credentials/CredentialTypeController.ts b/packages/main/src/controllers/credentials/CredentialTypeController.ts index 3c130bd..5645ac0 100644 --- a/packages/main/src/controllers/credentials/CredentialTypeController.ts +++ b/packages/main/src/controllers/credentials/CredentialTypeController.ts @@ -168,7 +168,7 @@ export class CredentialTypesController { ) } - await agent.modules.anoncreds.registerRevocationStatusList({ + const revStatusListResult = await agent.modules.anoncreds.registerRevocationStatusList({ revocationStatusList: { issuerId, revocationRegistryDefinitionId: revocationDefinitionId, From a0363f2457acb8a25527c0465c331062716fb1ad Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Thu, 12 Dec 2024 17:10:03 -0500 Subject: [PATCH 016/132] feat: update index --- examples/chatbot/data.ts | 1 + examples/chatbot/index.ts | 47 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/examples/chatbot/data.ts b/examples/chatbot/data.ts index 9110fc6..3e8e97e 100644 --- a/examples/chatbot/data.ts +++ b/examples/chatbot/data.ts @@ -40,6 +40,7 @@ export const rootMenuAsQA = { { id: 'poll', text: '⚽ World Cup poll' }, { id: 'rocky', text: '💪 Rocky quotes' }, { id: 'issue', text: 'Issue credential' }, + { id: 'revoke', text: 'Revoke credential' }, { id: 'proof', text: 'Request proof' }, { id: 'help', text: '🆘 Help' }, ], diff --git a/examples/chatbot/index.ts b/examples/chatbot/index.ts index fecbbb3..d498b3a 100644 --- a/examples/chatbot/index.ts +++ b/examples/chatbot/index.ts @@ -58,6 +58,8 @@ app.set('json spaces', 2) const expressHandler = new ExpressEventHandler(app) let phoneNumberCredentialDefinitionId: string | undefined +let phoneNumberRevocationDefinitionId: string | undefined +let phoneNumberRevocationCount: number = 0 type OngoingCall = { wsUrl: string @@ -83,9 +85,19 @@ const server = app.listen(PORT, async () => { ) try { + const credentialDefinition = (await apiClient.credentialTypes.create({ + id: randomUUID(), + name: "phoneNumber", + version: '1.0', + attributes: ['phoneNumber'] + })) + // const credentialDefinition = (await apiClient.credentialTypes.import(phoneCredDefData)) phoneNumberCredentialDefinitionId = - phoneNumberCredentialType?.id ?? (await apiClient.credentialTypes.import(phoneCredDefData)).id + phoneNumberCredentialType?.id ?? credentialDefinition.id + phoneNumberRevocationDefinitionId = + phoneNumberCredentialType?.revocationId ?? credentialDefinition.revocationId logger.info(`phoneNumberCredentialDefinitionId: ${phoneNumberCredentialDefinitionId}`) + logger.info(`phoneNumberCredentialDefinitionId: ${phoneNumberRevocationDefinitionId}`) } catch (error) { logger.error(`Could not create or retrieve phone number credential type: ${error}`) } @@ -154,7 +166,33 @@ const handleMenuSelection = async (options: { connectionId: string; item: string // Issue credential if (selectedItem === 'issue' || selectedItem === 'Issue credential') { - if (!phoneNumberCredentialDefinitionId || phoneNumberCredentialDefinitionId === '') { + if (!phoneNumberCredentialDefinitionId || phoneNumberCredentialDefinitionId === '' || + !phoneNumberRevocationDefinitionId || phoneNumberRevocationDefinitionId === '') { + await sendTextMessage({ + connectionId, + content: 'Service not available', + }) + } else { + const body = new CredentialIssuanceMessage({ + connectionId, + credentialDefinitionId: phoneNumberCredentialDefinitionId, + claims: [ + { + name: 'phoneNumber', + mimeType: 'text/plain', + value: '+5712345678', + }, + ], + revocationDefinitionId: phoneNumberRevocationDefinitionId, + revocationRegistryIndex: phoneNumberRevocationCount += 1, + }) + await apiClient.messages.send(body) + } + } + + if (selectedItem === 'revoke' || selectedItem === 'Revoke credential') { + if (!phoneNumberCredentialDefinitionId || phoneNumberCredentialDefinitionId === '' || + !phoneNumberRevocationDefinitionId || phoneNumberRevocationDefinitionId === '') { await sendTextMessage({ connectionId, content: 'Service not available', @@ -170,6 +208,8 @@ const handleMenuSelection = async (options: { connectionId: string; item: string value: '+5712345678', }, ], + revocationDefinitionId: phoneNumberRevocationDefinitionId, + revocationRegistryIndex: phoneNumberRevocationCount, }) await apiClient.messages.send(body) } @@ -383,6 +423,9 @@ expressHandler.messageReceived(async (req, res) => { const body = new CallOfferRequestMessage({ connectionId: obj.connectionId, description: 'Start call', + offerExpirationTime: new Date( + Date.now() + 60 * 1000, + ), parameters: { wsUrl, roomId, peerId }, }) await apiClient.messages.send(body) From ce1fc60a85716cb784ee163f4d8ea7e009543444 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Thu, 12 Dec 2024 20:20:28 -0500 Subject: [PATCH 017/132] feat: update credential generation --- package.json | 1 + .../credentials/CredentialTypeController.ts | 16 ++++++++++----- packages/main/src/didWebServer.ts | 20 +++++++++---------- .../messages/calls/CallOfferRequestMessage.ts | 3 +-- 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 2b6ce82..a02ddbe 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "prebuild": "yarn workspace @2060.io/service-agent-model run build", "build": "yarn workspaces run build", "start": "yarn workspace @2060.io/service-agent-main run start", + "start:dev": "yarn workspace @2060.io/service-agent-main run start:dev", "check-types": "yarn check-types:build", "check-types:build": "yarn workspaces run tsc --noEmit -p tsconfig.build.json", "format": "prettier \"packages/*/src/**/*.ts\" --write", diff --git a/packages/main/src/controllers/credentials/CredentialTypeController.ts b/packages/main/src/controllers/credentials/CredentialTypeController.ts index 5645ac0..c08cb71 100644 --- a/packages/main/src/controllers/credentials/CredentialTypeController.ts +++ b/packages/main/src/controllers/credentials/CredentialTypeController.ts @@ -176,6 +176,7 @@ export class CredentialTypesController { options: {}, }) + this.logger.log(`revStatusListResult: ${JSON.stringify(revStatusListResult)}`) this.logger.log(`Credential Definition Id: ${credentialDefinitionId}`) this.logger.log(`Revocation Definition Id: ${revocationDefinitionId}`) @@ -190,15 +191,20 @@ export class CredentialTypesController { const revocationDefinitionRepository = agent.dependencyManager.resolve( AnonCredsRevocationRegistryDefinitionRepository, ) - const revocationDefinitionRecord = await revocationDefinitionRepository.getByRevocationRegistryDefinitionId( - agent.context, - revocationDefinitionId, - ) + const revocationDefinitionRecord = + await revocationDefinitionRepository.getByRevocationRegistryDefinitionId( + agent.context, + revocationDefinitionId, + ) credentialDefinitionRecord.setTag('name', options.name) credentialDefinitionRecord.setTag('version', options.version) credentialDefinitionRecord.setTag('revocationDefinitionId', revocationDefinitionId) + await credentialDefinitionRepository.update(agent.context, credentialDefinitionRecord) - revocationDefinitionRecord.metadata.add('revStatusList', revStatusListResult.revocationStatusListState.revocationStatusList!) + revocationDefinitionRecord.metadata.set( + 'revStatusList', + revStatusListResult.revocationStatusListState.revocationStatusList!, + ) await revocationDefinitionRepository.update(agent.context, revocationDefinitionRecord) return { diff --git a/packages/main/src/didWebServer.ts b/packages/main/src/didWebServer.ts index 7432301..f046da0 100644 --- a/packages/main/src/didWebServer.ts +++ b/packages/main/src/didWebServer.ts @@ -147,11 +147,11 @@ export const addDidWebRoutes = async ( ) if (revocationDefinitionRecord) { - res.send({ - resource: revocationDefinitionRecord.revocationRegistryDefinition, + res.send({ + resource: revocationDefinitionRecord.revocationRegistryDefinition, resourceMetadata: { - revocationStatusListEndpoint: `${anoncredsBaseUrl}/anoncreds/v1/revStatus/${revocationDefinitionId}` - } + statusListEndpoint: `${anoncredsBaseUrl}/anoncreds/v1/revStatus/${revocationDefinitionId}`, + }, }) return } @@ -159,7 +159,7 @@ export const addDidWebRoutes = async ( res.send(404) }) - app.get('/anoncreds/v1/revStatus/:revocationDefinitionId', async (req, res) => { + app.get('/anoncreds/v1/revStatus/:revocationDefinitionId/:timestamp?', async (req, res) => { const revocationDefinitionId = req.params.revocationDefinitionId agent.config.logger.debug(`revocate definition requested: ${revocationDefinitionId}`) @@ -175,12 +175,12 @@ export const addDidWebRoutes = async ( if (revocationDefinitionRecord) { const revStatusList = revocationDefinitionRecord.metadata.get('revStatusList') - res.send({ - resource: revStatusList, + res.send({ + resource: revStatusList, resourceMetadata: { - previousVersionId: "", - nextVersionId: "", - } + previousVersionId: '', + nextVersionId: '', + }, }) return } diff --git a/packages/model/src/messages/calls/CallOfferRequestMessage.ts b/packages/model/src/messages/calls/CallOfferRequestMessage.ts index c739912..9ba4401 100644 --- a/packages/model/src/messages/calls/CallOfferRequestMessage.ts +++ b/packages/model/src/messages/calls/CallOfferRequestMessage.ts @@ -1,5 +1,4 @@ -import { DateParser } from '@credo-ts/core/build/utils/transformers' -import { Expose, Transform } from 'class-transformer' +import { Expose } from 'class-transformer' import { IsOptional, IsString } from 'class-validator' import { BaseMessage, BaseMessageOptions } from '../BaseMessage' From aa74e169ca08183fbb0ab6e5f7a54073eb75308c Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Thu, 12 Dec 2024 21:53:29 -0500 Subject: [PATCH 018/132] fix: update --- packages/model/src/messages/calls/CallOfferRequestMessage.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/model/src/messages/calls/CallOfferRequestMessage.ts b/packages/model/src/messages/calls/CallOfferRequestMessage.ts index 9ba4401..32a3442 100644 --- a/packages/model/src/messages/calls/CallOfferRequestMessage.ts +++ b/packages/model/src/messages/calls/CallOfferRequestMessage.ts @@ -1,4 +1,5 @@ -import { Expose } from 'class-transformer' +import { DateParser } from '@credo-ts/core/build/utils/transformers' +import { Expose, Transform } from 'class-transformer' import { IsOptional, IsString } from 'class-validator' import { BaseMessage, BaseMessageOptions } from '../BaseMessage' @@ -31,9 +32,11 @@ export class CallOfferRequestMessage extends BaseMessage { public static readonly type = MessageType.CallOfferRequestMessage @IsOptional() + @Transform(({ value }) => DateParser(value)) public offerExpirationTime?: Date @IsOptional() + @Transform(({ value }) => DateParser(value)) public offerStartTime?: Date @Expose() From 709567bf16ace1c7ca95933b46edd877d1d62c42 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Thu, 12 Dec 2024 23:14:03 -0500 Subject: [PATCH 019/132] fix: update --- .../src/controllers/credentials/CredentialTypeController.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/main/src/controllers/credentials/CredentialTypeController.ts b/packages/main/src/controllers/credentials/CredentialTypeController.ts index c08cb71..2bf7ded 100644 --- a/packages/main/src/controllers/credentials/CredentialTypeController.ts +++ b/packages/main/src/controllers/credentials/CredentialTypeController.ts @@ -61,7 +61,7 @@ export class CredentialTypesController { return { id: record.credentialDefinitionId, name: (record.getTag('name') as string) ?? schema?.name, - version: (record.getTag('revocationDefinitionId') as string) ?? schema?.version, + version: (record.getTag('version') as string) ?? schema?.version, attributes: schema?.attrNames || [], revocationId: record.getTag('revocationDefinitionId') as string, } @@ -176,7 +176,6 @@ export class CredentialTypesController { options: {}, }) - this.logger.log(`revStatusListResult: ${JSON.stringify(revStatusListResult)}`) this.logger.log(`Credential Definition Id: ${credentialDefinitionId}`) this.logger.log(`Revocation Definition Id: ${revocationDefinitionId}`) From 69b2465b4f206692198ec60445e5134b568f6f9a Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Fri, 13 Dec 2024 08:52:27 -0500 Subject: [PATCH 020/132] docs: update --- doc/service-agent-api.md | 9 +++++++-- .../model/src/messages/CredentialRequestMessage.ts | 14 -------------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/doc/service-agent-api.md b/doc/service-agent-api.md index 3486bec..16938a9 100644 --- a/doc/service-agent-api.md +++ b/doc/service-agent-api.md @@ -149,16 +149,21 @@ By sending this message, a Verifiable Credential is effectively issued and sent This message could be sent as a response to a Credential Request. In such case, `threadId` is used to identify credential details. But it can also start a new Credential Issuance flow, and specify Parameters: - - (optional) Credential Definition ID +- (optional) Revocation Definition ID +- (optional) Revocation Index - (optional) Claims +> **Note:** When using revocation parameters (`revocationDefinitionId` and `revocationRegistryIndex`), it is essential to ensure the `id` was preserved from the time it was generated with the credential. The `revocationRegistryIndex` serves as a reference to the specific credential in the revocation registry. + ```json { ... "type": "credential-issuance", "credentialDefinitionId": "id", - "claims": [{ "name": "claim-name", "mimeType": "mime-type", "value": "claim-value" }, ...] + "revocationDefinitionId": "id", + "revocationRegistryIndex": 1, + "claims": [{ "name": "claim-name", "mimeType": "mime-type", "value": "claim-value" }, ...] } ``` diff --git a/packages/model/src/messages/CredentialRequestMessage.ts b/packages/model/src/messages/CredentialRequestMessage.ts index bd07194..295a311 100644 --- a/packages/model/src/messages/CredentialRequestMessage.ts +++ b/packages/model/src/messages/CredentialRequestMessage.ts @@ -7,8 +7,6 @@ import { MessageType } from './MessageType' export interface CredentialRequestMessageOptions extends BaseMessageOptions { credentialDefinitionId: string - revocationDefinitionId?: string - revocationRegistryIndex?: number claims: ClaimOptions[] } @@ -22,8 +20,6 @@ export class CredentialRequestMessage extends BaseMessage { this.timestamp = options.timestamp ?? new Date() this.connectionId = options.connectionId this.credentialDefinitionId = options.credentialDefinitionId - this.revocationDefinitionId = options.revocationDefinitionId - this.revocationRegistryIndex = options.revocationRegistryIndex this.claims = options.claims.map(item => new Claim(item)) } } @@ -35,16 +31,6 @@ export class CredentialRequestMessage extends BaseMessage { @IsString() public credentialDefinitionId!: string - @Expose() - @IsString() - @IsOptional() - public revocationDefinitionId?: string - - @Expose() - @IsNumber() - @IsOptional() - public revocationRegistryIndex?: number - @Expose() @Type(() => Claim) @IsArray() From 04b345f33b4319c1d48b7ad642d3ce6f6e4cb07b Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Fri, 13 Dec 2024 08:53:18 -0500 Subject: [PATCH 021/132] fix: update --- packages/model/src/messages/CredentialRequestMessage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/model/src/messages/CredentialRequestMessage.ts b/packages/model/src/messages/CredentialRequestMessage.ts index 295a311..0e543bd 100644 --- a/packages/model/src/messages/CredentialRequestMessage.ts +++ b/packages/model/src/messages/CredentialRequestMessage.ts @@ -1,5 +1,5 @@ import { Expose, Type } from 'class-transformer' -import { IsOptional, IsString, IsArray, IsInstance, ValidateNested, IsNumber } from 'class-validator' +import { IsOptional, IsString, IsArray, IsInstance, ValidateNested } from 'class-validator' import { BaseMessage, BaseMessageOptions } from './BaseMessage' import { Claim, ClaimOptions } from './CredentialIssuanceMessage' From 37c335a23b921b77773514f8fa6fea2d8f1df6e1 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Fri, 13 Dec 2024 10:34:59 -0500 Subject: [PATCH 022/132] fix: return --- examples/chatbot/data.ts | 3 +-- examples/chatbot/index.ts | 49 +++------------------------------------ 2 files changed, 4 insertions(+), 48 deletions(-) diff --git a/examples/chatbot/data.ts b/examples/chatbot/data.ts index 3e8e97e..48581e1 100644 --- a/examples/chatbot/data.ts +++ b/examples/chatbot/data.ts @@ -40,7 +40,6 @@ export const rootMenuAsQA = { { id: 'poll', text: '⚽ World Cup poll' }, { id: 'rocky', text: '💪 Rocky quotes' }, { id: 'issue', text: 'Issue credential' }, - { id: 'revoke', text: 'Revoke credential' }, { id: 'proof', text: 'Request proof' }, { id: 'help', text: '🆘 Help' }, ], @@ -100,4 +99,4 @@ export const rockyQuotes = [ 'I must break you!', 'Yo, Adrian, I did it!', "It ain't about how hard you hit. It's about how hard you can get hit and keep moving forward; how much you can take and keep moving forward. That's how winning is done!", -] +] \ No newline at end of file diff --git a/examples/chatbot/index.ts b/examples/chatbot/index.ts index d498b3a..d23ed10 100644 --- a/examples/chatbot/index.ts +++ b/examples/chatbot/index.ts @@ -58,8 +58,6 @@ app.set('json spaces', 2) const expressHandler = new ExpressEventHandler(app) let phoneNumberCredentialDefinitionId: string | undefined -let phoneNumberRevocationDefinitionId: string | undefined -let phoneNumberRevocationCount: number = 0 type OngoingCall = { wsUrl: string @@ -85,19 +83,9 @@ const server = app.listen(PORT, async () => { ) try { - const credentialDefinition = (await apiClient.credentialTypes.create({ - id: randomUUID(), - name: "phoneNumber", - version: '1.0', - attributes: ['phoneNumber'] - })) - // const credentialDefinition = (await apiClient.credentialTypes.import(phoneCredDefData)) phoneNumberCredentialDefinitionId = - phoneNumberCredentialType?.id ?? credentialDefinition.id - phoneNumberRevocationDefinitionId = - phoneNumberCredentialType?.revocationId ?? credentialDefinition.revocationId + phoneNumberCredentialType?.id ?? (await apiClient.credentialTypes.import(phoneCredDefData)).id logger.info(`phoneNumberCredentialDefinitionId: ${phoneNumberCredentialDefinitionId}`) - logger.info(`phoneNumberCredentialDefinitionId: ${phoneNumberRevocationDefinitionId}`) } catch (error) { logger.error(`Could not create or retrieve phone number credential type: ${error}`) } @@ -166,33 +154,7 @@ const handleMenuSelection = async (options: { connectionId: string; item: string // Issue credential if (selectedItem === 'issue' || selectedItem === 'Issue credential') { - if (!phoneNumberCredentialDefinitionId || phoneNumberCredentialDefinitionId === '' || - !phoneNumberRevocationDefinitionId || phoneNumberRevocationDefinitionId === '') { - await sendTextMessage({ - connectionId, - content: 'Service not available', - }) - } else { - const body = new CredentialIssuanceMessage({ - connectionId, - credentialDefinitionId: phoneNumberCredentialDefinitionId, - claims: [ - { - name: 'phoneNumber', - mimeType: 'text/plain', - value: '+5712345678', - }, - ], - revocationDefinitionId: phoneNumberRevocationDefinitionId, - revocationRegistryIndex: phoneNumberRevocationCount += 1, - }) - await apiClient.messages.send(body) - } - } - - if (selectedItem === 'revoke' || selectedItem === 'Revoke credential') { - if (!phoneNumberCredentialDefinitionId || phoneNumberCredentialDefinitionId === '' || - !phoneNumberRevocationDefinitionId || phoneNumberRevocationDefinitionId === '') { + if (!phoneNumberCredentialDefinitionId || phoneNumberCredentialDefinitionId === '') { await sendTextMessage({ connectionId, content: 'Service not available', @@ -208,8 +170,6 @@ const handleMenuSelection = async (options: { connectionId: string; item: string value: '+5712345678', }, ], - revocationDefinitionId: phoneNumberRevocationDefinitionId, - revocationRegistryIndex: phoneNumberRevocationCount, }) await apiClient.messages.send(body) } @@ -423,9 +383,6 @@ expressHandler.messageReceived(async (req, res) => { const body = new CallOfferRequestMessage({ connectionId: obj.connectionId, description: 'Start call', - offerExpirationTime: new Date( - Date.now() + 60 * 1000, - ), parameters: { wsUrl, roomId, peerId }, }) await apiClient.messages.send(body) @@ -582,4 +539,4 @@ app.put('/call-failure/:connectionId', (req, res) => { ) }) -export { app, server } +export { app, server } \ No newline at end of file From a9aff6b8cef76d203f2896c60ba3768ce2ea2912 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Fri, 13 Dec 2024 10:36:21 -0500 Subject: [PATCH 023/132] fix: return --- examples/chatbot/data.ts | 2 +- examples/chatbot/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/chatbot/data.ts b/examples/chatbot/data.ts index 48581e1..9110fc6 100644 --- a/examples/chatbot/data.ts +++ b/examples/chatbot/data.ts @@ -99,4 +99,4 @@ export const rockyQuotes = [ 'I must break you!', 'Yo, Adrian, I did it!', "It ain't about how hard you hit. It's about how hard you can get hit and keep moving forward; how much you can take and keep moving forward. That's how winning is done!", -] \ No newline at end of file +] diff --git a/examples/chatbot/index.ts b/examples/chatbot/index.ts index d23ed10..fecbbb3 100644 --- a/examples/chatbot/index.ts +++ b/examples/chatbot/index.ts @@ -539,4 +539,4 @@ app.put('/call-failure/:connectionId', (req, res) => { ) }) -export { app, server } \ No newline at end of file +export { app, server } From 0815f41a811d9f703643014fb94eb0dc5294e851 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Fri, 13 Dec 2024 13:52:55 -0500 Subject: [PATCH 024/132] feat: add CredentialRevocationMessage --- .../src/controllers/message/MessageService.ts | 17 ++++++++ .../messages/CredentialRevocationMessage.ts | 42 +++++++++++++++++++ packages/model/src/messages/MessageType.ts | 1 + packages/model/src/messages/index.ts | 1 + 4 files changed, 61 insertions(+) create mode 100644 packages/model/src/messages/CredentialRevocationMessage.ts diff --git a/packages/main/src/controllers/message/MessageService.ts b/packages/main/src/controllers/message/MessageService.ts index d7c44d3..2f4376f 100644 --- a/packages/main/src/controllers/message/MessageService.ts +++ b/packages/main/src/controllers/message/MessageService.ts @@ -17,6 +17,7 @@ import { EMrtdDataRequestMessage, VerifiableCredentialRequestedProofItem, RequestedCredential, + CredentialRevocationMessage, } from '@2060.io/service-agent-model' import { ActionMenuRole, ActionMenuOption } from '@credo-ts/action-menu' import { AnonCredsRequestedAttribute } from '@credo-ts/anoncreds' @@ -237,6 +238,22 @@ export class MessageService { ) } } + } else if (messageType === CredentialRevocationMessage.type) { + const msg = JsonTransformer.fromJSON(message, CredentialRevocationMessage) + + this.logger.debug!(`CredentialRevocationMessage: ${JSON.stringify(message)}`) + const credential = (await agent.credentials.getAll()).find(item => item.threadId === message.threadId) + if (credential) { + await agent.credentials.sendRevocationNotification({ + credentialRecordId: credential.id, + revocationFormat: 'anoncreds', + revocationId: `${msg.revocationDefinitionId}::${msg.revocationRegistryIndex}`, + }) + } else { + throw new Error( + `No credentials were found for revocation associated with the provided credentialDefinitionId: ${msg.credentialDefinitionId}.`, + ) + } } else if (messageType === InvitationMessage.type) { const msg = JsonTransformer.fromJSON(message, InvitationMessage) const { label, imageUrl, did } = msg diff --git a/packages/model/src/messages/CredentialRevocationMessage.ts b/packages/model/src/messages/CredentialRevocationMessage.ts new file mode 100644 index 0000000..f6732f5 --- /dev/null +++ b/packages/model/src/messages/CredentialRevocationMessage.ts @@ -0,0 +1,42 @@ +import { Expose } from 'class-transformer' +import { IsString, IsNumber } from 'class-validator' + +import { BaseMessage, BaseMessageOptions } from './BaseMessage' +import { MessageType } from './MessageType' + +export interface CredentialRevocationMessageOptions extends BaseMessageOptions { + credentialDefinitionId: string + revocationDefinitionId: string + revocationRegistryIndex: number +} + +export class CredentialRevocationMessage extends BaseMessage { + public constructor(options: CredentialRevocationMessageOptions) { + super() + + if (options) { + this.id = options.id ?? this.generateId() + this.threadId = options.threadId + this.timestamp = options.timestamp ?? new Date() + this.connectionId = options.connectionId + this.credentialDefinitionId = options.credentialDefinitionId + this.revocationDefinitionId = options.revocationDefinitionId + this.revocationRegistryIndex = options.revocationRegistryIndex + } + } + + public readonly type = CredentialRevocationMessage.type + public static readonly type = MessageType.CredentialRevocationMessage + + @Expose() + @IsString() + public credentialDefinitionId!: string + + @Expose() + @IsString() + public revocationDefinitionId!: string + + @Expose() + @IsNumber() + public revocationRegistryIndex!: number +} diff --git a/packages/model/src/messages/MessageType.ts b/packages/model/src/messages/MessageType.ts index e01e05b..6a8dea8 100644 --- a/packages/model/src/messages/MessageType.ts +++ b/packages/model/src/messages/MessageType.ts @@ -7,6 +7,7 @@ export enum MessageType { ContextualMenuSelectMessage = 'contextual-menu-select', ContextualMenuUpdateMessage = 'contextual-menu-update', CredentialIssuanceMessage = 'credential-issuance', + CredentialRevocationMessage = 'credential-revocation', CredentialReceptionMessage = 'credential-reception', CredentialRequestMessage = 'credential-request', EMrtdDataRequestMessage = 'emrtd-data-request', diff --git a/packages/model/src/messages/index.ts b/packages/model/src/messages/index.ts index c0637ec..3051237 100644 --- a/packages/model/src/messages/index.ts +++ b/packages/model/src/messages/index.ts @@ -2,6 +2,7 @@ export * from './BaseMessage' export * from './CredentialIssuanceMessage' export * from './CredentialReceptionMessage' export * from './CredentialRequestMessage' +export * from './CredentialRevocationMessage' export * from './ContextualMenuRequest' export * from './ContextualMenuSelect' export * from './ContextualMenuUpdate' From 8e3ac1f17e2c42a6a89c3bcb7bfbfd4ce51e212c Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Mon, 16 Dec 2024 12:34:55 -0500 Subject: [PATCH 025/132] feat: add CredentialRevocationMessage --- examples/chatbot/data.ts | 5 ++++ examples/chatbot/index.ts | 29 +++++++++++++++++-- .../src/controllers/message/MessageService.ts | 13 +++++---- .../messages/CredentialRevocationMessage.ts | 14 +-------- 4 files changed, 40 insertions(+), 21 deletions(-) diff --git a/examples/chatbot/data.ts b/examples/chatbot/data.ts index 9110fc6..dd70776 100644 --- a/examples/chatbot/data.ts +++ b/examples/chatbot/data.ts @@ -23,6 +23,10 @@ export const rootContextMenu = { title: 'Issue credential', id: 'issue', }, + { + title: 'Revoke credential', + id: 'revoke', + }, { title: 'Request proof', id: 'proof', @@ -40,6 +44,7 @@ export const rootMenuAsQA = { { id: 'poll', text: '⚽ World Cup poll' }, { id: 'rocky', text: '💪 Rocky quotes' }, { id: 'issue', text: 'Issue credential' }, + { id: 'revoke', text: 'Revoke credential' }, { id: 'proof', text: 'Request proof' }, { id: 'help', text: '🆘 Help' }, ], diff --git a/examples/chatbot/index.ts b/examples/chatbot/index.ts index fecbbb3..703dc26 100644 --- a/examples/chatbot/index.ts +++ b/examples/chatbot/index.ts @@ -4,6 +4,7 @@ import { ContextualMenuSelectMessage, ContextualMenuUpdateMessage, CredentialIssuanceMessage, + CredentialRevocationMessage, EMrtdDataRequestMessage, EMrtdDataSubmitMessage, IdentityProofRequestMessage, @@ -58,6 +59,8 @@ app.set('json spaces', 2) const expressHandler = new ExpressEventHandler(app) let phoneNumberCredentialDefinitionId: string | undefined +let phoneNumberRevocationDefinitionId: string | undefined +let phoneNumberRevocationCount: number = 0 type OngoingCall = { wsUrl: string @@ -84,8 +87,11 @@ const server = app.listen(PORT, async () => { try { phoneNumberCredentialDefinitionId = - phoneNumberCredentialType?.id ?? (await apiClient.credentialTypes.import(phoneCredDefData)).id + phoneNumberCredentialType?.id ?? credentialDefinition.id + phoneNumberRevocationDefinitionId = + phoneNumberCredentialType?.revocationId ?? credentialDefinition.revocationId logger.info(`phoneNumberCredentialDefinitionId: ${phoneNumberCredentialDefinitionId}`) + logger.info(`phoneNumberRevocationDefinitionId: ${phoneNumberRevocationDefinitionId}`) } catch (error) { logger.error(`Could not create or retrieve phone number credential type: ${error}`) } @@ -154,7 +160,8 @@ const handleMenuSelection = async (options: { connectionId: string; item: string // Issue credential if (selectedItem === 'issue' || selectedItem === 'Issue credential') { - if (!phoneNumberCredentialDefinitionId || phoneNumberCredentialDefinitionId === '') { + if (!phoneNumberCredentialDefinitionId || phoneNumberCredentialDefinitionId === '' || + !phoneNumberRevocationDefinitionId || phoneNumberRevocationDefinitionId === '') { await sendTextMessage({ connectionId, content: 'Service not available', @@ -170,6 +177,24 @@ const handleMenuSelection = async (options: { connectionId: string; item: string value: '+5712345678', }, ], + revocationDefinitionId: phoneNumberRevocationDefinitionId, + revocationRegistryIndex: phoneNumberRevocationCount += 1, + }) + await apiClient.messages.send(body) + } + } + + if (selectedItem === 'revoke' || selectedItem === 'Revoke credential') { + if (!phoneNumberCredentialDefinitionId || phoneNumberCredentialDefinitionId === '' || + !phoneNumberRevocationDefinitionId || phoneNumberRevocationDefinitionId === '') { + await sendTextMessage({ + connectionId, + content: 'Service not available', + }) + } else { + const body = new CredentialRevocationMessage({ + connectionId, + revocationDefinitionId: phoneNumberRevocationDefinitionId, }) await apiClient.messages.send(body) } diff --git a/packages/main/src/controllers/message/MessageService.ts b/packages/main/src/controllers/message/MessageService.ts index 2f4376f..21817b4 100644 --- a/packages/main/src/controllers/message/MessageService.ts +++ b/packages/main/src/controllers/message/MessageService.ts @@ -241,18 +241,19 @@ export class MessageService { } else if (messageType === CredentialRevocationMessage.type) { const msg = JsonTransformer.fromJSON(message, CredentialRevocationMessage) - this.logger.debug!(`CredentialRevocationMessage: ${JSON.stringify(message)}`) - const credential = (await agent.credentials.getAll()).find(item => item.threadId === message.threadId) + const credential = (await agent.credentials.getAll()).find( + item => + item.getTag('anonCredsRevocationRegistryId') === msg.revocationDefinitionId && + item.connectionId === msg.connectionId, + ) if (credential) { await agent.credentials.sendRevocationNotification({ credentialRecordId: credential.id, revocationFormat: 'anoncreds', - revocationId: `${msg.revocationDefinitionId}::${msg.revocationRegistryIndex}`, + revocationId: `${credential.getTag('anonCredsRevocationRegistryId')}::${credential.getTag('anonCredsCredentialRevocationId')}`, }) } else { - throw new Error( - `No credentials were found for revocation associated with the provided credentialDefinitionId: ${msg.credentialDefinitionId}.`, - ) + throw new Error(`No credentials were found for connection: ${msg.connectionId}.`) } } else if (messageType === InvitationMessage.type) { const msg = JsonTransformer.fromJSON(message, InvitationMessage) diff --git a/packages/model/src/messages/CredentialRevocationMessage.ts b/packages/model/src/messages/CredentialRevocationMessage.ts index f6732f5..b4d6399 100644 --- a/packages/model/src/messages/CredentialRevocationMessage.ts +++ b/packages/model/src/messages/CredentialRevocationMessage.ts @@ -1,13 +1,11 @@ import { Expose } from 'class-transformer' -import { IsString, IsNumber } from 'class-validator' +import { IsString } from 'class-validator' import { BaseMessage, BaseMessageOptions } from './BaseMessage' import { MessageType } from './MessageType' export interface CredentialRevocationMessageOptions extends BaseMessageOptions { - credentialDefinitionId: string revocationDefinitionId: string - revocationRegistryIndex: number } export class CredentialRevocationMessage extends BaseMessage { @@ -19,24 +17,14 @@ export class CredentialRevocationMessage extends BaseMessage { this.threadId = options.threadId this.timestamp = options.timestamp ?? new Date() this.connectionId = options.connectionId - this.credentialDefinitionId = options.credentialDefinitionId this.revocationDefinitionId = options.revocationDefinitionId - this.revocationRegistryIndex = options.revocationRegistryIndex } } public readonly type = CredentialRevocationMessage.type public static readonly type = MessageType.CredentialRevocationMessage - @Expose() - @IsString() - public credentialDefinitionId!: string - @Expose() @IsString() public revocationDefinitionId!: string - - @Expose() - @IsNumber() - public revocationRegistryIndex!: number } From 44e3be703d808bf2aa9faf8c97c32b26715ea5c8 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Mon, 16 Dec 2024 14:14:05 -0500 Subject: [PATCH 026/132] add: docs --- doc/service-agent-api.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/doc/service-agent-api.md b/doc/service-agent-api.md index 16938a9..04052b2 100644 --- a/doc/service-agent-api.md +++ b/doc/service-agent-api.md @@ -166,6 +166,19 @@ Parameters: "claims": [{ "name": "claim-name", "mimeType": "mime-type", "value": "claim-value" }, ...] } ``` +#### Credential Revocation + +By sending this message, a Verifiable Credential is effectively revocated and sent to the destination connection. + +This message could be sent as a response to a Credential issuance. In such case, `connectionId` and `revocationDefinitionId` is used to identify credential details. + +```json +{ + ... + "type": "credential-revocation", + "revocationDefinitionId": "id", +} +``` #### Credential Reception From 6a559e1a48277cdb98bb2dc2fcc49f2a2059e9e0 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Mon, 16 Dec 2024 14:20:58 -0500 Subject: [PATCH 027/132] docs: update --- packages/main/src/didWebServer.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/main/src/didWebServer.ts b/packages/main/src/didWebServer.ts index f046da0..6939c36 100644 --- a/packages/main/src/didWebServer.ts +++ b/packages/main/src/didWebServer.ts @@ -132,6 +132,7 @@ export const addDidWebRoutes = async ( res.send(404) }) + // Endpoint to retrieve a revocation registry definition by its ID app.get('/anoncreds/v1/revRegDef/:revocationDefinitionId', async (req, res) => { const revocationDefinitionId = req.params.revocationDefinitionId @@ -159,6 +160,8 @@ export const addDidWebRoutes = async ( res.send(404) }) + // Endpoint to retrieve the revocation status list for a specific revocation definition ID + // Optional: Accepts a timestamp parameter (not currently used in the logic) app.get('/anoncreds/v1/revStatus/:revocationDefinitionId/:timestamp?', async (req, res) => { const revocationDefinitionId = req.params.revocationDefinitionId @@ -225,6 +228,7 @@ export const addDidWebRoutes = async ( } }) + // Endpoint to upload a tails file for a specific tailsFileId app.put('/:tailsFileId', multer({ storage: fileStorage }).single('file'), async (req, res) => { agent.config.logger.info(`tails file upload: ${req.params.tailsFileId}`) From 9912183509efd56d5c99a568fbabbf627a4ad579 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Mon, 16 Dec 2024 14:33:45 -0500 Subject: [PATCH 028/132] feat: update chatbot demo --- examples/chatbot/index.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/examples/chatbot/index.ts b/examples/chatbot/index.ts index 703dc26..a253a58 100644 --- a/examples/chatbot/index.ts +++ b/examples/chatbot/index.ts @@ -86,6 +86,13 @@ const server = app.listen(PORT, async () => { ) try { + const credentialDefinition = (await apiClient.credentialTypes.create({ + id: randomUUID(), + name: "phoneNumber", + version: '1.0', + attributes: ['phoneNumber'] + })) + // const credentialDefinition = (await apiClient.credentialTypes.import(phoneCredDefData)) phoneNumberCredentialDefinitionId = phoneNumberCredentialType?.id ?? credentialDefinition.id phoneNumberRevocationDefinitionId = From 32b3661c2fba9be3d0f89a54a7d117dc29156c95 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Mon, 16 Dec 2024 14:56:21 -0500 Subject: [PATCH 029/132] feat: update chatbot demo --- examples/chatbot/index.ts | 11 ++--------- .../credentials/CredentialTypeController.ts | 2 +- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/examples/chatbot/index.ts b/examples/chatbot/index.ts index a253a58..852fa26 100644 --- a/examples/chatbot/index.ts +++ b/examples/chatbot/index.ts @@ -86,13 +86,7 @@ const server = app.listen(PORT, async () => { ) try { - const credentialDefinition = (await apiClient.credentialTypes.create({ - id: randomUUID(), - name: "phoneNumber", - version: '1.0', - attributes: ['phoneNumber'] - })) - // const credentialDefinition = (await apiClient.credentialTypes.import(phoneCredDefData)) + const credentialDefinition = (await apiClient.credentialTypes.import(phoneCredDefData)) phoneNumberCredentialDefinitionId = phoneNumberCredentialType?.id ?? credentialDefinition.id phoneNumberRevocationDefinitionId = @@ -167,8 +161,7 @@ const handleMenuSelection = async (options: { connectionId: string; item: string // Issue credential if (selectedItem === 'issue' || selectedItem === 'Issue credential') { - if (!phoneNumberCredentialDefinitionId || phoneNumberCredentialDefinitionId === '' || - !phoneNumberRevocationDefinitionId || phoneNumberRevocationDefinitionId === '') { + if (!phoneNumberCredentialDefinitionId || phoneNumberCredentialDefinitionId === '') { await sendTextMessage({ connectionId, content: 'Service not available', diff --git a/packages/main/src/controllers/credentials/CredentialTypeController.ts b/packages/main/src/controllers/credentials/CredentialTypeController.ts index 2bf7ded..e93d7b5 100644 --- a/packages/main/src/controllers/credentials/CredentialTypeController.ts +++ b/packages/main/src/controllers/credentials/CredentialTypeController.ts @@ -151,7 +151,7 @@ export class CredentialTypesController { revocationRegistryDefinition: { credentialDefinitionId, tag: 'default', - maximumCredentialNumber: 10, + maximumCredentialNumber: 1000, issuerId, }, options: {}, From 26ce01ba79f423473707e9d1a5059fa70098bb16 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Mon, 16 Dec 2024 15:06:01 -0500 Subject: [PATCH 030/132] docs: update chatbot demo --- examples/chatbot/index.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/examples/chatbot/index.ts b/examples/chatbot/index.ts index 852fa26..5f02b50 100644 --- a/examples/chatbot/index.ts +++ b/examples/chatbot/index.ts @@ -86,7 +86,16 @@ const server = app.listen(PORT, async () => { ) try { - const credentialDefinition = (await apiClient.credentialTypes.import(phoneCredDefData)) + /** + * Note: To test credential revocation locally, use the following configuration: + * const credentialDefinition = (await apiClient.credentialTypes.create({ + * id: randomUUID(), + * name: "phoneNumber", + * version: '1.0', + * attributes: ['phoneNumber'] + * })) + */ + const credentialDefinition = (await apiClient.credentialTypes.import(phoneCredDefData)) phoneNumberCredentialDefinitionId = phoneNumberCredentialType?.id ?? credentialDefinition.id phoneNumberRevocationDefinitionId = From 63be523db3b06c897f5a1cc1b25cc17b06d3aa2f Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Tue, 17 Dec 2024 00:50:31 -0500 Subject: [PATCH 031/132] feat: add revocation entity for save credential --- packages/model/src/types.ts | 2 ++ .../src/connections/connection.module.ts | 1 + packages/nestjs-client/src/index.ts | 1 + .../src/messages/message.service.ts | 32 +++++++++++++++++- packages/nestjs-client/src/models/index.ts | 1 + .../src/models/revocation.entity.ts | 33 +++++++++++++++++++ 6 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 packages/nestjs-client/src/models/index.ts create mode 100644 packages/nestjs-client/src/models/revocation.entity.ts diff --git a/packages/model/src/types.ts b/packages/model/src/types.ts index 36be6d6..9b9ecd9 100644 --- a/packages/model/src/types.ts +++ b/packages/model/src/types.ts @@ -5,6 +5,8 @@ import type { ProofState, } from '@credo-ts/core' +export { CredentialState } from '@credo-ts/core' + import { Claim, ClaimOptions } from './messages/CredentialIssuanceMessage' export interface AgentInfo { diff --git a/packages/nestjs-client/src/connections/connection.module.ts b/packages/nestjs-client/src/connections/connection.module.ts index df4a947..cdb9dd7 100644 --- a/packages/nestjs-client/src/connections/connection.module.ts +++ b/packages/nestjs-client/src/connections/connection.module.ts @@ -22,6 +22,7 @@ export class ConnectionsEventModule { useClass: options.eventHandler, }, ], + exports: [ConnectionsRepository], } } } diff --git a/packages/nestjs-client/src/index.ts b/packages/nestjs-client/src/index.ts index ff54409..b5950bb 100644 --- a/packages/nestjs-client/src/index.ts +++ b/packages/nestjs-client/src/index.ts @@ -2,3 +2,4 @@ export * from './app.module' export * from './connections' export * from './interfaces' export * from './messages' +export * from './models' diff --git a/packages/nestjs-client/src/messages/message.service.ts b/packages/nestjs-client/src/messages/message.service.ts index 9fc6b88..a16db0f 100644 --- a/packages/nestjs-client/src/messages/message.service.ts +++ b/packages/nestjs-client/src/messages/message.service.ts @@ -1,9 +1,18 @@ import { ApiClient, ApiVersion } from '@2060.io/service-agent-client' -import { MessageReceived, ReceiptsMessage } from '@2060.io/service-agent-model' +import { + CredentialReceptionMessage, + CredentialState, + MessageReceived, + ReceiptsMessage, +} from '@2060.io/service-agent-model' import { Inject, Injectable, Logger, Optional } from '@nestjs/common' +import { InjectRepository } from '@nestjs/typeorm' import { MessageState } from 'credo-ts-receipts' +import { Repository } from 'typeorm' +import { ConnectionsRepository } from '../connections' import { EventHandler } from '../interfaces' +import { RevocationEntity } from '../models' import { MESSAGE_EVENT, MESSAGE_MODULE_OPTIONS, MessageModuleOptions } from './message.config' @@ -16,6 +25,9 @@ export class MessageEventService { constructor( @Inject(MESSAGE_MODULE_OPTIONS) private options: MessageModuleOptions, + @InjectRepository(RevocationEntity) + private readonly revocationRepository: Repository, + private readonly connectionRepository: ConnectionsRepository, @Optional() @Inject(MESSAGE_EVENT) private eventHandler?: EventHandler, ) { this.url = options.url @@ -41,6 +53,24 @@ export class MessageEventService { await this.apiClient.messages.send(body) + if (event.message instanceof CredentialReceptionMessage) { + try { + const credential = (await this.apiClient.credentialTypes.getAll())[0] + + const connectionId = await this.connectionRepository.findById(event.message.connectionId) + + if (connectionId && event.message.state === CredentialState.Done) { + const credentialRev = this.revocationRepository.create({ + connection: connectionId, + revocationDefinitionId: credential.revocationId, + }) + await this.revocationRepository.save(credentialRev) + } + } catch (error) { + this.logger.error(`Cannot create the registry: ${error}`) + } + } + if (this.eventHandler) { await this.eventHandler.inputMessage(event.message) } diff --git a/packages/nestjs-client/src/models/index.ts b/packages/nestjs-client/src/models/index.ts new file mode 100644 index 0000000..6cf90b6 --- /dev/null +++ b/packages/nestjs-client/src/models/index.ts @@ -0,0 +1 @@ +export * from '../models/revocation.entity' diff --git a/packages/nestjs-client/src/models/revocation.entity.ts b/packages/nestjs-client/src/models/revocation.entity.ts new file mode 100644 index 0000000..986a67a --- /dev/null +++ b/packages/nestjs-client/src/models/revocation.entity.ts @@ -0,0 +1,33 @@ +import { + Entity, + Column, + PrimaryGeneratedColumn, + CreateDateColumn, + UpdateDateColumn, + JoinColumn, + OneToOne, +} from 'typeorm' + +import { ConnectionEntity } from '../connections' + +@Entity('revocations') +export class RevocationEntity { + @PrimaryGeneratedColumn('increment') + id?: string + + @Column({ type: 'varchar', nullable: false }) + revocationDefinitionId?: string + + @Column({ type: 'integer', generated: 'increment', nullable: false }) + revocationRegistryIndex?: number + + @OneToOne(() => ConnectionEntity, { nullable: false }) + @JoinColumn({ name: 'connection_id', referencedColumnName: 'id' }) + connection?: ConnectionEntity + + @CreateDateColumn() + createdTs?: Date + + @UpdateDateColumn() + updatedTs?: Date +} From 9f342c6ab94b8e6f14cb70ba96db9edadd82bda4 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Tue, 17 Dec 2024 01:21:06 -0500 Subject: [PATCH 032/132] fix: add revocation entity for save credential --- packages/nestjs-client/src/interfaces.ts | 17 +++++++++++ .../src/messages/message.service.ts | 29 ++++++++++--------- .../src/models/revocation.entity.ts | 3 ++ 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/packages/nestjs-client/src/interfaces.ts b/packages/nestjs-client/src/interfaces.ts index 81d1f69..5d810ed 100644 --- a/packages/nestjs-client/src/interfaces.ts +++ b/packages/nestjs-client/src/interfaces.ts @@ -32,4 +32,21 @@ export interface EventHandler { * @param message - An instance of BaseMessage containing the input message details. */ inputMessage(message: BaseMessage): Promise | void + + /** + * Processes the creation of a unique hash for a credential. + * This method should ensure proper handling of credential generation + * by identifying the session associated with the provided connection ID. + * + * The implementation of this method must: + * 1. Identify the session or context using the given connectionId. + * 2. Generate a unique hash string based on the connection session + * and any other required data for the credential. + * + * @param connectionId - The unique identifier of the connection used + * to determine the session context. + * @returns A Promise that resolves to a unique hash Uint8Array representing + * the generated credential. + */ + credentialHash(connectionId: string): Promise } diff --git a/packages/nestjs-client/src/messages/message.service.ts b/packages/nestjs-client/src/messages/message.service.ts index a16db0f..54a6d27 100644 --- a/packages/nestjs-client/src/messages/message.service.ts +++ b/packages/nestjs-client/src/messages/message.service.ts @@ -53,25 +53,26 @@ export class MessageEventService { await this.apiClient.messages.send(body) - if (event.message instanceof CredentialReceptionMessage) { - try { - const credential = (await this.apiClient.credentialTypes.getAll())[0] + if (this.eventHandler) { + if (event.message instanceof CredentialReceptionMessage) { + try { + const credential = (await this.apiClient.credentialTypes.getAll())[0] - const connectionId = await this.connectionRepository.findById(event.message.connectionId) + const connectionId = await this.connectionRepository.findById(event.message.connectionId) - if (connectionId && event.message.state === CredentialState.Done) { - const credentialRev = this.revocationRepository.create({ - connection: connectionId, - revocationDefinitionId: credential.revocationId, - }) - await this.revocationRepository.save(credentialRev) + if (connectionId && event.message.state === CredentialState.Done) { + const credentialRev = this.revocationRepository.create({ + connection: connectionId, + hash: Buffer.from(await this.eventHandler.credentialHash(event.message.connectionId)), + revocationDefinitionId: credential.revocationId, + }) + await this.revocationRepository.save(credentialRev) + } + } catch (error) { + this.logger.error(`Cannot create the registry: ${error}`) } - } catch (error) { - this.logger.error(`Cannot create the registry: ${error}`) } - } - if (this.eventHandler) { await this.eventHandler.inputMessage(event.message) } } diff --git a/packages/nestjs-client/src/models/revocation.entity.ts b/packages/nestjs-client/src/models/revocation.entity.ts index 986a67a..5307ad8 100644 --- a/packages/nestjs-client/src/models/revocation.entity.ts +++ b/packages/nestjs-client/src/models/revocation.entity.ts @@ -25,6 +25,9 @@ export class RevocationEntity { @JoinColumn({ name: 'connection_id', referencedColumnName: 'id' }) connection?: ConnectionEntity + @Column({ type: 'blob', nullable: true }) + hash?: Buffer + @CreateDateColumn() createdTs?: Date From 92e3c3982cb1f8f74b94e9e4ff75ef49c3e61820 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Tue, 17 Dec 2024 09:05:22 -0500 Subject: [PATCH 033/132] feat: update registry --- .../src/messages/message.service.ts | 24 ++++++++++++------- .../src/models/revocation.entity.ts | 4 ++-- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/nestjs-client/src/messages/message.service.ts b/packages/nestjs-client/src/messages/message.service.ts index 54a6d27..32b4d43 100644 --- a/packages/nestjs-client/src/messages/message.service.ts +++ b/packages/nestjs-client/src/messages/message.service.ts @@ -56,17 +56,23 @@ export class MessageEventService { if (this.eventHandler) { if (event.message instanceof CredentialReceptionMessage) { try { - const credential = (await this.apiClient.credentialTypes.getAll())[0] - + const [credential] = await this.apiClient.credentialTypes.getAll() const connectionId = await this.connectionRepository.findById(event.message.connectionId) + const hash = Buffer.from(await this.eventHandler.credentialHash(event.message.connectionId)) + const currentCred = await this.revocationRepository.findOneBy({ hash }) + const isCredentialDone = event.message.state === CredentialState.Done - if (connectionId && event.message.state === CredentialState.Done) { - const credentialRev = this.revocationRepository.create({ - connection: connectionId, - hash: Buffer.from(await this.eventHandler.credentialHash(event.message.connectionId)), - revocationDefinitionId: credential.revocationId, - }) - await this.revocationRepository.save(credentialRev) + if (connectionId && isCredentialDone) { + if (!currentCred) { + const credentialRev = this.revocationRepository.create({ + connectionId, + hash, + revocationDefinitionId: credential.revocationId, + }) + await this.revocationRepository.save(credentialRev) + } else { + this.revocationRepository.update(currentCred.id, { connectionId }) + } } } catch (error) { this.logger.error(`Cannot create the registry: ${error}`) diff --git a/packages/nestjs-client/src/models/revocation.entity.ts b/packages/nestjs-client/src/models/revocation.entity.ts index 5307ad8..2e05da2 100644 --- a/packages/nestjs-client/src/models/revocation.entity.ts +++ b/packages/nestjs-client/src/models/revocation.entity.ts @@ -13,7 +13,7 @@ import { ConnectionEntity } from '../connections' @Entity('revocations') export class RevocationEntity { @PrimaryGeneratedColumn('increment') - id?: string + id!: string @Column({ type: 'varchar', nullable: false }) revocationDefinitionId?: string @@ -23,7 +23,7 @@ export class RevocationEntity { @OneToOne(() => ConnectionEntity, { nullable: false }) @JoinColumn({ name: 'connection_id', referencedColumnName: 'id' }) - connection?: ConnectionEntity + connectionId?: ConnectionEntity @Column({ type: 'blob', nullable: true }) hash?: Buffer From 16c8b78764580b42a52274ce70a884426d8f41b8 Mon Sep 17 00:00:00 2001 From: andresfv95 Date: Tue, 17 Dec 2024 09:12:30 -0500 Subject: [PATCH 034/132] fix: Update doc/service-agent-api.md Co-authored-by: Ariel Gentile --- doc/service-agent-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/service-agent-api.md b/doc/service-agent-api.md index 04052b2..61921fe 100644 --- a/doc/service-agent-api.md +++ b/doc/service-agent-api.md @@ -168,7 +168,7 @@ Parameters: ``` #### Credential Revocation -By sending this message, a Verifiable Credential is effectively revocated and sent to the destination connection. +By sending this message, a Verifiable Credential is effectively revoked and a notification is sent to the DIDComm connection it has been issued to. This message could be sent as a response to a Credential issuance. In such case, `connectionId` and `revocationDefinitionId` is used to identify credential details. From 893671d431f6de7cdeee69a4a595743890bb7076 Mon Sep 17 00:00:00 2001 From: andresfv95 Date: Tue, 17 Dec 2024 09:12:44 -0500 Subject: [PATCH 035/132] fix: Update packages/main/src/controllers/credentials/CredentialTypeController.ts Co-authored-by: Ariel Gentile --- .../src/controllers/credentials/CredentialTypeController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/main/src/controllers/credentials/CredentialTypeController.ts b/packages/main/src/controllers/credentials/CredentialTypeController.ts index e93d7b5..095b62c 100644 --- a/packages/main/src/controllers/credentials/CredentialTypeController.ts +++ b/packages/main/src/controllers/credentials/CredentialTypeController.ts @@ -177,7 +177,7 @@ export class CredentialTypesController { }) this.logger.log(`Credential Definition Id: ${credentialDefinitionId}`) - this.logger.log(`Revocation Definition Id: ${revocationDefinitionId}`) + this.logger.log(`Revocation Registry Definition Id: ${revocationDefinitionId}`) // Apply name and version as tags const credentialDefinitionRepository = agent.dependencyManager.resolve( From 3c975ef60b82e20d8027a8aa4e529556bec63237 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Tue, 17 Dec 2024 16:35:44 -0500 Subject: [PATCH 036/132] fix: add the suggested commits --- README.md | 1 + doc/service-agent-api.md | 7 +- examples/chatbot/data.ts | 5 - examples/chatbot/index.ts | 45 +++++---- .../credentials/CredentialTypeController.ts | 95 ++++++++++--------- .../src/controllers/message/MessageService.ts | 38 +++++--- .../src/messages/CredentialIssuanceMessage.ts | 10 +- .../messages/CredentialRevocationMessage.ts | 12 +-- packages/model/src/types.ts | 2 +- 9 files changed, 112 insertions(+), 103 deletions(-) diff --git a/README.md b/README.md index 940214d..baad90f 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ but likely needed for production and test deployments. | POSTGRES_ADMIN_PASSWORD | PosgreSQL database admin password | None | | REDIS_HOST | Redis database host user. This system will only function if this variable is defined. (Recommended for production mode) | None | | REDIS_PASSWORD | Redis database password | None | +| SUPPORTING_REVOCATION | Enables support for revocation features (true/false) | false | > **Note**: While not mandatory, it is recommended to set an agent public DID matching external hostname (e.g. if your Service Agent instance is accessable in `https://myagent.com:3000` you must set AGENT_PUBLIC_DID to `did:web:myagent.com%3A3000`), which will make possible for the agent to create its own creadential types and therefore issue credentials. Note that you'll need HTTPS in order to fully support did:web specification. diff --git a/doc/service-agent-api.md b/doc/service-agent-api.md index 61921fe..b676bc5 100644 --- a/doc/service-agent-api.md +++ b/doc/service-agent-api.md @@ -154,14 +154,14 @@ Parameters: - (optional) Revocation Index - (optional) Claims -> **Note:** When using revocation parameters (`revocationDefinitionId` and `revocationRegistryIndex`), it is essential to ensure the `id` was preserved from the time it was generated with the credential. The `revocationRegistryIndex` serves as a reference to the specific credential in the revocation registry. +> **Note:** When using revocation parameters (`revocationRegistryDefinitionId` and `revocationRegistryIndex`), it is essential to ensure the `id` was preserved from the time it was generated with the credential. The `revocationRegistryIndex` serves as a reference to the specific credential in the revocation registry. ```json { ... "type": "credential-issuance", "credentialDefinitionId": "id", - "revocationDefinitionId": "id", + "revocationRegistryDefinitionId": "id", "revocationRegistryIndex": 1, "claims": [{ "name": "claim-name", "mimeType": "mime-type", "value": "claim-value" }, ...] } @@ -170,13 +170,12 @@ Parameters: By sending this message, a Verifiable Credential is effectively revoked and a notification is sent to the DIDComm connection it has been issued to. -This message could be sent as a response to a Credential issuance. In such case, `connectionId` and `revocationDefinitionId` is used to identify credential details. +This message could be sent as a credential revocation notification. In such case, `threadId` is used to identify credential details. ```json { ... "type": "credential-revocation", - "revocationDefinitionId": "id", } ``` diff --git a/examples/chatbot/data.ts b/examples/chatbot/data.ts index dd70776..9110fc6 100644 --- a/examples/chatbot/data.ts +++ b/examples/chatbot/data.ts @@ -23,10 +23,6 @@ export const rootContextMenu = { title: 'Issue credential', id: 'issue', }, - { - title: 'Revoke credential', - id: 'revoke', - }, { title: 'Request proof', id: 'proof', @@ -44,7 +40,6 @@ export const rootMenuAsQA = { { id: 'poll', text: '⚽ World Cup poll' }, { id: 'rocky', text: '💪 Rocky quotes' }, { id: 'issue', text: 'Issue credential' }, - { id: 'revoke', text: 'Revoke credential' }, { id: 'proof', text: 'Request proof' }, { id: 'help', text: '🆘 Help' }, ], diff --git a/examples/chatbot/index.ts b/examples/chatbot/index.ts index 5f02b50..2ea8f22 100644 --- a/examples/chatbot/index.ts +++ b/examples/chatbot/index.ts @@ -23,6 +23,7 @@ import { VerifiableCredentialSubmittedProofItem, MediaMessage, MrtdSubmitState, + CredentialReceptionMessage, } from '@2060.io/service-agent-model' import cors from 'cors' import { randomUUID } from 'crypto' @@ -99,9 +100,9 @@ const server = app.listen(PORT, async () => { phoneNumberCredentialDefinitionId = phoneNumberCredentialType?.id ?? credentialDefinition.id phoneNumberRevocationDefinitionId = - phoneNumberCredentialType?.revocationId ?? credentialDefinition.revocationId + phoneNumberCredentialType?.revocationId ? phoneNumberCredentialType?.revocationId?.[0] : credentialDefinition.revocationId?.[0] logger.info(`phoneNumberCredentialDefinitionId: ${phoneNumberCredentialDefinitionId}`) - logger.info(`phoneNumberRevocationDefinitionId: ${phoneNumberRevocationDefinitionId}`) + logger.info(`phoneNumberRevocationDefinitionId: ${credentialDefinition.revocationId}`) } catch (error) { logger.error(`Could not create or retrieve phone number credential type: ${error}`) } @@ -186,29 +187,13 @@ const handleMenuSelection = async (options: { connectionId: string; item: string value: '+5712345678', }, ], - revocationDefinitionId: phoneNumberRevocationDefinitionId, + revocationRegistryDefinitionId: phoneNumberRevocationDefinitionId, revocationRegistryIndex: phoneNumberRevocationCount += 1, }) await apiClient.messages.send(body) } } - if (selectedItem === 'revoke' || selectedItem === 'Revoke credential') { - if (!phoneNumberCredentialDefinitionId || phoneNumberCredentialDefinitionId === '' || - !phoneNumberRevocationDefinitionId || phoneNumberRevocationDefinitionId === '') { - await sendTextMessage({ - connectionId, - content: 'Service not available', - }) - } else { - const body = new CredentialRevocationMessage({ - connectionId, - revocationDefinitionId: phoneNumberRevocationDefinitionId, - }) - await apiClient.messages.send(body) - } - } - // Proof if (selectedItem === 'proof' || selectedItem === 'Request proof') { if (!phoneNumberCredentialDefinitionId || phoneNumberCredentialDefinitionId === '') { @@ -437,6 +422,22 @@ expressHandler.messageReceived(async (req, res) => { connectionId, }) await apiClient.messages.send(body) + } else if (content.startsWith('/revoke')) { + const parsedContents = content.split(' ') + let threadId = parsedContents[1] + if (!phoneNumberCredentialDefinitionId || phoneNumberCredentialDefinitionId === '' || + !phoneNumberRevocationDefinitionId || phoneNumberRevocationDefinitionId === '') { + await sendTextMessage({ + connectionId, + content: 'Service not available', + }) + } else { + const body = new CredentialRevocationMessage({ + connectionId, + threadId, + }) + await apiClient.messages.send(body) + } } else if (content.startsWith('/proof')) { const body = new IdentityProofRequestMessage({ connectionId, @@ -520,6 +521,12 @@ expressHandler.messageReceived(async (req, res) => { content: `Problem: emrtd ${obj.state}`, }) } + } else if (obj.type === CredentialReceptionMessage.type) { + await submitMessageReceipt(obj, 'viewed') + obj.state === 'done' && await sendTextMessage({ + connectionId: obj.connectionId, + content: `For revocation, please provide the thread ID: ${obj.threadId}`, + }) } }) diff --git a/packages/main/src/controllers/credentials/CredentialTypeController.ts b/packages/main/src/controllers/credentials/CredentialTypeController.ts index 095b62c..fb41e98 100644 --- a/packages/main/src/controllers/credentials/CredentialTypeController.ts +++ b/packages/main/src/controllers/credentials/CredentialTypeController.ts @@ -38,8 +38,11 @@ import { CreateCredentialTypeDto } from './CredentialTypeDto' }) export class CredentialTypesController { private readonly logger = new Logger(CredentialTypesController.name) + private readonly supportRevocation: boolean - constructor(private readonly agentService: AgentService) {} + constructor(private readonly agentService: AgentService) { + this.supportRevocation = process.env.SUPPORTING_REVOCATION === 'true' + } /** * Get all created credential types @@ -57,13 +60,14 @@ export class CredentialTypesController { const schemaResult = await agent.modules.anoncreds.getSchema(record.credentialDefinition.schemaId) const schema = schemaResult.schema + const revocationRegistryIds = record.getTag('revocationRegistryDefinitionId') as string return { id: record.credentialDefinitionId, name: (record.getTag('name') as string) ?? schema?.name, version: (record.getTag('version') as string) ?? schema?.version, attributes: schema?.attrNames || [], - revocationId: record.getTag('revocationDefinitionId') as string, + revocationId: revocationRegistryIds ? revocationRegistryIds.split('::') : undefined, } }), ) @@ -133,7 +137,7 @@ export class CredentialTypesController { const registrationResult = await agent.modules.anoncreds.registerCredentialDefinition({ credentialDefinition: { issuerId, schemaId, tag: `${options.name}.${options.version}` }, - options: { supportRevocation: true }, + options: { supportRevocation: this.supportRevocation }, }) const credentialDefinitionId = registrationResult.credentialDefinitionState.credentialDefinitionId @@ -147,37 +151,53 @@ export class CredentialTypesController { ) } - const revocationResult = await agent.modules.anoncreds.registerRevocationRegistryDefinition({ - revocationRegistryDefinition: { - credentialDefinitionId, - tag: 'default', - maximumCredentialNumber: 1000, - issuerId, - }, - options: {}, - }) - const revocationDefinitionId = - revocationResult.revocationRegistryDefinitionState.revocationRegistryDefinitionId - this.logger.debug!( - `revocationRegistryDefinitionState: ${JSON.stringify(revocationResult.revocationRegistryDefinitionState)}`, - ) + let revocationRegistryDefinitionId + if (this.supportRevocation) { + const revocationResult = await agent.modules.anoncreds.registerRevocationRegistryDefinition({ + revocationRegistryDefinition: { + credentialDefinitionId, + tag: 'default', + maximumCredentialNumber: 1000, + issuerId, + }, + options: {}, + }) + revocationRegistryDefinitionId = + revocationResult.revocationRegistryDefinitionState.revocationRegistryDefinitionId + this.logger.debug!( + `revocationRegistryDefinitionState: ${JSON.stringify(revocationResult.revocationRegistryDefinitionState)}`, + ) - if (!revocationDefinitionId) { - throw new Error( - `Cannot create credential revocations: ${JSON.stringify(registrationResult.registrationMetadata)}`, + if (!revocationRegistryDefinitionId) { + throw new Error( + `Cannot create credential revocations: ${JSON.stringify(registrationResult.registrationMetadata)}`, + ) + } + + const revStatusListResult = await agent.modules.anoncreds.registerRevocationStatusList({ + revocationStatusList: { + issuerId, + revocationRegistryDefinitionId: revocationRegistryDefinitionId, + }, + options: {}, + }) + const revocationDefinitionRepository = agent.dependencyManager.resolve( + AnonCredsRevocationRegistryDefinitionRepository, + ) + const revocationDefinitionRecord = + await revocationDefinitionRepository.getByRevocationRegistryDefinitionId( + agent.context, + revocationRegistryDefinitionId, + ) + revocationDefinitionRecord.metadata.set( + 'revStatusList', + revStatusListResult.revocationStatusListState.revocationStatusList!, ) + await revocationDefinitionRepository.update(agent.context, revocationDefinitionRecord) } - const revStatusListResult = await agent.modules.anoncreds.registerRevocationStatusList({ - revocationStatusList: { - issuerId, - revocationRegistryDefinitionId: revocationDefinitionId, - }, - options: {}, - }) - this.logger.log(`Credential Definition Id: ${credentialDefinitionId}`) - this.logger.log(`Revocation Registry Definition Id: ${revocationDefinitionId}`) + this.logger.log(`Revocation Registry Definition Id: ${revocationRegistryDefinitionId}`) // Apply name and version as tags const credentialDefinitionRepository = agent.dependencyManager.resolve( @@ -187,24 +207,11 @@ export class CredentialTypesController { agent.context, credentialDefinitionId, ) - const revocationDefinitionRepository = agent.dependencyManager.resolve( - AnonCredsRevocationRegistryDefinitionRepository, - ) - const revocationDefinitionRecord = - await revocationDefinitionRepository.getByRevocationRegistryDefinitionId( - agent.context, - revocationDefinitionId, - ) credentialDefinitionRecord.setTag('name', options.name) credentialDefinitionRecord.setTag('version', options.version) - credentialDefinitionRecord.setTag('revocationDefinitionId', revocationDefinitionId) + credentialDefinitionRecord.setTag('revocationRegistryDefinitionId', revocationRegistryDefinitionId) await credentialDefinitionRepository.update(agent.context, credentialDefinitionRecord) - revocationDefinitionRecord.metadata.set( - 'revStatusList', - revStatusListResult.revocationStatusListState.revocationStatusList!, - ) - await revocationDefinitionRepository.update(agent.context, revocationDefinitionRecord) return { id: credentialDefinitionId, @@ -212,7 +219,7 @@ export class CredentialTypesController { name: options.name, version: options.version, schemaId, - revocationId: revocationDefinitionId, + revocationId: revocationRegistryDefinitionId?.split('::'), } } catch (error) { throw new HttpException( diff --git a/packages/main/src/controllers/message/MessageService.ts b/packages/main/src/controllers/message/MessageService.ts index 21817b4..56a5c37 100644 --- a/packages/main/src/controllers/message/MessageService.ts +++ b/packages/main/src/controllers/message/MessageService.ts @@ -224,7 +224,7 @@ export class MessageService { return { name: item.name, mimeType: item.mimeType, value: item.value } }), credentialDefinitionId: msg.credentialDefinitionId, - revocationRegistryDefinitionId: msg.revocationDefinitionId, + revocationRegistryDefinitionId: msg.revocationRegistryDefinitionId, revocationRegistryIndex: msg.revocationRegistryIndex, }, }, @@ -241,17 +241,31 @@ export class MessageService { } else if (messageType === CredentialRevocationMessage.type) { const msg = JsonTransformer.fromJSON(message, CredentialRevocationMessage) - const credential = (await agent.credentials.getAll()).find( - item => - item.getTag('anonCredsRevocationRegistryId') === msg.revocationDefinitionId && - item.connectionId === msg.connectionId, - ) - if (credential) { - await agent.credentials.sendRevocationNotification({ - credentialRecordId: credential.id, - revocationFormat: 'anoncreds', - revocationId: `${credential.getTag('anonCredsRevocationRegistryId')}::${credential.getTag('anonCredsCredentialRevocationId')}`, - }) + const credentials = await agent.credentials.findAllByQuery({ threadId: msg.threadId }) + if (credentials && credentials.length > 0) { + for (const credential of credentials) { + const isRevoke = Boolean( + credential.getTag('anonCredsRevocationRegistryId') && + credential.getTag('anonCredsCredentialRevocationId'), + ) + isRevoke && + (await agent.modules.anoncreds.updateRevocationStatusList({ + revocationStatusList: { + revocationRegistryDefinitionId: credential.getTag( + 'anonCredsRevocationRegistryId', + ) as string, + revokedCredentialIndexes: [Number(credential.getTag('anonCredsCredentialRevocationId'))], + }, + options: {}, + })) + + isRevoke && + (await agent.credentials.sendRevocationNotification({ + credentialRecordId: credential.id, + revocationFormat: 'anoncreds', + revocationId: `${credential.getTag('anonCredsRevocationRegistryId')}::${credential.getTag('anonCredsCredentialRevocationId')}`, + })) + } } else { throw new Error(`No credentials were found for connection: ${msg.connectionId}.`) } diff --git a/packages/model/src/messages/CredentialIssuanceMessage.ts b/packages/model/src/messages/CredentialIssuanceMessage.ts index af9a209..3e467da 100644 --- a/packages/model/src/messages/CredentialIssuanceMessage.ts +++ b/packages/model/src/messages/CredentialIssuanceMessage.ts @@ -37,7 +37,7 @@ export class Claim { export interface CredentialIssuanceMessageOptions extends BaseMessageOptions { credentialDefinitionId: string - revocationDefinitionId?: string + revocationRegistryDefinitionId?: string revocationRegistryIndex?: number claims?: Claim[] } @@ -52,7 +52,7 @@ export class CredentialIssuanceMessage extends BaseMessage { this.timestamp = options.timestamp ?? new Date() this.connectionId = options.connectionId this.credentialDefinitionId = options.credentialDefinitionId - this.revocationDefinitionId = options.revocationDefinitionId + this.revocationRegistryDefinitionId = options.revocationRegistryDefinitionId this.revocationRegistryIndex = options.revocationRegistryIndex this.claims = options.claims?.map(item => new Claim(item)) } @@ -61,21 +61,17 @@ export class CredentialIssuanceMessage extends BaseMessage { public readonly type = CredentialIssuanceMessage.type public static readonly type = MessageType.CredentialIssuanceMessage - @Expose() @IsString() public credentialDefinitionId?: string - @Expose() @IsString() @IsOptional() - public revocationDefinitionId?: string + public revocationRegistryDefinitionId?: string - @Expose() @IsNumber() @IsOptional() public revocationRegistryIndex?: number - @Expose() @Type(() => Claim) @IsArray() @ValidateNested() diff --git a/packages/model/src/messages/CredentialRevocationMessage.ts b/packages/model/src/messages/CredentialRevocationMessage.ts index b4d6399..a0b15a7 100644 --- a/packages/model/src/messages/CredentialRevocationMessage.ts +++ b/packages/model/src/messages/CredentialRevocationMessage.ts @@ -1,12 +1,7 @@ -import { Expose } from 'class-transformer' -import { IsString } from 'class-validator' - import { BaseMessage, BaseMessageOptions } from './BaseMessage' import { MessageType } from './MessageType' -export interface CredentialRevocationMessageOptions extends BaseMessageOptions { - revocationDefinitionId: string -} +export interface CredentialRevocationMessageOptions extends BaseMessageOptions {} export class CredentialRevocationMessage extends BaseMessage { public constructor(options: CredentialRevocationMessageOptions) { @@ -17,14 +12,9 @@ export class CredentialRevocationMessage extends BaseMessage { this.threadId = options.threadId this.timestamp = options.timestamp ?? new Date() this.connectionId = options.connectionId - this.revocationDefinitionId = options.revocationDefinitionId } } public readonly type = CredentialRevocationMessage.type public static readonly type = MessageType.CredentialRevocationMessage - - @Expose() - @IsString() - public revocationDefinitionId!: string } diff --git a/packages/model/src/types.ts b/packages/model/src/types.ts index 36be6d6..9e6b8b4 100644 --- a/packages/model/src/types.ts +++ b/packages/model/src/types.ts @@ -25,7 +25,7 @@ export interface CreateCredentialTypeOptions { version: string attributes: string[] schemaId?: string - revocationId?: string + revocationId?: string[] } type JsonObject = { From 81c95cec7dcbe96c3d9126ea06abeed535d8a8da Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Tue, 17 Dec 2024 18:27:43 -0500 Subject: [PATCH 037/132] docs: update --- packages/nestjs-client/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/nestjs-client/README.md b/packages/nestjs-client/README.md index 4c5acc5..25306c8 100644 --- a/packages/nestjs-client/README.md +++ b/packages/nestjs-client/README.md @@ -4,9 +4,9 @@ ```node @Module({ imports: [ - ConnectionsEventModule.register({ + ConnectionsEventModule.forRoot({ messageHandler: CustomMessageHandler, // Class with input method - imports: [TypeOrmConfig] + imports: [] }) ] }) @@ -18,9 +18,9 @@ export class AppModule {} ```node @Module({ imports: [ - MessageEventModule.register({ + MessageEventModule.forRoot({ messageHandler: CustomMessageHandler, // Class with input method - imports: [TypeOrmConfig] + imports: [] url: 'http://sa-url.com', version: ApiVersion.V1, }), From 84aa616d118b40fc8ae156db00c51d55e55a0592 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Tue, 17 Dec 2024 18:28:21 -0500 Subject: [PATCH 038/132] feat: create credential entity with revocations --- .../credential.entity.ts} | 13 ++++--- .../src/credentials/credential.service.ts | 38 +++++++++++++++++++ .../nestjs-client/src/credentials/index.ts | 1 + packages/nestjs-client/src/index.ts | 2 +- packages/nestjs-client/src/models/index.ts | 1 - 5 files changed, 48 insertions(+), 7 deletions(-) rename packages/nestjs-client/src/{models/revocation.entity.ts => credentials/credential.entity.ts} (72%) create mode 100644 packages/nestjs-client/src/credentials/credential.service.ts create mode 100644 packages/nestjs-client/src/credentials/index.ts delete mode 100644 packages/nestjs-client/src/models/index.ts diff --git a/packages/nestjs-client/src/models/revocation.entity.ts b/packages/nestjs-client/src/credentials/credential.entity.ts similarity index 72% rename from packages/nestjs-client/src/models/revocation.entity.ts rename to packages/nestjs-client/src/credentials/credential.entity.ts index 2e05da2..81faea1 100644 --- a/packages/nestjs-client/src/models/revocation.entity.ts +++ b/packages/nestjs-client/src/credentials/credential.entity.ts @@ -10,15 +10,18 @@ import { import { ConnectionEntity } from '../connections' -@Entity('revocations') -export class RevocationEntity { - @PrimaryGeneratedColumn('increment') +@Entity('credentials') +export class CredentialEntity { + @PrimaryGeneratedColumn('uuid') id!: string - + @Column({ type: 'varchar', nullable: false }) + credentialDefinitionId?: string + + @Column({ type: 'varchar', nullable: true }) revocationDefinitionId?: string - @Column({ type: 'integer', generated: 'increment', nullable: false }) + @Column({ type: 'integer', generated: 'increment', nullable: true }) revocationRegistryIndex?: number @OneToOne(() => ConnectionEntity, { nullable: false }) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts new file mode 100644 index 0000000..80e284f --- /dev/null +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -0,0 +1,38 @@ +import { + CredentialTypeInfo, +} from '@2060.io/service-agent-model' +import { Injectable, Logger } from "@nestjs/common" +import { InjectRepository } from '@nestjs/typeorm' +import { CredentialEntity } from "./credential.entity" +import { Repository } from "typeorm" + +@Injectable() +export class CredentialEventService { + private readonly logger = new Logger(CredentialEventService.name) + + constructor( + @InjectRepository(CredentialEntity) + private readonly credentialRepository: Repository, + ) {} + + /** + * Creates a credential using the provided records. + * This method requires a `CredentialTypeInfo` object with necessary parameters + * such as the credential's name, version, and attributes. + * + * @param {CredentialTypeInfo} records - An object containing the attributes + * of the credential to be created. + * + * Example of constructing the `records` object: + * const records = { + * name: "Chabot", + * version: "1.0", + * attributes: ["email"] + * }; + * + * @returns {Promise} A promise that resolves when the credential is created successfully. + */ + async CreateCredential(records: CredentialTypeInfo): Promise { + } +} + diff --git a/packages/nestjs-client/src/credentials/index.ts b/packages/nestjs-client/src/credentials/index.ts new file mode 100644 index 0000000..67c94f4 --- /dev/null +++ b/packages/nestjs-client/src/credentials/index.ts @@ -0,0 +1 @@ +export * from './credential.entity' diff --git a/packages/nestjs-client/src/index.ts b/packages/nestjs-client/src/index.ts index b5950bb..c03be6b 100644 --- a/packages/nestjs-client/src/index.ts +++ b/packages/nestjs-client/src/index.ts @@ -1,5 +1,5 @@ export * from './app.module' export * from './connections' +export * from './credentials' export * from './interfaces' export * from './messages' -export * from './models' diff --git a/packages/nestjs-client/src/models/index.ts b/packages/nestjs-client/src/models/index.ts deleted file mode 100644 index 6cf90b6..0000000 --- a/packages/nestjs-client/src/models/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from '../models/revocation.entity' From 25879ddea6d1385bfc8ed4242c5242e08e956b76 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Wed, 18 Dec 2024 08:37:56 -0500 Subject: [PATCH 039/132] fix add temporal --- packages/nestjs-client/src/app.module.ts | 45 ++++++++++--- .../src/credentials/credential.service.ts | 63 ++++++++++++++++++- .../src/messages/message.service.ts | 14 ++--- 3 files changed, 107 insertions(+), 15 deletions(-) diff --git a/packages/nestjs-client/src/app.module.ts b/packages/nestjs-client/src/app.module.ts index 22db711..014e4d2 100644 --- a/packages/nestjs-client/src/app.module.ts +++ b/packages/nestjs-client/src/app.module.ts @@ -1,31 +1,62 @@ -import { Module, DynamicModule } from '@nestjs/common' +import { Module, DynamicModule, Provider, Type } from '@nestjs/common' import { ConnectionsEventModule } from './connections' import { MessageEventModule } from './messages' +import { ApiVersion } from 'packages/client/build'; +import { EventHandler } from './interfaces'; export interface EventsModuleOptions { - prefix?: string - enableMessages?: boolean - enableConnections?: boolean + enableMessages?: boolean; + enableConnections?: boolean; + eventHandler?: Type; + url?: string; + version?: string; } +export const EVENTS_MODULE_OPTIONS = 'EVENTS_MODULE_OPTIONS' + @Module({}) export class EventsModule { static register(options: EventsModuleOptions = {}): DynamicModule { const imports = [] + const providers: Provider[] = [ + { + provide: EVENTS_MODULE_OPTIONS, + useValue: options, + }, + ]; + + if (options.enableMessages !== false) { - imports.push(MessageEventModule) + imports.push( + MessageEventModule.forRoot({ + eventHandler: options.eventHandler, + url: options.url, + version: options.version as ApiVersion, + }) + ) } if (options.enableConnections !== false) { - imports.push(ConnectionsEventModule) + imports.push( + ConnectionsEventModule.forRoot({ + eventHandler: options.eventHandler, + }) + ); } return { module: EventsModule, imports, - exports: imports, + providers, + exports: [ + ...imports, + { + provide: EVENTS_MODULE_OPTIONS, + useValue: options, + }, + ], } } } diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 80e284f..13d96f7 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -1,4 +1,6 @@ import { + Claim, + CredentialIssuanceMessage, CredentialTypeInfo, } from '@2060.io/service-agent-model' import { Injectable, Logger } from "@nestjs/common" @@ -32,7 +34,66 @@ export class CredentialEventService { * * @returns {Promise} A promise that resolves when the credential is created successfully. */ - async CreateCredential(records: CredentialTypeInfo): Promise { + async createCredential(records: CredentialTypeInfo): Promise { + const [credential] = await this.apiClient.credentialTypes.getAll() + + if (!credential || credential.length === 0) { + const newCredential = await this.apiClient.credentialTypes.create({ + id: records.id, + name: records.name, + version: records.version, + attributes: records.attributes, + }) + credential.push(newCredential) + } + return credential + } + + /** + * Sends a credential issuance to the specified connection using the provided claims. + * This method initiates the issuance process by sending claims as part of a credential to + * the recipient identified by the connection ID. + * + * @param {string} connectionId - The unique identifier of the connection to which the credential + * will be issued. This represents the recipient of the credential. + * + * @param {Record} records - A key value objects, where each key represents an attribute + * of the credential. + * + * Example of constructing the `records` array: + * const records = { + * { name: "email", value: "john.doe@example.com" }, + * { name: "name", value: "John Doe" }, + * } + * + * @returns {Promise} A promise that resolves when the credential issuance is successfully + * sent. If an error occurs during the process, the promise will be rejected. + */ + async sendCredentialIssuance(connectionId: string, records: Record): Promise { + const claims: Claim[] = [] + if (records) { + Object.entries(records).forEach(([key, value]) => { + claims.push( + new Claim({ + name: key, + value: value ?? null, + }), + ) + }) + } + + let credentialId + let credential = (await this.apiClient.credentialTypes.getAll())[0] + if (!credential) credentialId = (await this.sendCredentialType())[0]?.id + await this.apiClient.messages.send( + new CredentialIssuanceMessage({ + connectionId: connectionId, + credentialDefinitionId: credentialId, + claims: claims, + }), + ) + + this.logger.debug('sendCredential with claims: ' + JSON.stringify(claims)) } } diff --git a/packages/nestjs-client/src/messages/message.service.ts b/packages/nestjs-client/src/messages/message.service.ts index 32b4d43..57342b1 100644 --- a/packages/nestjs-client/src/messages/message.service.ts +++ b/packages/nestjs-client/src/messages/message.service.ts @@ -12,9 +12,9 @@ import { Repository } from 'typeorm' import { ConnectionsRepository } from '../connections' import { EventHandler } from '../interfaces' -import { RevocationEntity } from '../models' import { MESSAGE_EVENT, MESSAGE_MODULE_OPTIONS, MessageModuleOptions } from './message.config' +import { CredentialEntity } from '../credentials' @Injectable() export class MessageEventService { @@ -25,8 +25,8 @@ export class MessageEventService { constructor( @Inject(MESSAGE_MODULE_OPTIONS) private options: MessageModuleOptions, - @InjectRepository(RevocationEntity) - private readonly revocationRepository: Repository, + @InjectRepository(CredentialEntity) + private readonly credentialRepository: Repository, private readonly connectionRepository: ConnectionsRepository, @Optional() @Inject(MESSAGE_EVENT) private eventHandler?: EventHandler, ) { @@ -59,19 +59,19 @@ export class MessageEventService { const [credential] = await this.apiClient.credentialTypes.getAll() const connectionId = await this.connectionRepository.findById(event.message.connectionId) const hash = Buffer.from(await this.eventHandler.credentialHash(event.message.connectionId)) - const currentCred = await this.revocationRepository.findOneBy({ hash }) + const currentCred = await this.credentialRepository.findOneBy({ hash }) const isCredentialDone = event.message.state === CredentialState.Done if (connectionId && isCredentialDone) { if (!currentCred) { - const credentialRev = this.revocationRepository.create({ + const credentialRev = this.credentialRepository.create({ connectionId, hash, revocationDefinitionId: credential.revocationId, }) - await this.revocationRepository.save(credentialRev) + await this.credentialRepository.save(credentialRev) } else { - this.revocationRepository.update(currentCred.id, { connectionId }) + this.credentialRepository.update(currentCred.id, { connectionId }) } } } catch (error) { From 1f67b825f2b1e78892a3fb2b7008d887e815dc07 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Wed, 18 Dec 2024 08:50:50 -0500 Subject: [PATCH 040/132] docs: update --- README.md | 1 - doc/service-agent-api.md | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index baad90f..940214d 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,6 @@ but likely needed for production and test deployments. | POSTGRES_ADMIN_PASSWORD | PosgreSQL database admin password | None | | REDIS_HOST | Redis database host user. This system will only function if this variable is defined. (Recommended for production mode) | None | | REDIS_PASSWORD | Redis database password | None | -| SUPPORTING_REVOCATION | Enables support for revocation features (true/false) | false | > **Note**: While not mandatory, it is recommended to set an agent public DID matching external hostname (e.g. if your Service Agent instance is accessable in `https://myagent.com:3000` you must set AGENT_PUBLIC_DID to `did:web:myagent.com%3A3000`), which will make possible for the agent to create its own creadential types and therefore issue credentials. Note that you'll need HTTPS in order to fully support did:web specification. diff --git a/doc/service-agent-api.md b/doc/service-agent-api.md index b676bc5..85aafa3 100644 --- a/doc/service-agent-api.md +++ b/doc/service-agent-api.md @@ -154,7 +154,7 @@ Parameters: - (optional) Revocation Index - (optional) Claims -> **Note:** When using revocation parameters (`revocationRegistryDefinitionId` and `revocationRegistryIndex`), it is essential to ensure the `id` was preserved from the time it was generated with the credential. The `revocationRegistryIndex` serves as a reference to the specific credential in the revocation registry. +**Note:** When using revocation parameters (`revocationRegistryDefinitionId` and `revocationRegistryIndex`), it is crucial to preserve both values as they were originally generated with the credential. Each revocation registry has a finite capacity for credentials (default is 1000), and the `revocationRegistryIndex` uniquely identifies the specific credential within the registry. Failing to maintain these parameters correctly may lead to issues during the credential revocation process. ```json { @@ -170,7 +170,7 @@ Parameters: By sending this message, a Verifiable Credential is effectively revoked and a notification is sent to the DIDComm connection it has been issued to. -This message could be sent as a credential revocation notification. In such case, `threadId` is used to identify credential details. +In this context, `threadId` is used to identify the details of the credential ```json { From 77ce73c997f44f53dd2f642c8696fb4cc9a352aa Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Wed, 18 Dec 2024 08:55:09 -0500 Subject: [PATCH 041/132] fix: add supportRevocation (false by default) --- examples/chatbot/index.ts | 3 ++- .../credentials/CredentialTypeController.ts | 9 +++------ .../src/controllers/credentials/CredentialTypeDto.ts | 12 +++++++++++- packages/model/src/types.ts | 1 + 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/examples/chatbot/index.ts b/examples/chatbot/index.ts index 2ea8f22..b8e9040 100644 --- a/examples/chatbot/index.ts +++ b/examples/chatbot/index.ts @@ -93,7 +93,8 @@ const server = app.listen(PORT, async () => { * id: randomUUID(), * name: "phoneNumber", * version: '1.0', - * attributes: ['phoneNumber'] + * attributes: ['phoneNumber'], + * supportRevocation: true * })) */ const credentialDefinition = (await apiClient.credentialTypes.import(phoneCredDefData)) diff --git a/packages/main/src/controllers/credentials/CredentialTypeController.ts b/packages/main/src/controllers/credentials/CredentialTypeController.ts index fb41e98..8fd0c81 100644 --- a/packages/main/src/controllers/credentials/CredentialTypeController.ts +++ b/packages/main/src/controllers/credentials/CredentialTypeController.ts @@ -38,11 +38,8 @@ import { CreateCredentialTypeDto } from './CredentialTypeDto' }) export class CredentialTypesController { private readonly logger = new Logger(CredentialTypesController.name) - private readonly supportRevocation: boolean - constructor(private readonly agentService: AgentService) { - this.supportRevocation = process.env.SUPPORTING_REVOCATION === 'true' - } + constructor(private readonly agentService: AgentService) {} /** * Get all created credential types @@ -137,7 +134,7 @@ export class CredentialTypesController { const registrationResult = await agent.modules.anoncreds.registerCredentialDefinition({ credentialDefinition: { issuerId, schemaId, tag: `${options.name}.${options.version}` }, - options: { supportRevocation: this.supportRevocation }, + options: { supportRevocation: options.supportRevocation }, }) const credentialDefinitionId = registrationResult.credentialDefinitionState.credentialDefinitionId @@ -152,7 +149,7 @@ export class CredentialTypesController { } let revocationRegistryDefinitionId - if (this.supportRevocation) { + if (options.supportRevocation) { const revocationResult = await agent.modules.anoncreds.registerRevocationRegistryDefinition({ revocationRegistryDefinition: { credentialDefinitionId, diff --git a/packages/main/src/controllers/credentials/CredentialTypeDto.ts b/packages/main/src/controllers/credentials/CredentialTypeDto.ts index cab5b3f..9800db3 100644 --- a/packages/main/src/controllers/credentials/CredentialTypeDto.ts +++ b/packages/main/src/controllers/credentials/CredentialTypeDto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger' -import { IsString, IsNotEmpty, IsOptional } from 'class-validator' +import { IsString, IsNotEmpty, IsOptional, IsBoolean } from 'class-validator' export class CreateCredentialTypeDto { @ApiProperty({ @@ -32,4 +32,14 @@ export class CreateCredentialTypeDto { @IsOptional() @IsNotEmpty() schemaId?: string + + @ApiProperty({ + description: 'Indicates whether to enable credential revocation support. If enabled, it allows revocation of issued credentials.', + example: true, + default: false, + }) + @IsBoolean() + @IsOptional() + @IsNotEmpty() + supportRevocation: boolean = false } diff --git a/packages/model/src/types.ts b/packages/model/src/types.ts index 9e6b8b4..e99f22e 100644 --- a/packages/model/src/types.ts +++ b/packages/model/src/types.ts @@ -26,6 +26,7 @@ export interface CreateCredentialTypeOptions { attributes: string[] schemaId?: string revocationId?: string[] + supportRevocation?: boolean } type JsonObject = { From ef96911c9b1ffab7859ca922c6e8d2576433597f Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Wed, 18 Dec 2024 11:55:34 -0500 Subject: [PATCH 042/132] feat: add createRevocationRegistry --- .../src/services/CredentialTypeService.ts | 12 +- .../credentials/CredentialTypeController.ts | 141 +++++++++++------- .../credentials/CredentialTypeDto.ts | 3 +- packages/model/src/types.ts | 2 +- 4 files changed, 104 insertions(+), 54 deletions(-) diff --git a/packages/client/src/services/CredentialTypeService.ts b/packages/client/src/services/CredentialTypeService.ts index 4d8266f..0b59764 100644 --- a/packages/client/src/services/CredentialTypeService.ts +++ b/packages/client/src/services/CredentialTypeService.ts @@ -40,13 +40,13 @@ export class CredentialTypeService { return (await response.json()) as CredentialTypeInfo } - public async create(credentialType: CredentialTypeInfo): Promise { + public async create(credentialType: CredentialTypeInfo): Promise { const response = await fetch(`${this.url}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(credentialType), }) - return response.json() + return ( await response.json()) as CredentialTypeInfo } public async getAll(): Promise { @@ -63,4 +63,12 @@ export class CredentialTypeService { return types.map(value => value as CredentialTypeInfo) } + + public async createRevocation(credentialDefinitionId: string): Promise { + const response = await fetch(`${this.url}/revoke/${credentialDefinitionId}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }) + return response.text() + } } diff --git a/packages/main/src/controllers/credentials/CredentialTypeController.ts b/packages/main/src/controllers/credentials/CredentialTypeController.ts index 8fd0c81..6c613af 100644 --- a/packages/main/src/controllers/credentials/CredentialTypeController.ts +++ b/packages/main/src/controllers/credentials/CredentialTypeController.ts @@ -57,14 +57,24 @@ export class CredentialTypesController { const schemaResult = await agent.modules.anoncreds.getSchema(record.credentialDefinition.schemaId) const schema = schemaResult.schema - const revocationRegistryIds = record.getTag('revocationRegistryDefinitionId') as string + + const revocationDefinitionRepository = agent.dependencyManager.resolve( + AnonCredsRevocationRegistryDefinitionRepository, + ) + const revocationRegistries = await revocationDefinitionRepository.findAllByCredentialDefinitionId( + agent.context, + record.credentialDefinitionId, + ) + const revocationRegistryDefinitionIds = revocationRegistries.map( + item => item.revocationRegistryDefinitionId, + ) return { id: record.credentialDefinitionId, name: (record.getTag('name') as string) ?? schema?.name, version: (record.getTag('version') as string) ?? schema?.version, attributes: schema?.attrNames || [], - revocationId: revocationRegistryIds ? revocationRegistryIds.split('::') : undefined, + revocationIds: revocationRegistryDefinitionIds, } }), ) @@ -148,53 +158,7 @@ export class CredentialTypesController { ) } - let revocationRegistryDefinitionId - if (options.supportRevocation) { - const revocationResult = await agent.modules.anoncreds.registerRevocationRegistryDefinition({ - revocationRegistryDefinition: { - credentialDefinitionId, - tag: 'default', - maximumCredentialNumber: 1000, - issuerId, - }, - options: {}, - }) - revocationRegistryDefinitionId = - revocationResult.revocationRegistryDefinitionState.revocationRegistryDefinitionId - this.logger.debug!( - `revocationRegistryDefinitionState: ${JSON.stringify(revocationResult.revocationRegistryDefinitionState)}`, - ) - - if (!revocationRegistryDefinitionId) { - throw new Error( - `Cannot create credential revocations: ${JSON.stringify(registrationResult.registrationMetadata)}`, - ) - } - - const revStatusListResult = await agent.modules.anoncreds.registerRevocationStatusList({ - revocationStatusList: { - issuerId, - revocationRegistryDefinitionId: revocationRegistryDefinitionId, - }, - options: {}, - }) - const revocationDefinitionRepository = agent.dependencyManager.resolve( - AnonCredsRevocationRegistryDefinitionRepository, - ) - const revocationDefinitionRecord = - await revocationDefinitionRepository.getByRevocationRegistryDefinitionId( - agent.context, - revocationRegistryDefinitionId, - ) - revocationDefinitionRecord.metadata.set( - 'revStatusList', - revStatusListResult.revocationStatusListState.revocationStatusList!, - ) - await revocationDefinitionRepository.update(agent.context, revocationDefinitionRecord) - } - this.logger.log(`Credential Definition Id: ${credentialDefinitionId}`) - this.logger.log(`Revocation Registry Definition Id: ${revocationRegistryDefinitionId}`) // Apply name and version as tags const credentialDefinitionRepository = agent.dependencyManager.resolve( @@ -206,7 +170,6 @@ export class CredentialTypesController { ) credentialDefinitionRecord.setTag('name', options.name) credentialDefinitionRecord.setTag('version', options.version) - credentialDefinitionRecord.setTag('revocationRegistryDefinitionId', revocationRegistryDefinitionId) await credentialDefinitionRepository.update(agent.context, credentialDefinitionRecord) @@ -216,7 +179,6 @@ export class CredentialTypesController { name: options.name, version: options.version, schemaId, - revocationId: revocationRegistryDefinitionId?.split('::'), } } catch (error) { throw new HttpException( @@ -461,4 +423,83 @@ export class CredentialTypesController { ) } } + + /** + * Create a new revocation registry definition + * + * @param credentialDefinitionId + * @returns RevocationTypeInfo + */ + @Post('/revoke/:credentialDefinitionId') + public async createRevocationRegistry( + @Param('credentialDefinitionId') credentialDefinitionId: string, + ): Promise { + try { + const agent = await this.agentService.getAgent() + + const cred = await agent.modules.anoncreds.getCredentialDefinition(credentialDefinitionId) + + if (!cred || !cred.credentialDefinition?.value.revocation) { + throw new Error( + `No suitable revocation configuration found for the given credentialDefinitionId: ${credentialDefinitionId}`, + ) + } + const revocationResult = await agent.modules.anoncreds.registerRevocationRegistryDefinition({ + revocationRegistryDefinition: { + credentialDefinitionId, + tag: 'default', + maximumCredentialNumber: 1000, + issuerId: cred.credentialDefinition.issuerId, + }, + options: {}, + }) + const revocationRegistryDefinitionId = + revocationResult.revocationRegistryDefinitionState.revocationRegistryDefinitionId + this.logger.debug!( + `revocationRegistryDefinitionState: ${JSON.stringify(revocationResult.revocationRegistryDefinitionState)}`, + ) + + if (!revocationRegistryDefinitionId) { + throw new Error( + `Cannot create credential revocations: ${JSON.stringify(revocationResult.registrationMetadata)}`, + ) + } + + const revStatusListResult = await agent.modules.anoncreds.registerRevocationStatusList({ + revocationStatusList: { + issuerId: cred.credentialDefinition.issuerId, + revocationRegistryDefinitionId: revocationRegistryDefinitionId, + }, + options: {}, + }) + const revocationDefinitionRepository = agent.dependencyManager.resolve( + AnonCredsRevocationRegistryDefinitionRepository, + ) + const revocationDefinitionRecord = + await revocationDefinitionRepository.getByRevocationRegistryDefinitionId( + agent.context, + revocationRegistryDefinitionId, + ) + revocationDefinitionRecord.metadata.set( + 'revStatusList', + revStatusListResult.revocationStatusListState.revocationStatusList!, + ) + await revocationDefinitionRepository.update(agent.context, revocationDefinitionRecord) + + this.logger.log(`Revocation Registry Definition Id: ${revocationRegistryDefinitionId}`) + + return revocationRegistryDefinitionId + } catch (error) { + throw new HttpException( + { + statusCode: HttpStatus.INTERNAL_SERVER_ERROR, + error: `something went wrong: ${error}`, + }, + HttpStatus.INTERNAL_SERVER_ERROR, + { + cause: error, + }, + ) + } + } } diff --git a/packages/main/src/controllers/credentials/CredentialTypeDto.ts b/packages/main/src/controllers/credentials/CredentialTypeDto.ts index 9800db3..2288e3e 100644 --- a/packages/main/src/controllers/credentials/CredentialTypeDto.ts +++ b/packages/main/src/controllers/credentials/CredentialTypeDto.ts @@ -34,7 +34,8 @@ export class CreateCredentialTypeDto { schemaId?: string @ApiProperty({ - description: 'Indicates whether to enable credential revocation support. If enabled, it allows revocation of issued credentials.', + description: + 'Indicates whether to enable credential revocation support. If enabled, it allows revocation of issued credentials.', example: true, default: false, }) diff --git a/packages/model/src/types.ts b/packages/model/src/types.ts index e99f22e..85ab584 100644 --- a/packages/model/src/types.ts +++ b/packages/model/src/types.ts @@ -25,7 +25,7 @@ export interface CreateCredentialTypeOptions { version: string attributes: string[] schemaId?: string - revocationId?: string[] + revocationIds?: string[] supportRevocation?: boolean } From 93db6a31c05a1f77ff651d70d011ba05a6dc7bbf Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Wed, 18 Dec 2024 13:10:50 -0500 Subject: [PATCH 043/132] feat: add CreateRevocationRegistryDto --- .../client/src/services/CredentialTypeService.ts | 5 +++-- .../credentials/CreateRevocationRegistryDto.ts | 12 ++++++++++++ .../credentials/CredentialTypeController.ts | 8 ++++---- 3 files changed, 19 insertions(+), 6 deletions(-) create mode 100644 packages/main/src/controllers/credentials/CreateRevocationRegistryDto.ts diff --git a/packages/client/src/services/CredentialTypeService.ts b/packages/client/src/services/CredentialTypeService.ts index 0b59764..ecface2 100644 --- a/packages/client/src/services/CredentialTypeService.ts +++ b/packages/client/src/services/CredentialTypeService.ts @@ -46,7 +46,7 @@ export class CredentialTypeService { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(credentialType), }) - return ( await response.json()) as CredentialTypeInfo + return (await response.json()) as CredentialTypeInfo } public async getAll(): Promise { @@ -65,9 +65,10 @@ export class CredentialTypeService { } public async createRevocation(credentialDefinitionId: string): Promise { - const response = await fetch(`${this.url}/revoke/${credentialDefinitionId}`, { + const response = await fetch(`${this.url}/revoke`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ credentialDefinitionId }), }) return response.text() } diff --git a/packages/main/src/controllers/credentials/CreateRevocationRegistryDto.ts b/packages/main/src/controllers/credentials/CreateRevocationRegistryDto.ts new file mode 100644 index 0000000..3521c20 --- /dev/null +++ b/packages/main/src/controllers/credentials/CreateRevocationRegistryDto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger' +import { IsString, IsNotEmpty } from 'class-validator' + +export class CreateRevocationRegistryDto { + @ApiProperty({ + description: 'credentialDefinitionId', + example: 'did:web', + }) + @IsString() + @IsNotEmpty() + credentialDefinitionId!: string +} diff --git a/packages/main/src/controllers/credentials/CredentialTypeController.ts b/packages/main/src/controllers/credentials/CredentialTypeController.ts index 6c613af..276fc46 100644 --- a/packages/main/src/controllers/credentials/CredentialTypeController.ts +++ b/packages/main/src/controllers/credentials/CredentialTypeController.ts @@ -29,6 +29,7 @@ import { ApiBody, ApiTags } from '@nestjs/swagger' import { AgentService } from '../../services/AgentService' +import { CreateRevocationRegistryDto } from './CreateRevocationRegistryDto' import { CreateCredentialTypeDto } from './CredentialTypeDto' @ApiTags('credential-types') @@ -430,12 +431,11 @@ export class CredentialTypesController { * @param credentialDefinitionId * @returns RevocationTypeInfo */ - @Post('/revoke/:credentialDefinitionId') - public async createRevocationRegistry( - @Param('credentialDefinitionId') credentialDefinitionId: string, - ): Promise { + @Post('/revoke') + public async createRevocationRegistry(@Body() options: CreateRevocationRegistryDto): Promise { try { const agent = await this.agentService.getAgent() + const credentialDefinitionId = options.credentialDefinitionId const cred = await agent.modules.anoncreds.getCredentialDefinition(credentialDefinitionId) From d661c9e54157191b10bd23b2dce32c31be8265ef Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Wed, 18 Dec 2024 13:12:45 -0500 Subject: [PATCH 044/132] fix: add CreateRevocationRegistryDto --- examples/chatbot/index.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/chatbot/index.ts b/examples/chatbot/index.ts index b8e9040..645ac13 100644 --- a/examples/chatbot/index.ts +++ b/examples/chatbot/index.ts @@ -100,10 +100,12 @@ const server = app.listen(PORT, async () => { const credentialDefinition = (await apiClient.credentialTypes.import(phoneCredDefData)) phoneNumberCredentialDefinitionId = phoneNumberCredentialType?.id ?? credentialDefinition.id - phoneNumberRevocationDefinitionId = - phoneNumberCredentialType?.revocationId ? phoneNumberCredentialType?.revocationId?.[0] : credentialDefinition.revocationId?.[0] logger.info(`phoneNumberCredentialDefinitionId: ${phoneNumberCredentialDefinitionId}`) - logger.info(`phoneNumberRevocationDefinitionId: ${credentialDefinition.revocationId}`) + phoneNumberRevocationDefinitionId = + phoneNumberCredentialType?.revocationIds ? + phoneNumberCredentialType?.revocationIds?.[0] : + await apiClient.credentialTypes.createRevocation(phoneNumberCredentialDefinitionId) + logger.info(`phoneNumberRevocationDefinitionId: ${phoneNumberRevocationDefinitionId}`) } catch (error) { logger.error(`Could not create or retrieve phone number credential type: ${error}`) } From 7eee7bb207de4dc73650191f18e1ec248e40f4a0 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Wed, 18 Dec 2024 17:50:19 -0500 Subject: [PATCH 045/132] fix: add get revocationDefinition --- .../credentials/CredentialTypeController.ts | 49 ++++++++++++------- packages/model/src/types.ts | 1 - 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/packages/main/src/controllers/credentials/CredentialTypeController.ts b/packages/main/src/controllers/credentials/CredentialTypeController.ts index 276fc46..4f5b2c9 100644 --- a/packages/main/src/controllers/credentials/CredentialTypeController.ts +++ b/packages/main/src/controllers/credentials/CredentialTypeController.ts @@ -59,23 +59,11 @@ export class CredentialTypesController { const schema = schemaResult.schema - const revocationDefinitionRepository = agent.dependencyManager.resolve( - AnonCredsRevocationRegistryDefinitionRepository, - ) - const revocationRegistries = await revocationDefinitionRepository.findAllByCredentialDefinitionId( - agent.context, - record.credentialDefinitionId, - ) - const revocationRegistryDefinitionIds = revocationRegistries.map( - item => item.revocationRegistryDefinitionId, - ) - return { id: record.credentialDefinitionId, name: (record.getTag('name') as string) ?? schema?.name, version: (record.getTag('version') as string) ?? schema?.version, attributes: schema?.attrNames || [], - revocationIds: revocationRegistryDefinitionIds, } }), ) @@ -431,7 +419,7 @@ export class CredentialTypesController { * @param credentialDefinitionId * @returns RevocationTypeInfo */ - @Post('/revoke') + @Post('/revocationDefinition') public async createRevocationRegistry(@Body() options: CreateRevocationRegistryDto): Promise { try { const agent = await this.agentService.getAgent() @@ -455,15 +443,14 @@ export class CredentialTypesController { }) const revocationRegistryDefinitionId = revocationResult.revocationRegistryDefinitionState.revocationRegistryDefinitionId - this.logger.debug!( - `revocationRegistryDefinitionState: ${JSON.stringify(revocationResult.revocationRegistryDefinitionState)}`, - ) - if (!revocationRegistryDefinitionId) { throw new Error( `Cannot create credential revocations: ${JSON.stringify(revocationResult.registrationMetadata)}`, ) } + this.logger.debug!( + `revocationRegistryDefinitionState: ${JSON.stringify(revocationResult.revocationRegistryDefinitionState)}`, + ) const revStatusListResult = await agent.modules.anoncreds.registerRevocationStatusList({ revocationStatusList: { @@ -472,6 +459,9 @@ export class CredentialTypesController { }, options: {}, }) + if (!revStatusListResult.revocationStatusListState.revocationStatusList) { + throw new Error(`Failed to create revocation status list`) + } const revocationDefinitionRepository = agent.dependencyManager.resolve( AnonCredsRevocationRegistryDefinitionRepository, ) @@ -482,7 +472,7 @@ export class CredentialTypesController { ) revocationDefinitionRecord.metadata.set( 'revStatusList', - revStatusListResult.revocationStatusListState.revocationStatusList!, + revStatusListResult.revocationStatusListState.revocationStatusList, ) await revocationDefinitionRepository.update(agent.context, revocationDefinitionRecord) @@ -502,4 +492,27 @@ export class CredentialTypesController { ) } } + + /** + * Get all revocation definitions by credentialDefinitionId + * + * @returns string[] with revocationRegistryDefinitionIds + */ + @Get('/revocationDefinition') + public async getRevocationDefinitions(@Body() options: CreateRevocationRegistryDto): Promise { + const agent = await this.agentService.getAgent() + + const revocationDefinitionRepository = agent.dependencyManager.resolve( + AnonCredsRevocationRegistryDefinitionRepository, + ) + const revocationRegistries = await revocationDefinitionRepository.findAllByCredentialDefinitionId( + agent.context, + options.credentialDefinitionId, + ) + const revocationRegistryDefinitionIds = revocationRegistries.map( + item => item.revocationRegistryDefinitionId, + ) + + return revocationRegistryDefinitionIds + } } diff --git a/packages/model/src/types.ts b/packages/model/src/types.ts index 85ab584..ed8762c 100644 --- a/packages/model/src/types.ts +++ b/packages/model/src/types.ts @@ -25,7 +25,6 @@ export interface CreateCredentialTypeOptions { version: string attributes: string[] schemaId?: string - revocationIds?: string[] supportRevocation?: boolean } From 2f3d0e048dccbd4574d2be41bad2e53aef1c9838 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Wed, 18 Dec 2024 17:51:58 -0500 Subject: [PATCH 046/132] fix: add get revocationDefinition --- .../src/controllers/credentials/CredentialTypeController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/main/src/controllers/credentials/CredentialTypeController.ts b/packages/main/src/controllers/credentials/CredentialTypeController.ts index 4f5b2c9..d6404af 100644 --- a/packages/main/src/controllers/credentials/CredentialTypeController.ts +++ b/packages/main/src/controllers/credentials/CredentialTypeController.ts @@ -513,6 +513,6 @@ export class CredentialTypesController { item => item.revocationRegistryDefinitionId, ) - return revocationRegistryDefinitionIds + return revocationRegistryDefinitionIds } } From 25d4a979ce3fa9a2fff1abb87fc2997d92f4b3f0 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Wed, 18 Dec 2024 18:05:46 -0500 Subject: [PATCH 047/132] fix: add maximumCredentialNumber --- .../credentials/CreateRevocationRegistryDto.ts | 14 ++++++++++++-- .../credentials/CredentialTypeController.ts | 6 +++--- packages/model/src/types.ts | 5 +++++ 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/main/src/controllers/credentials/CreateRevocationRegistryDto.ts b/packages/main/src/controllers/credentials/CreateRevocationRegistryDto.ts index 3521c20..6e19116 100644 --- a/packages/main/src/controllers/credentials/CreateRevocationRegistryDto.ts +++ b/packages/main/src/controllers/credentials/CreateRevocationRegistryDto.ts @@ -1,12 +1,22 @@ import { ApiProperty } from '@nestjs/swagger' -import { IsString, IsNotEmpty } from 'class-validator' +import { IsString, IsNotEmpty, IsNumber } from 'class-validator' export class CreateRevocationRegistryDto { @ApiProperty({ description: 'credentialDefinitionId', - example: 'did:web', + example: + 'did:web:chatbot-demo.dev.2060.io?service=anoncreds&relativeRef=/credDef/8TsGLaSPVKPVMXK8APzBRcXZryxutvQuZnnTcDmbqd9p', }) @IsString() @IsNotEmpty() credentialDefinitionId!: string + + @ApiProperty({ + description: 'maximumCredentialNumber', + default: 1000, + example: 1000, + }) + @IsNumber() + @IsNotEmpty() + maximumCredentialNumber: number = 1000 } diff --git a/packages/main/src/controllers/credentials/CredentialTypeController.ts b/packages/main/src/controllers/credentials/CredentialTypeController.ts index d6404af..998707c 100644 --- a/packages/main/src/controllers/credentials/CredentialTypeController.ts +++ b/packages/main/src/controllers/credentials/CredentialTypeController.ts @@ -419,7 +419,7 @@ export class CredentialTypesController { * @param credentialDefinitionId * @returns RevocationTypeInfo */ - @Post('/revocationDefinition') + @Post('/revocationRegistry') public async createRevocationRegistry(@Body() options: CreateRevocationRegistryDto): Promise { try { const agent = await this.agentService.getAgent() @@ -436,7 +436,7 @@ export class CredentialTypesController { revocationRegistryDefinition: { credentialDefinitionId, tag: 'default', - maximumCredentialNumber: 1000, + maximumCredentialNumber: options.maximumCredentialNumber, issuerId: cred.credentialDefinition.issuerId, }, options: {}, @@ -498,7 +498,7 @@ export class CredentialTypesController { * * @returns string[] with revocationRegistryDefinitionIds */ - @Get('/revocationDefinition') + @Get('/revocationRegistry') public async getRevocationDefinitions(@Body() options: CreateRevocationRegistryDto): Promise { const agent = await this.agentService.getAgent() diff --git a/packages/model/src/types.ts b/packages/model/src/types.ts index ed8762c..5917016 100644 --- a/packages/model/src/types.ts +++ b/packages/model/src/types.ts @@ -47,6 +47,11 @@ export interface CredentialTypeInfo extends CreateCredentialTypeOptions { id: string } +export interface RevocationRegistry { + credentialDefinitionId: string + maximumCredentialNumber: number +} + export interface CreatePresentationRequestOptions { requestedCredentials: RequestedCredential[] } From 9131bc7b53aa32d69ff3369036185d1b4044a537 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Wed, 18 Dec 2024 22:04:13 -0500 Subject: [PATCH 048/132] feat: add apiClient revocation registry --- packages/client/src/ApiClient.ts | 8 ++- .../src/services/CredentialTypeService.ts | 9 --- .../src/services/RevocationRegistryService.ts | 70 +++++++++++++++++++ .../CreateRevocationRegistryDto.ts | 3 +- packages/model/src/types.ts | 2 +- 5 files changed, 79 insertions(+), 13 deletions(-) create mode 100644 packages/client/src/services/RevocationRegistryService.ts diff --git a/packages/client/src/ApiClient.ts b/packages/client/src/ApiClient.ts index 1e8af97..e2148e8 100644 --- a/packages/client/src/ApiClient.ts +++ b/packages/client/src/ApiClient.ts @@ -1,5 +1,6 @@ // src/ApiClient.ts +import { RevocationRegistryService } from './services' import { CredentialTypeService } from './services/CredentialTypeService' import { MessageService } from './services/MessageService' import { ApiVersion } from './types/enums' @@ -23,18 +24,20 @@ import { ApiVersion } from './types/enums' * const apiClient = new ApiClient('http://localhost', ApiVersion.V1) * * // Example to query available credentials - * await apiClient.credentialType.getAllCredentialTypes() + * await apiClient.credentialType.getAll() * * // Example to send a message - * apiClient.message.sendMessage(message: BaseMessage) + * apiClient.message.send(message: BaseMessage) * * The `ApiClient` class provides easy methods for interacting with: * - `message`: Send and manage messages. * - `credentialType`: Query and manage credential types. + * - `revocationRegistry`: Query and manage the revocation registry for credential definitions. */ export class ApiClient { public readonly messages: MessageService public readonly credentialTypes: CredentialTypeService + public readonly revocationRegistry: RevocationRegistryService constructor( private baseURL: string, @@ -42,5 +45,6 @@ export class ApiClient { ) { this.messages = new MessageService(baseURL, version) this.credentialTypes = new CredentialTypeService(baseURL, version) + this.revocationRegistry = new RevocationRegistryService(baseURL, version) } } diff --git a/packages/client/src/services/CredentialTypeService.ts b/packages/client/src/services/CredentialTypeService.ts index ecface2..85c47c7 100644 --- a/packages/client/src/services/CredentialTypeService.ts +++ b/packages/client/src/services/CredentialTypeService.ts @@ -63,13 +63,4 @@ export class CredentialTypeService { return types.map(value => value as CredentialTypeInfo) } - - public async createRevocation(credentialDefinitionId: string): Promise { - const response = await fetch(`${this.url}/revoke`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ credentialDefinitionId }), - }) - return response.text() - } } diff --git a/packages/client/src/services/RevocationRegistryService.ts b/packages/client/src/services/RevocationRegistryService.ts new file mode 100644 index 0000000..d406193 --- /dev/null +++ b/packages/client/src/services/RevocationRegistryService.ts @@ -0,0 +1,70 @@ +// src/services/RevocationRegistryService.ts + +import { RevocationRegistryInfo } from '@2060.io/service-agent-model' +import { Logger } from 'tslog' + +import { ApiVersion } from '../types/enums' + +const logger = new Logger({ + name: 'RevocationRegistryService', + type: 'pretty', + prettyLogTemplate: '{{logLevelName}} [{{name}}]: ', +}) + +/** + * `RevocationRegistryService` class for managing credential types and interacting with + * the available endpoints related to credential types in the Agent Service. + * + * This class provides methods for querying, creating, and managing revocation registry on credential types. + * For a list of available endpoints and functionality, refer to the methods within this class. + */ +export class RevocationRegistryService { + private url: string + + constructor( + private baseURL: string, + private version: ApiVersion, + ) { + this.url = `${this.baseURL.replace(/\/$/, '')}/${this.version}/credential-types` + } + + public async create(options: RevocationRegistryInfo): Promise { + logger.info(`Creating revocation registry with credentialDefinitionId: ${JSON.stringify(options)}`) + const response = await fetch(`${this.url}/revocationRegistry`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ options }), + }) + return response.text() + } + + public async get(credentialDefinitionId: string): Promise { + logger.info(`Searching revocation registry with credentialDefinitionId: ${credentialDefinitionId}`) + const response = await fetch(`${this.url}/revocationRegistry`, { + method: 'GET', + headers: { accept: 'application/json', 'Content-Type': 'application/json' }, + body: JSON.stringify({ credentialDefinitionId }), + }) + + if (!response.ok) { + throw new Error(`Failed to fetch revocation definitions: ${response.statusText}`) + } + + return (await response.json()) as string[] + } + + public async getAll(): Promise { + logger.info(`Searching all revocation registry`) + const response = await fetch(`${this.url}/revocationRegistry`, { + method: 'GET', + headers: { accept: 'application/json', 'Content-Type': 'application/json' }, + body: JSON.stringify({ credentialDefinitionId: '' }), + }) + + if (!response.ok) { + throw new Error(`Failed to fetch revocation definitions: ${response.statusText}`) + } + + return (await response.json()) as string[] + } +} diff --git a/packages/main/src/controllers/credentials/CreateRevocationRegistryDto.ts b/packages/main/src/controllers/credentials/CreateRevocationRegistryDto.ts index 6e19116..1997a1f 100644 --- a/packages/main/src/controllers/credentials/CreateRevocationRegistryDto.ts +++ b/packages/main/src/controllers/credentials/CreateRevocationRegistryDto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger' -import { IsString, IsNotEmpty, IsNumber } from 'class-validator' +import { IsString, IsNotEmpty, IsNumber, IsOptional } from 'class-validator' export class CreateRevocationRegistryDto { @ApiProperty({ @@ -18,5 +18,6 @@ export class CreateRevocationRegistryDto { }) @IsNumber() @IsNotEmpty() + @IsOptional() maximumCredentialNumber: number = 1000 } diff --git a/packages/model/src/types.ts b/packages/model/src/types.ts index 5917016..f696ca8 100644 --- a/packages/model/src/types.ts +++ b/packages/model/src/types.ts @@ -47,7 +47,7 @@ export interface CredentialTypeInfo extends CreateCredentialTypeOptions { id: string } -export interface RevocationRegistry { +export interface RevocationRegistryInfo { credentialDefinitionId: string maximumCredentialNumber: number } From ea7a063bcc77756eb07cbfaf64ff87e9f37af692 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Wed, 18 Dec 2024 22:11:53 -0500 Subject: [PATCH 049/132] fix: add apiClient revocation registry --- packages/client/src/services/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/client/src/services/index.ts b/packages/client/src/services/index.ts index d7a4aa1..6f1c474 100644 --- a/packages/client/src/services/index.ts +++ b/packages/client/src/services/index.ts @@ -1,2 +1,3 @@ export * from './CredentialTypeService' export * from './MessageService' +export * from './RevocationRegistryService' From 491cfdf6ac93b0c9cab3eaab635e436e569e0cf2 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Wed, 18 Dec 2024 22:55:42 -0500 Subject: [PATCH 050/132] fix: add apiClient revocation registry --- .../src/services/RevocationRegistryService.ts | 15 +++++++------- .../credentials/CredentialTypeController.ts | 20 ++++++++++++++----- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/packages/client/src/services/RevocationRegistryService.ts b/packages/client/src/services/RevocationRegistryService.ts index d406193..208e796 100644 --- a/packages/client/src/services/RevocationRegistryService.ts +++ b/packages/client/src/services/RevocationRegistryService.ts @@ -40,11 +40,13 @@ export class RevocationRegistryService { public async get(credentialDefinitionId: string): Promise { logger.info(`Searching revocation registry with credentialDefinitionId: ${credentialDefinitionId}`) - const response = await fetch(`${this.url}/revocationRegistry`, { - method: 'GET', - headers: { accept: 'application/json', 'Content-Type': 'application/json' }, - body: JSON.stringify({ credentialDefinitionId }), - }) + const response = await fetch( + `${this.url}/revocationRegistry?credentialDefinitionId=${encodeURIComponent(credentialDefinitionId)}`, + { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + }, + ) if (!response.ok) { throw new Error(`Failed to fetch revocation definitions: ${response.statusText}`) @@ -57,8 +59,7 @@ export class RevocationRegistryService { logger.info(`Searching all revocation registry`) const response = await fetch(`${this.url}/revocationRegistry`, { method: 'GET', - headers: { accept: 'application/json', 'Content-Type': 'application/json' }, - body: JSON.stringify({ credentialDefinitionId: '' }), + headers: { 'Content-Type': 'application/json' }, }) if (!response.ok) { diff --git a/packages/main/src/controllers/credentials/CredentialTypeController.ts b/packages/main/src/controllers/credentials/CredentialTypeController.ts index 998707c..13d47ec 100644 --- a/packages/main/src/controllers/credentials/CredentialTypeController.ts +++ b/packages/main/src/controllers/credentials/CredentialTypeController.ts @@ -24,6 +24,7 @@ import { NotFoundException, Param, Post, + Query, } from '@nestjs/common' import { ApiBody, ApiTags } from '@nestjs/swagger' @@ -499,16 +500,25 @@ export class CredentialTypesController { * @returns string[] with revocationRegistryDefinitionIds */ @Get('/revocationRegistry') - public async getRevocationDefinitions(@Body() options: CreateRevocationRegistryDto): Promise { + public async getRevocationDefinitions( + @Query('credentialDefinitionId') credentialDefinitionId?: string, + ): Promise { const agent = await this.agentService.getAgent() const revocationDefinitionRepository = agent.dependencyManager.resolve( AnonCredsRevocationRegistryDefinitionRepository, ) - const revocationRegistries = await revocationDefinitionRepository.findAllByCredentialDefinitionId( - agent.context, - options.credentialDefinitionId, - ) + let revocationRegistries + if (!credentialDefinitionId) { + revocationRegistries = await revocationDefinitionRepository.getAll( + agent.context, + ) + } else { + revocationRegistries = await revocationDefinitionRepository.findAllByCredentialDefinitionId( + agent.context, + credentialDefinitionId, + ) + } const revocationRegistryDefinitionIds = revocationRegistries.map( item => item.revocationRegistryDefinitionId, ) From 0c98a558d1fbfac90d707e7fedafb71235d19483 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Wed, 18 Dec 2024 23:51:48 -0500 Subject: [PATCH 051/132] fix: add apiClient revocation registry --- examples/chatbot/index.ts | 11 +++++++---- .../src/services/RevocationRegistryService.ts | 15 +++++++++------ .../credentials/CredentialTypeController.ts | 4 +--- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/examples/chatbot/index.ts b/examples/chatbot/index.ts index 645ac13..64e5193 100644 --- a/examples/chatbot/index.ts +++ b/examples/chatbot/index.ts @@ -101,10 +101,13 @@ const server = app.listen(PORT, async () => { phoneNumberCredentialDefinitionId = phoneNumberCredentialType?.id ?? credentialDefinition.id logger.info(`phoneNumberCredentialDefinitionId: ${phoneNumberCredentialDefinitionId}`) - phoneNumberRevocationDefinitionId = - phoneNumberCredentialType?.revocationIds ? - phoneNumberCredentialType?.revocationIds?.[0] : - await apiClient.credentialTypes.createRevocation(phoneNumberCredentialDefinitionId) + phoneNumberRevocationDefinitionId = + phoneNumberCredentialType?.id ? + (await apiClient.revocationRegistry.get(phoneNumberCredentialDefinitionId))[0] : + await apiClient.revocationRegistry.create({ + credentialDefinitionId: phoneNumberCredentialDefinitionId, + maximumCredentialNumber: 1000 + }) logger.info(`phoneNumberRevocationDefinitionId: ${phoneNumberRevocationDefinitionId}`) } catch (error) { logger.error(`Could not create or retrieve phone number credential type: ${error}`) diff --git a/packages/client/src/services/RevocationRegistryService.ts b/packages/client/src/services/RevocationRegistryService.ts index 208e796..08e21a6 100644 --- a/packages/client/src/services/RevocationRegistryService.ts +++ b/packages/client/src/services/RevocationRegistryService.ts @@ -28,18 +28,22 @@ export class RevocationRegistryService { this.url = `${this.baseURL.replace(/\/$/, '')}/${this.version}/credential-types` } - public async create(options: RevocationRegistryInfo): Promise { - logger.info(`Creating revocation registry with credentialDefinitionId: ${JSON.stringify(options)}`) + public async create(options: RevocationRegistryInfo): Promise { const response = await fetch(`${this.url}/revocationRegistry`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ options }), + body: JSON.stringify(options), }) - return response.text() + + if (!response.ok) { + logger.error(`Failed to create revocation registry`) + return undefined + } + + return await response.text() } public async get(credentialDefinitionId: string): Promise { - logger.info(`Searching revocation registry with credentialDefinitionId: ${credentialDefinitionId}`) const response = await fetch( `${this.url}/revocationRegistry?credentialDefinitionId=${encodeURIComponent(credentialDefinitionId)}`, { @@ -56,7 +60,6 @@ export class RevocationRegistryService { } public async getAll(): Promise { - logger.info(`Searching all revocation registry`) const response = await fetch(`${this.url}/revocationRegistry`, { method: 'GET', headers: { 'Content-Type': 'application/json' }, diff --git a/packages/main/src/controllers/credentials/CredentialTypeController.ts b/packages/main/src/controllers/credentials/CredentialTypeController.ts index 13d47ec..63d58e3 100644 --- a/packages/main/src/controllers/credentials/CredentialTypeController.ts +++ b/packages/main/src/controllers/credentials/CredentialTypeController.ts @@ -510,9 +510,7 @@ export class CredentialTypesController { ) let revocationRegistries if (!credentialDefinitionId) { - revocationRegistries = await revocationDefinitionRepository.getAll( - agent.context, - ) + revocationRegistries = await revocationDefinitionRepository.getAll(agent.context) } else { revocationRegistries = await revocationDefinitionRepository.findAllByCredentialDefinitionId( agent.context, From 9f4fd4a706052141d32174f04ba5f9b443831599 Mon Sep 17 00:00:00 2001 From: andresfv95 Date: Thu, 19 Dec 2024 08:05:37 -0500 Subject: [PATCH 052/132] fix: Update packages/client/src/services/RevocationRegistryService.ts Co-authored-by: Ariel Gentile --- packages/client/src/services/RevocationRegistryService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/services/RevocationRegistryService.ts b/packages/client/src/services/RevocationRegistryService.ts index 08e21a6..39256f5 100644 --- a/packages/client/src/services/RevocationRegistryService.ts +++ b/packages/client/src/services/RevocationRegistryService.ts @@ -66,7 +66,7 @@ export class RevocationRegistryService { }) if (!response.ok) { - throw new Error(`Failed to fetch revocation definitions: ${response.statusText}`) + throw new Error(`Failed to fetch revocation registries: ${response.statusText}`) } return (await response.json()) as string[] From 661c060440d990037e6b91b52eedbb5c7b33a902 Mon Sep 17 00:00:00 2001 From: andresfv95 Date: Thu, 19 Dec 2024 08:12:32 -0500 Subject: [PATCH 053/132] fix: Update packages/main/src/controllers/message/MessageService.ts Co-authored-by: Ariel Gentile --- packages/main/src/controllers/message/MessageService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/main/src/controllers/message/MessageService.ts b/packages/main/src/controllers/message/MessageService.ts index 56a5c37..bfc3c82 100644 --- a/packages/main/src/controllers/message/MessageService.ts +++ b/packages/main/src/controllers/message/MessageService.ts @@ -244,7 +244,7 @@ export class MessageService { const credentials = await agent.credentials.findAllByQuery({ threadId: msg.threadId }) if (credentials && credentials.length > 0) { for (const credential of credentials) { - const isRevoke = Boolean( + const isRevocable = Boolean( credential.getTag('anonCredsRevocationRegistryId') && credential.getTag('anonCredsCredentialRevocationId'), ) From 410adfac98852459db97bfc33d07be4723371498 Mon Sep 17 00:00:00 2001 From: andresfv95 Date: Thu, 19 Dec 2024 08:12:56 -0500 Subject: [PATCH 054/132] fix: Update examples/chatbot/index.ts Co-authored-by: Ariel Gentile --- examples/chatbot/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/chatbot/index.ts b/examples/chatbot/index.ts index 64e5193..de3a39d 100644 --- a/examples/chatbot/index.ts +++ b/examples/chatbot/index.ts @@ -102,8 +102,7 @@ const server = app.listen(PORT, async () => { phoneNumberCredentialType?.id ?? credentialDefinition.id logger.info(`phoneNumberCredentialDefinitionId: ${phoneNumberCredentialDefinitionId}`) phoneNumberRevocationDefinitionId = - phoneNumberCredentialType?.id ? - (await apiClient.revocationRegistry.get(phoneNumberCredentialDefinitionId))[0] : + (await apiClient.revocationRegistry.get(phoneNumberCredentialDefinitionId))[0] ?? await apiClient.revocationRegistry.create({ credentialDefinitionId: phoneNumberCredentialDefinitionId, maximumCredentialNumber: 1000 From daeecf351233c03a8c05faf4594607124a21ad85 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Thu, 19 Dec 2024 08:18:05 -0500 Subject: [PATCH 055/132] fix: update without React-style --- .../src/controllers/message/MessageService.ts | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/packages/main/src/controllers/message/MessageService.ts b/packages/main/src/controllers/message/MessageService.ts index bfc3c82..c9d96a1 100644 --- a/packages/main/src/controllers/message/MessageService.ts +++ b/packages/main/src/controllers/message/MessageService.ts @@ -248,23 +248,21 @@ export class MessageService { credential.getTag('anonCredsRevocationRegistryId') && credential.getTag('anonCredsCredentialRevocationId'), ) - isRevoke && - (await agent.modules.anoncreds.updateRevocationStatusList({ - revocationStatusList: { - revocationRegistryDefinitionId: credential.getTag( - 'anonCredsRevocationRegistryId', - ) as string, - revokedCredentialIndexes: [Number(credential.getTag('anonCredsCredentialRevocationId'))], - }, - options: {}, - })) - - isRevoke && - (await agent.credentials.sendRevocationNotification({ - credentialRecordId: credential.id, - revocationFormat: 'anoncreds', - revocationId: `${credential.getTag('anonCredsRevocationRegistryId')}::${credential.getTag('anonCredsCredentialRevocationId')}`, - })) + if (!isRevocable) throw new Error(`Credential for threadId ${msg.threadId} is not revocable)`) + + await agent.modules.anoncreds.updateRevocationStatusList({ + revocationStatusList: { + revocationRegistryDefinitionId: credential.getTag('anonCredsRevocationRegistryId') as string, + revokedCredentialIndexes: [Number(credential.getTag('anonCredsCredentialRevocationId'))], + }, + options: {}, + }) + + await agent.credentials.sendRevocationNotification({ + credentialRecordId: credential.id, + revocationFormat: 'anoncreds', + revocationId: `${credential.getTag('anonCredsRevocationRegistryId')}::${credential.getTag('anonCredsCredentialRevocationId')}`, + }) } } else { throw new Error(`No credentials were found for connection: ${msg.connectionId}.`) From 43ff688b371255cf3acb4baf046c72272c49d823 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Thu, 19 Dec 2024 08:20:51 -0500 Subject: [PATCH 056/132] fix: check updateRevocationStatusList --- packages/main/src/controllers/message/MessageService.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/main/src/controllers/message/MessageService.ts b/packages/main/src/controllers/message/MessageService.ts index c9d96a1..342c5d8 100644 --- a/packages/main/src/controllers/message/MessageService.ts +++ b/packages/main/src/controllers/message/MessageService.ts @@ -250,13 +250,16 @@ export class MessageService { ) if (!isRevocable) throw new Error(`Credential for threadId ${msg.threadId} is not revocable)`) - await agent.modules.anoncreds.updateRevocationStatusList({ + const uptStatusListResult = await agent.modules.anoncreds.updateRevocationStatusList({ revocationStatusList: { revocationRegistryDefinitionId: credential.getTag('anonCredsRevocationRegistryId') as string, revokedCredentialIndexes: [Number(credential.getTag('anonCredsCredentialRevocationId'))], }, options: {}, }) + if (!uptStatusListResult.revocationStatusListState.revocationStatusList) { + throw new Error(`Failed to update revocation status list`) + } await agent.credentials.sendRevocationNotification({ credentialRecordId: credential.id, From 54545c9f4e7bc1e868d3ec7ba8f5aed3f1847521 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Thu, 19 Dec 2024 12:04:27 -0500 Subject: [PATCH 057/132] refactor: remove config file and change by types --- .../src/connections/connection.config.ts | 10 -- .../src/connections/connection.module.ts | 11 +- .../src/connections/connection.service.ts | 3 +- .../nestjs-client/src/connections/index.ts | 1 - .../src/credentials/credential.entity.ts | 2 +- .../src/credentials/credential.service.ts | 100 +++++++++--------- packages/nestjs-client/src/index.ts | 1 + packages/nestjs-client/src/messages/index.ts | 1 - .../src/messages/message.config.ts | 14 --- .../src/messages/message.module.ts | 11 +- .../src/messages/message.service.ts | 12 +-- packages/nestjs-client/src/types.ts | 31 ++++++ 12 files changed, 102 insertions(+), 95 deletions(-) delete mode 100644 packages/nestjs-client/src/connections/connection.config.ts delete mode 100644 packages/nestjs-client/src/messages/message.config.ts create mode 100644 packages/nestjs-client/src/types.ts diff --git a/packages/nestjs-client/src/connections/connection.config.ts b/packages/nestjs-client/src/connections/connection.config.ts deleted file mode 100644 index a0bed1d..0000000 --- a/packages/nestjs-client/src/connections/connection.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { DynamicModule, Type } from '@nestjs/common' - -import { EventHandler } from '../interfaces' - -export interface ConnectionsModuleOptions { - eventHandler: Type - imports: DynamicModule[] -} - -export const CONNECTIONS_EVENT = 'CONNECTIONS_EVENT' diff --git a/packages/nestjs-client/src/connections/connection.module.ts b/packages/nestjs-client/src/connections/connection.module.ts index cdb9dd7..2c06bb4 100644 --- a/packages/nestjs-client/src/connections/connection.module.ts +++ b/packages/nestjs-client/src/connections/connection.module.ts @@ -1,24 +1,23 @@ import { Module, DynamicModule } from '@nestjs/common' -import { TypeOrmModule } from '@nestjs/typeorm' -import { CONNECTIONS_EVENT, ConnectionsModuleOptions } from './connection.config' +import { ConnectionEventOptions } from '../types' + import { ConnectionsEventController } from './connection.controller' -import { ConnectionEntity } from './connection.entity' import { ConnectionsRepository } from './connection.repository' import { ConnectionsEventService } from './connection.service' @Module({}) export class ConnectionsEventModule { - static forRoot(options: ConnectionsModuleOptions): DynamicModule { + static forRoot(options: ConnectionEventOptions): DynamicModule { return { module: ConnectionsEventModule, - imports: [TypeOrmModule.forFeature([ConnectionEntity]), ...options.imports], + imports: options.imports, controllers: [ConnectionsEventController], providers: [ ConnectionsEventService, ConnectionsRepository, { - provide: CONNECTIONS_EVENT, + provide: 'CONNECTIONS_EVENT', useClass: options.eventHandler, }, ], diff --git a/packages/nestjs-client/src/connections/connection.service.ts b/packages/nestjs-client/src/connections/connection.service.ts index 5fb2555..31023e8 100644 --- a/packages/nestjs-client/src/connections/connection.service.ts +++ b/packages/nestjs-client/src/connections/connection.service.ts @@ -3,7 +3,6 @@ import { Inject, Injectable, Logger, Optional } from '@nestjs/common' import { EventHandler } from '../interfaces' -import { CONNECTIONS_EVENT } from './connection.config' import { ConnectionEntity } from './connection.entity' import { ConnectionsRepository } from './connection.repository' @@ -14,7 +13,7 @@ export class ConnectionsEventService { constructor( @Inject() private readonly repository: ConnectionsRepository, - @Optional() @Inject(CONNECTIONS_EVENT) private eventHandler?: EventHandler, + @Optional() @Inject('CONNECTIONS_EVENT') private eventHandler?: EventHandler, ) {} async update(event: ConnectionStateUpdated): Promise { diff --git a/packages/nestjs-client/src/connections/index.ts b/packages/nestjs-client/src/connections/index.ts index d9a4ed6..cd9e588 100644 --- a/packages/nestjs-client/src/connections/index.ts +++ b/packages/nestjs-client/src/connections/index.ts @@ -1,4 +1,3 @@ -export * from './connection.config' export * from './connection.controller' export * from './connection.entity' export * from './connection.module' diff --git a/packages/nestjs-client/src/credentials/credential.entity.ts b/packages/nestjs-client/src/credentials/credential.entity.ts index 81faea1..2b9b7e6 100644 --- a/packages/nestjs-client/src/credentials/credential.entity.ts +++ b/packages/nestjs-client/src/credentials/credential.entity.ts @@ -14,7 +14,7 @@ import { ConnectionEntity } from '../connections' export class CredentialEntity { @PrimaryGeneratedColumn('uuid') id!: string - + @Column({ type: 'varchar', nullable: false }) credentialDefinitionId?: string diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 13d96f7..4e53a6f 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -1,21 +1,26 @@ -import { - Claim, - CredentialIssuanceMessage, - CredentialTypeInfo, -} from '@2060.io/service-agent-model' -import { Injectable, Logger } from "@nestjs/common" +import { ApiClient, ApiVersion } from '@2060.io/service-agent-client' +import { Claim, CredentialIssuanceMessage, CredentialTypeInfo } from '@2060.io/service-agent-model' +import { Injectable, Logger } from '@nestjs/common' import { InjectRepository } from '@nestjs/typeorm' -import { CredentialEntity } from "./credential.entity" -import { Repository } from "typeorm" +import { Repository } from 'typeorm' + +import { CredentialEntity } from './credential.entity' @Injectable() export class CredentialEventService { private readonly logger = new Logger(CredentialEventService.name) + // private readonly url: string + // private readonly version: ApiVersion + // private readonly apiClient: ApiClient constructor( @InjectRepository(CredentialEntity) private readonly credentialRepository: Repository, - ) {} + ) { + // this.url = options.url + // this.version = options.version + // this.apiClient = new ApiClient(this.url, this.version) + } /** * Creates a credential using the provided records. @@ -24,7 +29,7 @@ export class CredentialEventService { * * @param {CredentialTypeInfo} records - An object containing the attributes * of the credential to be created. - * + * * Example of constructing the `records` object: * const records = { * name: "Chabot", @@ -34,20 +39,20 @@ export class CredentialEventService { * * @returns {Promise} A promise that resolves when the credential is created successfully. */ - async createCredential(records: CredentialTypeInfo): Promise { - const [credential] = await this.apiClient.credentialTypes.getAll() + // async createCredential(records: CredentialTypeInfo): Promise { + // const [credential] = await this.apiClient.credentialTypes.getAll() - if (!credential || credential.length === 0) { - const newCredential = await this.apiClient.credentialTypes.create({ - id: records.id, - name: records.name, - version: records.version, - attributes: records.attributes, - }) - credential.push(newCredential) - } - return credential - } + // if (!credential || credential.length === 0) { + // const newCredential = await this.apiClient.credentialTypes.create({ + // id: records.id, + // name: records.name, + // version: records.version, + // attributes: records.attributes, + // }) + // credential.push(newCredential) + // } + // return credential + // } /** * Sends a credential issuance to the specified connection using the provided claims. @@ -59,7 +64,7 @@ export class CredentialEventService { * * @param {Record} records - A key value objects, where each key represents an attribute * of the credential. - * + * * Example of constructing the `records` array: * const records = { * { name: "email", value: "john.doe@example.com" }, @@ -70,30 +75,27 @@ export class CredentialEventService { * sent. If an error occurs during the process, the promise will be rejected. */ async sendCredentialIssuance(connectionId: string, records: Record): Promise { - const claims: Claim[] = [] - if (records) { - Object.entries(records).forEach(([key, value]) => { - claims.push( - new Claim({ - name: key, - value: value ?? null, - }), - ) - }) - } - - let credentialId - let credential = (await this.apiClient.credentialTypes.getAll())[0] - if (!credential) credentialId = (await this.sendCredentialType())[0]?.id - await this.apiClient.messages.send( - new CredentialIssuanceMessage({ - connectionId: connectionId, - credentialDefinitionId: credentialId, - claims: claims, - }), - ) - - this.logger.debug('sendCredential with claims: ' + JSON.stringify(claims)) + // const claims: Claim[] = [] + // if (records) { + // Object.entries(records).forEach(([key, value]) => { + // claims.push( + // new Claim({ + // name: key, + // value: value ?? null, + // }), + // ) + // }) + // } + // let credentialId + // let credential = (await this.apiClient.credentialTypes.getAll())[0] + // if (!credential) credentialId = (await this.sendCredentialType())[0]?.id + // await this.apiClient.messages.send( + // new CredentialIssuanceMessage({ + // connectionId: connectionId, + // credentialDefinitionId: credentialId, + // claims: claims, + // }), + // ) + // this.logger.debug('sendCredential with claims: ' + JSON.stringify(claims)) } } - diff --git a/packages/nestjs-client/src/index.ts b/packages/nestjs-client/src/index.ts index c03be6b..534ed5a 100644 --- a/packages/nestjs-client/src/index.ts +++ b/packages/nestjs-client/src/index.ts @@ -3,3 +3,4 @@ export * from './connections' export * from './credentials' export * from './interfaces' export * from './messages' +export * from './types' diff --git a/packages/nestjs-client/src/messages/index.ts b/packages/nestjs-client/src/messages/index.ts index 0733792..1c4e14d 100644 --- a/packages/nestjs-client/src/messages/index.ts +++ b/packages/nestjs-client/src/messages/index.ts @@ -1,4 +1,3 @@ -export * from './message.config' export * from './message.controller' export * from './message.module' export * from './message.service' diff --git a/packages/nestjs-client/src/messages/message.config.ts b/packages/nestjs-client/src/messages/message.config.ts deleted file mode 100644 index 730232a..0000000 --- a/packages/nestjs-client/src/messages/message.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ApiVersion } from '@2060.io/service-agent-client' -import { DynamicModule, Type } from '@nestjs/common' - -import { EventHandler } from '../interfaces' - -export interface MessageModuleOptions { - eventHandler: Type - imports: DynamicModule[] - url: string - version: ApiVersion -} - -export const MESSAGE_MODULE_OPTIONS = 'MESSAGE_MODULE_OPTIONS' -export const MESSAGE_EVENT = 'MESSAGE_EVENT' diff --git a/packages/nestjs-client/src/messages/message.module.ts b/packages/nestjs-client/src/messages/message.module.ts index 74249f1..c1519fe 100644 --- a/packages/nestjs-client/src/messages/message.module.ts +++ b/packages/nestjs-client/src/messages/message.module.ts @@ -1,24 +1,25 @@ import { DynamicModule, Module } from '@nestjs/common' -import { MESSAGE_EVENT, MESSAGE_MODULE_OPTIONS, MessageModuleOptions } from './message.config' +import { MessageEventOptions } from '../types' + import { MessageEventController } from './message.controller' import { MessageEventService } from './message.service' @Module({}) export class MessageEventModule { - static forRoot(options: MessageModuleOptions): DynamicModule { + static forRoot(options: MessageEventOptions): DynamicModule { return { module: MessageEventModule, - imports: [...options.imports], + imports: options.imports, controllers: [MessageEventController], providers: [ MessageEventService, { - provide: MESSAGE_MODULE_OPTIONS, + provide: 'MESSAGE_MODULE_OPTIONS', useValue: options, }, { - provide: MESSAGE_EVENT, + provide: 'MESSAGE_EVENT', useClass: options.eventHandler, }, ], diff --git a/packages/nestjs-client/src/messages/message.service.ts b/packages/nestjs-client/src/messages/message.service.ts index 57342b1..bc06c4d 100644 --- a/packages/nestjs-client/src/messages/message.service.ts +++ b/packages/nestjs-client/src/messages/message.service.ts @@ -11,10 +11,9 @@ import { MessageState } from 'credo-ts-receipts' import { Repository } from 'typeorm' import { ConnectionsRepository } from '../connections' -import { EventHandler } from '../interfaces' - -import { MESSAGE_EVENT, MESSAGE_MODULE_OPTIONS, MessageModuleOptions } from './message.config' import { CredentialEntity } from '../credentials' +import { EventHandler } from '../interfaces' +import { MessageEventOptions } from '../types' @Injectable() export class MessageEventService { @@ -24,14 +23,15 @@ export class MessageEventService { private readonly apiClient: ApiClient constructor( - @Inject(MESSAGE_MODULE_OPTIONS) private options: MessageModuleOptions, + @Inject('MESSAGE_MODULE_OPTIONS') private options: MessageEventOptions, @InjectRepository(CredentialEntity) private readonly credentialRepository: Repository, private readonly connectionRepository: ConnectionsRepository, - @Optional() @Inject(MESSAGE_EVENT) private eventHandler?: EventHandler, + @Optional() @Inject('MESSAGE_EVENT') private eventHandler?: EventHandler, ) { + if (!options.url) throw new Error(`For this module to be used the value url must be added`) this.url = options.url - this.version = options.version + this.version = options.version ?? ApiVersion.V1 this.apiClient = new ApiClient(this.url, this.version) diff --git a/packages/nestjs-client/src/types.ts b/packages/nestjs-client/src/types.ts new file mode 100644 index 0000000..83a37f7 --- /dev/null +++ b/packages/nestjs-client/src/types.ts @@ -0,0 +1,31 @@ +import { ApiVersion } from '@2060.io/service-agent-client' +import { DynamicModule, Type } from '@nestjs/common' + +import { EventHandler } from './interfaces' + +export interface MessageEventOptions { + eventHandler: Type + imports?: DynamicModule[] + url?: string + version?: ApiVersion +} + +export interface ConnectionEventOptions { + eventHandler: Type + imports?: DynamicModule[] +} + +export interface ModulesConfig { + messages?: boolean + connections?: boolean +} + +export interface EventsModuleOptions { + modules: ModulesConfig + options: { + eventHandler?: Type + imports?: DynamicModule[] + url?: string + version?: ApiVersion + } +} From a2947a55efbb663f21460e78974a59eb20e184fe Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Thu, 19 Dec 2024 12:04:36 -0500 Subject: [PATCH 058/132] refactor: remove config file and change by types --- packages/nestjs-client/src/app.module.ts | 55 +++++++----------------- 1 file changed, 16 insertions(+), 39 deletions(-) diff --git a/packages/nestjs-client/src/app.module.ts b/packages/nestjs-client/src/app.module.ts index 014e4d2..aef496e 100644 --- a/packages/nestjs-client/src/app.module.ts +++ b/packages/nestjs-client/src/app.module.ts @@ -1,62 +1,39 @@ -import { Module, DynamicModule, Provider, Type } from '@nestjs/common' +import { Module, DynamicModule } from '@nestjs/common' import { ConnectionsEventModule } from './connections' import { MessageEventModule } from './messages' -import { ApiVersion } from 'packages/client/build'; -import { EventHandler } from './interfaces'; - -export interface EventsModuleOptions { - enableMessages?: boolean; - enableConnections?: boolean; - eventHandler?: Type; - url?: string; - version?: string; -} - -export const EVENTS_MODULE_OPTIONS = 'EVENTS_MODULE_OPTIONS' +import { EventsModuleOptions } from './types' @Module({}) export class EventsModule { - static register(options: EventsModuleOptions = {}): DynamicModule { + static register(options: EventsModuleOptions): DynamicModule { const imports = [] + const { modules, options: moduleOptions } = options - const providers: Provider[] = [ - { - provide: EVENTS_MODULE_OPTIONS, - useValue: options, - }, - ]; - - - if (options.enableMessages !== false) { + if (modules.messages && moduleOptions.eventHandler) { imports.push( MessageEventModule.forRoot({ - eventHandler: options.eventHandler, - url: options.url, - version: options.version as ApiVersion, - }) + eventHandler: moduleOptions.eventHandler, + imports: moduleOptions.imports ?? [], + url: moduleOptions.url, + version: moduleOptions.version, + }), ) } - if (options.enableConnections !== false) { + if (modules.connections && moduleOptions.eventHandler) { imports.push( ConnectionsEventModule.forRoot({ - eventHandler: options.eventHandler, - }) - ); + eventHandler: moduleOptions.eventHandler, + imports: moduleOptions.imports ?? [], + }), + ) } return { module: EventsModule, imports, - providers, - exports: [ - ...imports, - { - provide: EVENTS_MODULE_OPTIONS, - useValue: options, - }, - ], + exports: imports, } } } From 36cfe1cb62d8c6a8a3ed72993a5fdbfa65a9c62c Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Fri, 20 Dec 2024 10:57:48 -0500 Subject: [PATCH 059/132] docs: update --- packages/nestjs-client/README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/nestjs-client/README.md b/packages/nestjs-client/README.md index 25306c8..194f37d 100644 --- a/packages/nestjs-client/README.md +++ b/packages/nestjs-client/README.md @@ -27,4 +27,26 @@ export class AppModule {} ], }) +``` + + +## How to use for main module +```node +@Module({ + imports: [ + EventsModule.register({ + modules: { + messages: true, + connections: false, + }, + options: { + eventHandler: CoreService, + imports: [], + url: process.env.SERVICE_AGENT_ADMIN_URL, + version: ApiVersion.V1, + } + }) + ], +}) + ``` \ No newline at end of file From e325a7e2aa54d07e15fb9ea7552ec51e6758e4f4 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Fri, 20 Dec 2024 10:59:16 -0500 Subject: [PATCH 060/132] feat: update modules --- packages/nestjs-client/src/app.module.ts | 16 ++++++++++-- .../src/connections/connection.module.ts | 2 +- .../src/credentials/credential.module.ts | 23 +++++++++++++++++ .../src/messages/message.module.ts | 4 +-- packages/nestjs-client/src/types.ts | 25 +++++++++++++++++-- 5 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 packages/nestjs-client/src/credentials/credential.module.ts diff --git a/packages/nestjs-client/src/app.module.ts b/packages/nestjs-client/src/app.module.ts index aef496e..4b49654 100644 --- a/packages/nestjs-client/src/app.module.ts +++ b/packages/nestjs-client/src/app.module.ts @@ -3,6 +3,7 @@ import { Module, DynamicModule } from '@nestjs/common' import { ConnectionsEventModule } from './connections' import { MessageEventModule } from './messages' import { EventsModuleOptions } from './types' +import { CredentialEventModule } from './credentials' @Module({}) export class EventsModule { @@ -10,7 +11,7 @@ export class EventsModule { const imports = [] const { modules, options: moduleOptions } = options - if (modules.messages && moduleOptions.eventHandler) { + if (modules.messages) { imports.push( MessageEventModule.forRoot({ eventHandler: moduleOptions.eventHandler, @@ -21,7 +22,7 @@ export class EventsModule { ) } - if (modules.connections && moduleOptions.eventHandler) { + if (modules.connections) { imports.push( ConnectionsEventModule.forRoot({ eventHandler: moduleOptions.eventHandler, @@ -30,6 +31,17 @@ export class EventsModule { ) } + if (modules.credentials) { + imports.push( + CredentialEventModule.forRoot({ + imports: moduleOptions.imports ?? [], + url: moduleOptions.url, + version: moduleOptions.version, + creds: moduleOptions.creds + }), + ) + } + return { module: EventsModule, imports, diff --git a/packages/nestjs-client/src/connections/connection.module.ts b/packages/nestjs-client/src/connections/connection.module.ts index 2c06bb4..c89044b 100644 --- a/packages/nestjs-client/src/connections/connection.module.ts +++ b/packages/nestjs-client/src/connections/connection.module.ts @@ -18,7 +18,7 @@ export class ConnectionsEventModule { ConnectionsRepository, { provide: 'CONNECTIONS_EVENT', - useClass: options.eventHandler, + useValue: options.eventHandler, }, ], exports: [ConnectionsRepository], diff --git a/packages/nestjs-client/src/credentials/credential.module.ts b/packages/nestjs-client/src/credentials/credential.module.ts new file mode 100644 index 0000000..2c26ef3 --- /dev/null +++ b/packages/nestjs-client/src/credentials/credential.module.ts @@ -0,0 +1,23 @@ +import { DynamicModule, Module } from '@nestjs/common' + +import { CredentialEventOptions } from '../types' +import { CredentialEventService } from './credential.service' + +@Module({}) +export class CredentialEventModule { + static forRoot(options: CredentialEventOptions): DynamicModule { + return { + module: CredentialEventModule, + imports: options.imports, + controllers: [], + providers: [ + CredentialEventService, + { + provide: 'EVENT_MODULE_OPTIONS', + useValue: options, + }, + ], + exports: [CredentialEventService] + } + } +} diff --git a/packages/nestjs-client/src/messages/message.module.ts b/packages/nestjs-client/src/messages/message.module.ts index c1519fe..d3bbc3c 100644 --- a/packages/nestjs-client/src/messages/message.module.ts +++ b/packages/nestjs-client/src/messages/message.module.ts @@ -15,12 +15,12 @@ export class MessageEventModule { providers: [ MessageEventService, { - provide: 'MESSAGE_MODULE_OPTIONS', + provide: 'EVENT_MODULE_OPTIONS', useValue: options, }, { provide: 'MESSAGE_EVENT', - useClass: options.eventHandler, + useValue: options.eventHandler, }, ], } diff --git a/packages/nestjs-client/src/types.ts b/packages/nestjs-client/src/types.ts index 83a37f7..3e08117 100644 --- a/packages/nestjs-client/src/types.ts +++ b/packages/nestjs-client/src/types.ts @@ -4,20 +4,34 @@ import { DynamicModule, Type } from '@nestjs/common' import { EventHandler } from './interfaces' export interface MessageEventOptions { - eventHandler: Type + eventHandler?: Type imports?: DynamicModule[] url?: string version?: ApiVersion } export interface ConnectionEventOptions { - eventHandler: Type + eventHandler?: Type imports?: DynamicModule[] } +export interface CredentialEventOptions { + imports?: DynamicModule[] + url?: string + version?: ApiVersion + creds?: { + name?: string + version?: string + attributes?: string[] + supportRevocation?: boolean + maximumCredentialNumber?: number + } +} + export interface ModulesConfig { messages?: boolean connections?: boolean + credentials?: boolean } export interface EventsModuleOptions { @@ -27,5 +41,12 @@ export interface EventsModuleOptions { imports?: DynamicModule[] url?: string version?: ApiVersion + creds?: { + name?: string + version?: string + attributes?: string[] + supportRevocation?: boolean + maximumCredentialNumber?: number + } } } From 81809468b0ef01e0653cc146a08347a80de9aa2e Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Fri, 20 Dec 2024 11:31:55 -0500 Subject: [PATCH 061/132] feat: implement credential module --- packages/nestjs-client/src/app.module.ts | 4 +- .../src/credentials/credential.entity.ts | 30 ++++++------- .../src/credentials/credential.module.ts | 3 +- .../nestjs-client/src/credentials/index.ts | 2 + .../src/messages/message.service.ts | 45 +++++++------------ 5 files changed, 36 insertions(+), 48 deletions(-) diff --git a/packages/nestjs-client/src/app.module.ts b/packages/nestjs-client/src/app.module.ts index 4b49654..3aac874 100644 --- a/packages/nestjs-client/src/app.module.ts +++ b/packages/nestjs-client/src/app.module.ts @@ -1,9 +1,9 @@ import { Module, DynamicModule } from '@nestjs/common' import { ConnectionsEventModule } from './connections' +import { CredentialEventModule } from './credentials' import { MessageEventModule } from './messages' import { EventsModuleOptions } from './types' -import { CredentialEventModule } from './credentials' @Module({}) export class EventsModule { @@ -37,7 +37,7 @@ export class EventsModule { imports: moduleOptions.imports ?? [], url: moduleOptions.url, version: moduleOptions.version, - creds: moduleOptions.creds + creds: moduleOptions.creds, }), ) } diff --git a/packages/nestjs-client/src/credentials/credential.entity.ts b/packages/nestjs-client/src/credentials/credential.entity.ts index 2b9b7e6..4b3737e 100644 --- a/packages/nestjs-client/src/credentials/credential.entity.ts +++ b/packages/nestjs-client/src/credentials/credential.entity.ts @@ -1,14 +1,4 @@ -import { - Entity, - Column, - PrimaryGeneratedColumn, - CreateDateColumn, - UpdateDateColumn, - JoinColumn, - OneToOne, -} from 'typeorm' - -import { ConnectionEntity } from '../connections' +import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm' @Entity('credentials') export class CredentialEntity { @@ -16,21 +6,29 @@ export class CredentialEntity { id!: string @Column({ type: 'varchar', nullable: false }) - credentialDefinitionId?: string + credentialDefinitionId!: string @Column({ type: 'varchar', nullable: true }) revocationDefinitionId?: string - @Column({ type: 'integer', generated: 'increment', nullable: true }) + @Column({ type: 'integer', nullable: true }) revocationRegistryIndex?: number - @OneToOne(() => ConnectionEntity, { nullable: false }) - @JoinColumn({ name: 'connection_id', referencedColumnName: 'id' }) - connectionId?: ConnectionEntity + @Column({ type: 'integer', nullable: false }) + maximumCredentialNumber!: number + + @Column({ type: 'varchar', nullable: true }) + connectionId?: string + + @Column({ type: 'varchar', nullable: true }) + threadId?: string @Column({ type: 'blob', nullable: true }) hash?: Buffer + @Column({ nullable: true }) + revoked?: boolean + @CreateDateColumn() createdTs?: Date diff --git a/packages/nestjs-client/src/credentials/credential.module.ts b/packages/nestjs-client/src/credentials/credential.module.ts index 2c26ef3..c2e748f 100644 --- a/packages/nestjs-client/src/credentials/credential.module.ts +++ b/packages/nestjs-client/src/credentials/credential.module.ts @@ -1,6 +1,7 @@ import { DynamicModule, Module } from '@nestjs/common' import { CredentialEventOptions } from '../types' + import { CredentialEventService } from './credential.service' @Module({}) @@ -17,7 +18,7 @@ export class CredentialEventModule { useValue: options, }, ], - exports: [CredentialEventService] + exports: [CredentialEventService], } } } diff --git a/packages/nestjs-client/src/credentials/index.ts b/packages/nestjs-client/src/credentials/index.ts index 67c94f4..8bb2212 100644 --- a/packages/nestjs-client/src/credentials/index.ts +++ b/packages/nestjs-client/src/credentials/index.ts @@ -1 +1,3 @@ export * from './credential.entity' +export * from './credential.service' +export * from './credential.module' diff --git a/packages/nestjs-client/src/messages/message.service.ts b/packages/nestjs-client/src/messages/message.service.ts index bc06c4d..2f64674 100644 --- a/packages/nestjs-client/src/messages/message.service.ts +++ b/packages/nestjs-client/src/messages/message.service.ts @@ -6,12 +6,9 @@ import { ReceiptsMessage, } from '@2060.io/service-agent-model' import { Inject, Injectable, Logger, Optional } from '@nestjs/common' -import { InjectRepository } from '@nestjs/typeorm' import { MessageState } from 'credo-ts-receipts' -import { Repository } from 'typeorm' -import { ConnectionsRepository } from '../connections' -import { CredentialEntity } from '../credentials' +import { CredentialEventService } from '../credentials' import { EventHandler } from '../interfaces' import { MessageEventOptions } from '../types' @@ -23,27 +20,31 @@ export class MessageEventService { private readonly apiClient: ApiClient constructor( - @Inject('MESSAGE_MODULE_OPTIONS') private options: MessageEventOptions, - @InjectRepository(CredentialEntity) - private readonly credentialRepository: Repository, - private readonly connectionRepository: ConnectionsRepository, + @Inject('EVENT_MODULE_OPTIONS') private options: MessageEventOptions, @Optional() @Inject('MESSAGE_EVENT') private eventHandler?: EventHandler, + @Optional() @Inject() private credentialEvent?: CredentialEventService, ) { if (!options.url) throw new Error(`For this module to be used the value url must be added`) this.url = options.url this.version = options.version ?? ApiVersion.V1 + if (!credentialEvent) + this.logger.warn( + `To handle credential events and their revocation, make sure to initialize the CredentialEventModule.`, + ) + this.apiClient = new ApiClient(this.url, this.version) this.logger.debug(`Initialized with url: ${this.url}, version: ${this.version}`) } async received(event: MessageReceived): Promise { + const message = event.message const body = new ReceiptsMessage({ - connectionId: event.message.connectionId, + connectionId: message.connectionId, receipts: [ { - messageId: event.message.id, + messageId: message.id, state: MessageState.Viewed, timestamp: new Date(), }, @@ -54,32 +55,18 @@ export class MessageEventService { await this.apiClient.messages.send(body) if (this.eventHandler) { - if (event.message instanceof CredentialReceptionMessage) { + if (message instanceof CredentialReceptionMessage) { try { - const [credential] = await this.apiClient.credentialTypes.getAll() - const connectionId = await this.connectionRepository.findById(event.message.connectionId) - const hash = Buffer.from(await this.eventHandler.credentialHash(event.message.connectionId)) - const currentCred = await this.credentialRepository.findOneBy({ hash }) - const isCredentialDone = event.message.state === CredentialState.Done - - if (connectionId && isCredentialDone) { - if (!currentCred) { - const credentialRev = this.credentialRepository.create({ - connectionId, - hash, - revocationDefinitionId: credential.revocationId, - }) - await this.credentialRepository.save(credentialRev) - } else { - this.credentialRepository.update(currentCred.id, { connectionId }) - } + const isCredentialDone = message.state === CredentialState.Done + if (this.credentialEvent && isCredentialDone && message.threadId) { + this.credentialEvent.accept(message.connectionId, message.threadId) } } catch (error) { this.logger.error(`Cannot create the registry: ${error}`) } } - await this.eventHandler.inputMessage(event.message) + await this.eventHandler.inputMessage(message) } } From 9129370fba2770f2d4e706d21e6db23ebe28cd59 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Fri, 20 Dec 2024 12:07:28 -0500 Subject: [PATCH 062/132] feat: handle credential --- .../src/credentials/credential.service.ts | 152 ++++++++++-------- 1 file changed, 88 insertions(+), 64 deletions(-) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 4e53a6f..3d9b7df 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -1,58 +1,81 @@ import { ApiClient, ApiVersion } from '@2060.io/service-agent-client' -import { Claim, CredentialIssuanceMessage, CredentialTypeInfo } from '@2060.io/service-agent-model' -import { Injectable, Logger } from '@nestjs/common' +import { Claim, CredentialIssuanceMessage, CredentialRevocationMessage } from '@2060.io/service-agent-model' +import { Inject, Injectable, Logger, OnModuleInit } from '@nestjs/common' import { InjectRepository } from '@nestjs/typeorm' -import { Repository } from 'typeorm' +import { EntityManager, Repository } from 'typeorm' + +import { CredentialEventOptions } from '../types' import { CredentialEntity } from './credential.entity' @Injectable() -export class CredentialEventService { +export class CredentialEventService implements OnModuleInit { private readonly logger = new Logger(CredentialEventService.name) - // private readonly url: string - // private readonly version: ApiVersion - // private readonly apiClient: ApiClient + + // Service agent client API + private readonly url: string + private readonly apiVersion: ApiVersion + private readonly apiClient: ApiClient + + //Credential type definitions + private readonly name: string + private readonly version: string + private readonly attributes: string[] + private readonly supportRevocation: boolean + private readonly maximumCredentialNumber: number constructor( @InjectRepository(CredentialEntity) private readonly credentialRepository: Repository, + @Inject('EVENT_MODULE_OPTIONS') private options: CredentialEventOptions, + private readonly entityManager: EntityManager, ) { - // this.url = options.url - // this.version = options.version - // this.apiClient = new ApiClient(this.url, this.version) + if (!options.url) throw new Error(`For this module to be used the value url must be added`) + this.url = options.url + this.apiVersion = options.version ?? ApiVersion.V1 + + if (!options.creds?.attributes) + throw new Error(`For this module to be used, the parameter credential types must be added`) + this.name = options.creds?.name ?? 'Chatbot' + this.version = options.creds?.version ?? '1.0' + this.attributes = options.creds?.attributes + this.supportRevocation = options.creds?.supportRevocation ?? false + this.maximumCredentialNumber = options.creds?.maximumCredentialNumber ?? 1000 + + this.apiClient = new ApiClient(this.url, this.apiVersion) + + this.logger.debug(`Initialized with url: ${this.url}, version: ${this.apiVersion}`) } - /** - * Creates a credential using the provided records. - * This method requires a `CredentialTypeInfo` object with necessary parameters - * such as the credential's name, version, and attributes. - * - * @param {CredentialTypeInfo} records - An object containing the attributes - * of the credential to be created. - * - * Example of constructing the `records` object: - * const records = { - * name: "Chabot", - * version: "1.0", - * attributes: ["email"] - * }; - * - * @returns {Promise} A promise that resolves when the credential is created successfully. - */ - // async createCredential(records: CredentialTypeInfo): Promise { - // const [credential] = await this.apiClient.credentialTypes.getAll() - - // if (!credential || credential.length === 0) { - // const newCredential = await this.apiClient.credentialTypes.create({ - // id: records.id, - // name: records.name, - // version: records.version, - // attributes: records.attributes, - // }) - // credential.push(newCredential) - // } - // return credential - // } + async onModuleInit() { + const [credential] = await this.apiClient.credentialTypes.getAll() + + if (!credential) { + const credential = await this.apiClient.credentialTypes.create({ + id: '', // TODO: implement uuid + name: this.name, + version: this.version, + attributes: this.attributes, + supportRevocation: this.supportRevocation, + }) + + await this.createRevocationRegistry(credential.id) + } + } + + async createRevocationRegistry(credentialDefinitionId: string) { + const revocationRegistry = await this.apiClient.revocationRegistry.create({ + credentialDefinitionId, + maximumCredentialNumber: this.maximumCredentialNumber, + }) + const credentialRev = this.credentialRepository.create({ + credentialDefinitionId, + revocationDefinitionId: revocationRegistry, + revocationRegistryIndex: 0, + maximumCredentialNumber: this.maximumCredentialNumber, + }) + await this.credentialRepository.save(credentialRev) + } /** * Sends a credential issuance to the specified connection using the provided claims. @@ -74,28 +97,29 @@ export class CredentialEventService { * @returns {Promise} A promise that resolves when the credential issuance is successfully * sent. If an error occurs during the process, the promise will be rejected. */ - async sendCredentialIssuance(connectionId: string, records: Record): Promise { - // const claims: Claim[] = [] - // if (records) { - // Object.entries(records).forEach(([key, value]) => { - // claims.push( - // new Claim({ - // name: key, - // value: value ?? null, - // }), - // ) - // }) - // } - // let credentialId - // let credential = (await this.apiClient.credentialTypes.getAll())[0] - // if (!credential) credentialId = (await this.sendCredentialType())[0]?.id - // await this.apiClient.messages.send( - // new CredentialIssuanceMessage({ - // connectionId: connectionId, - // credentialDefinitionId: credentialId, - // claims: claims, - // }), - // ) - // this.logger.debug('sendCredential with claims: ' + JSON.stringify(claims)) + + async accept(connectionId: string, threadId: string): Promise { + const cred = await this.credentialRepository.findOne({ + where: { connectionId: connectionId }, + order: { createdTs: 'DESC' }, + }) + if (!cred) throw new Error(`Credential not found with connectionId: ${connectionId}`) + await this.credentialRepository.update(cred.id, { threadId }) + } + + async revoke(threadId: string): Promise { + const cred = await this.credentialRepository.findOne({ where: { threadId } }) + if (!cred || !cred.connectionId) { + throw new Error(`Credencial with threadId ${threadId} not found`) + } + + await this.credentialRepository.update(cred.id, { revoked: true }) + await this.apiClient.messages.send( + new CredentialRevocationMessage({ + connectionId: cred.connectionId, + threadId, + }), + ) + this.logger.log(`Revoke Credential: ${cred.id}`) } } From f44010edbab1bf477326ae99e579a6762d6b401e Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Fri, 20 Dec 2024 15:04:02 -0500 Subject: [PATCH 063/132] feat: add credential service --- packages/nestjs-client/package.json | 1 + .../src/credentials/credential.entity.ts | 8 +- .../src/credentials/credential.service.ts | 104 +++++++++++++++--- 3 files changed, 93 insertions(+), 20 deletions(-) diff --git a/packages/nestjs-client/package.json b/packages/nestjs-client/package.json index 4b7d709..e666c39 100644 --- a/packages/nestjs-client/package.json +++ b/packages/nestjs-client/package.json @@ -21,6 +21,7 @@ "dependencies": { "@2060.io/service-agent-model": "*", "@2060.io/service-agent-client": "*", + "@credo-ts/core": "^0.5.11", "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", "@nestjs/platform-express": "^10.0.0", diff --git a/packages/nestjs-client/src/credentials/credential.entity.ts b/packages/nestjs-client/src/credentials/credential.entity.ts index 4b3737e..e8507e3 100644 --- a/packages/nestjs-client/src/credentials/credential.entity.ts +++ b/packages/nestjs-client/src/credentials/credential.entity.ts @@ -8,11 +8,11 @@ export class CredentialEntity { @Column({ type: 'varchar', nullable: false }) credentialDefinitionId!: string - @Column({ type: 'varchar', nullable: true }) - revocationDefinitionId?: string + @Column({ type: 'varchar', nullable: false }) + revocationDefinitionId!: string - @Column({ type: 'integer', nullable: true }) - revocationRegistryIndex?: number + @Column({ type: 'integer', nullable: false }) + revocationRegistryIndex!: number @Column({ type: 'integer', nullable: false }) maximumCredentialNumber!: number diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 3d9b7df..57bca8e 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -1,5 +1,6 @@ import { ApiClient, ApiVersion } from '@2060.io/service-agent-client' import { Claim, CredentialIssuanceMessage, CredentialRevocationMessage } from '@2060.io/service-agent-model' +import { Sha256, utils } from '@credo-ts/core' import { Inject, Injectable, Logger, OnModuleInit } from '@nestjs/common' import { InjectRepository } from '@nestjs/typeorm' import { EntityManager, Repository } from 'typeorm' @@ -16,7 +17,7 @@ export class CredentialEventService implements OnModuleInit { private readonly url: string private readonly apiVersion: ApiVersion private readonly apiClient: ApiClient - + //Credential type definitions private readonly name: string private readonly version: string @@ -52,7 +53,7 @@ export class CredentialEventService implements OnModuleInit { if (!credential) { const credential = await this.apiClient.credentialTypes.create({ - id: '', // TODO: implement uuid + id: utils.uuid(), name: this.name, version: this.version, attributes: this.attributes, @@ -63,20 +64,6 @@ export class CredentialEventService implements OnModuleInit { } } - async createRevocationRegistry(credentialDefinitionId: string) { - const revocationRegistry = await this.apiClient.revocationRegistry.create({ - credentialDefinitionId, - maximumCredentialNumber: this.maximumCredentialNumber, - }) - const credentialRev = this.credentialRepository.create({ - credentialDefinitionId, - revocationDefinitionId: revocationRegistry, - revocationRegistryIndex: 0, - maximumCredentialNumber: this.maximumCredentialNumber, - }) - await this.credentialRepository.save(credentialRev) - } - /** * Sends a credential issuance to the specified connection using the provided claims. * This method initiates the issuance process by sending claims as part of a credential to @@ -97,7 +84,67 @@ export class CredentialEventService implements OnModuleInit { * @returns {Promise} A promise that resolves when the credential issuance is successfully * sent. If an error occurs during the process, the promise will be rejected. */ + async issuance(connectionId: string, records: Record, hash: string): Promise { + const [{ id: credentialDefinitionId }] = await this.apiClient.credentialTypes.getAll() + const claims: Claim[] = [] + if (records) { + Object.entries(records).forEach(([key, value]) => { + claims.push( + new Claim({ + name: key, + value: value ?? null, + }), + ) + }) + } + const { revocationDefinitionId, revocationRegistryIndex } = await this.entityManager.transaction( + async transaction => { + const lastCred = await transaction.findOne(CredentialEntity, { + where: { + credentialDefinitionId, + }, + order: { revocationRegistryIndex: 'DESC' }, + lock: { mode: 'pessimistic_write' }, + }) + if (!lastCred) + throw new Error( + 'No valid registry definition found. Please restart the service and ensure the module is imported correctly', + ) + + const newCredential = await transaction.save(CredentialEntity, { + connectionId, + credentialDefinitionId, + revocationDefinitionId: lastCred.revocationDefinitionId, + revocationRegistryIndex: lastCred.revocationRegistryIndex + 1, + hash: Buffer.from(new Sha256().hash(hash)), + maximumCredentialNumber: lastCred.maximumCredentialNumber, + }) + return { + revocationDefinitionId: newCredential.revocationDefinitionId, + revocationRegistryIndex: newCredential.revocationRegistryIndex, + } + }, + ) + + await this.apiClient.messages.send( + new CredentialIssuanceMessage({ + connectionId, + credentialDefinitionId, + revocationRegistryDefinitionId: revocationDefinitionId, + revocationRegistryIndex: revocationRegistryIndex, + claims: claims, + }), + ) + this.logger.debug('sendCredential with claims: ' + JSON.stringify(claims)) + } + + /** + * Accepts a credential by associating it with the provided thread ID. + * @param connectionId - The connection ID associated with the credential. + * @param threadId - The thread ID to link with the credential. + * @throws Error if no credential is found with the specified connection ID. + */ async accept(connectionId: string, threadId: string): Promise { const cred = await this.credentialRepository.findOne({ where: { connectionId: connectionId }, @@ -107,6 +154,11 @@ export class CredentialEventService implements OnModuleInit { await this.credentialRepository.update(cred.id, { threadId }) } + /** + * Revokes a credential associated with the provided thread ID. + * @param threadId - The thread ID linked to the credential to revoke. + * @throws Error if no credential is found with the specified thread ID or if the credential has no connection ID. + */ async revoke(threadId: string): Promise { const cred = await this.credentialRepository.findOne({ where: { threadId } }) if (!cred || !cred.connectionId) { @@ -122,4 +174,24 @@ export class CredentialEventService implements OnModuleInit { ) this.logger.log(`Revoke Credential: ${cred.id}`) } + + // private methods + private async createRevocationRegistry(credentialDefinitionId: string) { + const revocationRegistry = await this.apiClient.revocationRegistry.create({ + credentialDefinitionId, + maximumCredentialNumber: this.maximumCredentialNumber, + }) + const credentialRev = this.credentialRepository.create({ + credentialDefinitionId, + revocationDefinitionId: revocationRegistry, + revocationRegistryIndex: 0, + maximumCredentialNumber: this.maximumCredentialNumber, + }) + await this.credentialRepository.save(credentialRev) + } + + private async createRegistry(record: Partial) { + const credentialRev = this.credentialRepository.create(record) + await this.credentialRepository.save(credentialRev) + } } From ba860869460d9938fa72a3cd2af731e41cb1d3b0 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Fri, 20 Dec 2024 15:45:37 -0500 Subject: [PATCH 064/132] fix: add credential service --- .../src/credentials/credential.service.ts | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 57bca8e..85040f3 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -86,17 +86,9 @@ export class CredentialEventService implements OnModuleInit { */ async issuance(connectionId: string, records: Record, hash: string): Promise { const [{ id: credentialDefinitionId }] = await this.apiClient.credentialTypes.getAll() - const claims: Claim[] = [] - if (records) { - Object.entries(records).forEach(([key, value]) => { - claims.push( - new Claim({ - name: key, - value: value ?? null, - }), - ) - }) - } + const claims = Object.entries(records).map( + ([key, value]) => new Claim({ name: key, value: value ?? null }), + ) const { revocationDefinitionId, revocationRegistryIndex } = await this.entityManager.transaction( async transaction => { From c40f6330e8986954860ab57e669784534125066f Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Thu, 2 Jan 2025 10:49:02 -0500 Subject: [PATCH 065/132] feat: improve handling by revocationDefinitionId --- .../src/credentials/credential.service.ts | 57 ++++++++++++++----- 1 file changed, 44 insertions(+), 13 deletions(-) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 85040f3..5f1964b 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -3,7 +3,7 @@ import { Claim, CredentialIssuanceMessage, CredentialRevocationMessage } from '@ import { Sha256, utils } from '@credo-ts/core' import { Inject, Injectable, Logger, OnModuleInit } from '@nestjs/common' import { InjectRepository } from '@nestjs/typeorm' -import { EntityManager, Repository } from 'typeorm' +import { EntityManager, Equal, In, Not, Repository } from 'typeorm' import { CredentialEventOptions } from '../types' @@ -61,6 +61,7 @@ export class CredentialEventService implements OnModuleInit { }) await this.createRevocationRegistry(credential.id) + await this.createRevocationRegistry(credential.id) } } @@ -92,17 +93,43 @@ export class CredentialEventService implements OnModuleInit { const { revocationDefinitionId, revocationRegistryIndex } = await this.entityManager.transaction( async transaction => { - const lastCred = await transaction.findOne(CredentialEntity, { + const invalidRegistries = await transaction.find(CredentialEntity, { + select: ['revocationDefinitionId'], + where: { + credentialDefinitionId, + revocationRegistryIndex: Equal(this.maximumCredentialNumber), + }, + }) + const invalidRevocationIds = invalidRegistries.map(reg => reg.revocationDefinitionId) + + let lastCred = await transaction.findOne(CredentialEntity, { where: { credentialDefinitionId, + revocationRegistryIndex: Not(Equal(0)), + ...(invalidRevocationIds.length > 0 + ? { + revocationDefinitionId: Not(In(invalidRevocationIds)), + } + : {}), }, order: { revocationRegistryIndex: 'DESC' }, lock: { mode: 'pessimistic_write' }, }) - if (!lastCred) - throw new Error( - 'No valid registry definition found. Please restart the service and ensure the module is imported correctly', - ) + if (!lastCred) { + lastCred = await transaction.findOne(CredentialEntity, { + where: { + credentialDefinitionId, + revocationRegistryIndex: Equal(0), + }, + order: { createdTs: 'DESC' }, + lock: { mode: 'pessimistic_write' }, + }) + + if (!lastCred) + throw new Error( + 'No valid registry definition found. Please restart the service and ensure the module is imported correctly', + ) + } const newCredential = await transaction.save(CredentialEntity, { connectionId, @@ -128,6 +155,10 @@ export class CredentialEventService implements OnModuleInit { claims: claims, }), ) + if (revocationRegistryIndex === this.maximumCredentialNumber) { + const revRegistry = await this.createRevocationRegistry(credentialDefinitionId) + this.logger.log(`Revocation registry successfully created with ID ${revRegistry}`) + } this.logger.debug('sendCredential with claims: ' + JSON.stringify(claims)) } @@ -140,7 +171,7 @@ export class CredentialEventService implements OnModuleInit { async accept(connectionId: string, threadId: string): Promise { const cred = await this.credentialRepository.findOne({ where: { connectionId: connectionId }, - order: { createdTs: 'DESC' }, + order: { createdTs: 'DESC' }, // TODO: improve the search method on differents revocation }) if (!cred) throw new Error(`Credential not found with connectionId: ${connectionId}`) await this.credentialRepository.update(cred.id, { threadId }) @@ -168,11 +199,15 @@ export class CredentialEventService implements OnModuleInit { } // private methods - private async createRevocationRegistry(credentialDefinitionId: string) { + private async createRevocationRegistry(credentialDefinitionId: string): Promise { const revocationRegistry = await this.apiClient.revocationRegistry.create({ credentialDefinitionId, maximumCredentialNumber: this.maximumCredentialNumber, }) + if (!revocationRegistry) + throw new Error( + `Unable to create a new revocation registry for CredentialDefinitionId: ${credentialDefinitionId}`, + ) const credentialRev = this.credentialRepository.create({ credentialDefinitionId, revocationDefinitionId: revocationRegistry, @@ -180,10 +215,6 @@ export class CredentialEventService implements OnModuleInit { maximumCredentialNumber: this.maximumCredentialNumber, }) await this.credentialRepository.save(credentialRev) - } - - private async createRegistry(record: Partial) { - const credentialRev = this.credentialRepository.create(record) - await this.credentialRepository.save(credentialRev) + return revocationRegistry } } From d72de27d40901cf1c33d105d3872c0ce246e54f5 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Thu, 2 Jan 2025 12:24:49 -0500 Subject: [PATCH 066/132] fix: event handler --- .../src/credentials/credential.service.ts | 8 ++++---- packages/nestjs-client/src/interfaces.ts | 17 ----------------- 2 files changed, 4 insertions(+), 21 deletions(-) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 5f1964b..f7d326b 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -182,17 +182,17 @@ export class CredentialEventService implements OnModuleInit { * @param threadId - The thread ID linked to the credential to revoke. * @throws Error if no credential is found with the specified thread ID or if the credential has no connection ID. */ - async revoke(threadId: string): Promise { - const cred = await this.credentialRepository.findOne({ where: { threadId } }) + async revoke(connectionId: string): Promise { + const cred = await this.credentialRepository.findOne({ where: { connectionId } }) if (!cred || !cred.connectionId) { - throw new Error(`Credencial with threadId ${threadId} not found`) + throw new Error(`Credencial with threadId ${cred?.threadId} not found`) } await this.credentialRepository.update(cred.id, { revoked: true }) await this.apiClient.messages.send( new CredentialRevocationMessage({ connectionId: cred.connectionId, - threadId, + threadId: cred?.threadId, }), ) this.logger.log(`Revoke Credential: ${cred.id}`) diff --git a/packages/nestjs-client/src/interfaces.ts b/packages/nestjs-client/src/interfaces.ts index 5d810ed..81d1f69 100644 --- a/packages/nestjs-client/src/interfaces.ts +++ b/packages/nestjs-client/src/interfaces.ts @@ -32,21 +32,4 @@ export interface EventHandler { * @param message - An instance of BaseMessage containing the input message details. */ inputMessage(message: BaseMessage): Promise | void - - /** - * Processes the creation of a unique hash for a credential. - * This method should ensure proper handling of credential generation - * by identifying the session associated with the provided connection ID. - * - * The implementation of this method must: - * 1. Identify the session or context using the given connectionId. - * 2. Generate a unique hash string based on the connection session - * and any other required data for the credential. - * - * @param connectionId - The unique identifier of the connection used - * to determine the session context. - * @returns A Promise that resolves to a unique hash Uint8Array representing - * the generated credential. - */ - credentialHash(connectionId: string): Promise } From 25eab971c0fa05da2acb98c5ce83bf5be01a9856 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Thu, 2 Jan 2025 15:20:32 -0500 Subject: [PATCH 067/132] fix: improve handling modules on nestjs client --- packages/nestjs-client/src/app.module.ts | 3 ++- .../src/connections/connection.module.ts | 5 +++- .../src/credentials/credential.entity.ts | 4 ++-- .../src/credentials/credential.service.ts | 24 ++++++++++++++++++- .../src/messages/message.module.ts | 5 +++- .../src/messages/message.service.ts | 5 ++-- 6 files changed, 38 insertions(+), 8 deletions(-) diff --git a/packages/nestjs-client/src/app.module.ts b/packages/nestjs-client/src/app.module.ts index 3aac874..5c69986 100644 --- a/packages/nestjs-client/src/app.module.ts +++ b/packages/nestjs-client/src/app.module.ts @@ -1,10 +1,11 @@ -import { Module, DynamicModule } from '@nestjs/common' +import { Module, DynamicModule, Global } from '@nestjs/common' import { ConnectionsEventModule } from './connections' import { CredentialEventModule } from './credentials' import { MessageEventModule } from './messages' import { EventsModuleOptions } from './types' +@Global() @Module({}) export class EventsModule { static register(options: EventsModuleOptions): DynamicModule { diff --git a/packages/nestjs-client/src/connections/connection.module.ts b/packages/nestjs-client/src/connections/connection.module.ts index c89044b..bbc144e 100644 --- a/packages/nestjs-client/src/connections/connection.module.ts +++ b/packages/nestjs-client/src/connections/connection.module.ts @@ -9,6 +9,9 @@ import { ConnectionsEventService } from './connection.service' @Module({}) export class ConnectionsEventModule { static forRoot(options: ConnectionEventOptions): DynamicModule { + if (!options.eventHandler) { + throw new Error('Event handler is required but not provided.') + } return { module: ConnectionsEventModule, imports: options.imports, @@ -18,7 +21,7 @@ export class ConnectionsEventModule { ConnectionsRepository, { provide: 'CONNECTIONS_EVENT', - useValue: options.eventHandler, + useClass: options.eventHandler, }, ], exports: [ConnectionsRepository], diff --git a/packages/nestjs-client/src/credentials/credential.entity.ts b/packages/nestjs-client/src/credentials/credential.entity.ts index e8507e3..28201d3 100644 --- a/packages/nestjs-client/src/credentials/credential.entity.ts +++ b/packages/nestjs-client/src/credentials/credential.entity.ts @@ -23,8 +23,8 @@ export class CredentialEntity { @Column({ type: 'varchar', nullable: true }) threadId?: string - @Column({ type: 'blob', nullable: true }) - hash?: Buffer + @Column({ type: 'varchar', nullable: true }) + hash?: string @Column({ nullable: true }) revoked?: boolean diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index f7d326b..65f599b 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -90,6 +90,13 @@ export class CredentialEventService implements OnModuleInit { const claims = Object.entries(records).map( ([key, value]) => new Claim({ name: key, value: value ?? null }), ) + const isRevoked = await this.credentialRepository.findOne({ + where: { + hash: Equal(Buffer.from(new Sha256().hash(hash)).toString('hex')), + revoked: false, + }, + }) + if (isRevoked) throw new Error('Please revoke the credential with the same data first.') const { revocationDefinitionId, revocationRegistryIndex } = await this.entityManager.transaction( async transaction => { @@ -136,7 +143,7 @@ export class CredentialEventService implements OnModuleInit { credentialDefinitionId, revocationDefinitionId: lastCred.revocationDefinitionId, revocationRegistryIndex: lastCred.revocationRegistryIndex + 1, - hash: Buffer.from(new Sha256().hash(hash)), + hash: Buffer.from(new Sha256().hash(hash)).toString('hex'), maximumCredentialNumber: lastCred.maximumCredentialNumber, }) return { @@ -177,6 +184,21 @@ export class CredentialEventService implements OnModuleInit { await this.credentialRepository.update(cred.id, { threadId }) } + /** + * Rejected a credential by associating it with the provided thread ID. + * @param connectionId - The connection ID associated with the credential. + * @param threadId - The thread ID to link with the credential. + * @throws Error if no credential is found with the specified connection ID. + */ + async reject(connectionId: string, threadId: string): Promise { + const cred = await this.credentialRepository.findOne({ + where: { connectionId: connectionId }, + order: { createdTs: 'DESC' }, // TODO: improve the search method on differents revocation + }) + if (!cred) throw new Error(`Credential not found with connectionId: ${connectionId}`) + await this.credentialRepository.update(cred.id, { threadId, revoked: true }) + } + /** * Revokes a credential associated with the provided thread ID. * @param threadId - The thread ID linked to the credential to revoke. diff --git a/packages/nestjs-client/src/messages/message.module.ts b/packages/nestjs-client/src/messages/message.module.ts index d3bbc3c..be00a6f 100644 --- a/packages/nestjs-client/src/messages/message.module.ts +++ b/packages/nestjs-client/src/messages/message.module.ts @@ -8,6 +8,9 @@ import { MessageEventService } from './message.service' @Module({}) export class MessageEventModule { static forRoot(options: MessageEventOptions): DynamicModule { + if (!options.eventHandler) { + throw new Error('Event handler is required but not provided.') + } return { module: MessageEventModule, imports: options.imports, @@ -20,7 +23,7 @@ export class MessageEventModule { }, { provide: 'MESSAGE_EVENT', - useValue: options.eventHandler, + useClass: options.eventHandler, }, ], } diff --git a/packages/nestjs-client/src/messages/message.service.ts b/packages/nestjs-client/src/messages/message.service.ts index 2f64674..2f1b7b0 100644 --- a/packages/nestjs-client/src/messages/message.service.ts +++ b/packages/nestjs-client/src/messages/message.service.ts @@ -58,8 +58,9 @@ export class MessageEventService { if (message instanceof CredentialReceptionMessage) { try { const isCredentialDone = message.state === CredentialState.Done - if (this.credentialEvent && isCredentialDone && message.threadId) { - this.credentialEvent.accept(message.connectionId, message.threadId) + if (this.credentialEvent && message.threadId) { + if (isCredentialDone) this.credentialEvent.accept(message.connectionId, message.threadId) + else this.credentialEvent.reject(message.connectionId, message.threadId) } } catch (error) { this.logger.error(`Cannot create the registry: ${error}`) From 9362423b7933f9c85d64c7c4af32919536cf1c0c Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Thu, 2 Jan 2025 16:00:40 -0500 Subject: [PATCH 068/132] feat: update nestjs demo for create credentials --- examples/demo-dts/src/app.module.ts | 32 +++++--- .../demo-dts/src/common/enums/cmd.enum.ts | 4 + examples/demo-dts/src/common/enums/index.ts | 1 + examples/demo-dts/src/config/app.config.ts | 1 - examples/demo-dts/src/core.module.ts | 6 +- examples/demo-dts/src/core.service.ts | 73 +++++++++++++++++-- examples/demo-dts/src/i18n/en/msg.json | 5 ++ examples/demo-dts/src/i18n/es/msg.json | 5 ++ examples/demo-dts/yarn.lock | 54 +++++++++++--- 9 files changed, 147 insertions(+), 34 deletions(-) create mode 100644 examples/demo-dts/src/common/enums/cmd.enum.ts diff --git a/examples/demo-dts/src/app.module.ts b/examples/demo-dts/src/app.module.ts index 2e94198..ec5c292 100644 --- a/examples/demo-dts/src/app.module.ts +++ b/examples/demo-dts/src/app.module.ts @@ -4,7 +4,7 @@ import { ConfigModule } from '@nestjs/config' import appConfig from '@/config/app.config' import { AcceptLanguageResolver, HeaderResolver, I18nModule, QueryResolver } from 'nestjs-i18n' import * as path from 'path' -import { ConnectionsEventModule, MessageEventModule } from '@2060.io/service-agent-nestjs-client' +import { EventsModule } from '@2060.io/service-agent-nestjs-client' import { ApiVersion } from '@2060.io/service-agent-client' import { CoreModule } from '@/core.module' @@ -27,18 +27,26 @@ import { CoreModule } from '@/core.module' isGlobal: true, load: [appConfig], }), - MessageEventModule.forRoot({ - eventHandler: CoreService, - imports: [], - url: process.env.SERVICE_AGENT_ADMIN_URL, - version: ApiVersion.V1, - }), - ConnectionsEventModule.forRoot({ - eventHandler: CoreService, - imports: [], + EventsModule.register({ + modules: { + messages: true, + connections: true, + credentials: true, + }, + options: { + eventHandler: CoreService, + imports: [], + url: process.env.SERVICE_AGENT_ADMIN_URL, + version: ApiVersion.V1, + creds: { + name: 'demo dts', + version: '1.0', + attributes: ['fullName', 'issuanceDate'], + supportRevocation: true, + maximumCredentialNumber: 1000, + }, + }, }), ], - controllers: [], - providers: [CoreService], }) export class AppModule {} diff --git a/examples/demo-dts/src/common/enums/cmd.enum.ts b/examples/demo-dts/src/common/enums/cmd.enum.ts new file mode 100644 index 0000000..0488ebd --- /dev/null +++ b/examples/demo-dts/src/common/enums/cmd.enum.ts @@ -0,0 +1,4 @@ +export enum Cmd { + CREDENTIAL = '/credential', + REVOKE = '/revoke', +} diff --git a/examples/demo-dts/src/common/enums/index.ts b/examples/demo-dts/src/common/enums/index.ts index c91c18a..a1a7f85 100644 --- a/examples/demo-dts/src/common/enums/index.ts +++ b/examples/demo-dts/src/common/enums/index.ts @@ -1,2 +1,3 @@ +export * from './cmd.enum' export * from './menu-select.enum' export * from './state-step.enum' diff --git a/examples/demo-dts/src/config/app.config.ts b/examples/demo-dts/src/config/app.config.ts index 4cf947b..65a0031 100644 --- a/examples/demo-dts/src/config/app.config.ts +++ b/examples/demo-dts/src/config/app.config.ts @@ -1,4 +1,3 @@ -import { ApiVersion } from '@2060.io/service-agent-client' import { registerAs } from '@nestjs/config' /** diff --git a/examples/demo-dts/src/core.module.ts b/examples/demo-dts/src/core.module.ts index f180720..56929ad 100644 --- a/examples/demo-dts/src/core.module.ts +++ b/examples/demo-dts/src/core.module.ts @@ -2,13 +2,13 @@ import { Global, Module } from '@nestjs/common' import { SessionEntity } from '@/models' import { CoreService } from '@/core.service' import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm' -import { ConnectionEntity } from '@2060.io/service-agent-nestjs-client' +import { ConnectionEntity, CredentialEntity } from '@2060.io/service-agent-nestjs-client' import { ConfigModule, ConfigService } from '@nestjs/config' @Global() @Module({ imports: [ - TypeOrmModule.forFeature([ConnectionEntity, SessionEntity]), + TypeOrmModule.forFeature([ConnectionEntity, CredentialEntity, SessionEntity]), TypeOrmModule.forRootAsync({ imports: [ConfigModule], useFactory: async (configService: ConfigService): Promise => ({ @@ -18,7 +18,7 @@ import { ConfigModule, ConfigService } from '@nestjs/config' username: configService.get('appConfig.postgresUser'), password: configService.get('appConfig.postgresPassword'), database: configService.get('appConfig.postgresDbName'), - entities: [ConnectionEntity, SessionEntity], + entities: [ConnectionEntity, CredentialEntity, SessionEntity], synchronize: true, ssl: false, logging: false, diff --git a/examples/demo-dts/src/core.service.ts b/examples/demo-dts/src/core.service.ts index c96cbc1..5320c53 100644 --- a/examples/demo-dts/src/core.service.ts +++ b/examples/demo-dts/src/core.service.ts @@ -1,7 +1,9 @@ import { BaseMessage, ConnectionStateUpdated, + ContextualMenuItem, ContextualMenuSelectMessage, + ContextualMenuUpdateMessage, CredentialReceptionMessage, EMrtdDataSubmitMessage, MediaMessage, @@ -11,11 +13,11 @@ import { TextMessage, } from '@2060.io/service-agent-model' import { ApiClient, ApiVersion } from '@2060.io/service-agent-client' -import { EventHandler } from '@2060.io/service-agent-nestjs-client' +import { CredentialEventService, EventHandler } from '@2060.io/service-agent-nestjs-client' import { Injectable, Logger } from '@nestjs/common' import { SessionEntity } from './models' import { JsonTransformer } from '@credo-ts/core' -import { StateStep } from './common' +import { Cmd, StateStep } from './common' import { Repository } from 'typeorm' import { InjectRepository } from '@nestjs/typeorm' import { I18nService } from 'nestjs-i18n' @@ -31,6 +33,7 @@ export class CoreService implements EventHandler { private readonly sessionRepository: Repository, private readonly i18n: I18nService, private readonly configService: ConfigService, + private readonly credentialEvent: CredentialEventService, ) { const baseUrl = configService.get('appConfig.serviceAgentAdminUrl') this.apiClient = new ApiClient(baseUrl, ApiVersion.V1) @@ -102,7 +105,8 @@ export class CoreService implements EventHandler { * @param event - The event containing connection update details. */ async newConnection(event: ConnectionStateUpdated): Promise { - await this.handleSession(event.connectionId) + const session = await this.handleSession(event.connectionId) + await this.sendContextualMenu(session) } /** @@ -128,8 +132,8 @@ export class CoreService implements EventHandler { * metadata remain intact while cleaning up unnecessary or sensitive data. */ async closeConnection(event: ConnectionStateUpdated): Promise { - let session = await this.handleSession(event.connectionId) - session = await this.purgeUserData(session) + const session = await this.handleSession(event.connectionId) + await this.purgeUserData(session) } private async welcomeMessage(connectionId: string) { @@ -170,8 +174,24 @@ export class CoreService implements EventHandler { * @param selectionId - Identifier of the user's selection. * @param session - The current session associated with the message. */ - private handleContextualAction(selectionId: string, session: SessionEntity): Promise { - throw new Error('Function not implemented.') + private async handleContextualAction(selectionId: string, session: SessionEntity): Promise { + switch (session.state) { + case StateStep.START: + if (selectionId === Cmd.CREDENTIAL) { + const values = { + fullName: 'example', + issuanceDate: new Date().toISOString().split('T')[0], + } + await this.credentialEvent.issuance(session.connectionId, values, values.fullName) + } + if (selectionId === Cmd.REVOKE) { + await this.credentialEvent.revoke(session.connectionId) + } + break + default: + break + } + return await this.sessionRepository.save(session) } /** @@ -182,7 +202,11 @@ export class CoreService implements EventHandler { * @param session - The active session to update. */ private async handleStateInput(content: any, session: SessionEntity): Promise { - throw new Error('Function not implemented.') + try { + } catch (error) { + this.logger.error('handleStateInput: ' + error) + } + return await this.sendContextualMenu(session) } /** @@ -246,4 +270,37 @@ export class CoreService implements EventHandler { // Additional sensitive data can be reset here if needed. return await this.sessionRepository.save(session) } + + // send special flows + private async sendContextualMenu(session: SessionEntity): Promise { + const item: ContextualMenuItem[] = [] + switch (session.state) { + case StateStep.START: + item.push( + new ContextualMenuItem({ + id: Cmd.CREDENTIAL, + title: this.getText('CMD.CREDENTIAL', session.lang), + }), + ) + item.push( + new ContextualMenuItem({ + id: Cmd.REVOKE, + title: this.getText('CMD.REVOKE', session.lang), + }), + ) + break + default: + break + } + + await this.apiClient.messages.send( + new ContextualMenuUpdateMessage({ + title: this.getText('ROOT_TITTLE', session.lang), + connectionId: session.connectionId, + options: item, + timestamp: new Date(), + }), + ) + return await this.sessionRepository.save(session) + } } diff --git a/examples/demo-dts/src/i18n/en/msg.json b/examples/demo-dts/src/i18n/en/msg.json index b362ac6..0bb8eee 100644 --- a/examples/demo-dts/src/i18n/en/msg.json +++ b/examples/demo-dts/src/i18n/en/msg.json @@ -1,4 +1,9 @@ { "WELCOME":"Welcome to our demo chatbot", + "ROOT_TITTLE": "Context Menu", + "CMD": { + "CREDENTIAL":"New credential", + "REVOKE":"Revoke credential" + }, "HELP":"Check the contextual menu for available actions." } \ No newline at end of file diff --git a/examples/demo-dts/src/i18n/es/msg.json b/examples/demo-dts/src/i18n/es/msg.json index 172aff1..d612b3b 100644 --- a/examples/demo-dts/src/i18n/es/msg.json +++ b/examples/demo-dts/src/i18n/es/msg.json @@ -1,4 +1,9 @@ { "WELCOME":"Bienvenido a nuestro chatbot Demo", + "ROOT_TITTLE": "Menú Contextual", + "CMD": { + "CREDENTIAL":"Nueva credencial", + "REVOKE":"Revocar credencial" + }, "HELP":"Acceder al menu contextual para ver las opciones disponibles." } \ No newline at end of file diff --git a/examples/demo-dts/yarn.lock b/examples/demo-dts/yarn.lock index 3fe4a9d..bdd412d 100644 --- a/examples/demo-dts/yarn.lock +++ b/examples/demo-dts/yarn.lock @@ -2,13 +2,13 @@ # yarn lockfile v1 -"@2060.io/service-agent-model@file:../../packages/model/build": +"@2060.io/service-agent-client@file:../../packages/client/build": version "0.0.0" -"@2060.io/service-agent-nestjs-client@file:../../packages/nestjs-client/build": +"@2060.io/service-agent-model@file:../../packages/model/build": version "0.0.0" -"@2060.io/service-agent-client@file:../../packages/client/build": +"@2060.io/service-agent-nestjs-client@file:../../packages/nestjs-client/build": version "0.0.0" "@ampproject/remapping@^2.2.0": @@ -934,6 +934,11 @@ resolved "https://registry.yarnpkg.com/@lukeed/csprng/-/csprng-1.1.0.tgz#1e3e4bd05c1cc7a0b2ddbd8a03f39f6e4b5e6cfe" integrity sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA== +"@microsoft/tsdoc@^0.15.0": + version "0.15.1" + resolved "https://registry.yarnpkg.com/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz#d4f6937353bc4568292654efb0a0e0532adbcba2" + integrity sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw== + "@multiformats/base-x@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@multiformats/base-x/-/base-x-4.0.1.tgz#95ff0fa58711789d53aefb2590a8b7a4e715d121" @@ -999,6 +1004,11 @@ path-to-regexp "3.3.0" tslib "2.7.0" +"@nestjs/mapped-types@2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@nestjs/mapped-types/-/mapped-types-2.0.6.tgz#d2d8523709fd5d872a9b9e0c38162746e2a7f44e" + integrity sha512-84ze+CPfp1OWdpRi1/lOu59hOhTz38eVzJvRKrg9ykRFwDz+XleKfMsG0gUqNZYFa6v53XYzeD+xItt8uDW7NQ== + "@nestjs/platform-express@^10.0.0": version "10.4.6" resolved "https://registry.yarnpkg.com/@nestjs/platform-express/-/platform-express-10.4.6.tgz#6c39c522fa66036b4256714fea203fbeb49fc4de" @@ -1021,6 +1031,18 @@ jsonc-parser "3.3.1" pluralize "8.0.0" +"@nestjs/swagger@^8.0.7": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@nestjs/swagger/-/swagger-8.1.0.tgz#e55434053df454b5058ead94e33d94dcf4525e8f" + integrity sha512-8hzH+r/31XshzXHC9vww4T0xjDAxMzvOaT1xAOvvY1LtXTWyNRCUP2iQsCYJOnnMrR+vydWjvRZiuB3hdvaHxA== + dependencies: + "@microsoft/tsdoc" "^0.15.0" + "@nestjs/mapped-types" "2.0.6" + js-yaml "4.1.0" + lodash "4.17.21" + path-to-regexp "3.3.0" + swagger-ui-dist "5.18.2" + "@nestjs/testing@^10.0.0": version "10.4.6" resolved "https://registry.yarnpkg.com/@nestjs/testing/-/testing-10.4.6.tgz#3797a40c0628788e381f299d3c72acac364ca4ef" @@ -1244,6 +1266,11 @@ "@protokoll/core" "0.2.36" compare-versions "^6.1.1" +"@scarf/scarf@=1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@scarf/scarf/-/scarf-1.4.0.tgz#3bbb984085dbd6d982494538b523be1ce6562972" + integrity sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ== + "@sd-jwt/core@0.7.2", "@sd-jwt/core@^0.7.0": version "0.7.2" resolved "https://registry.yarnpkg.com/@sd-jwt/core/-/core-0.7.2.tgz#cfbcd853d507e2c31bf66ea5b2c1748291924ec3" @@ -4331,6 +4358,13 @@ js-base64@^3.7.6: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== +js-yaml@4.1.0, js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + js-yaml@^3.13.1: version "3.14.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" @@ -4339,13 +4373,6 @@ js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" -js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== - dependencies: - argparse "^2.0.1" - jsesc@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e" @@ -5825,6 +5852,13 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +swagger-ui-dist@5.18.2: + version "5.18.2" + resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-5.18.2.tgz#62013074374d272c04ed3030704b88db5aa8c0b7" + integrity sha512-J+y4mCw/zXh1FOj5wGJvnAajq6XgHOyywsa9yITmwxIlJbMqITq3gYRZHaeqLVH/eV/HOPphE6NjF+nbSNC5Zw== + dependencies: + "@scarf/scarf" "=1.4.0" + symbol-observable@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-4.0.0.tgz#5b425f192279e87f2f9b937ac8540d1984b39205" From 5545af5f55bb0b88c7f67a7444664527137c9222 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Fri, 3 Jan 2025 01:24:50 -0500 Subject: [PATCH 069/132] feat: implement credential reject --- .../src/credentials/credential.entity.ts | 2 +- .../src/credentials/credential.service.ts | 47 ++++++++++++------- .../src/messages/message.service.ts | 13 +++-- 3 files changed, 40 insertions(+), 22 deletions(-) diff --git a/packages/nestjs-client/src/credentials/credential.entity.ts b/packages/nestjs-client/src/credentials/credential.entity.ts index 28201d3..b22a9e8 100644 --- a/packages/nestjs-client/src/credentials/credential.entity.ts +++ b/packages/nestjs-client/src/credentials/credential.entity.ts @@ -26,7 +26,7 @@ export class CredentialEntity { @Column({ type: 'varchar', nullable: true }) hash?: string - @Column({ nullable: true }) + @Column({ nullable: true, default: false }) revoked?: boolean @CreateDateColumn() diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 65f599b..52f3ad4 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -98,13 +98,13 @@ export class CredentialEventService implements OnModuleInit { }) if (isRevoked) throw new Error('Please revoke the credential with the same data first.') - const { revocationDefinitionId, revocationRegistryIndex } = await this.entityManager.transaction( + const { revocationRegistryDefinitionId, revocationRegistryIndex } = await this.entityManager.transaction( async transaction => { const invalidRegistries = await transaction.find(CredentialEntity, { select: ['revocationDefinitionId'], where: { credentialDefinitionId, - revocationRegistryIndex: Equal(this.maximumCredentialNumber), + revocationRegistryIndex: Equal(this.maximumCredentialNumber - 1), }, }) const invalidRevocationIds = invalidRegistries.map(reg => reg.revocationDefinitionId) @@ -112,7 +112,7 @@ export class CredentialEventService implements OnModuleInit { let lastCred = await transaction.findOne(CredentialEntity, { where: { credentialDefinitionId, - revocationRegistryIndex: Not(Equal(0)), + revocationRegistryIndex: Not(Equal(this.maximumCredentialNumber)), ...(invalidRevocationIds.length > 0 ? { revocationDefinitionId: Not(In(invalidRevocationIds)), @@ -126,7 +126,7 @@ export class CredentialEventService implements OnModuleInit { lastCred = await transaction.findOne(CredentialEntity, { where: { credentialDefinitionId, - revocationRegistryIndex: Equal(0), + revocationRegistryIndex: Equal(this.maximumCredentialNumber), }, order: { createdTs: 'DESC' }, lock: { mode: 'pessimistic_write' }, @@ -136,6 +136,7 @@ export class CredentialEventService implements OnModuleInit { throw new Error( 'No valid registry definition found. Please restart the service and ensure the module is imported correctly', ) + lastCred.revocationRegistryIndex = -1 } const newCredential = await transaction.save(CredentialEntity, { @@ -147,7 +148,7 @@ export class CredentialEventService implements OnModuleInit { maximumCredentialNumber: lastCred.maximumCredentialNumber, }) return { - revocationDefinitionId: newCredential.revocationDefinitionId, + revocationRegistryDefinitionId: newCredential.revocationDefinitionId, revocationRegistryIndex: newCredential.revocationRegistryIndex, } }, @@ -157,12 +158,12 @@ export class CredentialEventService implements OnModuleInit { new CredentialIssuanceMessage({ connectionId, credentialDefinitionId, - revocationRegistryDefinitionId: revocationDefinitionId, - revocationRegistryIndex: revocationRegistryIndex, + revocationRegistryDefinitionId, + revocationRegistryIndex, claims: claims, }), ) - if (revocationRegistryIndex === this.maximumCredentialNumber) { + if (revocationRegistryIndex === this.maximumCredentialNumber - 1) { const revRegistry = await this.createRevocationRegistry(credentialDefinitionId) this.logger.log(`Revocation registry successfully created with ID ${revRegistry}`) } @@ -177,11 +178,16 @@ export class CredentialEventService implements OnModuleInit { */ async accept(connectionId: string, threadId: string): Promise { const cred = await this.credentialRepository.findOne({ - where: { connectionId: connectionId }, + where: { + connectionId, + revoked: false, + }, order: { createdTs: 'DESC' }, // TODO: improve the search method on differents revocation }) if (!cred) throw new Error(`Credential not found with connectionId: ${connectionId}`) - await this.credentialRepository.update(cred.id, { threadId }) + + cred.threadId = threadId + await this.credentialRepository.save(cred) } /** @@ -192,11 +198,16 @@ export class CredentialEventService implements OnModuleInit { */ async reject(connectionId: string, threadId: string): Promise { const cred = await this.credentialRepository.findOne({ - where: { connectionId: connectionId }, - order: { createdTs: 'DESC' }, // TODO: improve the search method on differents revocation + where: { + connectionId, + revoked: false, + }, }) if (!cred) throw new Error(`Credential not found with connectionId: ${connectionId}`) - await this.credentialRepository.update(cred.id, { threadId, revoked: true }) + + cred.threadId = threadId + cred.revoked = true + await this.credentialRepository.save(cred) } /** @@ -205,12 +216,16 @@ export class CredentialEventService implements OnModuleInit { * @throws Error if no credential is found with the specified thread ID or if the credential has no connection ID. */ async revoke(connectionId: string): Promise { - const cred = await this.credentialRepository.findOne({ where: { connectionId } }) + const cred = await this.credentialRepository.findOne({ + where: { connectionId }, + order: { createdTs: 'DESC' }, + }) if (!cred || !cred.connectionId) { throw new Error(`Credencial with threadId ${cred?.threadId} not found`) } - await this.credentialRepository.update(cred.id, { revoked: true }) + cred.revoked = true + await this.credentialRepository.save(cred) await this.apiClient.messages.send( new CredentialRevocationMessage({ connectionId: cred.connectionId, @@ -233,7 +248,7 @@ export class CredentialEventService implements OnModuleInit { const credentialRev = this.credentialRepository.create({ credentialDefinitionId, revocationDefinitionId: revocationRegistry, - revocationRegistryIndex: 0, + revocationRegistryIndex: this.maximumCredentialNumber, maximumCredentialNumber: this.maximumCredentialNumber, }) await this.credentialRepository.save(credentialRev) diff --git a/packages/nestjs-client/src/messages/message.service.ts b/packages/nestjs-client/src/messages/message.service.ts index 2f1b7b0..343968e 100644 --- a/packages/nestjs-client/src/messages/message.service.ts +++ b/packages/nestjs-client/src/messages/message.service.ts @@ -5,6 +5,7 @@ import { MessageReceived, ReceiptsMessage, } from '@2060.io/service-agent-model' +import { JsonTransformer } from '@credo-ts/core' import { Inject, Injectable, Logger, Optional } from '@nestjs/common' import { MessageState } from 'credo-ts-receipts' @@ -55,12 +56,14 @@ export class MessageEventService { await this.apiClient.messages.send(body) if (this.eventHandler) { - if (message instanceof CredentialReceptionMessage) { + if (message.type === CredentialReceptionMessage.type) { try { - const isCredentialDone = message.state === CredentialState.Done - if (this.credentialEvent && message.threadId) { - if (isCredentialDone) this.credentialEvent.accept(message.connectionId, message.threadId) - else this.credentialEvent.reject(message.connectionId, message.threadId) + const msg = JsonTransformer.fromJSON(message, CredentialReceptionMessage) + const isCredentialDone = msg.state === CredentialState.Done + if (this.credentialEvent) { + if (!msg.threadId) throw new Error('threadId is required for credential') + if (isCredentialDone) await this.credentialEvent.accept(msg.connectionId, msg.threadId) + else await this.credentialEvent.reject(msg.connectionId, msg.threadId) } } catch (error) { this.logger.error(`Cannot create the registry: ${error}`) From 156bdc0d95c783002f44961e40c02d315098f153 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Fri, 3 Jan 2025 08:39:48 -0500 Subject: [PATCH 070/132] fix: remove model types methods --- packages/model/src/types.ts | 2 -- packages/nestjs-client/src/messages/message.service.ts | 9 ++------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/packages/model/src/types.ts b/packages/model/src/types.ts index adab8cb..f696ca8 100644 --- a/packages/model/src/types.ts +++ b/packages/model/src/types.ts @@ -5,8 +5,6 @@ import type { ProofState, } from '@credo-ts/core' -export { CredentialState } from '@credo-ts/core' - import { Claim, ClaimOptions } from './messages/CredentialIssuanceMessage' export interface AgentInfo { diff --git a/packages/nestjs-client/src/messages/message.service.ts b/packages/nestjs-client/src/messages/message.service.ts index 343968e..ceb5c01 100644 --- a/packages/nestjs-client/src/messages/message.service.ts +++ b/packages/nestjs-client/src/messages/message.service.ts @@ -1,11 +1,6 @@ import { ApiClient, ApiVersion } from '@2060.io/service-agent-client' -import { - CredentialReceptionMessage, - CredentialState, - MessageReceived, - ReceiptsMessage, -} from '@2060.io/service-agent-model' -import { JsonTransformer } from '@credo-ts/core' +import { CredentialReceptionMessage, MessageReceived, ReceiptsMessage } from '@2060.io/service-agent-model' +import { CredentialState, JsonTransformer } from '@credo-ts/core' import { Inject, Injectable, Logger, Optional } from '@nestjs/common' import { MessageState } from 'credo-ts-receipts' From 29272a939ac4cd7ddd41ef906049abd2aaa64520 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Fri, 3 Jan 2025 09:01:44 -0500 Subject: [PATCH 071/132] feat: case control when hash exist --- .../src/credentials/credential.service.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 52f3ad4..f560ca2 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -48,6 +48,10 @@ export class CredentialEventService implements OnModuleInit { this.logger.debug(`Initialized with url: ${this.url}, version: ${this.apiVersion}`) } + /** + * When the module is instantiated, two default lists of possible revocation registries will be created. + * This is to ensure seamless handling in case one of the registries runs out of revocation capacity. + */ async onModuleInit() { const [credential] = await this.apiClient.credentialTypes.getAll() @@ -96,7 +100,11 @@ export class CredentialEventService implements OnModuleInit { revoked: false, }, }) - if (isRevoked) throw new Error('Please revoke the credential with the same data first.') + if (isRevoked) { + isRevoked.connectionId = connectionId + await this.credentialRepository.save(isRevoked) + throw new Error('Please revoke the credential with the same data first.') + } const { revocationRegistryDefinitionId, revocationRegistryIndex } = await this.entityManager.transaction( async transaction => { From 5881ea6fd55a684fa20c58303766fe0ced781594 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Fri, 3 Jan 2025 09:15:21 -0500 Subject: [PATCH 072/132] docs: update --- packages/nestjs-client/README.md | 53 +++++++++++++++----------------- 1 file changed, 24 insertions(+), 29 deletions(-) diff --git a/packages/nestjs-client/README.md b/packages/nestjs-client/README.md index 194f37d..3ad7f82 100644 --- a/packages/nestjs-client/README.md +++ b/packages/nestjs-client/README.md @@ -1,52 +1,47 @@ -# Nestjs-client - -## How to use the connections? -```node -@Module({ - imports: [ - ConnectionsEventModule.forRoot({ - messageHandler: CustomMessageHandler, // Class with input method - imports: [] - }) - ] -}) -export class AppModule {} -``` +# Nestjs-client for Service Agent +The `nestjs-client` library is designed to manage recurring components for chatbots developed using the [2060 Service Agent](../../README.md). Its modules follow a plug-and-play architecture, allowing developers to include them as needed. However, certain modules, such as credential management, recommend incorporating the message handling module to function correctly. +**Example of Using Independent Modules:** -## How to use messages -```node +```typescript @Module({ imports: [ MessageEventModule.forRoot({ messageHandler: CustomMessageHandler, // Class with input method - imports: [] + imports: [], url: 'http://sa-url.com', version: ApiVersion.V1, }), + CredentialManagementModule.forRoot({ + // Configuration options + }), ], }) - +export class AppModule {} ``` +**Example of Using the Recommended `EventsModule`:** -## How to use for main module -```node +```typescript @Module({ imports: [ EventsModule.register({ modules: { - messages: true, - connections: false, + messages: true, + credentials: true, }, options: { - eventHandler: CoreService, - imports: [], - url: process.env.SERVICE_AGENT_ADMIN_URL, - version: ApiVersion.V1, - } - }) + eventHandler: CoreService, + imports: [], + url: process.env.SERVICE_AGENT_ADMIN_URL, + version: ApiVersion.V1, + }, + }), ], }) +export class AppModule {} +``` + +In the first example, individual modules like `MessageEventModule` and `CredentialManagementModule` are configured separately. In the second example, the `EventsModule` is used to register multiple modules simultaneously, which is the recommended approach for streamlined configuration and better integration. -``` \ No newline at end of file +For more information on dynamic modules and their configuration in NestJS, refer to the official [documentation](https://docs.nestjs.com/fundamentals/dynamic-modules) \ No newline at end of file From 0a01eda8c1926c39333c1298ab9294926e2fa0e5 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Fri, 3 Jan 2025 15:40:35 -0500 Subject: [PATCH 073/132] fix: revoke when hash exist --- packages/nestjs-client/src/credentials/credential.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index f560ca2..3479df5 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -103,7 +103,7 @@ export class CredentialEventService implements OnModuleInit { if (isRevoked) { isRevoked.connectionId = connectionId await this.credentialRepository.save(isRevoked) - throw new Error('Please revoke the credential with the same data first.') + await this.revoke(connectionId) } const { revocationRegistryDefinitionId, revocationRegistryIndex } = await this.entityManager.transaction( From 43ff31e7ece3540b5a663d1b47930bd3df30fc09 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Fri, 3 Jan 2025 16:52:18 -0500 Subject: [PATCH 074/132] fix: revoke when hash exist --- .../nestjs-client/src/credentials/credential.service.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 3479df5..17eade8 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -100,11 +100,7 @@ export class CredentialEventService implements OnModuleInit { revoked: false, }, }) - if (isRevoked) { - isRevoked.connectionId = connectionId - await this.credentialRepository.save(isRevoked) - await this.revoke(connectionId) - } + if (isRevoked && isRevoked.connectionId) await this.revoke(isRevoked.connectionId) const { revocationRegistryDefinitionId, revocationRegistryIndex } = await this.entityManager.transaction( async transaction => { From 0b21552a0c29760b605b1ee516e94da85f49d2fd Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Fri, 3 Jan 2025 17:10:46 -0500 Subject: [PATCH 075/132] fix: revoke when hash exist --- .../nestjs-client/src/credentials/credential.service.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 17eade8..3479df5 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -100,7 +100,11 @@ export class CredentialEventService implements OnModuleInit { revoked: false, }, }) - if (isRevoked && isRevoked.connectionId) await this.revoke(isRevoked.connectionId) + if (isRevoked) { + isRevoked.connectionId = connectionId + await this.credentialRepository.save(isRevoked) + await this.revoke(connectionId) + } const { revocationRegistryDefinitionId, revocationRegistryIndex } = await this.entityManager.transaction( async transaction => { From 23dd7728edc639d266d226aabfa1c17ccd42dec1 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Fri, 10 Jan 2025 16:09:02 -0500 Subject: [PATCH 076/132] fix: update variable GLOBAL_MODULE_OPTIONS andname Credential Service --- packages/nestjs-client/src/app.module.ts | 4 ++-- .../src/credentials/credential.module.ts | 16 ++++++++-------- .../src/credentials/credential.service.ts | 8 ++++---- .../nestjs-client/src/messages/message.module.ts | 2 +- .../src/messages/message.service.ts | 16 ++++++++-------- packages/nestjs-client/src/types.ts | 2 +- 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/packages/nestjs-client/src/app.module.ts b/packages/nestjs-client/src/app.module.ts index 5c69986..0e6820b 100644 --- a/packages/nestjs-client/src/app.module.ts +++ b/packages/nestjs-client/src/app.module.ts @@ -1,7 +1,7 @@ import { Module, DynamicModule, Global } from '@nestjs/common' import { ConnectionsEventModule } from './connections' -import { CredentialEventModule } from './credentials' +import { CredentialModule } from './credentials' import { MessageEventModule } from './messages' import { EventsModuleOptions } from './types' @@ -34,7 +34,7 @@ export class EventsModule { if (modules.credentials) { imports.push( - CredentialEventModule.forRoot({ + CredentialModule.forRoot({ imports: moduleOptions.imports ?? [], url: moduleOptions.url, version: moduleOptions.version, diff --git a/packages/nestjs-client/src/credentials/credential.module.ts b/packages/nestjs-client/src/credentials/credential.module.ts index c2e748f..3382ca8 100644 --- a/packages/nestjs-client/src/credentials/credential.module.ts +++ b/packages/nestjs-client/src/credentials/credential.module.ts @@ -1,24 +1,24 @@ import { DynamicModule, Module } from '@nestjs/common' -import { CredentialEventOptions } from '../types' +import { CredentialOptions } from '../types' -import { CredentialEventService } from './credential.service' +import { CredentialService } from './credential.service' @Module({}) -export class CredentialEventModule { - static forRoot(options: CredentialEventOptions): DynamicModule { +export class CredentialModule { + static forRoot(options: CredentialOptions): DynamicModule { return { - module: CredentialEventModule, + module: CredentialModule, imports: options.imports, controllers: [], providers: [ - CredentialEventService, + CredentialService, { - provide: 'EVENT_MODULE_OPTIONS', + provide: 'GLOBAL_MODULE_OPTIONS', useValue: options, }, ], - exports: [CredentialEventService], + exports: [CredentialService], } } } diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 3479df5..b1cd1bb 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -5,13 +5,13 @@ import { Inject, Injectable, Logger, OnModuleInit } from '@nestjs/common' import { InjectRepository } from '@nestjs/typeorm' import { EntityManager, Equal, In, Not, Repository } from 'typeorm' -import { CredentialEventOptions } from '../types' +import { CredentialOptions } from '../types' import { CredentialEntity } from './credential.entity' @Injectable() -export class CredentialEventService implements OnModuleInit { - private readonly logger = new Logger(CredentialEventService.name) +export class CredentialService implements OnModuleInit { + private readonly logger = new Logger(CredentialService.name) // Service agent client API private readonly url: string @@ -28,7 +28,7 @@ export class CredentialEventService implements OnModuleInit { constructor( @InjectRepository(CredentialEntity) private readonly credentialRepository: Repository, - @Inject('EVENT_MODULE_OPTIONS') private options: CredentialEventOptions, + @Inject('GLOBAL_MODULE_OPTIONS') private options: CredentialOptions, private readonly entityManager: EntityManager, ) { if (!options.url) throw new Error(`For this module to be used the value url must be added`) diff --git a/packages/nestjs-client/src/messages/message.module.ts b/packages/nestjs-client/src/messages/message.module.ts index be00a6f..37be5f9 100644 --- a/packages/nestjs-client/src/messages/message.module.ts +++ b/packages/nestjs-client/src/messages/message.module.ts @@ -18,7 +18,7 @@ export class MessageEventModule { providers: [ MessageEventService, { - provide: 'EVENT_MODULE_OPTIONS', + provide: 'GLOBAL_MODULE_OPTIONS', useValue: options, }, { diff --git a/packages/nestjs-client/src/messages/message.service.ts b/packages/nestjs-client/src/messages/message.service.ts index ceb5c01..7bc6770 100644 --- a/packages/nestjs-client/src/messages/message.service.ts +++ b/packages/nestjs-client/src/messages/message.service.ts @@ -4,7 +4,7 @@ import { CredentialState, JsonTransformer } from '@credo-ts/core' import { Inject, Injectable, Logger, Optional } from '@nestjs/common' import { MessageState } from 'credo-ts-receipts' -import { CredentialEventService } from '../credentials' +import { CredentialService } from '../credentials' import { EventHandler } from '../interfaces' import { MessageEventOptions } from '../types' @@ -16,17 +16,17 @@ export class MessageEventService { private readonly apiClient: ApiClient constructor( - @Inject('EVENT_MODULE_OPTIONS') private options: MessageEventOptions, + @Inject('GLOBAL_MODULE_OPTIONS') private options: MessageEventOptions, @Optional() @Inject('MESSAGE_EVENT') private eventHandler?: EventHandler, - @Optional() @Inject() private credentialEvent?: CredentialEventService, + @Optional() @Inject() private credentialService?: CredentialService, ) { if (!options.url) throw new Error(`For this module to be used the value url must be added`) this.url = options.url this.version = options.version ?? ApiVersion.V1 - if (!credentialEvent) + if (!credentialService) this.logger.warn( - `To handle credential events and their revocation, make sure to initialize the CredentialEventModule.`, + `To handle credential events and their revocation, make sure to initialize the CredentialService.`, ) this.apiClient = new ApiClient(this.url, this.version) @@ -55,10 +55,10 @@ export class MessageEventService { try { const msg = JsonTransformer.fromJSON(message, CredentialReceptionMessage) const isCredentialDone = msg.state === CredentialState.Done - if (this.credentialEvent) { + if (this.credentialService) { if (!msg.threadId) throw new Error('threadId is required for credential') - if (isCredentialDone) await this.credentialEvent.accept(msg.connectionId, msg.threadId) - else await this.credentialEvent.reject(msg.connectionId, msg.threadId) + if (isCredentialDone) await this.credentialService.accept(msg.connectionId, msg.threadId) + else await this.credentialService.reject(msg.connectionId, msg.threadId) } } catch (error) { this.logger.error(`Cannot create the registry: ${error}`) diff --git a/packages/nestjs-client/src/types.ts b/packages/nestjs-client/src/types.ts index 3e08117..bfa2aab 100644 --- a/packages/nestjs-client/src/types.ts +++ b/packages/nestjs-client/src/types.ts @@ -15,7 +15,7 @@ export interface ConnectionEventOptions { imports?: DynamicModule[] } -export interface CredentialEventOptions { +export interface CredentialOptions { imports?: DynamicModule[] url?: string version?: ApiVersion From 0803f74ea37f6e156c420b1081799ebda198f95a Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Mon, 13 Jan 2025 08:54:27 -0500 Subject: [PATCH 077/132] fix: remove maximumCredentialNumber --- .../src/credentials/credential.entity.ts | 11 ++++------- .../src/credentials/credential.service.ts | 5 ++--- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/nestjs-client/src/credentials/credential.entity.ts b/packages/nestjs-client/src/credentials/credential.entity.ts index b22a9e8..6e33cec 100644 --- a/packages/nestjs-client/src/credentials/credential.entity.ts +++ b/packages/nestjs-client/src/credentials/credential.entity.ts @@ -8,14 +8,11 @@ export class CredentialEntity { @Column({ type: 'varchar', nullable: false }) credentialDefinitionId!: string - @Column({ type: 'varchar', nullable: false }) - revocationDefinitionId!: string - - @Column({ type: 'integer', nullable: false }) - revocationRegistryIndex!: number + @Column({ type: 'varchar', nullable: true }) + revocationDefinitionId?: string - @Column({ type: 'integer', nullable: false }) - maximumCredentialNumber!: number + @Column({ type: 'integer', nullable: true }) + revocationRegistryIndex?: number @Column({ type: 'varchar', nullable: true }) connectionId?: string diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index b1cd1bb..2079f32 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -130,7 +130,7 @@ export class CredentialService implements OnModuleInit { order: { revocationRegistryIndex: 'DESC' }, lock: { mode: 'pessimistic_write' }, }) - if (!lastCred) { + if (!lastCred || !lastCred.revocationRegistryIndex) { lastCred = await transaction.findOne(CredentialEntity, { where: { credentialDefinitionId, @@ -153,7 +153,7 @@ export class CredentialService implements OnModuleInit { revocationDefinitionId: lastCred.revocationDefinitionId, revocationRegistryIndex: lastCred.revocationRegistryIndex + 1, hash: Buffer.from(new Sha256().hash(hash)).toString('hex'), - maximumCredentialNumber: lastCred.maximumCredentialNumber, + maximumCredentialNumber: this.maximumCredentialNumber, }) return { revocationRegistryDefinitionId: newCredential.revocationDefinitionId, @@ -257,7 +257,6 @@ export class CredentialService implements OnModuleInit { credentialDefinitionId, revocationDefinitionId: revocationRegistry, revocationRegistryIndex: this.maximumCredentialNumber, - maximumCredentialNumber: this.maximumCredentialNumber, }) await this.credentialRepository.save(credentialRev) return revocationRegistry From a82955c23d78a75a34df5df4ef22abeecf66cb1e Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Mon, 13 Jan 2025 10:47:21 -0500 Subject: [PATCH 078/132] fix: change name hash to hashIdentifier --- .../nestjs-client/src/credentials/credential.entity.ts | 2 +- .../nestjs-client/src/credentials/credential.service.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/nestjs-client/src/credentials/credential.entity.ts b/packages/nestjs-client/src/credentials/credential.entity.ts index 6e33cec..c8f288e 100644 --- a/packages/nestjs-client/src/credentials/credential.entity.ts +++ b/packages/nestjs-client/src/credentials/credential.entity.ts @@ -21,7 +21,7 @@ export class CredentialEntity { threadId?: string @Column({ type: 'varchar', nullable: true }) - hash?: string + hashIdentifier?: string @Column({ nullable: true, default: false }) revoked?: boolean diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 2079f32..1f1e5c0 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -89,14 +89,14 @@ export class CredentialService implements OnModuleInit { * @returns {Promise} A promise that resolves when the credential issuance is successfully * sent. If an error occurs during the process, the promise will be rejected. */ - async issuance(connectionId: string, records: Record, hash: string): Promise { + async issuance(connectionId: string, records: Record, hashIdentifier: string): Promise { const [{ id: credentialDefinitionId }] = await this.apiClient.credentialTypes.getAll() const claims = Object.entries(records).map( ([key, value]) => new Claim({ name: key, value: value ?? null }), ) const isRevoked = await this.credentialRepository.findOne({ where: { - hash: Equal(Buffer.from(new Sha256().hash(hash)).toString('hex')), + hashIdentifier: Equal(Buffer.from(new Sha256().hash(hashIdentifier)).toString('hex')), revoked: false, }, }) @@ -152,7 +152,7 @@ export class CredentialService implements OnModuleInit { credentialDefinitionId, revocationDefinitionId: lastCred.revocationDefinitionId, revocationRegistryIndex: lastCred.revocationRegistryIndex + 1, - hash: Buffer.from(new Sha256().hash(hash)).toString('hex'), + hashIdentifier: Buffer.from(new Sha256().hash(hashIdentifier)).toString('hex'), maximumCredentialNumber: this.maximumCredentialNumber, }) return { @@ -234,7 +234,7 @@ export class CredentialService implements OnModuleInit { cred.revoked = true await this.credentialRepository.save(cred) - await this.apiClient.messages.send( + this.supportRevocation && await this.apiClient.messages.send( new CredentialRevocationMessage({ connectionId: cred.connectionId, threadId: cred?.threadId, From e6a388e20c2e2abc197aede5af3ec435b30c43fd Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Mon, 13 Jan 2025 12:20:01 -0500 Subject: [PATCH 079/132] fix: create registry only when is need --- .../src/credentials/credential.service.ts | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 1f1e5c0..d957415 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -108,6 +108,17 @@ export class CredentialService implements OnModuleInit { const { revocationRegistryDefinitionId, revocationRegistryIndex } = await this.entityManager.transaction( async transaction => { + if (!this.supportRevocation) { + await transaction.save(CredentialEntity, { + connectionId, + credentialDefinitionId, + hashIdentifier: Buffer.from(new Sha256().hash(hashIdentifier)).toString('hex'), + }) + return { + revocationRegistryDefinitionId: undefined, + revocationRegistryIndex: undefined, + } + } const invalidRegistries = await transaction.find(CredentialEntity, { select: ['revocationDefinitionId'], where: { @@ -234,17 +245,22 @@ export class CredentialService implements OnModuleInit { cred.revoked = true await this.credentialRepository.save(cred) - this.supportRevocation && await this.apiClient.messages.send( - new CredentialRevocationMessage({ - connectionId: cred.connectionId, - threadId: cred?.threadId, - }), - ) + this.supportRevocation && + (await this.apiClient.messages.send( + new CredentialRevocationMessage({ + connectionId: cred.connectionId, + threadId: cred?.threadId, + }), + )) this.logger.log(`Revoke Credential: ${cred.id}`) } // private methods - private async createRevocationRegistry(credentialDefinitionId: string): Promise { + private async createRevocationRegistry(credentialDefinitionId: string): Promise { + if (!this.supportRevocation) { + await this.credentialRepository.save({ credentialDefinitionId }) + return null + } const revocationRegistry = await this.apiClient.revocationRegistry.create({ credentialDefinitionId, maximumCredentialNumber: this.maximumCredentialNumber, @@ -253,12 +269,11 @@ export class CredentialService implements OnModuleInit { throw new Error( `Unable to create a new revocation registry for CredentialDefinitionId: ${credentialDefinitionId}`, ) - const credentialRev = this.credentialRepository.create({ + await this.credentialRepository.save({ credentialDefinitionId, revocationDefinitionId: revocationRegistry, revocationRegistryIndex: this.maximumCredentialNumber, }) - await this.credentialRepository.save(credentialRev) return revocationRegistry } } From a7640b2209a883ffc5092bf72c3eec7c5c7b43cc Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Mon, 13 Jan 2025 17:16:53 -0500 Subject: [PATCH 080/132] fix: accept multiplex credential --- packages/nestjs-client/src/app.module.ts | 1 - .../src/credentials/credential.service.ts | 99 +++++++++++++------ packages/nestjs-client/src/types.ts | 14 --- 3 files changed, 69 insertions(+), 45 deletions(-) diff --git a/packages/nestjs-client/src/app.module.ts b/packages/nestjs-client/src/app.module.ts index 0e6820b..236392a 100644 --- a/packages/nestjs-client/src/app.module.ts +++ b/packages/nestjs-client/src/app.module.ts @@ -38,7 +38,6 @@ export class EventsModule { imports: moduleOptions.imports ?? [], url: moduleOptions.url, version: moduleOptions.version, - creds: moduleOptions.creds, }), ) } diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index d957415..42ac1cd 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -1,7 +1,7 @@ import { ApiClient, ApiVersion } from '@2060.io/service-agent-client' import { Claim, CredentialIssuanceMessage, CredentialRevocationMessage } from '@2060.io/service-agent-model' import { Sha256, utils } from '@credo-ts/core' -import { Inject, Injectable, Logger, OnModuleInit } from '@nestjs/common' +import { Inject, Injectable, Logger } from '@nestjs/common' import { InjectRepository } from '@nestjs/typeorm' import { EntityManager, Equal, In, Not, Repository } from 'typeorm' @@ -10,7 +10,7 @@ import { CredentialOptions } from '../types' import { CredentialEntity } from './credential.entity' @Injectable() -export class CredentialService implements OnModuleInit { +export class CredentialService { private readonly logger = new Logger(CredentialService.name) // Service agent client API @@ -19,11 +19,11 @@ export class CredentialService implements OnModuleInit { private readonly apiClient: ApiClient //Credential type definitions - private readonly name: string - private readonly version: string - private readonly attributes: string[] - private readonly supportRevocation: boolean - private readonly maximumCredentialNumber: number + private name: string = 'Chatbot' + private version: string = '1.0' + private supportRevocation: boolean = false + private maximumCredentialNumber: number = 1000 + private autoRevocationEnabled: boolean = false constructor( @InjectRepository(CredentialEntity) @@ -34,33 +34,55 @@ export class CredentialService implements OnModuleInit { if (!options.url) throw new Error(`For this module to be used the value url must be added`) this.url = options.url this.apiVersion = options.version ?? ApiVersion.V1 - - if (!options.creds?.attributes) - throw new Error(`For this module to be used, the parameter credential types must be added`) - this.name = options.creds?.name ?? 'Chatbot' - this.version = options.creds?.version ?? '1.0' - this.attributes = options.creds?.attributes - this.supportRevocation = options.creds?.supportRevocation ?? false - this.maximumCredentialNumber = options.creds?.maximumCredentialNumber ?? 1000 - this.apiClient = new ApiClient(this.url, this.apiVersion) this.logger.debug(`Initialized with url: ${this.url}, version: ${this.apiVersion}`) } /** - * When the module is instantiated, two default lists of possible revocation registries will be created. - * This is to ensure seamless handling in case one of the registries runs out of revocation capacity. + * Ensures the creation and initialization of a credential definition with support for revocation registries. + * + * When this method is called: + * - Two default lists of possible revocation registries will be created. (only with supportRevocation) + * - This guarantees seamless handling in case one of the registries runs out of revocation capacity. (only with supportRevocation) + * + * **Usage Recommendation:** + * It is recommended to call this method inside an `OnModuleInit` lifecycle hook to validate the existence + * of credentials as soon as the project is initialized. + * + * @param name - The name of the credential definition. Defaults to "Chatbot" if not provided. + * @param version - The version of the credential definition. Defaults to "1.0" if not provided. + * @param attributes - A list of attributes that the credential definition supports. + * @param supportRevocation - Whether revocation is supported. Defaults to `false` if not provided. + * @param maximumCredentialNumber - The maximum number of credentials. Defaults to `1000` if not provided. + * + * @returns A promise that resolves when the credential definition and revocation registries are created. */ - async onModuleInit() { + async create( + attributes: string[], + options: { + name?: string + version?: string + supportRevocation?: boolean + maximumCredentialNumber?: number + autoRevocationEnabled?: boolean + } = {}, + ) { + const { name, version, supportRevocation, maximumCredentialNumber, autoRevocationEnabled } = options + const [credential] = await this.apiClient.credentialTypes.getAll() + if (name !== undefined) this.name = name + if (version !== undefined) this.version = version + if (supportRevocation !== undefined) this.supportRevocation = supportRevocation + if (maximumCredentialNumber !== undefined) this.maximumCredentialNumber = maximumCredentialNumber + if (autoRevocationEnabled !== undefined) this.autoRevocationEnabled = autoRevocationEnabled if (!credential) { const credential = await this.apiClient.credentialTypes.create({ id: utils.uuid(), name: this.name, version: this.version, - attributes: this.attributes, + attributes, supportRevocation: this.supportRevocation, }) @@ -89,20 +111,33 @@ export class CredentialService implements OnModuleInit { * @returns {Promise} A promise that resolves when the credential issuance is successfully * sent. If an error occurs during the process, the promise will be rejected. */ - async issuance(connectionId: string, records: Record, hashIdentifier: string): Promise { - const [{ id: credentialDefinitionId }] = await this.apiClient.credentialTypes.getAll() + async issuance( + connectionId: string, + records: Record, + options?: { identifier?: string }, + ): Promise { + const { identifier = null } = options || {} + const hashIdentifier = identifier ? this.hashIdentifier(identifier) : null + const credentials = await this.apiClient.credentialTypes.getAll() + if (!credentials || credentials.length === 0) { + throw new Error( + 'No credential definitions found. Please configure a credential using the create method before proceeding.', + ) + } + const [{ id: credentialDefinitionId }] = credentials + const claims = Object.entries(records).map( ([key, value]) => new Claim({ name: key, value: value ?? null }), ) - const isRevoked = await this.credentialRepository.findOne({ + const cred = await this.credentialRepository.findOne({ where: { - hashIdentifier: Equal(Buffer.from(new Sha256().hash(hashIdentifier)).toString('hex')), revoked: false, + ...(hashIdentifier ? { hashIdentifier } : {}), }, }) - if (isRevoked) { - isRevoked.connectionId = connectionId - await this.credentialRepository.save(isRevoked) + if (cred && this.autoRevocationEnabled) { + cred.connectionId = connectionId + await this.credentialRepository.save(cred) await this.revoke(connectionId) } @@ -112,7 +147,7 @@ export class CredentialService implements OnModuleInit { await transaction.save(CredentialEntity, { connectionId, credentialDefinitionId, - hashIdentifier: Buffer.from(new Sha256().hash(hashIdentifier)).toString('hex'), + ...(hashIdentifier ? { hashIdentifier } : {}), }) return { revocationRegistryDefinitionId: undefined, @@ -163,7 +198,7 @@ export class CredentialService implements OnModuleInit { credentialDefinitionId, revocationDefinitionId: lastCred.revocationDefinitionId, revocationRegistryIndex: lastCred.revocationRegistryIndex + 1, - hashIdentifier: Buffer.from(new Sha256().hash(hashIdentifier)).toString('hex'), + ...(hashIdentifier ? { hashIdentifier } : {}), maximumCredentialNumber: this.maximumCredentialNumber, }) return { @@ -201,7 +236,7 @@ export class CredentialService implements OnModuleInit { connectionId, revoked: false, }, - order: { createdTs: 'DESC' }, // TODO: improve the search method on differents revocation + order: { createdTs: 'DESC' }, }) if (!cred) throw new Error(`Credential not found with connectionId: ${connectionId}`) @@ -276,4 +311,8 @@ export class CredentialService implements OnModuleInit { }) return revocationRegistry } + + private hashIdentifier(identifier: string): string { + return Buffer.from(new Sha256().hash(identifier)).toString('hex') + } } diff --git a/packages/nestjs-client/src/types.ts b/packages/nestjs-client/src/types.ts index bfa2aab..4c60d25 100644 --- a/packages/nestjs-client/src/types.ts +++ b/packages/nestjs-client/src/types.ts @@ -19,13 +19,6 @@ export interface CredentialOptions { imports?: DynamicModule[] url?: string version?: ApiVersion - creds?: { - name?: string - version?: string - attributes?: string[] - supportRevocation?: boolean - maximumCredentialNumber?: number - } } export interface ModulesConfig { @@ -41,12 +34,5 @@ export interface EventsModuleOptions { imports?: DynamicModule[] url?: string version?: ApiVersion - creds?: { - name?: string - version?: string - attributes?: string[] - supportRevocation?: boolean - maximumCredentialNumber?: number - } } } From b5c46488827e835230b71367e166917309e50206 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Mon, 13 Jan 2025 17:30:59 -0500 Subject: [PATCH 081/132] fix: update nestjs client --- examples/demo-dts/src/app.module.ts | 7 ------- examples/demo-dts/src/core.service.ts | 20 ++++++++++++++------ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/examples/demo-dts/src/app.module.ts b/examples/demo-dts/src/app.module.ts index ec5c292..798ea45 100644 --- a/examples/demo-dts/src/app.module.ts +++ b/examples/demo-dts/src/app.module.ts @@ -38,13 +38,6 @@ import { CoreModule } from '@/core.module' imports: [], url: process.env.SERVICE_AGENT_ADMIN_URL, version: ApiVersion.V1, - creds: { - name: 'demo dts', - version: '1.0', - attributes: ['fullName', 'issuanceDate'], - supportRevocation: true, - maximumCredentialNumber: 1000, - }, }, }), ], diff --git a/examples/demo-dts/src/core.service.ts b/examples/demo-dts/src/core.service.ts index 5320c53..c660692 100644 --- a/examples/demo-dts/src/core.service.ts +++ b/examples/demo-dts/src/core.service.ts @@ -13,8 +13,8 @@ import { TextMessage, } from '@2060.io/service-agent-model' import { ApiClient, ApiVersion } from '@2060.io/service-agent-client' -import { CredentialEventService, EventHandler } from '@2060.io/service-agent-nestjs-client' -import { Injectable, Logger } from '@nestjs/common' +import { CredentialService, EventHandler } from '@2060.io/service-agent-nestjs-client' +import { Injectable, Logger, OnModuleInit } from '@nestjs/common' import { SessionEntity } from './models' import { JsonTransformer } from '@credo-ts/core' import { Cmd, StateStep } from './common' @@ -24,7 +24,7 @@ import { I18nService } from 'nestjs-i18n' import { ConfigService } from '@nestjs/config' @Injectable() -export class CoreService implements EventHandler { +export class CoreService implements EventHandler, OnModuleInit { private readonly apiClient: ApiClient private readonly logger = new Logger(CoreService.name) @@ -33,12 +33,20 @@ export class CoreService implements EventHandler { private readonly sessionRepository: Repository, private readonly i18n: I18nService, private readonly configService: ConfigService, - private readonly credentialEvent: CredentialEventService, + private readonly credentialService: CredentialService, ) { const baseUrl = configService.get('appConfig.serviceAgentAdminUrl') this.apiClient = new ApiClient(baseUrl, ApiVersion.V1) } + async onModuleInit() { + await this.credentialService.create(['fullName', 'issuanceDate'], { + name: 'demo dts', + supportRevocation: true, + maximumCredentialNumber: 1000, + }) + } + /** * Handles incoming messages and manages the input flow. * Routes the message to the appropriate handler based on its type. @@ -182,10 +190,10 @@ export class CoreService implements EventHandler { fullName: 'example', issuanceDate: new Date().toISOString().split('T')[0], } - await this.credentialEvent.issuance(session.connectionId, values, values.fullName) + await this.credentialService.issuance(session.connectionId, values, { identifier: values.fullName }) } if (selectionId === Cmd.REVOKE) { - await this.credentialEvent.revoke(session.connectionId) + await this.credentialService.revoke(session.connectionId) } break default: From 688cbe76f9c51943b6a43f03288f47c940b905ab Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Mon, 13 Jan 2025 18:36:05 -0500 Subject: [PATCH 082/132] fix: update nestjs client --- examples/demo-dts/src/core.service.ts | 3 ++- packages/nestjs-client/src/credentials/credential.service.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/demo-dts/src/core.service.ts b/examples/demo-dts/src/core.service.ts index c660692..d6f2520 100644 --- a/examples/demo-dts/src/core.service.ts +++ b/examples/demo-dts/src/core.service.ts @@ -43,7 +43,8 @@ export class CoreService implements EventHandler, OnModuleInit { await this.credentialService.create(['fullName', 'issuanceDate'], { name: 'demo dts', supportRevocation: true, - maximumCredentialNumber: 1000, + maximumCredentialNumber: 5, + autoRevocationEnabled: true, }) } diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 42ac1cd..60f761f 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -176,7 +176,7 @@ export class CredentialService { order: { revocationRegistryIndex: 'DESC' }, lock: { mode: 'pessimistic_write' }, }) - if (!lastCred || !lastCred.revocationRegistryIndex) { + if (!lastCred || lastCred.revocationRegistryIndex == null) { lastCred = await transaction.findOne(CredentialEntity, { where: { credentialDefinitionId, From 219c8cf1bbb6a55971469cd9bd7cc0167380d51e Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Tue, 14 Jan 2025 01:07:17 -0500 Subject: [PATCH 083/132] fix: update errors --- packages/nestjs-client/src/credentials/credential.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 60f761f..909e74d 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -257,7 +257,7 @@ export class CredentialService { revoked: false, }, }) - if (!cred) throw new Error(`Credential not found with connectionId: ${connectionId}`) + if (!cred) throw new Error(`Credencial with connectionId ${connectionId} not found.`) cred.threadId = threadId cred.revoked = true @@ -275,7 +275,7 @@ export class CredentialService { order: { createdTs: 'DESC' }, }) if (!cred || !cred.connectionId) { - throw new Error(`Credencial with threadId ${cred?.threadId} not found`) + throw new Error(`Credencial with connectionId ${connectionId} not found.`) } cred.revoked = true From 7e7086d4de09dc649a445f926c1499497cdcd0e7 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Tue, 14 Jan 2025 10:42:22 -0500 Subject: [PATCH 084/132] fix: ROOT_TITLE --- examples/demo-dts/src/core.service.ts | 2 +- examples/demo-dts/src/i18n/en/msg.json | 2 +- examples/demo-dts/src/i18n/es/msg.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/demo-dts/src/core.service.ts b/examples/demo-dts/src/core.service.ts index d6f2520..710802d 100644 --- a/examples/demo-dts/src/core.service.ts +++ b/examples/demo-dts/src/core.service.ts @@ -304,7 +304,7 @@ export class CoreService implements EventHandler, OnModuleInit { await this.apiClient.messages.send( new ContextualMenuUpdateMessage({ - title: this.getText('ROOT_TITTLE', session.lang), + title: this.getText('ROOT_TITLE', session.lang), connectionId: session.connectionId, options: item, timestamp: new Date(), diff --git a/examples/demo-dts/src/i18n/en/msg.json b/examples/demo-dts/src/i18n/en/msg.json index 0bb8eee..a82f45e 100644 --- a/examples/demo-dts/src/i18n/en/msg.json +++ b/examples/demo-dts/src/i18n/en/msg.json @@ -1,6 +1,6 @@ { "WELCOME":"Welcome to our demo chatbot", - "ROOT_TITTLE": "Context Menu", + "ROOT_TITLE": "Context Menu", "CMD": { "CREDENTIAL":"New credential", "REVOKE":"Revoke credential" diff --git a/examples/demo-dts/src/i18n/es/msg.json b/examples/demo-dts/src/i18n/es/msg.json index d612b3b..758918b 100644 --- a/examples/demo-dts/src/i18n/es/msg.json +++ b/examples/demo-dts/src/i18n/es/msg.json @@ -1,6 +1,6 @@ { "WELCOME":"Bienvenido a nuestro chatbot Demo", - "ROOT_TITTLE": "Menú Contextual", + "ROOT_TITLE": "Menú Contextual", "CMD": { "CREDENTIAL":"Nueva credencial", "REVOKE":"Revocar credencial" From 22d3752561c87a9198b3c5f3f6e4c93e4969f348 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Tue, 14 Jan 2025 10:43:01 -0500 Subject: [PATCH 085/132] fix: revoke credential --- .../src/credentials/credential.service.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 909e74d..b9b754d 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -116,8 +116,7 @@ export class CredentialService { records: Record, options?: { identifier?: string }, ): Promise { - const { identifier = null } = options || {} - const hashIdentifier = identifier ? this.hashIdentifier(identifier) : null + const hashIdentifier = options?.identifier ? this.hashIdentifier(options.identifier) : null const credentials = await this.apiClient.credentialTypes.getAll() if (!credentials || credentials.length === 0) { throw new Error( @@ -138,7 +137,7 @@ export class CredentialService { if (cred && this.autoRevocationEnabled) { cred.connectionId = connectionId await this.credentialRepository.save(cred) - await this.revoke(connectionId) + await this.revoke(connectionId, { identifier: options?.identifier ?? undefined }) } const { revocationRegistryDefinitionId, revocationRegistryIndex } = await this.entityManager.transaction( @@ -256,6 +255,7 @@ export class CredentialService { connectionId, revoked: false, }, + order: { createdTs: 'DESC' }, }) if (!cred) throw new Error(`Credencial with connectionId ${connectionId} not found.`) @@ -269,9 +269,10 @@ export class CredentialService { * @param threadId - The thread ID linked to the credential to revoke. * @throws Error if no credential is found with the specified thread ID or if the credential has no connection ID. */ - async revoke(connectionId: string): Promise { + async revoke(connectionId: string, options?: { identifier?: string }): Promise { + const hashIdentifier = options?.identifier ? this.hashIdentifier(options.identifier) : null const cred = await this.credentialRepository.findOne({ - where: { connectionId }, + where: { connectionId, revoked: false, ...(hashIdentifier ? { hashIdentifier } : {}) }, order: { createdTs: 'DESC' }, }) if (!cred || !cred.connectionId) { From 6da6cf4e8580e93ceec64ddcbdfcd7a18f39dc1a Mon Sep 17 00:00:00 2001 From: andresfv95 Date: Wed, 15 Jan 2025 14:18:55 -0500 Subject: [PATCH 086/132] fix: Update packages/nestjs-client/src/credentials/credential.service.ts Co-authored-by: Ariel Gentile --- packages/nestjs-client/src/credentials/credential.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index b9b754d..0c88bfd 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -111,7 +111,7 @@ export class CredentialService { * @returns {Promise} A promise that resolves when the credential issuance is successfully * sent. If an error occurs during the process, the promise will be rejected. */ - async issuance( + async issue( connectionId: string, records: Record, options?: { identifier?: string }, From 189c2928dce8b8f279343e77945e409ea1a1491a Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Wed, 15 Jan 2025 14:19:55 -0500 Subject: [PATCH 087/132] fix: issue --- examples/demo-dts/src/core.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/demo-dts/src/core.service.ts b/examples/demo-dts/src/core.service.ts index 710802d..3b51749 100644 --- a/examples/demo-dts/src/core.service.ts +++ b/examples/demo-dts/src/core.service.ts @@ -191,7 +191,7 @@ export class CoreService implements EventHandler, OnModuleInit { fullName: 'example', issuanceDate: new Date().toISOString().split('T')[0], } - await this.credentialService.issuance(session.connectionId, values, { identifier: values.fullName }) + await this.credentialService.issue(session.connectionId, values, { identifier: values.fullName }) } if (selectionId === Cmd.REVOKE) { await this.credentialService.revoke(session.connectionId) From 29abfc048ae8d1d9f35eff4c46f9fd59db195a06 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Wed, 15 Jan 2025 14:28:34 -0500 Subject: [PATCH 088/132] fix: added claims on issue method --- examples/demo-dts/src/core.service.ts | 11 ++++++----- .../src/credentials/credential.service.ts | 5 +---- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/examples/demo-dts/src/core.service.ts b/examples/demo-dts/src/core.service.ts index 3b51749..7069f3a 100644 --- a/examples/demo-dts/src/core.service.ts +++ b/examples/demo-dts/src/core.service.ts @@ -1,5 +1,6 @@ import { BaseMessage, + Claim, ConnectionStateUpdated, ContextualMenuItem, ContextualMenuSelectMessage, @@ -187,11 +188,11 @@ export class CoreService implements EventHandler, OnModuleInit { switch (session.state) { case StateStep.START: if (selectionId === Cmd.CREDENTIAL) { - const values = { - fullName: 'example', - issuanceDate: new Date().toISOString().split('T')[0], - } - await this.credentialService.issue(session.connectionId, values, { identifier: values.fullName }) + const claims = [ + new Claim({ name: 'fullName', value: 'example' }), + new Claim({ name: 'issuanceDate', value: new Date().toISOString().split('T')[0] }) + ] + await this.credentialService.issue(session.connectionId, claims, { identifier: claims[0].value }) } if (selectionId === Cmd.REVOKE) { await this.credentialService.revoke(session.connectionId) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 0c88bfd..c0b718d 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -113,7 +113,7 @@ export class CredentialService { */ async issue( connectionId: string, - records: Record, + claims: Claim[], options?: { identifier?: string }, ): Promise { const hashIdentifier = options?.identifier ? this.hashIdentifier(options.identifier) : null @@ -125,9 +125,6 @@ export class CredentialService { } const [{ id: credentialDefinitionId }] = credentials - const claims = Object.entries(records).map( - ([key, value]) => new Claim({ name: key, value: value ?? null }), - ) const cred = await this.credentialRepository.findOne({ where: { revoked: false, From d95e48e59d63488f866758097538db5bd99324bb Mon Sep 17 00:00:00 2001 From: andresfv95 Date: Wed, 15 Jan 2025 14:29:58 -0500 Subject: [PATCH 089/132] fix: Update packages/nestjs-client/src/credentials/credential.service.ts Co-authored-by: Ariel Gentile --- packages/nestjs-client/src/credentials/credential.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index c0b718d..d15d60b 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -114,7 +114,7 @@ export class CredentialService { async issue( connectionId: string, claims: Claim[], - options?: { identifier?: string }, + options?: { refId?: string }, ): Promise { const hashIdentifier = options?.identifier ? this.hashIdentifier(options.identifier) : null const credentials = await this.apiClient.credentialTypes.getAll() From 19831d75a348e1768843c0f0098bfc807f81bd3a Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Wed, 15 Jan 2025 14:32:41 -0500 Subject: [PATCH 090/132] fix: add hashRefId --- examples/demo-dts/src/core.service.ts | 2 +- .../src/credentials/credential.service.ts | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/demo-dts/src/core.service.ts b/examples/demo-dts/src/core.service.ts index 7069f3a..3c3e998 100644 --- a/examples/demo-dts/src/core.service.ts +++ b/examples/demo-dts/src/core.service.ts @@ -192,7 +192,7 @@ export class CoreService implements EventHandler, OnModuleInit { new Claim({ name: 'fullName', value: 'example' }), new Claim({ name: 'issuanceDate', value: new Date().toISOString().split('T')[0] }) ] - await this.credentialService.issue(session.connectionId, claims, { identifier: claims[0].value }) + await this.credentialService.issue(session.connectionId, claims, { refId: claims[0].value }) } if (selectionId === Cmd.REVOKE) { await this.credentialService.revoke(session.connectionId) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index d15d60b..b99d3d0 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -116,7 +116,7 @@ export class CredentialService { claims: Claim[], options?: { refId?: string }, ): Promise { - const hashIdentifier = options?.identifier ? this.hashIdentifier(options.identifier) : null + const hashRefId = options?.refId ? this.hashRefId(options.refId) : null const credentials = await this.apiClient.credentialTypes.getAll() if (!credentials || credentials.length === 0) { throw new Error( @@ -128,13 +128,13 @@ export class CredentialService { const cred = await this.credentialRepository.findOne({ where: { revoked: false, - ...(hashIdentifier ? { hashIdentifier } : {}), + ...(hashRefId ? { hashRefId } : {}), }, }) if (cred && this.autoRevocationEnabled) { cred.connectionId = connectionId await this.credentialRepository.save(cred) - await this.revoke(connectionId, { identifier: options?.identifier ?? undefined }) + await this.revoke(connectionId, { refId: options?.refId ?? undefined }) } const { revocationRegistryDefinitionId, revocationRegistryIndex } = await this.entityManager.transaction( @@ -143,7 +143,7 @@ export class CredentialService { await transaction.save(CredentialEntity, { connectionId, credentialDefinitionId, - ...(hashIdentifier ? { hashIdentifier } : {}), + ...(hashRefId ? { hashRefId } : {}), }) return { revocationRegistryDefinitionId: undefined, @@ -194,7 +194,7 @@ export class CredentialService { credentialDefinitionId, revocationDefinitionId: lastCred.revocationDefinitionId, revocationRegistryIndex: lastCred.revocationRegistryIndex + 1, - ...(hashIdentifier ? { hashIdentifier } : {}), + ...(hashRefId ? { hashRefId } : {}), maximumCredentialNumber: this.maximumCredentialNumber, }) return { @@ -266,10 +266,10 @@ export class CredentialService { * @param threadId - The thread ID linked to the credential to revoke. * @throws Error if no credential is found with the specified thread ID or if the credential has no connection ID. */ - async revoke(connectionId: string, options?: { identifier?: string }): Promise { - const hashIdentifier = options?.identifier ? this.hashIdentifier(options.identifier) : null + async revoke(connectionId: string, options?: { refId?: string }): Promise { + const hashRefId = options?.refId ? this.hashRefId(options.refId) : null const cred = await this.credentialRepository.findOne({ - where: { connectionId, revoked: false, ...(hashIdentifier ? { hashIdentifier } : {}) }, + where: { connectionId, revoked: false, ...(hashRefId ? { hashRefId } : {}) }, order: { createdTs: 'DESC' }, }) if (!cred || !cred.connectionId) { @@ -310,7 +310,7 @@ export class CredentialService { return revocationRegistry } - private hashIdentifier(identifier: string): string { + private hashRefId(identifier: string): string { return Buffer.from(new Sha256().hash(identifier)).toString('hex') } } From c74372ed823da166eb9f3833fee6063309689239 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Wed, 15 Jan 2025 15:46:50 -0500 Subject: [PATCH 091/132] fix: add optional credentialDefinitionId on issue method --- .../nestjs-client/src/credentials/credential.service.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index b99d3d0..8e1150a 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -114,7 +114,10 @@ export class CredentialService { async issue( connectionId: string, claims: Claim[], - options?: { refId?: string }, + options?: { + refId?: string + credentialDefinitionId?: string + }, ): Promise { const hashRefId = options?.refId ? this.hashRefId(options.refId) : null const credentials = await this.apiClient.credentialTypes.getAll() @@ -123,7 +126,9 @@ export class CredentialService { 'No credential definitions found. Please configure a credential using the create method before proceeding.', ) } - const [{ id: credentialDefinitionId }] = credentials + let credentialDefinitionId + if (options?.credentialDefinitionId) credentialDefinitionId = options.credentialDefinitionId + else [{ id: credentialDefinitionId }] = credentials const cred = await this.credentialRepository.findOne({ where: { From 57ef45736e40eec9083bb6e14be3ebbe0ca24b9c Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Wed, 15 Jan 2025 15:57:05 -0500 Subject: [PATCH 092/132] fix: implement selection credential by name and version --- packages/nestjs-client/src/credentials/credential.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 8e1150a..33bee8d 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -70,13 +70,14 @@ export class CredentialService { ) { const { name, version, supportRevocation, maximumCredentialNumber, autoRevocationEnabled } = options - const [credential] = await this.apiClient.credentialTypes.getAll() + const credentials = await this.apiClient.credentialTypes.getAll() if (name !== undefined) this.name = name if (version !== undefined) this.version = version if (supportRevocation !== undefined) this.supportRevocation = supportRevocation if (maximumCredentialNumber !== undefined) this.maximumCredentialNumber = maximumCredentialNumber if (autoRevocationEnabled !== undefined) this.autoRevocationEnabled = autoRevocationEnabled + const credential = credentials.find(cred => cred.name === name && cred.version === version) if (!credential) { const credential = await this.apiClient.credentialTypes.create({ id: utils.uuid(), From bfb8cf416f74baafdc7f1791036723d5a40a67c2 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Wed, 15 Jan 2025 16:36:40 -0500 Subject: [PATCH 093/132] docs: update --- .../src/credentials/credential.service.ts | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 33bee8d..d1fa2ae 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -94,23 +94,34 @@ export class CredentialService { /** * Sends a credential issuance to the specified connection using the provided claims. - * This method initiates the issuance process by sending claims as part of a credential to - * the recipient identified by the connection ID. + * This method initiates the issuance process by sending claims as part of a credential + * to the recipient identified by the connection ID. * * @param {string} connectionId - The unique identifier of the connection to which the credential * will be issued. This represents the recipient of the credential. * - * @param {Record} records - A key value objects, where each key represents an attribute - * of the credential. - * - * Example of constructing the `records` array: - * const records = { + * @param {Claim[]} claims - An array of claims representing the data to be included in the credential. + * Each claim has a `name` (key) and a `value` (data). Example: + * ```javascript + * const claims = [ * { name: "email", value: "john.doe@example.com" }, - * { name: "name", value: "John Doe" }, - * } + * { name: "name", value: "John Doe" } + * ]; + * ``` + * + * @param {object} [options] - Additional options for credential issuance. + * + * ### Options + * - `refId` (optional, `string`): A unique identifier for the credential. If provided: + * - Ensures the credential is unique and any existing credential with the same `refId` is revoked. + * - Used for managing unique credentials like official documents. + * - Encrypted in the database for security. + * - `credentialDefinitionId` (optional, `string`): Specifies the ID of the credential definition to use. + * - If not provided, the first available credential definition is used. + * + * @returns {Promise} A promise that resolves when the credential issuance is successfully sent. + * If an error occurs during the process, the promise will be rejected with the relevant error message. * - * @returns {Promise} A promise that resolves when the credential issuance is successfully - * sent. If an error occurs during the process, the promise will be rejected. */ async issue( connectionId: string, From bdb02a751fd3cfc2b73050bdf611acc41bea5afa Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Wed, 15 Jan 2025 17:32:10 -0500 Subject: [PATCH 094/132] fix: implement selection credential by name and version --- .../src/credentials/credential.service.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index d1fa2ae..2a394ad 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -133,14 +133,13 @@ export class CredentialService { ): Promise { const hashRefId = options?.refId ? this.hashRefId(options.refId) : null const credentials = await this.apiClient.credentialTypes.getAll() - if (!credentials || credentials.length === 0) { + const credential = credentials.find(c => c.id === options?.credentialDefinitionId) ?? credentials[0] + if (!credential) { throw new Error( 'No credential definitions found. Please configure a credential using the create method before proceeding.', ) } - let credentialDefinitionId - if (options?.credentialDefinitionId) credentialDefinitionId = options.credentialDefinitionId - else [{ id: credentialDefinitionId }] = credentials + const { id: credentialDefinitionId, supportRevocation } = credential const cred = await this.credentialRepository.findOne({ where: { @@ -295,7 +294,10 @@ export class CredentialService { cred.revoked = true await this.credentialRepository.save(cred) - this.supportRevocation && + + const credentials = await this.apiClient.credentialTypes.getAll() + const credential = credentials.find(c => c.id === cred.credentialDefinitionId) ?? credentials[0] + credential.supportRevocation && (await this.apiClient.messages.send( new CredentialRevocationMessage({ connectionId: cred.connectionId, From c03f1cc73b4376471972fd2ff0542a2ea1b09af2 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Wed, 15 Jan 2025 17:33:54 -0500 Subject: [PATCH 095/132] fix: updated name method create --- packages/nestjs-client/src/credentials/credential.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 2a394ad..9da8067 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -58,7 +58,7 @@ export class CredentialService { * * @returns A promise that resolves when the credential definition and revocation registries are created. */ - async create( + async createType( attributes: string[], options: { name?: string From b5a85104fb2e1176fe6edd1076ecf4e9cce4c3b0 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Wed, 15 Jan 2025 17:36:31 -0500 Subject: [PATCH 096/132] fix: updated for many credentials --- .../src/credentials/credential.service.ts | 62 +++++++++---------- 1 file changed, 29 insertions(+), 33 deletions(-) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 9da8067..798b656 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -19,11 +19,7 @@ export class CredentialService { private readonly apiClient: ApiClient //Credential type definitions - private name: string = 'Chatbot' - private version: string = '1.0' - private supportRevocation: boolean = false private maximumCredentialNumber: number = 1000 - private autoRevocationEnabled: boolean = false constructor( @InjectRepository(CredentialEntity) @@ -40,23 +36,22 @@ export class CredentialService { } /** - * Ensures the creation and initialization of a credential definition with support for revocation registries. + * Creates and initializes a credential type (credential definition) with optional revocation registry support. * - * When this method is called: - * - Two default lists of possible revocation registries will be created. (only with supportRevocation) - * - This guarantees seamless handling in case one of the registries runs out of revocation capacity. (only with supportRevocation) + * This method specifically handles the creation of credential types/definitions, which serve as templates + * for issuing actual credentials. * - * **Usage Recommendation:** - * It is recommended to call this method inside an `OnModuleInit` lifecycle hook to validate the existence - * of credentials as soon as the project is initialized. + * When revocation support is enabled: + * - Two default revocation registries will be created + * - This ensures continuity if one registry reaches capacity * - * @param name - The name of the credential definition. Defaults to "Chatbot" if not provided. - * @param version - The version of the credential definition. Defaults to "1.0" if not provided. - * @param attributes - A list of attributes that the credential definition supports. - * @param supportRevocation - Whether revocation is supported. Defaults to `false` if not provided. - * @param maximumCredentialNumber - The maximum number of credentials. Defaults to `1000` if not provided. + * @param attributes - List of attributes that credentials of this type will support + * @param options.name - Name identifier for the credential type + * @param options.version - Version of the credential type + * @param options.supportRevocation - Whether credentials can be revoked + * @param options.maximumCredentialNumber - Maximum number of credentials that can be issued * - * @returns A promise that resolves when the credential definition and revocation registries are created. + * @returns Promise - Resolves when the credential type and revocation registries are created */ async createType( attributes: string[], @@ -65,30 +60,25 @@ export class CredentialService { version?: string supportRevocation?: boolean maximumCredentialNumber?: number - autoRevocationEnabled?: boolean } = {}, ) { - const { name, version, supportRevocation, maximumCredentialNumber, autoRevocationEnabled } = options + const { name = 'Chatbot', version = '1.0', supportRevocation, maximumCredentialNumber } = options const credentials = await this.apiClient.credentialTypes.getAll() - if (name !== undefined) this.name = name - if (version !== undefined) this.version = version - if (supportRevocation !== undefined) this.supportRevocation = supportRevocation if (maximumCredentialNumber !== undefined) this.maximumCredentialNumber = maximumCredentialNumber - if (autoRevocationEnabled !== undefined) this.autoRevocationEnabled = autoRevocationEnabled const credential = credentials.find(cred => cred.name === name && cred.version === version) if (!credential) { const credential = await this.apiClient.credentialTypes.create({ id: utils.uuid(), - name: this.name, - version: this.version, + name, + version, attributes, - supportRevocation: this.supportRevocation, + supportRevocation, }) - await this.createRevocationRegistry(credential.id) - await this.createRevocationRegistry(credential.id) + await this.saveCredential(credential.id, supportRevocation) + await this.saveCredential(credential.id, supportRevocation) } } @@ -118,6 +108,7 @@ export class CredentialService { * - Encrypted in the database for security. * - `credentialDefinitionId` (optional, `string`): Specifies the ID of the credential definition to use. * - If not provided, the first available credential definition is used. + * - `autoRevocationEnabled` (optional, `boolean`): Whether automatic revocation is enabled (default false) * * @returns {Promise} A promise that resolves when the credential issuance is successfully sent. * If an error occurs during the process, the promise will be rejected with the relevant error message. @@ -129,8 +120,10 @@ export class CredentialService { options?: { refId?: string credentialDefinitionId?: string + autoRevocationEnabled?: boolean }, ): Promise { + const { autoRevocationEnabled = false } = options ?? {} const hashRefId = options?.refId ? this.hashRefId(options.refId) : null const credentials = await this.apiClient.credentialTypes.getAll() const credential = credentials.find(c => c.id === options?.credentialDefinitionId) ?? credentials[0] @@ -147,7 +140,7 @@ export class CredentialService { ...(hashRefId ? { hashRefId } : {}), }, }) - if (cred && this.autoRevocationEnabled) { + if (cred && autoRevocationEnabled) { cred.connectionId = connectionId await this.credentialRepository.save(cred) await this.revoke(connectionId, { refId: options?.refId ?? undefined }) @@ -155,7 +148,7 @@ export class CredentialService { const { revocationRegistryDefinitionId, revocationRegistryIndex } = await this.entityManager.transaction( async transaction => { - if (!this.supportRevocation) { + if (!supportRevocation) { await transaction.save(CredentialEntity, { connectionId, credentialDefinitionId, @@ -230,7 +223,7 @@ export class CredentialService { }), ) if (revocationRegistryIndex === this.maximumCredentialNumber - 1) { - const revRegistry = await this.createRevocationRegistry(credentialDefinitionId) + const revRegistry = await this.saveCredential(credentialDefinitionId, supportRevocation) this.logger.log(`Revocation registry successfully created with ID ${revRegistry}`) } this.logger.debug('sendCredential with claims: ' + JSON.stringify(claims)) @@ -308,8 +301,11 @@ export class CredentialService { } // private methods - private async createRevocationRegistry(credentialDefinitionId: string): Promise { - if (!this.supportRevocation) { + private async saveCredential( + credentialDefinitionId: string, + supportRevocation: boolean = false, + ): Promise { + if (!supportRevocation) { await this.credentialRepository.save({ credentialDefinitionId }) return null } From 70b3b6248c6c131bee1c04c31635c3f602002750 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Wed, 15 Jan 2025 17:37:03 -0500 Subject: [PATCH 097/132] fix: updated for many credentials --- packages/nestjs-client/src/credentials/credential.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 798b656..c2d2cbf 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -100,7 +100,7 @@ export class CredentialService { * ``` * * @param {object} [options] - Additional options for credential issuance. - * + * * ### Options * - `refId` (optional, `string`): A unique identifier for the credential. If provided: * - Ensures the credential is unique and any existing credential with the same `refId` is revoked. From 275de0deb8d0c9afb5dfaa046b3f9384f624c7ca Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Wed, 15 Jan 2025 17:41:37 -0500 Subject: [PATCH 098/132] fix: update on demo dts --- examples/demo-dts/src/core.service.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/demo-dts/src/core.service.ts b/examples/demo-dts/src/core.service.ts index 3c3e998..bd20acb 100644 --- a/examples/demo-dts/src/core.service.ts +++ b/examples/demo-dts/src/core.service.ts @@ -41,11 +41,10 @@ export class CoreService implements EventHandler, OnModuleInit { } async onModuleInit() { - await this.credentialService.create(['fullName', 'issuanceDate'], { + await this.credentialService.createType(['fullName', 'issuanceDate'], { name: 'demo dts', supportRevocation: true, maximumCredentialNumber: 5, - autoRevocationEnabled: true, }) } @@ -192,7 +191,7 @@ export class CoreService implements EventHandler, OnModuleInit { new Claim({ name: 'fullName', value: 'example' }), new Claim({ name: 'issuanceDate', value: new Date().toISOString().split('T')[0] }) ] - await this.credentialService.issue(session.connectionId, claims, { refId: claims[0].value }) + await this.credentialService.issue(session.connectionId, claims, { refId: claims[0].value, autoRevocationEnabled: true }) } if (selectionId === Cmd.REVOKE) { await this.credentialService.revoke(session.connectionId) From f5c0fbafb7032e9d3a224d72c3c3c0d41b8a81ee Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Wed, 15 Jan 2025 17:44:20 -0500 Subject: [PATCH 099/132] fix: update on demo dts --- examples/demo-dts/src/core.service.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/examples/demo-dts/src/core.service.ts b/examples/demo-dts/src/core.service.ts index bd20acb..a368508 100644 --- a/examples/demo-dts/src/core.service.ts +++ b/examples/demo-dts/src/core.service.ts @@ -189,9 +189,12 @@ export class CoreService implements EventHandler, OnModuleInit { if (selectionId === Cmd.CREDENTIAL) { const claims = [ new Claim({ name: 'fullName', value: 'example' }), - new Claim({ name: 'issuanceDate', value: new Date().toISOString().split('T')[0] }) + new Claim({ name: 'issuanceDate', value: new Date().toISOString().split('T')[0] }), ] - await this.credentialService.issue(session.connectionId, claims, { refId: claims[0].value, autoRevocationEnabled: true }) + await this.credentialService.issue(session.connectionId, claims, { + refId: claims[0].value, + autoRevocationEnabled: true, + }) } if (selectionId === Cmd.REVOKE) { await this.credentialService.revoke(session.connectionId) From 96c88e1bd5d80ce05f012d27be9cee63665c409e Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Thu, 16 Jan 2025 00:54:07 -0500 Subject: [PATCH 100/132] fix: remove name hashRefId --- .../src/credentials/credential.service.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index c2d2cbf..6e5c85c 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -124,7 +124,7 @@ export class CredentialService { }, ): Promise { const { autoRevocationEnabled = false } = options ?? {} - const hashRefId = options?.refId ? this.hashRefId(options.refId) : null + const hashIdentifier = options?.refId ? this.hashIdentifier(options.refId) : null const credentials = await this.apiClient.credentialTypes.getAll() const credential = credentials.find(c => c.id === options?.credentialDefinitionId) ?? credentials[0] if (!credential) { @@ -137,7 +137,7 @@ export class CredentialService { const cred = await this.credentialRepository.findOne({ where: { revoked: false, - ...(hashRefId ? { hashRefId } : {}), + ...(hashIdentifier ? { hashIdentifier } : {}), }, }) if (cred && autoRevocationEnabled) { @@ -152,7 +152,7 @@ export class CredentialService { await transaction.save(CredentialEntity, { connectionId, credentialDefinitionId, - ...(hashRefId ? { hashRefId } : {}), + ...(hashIdentifier ? { hashIdentifier } : {}), }) return { revocationRegistryDefinitionId: undefined, @@ -203,7 +203,7 @@ export class CredentialService { credentialDefinitionId, revocationDefinitionId: lastCred.revocationDefinitionId, revocationRegistryIndex: lastCred.revocationRegistryIndex + 1, - ...(hashRefId ? { hashRefId } : {}), + ...(hashIdentifier ? { hashIdentifier } : {}), maximumCredentialNumber: this.maximumCredentialNumber, }) return { @@ -276,9 +276,9 @@ export class CredentialService { * @throws Error if no credential is found with the specified thread ID or if the credential has no connection ID. */ async revoke(connectionId: string, options?: { refId?: string }): Promise { - const hashRefId = options?.refId ? this.hashRefId(options.refId) : null + const hashIdentifier = options?.refId ? this.hashIdentifier(options.refId) : null const cred = await this.credentialRepository.findOne({ - where: { connectionId, revoked: false, ...(hashRefId ? { hashRefId } : {}) }, + where: { connectionId, revoked: false, ...(hashIdentifier ? { hashIdentifier } : {}) }, order: { createdTs: 'DESC' }, }) if (!cred || !cred.connectionId) { @@ -325,7 +325,7 @@ export class CredentialService { return revocationRegistry } - private hashRefId(identifier: string): string { + private hashIdentifier(identifier: string): string { return Buffer.from(new Sha256().hash(identifier)).toString('hex') } } From e230f4880a15862cc9c37d65e13c1c3dd18e2e2e Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Thu, 16 Jan 2025 01:32:16 -0500 Subject: [PATCH 101/132] fix: add supportRevocation by get credential types --- .../src/controllers/credentials/CredentialTypeController.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/main/src/controllers/credentials/CredentialTypeController.ts b/packages/main/src/controllers/credentials/CredentialTypeController.ts index 63d58e3..7d71574 100644 --- a/packages/main/src/controllers/credentials/CredentialTypeController.ts +++ b/packages/main/src/controllers/credentials/CredentialTypeController.ts @@ -59,12 +59,14 @@ export class CredentialTypesController { const schemaResult = await agent.modules.anoncreds.getSchema(record.credentialDefinition.schemaId) const schema = schemaResult.schema + const supportRevocation = record.credentialDefinition?.value?.revocation !== undefined return { id: record.credentialDefinitionId, name: (record.getTag('name') as string) ?? schema?.name, version: (record.getTag('version') as string) ?? schema?.version, attributes: schema?.attrNames || [], + supportRevocation, } }), ) From 6e62c40e1ef4aafd3bff3861b9cf31300e1adf75 Mon Sep 17 00:00:00 2001 From: andresfv95 Date: Thu, 16 Jan 2025 10:36:09 -0500 Subject: [PATCH 102/132] fix: Update packages/nestjs-client/src/credentials/credential.service.ts Co-authored-by: Ariel Gentile --- packages/nestjs-client/src/credentials/credential.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 6e5c85c..e1e48e3 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -105,7 +105,7 @@ export class CredentialService { * - `refId` (optional, `string`): A unique identifier for the credential. If provided: * - Ensures the credential is unique and any existing credential with the same `refId` is revoked. * - Used for managing unique credentials like official documents. - * - Encrypted in the database for security. + * - Hashed in the database for security. * - `credentialDefinitionId` (optional, `string`): Specifies the ID of the credential definition to use. * - If not provided, the first available credential definition is used. * - `autoRevocationEnabled` (optional, `boolean`): Whether automatic revocation is enabled (default false) From c75b92f26b0c774d8bb5f9b48bb0ffa924320e86 Mon Sep 17 00:00:00 2001 From: andresfv95 Date: Thu, 16 Jan 2025 10:52:09 -0500 Subject: [PATCH 103/132] fix: Update packages/main/src/controllers/credentials/CredentialTypeController.ts Co-authored-by: Ariel Gentile --- .../src/controllers/credentials/CredentialTypeController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/main/src/controllers/credentials/CredentialTypeController.ts b/packages/main/src/controllers/credentials/CredentialTypeController.ts index 7d71574..d1e0b87 100644 --- a/packages/main/src/controllers/credentials/CredentialTypeController.ts +++ b/packages/main/src/controllers/credentials/CredentialTypeController.ts @@ -66,7 +66,7 @@ export class CredentialTypesController { name: (record.getTag('name') as string) ?? schema?.name, version: (record.getTag('version') as string) ?? schema?.version, attributes: schema?.attrNames || [], - supportRevocation, + revocationSupported, } }), ) From 4cf2e30280862b871256a8529832a38ddca202bc Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Thu, 16 Jan 2025 11:23:55 -0500 Subject: [PATCH 104/132] fix: add revocationSupported --- .../src/controllers/credentials/CredentialTypeController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/main/src/controllers/credentials/CredentialTypeController.ts b/packages/main/src/controllers/credentials/CredentialTypeController.ts index d1e0b87..3e284a8 100644 --- a/packages/main/src/controllers/credentials/CredentialTypeController.ts +++ b/packages/main/src/controllers/credentials/CredentialTypeController.ts @@ -59,7 +59,7 @@ export class CredentialTypesController { const schemaResult = await agent.modules.anoncreds.getSchema(record.credentialDefinition.schemaId) const schema = schemaResult.schema - const supportRevocation = record.credentialDefinition?.value?.revocation !== undefined + const revocationSupported = record.credentialDefinition?.value?.revocation !== undefined return { id: record.credentialDefinitionId, From 9231f636232a947d3cf06565bdfe4890f4346d42 Mon Sep 17 00:00:00 2001 From: andresfv95 Date: Thu, 16 Jan 2025 11:25:00 -0500 Subject: [PATCH 105/132] fix: Update packages/nestjs-client/src/credentials/credential.service.ts Co-authored-by: Ariel Gentile --- packages/nestjs-client/src/credentials/credential.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index e1e48e3..3b8c4a6 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -64,7 +64,7 @@ export class CredentialService { ) { const { name = 'Chatbot', version = '1.0', supportRevocation, maximumCredentialNumber } = options - const credentials = await this.apiClient.credentialTypes.getAll() + const credentialTypes = await this.apiClient.credentialTypes.getAll() if (maximumCredentialNumber !== undefined) this.maximumCredentialNumber = maximumCredentialNumber const credential = credentials.find(cred => cred.name === name && cred.version === version) From 107f25017f412411b439f7cb1d7f396ec5402b07 Mon Sep 17 00:00:00 2001 From: andresfv95 Date: Thu, 16 Jan 2025 11:25:38 -0500 Subject: [PATCH 106/132] fix: Update packages/nestjs-client/src/credentials/credential.service.ts Co-authored-by: Ariel Gentile --- packages/nestjs-client/src/credentials/credential.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 3b8c4a6..9405b34 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -67,7 +67,7 @@ export class CredentialService { const credentialTypes = await this.apiClient.credentialTypes.getAll() if (maximumCredentialNumber !== undefined) this.maximumCredentialNumber = maximumCredentialNumber - const credential = credentials.find(cred => cred.name === name && cred.version === version) + const credentialType = credentialTypes.find(credType => credType.name === name && credType.version === version) if (!credential) { const credential = await this.apiClient.credentialTypes.create({ id: utils.uuid(), From e6139e7057f3541147a20359fce9a648b980f290 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Thu, 16 Jan 2025 11:32:02 -0500 Subject: [PATCH 107/132] fix: name credType --- .../src/credentials/credential.service.ts | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 9405b34..ad527e6 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -68,8 +68,8 @@ export class CredentialService { if (maximumCredentialNumber !== undefined) this.maximumCredentialNumber = maximumCredentialNumber const credentialType = credentialTypes.find(credType => credType.name === name && credType.version === version) - if (!credential) { - const credential = await this.apiClient.credentialTypes.create({ + if (!credentialType) { + const credentialType = await this.apiClient.credentialTypes.create({ id: utils.uuid(), name, version, @@ -77,8 +77,8 @@ export class CredentialService { supportRevocation, }) - await this.saveCredential(credential.id, supportRevocation) - await this.saveCredential(credential.id, supportRevocation) + await this.saveCredentialType(credentialType.id, supportRevocation) + await this.saveCredentialType(credentialType.id, supportRevocation) } } @@ -125,14 +125,14 @@ export class CredentialService { ): Promise { const { autoRevocationEnabled = false } = options ?? {} const hashIdentifier = options?.refId ? this.hashIdentifier(options.refId) : null - const credentials = await this.apiClient.credentialTypes.getAll() - const credential = credentials.find(c => c.id === options?.credentialDefinitionId) ?? credentials[0] - if (!credential) { + const credentialTypes = await this.apiClient.credentialTypes.getAll() + const credentialType = credentialTypes.find(credType => credType.id === options?.credentialDefinitionId) ?? credentialTypes[0] + if (!credentialType) { throw new Error( 'No credential definitions found. Please configure a credential using the create method before proceeding.', ) } - const { id: credentialDefinitionId, supportRevocation } = credential + const { id: credentialDefinitionId, supportRevocation } = credentialType const cred = await this.credentialRepository.findOne({ where: { @@ -223,7 +223,7 @@ export class CredentialService { }), ) if (revocationRegistryIndex === this.maximumCredentialNumber - 1) { - const revRegistry = await this.saveCredential(credentialDefinitionId, supportRevocation) + const revRegistry = await this.saveCredentialType(credentialDefinitionId, supportRevocation) this.logger.log(`Revocation registry successfully created with ID ${revRegistry}`) } this.logger.debug('sendCredential with claims: ' + JSON.stringify(claims)) @@ -288,9 +288,9 @@ export class CredentialService { cred.revoked = true await this.credentialRepository.save(cred) - const credentials = await this.apiClient.credentialTypes.getAll() - const credential = credentials.find(c => c.id === cred.credentialDefinitionId) ?? credentials[0] - credential.supportRevocation && + const credentialTypes = await this.apiClient.credentialTypes.getAll() + const credentialType = credentialTypes.find(credType => credType.id === cred.credentialDefinitionId) ?? credentialTypes[0] + credentialType.supportRevocation && (await this.apiClient.messages.send( new CredentialRevocationMessage({ connectionId: cred.connectionId, @@ -301,7 +301,7 @@ export class CredentialService { } // private methods - private async saveCredential( + private async saveCredentialType( credentialDefinitionId: string, supportRevocation: boolean = false, ): Promise { From 9018b1d49f45a1d4501daa7befbfac9f3f411fc7 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Thu, 16 Jan 2025 11:34:44 -0500 Subject: [PATCH 108/132] fix: name refIdHash --- .../src/credentials/credential.entity.ts | 2 +- .../src/credentials/credential.service.ts | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/nestjs-client/src/credentials/credential.entity.ts b/packages/nestjs-client/src/credentials/credential.entity.ts index c8f288e..c0ebe9c 100644 --- a/packages/nestjs-client/src/credentials/credential.entity.ts +++ b/packages/nestjs-client/src/credentials/credential.entity.ts @@ -21,7 +21,7 @@ export class CredentialEntity { threadId?: string @Column({ type: 'varchar', nullable: true }) - hashIdentifier?: string + refIdHash?: string @Column({ nullable: true, default: false }) revoked?: boolean diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index ad527e6..405fd63 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -124,7 +124,7 @@ export class CredentialService { }, ): Promise { const { autoRevocationEnabled = false } = options ?? {} - const hashIdentifier = options?.refId ? this.hashIdentifier(options.refId) : null + const refIdHash = options?.refId ? this.refIdHash(options.refId) : null const credentialTypes = await this.apiClient.credentialTypes.getAll() const credentialType = credentialTypes.find(credType => credType.id === options?.credentialDefinitionId) ?? credentialTypes[0] if (!credentialType) { @@ -137,7 +137,7 @@ export class CredentialService { const cred = await this.credentialRepository.findOne({ where: { revoked: false, - ...(hashIdentifier ? { hashIdentifier } : {}), + ...(refIdHash ? { refIdHash } : {}), }, }) if (cred && autoRevocationEnabled) { @@ -152,7 +152,7 @@ export class CredentialService { await transaction.save(CredentialEntity, { connectionId, credentialDefinitionId, - ...(hashIdentifier ? { hashIdentifier } : {}), + ...(refIdHash ? { refIdHash } : {}), }) return { revocationRegistryDefinitionId: undefined, @@ -203,7 +203,7 @@ export class CredentialService { credentialDefinitionId, revocationDefinitionId: lastCred.revocationDefinitionId, revocationRegistryIndex: lastCred.revocationRegistryIndex + 1, - ...(hashIdentifier ? { hashIdentifier } : {}), + ...(refIdHash ? { refIdHash } : {}), maximumCredentialNumber: this.maximumCredentialNumber, }) return { @@ -276,9 +276,9 @@ export class CredentialService { * @throws Error if no credential is found with the specified thread ID or if the credential has no connection ID. */ async revoke(connectionId: string, options?: { refId?: string }): Promise { - const hashIdentifier = options?.refId ? this.hashIdentifier(options.refId) : null + const refIdHash = options?.refId ? this.refIdHash(options.refId) : null const cred = await this.credentialRepository.findOne({ - where: { connectionId, revoked: false, ...(hashIdentifier ? { hashIdentifier } : {}) }, + where: { connectionId, revoked: false, ...(refIdHash ? { refIdHash } : {}) }, order: { createdTs: 'DESC' }, }) if (!cred || !cred.connectionId) { @@ -325,7 +325,7 @@ export class CredentialService { return revocationRegistry } - private hashIdentifier(identifier: string): string { - return Buffer.from(new Sha256().hash(identifier)).toString('hex') + private refIdHash(refId: string): string { + return Buffer.from(new Sha256().hash(refId)).toString('hex') } } From 41f18af476570f28139c4b58847d10e9383a6be5 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Thu, 16 Jan 2025 11:38:22 -0500 Subject: [PATCH 109/132] fix: add revokeIfAlreadyIssued --- examples/demo-dts/src/core.service.ts | 2 +- .../nestjs-client/src/credentials/credential.service.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/demo-dts/src/core.service.ts b/examples/demo-dts/src/core.service.ts index a368508..aed0e16 100644 --- a/examples/demo-dts/src/core.service.ts +++ b/examples/demo-dts/src/core.service.ts @@ -193,7 +193,7 @@ export class CoreService implements EventHandler, OnModuleInit { ] await this.credentialService.issue(session.connectionId, claims, { refId: claims[0].value, - autoRevocationEnabled: true, + revokeIfAlreadyIssued: true, }) } if (selectionId === Cmd.REVOKE) { diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 405fd63..e98a31d 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -108,7 +108,7 @@ export class CredentialService { * - Hashed in the database for security. * - `credentialDefinitionId` (optional, `string`): Specifies the ID of the credential definition to use. * - If not provided, the first available credential definition is used. - * - `autoRevocationEnabled` (optional, `boolean`): Whether automatic revocation is enabled (default false) + * - `revokeIfAlreadyIssued` (optional, `boolean`): Whether automatic revocation is enabled (default false) * * @returns {Promise} A promise that resolves when the credential issuance is successfully sent. * If an error occurs during the process, the promise will be rejected with the relevant error message. @@ -120,10 +120,10 @@ export class CredentialService { options?: { refId?: string credentialDefinitionId?: string - autoRevocationEnabled?: boolean + revokeIfAlreadyIssued?: boolean }, ): Promise { - const { autoRevocationEnabled = false } = options ?? {} + const { revokeIfAlreadyIssued = false } = options ?? {} const refIdHash = options?.refId ? this.refIdHash(options.refId) : null const credentialTypes = await this.apiClient.credentialTypes.getAll() const credentialType = credentialTypes.find(credType => credType.id === options?.credentialDefinitionId) ?? credentialTypes[0] @@ -140,7 +140,7 @@ export class CredentialService { ...(refIdHash ? { refIdHash } : {}), }, }) - if (cred && autoRevocationEnabled) { + if (cred && revokeIfAlreadyIssued) { cred.connectionId = connectionId await this.credentialRepository.save(cred) await this.revoke(connectionId, { refId: options?.refId ?? undefined }) From a68003ea6fd9128924ba7924e00742261ebc23bd Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Thu, 16 Jan 2025 11:46:08 -0500 Subject: [PATCH 110/132] docs: update --- packages/nestjs-client/src/credentials/credential.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index e98a31d..22cecca 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -103,7 +103,9 @@ export class CredentialService { * * ### Options * - `refId` (optional, `string`): A unique identifier for the credential. If provided: - * - Ensures the credential is unique and any existing credential with the same `refId` is revoked. + * - When `revokeIfAlreadyIssued` is set to `true`, any existing credential with the same `refId` + * will be revoked, ensuring the credential is unique. + * - If `revokeIfAlreadyIssued` is set to `false` (default), multiple credentials with the same `refId` can exist * - Used for managing unique credentials like official documents. * - Hashed in the database for security. * - `credentialDefinitionId` (optional, `string`): Specifies the ID of the credential definition to use. From 37cec5b55ce62c97a4fb9b6a067cf41795230863 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Thu, 16 Jan 2025 11:47:18 -0500 Subject: [PATCH 111/132] fix: linter --- .../src/credentials/credential.service.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 22cecca..e78661c 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -67,7 +67,9 @@ export class CredentialService { const credentialTypes = await this.apiClient.credentialTypes.getAll() if (maximumCredentialNumber !== undefined) this.maximumCredentialNumber = maximumCredentialNumber - const credentialType = credentialTypes.find(credType => credType.name === name && credType.version === version) + const credentialType = credentialTypes.find( + credType => credType.name === name && credType.version === version, + ) if (!credentialType) { const credentialType = await this.apiClient.credentialTypes.create({ id: utils.uuid(), @@ -103,7 +105,7 @@ export class CredentialService { * * ### Options * - `refId` (optional, `string`): A unique identifier for the credential. If provided: - * - When `revokeIfAlreadyIssued` is set to `true`, any existing credential with the same `refId` + * - When `revokeIfAlreadyIssued` is set to `true`, any existing credential with the same `refId` * will be revoked, ensuring the credential is unique. * - If `revokeIfAlreadyIssued` is set to `false` (default), multiple credentials with the same `refId` can exist * - Used for managing unique credentials like official documents. @@ -128,7 +130,8 @@ export class CredentialService { const { revokeIfAlreadyIssued = false } = options ?? {} const refIdHash = options?.refId ? this.refIdHash(options.refId) : null const credentialTypes = await this.apiClient.credentialTypes.getAll() - const credentialType = credentialTypes.find(credType => credType.id === options?.credentialDefinitionId) ?? credentialTypes[0] + const credentialType = + credentialTypes.find(credType => credType.id === options?.credentialDefinitionId) ?? credentialTypes[0] if (!credentialType) { throw new Error( 'No credential definitions found. Please configure a credential using the create method before proceeding.', @@ -291,7 +294,8 @@ export class CredentialService { await this.credentialRepository.save(cred) const credentialTypes = await this.apiClient.credentialTypes.getAll() - const credentialType = credentialTypes.find(credType => credType.id === cred.credentialDefinitionId) ?? credentialTypes[0] + const credentialType = + credentialTypes.find(credType => credType.id === cred.credentialDefinitionId) ?? credentialTypes[0] credentialType.supportRevocation && (await this.apiClient.messages.send( new CredentialRevocationMessage({ From c0743e3644693f1a21d9669c9d3832b7626cdd68 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Thu, 16 Jan 2025 12:02:47 -0500 Subject: [PATCH 112/132] fix: add CredentialTypeResult --- packages/client/src/services/CredentialTypeService.ts | 10 +++++++--- .../credentials/CredentialTypeController.ts | 8 ++++++-- packages/model/src/types.ts | 4 ++++ .../src/credentials/credential.service.ts | 8 ++++---- 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/packages/client/src/services/CredentialTypeService.ts b/packages/client/src/services/CredentialTypeService.ts index 85c47c7..f2f8913 100644 --- a/packages/client/src/services/CredentialTypeService.ts +++ b/packages/client/src/services/CredentialTypeService.ts @@ -1,6 +1,10 @@ // src/services/CredentialTypeService.ts -import { CredentialTypeInfo, ImportCredentialTypeOptions } from '@2060.io/service-agent-model' +import { + CredentialTypeInfo, + CredentialTypeResult, + ImportCredentialTypeOptions, +} from '@2060.io/service-agent-model' import { Logger } from 'tslog' import { ApiVersion } from '../types/enums' @@ -49,7 +53,7 @@ export class CredentialTypeService { return (await response.json()) as CredentialTypeInfo } - public async getAll(): Promise { + public async getAll(): Promise { const response = await fetch(this.url, { method: 'GET', headers: { accept: 'application/json', 'Content-Type': 'application/json' }, @@ -61,6 +65,6 @@ export class CredentialTypeService { throw new Error('Invalid response from Service Agent') } - return types.map(value => value as CredentialTypeInfo) + return types.map(value => value as CredentialTypeResult) } } diff --git a/packages/main/src/controllers/credentials/CredentialTypeController.ts b/packages/main/src/controllers/credentials/CredentialTypeController.ts index 3e284a8..75d57ae 100644 --- a/packages/main/src/controllers/credentials/CredentialTypeController.ts +++ b/packages/main/src/controllers/credentials/CredentialTypeController.ts @@ -1,4 +1,8 @@ -import { CredentialTypeInfo, ImportCredentialTypeOptions } from '@2060.io/service-agent-model' +import { + CredentialTypeInfo, + CredentialTypeResult, + ImportCredentialTypeOptions, +} from '@2060.io/service-agent-model' import { AnonCredsCredentialDefinition, AnonCredsCredentialDefinitionPrivateRecord, @@ -49,7 +53,7 @@ export class CredentialTypesController { * @returns */ @Get('/') - public async getAllCredentialTypes(): Promise { + public async getAllCredentialTypes(): Promise { const agent = await this.agentService.getAgent() const credentialDefinitions = await agent.modules.anoncreds.getCreatedCredentialDefinitions({}) diff --git a/packages/model/src/types.ts b/packages/model/src/types.ts index 6f380a3..47a738e 100644 --- a/packages/model/src/types.ts +++ b/packages/model/src/types.ts @@ -47,6 +47,10 @@ export interface CredentialTypeInfo extends CreateCredentialTypeOptions { id: string } +export interface CredentialTypeResult extends Omit { + revocationSupported: boolean +} + export interface RevocationRegistryInfo { credentialDefinitionId: string maximumCredentialNumber: number diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index e78661c..363262f 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -137,7 +137,7 @@ export class CredentialService { 'No credential definitions found. Please configure a credential using the create method before proceeding.', ) } - const { id: credentialDefinitionId, supportRevocation } = credentialType + const { id: credentialDefinitionId, revocationSupported } = credentialType const cred = await this.credentialRepository.findOne({ where: { @@ -153,7 +153,7 @@ export class CredentialService { const { revocationRegistryDefinitionId, revocationRegistryIndex } = await this.entityManager.transaction( async transaction => { - if (!supportRevocation) { + if (!revocationSupported) { await transaction.save(CredentialEntity, { connectionId, credentialDefinitionId, @@ -228,7 +228,7 @@ export class CredentialService { }), ) if (revocationRegistryIndex === this.maximumCredentialNumber - 1) { - const revRegistry = await this.saveCredentialType(credentialDefinitionId, supportRevocation) + const revRegistry = await this.saveCredentialType(credentialDefinitionId, revocationSupported) this.logger.log(`Revocation registry successfully created with ID ${revRegistry}`) } this.logger.debug('sendCredential with claims: ' + JSON.stringify(claims)) @@ -296,7 +296,7 @@ export class CredentialService { const credentialTypes = await this.apiClient.credentialTypes.getAll() const credentialType = credentialTypes.find(credType => credType.id === cred.credentialDefinitionId) ?? credentialTypes[0] - credentialType.supportRevocation && + credentialType.revocationSupported && (await this.apiClient.messages.send( new CredentialRevocationMessage({ connectionId: cred.connectionId, From e44a5c489f5deb184bac14411fe925883b383746 Mon Sep 17 00:00:00 2001 From: andresfv95 Date: Thu, 16 Jan 2025 18:01:19 -0500 Subject: [PATCH 113/132] fix: Update packages/nestjs-client/src/credentials/credential.service.ts Co-authored-by: Ariel Gentile --- packages/nestjs-client/src/credentials/credential.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 363262f..7f1031e 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -268,7 +268,7 @@ export class CredentialService { }, order: { createdTs: 'DESC' }, }) - if (!cred) throw new Error(`Credencial with connectionId ${connectionId} not found.`) + if (!cred) throw new Error(`Credential with connectionId ${connectionId} not found.`) cred.threadId = threadId cred.revoked = true From cedf3f06af603c293f927f951696693fa3f6e229 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Thu, 16 Jan 2025 18:33:52 -0500 Subject: [PATCH 114/132] fix: change name refIdHash by hash --- .../nestjs-client/src/credentials/credential.service.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 7f1031e..5c5c81b 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -128,7 +128,7 @@ export class CredentialService { }, ): Promise { const { revokeIfAlreadyIssued = false } = options ?? {} - const refIdHash = options?.refId ? this.refIdHash(options.refId) : null + const refIdHash = options?.refId ? this.hash(options.refId) : null const credentialTypes = await this.apiClient.credentialTypes.getAll() const credentialType = credentialTypes.find(credType => credType.id === options?.credentialDefinitionId) ?? credentialTypes[0] @@ -281,7 +281,7 @@ export class CredentialService { * @throws Error if no credential is found with the specified thread ID or if the credential has no connection ID. */ async revoke(connectionId: string, options?: { refId?: string }): Promise { - const refIdHash = options?.refId ? this.refIdHash(options.refId) : null + const refIdHash = options?.refId ? this.hash(options.refId) : null const cred = await this.credentialRepository.findOne({ where: { connectionId, revoked: false, ...(refIdHash ? { refIdHash } : {}) }, order: { createdTs: 'DESC' }, @@ -331,7 +331,7 @@ export class CredentialService { return revocationRegistry } - private refIdHash(refId: string): string { - return Buffer.from(new Sha256().hash(refId)).toString('hex') + private hash(value: string): string { + return Buffer.from(new Sha256().hash(value)).toString('hex') } } From 43d85ada403fa8aed82a0c8a550bec0320dd64fb Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Thu, 16 Jan 2025 18:37:30 -0500 Subject: [PATCH 115/132] fix: remove name and version from optionals fields --- examples/demo-dts/src/core.service.ts | 6 ++++-- .../nestjs-client/src/credentials/credential.service.ts | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/examples/demo-dts/src/core.service.ts b/examples/demo-dts/src/core.service.ts index aed0e16..bb12250 100644 --- a/examples/demo-dts/src/core.service.ts +++ b/examples/demo-dts/src/core.service.ts @@ -41,8 +41,10 @@ export class CoreService implements EventHandler, OnModuleInit { } async onModuleInit() { - await this.credentialService.createType(['fullName', 'issuanceDate'], { - name: 'demo dts', + await this.credentialService.createType( + 'demo dts', + '1.0', + ['fullName', 'issuanceDate'], { supportRevocation: true, maximumCredentialNumber: 5, }) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 5c5c81b..5606c6e 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -54,15 +54,15 @@ export class CredentialService { * @returns Promise - Resolves when the credential type and revocation registries are created */ async createType( + name: string, + version: string, attributes: string[], options: { - name?: string - version?: string supportRevocation?: boolean maximumCredentialNumber?: number } = {}, ) { - const { name = 'Chatbot', version = '1.0', supportRevocation, maximumCredentialNumber } = options + const { supportRevocation, maximumCredentialNumber } = options const credentialTypes = await this.apiClient.credentialTypes.getAll() if (maximumCredentialNumber !== undefined) this.maximumCredentialNumber = maximumCredentialNumber From 8012af1080ab066bd978e3a93972c72076547165 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Fri, 17 Jan 2025 08:26:38 -0500 Subject: [PATCH 116/132] fix: implement revocation for all non-revoked credentials --- .../src/credentials/credential.service.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 5606c6e..4592c06 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -139,16 +139,18 @@ export class CredentialService { } const { id: credentialDefinitionId, revocationSupported } = credentialType - const cred = await this.credentialRepository.findOne({ + const creds = await this.credentialRepository.find({ where: { revoked: false, ...(refIdHash ? { refIdHash } : {}), }, }) - if (cred && revokeIfAlreadyIssued) { - cred.connectionId = connectionId - await this.credentialRepository.save(cred) - await this.revoke(connectionId, { refId: options?.refId ?? undefined }) + if (creds && revokeIfAlreadyIssued) { + for (const cred of creds) { + cred.connectionId = connectionId + await this.credentialRepository.save(cred) + await this.revoke(connectionId, { refId: options?.refId ?? undefined }) + } } const { revocationRegistryDefinitionId, revocationRegistryIndex } = await this.entityManager.transaction( From 53ec35d534ffb389e4fe7fc30a8e2256758580e3 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Fri, 17 Jan 2025 09:05:05 -0500 Subject: [PATCH 117/132] fix: updated to createRevocationRegistry --- .../src/credentials/credential.service.ts | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 4592c06..7835e25 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -79,8 +79,13 @@ export class CredentialService { supportRevocation, }) - await this.saveCredentialType(credentialType.id, supportRevocation) - await this.saveCredentialType(credentialType.id, supportRevocation) + if (supportRevocation) { + await this.credentialRepository.save({ credentialDefinitionId: credentialType.id }) + } else { + // Both records are created to handle multiple credentials + await this.createRevocationRegistry(credentialType.id) + await this.createRevocationRegistry(credentialType.id) + } } } @@ -230,7 +235,7 @@ export class CredentialService { }), ) if (revocationRegistryIndex === this.maximumCredentialNumber - 1) { - const revRegistry = await this.saveCredentialType(credentialDefinitionId, revocationSupported) + const revRegistry = await this.createRevocationRegistry(credentialDefinitionId) this.logger.log(`Revocation registry successfully created with ID ${revRegistry}`) } this.logger.debug('sendCredential with claims: ' + JSON.stringify(claims)) @@ -309,14 +314,7 @@ export class CredentialService { } // private methods - private async saveCredentialType( - credentialDefinitionId: string, - supportRevocation: boolean = false, - ): Promise { - if (!supportRevocation) { - await this.credentialRepository.save({ credentialDefinitionId }) - return null - } + private async createRevocationRegistry(credentialDefinitionId: string): Promise { const revocationRegistry = await this.apiClient.revocationRegistry.create({ credentialDefinitionId, maximumCredentialNumber: this.maximumCredentialNumber, From 9831c46f6077f21fd54aa8feaeb8234d76eb6217 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Fri, 17 Jan 2025 10:20:10 -0500 Subject: [PATCH 118/132] fix: change name to handleAcceptance/handleRejection --- packages/nestjs-client/src/credentials/credential.service.ts | 4 ++-- packages/nestjs-client/src/messages/message.service.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 7835e25..fe5c3f4 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -247,7 +247,7 @@ export class CredentialService { * @param threadId - The thread ID to link with the credential. * @throws Error if no credential is found with the specified connection ID. */ - async accept(connectionId: string, threadId: string): Promise { + async handleAcceptance(connectionId: string, threadId: string): Promise { const cred = await this.credentialRepository.findOne({ where: { connectionId, @@ -267,7 +267,7 @@ export class CredentialService { * @param threadId - The thread ID to link with the credential. * @throws Error if no credential is found with the specified connection ID. */ - async reject(connectionId: string, threadId: string): Promise { + async handleRejection(connectionId: string, threadId: string): Promise { const cred = await this.credentialRepository.findOne({ where: { connectionId, diff --git a/packages/nestjs-client/src/messages/message.service.ts b/packages/nestjs-client/src/messages/message.service.ts index 7bc6770..f8f09ae 100644 --- a/packages/nestjs-client/src/messages/message.service.ts +++ b/packages/nestjs-client/src/messages/message.service.ts @@ -57,8 +57,8 @@ export class MessageEventService { const isCredentialDone = msg.state === CredentialState.Done if (this.credentialService) { if (!msg.threadId) throw new Error('threadId is required for credential') - if (isCredentialDone) await this.credentialService.accept(msg.connectionId, msg.threadId) - else await this.credentialService.reject(msg.connectionId, msg.threadId) + if (isCredentialDone) await this.credentialService.handleAcceptance(msg.connectionId, msg.threadId) + else await this.credentialService.handleRejection(msg.connectionId, msg.threadId) } } catch (error) { this.logger.error(`Cannot create the registry: ${error}`) From 696e2850336d16c272df6e0a50c3bf307ce12f84 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Fri, 17 Jan 2025 10:54:11 -0500 Subject: [PATCH 119/132] fix: acept and reject only by thread --- .../src/credentials/credential.service.ts | 126 ++++++++---------- .../src/messages/message.service.ts | 4 +- 2 files changed, 61 insertions(+), 69 deletions(-) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index fe5c3f4..7c39276 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -158,83 +158,75 @@ export class CredentialService { } } - const { revocationRegistryDefinitionId, revocationRegistryIndex } = await this.entityManager.transaction( - async transaction => { - if (!revocationSupported) { - await transaction.save(CredentialEntity, { - connectionId, - credentialDefinitionId, - ...(refIdHash ? { refIdHash } : {}), - }) - return { - revocationRegistryDefinitionId: undefined, - revocationRegistryIndex: undefined, - } - } - const invalidRegistries = await transaction.find(CredentialEntity, { - select: ['revocationDefinitionId'], - where: { - credentialDefinitionId, - revocationRegistryIndex: Equal(this.maximumCredentialNumber - 1), - }, + const cred: CredentialEntity = await this.entityManager.transaction(async transaction => { + if (!revocationSupported) { + return await transaction.save(CredentialEntity, { + connectionId, + credentialDefinitionId, + ...(refIdHash ? { refIdHash } : {}), }) - const invalidRevocationIds = invalidRegistries.map(reg => reg.revocationDefinitionId) + } + const invalidRegistries = await transaction.find(CredentialEntity, { + select: ['revocationDefinitionId'], + where: { + credentialDefinitionId, + revocationRegistryIndex: Equal(this.maximumCredentialNumber - 1), + }, + }) + const invalidRevocationIds = invalidRegistries.map(reg => reg.revocationDefinitionId) - let lastCred = await transaction.findOne(CredentialEntity, { + let lastCred = await transaction.findOne(CredentialEntity, { + where: { + credentialDefinitionId, + revocationRegistryIndex: Not(Equal(this.maximumCredentialNumber)), + ...(invalidRevocationIds.length > 0 + ? { + revocationDefinitionId: Not(In(invalidRevocationIds)), + } + : {}), + }, + order: { revocationRegistryIndex: 'DESC' }, + lock: { mode: 'pessimistic_write' }, + }) + if (!lastCred || lastCred.revocationRegistryIndex == null) { + lastCred = await transaction.findOne(CredentialEntity, { where: { credentialDefinitionId, - revocationRegistryIndex: Not(Equal(this.maximumCredentialNumber)), - ...(invalidRevocationIds.length > 0 - ? { - revocationDefinitionId: Not(In(invalidRevocationIds)), - } - : {}), + revocationRegistryIndex: Equal(this.maximumCredentialNumber), }, - order: { revocationRegistryIndex: 'DESC' }, + order: { createdTs: 'DESC' }, lock: { mode: 'pessimistic_write' }, }) - if (!lastCred || lastCred.revocationRegistryIndex == null) { - lastCred = await transaction.findOne(CredentialEntity, { - where: { - credentialDefinitionId, - revocationRegistryIndex: Equal(this.maximumCredentialNumber), - }, - order: { createdTs: 'DESC' }, - lock: { mode: 'pessimistic_write' }, - }) - if (!lastCred) - throw new Error( - 'No valid registry definition found. Please restart the service and ensure the module is imported correctly', - ) - lastCred.revocationRegistryIndex = -1 - } + if (!lastCred) + throw new Error( + 'No valid registry definition found. Please restart the service and ensure the module is imported correctly', + ) + lastCred.revocationRegistryIndex = -1 + } - const newCredential = await transaction.save(CredentialEntity, { - connectionId, - credentialDefinitionId, - revocationDefinitionId: lastCred.revocationDefinitionId, - revocationRegistryIndex: lastCred.revocationRegistryIndex + 1, - ...(refIdHash ? { refIdHash } : {}), - maximumCredentialNumber: this.maximumCredentialNumber, - }) - return { - revocationRegistryDefinitionId: newCredential.revocationDefinitionId, - revocationRegistryIndex: newCredential.revocationRegistryIndex, - } - }, - ) + return await transaction.save(CredentialEntity, { + connectionId, + credentialDefinitionId, + revocationDefinitionId: lastCred.revocationDefinitionId, + revocationRegistryIndex: lastCred.revocationRegistryIndex + 1, + ...(refIdHash ? { refIdHash } : {}), + maximumCredentialNumber: this.maximumCredentialNumber, + }) + }) - await this.apiClient.messages.send( + const thread = await this.apiClient.messages.send( new CredentialIssuanceMessage({ connectionId, credentialDefinitionId, - revocationRegistryDefinitionId, - revocationRegistryIndex, + revocationRegistryDefinitionId: cred.revocationDefinitionId, + revocationRegistryIndex: cred.revocationRegistryIndex, claims: claims, }), ) - if (revocationRegistryIndex === this.maximumCredentialNumber - 1) { + cred.threadId = thread.id + await this.credentialRepository.save(cred) + if (cred.revocationRegistryIndex === this.maximumCredentialNumber - 1) { const revRegistry = await this.createRevocationRegistry(credentialDefinitionId) this.logger.log(`Revocation registry successfully created with ID ${revRegistry}`) } @@ -247,15 +239,15 @@ export class CredentialService { * @param threadId - The thread ID to link with the credential. * @throws Error if no credential is found with the specified connection ID. */ - async handleAcceptance(connectionId: string, threadId: string): Promise { + async handleAcceptance(threadId: string): Promise { const cred = await this.credentialRepository.findOne({ where: { - connectionId, + threadId, revoked: false, }, order: { createdTs: 'DESC' }, }) - if (!cred) throw new Error(`Credential not found with connectionId: ${connectionId}`) + if (!cred) throw new Error(`Credential not found with connectionId: ${threadId}`) cred.threadId = threadId await this.credentialRepository.save(cred) @@ -267,15 +259,15 @@ export class CredentialService { * @param threadId - The thread ID to link with the credential. * @throws Error if no credential is found with the specified connection ID. */ - async handleRejection(connectionId: string, threadId: string): Promise { + async handleRejection(threadId: string): Promise { const cred = await this.credentialRepository.findOne({ where: { - connectionId, + threadId, revoked: false, }, order: { createdTs: 'DESC' }, }) - if (!cred) throw new Error(`Credential with connectionId ${connectionId} not found.`) + if (!cred) throw new Error(`Credential with connectionId ${threadId} not found.`) cred.threadId = threadId cred.revoked = true diff --git a/packages/nestjs-client/src/messages/message.service.ts b/packages/nestjs-client/src/messages/message.service.ts index f8f09ae..507b26a 100644 --- a/packages/nestjs-client/src/messages/message.service.ts +++ b/packages/nestjs-client/src/messages/message.service.ts @@ -57,8 +57,8 @@ export class MessageEventService { const isCredentialDone = msg.state === CredentialState.Done if (this.credentialService) { if (!msg.threadId) throw new Error('threadId is required for credential') - if (isCredentialDone) await this.credentialService.handleAcceptance(msg.connectionId, msg.threadId) - else await this.credentialService.handleRejection(msg.connectionId, msg.threadId) + if (isCredentialDone) await this.credentialService.handleAcceptance(msg.threadId) + else await this.credentialService.handleRejection(msg.threadId) } } catch (error) { this.logger.error(`Cannot create the registry: ${error}`) From 87e7586da4ef84430d6bc85bb2258b8e7d93a147 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Fri, 17 Jan 2025 11:22:01 -0500 Subject: [PATCH 120/132] fix: implement to CredentialStatus --- .../src/credentials/credential.entity.ts | 6 ++++-- .../src/credentials/credential.service.ts | 16 +++++++++------- packages/nestjs-client/src/types.ts | 7 +++++++ 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/packages/nestjs-client/src/credentials/credential.entity.ts b/packages/nestjs-client/src/credentials/credential.entity.ts index c0ebe9c..b9c8c3e 100644 --- a/packages/nestjs-client/src/credentials/credential.entity.ts +++ b/packages/nestjs-client/src/credentials/credential.entity.ts @@ -1,5 +1,7 @@ import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm' +import { CredentialStatus } from '../types' + @Entity('credentials') export class CredentialEntity { @PrimaryGeneratedColumn('uuid') @@ -23,8 +25,8 @@ export class CredentialEntity { @Column({ type: 'varchar', nullable: true }) refIdHash?: string - @Column({ nullable: true, default: false }) - revoked?: boolean + @Column({ nullable: true }) + status?: CredentialStatus @CreateDateColumn() createdTs?: Date diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 7c39276..e78e80d 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -5,7 +5,7 @@ import { Inject, Injectable, Logger } from '@nestjs/common' import { InjectRepository } from '@nestjs/typeorm' import { EntityManager, Equal, In, Not, Repository } from 'typeorm' -import { CredentialOptions } from '../types' +import { CredentialOptions, CredentialStatus } from '../types' import { CredentialEntity } from './credential.entity' @@ -146,7 +146,7 @@ export class CredentialService { const creds = await this.credentialRepository.find({ where: { - revoked: false, + status: CredentialStatus.ACCEPTED, ...(refIdHash ? { refIdHash } : {}), }, }) @@ -225,6 +225,7 @@ export class CredentialService { }), ) cred.threadId = thread.id + cred.status = CredentialStatus.OFFERED await this.credentialRepository.save(cred) if (cred.revocationRegistryIndex === this.maximumCredentialNumber - 1) { const revRegistry = await this.createRevocationRegistry(credentialDefinitionId) @@ -243,13 +244,14 @@ export class CredentialService { const cred = await this.credentialRepository.findOne({ where: { threadId, - revoked: false, + status: CredentialStatus.OFFERED, }, order: { createdTs: 'DESC' }, }) if (!cred) throw new Error(`Credential not found with connectionId: ${threadId}`) cred.threadId = threadId + cred.status = CredentialStatus.ACCEPTED await this.credentialRepository.save(cred) } @@ -263,14 +265,14 @@ export class CredentialService { const cred = await this.credentialRepository.findOne({ where: { threadId, - revoked: false, + status: CredentialStatus.OFFERED, }, order: { createdTs: 'DESC' }, }) if (!cred) throw new Error(`Credential with connectionId ${threadId} not found.`) cred.threadId = threadId - cred.revoked = true + cred.status = CredentialStatus.REJECTED await this.credentialRepository.save(cred) } @@ -282,14 +284,14 @@ export class CredentialService { async revoke(connectionId: string, options?: { refId?: string }): Promise { const refIdHash = options?.refId ? this.hash(options.refId) : null const cred = await this.credentialRepository.findOne({ - where: { connectionId, revoked: false, ...(refIdHash ? { refIdHash } : {}) }, + where: { connectionId, status: CredentialStatus.ACCEPTED, ...(refIdHash ? { refIdHash } : {}) }, order: { createdTs: 'DESC' }, }) if (!cred || !cred.connectionId) { throw new Error(`Credencial with connectionId ${connectionId} not found.`) } - cred.revoked = true + cred.status = CredentialStatus.REVOKED await this.credentialRepository.save(cred) const credentialTypes = await this.apiClient.credentialTypes.getAll() diff --git a/packages/nestjs-client/src/types.ts b/packages/nestjs-client/src/types.ts index 4c60d25..82a3e68 100644 --- a/packages/nestjs-client/src/types.ts +++ b/packages/nestjs-client/src/types.ts @@ -36,3 +36,10 @@ export interface EventsModuleOptions { version?: ApiVersion } } + +export enum CredentialStatus { + OFFERED = 'offered', + ACCEPTED = 'accepted', + REJECTED = 'rejected', + REVOKED = 'revoked', +} From 4425e97038678913ad9a24c8420e7f84ad193297 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Fri, 17 Jan 2025 11:40:04 -0500 Subject: [PATCH 121/132] fix: acept and reject only by thread --- packages/nestjs-client/src/credentials/credential.service.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index e78e80d..293291b 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -250,7 +250,6 @@ export class CredentialService { }) if (!cred) throw new Error(`Credential not found with connectionId: ${threadId}`) - cred.threadId = threadId cred.status = CredentialStatus.ACCEPTED await this.credentialRepository.save(cred) } @@ -271,7 +270,6 @@ export class CredentialService { }) if (!cred) throw new Error(`Credential with connectionId ${threadId} not found.`) - cred.threadId = threadId cred.status = CredentialStatus.REJECTED await this.credentialRepository.save(cred) } From 692f64061257883fd9b933b416edb9e66257590f Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Fri, 17 Jan 2025 11:46:30 -0500 Subject: [PATCH 122/132] fix: acept and reject only by thread --- packages/nestjs-client/src/credentials/credential.service.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 293291b..7228e3f 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -152,7 +152,6 @@ export class CredentialService { }) if (creds && revokeIfAlreadyIssued) { for (const cred of creds) { - cred.connectionId = connectionId await this.credentialRepository.save(cred) await this.revoke(connectionId, { refId: options?.refId ?? undefined }) } @@ -248,7 +247,7 @@ export class CredentialService { }, order: { createdTs: 'DESC' }, }) - if (!cred) throw new Error(`Credential not found with connectionId: ${threadId}`) + if (!cred) throw new Error(`Credential not found with threadId: ${threadId}`) cred.status = CredentialStatus.ACCEPTED await this.credentialRepository.save(cred) @@ -268,7 +267,7 @@ export class CredentialService { }, order: { createdTs: 'DESC' }, }) - if (!cred) throw new Error(`Credential with connectionId ${threadId} not found.`) + if (!cred) throw new Error(`Credential with threadId ${threadId} not found.`) cred.status = CredentialStatus.REJECTED await this.credentialRepository.save(cred) From 5763e9fa65398b8eefb28a5ea65483754d0ae986 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Sat, 18 Jan 2025 13:23:30 -0500 Subject: [PATCH 123/132] feat: Creation of the RevocationRegistryEntity for managing revocation records --- examples/demo-dts/docker-compose.yml | 2 + examples/demo-dts/src/core.module.ts | 6 +- .../src/controllers/message/MessageService.ts | 7 +- packages/main/src/events/MessageEvents.ts | 2 + .../src/credentials/credential.entity.ts | 22 ++- .../src/credentials/credential.service.ts | 150 ++++++++---------- .../nestjs-client/src/credentials/index.ts | 1 + .../credentials/revocation-registry.entity.ts | 22 +++ 8 files changed, 116 insertions(+), 96 deletions(-) create mode 100644 packages/nestjs-client/src/credentials/revocation-registry.entity.ts diff --git a/examples/demo-dts/docker-compose.yml b/examples/demo-dts/docker-compose.yml index db3de2b..6c516c8 100644 --- a/examples/demo-dts/docker-compose.yml +++ b/examples/demo-dts/docker-compose.yml @@ -25,6 +25,8 @@ services: - AGENT_WALLET_KEY_DERIVATION_METHOD=ARGON2I_INT - AGENT_INVITATION_BASE_URL=https://hologram.zone/ - REDIS_HOST=redis + volumes: + - ./afj:/root/.afj chatbot-dts: build: diff --git a/examples/demo-dts/src/core.module.ts b/examples/demo-dts/src/core.module.ts index 56929ad..b40bc1a 100644 --- a/examples/demo-dts/src/core.module.ts +++ b/examples/demo-dts/src/core.module.ts @@ -2,13 +2,13 @@ import { Global, Module } from '@nestjs/common' import { SessionEntity } from '@/models' import { CoreService } from '@/core.service' import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm' -import { ConnectionEntity, CredentialEntity } from '@2060.io/service-agent-nestjs-client' +import { ConnectionEntity, CredentialEntity, RevocationRegistryEntity } from '@2060.io/service-agent-nestjs-client' import { ConfigModule, ConfigService } from '@nestjs/config' @Global() @Module({ imports: [ - TypeOrmModule.forFeature([ConnectionEntity, CredentialEntity, SessionEntity]), + TypeOrmModule.forFeature([ConnectionEntity, CredentialEntity, RevocationRegistryEntity, SessionEntity]), TypeOrmModule.forRootAsync({ imports: [ConfigModule], useFactory: async (configService: ConfigService): Promise => ({ @@ -18,7 +18,7 @@ import { ConfigModule, ConfigService } from '@nestjs/config' username: configService.get('appConfig.postgresUser'), password: configService.get('appConfig.postgresPassword'), database: configService.get('appConfig.postgresDbName'), - entities: [ConnectionEntity, CredentialEntity, SessionEntity], + entities: [ConnectionEntity, CredentialEntity, RevocationRegistryEntity, SessionEntity], synchronize: true, ssl: false, logging: false, diff --git a/packages/main/src/controllers/message/MessageService.ts b/packages/main/src/controllers/message/MessageService.ts index 342c5d8..f76d929 100644 --- a/packages/main/src/controllers/message/MessageService.ts +++ b/packages/main/src/controllers/message/MessageService.ts @@ -241,7 +241,12 @@ export class MessageService { } else if (messageType === CredentialRevocationMessage.type) { const msg = JsonTransformer.fromJSON(message, CredentialRevocationMessage) - const credentials = await agent.credentials.findAllByQuery({ threadId: msg.threadId }) + let credentials = await agent.credentials.findAllByQuery({ threadId: msg.threadId }) + if (!credentials?.length && msg.threadId) { + const record = await agent.genericRecords.findById(msg.threadId) + const threadId = record?.getTag('messageId') as string + credentials = await agent.credentials.findAllByQuery({ threadId }) + } if (credentials && credentials.length > 0) { for (const credential of credentials) { const isRevocable = Boolean( diff --git a/packages/main/src/events/MessageEvents.ts b/packages/main/src/events/MessageEvents.ts index b505e6b..647c671 100644 --- a/packages/main/src/events/MessageEvents.ts +++ b/packages/main/src/events/MessageEvents.ts @@ -330,6 +330,8 @@ export const messageEvents = async (agent: ServiceAgent, config: ServerConfig) = config.logger.debug(`CredentialStateChangedEvent received. Record id: ${JSON.stringify(payload.credentialRecord.id)}, state: ${JSON.stringify(payload.credentialRecord.state)}`) const record = payload.credentialRecord + const messageRecord = await agent.genericRecords.findById(record.threadId) + record.threadId = (messageRecord?.getTag('messageId') as string) ?? record.threadId if (record.state === CredentialState.ProposalReceived) { const credentialProposalMessage = await agent.credentials.findProposalMessage(record.id) diff --git a/packages/nestjs-client/src/credentials/credential.entity.ts b/packages/nestjs-client/src/credentials/credential.entity.ts index b9c8c3e..6290739 100644 --- a/packages/nestjs-client/src/credentials/credential.entity.ts +++ b/packages/nestjs-client/src/credentials/credential.entity.ts @@ -1,7 +1,17 @@ -import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm' +import { + Entity, + Column, + PrimaryGeneratedColumn, + CreateDateColumn, + UpdateDateColumn, + ManyToOne, + JoinColumn, +} from 'typeorm' import { CredentialStatus } from '../types' +import { RevocationRegistryEntity } from './revocation-registry.entity' + @Entity('credentials') export class CredentialEntity { @PrimaryGeneratedColumn('uuid') @@ -10,12 +20,6 @@ export class CredentialEntity { @Column({ type: 'varchar', nullable: false }) credentialDefinitionId!: string - @Column({ type: 'varchar', nullable: true }) - revocationDefinitionId?: string - - @Column({ type: 'integer', nullable: true }) - revocationRegistryIndex?: number - @Column({ type: 'varchar', nullable: true }) connectionId?: string @@ -28,6 +32,10 @@ export class CredentialEntity { @Column({ nullable: true }) status?: CredentialStatus + @ManyToOne(() => RevocationRegistryEntity, { nullable: true }) + @JoinColumn({ name: 'revocationRegistryId' }) + revocationRegistry?: RevocationRegistryEntity + @CreateDateColumn() createdTs?: Date diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 7228e3f..81c1ae5 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -3,11 +3,12 @@ import { Claim, CredentialIssuanceMessage, CredentialRevocationMessage } from '@ import { Sha256, utils } from '@credo-ts/core' import { Inject, Injectable, Logger } from '@nestjs/common' import { InjectRepository } from '@nestjs/typeorm' -import { EntityManager, Equal, In, Not, Repository } from 'typeorm' +import { EntityManager, FindOneOptions, Repository } from 'typeorm' import { CredentialOptions, CredentialStatus } from '../types' import { CredentialEntity } from './credential.entity' +import { RevocationRegistryEntity } from './revocation-registry.entity' @Injectable() export class CredentialService { @@ -18,12 +19,11 @@ export class CredentialService { private readonly apiVersion: ApiVersion private readonly apiClient: ApiClient - //Credential type definitions - private maximumCredentialNumber: number = 1000 - constructor( @InjectRepository(CredentialEntity) private readonly credentialRepository: Repository, + @InjectRepository(RevocationRegistryEntity) + private readonly revocationRepository: Repository, @Inject('GLOBAL_MODULE_OPTIONS') private options: CredentialOptions, private readonly entityManager: EntityManager, ) { @@ -65,8 +65,6 @@ export class CredentialService { const { supportRevocation, maximumCredentialNumber } = options const credentialTypes = await this.apiClient.credentialTypes.getAll() - if (maximumCredentialNumber !== undefined) this.maximumCredentialNumber = maximumCredentialNumber - const credentialType = credentialTypes.find( credType => credType.name === name && credType.version === version, ) @@ -80,11 +78,9 @@ export class CredentialService { }) if (supportRevocation) { - await this.credentialRepository.save({ credentialDefinitionId: credentialType.id }) - } else { // Both records are created to handle multiple credentials - await this.createRevocationRegistry(credentialType.id) - await this.createRevocationRegistry(credentialType.id) + await this.createRevocationRegistry(credentialType.id, maximumCredentialNumber) + await this.createRevocationRegistry(credentialType.id, maximumCredentialNumber) } } } @@ -152,8 +148,7 @@ export class CredentialService { }) if (creds && revokeIfAlreadyIssued) { for (const cred of creds) { - await this.credentialRepository.save(cred) - await this.revoke(connectionId, { refId: options?.refId ?? undefined }) + await this.revoke(connectionId, cred.threadId) } } @@ -165,52 +160,20 @@ export class CredentialService { ...(refIdHash ? { refIdHash } : {}), }) } - const invalidRegistries = await transaction.find(CredentialEntity, { - select: ['revocationDefinitionId'], - where: { - credentialDefinitionId, - revocationRegistryIndex: Equal(this.maximumCredentialNumber - 1), - }, - }) - const invalidRevocationIds = invalidRegistries.map(reg => reg.revocationDefinitionId) - - let lastCred = await transaction.findOne(CredentialEntity, { - where: { - credentialDefinitionId, - revocationRegistryIndex: Not(Equal(this.maximumCredentialNumber)), - ...(invalidRevocationIds.length > 0 - ? { - revocationDefinitionId: Not(In(invalidRevocationIds)), - } - : {}), - }, - order: { revocationRegistryIndex: 'DESC' }, - lock: { mode: 'pessimistic_write' }, - }) - if (!lastCred || lastCred.revocationRegistryIndex == null) { - lastCred = await transaction.findOne(CredentialEntity, { - where: { - credentialDefinitionId, - revocationRegistryIndex: Equal(this.maximumCredentialNumber), - }, - order: { createdTs: 'DESC' }, - lock: { mode: 'pessimistic_write' }, - }) - - if (!lastCred) - throw new Error( - 'No valid registry definition found. Please restart the service and ensure the module is imported correctly', - ) - lastCred.revocationRegistryIndex = -1 - } + let lastCred = await transaction + .createQueryBuilder(RevocationRegistryEntity, 'registry') + .where('registry.revocationRegistryIndex != registry.maximumCredentialNumber') + .orderBy('registry.revocationRegistryIndex', 'DESC') + .setLock('pessimistic_write') + .getOne() + if (!lastCred) lastCred = await this.createRevocationRegistry(credentialDefinitionId) + await transaction.save(RevocationRegistryEntity, lastCred) return await transaction.save(CredentialEntity, { connectionId, credentialDefinitionId, - revocationDefinitionId: lastCred.revocationDefinitionId, - revocationRegistryIndex: lastCred.revocationRegistryIndex + 1, + revocationRegistry: lastCred, ...(refIdHash ? { refIdHash } : {}), - maximumCredentialNumber: this.maximumCredentialNumber, }) }) @@ -218,17 +181,26 @@ export class CredentialService { new CredentialIssuanceMessage({ connectionId, credentialDefinitionId, - revocationRegistryDefinitionId: cred.revocationDefinitionId, - revocationRegistryIndex: cred.revocationRegistryIndex, + revocationRegistryDefinitionId: cred.revocationRegistry?.revocationDefinitionId, + revocationRegistryIndex: cred.revocationRegistry?.revocationRegistryIndex, claims: claims, }), ) cred.threadId = thread.id cred.status = CredentialStatus.OFFERED await this.credentialRepository.save(cred) - if (cred.revocationRegistryIndex === this.maximumCredentialNumber - 1) { - const revRegistry = await this.createRevocationRegistry(credentialDefinitionId) - this.logger.log(`Revocation registry successfully created with ID ${revRegistry}`) + if (cred.revocationRegistry) { + cred.revocationRegistry.revocationRegistryIndex += 1 + this.revocationRepository.save(cred.revocationRegistry) + if ( + cred.revocationRegistry?.revocationRegistryIndex === cred.revocationRegistry?.maximumCredentialNumber + ) { + const revRegistry = await this.createRevocationRegistry( + credentialDefinitionId, + cred.revocationRegistry?.maximumCredentialNumber, + ) + this.logger.log(`Revocation registry successfully created with ID ${revRegistry}`) + } } this.logger.debug('sendCredential with claims: ' + JSON.stringify(claims)) } @@ -275,18 +247,18 @@ export class CredentialService { /** * Revokes a credential associated with the provided thread ID. - * @param threadId - The thread ID linked to the credential to revoke. - * @throws Error if no credential is found with the specified thread ID or if the credential has no connection ID. + * @param connectionId - The connection ID to send the revoke. + * @param threadId - (Optional) The thread ID linked to the credential to revoke. + * @throws Error if no credential is found with the specified thread ID or connection ID, or if the credential has no connection ID. */ - async revoke(connectionId: string, options?: { refId?: string }): Promise { - const refIdHash = options?.refId ? this.hash(options.refId) : null - const cred = await this.credentialRepository.findOne({ - where: { connectionId, status: CredentialStatus.ACCEPTED, ...(refIdHash ? { refIdHash } : {}) }, - order: { createdTs: 'DESC' }, - }) - if (!cred || !cred.connectionId) { - throw new Error(`Credencial with connectionId ${connectionId} not found.`) - } + async revoke(connectionId: string, threadId?: string): Promise { + const options: FindOneOptions = threadId + ? { where: { threadId, status: CredentialStatus.ACCEPTED } } + : { where: { connectionId, status: CredentialStatus.ACCEPTED }, order: { createdTs: 'DESC' } } + const cred = await this.credentialRepository.findOne(options) + + if (!cred) + throw new Error(`Credential not found with threadId "${threadId}" or connectionId "${connectionId}".`) cred.status = CredentialStatus.REVOKED await this.credentialRepository.save(cred) @@ -294,30 +266,38 @@ export class CredentialService { const credentialTypes = await this.apiClient.credentialTypes.getAll() const credentialType = credentialTypes.find(credType => credType.id === cred.credentialDefinitionId) ?? credentialTypes[0] - credentialType.revocationSupported && - (await this.apiClient.messages.send( - new CredentialRevocationMessage({ - connectionId: cred.connectionId, - threadId: cred?.threadId, - }), - )) - this.logger.log(`Revoke Credential: ${cred.id}`) + if (!credentialType.revocationSupported) { + this.logger.warn(`Credential definition ${cred.credentialDefinitionId} does not support revocation.`) + return + } + + await this.apiClient.messages.send( + new CredentialRevocationMessage({ + connectionId, + threadId: cred?.threadId, + }), + ) + + this.logger.log(`Credential revoked: ${cred.id}`) } // private methods - private async createRevocationRegistry(credentialDefinitionId: string): Promise { - const revocationRegistry = await this.apiClient.revocationRegistry.create({ + private async createRevocationRegistry( + credentialDefinitionId: string, + maximumCredentialNumber: number = 1000, + ): Promise { + const revocationDefinitionId = await this.apiClient.revocationRegistry.create({ credentialDefinitionId, - maximumCredentialNumber: this.maximumCredentialNumber, + maximumCredentialNumber, }) - if (!revocationRegistry) + if (!revocationDefinitionId) throw new Error( `Unable to create a new revocation registry for CredentialDefinitionId: ${credentialDefinitionId}`, ) - await this.credentialRepository.save({ - credentialDefinitionId, - revocationDefinitionId: revocationRegistry, - revocationRegistryIndex: this.maximumCredentialNumber, + const revocationRegistry = await this.revocationRepository.save({ + revocationDefinitionId, + revocationRegistryIndex: 0, + maximumCredentialNumber, }) return revocationRegistry } diff --git a/packages/nestjs-client/src/credentials/index.ts b/packages/nestjs-client/src/credentials/index.ts index 8bb2212..e75a137 100644 --- a/packages/nestjs-client/src/credentials/index.ts +++ b/packages/nestjs-client/src/credentials/index.ts @@ -1,3 +1,4 @@ export * from './credential.entity' export * from './credential.service' export * from './credential.module' +export * from './revocation-registry.entity' diff --git a/packages/nestjs-client/src/credentials/revocation-registry.entity.ts b/packages/nestjs-client/src/credentials/revocation-registry.entity.ts new file mode 100644 index 0000000..a815a3d --- /dev/null +++ b/packages/nestjs-client/src/credentials/revocation-registry.entity.ts @@ -0,0 +1,22 @@ +import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm' + +@Entity('revocation_registries') +export class RevocationRegistryEntity { + @PrimaryGeneratedColumn('uuid') + id!: string + + @Column({ type: 'varchar', nullable: false }) + revocationDefinitionId!: string + + @Column({ type: 'integer', nullable: false, default: 0 }) + revocationRegistryIndex!: number + + @Column({ type: 'integer', nullable: false, default: 1000 }) + maximumCredentialNumber!: number + + @CreateDateColumn() + createdTs?: Date + + @UpdateDateColumn() + updatedTs?: Date +} From 0641fe22fad67e86ba308307890088974eb018e9 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Sat, 18 Jan 2025 13:32:49 -0500 Subject: [PATCH 124/132] feat: Creation of the RevocationRegistryEntity for managing revocation records --- .../nestjs-client/src/credentials/credential.service.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 81c1ae5..1a2e24b 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -3,7 +3,7 @@ import { Claim, CredentialIssuanceMessage, CredentialRevocationMessage } from '@ import { Sha256, utils } from '@credo-ts/core' import { Inject, Injectable, Logger } from '@nestjs/common' import { InjectRepository } from '@nestjs/typeorm' -import { EntityManager, FindOneOptions, Repository } from 'typeorm' +import { EntityManager, FindOneOptions, IsNull, Not, Repository } from 'typeorm' import { CredentialOptions, CredentialStatus } from '../types' @@ -254,10 +254,13 @@ export class CredentialService { async revoke(connectionId: string, threadId?: string): Promise { const options: FindOneOptions = threadId ? { where: { threadId, status: CredentialStatus.ACCEPTED } } - : { where: { connectionId, status: CredentialStatus.ACCEPTED }, order: { createdTs: 'DESC' } } + : { + where: { connectionId, status: CredentialStatus.ACCEPTED, threadId: Not(IsNull()) }, + order: { createdTs: 'DESC' }, + } const cred = await this.credentialRepository.findOne(options) - if (!cred) + if (!cred) throw new Error(`Credential not found with threadId "${threadId}" or connectionId "${connectionId}".`) cred.status = CredentialStatus.REVOKED From a10c1e441833b52915471e2288f60141d7ad2c4c Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Tue, 21 Jan 2025 14:23:56 -0300 Subject: [PATCH 125/132] fix: change Service Agent message threadId Signed-off-by: Ariel Gentile --- packages/main/src/events/MessageEvents.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/main/src/events/MessageEvents.ts b/packages/main/src/events/MessageEvents.ts index 647c671..85078dd 100644 --- a/packages/main/src/events/MessageEvents.ts +++ b/packages/main/src/events/MessageEvents.ts @@ -331,14 +331,13 @@ export const messageEvents = async (agent: ServiceAgent, config: ServerConfig) = ${JSON.stringify(payload.credentialRecord.id)}, state: ${JSON.stringify(payload.credentialRecord.state)}`) const record = payload.credentialRecord const messageRecord = await agent.genericRecords.findById(record.threadId) - record.threadId = (messageRecord?.getTag('messageId') as string) ?? record.threadId if (record.state === CredentialState.ProposalReceived) { const credentialProposalMessage = await agent.credentials.findProposalMessage(record.id) const message = new CredentialRequestMessage({ connectionId: record.connectionId!, id: record.id, - threadId: credentialProposalMessage?.threadId, + threadId: (messageRecord?.getTag('messageId') as string) ?? record.threadId, claims: credentialProposalMessage?.credentialPreview?.attributes.map( p => new Claim({ name: p.name, value: p.value, mimeType: p.mimeType }), From 96876b42535bd8e00b0ca64cafb6ccd5c8747af0 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Tue, 21 Jan 2025 16:52:17 -0500 Subject: [PATCH 126/132] fix: add credentialDefinitionId to revocation registry entity --- .../nestjs-client/src/credentials/credential.entity.ts | 6 +++--- .../nestjs-client/src/credentials/credential.service.ts | 7 +++++-- .../src/credentials/revocation-registry.entity.ts | 3 +++ 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/nestjs-client/src/credentials/credential.entity.ts b/packages/nestjs-client/src/credentials/credential.entity.ts index 6290739..b527f32 100644 --- a/packages/nestjs-client/src/credentials/credential.entity.ts +++ b/packages/nestjs-client/src/credentials/credential.entity.ts @@ -17,9 +17,6 @@ export class CredentialEntity { @PrimaryGeneratedColumn('uuid') id!: string - @Column({ type: 'varchar', nullable: false }) - credentialDefinitionId!: string - @Column({ type: 'varchar', nullable: true }) connectionId?: string @@ -32,6 +29,9 @@ export class CredentialEntity { @Column({ nullable: true }) status?: CredentialStatus + @Column({ type: 'integer', nullable: true }) + revocationIndex?: number + @ManyToOne(() => RevocationRegistryEntity, { nullable: true }) @JoinColumn({ name: 'revocationRegistryId' }) revocationRegistry?: RevocationRegistryEntity diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 1a2e24b..63db75e 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -268,9 +268,12 @@ export class CredentialService { const credentialTypes = await this.apiClient.credentialTypes.getAll() const credentialType = - credentialTypes.find(credType => credType.id === cred.credentialDefinitionId) ?? credentialTypes[0] + credentialTypes.find(credType => credType.id === cred.revocationRegistry?.credentialDefinitionId) ?? + credentialTypes[0] if (!credentialType.revocationSupported) { - this.logger.warn(`Credential definition ${cred.credentialDefinitionId} does not support revocation.`) + this.logger.warn( + `Credential definition ${cred.revocationRegistry?.credentialDefinitionId} does not support revocation.`, + ) return } diff --git a/packages/nestjs-client/src/credentials/revocation-registry.entity.ts b/packages/nestjs-client/src/credentials/revocation-registry.entity.ts index a815a3d..7929b58 100644 --- a/packages/nestjs-client/src/credentials/revocation-registry.entity.ts +++ b/packages/nestjs-client/src/credentials/revocation-registry.entity.ts @@ -5,6 +5,9 @@ export class RevocationRegistryEntity { @PrimaryGeneratedColumn('uuid') id!: string + @Column({ type: 'varchar', nullable: false }) + credentialDefinitionId!: string + @Column({ type: 'varchar', nullable: false }) revocationDefinitionId!: string From bc2386e06e2f9938155a998bd01130bad856b1a9 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Tue, 21 Jan 2025 17:13:46 -0500 Subject: [PATCH 127/132] fix: add credentialDefinitionId to where --- packages/nestjs-client/src/credentials/credential.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 63db75e..4a6453b 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -163,6 +163,7 @@ export class CredentialService { let lastCred = await transaction .createQueryBuilder(RevocationRegistryEntity, 'registry') .where('registry.revocationRegistryIndex != registry.maximumCredentialNumber') + .andWhere('registry.credentialDefinitionId = :credentialDefinitionId', { credentialDefinitionId }) .orderBy('registry.revocationRegistryIndex', 'DESC') .setLock('pessimistic_write') .getOne() From 27e8562defaaf6f81ea9b8641bb995796d4c24bc Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Tue, 21 Jan 2025 20:06:58 -0500 Subject: [PATCH 128/132] fix: implemented currentIndex --- .../nestjs-client/src/credentials/credential.service.ts | 6 +++--- .../src/credentials/revocation-registry.entity.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 4a6453b..ed1dc1a 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -183,7 +183,7 @@ export class CredentialService { connectionId, credentialDefinitionId, revocationRegistryDefinitionId: cred.revocationRegistry?.revocationDefinitionId, - revocationRegistryIndex: cred.revocationRegistry?.revocationRegistryIndex, + revocationRegistryIndex: cred.revocationRegistry?.currentIndex, claims: claims, }), ) @@ -191,10 +191,10 @@ export class CredentialService { cred.status = CredentialStatus.OFFERED await this.credentialRepository.save(cred) if (cred.revocationRegistry) { - cred.revocationRegistry.revocationRegistryIndex += 1 + cred.revocationRegistry.currentIndex += 1 this.revocationRepository.save(cred.revocationRegistry) if ( - cred.revocationRegistry?.revocationRegistryIndex === cred.revocationRegistry?.maximumCredentialNumber + cred.revocationRegistry?.currentIndex === cred.revocationRegistry?.maximumCredentialNumber ) { const revRegistry = await this.createRevocationRegistry( credentialDefinitionId, diff --git a/packages/nestjs-client/src/credentials/revocation-registry.entity.ts b/packages/nestjs-client/src/credentials/revocation-registry.entity.ts index 7929b58..aa664df 100644 --- a/packages/nestjs-client/src/credentials/revocation-registry.entity.ts +++ b/packages/nestjs-client/src/credentials/revocation-registry.entity.ts @@ -12,7 +12,7 @@ export class RevocationRegistryEntity { revocationDefinitionId!: string @Column({ type: 'integer', nullable: false, default: 0 }) - revocationRegistryIndex!: number + currentIndex!: number @Column({ type: 'integer', nullable: false, default: 1000 }) maximumCredentialNumber!: number From 061115e4c72bac3422c10295aae9ef7a84a6c13f Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Tue, 21 Jan 2025 20:21:51 -0500 Subject: [PATCH 129/132] fix: update revocationRegistryIndex current index --- .../src/credentials/credential.entity.ts | 2 +- .../src/credentials/credential.service.ts | 13 +++++-------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/nestjs-client/src/credentials/credential.entity.ts b/packages/nestjs-client/src/credentials/credential.entity.ts index b527f32..b3b42c0 100644 --- a/packages/nestjs-client/src/credentials/credential.entity.ts +++ b/packages/nestjs-client/src/credentials/credential.entity.ts @@ -30,7 +30,7 @@ export class CredentialEntity { status?: CredentialStatus @Column({ type: 'integer', nullable: true }) - revocationIndex?: number + revocationRegistryIndex?: number @ManyToOne(() => RevocationRegistryEntity, { nullable: true }) @JoinColumn({ name: 'revocationRegistryId' }) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index ed1dc1a..8649340 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -156,15 +156,14 @@ export class CredentialService { if (!revocationSupported) { return await transaction.save(CredentialEntity, { connectionId, - credentialDefinitionId, ...(refIdHash ? { refIdHash } : {}), }) } let lastCred = await transaction .createQueryBuilder(RevocationRegistryEntity, 'registry') - .where('registry.revocationRegistryIndex != registry.maximumCredentialNumber') + .where('registry.currentIndex != registry.maximumCredentialNumber') .andWhere('registry.credentialDefinitionId = :credentialDefinitionId', { credentialDefinitionId }) - .orderBy('registry.revocationRegistryIndex', 'DESC') + .orderBy('registry.currentIndex', 'DESC') .setLock('pessimistic_write') .getOne() if (!lastCred) lastCred = await this.createRevocationRegistry(credentialDefinitionId) @@ -172,7 +171,7 @@ export class CredentialService { await transaction.save(RevocationRegistryEntity, lastCred) return await transaction.save(CredentialEntity, { connectionId, - credentialDefinitionId, + revocationRegistryIndex: lastCred.currentIndex, revocationRegistry: lastCred, ...(refIdHash ? { refIdHash } : {}), }) @@ -193,9 +192,7 @@ export class CredentialService { if (cred.revocationRegistry) { cred.revocationRegistry.currentIndex += 1 this.revocationRepository.save(cred.revocationRegistry) - if ( - cred.revocationRegistry?.currentIndex === cred.revocationRegistry?.maximumCredentialNumber - ) { + if (cred.revocationRegistry?.currentIndex === cred.revocationRegistry?.maximumCredentialNumber) { const revRegistry = await this.createRevocationRegistry( credentialDefinitionId, cred.revocationRegistry?.maximumCredentialNumber, @@ -303,7 +300,7 @@ export class CredentialService { ) const revocationRegistry = await this.revocationRepository.save({ revocationDefinitionId, - revocationRegistryIndex: 0, + currentIndex: 0, maximumCredentialNumber, }) return revocationRegistry From 6b1c4de93ae9a39055359290e90ce449f4d797cd Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Tue, 21 Jan 2025 20:28:50 -0500 Subject: [PATCH 130/132] fix: add flow record --- packages/main/src/events/MessageEvents.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/main/src/events/MessageEvents.ts b/packages/main/src/events/MessageEvents.ts index 85078dd..6446bd7 100644 --- a/packages/main/src/events/MessageEvents.ts +++ b/packages/main/src/events/MessageEvents.ts @@ -330,14 +330,14 @@ export const messageEvents = async (agent: ServiceAgent, config: ServerConfig) = config.logger.debug(`CredentialStateChangedEvent received. Record id: ${JSON.stringify(payload.credentialRecord.id)}, state: ${JSON.stringify(payload.credentialRecord.state)}`) const record = payload.credentialRecord - const messageRecord = await agent.genericRecords.findById(record.threadId) + const flowRecord = await agent.genericRecords.findById(record.threadId) if (record.state === CredentialState.ProposalReceived) { const credentialProposalMessage = await agent.credentials.findProposalMessage(record.id) const message = new CredentialRequestMessage({ connectionId: record.connectionId!, id: record.id, - threadId: (messageRecord?.getTag('messageId') as string) ?? record.threadId, + threadId: (flowRecord?.getTag('messageId') as string) ?? record.threadId, claims: credentialProposalMessage?.credentialPreview?.attributes.map( p => new Claim({ name: p.name, value: p.value, mimeType: p.mimeType }), From 55497d56584cced60049f09f08d3f70eb4991cc1 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Tue, 21 Jan 2025 20:55:23 -0500 Subject: [PATCH 131/132] docs: update --- .../src/credentials/credential.service.ts | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/nestjs-client/src/credentials/credential.service.ts b/packages/nestjs-client/src/credentials/credential.service.ts index 8649340..744ee33 100644 --- a/packages/nestjs-client/src/credentials/credential.service.ts +++ b/packages/nestjs-client/src/credentials/credential.service.ts @@ -130,6 +130,8 @@ export class CredentialService { ): Promise { const { revokeIfAlreadyIssued = false } = options ?? {} const refIdHash = options?.refId ? this.hash(options.refId) : null + + // Find a specific credential type based on provided definition ID or default to first available const credentialTypes = await this.apiClient.credentialTypes.getAll() const credentialType = credentialTypes.find(credType => credType.id === options?.credentialDefinitionId) ?? credentialTypes[0] @@ -140,6 +142,7 @@ export class CredentialService { } const { id: credentialDefinitionId, revocationSupported } = credentialType + // If existing credentials are found and revocation is requested, revoke all of them const creds = await this.credentialRepository.find({ where: { status: CredentialStatus.ACCEPTED, @@ -152,6 +155,7 @@ export class CredentialService { } } + // Begin a transaction to save new credentials and handle revocation logic if supported const cred: CredentialEntity = await this.entityManager.transaction(async transaction => { if (!revocationSupported) { return await transaction.save(CredentialEntity, { @@ -159,13 +163,17 @@ export class CredentialService { ...(refIdHash ? { refIdHash } : {}), }) } + + // Find last issued credential in revocation registry or create a new registry if none exists let lastCred = await transaction .createQueryBuilder(RevocationRegistryEntity, 'registry') .where('registry.currentIndex != registry.maximumCredentialNumber') .andWhere('registry.credentialDefinitionId = :credentialDefinitionId', { credentialDefinitionId }) .orderBy('registry.currentIndex', 'DESC') - .setLock('pessimistic_write') + .setLock('pessimistic_write') // Lock row for safe concurrent access .getOne() + + // Create new registry if none found if (!lastCred) lastCred = await this.createRevocationRegistry(credentialDefinitionId) await transaction.save(RevocationRegistryEntity, lastCred) @@ -177,6 +185,7 @@ export class CredentialService { }) }) + // Send a message containing the newly issued credential details via API client const thread = await this.apiClient.messages.send( new CredentialIssuanceMessage({ connectionId, @@ -192,6 +201,8 @@ export class CredentialService { if (cred.revocationRegistry) { cred.revocationRegistry.currentIndex += 1 this.revocationRepository.save(cred.revocationRegistry) + + // Check if maximum capacity has been reached and create a new revocation registry if necessary if (cred.revocationRegistry?.currentIndex === cred.revocationRegistry?.maximumCredentialNumber) { const revRegistry = await this.createRevocationRegistry( credentialDefinitionId, @@ -245,11 +256,12 @@ export class CredentialService { /** * Revokes a credential associated with the provided thread ID. - * @param connectionId - The connection ID to send the revoke. + * @param connectionId - The connection ID to send the revoke. (Search by connection ID if no thread ID) * @param threadId - (Optional) The thread ID linked to the credential to revoke. * @throws Error if no credential is found with the specified thread ID or connection ID, or if the credential has no connection ID. */ async revoke(connectionId: string, threadId?: string): Promise { + // Define search options based on whether a thread ID is provided const options: FindOneOptions = threadId ? { where: { threadId, status: CredentialStatus.ACCEPTED } } : { @@ -261,6 +273,7 @@ export class CredentialService { if (!cred) throw new Error(`Credential not found with threadId "${threadId}" or connectionId "${connectionId}".`) + // Save the updated credential back to the repository with the new status 'revoked' cred.status = CredentialStatus.REVOKED await this.credentialRepository.save(cred) @@ -268,6 +281,8 @@ export class CredentialService { const credentialType = credentialTypes.find(credType => credType.id === cred.revocationRegistry?.credentialDefinitionId) ?? credentialTypes[0] + + // If revocation is not supported for this credential type, return if (!credentialType.revocationSupported) { this.logger.warn( `Credential definition ${cred.revocationRegistry?.credentialDefinitionId} does not support revocation.`, @@ -275,6 +290,7 @@ export class CredentialService { return } + // Send a revocation message using the API client await this.apiClient.messages.send( new CredentialRevocationMessage({ connectionId, @@ -286,6 +302,7 @@ export class CredentialService { } // private methods + // Method to create a revocation registry for a given credential definition private async createRevocationRegistry( credentialDefinitionId: string, maximumCredentialNumber: number = 1000, @@ -294,11 +311,14 @@ export class CredentialService { credentialDefinitionId, maximumCredentialNumber, }) + + // Check if the revocation definition ID was successfully created if (!revocationDefinitionId) throw new Error( `Unable to create a new revocation registry for CredentialDefinitionId: ${credentialDefinitionId}`, ) const revocationRegistry = await this.revocationRepository.save({ + credentialDefinitionId, revocationDefinitionId, currentIndex: 0, maximumCredentialNumber, From be2a2e477b89f5b3c1a84720fb9f033894a66ee9 Mon Sep 17 00:00:00 2001 From: Andres Vallecilla Date: Tue, 21 Jan 2025 23:34:15 -0500 Subject: [PATCH 132/132] fix: CredentialStateChangedEvent --- packages/main/src/events/MessageEvents.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/main/src/events/MessageEvents.ts b/packages/main/src/events/MessageEvents.ts index 6446bd7..b6629f6 100644 --- a/packages/main/src/events/MessageEvents.ts +++ b/packages/main/src/events/MessageEvents.ts @@ -337,7 +337,7 @@ export const messageEvents = async (agent: ServiceAgent, config: ServerConfig) = const message = new CredentialRequestMessage({ connectionId: record.connectionId!, id: record.id, - threadId: (flowRecord?.getTag('messageId') as string) ?? record.threadId, + threadId: credentialProposalMessage?.threadId, claims: credentialProposalMessage?.credentialPreview?.attributes.map( p => new Claim({ name: p.name, value: p.value, mimeType: p.mimeType }), @@ -355,7 +355,7 @@ export const messageEvents = async (agent: ServiceAgent, config: ServerConfig) = const message = new CredentialReceptionMessage({ connectionId: record.connectionId!, id: record.id, - threadId: record.threadId, + threadId: (flowRecord?.getTag('messageId') as string) ?? record.threadId, state: record.errorMessage === 'issuance-abandoned: e.msg.refused' ? CredentialState.Declined