@sphereon/ssi-sdk.vc-status-list 0.34.1-feature.SSISDK.17.bitstring.sl.13 → 0.34.1-feature.SSISDK.17.bitstring.sl.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,16 +3,17 @@ import { type CompactJWT, type CredentialProofFormat, type CWT, StatusListType }
3
3
  import type {
4
4
  CheckStatusIndexArgs,
5
5
  CreateStatusListArgs,
6
+ IMergeDetailsWithEntityArgs,
7
+ IToDetailsFromCredentialArgs,
6
8
  SignedStatusListData,
7
9
  StatusListOAuthEntryCredentialStatus,
8
10
  StatusListResult,
9
11
  StatusOAuth,
10
- ToStatusListDetailsArgs,
11
12
  UpdateStatusListFromEncodedListArgs,
12
13
  UpdateStatusListIndexArgs,
13
14
  } from '../types'
14
15
  import { determineProofFormat, ensureDate, getAssertedValue, getAssertedValues } from '../utils'
15
- import type { IOAuthStatusListImplementationResult, IStatusList } from './IStatusList'
16
+ import type { IExtractedCredentialDetails, IOAuthStatusListImplementationResult, IStatusList } from './IStatusList'
16
17
  import { StatusList } from '@sd-jwt/jwt-status-list'
17
18
  import type { IJwtService } from '@sphereon/ssi-sdk-ext.jwt-service'
18
19
  import type { IIdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resolution'
@@ -164,41 +165,92 @@ export class OAuthStatusListImplementation implements IStatusList {
164
165
  return statusList.getStatus(index)
165
166
  }
166
167
 
167
- async toStatusListDetails(args: ToStatusListDetailsArgs): Promise<StatusListResult & IOAuthStatusListImplementationResult> {
168
- const { statusListPayload } = args as { statusListPayload: CompactJWT | CWT }
169
- const proofFormat = determineProofFormat(statusListPayload)
170
- const decoded = proofFormat === 'jwt' ? decodeStatusListJWT(statusListPayload) : decodeStatusListCWT(statusListPayload)
171
- const { statusList, issuer, id, exp } = decoded
168
+ /**
169
+ * Performs the initial parsing of a StatusListCredential.
170
+ * This method handles expensive operations like JWT/CWT decoding once.
171
+ * It extracts all details available from the credential payload itself.
172
+ */
173
+ async extractCredentialDetails(credential: CompactJWT | CWT): Promise<IExtractedCredentialDetails> {
174
+ if (typeof credential !== 'string') {
175
+ return Promise.reject('statusListCredential must be a JWT or CWT string')
176
+ }
172
177
 
173
- const bitsPerStatus = statusList.getBitsPerStatus()
174
- const expiresAt = exp ? new Date(exp * 1000) : undefined
178
+ const proofFormat = determineProofFormat(credential)
179
+ const decoded = proofFormat === 'jwt' ? decodeStatusListJWT(credential) : decodeStatusListCWT(credential)
175
180
 
176
181
  return {
177
- // Base implementation fields
178
- id,
179
- encodedList: statusList.compressStatusList(),
180
- issuer,
181
- type: StatusListType.OAuthStatusList,
182
- proofFormat,
183
- length: statusList.statusList.length,
184
- statusListCredential: statusListPayload,
185
- statuslistContentType: this.buildContentType(proofFormat),
186
- correlationId: args.correlationId, // FIXME these do not need to be inside the impl
187
- driverType: args.driverType, // FIXME these do not need to be inside the impl
182
+ id: decoded.id,
183
+ issuer: decoded.issuer,
184
+ encodedList: decoded.statusList.compressStatusList(),
185
+ decodedPayload: decoded,
186
+ }
187
+ }
188
188
 
189
- // Flattened OAuth-specific fields
190
- bitsPerStatus,
191
- ...(expiresAt && { expiresAt }),
189
+ // For CREATE and READ contexts
190
+ async toStatusListDetails(args: IToDetailsFromCredentialArgs): Promise<StatusListResult & IOAuthStatusListImplementationResult>
191
+ // For UPDATE contexts
192
+ async toStatusListDetails(args: IMergeDetailsWithEntityArgs): Promise<StatusListResult & IOAuthStatusListImplementationResult>
193
+ async toStatusListDetails(
194
+ args: IToDetailsFromCredentialArgs | IMergeDetailsWithEntityArgs,
195
+ ): Promise<StatusListResult & IOAuthStatusListImplementationResult> {
196
+ if ('statusListCredential' in args) {
197
+ // CREATE/READ context
198
+ const { statusListCredential, bitsPerStatus, correlationId, driverType } = args
199
+ if (!bitsPerStatus || bitsPerStatus < 1) {
200
+ return Promise.reject(Error('bitsPerStatus must be set for OAuth status lists and must be 1 or higher'))
201
+ }
192
202
 
193
- // Legacy nested structure for backward compatibility
194
- oauthStatusList: {
203
+ const proofFormat = determineProofFormat(statusListCredential as string)
204
+ const decoded =
205
+ proofFormat === 'jwt' ? decodeStatusListJWT(statusListCredential as string) : decodeStatusListCWT(statusListCredential as string)
206
+ const { statusList, issuer, id, exp } = decoded
207
+ const expiresAt = exp ? new Date(exp * 1000) : undefined
208
+
209
+ return {
210
+ id,
211
+ encodedList: statusList.compressStatusList(),
212
+ issuer,
213
+ type: StatusListType.OAuthStatusList,
214
+ proofFormat,
215
+ length: statusList.statusList.length,
216
+ statusListCredential: statusListCredential as CompactJWT | CWT,
217
+ statuslistContentType: this.buildContentType(proofFormat),
218
+ correlationId,
219
+ driverType,
195
220
  bitsPerStatus,
196
221
  ...(expiresAt && { expiresAt }),
197
- },
198
-
199
- // Optional fields from args
200
- ...(args.correlationId && { correlationId: args.correlationId }),
201
- ...(args.driverType && { driverType: args.driverType }),
222
+ oauthStatusList: {
223
+ bitsPerStatus,
224
+ ...(expiresAt && { expiresAt }),
225
+ },
226
+ }
227
+ } else {
228
+ // UPDATE context
229
+ const { extractedDetails, statusListEntity } = args
230
+ const oauthEntity = statusListEntity as OAuthStatusListEntity
231
+ const decoded = extractedDetails.decodedPayload as { statusList: StatusList; exp?: number }
232
+
233
+ const proofFormat = determineProofFormat(statusListEntity.statusListCredential as string)
234
+ const expiresAt = decoded.exp ? new Date(decoded.exp * 1000) : undefined
235
+
236
+ return {
237
+ id: extractedDetails.id,
238
+ encodedList: extractedDetails.encodedList,
239
+ issuer: extractedDetails.issuer,
240
+ type: StatusListType.OAuthStatusList,
241
+ proofFormat,
242
+ length: decoded.statusList.statusList.length,
243
+ statusListCredential: statusListEntity.statusListCredential!,
244
+ statuslistContentType: this.buildContentType(proofFormat),
245
+ correlationId: statusListEntity.correlationId,
246
+ driverType: statusListEntity.driverType,
247
+ bitsPerStatus: oauthEntity.bitsPerStatus,
248
+ ...(expiresAt && { expiresAt }),
249
+ oauthStatusList: {
250
+ bitsPerStatus: oauthEntity.bitsPerStatus,
251
+ ...(expiresAt && { expiresAt }),
252
+ },
253
+ }
202
254
  }
203
255
  }
204
256
 
@@ -10,12 +10,13 @@ import {
10
10
  } from '@sphereon/ssi-types'
11
11
 
12
12
  import { StatusList } from '@sphereon/vc-status-list'
13
- import type { IStatusList, IStatusList2021ImplementationResult } from './IStatusList'
13
+ import type { IExtractedCredentialDetails, IStatusList, IStatusList2021ImplementationResult } from './IStatusList'
14
14
  import type {
15
15
  CheckStatusIndexArgs,
16
16
  CreateStatusListArgs,
17
+ IMergeDetailsWithEntityArgs,
18
+ IToDetailsFromCredentialArgs,
17
19
  StatusListResult,
18
- ToStatusListDetailsArgs,
19
20
  UpdateStatusListFromEncodedListArgs,
20
21
  UpdateStatusListIndexArgs,
21
22
  } from '../types'
@@ -171,44 +172,87 @@ export class StatusList2021Implementation implements IStatusList {
171
172
  return status ? Status2021.Invalid : Status2021.Valid
172
173
  }
173
174
 
174
- async toStatusListDetails(args: ToStatusListDetailsArgs): Promise<StatusListResult & IStatusList2021ImplementationResult> {
175
- const { statusListPayload } = args
176
- const uniform = CredentialMapper.toUniformCredential(statusListPayload)
175
+ /**
176
+ * Performs the initial parsing of a StatusListCredential.
177
+ * This method handles expensive operations like JWT/CWT decoding once.
178
+ * It extracts all details available from the credential payload itself.
179
+ */
180
+ async extractCredentialDetails(credential: StatusListCredential): Promise<IExtractedCredentialDetails> {
181
+ const uniform = CredentialMapper.toUniformCredential(credential)
177
182
  const { issuer, credentialSubject } = uniform
178
- const id = getAssertedValue('id', uniform.id)
179
- const encodedList = getAssertedProperty('encodedList', credentialSubject)
180
- const proofFormat: CredentialProofFormat = CredentialMapper.detectDocumentType(statusListPayload) === DocumentFormat.JWT ? 'jwt' : 'lds'
181
-
182
- const statusPurpose = getAssertedProperty('statusPurpose', credentialSubject)
183
- const indexingDirection = 'rightToLeft'
184
- const list = await StatusList.decode({ encodedList })
183
+ const subject = Array.isArray(credentialSubject) ? credentialSubject[0] : credentialSubject
185
184
 
186
185
  return {
187
- // Base implementation fields
188
- id,
189
- encodedList,
186
+ id: getAssertedValue('id', uniform.id),
190
187
  issuer,
191
- type: StatusListType.StatusList2021,
192
- proofFormat,
193
- length: list.length,
194
- statusListCredential: statusListPayload,
195
- statuslistContentType: this.buildContentType(proofFormat),
196
- correlationId: args.correlationId, // FIXME these do not need to be inside the impl
197
- driverType: args.driverType, // FIXME these do not need to be inside the impl
198
-
199
- // Flattened StatusList2021-specific fields
200
- indexingDirection,
201
- statusPurpose,
188
+ encodedList: getAssertedProperty('encodedList', subject),
189
+ }
190
+ }
202
191
 
203
- // Legacy nested structure for backward compatibility
204
- statusList2021: {
205
- indexingDirection,
192
+ async toStatusListDetails(args: IToDetailsFromCredentialArgs): Promise<StatusListResult & IStatusList2021ImplementationResult>
193
+ // For UPDATE contexts
194
+ async toStatusListDetails(args: IMergeDetailsWithEntityArgs): Promise<StatusListResult & IStatusList2021ImplementationResult>
195
+ async toStatusListDetails(
196
+ args: IToDetailsFromCredentialArgs | IMergeDetailsWithEntityArgs,
197
+ ): Promise<StatusListResult & IStatusList2021ImplementationResult> {
198
+ if ('statusListCredential' in args) {
199
+ // CREATE/READ context
200
+ const { statusListCredential, correlationId, driverType } = args
201
+ const uniform = CredentialMapper.toUniformCredential(statusListCredential)
202
+ const { issuer, credentialSubject } = uniform
203
+ const subject = Array.isArray(credentialSubject) ? credentialSubject[0] : credentialSubject
204
+
205
+ const id = getAssertedValue('id', uniform.id)
206
+ const encodedList = getAssertedProperty('encodedList', subject)
207
+ const statusPurpose = getAssertedProperty('statusPurpose', subject)
208
+ const proofFormat: CredentialProofFormat = CredentialMapper.detectDocumentType(statusListCredential) === DocumentFormat.JWT ? 'jwt' : 'lds'
209
+ const list = await StatusList.decode({ encodedList })
210
+
211
+ return {
212
+ id,
213
+ encodedList,
214
+ issuer,
215
+ type: StatusListType.StatusList2021,
216
+ proofFormat,
217
+ length: list.length,
218
+ statusListCredential,
219
+ statuslistContentType: this.buildContentType(proofFormat),
220
+ correlationId,
221
+ driverType,
222
+ indexingDirection: 'rightToLeft',
206
223
  statusPurpose,
207
-
208
- // Optional fields from args
209
- ...(args.correlationId && { correlationId: args.correlationId }),
210
- ...(args.driverType && { driverType: args.driverType }),
211
- },
224
+ statusList2021: {
225
+ indexingDirection: 'rightToLeft',
226
+ statusPurpose,
227
+ },
228
+ }
229
+ } else {
230
+ // UPDATE context
231
+ const { extractedDetails, statusListEntity } = args
232
+ const statusList2021Entity = statusListEntity as StatusList2021Entity
233
+
234
+ const proofFormat: CredentialProofFormat =
235
+ CredentialMapper.detectDocumentType(statusListEntity.statusListCredential!) === DocumentFormat.JWT ? 'jwt' : 'lds'
236
+ const list = await StatusList.decode({ encodedList: extractedDetails.encodedList })
237
+
238
+ return {
239
+ id: extractedDetails.id,
240
+ encodedList: extractedDetails.encodedList,
241
+ issuer: extractedDetails.issuer,
242
+ type: StatusListType.StatusList2021,
243
+ proofFormat,
244
+ length: list.length,
245
+ statusListCredential: statusListEntity.statusListCredential!,
246
+ statuslistContentType: this.buildContentType(proofFormat),
247
+ correlationId: statusListEntity.correlationId,
248
+ driverType: statusListEntity.driverType,
249
+ indexingDirection: statusList2021Entity.indexingDirection,
250
+ statusPurpose: statusList2021Entity.statusPurpose,
251
+ statusList2021: {
252
+ indexingDirection: statusList2021Entity.indexingDirection,
253
+ statusPurpose: statusList2021Entity.statusPurpose,
254
+ },
255
+ }
212
256
  }
213
257
  }
214
258
 
package/src/index.ts CHANGED
@@ -3,3 +3,4 @@
3
3
 
4
4
  export * from './types'
5
5
  export * from './functions'
6
+ export { determineStatusListType } from './utils'
@@ -19,6 +19,8 @@ import type { SdJwtVcPayload } from '@sd-jwt/sd-jwt-vc'
19
19
  import type { StatusListOpts } from '@sphereon/oid4vci-common'
20
20
  import { BitstringStatusPurpose } from '@4sure-tech/vc-bitstring-status-lists'
21
21
  import { IVcdmCredentialPlugin } from '@sphereon/ssi-sdk.credential-vcdm'
22
+ import { IExtractedCredentialDetails } from '../impl/IStatusList'
23
+ import { BitstringStatusListArgs, IStatusListEntity } from '@sphereon/ssi-sdk.data-store'
22
24
 
23
25
  export enum StatusOAuth {
24
26
  Valid = 0,
@@ -31,8 +33,6 @@ export enum Status2021 {
31
33
  Invalid = 1,
32
34
  }
33
35
 
34
- export type BitstringStatus = number
35
-
36
36
  export type StatusList2021Args = {
37
37
  indexingDirection: StatusListIndexingDirection
38
38
  statusPurpose?: StatusPurpose2021
@@ -44,14 +44,6 @@ export type OAuthStatusListArgs = {
44
44
  expiresAt?: Date
45
45
  }
46
46
 
47
- export type BitstringStatusListArgs = {
48
- statusPurpose: BitstringStatusPurpose
49
- bitsPerStatus: number
50
- ttl?: number
51
- validFrom?: Date
52
- validUntil?: Date
53
- }
54
-
55
47
  export type BaseCreateNewStatusListArgs = {
56
48
  type: StatusListType
57
49
  id: string
@@ -102,7 +94,7 @@ export interface UpdateStatusListFromStatusListCredentialArgs {
102
94
  statusListCredential: StatusListCredential // | CompactJWT
103
95
  keyRef?: string
104
96
  statusListIndex: number | string
105
- value: number | Status2021 | StatusOAuth | BitstringStatus
97
+ value: number | Status2021 | StatusOAuth
106
98
  }
107
99
 
108
100
  export interface StatusListResult {
@@ -117,20 +109,6 @@ export interface StatusListResult {
117
109
  correlationId?: string
118
110
  driverType?: StatusListDriverType
119
111
 
120
- // Flattened StatusList2021 fields
121
- indexingDirection?: StatusListIndexingDirection
122
- statusPurpose?: StatusPurpose2021 | BitstringStatusPurpose | BitstringStatusPurpose[]
123
-
124
- // Flattened OAuth fields
125
- bitsPerStatus?: number
126
- expiresAt?: Date
127
-
128
- // Flattened Bitstring fields
129
- validFrom?: Date
130
- validUntil?: Date
131
- ttl?: number
132
-
133
- // Legacy nested structures for backward compatibility
134
112
  statusList2021?: {
135
113
  indexingDirection: StatusListIndexingDirection
136
114
  statusPurpose: StatusPurpose2021
@@ -163,16 +141,6 @@ export interface StatusListOAuthEntryCredentialStatus extends ICredentialStatus
163
141
  expiresAt?: Date
164
142
  }
165
143
 
166
- export interface BitstringStatusListEntryCredentialStatus extends ICredentialStatus {
167
- type: 'BitstringStatusListEntry'
168
- statusPurpose: BitstringStatusPurpose | BitstringStatusPurpose[]
169
- statusListIndex: string
170
- statusListCredential: string
171
- bitsPerStatus?: number
172
- statusMessage?: Array<BitstringStatus>
173
- statusReference?: string | string[]
174
- }
175
-
176
144
  export interface StatusList2021ToVerifiableCredentialArgs {
177
145
  issuer: string | IIssuer
178
146
  id: string
@@ -198,7 +166,7 @@ export interface CreateStatusListArgs {
198
166
  export interface UpdateStatusListIndexArgs {
199
167
  statusListCredential: StatusListCredential // | CompactJWT
200
168
  statusListIndex: number | string
201
- value: number | Status2021 | StatusOAuth | BitstringStatus
169
+ value: number | Status2021 | StatusOAuth
202
170
  bitsPerStatus?: number
203
171
  keyRef?: string
204
172
  expiresAt?: Date
@@ -210,11 +178,22 @@ export interface CheckStatusIndexArgs {
210
178
  bitsPerStatus?: number
211
179
  }
212
180
 
213
- export interface ToStatusListDetailsArgs {
214
- statusListPayload: StatusListCredential
181
+ // For the CREATE and READ contexts
182
+ export interface IToDetailsFromCredentialArgs {
183
+ // The source credential we are converting
184
+ statusListCredential: StatusListCredential
185
+
186
+ // The required metadata that is NOT in the credential itself
187
+ statusListType: StatusListType
188
+ bitsPerStatus?: number
215
189
  correlationId?: string
216
190
  driverType?: StatusListDriverType
217
- bitsPerStatus?: number
191
+ }
192
+
193
+ // For the UPDATE context
194
+ export interface IMergeDetailsWithEntityArgs {
195
+ extractedDetails: IExtractedCredentialDetails
196
+ statusListEntity: IStatusListEntity
218
197
  }
219
198
 
220
199
  /**
package/src/utils.ts CHANGED
@@ -53,31 +53,60 @@ export function assertValidProofType(type: StatusListType, proofFormat: Credenti
53
53
 
54
54
  export function determineStatusListType(credential: StatusListCredential): StatusListType {
55
55
  const proofFormat = determineProofFormat(credential)
56
+
56
57
  switch (proofFormat) {
57
58
  case 'jwt':
58
- const payload: StatusListCredential = jwtDecode(credential as string)
59
- const keys = Object.keys(payload)
60
- if (keys.includes('status_list')) {
61
- return StatusListType.OAuthStatusList
62
- } else if (keys.includes('vc')) {
63
- return StatusListType.StatusList2021
64
- }
65
- break
59
+ return determineJwtStatusListType(credential as string)
66
60
  case 'lds':
67
- const uniform = CredentialMapper.toUniformCredential(credential)
68
- const type = uniform.type.find((t) => {
69
- return Object.values(StatusListType).some((statusType) => t.includes(statusType))
70
- })
71
- if (!type) {
72
- throw new Error('Invalid status list credential type')
73
- }
74
- return type.replace('Credential', '') as StatusListType
75
-
61
+ return determineLdsStatusListType(credential)
76
62
  case 'cbor':
77
63
  return StatusListType.OAuthStatusList
64
+ default:
65
+ throw new Error('Cannot determine status list type from credential payload')
66
+ }
67
+ }
68
+
69
+ function determineJwtStatusListType(credential: string): StatusListType {
70
+ const payload: any = jwtDecode(credential)
71
+
72
+ // OAuth status list format
73
+ if ('status_list' in payload) {
74
+ return StatusListType.OAuthStatusList
75
+ }
76
+
77
+ // Direct credential subject
78
+ if ('credentialSubject' in payload) {
79
+ return getStatusListTypeFromSubject(payload.credentialSubject)
80
+ }
81
+
82
+ // Wrapped VC format
83
+ if ('vc' in payload && 'credentialSubject' in payload.vc) {
84
+ return getStatusListTypeFromSubject(payload.vc.credentialSubject)
85
+ }
86
+
87
+ throw new Error('Invalid status list credential: credentialSubject not found')
88
+ }
89
+
90
+ function determineLdsStatusListType(credential: StatusListCredential): StatusListType {
91
+ const uniform = CredentialMapper.toUniformCredential(credential)
92
+ const statusListType = uniform.type.find((type) => Object.values(StatusListType).some((statusType) => type.includes(statusType)))
93
+
94
+ if (!statusListType) {
95
+ throw new Error('Invalid status list credential type')
78
96
  }
79
97
 
80
- throw new Error('Cannot determine status list type from credential payload')
98
+ return statusListType.replace('Credential', '') as StatusListType
99
+ }
100
+
101
+ function getStatusListTypeFromSubject(credentialSubject: any): StatusListType {
102
+ switch (credentialSubject.type) {
103
+ case 'StatusList2021':
104
+ return StatusListType.StatusList2021
105
+ case 'BitstringStatusList':
106
+ return StatusListType.BitstringStatusList
107
+ default:
108
+ throw new Error(`Unknown credential subject type: ${credentialSubject.type}`)
109
+ }
81
110
  }
82
111
 
83
112
  export function determineProofFormat(credential: StatusListCredential): CredentialProofFormat {