Skip to content

Commit

Permalink
Merge branch 'feat/add-revocation-registry' into feat/create-revocati…
Browse files Browse the repository at this point in the history
…on-nestjs-client
  • Loading branch information
lotharking committed Dec 19, 2024
2 parents 5cd0912 + 43ff688 commit 2e83368
Show file tree
Hide file tree
Showing 9 changed files with 164 additions and 55 deletions.
10 changes: 6 additions & 4 deletions examples/chatbot/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,12 @@ 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 =
(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}`)
Expand Down
8 changes: 6 additions & 2 deletions packages/client/src/ApiClient.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -23,24 +24,27 @@ 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,
private version: ApiVersion = ApiVersion.V1,
) {
this.messages = new MessageService(baseURL, version)
this.credentialTypes = new CredentialTypeService(baseURL, version)
this.revocationRegistry = new RevocationRegistryService(baseURL, version)
}
}
9 changes: 0 additions & 9 deletions packages/client/src/services/CredentialTypeService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,4 @@ export class CredentialTypeService {

return types.map(value => value as CredentialTypeInfo)
}

public async createRevocation(credentialDefinitionId: string): Promise<string> {
const response = await fetch(`${this.url}/revoke`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ credentialDefinitionId }),
})
return response.text()
}
}
74 changes: 74 additions & 0 deletions packages/client/src/services/RevocationRegistryService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// 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<string | undefined> {
const response = await fetch(`${this.url}/revocationRegistry`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(options),
})

if (!response.ok) {
logger.error(`Failed to create revocation registry`)
return undefined
}

return await response.text()
}

public async get(credentialDefinitionId: string): Promise<string[]> {
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}`)
}

return (await response.json()) as string[]
}

public async getAll(): Promise<string[]> {
const response = await fetch(`${this.url}/revocationRegistry`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
})

if (!response.ok) {
throw new Error(`Failed to fetch revocation registries: ${response.statusText}`)
}

return (await response.json()) as string[]
}
}
1 change: 1 addition & 0 deletions packages/client/src/services/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './CredentialTypeService'
export * from './MessageService'
export * from './RevocationRegistryService'
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
import { ApiProperty } from '@nestjs/swagger'
import { IsString, IsNotEmpty } from 'class-validator'
import { IsString, IsNotEmpty, IsNumber, IsOptional } 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()
@IsOptional()
maximumCredentialNumber: number = 1000
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
NotFoundException,
Param,
Post,
Query,
} from '@nestjs/common'
import { ApiBody, ApiTags } from '@nestjs/swagger'

Expand Down Expand Up @@ -59,23 +60,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,
}
}),
)
Expand Down Expand Up @@ -431,7 +420,7 @@ export class CredentialTypesController {
* @param credentialDefinitionId
* @returns RevocationTypeInfo
*/
@Post('/revoke')
@Post('/revocationRegistry')
public async createRevocationRegistry(@Body() options: CreateRevocationRegistryDto): Promise<string> {
try {
const agent = await this.agentService.getAgent()
Expand All @@ -448,22 +437,21 @@ export class CredentialTypesController {
revocationRegistryDefinition: {
credentialDefinitionId,
tag: 'default',
maximumCredentialNumber: 1000,
maximumCredentialNumber: options.maximumCredentialNumber,
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)}`,
)
}
this.logger.debug!(
`revocationRegistryDefinitionState: ${JSON.stringify(revocationResult.revocationRegistryDefinitionState)}`,
)

const revStatusListResult = await agent.modules.anoncreds.registerRevocationStatusList({
revocationStatusList: {
Expand All @@ -472,6 +460,9 @@ export class CredentialTypesController {
},
options: {},
})
if (!revStatusListResult.revocationStatusListState.revocationStatusList) {
throw new Error(`Failed to create revocation status list`)
}
const revocationDefinitionRepository = agent.dependencyManager.resolve(
AnonCredsRevocationRegistryDefinitionRepository,
)
Expand All @@ -482,7 +473,7 @@ export class CredentialTypesController {
)
revocationDefinitionRecord.metadata.set(
'revStatusList',
revStatusListResult.revocationStatusListState.revocationStatusList!,
revStatusListResult.revocationStatusListState.revocationStatusList,
)
await revocationDefinitionRepository.update(agent.context, revocationDefinitionRecord)

Expand All @@ -502,4 +493,34 @@ export class CredentialTypesController {
)
}
}

/**
* Get all revocation definitions by credentialDefinitionId
*
* @returns string[] with revocationRegistryDefinitionIds
*/
@Get('/revocationRegistry')
public async getRevocationDefinitions(
@Query('credentialDefinitionId') credentialDefinitionId?: string,
): Promise<string[]> {
const agent = await this.agentService.getAgent()

const revocationDefinitionRepository = agent.dependencyManager.resolve(
AnonCredsRevocationRegistryDefinitionRepository,
)
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,
)

return revocationRegistryDefinitionIds
}
}
37 changes: 19 additions & 18 deletions packages/main/src/controllers/message/MessageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,27 +244,28 @@ 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'),
)
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)`)

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,
revocationFormat: 'anoncreds',
revocationId: `${credential.getTag('anonCredsRevocationRegistryId')}::${credential.getTag('anonCredsCredentialRevocationId')}`,
})
}
} else {
throw new Error(`No credentials were found for connection: ${msg.connectionId}.`)
Expand Down
6 changes: 5 additions & 1 deletion packages/model/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ export interface CreateCredentialTypeOptions {
version: string
attributes: string[]
schemaId?: string
revocationIds?: string[]
supportRevocation?: boolean
}

Expand All @@ -50,6 +49,11 @@ export interface CredentialTypeInfo extends CreateCredentialTypeOptions {
id: string
}

export interface RevocationRegistryInfo {
credentialDefinitionId: string
maximumCredentialNumber: number
}

export interface CreatePresentationRequestOptions {
requestedCredentials: RequestedCredential[]
}
Expand Down

0 comments on commit 2e83368

Please sign in to comment.