From c2461dd957af2325ce6bd91d1ceff477af23e7ce Mon Sep 17 00:00:00 2001
From: sovrgn <boyan.tsolov@vereign.com>
Date: Tue, 26 Mar 2024 13:46:23 +0200
Subject: [PATCH] feat: automatically handling revocation definitions and lists

---
 README.md                                     |  18 +-
 agent-swagger.json                            |  34 ++--
 libs/askar/src/agent.utils.ts                 | 154 ++++++++++++++++--
 libs/askar/src/askar-rest/rest.controller.ts  |   3 +-
 libs/askar/src/askar/agent.service.ts         | 153 +++++------------
 .../src/dtos/generics/creddef.record.dto.ts   |   4 -
 ...reate.credential.definition.request.dto.ts |   4 +-
 .../requests/offer.credential.request.dto.ts  |   5 +-
 .../credential.offer.response.dto.ts          |   1 +
 9 files changed, 219 insertions(+), 157 deletions(-)

diff --git a/README.md b/README.md
index 431bb289..43febe1a 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,6 @@
   - [Docker](#docker)
   - [Usage via Postman](#usage-via-postman)
   - [Send Didcomm messages between two OCMs](#send-didcomm-messages-between-two-ocms)
-  - [Revocation flow](#revocation-flow)
   - [License](#license)
 
 ## Introduction
@@ -159,14 +158,17 @@ Example:
 12. On the socket of issuer a event will be received with the message and connection id.
 
 
-## Revocation flow
+## License
+This project is licensed under the AGPL License - see the [LICENSE](LICENSE) file for details.
 
-1. Upon creating a credential definition, set `supportRevocation: true`, and get the revocationRegistryId from the response
-2. When issuing a credential with this credential definition, set the `revocationRegistryDefinitionId` property to the revocationRegistryId
-3. to check the status of a credential -> GET '/v1/credentials/:id/status (using the id of the credential you want to check the revocation status of)
-4. to revoke -> GET '/v1/redentials/:id/revoke (using the id of the credential you want to revoke)
 
 
-## License
-This project is licensed under the AGPL License - see the [LICENSE](LICENSE) file for details.
+DEF
+did:indy:bcovrin:test:7wV7McGyL5MnfpDF321NS3/anoncreds/v0/CLAIM_DEF/507389/asd1ST333
+
+
+
+id 1
+5a6aa65e-dd93-4c84-8f8a-c6d55b3552e5
 
+id 2
\ No newline at end of file
diff --git a/agent-swagger.json b/agent-swagger.json
index 5804270d..58cca66f 100644
--- a/agent-swagger.json
+++ b/agent-swagger.json
@@ -665,7 +665,7 @@
       }
     },
     "/api/v1/credentials/{id}/revoke": {
-      "get": {
+      "patch": {
         "operationId": "RestController_revokeCredentialById",
         "parameters": [
           {
@@ -1418,20 +1418,12 @@
           },
           "tag": {
             "type": "string"
-          },
-          "revocationRegistry": {
-            "type": "object"
-          },
-          "revocationStatusList": {
-            "type": "object"
           }
         },
         "required": [
           "schemaId",
           "issuerId",
-          "tag",
-          "revocationRegistry",
-          "revocationStatusList"
+          "tag"
         ]
       },
       "CreateCredentialDefinitionRequestDto": {
@@ -1444,7 +1436,8 @@
             "type": "string"
           },
           "supportRevocation": {
-            "type": "boolean"
+            "type": "boolean",
+            "default": false
           }
         },
         "required": [
@@ -1483,18 +1476,14 @@
               "$ref": "#/components/schemas/OfferCredentialAttributes"
             }
           },
-          "revocationRegistryDefinitionId": {
-            "type": "string"
-          },
-          "revocationRegistryIndex": {
-            "type": "number"
+          "revocable": {
+            "type": "boolean"
           }
         },
         "required": [
           "credentialDefinitionId",
           "attributes",
-          "revocationRegistryDefinitionId",
-          "revocationRegistryIndex"
+          "revocable"
         ]
       },
       "CredentialRecordDto": {
@@ -1562,6 +1551,12 @@
           },
           "credentialRecord": {
             "$ref": "#/components/schemas/CredentialRecordDto"
+          },
+          "revocationStatusListIndex": {
+            "type": "number"
+          },
+          "revocationRegistryId": {
+            "type": "string"
           }
         },
         "required": [
@@ -1753,6 +1748,9 @@
           },
           "message": {
             "type": "string"
+          },
+          "revocationNotification": {
+            "type": "object"
           }
         },
         "required": [
diff --git a/libs/askar/src/agent.utils.ts b/libs/askar/src/agent.utils.ts
index e3591cba..92b49705 100644
--- a/libs/askar/src/agent.utils.ts
+++ b/libs/askar/src/agent.utils.ts
@@ -77,6 +77,9 @@ import { Request, Response, Express } from "express";
 import url from "url";
 import { JsonLdCredentialFormatService } from "./credo/JsonLdCredentialFormatService";
 import { AskarService } from ".";
+import { OfferCredentialRequestDto } from "@ocm-engine/dtos";
+import { uuid } from "@credo-ts/core/build/utils/uuid";
+import { GenericRecord } from "@credo-ts/core/build/modules/generic-records/repository/GenericRecord";
 
 
 
@@ -662,14 +665,18 @@ export const getRevocationDetails = async (
 ) => {
   const credential = await askar.agent.credentials.getById(credentialId);
 
-  const metadata = credential.metadata.get<AnonCredsCredentialMetadata>(
-    '_anoncreds/credential',
-  );
+  // const metadata = credential.metadata.get<AnonCredsCredentialMetadata>(
+  //   '_anoncreds/credential',
+  // );
+  
+  const credentialRevocationRegistryDefinitionId = credential.getTag(
+    'anonCredsRevocationRegistryId'
+  ) as string
+  const credentialRevocationIndex = credential.getTag('anonCredsCredentialRevocationId') as string
 
   if (
-    !metadata ||
-    !metadata.revocationRegistryId ||
-    !metadata.credentialRevocationId
+    !credentialRevocationRegistryDefinitionId ||
+    !credentialRevocationIndex
   ) {
     throw new Error(
       `Credential with id=${credentialId} has no metadata, likely issued without support for revocation.`,
@@ -678,31 +685,154 @@ export const getRevocationDetails = async (
 
   const { revocationRegistryDefinition } =
     await askar.agent.modules.anoncreds.getRevocationRegistryDefinition(
-      metadata.revocationRegistryId,
+      credentialRevocationRegistryDefinitionId,
     );
 
   if (!revocationRegistryDefinition) {
     throw new Error(
-      `Could not find the revocation registry definition for id=${metadata.revocationRegistryId}.`,
+      `Could not find the revocation registry definition for id=${credentialRevocationRegistryDefinitionId}.`,
     );
   }
 
   const timestamp = Math.floor(Date.now() / 1000);
   const { revocationStatusList } =
     await askar.agent.modules.anoncreds.getRevocationStatusList(
-      metadata.revocationRegistryId,
+      credentialRevocationRegistryDefinitionId,
       timestamp,
     );
 
   if (!revocationStatusList) {
     throw new Error(
-      `Could not find the revocation status list for revocation registry definition id: ${metadata.revocationRegistryId} and timestamp: ${timestamp}.`,
+      `Could not find the revocation status list for revocation registry definition id: ${credentialRevocationRegistryDefinitionId} and timestamp: ${timestamp}.`,
     );
   }
 
   return {
-    credentialRevocationId: Number(metadata.credentialRevocationId),
+    credentialRevocationId: Number(credentialRevocationIndex),
     revocationStatusList,
-    revocationRegistryId: metadata.revocationRegistryId,
+    revocationRegistryId: credentialRevocationRegistryDefinitionId,
   };
+}
+
+
+export const handleRevocationForCredDef = async (
+  context: AskarService,
+  offerCredentialDto: OfferCredentialRequestDto
+): Promise<{
+  revocRecord: GenericRecord[],
+  statusListIndex: string | number,
+  revocationRegistryDefinitionId: string | undefined,
+}> => {
+  let statusListIndex = undefined;
+  let customRevocRecord = undefined;
+  let revocationRegistryDefinitionId = undefined;
+  
+  console.log('Issuing revocable credential.');
+
+  customRevocRecord = await context.agent.genericRecords.findAllByQuery({
+    credentialDefinitionId: offerCredentialDto.credentialDefinitionId,
+  })
+  
+  console.log('Using saved revocation custom record.');
+
+  if (!customRevocRecord || customRevocRecord.length < 1 || !customRevocRecord[0]) {
+    console.log("Registering new revocation records.");
+    
+    const {
+      revocStatusList,
+      revocReg,
+    } = await registerRevocationDefAndList(context, offerCredentialDto.credentialDefinitionId);
+
+    customRevocRecord = await context.agent.genericRecords.save({
+      id: uuid(),
+      tags: {
+        credentialDefinitionId: offerCredentialDto.credentialDefinitionId,
+        revocationRegistryDefinitionId: revocReg.revocationRegistryDefinitionState.revocationRegistryDefinitionId,
+        latestIndex: '0',
+      },
+      content: {
+        credentialDefinitionId: offerCredentialDto.credentialDefinitionId,
+        revocationRegistryDefinitionId: revocReg.revocationRegistryDefinitionState.revocationRegistryDefinitionId,
+      }
+    })
+
+    customRevocRecord = [customRevocRecord]
+    
+    if (!customRevocRecord) {
+      throw new Error("Could not save generic record for revocation status tracking.");
+    }
+  }
+
+  statusListIndex = Number(customRevocRecord[0].getTags()['latestIndex'])
+  revocationRegistryDefinitionId = customRevocRecord[0].getTags()['revocationRegistryDefinitionId'] as string;
+
+  if (
+    statusListIndex && 
+    statusListIndex > context.agentConfig.revocationListSize
+  ) {
+    const {
+      revocStatusList,
+      revocReg,
+    } = await registerRevocationDefAndList(context, offerCredentialDto.credentialDefinitionId);
+
+    customRevocRecord[0].setTag("latestIndex", "0")
+    customRevocRecord[0].setTag("revocationRegistryDefinitionId", revocReg.revocationRegistryDefinitionState.revocationRegistryDefinitionId)
+    await context.agent.genericRecords.update(customRevocRecord[0])
+
+    revocationRegistryDefinitionId = revocReg.revocationRegistryDefinitionState.revocationRegistryDefinitionId;
+    statusListIndex = "0";
+  }
+
+
+  return {
+    revocRecord: customRevocRecord,
+    statusListIndex: statusListIndex,
+    revocationRegistryDefinitionId,
+  }
+}
+
+
+
+
+export const registerRevocationDefAndList = async (
+  context: AskarService,
+  credentialDefinitionId: string
+) => {
+  const dids = await context.agent.dids.getCreatedDids({ method: "indy" });
+
+  let revocReg = await context.agent.modules.anoncreds.registerRevocationRegistryDefinition({
+    revocationRegistryDefinition: {
+      maximumCredentialNumber: context.agentConfig.revocationListSize,
+      issuerId: dids[0].did,
+      tag: uuid(),
+      credentialDefinitionId: credentialDefinitionId,
+    },
+    options: {}
+  });
+  
+  if (
+    !revocReg || 
+    revocReg.revocationRegistryDefinitionState.state !== 'finished'
+  ) {
+    throw new Error("Failed to register revocation definition on ledger."+revocReg)
+  }
+
+  const revocationRegistryDefinitionId = revocReg.revocationRegistryDefinitionState.revocationRegistryDefinitionId;
+
+  let revocStatusList = await context.agent.modules.anoncreds.registerRevocationStatusList({
+    revocationStatusList: {
+      revocationRegistryDefinitionId,
+      issuerId: dids[0].did,
+    },
+    options: {}
+  })
+
+  if (!revocStatusList || revocStatusList.revocationStatusListState.state !== 'finished') {
+    throw new Error("Failed to register revocation status list on ledger."+revocStatusList);
+  }
+
+  return {
+    revocStatusList,
+    revocReg,
+  }
 }
\ No newline at end of file
diff --git a/libs/askar/src/askar-rest/rest.controller.ts b/libs/askar/src/askar-rest/rest.controller.ts
index c10eae51..d631d55a 100644
--- a/libs/askar/src/askar-rest/rest.controller.ts
+++ b/libs/askar/src/askar-rest/rest.controller.ts
@@ -8,6 +8,7 @@ import {
   Post,
   UseFilters,
   UseGuards,
+  Patch,
 } from "@nestjs/common";
 
 import { AgentService } from "../askar/agent.service";
@@ -169,7 +170,7 @@ export class RestController {
     return this.agentService.getCredentialStatus(credentialId);
   }
 
-  @Get("/credentials/:id/revoke")
+  @Patch("/credentials/:id/revoke")
   async revokeCredentialById(@Param("id") credentialId: string) {
     return this.agentService.revokeCredentialById(credentialId);
   }
diff --git a/libs/askar/src/askar/agent.service.ts b/libs/askar/src/askar/agent.service.ts
index 02920718..4fa362c9 100644
--- a/libs/askar/src/askar/agent.service.ts
+++ b/libs/askar/src/askar/agent.service.ts
@@ -54,10 +54,11 @@ import {
   JsonCredential,
   W3cJsonLdVerifiableCredential,
 } from "@credo-ts/core";
-import { AnonCredsRequestedAttribute } from "@credo-ts/anoncreds";
+import { AnonCredsCredentialMetadata, AnonCredsRequestedAttribute } from "@credo-ts/anoncreds";
 import { uuid } from "@credo-ts/core/build/utils/uuid";
 import {
   getRevocationDetails,
+  handleRevocationForCredDef,
   waitForCredentialExchangeRecordSubject,
   waitForProofExchangeRecordSubject,
 } from "../agent.utils";
@@ -410,63 +411,15 @@ export class AgentService {
       throw new CredentialNotCreatedError();
     }    
 
-    // TODO: add types to responses of anoncreds
-
-    let revocReg;
-    if (supportRevocation) {
-      revocReg = await this.askar.agent.modules.anoncreds.registerRevocationRegistryDefinition({
-        revocationRegistryDefinition: {
-          maximumCredentialNumber: this.askar.agentConfig.revocationListSize,
-          issuerId: dids[0].did,
-          tag: credentialDefinitionDto.tag,
-          credentialDefinitionId: credDef.credentialDefinitionState.credentialDefinitionId,
-        },
-        options: {}
-      });
-      console.log('revocationRegisterResponse:', revocReg);
-      
-    }
-
-    // TODO: handle revoc registry error
-
-
     const response = new CreddefRecordDto();
 
-    response.revocationRegistry = revocReg;
-
     response.id = credDef.credentialDefinitionState.credentialDefinitionId;
     response.schemaId =
       credDef.credentialDefinitionState.credentialDefinition.schemaId;
     response.issuerId =
       credDef.credentialDefinitionState.credentialDefinition.issuerId;
     response.tag = credDef.credentialDefinitionState.credentialDefinition.tag;
-
-    let revocStatusList;
-    if (revocReg && revocReg.revocationRegistryDefinitionState.state === 'finished') {
-      response.revocationRegistry = revocReg.revocationRegistryDefinitionState;
-
-      const revocationRegistryDefinitionId = revocReg.revocationRegistryDefinitionState.revocationRegistryDefinitionId;
-
-      revocStatusList = await this.askar.agent.modules.anoncreds.registerRevocationStatusList({
-        revocationStatusList: {
-          revocationRegistryDefinitionId,
-          issuerId: dids[0].did,
-        },
-        options: {}
-      })
-
-      console.log('RevocationStatusListRegisterResponse: ', revocStatusList);
       
-    }
-
-    response.revocationStatusList = revocStatusList;
-    
-    if (revocStatusList && revocStatusList.revocationStatusListState.state === 'finished') {
-      response.revocationStatusList = revocStatusList.revocationStatusListState;
-    }
-
-    // TODO: handle revoc status list error    
-        
     return response;
   };
 
@@ -710,54 +663,16 @@ export class AgentService {
       JSON.stringify(offerCredentialDto, null, 2),
     );
 
-    let statusListIndex = undefined;
-    let customRevocRecord = undefined;
-    if (offerCredentialDto.revocationRegistryDefinitionId) {
-      console.log('Issuing revocable credential.');
-
-      customRevocRecord = await this.askar.agent.genericRecords.findAllByQuery({
-        credentialDefinitionId: offerCredentialDto.credentialDefinitionId,
-        revocationRegistryDefinitionId: offerCredentialDto.revocationRegistryDefinitionId,
-      })
-      
-      console.log('Using saved revocation custom record:',customRevocRecord);
-
-      if (!customRevocRecord || customRevocRecord.length < 1 || !customRevocRecord[0]) {
-        customRevocRecord = await this.askar.agent.genericRecords.save({
-          id: uuid(),
-          tags: {
-            credentialDefinitionId: offerCredentialDto.credentialDefinitionId,
-            revocationRegistryDefinitionId: offerCredentialDto.revocationRegistryDefinitionId,
-            latestIndex: '0',
-          },
-          content: {
-            credentialDefinitionId: offerCredentialDto.credentialDefinitionId,
-            revocationRegistryDefinitionId: offerCredentialDto.revocationRegistryDefinitionId,
-          }
-        })
-
-        customRevocRecord = [customRevocRecord]
-        
-        if (!customRevocRecord) {
-          throw new Error("Could not save generic record for revocation status tracking.");
-        }
-      }
-
-      statusListIndex = Number(customRevocRecord[0].getTags()['latestIndex'])
-    }
-
-    if (
-      statusListIndex && 
-      statusListIndex >= this.askar.agentConfig.revocationListSize - 1
-    ) {
-      throw new Error('Revocation Status List has been exhausted. Please create a new Revocation Registry Definition for this Credential Definition. ... Not yet implemented.');
+    let customRevocationRecord = undefined;
+    let statusListIdx = undefined;
+    let revocationRegistryDef = undefined;
+    if (offerCredentialDto.revocable) {
+      const { revocRecord, statusListIndex, revocationRegistryDefinitionId } = await handleRevocationForCredDef(this.askar, offerCredentialDto);
+      customRevocationRecord = revocRecord;
+      statusListIdx = statusListIndex;
+      revocationRegistryDef = revocationRegistryDefinitionId;
     }
 
-    // TODO: if latestIndex is conf.maxListSize-1 -> create new revocDef and statusList
-    // and use them instead
-    // REVIEW: is there a way to handle this automatically as well?
-
-
     if (!offerCredentialDto.connectionId) {
       const { credentialRecord, message } =
         await this.askar.agent.credentials.createOffer({
@@ -766,8 +681,8 @@ export class AgentService {
             anoncreds: {
               credentialDefinitionId: offerCredentialDto.credentialDefinitionId,
               attributes: offerCredentialDto.attributes,
-              revocationRegistryDefinitionId: offerCredentialDto.revocationRegistryDefinitionId || undefined,
-              revocationRegistryIndex: typeof statusListIndex === 'undefined' ? undefined : statusListIndex+1,
+              revocationRegistryDefinitionId: revocationRegistryDef ? revocationRegistryDef : undefined,
+              revocationRegistryIndex: typeof statusListIdx === 'undefined' ? undefined : Number(statusListIdx),
             },
           },
           autoAcceptCredential: AutoAcceptCredential.ContentApproved,
@@ -777,15 +692,16 @@ export class AgentService {
       await this.askar.agent.credentials.update(credentialRecord);
       
 
-      // INFO: update with latest index
+      // INFO: update with latest index once it is issued
       if (
-        offerCredentialDto.revocationRegistryDefinitionId && 
-        customRevocRecord &&
-        customRevocRecord.length > 0 &&
-        typeof statusListIndex !== 'undefined'
+        offerCredentialDto.revocable &&
+        revocationRegistryDef && 
+        customRevocationRecord &&
+        customRevocationRecord.length > 0 &&
+        typeof statusListIdx !== 'undefined'
       ) {
-        customRevocRecord[0].setTag("latestIndex", (statusListIndex+1).toString())
-        await this.askar.agent.genericRecords.update(customRevocRecord[0])
+        customRevocationRecord[0].setTag("latestIndex", (Number(statusListIdx)+1).toString())
+        await this.askar.agent.genericRecords.update(customRevocationRecord[0])
       }
 
 
@@ -812,7 +728,8 @@ export class AgentService {
         credentialUrl: credentialUrl,
         shortCredentialUrl: shortCredentialUrl,
         credentialRecord: dto,
-        revocationStatusListIndex: statusListIndex,
+        revocationStatusListIndex: Number(statusListIdx),
+        revocationRegistryId: revocationRegistryDef,
       };
     }
     const credentialExchangeRecord =
@@ -823,8 +740,8 @@ export class AgentService {
           anoncreds: {
             credentialDefinitionId: offerCredentialDto.credentialDefinitionId,
             attributes: offerCredentialDto.attributes,
-            revocationRegistryDefinitionId: offerCredentialDto.revocationRegistryDefinitionId || undefined,
-            revocationRegistryIndex: typeof statusListIndex === 'undefined' ? undefined : statusListIndex+1,
+            revocationRegistryDefinitionId: revocationRegistryDef || undefined,
+            revocationRegistryIndex: typeof statusListIdx === 'undefined' ? undefined : Number(statusListIdx),
           },
         },
       });
@@ -844,7 +761,8 @@ export class AgentService {
       credentialUrl: null,
       shortCredentialUrl: null,
       credentialRecord: dto,
-      revocationStatusListIndex: statusListIndex,
+      revocationStatusListIndex: Number(statusListIdx),
+      revocationRegistryId: revocationRegistryDef,
     };
   };
 
@@ -1061,7 +979,7 @@ export class AgentService {
       throw new Error(
         `Credential with id=${credentialId}, with revocation id=${credentialRevocationId}, is already in a revoked state.`,
       );
-    }
+    }    
 
     const updatedList = await this.askar.agent.modules.anoncreds.updateRevocationStatusList({
       revocationStatusList: {
@@ -1078,12 +996,27 @@ export class AgentService {
         )}`,
       );
     }
+    
+
+    try {
+      const sentRevocationNotification = await this.askar.agent.credentials.sendRevocationNotification({
+        credentialRecordId: credentialId,
+        revocationId: `${revocationRegistryId}::${credentialRevocationId.toString()}`,
+        revocationFormat: 'anoncreds',
+      })
+      console.log(sentRevocationNotification);
+    } catch (err) {
+      console.log(err);
+    }
+    // TODO: handle error from sentRevocationNotification
+
+    
 
     return {
       revocationRegistryId: revocationRegistryId,
       revocationId: credentialRevocationId,
       valid: false,
-      message: 'Credential Revocation Status Updated.'
+      message: 'Credential Revocation Status Updated.',
     }
   }
 
diff --git a/libs/dtos/src/dtos/generics/creddef.record.dto.ts b/libs/dtos/src/dtos/generics/creddef.record.dto.ts
index 01d23a1d..873b8545 100644
--- a/libs/dtos/src/dtos/generics/creddef.record.dto.ts
+++ b/libs/dtos/src/dtos/generics/creddef.record.dto.ts
@@ -13,8 +13,4 @@ export class CreddefRecordDto extends BaseRecordDto {
   @IsNotEmpty()
   @IsString()
   tag: string;
-
-  revocationRegistry: any; // TODO: validate better once format is clear
-
-  revocationStatusList: any; // TODO: validate better once format is clear
 }
diff --git a/libs/dtos/src/dtos/requests/create.credential.definition.request.dto.ts b/libs/dtos/src/dtos/requests/create.credential.definition.request.dto.ts
index dedf6c90..f3e35903 100644
--- a/libs/dtos/src/dtos/requests/create.credential.definition.request.dto.ts
+++ b/libs/dtos/src/dtos/requests/create.credential.definition.request.dto.ts
@@ -9,7 +9,7 @@ export class CreateCredentialDefinitionRequestDto {
   @IsString()
   tag: string;
 
-  @IsOptional()
   @IsBoolean()
-  supportRevocation?: boolean;
+  @IsOptional()
+  supportRevocation?: boolean = false;
 }
diff --git a/libs/dtos/src/dtos/requests/offer.credential.request.dto.ts b/libs/dtos/src/dtos/requests/offer.credential.request.dto.ts
index b23a63f8..9876716f 100644
--- a/libs/dtos/src/dtos/requests/offer.credential.request.dto.ts
+++ b/libs/dtos/src/dtos/requests/offer.credential.request.dto.ts
@@ -1,6 +1,7 @@
 import {
   ArrayMinSize,
   IsArray,
+  IsBoolean,
   IsNotEmpty,
   IsNumber,
   IsOptional,
@@ -36,9 +37,9 @@ export class OfferCredentialRequestDto {
   @Type(() => OfferCredentialAttributes)
   attributes: Array<OfferCredentialAttributes>;
 
+  @IsBoolean()
   @IsOptional()
-  @IsString()
-  revocationRegistryDefinitionId: string;
+  revocable: boolean;
 }
 
 export class OfferJsonCredentialRequests {
diff --git a/libs/dtos/src/dtos/responses/credential.offer.response.dto.ts b/libs/dtos/src/dtos/responses/credential.offer.response.dto.ts
index bdcf47f8..f8a8ecd9 100644
--- a/libs/dtos/src/dtos/responses/credential.offer.response.dto.ts
+++ b/libs/dtos/src/dtos/responses/credential.offer.response.dto.ts
@@ -5,4 +5,5 @@ export class CredentialOfferResponseDto {
   public shortCredentialUrl: string | null;
   public credentialRecord: CredentialRecordDto;
   public revocationStatusListIndex?: number | undefined;
+  public revocationRegistryId?: string | undefined;
 }
-- 
GitLab