Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion .env.demo
Original file line number Diff line number Diff line change
Expand Up @@ -252,4 +252,5 @@ NATS_AUTH_TYPE=nkey
# 'nkey' | 'creds' | 'usernamePassword' | 'none'
NOTIFICATION_NATS_AUTH_TYPE=

ENABLE_NATS_NOTIFICATION=false
ENABLE_NATS_NOTIFICATION=false
STATUS_LIST_HOST=
3 changes: 2 additions & 1 deletion .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -280,4 +280,5 @@ NATS_AUTH_TYPE=nkey
# 'nkey' | 'creds' | 'usernamePassword' | 'none'
NOTIFICATION_NATS_AUTH_TYPE=

ENABLE_NATS_NOTIFICATION=false
ENABLE_NATS_NOTIFICATION=false
STATUS_LIST_HOST=
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; statusListDetails?: object }): Promise<object> {
return this.agentServiceService.oidcRevokeCredential(payload.url, payload.orgId, payload.statusListDetails);
}

@MessagePattern({ cmd: 'agent-create-x509-certificate' })
async createX509Certificate(payload: {
options: X509CreateCertificateOptions;
Expand Down
13 changes: 13 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,19 @@ export class AgentServiceService {
}
}

async oidcRevokeCredential(url: string, orgId: string, statusListDetails?: object): Promise<object> {
try {
const getApiKey = await this.getOrgAgentApiKey(orgId);
const data = await this.commonService
.httpPost(`${url}`, statusListDetails || {}, { headers: { authorization: getApiKey } })
.then(async (response) => response);
return data;
} catch (error) {
this.logger.error(`Error in _oidcRevokeCredential in agent service : ${JSON.stringify(error)}`);
throw error;
}
}

async oidcIssuerTemplate(templatePayload, url: string, orgId: string): Promise<object> {
try {
const getApiKey = await this.getOrgAgentApiKey(orgId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,10 @@ export class CreateOidcCredentialOfferDto {
noticeUrl?: string;

issuerId?: string;

@ApiPropertyOptional({ example: true, description: 'Flag to enable revocation for the issued credentials' })
@IsOptional()
isRevocable?: boolean;
Comment on lines +199 to +201
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

🧩 Analysis chain

🏁 Script executed:

cat -n apps/api-gateway/src/oid4vc-issuance/dtos/issuer-sessions.dto.ts | sed -n '180,200p'

Repository: credebl/platform

Length of output: 846


🏁 Script executed:

cat -n apps/api-gateway/src/oid4vc-issuance/dtos/issuer-sessions.dto.ts | sed -n '380,400p'

Repository: credebl/platform

Length of output: 811


🏁 Script executed:

wc -l apps/api-gateway/src/oid4vc-issuance/dtos/issuer-sessions.dto.ts

Repository: credebl/platform

Length of output: 128


🏁 Script executed:

rg "isRevocable" --type ts -B 2 -A 2

Repository: credebl/platform

Length of output: 4843


🏁 Script executed:

rg "if.*isRevocable|\.isRevocable" --type ts -B 1 -A 1

Repository: credebl/platform

Length of output: 1701


🏁 Script executed:

head -30 apps/api-gateway/src/oid4vc-issuance/dtos/issuer-sessions.dto.ts

Repository: credebl/platform

Length of output: 938


Add @IsBoolean() validator to isRevocable fields in both DTOs.

Without boolean validation, requests can send "false" or "true" strings which pass validation. Since JavaScript treats non-empty strings as truthy, the if (dto.isRevocable) checks in the service layer will incorrectly enable revocation even when "false" is provided.

Both occurrences in CreateOidcCredentialOfferDto (lines 191-193) and CreateCredentialOfferD2ADto (lines 389-391) need the @IsBoolean() decorator added after @IsOptional(). Update the imports to include IsBoolean from class-validator.

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

In `@apps/api-gateway/src/oid4vc-issuance/dtos/issuer-sessions.dto.ts` around
lines 191 - 193, The isRevocable property in both CreateOidcCredentialOfferDto
and CreateCredentialOfferD2ADto lacks a boolean validator, allowing string
"true"/"false" to pass and cause incorrect truthy checks; add `@IsBoolean`()
immediately after `@IsOptional`() on the isRevocable field in both DTO classes and
update the imports to include IsBoolean from class-validator so the validation
enforces a real boolean type before service-layer checks.

}

export class GetAllCredentialOfferDto {
Expand Down Expand Up @@ -300,7 +304,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 @@ -390,6 +394,10 @@ export class CreateCredentialOfferD2ADto {
@IsOptional()
issuerId?: string;

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

@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
5 changes: 5 additions & 0 deletions apps/oid4vc-issuance/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import { Oid4vcIssuanceModule } from './oid4vc-issuance.module';
const logger = new Logger();

async function bootstrap(): Promise<void> {
if (!process.env.STATUS_LIST_HOST) {
logger.error('STATUS_LIST_HOST is not configured. Microservice cannot start.');
process.exit(1);
}

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