Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
14fe14d
feat: Implement status list allocation for OID4VC credential issuance
sagarkhole4 Mar 26, 2026
2248f9b
feat: Implement status list allocation for OID4VC credential issuance,
sagarkhole4 Mar 26, 2026
812d622
feat: Implement status list allocation for OID4VC credential issuance,
sagarkhole4 Mar 26, 2026
69c9eb3
update OID4VC issuance logic
sagarkhole4 Mar 26, 2026
9294e40
fix:sd-jwt revocation flow
sagarkhole4 Mar 27, 2026
3464eb0
fix: restrict status list allocation to SD-JWT format only
sagarkhole4 Mar 27, 2026
380e0cc
Merge branch 'main' into feat/sd-jwt-revocation-flow
sagarkhole4 Mar 30, 2026
3eef6d3
feat: Implement status list allocation for OID4VC credential issuance
sagarkhole4 Mar 26, 2026
a9b4613
feat: Implement status list allocation for OID4VC credential issuance,
sagarkhole4 Mar 26, 2026
53be9b5
feat: Implement status list allocation for OID4VC credential issuance,
sagarkhole4 Mar 26, 2026
05e30f9
update OID4VC issuance logic
sagarkhole4 Mar 26, 2026
51d314b
fix:sd-jwt revocation flow
sagarkhole4 Mar 27, 2026
fe14467
fix: restrict status list allocation to SD-JWT format only
sagarkhole4 Mar 27, 2026
379fc76
fix: removed duplicate webhookSecret column for org agent
Apr 1, 2026
7ba2bf7
fix: discloser frame issue for sdjwt
Apr 6, 2026
1fa32a1
refactor: stabilize SD-JWT status list allocation and index searching
sagarkhole4 Apr 20, 2026
c6258ac
Merge branch 'feat/sd-jwt-revocation-flow' of https://github.com/cred…
sagarkhole4 Apr 20, 2026
ac3c2f5
refactor(issuance):handle missing STATUS_LIST_HOST configuration
sagarkhole4 Apr 21, 2026
e978e29
fix: stabilize sd-jwt revocation rollback and synchronize status list…
sagarkhole4 Apr 21, 2026
b5feb17
Merge branch 'feat/sd-jwt-revocation-flow' of https://github.com/cred…
sagarkhole4 Apr 21, 2026
661b9f0
refactor:removed duplicate veribles from .env.demo
sagarkhole4 Apr 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.demo
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,7 @@ CONSUMER_CONFIG_MAX_DELIVER=4
AGGREGATE_STREAM=aggregate
DID_STREAM=did-notify
PULL_CONSUMER=hub-pull-consumer
STATUS_LIST_HOST=

NATS_CREDS_FILE=

Expand Down
5 changes: 5 additions & 0 deletions apps/agent-service/src/agent-service.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,11 @@ export class AgentServiceController {
return this.agentServiceService.oidcDeleteCredentialOffer(payload.url, payload.orgId);
}

@MessagePattern({ cmd: 'agent-service-oid4vc-revoke-credential' })
async oidcRevokeCredential(payload: { url: string; orgId: string }): Promise<object> {
return this.agentServiceService.oidcRevokeCredential(payload.url, payload.orgId);
}

@MessagePattern({ cmd: 'agent-create-x509-certificate' })
async createX509Certificate(payload: {
options: X509CreateCertificateOptions;
Expand Down
14 changes: 14 additions & 0 deletions apps/agent-service/src/agent-service.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1489,6 +1489,20 @@ export class AgentServiceService {
}
}

async oidcRevokeCredential(url: string, orgId: string): Promise<object> {
try {
const getApiKey = await this.getOrgAgentApiKey(orgId);
const data = await this.commonService
.httpPost(`${url}`, {}, { headers: { authorization: getApiKey } })
.then(async (response) => response);
return data;
} catch (error) {
const errorMsg = error.response ? JSON.stringify(error.response.data) : error.message;
this.logger.error(`Error in oidcRevokeCredential in agent service : ${errorMsg}`);
throw error;
Comment thread
sagarkhole4 marked this conversation as resolved.
}
}

async oidcIssuerTemplate(templatePayload, url: string, orgId: string): Promise<object> {
try {
const getApiKey = await this.getOrgAgentApiKey(orgId);
Expand Down
15 changes: 13 additions & 2 deletions apps/api-gateway/src/oid4vc-issuance/dtos/issuer-sessions.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import {
ValidatorConstraintInterface,
ValidationArguments,
Validate,
IsDate
IsDate,
IsBoolean
} from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
Expand Down Expand Up @@ -196,6 +197,11 @@ export class CreateOidcCredentialOfferDto {
noticeUrl?: string;

issuerId?: string;

@ApiPropertyOptional({ example: true, description: 'Flag to enable revocation for the issued credentials' })
@IsOptional()
@IsBoolean()
isRevocable?: boolean = false;
}

export class GetAllCredentialOfferDto {
Expand Down Expand Up @@ -301,7 +307,7 @@ export class CredentialDto {
},
iat: 1698151532,
nbf: dateToSeconds(new Date()),
exp: dateToSeconds(new Date(Date.now() + 5 * 365 * 24 * 60 * 60 * 1000))
exp: dateToSeconds(new Date(Date.now() + 157680000000))
}
]
})
Expand Down Expand Up @@ -391,6 +397,11 @@ export class CreateCredentialOfferD2ADto {
@IsOptional()
issuerId?: string;

@ApiPropertyOptional({ example: true, description: 'Flag to enable revocation for the issued credentials' })
@IsOptional()
@IsBoolean()
isRevocable?: boolean = false;

@ExactlyOneOf(['preAuthorizedCodeFlowConfig', 'authorizationCodeFlowConfig'], {
message: 'Provide exactly one of preAuthorizedCodeFlowConfig or authorizationCodeFlowConfig.'
})
Expand Down
24 changes: 24 additions & 0 deletions apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -672,4 +672,28 @@ export class Oid4vcIssuanceController {

return res.status(HttpStatus.CREATED).json(finalResponse);
}

@Post('/orgs/:orgId/openid4vc/issuance-sessions/:issuanceSessionId/revoke')
@ApiOperation({
summary: 'Revoke an OID4VC Credential',
description: 'Instantly revokes a credential issued during a specific OID4VC session.'
})
@ApiResponse({ status: HttpStatus.OK, description: 'Credential revoked successfully.', type: ApiResponseDto })
@ApiBearerAuth()
@Roles(OrgRoles.OWNER)
@UseGuards(AuthGuard('jwt'), OrgRolesGuard)
async revokeCredential(
@Param('orgId') orgId: string,
@Param('issuanceSessionId') issuanceSessionId: string,
@Res() res: Response
): Promise<Response> {
const revoked = await this.oid4vcIssuanceService.revokeCredential(issuanceSessionId, orgId);

const finalResponse: IResponse = {
statusCode: HttpStatus.OK,
message: 'Credential revoked successfully.',
data: revoked
};
return res.status(HttpStatus.OK).json(finalResponse);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@ export class Oid4vcIssuanceService extends BaseService {
return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-credential-offer-delete', payload);
}

async revokeCredential(issuanceSessionId: string, orgId: string): Promise<object> {
const payload = { issuanceSessionId, orgId };
return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-revoke-credential', payload);
}

oidcIssueCredentialWebhook(
oidcIssueCredentialDto: OidcIssueCredentialDto,
id: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export interface CreateOidcCredentialOffer {
authenticationType: AuthenticationType; // only option selector
credentials: CredentialRequest[]; // one or more credentials
noticeUrl?: string;
isRevocable?: boolean;
}

export interface GetAllCredentialOffer {
Expand Down
61 changes: 41 additions & 20 deletions apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@

export interface DisclosureFrame {
_sd?: string[];
[claim: string]: DisclosureFrame | string[] | undefined;
[claim: string]: DisclosureFrame | DisclosureFrame[] | string[] | undefined;
}

export interface validityInfo {
Expand All @@ -58,6 +58,7 @@
authorizationServerUrl: string;
};
publicIssuerId?: string;
isRevocable?: boolean;
}

export interface ResolvedSignerOption {
Expand All @@ -84,12 +85,18 @@
format: CredentialFormat;
payload: Record<string, unknown>;
disclosureFrame?: DisclosureFrame;
statusListDetails?: {
listId: string;
index: number;
listSize?: number;
};
}

export interface BuiltCredentialOfferBase {
signerOption?: ResolvedSignerOption;
credentials: BuiltCredential[];
publicIssuerId?: string;
isRevocable?: boolean;
}

export type CredentialOfferPayload = BuiltCredentialOfferBase &
Expand Down Expand Up @@ -223,36 +230,46 @@
return { valid: 0 === errors.length, errors };
}

function buildDisclosureFrameFromTemplate(attributes: CredentialAttribute[]): DisclosureFrame {
function buildDisclosureFrameFromTemplate(
attributes: CredentialAttribute[],
payload?: Record<string, any>
): DisclosureFrame {
const frame: DisclosureFrame = {};
const sd: string[] = [];

for (const attr of attributes) {
const childFrame =
attr.children && 0 < attr.children.length ? buildDisclosureFrameFromTemplate(attr.children) : undefined;

const hasChildDisclosure =
childFrame && (childFrame._sd?.length || Object.keys(childFrame).some((k) => '_sd' !== k));

// Case 1: this attribute itself is disclosed
if (attr.disclose) {
// If it has children, children are handled separately
if (!attr.children || 0 === attr.children.length) {
sd.push(attr.key);
continue;
const hasChildren = attr.children && 0 < attr.children.length;

if (hasChildren) {
const payloadValue = payload?.[attr.key];
//todo:

Check warning on line 245 in apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this "TODO" comment.

See more on https://sonarcloud.io/project/issues?id=credebl_platform&issues=AZ1hMR7tlZMhgCvZPboE&open=AZ1hMR7tlZMhgCvZPboE&pullRequest=1594
//1) Need to handle the type validation here to ensure payloadValue is in expected format (object or array of objects)
//2) Need to add add validation based on template definition (e.g. if template defines an array, payload must be an array, etc.)
Comment on lines +245 to +247

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Address the TODO comment before merging.

The comment acknowledges missing type validation for payload values. Consider creating an issue to track this work if it cannot be completed in this PR.

Do you want me to open a new issue to track this validation task?

🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[warning] 245-245: Complete the task associated to this "TODO" comment.

See more on https://sonarcloud.io/project/issues?id=credebl_platform&issues=AZ1hMR7tlZMhgCvZPboE&open=AZ1hMR7tlZMhgCvZPboE&pullRequest=1594

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts` around
lines 245 - 247, The TODO indicates missing type validation for payload values:
locate the block referencing payloadValue and the template definition (search
for symbols payloadValue and templateDefinition or the function that builds
credential sessions, e.g., CredentialSessionsBuilder/buildCredentialSession) and
add runtime checks that enforce payloadValue is either an object or an array of
objects, then validate structure according to the template (if
templateDefinition.type === 'array' require an array of objects; if 'object'
require a single object). On validation failure throw/return a descriptive error
(or ValidationError) and add unit tests covering object vs array cases and
template mismatches; keep error messages tied to the same function names
(payloadValue/templateDefinition) so logs/tests can find the source.

if (Array.isArray(payloadValue)) {
// Array of objects → [{ _sd: [...] }, ...]
const childFrame = buildDisclosureFrameFromTemplate(attr.children!);

Check warning on line 250 in apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This assertion is unnecessary since it does not change the type of the expression.

See more on https://sonarcloud.io/project/issues?id=credebl_platform&issues=AZ1hMR7tlZMhgCvZPboF&open=AZ1hMR7tlZMhgCvZPboF&pullRequest=1594
frame[attr.key] = payloadValue.map(() => ({ ...childFrame }));
Comment on lines +248 to +251

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Array handling has two issues: inconsistent payload propagation and shallow copy.

  1. Missing payload propagation: Unlike the object branch (line 254), the array branch doesn't pass individual payload elements to the recursive call. This means nested children within array elements won't get payload-aware processing (e.g., detecting nested arrays).

  2. Shallow copy hazard: { ...childFrame } creates shallow copies that share references to nested objects like _sd arrays. If any frame element is mutated, all copies are affected.

🔧 Proposed fix
       if (Array.isArray(payloadValue)) {
-        // Array of objects → [{ _sd: [...] }, ...]
-        const childFrame = buildDisclosureFrameFromTemplate(attr.children!);
-        frame[attr.key] = payloadValue.map(() => ({ ...childFrame }));
+        // Array of objects → [{ _sd: [...] }, ...] — build per-element frame
+        frame[attr.key] = payloadValue.map((element) =>
+          buildDisclosureFrameFromTemplate(attr.children, element as Record<string, any>)
+        );
       } else {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (Array.isArray(payloadValue)) {
// Array of objects → [{ _sd: [...] }, ...]
const childFrame = buildDisclosureFrameFromTemplate(attr.children!);
frame[attr.key] = payloadValue.map(() => ({ ...childFrame }));
if (Array.isArray(payloadValue)) {
// Array of objects → [{ _sd: [...] }, ...] — build per-element frame
frame[attr.key] = payloadValue.map((element) =>
buildDisclosureFrameFromTemplate(attr.children, element as Record<string, any>)
);
🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[warning] 250-250: This assertion is unnecessary since it does not change the type of the expression.

See more on https://sonarcloud.io/project/issues?id=credebl_platform&issues=AZ1hMR7tlZMhgCvZPboF&open=AZ1hMR7tlZMhgCvZPboF&pullRequest=1594

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts` around
lines 248 - 251, The array branch in buildDisclosureFrameFromTemplate fails to
pass each element's payload into the recursive call and uses a shallow spread
for copies; update the Array.isArray(payloadValue) case so for each element you
call buildDisclosureFrameFromTemplate(attr.children!, payloadElement) (or
equivalent overload) to propagate the element's payload, and create deep
independent copies of the returned child frame (e.g., by constructing each entry
via a fresh recursive call or using a deterministic deep-clone of the child
frame) before assigning frame[attr.key] = payloadValue.map(...), ensuring nested
_sd arrays and objects are not shared across array items.

} else {
// Plain object → { _sd: [...] }
const childFrame = buildDisclosureFrameFromTemplate(attr.children!, payloadValue as Record<string, any>);

Check warning on line 254 in apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This assertion is unnecessary since it does not change the type of the expression.

See more on https://sonarcloud.io/project/issues?id=credebl_platform&issues=AZ1hMR7tlZMhgCvZPboG&open=AZ1hMR7tlZMhgCvZPboG&pullRequest=1594
const hasChildDisclosure = childFrame._sd?.length || Object.keys(childFrame).some((k) => '_sd' !== k);
if (hasChildDisclosure) {
frame[attr.key] = childFrame;
}
}
continue;
}

// Case 2: attribute has disclosed children
if (hasChildDisclosure) {
frame[attr.key] = childFrame!;
// Root attribute — add to _sd if disclosed
if (attr.disclose) {
sd.push(attr.key);
}
}

if (0 < sd.length) {
frame._sd = sd;
}

// console.log('Built disclosure frame:', JSON.stringify(frame, null, 2));

Check warning on line 272 in apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this commented out code.

See more on https://sonarcloud.io/project/issues?id=credebl_platform&issues=AZ1hMR7tlZMhgCvZPboH&open=AZ1hMR7tlZMhgCvZPboH&pullRequest=1594
return frame;
}

Expand Down Expand Up @@ -366,8 +383,11 @@
const apiFormat = mapDbFormatToApiFormat(templateRecord.format);
const idSuffix = formatSuffix(apiFormat);
const credentialSupportedId = `${templateRecord.name}-${idSuffix}`;
const disclosureFrame = buildDisclosureFrameFromTemplate(sdJwtTemplate.attributes);

const disclosureFrame = buildDisclosureFrameFromTemplate(
sdJwtTemplate.attributes,
payloadCopy as Record<string, any>
);
return {
credentialSupportedId,
signerOptions: templateSignerOption ? templateSignerOption : undefined,
Expand Down Expand Up @@ -477,7 +497,8 @@

const baseEnvelope: BuiltCredentialOfferBase = {
credentials: builtCredentials,
...(finalPublicIssuerId ? { publicIssuerId: finalPublicIssuerId } : {})
...(finalPublicIssuerId ? { publicIssuerId: finalPublicIssuerId } : {}),
isRevocable: dto.isRevocable
};

// Determine which authorization flow to return:
Expand Down
4 changes: 4 additions & 0 deletions apps/oid4vc-issuance/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import { Oid4vcIssuanceModule } from './oid4vc-issuance.module';
const logger = new Logger();

async function bootstrap(): Promise<void> {
if (!process.env.STATUS_LIST_HOST) {
logger.warn('STATUS_LIST_HOST is not configured. Revocable SD-JWT flow will be disabled.');
}

const app = await NestFactory.createMicroservice<MicroserviceOptions>(Oid4vcIssuanceModule, {
transport: Transport.NATS,
options: getNatsOptions(
Expand Down
13 changes: 7 additions & 6 deletions apps/oid4vc-issuance/src/oid4vc-issuance.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,16 +165,17 @@ export class Oid4vcIssuanceController {
}

@MessagePattern({ cmd: 'oid4vc-credential-offer-delete' })
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async deleteCredentialOffers(payload: {
orgId: string;
credentialId: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}): Promise<any> {
async deleteCredentialOffers(payload: { orgId: string; credentialId: string }): Promise<object> {
const { orgId, credentialId } = payload;
return this.oid4vcIssuanceService.deleteCredentialOffers(orgId, credentialId);
}

@MessagePattern({ cmd: 'oid4vc-revoke-credential' })
async revokeCredential(payload: { issuanceSessionId: string; orgId: string }): Promise<object> {
const { issuanceSessionId, orgId } = payload;
return this.oid4vcIssuanceService.revokeCredential(issuanceSessionId, orgId);
}

@MessagePattern({ cmd: 'webhook-oid4vc-issue-credential' })
async oidcIssueCredentialWebhook(payload: Oid4vcCredentialOfferWebhookPayload): Promise<object> {
return this.oid4vcIssuanceService.storeOidcCredentialWebhook(payload);
Expand Down
10 changes: 9 additions & 1 deletion apps/oid4vc-issuance/src/oid4vc-issuance.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { ConfigModule as PlatformConfig } from '@credebl/config/config.module';
import { Oid4vcIssuanceRepository } from './oid4vc-issuance.repository';
import { NATSClient } from '@credebl/common/NATSClient';
import { PrismaService } from '@credebl/prisma-service';
import { StatusListAllocatorService } from './status-list-allocator.service';

@Module({
imports: [
Expand All @@ -35,6 +36,13 @@ import { PrismaService } from '@credebl/prisma-service';
CacheModule.register()
],
controllers: [Oid4vcIssuanceController],
providers: [Oid4vcIssuanceService, Oid4vcIssuanceRepository, PrismaService, Logger, NATSClient]
providers: [
Oid4vcIssuanceService,
Oid4vcIssuanceRepository,
PrismaService,
Logger,
NATSClient,
StatusListAllocatorService
]
})
export class Oid4vcIssuanceModule {}
Loading
Loading