feat: sd-jwt revocation flow#357
Conversation
…e sessions and integrate SdJwtVcModule. Signed-off-by: Sagar Khole <sagar.khole@ayanworks.com>
…e sessions and integrate SdJwtVcModule. Signed-off-by: Sagar Khole <sagar.khole@ayanworks.com>
Signed-off-by: Sagar Khole <sagar.khole@ayanworks.com>
Signed-off-by: Sagar Khole <sagar.khole@ayanworks.com>
Signed-off-by: Sagar Khole <sagar.khole@ayanworks.com>
Signed-off-by: Sagar Khole <sagar.khole@ayanworks.com>
📝 WalkthroughWalkthroughThe PR introduces credential revocation via status lists for OpenID4VC issuance. It adds CLI configuration for status-list server endpoints, extends the issuance controller and service to create and manage status lists, implements a revocation endpoint, and provides utilities for remote status-list operations including JWT signing and concurrent-modification locking. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant Controller as IssuanceSessionsController
participant Service as IssuanceSessionsService
participant StatusListUtil as statusListService
participant Server as Status List Server
participant Agent
rect rgba(135, 206, 250, 0.5)
Note over Client,Agent: Credential Offer Creation with Status List
Client->>Controller: POST /issuance-sessions (with statusListDetails)
Controller->>Service: createCredentialOffer(options, agentReq)
Service->>Service: Map credentials async
loop For each credential
Service->>StatusListUtil: checkAndCreateStatusList(agent, listId, issuerDid)
StatusListUtil->>Server: GET /status-lists/{listId}
alt Status List Exists
Server-->>StatusListUtil: 200 OK
else Status List Not Found
Server-->>StatusListUtil: 404 Not Found
StatusListUtil->>Agent: Resolve issuer DID verification method
Agent-->>StatusListUtil: Verification method
StatusListUtil->>StatusListUtil: Sign statuslist+jwt
StatusListUtil->>Server: POST /status-lists (id, jwt)
Server-->>StatusListUtil: 201 Created
end
Service->>Service: Build status block with uri & idx
Service->>Service: Store credential in offerStatusInfo
end
Service->>Service: Set issuanceMetadata.StatusListInfo
Service-->>Controller: Offer with status list injection
Controller-->>Client: 200 OK (credential offer)
end
rect rgba(144, 238, 144, 0.5)
Note over Client,Agent: Credential Revocation
Client->>Controller: POST /issuance-sessions/{id}/revoke
Controller->>Service: revokeBySessionId(agentReq, sessionId)
Service->>Service: Load issuance session record
Service->>Service: Extract StatusListInfo from metadata
loop For each revocation entry
Service->>StatusListUtil: revokeCredentialInStatusList(agent, listId, idx, issuerDid)
StatusListUtil->>StatusListUtil: Acquire per-listId lock
StatusListUtil->>Server: GET /status-lists/{listId}
Server-->>StatusListUtil: Current JWT
StatusListUtil->>StatusListUtil: Parse JWT → StatusList
StatusListUtil->>StatusListUtil: Set index status to revoked (1)
StatusListUtil->>StatusListUtil: Sign updated statuslist+jwt
StatusListUtil->>Server: PATCH /status-lists/{listId} (new jwt)
Server-->>StatusListUtil: 200 OK
StatusListUtil->>StatusListUtil: Release per-listId lock
end
Service-->>Controller: success message
Controller-->>Client: 200 OK (revocation complete)
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
|
@coderabbitai Please review |
|
✅ Actions performedReview triggered.
|
| "apiKey": "supersecret-that-too-16chars", | ||
| "updateJwtSecret": false | ||
| "updateJwtSecret": false, | ||
| "statusListServerUrl": "https://dev-status-list.sovio.id/", |
There was a problem hiding this comment.
I think these details should be stored in the .env file, as it would make updating the credentials easier.
| effectiveIssuerDid, | ||
| effectiveStatusList.listSize, | ||
| ) | ||
| const listUri = `${getServerUrl()}/status-lists/${effectiveStatusList.listId}` |
There was a problem hiding this comment.
I think we should move the status-lists into a common constants file so it can be easily modified from a single place without touching the core code.
| throw new Error(`No status list information found for session ${sessionId}`) | ||
| } | ||
|
|
||
| const { revokeCredentialInStatusList } = await import('../../../utils/statusListService') |
There was a problem hiding this comment.
Importing this inside a method might cause duplicate imports if revokeCredentialInStatusList is needed in other methods in the future.
| function getApiKeyHeaders() { | ||
| const key = process.env.STATUS_LIST_API_KEY; | ||
| const headers: Record<string, string> = { 'Content-Type': 'application/json' }; | ||
| if (key) { |
There was a problem hiding this comment.
If the key is not configured, the request will be sent without the header and may fail. We should throw an error if STATUS_LIST_API_KEY is missing.
| return headers; | ||
| } | ||
|
|
||
| async function getKmsKeyIdForDid(agent: any, did: string, verificationMethodId: string) { |
There was a problem hiding this comment.
I think we can take agent type for '@credo-ts/core'
| async function signStatusList(agent: any, verificationMethodId: string, statusList: StatusList, listId: string, issuerDid: string): Promise<string> { | ||
| const payload = new JwtPayload({ | ||
| iss: issuerDid, | ||
| sub: `${getServerUrl()}/status-lists/${listId}`, |
There was a problem hiding this comment.
Same as previous comment, we should use status-lists from common constant
| ], | ||
| "lib": [ | ||
| "ESNext", | ||
| "DOM" |
There was a problem hiding this comment.
Why we are adding DOM here?
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (1)
src/controllers/x509/x509.types.ts (1)
103-113: Example values don't match enum type.The
@exampleshows string names (["digitalSignature", "keyEncipherment", "crlSign"]), butX509KeyUsageis a numeric enum (e.g.,DigitalSignature = 1,KeyEncipherment = 4). API consumers may be confused about whether to send enum names or numeric values.Update the example to reflect the actual expected format, or clarify how the API deserializes string names to enum values.
📝 Suggested example update
export interface KeyUsageDto { /** - * `@example` ["digitalSignature", "keyEncipherment", "crlSign"] + * `@example` [1, 4, 64] + * `@description` Array of X509KeyUsage values: DigitalSignature=1, NonRepudiation=2, KeyEncipherment=4, DataEncipherment=8, KeyAgreement=16, KeyCertSign=32, CrlSign=64, EncipherOnly=128, DecipherOnly=256 */ usages: X509KeyUsage[]🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/controllers/x509/x509.types.ts` around lines 103 - 113, The example for KeyUsageDto is misleading because usages is typed as X509KeyUsage (a numeric enum); update the example to show numeric enum values or clarify conversion: change the usages example to use the numeric enum values (e.g., [1, 4, 128]) or add a note that string names are accepted and will be mapped to X509KeyUsage; modify the KeyUsageDto JSDoc for the usages property and/or add a brief remark next to markAsCritical to indicate optionality so consumers know the expected payload format for the usages field.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/controllers/openid4vc/issuance-sessions/issuance-sessions.service.ts`:
- Around line 194-197: The code treats a missing issuanceMetadata.StatusListInfo
as an internal error; instead, update IssuanceSessionsService to return a client
error (4xx) for this input/caller problem by replacing the thrown generic Error
when statusInfo is falsy/empty with a specific HTTP error (e.g., BadRequest or a
400 HttpError) that includes the sessionId and a clear message; locate the check
around statusInfo = record.issuanceMetadata?.StatusListInfo and change the throw
to the appropriate HttpError type used in the codebase so callers receive a 4xx
rather than a 500.
- Around line 53-88: The code currently always injects a top-level SD-JWT-style
status block and doesn't validate that revocability is actually supported for
the chosen signer/format; update the logic around effectiveIssuerDid,
effectiveStatusList, isRevocable, and the payload merge to (1) validate at offer
creation time that isRevocable is only allowed when a revocation mechanism will
actually be attached (e.g., DID signer + SD-JWT/status-list for SD-JWT formats,
or DID signer + W3C-style credentialStatus for W3C VC formats; reject
combinations like isRevocable true with x5c signer or mso_mdoc or when no
effectiveStatusList), (2) compute and attach format-specific status payloads:
for SD-JWT formats use status: { status_list: { uri, idx } }, for W3C VC formats
use credentialStatus with the equivalent uri/index structure, and for mso_mdoc
do not add a JSON status claim (status is handled in CBOR MSO), and (3) keep
using checkAndCreateStatusList and offerStatusInfo but only push into
offerStatusInfo when you actually attach a revocation mechanism; use the
existing symbols effectiveIssuerDid, effectiveStatusList,
checkAndCreateStatusList, offerStatusInfo, supported, statusBlock and replace
the unconditional payload injection with format-aware branches that either
attach the appropriate claim or throw/reject when revocability cannot be
supported.
In `@src/utils/statusListService.ts`:
- Around line 69-71: The fetch calls in statusListService (the call that uses
uri and getApiKeyHeaders) must be routed through the shared HTTP helper that
enforces an abort/timeout and retry policy; replace direct fetch(uri, { headers:
getApiKeyHeaders() }) usages with the helper so requests are bounded by a
timeout/AbortSignal and ensure retries are only applied to safe read operations
(e.g., the status-list GET path) while write paths (issuance/revocation updates)
use a single timed request without retries; update every occurrence (the fetch
at lines using uri/getApiKeyHeaders and the other fetch blocks around 88-92 and
125-149) to use the helper and propagate the AbortSignal or timeout error
handling back to the caller.
- Around line 68-99: The GET→404→POST path for creating status lists is racy for
concurrent callers: wrap the creation logic for a given listId in a per-listId
lock (e.g., acquire/release based on listId) so only one caller performs the
fetch/create flow, or at minimum handle POST conflicts by treating a 409 as
success and re-reading the list after POST; specifically guard the block that
fetches uri, constructs StatusList, resolves issuer DID via agent.dids.resolve,
signs with signStatusList (using keyId), and posts to
`${getServerUrl()}/status-lists` (i.e., the code referencing listId, listSize,
StatusList, signStatusList, postRes) so concurrent Promise.all callers do not
both try to create the same list and fail.
- Around line 37-63: signStatusList currently hardcodes alg: 'EdDSA' and relies
on a non-specific verification method; change it to select the signing key and
algorithm based on the verification relationship (e.g., assertionMethod) and the
actual key type or provided signerOptions rather than array position, and set
the protected header kid to the full DID URL of the selected verification
method. Concretely: update the logic that calls getKmsKeyIdForDid and the
JwsProtectedHeaderOptions (the header variable) to accept/respect a
signerOptions or derive alg from the resolved key type (Ed25519->EdDSA,
P-256->ES256, secp256k1->ES256K), lookup the correct verificationMethod by its
relationship (not index 0), populate protectedHeaderOptions.kid with the DID URL
of that method, and pass the resolved alg and kid into
jwsService.createJwsCompact; apply the same pattern to the other signing sites
flagged (the other blocks using JwsProtectedHeaderOptions and createJwsCompact).
In `@tsconfig.build.json`:
- Line 39: The file ends with a closing brace but lacks a trailing newline;
update the tsconfig.build.json so that after the final closing brace (`}`) there
is a single newline character at EOF (ensure the file ends with a newline to
satisfy linters and POSIX conventions).
- Around line 19-22: The tsconfig.build.json currently includes "DOM" in the
"lib" array which brings browser types into a Node backend and causes conflicts;
remove "DOM" from the "lib" array so it only contains "ESNext" (or replace with
the minimal required ES libs) and rely on the existing "types": ["node"] for
Node typings; update any project docs or a one-line comment if there's a
specific reason to keep DOM types, otherwise delete the "DOM" entry to resolve
the type conflicts.
---
Nitpick comments:
In `@src/controllers/x509/x509.types.ts`:
- Around line 103-113: The example for KeyUsageDto is misleading because usages
is typed as X509KeyUsage (a numeric enum); update the example to show numeric
enum values or clarify conversion: change the usages example to use the numeric
enum values (e.g., [1, 4, 128]) or add a note that string names are accepted and
will be mapped to X509KeyUsage; modify the KeyUsageDto JSDoc for the usages
property and/or add a brief remark next to markAsCritical to indicate
optionality so consumers know the expected payload format for the usages field.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: b154db66-7575-4f6c-917e-11e4e5ca9a08
📒 Files selected for processing (12)
samples/cliConfig.jsonsrc/cli.tssrc/cliAgent.tssrc/controllers/openid4vc/issuance-sessions/issuance-sessions.Controller.tssrc/controllers/openid4vc/issuance-sessions/issuance-sessions.service.tssrc/controllers/openid4vc/types/issuer.types.tssrc/controllers/types.tssrc/controllers/x509/x509.types.tssrc/routes/routes.tssrc/routes/swagger.jsonsrc/utils/statusListService.tstsconfig.build.json
👮 Files not reviewed due to content moderation or server errors (6)
- src/controllers/openid4vc/types/issuer.types.ts
- samples/cliConfig.json
- src/controllers/openid4vc/issuance-sessions/issuance-sessions.Controller.ts
- src/cliAgent.ts
- src/cli.ts
- src/routes/swagger.json
| const effectiveIssuerDid = cred.signerOptions?.method === SignerMethod.Did ? cred.signerOptions.did : undefined | ||
| const effectiveStatusList = cred.statusListDetails || options.statusListDetails | ||
|
|
||
| let statusBlock = undefined | ||
| if (options.isRevocable && effectiveIssuerDid && effectiveStatusList) { | ||
| await checkAndCreateStatusList( | ||
| agentReq.agent as any, | ||
| effectiveStatusList.listId, | ||
| effectiveIssuerDid, | ||
| effectiveStatusList.listSize, | ||
| ) | ||
| const listUri = `${getServerUrl()}/status-lists/${effectiveStatusList.listId}` | ||
|
|
||
| statusBlock = { | ||
| status_list: { | ||
| uri: listUri, | ||
| idx: effectiveStatusList.index | ||
| } | ||
| } | ||
|
|
||
| offerStatusInfo.push({ | ||
| credentialSupportedId: cred.credentialSupportedId, | ||
| listId: effectiveStatusList.listId, | ||
| index: effectiveStatusList.index, | ||
| issuerDid: effectiveIssuerDid | ||
| }) | ||
| } | ||
|
|
||
| const currentVct = cred.payload && 'vct' in cred.payload ? (cred.payload as any).vct : undefined | ||
| return { | ||
| ...cred, | ||
| payload: { | ||
| ...cred.payload, | ||
| vct: currentVct ?? (typeof supported.vct === 'string' ? supported.vct : undefined), | ||
| ...(statusBlock ? { status: statusBlock } : {}) | ||
| }, |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In OpenID4VCI / VC specs, which credential formats use SD-JWT-style revocation metadata such as a top-level status.status_list object, and how should revocation/status be represented for W3C VC and mso_mdoc credentials?
💡 Result:
Which formats use SD-JWT-style status.status_list metadata?
Only SD-JWT VC credentials (OID4VCI dc+sd-jwt) use the SD-JWT/OAuth Status List style top-level status claim that can contain a status_list object (URI + index) pointing to a Status List Token. (openid.net)
How to represent revocation/status for W3C VC credentials
For W3C VC formats in OID4VCI (jwt_vc_json, jwt_vc_json-ld, ldp_vc), status/revocation is represented using the W3C VC Data Model credentialStatus property (one object or an array), e.g., a BitstringStatusListEntry with fields like statusPurpose, statusListIndex, and statusListCredential. (openid.net)
How to represent revocation/status for mso_mdoc
For mso_mdoc, status is not expressed as an SD-JWT-style JSON status.status_list claim. Instead, ISO mdoc uses a status parameter inside the MSO (Mobile Security Object) that carries the CBOR Status structure defined for the OAuth Status List mechanism. (openid.net)
Sources
- OID4VCI draft 16 (format identifiers, incl.
dc+sd-jwt,mso_mdoc, W3C VC formats). (openid.net) - IETF SD-JWT VC draft (defines optional top-level
statusclaim for SD-JWT VCs). (datatracker.ietf.org) - IETF OAuth Status List draft (defines
statuscontainingstatus_listfor JOSE tokens; and ISO mdoc mapping via MSOstatus). (datatracker.ietf.org) - W3C VC Data Model v2.0 (
credentialStatus, incl. Bitstring status list entry example). (w3.org)
Citations:
- 1: https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-16.html
- 2: https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-16.html
- 3: https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-16.html
- 4: https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-16.html
- 5: https://datatracker.ietf.org/doc/draft-ietf-oauth-sd-jwt-vc/
- 6: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-status-list-12
- 7: https://www.w3.org/TR/vc-data-model-2.0/
🏁 Script executed:
# Get the full context of the issuance-sessions.service.ts file around the cited lines and find format-related logic
head -150 src/controllers/openid4vc/issuance-sessions/issuance-sessions.service.ts | tail -100Repository: credebl/agent-controller
Length of output: 3404
🏁 Script executed:
# Search for credential format handling and type definitions in this service
rg -A 5 -B 5 "vct|format|payload" src/controllers/openid4vc/issuance-sessions/issuance-sessions.service.ts | head -200Repository: credebl/agent-controller
Length of output: 1571
🏁 Script executed:
# Look for credential type/format definitions and supported formats
fd -t f "credential" src/controllers/openid4vc/ | head -20Repository: credebl/agent-controller
Length of output: 129
🏁 Script executed:
# Search for where credential payloads are structured and if there's format-specific handling
rg "credentialSupportedId|supported\." src/controllers/openid4vc/issuance-sessions/ -A 3 -B 2Repository: credebl/agent-controller
Length of output: 4940
🏁 Script executed:
# Find credential format configuration and supported types
rg "dc\+sd-jwt|jwt_vc_json|mso_mdoc|ldp_vc" src/ --type ts --type jsRepository: credebl/agent-controller
Length of output: 1044
🏁 Script executed:
# Look for format definitions in credential configuration
rg "format.*:" src/controllers/openid4vc/ -A 2 -B 2 | head -100Repository: credebl/agent-controller
Length of output: 4028
🏁 Script executed:
# Search for where credentialConfigurationsSupported is populated
rg "credentialConfigurationsSupported" src/ -B 5 -A 5Repository: credebl/agent-controller
Length of output: 8256
🏁 Script executed:
# Check if there's any format-specific payload handling downstream
rg "W3C|credentialStatus|mso_mdoc" src/ --type ts --type jsRepository: credebl/agent-controller
Length of output: 1161
🏁 Script executed:
# Find the issuer module and createCredentialOffer implementation
fd -t f "issuer" src/ | grep -E "\.ts$" | head -20Repository: credebl/agent-controller
Length of output: 276
🏁 Script executed:
# Search for createCredentialOffer implementation
rg "createCredentialOffer" src/ -B 3 -A 10 | head -150Repository: credebl/agent-controller
Length of output: 8077
🏁 Script executed:
# Check if there's any payload transformation based on format
rg "format.*payload|payload.*format" src/controllers/openid4vc/ -B 2 -A 2Repository: credebl/agent-controller
Length of output: 50
🏁 Script executed:
# Look for W3C credential status handling
rg "credentialStatus" src/ -B 2 -A 2Repository: credebl/agent-controller
Length of output: 1792
Add format-specific status payload structure and validate revocability against actual revocation support.
This code unconditionally injects a top-level status field (SD-JWT/OAuth Status List style) regardless of credential format. Per OpenID4VCI specs:
- SD-JWT formats (
vc+sd-jwt,dc+sd-jwt) usestatus.status_listwith URI and index - W3C VC formats (
jwt_vc_json,jwt_vc_json-ld,ldp_vc) must use acredentialStatusproperty instead - mso_mdoc uses CBOR-encoded status in the MSO, not a JSON
statusclaim
Additionally, the isRevocable flag is set independently of whether status metadata is actually attached. Status metadata only appears when a DID signer is used; x5c-signed credentials with isRevocable: true will silently have no revocation mechanism. Reject unsupported revocability/signer/format combinations at offer creation time rather than creating sessions that cannot be revoked.
🧰 Tools
🪛 ESLint
[error] 53-53: Insert ··
(prettier/prettier)
[error] 54-54: Replace ······ with ········
(prettier/prettier)
[error] 56-56: Insert ··
(prettier/prettier)
[error] 57-57: Replace ······ with ········
(prettier/prettier)
[error] 58-58: Insert ··
(prettier/prettier)
[error] 59-59: Replace ·········· with ············
(prettier/prettier)
[error] 60-60: Insert ··
(prettier/prettier)
[error] 61-61: Insert ··
(prettier/prettier)
[error] 62-62: Insert ··
(prettier/prettier)
[error] 63-63: Insert ··
(prettier/prettier)
[error] 64-64: Insert ··
(prettier/prettier)
[error] 66-66: Insert ··
(prettier/prettier)
[error] 67-67: Insert ··
(prettier/prettier)
[error] 68-68: Insert ··
(prettier/prettier)
[error] 69-69: Replace idx:·effectiveStatusList.index with ··idx:·effectiveStatusList.index,
(prettier/prettier)
[error] 70-70: Replace ··········} with ············},
(prettier/prettier)
[error] 71-71: Insert ··
(prettier/prettier)
[error] 73-73: Replace ········ with ··········
(prettier/prettier)
[error] 74-74: Insert ··
(prettier/prettier)
[error] 75-75: Replace ·········· with ············
(prettier/prettier)
[error] 76-76: Insert ··
(prettier/prettier)
[error] 77-77: Replace ··········issuerDid:·effectiveIssuerDid with ············issuerDid:·effectiveIssuerDid,
(prettier/prettier)
[error] 78-78: Insert ··
(prettier/prettier)
[error] 79-79: Insert ··
(prettier/prettier)
[error] 81-81: Insert ··
(prettier/prettier)
[error] 82-82: Replace ······ with ········
(prettier/prettier)
[error] 83-83: Insert ··
(prettier/prettier)
[error] 84-84: Replace ········ with ··········
(prettier/prettier)
[error] 85-85: Insert ··
(prettier/prettier)
[error] 86-86: Replace ·········· with ············
(prettier/prettier)
[error] 87-87: Replace ...(statusBlock·?·{·status:·statusBlock·}·:·{}) with ··...(statusBlock·?·{·status:·statusBlock·}·:·{}),
(prettier/prettier)
[error] 88-88: Insert ··
(prettier/prettier)
🪛 GitHub Check: SonarCloud Code Analysis
[warning] 81-81: This assertion is unnecessary since it does not change the type of the expression.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/controllers/openid4vc/issuance-sessions/issuance-sessions.service.ts`
around lines 53 - 88, The code currently always injects a top-level SD-JWT-style
status block and doesn't validate that revocability is actually supported for
the chosen signer/format; update the logic around effectiveIssuerDid,
effectiveStatusList, isRevocable, and the payload merge to (1) validate at offer
creation time that isRevocable is only allowed when a revocation mechanism will
actually be attached (e.g., DID signer + SD-JWT/status-list for SD-JWT formats,
or DID signer + W3C-style credentialStatus for W3C VC formats; reject
combinations like isRevocable true with x5c signer or mso_mdoc or when no
effectiveStatusList), (2) compute and attach format-specific status payloads:
for SD-JWT formats use status: { status_list: { uri, idx } }, for W3C VC formats
use credentialStatus with the equivalent uri/index structure, and for mso_mdoc
do not add a JSON status claim (status is handled in CBOR MSO), and (3) keep
using checkAndCreateStatusList and offerStatusInfo but only push into
offerStatusInfo when you actually attach a revocation mechanism; use the
existing symbols effectiveIssuerDid, effectiveStatusList,
checkAndCreateStatusList, offerStatusInfo, supported, statusBlock and replace
the unconditional payload injection with format-aware branches that either
attach the appropriate claim or throw/reject when revocability cannot be
supported.
| const statusInfo = record.issuanceMetadata?.StatusListInfo as any[] | ||
| if (!statusInfo || statusInfo.length === 0) { | ||
| throw new Error(`No status list information found for session ${sessionId}`) | ||
| } |
There was a problem hiding this comment.
Return a 4xx when the session isn't revocable.
Missing StatusListInfo is a caller/input problem, not an internal failure. Throwing a plain Error here will likely surface as a 500.
🩹 Suggested fix
- const statusInfo = record.issuanceMetadata?.StatusListInfo as any[]
- if (!statusInfo || statusInfo.length === 0) {
- throw new Error(`No status list information found for session ${sessionId}`)
+ const statusInfo = record.issuanceMetadata?.StatusListInfo as any[]
+ if (!Array.isArray(statusInfo) || statusInfo.length === 0) {
+ throw new BadRequestError(`Issuance session ${sessionId} is not revocable`)
}📝 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.
| const statusInfo = record.issuanceMetadata?.StatusListInfo as any[] | |
| if (!statusInfo || statusInfo.length === 0) { | |
| throw new Error(`No status list information found for session ${sessionId}`) | |
| } | |
| const statusInfo = record.issuanceMetadata?.StatusListInfo as any[] | |
| if (!Array.isArray(statusInfo) || statusInfo.length === 0) { | |
| throw new BadRequestError(`Issuance session ${sessionId} is not revocable`) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/controllers/openid4vc/issuance-sessions/issuance-sessions.service.ts`
around lines 194 - 197, The code treats a missing
issuanceMetadata.StatusListInfo as an internal error; instead, update
IssuanceSessionsService to return a client error (4xx) for this input/caller
problem by replacing the thrown generic Error when statusInfo is falsy/empty
with a specific HTTP error (e.g., BadRequest or a 400 HttpError) that includes
the sessionId and a clear message; locate the check around statusInfo =
record.issuanceMetadata?.StatusListInfo and change the throw to the appropriate
HttpError type used in the codebase so callers receive a 4xx rather than a 500.
| async function signStatusList(agent: any, verificationMethodId: string, statusList: StatusList, listId: string, issuerDid: string): Promise<string> { | ||
| const payload = new JwtPayload({ | ||
| iss: issuerDid, | ||
| sub: `${getServerUrl()}/status-lists/${listId}`, | ||
| iat: Math.floor(Date.now() / 1000), | ||
| additionalClaims: { | ||
| status_list: { | ||
| bits: statusList.getBitsPerStatus(), | ||
| lst: statusList.compressStatusList(), | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| const header: JwsProtectedHeaderOptions = { | ||
| alg: 'EdDSA', | ||
| typ: 'statuslist+jwt', | ||
| }; | ||
|
|
||
| const jwsService = agent.dependencyManager.resolve(JwsService); | ||
| const kmsKeyId = await getKmsKeyIdForDid(agent, issuerDid, verificationMethodId); | ||
|
|
||
| return jwsService.createJwsCompact(agent.context, { | ||
| keyId: kmsKeyId, | ||
| payload, | ||
| protectedHeaderOptions: header, | ||
| }); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
For JOSE/JWS, is alg: "EdDSA"valid only for Ed25519/Ed448 keys, or can it be used with P-256/secp256k1 keys? For DID-based JWT signing, is selectingverificationMethod[0] from the DID Document a safe way to choose the signing key?
💡 Result:
1) JOSE/JWS: is alg: "EdDSA" only for Ed25519/Ed448?
Yes. In JOSE/JWS, alg: "EdDSA" is the registered JWS algorithm for EdDSA with Ed25519 or Ed448 (the curve is indicated by the key’s crv, e.g., Ed25519 or Ed448). It is not used with NIST/SECG “EC” curves like P-256 or secp256k1. [1]
For those curves, JOSE uses ECDSA-specific algorithm identifiers, e.g.:
- P-256 →
alg: "ES256"[2] - secp256k1 →
alg: "ES256K"[3]
2) DID-based JWT signing: is picking verificationMethod[0] “safe”?
No. Selecting verificationMethod[0] is not a safe/general key-selection rule because DID Documents commonly contain multiple keys with different intended uses, expressed via verification relationships like authentication, assertionMethod, and keyAgreement. For signing “assertions”, you’re expected to use a verification method authorized via assertionMethod (not whatever happens to be first in an array). [4]
A safer pattern is:
- Put the specific key you used in the JWT protected header as
kid, wherekidis a DID URL identifying a particular verification method in the DID Document, then verify that this key is authorized for the intended relationship/purpose. (This is also how several DID/JWT profiles require key identification for verification.) [5]
Sources
[1] RFC 8037 (EdDSA for JOSE/JWS) (datatracker.ietf.org)
[2] RFC 7518 (JWA: ES256 = ECDSA P-256 + SHA-256) (datatracker.ietf.org)
[3] RFC 8812 (ES256K for secp256k1 in JOSE) (ietf.org)
[4] W3C DID Core (verification relationships like assertionMethod, authentication, keyAgreement) (w3.org)
[5] OpenID4VCI draft: kid as DID URL identifying a particular key in the DID Document (and required for signature verification in that profile) (openid.net)
Citations:
- 1: https://datatracker.ietf.org/doc/html/rfc8037?utm_source=openai
- 2: https://datatracker.ietf.org/doc/html/rfc7518?utm_source=openai
- 3: https://www.ietf.org/ietf-ftp/rfc/rfc8812.pdf?utm_source=openai
- 4: https://www.w3.org/TR/2020/WD-did-core-20201012/?utm_source=openai
- 5: https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-12.html?utm_source=openai
Stop hardcoding alg: 'EdDSA' and selecting the first verification method.
The algorithm EdDSA is only valid for Ed25519/Ed448 keys. Issuers using P-256 or secp256k1 keys must use ES256 or ES256K respectively; selecting the first verification method and always applying EdDSA will fail or produce invalid signatures for these curves.
Additionally, DID Documents often contain multiple keys with different purposes (authentication, assertion, key agreement). Picking verificationMethod[0] is unsafe and does not follow DID signing patterns. Instead, select the appropriate key based on its verification relationship (e.g., assertionMethod for signing assertions) and identify it via kid as a DID URL in the protected header. The actual signer key and algorithm should come from signerOptions or be derived from the key's type, not hardcoded or taken from array position.
Applies to lines 37–63, 78–87, 138–143.
🧰 Tools
🪛 ESLint
[error] 37-37: Replace agent:·any,·verificationMethodId:·string,·statusList:·StatusList,·listId:·string,·issuerDid:·string with ⏎··agent:·any,⏎··verificationMethodId:·string,⏎··statusList:·StatusList,⏎··listId:·string,⏎··issuerDid:·string,⏎
(prettier/prettier)
[error] 38-38: Delete ··
(prettier/prettier)
[error] 39-39: Replace ········ with ····
(prettier/prettier)
[error] 40-40: Delete ····
(prettier/prettier)
[error] 41-41: Replace ········ with ····
(prettier/prettier)
[error] 42-42: Delete ····
(prettier/prettier)
[error] 43-43: Replace ············ with ······
(prettier/prettier)
[error] 44-44: Replace ················ with ········
(prettier/prettier)
[error] 45-45: Delete ········
(prettier/prettier)
[error] 46-46: Replace ······} with },
(prettier/prettier)
[error] 47-47: Replace ········} with ····},
(prettier/prettier)
[error] 48-48: Replace ····}); with ··})
(prettier/prettier)
[error] 50-50: Delete ··
(prettier/prettier)
[error] 51-51: Delete ····
(prettier/prettier)
[error] 52-52: Delete ····
(prettier/prettier)
[error] 53-53: Replace ····}; with ··}
(prettier/prettier)
[error] 55-55: Replace ····const·jwsService·=·agent.dependencyManager.resolve(JwsService); with ··const·jwsService·=·agent.dependencyManager.resolve(JwsService)
(prettier/prettier)
[error] 56-56: Replace ····const·kmsKeyId·=·await·getKmsKeyIdForDid(agent,·issuerDid,·verificationMethodId); with ··const·kmsKeyId·=·await·getKmsKeyIdForDid(agent,·issuerDid,·verificationMethodId)
(prettier/prettier)
[error] 58-58: Replace ···· with ··
(prettier/prettier)
[error] 59-59: Delete ····
(prettier/prettier)
[error] 60-60: Delete ····
(prettier/prettier)
[error] 61-61: Delete ····
(prettier/prettier)
[error] 62-62: Replace ··}); with })
(prettier/prettier)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/utils/statusListService.ts` around lines 37 - 63, signStatusList
currently hardcodes alg: 'EdDSA' and relies on a non-specific verification
method; change it to select the signing key and algorithm based on the
verification relationship (e.g., assertionMethod) and the actual key type or
provided signerOptions rather than array position, and set the protected header
kid to the full DID URL of the selected verification method. Concretely: update
the logic that calls getKmsKeyIdForDid and the JwsProtectedHeaderOptions (the
header variable) to accept/respect a signerOptions or derive alg from the
resolved key type (Ed25519->EdDSA, P-256->ES256, secp256k1->ES256K), lookup the
correct verificationMethod by its relationship (not index 0), populate
protectedHeaderOptions.kid with the DID URL of that method, and pass the
resolved alg and kid into jwsService.createJwsCompact; apply the same pattern to
the other signing sites flagged (the other blocks using
JwsProtectedHeaderOptions and createJwsCompact).
| try { | ||
| const res = await fetch(uri, { | ||
| headers: getApiKeyHeaders() | ||
| }); | ||
|
|
||
| if (res.status === 404) { | ||
| console.log(`Status list ${listId} not found, creating a new one...`); | ||
| const size = listSize || Number(process.env.STATUS_LIST_DEFAULT_SIZE); | ||
| const statusList = new StatusList(new Array(size).fill(0), 1); | ||
|
|
||
| const didDocument = await agent.dids.resolve(issuerDid); | ||
| const verificationMethod = didDocument.didDocument?.verificationMethod?.[0]; | ||
|
|
||
| if (!verificationMethod) { | ||
| throw new Error(`Could not find verification method for DID ${issuerDid}`); | ||
| } | ||
|
|
||
| const keyId = verificationMethod.id; | ||
|
|
||
| const jwt = await signStatusList(agent, keyId, statusList, listId, issuerDid); | ||
| const postRes = await fetch(`${getServerUrl()}/status-lists`, { | ||
| method: 'POST', | ||
| headers: getApiKeyHeaders(), | ||
| body: JSON.stringify({ id: listId, jwt }), | ||
| }); | ||
|
|
||
| if (!postRes.ok) { | ||
| const errBody = await postRes.text(); | ||
| throw new Error(`Failed to create list on server: ${postRes.status} ${errBody}`); | ||
| } | ||
|
|
||
| console.log(`Successfully created and published new status list ${listId}`); |
There was a problem hiding this comment.
Make list creation idempotent under concurrency.
This is still a GET→404→POST race. Because the caller now fans out with Promise.all(...) on Line 23 of src/controllers/openid4vc/issuance-sessions/issuance-sessions.service.ts, two credentials that target the same fresh listId can both observe 404, and one POST will fail even though the list was created successfully. Guard this path with the same per-listId lock, or at least treat a conflict response as success and re-read the list.
⚙️ Minimal hardening
- if (!postRes.ok) {
+ if (postRes.status === 409) {
+ return
+ }
+ if (!postRes.ok) {
const errBody = await postRes.text();
throw new Error(`Failed to create list on server: ${postRes.status} ${errBody}`);
}🧰 Tools
🪛 ESLint
[error] 68-68: Delete ··
(prettier/prettier)
[error] 69-69: Delete ····
(prettier/prettier)
[error] 70-70: Replace ············headers:·getApiKeyHeaders() with ······headers:·getApiKeyHeaders(),
(prettier/prettier)
[error] 71-71: Replace ········}); with ····})
(prettier/prettier)
[error] 73-73: Delete ····
(prettier/prettier)
[error] 74-74: Replace ············console.log(Status·list·${listId}·not·found,·creating·a·new·one...); with ······console.log(Status·list·${listId}·not·found,·creating·a·new·one...)
(prettier/prettier)
[error] 74-74: Unexpected console statement.
(no-console)
[error] 75-75: Replace ············const·size·=·listSize·||·Number(process.env.STATUS_LIST_DEFAULT_SIZE); with ······const·size·=·listSize·||·Number(process.env.STATUS_LIST_DEFAULT_SIZE)
(prettier/prettier)
[error] 76-76: Replace ············const·statusList·=·new·StatusList(new·Array(size).fill(0),·1); with ······const·statusList·=·new·StatusList(new·Array(size).fill(0),·1)
(prettier/prettier)
[error] 78-78: Replace ············const·didDocument·=·await·agent.dids.resolve(issuerDid); with ······const·didDocument·=·await·agent.dids.resolve(issuerDid)
(prettier/prettier)
[error] 79-79: Replace ············const·verificationMethod·=·didDocument.didDocument?.verificationMethod?.[0]; with ······const·verificationMethod·=·didDocument.didDocument?.verificationMethod?.[0]
(prettier/prettier)
[error] 81-81: Replace ············ with ······
(prettier/prettier)
[error] 82-82: Replace ················throw·new·Error(Could·not·find·verification·method·for·DID·${issuerDid}); with ········throw·new·Error(Could·not·find·verification·method·for·DID·${issuerDid})
(prettier/prettier)
[error] 83-83: Replace ············ with ······
(prettier/prettier)
[error] 85-85: Replace ············const·keyId·=·verificationMethod.id; with ······const·keyId·=·verificationMethod.id
(prettier/prettier)
[error] 87-87: Replace ············const·jwt·=·await·signStatusList(agent,·keyId,·statusList,·listId,·issuerDid); with ······const·jwt·=·await·signStatusList(agent,·keyId,·statusList,·listId,·issuerDid)
(prettier/prettier)
[error] 88-88: Replace ············ with ······
(prettier/prettier)
[error] 89-89: Delete ········
(prettier/prettier)
[error] 90-90: Delete ········
(prettier/prettier)
[error] 91-91: Replace ················ with ········
(prettier/prettier)
[error] 92-92: Replace ······}); with })
(prettier/prettier)
[error] 94-94: Replace ············ with ······
(prettier/prettier)
[error] 95-95: Replace ················const·errBody·=·await·postRes.text(); with ········const·errBody·=·await·postRes.text()
(prettier/prettier)
[error] 96-96: Replace ················throw·new·Error(Failed·to·create·list·on·server:·${postRes.status}·${errBody}); with ········throw·new·Error(Failed·to·create·list·on·server:·${postRes.status}·${errBody})
(prettier/prettier)
[error] 97-97: Delete ······
(prettier/prettier)
[error] 99-99: Replace ············console.log(Successfully·created·and·published·new·status·list·${listId}); with ······console.log(Successfully·created·and·published·new·status·list·${listId})
(prettier/prettier)
[error] 99-99: Unexpected console statement.
(no-console)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/utils/statusListService.ts` around lines 68 - 99, The GET→404→POST path
for creating status lists is racy for concurrent callers: wrap the creation
logic for a given listId in a per-listId lock (e.g., acquire/release based on
listId) so only one caller performs the fetch/create flow, or at minimum handle
POST conflicts by treating a 409 as success and re-reading the list after POST;
specifically guard the block that fetches uri, constructs StatusList, resolves
issuer DID via agent.dids.resolve, signs with signStatusList (using keyId), and
posts to `${getServerUrl()}/status-lists` (i.e., the code referencing listId,
listSize, StatusList, signStatusList, postRes) so concurrent Promise.all callers
do not both try to create the same list and fail.
| const res = await fetch(uri, { | ||
| headers: getApiKeyHeaders() | ||
| }); |
There was a problem hiding this comment.
Bound the status-list server calls with timeouts.
These fetches run on issuance/revocation request paths with no abort/timeout, so a slow or hung status-list server can stall the API request indefinitely. Please route them through a shared HTTP helper with a bounded timeout, and only retry the safe read path.
Also applies to: 88-92, 125-149
🧰 Tools
🪛 ESLint
[error] 69-69: Delete ····
(prettier/prettier)
[error] 70-70: Replace ············headers:·getApiKeyHeaders() with ······headers:·getApiKeyHeaders(),
(prettier/prettier)
[error] 71-71: Replace ········}); with ····})
(prettier/prettier)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/utils/statusListService.ts` around lines 69 - 71, The fetch calls in
statusListService (the call that uses uri and getApiKeyHeaders) must be routed
through the shared HTTP helper that enforces an abort/timeout and retry policy;
replace direct fetch(uri, { headers: getApiKeyHeaders() }) usages with the
helper so requests are bounded by a timeout/AbortSignal and ensure retries are
only applied to safe read operations (e.g., the status-list GET path) while
write paths (issuance/revocation updates) use a single timed request without
retries; update every occurrence (the fetch at lines using uri/getApiKeyHeaders
and the other fetch blocks around 88-92 and 125-149) to use the helper and
propagate the AbortSignal or timeout error handling back to the caller.
| "lib": [ | ||
| "ESNext", | ||
| "DOM" | ||
| ] |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Search for usage of common DOM-specific types and APIs that would require the DOM lib.
echo "Checking for DOM-specific type usage..."
# Check for common DOM types in type annotations
rg -n --type=ts --type=tsx -P '\b(HTMLElement|Document|Window|Navigator|Location|Element|NodeList|DOMParser|XMLHttpRequest|FormData|Blob|File)\b' -g '!node_modules' -g '!build' -C 2
echo -e "\n---\nChecking for DOM API usage..."
# Check for DOM API usage
rg -n --type=ts --type=tsx -P '\b(window\.|document\.|navigator\.|localStorage\.|sessionStorage\.)\w+' -g '!node_modules' -g '!build' -C 2Repository: credebl/agent-controller
Length of output: 215
🏁 Script executed:
# First, let's view the tsconfig.build.json file to understand the full context
cat -n tsconfig.build.jsonRepository: credebl/agent-controller
Length of output: 1063
🏁 Script executed:
# Check the package.json to see what dependencies might require DOM types
cat -n package.json | head -50Repository: credebl/agent-controller
Length of output: 2219
🏁 Script executed:
# Rerun the DOM detection with corrected file type (use 'ts' only and broader search)
rg -n --type=ts '(HTMLElement|Document|Window|Navigator|Location|Element|NodeList|DOMParser|XMLHttpRequest|FormData|Blob|File|window\.|document\.|navigator\.|localStorage\.|sessionStorage\.)' -g '!node_modules' -g '!build' -C 2Repository: credebl/agent-controller
Length of output: 29626
🏁 Script executed:
# Check specifically for fetch usage
rg -n 'fetch\(' -g '!node_modules' -g '!build'Repository: credebl/agent-controller
Length of output: 577
Remove "DOM" from the lib array—it introduces unnecessary type conflicts without being used.
Adding "DOM" includes browser-specific type definitions in a Node.js backend, causing potential conflicts with Node.js globals like setTimeout, setInterval, and fetch. A search of the codebase shows:
- No DOM-specific types (
HTMLElement,Document,Window, etc.) are used anywhere fetch()is used in several files, but Node.js 18+ has native fetch support with types provided by@types/node, which is already configured via"types": ["node"]- No clear connection to the PR's SD-JWT revocation flow changes
Either remove "DOM" or document specifically why it's required. The current TypeScript configuration with "types": ["node"] is sufficient for this backend project.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tsconfig.build.json` around lines 19 - 22, The tsconfig.build.json currently
includes "DOM" in the "lib" array which brings browser types into a Node backend
and causes conflicts; remove "DOM" from the "lib" array so it only contains
"ESNext" (or replace with the minimal required ES libs) and rely on the existing
"types": ["node"] for Node typings; update any project docs or a one-line
comment if there's a specific reason to keep DOM types, otherwise delete the
"DOM" entry to resolve the type conflicts.
| "scripts" | ||
| ] | ||
| } | ||
| } No newline at end of file |
There was a problem hiding this comment.
Add trailing newline.
The file is missing a trailing newline. Most style guides and linters (ESLint, Prettier) prefer files to end with a newline for better POSIX compliance and cleaner diffs.
📝 Suggested fix
Add a newline after the closing brace on line 39.
]
}
+📝 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.
| } | |
| } | |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tsconfig.build.json` at line 39, The file ends with a closing brace but lacks
a trailing newline; update the tsconfig.build.json so that after the final
closing brace (`}`) there is a single newline character at EOF (ensure the file
ends with a newline to satisfy linters and POSIX conventions).



Feat/sd jwt revocation flow
Summary by CodeRabbit
New Features
Documentation