@cheqd/did-provider-cheqd 3.3.2 → 3.4.0-develop.2

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.
@@ -37,7 +37,9 @@ import {
37
37
  ICredentialIssuer,
38
38
  IDIDManager,
39
39
  IDataStore,
40
- IResolver
40
+ IResolver,
41
+ W3CVerifiableCredential,
42
+ ICredentialVerifier
41
43
  } from '@veramo/core'
42
44
  import {
43
45
  CheqdDIDProvider,
@@ -45,7 +47,13 @@ import {
45
47
  TImportableEd25519Key,
46
48
  ResourcePayload,
47
49
  StatusList2021ResourcePayload,
48
- DefaultRESTUrls
50
+ DefaultRESTUrls,
51
+ DefaultStatusList2021Encodings,
52
+ DefaultStatusList2021ResourceTypes,
53
+ DefaultStatusList2021StatusPurposeTypes,
54
+ DefaultStatusList2021Encoding,
55
+ DefaultStatusList2021ResourceType,
56
+ DefaultStatusList2021StatusPurposeType,
49
57
  } from '../did-manager/cheqd-did-provider.js'
50
58
  import {
51
59
  fromString,
@@ -64,12 +72,18 @@ import {
64
72
  LitProtocol,
65
73
  TxNonceFormat
66
74
  } from '../dkg-threshold/lit-protocol.js';
67
- import { blobToHexString, randomFromRange, toBlob, unescapeUnicode } from '../utils/helpers.js'
75
+ import {
76
+ blobToHexString,
77
+ randomFromRange,
78
+ toBlob,
79
+ unescapeUnicode
80
+ } from '../utils/helpers.js'
68
81
  import { resolverUrl } from '../did-manager/cheqd-did-resolver.js'
82
+ import { AlternativeUri } from '@cheqd/ts-proto/cheqd/resource/v2/resource.js'
69
83
 
70
84
  const debug = Debug('veramo:did-provider-cheqd')
71
85
 
72
- export type IContext = IAgentContext<IDIDManager & IKeyManager & IDataStore & IResolver & ICredentialIssuer & ICheqd>
86
+ export type IContext = IAgentContext<IDIDManager & IKeyManager & IDataStore & IResolver & ICredentialIssuer & ICredentialVerifier & ICheqd>
73
87
  export type TExportedDIDDocWithKeys = { didDoc: DIDDocument, keys: TImportableEd25519Key[], versionId?: string }
74
88
  export type TExportedDIDDocWithLinkedResourceWithKeys = TExportedDIDDocWithKeys & { linkedResource: LinkedResource }
75
89
  export type LinkedResourceMetadataResolutionResult = { resourceURI: string, resourceCollectionId: string, resourceId: string, resourceName: string, resourceType: string, mediaType: string, resourceVersion?: string, created: string, checksum: string, previousVersionId: string | null, nextVersionId: string | null }
@@ -79,28 +93,70 @@ export type ShallowTypedTxTxResponses = { height: string, txhash: string, codesp
79
93
  export type ShallowTypedTxsResponse = { txs: ShallowTypedTx[], tx_responses: ShallowTypedTxTxResponses[], pagination: string | null, total: string } | undefined
80
94
  export type VerificationResult = { verified: boolean, revoked?: boolean, suspended?: boolean, error?: IVerifyResult['error'] }
81
95
  export type StatusCheckResult = { revoked?: boolean, suspended?: boolean, error?: IError }
82
- export type RevocationResult = { revoked: boolean, error?: IError, statusList?: Bitstring, encryptedStatusList?: string, encryptedSymmetricKey?: string, symmetricKey?: string, published?: boolean, resourceMetadata?: LinkedResourceMetadataResolutionResult }
83
- export type SuspensionResult = { suspended: boolean, error?: IError, statusList?: Bitstring, encryptedStatusList?: string, encryptedSymmetricKey?: string, symmetricKey?: string, published?: boolean, resourceMetadata?: LinkedResourceMetadataResolutionResult }
84
- export type UnsuspensionResult = { unsuspended: boolean, error?: IError, statusList?: Bitstring, encryptedStatusList?: string, encryptedSymmetricKey?: string, symmetricKey?: string, published?: boolean, resourceMetadata?: LinkedResourceMetadataResolutionResult }
96
+ export type RevocationResult = { revoked: boolean, error?: IError, statusList?: StatusList2021Revocation, encryptedStatusList?: string, encryptedSymmetricKey?: string, symmetricKey?: string, published?: boolean, resourceMetadata?: LinkedResourceMetadataResolutionResult }
97
+ export type BulkRevocationResult = { revoked: boolean[], error?: IError, statusList?: StatusList2021Revocation, encryptedStatusList?: string, encryptedSymmetricKey?: string, symmetricKey?: string, published?: boolean, resourceMetadata?: LinkedResourceMetadataResolutionResult }
98
+ export type SuspensionResult = { suspended: boolean, error?: IError, statusList?: StatusList2021Suspension, encryptedStatusList?: string, encryptedSymmetricKey?: string, symmetricKey?: string, published?: boolean, resourceMetadata?: LinkedResourceMetadataResolutionResult }
99
+ export type BulkSuspensionResult = { suspended: boolean[], error?: IError, statusList?: StatusList2021Suspension, encryptedStatusList?: string, encryptedSymmetricKey?: string, symmetricKey?: string, published?: boolean, resourceMetadata?: LinkedResourceMetadataResolutionResult }
100
+ export type UnsuspensionResult = { unsuspended: boolean, error?: IError, statusList?: StatusList2021Suspension, encryptedStatusList?: string, encryptedSymmetricKey?: string, symmetricKey?: string, published?: boolean, resourceMetadata?: LinkedResourceMetadataResolutionResult }
101
+ export type BulkUnsuspensionResult = { unsuspended: boolean[], error?: IError, statusList?: StatusList2021Suspension, encryptedStatusList?: string, encryptedSymmetricKey?: string, symmetricKey?: string, published?: boolean, resourceMetadata?: LinkedResourceMetadataResolutionResult }
85
102
  export type Bitstring = string
103
+ export type StatusList2021Revocation = { StatusList2021: { type: typeof DefaultStatusList2021ResourceTypes.revocation, encodedList: string, validFrom: string, validUntil?: string }, metadata: { encrypted: boolean, encoding: DefaultStatusList2021Encoding, paymentConditions?: PaymentCondition[] } }
104
+ export type StatusList2021Suspension = { StatusList2021: { type: typeof DefaultStatusList2021ResourceTypes.suspension, encodedList: string, validFrom: string, validUntil?: string }, metadata: { encrypted: boolean, encoding: DefaultStatusList2021Encoding, paymentConditions?: PaymentCondition[] } }
86
105
  export type AccessControlConditionType = typeof AccessControlConditionTypes[keyof typeof AccessControlConditionTypes]
87
106
  export type AccessControlConditionReturnValueComparator = typeof AccessControlConditionReturnValueComparators[keyof typeof AccessControlConditionReturnValueComparators]
88
107
  export type AccessControlConditionMemoNonceArgs = { senderAddressObserved: string, recipientAddressObserved: string, amountObserved: string, specificNonce?: string, nonceFormat?: TxNonceFormat, type: Extract<AccessControlConditionType, 'memoNonce'> }
89
108
  export type AccessControlConditionBalanceArgs = { addressObserved: string, amountObserved: string, comparator: AccessControlConditionReturnValueComparator, type: Extract<AccessControlConditionType, 'balance'>}
109
+ export type PaymentCondition = { feePaymentAddress: string, feePaymentAmount: string, type: Extract<AccessControlConditionType, 'timelockPayment'> }
110
+ export type CreateStatusList2021Result = { created: boolean, error?: Error, statusList2021: StatusList2021Revocation | StatusList2021Suspension, resourceMetadata: LinkedResourceMetadataResolutionResult, encrypted?: boolean, encryptedSymmetricKey?: string, symmetricKey?: string, encryptedStatusList2021?: string, unifiedAccessControlConditions?: CosmosAccessControlCondition[] }
90
111
  export type CreateEncryptedStatusList2021Result = { created: boolean, error?: Error, encryptedSymmetricKey: string, symmetricKey?: string, encryptedStatusList2021: string, unifiedAccessControlConditions: CosmosAccessControlCondition[] }
91
112
  export type GenerateEncryptedStatusList2021Result = { encryptedSymmetricKey: string, encryptedStatusList2021: string, unifiedAccessControlConditions: CosmosAccessControlCondition[] }
92
113
  export type TransactionResult = { successful: boolean, transactionHash?: string, events?: DeliverTxResponse['events'], rawLog?: string, txResponse?: DeliverTxResponse, error?: IError }
93
114
  export type ObservationResult = { subscribed: boolean, meetsCondition: boolean, transactionHash?: string, events?: DeliverTxResponse['events'], rawLog?: string, txResponse?: ShallowTypedTxTxResponses, error?: IError }
94
115
 
95
- export const AccessControlConditionTypes = { memoNonce: 'memoNonce', balance: 'balance' } as const
116
+ // TODO: revisit while implementing timelock payment
117
+ //? CheckSequenceVerify (CSV) is a script opcode used to implement relative timelocks.
118
+ //? It is a conditional opcode that consumes a minimum of two stack items and compares their relative locktime values.
119
+ //? If the relative locktime of the first item is equal to or greater than the relative locktime of the second item, then the script evaluates to true, otherwise it evaluates to false.
120
+ //? The relative locktime of an item is the number of seconds since the item was confirmed.
121
+ //? The following apply:
122
+ //? - Relative locktimes are measured from the time a transaction is confirmed, not from the time a transaction is created.
123
+ //? - Relative locktimes are not affected by time zone or daylight saving time.
124
+ //? - Relative locktimes are not affected by the time a transaction is broadcast.
125
+ //? - Relative locktimes are not affected by the time a transaction is mined.
126
+ //? - Relative locktimes are not affected by the time a transaction is included in a block.
127
+ //? - Relative locktimes are not affected by the time a block is mined.
128
+
129
+ // Implementation as per:
130
+ //
131
+ // - Generate a random nonce client-side.
132
+ // - Utilise the nonce as the memo.
133
+ // - Observed by the recipient client-side.
134
+ // - Gives turn to initiate decryption of the status list + status verification.
135
+ // - Define time interval OR block height interval client-side.
136
+ // - Utilised as inverse timelock.
137
+ // - Observed by dkg nodes.
138
+ // - TODO: Add chained AND conditions to return value tests, dkg node-side.
139
+ // - e.g. if timelockPayment (a <= now <= b) AND memoNonce strict equality (uuid, IMPORTANT: condition is updateable).
140
+ // - Optionally, recycled per encryption of the status list.
141
+ // - Will be published as part of the status list, if recycled + interval is altered.
142
+
143
+ // Issuer initiated immutable steps:
144
+ // - Initilise ACC with timelockPayment, nonce. <-- Nonce is timestamped as per time of encryption.
145
+ // - On a per transaction basis:
146
+ // - Update ACC with revelant memoNonce.
147
+ // - Rotate symmetric key.
148
+
149
+ export const AccessControlConditionTypes = { timelockPayment: 'timelockPayment', memoNonce: 'memoNonce', balance: 'balance' } as const
96
150
  export const AccessControlConditionReturnValueComparators = { lessThan: '<', greaterThan: '>', equalTo: '=', lessThanOrEqualTo: '<=', greaterThanOrEqualTo: '>=' } as const
97
151
 
152
+ export const RemoteListPattern = /^(https:\/\/)?[a-z0-9_-]+(\.[a-z0-9_-]+)*\.[a-z]{2,}\/1\.0\/identifiers\/did:cheqd:[a-z]+:[a-zA-Z0-9-]+\?((resourceName=[^&]*)&(resourceType=[^&]*)|((resourceType=[^&]*)&(resourceName=[^&]*)))$/
153
+
98
154
  const CreateIdentifierMethodName = 'cheqdCreateIdentifier'
99
155
  const UpdateIdentifierMethodName = 'cheqdUpdateIdentifier'
100
156
  const DeactivateIdentifierMethodName = 'cheqdDeactivateIdentifier'
101
157
  const CreateResourceMethodName = 'cheqdCreateLinkedResource'
102
158
  const CreateStatusList2021MethodName = 'cheqdCreateStatusList2021'
103
- const CreateEncryptedStatusList2021MethodName = 'cheqdCreateEncryptedStatusList2021'
159
+ const BroadcastStatusList2021MethodName = 'cheqdBroadcastStatusList2021'
104
160
  const GenerateDidDocMethodName = 'cheqdGenerateDidDoc'
105
161
  const GenerateDidDocWithLinkedResourceMethodName = 'cheqdGenerateDidDocWithLinkedResource'
106
162
  const GenerateKeyPairMethodName = 'cheqdGenerateIdentityKeys'
@@ -158,6 +214,41 @@ export interface ICheqdCreateLinkedResourceArgs {
158
214
  }
159
215
 
160
216
  export interface ICheqdCreateStatusList2021Args {
217
+ kms: string
218
+ issuerDid: string
219
+ statusListName: string
220
+ statusPurpose: DefaultStatusList2021StatusPurposeType
221
+ encrypted: boolean
222
+ paymentConditions?: PaymentCondition[]
223
+ resourceVersion?: ResourcePayload['version']
224
+ alsoKnownAs?: ResourcePayload['alsoKnownAs']
225
+ statusListLength?: number
226
+ statusListEncoding?: DefaultStatusList2021Encoding
227
+ validUntil?: string
228
+ }
229
+
230
+ export interface ICheqdCreateUnencryptedStatusList2021Args {
231
+ kms: string
232
+ payload: StatusList2021ResourcePayload
233
+ network: CheqdNetwork
234
+ file?: string
235
+ signInputs?: ISignInputs[]
236
+ fee?: DidStdFee
237
+ }
238
+
239
+ export interface ICheqdCreateEncryptedStatusList2021Args extends ICheqdCreateUnencryptedStatusList2021Args {
240
+ encryptionOptions: {
241
+ accessControlConditions: (AccessControlConditionMemoNonceArgs | AccessControlConditionBalanceArgs)[]
242
+ returnSymmetricKey?: boolean
243
+ }
244
+ bootstrapOptions: {
245
+ chain?: LitCompatibleCosmosChain,
246
+ litNetwork?: LitNetwork,
247
+ }
248
+ [key: string]: any
249
+ }
250
+
251
+ export interface ICheqdBroadcastStatusList2021Args {
161
252
  kms: string
162
253
  payload: StatusList2021ResourcePayload
163
254
  network: CheqdNetwork
@@ -166,7 +257,7 @@ export interface ICheqdCreateStatusList2021Args {
166
257
  fee?: DidStdFee
167
258
  }
168
259
 
169
- export interface ICheqdCreateEncryptedStatusList2021Args extends ICheqdCreateStatusList2021Args {
260
+ export interface ICheqdBroadcastEncryptedStatusList2021Args extends ICheqdCreateUnencryptedStatusList2021Args {
170
261
  encryptionOptions: {
171
262
  accessControlConditions: (AccessControlConditionMemoNonceArgs | AccessControlConditionBalanceArgs)[]
172
263
  returnSymmetricKey?: boolean
@@ -199,7 +290,7 @@ export interface ICheqdGenerateVersionIdArgs {
199
290
  export interface ICheqdGenerateStatusList2021Args {
200
291
  length?: number
201
292
  buffer?: Uint8Array
202
- bitstringEncoding?: 'base64' | 'base64url' | 'hex'
293
+ bitstringEncoding?: DefaultStatusList2021Encoding
203
294
  [key: string]: any
204
295
  }
205
296
 
@@ -241,7 +332,7 @@ export interface ICheqdIssueSuspendableCredentialWithStatusList2021Args {
241
332
  }
242
333
 
243
334
  export interface ICheqdVerifyCredentialWithStatusList2021Args {
244
- credential: VerifiableCredential
335
+ credential: W3CVerifiableCredential
245
336
  fetchList?: boolean
246
337
  encryptedSymmetricKey?: string
247
338
  options?: ICheqdStatusList2021Options
@@ -269,7 +360,7 @@ export interface ICheqdVerifyPresentationWithStatusList2021Args {
269
360
  }
270
361
 
271
362
  export interface ICheqdCheckCredentialStatusWithStatusList2021Args {
272
- credential: VerifiableCredential
363
+ credential: W3CVerifiableCredential
273
364
  fetchList?: boolean
274
365
  encryptedSymmetricKey?: string
275
366
  options?: ICheqdStatusList2021Options
@@ -283,7 +374,8 @@ export interface ICheqdCheckCredentialStatusWithStatusList2021Args {
283
374
  }
284
375
 
285
376
  export interface ICheqdRevokeCredentialWithStatusList2021Args {
286
- credential: VerifiableCredential
377
+ credential?: W3CVerifiableCredential
378
+ revocationOptions?: ICheqdRevokeCredentialWithStatusList2021Options
287
379
  fetchList?: boolean
288
380
  publish?: boolean
289
381
  publishEncrypted?: boolean
@@ -298,7 +390,8 @@ export interface ICheqdRevokeCredentialWithStatusList2021Args {
298
390
  }
299
391
 
300
392
  export interface ICheqdRevokeBulkCredentialsWithStatusList2021Args {
301
- credentials: VerifiableCredential[]
393
+ credentials?: W3CVerifiableCredential[]
394
+ revocationOptions?: ICheqdRevokeBulkCredentialsWithStatusList2021Options
302
395
  fetchList?: boolean
303
396
  publish?: boolean
304
397
  publishEncrypted?: boolean
@@ -313,7 +406,8 @@ export interface ICheqdRevokeBulkCredentialsWithStatusList2021Args {
313
406
  }
314
407
 
315
408
  export interface ICheqdSuspendCredentialWithStatusList2021Args {
316
- credential: VerifiableCredential
409
+ credential?: W3CVerifiableCredential
410
+ suspensionOptions?: ICheqdSuspendCredentialWithStatusList2021Options
317
411
  fetchList?: boolean
318
412
  publish?: boolean
319
413
  publishEncrypted?: boolean
@@ -328,7 +422,8 @@ export interface ICheqdSuspendCredentialWithStatusList2021Args {
328
422
  }
329
423
 
330
424
  export interface ICheqdSuspendBulkCredentialsWithStatusList2021Args {
331
- credentials: VerifiableCredential[]
425
+ credentials?: W3CVerifiableCredential[]
426
+ suspensionOptions?: ICheqdSuspendBulkCredentialsWithStatusList2021Options
332
427
  fetchList?: boolean
333
428
  publish?: boolean
334
429
  publishEncrypted?: boolean
@@ -343,7 +438,8 @@ export interface ICheqdSuspendBulkCredentialsWithStatusList2021Args {
343
438
  }
344
439
 
345
440
  export interface ICheqdUnsuspendCredentialWithStatusList2021Args {
346
- credential: VerifiableCredential
441
+ credential?: W3CVerifiableCredential
442
+ unsuspensionOptions?: ICheqdUnsuspendCredentialWithStatusList2021Options
347
443
  fetchList?: boolean
348
444
  publish?: boolean
349
445
  publishEncrypted?: boolean
@@ -358,7 +454,8 @@ export interface ICheqdUnsuspendCredentialWithStatusList2021Args {
358
454
  }
359
455
 
360
456
  export interface ICheqdUnsuspendBulkCredentialsWithStatusList2021Args {
361
- credentials: VerifiableCredential[]
457
+ credentials?: W3CVerifiableCredential[]
458
+ unsuspensionOptions?: ICheqdUnsuspendBulkCredentialsWithStatusList2021Options
362
459
  fetchList?: boolean
363
460
  publish?: boolean
364
461
  publishEncrypted?: boolean
@@ -396,13 +493,55 @@ export interface ICheqdStatusList2021Options {
396
493
  [key: string]: any
397
494
  }
398
495
 
496
+ export interface ICheqdRevokeCredentialWithStatusList2021Options {
497
+ issuerDid: string
498
+ statusListName: string
499
+ statusListIndex: number
500
+ statusListVersion?: string
501
+ }
502
+
503
+ export interface ICheqdRevokeBulkCredentialsWithStatusList2021Options {
504
+ issuerDid: string
505
+ statusListName: string
506
+ statusListIndices: number[]
507
+ statusListVersion?: string
508
+ }
509
+
510
+ export interface ICheqdSuspendCredentialWithStatusList2021Options {
511
+ issuerDid: string
512
+ statusListName: string
513
+ statusListIndex: number
514
+ statusListVersion?: string
515
+ }
516
+
517
+ export interface ICheqdSuspendBulkCredentialsWithStatusList2021Options {
518
+ issuerDid: string
519
+ statusListName: string
520
+ statusListIndices: number[]
521
+ statusListVersion?: string
522
+ }
523
+
524
+ export interface ICheqdUnsuspendCredentialWithStatusList2021Options {
525
+ issuerDid: string
526
+ statusListName: string
527
+ statusListIndex: number
528
+ statusListVersion?: string
529
+ }
530
+
531
+ export interface ICheqdUnsuspendBulkCredentialsWithStatusList2021Options {
532
+ issuerDid: string
533
+ statusListName: string
534
+ statusListIndices: number[]
535
+ statusListVersion?: string
536
+ }
537
+
399
538
  export interface ICheqd extends IPluginMethodMap {
400
539
  [CreateIdentifierMethodName]: (args: ICheqdCreateIdentifierArgs, context: IContext) => Promise<Omit<IIdentifier, 'provider'>>
401
540
  [UpdateIdentifierMethodName]: (args: ICheqdUpdateIdentifierArgs, context: IContext) => Promise<Omit<IIdentifier, 'provider'>>,
402
541
  [DeactivateIdentifierMethodName]: (args: ICheqdDeactivateIdentifierArgs, context: IContext) => Promise<boolean>,
403
542
  [CreateResourceMethodName]: (args: ICheqdCreateLinkedResourceArgs, context: IContext) => Promise<boolean>,
404
- [CreateStatusList2021MethodName]: (args: ICheqdCreateStatusList2021Args, context: IContext) => Promise<boolean>,
405
- [CreateEncryptedStatusList2021MethodName]: (args: ICheqdCreateEncryptedStatusList2021Args, context: IContext) => Promise<CreateEncryptedStatusList2021Result>,
543
+ [CreateStatusList2021MethodName]: (args: ICheqdCreateStatusList2021Args, context: IContext) => Promise<CreateStatusList2021Result>,
544
+ [BroadcastStatusList2021MethodName]: (args: ICheqdBroadcastStatusList2021Args, context: IContext) => Promise<boolean>,
406
545
  [GenerateDidDocMethodName]: (args: ICheqdGenerateDidDocArgs, context: IContext) => Promise<TExportedDIDDocWithKeys>,
407
546
  [GenerateDidDocWithLinkedResourceMethodName]: (args: ICheqdGenerateDidDocWithLinkedResourceArgs, context: IContext) => Promise<TExportedDIDDocWithLinkedResourceWithKeys>,
408
547
  [GenerateKeyPairMethodName]: (args: ICheqdGenerateKeyPairArgs, context: IContext) => Promise<TImportableEd25519Key>
@@ -415,11 +554,11 @@ export interface ICheqd extends IPluginMethodMap {
415
554
  [VerifyPresentationMethodName]: (args: ICheqdVerifyPresentationWithStatusList2021Args, context: IContext) => Promise<VerificationResult>
416
555
  [CheckCredentialStatusMethodName]: (args: ICheqdCheckCredentialStatusWithStatusList2021Args, context: IContext) => Promise<StatusCheckResult>
417
556
  [RevokeCredentialMethodName]: (args: ICheqdRevokeCredentialWithStatusList2021Args, context: IContext) => Promise<RevocationResult>
418
- [RevokeCredentialsMethodName]: (args: ICheqdRevokeBulkCredentialsWithStatusList2021Args, context: IContext) => Promise<RevocationResult[]>
557
+ [RevokeCredentialsMethodName]: (args: ICheqdRevokeBulkCredentialsWithStatusList2021Args, context: IContext) => Promise<BulkRevocationResult>
419
558
  [SuspendCredentialMethodName]: (args: ICheqdSuspendCredentialWithStatusList2021Args, context: IContext) => Promise<SuspensionResult>
420
- [SuspendCredentialsMethodName]: (args: ICheqdSuspendBulkCredentialsWithStatusList2021Args, context: IContext) => Promise<SuspensionResult[]>
559
+ [SuspendCredentialsMethodName]: (args: ICheqdSuspendBulkCredentialsWithStatusList2021Args, context: IContext) => Promise<BulkSuspensionResult>
421
560
  [UnsuspendCredentialMethodName]: (args: ICheqdUnsuspendCredentialWithStatusList2021Args, context: IContext) => Promise<UnsuspensionResult>
422
- [UnsuspendCredentialsMethodName]: (args: ICheqdUnsuspendBulkCredentialsWithStatusList2021Args, context: IContext) => Promise<UnsuspensionResult[]>
561
+ [UnsuspendCredentialsMethodName]: (args: ICheqdUnsuspendBulkCredentialsWithStatusList2021Args, context: IContext) => Promise<BulkUnsuspensionResult>
423
562
  [TransactVerifierPaysIssuerMethodName]: (args: ICheqdTransactVerifierPaysIssuerArgs, context: IContext) => Promise<TransactionResult>
424
563
  [ObserveVerifierPaysIssuerMethodName]: (args: ICheqdObserveVerifierPaysIssuerArgs, context: IContext) => Promise<ObservationResult>
425
564
  }
@@ -517,17 +656,17 @@ export class Cheqd implements IAgentPlugin {
517
656
  ]
518
657
  },
519
658
  "returnType": {
520
- "type": "boolean"
659
+ "type": "object"
521
660
  }
522
661
  },
523
- "cheqdCreateEncryptedStatusList2021": {
524
- "description": "Create a new Encrypted Status List 2021",
662
+ "cheqdBroadcastStatusList2021": {
663
+ "description": "Broadcast a Status List 2021 to cheqd ledger",
525
664
  "arguments": {
526
665
  "type": "object",
527
666
  "properties": {
528
667
  "args": {
529
668
  "type": "object",
530
- "description": "A cheqdCreateEncryptedStatusList2021Args object as any for extensibility"
669
+ "description": "A cheqdBroadcastStatusList2021Args object as any for extensibility"
531
670
  }
532
671
  },
533
672
  "required": [
@@ -897,7 +1036,7 @@ export class Cheqd implements IAgentPlugin {
897
1036
  [DeactivateIdentifierMethodName]: this.DeactivateIdentifier.bind(this),
898
1037
  [CreateResourceMethodName]: this.CreateResource.bind(this),
899
1038
  [CreateStatusList2021MethodName]: this.CreateStatusList2021.bind(this),
900
- [CreateEncryptedStatusList2021MethodName]: this.CreateEncryptedStatusList2021.bind(this),
1039
+ [BroadcastStatusList2021MethodName]: this.BroadcastStatusList2021.bind(this),
901
1040
  [GenerateDidDocMethodName]: this.GenerateDidDoc.bind(this),
902
1041
  [GenerateDidDocWithLinkedResourceMethodName]: this.GenerateDidDocWithLinkedResource.bind(this),
903
1042
  [GenerateKeyPairMethodName]: this.GenerateIdentityKeys.bind(this),
@@ -1040,41 +1179,145 @@ export class Cheqd implements IAgentPlugin {
1040
1179
  throw new Error('[did-provider-cheqd]: kms is required')
1041
1180
  }
1042
1181
 
1043
- if (typeof args.payload !== 'object') {
1044
- throw new Error('[did-provider-cheqd]: payload object is required')
1182
+ if (typeof args.issuerDid !== 'string' || !args.issuerDid) {
1183
+ throw new Error('[did-provider-cheqd]: issuerDid is required')
1045
1184
  }
1046
1185
 
1047
- if (typeof args.network !== 'string') {
1048
- throw new Error('[did-provider-cheqd]: network is required')
1186
+ if (typeof args.statusListName !== 'string' || !args.statusListName) {
1187
+ throw new Error('[did-provider-cheqd]: statusListName is required')
1049
1188
  }
1050
1189
 
1051
- if (args?.file) {
1052
- args.payload.data = await Cheqd.getFile(args.file)
1190
+ if (typeof args.statusPurpose !== 'string' || !args.statusPurpose) {
1191
+ throw new Error('[did-provider-cheqd]: statusPurpose is required')
1053
1192
  }
1054
1193
 
1055
- if (typeof args?.payload?.data === 'string') {
1056
- args.payload.data = fromString(args.payload.data, 'base64')
1194
+ if (typeof args.encrypted === 'undefined') {
1195
+ throw new Error('[did-provider-cheqd]: encrypted is required')
1057
1196
  }
1058
1197
 
1059
- // TODO: validate data as per bitstring
1198
+ // validate statusPurpose
1199
+ if (!Object.values(DefaultStatusList2021StatusPurposeTypes).includes(args.statusPurpose)) {
1200
+ throw new Error(`[did-provider-cheqd]: statusPurpose must be one of ${Object.values(DefaultStatusList2021StatusPurposeTypes).join(', ')}`)
1201
+ }
1060
1202
 
1061
- // set default resource type in runtime
1062
- args.payload.resourceType = 'StatusList2021'
1203
+ // validate statusListLength
1204
+ if (args?.statusListLength) {
1205
+ if (typeof args.statusListLength !== 'number') {
1206
+ throw new Error('[did-provider-cheqd]: statusListLength must be number')
1207
+ }
1063
1208
 
1064
- this.providerId = Cheqd.generateProviderId(args.network)
1065
- this.didProvider = await Cheqd.loadProvider({ id: this.providerId } as DIDDocument, this.supportedDidProviders)
1209
+ if (args.statusListLength < Cheqd.defaultStatusList2021Length) {
1210
+ throw new Error(`[did-provider-cheqd]: statusListLength must be greater than or equal to ${Cheqd.defaultStatusList2021Length} number of entries`)
1211
+ }
1212
+ }
1066
1213
 
1067
- return await this.didProvider.createResource({
1068
- options: {
1069
- kms: args.kms,
1070
- payload: args.payload,
1071
- signInputs: args.signInputs,
1072
- fee: args?.fee
1214
+ // validate statusListEncoding
1215
+ if (args?.statusListEncoding) {
1216
+ if (typeof args.statusListEncoding !== 'string') {
1217
+ throw new Error('[did-provider-cheqd]: statusListEncoding must be string')
1073
1218
  }
1074
- }, context)
1219
+
1220
+ if (!Object.values(DefaultStatusList2021Encodings).includes(args.statusListEncoding)) {
1221
+ throw new Error(`[did-provider-cheqd]: statusListEncoding must be one of ${Object.values(DefaultStatusList2021Encodings).join(', ')}`)
1222
+ }
1223
+ }
1224
+
1225
+ // validate validUntil
1226
+ if (args?.validUntil) {
1227
+ if (typeof args.validUntil !== 'string') {
1228
+ throw new Error('[did-provider-cheqd]: validUntil must be string')
1229
+ }
1230
+
1231
+ if (new Date() <= new Date(args.validUntil)) {
1232
+ throw new Error('[did-provider-cheqd]: validUntil must be greater than current date')
1233
+ }
1234
+ }
1235
+
1236
+ // validate args in pairs - case: encrypted
1237
+ if (args.encrypted) {
1238
+ // validate paymentConditions
1239
+ if (!args?.paymentConditions || !args?.paymentConditions?.length || !Array.isArray(args?.paymentConditions) || args?.paymentConditions.length === 0) {
1240
+ throw new Error('[did-provider-cheqd]: paymentConditions is required')
1241
+ }
1242
+
1243
+ if (!args?.paymentConditions?.every((condition) => condition.feePaymentAddress && condition.feePaymentAmount)) {
1244
+ throw new Error('[did-provider-cheqd]: paymentConditions must contain feePaymentAddress and feeAmount')
1245
+ }
1246
+
1247
+ if (!args?.paymentConditions?.every((condition) => typeof condition.feePaymentAddress === 'string' && typeof condition.feePaymentAmount === 'string')) {
1248
+ throw new Error('[did-provider-cheqd]: feePaymentAddress and feePaymentAmount must be string')
1249
+ }
1250
+
1251
+ if (!args?.paymentConditions?.every((condition) => condition.type === AccessControlConditionTypes.timelockPayment)) {
1252
+ throw new Error('[did-provider-cheqd]: paymentConditions must be of type timelockPayment')
1253
+ }
1254
+ }
1255
+
1256
+ // get network
1257
+ const network = args.issuerDid.split(':')[2]
1258
+
1259
+ // generate bitstring
1260
+ const bitstring = await context.agent[GenerateStatusList2021MethodName]({ length: args?.statusListLength || Cheqd.defaultStatusList2021Length, bitstringEncoding: args?.statusListEncoding || DefaultStatusList2021Encodings.base64url })
1261
+
1262
+ // construct data
1263
+ const data = args.encrypted
1264
+ ? (await (async function () {
1265
+ // TODO: implement
1266
+ throw new Error('[did-provider-cheqd]: encrypted status list is not implemented yet')
1267
+ }()))
1268
+ : (await (async function () {
1269
+ switch (args.statusPurpose) {
1270
+ case DefaultStatusList2021StatusPurposeTypes.revocation:
1271
+ return {
1272
+ StatusList2021: {
1273
+ type: DefaultStatusList2021ResourceTypes.revocation,
1274
+ encodedList: bitstring,
1275
+ validFrom: new Date().toISOString(),
1276
+ validUntil: args?.validUntil
1277
+ },
1278
+ metadata: {
1279
+ encrypted: false,
1280
+ encoding: args?.statusListEncoding || DefaultStatusList2021Encodings.base64url,
1281
+ }
1282
+ } satisfies StatusList2021Revocation
1283
+ case DefaultStatusList2021StatusPurposeTypes.suspension:
1284
+ return {
1285
+ StatusList2021: {
1286
+ type: DefaultStatusList2021ResourceTypes.suspension,
1287
+ encodedList: bitstring,
1288
+ validFrom: new Date().toISOString(),
1289
+ validUntil: args?.validUntil
1290
+ },
1291
+ metadata: {
1292
+ encrypted: false,
1293
+ encoding: args?.statusListEncoding || DefaultStatusList2021Encodings.base64url,
1294
+ }
1295
+ } satisfies StatusList2021Suspension
1296
+ default:
1297
+ throw new Error('[did-provider-cheqd]: statusPurpose is not valid')
1298
+ }
1299
+ }()))
1300
+
1301
+ // construct payload
1302
+ const payload = {
1303
+ id: v4(),
1304
+ collectionId: args.issuerDid.split(':').reverse()[0],
1305
+ name: args.statusListName,
1306
+ resourceType: DefaultStatusList2021ResourceTypes[args.statusPurpose],
1307
+ version: args?.resourceVersion || new Date().toISOString(),
1308
+ alsoKnownAs: args?.alsoKnownAs || [],
1309
+ data: fromString(JSON.stringify(data), 'utf-8'),
1310
+ } satisfies StatusList2021ResourcePayload
1311
+
1312
+ // return result
1313
+ return {
1314
+ created: await context.agent[BroadcastStatusList2021MethodName]({ kms: args.kms, payload, network: network as CheqdNetwork }),
1315
+ statusList2021: data,
1316
+ resourceMetadata: await Cheqd.fetchStatusList2021Metadata({ credentialStatus: { id: `${resolverUrl}${args.issuerDid}?resourceName=${args.statusListName}&resourceType=${DefaultStatusList2021ResourceTypes[args.statusPurpose]}`, type: 'StatusList2021Entry' } } as VerifiableCredential)
1317
+ } satisfies CreateStatusList2021Result
1075
1318
  }
1076
1319
 
1077
- private async CreateEncryptedStatusList2021(args: ICheqdCreateEncryptedStatusList2021Args, context: IContext): Promise<CreateEncryptedStatusList2021Result> {
1320
+ private async BroadcastStatusList2021(args: ICheqdBroadcastStatusList2021Args, context: IContext) {
1078
1321
  if (typeof args.kms !== 'string') {
1079
1322
  throw new Error('[did-provider-cheqd]: kms is required')
1080
1323
  }
@@ -1097,65 +1340,15 @@ export class Cheqd implements IAgentPlugin {
1097
1340
 
1098
1341
  // TODO: validate data as per bitstring
1099
1342
 
1100
- if (!args?.encryptionOptions) {
1101
- throw new Error('[did-provider-cheqd]: encryptionOptions is required')
1102
- }
1103
-
1104
- if (!args?.bootstrapOptions) {
1105
- throw new Error('[did-provider-cheqd]: bootstrapOptions is required')
1106
- }
1107
-
1108
- if (!args?.encryptionOptions?.accessControlConditions) {
1109
- throw new Error('[did-provider-cheqd]: accessControlConditions is required')
1343
+ // validate resource type
1344
+ if (!Object.values(DefaultStatusList2021ResourceTypes).includes(args?.payload?.resourceType)) {
1345
+ throw new Error(`[did-provider-cheqd]: resourceType must be one of ${Object.values(DefaultStatusList2021ResourceTypes).join(', ')}`)
1110
1346
  }
1111
1347
 
1112
- // instantiate dkg-threshold client, in which case lit-protocol is used
1113
- const lit = await LitProtocol.create({
1114
- chain: args.bootstrapOptions?.chain,
1115
- litNetwork: args.bootstrapOptions?.litNetwork
1116
- })
1117
-
1118
- // construct access control conditions
1119
- const unifiedAccessControlConditions = await Promise.all(args.encryptionOptions.accessControlConditions.map(async (condition) => {
1120
- switch (condition.type) {
1121
- case AccessControlConditionTypes.memoNonce:
1122
- return await LitProtocol.generateCosmosAccessControlConditionTransactionMemo({
1123
- key: '$.txs.*.body.memo',
1124
- comparator: 'contains',
1125
- value: condition?.specificNonce || await LitProtocol.generateTxNonce(condition?.nonceFormat)
1126
- },
1127
- condition.amountObserved,
1128
- condition.senderAddressObserved,
1129
- condition.recipientAddressObserved,
1130
- args.bootstrapOptions.chain
1131
- )
1132
- case AccessControlConditionTypes.balance:
1133
- return await LitProtocol.generateCosmosAccessControlConditionBalance({
1134
- key: '$.balances[0].amount',
1135
- comparator: condition.comparator,
1136
- value: condition.amountObserved
1137
- },
1138
- args.bootstrapOptions.chain,
1139
- condition.addressObserved
1140
- )
1141
- default:
1142
- throw new Error(`[did-provider-cheqd]: accessControlCondition type is not supported`)
1143
- }
1144
- }))
1145
-
1146
- // encrypt data
1147
- const { encryptedString, encryptedSymmetricKey, symmetricKey } = await lit.encrypt(toString(args.payload.data!, 'base64url'), unifiedAccessControlConditions, true)
1148
-
1149
- // set encrypted data
1150
- args.payload.data = new Uint8Array(await encryptedString.arrayBuffer())
1151
-
1152
- // set default resource type in runtime
1153
- args.payload.resourceType = 'StatusList2021'
1154
-
1155
1348
  this.providerId = Cheqd.generateProviderId(args.network)
1156
1349
  this.didProvider = await Cheqd.loadProvider({ id: this.providerId } as DIDDocument, this.supportedDidProviders)
1157
1350
 
1158
- const created = await this.didProvider.createResource({
1351
+ return await this.didProvider.createResource({
1159
1352
  options: {
1160
1353
  kms: args.kms,
1161
1354
  payload: args.payload,
@@ -1163,14 +1356,6 @@ export class Cheqd implements IAgentPlugin {
1163
1356
  fee: args?.fee
1164
1357
  }
1165
1358
  }, context)
1166
-
1167
- return {
1168
- created,
1169
- encryptedSymmetricKey,
1170
- encryptedStatusList2021: await blobToHexString(encryptedString),
1171
- symmetricKey: args?.encryptionOptions?.returnSymmetricKey ? toString(symmetricKey!, 'hex') : undefined,
1172
- unifiedAccessControlConditions
1173
- } satisfies CreateEncryptedStatusList2021Result
1174
1359
  }
1175
1360
 
1176
1361
  private async GenerateDidDoc(
@@ -1282,7 +1467,7 @@ export class Cheqd implements IAgentPlugin {
1282
1467
  return toString(fromString(encoded, 'base64url'), 'hex')
1283
1468
  default:
1284
1469
  return encoded
1285
- }
1470
+ }
1286
1471
  }
1287
1472
 
1288
1473
  private async GenerateEncryptedStatusList2021(args: ICheqdGenerateEncryptedStatusList2021Args, context: IContext): Promise<GenerateEncryptedStatusList2021Result> {
@@ -1359,7 +1544,7 @@ export class Cheqd implements IAgentPlugin {
1359
1544
  : args.issuanceOptions.credential.issuer as string
1360
1545
 
1361
1546
  // generate status list credential
1362
- const statusListCredential = `${resolverUrl}${issuer}?resourceName=${args.statusOptions.statusListName}`
1547
+ const statusListCredential = `${resolverUrl}${issuer}?resourceName=${args.statusOptions.statusListName}&resourceType=StatusList2021Revocation`
1363
1548
 
1364
1549
  // construct credential status
1365
1550
  const credentialStatus = {
@@ -1367,7 +1552,6 @@ export class Cheqd implements IAgentPlugin {
1367
1552
  type: 'StatusList2021Entry',
1368
1553
  statusPurpose: 'revocation',
1369
1554
  statusListIndex: `${statusListIndex}`,
1370
- statusListCredential,
1371
1555
  }
1372
1556
 
1373
1557
  // add credential status to credential
@@ -1411,7 +1595,7 @@ export class Cheqd implements IAgentPlugin {
1411
1595
  : args.issuanceOptions.credential.issuer as string
1412
1596
 
1413
1597
  // generate status list credential
1414
- const statusListCredential = `${resolverUrl}${issuer}?resourceName=${args.statusOptions.statusListName}`
1598
+ const statusListCredential = `${resolverUrl}${issuer}?resourceName=${args.statusOptions.statusListName}&resourceType=StatusList2021Suspension`
1415
1599
 
1416
1600
  // construct credential status
1417
1601
  const credentialStatus = {
@@ -1419,7 +1603,6 @@ export class Cheqd implements IAgentPlugin {
1419
1603
  type: 'StatusList2021Entry',
1420
1604
  statusPurpose: 'suspension',
1421
1605
  statusListIndex: `${statusListIndex}`,
1422
- statusListCredential,
1423
1606
  }
1424
1607
 
1425
1608
  // add credential status to credential
@@ -1467,16 +1650,19 @@ export class Cheqd implements IAgentPlugin {
1467
1650
  return { verified: false, error: verificationResult.error }
1468
1651
  }
1469
1652
 
1653
+ // if jwt credential, decode it
1654
+ const credential = typeof args.credential === 'string' ? await Cheqd.decodeCredentialJWT(args.credential) : args.credential
1655
+
1470
1656
  // verify credential status
1471
- switch (args.credential.credentialStatus?.statusPurpose) {
1657
+ switch (credential.credentialStatus?.statusPurpose) {
1472
1658
  case 'revocation':
1473
- if (await Cheqd.checkRevoked(args.credential, { ...args.options, topArgs: args })) return { verified: false, revoked: true }
1659
+ if (await Cheqd.checkRevoked(credential, { ...args.options, topArgs: args })) return { verified: false, revoked: true }
1474
1660
  return { verified: true, revoked: false }
1475
1661
  case 'suspension':
1476
- if (await Cheqd.checkSuspended(args.credential, { ...args.options, topArgs: args })) return { verified: false, suspended: true }
1662
+ if (await Cheqd.checkSuspended(credential, { ...args.options, topArgs: args })) return { verified: false, suspended: true }
1477
1663
  return { verified: true, suspended: false }
1478
1664
  default:
1479
- throw new Error(`[did-provider-cheqd]: verify credential: Unsupported status purpose: ${args.credential.credentialStatus?.statusPurpose}`)
1665
+ throw new Error(`[did-provider-cheqd]: verify credential: Unsupported status purpose: ${credential.credentialStatus?.statusPurpose}`)
1480
1666
  }
1481
1667
  }
1482
1668
 
@@ -1499,17 +1685,7 @@ export class Cheqd implements IAgentPlugin {
1499
1685
  // verify credential(s) status(es)
1500
1686
  for (let credential of args.presentation.verifiableCredential) {
1501
1687
  // if jwt credential, decode it
1502
- if (typeof credential === 'string') {
1503
- const decodedCredential = decodeJWT(credential)
1504
-
1505
- // validate credential payload
1506
- if (!decodedCredential.payload) throw new Error('[did-provider-cheqd]: verify presentation: decodedCredential.payload is required')
1507
-
1508
- // validate credential payload ref as VerifiableCredential
1509
- if (!decodedCredential.payload.vc) throw new Error('[did-provider-cheqd]: verify presentation: decodedCredential.payload.vc is required')
1510
-
1511
- credential = decodedCredential.payload.vc as VerifiableCredential
1512
- }
1688
+ if (typeof credential === 'string') credential = await Cheqd.decodeCredentialJWT(credential)
1513
1689
 
1514
1690
  switch (credential.credentialStatus?.statusPurpose) {
1515
1691
  case 'revocation':
@@ -1527,22 +1703,76 @@ export class Cheqd implements IAgentPlugin {
1527
1703
  }
1528
1704
 
1529
1705
  private async CheckCredentialStatusWithStatusList2021(args: ICheqdCheckCredentialStatusWithStatusList2021Args, context: IContext): Promise<StatusCheckResult> {
1530
- switch (args.credential.credentialStatus?.statusPurpose) {
1706
+ // if jwt credential, decode it
1707
+ const credential = typeof args.credential === 'string' ? await Cheqd.decodeCredentialJWT(args.credential) : args.credential
1708
+
1709
+ switch (credential.credentialStatus?.statusPurpose) {
1531
1710
  case 'revocation':
1532
- if (await Cheqd.checkRevoked(args.credential, { ...args.options, topArgs: args })) return { revoked: true }
1711
+ if (await Cheqd.checkRevoked(credential, { ...args.options, topArgs: args })) return { revoked: true }
1533
1712
  return { revoked: false }
1534
1713
  case 'suspension':
1535
- if (await Cheqd.checkSuspended(args.credential, { ...args.options, topArgs: args })) return { suspended: true }
1714
+ if (await Cheqd.checkSuspended(credential, { ...args.options, topArgs: args })) return { suspended: true }
1536
1715
  return { suspended: false }
1537
1716
  default:
1538
- throw new Error(`[did-provider-cheqd]: check status: Unsupported status purpose: ${args.credential.credentialStatus?.statusPurpose}`)
1717
+ throw new Error(`[did-provider-cheqd]: check status: Unsupported status purpose: ${credential.credentialStatus?.statusPurpose}`)
1539
1718
  }
1540
1719
  }
1541
1720
 
1542
1721
  private async RevokeCredentialWithStatusList2021(args: ICheqdRevokeCredentialWithStatusList2021Args, context: IContext): Promise<RevocationResult> {
1722
+ // verify credential, if provided and revocation options are not
1723
+ if (args?.credential && !args?.revocationOptions) {
1724
+ const verificationResult = await context.agent.verifyCredential({
1725
+ credential: args.credential,
1726
+ policies: {
1727
+ credentialStatus: false
1728
+ }
1729
+ } satisfies IVerifyCredentialArgs)
1730
+
1731
+ // early return if verification failed
1732
+ if (!verificationResult.verified) {
1733
+ return { revoked: false, error: verificationResult.error }
1734
+ }
1735
+ }
1736
+
1737
+ // if revocation options are provided, give precedence
1738
+ if (args?.revocationOptions) {
1739
+ // validate revocation options - case: revocationOptions.issuerDid
1740
+ if (!args.revocationOptions.issuerDid) throw new Error('[did-provider-cheqd]: revocation: revocationOptions.issuerDid is required')
1741
+
1742
+ // validate revocation options - case: revocationOptions.statusListName
1743
+ if (!args.revocationOptions.statusListName) throw new Error('[did-provider-cheqd]: revocation: revocationOptions.statusListName is required')
1744
+
1745
+ // validate revocation options - case: revocationOptions.statusListIndex
1746
+ if (!args.revocationOptions.statusListIndex) throw new Error('[did-provider-cheqd]: revocation: revocationOptions.statusListIndex is required')
1747
+
1748
+ // construct status list credential
1749
+ const statusListCredential = `${resolverUrl}${args.revocationOptions.issuerDid}?resourceName=${args.revocationOptions.statusListName}&resourceType=StatusList2021Revocation`
1750
+
1751
+ // construct credential status
1752
+ args.credential = {
1753
+ '@context': [],
1754
+ issuer: args.revocationOptions.issuerDid,
1755
+ credentialSubject: {},
1756
+ credentialStatus: {
1757
+ id: `${statusListCredential}#${args.revocationOptions.statusListIndex}`,
1758
+ type: 'StatusList2021Entry',
1759
+ statusPurpose: 'revocation',
1760
+ statusListIndex: `${args.revocationOptions.statusListIndex}`,
1761
+ },
1762
+ issuanceDate: '',
1763
+ proof: {}
1764
+ }
1765
+ }
1766
+
1767
+ // validate args - case: credential
1768
+ if (!args.credential) throw new Error('[did-provider-cheqd]: revocation: credential is required')
1769
+
1770
+ // if jwt credential, decode it
1771
+ const credential = typeof args.credential === 'string' ? await Cheqd.decodeCredentialJWT(args.credential) : args.credential
1772
+
1543
1773
  // validate status purpose
1544
- if (args.credential.credentialStatus?.statusPurpose !== 'revocation') {
1545
- throw new Error(`[did-provider-cheqd]: revocation: Unsupported status purpose: ${args.credential.credentialStatus?.statusPurpose}`)
1774
+ if (credential.credentialStatus?.statusPurpose !== 'revocation') {
1775
+ throw new Error(`[did-provider-cheqd]: revocation: Unsupported status purpose: ${credential.credentialStatus?.statusPurpose}`)
1546
1776
  }
1547
1777
 
1548
1778
  // validate args in pairs - case: statusListFile and statusList
@@ -1566,73 +1796,168 @@ export class Cheqd implements IAgentPlugin {
1566
1796
  }
1567
1797
 
1568
1798
  // revoke credential
1569
- return await Cheqd.revokeCredential(args.credential, {
1799
+ return await Cheqd.revokeCredential(credential, {
1570
1800
  ...args.options,
1571
1801
  topArgs: args,
1572
1802
  publishOptions: {
1573
1803
  context,
1804
+ statusListEncoding: args?.options?.statusListEncoding,
1805
+ statusListValidUntil: args?.options?.statusListValidUntil,
1574
1806
  resourceId: args?.options?.resourceId,
1575
1807
  resourceVersion: args?.options?.resourceVersion,
1808
+ resourceAlsoKnownAs: args?.options?.alsoKnownAs,
1576
1809
  signInputs: args?.options?.signInputs,
1577
1810
  fee: args?.options?.fee
1578
1811
  }
1579
1812
  })
1580
1813
  }
1581
1814
 
1582
- private async RevokeBulkCredentialsWithStatusList2021(args: ICheqdRevokeBulkCredentialsWithStatusList2021Args, context: IContext): Promise<RevocationResult[]> {
1583
- // TODO: implement
1584
- throw new Error('[did-provider-cheqd]: revocation: bulk revocation is not implemented yet')
1585
- }
1815
+ private async RevokeBulkCredentialsWithStatusList2021(args: ICheqdRevokeBulkCredentialsWithStatusList2021Args, context: IContext): Promise<BulkRevocationResult> {
1816
+ // verify credential, if provided and revocation options are not
1817
+ if (args?.credentials && !args?.revocationOptions) {
1818
+
1819
+ const verificationResult = await Promise.all(args.credentials.map(async (credential) => {
1820
+ return await context.agent.verifyCredential({
1821
+ credential,
1822
+ policies: {
1823
+ credentialStatus: false
1824
+ }
1825
+ } satisfies IVerifyCredentialArgs)
1826
+ }))
1586
1827
 
1587
- private async SuspendCredentialWithStatusList2021(args: ICheqdSuspendCredentialWithStatusList2021Args, context: IContext): Promise<SuspensionResult> {
1588
- // validate status purpose
1589
- if (args.credential.credentialStatus?.statusPurpose !== 'suspension') {
1590
- throw new Error(`[did-provider-cheqd]: suspension: Unsupported status purpose: ${args.credential.credentialStatus?.statusPurpose}`)
1828
+ // early return if verification failed for any credential
1829
+ if (verificationResult.some(result => !result.verified)) {
1830
+ // define verified
1831
+ return { revoked: Array(args.credentials.length).fill(false), error: verificationResult.find(result => !result.verified)!.error || { message: 'verification: could not verify credential' } }
1832
+ }
1833
+ }
1834
+
1835
+ // if revocation options are provided, give precedence
1836
+ if (args?.revocationOptions) {
1837
+ // validate revocation options - case: revocationOptions.issuerDid
1838
+ if (!args.revocationOptions.issuerDid) throw new Error('[did-provider-cheqd]: revocation: revocationOptions.issuerDid is required')
1839
+
1840
+ // validate revocation options - case: revocationOptions.statusListName
1841
+ if (!args.revocationOptions.statusListName) throw new Error('[did-provider-cheqd]: revocation: revocationOptions.statusListName is required')
1842
+
1843
+ // validate revocation options - case: revocationOptions.statusListIndices
1844
+ if (!args.revocationOptions.statusListIndices || !args.revocationOptions.statusListIndices.length || args.revocationOptions.statusListIndices.length === 0 || !args.revocationOptions.statusListIndices.every(index => !isNaN(+index))) throw new Error('[did-provider-cheqd]: revocation: revocationOptions.statusListIndex is required and must be an array of indices')
1845
+
1846
+ // construct status list credential
1847
+ const statusListCredential = `${resolverUrl}${args.revocationOptions.issuerDid}?resourceName=${args.revocationOptions.statusListName}&resourceType=StatusList2021Revocation`
1848
+
1849
+ // construct credential status
1850
+ args.credentials = args.revocationOptions.statusListIndices.map(index => ({
1851
+ '@context': [],
1852
+ issuer: args.revocationOptions!.issuerDid,
1853
+ credentialSubject: {},
1854
+ credentialStatus: {
1855
+ id: `${statusListCredential}#${index}`,
1856
+ type: 'StatusList2021Entry',
1857
+ statusPurpose: 'revocation',
1858
+ statusListIndex: `${index}`,
1859
+ },
1860
+ issuanceDate: '',
1861
+ proof: {}
1862
+ }))
1591
1863
  }
1592
1864
 
1865
+ // validate args - case: credentials
1866
+ if (!args.credentials || !args.credentials.length || args.credentials.length === 0) throw new Error('[did-provider-cheqd]: revocation: credentials is required and must be an array of credentials')
1867
+
1868
+ // if jwt credentials, decode them
1869
+ const credentials = await Promise.all(args.credentials.map(async credential => typeof credential === 'string' ? await Cheqd.decodeCredentialJWT(credential) : credential))
1870
+
1593
1871
  // validate args in pairs - case: statusListFile and statusList
1594
1872
  if (args.options?.statusListFile && args.options?.statusList) {
1595
- throw new Error('[did-provider-cheqd]: suspension: statusListFile and statusList are mutually exclusive')
1873
+ throw new Error('[did-provider-cheqd]: revocation: statusListFile and statusList are mutually exclusive')
1596
1874
  }
1597
1875
 
1598
1876
  // validate args in pairs - case: statusListFile and fetchList
1599
1877
  if (args.options?.statusListFile && args.options?.fetchList) {
1600
- throw new Error('[did-provider-cheqd]: suspension: statusListFile and fetchList are mutually exclusive')
1878
+ throw new Error('[did-provider-cheqd]: revocation: statusListFile and fetchList are mutually exclusive')
1601
1879
  }
1602
1880
 
1603
1881
  // validate args in pairs - case: statusList and fetchList
1604
1882
  if (args.options?.statusList && args.options?.fetchList) {
1605
- throw new Error('[did-provider-cheqd]: suspension: statusList and fetchList are mutually exclusive')
1883
+ throw new Error('[did-provider-cheqd]: revocation: statusList and fetchList are mutually exclusive')
1606
1884
  }
1607
1885
 
1608
1886
  // validate args in pairs - case: publish
1609
1887
  if (args.options?.publish && !args.fetchList && !(args.options?.statusListFile || args.options?.statusList)) {
1610
- throw new Error('[did-provider-cheqd]: suspension: publish requires statusListFile or statusList, if fetchList is disabled')
1888
+ throw new Error('[did-provider-cheqd]: revocation: publish requires statusListFile or statusList, if fetchList is disabled')
1611
1889
  }
1612
1890
 
1613
- // suspend credential
1614
- return await Cheqd.suspendCredential(args.credential, {
1891
+ // revoke credentials
1892
+ return await Cheqd.revokeCredentials(credentials, {
1615
1893
  ...args.options,
1616
1894
  topArgs: args,
1617
1895
  publishOptions: {
1618
1896
  context,
1619
1897
  resourceId: args?.options?.resourceId,
1620
1898
  resourceVersion: args?.options?.resourceVersion,
1899
+ resourceAlsoKnownAs: args?.options?.alsoKnownAs,
1621
1900
  signInputs: args?.options?.signInputs,
1622
1901
  fee: args?.options?.fee
1623
1902
  }
1624
1903
  })
1625
1904
  }
1626
1905
 
1627
- private async SuspendBulkCredentialsWithStatusList2021(args: ICheqdSuspendBulkCredentialsWithStatusList2021Args, context: IContext): Promise<SuspensionResult[]> {
1628
- // TODO: implement
1629
- throw new Error('[did-provider-cheqd]: suspension: bulk suspension is not implemented yet')
1630
- }
1906
+ private async SuspendCredentialWithStatusList2021(args: ICheqdSuspendCredentialWithStatusList2021Args, context: IContext): Promise<SuspensionResult> {
1907
+ // verify credential, if provided and suspension options are not
1908
+ if (args?.credential && !args?.suspensionOptions) {
1909
+ const verificationResult = await context.agent.verifyCredential({
1910
+ credential: args.credential,
1911
+ policies: {
1912
+ credentialStatus: false
1913
+ }
1914
+ } satisfies IVerifyCredentialArgs)
1915
+
1916
+ // early return if verification failed
1917
+ if (!verificationResult.verified) {
1918
+ return { suspended: false, error: verificationResult.error }
1919
+ }
1920
+ }
1921
+
1922
+ // if suspension options are provided, give precedence
1923
+ if (args?.suspensionOptions) {
1924
+ // validate suspension options - case: suspensionOptions.issuerDid
1925
+ if (!args.suspensionOptions.issuerDid) throw new Error('[did-provider-cheqd]: suspension: suspensionOptions.issuerDid is required')
1926
+
1927
+ // validate suspension options - case: suspensionOptions.statusListName
1928
+ if (!args.suspensionOptions.statusListName) throw new Error('[did-provider-cheqd]: suspension: suspensionOptions.statusListName is required')
1929
+
1930
+ // validate suspension options - case: suspensionOptions.statusListIndex
1931
+ if (!args.suspensionOptions.statusListIndex) throw new Error('[did-provider-cheqd]: suspension: suspensionOptions.statusListIndex is required')
1932
+
1933
+ // construct status list credential
1934
+ const statusListCredential = `${resolverUrl}${args.suspensionOptions.issuerDid}?resourceName=${args.suspensionOptions.statusListName}&resourceType=StatusList2021Suspension`
1935
+
1936
+ // construct credential status
1937
+ args.credential = {
1938
+ '@context': [],
1939
+ issuer: args.suspensionOptions.issuerDid,
1940
+ credentialSubject: {},
1941
+ credentialStatus: {
1942
+ id: `${statusListCredential}#${args.suspensionOptions.statusListIndex}`,
1943
+ type: 'StatusList2021Entry',
1944
+ statusPurpose: 'suspension',
1945
+ statusListIndex: `${args.suspensionOptions.statusListIndex}`,
1946
+ },
1947
+ issuanceDate: '',
1948
+ proof: {}
1949
+ }
1950
+ }
1951
+
1952
+ // validate args - case: credential
1953
+ if (!args.credential) throw new Error('[did-provider-cheqd]: suspension: credential is required')
1954
+
1955
+ // if jwt credential, decode it
1956
+ const credential = typeof args.credential === 'string' ? await Cheqd.decodeCredentialJWT(args.credential) : args.credential
1631
1957
 
1632
- private async UnsuspendCredentialWithStatusList2021(args: ICheqdUnsuspendCredentialWithStatusList2021Args, context: IContext): Promise<UnsuspensionResult> {
1633
1958
  // validate status purpose
1634
- if (args.credential.credentialStatus?.statusPurpose !== 'suspension') {
1635
- throw new Error(`[did-provider-cheqd]: suspension: Unsupported status purpose: ${args.credential.credentialStatus?.statusPurpose}`)
1959
+ if (credential.credentialStatus?.statusPurpose !== 'suspension') {
1960
+ throw new Error(`[did-provider-cheqd]: suspension: Unsupported status purpose: ${credential.credentialStatus?.statusPurpose}`)
1636
1961
  }
1637
1962
 
1638
1963
  // validate args in pairs - case: statusListFile and statusList
@@ -1656,81 +1981,355 @@ export class Cheqd implements IAgentPlugin {
1656
1981
  }
1657
1982
 
1658
1983
  // suspend credential
1659
- return await Cheqd.unsuspendCredential(args.credential, {
1984
+ return await Cheqd.suspendCredential(credential, {
1660
1985
  ...args.options,
1661
1986
  topArgs: args,
1662
1987
  publishOptions: {
1663
1988
  context,
1989
+ statusListEncoding: args?.options?.statusListEncoding,
1990
+ statusListValidUntil: args?.options?.statusListValidUntil,
1664
1991
  resourceId: args?.options?.resourceId,
1665
1992
  resourceVersion: args?.options?.resourceVersion,
1993
+ resourceAlsoKnownAs: args?.options?.alsoKnownAs,
1666
1994
  signInputs: args?.options?.signInputs,
1667
1995
  fee: args?.options?.fee
1668
1996
  }
1669
1997
  })
1670
1998
  }
1671
1999
 
1672
- private async UnsuspendBulkCredentialsWithStatusList2021(args: ICheqdUnsuspendBulkCredentialsWithStatusList2021Args, context: IContext): Promise<UnsuspensionResult[]> {
1673
- // TODO: implement
1674
- throw new Error('[did-provider-cheqd]: suspension: bulk unsuspension is not implemented yet')
1675
- }
1676
-
1677
- private async TransactVerifierPaysIssuer(args: ICheqdTransactVerifierPaysIssuerArgs, context: IContext): Promise<TransactionResult> {
1678
- try {
1679
- // delegate to provider
1680
- const transactionResult = await this.didProvider.transactSendTokens({
1681
- recipientAddress: args.recipientAddress,
1682
- amount: args.amount,
1683
- memoNonce: args.memoNonce,
1684
- txBytes: args.txBytes,
1685
- })
2000
+ private async SuspendBulkCredentialsWithStatusList2021(args: ICheqdSuspendBulkCredentialsWithStatusList2021Args, context: IContext): Promise<BulkSuspensionResult> {
2001
+ // verify credential, if provided and suspension options are not
2002
+ if (args?.credentials && !args?.suspensionOptions) {
2003
+
2004
+ const verificationResult = await Promise.all(args.credentials.map(async (credential) => {
2005
+ return await context.agent.verifyCredential({
2006
+ credential,
2007
+ policies: {
2008
+ credentialStatus: false
2009
+ }
2010
+ } satisfies IVerifyCredentialArgs)
2011
+ }))
1686
2012
 
1687
- // return transaction result
1688
- return {
1689
- successful: !transactionResult.code,
1690
- transactionHash: transactionResult.transactionHash,
1691
- events: transactionResult.events,
1692
- rawLog: transactionResult.rawLog,
1693
- txResponse: args?.returnTxResponse ? transactionResult : undefined
1694
- } satisfies TransactionResult
1695
- } catch (error) {
1696
- // return error
1697
- return {
1698
- successful: false,
1699
- error: error as IError
1700
- } satisfies TransactionResult
2013
+ // early return if verification failed for any credential
2014
+ if (verificationResult.some(result => !result.verified)) {
2015
+ // define verified
2016
+ return { suspended: Array(args.credentials.length).fill(false), error: verificationResult.find(result => !result.verified)!.error || { message: 'verification: could not verify credential' } }
2017
+ }
1701
2018
  }
1702
- }
1703
2019
 
1704
- private async ObserveVerifierPaysIssuer(args: ICheqdObserveVerifierPaysIssuerArgs, context: IContext): Promise<ObservationResult> {
1705
- // verify with raw unified access control conditions, if any
1706
- if (args?.unifiedAccessControlCondition) {
1707
- try {
1708
- // define network
1709
- const network = (function() {
1710
- switch (args.unifiedAccessControlCondition.chain) {
1711
- case LitCompatibleCosmosChains.cheqdMainnet:
1712
- return CheqdNetwork.Mainnet
1713
- case LitCompatibleCosmosChains.cheqdTestnet:
1714
- return CheqdNetwork.Testnet
1715
- default:
1716
- throw new Error(`[did-provider-cheqd]: observe: Unsupported chain: ${args.unifiedAccessControlCondition.chain}`)
1717
- }
1718
- }())
2020
+ // if suspension options are provided, give precedence
2021
+ if (args?.suspensionOptions) {
2022
+ // validate suspension options - case: suspensionOptions.issuerDid
2023
+ if (!args.suspensionOptions.issuerDid) throw new Error('[did-provider-cheqd]: suspension: suspensionOptions.issuerDid is required')
1719
2024
 
1720
- // construct url
1721
- const url = `${DefaultRESTUrls[network]}${args.unifiedAccessControlCondition.path}`
2025
+ // validate suspension options - case: suspensionOptions.statusListName
2026
+ if (!args.suspensionOptions.statusListName) throw new Error('[did-provider-cheqd]: suspension: suspensionOptions.statusListName is required')
1722
2027
 
1723
- // fetch relevant txs
1724
- const txs = await (await fetch(url)).json() as ShallowTypedTxsResponse
2028
+ // validate suspension options - case: suspensionOptions.statusListIndices
2029
+ if (!args.suspensionOptions.statusListIndices || !args.suspensionOptions.statusListIndices.length || args.suspensionOptions.statusListIndices.length === 0 || !args.suspensionOptions.statusListIndices.every(index => !isNaN(+index))) throw new Error('[did-provider-cheqd]: suspension: suspensionOptions.statusListIndex is required and must be an array of indices')
1725
2030
 
1726
- // skim through txs for relevant events, in which case memoNonce is present and strict equals to the one provided
1727
- const meetsConditionTxIndex = txs?.txs?.findIndex(tx => unescapeUnicode(tx.body.memo) === unescapeUnicode(args.unifiedAccessControlCondition!.returnValueTest.value))
2031
+ // construct status list credential
2032
+ const statusListCredential = `${resolverUrl}${args.suspensionOptions.issuerDid}?resourceName=${args.suspensionOptions.statusListName}&resourceType=StatusList2021Suspension`
1728
2033
 
1729
- // define meetsCondition
1730
- const meetsCondition = (typeof meetsConditionTxIndex !== 'undefined' && meetsConditionTxIndex !== -1)
2034
+ // construct credential status
2035
+ args.credentials = args.suspensionOptions.statusListIndices.map(index => ({
2036
+ '@context': [],
2037
+ issuer: args.suspensionOptions!.issuerDid,
2038
+ credentialSubject: {},
2039
+ credentialStatus: {
2040
+ id: `${statusListCredential}#${index}`,
2041
+ type: 'StatusList2021Entry',
2042
+ statusPurpose: 'suspension',
2043
+ statusListIndex: `${index}`,
2044
+ },
2045
+ issuanceDate: '',
2046
+ proof: {}
2047
+ }))
2048
+ }
1731
2049
 
1732
- // return observation result
1733
- return {
2050
+ // validate args - case: credentials
2051
+ if (!args.credentials || !args.credentials.length || args.credentials.length === 0) throw new Error('[did-provider-cheqd]: suspension: credentials is required and must be an array of credentials')
2052
+
2053
+ // if jwt credentials, decode them
2054
+ const credentials = await Promise.all(args.credentials.map(async credential => typeof credential === 'string' ? await Cheqd.decodeCredentialJWT(credential) : credential))
2055
+
2056
+ // validate args in pairs - case: statusListFile and statusList
2057
+ if (args.options?.statusListFile && args.options?.statusList) {
2058
+ throw new Error('[did-provider-cheqd]: suspension: statusListFile and statusList are mutually exclusive')
2059
+ }
2060
+
2061
+ // validate args in pairs - case: statusListFile and fetchList
2062
+ if (args.options?.statusListFile && args.options?.fetchList) {
2063
+ throw new Error('[did-provider-cheqd]: suspension: statusListFile and fetchList are mutually exclusive')
2064
+ }
2065
+
2066
+ // validate args in pairs - case: statusList and fetchList
2067
+ if (args.options?.statusList && args.options?.fetchList) {
2068
+ throw new Error('[did-provider-cheqd]: suspension: statusList and fetchList are mutually exclusive')
2069
+ }
2070
+
2071
+ // validate args in pairs - case: publish
2072
+ if (args.options?.publish && !args.fetchList && !(args.options?.statusListFile || args.options?.statusList)) {
2073
+ throw new Error('[did-provider-cheqd]: suspension: publish requires statusListFile or statusList, if fetchList is disabled')
2074
+ }
2075
+
2076
+ // suspend credentials
2077
+ return await Cheqd.suspendCredentials(credentials, {
2078
+ ...args.options,
2079
+ topArgs: args,
2080
+ publishOptions: {
2081
+ context,
2082
+ resourceId: args?.options?.resourceId,
2083
+ resourceVersion: args?.options?.resourceVersion,
2084
+ resourceAlsoKnownAs: args?.options?.alsoKnownAs,
2085
+ signInputs: args?.options?.signInputs,
2086
+ fee: args?.options?.fee
2087
+ }
2088
+ })
2089
+ }
2090
+
2091
+ private async UnsuspendCredentialWithStatusList2021(args: ICheqdUnsuspendCredentialWithStatusList2021Args, context: IContext): Promise<UnsuspensionResult> {
2092
+ // verify credential, if provided and unsuspension options are not
2093
+ if (args?.credential && !args?.unsuspensionOptions) {
2094
+ const verificationResult = await context.agent.verifyCredential({
2095
+ credential: args.credential,
2096
+ policies: {
2097
+ credentialStatus: false
2098
+ }
2099
+ } satisfies IVerifyCredentialArgs)
2100
+
2101
+ // early return if verification failed
2102
+ if (!verificationResult.verified) {
2103
+ return { unsuspended: false, error: verificationResult.error }
2104
+ }
2105
+ }
2106
+
2107
+ // if unsuspension options are provided, give precedence
2108
+ if (args?.unsuspensionOptions) {
2109
+ // validate unsuspension options - case: unsuspensionOptions.issuerDid
2110
+ if (!args.unsuspensionOptions.issuerDid) throw new Error('[did-provider-cheqd]: unsuspension: unsuspensionOptions.issuerDid is required')
2111
+
2112
+ // validate unsuspension options - case: unsuspensionOptions.statusListName
2113
+ if (!args.unsuspensionOptions.statusListName) throw new Error('[did-provider-cheqd]: unsuspension: unsuspensionOptions.statusListName is required')
2114
+
2115
+ // validate unsuspension options - case: unsuspensionOptions.statusListIndex
2116
+ if (!args.unsuspensionOptions.statusListIndex) throw new Error('[did-provider-cheqd]: unsuspension: unsuspensionOptions.statusListIndex is required')
2117
+
2118
+ // construct status list credential
2119
+ const statusListCredential = `${resolverUrl}${args.unsuspensionOptions.issuerDid}?resourceName=${args.unsuspensionOptions.statusListName}&resourceType=StatusList2021Suspension`
2120
+
2121
+ // construct credential status
2122
+ args.credential = {
2123
+ '@context': [],
2124
+ issuer: args.unsuspensionOptions.issuerDid,
2125
+ credentialSubject: {},
2126
+ credentialStatus: {
2127
+ id: `${statusListCredential}#${args.unsuspensionOptions.statusListIndex}`,
2128
+ type: 'StatusList2021Entry',
2129
+ statusPurpose: 'suspension',
2130
+ statusListIndex: `${args.unsuspensionOptions.statusListIndex}`,
2131
+ },
2132
+ issuanceDate: '',
2133
+ proof: {}
2134
+ }
2135
+ }
2136
+
2137
+ // validate args - case: credential
2138
+ if (!args.credential) throw new Error('[did-provider-cheqd]: unsuspension: credential is required')
2139
+
2140
+ // if jwt credential, decode it
2141
+ const credential = typeof args.credential === 'string' ? await Cheqd.decodeCredentialJWT(args.credential) : args.credential
2142
+
2143
+ // validate status purpose
2144
+ if (credential.credentialStatus?.statusPurpose !== 'suspension') {
2145
+ throw new Error(`[did-provider-cheqd]: suspension: Unsupported status purpose: ${credential.credentialStatus?.statusPurpose}`)
2146
+ }
2147
+
2148
+ // validate args in pairs - case: statusListFile and statusList
2149
+ if (args.options?.statusListFile && args.options?.statusList) {
2150
+ throw new Error('[did-provider-cheqd]: suspension: statusListFile and statusList are mutually exclusive')
2151
+ }
2152
+
2153
+ // validate args in pairs - case: statusListFile and fetchList
2154
+ if (args.options?.statusListFile && args.options?.fetchList) {
2155
+ throw new Error('[did-provider-cheqd]: suspension: statusListFile and fetchList are mutually exclusive')
2156
+ }
2157
+
2158
+ // validate args in pairs - case: statusList and fetchList
2159
+ if (args.options?.statusList && args.options?.fetchList) {
2160
+ throw new Error('[did-provider-cheqd]: suspension: statusList and fetchList are mutually exclusive')
2161
+ }
2162
+
2163
+ // validate args in pairs - case: publish
2164
+ if (args.options?.publish && !args.fetchList && !(args.options?.statusListFile || args.options?.statusList)) {
2165
+ throw new Error('[did-provider-cheqd]: suspension: publish requires statusListFile or statusList, if fetchList is disabled')
2166
+ }
2167
+
2168
+ // suspend credential
2169
+ return await Cheqd.unsuspendCredential(credential, {
2170
+ ...args.options,
2171
+ topArgs: args,
2172
+ publishOptions: {
2173
+ context,
2174
+ statusListEncoding: args?.options?.statusListEncoding,
2175
+ statusListValidUntil: args?.options?.statusListValidUntil,
2176
+ resourceId: args?.options?.resourceId,
2177
+ resourceVersion: args?.options?.resourceVersion,
2178
+ resourceAlsoKnownAs: args?.options?.alsoKnownAs,
2179
+ signInputs: args?.options?.signInputs,
2180
+ fee: args?.options?.fee
2181
+ }
2182
+ })
2183
+ }
2184
+
2185
+ private async UnsuspendBulkCredentialsWithStatusList2021(args: ICheqdUnsuspendBulkCredentialsWithStatusList2021Args, context: IContext): Promise<BulkUnsuspensionResult> {
2186
+ // verify credential, if provided and unsuspension options are not
2187
+ if (args?.credentials && !args?.unsuspensionOptions) {
2188
+
2189
+ const verificationResult = await Promise.all(args.credentials.map(async (credential) => {
2190
+ return await context.agent.verifyCredential({
2191
+ credential,
2192
+ policies: {
2193
+ credentialStatus: false
2194
+ }
2195
+ } satisfies IVerifyCredentialArgs)
2196
+ }))
2197
+
2198
+ // early return if verification failed for any credential
2199
+ if (verificationResult.some(result => !result.verified)) {
2200
+ // define verified
2201
+ return { unsuspended: Array(args.credentials.length).fill(false), error: verificationResult.find(result => !result.verified)!.error || { message: 'verification: could not verify credential' } }
2202
+ }
2203
+ }
2204
+
2205
+ // if unsuspension options are provided, give precedence
2206
+ if (args?.unsuspensionOptions) {
2207
+ // validate unsuspension options - case: unsuspensionOptions.issuerDid
2208
+ if (!args.unsuspensionOptions.issuerDid) throw new Error('[did-provider-cheqd]: unsuspension: unsuspensionOptions.issuerDid is required')
2209
+
2210
+ // validate unsuspension options - case: unsuspensionOptions.statusListName
2211
+ if (!args.unsuspensionOptions.statusListName) throw new Error('[did-provider-cheqd]: unsuspension: unsuspensionOptions.statusListName is required')
2212
+
2213
+ // validate unsuspension options - case: unsuspensionOptions.statusListIndices
2214
+ if (!args.unsuspensionOptions.statusListIndices || !args.unsuspensionOptions.statusListIndices.length || args.unsuspensionOptions.statusListIndices.length === 0 || !args.unsuspensionOptions.statusListIndices.every(index => !isNaN(+index))) throw new Error('[did-provider-cheqd]: unsuspension: unsuspensionOptions.statusListIndex is required and must be an array of indices')
2215
+
2216
+ // construct status list credential
2217
+ const statusListCredential = `${resolverUrl}${args.unsuspensionOptions.issuerDid}?resourceName=${args.unsuspensionOptions.statusListName}&resourceType=StatusList2021Suspension`
2218
+
2219
+ // construct credential status
2220
+ args.credentials = args.unsuspensionOptions.statusListIndices.map(index => ({
2221
+ '@context': [],
2222
+ issuer: args.unsuspensionOptions!.issuerDid,
2223
+ credentialSubject: {},
2224
+ credentialStatus: {
2225
+ id: `${statusListCredential}#${index}`,
2226
+ type: 'StatusList2021Entry',
2227
+ statusPurpose: 'suspension',
2228
+ statusListIndex: `${index}`,
2229
+ },
2230
+ issuanceDate: '',
2231
+ proof: {}
2232
+ }))
2233
+ }
2234
+
2235
+ // validate args - case: credentials
2236
+ if (!args.credentials || !args.credentials.length || args.credentials.length === 0) throw new Error('[did-provider-cheqd]: unsuspension: credentials is required and must be an array of credentials')
2237
+
2238
+ // if jwt credentials, decode them
2239
+ const credentials = await Promise.all(args.credentials.map(async credential => typeof credential === 'string' ? await Cheqd.decodeCredentialJWT(credential) : credential))
2240
+
2241
+ // validate args in pairs - case: statusListFile and statusList
2242
+ if (args.options?.statusListFile && args.options?.statusList) {
2243
+ throw new Error('[did-provider-cheqd]: unsuspension: statusListFile and statusList are mutually exclusive')
2244
+ }
2245
+
2246
+ // validate args in pairs - case: statusListFile and fetchList
2247
+ if (args.options?.statusListFile && args.options?.fetchList) {
2248
+ throw new Error('[did-provider-cheqd]: unsuspension: statusListFile and fetchList are mutually exclusive')
2249
+ }
2250
+
2251
+ // validate args in pairs - case: statusList and fetchList
2252
+ if (args.options?.statusList && args.options?.fetchList) {
2253
+ throw new Error('[did-provider-cheqd]: unsuspension: statusList and fetchList are mutually exclusive')
2254
+ }
2255
+
2256
+ // validate args in pairs - case: publish
2257
+ if (args.options?.publish && !args.fetchList && !(args.options?.statusListFile || args.options?.statusList)) {
2258
+ throw new Error('[did-provider-cheqd]: unsuspension: publish requires statusListFile or statusList, if fetchList is disabled')
2259
+ }
2260
+
2261
+ // suspend credentials
2262
+ return await Cheqd.unsuspendCredentials(credentials, {
2263
+ ...args.options,
2264
+ topArgs: args,
2265
+ publishOptions: {
2266
+ context,
2267
+ resourceId: args?.options?.resourceId,
2268
+ resourceVersion: args?.options?.resourceVersion,
2269
+ resourceAlsoKnownAs: args?.options?.alsoKnownAs,
2270
+ signInputs: args?.options?.signInputs,
2271
+ fee: args?.options?.fee
2272
+ }
2273
+ })
2274
+ }
2275
+
2276
+ private async TransactVerifierPaysIssuer(args: ICheqdTransactVerifierPaysIssuerArgs, context: IContext): Promise<TransactionResult> {
2277
+ try {
2278
+ // delegate to provider
2279
+ const transactionResult = await this.didProvider.transactSendTokens({
2280
+ recipientAddress: args.recipientAddress,
2281
+ amount: args.amount,
2282
+ memoNonce: args.memoNonce,
2283
+ txBytes: args.txBytes,
2284
+ })
2285
+
2286
+ // return transaction result
2287
+ return {
2288
+ successful: !transactionResult.code,
2289
+ transactionHash: transactionResult.transactionHash,
2290
+ events: transactionResult.events,
2291
+ rawLog: transactionResult.rawLog,
2292
+ txResponse: args?.returnTxResponse ? transactionResult : undefined
2293
+ } satisfies TransactionResult
2294
+ } catch (error) {
2295
+ // return error
2296
+ return {
2297
+ successful: false,
2298
+ error: error as IError
2299
+ } satisfies TransactionResult
2300
+ }
2301
+ }
2302
+
2303
+ private async ObserveVerifierPaysIssuer(args: ICheqdObserveVerifierPaysIssuerArgs, context: IContext): Promise<ObservationResult> {
2304
+ // verify with raw unified access control conditions, if any
2305
+ if (args?.unifiedAccessControlCondition) {
2306
+ try {
2307
+ // define network
2308
+ const network = (function() {
2309
+ switch (args.unifiedAccessControlCondition.chain) {
2310
+ case LitCompatibleCosmosChains.cheqdMainnet:
2311
+ return CheqdNetwork.Mainnet
2312
+ case LitCompatibleCosmosChains.cheqdTestnet:
2313
+ return CheqdNetwork.Testnet
2314
+ default:
2315
+ throw new Error(`[did-provider-cheqd]: observe: Unsupported chain: ${args.unifiedAccessControlCondition.chain}`)
2316
+ }
2317
+ }())
2318
+
2319
+ // construct url
2320
+ const url = `${DefaultRESTUrls[network]}${args.unifiedAccessControlCondition.path}`
2321
+
2322
+ // fetch relevant txs
2323
+ const txs = await (await fetch(url)).json() as ShallowTypedTxsResponse
2324
+
2325
+ // skim through txs for relevant events, in which case memoNonce is present and strict equals to the one provided
2326
+ const meetsConditionTxIndex = txs?.txs?.findIndex(tx => unescapeUnicode(tx.body.memo) === unescapeUnicode(args.unifiedAccessControlCondition!.returnValueTest.value))
2327
+
2328
+ // define meetsCondition
2329
+ const meetsCondition = (typeof meetsConditionTxIndex !== 'undefined' && meetsConditionTxIndex !== -1)
2330
+
2331
+ // return observation result
2332
+ return {
1734
2333
  subscribed: true,
1735
2334
  meetsCondition: meetsCondition,
1736
2335
  transactionHash: meetsCondition ? txs!.tx_responses[meetsConditionTxIndex].txhash : undefined,
@@ -1748,113 +2347,739 @@ export class Cheqd implements IAgentPlugin {
1748
2347
  }
1749
2348
  }
1750
2349
 
1751
- // validate access control conditions components - case: senderAddress
1752
- if (!args.senderAddress) {
1753
- throw new Error('[did-provider-cheqd]: observation: senderAddress is required')
1754
- }
2350
+ // validate access control conditions components - case: senderAddress
2351
+ if (!args.senderAddress) {
2352
+ throw new Error('[did-provider-cheqd]: observation: senderAddress is required')
2353
+ }
2354
+
2355
+ // validate access control conditions components - case: recipientAddress
2356
+ if (!args.recipientAddress) {
2357
+ throw new Error('[did-provider-cheqd]: observation: recipientAddress is required')
2358
+ }
2359
+
2360
+ // validate access control conditions components - case: amount
2361
+ if (!args.amount || !args.amount.amount || !args.amount.denom || args.amount.denom !== 'ncheq') {
2362
+ throw new Error('[did-provider-cheqd]: observation: amount is required, and must be an object with amount and denom valid string properties, amongst which denom must be `ncheq`')
2363
+ }
2364
+
2365
+ // validate access control conditions components - case: memoNonce
2366
+ if (!args.memoNonce) {
2367
+ throw new Error('[did-provider-cheqd]: observation: memoNonce is required')
2368
+ }
2369
+
2370
+ // validate access control conditions components - case: network
2371
+ if (!args.network) {
2372
+ throw new Error('[did-provider-cheqd]: observation: network is required')
2373
+ }
2374
+
2375
+ try {
2376
+ // otherwise, construct url, as per components
2377
+ const url = `${DefaultRESTUrls[args.network]}/cosmos/tx/v1beta1/txs?events=transfer.recipient='${args.recipientAddress}'&events=transfer.sender='${args.senderAddress}'&events=transfer.amount='${args.amount.amount}${args.amount.denom}'`
2378
+
2379
+ // fetch relevant txs
2380
+ const txs = await (await fetch(url)).json() as ShallowTypedTxsResponse
2381
+
2382
+ // skim through txs for relevant events, in which case memoNonce is present and strict equals to the one provided
2383
+ const meetsConditionTxIndex = txs?.txs?.findIndex(tx => unescapeUnicode(tx.body.memo) === unescapeUnicode(args.memoNonce))
2384
+
2385
+ // define meetsCondition
2386
+ const meetsCondition = (typeof meetsConditionTxIndex !== 'undefined' && meetsConditionTxIndex !== -1)
2387
+
2388
+ // return observation result
2389
+ return {
2390
+ subscribed: true,
2391
+ meetsCondition: meetsCondition,
2392
+ transactionHash: meetsCondition ? txs!.tx_responses[meetsConditionTxIndex].txhash : undefined,
2393
+ events: meetsCondition ? txs!.tx_responses[meetsConditionTxIndex].events : undefined,
2394
+ rawLog: meetsCondition ? txs!.tx_responses[meetsConditionTxIndex].raw_log : undefined,
2395
+ txResponse: meetsCondition ? (args?.returnTxResponse ? txs!.tx_responses[meetsConditionTxIndex] : undefined) : undefined
2396
+ } satisfies ObservationResult
2397
+ } catch (error) {
2398
+ // return error
2399
+ return {
2400
+ subscribed: false,
2401
+ meetsCondition: false,
2402
+ error: error as IError
2403
+ } satisfies ObservationResult
2404
+ }
2405
+ }
2406
+
2407
+ static async revokeCredential(credential: VerifiableCredential, options?: ICheqdStatusList2021Options): Promise<RevocationResult> {
2408
+ try {
2409
+ // validate status purpose
2410
+ if (credential?.credentialStatus?.statusPurpose !== 'revocation') throw new Error('[did-provider-cheqd]: revocation: Invalid status purpose')
2411
+
2412
+ // fetch status list 2021
2413
+ const publishedList = (await Cheqd.fetchStatusList2021(credential)) as StatusList2021Revocation
2414
+
2415
+ // early return, if encrypted and no decryption key provided
2416
+ if (publishedList.metadata.encrypted && !options?.topArgs?.symmetricKey) throw new Error('[did-provider-cheqd]: revocation: symmetricKey is required, if status list 2021 is encrypted')
2417
+
2418
+ // fetch status list 2021 inscribed in credential
2419
+ const statusList2021 = options?.topArgs?.fetchList
2420
+ ? (await async function () {
2421
+ // if not encrypted, return bitstring
2422
+ if (!publishedList.metadata.encrypted)
2423
+ return publishedList.metadata.encoding === 'base64url'
2424
+ ? publishedList.StatusList2021.encodedList
2425
+ : toString(fromString(publishedList.StatusList2021.encodedList, publishedList.metadata.encoding as DefaultStatusList2021Encoding), 'base64url')
2426
+
2427
+ // otherwise, decrypt and return bitstring
2428
+ const scopedRawBlob = await toBlob(fromString(publishedList.StatusList2021.encodedList, publishedList.metadata.encoding as DefaultStatusList2021Encoding))
2429
+
2430
+ // decrypt
2431
+ return await LitProtocol.decryptDirect(scopedRawBlob, fromString(options?.topArgs?.symmetricKey, 'hex'))
2432
+ }())
2433
+ : (await async function () {
2434
+ // transcode to base64url, if needed
2435
+ const publishedListTranscoded = publishedList.metadata.encoding === 'base64url'
2436
+ ? publishedList.StatusList2021.encodedList
2437
+ : toString(fromString(publishedList.StatusList2021.encodedList, publishedList.metadata.encoding as DefaultStatusList2021Encoding), 'base64url')
2438
+
2439
+ // if status list 2021 is not fetched, read from file
2440
+ if (options?.statusListFile) {
2441
+ // if not encrypted, return bitstring
2442
+ if (!publishedList.metadata.encrypted) {
2443
+ // construct encoded status list
2444
+ const encoded = new StatusList({ buffer: await Cheqd.getFile(options.statusListFile) }).encode() as Bitstring
2445
+
2446
+ // validate against published list
2447
+ if (encoded !== publishedListTranscoded) throw new Error('[did-provider-cheqd]: revocation: statusListFile does not match published status list 2021')
2448
+
2449
+ // return encoded
2450
+ return encoded
2451
+ }
2452
+
2453
+ // otherwise, decrypt and return bitstring
2454
+ const scopedRawBlob = await toBlob(await Cheqd.getFile(options.statusListFile))
2455
+
2456
+ // decrypt
2457
+ const decrypted = await LitProtocol.decryptDirect(scopedRawBlob, fromString(options?.topArgs?.symmetricKey, 'hex'))
2458
+
2459
+ // validate against published list
2460
+ if (decrypted !== publishedListTranscoded) throw new Error('[did-provider-cheqd]: revocation: statusListFile does not match published status list 2021')
2461
+
2462
+ // return decrypted
2463
+ return decrypted
2464
+ }
2465
+
2466
+ if (!options?.statusListInlineBitstring) throw new Error('[did-provider-cheqd]: revocation: statusListInlineBitstring is required, if statusListFile is not provided')
2467
+
2468
+ // validate against published list
2469
+ if (options?.statusListInlineBitstring !== publishedListTranscoded) throw new Error('[did-provider-cheqd]: revocation: statusListInlineBitstring does not match published status list 2021')
2470
+
2471
+ // otherwise, read from inline bitstring
2472
+ return options?.statusListInlineBitstring
2473
+ }())
2474
+
2475
+ // parse status list 2021
2476
+ const statusList = await StatusList.decode({ encodedList: statusList2021 })
2477
+
2478
+ // early exit, if credential is already revoked
2479
+ if (statusList.getStatus(Number(credential.credentialStatus.statusListIndex))) return { revoked: false }
2480
+
2481
+ // update revocation status
2482
+ statusList.setStatus(Number(credential.credentialStatus.statusListIndex), true)
2483
+
2484
+ // set in-memory status list ref
2485
+ const bitstring = await statusList.encode() as Bitstring
2486
+
2487
+ // cast top-level args
2488
+ const topArgs = options?.topArgs as ICheqdRevokeCredentialWithStatusList2021Args
2489
+
2490
+ // write status list 2021 to file, if provided
2491
+ if (topArgs?.writeToFile) {
2492
+ await Cheqd.writeFile(fromString(bitstring, 'base64url'), options?.statusListFile)
2493
+ }
2494
+
2495
+ // publish status list 2021, if provided
2496
+ const published = topArgs?.publish
2497
+ ? (await async function () {
2498
+ // fetch status list 2021 metadata
2499
+ const statusListMetadata = await Cheqd.fetchStatusList2021Metadata(credential)
2500
+
2501
+ // publish status list 2021 as new version
2502
+ const scoped = topArgs.publishEncrypted
2503
+ ? (await async function () {
2504
+ // instantiate dkg-threshold client, in which case lit-protocol is used
2505
+ const lit = await LitProtocol.create({
2506
+ chain: options?.topArgs?.bootstrapOptions?.chain,
2507
+ litNetwork: options?.topArgs?.bootstrapOptions?.litNetwork
2508
+ })
2509
+
2510
+ // encrypt
2511
+ const { encryptedString, encryptedSymmetricKey, symmetricKey } = await lit.encrypt(bitstring, options?.topArgs?.encryptionOptions?.unifiedAccessControlConditions, true)
2512
+
2513
+ // return tuple of publish result and encryption relevant metadata
2514
+ return [
2515
+ await Cheqd.publishStatusList2021(new Uint8Array(await encryptedString.arrayBuffer()), statusListMetadata, options?.publishOptions),
2516
+ { encryptedString, encryptedSymmetricKey, symmetricKey: toString(symmetricKey!, 'hex') }
2517
+ ]
2518
+ }())
2519
+ : (await async function () {
2520
+ // validate encoding, if provided
2521
+ if (options?.publishOptions?.statusListEncoding && !Object.values(DefaultStatusList2021Encodings).includes(options?.publishOptions?.statusListEncoding)) {
2522
+ throw new Error('[did-provider-cheqd]: revocation: Invalid status list encoding')
2523
+ }
2524
+
2525
+ // validate validUntil, if provided
2526
+ if (options?.publishOptions?.statusListValidUntil) {
2527
+ // validate validUntil as string
2528
+ if (typeof options?.publishOptions?.statusListValidUntil !== 'string') throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be string)')
2529
+
2530
+ // validate validUntil as date
2531
+ if (isNaN(Date.parse(options?.publishOptions?.statusListValidUntil))) throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be date)')
2532
+
2533
+ // validate validUntil as future date
2534
+ if (new Date(options?.publishOptions?.statusListValidUntil) < new Date()) throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be future date)')
2535
+
2536
+ // validate validUntil towards validFrom
2537
+ if (new Date(options?.publishOptions?.statusListValidUntil) <= new Date(publishedList.StatusList2021.validFrom)) throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be after validFrom)')
2538
+ }
2539
+
2540
+ // define status list content
2541
+ const content = {
2542
+ StatusList2021: {
2543
+ type: publishedList.StatusList2021.type,
2544
+ encodedList: publishedList.metadata.encoding === 'base64url' ? bitstring : toString(fromString(bitstring, 'base64url'), options!.publishOptions.statusListEncoding as DefaultStatusList2021Encoding),
2545
+ validFrom: publishedList.StatusList2021.validFrom,
2546
+ validUntil: options?.publishOptions?.statusListValidUntil || publishedList.StatusList2021.validUntil
2547
+ },
2548
+ metadata: {
2549
+ encoding: (options?.publishOptions?.statusListEncoding as DefaultStatusList2021Encoding | undefined) || publishedList.metadata.encoding,
2550
+ encrypted: false,
2551
+ }
2552
+ } satisfies StatusList2021Revocation
2553
+
2554
+ // return tuple of publish result and encryption relevant metadata
2555
+ return [
2556
+ await Cheqd.publishStatusList2021(fromString(JSON.stringify(content), 'utf-8'), statusListMetadata, options?.publishOptions),
2557
+ undefined
2558
+ ]
2559
+ }())
2560
+
2561
+ // early exit, if publish failed
2562
+ if (!scoped[0]) throw new Error('[did-provider-cheqd]: revocation: Failed to publish status list 2021')
2563
+
2564
+ // return publish result
2565
+ return scoped
2566
+ }())
2567
+ : undefined
2568
+
2569
+ return {
2570
+ revoked: true,
2571
+ published: topArgs?.publish ? true : undefined,
2572
+ statusList: topArgs?.returnUpdatedStatusList ? await Cheqd.fetchStatusList2021(credential) as StatusList2021Revocation : undefined,
2573
+ encryptedStatusList: topArgs?.returnUpdatedEncryptedStatusList ? await blobToHexString((published?.[1] as { encryptedString: Blob })?.encryptedString) : undefined,
2574
+ encryptedSymmetricKey: topArgs?.returnEncryptedSymmetricKey ? (published?.[1] as { encryptedSymmetricKey: string })?.encryptedSymmetricKey : undefined,
2575
+ symmetricKey: topArgs?.returnSymmetricKey ? (published?.[1] as { symmetricKey: string })?.symmetricKey : undefined,
2576
+ resourceMetadata: topArgs?.returnStatusListMetadata ? await Cheqd.fetchStatusList2021Metadata(credential) : undefined
2577
+ } satisfies RevocationResult
2578
+ } catch (error) {
2579
+ // silent fail + early exit
2580
+ console.error(error)
2581
+
2582
+ return { revoked: false, error: error as IError } satisfies RevocationResult
2583
+ }
2584
+ }
2585
+
2586
+ static async revokeCredentials(credentials: VerifiableCredential[], options?: ICheqdStatusList2021Options): Promise<BulkRevocationResult> {
2587
+ // validate credentials - case: empty
2588
+ if (!credentials.length || credentials.length === 0) throw new Error('[did-provider-cheqd]: revocation: No credentials provided')
2589
+
2590
+ // validate credentials - case: consistent issuer
2591
+ if (credentials.map((credential) => {
2592
+ return ((credential.issuer as { id: string }).id)
2593
+ ? (credential.issuer as { id: string }).id
2594
+ : credential.issuer as string
2595
+ }).filter((value, _, self) => value && value !== self[0]).length > 0) throw new Error('[did-provider-cheqd]: revocation: Credentials must be issued by the same issuer')
2596
+
2597
+ // validate credentials - case: status list index
2598
+ if (credentials.map((credential) => credential.credentialStatus!.statusListIndex).filter((value, index, self) => self.indexOf(value) !== index).length > 0) throw new Error('[did-provider-cheqd]: revocation: Credentials must have unique status list index')
2599
+
2600
+ // validate credentials - case: status purpose
2601
+ if (!credentials.every((credential) => credential.credentialStatus?.statusPurpose === 'revocation')) throw new Error('[did-provider-cheqd]: revocation: Invalid status purpose')
2602
+
2603
+ // validate credentials - case: status list id
2604
+ const remote = credentials[0].credentialStatus?.id
2605
+ ? (credentials[0].credentialStatus as { id: string }).id.split('#')[0]
2606
+ : (function(){
2607
+ throw new Error('[did-provider-cheqd]: revocation: Invalid status list id')
2608
+ }())
2609
+
2610
+ // validate credentials - case: status list id format
2611
+ if (!RemoteListPattern.test(remote)) throw new Error('[did-provider-cheqd]: revocation: Invalid status list id format: expected: https://<optional_subdomain>.<sld>.<tld>/1.0/identifiers/<did:cheqd:<namespace>:<method_specific_id>>?resourceName=<resource_name>&resourceType=<resource_type>')
2612
+
2613
+ if (!credentials.every((credential) => {
2614
+ return (credential.credentialStatus as { id: string }).id.split('#')[0] === remote
2615
+ })) throw new Error('[did-provider-cheqd]: revocation: Credentials must belong to the same status list')
2616
+
2617
+ // validate credentials - case: status list type
2618
+ if (!credentials.every((credential) => credential.credentialStatus?.type === 'StatusList2021Entry')) throw new Error('[did-provider-cheqd]: revocation: Invalid status list type')
2619
+
2620
+ try {
2621
+ // fetch status list 2021
2622
+ const publishedList = (await Cheqd.fetchStatusList2021(credentials[0])) as StatusList2021Revocation
2623
+
2624
+ // early return, if encrypted and no decryption key provided
2625
+ if (publishedList.metadata.encrypted && !options?.topArgs?.symmetricKey) throw new Error('[did-provider-cheqd]: revocation: symmetricKey is required, if status list 2021 is encrypted')
2626
+
2627
+ // fetch status list 2021 inscribed in credential
2628
+ const statusList2021 = options?.topArgs?.fetchList
2629
+ ? (await async function () {
2630
+ // if not encrypted, return bitstring
2631
+ if (!publishedList.metadata.encrypted)
2632
+ return publishedList.metadata.encoding === 'base64url'
2633
+ ? publishedList.StatusList2021.encodedList
2634
+ : toString(fromString(publishedList.StatusList2021.encodedList, publishedList.metadata.encoding as DefaultStatusList2021Encoding), 'base64url')
2635
+
2636
+ // otherwise, decrypt and return bitstring
2637
+ const scopedRawBlob = await toBlob(fromString(publishedList.StatusList2021.encodedList, publishedList.metadata.encoding as DefaultStatusList2021Encoding))
2638
+
2639
+ // decrypt
2640
+ return await LitProtocol.decryptDirect(scopedRawBlob, fromString(options?.topArgs?.symmetricKey, 'hex'))
2641
+ }())
2642
+ : (await async function () {
2643
+ // transcode to base64url, if needed
2644
+ const publishedListTranscoded = publishedList.metadata.encoding === 'base64url'
2645
+ ? publishedList.StatusList2021.encodedList
2646
+ : toString(fromString(publishedList.StatusList2021.encodedList, publishedList.metadata.encoding as DefaultStatusList2021Encoding), 'base64url')
2647
+
2648
+ // if status list 2021 is not fetched, read from file
2649
+ if (options?.statusListFile) {
2650
+ // if not encrypted, return bitstring
2651
+ if (!publishedList.metadata.encrypted) {
2652
+ // construct encoded status list
2653
+ const encoded = new StatusList({ buffer: await Cheqd.getFile(options.statusListFile) }).encode() as Bitstring
2654
+
2655
+ // validate against published list
2656
+ if (encoded !== publishedListTranscoded) throw new Error('[did-provider-cheqd]: revocation: statusListFile does not match published status list 2021')
2657
+
2658
+ // return encoded
2659
+ return encoded
2660
+ }
2661
+
2662
+ // otherwise, decrypt and return bitstring
2663
+ const scopedRawBlob = await toBlob(await Cheqd.getFile(options.statusListFile))
2664
+
2665
+ // decrypt
2666
+ const decrypted = await LitProtocol.decryptDirect(scopedRawBlob, fromString(options?.topArgs?.symmetricKey, 'hex'))
2667
+
2668
+ // validate against published list
2669
+ if (decrypted !== publishedListTranscoded) throw new Error('[did-provider-cheqd]: revocation: statusListFile does not match published status list 2021')
2670
+
2671
+ // return decrypted
2672
+ return decrypted
2673
+ }
2674
+
2675
+ if (!options?.statusListInlineBitstring) throw new Error('[did-provider-cheqd]: revocation: statusListInlineBitstring is required, if statusListFile is not provided')
2676
+
2677
+ // validate against published list
2678
+ if (options?.statusListInlineBitstring !== publishedListTranscoded) throw new Error('[did-provider-cheqd]: revocation: statusListInlineBitstring does not match published status list 2021')
2679
+
2680
+ // otherwise, read from inline bitstring
2681
+ return options?.statusListInlineBitstring
2682
+ }())
2683
+
2684
+ // parse status list 2021
2685
+ const statusList = await StatusList.decode({ encodedList: statusList2021 })
2686
+
2687
+ // initiate bulk revocation
2688
+ const revoked = await Promise.allSettled(credentials.map((credential) => {
2689
+ return async function () {
2690
+ // early return, if no credential status
2691
+ if (!credential.credentialStatus) return { revoked: false }
2692
+
2693
+ // early exit, if credential is already revoked
2694
+ if (statusList.getStatus(Number(credential.credentialStatus.statusListIndex))) return { revoked: false }
2695
+
2696
+ // update revocation status
2697
+ statusList.setStatus(Number(credential.credentialStatus.statusListIndex), true)
2698
+
2699
+ // return revocation status
2700
+ return { revoked: true }
2701
+ }()
2702
+ })) satisfies PromiseSettledResult<RevocationResult>[]
2703
+
2704
+ // revert bulk ops, if some failed
2705
+ if (revoked.some((result) => result.status === 'fulfilled' && !result.value.revoked ))
2706
+ throw new Error(`[did-provider-cheqd]: revocation: Bulk revocation failed: already revoked credentials in revocation bundle: raw log: ${JSON.stringify(revoked.map((result) => ({ revoked: result.status === 'fulfilled' ? result.value.revoked : false })))}`)
2707
+
2708
+ // set in-memory status list ref
2709
+ const bitstring = await statusList.encode() as Bitstring
2710
+
2711
+ // cast top-level args
2712
+ const topArgs = options?.topArgs as ICheqdRevokeCredentialWithStatusList2021Args
2713
+
2714
+ // write status list 2021 to file, if provided
2715
+ if (topArgs?.writeToFile) {
2716
+ await Cheqd.writeFile(fromString(bitstring, 'base64url'), options?.statusListFile)
2717
+ }
2718
+
2719
+ // publish status list 2021, if provided
2720
+ const published = topArgs?.publish
2721
+ ? (await async function () {
2722
+ // fetch status list 2021 metadata
2723
+ const statusListMetadata = await Cheqd.fetchStatusList2021Metadata(credentials[0])
2724
+
2725
+ // publish status list 2021 as new version
2726
+ const scoped = topArgs.publishEncrypted
2727
+ ? (await async function () {
2728
+ // instantiate dkg-threshold client, in which case lit-protocol is used
2729
+ const lit = await LitProtocol.create({
2730
+ chain: options?.topArgs?.bootstrapOptions?.chain,
2731
+ litNetwork: options?.topArgs?.bootstrapOptions?.litNetwork
2732
+ })
2733
+
2734
+ // encrypt
2735
+ const { encryptedString, encryptedSymmetricKey, symmetricKey } = await lit.encrypt(bitstring, options?.topArgs?.encryptionOptions?.unifiedAccessControlConditions, true)
2736
+
2737
+ // return tuple of publish result and encryption relevant metadata
2738
+ return [
2739
+ await Cheqd.publishStatusList2021(new Uint8Array(await encryptedString.arrayBuffer()), statusListMetadata, options?.publishOptions),
2740
+ { encryptedString, encryptedSymmetricKey, symmetricKey: toString(symmetricKey!, 'hex') }
2741
+ ]
2742
+ }())
2743
+ : (await async function () {
2744
+ // validate encoding, if provided
2745
+ if (options?.publishOptions?.statusListEncoding && !Object.values(DefaultStatusList2021Encodings).includes(options?.publishOptions?.statusListEncoding)) {
2746
+ throw new Error('[did-provider-cheqd]: revocation: Invalid status list encoding')
2747
+ }
2748
+
2749
+ // validate validUntil, if provided
2750
+ if (options?.publishOptions?.statusListValidUntil) {
2751
+ // validate validUntil as string
2752
+ if (typeof options?.publishOptions?.statusListValidUntil !== 'string') throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be string)')
2753
+
2754
+ // validate validUntil as date
2755
+ if (isNaN(Date.parse(options?.publishOptions?.statusListValidUntil))) throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be date)')
2756
+
2757
+ // validate validUntil as future date
2758
+ if (new Date(options?.publishOptions?.statusListValidUntil) < new Date()) throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be future date)')
2759
+
2760
+ // validate validUntil towards validFrom
2761
+ if (new Date(options?.publishOptions?.statusListValidUntil) <= new Date(publishedList.StatusList2021.validFrom)) throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be after validFrom)')
2762
+ }
2763
+
2764
+ // define status list content
2765
+ const content = {
2766
+ StatusList2021: {
2767
+ type: publishedList.StatusList2021.type,
2768
+ encodedList: publishedList.metadata.encoding === 'base64url' ? bitstring : toString(fromString(bitstring, 'base64url'), options!.publishOptions.statusListEncoding as DefaultStatusList2021Encoding),
2769
+ validFrom: publishedList.StatusList2021.validFrom,
2770
+ validUntil: options?.publishOptions?.statusListValidUntil || publishedList.StatusList2021.validUntil
2771
+ },
2772
+ metadata: {
2773
+ encoding: (options?.publishOptions?.statusListEncoding as DefaultStatusList2021Encoding | undefined) || publishedList.metadata.encoding,
2774
+ encrypted: false,
2775
+ }
2776
+ } satisfies StatusList2021Revocation
2777
+
2778
+ // return tuple of publish result and encryption relevant metadata
2779
+ return [
2780
+ await Cheqd.publishStatusList2021(fromString(JSON.stringify(content), 'utf-8'), statusListMetadata, options?.publishOptions),
2781
+ undefined
2782
+ ]
2783
+ }())
2784
+
2785
+ // early exit, if publish failed
2786
+ if (!scoped[0]) throw new Error('[did-provider-cheqd]: revocation: Failed to publish status list 2021')
2787
+
2788
+ // return publish result
2789
+ return scoped
2790
+ }())
2791
+ : undefined
2792
+
2793
+ return {
2794
+ revoked: revoked.map((result) => result.status === 'fulfilled' ? result.value.revoked : false),
2795
+ published: topArgs?.publish ? true : undefined,
2796
+ statusList: topArgs?.returnUpdatedStatusList ? await Cheqd.fetchStatusList2021(credentials[0]) as StatusList2021Revocation : undefined,
2797
+ encryptedStatusList: topArgs?.returnUpdatedEncryptedStatusList ? await blobToHexString((published?.[1] as { encryptedString: Blob })?.encryptedString) : undefined,
2798
+ encryptedSymmetricKey: topArgs?.returnEncryptedSymmetricKey ? (published?.[1] as { encryptedSymmetricKey: string })?.encryptedSymmetricKey : undefined,
2799
+ symmetricKey: topArgs?.returnSymmetricKey ? (published?.[1] as { symmetricKey: string })?.symmetricKey : undefined,
2800
+ resourceMetadata: topArgs?.returnStatusListMetadata ? await Cheqd.fetchStatusList2021Metadata(credentials[0]) : undefined
2801
+ } satisfies BulkRevocationResult
2802
+ } catch (error) {
2803
+ // silent fail + early exit
2804
+ console.error(error)
2805
+
2806
+ return { revoked: [], error: error as IError } satisfies BulkRevocationResult
2807
+ }
2808
+ }
2809
+
2810
+ static async suspendCredential(credential: VerifiableCredential, options?: ICheqdStatusList2021Options): Promise<SuspensionResult> {
2811
+ try {
2812
+ // validate status purpose
2813
+ if (credential?.credentialStatus?.statusPurpose !== 'suspension') throw new Error('[did-provider-cheqd]: suspension: Invalid status purpose')
2814
+
2815
+ // fetch status list 2021
2816
+ const publishedList = (await Cheqd.fetchStatusList2021(credential)) as StatusList2021Suspension
2817
+
2818
+ // early return, if encrypted and no decryption key provided
2819
+ if (publishedList.metadata.encrypted && !options?.topArgs?.symmetricKey) throw new Error('[did-provider-cheqd]: suspension: symmetricKey is required, if status list 2021 is encrypted')
2820
+
2821
+ // fetch status list 2021 inscribed in credential
2822
+ const statusList2021 = options?.topArgs?.fetchList
2823
+ ? (await async function () {
2824
+ // if not encrypted, return bitstring
2825
+ if (!publishedList.metadata.encrypted)
2826
+ return publishedList.metadata.encoding === 'base64url'
2827
+ ? publishedList.StatusList2021.encodedList
2828
+ : toString(fromString(publishedList.StatusList2021.encodedList, publishedList.metadata.encoding as DefaultStatusList2021Encoding), 'base64url')
2829
+
2830
+ // otherwise, decrypt and return bitstring
2831
+ const scopedRawBlob = await toBlob(await Cheqd.fetchStatusList2021(credential, true) as Uint8Array)
2832
+
2833
+ // decrypt
2834
+ return await LitProtocol.decryptDirect(scopedRawBlob, fromString(options?.topArgs?.symmetricKey, 'hex'))
2835
+ }())
2836
+ : (await async function () {
2837
+ // transcode to base64url, if needed
2838
+ const publishedListTranscoded = publishedList.metadata.encoding === 'base64url'
2839
+ ? publishedList.StatusList2021.encodedList
2840
+ : toString(fromString(publishedList.StatusList2021.encodedList, publishedList.metadata.encoding as DefaultStatusList2021Encoding), 'base64url')
2841
+
2842
+ // if status list 2021 is not fetched, read from file
2843
+ if (options?.statusListFile) {
2844
+ // if not encrypted, return bitstring
2845
+ if (!publishedList.metadata.encrypted) {
2846
+ // construct encoded status list
2847
+ const encoded = new StatusList({ buffer: await Cheqd.getFile(options.statusListFile) }).encode() as Bitstring
2848
+
2849
+ // validate against published list
2850
+ if (encoded !== publishedListTranscoded) throw new Error('[did-provider-cheqd]: revocation: statusListFile does not match published status list 2021')
2851
+
2852
+ // return encoded
2853
+ return encoded
2854
+ }
2855
+
2856
+ // otherwise, decrypt and return bitstring
2857
+ const scopedRawBlob = await toBlob(await Cheqd.getFile(options.statusListFile))
2858
+
2859
+ // decrypt
2860
+ const decrypted = await LitProtocol.decryptDirect(scopedRawBlob, fromString(options?.topArgs?.symmetricKey, 'hex'))
2861
+
2862
+ // validate against published list
2863
+ if (decrypted !== publishedListTranscoded) throw new Error('[did-provider-cheqd]: revocation: statusListFile does not match published status list 2021')
2864
+
2865
+ // return decrypted
2866
+ return decrypted
2867
+ }
2868
+
2869
+ if (!options?.statusListInlineBitstring) throw new Error('[did-provider-cheqd]: revocation: statusListInlineBitstring is required, if statusListFile is not provided')
2870
+
2871
+ // validate against published list
2872
+ if (options?.statusListInlineBitstring !== publishedListTranscoded) throw new Error('[did-provider-cheqd]: revocation: statusListInlineBitstring does not match published status list 2021')
2873
+
2874
+ // otherwise, read from inline bitstring
2875
+ return options?.statusListInlineBitstring
2876
+ }())
2877
+
2878
+ // parse status list 2021
2879
+ const statusList = await StatusList.decode({ encodedList: statusList2021 })
2880
+
2881
+ // early exit, if already suspended
2882
+ if (statusList.getStatus(Number(credential.credentialStatus.statusListIndex))) return { suspended: true } satisfies SuspensionResult
2883
+
2884
+ // update suspension status
2885
+ statusList.setStatus(Number(credential.credentialStatus.statusListIndex), true)
2886
+
2887
+ // set in-memory status list ref
2888
+ const bitstring = await statusList.encode() as Bitstring
2889
+
2890
+ // cast top-level args
2891
+ const topArgs = options?.topArgs as ICheqdSuspendCredentialWithStatusList2021Args
2892
+
2893
+ // write status list 2021 to file, if provided
2894
+ if (topArgs?.writeToFile) {
2895
+ await Cheqd.writeFile(fromString(bitstring, 'base64url'), options?.statusListFile)
2896
+ }
2897
+
2898
+ // publish status list 2021, if provided
2899
+ const published = topArgs?.publish
2900
+ ? (await async function () {
2901
+ // fetch status list 2021 metadata
2902
+ const statusListMetadata = await Cheqd.fetchStatusList2021Metadata(credential)
2903
+
2904
+ // publish status list 2021 as new version
2905
+ const scoped = topArgs.publishEncrypted
2906
+ ? (await async function () {
2907
+ // instantiate dkg-threshold client, in which case lit-protocol is used
2908
+ const lit = await LitProtocol.create({
2909
+ chain: options?.topArgs?.bootstrapOptions?.chain,
2910
+ litNetwork: options?.topArgs?.bootstrapOptions?.litNetwork
2911
+ })
2912
+
2913
+ // encrypt
2914
+ const { encryptedString, encryptedSymmetricKey, symmetricKey } = await lit.encrypt(bitstring, options?.topArgs?.encryptionOptions?.unifiedAccessControlConditions, true)
1755
2915
 
1756
- // validate access control conditions components - case: recipientAddress
1757
- if (!args.recipientAddress) {
1758
- throw new Error('[did-provider-cheqd]: observation: recipientAddress is required')
1759
- }
2916
+ // return tuple of publish result and encryption relevant metadata
2917
+ return [
2918
+ await Cheqd.publishStatusList2021(new Uint8Array(await encryptedString.arrayBuffer()), statusListMetadata, options?.publishOptions),
2919
+ { encryptedString, encryptedSymmetricKey, symmetricKey: toString(symmetricKey!, 'hex') }
2920
+ ]
2921
+ }())
2922
+ : (await async function () {
2923
+ // validate encoding, if provided
2924
+ if (options?.publishOptions?.statusListEncoding && !Object.values(DefaultStatusList2021Encodings).includes(options?.publishOptions?.statusListEncoding)) {
2925
+ throw new Error('[did-provider-cheqd]: revocation: Invalid status list encoding')
2926
+ }
1760
2927
 
1761
- // validate access control conditions components - case: amount
1762
- if (!args.amount || !args.amount.amount || !args.amount.denom || args.amount.denom !== 'ncheq') {
1763
- throw new Error('[did-provider-cheqd]: observation: amount is required, and must be an object with amount and denom valid string properties, amongst which denom must be `ncheq`')
1764
- }
2928
+ // validate validUntil, if provided
2929
+ if (options?.publishOptions?.statusListValidUntil) {
2930
+ // validate validUntil as string
2931
+ if (typeof options?.publishOptions?.statusListValidUntil !== 'string') throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be string)')
1765
2932
 
1766
- // validate access control conditions components - case: memoNonce
1767
- if (!args.memoNonce) {
1768
- throw new Error('[did-provider-cheqd]: observation: memoNonce is required')
1769
- }
2933
+ // validate validUntil as date
2934
+ if (isNaN(Date.parse(options?.publishOptions?.statusListValidUntil))) throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be date)')
1770
2935
 
1771
- // validate access control conditions components - case: network
1772
- if (!args.network) {
1773
- throw new Error('[did-provider-cheqd]: observation: network is required')
1774
- }
2936
+ // validate validUntil as future date
2937
+ if (new Date(options?.publishOptions?.statusListValidUntil) < new Date()) throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be future date)')
1775
2938
 
1776
- try {
1777
- // otherwise, construct url, as per components
1778
- const url = `${DefaultRESTUrls[args.network]}/cosmos/tx/v1beta1/txs?events=transfer.recipient='${args.recipientAddress}'&events=transfer.sender='${args.senderAddress}'&events=transfer.amount='${args.amount.amount}${args.amount.denom}'`
2939
+ // validate validUntil towards validFrom
2940
+ if (new Date(options?.publishOptions?.statusListValidUntil) <= new Date(publishedList.StatusList2021.validFrom)) throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be after validFrom)')
2941
+ }
1779
2942
 
1780
- // fetch relevant txs
1781
- const txs = await (await fetch(url)).json() as ShallowTypedTxsResponse
2943
+ // define status list content
2944
+ const content = {
2945
+ StatusList2021: {
2946
+ type: publishedList.StatusList2021.type,
2947
+ encodedList: publishedList.metadata.encoding === 'base64url' ? bitstring : toString(fromString(bitstring, 'base64url'), options!.publishOptions.statusListEncoding as DefaultStatusList2021Encoding),
2948
+ validFrom: publishedList.StatusList2021.validFrom,
2949
+ validUntil: options?.publishOptions?.statusListValidUntil || publishedList.StatusList2021.validUntil
2950
+ },
2951
+ metadata: {
2952
+ encoding: (options?.publishOptions?.statusListEncoding as DefaultStatusList2021Encoding | undefined) || publishedList.metadata.encoding,
2953
+ encrypted: false,
2954
+ }
2955
+ } satisfies StatusList2021Suspension
1782
2956
 
1783
- // skim through txs for relevant events, in which case memoNonce is present and strict equals to the one provided
1784
- const meetsConditionTxIndex = txs?.txs?.findIndex(tx => unescapeUnicode(tx.body.memo) === unescapeUnicode(args.memoNonce))
2957
+ // return tuple of publish result and encryption relevant metadata
2958
+ return [
2959
+ await Cheqd.publishStatusList2021(fromString(JSON.stringify(content), 'utf-8'), statusListMetadata, options?.publishOptions),
2960
+ undefined
2961
+ ]
2962
+ }())
1785
2963
 
1786
- // define meetsCondition
1787
- const meetsCondition = (typeof meetsConditionTxIndex !== 'undefined' && meetsConditionTxIndex !== -1)
2964
+ // early exit, if publish failed
2965
+ if (!scoped[0]) throw new Error('[did-provider-cheqd]: suspension: Failed to publish status list 2021')
2966
+
2967
+ // return publish result
2968
+ return scoped
2969
+ }())
2970
+ : undefined
1788
2971
 
1789
- // return observation result
1790
2972
  return {
1791
- subscribed: true,
1792
- meetsCondition: meetsCondition,
1793
- transactionHash: meetsCondition ? txs!.tx_responses[meetsConditionTxIndex].txhash : undefined,
1794
- events: meetsCondition ? txs!.tx_responses[meetsConditionTxIndex].events : undefined,
1795
- rawLog: meetsCondition ? txs!.tx_responses[meetsConditionTxIndex].raw_log : undefined,
1796
- txResponse: meetsCondition ? (args?.returnTxResponse ? txs!.tx_responses[meetsConditionTxIndex] : undefined) : undefined
1797
- } satisfies ObservationResult
2973
+ suspended: true,
2974
+ published: topArgs?.publish ? true : undefined,
2975
+ statusList: topArgs?.returnUpdatedStatusList ? await Cheqd.fetchStatusList2021(credential) as StatusList2021Suspension : undefined,
2976
+ encryptedStatusList: topArgs?.returnUpdatedEncryptedStatusList ? await blobToHexString((published?.[1] as { encryptedString: Blob })?.encryptedString) : undefined,
2977
+ encryptedSymmetricKey: topArgs?.returnEncryptedSymmetricKey ? (published?.[1] as { encryptedSymmetricKey: string })?.encryptedSymmetricKey : undefined,
2978
+ symmetricKey: topArgs?.returnSymmetricKey ? (published?.[1] as { symmetricKey: string })?.symmetricKey : undefined,
2979
+ resourceMetadata: topArgs?.returnStatusListMetadata ? await Cheqd.fetchStatusList2021Metadata(credential) : undefined
2980
+ } satisfies SuspensionResult
1798
2981
  } catch (error) {
1799
- // return error
1800
- return {
1801
- subscribed: false,
1802
- meetsCondition: false,
1803
- error: error as IError
1804
- } satisfies ObservationResult
2982
+ // silent fail + early exit
2983
+ console.error(error)
2984
+
2985
+ return { suspended: false, error: error as IError } satisfies SuspensionResult
1805
2986
  }
1806
2987
  }
1807
2988
 
1808
- static async revokeCredential(credential: VerifiableCredential, options?: ICheqdStatusList2021Options): Promise<RevocationResult> {
1809
- try {
1810
- // validate status purpose
1811
- if (credential?.credentialStatus?.statusPurpose !== 'revocation') throw new Error('[did-provider-cheqd]: revocation: Invalid status purpose')
2989
+ static async suspendCredentials(credentials: VerifiableCredential[], options?: ICheqdStatusList2021Options): Promise<BulkSuspensionResult> {
2990
+ // validate credentials - case: empty
2991
+ if (!credentials.length || credentials.length === 0) throw new Error('[did-provider-cheqd]: suspension: No credentials provided')
1812
2992
 
1813
- // fetch status list 2021 metadata
1814
- const metadata = (await Cheqd.fetchStatusList2021Metadata(credential))
2993
+ // validate credentials - case: consistent issuer
2994
+ if (credentials.map((credential) => {
2995
+ return ((credential.issuer as { id: string }).id)
2996
+ ? (credential.issuer as { id: string }).id
2997
+ : credential.issuer as string
2998
+ }).filter((value, _, self) => value && value !== self[0]).length > 0) throw new Error('[did-provider-cheqd]: suspension: Credentials must be issued by the same issuer')
1815
2999
 
1816
- // detect if encrypted
1817
- const isEncrypted = function() {
1818
- switch (metadata.mediaType) {
1819
- case 'application/octet-stream':
1820
- return true
1821
- case 'application/gzip':
1822
- return false
1823
- default:
1824
- throw new Error(`[did-provider-cheqd]: revocation: Unsupported media type: ${metadata.mediaType}`)
1825
- }
1826
- }()
3000
+ // validate credentials - case: status list index
3001
+ if (credentials.map((credential) => credential.credentialStatus!.statusListIndex).filter((value, index, self) => self.indexOf(value) !== index).length > 0) throw new Error('[did-provider-cheqd]: suspension: Credentials must have unique status list index')
3002
+
3003
+ // validate credentials - case: status purpose
3004
+ if (!credentials.every((credential) => credential.credentialStatus?.statusPurpose === 'suspension')) throw new Error('[did-provider-cheqd]: suspension: Invalid status purpose')
3005
+
3006
+ // validate credentials - case: status list id
3007
+ const remote = credentials[0].credentialStatus?.id
3008
+ ? (credentials[0].credentialStatus as { id: string }).id.split('#')[0]
3009
+ : (function(){
3010
+ throw new Error('[did-provider-cheqd]: suspension: Invalid status list id')
3011
+ }())
3012
+
3013
+ // validate credentials - case: status list id format
3014
+ if (!RemoteListPattern.test(remote)) throw new Error('[did-provider-cheqd]: suspension: Invalid status list id format: expected: https://<optional_subdomain>.<sld>.<tld>/1.0/identifiers/<did:cheqd:<namespace>:<method_specific_id>>?resourceName=<resource_name>&resourceType=<resource_type>')
3015
+
3016
+ if (!credentials.every((credential) => {
3017
+ return (credential.credentialStatus as { id: string }).id.split('#')[0] === remote
3018
+ })) throw new Error('[did-provider-cheqd]: suspension: Credentials must belong to the same status list')
3019
+
3020
+ // validate credentials - case: status list type
3021
+ if (!credentials.every((credential) => credential.credentialStatus?.type === 'StatusList2021Entry')) throw new Error('[did-provider-cheqd]: suspension: Invalid status list type')
3022
+
3023
+ try {
3024
+ // fetch status list 2021
3025
+ const publishedList = (await Cheqd.fetchStatusList2021(credentials[0])) as StatusList2021Suspension
1827
3026
 
1828
3027
  // early return, if encrypted and no decryption key provided
1829
- if (isEncrypted && !options?.topArgs?.symmetricKey) throw new Error('[did-provider-cheqd]: revocation: symmetricKey is required, if status list 2021 is encrypted')
3028
+ if (publishedList.metadata.encrypted && !options?.topArgs?.symmetricKey) throw new Error('[did-provider-cheqd]: suspension: symmetricKey is required, if status list 2021 is encrypted')
1830
3029
 
1831
3030
  // fetch status list 2021 inscribed in credential
1832
3031
  const statusList2021 = options?.topArgs?.fetchList
1833
3032
  ? (await async function () {
1834
3033
  // if not encrypted, return bitstring
1835
- if (!isEncrypted) return await Cheqd.fetchStatusList2021(credential)
3034
+ if (!publishedList.metadata.encrypted)
3035
+ return publishedList.metadata.encoding === 'base64url'
3036
+ ? publishedList.StatusList2021.encodedList
3037
+ : toString(fromString(publishedList.StatusList2021.encodedList, publishedList.metadata.encoding as DefaultStatusList2021Encoding), 'base64url')
1836
3038
 
1837
3039
  // otherwise, decrypt and return bitstring
1838
- const scopedRawBlob = await toBlob(await Cheqd.fetchStatusList2021(credential, true) as Uint8Array)
3040
+ const scopedRawBlob = await toBlob(await Cheqd.fetchStatusList2021(credentials[0], true) as Uint8Array)
1839
3041
 
1840
3042
  // decrypt
1841
3043
  return await LitProtocol.decryptDirect(scopedRawBlob, fromString(options?.topArgs?.symmetricKey, 'hex'))
1842
3044
  }())
1843
3045
  : (await async function () {
3046
+ // transcode to base64url, if needed
3047
+ const publishedListTranscoded = publishedList.metadata.encoding === 'base64url'
3048
+ ? publishedList.StatusList2021.encodedList
3049
+ : toString(fromString(publishedList.StatusList2021.encodedList, publishedList.metadata.encoding as DefaultStatusList2021Encoding), 'base64url')
3050
+
1844
3051
  // if status list 2021 is not fetched, read from file
1845
3052
  if (options?.statusListFile) {
1846
3053
  // if not encrypted, return bitstring
1847
- if (!isEncrypted) return new StatusList({ buffer: await Cheqd.getFile(options.statusListFile) }).encode()
3054
+ if (!publishedList.metadata.encrypted) {
3055
+ // construct encoded status list
3056
+ const encoded = new StatusList({ buffer: await Cheqd.getFile(options.statusListFile) }).encode() as Bitstring
3057
+
3058
+ // validate against published list
3059
+ if (encoded !== publishedListTranscoded) throw new Error('[did-provider-cheqd]: revocation: statusListFile does not match published status list 2021')
3060
+
3061
+ // return encoded
3062
+ return encoded
3063
+ }
1848
3064
 
1849
3065
  // otherwise, decrypt and return bitstring
1850
3066
  const scopedRawBlob = await toBlob(await Cheqd.getFile(options.statusListFile))
1851
3067
 
1852
3068
  // decrypt
1853
- return await LitProtocol.decryptDirect(scopedRawBlob, fromString(options?.topArgs?.symmetricKey, 'hex'))
3069
+ const decrypted = await LitProtocol.decryptDirect(scopedRawBlob, fromString(options?.topArgs?.symmetricKey, 'hex'))
3070
+
3071
+ // validate against published list
3072
+ if (decrypted !== publishedListTranscoded) throw new Error('[did-provider-cheqd]: revocation: statusListFile does not match published status list 2021')
3073
+
3074
+ // return decrypted
3075
+ return decrypted
1854
3076
  }
1855
3077
 
1856
3078
  if (!options?.statusListInlineBitstring) throw new Error('[did-provider-cheqd]: revocation: statusListInlineBitstring is required, if statusListFile is not provided')
1857
3079
 
3080
+ // validate against published list
3081
+ if (options?.statusListInlineBitstring !== publishedListTranscoded) throw new Error('[did-provider-cheqd]: revocation: statusListInlineBitstring does not match published status list 2021')
3082
+
1858
3083
  // otherwise, read from inline bitstring
1859
3084
  return options?.statusListInlineBitstring
1860
3085
  }())
@@ -1862,11 +3087,26 @@ export class Cheqd implements IAgentPlugin {
1862
3087
  // parse status list 2021
1863
3088
  const statusList = await StatusList.decode({ encodedList: statusList2021 })
1864
3089
 
1865
- // early exit, if credential is already revoked
1866
- if (statusList.getStatus(Number(credential.credentialStatus.statusListIndex))) return { revoked: false }
3090
+ // initiate bulk suspension
3091
+ const suspended = await Promise.allSettled(credentials.map((credential) => {
3092
+ return async function () {
3093
+ // early return, if no credential status
3094
+ if (!credential.credentialStatus) return { suspended: false }
1867
3095
 
1868
- // update revocation status
1869
- statusList.setStatus(Number(credential.credentialStatus.statusListIndex), true)
3096
+ // early exit, if credential is already suspended
3097
+ if (statusList.getStatus(Number(credential.credentialStatus.statusListIndex))) return { suspended: false }
3098
+
3099
+ // update suspension status
3100
+ statusList.setStatus(Number(credential.credentialStatus.statusListIndex), true)
3101
+
3102
+ // return suspension status
3103
+ return { suspended: true }
3104
+ }()
3105
+ })) satisfies PromiseSettledResult<SuspensionResult>[]
3106
+
3107
+ // revert bulk ops, if some failed
3108
+ if (suspended.some((result) => result.status === 'fulfilled' && !result.value.suspended ))
3109
+ throw new Error(`[did-provider-cheqd]: suspension: Bulk suspension failed: already suspended credentials in suspension bundle: raw log: ${JSON.stringify(suspended.map((result) => ({ suspended: result.status === 'fulfilled' ? result.value.suspended : false })))}`)
1870
3110
 
1871
3111
  // set in-memory status list ref
1872
3112
  const bitstring = await statusList.encode() as Bitstring
@@ -1883,7 +3123,7 @@ export class Cheqd implements IAgentPlugin {
1883
3123
  const published = topArgs?.publish
1884
3124
  ? (await async function () {
1885
3125
  // fetch status list 2021 metadata
1886
- const statusListMetadata = await Cheqd.fetchStatusList2021Metadata(credential)
3126
+ const statusListMetadata = await Cheqd.fetchStatusList2021Metadata(credentials[0])
1887
3127
 
1888
3128
  // publish status list 2021 as new version
1889
3129
  const scoped = topArgs.publishEncrypted
@@ -1903,10 +3143,50 @@ export class Cheqd implements IAgentPlugin {
1903
3143
  { encryptedString, encryptedSymmetricKey, symmetricKey: toString(symmetricKey!, 'hex') }
1904
3144
  ]
1905
3145
  }())
1906
- : [await Cheqd.publishStatusList2021(fromString(bitstring, 'base64url'), statusListMetadata, options?.publishOptions), undefined]
3146
+ : (await async function () {
3147
+ // validate encoding, if provided
3148
+ if (options?.publishOptions?.statusListEncoding && !Object.values(DefaultStatusList2021Encodings).includes(options?.publishOptions?.statusListEncoding)) {
3149
+ throw new Error('[did-provider-cheqd]: revocation: Invalid status list encoding')
3150
+ }
3151
+
3152
+ // validate validUntil, if provided
3153
+ if (options?.publishOptions?.statusListValidUntil) {
3154
+ // validate validUntil as string
3155
+ if (typeof options?.publishOptions?.statusListValidUntil !== 'string') throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be string)')
3156
+
3157
+ // validate validUntil as date
3158
+ if (isNaN(Date.parse(options?.publishOptions?.statusListValidUntil))) throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be date)')
3159
+
3160
+ // validate validUntil as future date
3161
+ if (new Date(options?.publishOptions?.statusListValidUntil) < new Date()) throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be future date)')
3162
+
3163
+ // validate validUntil towards validFrom
3164
+ if (new Date(options?.publishOptions?.statusListValidUntil) <= new Date(publishedList.StatusList2021.validFrom)) throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be after validFrom)')
3165
+ }
3166
+
3167
+ // define status list content
3168
+ const content = {
3169
+ StatusList2021: {
3170
+ type: publishedList.StatusList2021.type,
3171
+ encodedList: publishedList.metadata.encoding === 'base64url' ? bitstring : toString(fromString(bitstring, 'base64url'), options!.publishOptions.statusListEncoding as DefaultStatusList2021Encoding),
3172
+ validFrom: publishedList.StatusList2021.validFrom,
3173
+ validUntil: options?.publishOptions?.statusListValidUntil || publishedList.StatusList2021.validUntil
3174
+ },
3175
+ metadata: {
3176
+ encoding: (options?.publishOptions?.statusListEncoding as DefaultStatusList2021Encoding | undefined) || publishedList.metadata.encoding,
3177
+ encrypted: false,
3178
+ }
3179
+ } satisfies StatusList2021Suspension
3180
+
3181
+ // return tuple of publish result and encryption relevant metadata
3182
+ return [
3183
+ await Cheqd.publishStatusList2021(fromString(JSON.stringify(content), 'utf-8'), statusListMetadata, options?.publishOptions),
3184
+ undefined
3185
+ ]
3186
+ }())
1907
3187
 
1908
3188
  // early exit, if publish failed
1909
- if (!scoped[0]) throw new Error('[did-provider-cheqd]: revocation: Failed to publish status list 2021')
3189
+ if (!scoped[0]) throw new Error('[did-provider-cheqd]: suspension: Failed to publish status list 2021')
1910
3190
 
1911
3191
  // return publish result
1912
3192
  return scoped
@@ -1914,50 +3194,41 @@ export class Cheqd implements IAgentPlugin {
1914
3194
  : undefined
1915
3195
 
1916
3196
  return {
1917
- revoked: true,
3197
+ suspended: suspended.map((result) => result.status === 'fulfilled' ? result.value.suspended : false),
1918
3198
  published: topArgs?.publish ? true : undefined,
1919
- statusList: topArgs?.returnUpdatedStatusList ? bitstring : undefined,
3199
+ statusList: topArgs?.returnUpdatedStatusList ? await Cheqd.fetchStatusList2021(credentials[0]) as StatusList2021Suspension : undefined,
1920
3200
  encryptedStatusList: topArgs?.returnUpdatedEncryptedStatusList ? await blobToHexString((published?.[1] as { encryptedString: Blob })?.encryptedString) : undefined,
1921
3201
  encryptedSymmetricKey: topArgs?.returnEncryptedSymmetricKey ? (published?.[1] as { encryptedSymmetricKey: string })?.encryptedSymmetricKey : undefined,
1922
3202
  symmetricKey: topArgs?.returnSymmetricKey ? (published?.[1] as { symmetricKey: string })?.symmetricKey : undefined,
1923
- resourceMetadata: topArgs?.returnStatusListMetadata ? await Cheqd.fetchStatusList2021Metadata(credential) : undefined
1924
- } satisfies RevocationResult
3203
+ resourceMetadata: topArgs?.returnStatusListMetadata ? await Cheqd.fetchStatusList2021Metadata(credentials[0]) : undefined
3204
+ } satisfies BulkSuspensionResult
1925
3205
  } catch (error) {
1926
- // silent fail + early exit, optimised for parallelisation, use with Promise.allSettled
3206
+ // silent fail + early exit
1927
3207
  console.error(error)
1928
3208
 
1929
- return { revoked: false, error: error as IError } satisfies RevocationResult
3209
+ return { suspended: [], error: error as IError } satisfies BulkSuspensionResult
1930
3210
  }
1931
3211
  }
1932
3212
 
1933
- static async suspendCredential(credential: VerifiableCredential, options?: ICheqdStatusList2021Options): Promise<SuspensionResult> {
3213
+ static async unsuspendCredential(credential: VerifiableCredential, options?: ICheqdStatusList2021Options): Promise<UnsuspensionResult> {
1934
3214
  try {
1935
3215
  // validate status purpose
1936
- if (credential?.credentialStatus?.statusPurpose !== 'suspension') throw new Error('[did-provider-cheqd]: suspension: Invalid status purpose')
1937
-
1938
- // fetch status list 2021 metadata
1939
- const metadata = (await Cheqd.fetchStatusList2021Metadata(credential))
3216
+ if (credential?.credentialStatus?.statusPurpose !== 'suspension') throw new Error('[did-provider-cheqd]: unsuspension: Invalid status purpose')
1940
3217
 
1941
- // detect if encrypted
1942
- const isEncrypted = function() {
1943
- switch (metadata.mediaType) {
1944
- case 'application/octet-stream':
1945
- return true
1946
- case 'application/gzip':
1947
- return false
1948
- default:
1949
- throw new Error(`[did-provider-cheqd]: suspension: Unsupported media type: ${metadata.mediaType}`)
1950
- }
1951
- }()
3218
+ // fetch status list 2021
3219
+ const publishedList = (await Cheqd.fetchStatusList2021(credential)) as StatusList2021Suspension
1952
3220
 
1953
3221
  // early return, if encrypted and no decryption key provided
1954
- if (isEncrypted && !options?.topArgs?.symmetricKey) throw new Error('[did-provider-cheqd]: suspension: symmetricKey is required, if status list 2021 is encrypted')
3222
+ if (publishedList.metadata.encrypted && !options?.topArgs?.symmetricKey) throw new Error('[did-provider-cheqd]: suspension: symmetricKey is required, if status list 2021 is encrypted')
1955
3223
 
1956
3224
  // fetch status list 2021 inscribed in credential
1957
3225
  const statusList2021 = options?.topArgs?.fetchList
1958
3226
  ? (await async function () {
1959
3227
  // if not encrypted, return bitstring
1960
- if (!isEncrypted) return await Cheqd.fetchStatusList2021(credential)
3228
+ if (!publishedList.metadata.encrypted)
3229
+ return publishedList.metadata.encoding === 'base64url'
3230
+ ? publishedList.StatusList2021.encodedList
3231
+ : toString(fromString(publishedList.StatusList2021.encodedList, publishedList.metadata.encoding as DefaultStatusList2021Encoding), 'base64url')
1961
3232
 
1962
3233
  // otherwise, decrypt and return bitstring
1963
3234
  const scopedRawBlob = await toBlob(await Cheqd.fetchStatusList2021(credential, true) as Uint8Array)
@@ -1966,19 +3237,42 @@ export class Cheqd implements IAgentPlugin {
1966
3237
  return await LitProtocol.decryptDirect(scopedRawBlob, fromString(options?.topArgs?.symmetricKey, 'hex'))
1967
3238
  }())
1968
3239
  : (await async function () {
3240
+ // transcode to base64url, if needed
3241
+ const publishedListTranscoded = publishedList.metadata.encoding === 'base64url'
3242
+ ? publishedList.StatusList2021.encodedList
3243
+ : toString(fromString(publishedList.StatusList2021.encodedList, publishedList.metadata.encoding as DefaultStatusList2021Encoding), 'base64url')
3244
+
1969
3245
  // if status list 2021 is not fetched, read from file
1970
3246
  if (options?.statusListFile) {
1971
3247
  // if not encrypted, return bitstring
1972
- if (!isEncrypted) return new StatusList({ buffer: await Cheqd.getFile(options.statusListFile) }).encode()
3248
+ if (!publishedList.metadata.encrypted) {
3249
+ // construct encoded status list
3250
+ const encoded = new StatusList({ buffer: await Cheqd.getFile(options.statusListFile) }).encode() as Bitstring
3251
+
3252
+ // validate against published list
3253
+ if (encoded !== publishedListTranscoded) throw new Error('[did-provider-cheqd]: revocation: statusListFile does not match published status list 2021')
3254
+
3255
+ // return encoded
3256
+ return encoded
3257
+ }
1973
3258
 
1974
3259
  // otherwise, decrypt and return bitstring
1975
3260
  const scopedRawBlob = await toBlob(await Cheqd.getFile(options.statusListFile))
1976
3261
 
1977
3262
  // decrypt
1978
- return await LitProtocol.decryptDirect(scopedRawBlob, fromString(options?.topArgs?.symmetricKey, 'hex'))
3263
+ const decrypted = await LitProtocol.decryptDirect(scopedRawBlob, fromString(options?.topArgs?.symmetricKey, 'hex'))
3264
+
3265
+ // validate against published list
3266
+ if (decrypted !== publishedListTranscoded) throw new Error('[did-provider-cheqd]: revocation: statusListFile does not match published status list 2021')
3267
+
3268
+ // return decrypted
3269
+ return decrypted
1979
3270
  }
1980
3271
 
1981
- if (!options?.statusListInlineBitstring) throw new Error('[did-provider-cheqd]: suspension: statusListInlineBitstring is required, if statusListFile is not provided')
3272
+ if (!options?.statusListInlineBitstring) throw new Error('[did-provider-cheqd]: revocation: statusListInlineBitstring is required, if statusListFile is not provided')
3273
+
3274
+ // validate against published list
3275
+ if (options?.statusListInlineBitstring !== publishedListTranscoded) throw new Error('[did-provider-cheqd]: revocation: statusListInlineBitstring does not match published status list 2021')
1982
3276
 
1983
3277
  // otherwise, read from inline bitstring
1984
3278
  return options?.statusListInlineBitstring
@@ -1987,11 +3281,11 @@ export class Cheqd implements IAgentPlugin {
1987
3281
  // parse status list 2021
1988
3282
  const statusList = await StatusList.decode({ encodedList: statusList2021 })
1989
3283
 
1990
- // early exit, if already suspended
1991
- if (statusList.getStatus(Number(credential.credentialStatus.statusListIndex))) return { suspended: true } satisfies SuspensionResult
3284
+ // early exit, if already unsuspended
3285
+ if (!statusList.getStatus(Number(credential.credentialStatus.statusListIndex))) return { unsuspended: true } satisfies UnsuspensionResult
1992
3286
 
1993
3287
  // update suspension status
1994
- statusList.setStatus(Number(credential.credentialStatus.statusListIndex), true)
3288
+ statusList.setStatus(Number(credential.credentialStatus.statusListIndex), false)
1995
3289
 
1996
3290
  // set in-memory status list ref
1997
3291
  const bitstring = await statusList.encode() as Bitstring
@@ -2028,10 +3322,50 @@ export class Cheqd implements IAgentPlugin {
2028
3322
  { encryptedString, encryptedSymmetricKey, symmetricKey: toString(symmetricKey!, 'hex') }
2029
3323
  ]
2030
3324
  }())
2031
- : [await Cheqd.publishStatusList2021(fromString(bitstring, 'base64url'), statusListMetadata, options?.publishOptions), undefined]
3325
+ : (await async function () {
3326
+ // validate encoding, if provided
3327
+ if (options?.publishOptions?.statusListEncoding && !Object.values(DefaultStatusList2021Encodings).includes(options?.publishOptions?.statusListEncoding)) {
3328
+ throw new Error('[did-provider-cheqd]: revocation: Invalid status list encoding')
3329
+ }
3330
+
3331
+ // validate validUntil, if provided
3332
+ if (options?.publishOptions?.statusListValidUntil) {
3333
+ // validate validUntil as string
3334
+ if (typeof options?.publishOptions?.statusListValidUntil !== 'string') throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be string)')
3335
+
3336
+ // validate validUntil as date
3337
+ if (isNaN(Date.parse(options?.publishOptions?.statusListValidUntil))) throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be date)')
3338
+
3339
+ // validate validUntil as future date
3340
+ if (new Date(options?.publishOptions?.statusListValidUntil) < new Date()) throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be future date)')
3341
+
3342
+ // validate validUntil towards validFrom
3343
+ if (new Date(options?.publishOptions?.statusListValidUntil) <= new Date(publishedList.StatusList2021.validFrom)) throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be after validFrom)')
3344
+ }
3345
+
3346
+ // define status list content
3347
+ const content = {
3348
+ StatusList2021: {
3349
+ type: publishedList.StatusList2021.type,
3350
+ encodedList: publishedList.metadata.encoding === 'base64url' ? bitstring : toString(fromString(bitstring, 'base64url'), options!.publishOptions.statusListEncoding as DefaultStatusList2021Encoding),
3351
+ validFrom: publishedList.StatusList2021.validFrom,
3352
+ validUntil: options?.publishOptions?.statusListValidUntil || publishedList.StatusList2021.validUntil
3353
+ },
3354
+ metadata: {
3355
+ encoding: (options?.publishOptions?.statusListEncoding as DefaultStatusList2021Encoding | undefined) || publishedList.metadata.encoding,
3356
+ encrypted: false,
3357
+ }
3358
+ } satisfies StatusList2021Suspension
3359
+
3360
+ // return tuple of publish result and encryption relevant metadata
3361
+ return [
3362
+ await Cheqd.publishStatusList2021(fromString(JSON.stringify(content), 'utf-8'), statusListMetadata, options?.publishOptions),
3363
+ undefined
3364
+ ]
3365
+ }())
2032
3366
 
2033
3367
  // early exit, if publish failed
2034
- if (!scoped[0]) throw new Error('[did-provider-cheqd]: suspension: Failed to publish status list 2021')
3368
+ if (!scoped[0]) throw new Error('[did-provider-cheqd]: unsuspension: Failed to publish status list 2021')
2035
3369
 
2036
3370
  // return publish result
2037
3371
  return scoped
@@ -2039,71 +3373,115 @@ export class Cheqd implements IAgentPlugin {
2039
3373
  : undefined
2040
3374
 
2041
3375
  return {
2042
- suspended: true,
3376
+ unsuspended: true,
2043
3377
  published: topArgs?.publish ? true : undefined,
2044
- statusList: topArgs?.returnUpdatedStatusList ? bitstring : undefined,
3378
+ statusList: topArgs?.returnUpdatedStatusList ? await Cheqd.fetchStatusList2021(credential) as StatusList2021Suspension : undefined,
2045
3379
  encryptedStatusList: topArgs?.returnUpdatedEncryptedStatusList ? await blobToHexString((published?.[1] as { encryptedString: Blob })?.encryptedString) : undefined,
2046
3380
  encryptedSymmetricKey: topArgs?.returnEncryptedSymmetricKey ? (published?.[1] as { encryptedSymmetricKey: string })?.encryptedSymmetricKey : undefined,
2047
3381
  symmetricKey: topArgs?.returnSymmetricKey ? (published?.[1] as { symmetricKey: string })?.symmetricKey : undefined,
2048
3382
  resourceMetadata: topArgs?.returnStatusListMetadata ? await Cheqd.fetchStatusList2021Metadata(credential) : undefined
2049
- } satisfies SuspensionResult
3383
+ } satisfies UnsuspensionResult
2050
3384
  } catch (error) {
2051
- // silent fail + early exit, optimised for parallelisation, use with Promise.allSettled
3385
+ // silent fail + early exit
2052
3386
  console.error(error)
2053
3387
 
2054
- return { suspended: false, error: error as IError } satisfies SuspensionResult
3388
+ return { unsuspended: false, error: error as IError } satisfies UnsuspensionResult
2055
3389
  }
2056
3390
  }
2057
3391
 
2058
- static async unsuspendCredential(credential: VerifiableCredential, options?: ICheqdStatusList2021Options): Promise<UnsuspensionResult> {
2059
- try {
2060
- // validate status purpose
2061
- if (credential?.credentialStatus?.statusPurpose !== 'suspension') throw new Error('[did-provider-cheqd]: unsuspension: Invalid status purpose')
3392
+ static async unsuspendCredentials(credentials: VerifiableCredential[], options?: ICheqdStatusList2021Options): Promise<BulkUnsuspensionResult> {
3393
+ // validate credentials - case: empty
3394
+ if (!credentials.length || credentials.length === 0) throw new Error('[did-provider-cheqd]: unsuspension: No credentials provided')
2062
3395
 
2063
- // fetch status list 2021 metadata
2064
- const metadata = (await Cheqd.fetchStatusList2021Metadata(credential))
3396
+ // validate credentials - case: consistent issuer
3397
+ if (credentials.map((credential) => {
3398
+ return ((credential.issuer as { id: string }).id)
3399
+ ? (credential.issuer as { id: string }).id
3400
+ : credential.issuer as string
3401
+ }).filter((value, _, self) => value && value !== self[0]).length > 0) throw new Error('[did-provider-cheqd]: unsuspension: Credentials must be issued by the same issuer')
2065
3402
 
2066
- // detect if encrypted
2067
- const isEncrypted = function() {
2068
- switch (metadata.mediaType) {
2069
- case 'application/octet-stream':
2070
- return true
2071
- case 'application/gzip':
2072
- return false
2073
- default:
2074
- throw new Error(`[did-provider-cheqd]: unsuspension: Unsupported media type: ${metadata.mediaType}`)
2075
- }
2076
- }()
3403
+ // validate credentials - case: status list index
3404
+ if (credentials.map((credential) => credential.credentialStatus!.statusListIndex).filter((value, index, self) => self.indexOf(value) !== index).length > 0) throw new Error('[did-provider-cheqd]: unsuspension: Credentials must have unique status list index')
3405
+
3406
+ // validate credentials - case: status purpose
3407
+ if (!credentials.every((credential) => credential.credentialStatus?.statusPurpose === 'suspension')) throw new Error('[did-provider-cheqd]: unsuspension: Invalid status purpose')
3408
+
3409
+ // validate credentials - case: status list id
3410
+ const remote = credentials[0].credentialStatus?.id
3411
+ ? (credentials[0].credentialStatus as { id: string }).id.split('#')[0]
3412
+ : (function(){
3413
+ throw new Error('[did-provider-cheqd]: unsuspension: Invalid status list id')
3414
+ }())
3415
+
3416
+ // validate credentials - case: status list id format
3417
+ if (!RemoteListPattern.test(remote)) throw new Error('[did-provider-cheqd]: unsuspension: Invalid status list id format: expected: https://<optional_subdomain>.<sld>.<tld>/1.0/identifiers/<did:cheqd:<namespace>:<method_specific_id>>?resourceName=<resource_name>&resourceType=<resource_type>')
3418
+
3419
+ if (!credentials.every((credential) => {
3420
+ return (credential.credentialStatus as { id: string }).id.split('#')[0] === remote
3421
+ })) throw new Error('[did-provider-cheqd]: unsuspension: Credentials must belong to the same status list')
3422
+
3423
+ // validate credentials - case: status list type
3424
+ if (!credentials.every((credential) => credential.credentialStatus?.type === 'StatusList2021Entry')) throw new Error('[did-provider-cheqd]: unsuspension: Invalid status list type')
3425
+
3426
+ try {
3427
+ // fetch status list 2021
3428
+ const publishedList = (await Cheqd.fetchStatusList2021(credentials[0])) as StatusList2021Suspension
2077
3429
 
2078
3430
  // early return, if encrypted and no decryption key provided
2079
- if (isEncrypted && !options?.topArgs?.symmetricKey) throw new Error('[did-provider-cheqd]: unsuspension: symmetricKey is required, if status list 2021 is encrypted')
3431
+ if (publishedList.metadata.encrypted && !options?.topArgs?.symmetricKey) throw new Error('[did-provider-cheqd]: unsuspension: symmetricKey is required, if status list 2021 is encrypted')
2080
3432
 
2081
3433
  // fetch status list 2021 inscribed in credential
2082
3434
  const statusList2021 = options?.topArgs?.fetchList
2083
3435
  ? (await async function () {
2084
3436
  // if not encrypted, return bitstring
2085
- if (!isEncrypted) return await Cheqd.fetchStatusList2021(credential)
3437
+ if (!publishedList.metadata.encrypted)
3438
+ return publishedList.metadata.encoding === 'base64url'
3439
+ ? publishedList.StatusList2021.encodedList
3440
+ : toString(fromString(publishedList.StatusList2021.encodedList, publishedList.metadata.encoding as DefaultStatusList2021Encoding), 'base64url')
2086
3441
 
2087
3442
  // otherwise, decrypt and return bitstring
2088
- const scopedRawBlob = await toBlob(await Cheqd.fetchStatusList2021(credential, true) as Uint8Array)
3443
+ const scopedRawBlob = await toBlob(await Cheqd.fetchStatusList2021(credentials[0], true) as Uint8Array)
2089
3444
 
2090
3445
  // decrypt
2091
3446
  return await LitProtocol.decryptDirect(scopedRawBlob, fromString(options?.topArgs?.symmetricKey, 'hex'))
2092
3447
  }())
2093
3448
  : (await async function () {
3449
+ // transcode to base64url, if needed
3450
+ const publishedListTranscoded = publishedList.metadata.encoding === 'base64url'
3451
+ ? publishedList.StatusList2021.encodedList
3452
+ : toString(fromString(publishedList.StatusList2021.encodedList, publishedList.metadata.encoding as DefaultStatusList2021Encoding), 'base64url')
3453
+
2094
3454
  // if status list 2021 is not fetched, read from file
2095
3455
  if (options?.statusListFile) {
2096
3456
  // if not encrypted, return bitstring
2097
- if (!isEncrypted) return new StatusList({ buffer: await Cheqd.getFile(options.statusListFile) }).encode()
3457
+ if (!publishedList.metadata.encrypted) {
3458
+ // construct encoded status list
3459
+ const encoded = new StatusList({ buffer: await Cheqd.getFile(options.statusListFile) }).encode() as Bitstring
3460
+
3461
+ // validate against published list
3462
+ if (encoded !== publishedListTranscoded) throw new Error('[did-provider-cheqd]: revocation: statusListFile does not match published status list 2021')
3463
+
3464
+ // return encoded
3465
+ return encoded
3466
+ }
2098
3467
 
2099
3468
  // otherwise, decrypt and return bitstring
2100
3469
  const scopedRawBlob = await toBlob(await Cheqd.getFile(options.statusListFile))
2101
3470
 
2102
3471
  // decrypt
2103
- return await LitProtocol.decryptDirect(scopedRawBlob, fromString(options?.topArgs?.symmetricKey, 'hex'))
3472
+ const decrypted = await LitProtocol.decryptDirect(scopedRawBlob, fromString(options?.topArgs?.symmetricKey, 'hex'))
3473
+
3474
+ // validate against published list
3475
+ if (decrypted !== publishedListTranscoded) throw new Error('[did-provider-cheqd]: revocation: statusListFile does not match published status list 2021')
3476
+
3477
+ // return decrypted
3478
+ return decrypted
2104
3479
  }
2105
3480
 
2106
- if (!options?.statusListInlineBitstring) throw new Error('[did-provider-cheqd]: unsuspension: statusListInlineBitstring is required, if statusListFile is not provided')
3481
+ if (!options?.statusListInlineBitstring) throw new Error('[did-provider-cheqd]: revocation: statusListInlineBitstring is required, if statusListFile is not provided')
3482
+
3483
+ // validate against published list
3484
+ if (options?.statusListInlineBitstring !== publishedListTranscoded) throw new Error('[did-provider-cheqd]: revocation: statusListInlineBitstring does not match published status list 2021')
2107
3485
 
2108
3486
  // otherwise, read from inline bitstring
2109
3487
  return options?.statusListInlineBitstring
@@ -2112,17 +3490,32 @@ export class Cheqd implements IAgentPlugin {
2112
3490
  // parse status list 2021
2113
3491
  const statusList = await StatusList.decode({ encodedList: statusList2021 })
2114
3492
 
2115
- // early exit, if already unsuspended
2116
- if (!statusList.getStatus(Number(credential.credentialStatus.statusListIndex))) return { unsuspended: true } satisfies UnsuspensionResult
3493
+ // initiate bulk unsuspension
3494
+ const unsuspended = await Promise.allSettled(credentials.map((credential) => {
3495
+ return async function () {
3496
+ // early return, if no credential status
3497
+ if (!credential.credentialStatus) return { unsuspended: false }
2117
3498
 
2118
- // update suspension status
2119
- statusList.setStatus(Number(credential.credentialStatus.statusListIndex), false)
3499
+ // early exit, if credential is already unsuspended
3500
+ if (!statusList.getStatus(Number(credential.credentialStatus.statusListIndex))) return { unsuspended: true }
3501
+
3502
+ // update unsuspension status
3503
+ statusList.setStatus(Number(credential.credentialStatus.statusListIndex), false)
3504
+
3505
+ // return unsuspension status
3506
+ return { unsuspended: true }
3507
+ }()
3508
+ })) satisfies PromiseSettledResult<UnsuspensionResult>[]
3509
+
3510
+ // revert bulk ops, if some failed
3511
+ if (unsuspended.some((result) => result.status === 'fulfilled' && !result.value.unsuspended ))
3512
+ throw new Error(`[did-provider-cheqd]: unsuspension: Bulk unsuspension failed: already unsuspended credentials in unsuspension bundle: raw log: ${JSON.stringify(unsuspended.map((result) => ({ unsuspended: result.status === 'fulfilled' ? result.value.unsuspended : false })))}`)
2120
3513
 
2121
3514
  // set in-memory status list ref
2122
3515
  const bitstring = await statusList.encode() as Bitstring
2123
3516
 
2124
3517
  // cast top-level args
2125
- const topArgs = options?.topArgs as ICheqdSuspendCredentialWithStatusList2021Args
3518
+ const topArgs = options?.topArgs as ICheqdRevokeCredentialWithStatusList2021Args
2126
3519
 
2127
3520
  // write status list 2021 to file, if provided
2128
3521
  if (topArgs?.writeToFile) {
@@ -2133,7 +3526,7 @@ export class Cheqd implements IAgentPlugin {
2133
3526
  const published = topArgs?.publish
2134
3527
  ? (await async function () {
2135
3528
  // fetch status list 2021 metadata
2136
- const statusListMetadata = await Cheqd.fetchStatusList2021Metadata(credential)
3529
+ const statusListMetadata = await Cheqd.fetchStatusList2021Metadata(credentials[0])
2137
3530
 
2138
3531
  // publish status list 2021 as new version
2139
3532
  const scoped = topArgs.publishEncrypted
@@ -2153,7 +3546,47 @@ export class Cheqd implements IAgentPlugin {
2153
3546
  { encryptedString, encryptedSymmetricKey, symmetricKey: toString(symmetricKey!, 'hex') }
2154
3547
  ]
2155
3548
  }())
2156
- : [await Cheqd.publishStatusList2021(fromString(bitstring, 'base64url'), statusListMetadata, options?.publishOptions), undefined]
3549
+ : (await async function () {
3550
+ // validate encoding, if provided
3551
+ if (options?.publishOptions?.statusListEncoding && !Object.values(DefaultStatusList2021Encodings).includes(options?.publishOptions?.statusListEncoding)) {
3552
+ throw new Error('[did-provider-cheqd]: revocation: Invalid status list encoding')
3553
+ }
3554
+
3555
+ // validate validUntil, if provided
3556
+ if (options?.publishOptions?.statusListValidUntil) {
3557
+ // validate validUntil as string
3558
+ if (typeof options?.publishOptions?.statusListValidUntil !== 'string') throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be string)')
3559
+
3560
+ // validate validUntil as date
3561
+ if (isNaN(Date.parse(options?.publishOptions?.statusListValidUntil))) throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be date)')
3562
+
3563
+ // validate validUntil as future date
3564
+ if (new Date(options?.publishOptions?.statusListValidUntil) < new Date()) throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be future date)')
3565
+
3566
+ // validate validUntil towards validFrom
3567
+ if (new Date(options?.publishOptions?.statusListValidUntil) <= new Date(publishedList.StatusList2021.validFrom)) throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be after validFrom)')
3568
+ }
3569
+
3570
+ // define status list content
3571
+ const content = {
3572
+ StatusList2021: {
3573
+ type: publishedList.StatusList2021.type,
3574
+ encodedList: publishedList.metadata.encoding === 'base64url' ? bitstring : toString(fromString(bitstring, 'base64url'), options!.publishOptions.statusListEncoding as DefaultStatusList2021Encoding),
3575
+ validFrom: publishedList.StatusList2021.validFrom,
3576
+ validUntil: options?.publishOptions?.statusListValidUntil || publishedList.StatusList2021.validUntil
3577
+ },
3578
+ metadata: {
3579
+ encoding: (options?.publishOptions?.statusListEncoding as DefaultStatusList2021Encoding | undefined) || publishedList.metadata.encoding,
3580
+ encrypted: false,
3581
+ }
3582
+ } satisfies StatusList2021Suspension
3583
+
3584
+ // return tuple of publish result and encryption relevant metadata
3585
+ return [
3586
+ await Cheqd.publishStatusList2021(fromString(JSON.stringify(content), 'utf-8'), statusListMetadata, options?.publishOptions),
3587
+ undefined
3588
+ ]
3589
+ }())
2157
3590
 
2158
3591
  // early exit, if publish failed
2159
3592
  if (!scoped[0]) throw new Error('[did-provider-cheqd]: unsuspension: Failed to publish status list 2021')
@@ -2164,19 +3597,19 @@ export class Cheqd implements IAgentPlugin {
2164
3597
  : undefined
2165
3598
 
2166
3599
  return {
2167
- unsuspended: true,
3600
+ unsuspended: unsuspended.map((result) => result.status === 'fulfilled' ? result.value.unsuspended : false),
2168
3601
  published: topArgs?.publish ? true : undefined,
2169
- statusList: topArgs?.returnUpdatedStatusList ? bitstring : undefined,
3602
+ statusList: topArgs?.returnUpdatedStatusList ? await Cheqd.fetchStatusList2021(credentials[0]) as StatusList2021Suspension : undefined,
2170
3603
  encryptedStatusList: topArgs?.returnUpdatedEncryptedStatusList ? await blobToHexString((published?.[1] as { encryptedString: Blob })?.encryptedString) : undefined,
2171
3604
  encryptedSymmetricKey: topArgs?.returnEncryptedSymmetricKey ? (published?.[1] as { encryptedSymmetricKey: string })?.encryptedSymmetricKey : undefined,
2172
3605
  symmetricKey: topArgs?.returnSymmetricKey ? (published?.[1] as { symmetricKey: string })?.symmetricKey : undefined,
2173
- resourceMetadata: topArgs?.returnStatusListMetadata ? await Cheqd.fetchStatusList2021Metadata(credential) : undefined
2174
- } satisfies UnsuspensionResult
3606
+ resourceMetadata: topArgs?.returnStatusListMetadata ? await Cheqd.fetchStatusList2021Metadata(credentials[0]) : undefined
3607
+ } satisfies BulkUnsuspensionResult
2175
3608
  } catch (error) {
2176
- // silent fail + early exit, optimised for parallelisation, use with Promise.allSettled
3609
+ // silent fail + early exit
2177
3610
  console.error(error)
2178
3611
 
2179
- return { unsuspended: false, error: error as IError } satisfies UnsuspensionResult
3612
+ return { unsuspended: [], error: error as IError } satisfies BulkUnsuspensionResult
2180
3613
  }
2181
3614
  }
2182
3615
 
@@ -2186,32 +3619,23 @@ export class Cheqd implements IAgentPlugin {
2186
3619
  throw new Error(`[did-provider-cheqd]: revocation: Unsupported status purpose: ${credential.credentialStatus?.statusPurpose}`)
2187
3620
  }
2188
3621
 
2189
- // fetch status list 2021 metadata
2190
- const metadata = (await Cheqd.fetchStatusList2021Metadata(credential))
2191
-
2192
- // detect if encrypted
2193
- const isEncrypted = function() {
2194
- switch (metadata.mediaType) {
2195
- case 'application/octet-stream':
2196
- return true
2197
- case 'application/gzip':
2198
- return false
2199
- default:
2200
- throw new Error(`[did-provider-cheqd]: revocation: Unsupported media type: ${metadata.mediaType}`)
2201
- }
2202
- }()
3622
+ // fetch status list 2021
3623
+ const publishedList = (await Cheqd.fetchStatusList2021(credential)) as StatusList2021Revocation
2203
3624
 
2204
3625
  // early return, if encrypted and decryption key is not provided
2205
- if (isEncrypted && !options?.topArgs?.encryptedSymmetricKey) throw new Error('[did-provider-cheqd]: revocation: encryptedSymmetricKey is required, if status list 2021 is encrypted')
3626
+ if (publishedList.metadata.encrypted && !options?.topArgs?.encryptedSymmetricKey) throw new Error('[did-provider-cheqd]: revocation: encryptedSymmetricKey is required, if status list 2021 is encrypted')
2206
3627
 
2207
3628
  // fetch status list 2021 inscribed in credential
2208
3629
  const statusList2021 = options?.topArgs?.fetchList
2209
3630
  ? (await async function () {
2210
3631
  // if not encrypted, return bitstring
2211
- if (!isEncrypted) return await Cheqd.fetchStatusList2021(credential)
3632
+ if (!publishedList.metadata.encrypted)
3633
+ return publishedList.metadata.encoding === 'base64url'
3634
+ ? publishedList.StatusList2021.encodedList
3635
+ : toString(fromString(publishedList.StatusList2021.encodedList, publishedList.metadata.encoding as DefaultStatusList2021Encoding), 'base64url')
2212
3636
 
2213
3637
  // otherwise, decrypt and return bitstring
2214
- const scopedRawBlob = await toBlob(await Cheqd.fetchStatusList2021(credential, true) as Uint8Array)
3638
+ const scopedRawBlob = await toBlob(fromString(publishedList.StatusList2021.encodedList, publishedList.metadata.encoding as DefaultStatusList2021Encoding))
2215
3639
 
2216
3640
  // instantiate dkg-threshold client, in which case lit-protocol is used
2217
3641
  const lit = await LitProtocol.create({
@@ -2223,25 +3647,42 @@ export class Cheqd implements IAgentPlugin {
2223
3647
  return await lit.decrypt(scopedRawBlob, options?.topArgs?.encryptedSymmetricKey, options?.topArgs?.decryptionOptions?.unifiedAccessControlConditions)
2224
3648
  }())
2225
3649
  : (await async function () {
3650
+ // transcode to base64url, if needed
3651
+ const publishedListTranscoded = publishedList.metadata.encoding === 'base64url'
3652
+ ? publishedList.StatusList2021.encodedList
3653
+ : toString(fromString(publishedList.StatusList2021.encodedList, publishedList.metadata.encoding as DefaultStatusList2021Encoding), 'base64url')
3654
+
2226
3655
  // if status list 2021 is not fetched, read from file
2227
3656
  if (options?.statusListFile) {
2228
3657
  // if not encrypted, return bitstring
2229
- if (!isEncrypted) return new StatusList({ buffer: await Cheqd.getFile(options.statusListFile) }).encode()
3658
+ if (!publishedList.metadata.encrypted) {
3659
+ // construct encoded status list
3660
+ const encoded = new StatusList({ buffer: await Cheqd.getFile(options.statusListFile) }).encode() as Bitstring
3661
+
3662
+ // validate against published list
3663
+ if (encoded !== publishedListTranscoded) throw new Error('[did-provider-cheqd]: revocation: statusListFile does not match published status list 2021')
3664
+
3665
+ // return encoded
3666
+ return encoded
3667
+ }
2230
3668
 
2231
3669
  // otherwise, decrypt and return bitstring
2232
3670
  const scopedRawBlob = await toBlob(await Cheqd.getFile(options.statusListFile))
2233
3671
 
2234
- // instantiate dkg-threshold client, in which case lit-protocol is used
2235
- const lit = await LitProtocol.create({
2236
- chain: options?.topArgs?.bootstrapOptions?.chain,
2237
- litNetwork: options?.topArgs?.bootstrapOptions?.litNetwork
2238
- })
2239
-
2240
3672
  // decrypt
2241
- return await lit.decrypt(scopedRawBlob, options?.topArgs?.encryptedSymmetricKey, options?.topArgs?.decryptionOptions?.unifiedAccessControlConditions)
3673
+ const decrypted = await LitProtocol.decryptDirect(scopedRawBlob, fromString(options?.topArgs?.symmetricKey, 'hex'))
3674
+
3675
+ // validate against published list
3676
+ if (decrypted !== publishedListTranscoded) throw new Error('[did-provider-cheqd]: revocation: statusListFile does not match published status list 2021')
3677
+
3678
+ // return decrypted
3679
+ return decrypted
2242
3680
  }
2243
3681
 
2244
- if (!options?.statusListInlineBitstring) throw new Error(' [did-provider-cheqd]: revocation: statusListInlineBitstring is required, if statusListFile is not provided')
3682
+ if (!options?.statusListInlineBitstring) throw new Error('[did-provider-cheqd]: revocation: statusListInlineBitstring is required, if statusListFile is not provided')
3683
+
3684
+ // validate against published list
3685
+ if (options?.statusListInlineBitstring !== publishedListTranscoded) throw new Error('[did-provider-cheqd]: revocation: statusListInlineBitstring does not match published status list 2021')
2245
3686
 
2246
3687
  // otherwise, read from inline bitstring
2247
3688
  return options?.statusListInlineBitstring
@@ -2260,32 +3701,23 @@ export class Cheqd implements IAgentPlugin {
2260
3701
  throw new Error(`[did-provider-cheqd]: suspension: Unsupported status purpose: ${credential.credentialStatus?.statusPurpose}`)
2261
3702
  }
2262
3703
 
2263
- // fetch status list 2021 metadata
2264
- const metadata = (await Cheqd.fetchStatusList2021Metadata(credential))
2265
-
2266
- // detect if encrypted
2267
- const isEncrypted = function() {
2268
- switch (metadata.mediaType) {
2269
- case 'application/octet-stream':
2270
- return true
2271
- case 'application/gzip':
2272
- return false
2273
- default:
2274
- throw new Error(`[did-provider-cheqd]: suspension: Unsupported media type: ${metadata.mediaType}`)
2275
- }
2276
- }()
3704
+ // fetch status list 2021
3705
+ const publishedList = (await Cheqd.fetchStatusList2021(credential)) as StatusList2021Suspension
2277
3706
 
2278
3707
  // early return, if encrypted and decryption key is not provided
2279
- if (isEncrypted && !options?.topArgs?.encryptedSymmetricKey) throw new Error('[did-provider-cheqd]: suspension: encryptedSymmetricKey is required, if status list 2021 is encrypted')
3708
+ if (publishedList.metadata.encrypted && !options?.topArgs?.encryptedSymmetricKey) throw new Error('[did-provider-cheqd]: revocation: encryptedSymmetricKey is required, if status list 2021 is encrypted')
2280
3709
 
2281
3710
  // fetch status list 2021 inscribed in credential
2282
3711
  const statusList2021 = options?.topArgs?.fetchList
2283
3712
  ? (await async function () {
2284
3713
  // if not encrypted, return bitstring
2285
- if (!isEncrypted) return await Cheqd.fetchStatusList2021(credential)
3714
+ if (!publishedList.metadata.encrypted)
3715
+ return publishedList.metadata.encoding === 'base64url'
3716
+ ? publishedList.StatusList2021.encodedList
3717
+ : toString(fromString(publishedList.StatusList2021.encodedList, publishedList.metadata.encoding as DefaultStatusList2021Encoding), 'base64url')
2286
3718
 
2287
3719
  // otherwise, decrypt and return bitstring
2288
- const scopedRawBlob = await toBlob(await Cheqd.fetchStatusList2021(credential, true) as Uint8Array)
3720
+ const scopedRawBlob = await toBlob(fromString(publishedList.StatusList2021.encodedList, publishedList.metadata.encoding as DefaultStatusList2021Encoding))
2289
3721
 
2290
3722
  // instantiate dkg-threshold client, in which case lit-protocol is used
2291
3723
  const lit = await LitProtocol.create({
@@ -2297,25 +3729,42 @@ export class Cheqd implements IAgentPlugin {
2297
3729
  return await lit.decrypt(scopedRawBlob, options?.topArgs?.encryptedSymmetricKey, options?.topArgs?.decryptionOptions?.unifiedAccessControlConditions)
2298
3730
  }())
2299
3731
  : (await async function () {
3732
+ // transcode to base64url, if needed
3733
+ const publishedListTranscoded = publishedList.metadata.encoding === 'base64url'
3734
+ ? publishedList.StatusList2021.encodedList
3735
+ : toString(fromString(publishedList.StatusList2021.encodedList, publishedList.metadata.encoding as DefaultStatusList2021Encoding), 'base64url')
3736
+
2300
3737
  // if status list 2021 is not fetched, read from file
2301
3738
  if (options?.statusListFile) {
2302
3739
  // if not encrypted, return bitstring
2303
- if (!isEncrypted) return new StatusList({ buffer: await Cheqd.getFile(options.statusListFile) }).encode()
3740
+ if (!publishedList.metadata.encrypted) {
3741
+ // construct encoded status list
3742
+ const encoded = new StatusList({ buffer: await Cheqd.getFile(options.statusListFile) }).encode() as Bitstring
3743
+
3744
+ // validate against published list
3745
+ if (encoded !== publishedListTranscoded) throw new Error('[did-provider-cheqd]: revocation: statusListFile does not match published status list 2021')
3746
+
3747
+ // return encoded
3748
+ return encoded
3749
+ }
2304
3750
 
2305
3751
  // otherwise, decrypt and return bitstring
2306
3752
  const scopedRawBlob = await toBlob(await Cheqd.getFile(options.statusListFile))
2307
3753
 
2308
- // instantiate dkg-threshold client, in which case lit-protocol is used
2309
- const lit = await LitProtocol.create({
2310
- chain: options?.topArgs?.bootstrapOptions?.chain,
2311
- litNetwork: options?.topArgs?.bootstrapOptions?.litNetwork
2312
- })
2313
-
2314
3754
  // decrypt
2315
- return await lit.decrypt(scopedRawBlob, options?.topArgs?.encryptedSymmetricKey, options?.topArgs?.decryptionOptions?.unifiedAccessControlConditions)
3755
+ const decrypted = await LitProtocol.decryptDirect(scopedRawBlob, fromString(options?.topArgs?.symmetricKey, 'hex'))
3756
+
3757
+ // validate against published list
3758
+ if (decrypted !== publishedListTranscoded) throw new Error('[did-provider-cheqd]: revocation: statusListFile does not match published status list 2021')
3759
+
3760
+ // return decrypted
3761
+ return decrypted
2316
3762
  }
2317
3763
 
2318
- if (!options?.statusListInlineBitstring) throw new Error(' [did-provider-cheqd]: suspension: statusListInlineBitstring is required, if statusListFile is not provided')
3764
+ if (!options?.statusListInlineBitstring) throw new Error('[did-provider-cheqd]: revocation: statusListInlineBitstring is required, if statusListFile is not provided')
3765
+
3766
+ // validate against published list
3767
+ if (options?.statusListInlineBitstring !== publishedListTranscoded) throw new Error('[did-provider-cheqd]: revocation: statusListInlineBitstring does not match published status list 2021')
2319
3768
 
2320
3769
  // otherwise, read from inline bitstring
2321
3770
  return options?.statusListInlineBitstring
@@ -2328,18 +3777,19 @@ export class Cheqd implements IAgentPlugin {
2328
3777
  return !!statusList.getStatus(Number(credential.credentialStatus.statusListIndex))
2329
3778
  }
2330
3779
 
2331
- static async publishStatusList2021(statusList2021Raw: Uint8Array, statusList2021Metadata: LinkedResourceMetadataResolutionResult, options: { context: IContext, resourceId?: string, resourceVersion?: string, signInputs?: ISignInputs[], fee?: DidStdFee }): Promise<boolean> {
3780
+ static async publishStatusList2021(statusList2021Raw: Uint8Array, statusList2021Metadata: LinkedResourceMetadataResolutionResult, options: { context: IContext, resourceId?: string, resourceVersion?: string, resourceAlsoKnownAs?: AlternativeUri[], signInputs?: ISignInputs[], fee?: DidStdFee }): Promise<boolean> {
2332
3781
  // construct status list 2021 payload from previous version + new version
2333
3782
  const payload = {
2334
3783
  collectionId: statusList2021Metadata.resourceCollectionId,
2335
3784
  id: options?.resourceId || v4(),
2336
3785
  name: statusList2021Metadata.resourceName,
2337
3786
  version: options?.resourceVersion || new Date().toISOString(),
2338
- resourceType: 'StatusList2021',
3787
+ alsoKnownAs: options?.resourceAlsoKnownAs || [],
3788
+ resourceType: statusList2021Metadata.resourceType as DefaultStatusList2021ResourceType,
2339
3789
  data: statusList2021Raw
2340
3790
  } satisfies StatusList2021ResourcePayload
2341
3791
 
2342
- return await options.context.agent[CreateStatusList2021MethodName]({
3792
+ return await options.context.agent[BroadcastStatusList2021MethodName]({
2343
3793
  kms: (await options.context.agent.keyManagerGetKeyManagementSystems())[0],
2344
3794
  payload,
2345
3795
  network: statusList2021Metadata.resourceURI.split(':')[2] as CheqdNetwork,
@@ -2348,7 +3798,7 @@ export class Cheqd implements IAgentPlugin {
2348
3798
  })
2349
3799
  }
2350
3800
 
2351
- static async fetchStatusList2021(credential: VerifiableCredential, returnRaw = false): Promise<Bitstring | Uint8Array> {
3801
+ static async fetchStatusList2021(credential: VerifiableCredential, returnRaw = false): Promise<StatusList2021Revocation | StatusList2021Suspension | Uint8Array> {
2352
3802
  // validate credential status
2353
3803
  if (!credential.credentialStatus) throw new Error('[did-provider-cheqd]: fetch status list: Credential status is not present')
2354
3804
 
@@ -2358,31 +3808,34 @@ export class Cheqd implements IAgentPlugin {
2358
3808
  // validate credential status list status purpose
2359
3809
  if (credential.credentialStatus.statusPurpose !== 'revocation' && credential.credentialStatus.statusPurpose !== 'suspension') throw new Error('[did-provider-cheqd]: fetch status list: Credential status purpose is not valid')
2360
3810
 
2361
- // validate credential status list status list credential
2362
- if (!credential.credentialStatus.statusListCredential) throw new Error('[did-provider-cheqd]: fetch status list: Credential status list credential is not present')
2363
-
2364
3811
  // fetch status list 2021
2365
- const raw = await (await fetch(credential.credentialStatus.statusListCredential)).arrayBuffer()
3812
+ const content = await (await fetch(credential.credentialStatus.id.split('#')[0])).json() as StatusList2021Revocation | StatusList2021Suspension
2366
3813
 
2367
3814
  // return raw if requested
2368
- if (returnRaw) return new Uint8Array(raw)
2369
-
2370
- // otherwise, parse to bitstring and return
2371
- const bitstring = toString(new Uint8Array(raw), 'base64url')
3815
+ if (returnRaw) {
3816
+ return fromString(content.StatusList2021.encodedList, content.metadata.encoding as DefaultStatusList2021Encoding)
3817
+ }
2372
3818
 
2373
- return bitstring
3819
+ // otherwise, return content
3820
+ return content
2374
3821
  }
2375
3822
 
2376
3823
  static async fetchStatusList2021Metadata(credential: VerifiableCredential): Promise<LinkedResourceMetadataResolutionResult> {
2377
3824
  // get base url
2378
- const baseUrl = new URL(credential.credentialStatus?.statusListCredential)
2379
-
3825
+ const baseUrl = new URL(credential.credentialStatus!.id.split('#')[0])
3826
+
2380
3827
  // get resource name
2381
3828
  const resourceName = baseUrl.searchParams.get('resourceName')
2382
3829
 
3830
+ // get resource type
3831
+ const resourceType = baseUrl.searchParams.get('resourceType')
3832
+
2383
3833
  // unset resource name
2384
3834
  baseUrl.searchParams.delete('resourceName')
2385
3835
 
3836
+ // unset resource type
3837
+ baseUrl.searchParams.delete('resourceType')
3838
+
2386
3839
  // construct metadata url
2387
3840
  const metadataUrl = `${baseUrl.toString()}/metadata`
2388
3841
 
@@ -2393,7 +3846,7 @@ export class Cheqd implements IAgentPlugin {
2393
3846
  if (!collectionMetadata?.contentStream?.linkedResourceMetadata) throw new Error('[did-provider-cheqd]: fetch status list metadata: No linked resources found')
2394
3847
 
2395
3848
  // find relevant resources by resource name
2396
- const resourceVersioning = collectionMetadata.contentStream.linkedResourceMetadata.filter((resource) => resource.resourceName === resourceName)
3849
+ const resourceVersioning = collectionMetadata.contentStream.linkedResourceMetadata.filter((resource) => resource.resourceName === resourceName && resource.resourceType === resourceType)
2397
3850
 
2398
3851
  // early exit if no relevant resources
2399
3852
  if (!resourceVersioning.length || resourceVersioning.length === 0) throw new Error(`[did-provider-cheqd]: fetch status list metadata: No relevant resources found by resource name ${resourceName}`)
@@ -2453,4 +3906,16 @@ export class Cheqd implements IAgentPlugin {
2453
3906
  })
2454
3907
  })
2455
3908
  }
3909
+
3910
+ static async decodeCredentialJWT(jwt: string): Promise<VerifiableCredential> {
3911
+ const decodedCredential = decodeJWT(jwt)
3912
+
3913
+ // validate credential payload
3914
+ if (!decodedCredential.payload) throw new Error('[did-provider-cheqd]: decode jwt: decodedCredential.payload is required')
3915
+
3916
+ // validate credential payload vc property as VerifiableCredential
3917
+ if (!decodedCredential.payload.vc) throw new Error('[did-provider-cheqd]: decode jwt: decodedCredential.payload.vc is required')
3918
+
3919
+ return decodedCredential.payload.vc satisfies VerifiableCredential
3920
+ }
2456
3921
  }