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
2 changes: 2 additions & 0 deletions src/controllers/openid4vc/holder/credentialBindingResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ export function getCredentialBindingResolver({
supportsJwk &&
(credentialFormat === OpenId4VciCredentialFormatProfile.SdJwtVc ||
credentialFormat === OpenId4VciCredentialFormatProfile.SdJwtDc ||
credentialFormat === OpenId4VciCredentialFormatProfile.JwtVcJsonLd ||
credentialFormat === OpenId4VciCredentialFormatProfile.JwtVcJson ||
credentialFormat === OpenId4VciCredentialFormatProfile.MsoMdoc)
) {
return {
Expand Down
8 changes: 8 additions & 0 deletions src/controllers/openid4vc/holder/holder.Controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ export class HolderController extends Controller {
return await holderService.getMdocCredentials(request)
}

/**
* Fetch all W3C credentials in wallet
*/
@Get('/w3c-vcs')
public async getW3cCredentials(@Request() request: Req) {
return await holderService.getW3cCredentials(request)
}

/**
* Decode mso mdoc credential in wallet
*/
Expand Down
101 changes: 80 additions & 21 deletions src/controllers/openid4vc/holder/holder.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,15 @@
} from '@credo-ts/openid4vc'
import type { Request as Req } from 'express'

import { Mdoc, SdJwtVcRecord, MdocRecord } from '@credo-ts/core'
import {
Mdoc,
SdJwtVcRecord,
MdocRecord,
W3cCredentialRecord,
W3cCredentialService,
// W3cV2CredentialRecord,
// W3cV2CredentialService,
} from '@credo-ts/core'
import {
OpenId4VciAuthorizationFlow,
authorizationCodeGrantIdentifier,
Expand All @@ -36,6 +44,19 @@
return await agentReq.agent.mdoc.getAll()
}

public async getW3cCredentials(agentReq: Req) {
/*
// W3C V2.0 Support
const [v1Records, v2Records] = await Promise.all([
agentReq.agent.w3cCredentials.getAll(),
agentReq.agent.w3cV2Credentials.getAll(),
])

return [...v1Records, ...v2Records]
*/
return await agentReq.agent.w3cCredentials.getAll()
}

public async decodeMdocCredential(
agentReq: Req,
options: {
Expand Down Expand Up @@ -129,10 +150,28 @@
const storedCredentials = await Promise.all(
credentialResponse.credentials.map(async (response) => {
const credentialRecord = response.record
// TODO: We can add this later
// if (credential instanceof W3cJwtVerifiableCredential || credential instanceof W3cJsonLdVerifiableCredential) {
// return await agentReq.agent.w3cCredentials.storeCredential({ credential })
// }

if (
credentialRecord instanceof W3cCredentialRecord ||
(credentialRecord as any).type === 'W3cCredentialRecord'

Check warning on line 156 in src/controllers/openid4vc/holder/holder.service.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_afj-controller&issues=AZ5O_DwuDmikfhuLIni1&open=AZ5O_DwuDmikfhuLIni1&pullRequest=389
) {
return await agentReq.agent.w3cCredentials.store({
record: credentialRecord as W3cCredentialRecord,
})
}

/*
W3C V2.0 Support
if (
credentialRecord instanceof W3cV2CredentialRecord ||
(credentialRecord as any).type === 'W3cV2CredentialRecord'
) {
return await agentReq.agent.w3cV2Credentials.store({
record: credentialRecord as W3cV2CredentialRecord,
})
}
*/

if (credentialRecord instanceof MdocRecord) {
return await agentReq.agent.mdoc.store({ record: credentialRecord })
}
Expand All @@ -141,7 +180,9 @@
record: credentialRecord,
})
}
throw new Error(`Unsupported credential record type`)
throw new Error(
`Unsupported credential record type: ${(credentialRecord as any)?.type || typeof credentialRecord}`,

Check warning on line 184 in src/controllers/openid4vc/holder/holder.service.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_afj-controller&issues=AZ5O_DwuDmikfhuLIni2&open=AZ5O_DwuDmikfhuLIni2&pullRequest=389
)
}),
)

Expand Down Expand Up @@ -211,23 +252,28 @@
)
// const presentationExchangeService = agent.dependencyManager.resolve(DifPresentationExchangeService)

if (!resolved.dcql) throw new Error('Missing DCQL on request')
//
let dcqlCredentials
try {
dcqlCredentials = await agentReq.agent.modules.openid4vc.holder.selectCredentialsForDcqlRequest(
let acceptOptions: any = {
authorizationRequestPayload: resolved.authorizationRequestPayload,
origin: body.options?.origin,
}

if (resolved.dcql) {
const dcqlCredentials = await agentReq.agent.modules.openid4vc.holder.selectCredentialsForDcqlRequest(
resolved.dcql.queryResult,
)
} catch (error) {
throw error
acceptOptions.dcql = { credentials: dcqlCredentials as DcqlCredentialsForRequest }
} else if (resolved.presentationExchange) {
const pexCredentials =
await agentReq.agent.modules.openid4vc.holder.selectCredentialsForPresentationExchangeRequest(
resolved.presentationExchange.credentialsForRequest,
)
acceptOptions.presentationExchange = { credentials: pexCredentials }
} else {
throw new Error('Missing DCQL or Presentation Exchange on request')
}
const submissionResult = await agentReq.agent.modules.openid4vc.holder.acceptOpenId4VpAuthorizationRequest({
authorizationRequestPayload: resolved.authorizationRequestPayload,
dcql: {
credentials: dcqlCredentials as DcqlCredentialsForRequest,
},
origin: body.options?.origin,
})

const submissionResult =
await agentReq.agent.modules.openid4vc.holder.acceptOpenId4VpAuthorizationRequest(acceptOptions)
if (submissionResult.serverResponse) {
const { serverResponse, ...rest } = submissionResult

Expand All @@ -243,7 +289,20 @@
}

public async deleteCredential(agentReq: Req, { credentialId, credentialType }: DeleteCredentialBody) {
if (credentialType === CredentialType.SD_JWT) {
if (credentialType === CredentialType.W3C_VC) {
const w3cCredentialService = await agentReq.agent.dependencyManager.resolve(W3cCredentialService)
// const w3cV2CredentialService = await agentReq.agent.dependencyManager.resolve(W3cV2CredentialService)

try {
return await w3cCredentialService.removeCredentialRecord(agentReq.agent.context, credentialId)
} catch (error) {
/*
// W3C V2.0 Support
return await w3cV2CredentialService.removeCredentialRecord(agentReq.agent.context, credentialId)
*/
throw error
}
} else if (credentialType === CredentialType.SD_JWT) {
const sdJwtRecord = await agentReq.agent.sdJwtVc.getById(credentialId)
if (sdJwtRecord) {
return await agentReq.agent.sdJwtVc.deleteById(credentialId)
Expand Down
147 changes: 112 additions & 35 deletions src/controllers/openid4vc/issuance-sessions/issuance-sessions.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
import type { Request as Req } from 'express'

import { type OpenId4VcIssuanceSessionState } from '@credo-ts/openid4vc'
import { OpenId4VcIssuanceSessionRepository } from '@credo-ts/openid4vc'

Check failure on line 5 in src/controllers/openid4vc/issuance-sessions/issuance-sessions.service.ts

View workflow job for this annotation

GitHub Actions / Validate

There should be no empty line within import group

import { CREDENTIALS_CONTEXT_V1_URL, CREDENTIALS_CONTEXT_V2_URL } from '@credo-ts/core'

Check failure on line 7 in src/controllers/openid4vc/issuance-sessions/issuance-sessions.service.ts

View workflow job for this annotation

GitHub Actions / Validate

`@credo-ts/core` import should occur before import of `@credo-ts/openid4vc`
Comment on lines 5 to +7
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

Fix import order violations.

The pipeline and ESLint are flagging import order issues: the @credo-ts/core import should appear before @credo-ts/openid4vc, and the empty line between related @credo-ts imports should be removed.

📦 Proposed fix
 import type { OpenId4VcIssuanceSessionState } from '`@credo-ts/openid4vc`'
+import { CREDENTIALS_CONTEXT_V1_URL, CREDENTIALS_CONTEXT_V2_URL } from '`@credo-ts/core`'
 import { OpenId4VcIssuanceSessionRepository } from '`@credo-ts/openid4vc`'
-
-import { CREDENTIALS_CONTEXT_V1_URL, CREDENTIALS_CONTEXT_V2_URL } from '`@credo-ts/core`'
-
 import { CredentialFormat, SignerMethod } from '../../../enums/enum'
🧰 Tools
🪛 ESLint

[error] 5-5: There should be no empty line within import group

(import/order)


[error] 7-7: @credo-ts/core import should occur before import of @credo-ts/openid4vc

(import/order)

🪛 GitHub Actions: Continuous Integration / Validate

[error] 5-5: ESLint import/order: There should be no empty line within import group

🪛 GitHub Check: Validate

[failure] 7-7:
@credo-ts/core import should occur before import of @credo-ts/openid4vc


[failure] 5-5:
There should be no empty line within import group

🤖 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 `@src/controllers/openid4vc/issuance-sessions/issuance-sessions.service.ts`
around lines 5 - 7, Reorder and group the related `@credo-ts` imports to satisfy
import-order rules: move the CREDENTIALS_CONTEXT_V1_URL and
CREDENTIALS_CONTEXT_V2_URL import (symbols CREDENTIALS_CONTEXT_V1_URL,
CREDENTIALS_CONTEXT_V2_URL) so it appears before the
OpenId4VcIssuanceSessionRepository import (symbol
OpenId4VcIssuanceSessionRepository) and remove the blank line between them so
both `@credo-ts` imports are adjacent and correctly ordered.


import { CredentialFormat, SignerMethod } from '../../../enums/enum'
import { BadRequestError, NotFoundError } from '../../../errors/errors'
import { STATUS_LISTS_PATH } from '../../../utils/constant'
Expand All @@ -24,18 +26,27 @@
credentials.map(async (cred) => {
const supported = issuer.credentialConfigurationsSupported[cred.credentialSupportedId]

this.validateCredentialConfig(cred, supported)
const format = cred.format as unknown as CredentialFormat
const isJsonLdFormat = format === CredentialFormat.JwtVcJsonLd || format === CredentialFormat.LdpVc
const effectiveVersion = options.version === 'v2.0' && isJsonLdFormat ? 'v2.0' : undefined

this.validateCredentialConfig(cred, supported, effectiveVersion)

const statusBlock = await this.processStatusList(cred, options, agentReq, offerStatusInfo)

const currentVct = cred.payload && 'vct' in cred.payload ? cred.payload.vct : undefined
return {
...cred,
payload: {
const transformedPayload = this.transformPayloadForVersion(
{
...cred.payload,
vct: currentVct ?? (typeof supported.vct === 'string' ? supported.vct : undefined),
...(statusBlock ? { status: statusBlock } : {}),
},
effectiveVersion,
)
Comment thread
sagarkhole4 marked this conversation as resolved.

return {
...cred,
payload: transformedPayload,
}
}),
)
Expand All @@ -53,47 +64,19 @@
if (!issuerModule) {
throw new Error('OID4VC issuer module not initialized')
}
const preAuthorizedCodeFlowConfig = this.resolvePreAuthorizedCodeFlowConfig(options.preAuthorizedCodeFlowConfig)

const { credentialOffer, issuanceSession } = await issuerModule.createCredentialOffer({
issuerId: publicIssuerId,
issuanceMetadata: options.issuanceMetadata,
credentialConfigurationIds: credentials.map((c) => c.credentialSupportedId),
preAuthorizedCodeFlowConfig,
preAuthorizedCodeFlowConfig: options.preAuthorizedCodeFlowConfig,
authorizationCodeFlowConfig: options.authorizationCodeFlowConfig,
version: 'v1',
})

return { credentialOffer, issuanceSession }
}

private resolvePreAuthorizedCodeFlowConfig(
config: OpenId4VcIssuanceSessionsCreateOffer['preAuthorizedCodeFlowConfig'],
) {
if (!config) return undefined

const hasTxCode = config.txCode != null
const hasAuthServerUrl = config.authorizationServerUrl != null

if (hasTxCode !== hasAuthServerUrl) {
throw new BadRequestError(
'Both txCode and authorizationServerUrl must be provided together for normal flow, or both must be omitted for no-auth flow',
)
}

if (!hasTxCode) return {}

if (Object.keys(config.txCode!).length === 0) {
throw new BadRequestError('txCode must not be an empty object when provided')
}

if (config.authorizationServerUrl!.trim() === '') {
throw new BadRequestError('authorizationServerUrl must not be an empty string when provided')
}

return { txCode: config.txCode, authorizationServerUrl: config.authorizationServerUrl }
}

private validateCredentialConfig(cred: any, supported: any) {
private validateCredentialConfig(cred: any, supported: any, version?: string) {

Check failure on line 79 in src/controllers/openid4vc/issuance-sessions/issuance-sessions.service.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

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

See more on https://sonarcloud.io/project/issues?id=credebl_afj-controller&issues=AZ5O_DqiDmikfhuLIniw&open=AZ5O_DqiDmikfhuLIniw&pullRequest=389
if (!supported) {
throw new Error(`CredentialSupportedId '${cred.credentialSupportedId}' is not supported by issuer`)
}
Expand All @@ -103,6 +86,26 @@
)
}

const isW3cFormat =
cred.format === CredentialFormat.JwtVcJson ||
cred.format === CredentialFormat.JwtVcJsonLd ||
cred.format === CredentialFormat.LdpVc

if (isW3cFormat && !cred.payload?.credentialSubject) {
throw new BadRequestError(
`Credential payload for '${cred.credentialSupportedId}' must contain 'credentialSubject'`,
)
}

if (version === 'v2.0') {
if (cred.payload.issuer) {
const issuer = cred.payload.issuer
if (typeof issuer === 'object' && !issuer.id) {
throw new BadRequestError(`Issuer object for '${cred.credentialSupportedId}' must contain 'id' property`)
}
}
}

if (!cred.signerOptions?.method) {
throw new BadRequestError(
`signerOptions must be provided and allowed methods are ${Object.values(SignerMethod).join(', ')}`,
Expand All @@ -122,6 +125,80 @@
}
}

private transformPayloadForVersion(payload: any, version: 'v1.1' | 'v2.0' | undefined) {

Check failure on line 128 in src/controllers/openid4vc/issuance-sessions/issuance-sessions.service.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

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

See more on https://sonarcloud.io/project/issues?id=credebl_afj-controller&issues=AZ5O_DqiDmikfhuLInix&open=AZ5O_DqiDmikfhuLInix&pullRequest=389
if (version !== 'v2.0') {
return payload
}

const transformed = { ...payload }

const formatDate = (date: any) => {
if (!date) return undefined
if (date instanceof Date) return date.toISOString()
if (typeof date === 'string') {
try {
const d = new Date(date)
if (isNaN(d.getTime())) return date

Check warning on line 141 in src/controllers/openid4vc/issuance-sessions/issuance-sessions.service.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `Number.isNaN` over `isNaN`.

See more on https://sonarcloud.io/project/issues?id=credebl_afj-controller&issues=AZ5O_DqiDmikfhuLIniy&open=AZ5O_DqiDmikfhuLIniy&pullRequest=389
return d.toISOString()
} catch {
return date
}
}
return date
}

// Rule: issuanceDate -> validFrom
if (transformed.issuanceDate && !transformed.validFrom) {
transformed.validFrom = transformed.issuanceDate
}

// Rule: expirationDate -> validUntil
if (transformed.expirationDate && !transformed.validUntil) {
transformed.validUntil = transformed.expirationDate
delete transformed.expirationDate
}

// Normalize dates to ISO format
if (transformed.validFrom) transformed.validFrom = formatDate(transformed.validFrom)
if (transformed.validUntil) transformed.validUntil = formatDate(transformed.validUntil)

// Rule: issuer string -> object (standardizing for v2.0 if it is a DID)
if (typeof transformed.issuer === 'string' && transformed.issuer.startsWith('did:')) {
transformed.issuer = { id: transformed.issuer }
}

// Rule: Update @context for v2.0
const v1Context = CREDENTIALS_CONTEXT_V1_URL
const v2Context = CREDENTIALS_CONTEXT_V2_URL

if (version === 'v2.0') {
const currentCtx = Array.isArray(transformed['@context'])
? transformed['@context']
: typeof transformed['@context'] === 'string'
? [transformed['@context']]
: []

Check warning on line 179 in src/controllers/openid4vc/issuance-sessions/issuance-sessions.service.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_afj-controller&issues=AZ5O_DqiDmikfhuLIniz&open=AZ5O_DqiDmikfhuLIniz&pullRequest=389

const ctxSet = new Set(currentCtx)
ctxSet.delete(v1Context)
ctxSet.delete(v2Context)
// W3C V2.0 requires the V2 context to be the very first element.
transformed['@context'] = [v2Context, v1Context, ...Array.from(ctxSet)]
} else {
// W3C V1.1 / Default behavior
if (!transformed['@context']) {

Check warning on line 188 in src/controllers/openid4vc/issuance-sessions/issuance-sessions.service.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'If' statement should not be the only statement in 'else' block

See more on https://sonarcloud.io/project/issues?id=credebl_afj-controller&issues=AZ5O_DqiDmikfhuLIni0&open=AZ5O_DqiDmikfhuLIni0&pullRequest=389
transformed['@context'] = [v1Context]
} else if (Array.isArray(transformed['@context'])) {
const ctxSet = new Set(transformed['@context'])
ctxSet.delete(v1Context)
transformed['@context'] = [v1Context, ...Array.from(ctxSet)]
} else if (typeof transformed['@context'] === 'string') {
transformed['@context'] = [v1Context, transformed['@context']]
}
}

return transformed
}

private async processStatusList(
cred: any,
options: OpenId4VcIssuanceSessionsCreateOffer,
Expand Down
1 change: 1 addition & 0 deletions src/controllers/openid4vc/types/holder.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,5 @@ export interface DeleteCredentialBody {
export enum CredentialType {
SD_JWT = 'sd-jwt-vc',
MSO_MDOC = 'mso_mdoc',
W3C_VC = 'w3c-vc',
}
Loading
Loading