@@ -4,9 +4,12 @@ import type { Request as Req } from 'express'
44import { type OpenId4VcIssuanceSessionState } from '@credo-ts/openid4vc'
55import { OpenId4VcIssuanceSessionRepository } from '@credo-ts/openid4vc'
66
7- import { SignerMethod } from '../../../enums/enum'
7+ import { CredentialFormat , SignerMethod } from '../../../enums/enum'
88import { BadRequestError , NotFoundError } from '../../../errors/errors'
99
10+ import { checkAndCreateStatusList , getServerUrl , revokeCredentialInStatusList } from '../../../utils/statusListService'
11+ import { STATUS_LISTS_PATH } from '../../../utils/constant'
12+
1013class IssuanceSessionsService {
1114 public async createCredentialOffer ( options : OpenId4VcIssuanceSessionsCreateOffer , agentReq : Req ) {
1215 const { credentials, publicIssuerId } = options
@@ -15,49 +18,36 @@ class IssuanceSessionsService {
1518 if ( ! issuer ) {
1619 throw new NotFoundError ( `Issuer with id ${ publicIssuerId } not found` )
1720 }
18- const mappedCredentials = credentials . map ( ( cred ) => {
19- const supported = issuer ?. credentialConfigurationsSupported [ cred . credentialSupportedId ]
20- if ( ! supported ) {
21- throw new Error ( `CredentialSupportedId '${ cred . credentialSupportedId } ' is not supported by issuer` )
22- }
23- if ( supported . format !== cred . format ) {
24- throw new Error (
25- `Format mismatch for '${ cred . credentialSupportedId } ': expected '${ supported . format } ', got '${ cred . format } '` ,
26- )
27- }
28-
29- // must have signing options
30- if ( ! cred . signerOptions ?. method ) {
31- throw new BadRequestError (
32- `signerOptions must be provided and allowed methods are ${ Object . values ( SignerMethod ) . join ( ', ' ) } ` ,
33- )
34- }
35-
36- if ( cred . signerOptions . method == SignerMethod . Did && ! cred . signerOptions . did ) {
37- throw new BadRequestError (
38- `For ${ cred . credentialSupportedId } : did must be present inside signerOptions if SignerMethod is 'did' ` ,
39- )
40- }
41-
42- if ( cred . signerOptions . method === SignerMethod . X5c && ! cred . signerOptions . x5c ) {
43- throw new BadRequestError (
44- `For ${ cred . credentialSupportedId } : x5c must be present inside signerOptions if SignerMethod is 'x5c' ` ,
45- )
46- }
47-
48- const currentVct = cred . payload && 'vct' in cred . payload ? cred . payload . vct : undefined
49- return {
50- ...cred ,
51- payload : {
52- ...cred . payload ,
53- vct : currentVct ?? ( typeof supported . vct === 'string' ? supported . vct : undefined ) ,
54- } ,
55- }
56- } )
5721
58- options . issuanceMetadata ||= { }
22+ const offerStatusInfo : any [ ] = [ ]
23+
24+ const mappedCredentials = await Promise . all (
25+ credentials . map ( async ( cred ) => {
26+ const supported = issuer . credentialConfigurationsSupported [ cred . credentialSupportedId ]
27+
28+ this . validateCredentialConfig ( cred , supported )
29+
30+ const statusBlock = await this . processStatusList ( cred , options , agentReq , offerStatusInfo )
31+
32+ const currentVct = cred . payload && 'vct' in cred . payload ? cred . payload . vct : undefined
33+ return {
34+ ...cred ,
35+ payload : {
36+ ...cred . payload ,
37+ vct : currentVct ?? ( typeof supported . vct === 'string' ? supported . vct : undefined ) ,
38+ ...( statusBlock ? { status : statusBlock } : { } ) ,
39+ } ,
40+ }
41+ } ) ,
42+ )
5943
44+ options . issuanceMetadata ||= { }
6045 options . issuanceMetadata . credentials = mappedCredentials
46+ options . issuanceMetadata . isRevocable = options . isRevocable
47+
48+ if ( offerStatusInfo . length > 0 ) {
49+ options . issuanceMetadata . StatusListInfo = offerStatusInfo
50+ }
6151
6252 const issuerModule = agentReq . agent . modules . openid4vc . issuer
6353
@@ -75,6 +65,91 @@ class IssuanceSessionsService {
7565 return { credentialOffer, issuanceSession }
7666 }
7767
68+ private validateCredentialConfig ( cred : any , supported : any ) {
69+ if ( ! supported ) {
70+ throw new Error ( `CredentialSupportedId '${ cred . credentialSupportedId } ' is not supported by issuer` )
71+ }
72+ if ( supported . format !== cred . format ) {
73+ throw new Error (
74+ `Format mismatch for '${ cred . credentialSupportedId } ': expected '${ supported . format } ', got '${ cred . format } '` ,
75+ )
76+ }
77+
78+ if ( ! cred . signerOptions ?. method ) {
79+ throw new BadRequestError (
80+ `signerOptions must be provided and allowed methods are ${ Object . values ( SignerMethod ) . join ( ', ' ) } ` ,
81+ )
82+ }
83+
84+ if ( cred . signerOptions . method === SignerMethod . Did && ! cred . signerOptions . did ) {
85+ throw new BadRequestError (
86+ `For ${ cred . credentialSupportedId } : did must be present inside signerOptions if SignerMethod is 'did' ` ,
87+ )
88+ }
89+
90+ if ( cred . signerOptions . method === SignerMethod . X5c && ! cred . signerOptions . x5c ) {
91+ throw new BadRequestError (
92+ `For ${ cred . credentialSupportedId } : x5c must be present inside signerOptions if SignerMethod is 'x5c' ` ,
93+ )
94+ }
95+ }
96+
97+ private async processStatusList (
98+ cred : any ,
99+ options : OpenId4VcIssuanceSessionsCreateOffer ,
100+ agentReq : Req ,
101+ offerStatusInfo : any [ ] ,
102+ ) {
103+ if ( ! options . isRevocable ) {
104+ return undefined
105+ }
106+
107+ const effectiveIssuerDid = cred . signerOptions ?. method === SignerMethod . Did ? cred . signerOptions . did : undefined
108+ const effectiveStatusList = cred . statusListDetails || options . statusListDetails
109+
110+ if ( ! [ CredentialFormat . VcSdJwt , CredentialFormat . DcSdJwt ] . includes ( cred . format as unknown as CredentialFormat ) ) {
111+ throw new BadRequestError (
112+ `Revocation is only supported for SD-JWT formats (vc+sd-jwt, dc+sd-jwt), got '${ cred . format } '` ,
113+ )
114+ }
115+
116+ if ( ! process . env . STATUS_LIST_SERVER_URL ) {
117+ throw new BadRequestError ( 'Cannot create revocable credentials: STATUS_LIST_SERVER_URL is not configured' )
118+ }
119+
120+ if ( cred . signerOptions . method !== SignerMethod . Did || ! effectiveIssuerDid ) {
121+ throw new BadRequestError ( `Revocation is not supported without a DID signer (found ${ cred . signerOptions . method } )` )
122+ }
123+
124+ if ( ! effectiveStatusList ) {
125+ throw new BadRequestError ( 'Status list details must be provided for revocable credentials' )
126+ }
127+
128+ await checkAndCreateStatusList (
129+ agentReq . agent as any ,
130+ effectiveStatusList . listId ,
131+ effectiveIssuerDid ,
132+ effectiveStatusList . listSize ,
133+ )
134+
135+ const listUri = `${ getServerUrl ( ) } /${ STATUS_LISTS_PATH } /${ effectiveStatusList . listId } `
136+
137+ offerStatusInfo . push ( {
138+ credentialSupportedId : cred . credentialSupportedId ,
139+ listId : effectiveStatusList . listId ,
140+ index : effectiveStatusList . index ,
141+ issuerDid : effectiveIssuerDid ,
142+ } )
143+
144+ return {
145+ status_list : {
146+ uri : listUri ,
147+ idx : effectiveStatusList . index ,
148+ } ,
149+ }
150+ }
151+
152+
78153 public async getIssuanceSessionsById ( agentReq : Req , sessionId : string ) {
79154 const issuer = agentReq . agent . modules . openid4vc . issuer
80155 if ( ! issuer ) {
@@ -144,6 +219,30 @@ class IssuanceSessionsService {
144219 const issuanceSessionRepository = agentReq . agent . dependencyManager . resolve ( OpenId4VcIssuanceSessionRepository )
145220 await issuanceSessionRepository . deleteById ( agentReq . agent . context , sessionId )
146221 }
222+
223+ public async revokeBySessionId ( agentReq : Req , sessionId : string ) {
224+ const issuanceSessionRepository = agentReq . agent . dependencyManager . resolve ( OpenId4VcIssuanceSessionRepository )
225+ const record = await issuanceSessionRepository . findById ( agentReq . agent . context , sessionId )
226+
227+ if ( ! record ) {
228+ throw new NotFoundError ( `Issuance session with id ${ sessionId } not found` )
229+ }
230+
231+ const statusInfo = record . issuanceMetadata ?. StatusListInfo as any [ ]
232+ if ( ! statusInfo || statusInfo . length === 0 ) {
233+ throw new Error ( `No status list information found for session ${ sessionId } ` )
234+ }
235+
236+ if ( ! process . env . STATUS_LIST_SERVER_URL ) {
237+ throw new BadRequestError ( 'Cannot execute revocation: STATUS_LIST_SERVER_URL is not configured' )
238+ }
239+
240+ for ( const info of statusInfo ) {
241+ await revokeCredentialInStatusList ( agentReq . agent as any , info . listId , info . index , info . issuerDid )
242+ }
243+
244+ return { message : 'Credentials in session revoked successfully' }
245+ }
147246}
148247
149248export const issuanceSessionService = new IssuanceSessionsService ( )
0 commit comments