Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
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'],
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'], {
message: 'format must be either "mso_mdoc", "vc+sd-jwt" or "jwt_vc_json-ld"'
})
format: string;

@ApiProperty({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,8 +229,11 @@ 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
)
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" or "jwt_vc_json-ld"' })
readonly _doctypeAbsentGuard?: unknown;

@ValidateIf((o: CreateCredentialTemplateDto) => CredentialFormat.Mdoc === o.format)
Expand All @@ -246,7 +249,7 @@ 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) {
return SdJwtTemplateDto;
}
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { OpenId4VcIssuanceSessionState } from '@credebl/enum/enum';
* --------------------------------------------------------- */
export enum CredentialFormat {
SdJwtVc = 'vc+sd-jwt',
MsoMdoc = 'mso_mdoc'
MsoMdoc = 'mso_mdoc',
JwtVcJsonLd = 'jwt_vc_json-ld'
}

export enum SignerMethodOption {
Expand Down
109 changes: 106 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,23 @@
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 ('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' {
if (apiFormat === CredentialFormat.SdJwtVc) {
return 'sdjwt';
}
if (apiFormat === CredentialFormat.JwtVcJsonLd) {
return 'jwt-vc-json-ld';
}
return 'mdoc';
}

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

if (CredentialFormat.SdJwtVc === template.format) {
if (CredentialFormat.SdJwtVc === template.format || CredentialFormat.JwtVcJsonLd === 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 +465,97 @@
};
}

function buildJwtVcJsonLdCredential(

Check failure on line 468 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 18 to the 15 allowed.

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

Check warning on line 474 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=AZ5E7H7LjB2pEMc2JPkF&open=AZ5E7H7LjB2pEMc2JPkF&pullRequest=1646

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 wrappedPayload: Record<string, any> = {
credentialSubject: payloadCopy
};

if (nbf) {
wrappedPayload.nbf = nbf;
}
if (exp) {
wrappedPayload.exp = exp;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

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

Check warning on line 553 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=AZ5E7H7LjB2pEMc2JPkG&open=AZ5E7H7LjB2pEMc2JPkG&pullRequest=1646
format: apiFormat,
payload: wrappedPayload
};
}

export function buildCredentialOfferPayload(
dto: CreateOidcCredentialOfferDtoLike,
templates: credential_templates[],
Expand Down Expand Up @@ -489,6 +589,9 @@
if (apiFormat === CredentialFormat.SdJwtVc) {
return buildSdJwtCredential(credentialRequest, templateRecord, signerOptions, activeCertificateDetails);
}
if (apiFormat === CredentialFormat.JwtVcJsonLd) {
return buildJwtVcJsonLdCredential(credentialRequest, templateRecord, signerOptions, activeCertificateDetails);
}
if (apiFormat === CredentialFormat.Mdoc) {
return buildMdocCredential(credentialRequest, templateRecord, signerOptions, activeCertificateDetails);
}
Expand Down
39 changes: 37 additions & 2 deletions apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,6 @@
return claims;
}

//TODO: Fix this eslint issue
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function buildSdJwtCredentialConfig(name: string, template: SdJwtTemplate) {
const formatSuffix = 'sdjwt';
Expand Down Expand Up @@ -332,12 +331,44 @@
};
}

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function buildJwtVcJsonLdCredentialConfig(name: string, template: SdJwtTemplate) {
const formatSuffix = 'jwt-vc-json-ld';

// Determine the unique key for this credential configuration
const configKey = `${name}-${formatSuffix}`;
const credentialScope = `openid4vc:${template.vct}-${formatSuffix}`;

const claims = buildClaimsFromTemplate(template);

return {
[configKey]: {
format: CredentialFormat.JwtVcJsonLd,
scope: credentialScope,
vct: template.vct,
credential_signing_alg_values_supported: [...STATIC_CREDENTIAL_ALGS_FOR_SDJWT],
cryptographic_binding_methods_supported: [...STATIC_BINDING_METHODS_FOR_SDJWT],
proof_types_supported: {
jwt: {
proof_signing_alg_values_supported: ['ES256', 'EdDSA']
}
},
credential_metadata: {
claims,
display: []
}
}
};
}

//TODO: Fix this eslint issue
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function buildCredentialConfig(name: string, template: SdJwtTemplate | MdocTemplate, format: CredentialFormat) {
switch (format) {
case CredentialFormat.SdJwtVc:
return buildSdJwtCredentialConfig(name, template as SdJwtTemplate);
case CredentialFormat.JwtVcJsonLd:
return buildJwtVcJsonLdCredentialConfig(name, template as SdJwtTemplate);
case CredentialFormat.Mdoc:
return buildMdocCredentialConfig(name, template as MdocTemplate);
default:
Expand All @@ -361,7 +392,11 @@
const credentialConfig = buildCredentialConfig(
templateRow.name,
templateToBuild,
format === CredentialFormat.Mdoc ? CredentialFormat.Mdoc : CredentialFormat.SdJwtVc
format === CredentialFormat.Mdoc
? CredentialFormat.Mdoc
: format === CredentialFormat.JwtVcJsonLd
? CredentialFormat.JwtVcJsonLd
: CredentialFormat.SdJwtVc

Check warning on line 399 in apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=credebl_platform&issues=AZ5E7H-gjB2pEMc2JPkH&open=AZ5E7H-gjB2pEMc2JPkH&pullRequest=1646
);
const appearanceJson = coerceJsonObject<unknown>(templateRow.appearance);

Expand Down
3 changes: 2 additions & 1 deletion libs/enum/src/enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,8 @@ export enum X509ExtendedKeyUsage {

export enum CredentialFormat {
SdJwtVc = 'dc+sd-jwt',
Mdoc = 'mso_mdoc'
Mdoc = 'mso_mdoc',
JwtVcJsonLd = 'jwt_vc_json-ld'
}

export enum AttributeType {
Expand Down