Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
f268d48
feat(oid4vc): add support for jwt_vc_json-ld credential format
sagarkhole4 May 20, 2026
4011757
feat(oid4vc): add support for jwt_vc_json-ld credential format
sagarkhole4 May 20, 2026
03152e4
refactor(oid4vc): fix DTO formatting and nbf/exp verification
sagarkhole4 May 21, 2026
e931936
feat(oid4vc): fix jwt_vc_json-ld support and add holder service
sagarkhole4 Jun 5, 2026
05b4fbb
feat: replace context array with schemaUrl in SdJwtTemplate
sagarkhole4 Jun 15, 2026
70f9446
refactor(oid4vc): remove oid4vc-holder microservice and gateway integ…
sagarkhole4 Jun 23, 2026
4afb9e1
refactor(oid4vc): resolve security warnings, harden schema validation…
sagarkhole4 Jun 23, 2026
40c0412
feat(oid4vc): add support for jwt_vc_json-ld credential format
sagarkhole4 May 20, 2026
26d7b5c
feat(oid4vc): add support for jwt_vc_json-ld credential format
sagarkhole4 May 20, 2026
0d1652e
refactor(oid4vc): fix DTO formatting and nbf/exp verification
sagarkhole4 May 21, 2026
6211dd0
feat(oid4vc): fix jwt_vc_json-ld support and add holder service
sagarkhole4 Jun 5, 2026
2e05268
feat: replace context array with schemaUrl in SdJwtTemplate
sagarkhole4 Jun 15, 2026
ae7e8ea
refactor(oid4vc): remove oid4vc-holder microservice and gateway integ…
sagarkhole4 Jun 23, 2026
3a70c87
refactor(oid4vc): resolve security warnings, harden schema validation…
sagarkhole4 Jun 23, 2026
f73b179
feat(oid4vc):ldp_vc support for jsonLd
sagarkhole4 Jun 25, 2026
f584dc1
Merge branch 'feat/jwt_vc_jsonld' of https://github.com/credebl/platf…
sagarkhole4 Jun 25, 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 apps/api-gateway/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { LoggerModule } from '@credebl/logger/logger.module';
import { NotificationModule } from './notification/notification.module';
import { Oid4vcIssuanceModule } from './oid4vc-issuance/oid4vc-issuance.module';
import { Oid4vpModule } from './oid4vc-verification/oid4vc-verification.module';

import { OrganizationModule } from './organization/organization.module';
import { ConfigModule as PlatformConfig } from '@credebl/config/config.module';
import { PlatformModule } from './platform/platform.module';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,11 +274,13 @@ export class CredentialDto {

@ApiProperty({
description: 'Credential format type',
enum: ['mso_mdoc', 'vc+sd-jwt'],
enum: ['mso_mdoc', 'vc+sd-jwt', 'jwt_vc_json-ld', 'ldp_vc'],
example: 'mso_mdoc'
})
@IsString()
@IsIn(['mso_mdoc', 'vc+sd-jwt'], { message: 'format must be either "mso_mdoc" or "vc+sd-jwt"' })
@IsIn(['mso_mdoc', 'vc+sd-jwt', 'jwt_vc_json-ld', 'ldp_vc'], {
message: 'format must be either "mso_mdoc", "vc+sd-jwt", "jwt_vc_json-ld" or "ldp_vc"'
})
format: string;

@ApiProperty({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,14 @@ export class SdJwtTemplateDto {
@IsString()
vct: string;

@ApiPropertyOptional({
example: 'https://dev-schema.sovio.id/schemas/a99d55b6-c663-4af8-bcd3-4fe6699df91a',
description: 'JSON-LD schema URL for the credential'
})
@IsString()
@IsOptional()
schemaUrl?: string;

Comment on lines +199 to +206

@coderabbitai coderabbitai Bot Jun 5, 2026

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 | ⚡ Quick win

Add element-level string validation for context array.

The @IsArray decorator validates the field is an array, but doesn't validate that each element is a string. A malformed payload with non-string elements (e.g., context: [123, {}]) would pass validation.

Proposed fix
   `@ApiPropertyOptional`({
     example: ['https://www.w3.org/2018/credentials/v1', 'https://json-ld.org/contexts/person.jsonld'],
     description: 'JSON-LD context for the credential (optional, defaults to person schema for JSON-LD format)'
   })
   `@IsArray`()
+  `@IsString`({ each: true })
   `@IsOptional`()
   context?: string[];
📝 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
@ApiPropertyOptional({
example: ['https://www.w3.org/2018/credentials/v1', 'https://json-ld.org/contexts/person.jsonld'],
description: 'JSON-LD context for the credential (optional, defaults to person schema for JSON-LD format)'
})
@IsArray()
@IsOptional()
context?: string[];
`@ApiPropertyOptional`({
example: ['https://www.w3.org/2018/credentials/v1', 'https://json-ld.org/contexts/person.jsonld'],
description: 'JSON-LD context for the credential (optional, defaults to person schema for JSON-LD format)'
})
`@IsArray`()
`@IsString`({ each: true })
`@IsOptional`()
context?: string[];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts`
around lines 199 - 206, The context property is only decorated with `@IsArray` and
`@IsOptional`, so non-string elements can slip through; update the DTO by adding
element-level string validation to the context?: string[] property (use the
class-validator decorator that validates each array element is a string, e.g.,
`@IsString`({ each: true })) alongside the existing decorators on the property in
the Oid4vc issuer template DTO.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We do not expose or accept the context array directly from the user in the DTO. Instead, the backend automatically constructs it internally from the schemaUrl (which is strictly validated as a string via @IsString()) and a default constant. Therefore, this issue is already mitigated.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

@ApiProperty({
type: 'array',
items: { $ref: getSchemaPath(CredentialAttributeDto) },
Expand Down Expand Up @@ -229,8 +237,13 @@ export class CreateCredentialTemplateDto {
@IsEnum(CredentialFormat)
format: CredentialFormat;

@ValidateIf((o: CreateCredentialTemplateDto) => CredentialFormat.SdJwtVc === o.format)
@IsEmpty({ message: 'doctype must not be provided when format is "dc+sd-jwt"' })
@ValidateIf(
(o: CreateCredentialTemplateDto) =>
CredentialFormat.SdJwtVc === o.format ||
CredentialFormat.JwtVcJsonLd === o.format ||
CredentialFormat.LdpVc === o.format
)
Comment on lines +240 to +245

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 | ⚡ Quick win

Fix the lint-blocking ValidateIf arrow formatting.

The current multiline arrow predicate triggers the ESLint implicit-arrow-linebreak error and will fail CI.

Proposed fix
-  `@ValidateIf`(
-    (o: CreateCredentialTemplateDto) =>
-      CredentialFormat.SdJwtVc === o.format || CredentialFormat.JwtVcJsonLd === o.format
-  )
+  `@ValidateIf`(
+    (o: CreateCredentialTemplateDto) =>
+      CredentialFormat.SdJwtVc === o.format || CredentialFormat.JwtVcJsonLd === o.format
+  )
🧰 Tools
🪛 ESLint

[error] 234-234: Expected no linebreak before this expression.

(implicit-arrow-linebreak)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts`
around lines 232 - 235, The multiline arrow predicate for the `@ValidateIf`
decorator is causing an implicit-arrow-linebreak lint error; update the
predicate to be a single-line arrow function so ESLint accepts it. Locate the
`@ValidateIf` on CreateCredentialTemplateDto and replace the multiline predicate
with a single-line form such as (o: CreateCredentialTemplateDto) =>
CredentialFormat.SdJwtVc === o.format || CredentialFormat.JwtVcJsonLd ===
o.format, ensuring the decorator remains intact and the condition logic is
unchanged.

@IsEmpty({ message: 'doctype must not be provided when format is "dc+sd-jwt", "jwt_vc_json-ld" or "ldp_vc"' })
readonly _doctypeAbsentGuard?: unknown;

@ValidateIf((o: CreateCredentialTemplateDto) => CredentialFormat.Mdoc === o.format)
Expand All @@ -246,7 +259,11 @@ export class CreateCredentialTemplateDto {
@Type(({ object }) => {
if (object.format === CredentialFormat.Mdoc) {
return MdocTemplateDto;
} else if (object.format === CredentialFormat.SdJwtVc) {
} else if (
object.format === CredentialFormat.SdJwtVc ||
object.format === CredentialFormat.JwtVcJsonLd ||
object.format === CredentialFormat.LdpVc
) {
return SdJwtTemplateDto;
}
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { HttpModule } from '@nestjs/axios';
name: 'NATS_CLIENT',
transport: Transport.NATS,
options: getNatsOptions(
CommonConstants.ISSUANCE_SERVICE,
CommonConstants.OIDC4VC_ISSUANCE_SERVICE,
process.env.API_GATEWAY_NKEY_SEED,
process.env.NATS_CREDS_FILE
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ export class DcqlDto {
@ValidatorConstraint({ name: 'OnlyOneOf', async: false })
export class OnlyOneOfConstraint implements ValidatorConstraintInterface {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
validate(_: any, args: ValidationArguments): Promise<boolean> | boolean {
validate(_: unknown, args: ValidationArguments): Promise<boolean> | boolean {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const object = args.object as Record<string, any>;
const properties = args.constraints as string[];
Expand Down Expand Up @@ -356,10 +356,18 @@ export class PresentationRequestDto {
@IsDefined()
@IsEnum(ResponseMode)
responseMode: ResponseMode;
//TODO: check e2e flow and add ResponseMode based restrictions
@IsOptional()
expectedOrigins: string[];

@ApiPropertyOptional({
description: 'OpenID4VP version to use',
enum: ['v1', 'v1.draft21', 'v1.draft24'],
example: 'v1.draft24'
})
@IsOptional()
@IsEnum(['v1', 'v1.draft21', 'v1.draft24'])
version?: 'v1' | 'v1.draft21' | 'v1.draft24';

/**
* Dummy property used to run a class-level validation ensuring mutual exclusivity.
* This property is not serialized into requests/responses but is required so `class-validator`
Expand Down
3 changes: 3 additions & 0 deletions apps/oid4vc-issuance/constant/issuance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ export const accessTokenSignerKeyType = { kty: 'OKP', crv: 'Ed25519' } as {
crv: AccessTokenSignerKeyType;
};
export const batchCredentialIssuanceDefault = 0;

export const CREDENTIALS_CONTEXT_V1_URL = 'https://www.w3.org/2018/credentials/v1';
export const CREDENTIALS_CONTEXT_V2_URL = 'https://www.w3.org/ns/credentials/v2';
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { OpenId4VcIssuanceSessionState } from '@credebl/enum/enum';
* --------------------------------------------------------- */
export enum CredentialFormat {
SdJwtVc = 'vc+sd-jwt',
MsoMdoc = 'mso_mdoc'
MsoMdoc = 'mso_mdoc',
JwtVcJsonLd = 'jwt_vc_json-ld',
LdpVc = 'ldp_vc'
}

export enum SignerMethodOption {
Expand Down
2 changes: 2 additions & 0 deletions apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Prisma, SignerOption } from '@prisma/client';
import { AttributeType, CredentialFormat } from '@credebl/enum/enum';
export interface SdJwtTemplate {
vct: string;
schemaUrl?: string;
context?: string[];
attributes: CredentialAttribute[];
}

Expand Down
159 changes: 156 additions & 3 deletions apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,14 +150,29 @@
if (['sd-jwt', 'dc+sd-jwt', 'vc+sd-jwt', 'sdjwt', 'sd+jwt-vc'].includes(normalized)) {
return CredentialFormat.SdJwtVc;
}
if (['jwt_vc_json-ld', 'jwt-vc-json-ld', 'w3c-jwt-json-ld'].includes(normalized)) {
return CredentialFormat.JwtVcJsonLd;
}
if (['ldp_vc', 'ldp-vc'].includes(normalized)) {
return CredentialFormat.LdpVc;
}
if ('mso_mdoc' === normalized || 'mso-mdoc' === normalized || 'mdoc' === normalized) {
return CredentialFormat.Mdoc;
}
throw new UnprocessableEntityException(`Unsupported template format: ${dbFormat}`);
}

function formatSuffix(apiFormat: CredentialFormat): 'sdjwt' | 'mdoc' {
return apiFormat === CredentialFormat.SdJwtVc ? 'sdjwt' : 'mdoc';
function formatSuffix(apiFormat: CredentialFormat): 'sdjwt' | 'mdoc' | 'jwt-vc-json-ld' | 'ldp-vc' {
if (apiFormat === CredentialFormat.SdJwtVc) {
return 'sdjwt';
}
if (apiFormat === CredentialFormat.JwtVcJsonLd) {
return 'jwt-vc-json-ld';
}
if (apiFormat === CredentialFormat.LdpVc) {
return 'ldp-vc';
}
return 'mdoc';
}

export function buildCredentialOfferUrl(baseUrl: string, getAllCredentialOffer: GetAllCredentialOffer): string {
Expand Down Expand Up @@ -213,7 +228,11 @@
}
};

if (CredentialFormat.SdJwtVc === template.format) {
if (
CredentialFormat.SdJwtVc === template.format ||
CredentialFormat.JwtVcJsonLd === template.format ||
CredentialFormat.LdpVc === template.format
) {
validateAttributes((template.attributes as SdJwtTemplate).attributes ?? [], payload);
} else if (CredentialFormat.Mdoc === template.format) {
const namespaces = payload?.namespaces;
Expand Down Expand Up @@ -456,6 +475,137 @@
};
}

function buildJwtVcJsonLdCredential(

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 26 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=credebl_platform&issues=AZ7x9x3UIpeCryPN-gH4&open=AZ7x9x3UIpeCryPN-gH4&pullRequest=1646
credentialRequest: CredentialRequestDtoLike,
templateRecord: CredentialTemplateRecord,
signerOptions: ISignerOption[],
activeCertificateDetails?: X509CertificateRecord[]
): BuiltCredential {
const payloadCopy = { ...(credentialRequest.payload as Record<string, unknown>) };

let expectedSignerMethod: SignerMethodOption;
if (templateRecord.signerOption === SignerOption.DID) {
expectedSignerMethod = SignerMethodOption.DID;
} else if (
templateRecord.signerOption === SignerOption.X509_P256 ||
templateRecord.signerOption === SignerOption.X509_ED25519
) {
expectedSignerMethod = SignerMethodOption.X5C;
} else {
throw new UnprocessableEntityException(
`Unknown signer option "${templateRecord.signerOption}" for template ${templateRecord.id}`
);
}

const templateSignerOption: ISignerOption | undefined = signerOptions?.find((x) => x.method === expectedSignerMethod);
if (!templateSignerOption) {
throw new UnprocessableEntityException(
`Signer option "${expectedSignerMethod}" is not configured for template ${templateRecord.id}`
);
}

if (expectedSignerMethod === SignerMethodOption.X5C && credentialRequest.validityInfo) {
if (!activeCertificateDetails?.length) {
throw new UnprocessableEntityException('Active x.509 certificate details are required for x5c signer templates.');
}
const certificateDetail = activeCertificateDetails.find(
(x) => x.certificateBase64 === templateSignerOption.x5c?.[0]
);
if (!certificateDetail) {
throw new UnprocessableEntityException('No active x.509 certificate matches the configured signer option.');
}

const validationResult = validateCredentialDatesInCertificateWindow(
credentialRequest.validityInfo,
certificateDetail
);
if (!validationResult.isValid) {
throw new UnprocessableEntityException(`${JSON.stringify(validationResult.details)}`);
}
}

let nbf: number | undefined;
let exp: number | undefined;

if (credentialRequest.validityInfo) {
const credentialValidFrom = new Date(credentialRequest.validityInfo.validFrom);
const credentialValidTo = new Date(credentialRequest.validityInfo.validUntil);
const isCredentialDurationValid = credentialValidFrom <= credentialValidTo;
if (!isCredentialDurationValid) {
const errorDetails = {
credentialDurationValid: isCredentialDurationValid,
credentialValidFrom: credentialValidFrom.toISOString(),
credentialValidTo: credentialValidTo.toISOString()
};
throw new UnprocessableEntityException(`${JSON.stringify(errorDetails)}`);
}
nbf = dateToSeconds(credentialValidFrom);
exp = dateToSeconds(credentialValidTo);
}

const apiFormat = mapDbFormatToApiFormat(templateRecord.format);
const idSuffix = formatSuffix(apiFormat);
const credentialSupportedId = `${templateRecord.name}-${idSuffix}`;
const vct = (templateRecord.attributes as any)?.vct;
let typeName = '';
if ('string' === typeof vct) {
const cleanVct = vct.replace(/\/+$/, '');

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Simplify this regular expression to reduce its runtime, as it has super-linear performance due to backtracking.

See more on https://sonarcloud.io/project/issues?id=credebl_platform&issues=AZ70tRvlpYSNW_lXHZnK&open=AZ70tRvlpYSNW_lXHZnK&pullRequest=1646
const lastSlash = cleanVct.lastIndexOf('/');
typeName = -1 !== lastSlash ? cleanVct.substring(lastSlash + 1) : cleanVct;

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected negated condition.

See more on https://sonarcloud.io/project/issues?id=credebl_platform&issues=AZ70tRvlpYSNW_lXHZnL&open=AZ70tRvlpYSNW_lXHZnL&pullRequest=1646
} else {
typeName = templateRecord.name.replace(/\s+/g, '');
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const templateContext = (templateRecord.attributes as any)?.context;
const context = Array.isArray(templateContext) ? [...templateContext] : ['https://www.w3.org/2018/credentials/v1'];
const requiredContextUrl = 'https://www.w3.org/2018/credentials/v1';
const normalizeUrlForComparison = (value: unknown): string | null => {
if ('string' !== typeof value) {
return null;
}
try {
return new URL(value).toString();
} catch {
return null;
}
};
const normalizedRequiredContext = normalizeUrlForComparison(requiredContextUrl);
const hasRequiredContext = context.some(
(ctx) => null !== normalizedRequiredContext && normalizeUrlForComparison(ctx) === normalizedRequiredContext
);

if (!hasRequiredContext) {
context.unshift(requiredContextUrl);
}

const wrappedPayload: Record<string, any> = {
'@context': context,
type: ['VerifiableCredential', typeName],
credentialSubject: payloadCopy
};

if (nbf !== undefined) {
wrappedPayload.nbf = nbf;
}
if (exp !== undefined) {
wrappedPayload.exp = exp;
}

if (credentialRequest.validityInfo) {
wrappedPayload.issuanceDate = new Date(credentialRequest.validityInfo.validFrom).toISOString();
wrappedPayload.expirationDate = new Date(credentialRequest.validityInfo.validUntil).toISOString();
} else {
wrappedPayload.issuanceDate = new Date().toISOString();
}

return {
credentialSupportedId,
signerOptions: templateSignerOption ? templateSignerOption : undefined,

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unnecessary use of conditional expression for default assignment.

See more on https://sonarcloud.io/project/issues?id=credebl_platform&issues=AZ7x9x3UIpeCryPN-gH6&open=AZ7x9x3UIpeCryPN-gH6&pullRequest=1646
format: apiFormat,
payload: wrappedPayload
};
}

export function buildCredentialOfferPayload(
dto: CreateOidcCredentialOfferDtoLike,
templates: credential_templates[],
Expand Down Expand Up @@ -489,6 +639,9 @@
if (apiFormat === CredentialFormat.SdJwtVc) {
return buildSdJwtCredential(credentialRequest, templateRecord, signerOptions, activeCertificateDetails);
}
if (apiFormat === CredentialFormat.JwtVcJsonLd || apiFormat === CredentialFormat.LdpVc) {
return buildJwtVcJsonLdCredential(credentialRequest, templateRecord, signerOptions, activeCertificateDetails);
}
if (apiFormat === CredentialFormat.Mdoc) {
return buildMdocCredential(credentialRequest, templateRecord, signerOptions, activeCertificateDetails);
}
Expand Down
Loading