@cheqd/did-provider-cheqd 3.4.0-develop.1 → 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.
@@ -38,7 +38,8 @@ import {
38
38
  IDIDManager,
39
39
  IDataStore,
40
40
  IResolver,
41
- W3CVerifiableCredential
41
+ W3CVerifiableCredential,
42
+ ICredentialVerifier
42
43
  } from '@veramo/core'
43
44
  import {
44
45
  CheqdDIDProvider,
@@ -46,7 +47,13 @@ import {
46
47
  TImportableEd25519Key,
47
48
  ResourcePayload,
48
49
  StatusList2021ResourcePayload,
49
- DefaultRESTUrls
50
+ DefaultRESTUrls,
51
+ DefaultStatusList2021Encodings,
52
+ DefaultStatusList2021ResourceTypes,
53
+ DefaultStatusList2021StatusPurposeTypes,
54
+ DefaultStatusList2021Encoding,
55
+ DefaultStatusList2021ResourceType,
56
+ DefaultStatusList2021StatusPurposeType,
50
57
  } from '../did-manager/cheqd-did-provider.js'
51
58
  import {
52
59
  fromString,
@@ -65,12 +72,18 @@ import {
65
72
  LitProtocol,
66
73
  TxNonceFormat
67
74
  } from '../dkg-threshold/lit-protocol.js';
68
- import { blobToHexString, randomFromRange, toBlob, unescapeUnicode } from '../utils/helpers.js'
75
+ import {
76
+ blobToHexString,
77
+ randomFromRange,
78
+ toBlob,
79
+ unescapeUnicode
80
+ } from '../utils/helpers.js'
69
81
  import { resolverUrl } from '../did-manager/cheqd-did-resolver.js'
82
+ import { AlternativeUri } from '@cheqd/ts-proto/cheqd/resource/v2/resource.js'
70
83
 
71
84
  const debug = Debug('veramo:did-provider-cheqd')
72
85
 
73
- export type IContext = IAgentContext<IDIDManager & IKeyManager & IDataStore & IResolver & ICredentialIssuer & ICheqd>
86
+ export type IContext = IAgentContext<IDIDManager & IKeyManager & IDataStore & IResolver & ICredentialIssuer & ICredentialVerifier & ICheqd>
74
87
  export type TExportedDIDDocWithKeys = { didDoc: DIDDocument, keys: TImportableEd25519Key[], versionId?: string }
75
88
  export type TExportedDIDDocWithLinkedResourceWithKeys = TExportedDIDDocWithKeys & { linkedResource: LinkedResource }
76
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 }
@@ -80,28 +93,70 @@ export type ShallowTypedTxTxResponses = { height: string, txhash: string, codesp
80
93
  export type ShallowTypedTxsResponse = { txs: ShallowTypedTx[], tx_responses: ShallowTypedTxTxResponses[], pagination: string | null, total: string } | undefined
81
94
  export type VerificationResult = { verified: boolean, revoked?: boolean, suspended?: boolean, error?: IVerifyResult['error'] }
82
95
  export type StatusCheckResult = { revoked?: boolean, suspended?: boolean, error?: IError }
83
- export type RevocationResult = { revoked: boolean, error?: IError, statusList?: Bitstring, encryptedStatusList?: string, encryptedSymmetricKey?: string, symmetricKey?: string, published?: boolean, resourceMetadata?: LinkedResourceMetadataResolutionResult }
84
- export type SuspensionResult = { suspended: boolean, error?: IError, statusList?: Bitstring, encryptedStatusList?: string, encryptedSymmetricKey?: string, symmetricKey?: string, published?: boolean, resourceMetadata?: LinkedResourceMetadataResolutionResult }
85
- 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 }
86
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[] } }
87
105
  export type AccessControlConditionType = typeof AccessControlConditionTypes[keyof typeof AccessControlConditionTypes]
88
106
  export type AccessControlConditionReturnValueComparator = typeof AccessControlConditionReturnValueComparators[keyof typeof AccessControlConditionReturnValueComparators]
89
107
  export type AccessControlConditionMemoNonceArgs = { senderAddressObserved: string, recipientAddressObserved: string, amountObserved: string, specificNonce?: string, nonceFormat?: TxNonceFormat, type: Extract<AccessControlConditionType, 'memoNonce'> }
90
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[] }
91
111
  export type CreateEncryptedStatusList2021Result = { created: boolean, error?: Error, encryptedSymmetricKey: string, symmetricKey?: string, encryptedStatusList2021: string, unifiedAccessControlConditions: CosmosAccessControlCondition[] }
92
112
  export type GenerateEncryptedStatusList2021Result = { encryptedSymmetricKey: string, encryptedStatusList2021: string, unifiedAccessControlConditions: CosmosAccessControlCondition[] }
93
113
  export type TransactionResult = { successful: boolean, transactionHash?: string, events?: DeliverTxResponse['events'], rawLog?: string, txResponse?: DeliverTxResponse, error?: IError }
94
114
  export type ObservationResult = { subscribed: boolean, meetsCondition: boolean, transactionHash?: string, events?: DeliverTxResponse['events'], rawLog?: string, txResponse?: ShallowTypedTxTxResponses, error?: IError }
95
115
 
96
- 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
97
150
  export const AccessControlConditionReturnValueComparators = { lessThan: '<', greaterThan: '>', equalTo: '=', lessThanOrEqualTo: '<=', greaterThanOrEqualTo: '>=' } as const
98
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
+
99
154
  const CreateIdentifierMethodName = 'cheqdCreateIdentifier'
100
155
  const UpdateIdentifierMethodName = 'cheqdUpdateIdentifier'
101
156
  const DeactivateIdentifierMethodName = 'cheqdDeactivateIdentifier'
102
157
  const CreateResourceMethodName = 'cheqdCreateLinkedResource'
103
158
  const CreateStatusList2021MethodName = 'cheqdCreateStatusList2021'
104
- const CreateEncryptedStatusList2021MethodName = 'cheqdCreateEncryptedStatusList2021'
159
+ const BroadcastStatusList2021MethodName = 'cheqdBroadcastStatusList2021'
105
160
  const GenerateDidDocMethodName = 'cheqdGenerateDidDoc'
106
161
  const GenerateDidDocWithLinkedResourceMethodName = 'cheqdGenerateDidDocWithLinkedResource'
107
162
  const GenerateKeyPairMethodName = 'cheqdGenerateIdentityKeys'
@@ -159,6 +214,41 @@ export interface ICheqdCreateLinkedResourceArgs {
159
214
  }
160
215
 
161
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 {
162
252
  kms: string
163
253
  payload: StatusList2021ResourcePayload
164
254
  network: CheqdNetwork
@@ -167,7 +257,7 @@ export interface ICheqdCreateStatusList2021Args {
167
257
  fee?: DidStdFee
168
258
  }
169
259
 
170
- export interface ICheqdCreateEncryptedStatusList2021Args extends ICheqdCreateStatusList2021Args {
260
+ export interface ICheqdBroadcastEncryptedStatusList2021Args extends ICheqdCreateUnencryptedStatusList2021Args {
171
261
  encryptionOptions: {
172
262
  accessControlConditions: (AccessControlConditionMemoNonceArgs | AccessControlConditionBalanceArgs)[]
173
263
  returnSymmetricKey?: boolean
@@ -200,7 +290,7 @@ export interface ICheqdGenerateVersionIdArgs {
200
290
  export interface ICheqdGenerateStatusList2021Args {
201
291
  length?: number
202
292
  buffer?: Uint8Array
203
- bitstringEncoding?: 'base64' | 'base64url' | 'hex'
293
+ bitstringEncoding?: DefaultStatusList2021Encoding
204
294
  [key: string]: any
205
295
  }
206
296
 
@@ -284,7 +374,8 @@ export interface ICheqdCheckCredentialStatusWithStatusList2021Args {
284
374
  }
285
375
 
286
376
  export interface ICheqdRevokeCredentialWithStatusList2021Args {
287
- credential: W3CVerifiableCredential
377
+ credential?: W3CVerifiableCredential
378
+ revocationOptions?: ICheqdRevokeCredentialWithStatusList2021Options
288
379
  fetchList?: boolean
289
380
  publish?: boolean
290
381
  publishEncrypted?: boolean
@@ -299,7 +390,8 @@ export interface ICheqdRevokeCredentialWithStatusList2021Args {
299
390
  }
300
391
 
301
392
  export interface ICheqdRevokeBulkCredentialsWithStatusList2021Args {
302
- credentials: W3CVerifiableCredential[]
393
+ credentials?: W3CVerifiableCredential[]
394
+ revocationOptions?: ICheqdRevokeBulkCredentialsWithStatusList2021Options
303
395
  fetchList?: boolean
304
396
  publish?: boolean
305
397
  publishEncrypted?: boolean
@@ -314,7 +406,8 @@ export interface ICheqdRevokeBulkCredentialsWithStatusList2021Args {
314
406
  }
315
407
 
316
408
  export interface ICheqdSuspendCredentialWithStatusList2021Args {
317
- credential: W3CVerifiableCredential
409
+ credential?: W3CVerifiableCredential
410
+ suspensionOptions?: ICheqdSuspendCredentialWithStatusList2021Options
318
411
  fetchList?: boolean
319
412
  publish?: boolean
320
413
  publishEncrypted?: boolean
@@ -329,7 +422,8 @@ export interface ICheqdSuspendCredentialWithStatusList2021Args {
329
422
  }
330
423
 
331
424
  export interface ICheqdSuspendBulkCredentialsWithStatusList2021Args {
332
- credentials: W3CVerifiableCredential[]
425
+ credentials?: W3CVerifiableCredential[]
426
+ suspensionOptions?: ICheqdSuspendBulkCredentialsWithStatusList2021Options
333
427
  fetchList?: boolean
334
428
  publish?: boolean
335
429
  publishEncrypted?: boolean
@@ -344,7 +438,8 @@ export interface ICheqdSuspendBulkCredentialsWithStatusList2021Args {
344
438
  }
345
439
 
346
440
  export interface ICheqdUnsuspendCredentialWithStatusList2021Args {
347
- credential: W3CVerifiableCredential
441
+ credential?: W3CVerifiableCredential
442
+ unsuspensionOptions?: ICheqdUnsuspendCredentialWithStatusList2021Options
348
443
  fetchList?: boolean
349
444
  publish?: boolean
350
445
  publishEncrypted?: boolean
@@ -359,7 +454,8 @@ export interface ICheqdUnsuspendCredentialWithStatusList2021Args {
359
454
  }
360
455
 
361
456
  export interface ICheqdUnsuspendBulkCredentialsWithStatusList2021Args {
362
- credentials: W3CVerifiableCredential[]
457
+ credentials?: W3CVerifiableCredential[]
458
+ unsuspensionOptions?: ICheqdUnsuspendBulkCredentialsWithStatusList2021Options
363
459
  fetchList?: boolean
364
460
  publish?: boolean
365
461
  publishEncrypted?: boolean
@@ -397,13 +493,55 @@ export interface ICheqdStatusList2021Options {
397
493
  [key: string]: any
398
494
  }
399
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
+
400
538
  export interface ICheqd extends IPluginMethodMap {
401
539
  [CreateIdentifierMethodName]: (args: ICheqdCreateIdentifierArgs, context: IContext) => Promise<Omit<IIdentifier, 'provider'>>
402
540
  [UpdateIdentifierMethodName]: (args: ICheqdUpdateIdentifierArgs, context: IContext) => Promise<Omit<IIdentifier, 'provider'>>,
403
541
  [DeactivateIdentifierMethodName]: (args: ICheqdDeactivateIdentifierArgs, context: IContext) => Promise<boolean>,
404
542
  [CreateResourceMethodName]: (args: ICheqdCreateLinkedResourceArgs, context: IContext) => Promise<boolean>,
405
- [CreateStatusList2021MethodName]: (args: ICheqdCreateStatusList2021Args, context: IContext) => Promise<boolean>,
406
- [CreateEncryptedStatusList2021MethodName]: (args: ICheqdCreateEncryptedStatusList2021Args, context: IContext) => Promise<CreateEncryptedStatusList2021Result>,
543
+ [CreateStatusList2021MethodName]: (args: ICheqdCreateStatusList2021Args, context: IContext) => Promise<CreateStatusList2021Result>,
544
+ [BroadcastStatusList2021MethodName]: (args: ICheqdBroadcastStatusList2021Args, context: IContext) => Promise<boolean>,
407
545
  [GenerateDidDocMethodName]: (args: ICheqdGenerateDidDocArgs, context: IContext) => Promise<TExportedDIDDocWithKeys>,
408
546
  [GenerateDidDocWithLinkedResourceMethodName]: (args: ICheqdGenerateDidDocWithLinkedResourceArgs, context: IContext) => Promise<TExportedDIDDocWithLinkedResourceWithKeys>,
409
547
  [GenerateKeyPairMethodName]: (args: ICheqdGenerateKeyPairArgs, context: IContext) => Promise<TImportableEd25519Key>
@@ -416,11 +554,11 @@ export interface ICheqd extends IPluginMethodMap {
416
554
  [VerifyPresentationMethodName]: (args: ICheqdVerifyPresentationWithStatusList2021Args, context: IContext) => Promise<VerificationResult>
417
555
  [CheckCredentialStatusMethodName]: (args: ICheqdCheckCredentialStatusWithStatusList2021Args, context: IContext) => Promise<StatusCheckResult>
418
556
  [RevokeCredentialMethodName]: (args: ICheqdRevokeCredentialWithStatusList2021Args, context: IContext) => Promise<RevocationResult>
419
- [RevokeCredentialsMethodName]: (args: ICheqdRevokeBulkCredentialsWithStatusList2021Args, context: IContext) => Promise<RevocationResult[]>
557
+ [RevokeCredentialsMethodName]: (args: ICheqdRevokeBulkCredentialsWithStatusList2021Args, context: IContext) => Promise<BulkRevocationResult>
420
558
  [SuspendCredentialMethodName]: (args: ICheqdSuspendCredentialWithStatusList2021Args, context: IContext) => Promise<SuspensionResult>
421
- [SuspendCredentialsMethodName]: (args: ICheqdSuspendBulkCredentialsWithStatusList2021Args, context: IContext) => Promise<SuspensionResult[]>
559
+ [SuspendCredentialsMethodName]: (args: ICheqdSuspendBulkCredentialsWithStatusList2021Args, context: IContext) => Promise<BulkSuspensionResult>
422
560
  [UnsuspendCredentialMethodName]: (args: ICheqdUnsuspendCredentialWithStatusList2021Args, context: IContext) => Promise<UnsuspensionResult>
423
- [UnsuspendCredentialsMethodName]: (args: ICheqdUnsuspendBulkCredentialsWithStatusList2021Args, context: IContext) => Promise<UnsuspensionResult[]>
561
+ [UnsuspendCredentialsMethodName]: (args: ICheqdUnsuspendBulkCredentialsWithStatusList2021Args, context: IContext) => Promise<BulkUnsuspensionResult>
424
562
  [TransactVerifierPaysIssuerMethodName]: (args: ICheqdTransactVerifierPaysIssuerArgs, context: IContext) => Promise<TransactionResult>
425
563
  [ObserveVerifierPaysIssuerMethodName]: (args: ICheqdObserveVerifierPaysIssuerArgs, context: IContext) => Promise<ObservationResult>
426
564
  }
@@ -518,17 +656,17 @@ export class Cheqd implements IAgentPlugin {
518
656
  ]
519
657
  },
520
658
  "returnType": {
521
- "type": "boolean"
659
+ "type": "object"
522
660
  }
523
661
  },
524
- "cheqdCreateEncryptedStatusList2021": {
525
- "description": "Create a new Encrypted Status List 2021",
662
+ "cheqdBroadcastStatusList2021": {
663
+ "description": "Broadcast a Status List 2021 to cheqd ledger",
526
664
  "arguments": {
527
665
  "type": "object",
528
666
  "properties": {
529
667
  "args": {
530
668
  "type": "object",
531
- "description": "A cheqdCreateEncryptedStatusList2021Args object as any for extensibility"
669
+ "description": "A cheqdBroadcastStatusList2021Args object as any for extensibility"
532
670
  }
533
671
  },
534
672
  "required": [
@@ -898,7 +1036,7 @@ export class Cheqd implements IAgentPlugin {
898
1036
  [DeactivateIdentifierMethodName]: this.DeactivateIdentifier.bind(this),
899
1037
  [CreateResourceMethodName]: this.CreateResource.bind(this),
900
1038
  [CreateStatusList2021MethodName]: this.CreateStatusList2021.bind(this),
901
- [CreateEncryptedStatusList2021MethodName]: this.CreateEncryptedStatusList2021.bind(this),
1039
+ [BroadcastStatusList2021MethodName]: this.BroadcastStatusList2021.bind(this),
902
1040
  [GenerateDidDocMethodName]: this.GenerateDidDoc.bind(this),
903
1041
  [GenerateDidDocWithLinkedResourceMethodName]: this.GenerateDidDocWithLinkedResource.bind(this),
904
1042
  [GenerateKeyPairMethodName]: this.GenerateIdentityKeys.bind(this),
@@ -1041,41 +1179,145 @@ export class Cheqd implements IAgentPlugin {
1041
1179
  throw new Error('[did-provider-cheqd]: kms is required')
1042
1180
  }
1043
1181
 
1044
- if (typeof args.payload !== 'object') {
1045
- 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')
1046
1184
  }
1047
1185
 
1048
- if (typeof args.network !== 'string') {
1049
- 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')
1050
1188
  }
1051
1189
 
1052
- if (args?.file) {
1053
- 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')
1054
1192
  }
1055
1193
 
1056
- if (typeof args?.payload?.data === 'string') {
1057
- args.payload.data = fromString(args.payload.data, 'base64')
1194
+ if (typeof args.encrypted === 'undefined') {
1195
+ throw new Error('[did-provider-cheqd]: encrypted is required')
1058
1196
  }
1059
1197
 
1060
- // 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
+ }
1061
1202
 
1062
- // set default resource type in runtime
1063
- 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
+ }
1064
1208
 
1065
- this.providerId = Cheqd.generateProviderId(args.network)
1066
- 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
+ }
1067
1213
 
1068
- return await this.didProvider.createResource({
1069
- options: {
1070
- kms: args.kms,
1071
- payload: args.payload,
1072
- signInputs: args.signInputs,
1073
- 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')
1074
1218
  }
1075
- }, 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
1076
1318
  }
1077
1319
 
1078
- private async CreateEncryptedStatusList2021(args: ICheqdCreateEncryptedStatusList2021Args, context: IContext): Promise<CreateEncryptedStatusList2021Result> {
1320
+ private async BroadcastStatusList2021(args: ICheqdBroadcastStatusList2021Args, context: IContext) {
1079
1321
  if (typeof args.kms !== 'string') {
1080
1322
  throw new Error('[did-provider-cheqd]: kms is required')
1081
1323
  }
@@ -1098,65 +1340,15 @@ export class Cheqd implements IAgentPlugin {
1098
1340
 
1099
1341
  // TODO: validate data as per bitstring
1100
1342
 
1101
- if (!args?.encryptionOptions) {
1102
- throw new Error('[did-provider-cheqd]: encryptionOptions is required')
1103
- }
1104
-
1105
- if (!args?.bootstrapOptions) {
1106
- throw new Error('[did-provider-cheqd]: bootstrapOptions is required')
1107
- }
1108
-
1109
- if (!args?.encryptionOptions?.accessControlConditions) {
1110
- 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(', ')}`)
1111
1346
  }
1112
1347
 
1113
- // instantiate dkg-threshold client, in which case lit-protocol is used
1114
- const lit = await LitProtocol.create({
1115
- chain: args.bootstrapOptions?.chain,
1116
- litNetwork: args.bootstrapOptions?.litNetwork
1117
- })
1118
-
1119
- // construct access control conditions
1120
- const unifiedAccessControlConditions = await Promise.all(args.encryptionOptions.accessControlConditions.map(async (condition) => {
1121
- switch (condition.type) {
1122
- case AccessControlConditionTypes.memoNonce:
1123
- return await LitProtocol.generateCosmosAccessControlConditionTransactionMemo({
1124
- key: '$.txs.*.body.memo',
1125
- comparator: 'contains',
1126
- value: condition?.specificNonce || await LitProtocol.generateTxNonce(condition?.nonceFormat)
1127
- },
1128
- condition.amountObserved,
1129
- condition.senderAddressObserved,
1130
- condition.recipientAddressObserved,
1131
- args.bootstrapOptions.chain
1132
- )
1133
- case AccessControlConditionTypes.balance:
1134
- return await LitProtocol.generateCosmosAccessControlConditionBalance({
1135
- key: '$.balances[0].amount',
1136
- comparator: condition.comparator,
1137
- value: condition.amountObserved
1138
- },
1139
- args.bootstrapOptions.chain,
1140
- condition.addressObserved
1141
- )
1142
- default:
1143
- throw new Error(`[did-provider-cheqd]: accessControlCondition type is not supported`)
1144
- }
1145
- }))
1146
-
1147
- // encrypt data
1148
- const { encryptedString, encryptedSymmetricKey, symmetricKey } = await lit.encrypt(toString(args.payload.data!, 'base64url'), unifiedAccessControlConditions, true)
1149
-
1150
- // set encrypted data
1151
- args.payload.data = new Uint8Array(await encryptedString.arrayBuffer())
1152
-
1153
- // set default resource type in runtime
1154
- args.payload.resourceType = 'StatusList2021'
1155
-
1156
1348
  this.providerId = Cheqd.generateProviderId(args.network)
1157
1349
  this.didProvider = await Cheqd.loadProvider({ id: this.providerId } as DIDDocument, this.supportedDidProviders)
1158
1350
 
1159
- const created = await this.didProvider.createResource({
1351
+ return await this.didProvider.createResource({
1160
1352
  options: {
1161
1353
  kms: args.kms,
1162
1354
  payload: args.payload,
@@ -1164,14 +1356,6 @@ export class Cheqd implements IAgentPlugin {
1164
1356
  fee: args?.fee
1165
1357
  }
1166
1358
  }, context)
1167
-
1168
- return {
1169
- created,
1170
- encryptedSymmetricKey,
1171
- encryptedStatusList2021: await blobToHexString(encryptedString),
1172
- symmetricKey: args?.encryptionOptions?.returnSymmetricKey ? toString(symmetricKey!, 'hex') : undefined,
1173
- unifiedAccessControlConditions
1174
- } satisfies CreateEncryptedStatusList2021Result
1175
1359
  }
1176
1360
 
1177
1361
  private async GenerateDidDoc(
@@ -1360,7 +1544,7 @@ export class Cheqd implements IAgentPlugin {
1360
1544
  : args.issuanceOptions.credential.issuer as string
1361
1545
 
1362
1546
  // generate status list credential
1363
- const statusListCredential = `${resolverUrl}${issuer}?resourceName=${args.statusOptions.statusListName}`
1547
+ const statusListCredential = `${resolverUrl}${issuer}?resourceName=${args.statusOptions.statusListName}&resourceType=StatusList2021Revocation`
1364
1548
 
1365
1549
  // construct credential status
1366
1550
  const credentialStatus = {
@@ -1368,7 +1552,6 @@ export class Cheqd implements IAgentPlugin {
1368
1552
  type: 'StatusList2021Entry',
1369
1553
  statusPurpose: 'revocation',
1370
1554
  statusListIndex: `${statusListIndex}`,
1371
- statusListCredential,
1372
1555
  }
1373
1556
 
1374
1557
  // add credential status to credential
@@ -1412,7 +1595,7 @@ export class Cheqd implements IAgentPlugin {
1412
1595
  : args.issuanceOptions.credential.issuer as string
1413
1596
 
1414
1597
  // generate status list credential
1415
- const statusListCredential = `${resolverUrl}${issuer}?resourceName=${args.statusOptions.statusListName}`
1598
+ const statusListCredential = `${resolverUrl}${issuer}?resourceName=${args.statusOptions.statusListName}&resourceType=StatusList2021Suspension`
1416
1599
 
1417
1600
  // construct credential status
1418
1601
  const credentialStatus = {
@@ -1420,7 +1603,6 @@ export class Cheqd implements IAgentPlugin {
1420
1603
  type: 'StatusList2021Entry',
1421
1604
  statusPurpose: 'suspension',
1422
1605
  statusListIndex: `${statusListIndex}`,
1423
- statusListCredential,
1424
1606
  }
1425
1607
 
1426
1608
  // add credential status to credential
@@ -1468,7 +1650,9 @@ export class Cheqd implements IAgentPlugin {
1468
1650
  return { verified: false, error: verificationResult.error }
1469
1651
  }
1470
1652
 
1653
+ // if jwt credential, decode it
1471
1654
  const credential = typeof args.credential === 'string' ? await Cheqd.decodeCredentialJWT(args.credential) : args.credential
1655
+
1472
1656
  // verify credential status
1473
1657
  switch (credential.credentialStatus?.statusPurpose) {
1474
1658
  case 'revocation':
@@ -1519,7 +1703,9 @@ export class Cheqd implements IAgentPlugin {
1519
1703
  }
1520
1704
 
1521
1705
  private async CheckCredentialStatusWithStatusList2021(args: ICheqdCheckCredentialStatusWithStatusList2021Args, context: IContext): Promise<StatusCheckResult> {
1706
+ // if jwt credential, decode it
1522
1707
  const credential = typeof args.credential === 'string' ? await Cheqd.decodeCredentialJWT(args.credential) : args.credential
1708
+
1523
1709
  switch (credential.credentialStatus?.statusPurpose) {
1524
1710
  case 'revocation':
1525
1711
  if (await Cheqd.checkRevoked(credential, { ...args.options, topArgs: args })) return { revoked: true }
@@ -1533,7 +1719,57 @@ export class Cheqd implements IAgentPlugin {
1533
1719
  }
1534
1720
 
1535
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
1536
1771
  const credential = typeof args.credential === 'string' ? await Cheqd.decodeCredentialJWT(args.credential) : args.credential
1772
+
1537
1773
  // validate status purpose
1538
1774
  if (credential.credentialStatus?.statusPurpose !== 'revocation') {
1539
1775
  throw new Error(`[did-provider-cheqd]: revocation: Unsupported status purpose: ${credential.credentialStatus?.statusPurpose}`)
@@ -1565,21 +1801,160 @@ export class Cheqd implements IAgentPlugin {
1565
1801
  topArgs: args,
1566
1802
  publishOptions: {
1567
1803
  context,
1804
+ statusListEncoding: args?.options?.statusListEncoding,
1805
+ statusListValidUntil: args?.options?.statusListValidUntil,
1568
1806
  resourceId: args?.options?.resourceId,
1569
1807
  resourceVersion: args?.options?.resourceVersion,
1808
+ resourceAlsoKnownAs: args?.options?.alsoKnownAs,
1570
1809
  signInputs: args?.options?.signInputs,
1571
1810
  fee: args?.options?.fee
1572
1811
  }
1573
1812
  })
1574
1813
  }
1575
1814
 
1576
- private async RevokeBulkCredentialsWithStatusList2021(args: ICheqdRevokeBulkCredentialsWithStatusList2021Args, context: IContext): Promise<RevocationResult[]> {
1577
- // TODO: implement
1578
- throw new Error('[did-provider-cheqd]: revocation: bulk revocation is not implemented yet')
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
+ }))
1827
+
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
+ }))
1863
+ }
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
+
1871
+ // validate args in pairs - case: statusListFile and statusList
1872
+ if (args.options?.statusListFile && args.options?.statusList) {
1873
+ throw new Error('[did-provider-cheqd]: revocation: statusListFile and statusList are mutually exclusive')
1874
+ }
1875
+
1876
+ // validate args in pairs - case: statusListFile and fetchList
1877
+ if (args.options?.statusListFile && args.options?.fetchList) {
1878
+ throw new Error('[did-provider-cheqd]: revocation: statusListFile and fetchList are mutually exclusive')
1879
+ }
1880
+
1881
+ // validate args in pairs - case: statusList and fetchList
1882
+ if (args.options?.statusList && args.options?.fetchList) {
1883
+ throw new Error('[did-provider-cheqd]: revocation: statusList and fetchList are mutually exclusive')
1884
+ }
1885
+
1886
+ // validate args in pairs - case: publish
1887
+ if (args.options?.publish && !args.fetchList && !(args.options?.statusListFile || args.options?.statusList)) {
1888
+ throw new Error('[did-provider-cheqd]: revocation: publish requires statusListFile or statusList, if fetchList is disabled')
1889
+ }
1890
+
1891
+ // revoke credentials
1892
+ return await Cheqd.revokeCredentials(credentials, {
1893
+ ...args.options,
1894
+ topArgs: args,
1895
+ publishOptions: {
1896
+ context,
1897
+ resourceId: args?.options?.resourceId,
1898
+ resourceVersion: args?.options?.resourceVersion,
1899
+ resourceAlsoKnownAs: args?.options?.alsoKnownAs,
1900
+ signInputs: args?.options?.signInputs,
1901
+ fee: args?.options?.fee
1902
+ }
1903
+ })
1579
1904
  }
1580
1905
 
1581
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
1582
1956
  const credential = typeof args.credential === 'string' ? await Cheqd.decodeCredentialJWT(args.credential) : args.credential
1957
+
1583
1958
  // validate status purpose
1584
1959
  if (credential.credentialStatus?.statusPurpose !== 'suspension') {
1585
1960
  throw new Error(`[did-provider-cheqd]: suspension: Unsupported status purpose: ${credential.credentialStatus?.statusPurpose}`)
@@ -1611,26 +1986,73 @@ export class Cheqd implements IAgentPlugin {
1611
1986
  topArgs: args,
1612
1987
  publishOptions: {
1613
1988
  context,
1989
+ statusListEncoding: args?.options?.statusListEncoding,
1990
+ statusListValidUntil: args?.options?.statusListValidUntil,
1614
1991
  resourceId: args?.options?.resourceId,
1615
1992
  resourceVersion: args?.options?.resourceVersion,
1993
+ resourceAlsoKnownAs: args?.options?.alsoKnownAs,
1616
1994
  signInputs: args?.options?.signInputs,
1617
1995
  fee: args?.options?.fee
1618
1996
  }
1619
1997
  })
1620
1998
  }
1621
1999
 
1622
- private async SuspendBulkCredentialsWithStatusList2021(args: ICheqdSuspendBulkCredentialsWithStatusList2021Args, context: IContext): Promise<SuspensionResult[]> {
1623
- // TODO: implement
1624
- throw new Error('[did-provider-cheqd]: suspension: bulk suspension is not implemented yet')
1625
- }
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
+ }))
1626
2012
 
1627
- private async UnsuspendCredentialWithStatusList2021(args: ICheqdUnsuspendCredentialWithStatusList2021Args, context: IContext): Promise<UnsuspensionResult> {
1628
- const credential = typeof args.credential === 'string' ? await Cheqd.decodeCredentialJWT(args.credential) : args.credential
1629
- // validate status purpose
1630
- if (credential.credentialStatus?.statusPurpose !== 'suspension') {
1631
- throw new Error(`[did-provider-cheqd]: suspension: Unsupported status purpose: ${credential.credentialStatus?.statusPurpose}`)
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
+ }
2018
+ }
2019
+
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')
2024
+
2025
+ // validate suspension options - case: suspensionOptions.statusListName
2026
+ if (!args.suspensionOptions.statusListName) throw new Error('[did-provider-cheqd]: suspension: suspensionOptions.statusListName is required')
2027
+
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')
2030
+
2031
+ // construct status list credential
2032
+ const statusListCredential = `${resolverUrl}${args.suspensionOptions.issuerDid}?resourceName=${args.suspensionOptions.statusListName}&resourceType=StatusList2021Suspension`
2033
+
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
+ }))
1632
2048
  }
1633
2049
 
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
+
1634
2056
  // validate args in pairs - case: statusListFile and statusList
1635
2057
  if (args.options?.statusListFile && args.options?.statusList) {
1636
2058
  throw new Error('[did-provider-cheqd]: suspension: statusListFile and statusList are mutually exclusive')
@@ -1651,44 +2073,225 @@ export class Cheqd implements IAgentPlugin {
1651
2073
  throw new Error('[did-provider-cheqd]: suspension: publish requires statusListFile or statusList, if fetchList is disabled')
1652
2074
  }
1653
2075
 
1654
- // suspend credential
1655
- return await Cheqd.unsuspendCredential(credential, {
2076
+ // suspend credentials
2077
+ return await Cheqd.suspendCredentials(credentials, {
1656
2078
  ...args.options,
1657
2079
  topArgs: args,
1658
2080
  publishOptions: {
1659
2081
  context,
1660
2082
  resourceId: args?.options?.resourceId,
1661
2083
  resourceVersion: args?.options?.resourceVersion,
2084
+ resourceAlsoKnownAs: args?.options?.alsoKnownAs,
1662
2085
  signInputs: args?.options?.signInputs,
1663
2086
  fee: args?.options?.fee
1664
2087
  }
1665
2088
  })
1666
2089
  }
1667
2090
 
1668
- private async UnsuspendBulkCredentialsWithStatusList2021(args: ICheqdUnsuspendBulkCredentialsWithStatusList2021Args, context: IContext): Promise<UnsuspensionResult[]> {
1669
- // TODO: implement
1670
- throw new Error('[did-provider-cheqd]: suspension: bulk unsuspension is not implemented yet')
1671
- }
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)
1672
2100
 
1673
- private async TransactVerifierPaysIssuer(args: ICheqdTransactVerifierPaysIssuerArgs, context: IContext): Promise<TransactionResult> {
1674
- try {
1675
- // delegate to provider
1676
- const transactionResult = await this.didProvider.transactSendTokens({
1677
- recipientAddress: args.recipientAddress,
1678
- amount: args.amount,
1679
- memoNonce: args.memoNonce,
1680
- txBytes: args.txBytes,
1681
- })
2101
+ // early return if verification failed
2102
+ if (!verificationResult.verified) {
2103
+ return { unsuspended: false, error: verificationResult.error }
2104
+ }
2105
+ }
1682
2106
 
1683
- // return transaction result
1684
- return {
1685
- successful: !transactionResult.code,
1686
- transactionHash: transactionResult.transactionHash,
1687
- events: transactionResult.events,
1688
- rawLog: transactionResult.rawLog,
1689
- txResponse: args?.returnTxResponse ? transactionResult : undefined
1690
- } satisfies TransactionResult
1691
- } catch (error) {
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) {
1692
2295
  // return error
1693
2296
  return {
1694
2297
  successful: false,
@@ -1744,113 +2347,739 @@ export class Cheqd implements IAgentPlugin {
1744
2347
  }
1745
2348
  }
1746
2349
 
1747
- // validate access control conditions components - case: senderAddress
1748
- if (!args.senderAddress) {
1749
- throw new Error('[did-provider-cheqd]: observation: senderAddress is required')
1750
- }
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
+ })
1751
2912
 
1752
- // validate access control conditions components - case: recipientAddress
1753
- if (!args.recipientAddress) {
1754
- throw new Error('[did-provider-cheqd]: observation: recipientAddress is required')
1755
- }
2913
+ // encrypt
2914
+ const { encryptedString, encryptedSymmetricKey, symmetricKey } = await lit.encrypt(bitstring, options?.topArgs?.encryptionOptions?.unifiedAccessControlConditions, true)
1756
2915
 
1757
- // validate access control conditions components - case: amount
1758
- if (!args.amount || !args.amount.amount || !args.amount.denom || args.amount.denom !== 'ncheq') {
1759
- 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`')
1760
- }
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
+ }
1761
2927
 
1762
- // validate access control conditions components - case: memoNonce
1763
- if (!args.memoNonce) {
1764
- throw new Error('[did-provider-cheqd]: observation: memoNonce is required')
1765
- }
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)')
1766
2932
 
1767
- // validate access control conditions components - case: network
1768
- if (!args.network) {
1769
- throw new Error('[did-provider-cheqd]: observation: network is required')
1770
- }
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)')
1771
2935
 
1772
- try {
1773
- // otherwise, construct url, as per components
1774
- 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}'`
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
- // fetch relevant txs
1777
- const txs = await (await fetch(url)).json() as ShallowTypedTxsResponse
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
+ }
1778
2942
 
1779
- // skim through txs for relevant events, in which case memoNonce is present and strict equals to the one provided
1780
- const meetsConditionTxIndex = txs?.txs?.findIndex(tx => unescapeUnicode(tx.body.memo) === unescapeUnicode(args.memoNonce))
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
1781
2956
 
1782
- // define meetsCondition
1783
- const meetsCondition = (typeof meetsConditionTxIndex !== 'undefined' && meetsConditionTxIndex !== -1)
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
+ }())
2963
+
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
1784
2971
 
1785
- // return observation result
1786
2972
  return {
1787
- subscribed: true,
1788
- meetsCondition: meetsCondition,
1789
- transactionHash: meetsCondition ? txs!.tx_responses[meetsConditionTxIndex].txhash : undefined,
1790
- events: meetsCondition ? txs!.tx_responses[meetsConditionTxIndex].events : undefined,
1791
- rawLog: meetsCondition ? txs!.tx_responses[meetsConditionTxIndex].raw_log : undefined,
1792
- txResponse: meetsCondition ? (args?.returnTxResponse ? txs!.tx_responses[meetsConditionTxIndex] : undefined) : undefined
1793
- } 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
1794
2981
  } catch (error) {
1795
- // return error
1796
- return {
1797
- subscribed: false,
1798
- meetsCondition: false,
1799
- error: error as IError
1800
- } satisfies ObservationResult
2982
+ // silent fail + early exit
2983
+ console.error(error)
2984
+
2985
+ return { suspended: false, error: error as IError } satisfies SuspensionResult
1801
2986
  }
1802
2987
  }
1803
2988
 
1804
- static async revokeCredential(credential: VerifiableCredential, options?: ICheqdStatusList2021Options): Promise<RevocationResult> {
1805
- try {
1806
- // validate status purpose
1807
- 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')
1808
2992
 
1809
- // fetch status list 2021 metadata
1810
- 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')
1811
2999
 
1812
- // detect if encrypted
1813
- const isEncrypted = function() {
1814
- switch (metadata.mediaType) {
1815
- case 'application/octet-stream':
1816
- return true
1817
- case 'application/gzip':
1818
- return false
1819
- default:
1820
- throw new Error(`[did-provider-cheqd]: revocation: Unsupported media type: ${metadata.mediaType}`)
1821
- }
1822
- }()
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
1823
3026
 
1824
3027
  // early return, if encrypted and no decryption key provided
1825
- 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')
1826
3029
 
1827
3030
  // fetch status list 2021 inscribed in credential
1828
3031
  const statusList2021 = options?.topArgs?.fetchList
1829
3032
  ? (await async function () {
1830
3033
  // if not encrypted, return bitstring
1831
- 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')
1832
3038
 
1833
3039
  // otherwise, decrypt and return bitstring
1834
- const scopedRawBlob = await toBlob(await Cheqd.fetchStatusList2021(credential, true) as Uint8Array)
3040
+ const scopedRawBlob = await toBlob(await Cheqd.fetchStatusList2021(credentials[0], true) as Uint8Array)
1835
3041
 
1836
3042
  // decrypt
1837
3043
  return await LitProtocol.decryptDirect(scopedRawBlob, fromString(options?.topArgs?.symmetricKey, 'hex'))
1838
3044
  }())
1839
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
+
1840
3051
  // if status list 2021 is not fetched, read from file
1841
3052
  if (options?.statusListFile) {
1842
3053
  // if not encrypted, return bitstring
1843
- 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
+ }
1844
3064
 
1845
3065
  // otherwise, decrypt and return bitstring
1846
3066
  const scopedRawBlob = await toBlob(await Cheqd.getFile(options.statusListFile))
1847
3067
 
1848
3068
  // decrypt
1849
- 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
1850
3076
  }
1851
3077
 
1852
3078
  if (!options?.statusListInlineBitstring) throw new Error('[did-provider-cheqd]: revocation: statusListInlineBitstring is required, if statusListFile is not provided')
1853
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
+
1854
3083
  // otherwise, read from inline bitstring
1855
3084
  return options?.statusListInlineBitstring
1856
3085
  }())
@@ -1858,11 +3087,26 @@ export class Cheqd implements IAgentPlugin {
1858
3087
  // parse status list 2021
1859
3088
  const statusList = await StatusList.decode({ encodedList: statusList2021 })
1860
3089
 
1861
- // early exit, if credential is already revoked
1862
- 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 }
1863
3095
 
1864
- // update revocation status
1865
- 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 })))}`)
1866
3110
 
1867
3111
  // set in-memory status list ref
1868
3112
  const bitstring = await statusList.encode() as Bitstring
@@ -1879,7 +3123,7 @@ export class Cheqd implements IAgentPlugin {
1879
3123
  const published = topArgs?.publish
1880
3124
  ? (await async function () {
1881
3125
  // fetch status list 2021 metadata
1882
- const statusListMetadata = await Cheqd.fetchStatusList2021Metadata(credential)
3126
+ const statusListMetadata = await Cheqd.fetchStatusList2021Metadata(credentials[0])
1883
3127
 
1884
3128
  // publish status list 2021 as new version
1885
3129
  const scoped = topArgs.publishEncrypted
@@ -1899,10 +3143,50 @@ export class Cheqd implements IAgentPlugin {
1899
3143
  { encryptedString, encryptedSymmetricKey, symmetricKey: toString(symmetricKey!, 'hex') }
1900
3144
  ]
1901
3145
  }())
1902
- : [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
+ }())
1903
3187
 
1904
3188
  // early exit, if publish failed
1905
- 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')
1906
3190
 
1907
3191
  // return publish result
1908
3192
  return scoped
@@ -1910,50 +3194,41 @@ export class Cheqd implements IAgentPlugin {
1910
3194
  : undefined
1911
3195
 
1912
3196
  return {
1913
- revoked: true,
3197
+ suspended: suspended.map((result) => result.status === 'fulfilled' ? result.value.suspended : false),
1914
3198
  published: topArgs?.publish ? true : undefined,
1915
- statusList: topArgs?.returnUpdatedStatusList ? bitstring : undefined,
3199
+ statusList: topArgs?.returnUpdatedStatusList ? await Cheqd.fetchStatusList2021(credentials[0]) as StatusList2021Suspension : undefined,
1916
3200
  encryptedStatusList: topArgs?.returnUpdatedEncryptedStatusList ? await blobToHexString((published?.[1] as { encryptedString: Blob })?.encryptedString) : undefined,
1917
3201
  encryptedSymmetricKey: topArgs?.returnEncryptedSymmetricKey ? (published?.[1] as { encryptedSymmetricKey: string })?.encryptedSymmetricKey : undefined,
1918
3202
  symmetricKey: topArgs?.returnSymmetricKey ? (published?.[1] as { symmetricKey: string })?.symmetricKey : undefined,
1919
- resourceMetadata: topArgs?.returnStatusListMetadata ? await Cheqd.fetchStatusList2021Metadata(credential) : undefined
1920
- } satisfies RevocationResult
3203
+ resourceMetadata: topArgs?.returnStatusListMetadata ? await Cheqd.fetchStatusList2021Metadata(credentials[0]) : undefined
3204
+ } satisfies BulkSuspensionResult
1921
3205
  } catch (error) {
1922
- // silent fail + early exit, optimised for parallelisation, use with Promise.allSettled
3206
+ // silent fail + early exit
1923
3207
  console.error(error)
1924
3208
 
1925
- return { revoked: false, error: error as IError } satisfies RevocationResult
3209
+ return { suspended: [], error: error as IError } satisfies BulkSuspensionResult
1926
3210
  }
1927
3211
  }
1928
3212
 
1929
- static async suspendCredential(credential: VerifiableCredential, options?: ICheqdStatusList2021Options): Promise<SuspensionResult> {
3213
+ static async unsuspendCredential(credential: VerifiableCredential, options?: ICheqdStatusList2021Options): Promise<UnsuspensionResult> {
1930
3214
  try {
1931
3215
  // validate status purpose
1932
- if (credential?.credentialStatus?.statusPurpose !== 'suspension') throw new Error('[did-provider-cheqd]: suspension: Invalid status purpose')
1933
-
1934
- // fetch status list 2021 metadata
1935
- const metadata = (await Cheqd.fetchStatusList2021Metadata(credential))
3216
+ if (credential?.credentialStatus?.statusPurpose !== 'suspension') throw new Error('[did-provider-cheqd]: unsuspension: Invalid status purpose')
1936
3217
 
1937
- // detect if encrypted
1938
- const isEncrypted = function() {
1939
- switch (metadata.mediaType) {
1940
- case 'application/octet-stream':
1941
- return true
1942
- case 'application/gzip':
1943
- return false
1944
- default:
1945
- throw new Error(`[did-provider-cheqd]: suspension: Unsupported media type: ${metadata.mediaType}`)
1946
- }
1947
- }()
3218
+ // fetch status list 2021
3219
+ const publishedList = (await Cheqd.fetchStatusList2021(credential)) as StatusList2021Suspension
1948
3220
 
1949
3221
  // early return, if encrypted and no decryption key provided
1950
- 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')
1951
3223
 
1952
3224
  // fetch status list 2021 inscribed in credential
1953
3225
  const statusList2021 = options?.topArgs?.fetchList
1954
3226
  ? (await async function () {
1955
3227
  // if not encrypted, return bitstring
1956
- 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')
1957
3232
 
1958
3233
  // otherwise, decrypt and return bitstring
1959
3234
  const scopedRawBlob = await toBlob(await Cheqd.fetchStatusList2021(credential, true) as Uint8Array)
@@ -1962,19 +3237,42 @@ export class Cheqd implements IAgentPlugin {
1962
3237
  return await LitProtocol.decryptDirect(scopedRawBlob, fromString(options?.topArgs?.symmetricKey, 'hex'))
1963
3238
  }())
1964
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
+
1965
3245
  // if status list 2021 is not fetched, read from file
1966
3246
  if (options?.statusListFile) {
1967
3247
  // if not encrypted, return bitstring
1968
- 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
+ }
1969
3258
 
1970
3259
  // otherwise, decrypt and return bitstring
1971
3260
  const scopedRawBlob = await toBlob(await Cheqd.getFile(options.statusListFile))
1972
3261
 
1973
3262
  // decrypt
1974
- 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
1975
3270
  }
1976
3271
 
1977
- 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')
1978
3276
 
1979
3277
  // otherwise, read from inline bitstring
1980
3278
  return options?.statusListInlineBitstring
@@ -1983,11 +3281,11 @@ export class Cheqd implements IAgentPlugin {
1983
3281
  // parse status list 2021
1984
3282
  const statusList = await StatusList.decode({ encodedList: statusList2021 })
1985
3283
 
1986
- // early exit, if already suspended
1987
- 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
1988
3286
 
1989
3287
  // update suspension status
1990
- statusList.setStatus(Number(credential.credentialStatus.statusListIndex), true)
3288
+ statusList.setStatus(Number(credential.credentialStatus.statusListIndex), false)
1991
3289
 
1992
3290
  // set in-memory status list ref
1993
3291
  const bitstring = await statusList.encode() as Bitstring
@@ -2024,10 +3322,50 @@ export class Cheqd implements IAgentPlugin {
2024
3322
  { encryptedString, encryptedSymmetricKey, symmetricKey: toString(symmetricKey!, 'hex') }
2025
3323
  ]
2026
3324
  }())
2027
- : [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
+ }())
2028
3366
 
2029
3367
  // early exit, if publish failed
2030
- 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')
2031
3369
 
2032
3370
  // return publish result
2033
3371
  return scoped
@@ -2035,71 +3373,115 @@ export class Cheqd implements IAgentPlugin {
2035
3373
  : undefined
2036
3374
 
2037
3375
  return {
2038
- suspended: true,
3376
+ unsuspended: true,
2039
3377
  published: topArgs?.publish ? true : undefined,
2040
- statusList: topArgs?.returnUpdatedStatusList ? bitstring : undefined,
3378
+ statusList: topArgs?.returnUpdatedStatusList ? await Cheqd.fetchStatusList2021(credential) as StatusList2021Suspension : undefined,
2041
3379
  encryptedStatusList: topArgs?.returnUpdatedEncryptedStatusList ? await blobToHexString((published?.[1] as { encryptedString: Blob })?.encryptedString) : undefined,
2042
3380
  encryptedSymmetricKey: topArgs?.returnEncryptedSymmetricKey ? (published?.[1] as { encryptedSymmetricKey: string })?.encryptedSymmetricKey : undefined,
2043
3381
  symmetricKey: topArgs?.returnSymmetricKey ? (published?.[1] as { symmetricKey: string })?.symmetricKey : undefined,
2044
3382
  resourceMetadata: topArgs?.returnStatusListMetadata ? await Cheqd.fetchStatusList2021Metadata(credential) : undefined
2045
- } satisfies SuspensionResult
3383
+ } satisfies UnsuspensionResult
2046
3384
  } catch (error) {
2047
- // silent fail + early exit, optimised for parallelisation, use with Promise.allSettled
3385
+ // silent fail + early exit
2048
3386
  console.error(error)
2049
3387
 
2050
- return { suspended: false, error: error as IError } satisfies SuspensionResult
3388
+ return { unsuspended: false, error: error as IError } satisfies UnsuspensionResult
2051
3389
  }
2052
3390
  }
2053
3391
 
2054
- static async unsuspendCredential(credential: VerifiableCredential, options?: ICheqdStatusList2021Options): Promise<UnsuspensionResult> {
2055
- try {
2056
- // validate status purpose
2057
- 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')
2058
3395
 
2059
- // fetch status list 2021 metadata
2060
- 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')
2061
3402
 
2062
- // detect if encrypted
2063
- const isEncrypted = function() {
2064
- switch (metadata.mediaType) {
2065
- case 'application/octet-stream':
2066
- return true
2067
- case 'application/gzip':
2068
- return false
2069
- default:
2070
- throw new Error(`[did-provider-cheqd]: unsuspension: Unsupported media type: ${metadata.mediaType}`)
2071
- }
2072
- }()
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
2073
3429
 
2074
3430
  // early return, if encrypted and no decryption key provided
2075
- 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')
2076
3432
 
2077
3433
  // fetch status list 2021 inscribed in credential
2078
3434
  const statusList2021 = options?.topArgs?.fetchList
2079
3435
  ? (await async function () {
2080
3436
  // if not encrypted, return bitstring
2081
- 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')
2082
3441
 
2083
3442
  // otherwise, decrypt and return bitstring
2084
- const scopedRawBlob = await toBlob(await Cheqd.fetchStatusList2021(credential, true) as Uint8Array)
3443
+ const scopedRawBlob = await toBlob(await Cheqd.fetchStatusList2021(credentials[0], true) as Uint8Array)
2085
3444
 
2086
3445
  // decrypt
2087
3446
  return await LitProtocol.decryptDirect(scopedRawBlob, fromString(options?.topArgs?.symmetricKey, 'hex'))
2088
3447
  }())
2089
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
+
2090
3454
  // if status list 2021 is not fetched, read from file
2091
3455
  if (options?.statusListFile) {
2092
3456
  // if not encrypted, return bitstring
2093
- 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
+ }
2094
3467
 
2095
3468
  // otherwise, decrypt and return bitstring
2096
3469
  const scopedRawBlob = await toBlob(await Cheqd.getFile(options.statusListFile))
2097
3470
 
2098
3471
  // decrypt
2099
- 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
2100
3479
  }
2101
3480
 
2102
- 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')
2103
3485
 
2104
3486
  // otherwise, read from inline bitstring
2105
3487
  return options?.statusListInlineBitstring
@@ -2108,17 +3490,32 @@ export class Cheqd implements IAgentPlugin {
2108
3490
  // parse status list 2021
2109
3491
  const statusList = await StatusList.decode({ encodedList: statusList2021 })
2110
3492
 
2111
- // early exit, if already unsuspended
2112
- 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 }
2113
3498
 
2114
- // update suspension status
2115
- 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 })))}`)
2116
3513
 
2117
3514
  // set in-memory status list ref
2118
3515
  const bitstring = await statusList.encode() as Bitstring
2119
3516
 
2120
3517
  // cast top-level args
2121
- const topArgs = options?.topArgs as ICheqdSuspendCredentialWithStatusList2021Args
3518
+ const topArgs = options?.topArgs as ICheqdRevokeCredentialWithStatusList2021Args
2122
3519
 
2123
3520
  // write status list 2021 to file, if provided
2124
3521
  if (topArgs?.writeToFile) {
@@ -2129,7 +3526,7 @@ export class Cheqd implements IAgentPlugin {
2129
3526
  const published = topArgs?.publish
2130
3527
  ? (await async function () {
2131
3528
  // fetch status list 2021 metadata
2132
- const statusListMetadata = await Cheqd.fetchStatusList2021Metadata(credential)
3529
+ const statusListMetadata = await Cheqd.fetchStatusList2021Metadata(credentials[0])
2133
3530
 
2134
3531
  // publish status list 2021 as new version
2135
3532
  const scoped = topArgs.publishEncrypted
@@ -2149,7 +3546,47 @@ export class Cheqd implements IAgentPlugin {
2149
3546
  { encryptedString, encryptedSymmetricKey, symmetricKey: toString(symmetricKey!, 'hex') }
2150
3547
  ]
2151
3548
  }())
2152
- : [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
+ }())
2153
3590
 
2154
3591
  // early exit, if publish failed
2155
3592
  if (!scoped[0]) throw new Error('[did-provider-cheqd]: unsuspension: Failed to publish status list 2021')
@@ -2160,19 +3597,19 @@ export class Cheqd implements IAgentPlugin {
2160
3597
  : undefined
2161
3598
 
2162
3599
  return {
2163
- unsuspended: true,
3600
+ unsuspended: unsuspended.map((result) => result.status === 'fulfilled' ? result.value.unsuspended : false),
2164
3601
  published: topArgs?.publish ? true : undefined,
2165
- statusList: topArgs?.returnUpdatedStatusList ? bitstring : undefined,
3602
+ statusList: topArgs?.returnUpdatedStatusList ? await Cheqd.fetchStatusList2021(credentials[0]) as StatusList2021Suspension : undefined,
2166
3603
  encryptedStatusList: topArgs?.returnUpdatedEncryptedStatusList ? await blobToHexString((published?.[1] as { encryptedString: Blob })?.encryptedString) : undefined,
2167
3604
  encryptedSymmetricKey: topArgs?.returnEncryptedSymmetricKey ? (published?.[1] as { encryptedSymmetricKey: string })?.encryptedSymmetricKey : undefined,
2168
3605
  symmetricKey: topArgs?.returnSymmetricKey ? (published?.[1] as { symmetricKey: string })?.symmetricKey : undefined,
2169
- resourceMetadata: topArgs?.returnStatusListMetadata ? await Cheqd.fetchStatusList2021Metadata(credential) : undefined
2170
- } satisfies UnsuspensionResult
3606
+ resourceMetadata: topArgs?.returnStatusListMetadata ? await Cheqd.fetchStatusList2021Metadata(credentials[0]) : undefined
3607
+ } satisfies BulkUnsuspensionResult
2171
3608
  } catch (error) {
2172
- // silent fail + early exit, optimised for parallelisation, use with Promise.allSettled
3609
+ // silent fail + early exit
2173
3610
  console.error(error)
2174
3611
 
2175
- return { unsuspended: false, error: error as IError } satisfies UnsuspensionResult
3612
+ return { unsuspended: [], error: error as IError } satisfies BulkUnsuspensionResult
2176
3613
  }
2177
3614
  }
2178
3615
 
@@ -2182,32 +3619,23 @@ export class Cheqd implements IAgentPlugin {
2182
3619
  throw new Error(`[did-provider-cheqd]: revocation: Unsupported status purpose: ${credential.credentialStatus?.statusPurpose}`)
2183
3620
  }
2184
3621
 
2185
- // fetch status list 2021 metadata
2186
- const metadata = (await Cheqd.fetchStatusList2021Metadata(credential))
2187
-
2188
- // detect if encrypted
2189
- const isEncrypted = function() {
2190
- switch (metadata.mediaType) {
2191
- case 'application/octet-stream':
2192
- return true
2193
- case 'application/gzip':
2194
- return false
2195
- default:
2196
- throw new Error(`[did-provider-cheqd]: revocation: Unsupported media type: ${metadata.mediaType}`)
2197
- }
2198
- }()
3622
+ // fetch status list 2021
3623
+ const publishedList = (await Cheqd.fetchStatusList2021(credential)) as StatusList2021Revocation
2199
3624
 
2200
3625
  // early return, if encrypted and decryption key is not provided
2201
- 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')
2202
3627
 
2203
3628
  // fetch status list 2021 inscribed in credential
2204
3629
  const statusList2021 = options?.topArgs?.fetchList
2205
3630
  ? (await async function () {
2206
3631
  // if not encrypted, return bitstring
2207
- 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')
2208
3636
 
2209
3637
  // otherwise, decrypt and return bitstring
2210
- 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))
2211
3639
 
2212
3640
  // instantiate dkg-threshold client, in which case lit-protocol is used
2213
3641
  const lit = await LitProtocol.create({
@@ -2219,25 +3647,42 @@ export class Cheqd implements IAgentPlugin {
2219
3647
  return await lit.decrypt(scopedRawBlob, options?.topArgs?.encryptedSymmetricKey, options?.topArgs?.decryptionOptions?.unifiedAccessControlConditions)
2220
3648
  }())
2221
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
+
2222
3655
  // if status list 2021 is not fetched, read from file
2223
3656
  if (options?.statusListFile) {
2224
3657
  // if not encrypted, return bitstring
2225
- 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
+ }
2226
3668
 
2227
3669
  // otherwise, decrypt and return bitstring
2228
3670
  const scopedRawBlob = await toBlob(await Cheqd.getFile(options.statusListFile))
2229
3671
 
2230
- // instantiate dkg-threshold client, in which case lit-protocol is used
2231
- const lit = await LitProtocol.create({
2232
- chain: options?.topArgs?.bootstrapOptions?.chain,
2233
- litNetwork: options?.topArgs?.bootstrapOptions?.litNetwork
2234
- })
2235
-
2236
3672
  // decrypt
2237
- 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
2238
3680
  }
2239
3681
 
2240
- 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')
2241
3686
 
2242
3687
  // otherwise, read from inline bitstring
2243
3688
  return options?.statusListInlineBitstring
@@ -2256,32 +3701,23 @@ export class Cheqd implements IAgentPlugin {
2256
3701
  throw new Error(`[did-provider-cheqd]: suspension: Unsupported status purpose: ${credential.credentialStatus?.statusPurpose}`)
2257
3702
  }
2258
3703
 
2259
- // fetch status list 2021 metadata
2260
- const metadata = (await Cheqd.fetchStatusList2021Metadata(credential))
2261
-
2262
- // detect if encrypted
2263
- const isEncrypted = function() {
2264
- switch (metadata.mediaType) {
2265
- case 'application/octet-stream':
2266
- return true
2267
- case 'application/gzip':
2268
- return false
2269
- default:
2270
- throw new Error(`[did-provider-cheqd]: suspension: Unsupported media type: ${metadata.mediaType}`)
2271
- }
2272
- }()
3704
+ // fetch status list 2021
3705
+ const publishedList = (await Cheqd.fetchStatusList2021(credential)) as StatusList2021Suspension
2273
3706
 
2274
3707
  // early return, if encrypted and decryption key is not provided
2275
- 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')
2276
3709
 
2277
3710
  // fetch status list 2021 inscribed in credential
2278
3711
  const statusList2021 = options?.topArgs?.fetchList
2279
3712
  ? (await async function () {
2280
3713
  // if not encrypted, return bitstring
2281
- 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')
2282
3718
 
2283
3719
  // otherwise, decrypt and return bitstring
2284
- 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))
2285
3721
 
2286
3722
  // instantiate dkg-threshold client, in which case lit-protocol is used
2287
3723
  const lit = await LitProtocol.create({
@@ -2293,25 +3729,42 @@ export class Cheqd implements IAgentPlugin {
2293
3729
  return await lit.decrypt(scopedRawBlob, options?.topArgs?.encryptedSymmetricKey, options?.topArgs?.decryptionOptions?.unifiedAccessControlConditions)
2294
3730
  }())
2295
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
+
2296
3737
  // if status list 2021 is not fetched, read from file
2297
3738
  if (options?.statusListFile) {
2298
3739
  // if not encrypted, return bitstring
2299
- 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
+ }
2300
3750
 
2301
3751
  // otherwise, decrypt and return bitstring
2302
3752
  const scopedRawBlob = await toBlob(await Cheqd.getFile(options.statusListFile))
2303
3753
 
2304
- // instantiate dkg-threshold client, in which case lit-protocol is used
2305
- const lit = await LitProtocol.create({
2306
- chain: options?.topArgs?.bootstrapOptions?.chain,
2307
- litNetwork: options?.topArgs?.bootstrapOptions?.litNetwork
2308
- })
2309
-
2310
3754
  // decrypt
2311
- 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
2312
3762
  }
2313
3763
 
2314
- 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')
2315
3768
 
2316
3769
  // otherwise, read from inline bitstring
2317
3770
  return options?.statusListInlineBitstring
@@ -2324,18 +3777,19 @@ export class Cheqd implements IAgentPlugin {
2324
3777
  return !!statusList.getStatus(Number(credential.credentialStatus.statusListIndex))
2325
3778
  }
2326
3779
 
2327
- 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> {
2328
3781
  // construct status list 2021 payload from previous version + new version
2329
3782
  const payload = {
2330
3783
  collectionId: statusList2021Metadata.resourceCollectionId,
2331
3784
  id: options?.resourceId || v4(),
2332
3785
  name: statusList2021Metadata.resourceName,
2333
3786
  version: options?.resourceVersion || new Date().toISOString(),
2334
- resourceType: 'StatusList2021',
3787
+ alsoKnownAs: options?.resourceAlsoKnownAs || [],
3788
+ resourceType: statusList2021Metadata.resourceType as DefaultStatusList2021ResourceType,
2335
3789
  data: statusList2021Raw
2336
3790
  } satisfies StatusList2021ResourcePayload
2337
3791
 
2338
- return await options.context.agent[CreateStatusList2021MethodName]({
3792
+ return await options.context.agent[BroadcastStatusList2021MethodName]({
2339
3793
  kms: (await options.context.agent.keyManagerGetKeyManagementSystems())[0],
2340
3794
  payload,
2341
3795
  network: statusList2021Metadata.resourceURI.split(':')[2] as CheqdNetwork,
@@ -2344,7 +3798,7 @@ export class Cheqd implements IAgentPlugin {
2344
3798
  })
2345
3799
  }
2346
3800
 
2347
- static async fetchStatusList2021(credential: VerifiableCredential, returnRaw = false): Promise<Bitstring | Uint8Array> {
3801
+ static async fetchStatusList2021(credential: VerifiableCredential, returnRaw = false): Promise<StatusList2021Revocation | StatusList2021Suspension | Uint8Array> {
2348
3802
  // validate credential status
2349
3803
  if (!credential.credentialStatus) throw new Error('[did-provider-cheqd]: fetch status list: Credential status is not present')
2350
3804
 
@@ -2354,31 +3808,34 @@ export class Cheqd implements IAgentPlugin {
2354
3808
  // validate credential status list status purpose
2355
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')
2356
3810
 
2357
- // validate credential status list status list credential
2358
- if (!credential.credentialStatus.statusListCredential) throw new Error('[did-provider-cheqd]: fetch status list: Credential status list credential is not present')
2359
-
2360
3811
  // fetch status list 2021
2361
- const raw = await (await fetch(credential.credentialStatus.statusListCredential)).arrayBuffer()
3812
+ const content = await (await fetch(credential.credentialStatus.id.split('#')[0])).json() as StatusList2021Revocation | StatusList2021Suspension
2362
3813
 
2363
3814
  // return raw if requested
2364
- if (returnRaw) return new Uint8Array(raw)
2365
-
2366
- // otherwise, parse to bitstring and return
2367
- const bitstring = toString(new Uint8Array(raw), 'base64url')
3815
+ if (returnRaw) {
3816
+ return fromString(content.StatusList2021.encodedList, content.metadata.encoding as DefaultStatusList2021Encoding)
3817
+ }
2368
3818
 
2369
- return bitstring
3819
+ // otherwise, return content
3820
+ return content
2370
3821
  }
2371
3822
 
2372
3823
  static async fetchStatusList2021Metadata(credential: VerifiableCredential): Promise<LinkedResourceMetadataResolutionResult> {
2373
3824
  // get base url
2374
- const baseUrl = new URL(credential.credentialStatus?.statusListCredential)
2375
-
3825
+ const baseUrl = new URL(credential.credentialStatus!.id.split('#')[0])
3826
+
2376
3827
  // get resource name
2377
3828
  const resourceName = baseUrl.searchParams.get('resourceName')
2378
3829
 
3830
+ // get resource type
3831
+ const resourceType = baseUrl.searchParams.get('resourceType')
3832
+
2379
3833
  // unset resource name
2380
3834
  baseUrl.searchParams.delete('resourceName')
2381
3835
 
3836
+ // unset resource type
3837
+ baseUrl.searchParams.delete('resourceType')
3838
+
2382
3839
  // construct metadata url
2383
3840
  const metadataUrl = `${baseUrl.toString()}/metadata`
2384
3841
 
@@ -2389,7 +3846,7 @@ export class Cheqd implements IAgentPlugin {
2389
3846
  if (!collectionMetadata?.contentStream?.linkedResourceMetadata) throw new Error('[did-provider-cheqd]: fetch status list metadata: No linked resources found')
2390
3847
 
2391
3848
  // find relevant resources by resource name
2392
- const resourceVersioning = collectionMetadata.contentStream.linkedResourceMetadata.filter((resource) => resource.resourceName === resourceName)
3849
+ const resourceVersioning = collectionMetadata.contentStream.linkedResourceMetadata.filter((resource) => resource.resourceName === resourceName && resource.resourceType === resourceType)
2393
3850
 
2394
3851
  // early exit if no relevant resources
2395
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}`)