@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.
@@ -3,7 +3,7 @@
3
3
  // unused vars are kept by convention
4
4
  // non-null assertion is used when we know better than the compiler that the value is not null or undefined
5
5
  import { CheqdNetwork, createDidPayload, createDidVerificationMethod, createKeyPairBase64, createKeyPairHex, createVerificationKeys } from '@cheqd/sdk';
6
- import { DefaultRESTUrls } from '../did-manager/cheqd-did-provider.js';
6
+ import { DefaultRESTUrls, DefaultStatusList2021Encodings, DefaultStatusList2021ResourceTypes, DefaultStatusList2021StatusPurposeTypes, } from '../did-manager/cheqd-did-provider.js';
7
7
  import { fromString, toString } from 'uint8arrays';
8
8
  import { decodeJWT } from 'did-jwt';
9
9
  import { StatusList } from '@digitalbazaar/vc-status-list';
@@ -14,14 +14,45 @@ import { LitCompatibleCosmosChains, LitProtocol } from '../dkg-threshold/lit-pro
14
14
  import { blobToHexString, randomFromRange, toBlob, unescapeUnicode } from '../utils/helpers.js';
15
15
  import { resolverUrl } from '../did-manager/cheqd-did-resolver.js';
16
16
  const debug = Debug('veramo:did-provider-cheqd');
17
- export const AccessControlConditionTypes = { memoNonce: 'memoNonce', balance: 'balance' };
17
+ // TODO: revisit while implementing timelock payment
18
+ //? CheckSequenceVerify (CSV) is a script opcode used to implement relative timelocks.
19
+ //? It is a conditional opcode that consumes a minimum of two stack items and compares their relative locktime values.
20
+ //? 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.
21
+ //? The relative locktime of an item is the number of seconds since the item was confirmed.
22
+ //? The following apply:
23
+ //? - Relative locktimes are measured from the time a transaction is confirmed, not from the time a transaction is created.
24
+ //? - Relative locktimes are not affected by time zone or daylight saving time.
25
+ //? - Relative locktimes are not affected by the time a transaction is broadcast.
26
+ //? - Relative locktimes are not affected by the time a transaction is mined.
27
+ //? - Relative locktimes are not affected by the time a transaction is included in a block.
28
+ //? - Relative locktimes are not affected by the time a block is mined.
29
+ // Implementation as per:
30
+ //
31
+ // - Generate a random nonce client-side.
32
+ // - Utilise the nonce as the memo.
33
+ // - Observed by the recipient client-side.
34
+ // - Gives turn to initiate decryption of the status list + status verification.
35
+ // - Define time interval OR block height interval client-side.
36
+ // - Utilised as inverse timelock.
37
+ // - Observed by dkg nodes.
38
+ // - TODO: Add chained AND conditions to return value tests, dkg node-side.
39
+ // - e.g. if timelockPayment (a <= now <= b) AND memoNonce strict equality (uuid, IMPORTANT: condition is updateable).
40
+ // - Optionally, recycled per encryption of the status list.
41
+ // - Will be published as part of the status list, if recycled + interval is altered.
42
+ // Issuer initiated immutable steps:
43
+ // - Initilise ACC with timelockPayment, nonce. <-- Nonce is timestamped as per time of encryption.
44
+ // - On a per transaction basis:
45
+ // - Update ACC with revelant memoNonce.
46
+ // - Rotate symmetric key.
47
+ export const AccessControlConditionTypes = { timelockPayment: 'timelockPayment', memoNonce: 'memoNonce', balance: 'balance' };
18
48
  export const AccessControlConditionReturnValueComparators = { lessThan: '<', greaterThan: '>', equalTo: '=', lessThanOrEqualTo: '<=', greaterThanOrEqualTo: '>=' };
49
+ 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=[^&]*)))$/;
19
50
  const CreateIdentifierMethodName = 'cheqdCreateIdentifier';
20
51
  const UpdateIdentifierMethodName = 'cheqdUpdateIdentifier';
21
52
  const DeactivateIdentifierMethodName = 'cheqdDeactivateIdentifier';
22
53
  const CreateResourceMethodName = 'cheqdCreateLinkedResource';
23
54
  const CreateStatusList2021MethodName = 'cheqdCreateStatusList2021';
24
- const CreateEncryptedStatusList2021MethodName = 'cheqdCreateEncryptedStatusList2021';
55
+ const BroadcastStatusList2021MethodName = 'cheqdBroadcastStatusList2021';
25
56
  const GenerateDidDocMethodName = 'cheqdGenerateDidDoc';
26
57
  const GenerateDidDocWithLinkedResourceMethodName = 'cheqdGenerateDidDocWithLinkedResource';
27
58
  const GenerateKeyPairMethodName = 'cheqdGenerateIdentityKeys';
@@ -136,17 +167,17 @@ export class Cheqd {
136
167
  ]
137
168
  },
138
169
  "returnType": {
139
- "type": "boolean"
170
+ "type": "object"
140
171
  }
141
172
  },
142
- "cheqdCreateEncryptedStatusList2021": {
143
- "description": "Create a new Encrypted Status List 2021",
173
+ "cheqdBroadcastStatusList2021": {
174
+ "description": "Broadcast a Status List 2021 to cheqd ledger",
144
175
  "arguments": {
145
176
  "type": "object",
146
177
  "properties": {
147
178
  "args": {
148
179
  "type": "object",
149
- "description": "A cheqdCreateEncryptedStatusList2021Args object as any for extensibility"
180
+ "description": "A cheqdBroadcastStatusList2021Args object as any for extensibility"
150
181
  }
151
182
  },
152
183
  "required": [
@@ -512,7 +543,7 @@ export class Cheqd {
512
543
  [DeactivateIdentifierMethodName]: this.DeactivateIdentifier.bind(this),
513
544
  [CreateResourceMethodName]: this.CreateResource.bind(this),
514
545
  [CreateStatusList2021MethodName]: this.CreateStatusList2021.bind(this),
515
- [CreateEncryptedStatusList2021MethodName]: this.CreateEncryptedStatusList2021.bind(this),
546
+ [BroadcastStatusList2021MethodName]: this.BroadcastStatusList2021.bind(this),
516
547
  [GenerateDidDocMethodName]: this.GenerateDidDoc.bind(this),
517
548
  [GenerateDidDocWithLinkedResourceMethodName]: this.GenerateDidDocWithLinkedResource.bind(this),
518
549
  [GenerateKeyPairMethodName]: this.GenerateIdentityKeys.bind(this),
@@ -630,33 +661,125 @@ export class Cheqd {
630
661
  if (typeof args.kms !== 'string') {
631
662
  throw new Error('[did-provider-cheqd]: kms is required');
632
663
  }
633
- if (typeof args.payload !== 'object') {
634
- throw new Error('[did-provider-cheqd]: payload object is required');
664
+ if (typeof args.issuerDid !== 'string' || !args.issuerDid) {
665
+ throw new Error('[did-provider-cheqd]: issuerDid is required');
635
666
  }
636
- if (typeof args.network !== 'string') {
637
- throw new Error('[did-provider-cheqd]: network is required');
667
+ if (typeof args.statusListName !== 'string' || !args.statusListName) {
668
+ throw new Error('[did-provider-cheqd]: statusListName is required');
638
669
  }
639
- if (args?.file) {
640
- args.payload.data = await Cheqd.getFile(args.file);
670
+ if (typeof args.statusPurpose !== 'string' || !args.statusPurpose) {
671
+ throw new Error('[did-provider-cheqd]: statusPurpose is required');
641
672
  }
642
- if (typeof args?.payload?.data === 'string') {
643
- args.payload.data = fromString(args.payload.data, 'base64');
673
+ if (typeof args.encrypted === 'undefined') {
674
+ throw new Error('[did-provider-cheqd]: encrypted is required');
644
675
  }
645
- // TODO: validate data as per bitstring
646
- // set default resource type in runtime
647
- args.payload.resourceType = 'StatusList2021';
648
- this.providerId = Cheqd.generateProviderId(args.network);
649
- this.didProvider = await Cheqd.loadProvider({ id: this.providerId }, this.supportedDidProviders);
650
- return await this.didProvider.createResource({
651
- options: {
652
- kms: args.kms,
653
- payload: args.payload,
654
- signInputs: args.signInputs,
655
- fee: args?.fee
676
+ // validate statusPurpose
677
+ if (!Object.values(DefaultStatusList2021StatusPurposeTypes).includes(args.statusPurpose)) {
678
+ throw new Error(`[did-provider-cheqd]: statusPurpose must be one of ${Object.values(DefaultStatusList2021StatusPurposeTypes).join(', ')}`);
679
+ }
680
+ // validate statusListLength
681
+ if (args?.statusListLength) {
682
+ if (typeof args.statusListLength !== 'number') {
683
+ throw new Error('[did-provider-cheqd]: statusListLength must be number');
656
684
  }
657
- }, context);
685
+ if (args.statusListLength < Cheqd.defaultStatusList2021Length) {
686
+ throw new Error(`[did-provider-cheqd]: statusListLength must be greater than or equal to ${Cheqd.defaultStatusList2021Length} number of entries`);
687
+ }
688
+ }
689
+ // validate statusListEncoding
690
+ if (args?.statusListEncoding) {
691
+ if (typeof args.statusListEncoding !== 'string') {
692
+ throw new Error('[did-provider-cheqd]: statusListEncoding must be string');
693
+ }
694
+ if (!Object.values(DefaultStatusList2021Encodings).includes(args.statusListEncoding)) {
695
+ throw new Error(`[did-provider-cheqd]: statusListEncoding must be one of ${Object.values(DefaultStatusList2021Encodings).join(', ')}`);
696
+ }
697
+ }
698
+ // validate validUntil
699
+ if (args?.validUntil) {
700
+ if (typeof args.validUntil !== 'string') {
701
+ throw new Error('[did-provider-cheqd]: validUntil must be string');
702
+ }
703
+ if (new Date() <= new Date(args.validUntil)) {
704
+ throw new Error('[did-provider-cheqd]: validUntil must be greater than current date');
705
+ }
706
+ }
707
+ // validate args in pairs - case: encrypted
708
+ if (args.encrypted) {
709
+ // validate paymentConditions
710
+ if (!args?.paymentConditions || !args?.paymentConditions?.length || !Array.isArray(args?.paymentConditions) || args?.paymentConditions.length === 0) {
711
+ throw new Error('[did-provider-cheqd]: paymentConditions is required');
712
+ }
713
+ if (!args?.paymentConditions?.every((condition) => condition.feePaymentAddress && condition.feePaymentAmount)) {
714
+ throw new Error('[did-provider-cheqd]: paymentConditions must contain feePaymentAddress and feeAmount');
715
+ }
716
+ if (!args?.paymentConditions?.every((condition) => typeof condition.feePaymentAddress === 'string' && typeof condition.feePaymentAmount === 'string')) {
717
+ throw new Error('[did-provider-cheqd]: feePaymentAddress and feePaymentAmount must be string');
718
+ }
719
+ if (!args?.paymentConditions?.every((condition) => condition.type === AccessControlConditionTypes.timelockPayment)) {
720
+ throw new Error('[did-provider-cheqd]: paymentConditions must be of type timelockPayment');
721
+ }
722
+ }
723
+ // get network
724
+ const network = args.issuerDid.split(':')[2];
725
+ // generate bitstring
726
+ const bitstring = await context.agent[GenerateStatusList2021MethodName]({ length: args?.statusListLength || Cheqd.defaultStatusList2021Length, bitstringEncoding: args?.statusListEncoding || DefaultStatusList2021Encodings.base64url });
727
+ // construct data
728
+ const data = args.encrypted
729
+ ? (await (async function () {
730
+ // TODO: implement
731
+ throw new Error('[did-provider-cheqd]: encrypted status list is not implemented yet');
732
+ }()))
733
+ : (await (async function () {
734
+ switch (args.statusPurpose) {
735
+ case DefaultStatusList2021StatusPurposeTypes.revocation:
736
+ return {
737
+ StatusList2021: {
738
+ type: DefaultStatusList2021ResourceTypes.revocation,
739
+ encodedList: bitstring,
740
+ validFrom: new Date().toISOString(),
741
+ validUntil: args?.validUntil
742
+ },
743
+ metadata: {
744
+ encrypted: false,
745
+ encoding: args?.statusListEncoding || DefaultStatusList2021Encodings.base64url,
746
+ }
747
+ };
748
+ case DefaultStatusList2021StatusPurposeTypes.suspension:
749
+ return {
750
+ StatusList2021: {
751
+ type: DefaultStatusList2021ResourceTypes.suspension,
752
+ encodedList: bitstring,
753
+ validFrom: new Date().toISOString(),
754
+ validUntil: args?.validUntil
755
+ },
756
+ metadata: {
757
+ encrypted: false,
758
+ encoding: args?.statusListEncoding || DefaultStatusList2021Encodings.base64url,
759
+ }
760
+ };
761
+ default:
762
+ throw new Error('[did-provider-cheqd]: statusPurpose is not valid');
763
+ }
764
+ }()));
765
+ // construct payload
766
+ const payload = {
767
+ id: v4(),
768
+ collectionId: args.issuerDid.split(':').reverse()[0],
769
+ name: args.statusListName,
770
+ resourceType: DefaultStatusList2021ResourceTypes[args.statusPurpose],
771
+ version: args?.resourceVersion || new Date().toISOString(),
772
+ alsoKnownAs: args?.alsoKnownAs || [],
773
+ data: fromString(JSON.stringify(data), 'utf-8'),
774
+ };
775
+ // return result
776
+ return {
777
+ created: await context.agent[BroadcastStatusList2021MethodName]({ kms: args.kms, payload, network: network }),
778
+ statusList2021: data,
779
+ resourceMetadata: await Cheqd.fetchStatusList2021Metadata({ credentialStatus: { id: `${resolverUrl}${args.issuerDid}?resourceName=${args.statusListName}&resourceType=${DefaultStatusList2021ResourceTypes[args.statusPurpose]}`, type: 'StatusList2021Entry' } })
780
+ };
658
781
  }
659
- async CreateEncryptedStatusList2021(args, context) {
782
+ async BroadcastStatusList2021(args, context) {
660
783
  if (typeof args.kms !== 'string') {
661
784
  throw new Error('[did-provider-cheqd]: kms is required');
662
785
  }
@@ -673,48 +796,13 @@ export class Cheqd {
673
796
  args.payload.data = fromString(args.payload.data, 'base64');
674
797
  }
675
798
  // TODO: validate data as per bitstring
676
- if (!args?.encryptionOptions) {
677
- throw new Error('[did-provider-cheqd]: encryptionOptions is required');
678
- }
679
- if (!args?.bootstrapOptions) {
680
- throw new Error('[did-provider-cheqd]: bootstrapOptions is required');
799
+ // validate resource type
800
+ if (!Object.values(DefaultStatusList2021ResourceTypes).includes(args?.payload?.resourceType)) {
801
+ throw new Error(`[did-provider-cheqd]: resourceType must be one of ${Object.values(DefaultStatusList2021ResourceTypes).join(', ')}`);
681
802
  }
682
- if (!args?.encryptionOptions?.accessControlConditions) {
683
- throw new Error('[did-provider-cheqd]: accessControlConditions is required');
684
- }
685
- // instantiate dkg-threshold client, in which case lit-protocol is used
686
- const lit = await LitProtocol.create({
687
- chain: args.bootstrapOptions?.chain,
688
- litNetwork: args.bootstrapOptions?.litNetwork
689
- });
690
- // construct access control conditions
691
- const unifiedAccessControlConditions = await Promise.all(args.encryptionOptions.accessControlConditions.map(async (condition) => {
692
- switch (condition.type) {
693
- case AccessControlConditionTypes.memoNonce:
694
- return await LitProtocol.generateCosmosAccessControlConditionTransactionMemo({
695
- key: '$.txs.*.body.memo',
696
- comparator: 'contains',
697
- value: condition?.specificNonce || await LitProtocol.generateTxNonce(condition?.nonceFormat)
698
- }, condition.amountObserved, condition.senderAddressObserved, condition.recipientAddressObserved, args.bootstrapOptions.chain);
699
- case AccessControlConditionTypes.balance:
700
- return await LitProtocol.generateCosmosAccessControlConditionBalance({
701
- key: '$.balances[0].amount',
702
- comparator: condition.comparator,
703
- value: condition.amountObserved
704
- }, args.bootstrapOptions.chain, condition.addressObserved);
705
- default:
706
- throw new Error(`[did-provider-cheqd]: accessControlCondition type is not supported`);
707
- }
708
- }));
709
- // encrypt data
710
- const { encryptedString, encryptedSymmetricKey, symmetricKey } = await lit.encrypt(toString(args.payload.data, 'base64url'), unifiedAccessControlConditions, true);
711
- // set encrypted data
712
- args.payload.data = new Uint8Array(await encryptedString.arrayBuffer());
713
- // set default resource type in runtime
714
- args.payload.resourceType = 'StatusList2021';
715
803
  this.providerId = Cheqd.generateProviderId(args.network);
716
804
  this.didProvider = await Cheqd.loadProvider({ id: this.providerId }, this.supportedDidProviders);
717
- const created = await this.didProvider.createResource({
805
+ return await this.didProvider.createResource({
718
806
  options: {
719
807
  kms: args.kms,
720
808
  payload: args.payload,
@@ -722,13 +810,6 @@ export class Cheqd {
722
810
  fee: args?.fee
723
811
  }
724
812
  }, context);
725
- return {
726
- created,
727
- encryptedSymmetricKey,
728
- encryptedStatusList2021: await blobToHexString(encryptedString),
729
- symmetricKey: args?.encryptionOptions?.returnSymmetricKey ? toString(symmetricKey, 'hex') : undefined,
730
- unifiedAccessControlConditions
731
- };
732
813
  }
733
814
  async GenerateDidDoc(args, context) {
734
815
  if (typeof args.verificationMethod !== 'string') {
@@ -878,14 +959,13 @@ export class Cheqd {
878
959
  ? args.issuanceOptions.credential.issuer.id
879
960
  : args.issuanceOptions.credential.issuer;
880
961
  // generate status list credential
881
- const statusListCredential = `${resolverUrl}${issuer}?resourceName=${args.statusOptions.statusListName}`;
962
+ const statusListCredential = `${resolverUrl}${issuer}?resourceName=${args.statusOptions.statusListName}&resourceType=StatusList2021Revocation`;
882
963
  // construct credential status
883
964
  const credentialStatus = {
884
965
  id: `${statusListCredential}#${statusListIndex}`,
885
966
  type: 'StatusList2021Entry',
886
967
  statusPurpose: 'revocation',
887
968
  statusListIndex: `${statusListIndex}`,
888
- statusListCredential,
889
969
  };
890
970
  // add credential status to credential
891
971
  args.issuanceOptions.credential.credentialStatus = credentialStatus;
@@ -920,14 +1000,13 @@ export class Cheqd {
920
1000
  ? args.issuanceOptions.credential.issuer.id
921
1001
  : args.issuanceOptions.credential.issuer;
922
1002
  // generate status list credential
923
- const statusListCredential = `${resolverUrl}${issuer}?resourceName=${args.statusOptions.statusListName}`;
1003
+ const statusListCredential = `${resolverUrl}${issuer}?resourceName=${args.statusOptions.statusListName}&resourceType=StatusList2021Suspension`;
924
1004
  // construct credential status
925
1005
  const credentialStatus = {
926
1006
  id: `${statusListCredential}#${statusListIndex}`,
927
1007
  type: 'StatusList2021Entry',
928
1008
  statusPurpose: 'suspension',
929
1009
  statusListIndex: `${statusListIndex}`,
930
- statusListCredential,
931
1010
  };
932
1011
  // add credential status to credential
933
1012
  args.issuanceOptions.credential.credentialStatus = credentialStatus;
@@ -966,6 +1045,7 @@ export class Cheqd {
966
1045
  if (!verificationResult.verified) {
967
1046
  return { verified: false, error: verificationResult.error };
968
1047
  }
1048
+ // if jwt credential, decode it
969
1049
  const credential = typeof args.credential === 'string' ? await Cheqd.decodeCredentialJWT(args.credential) : args.credential;
970
1050
  // verify credential status
971
1051
  switch (credential.credentialStatus?.statusPurpose) {
@@ -1016,6 +1096,7 @@ export class Cheqd {
1016
1096
  return { verified: true };
1017
1097
  }
1018
1098
  async CheckCredentialStatusWithStatusList2021(args, context) {
1099
+ // if jwt credential, decode it
1019
1100
  const credential = typeof args.credential === 'string' ? await Cheqd.decodeCredentialJWT(args.credential) : args.credential;
1020
1101
  switch (credential.credentialStatus?.statusPurpose) {
1021
1102
  case 'revocation':
@@ -1031,6 +1112,51 @@ export class Cheqd {
1031
1112
  }
1032
1113
  }
1033
1114
  async RevokeCredentialWithStatusList2021(args, context) {
1115
+ // verify credential, if provided and revocation options are not
1116
+ if (args?.credential && !args?.revocationOptions) {
1117
+ const verificationResult = await context.agent.verifyCredential({
1118
+ credential: args.credential,
1119
+ policies: {
1120
+ credentialStatus: false
1121
+ }
1122
+ });
1123
+ // early return if verification failed
1124
+ if (!verificationResult.verified) {
1125
+ return { revoked: false, error: verificationResult.error };
1126
+ }
1127
+ }
1128
+ // if revocation options are provided, give precedence
1129
+ if (args?.revocationOptions) {
1130
+ // validate revocation options - case: revocationOptions.issuerDid
1131
+ if (!args.revocationOptions.issuerDid)
1132
+ throw new Error('[did-provider-cheqd]: revocation: revocationOptions.issuerDid is required');
1133
+ // validate revocation options - case: revocationOptions.statusListName
1134
+ if (!args.revocationOptions.statusListName)
1135
+ throw new Error('[did-provider-cheqd]: revocation: revocationOptions.statusListName is required');
1136
+ // validate revocation options - case: revocationOptions.statusListIndex
1137
+ if (!args.revocationOptions.statusListIndex)
1138
+ throw new Error('[did-provider-cheqd]: revocation: revocationOptions.statusListIndex is required');
1139
+ // construct status list credential
1140
+ const statusListCredential = `${resolverUrl}${args.revocationOptions.issuerDid}?resourceName=${args.revocationOptions.statusListName}&resourceType=StatusList2021Revocation`;
1141
+ // construct credential status
1142
+ args.credential = {
1143
+ '@context': [],
1144
+ issuer: args.revocationOptions.issuerDid,
1145
+ credentialSubject: {},
1146
+ credentialStatus: {
1147
+ id: `${statusListCredential}#${args.revocationOptions.statusListIndex}`,
1148
+ type: 'StatusList2021Entry',
1149
+ statusPurpose: 'revocation',
1150
+ statusListIndex: `${args.revocationOptions.statusListIndex}`,
1151
+ },
1152
+ issuanceDate: '',
1153
+ proof: {}
1154
+ };
1155
+ }
1156
+ // validate args - case: credential
1157
+ if (!args.credential)
1158
+ throw new Error('[did-provider-cheqd]: revocation: credential is required');
1159
+ // if jwt credential, decode it
1034
1160
  const credential = typeof args.credential === 'string' ? await Cheqd.decodeCredentialJWT(args.credential) : args.credential;
1035
1161
  // validate status purpose
1036
1162
  if (credential.credentialStatus?.statusPurpose !== 'revocation') {
@@ -1058,18 +1184,142 @@ export class Cheqd {
1058
1184
  topArgs: args,
1059
1185
  publishOptions: {
1060
1186
  context,
1187
+ statusListEncoding: args?.options?.statusListEncoding,
1188
+ statusListValidUntil: args?.options?.statusListValidUntil,
1061
1189
  resourceId: args?.options?.resourceId,
1062
1190
  resourceVersion: args?.options?.resourceVersion,
1191
+ resourceAlsoKnownAs: args?.options?.alsoKnownAs,
1063
1192
  signInputs: args?.options?.signInputs,
1064
1193
  fee: args?.options?.fee
1065
1194
  }
1066
1195
  });
1067
1196
  }
1068
1197
  async RevokeBulkCredentialsWithStatusList2021(args, context) {
1069
- // TODO: implement
1070
- throw new Error('[did-provider-cheqd]: revocation: bulk revocation is not implemented yet');
1198
+ // verify credential, if provided and revocation options are not
1199
+ if (args?.credentials && !args?.revocationOptions) {
1200
+ const verificationResult = await Promise.all(args.credentials.map(async (credential) => {
1201
+ return await context.agent.verifyCredential({
1202
+ credential,
1203
+ policies: {
1204
+ credentialStatus: false
1205
+ }
1206
+ });
1207
+ }));
1208
+ // early return if verification failed for any credential
1209
+ if (verificationResult.some(result => !result.verified)) {
1210
+ // define verified
1211
+ return { revoked: Array(args.credentials.length).fill(false), error: verificationResult.find(result => !result.verified).error || { message: 'verification: could not verify credential' } };
1212
+ }
1213
+ }
1214
+ // if revocation options are provided, give precedence
1215
+ if (args?.revocationOptions) {
1216
+ // validate revocation options - case: revocationOptions.issuerDid
1217
+ if (!args.revocationOptions.issuerDid)
1218
+ throw new Error('[did-provider-cheqd]: revocation: revocationOptions.issuerDid is required');
1219
+ // validate revocation options - case: revocationOptions.statusListName
1220
+ if (!args.revocationOptions.statusListName)
1221
+ throw new Error('[did-provider-cheqd]: revocation: revocationOptions.statusListName is required');
1222
+ // validate revocation options - case: revocationOptions.statusListIndices
1223
+ if (!args.revocationOptions.statusListIndices || !args.revocationOptions.statusListIndices.length || args.revocationOptions.statusListIndices.length === 0 || !args.revocationOptions.statusListIndices.every(index => !isNaN(+index)))
1224
+ throw new Error('[did-provider-cheqd]: revocation: revocationOptions.statusListIndex is required and must be an array of indices');
1225
+ // construct status list credential
1226
+ const statusListCredential = `${resolverUrl}${args.revocationOptions.issuerDid}?resourceName=${args.revocationOptions.statusListName}&resourceType=StatusList2021Revocation`;
1227
+ // construct credential status
1228
+ args.credentials = args.revocationOptions.statusListIndices.map(index => ({
1229
+ '@context': [],
1230
+ issuer: args.revocationOptions.issuerDid,
1231
+ credentialSubject: {},
1232
+ credentialStatus: {
1233
+ id: `${statusListCredential}#${index}`,
1234
+ type: 'StatusList2021Entry',
1235
+ statusPurpose: 'revocation',
1236
+ statusListIndex: `${index}`,
1237
+ },
1238
+ issuanceDate: '',
1239
+ proof: {}
1240
+ }));
1241
+ }
1242
+ // validate args - case: credentials
1243
+ if (!args.credentials || !args.credentials.length || args.credentials.length === 0)
1244
+ throw new Error('[did-provider-cheqd]: revocation: credentials is required and must be an array of credentials');
1245
+ // if jwt credentials, decode them
1246
+ const credentials = await Promise.all(args.credentials.map(async (credential) => typeof credential === 'string' ? await Cheqd.decodeCredentialJWT(credential) : credential));
1247
+ // validate args in pairs - case: statusListFile and statusList
1248
+ if (args.options?.statusListFile && args.options?.statusList) {
1249
+ throw new Error('[did-provider-cheqd]: revocation: statusListFile and statusList are mutually exclusive');
1250
+ }
1251
+ // validate args in pairs - case: statusListFile and fetchList
1252
+ if (args.options?.statusListFile && args.options?.fetchList) {
1253
+ throw new Error('[did-provider-cheqd]: revocation: statusListFile and fetchList are mutually exclusive');
1254
+ }
1255
+ // validate args in pairs - case: statusList and fetchList
1256
+ if (args.options?.statusList && args.options?.fetchList) {
1257
+ throw new Error('[did-provider-cheqd]: revocation: statusList and fetchList are mutually exclusive');
1258
+ }
1259
+ // validate args in pairs - case: publish
1260
+ if (args.options?.publish && !args.fetchList && !(args.options?.statusListFile || args.options?.statusList)) {
1261
+ throw new Error('[did-provider-cheqd]: revocation: publish requires statusListFile or statusList, if fetchList is disabled');
1262
+ }
1263
+ // revoke credentials
1264
+ return await Cheqd.revokeCredentials(credentials, {
1265
+ ...args.options,
1266
+ topArgs: args,
1267
+ publishOptions: {
1268
+ context,
1269
+ resourceId: args?.options?.resourceId,
1270
+ resourceVersion: args?.options?.resourceVersion,
1271
+ resourceAlsoKnownAs: args?.options?.alsoKnownAs,
1272
+ signInputs: args?.options?.signInputs,
1273
+ fee: args?.options?.fee
1274
+ }
1275
+ });
1071
1276
  }
1072
1277
  async SuspendCredentialWithStatusList2021(args, context) {
1278
+ // verify credential, if provided and suspension options are not
1279
+ if (args?.credential && !args?.suspensionOptions) {
1280
+ const verificationResult = await context.agent.verifyCredential({
1281
+ credential: args.credential,
1282
+ policies: {
1283
+ credentialStatus: false
1284
+ }
1285
+ });
1286
+ // early return if verification failed
1287
+ if (!verificationResult.verified) {
1288
+ return { suspended: false, error: verificationResult.error };
1289
+ }
1290
+ }
1291
+ // if suspension options are provided, give precedence
1292
+ if (args?.suspensionOptions) {
1293
+ // validate suspension options - case: suspensionOptions.issuerDid
1294
+ if (!args.suspensionOptions.issuerDid)
1295
+ throw new Error('[did-provider-cheqd]: suspension: suspensionOptions.issuerDid is required');
1296
+ // validate suspension options - case: suspensionOptions.statusListName
1297
+ if (!args.suspensionOptions.statusListName)
1298
+ throw new Error('[did-provider-cheqd]: suspension: suspensionOptions.statusListName is required');
1299
+ // validate suspension options - case: suspensionOptions.statusListIndex
1300
+ if (!args.suspensionOptions.statusListIndex)
1301
+ throw new Error('[did-provider-cheqd]: suspension: suspensionOptions.statusListIndex is required');
1302
+ // construct status list credential
1303
+ const statusListCredential = `${resolverUrl}${args.suspensionOptions.issuerDid}?resourceName=${args.suspensionOptions.statusListName}&resourceType=StatusList2021Suspension`;
1304
+ // construct credential status
1305
+ args.credential = {
1306
+ '@context': [],
1307
+ issuer: args.suspensionOptions.issuerDid,
1308
+ credentialSubject: {},
1309
+ credentialStatus: {
1310
+ id: `${statusListCredential}#${args.suspensionOptions.statusListIndex}`,
1311
+ type: 'StatusList2021Entry',
1312
+ statusPurpose: 'suspension',
1313
+ statusListIndex: `${args.suspensionOptions.statusListIndex}`,
1314
+ },
1315
+ issuanceDate: '',
1316
+ proof: {}
1317
+ };
1318
+ }
1319
+ // validate args - case: credential
1320
+ if (!args.credential)
1321
+ throw new Error('[did-provider-cheqd]: suspension: credential is required');
1322
+ // if jwt credential, decode it
1073
1323
  const credential = typeof args.credential === 'string' ? await Cheqd.decodeCredentialJWT(args.credential) : args.credential;
1074
1324
  // validate status purpose
1075
1325
  if (credential.credentialStatus?.statusPurpose !== 'suspension') {
@@ -1097,18 +1347,142 @@ export class Cheqd {
1097
1347
  topArgs: args,
1098
1348
  publishOptions: {
1099
1349
  context,
1350
+ statusListEncoding: args?.options?.statusListEncoding,
1351
+ statusListValidUntil: args?.options?.statusListValidUntil,
1100
1352
  resourceId: args?.options?.resourceId,
1101
1353
  resourceVersion: args?.options?.resourceVersion,
1354
+ resourceAlsoKnownAs: args?.options?.alsoKnownAs,
1102
1355
  signInputs: args?.options?.signInputs,
1103
1356
  fee: args?.options?.fee
1104
1357
  }
1105
1358
  });
1106
1359
  }
1107
1360
  async SuspendBulkCredentialsWithStatusList2021(args, context) {
1108
- // TODO: implement
1109
- throw new Error('[did-provider-cheqd]: suspension: bulk suspension is not implemented yet');
1361
+ // verify credential, if provided and suspension options are not
1362
+ if (args?.credentials && !args?.suspensionOptions) {
1363
+ const verificationResult = await Promise.all(args.credentials.map(async (credential) => {
1364
+ return await context.agent.verifyCredential({
1365
+ credential,
1366
+ policies: {
1367
+ credentialStatus: false
1368
+ }
1369
+ });
1370
+ }));
1371
+ // early return if verification failed for any credential
1372
+ if (verificationResult.some(result => !result.verified)) {
1373
+ // define verified
1374
+ return { suspended: Array(args.credentials.length).fill(false), error: verificationResult.find(result => !result.verified).error || { message: 'verification: could not verify credential' } };
1375
+ }
1376
+ }
1377
+ // if suspension options are provided, give precedence
1378
+ if (args?.suspensionOptions) {
1379
+ // validate suspension options - case: suspensionOptions.issuerDid
1380
+ if (!args.suspensionOptions.issuerDid)
1381
+ throw new Error('[did-provider-cheqd]: suspension: suspensionOptions.issuerDid is required');
1382
+ // validate suspension options - case: suspensionOptions.statusListName
1383
+ if (!args.suspensionOptions.statusListName)
1384
+ throw new Error('[did-provider-cheqd]: suspension: suspensionOptions.statusListName is required');
1385
+ // validate suspension options - case: suspensionOptions.statusListIndices
1386
+ if (!args.suspensionOptions.statusListIndices || !args.suspensionOptions.statusListIndices.length || args.suspensionOptions.statusListIndices.length === 0 || !args.suspensionOptions.statusListIndices.every(index => !isNaN(+index)))
1387
+ throw new Error('[did-provider-cheqd]: suspension: suspensionOptions.statusListIndex is required and must be an array of indices');
1388
+ // construct status list credential
1389
+ const statusListCredential = `${resolverUrl}${args.suspensionOptions.issuerDid}?resourceName=${args.suspensionOptions.statusListName}&resourceType=StatusList2021Suspension`;
1390
+ // construct credential status
1391
+ args.credentials = args.suspensionOptions.statusListIndices.map(index => ({
1392
+ '@context': [],
1393
+ issuer: args.suspensionOptions.issuerDid,
1394
+ credentialSubject: {},
1395
+ credentialStatus: {
1396
+ id: `${statusListCredential}#${index}`,
1397
+ type: 'StatusList2021Entry',
1398
+ statusPurpose: 'suspension',
1399
+ statusListIndex: `${index}`,
1400
+ },
1401
+ issuanceDate: '',
1402
+ proof: {}
1403
+ }));
1404
+ }
1405
+ // validate args - case: credentials
1406
+ if (!args.credentials || !args.credentials.length || args.credentials.length === 0)
1407
+ throw new Error('[did-provider-cheqd]: suspension: credentials is required and must be an array of credentials');
1408
+ // if jwt credentials, decode them
1409
+ const credentials = await Promise.all(args.credentials.map(async (credential) => typeof credential === 'string' ? await Cheqd.decodeCredentialJWT(credential) : credential));
1410
+ // validate args in pairs - case: statusListFile and statusList
1411
+ if (args.options?.statusListFile && args.options?.statusList) {
1412
+ throw new Error('[did-provider-cheqd]: suspension: statusListFile and statusList are mutually exclusive');
1413
+ }
1414
+ // validate args in pairs - case: statusListFile and fetchList
1415
+ if (args.options?.statusListFile && args.options?.fetchList) {
1416
+ throw new Error('[did-provider-cheqd]: suspension: statusListFile and fetchList are mutually exclusive');
1417
+ }
1418
+ // validate args in pairs - case: statusList and fetchList
1419
+ if (args.options?.statusList && args.options?.fetchList) {
1420
+ throw new Error('[did-provider-cheqd]: suspension: statusList and fetchList are mutually exclusive');
1421
+ }
1422
+ // validate args in pairs - case: publish
1423
+ if (args.options?.publish && !args.fetchList && !(args.options?.statusListFile || args.options?.statusList)) {
1424
+ throw new Error('[did-provider-cheqd]: suspension: publish requires statusListFile or statusList, if fetchList is disabled');
1425
+ }
1426
+ // suspend credentials
1427
+ return await Cheqd.suspendCredentials(credentials, {
1428
+ ...args.options,
1429
+ topArgs: args,
1430
+ publishOptions: {
1431
+ context,
1432
+ resourceId: args?.options?.resourceId,
1433
+ resourceVersion: args?.options?.resourceVersion,
1434
+ resourceAlsoKnownAs: args?.options?.alsoKnownAs,
1435
+ signInputs: args?.options?.signInputs,
1436
+ fee: args?.options?.fee
1437
+ }
1438
+ });
1110
1439
  }
1111
1440
  async UnsuspendCredentialWithStatusList2021(args, context) {
1441
+ // verify credential, if provided and unsuspension options are not
1442
+ if (args?.credential && !args?.unsuspensionOptions) {
1443
+ const verificationResult = await context.agent.verifyCredential({
1444
+ credential: args.credential,
1445
+ policies: {
1446
+ credentialStatus: false
1447
+ }
1448
+ });
1449
+ // early return if verification failed
1450
+ if (!verificationResult.verified) {
1451
+ return { unsuspended: false, error: verificationResult.error };
1452
+ }
1453
+ }
1454
+ // if unsuspension options are provided, give precedence
1455
+ if (args?.unsuspensionOptions) {
1456
+ // validate unsuspension options - case: unsuspensionOptions.issuerDid
1457
+ if (!args.unsuspensionOptions.issuerDid)
1458
+ throw new Error('[did-provider-cheqd]: unsuspension: unsuspensionOptions.issuerDid is required');
1459
+ // validate unsuspension options - case: unsuspensionOptions.statusListName
1460
+ if (!args.unsuspensionOptions.statusListName)
1461
+ throw new Error('[did-provider-cheqd]: unsuspension: unsuspensionOptions.statusListName is required');
1462
+ // validate unsuspension options - case: unsuspensionOptions.statusListIndex
1463
+ if (!args.unsuspensionOptions.statusListIndex)
1464
+ throw new Error('[did-provider-cheqd]: unsuspension: unsuspensionOptions.statusListIndex is required');
1465
+ // construct status list credential
1466
+ const statusListCredential = `${resolverUrl}${args.unsuspensionOptions.issuerDid}?resourceName=${args.unsuspensionOptions.statusListName}&resourceType=StatusList2021Suspension`;
1467
+ // construct credential status
1468
+ args.credential = {
1469
+ '@context': [],
1470
+ issuer: args.unsuspensionOptions.issuerDid,
1471
+ credentialSubject: {},
1472
+ credentialStatus: {
1473
+ id: `${statusListCredential}#${args.unsuspensionOptions.statusListIndex}`,
1474
+ type: 'StatusList2021Entry',
1475
+ statusPurpose: 'suspension',
1476
+ statusListIndex: `${args.unsuspensionOptions.statusListIndex}`,
1477
+ },
1478
+ issuanceDate: '',
1479
+ proof: {}
1480
+ };
1481
+ }
1482
+ // validate args - case: credential
1483
+ if (!args.credential)
1484
+ throw new Error('[did-provider-cheqd]: unsuspension: credential is required');
1485
+ // if jwt credential, decode it
1112
1486
  const credential = typeof args.credential === 'string' ? await Cheqd.decodeCredentialJWT(args.credential) : args.credential;
1113
1487
  // validate status purpose
1114
1488
  if (credential.credentialStatus?.statusPurpose !== 'suspension') {
@@ -1136,16 +1510,95 @@ export class Cheqd {
1136
1510
  topArgs: args,
1137
1511
  publishOptions: {
1138
1512
  context,
1513
+ statusListEncoding: args?.options?.statusListEncoding,
1514
+ statusListValidUntil: args?.options?.statusListValidUntil,
1139
1515
  resourceId: args?.options?.resourceId,
1140
1516
  resourceVersion: args?.options?.resourceVersion,
1517
+ resourceAlsoKnownAs: args?.options?.alsoKnownAs,
1141
1518
  signInputs: args?.options?.signInputs,
1142
1519
  fee: args?.options?.fee
1143
1520
  }
1144
1521
  });
1145
1522
  }
1146
1523
  async UnsuspendBulkCredentialsWithStatusList2021(args, context) {
1147
- // TODO: implement
1148
- throw new Error('[did-provider-cheqd]: suspension: bulk unsuspension is not implemented yet');
1524
+ // verify credential, if provided and unsuspension options are not
1525
+ if (args?.credentials && !args?.unsuspensionOptions) {
1526
+ const verificationResult = await Promise.all(args.credentials.map(async (credential) => {
1527
+ return await context.agent.verifyCredential({
1528
+ credential,
1529
+ policies: {
1530
+ credentialStatus: false
1531
+ }
1532
+ });
1533
+ }));
1534
+ // early return if verification failed for any credential
1535
+ if (verificationResult.some(result => !result.verified)) {
1536
+ // define verified
1537
+ return { unsuspended: Array(args.credentials.length).fill(false), error: verificationResult.find(result => !result.verified).error || { message: 'verification: could not verify credential' } };
1538
+ }
1539
+ }
1540
+ // if unsuspension options are provided, give precedence
1541
+ if (args?.unsuspensionOptions) {
1542
+ // validate unsuspension options - case: unsuspensionOptions.issuerDid
1543
+ if (!args.unsuspensionOptions.issuerDid)
1544
+ throw new Error('[did-provider-cheqd]: unsuspension: unsuspensionOptions.issuerDid is required');
1545
+ // validate unsuspension options - case: unsuspensionOptions.statusListName
1546
+ if (!args.unsuspensionOptions.statusListName)
1547
+ throw new Error('[did-provider-cheqd]: unsuspension: unsuspensionOptions.statusListName is required');
1548
+ // validate unsuspension options - case: unsuspensionOptions.statusListIndices
1549
+ if (!args.unsuspensionOptions.statusListIndices || !args.unsuspensionOptions.statusListIndices.length || args.unsuspensionOptions.statusListIndices.length === 0 || !args.unsuspensionOptions.statusListIndices.every(index => !isNaN(+index)))
1550
+ throw new Error('[did-provider-cheqd]: unsuspension: unsuspensionOptions.statusListIndex is required and must be an array of indices');
1551
+ // construct status list credential
1552
+ const statusListCredential = `${resolverUrl}${args.unsuspensionOptions.issuerDid}?resourceName=${args.unsuspensionOptions.statusListName}&resourceType=StatusList2021Suspension`;
1553
+ // construct credential status
1554
+ args.credentials = args.unsuspensionOptions.statusListIndices.map(index => ({
1555
+ '@context': [],
1556
+ issuer: args.unsuspensionOptions.issuerDid,
1557
+ credentialSubject: {},
1558
+ credentialStatus: {
1559
+ id: `${statusListCredential}#${index}`,
1560
+ type: 'StatusList2021Entry',
1561
+ statusPurpose: 'suspension',
1562
+ statusListIndex: `${index}`,
1563
+ },
1564
+ issuanceDate: '',
1565
+ proof: {}
1566
+ }));
1567
+ }
1568
+ // validate args - case: credentials
1569
+ if (!args.credentials || !args.credentials.length || args.credentials.length === 0)
1570
+ throw new Error('[did-provider-cheqd]: unsuspension: credentials is required and must be an array of credentials');
1571
+ // if jwt credentials, decode them
1572
+ const credentials = await Promise.all(args.credentials.map(async (credential) => typeof credential === 'string' ? await Cheqd.decodeCredentialJWT(credential) : credential));
1573
+ // validate args in pairs - case: statusListFile and statusList
1574
+ if (args.options?.statusListFile && args.options?.statusList) {
1575
+ throw new Error('[did-provider-cheqd]: unsuspension: statusListFile and statusList are mutually exclusive');
1576
+ }
1577
+ // validate args in pairs - case: statusListFile and fetchList
1578
+ if (args.options?.statusListFile && args.options?.fetchList) {
1579
+ throw new Error('[did-provider-cheqd]: unsuspension: statusListFile and fetchList are mutually exclusive');
1580
+ }
1581
+ // validate args in pairs - case: statusList and fetchList
1582
+ if (args.options?.statusList && args.options?.fetchList) {
1583
+ throw new Error('[did-provider-cheqd]: unsuspension: statusList and fetchList are mutually exclusive');
1584
+ }
1585
+ // validate args in pairs - case: publish
1586
+ if (args.options?.publish && !args.fetchList && !(args.options?.statusListFile || args.options?.statusList)) {
1587
+ throw new Error('[did-provider-cheqd]: unsuspension: publish requires statusListFile or statusList, if fetchList is disabled');
1588
+ }
1589
+ // suspend credentials
1590
+ return await Cheqd.unsuspendCredentials(credentials, {
1591
+ ...args.options,
1592
+ topArgs: args,
1593
+ publishOptions: {
1594
+ context,
1595
+ resourceId: args?.options?.resourceId,
1596
+ resourceVersion: args?.options?.resourceVersion,
1597
+ resourceAlsoKnownAs: args?.options?.alsoKnownAs,
1598
+ signInputs: args?.options?.signInputs,
1599
+ fee: args?.options?.fee
1600
+ }
1601
+ });
1149
1602
  }
1150
1603
  async TransactVerifierPaysIssuer(args, context) {
1151
1604
  try {
@@ -1268,46 +1721,56 @@ export class Cheqd {
1268
1721
  // validate status purpose
1269
1722
  if (credential?.credentialStatus?.statusPurpose !== 'revocation')
1270
1723
  throw new Error('[did-provider-cheqd]: revocation: Invalid status purpose');
1271
- // fetch status list 2021 metadata
1272
- const metadata = (await Cheqd.fetchStatusList2021Metadata(credential));
1273
- // detect if encrypted
1274
- const isEncrypted = function () {
1275
- switch (metadata.mediaType) {
1276
- case 'application/octet-stream':
1277
- return true;
1278
- case 'application/gzip':
1279
- return false;
1280
- default:
1281
- throw new Error(`[did-provider-cheqd]: revocation: Unsupported media type: ${metadata.mediaType}`);
1282
- }
1283
- }();
1724
+ // fetch status list 2021
1725
+ const publishedList = (await Cheqd.fetchStatusList2021(credential));
1284
1726
  // early return, if encrypted and no decryption key provided
1285
- if (isEncrypted && !options?.topArgs?.symmetricKey)
1727
+ if (publishedList.metadata.encrypted && !options?.topArgs?.symmetricKey)
1286
1728
  throw new Error('[did-provider-cheqd]: revocation: symmetricKey is required, if status list 2021 is encrypted');
1287
1729
  // fetch status list 2021 inscribed in credential
1288
1730
  const statusList2021 = options?.topArgs?.fetchList
1289
1731
  ? (await async function () {
1290
1732
  // if not encrypted, return bitstring
1291
- if (!isEncrypted)
1292
- return await Cheqd.fetchStatusList2021(credential);
1733
+ if (!publishedList.metadata.encrypted)
1734
+ return publishedList.metadata.encoding === 'base64url'
1735
+ ? publishedList.StatusList2021.encodedList
1736
+ : toString(fromString(publishedList.StatusList2021.encodedList, publishedList.metadata.encoding), 'base64url');
1293
1737
  // otherwise, decrypt and return bitstring
1294
- const scopedRawBlob = await toBlob(await Cheqd.fetchStatusList2021(credential, true));
1738
+ const scopedRawBlob = await toBlob(fromString(publishedList.StatusList2021.encodedList, publishedList.metadata.encoding));
1295
1739
  // decrypt
1296
1740
  return await LitProtocol.decryptDirect(scopedRawBlob, fromString(options?.topArgs?.symmetricKey, 'hex'));
1297
1741
  }())
1298
1742
  : (await async function () {
1743
+ // transcode to base64url, if needed
1744
+ const publishedListTranscoded = publishedList.metadata.encoding === 'base64url'
1745
+ ? publishedList.StatusList2021.encodedList
1746
+ : toString(fromString(publishedList.StatusList2021.encodedList, publishedList.metadata.encoding), 'base64url');
1299
1747
  // if status list 2021 is not fetched, read from file
1300
1748
  if (options?.statusListFile) {
1301
1749
  // if not encrypted, return bitstring
1302
- if (!isEncrypted)
1303
- return new StatusList({ buffer: await Cheqd.getFile(options.statusListFile) }).encode();
1750
+ if (!publishedList.metadata.encrypted) {
1751
+ // construct encoded status list
1752
+ const encoded = new StatusList({ buffer: await Cheqd.getFile(options.statusListFile) }).encode();
1753
+ // validate against published list
1754
+ if (encoded !== publishedListTranscoded)
1755
+ throw new Error('[did-provider-cheqd]: revocation: statusListFile does not match published status list 2021');
1756
+ // return encoded
1757
+ return encoded;
1758
+ }
1304
1759
  // otherwise, decrypt and return bitstring
1305
1760
  const scopedRawBlob = await toBlob(await Cheqd.getFile(options.statusListFile));
1306
1761
  // decrypt
1307
- return await LitProtocol.decryptDirect(scopedRawBlob, fromString(options?.topArgs?.symmetricKey, 'hex'));
1762
+ const decrypted = await LitProtocol.decryptDirect(scopedRawBlob, fromString(options?.topArgs?.symmetricKey, 'hex'));
1763
+ // validate against published list
1764
+ if (decrypted !== publishedListTranscoded)
1765
+ throw new Error('[did-provider-cheqd]: revocation: statusListFile does not match published status list 2021');
1766
+ // return decrypted
1767
+ return decrypted;
1308
1768
  }
1309
1769
  if (!options?.statusListInlineBitstring)
1310
1770
  throw new Error('[did-provider-cheqd]: revocation: statusListInlineBitstring is required, if statusListFile is not provided');
1771
+ // validate against published list
1772
+ if (options?.statusListInlineBitstring !== publishedListTranscoded)
1773
+ throw new Error('[did-provider-cheqd]: revocation: statusListInlineBitstring does not match published status list 2021');
1311
1774
  // otherwise, read from inline bitstring
1312
1775
  return options?.statusListInlineBitstring;
1313
1776
  }());
@@ -1347,7 +1810,45 @@ export class Cheqd {
1347
1810
  { encryptedString, encryptedSymmetricKey, symmetricKey: toString(symmetricKey, 'hex') }
1348
1811
  ];
1349
1812
  }())
1350
- : [await Cheqd.publishStatusList2021(fromString(bitstring, 'base64url'), statusListMetadata, options?.publishOptions), undefined];
1813
+ : (await async function () {
1814
+ // validate encoding, if provided
1815
+ if (options?.publishOptions?.statusListEncoding && !Object.values(DefaultStatusList2021Encodings).includes(options?.publishOptions?.statusListEncoding)) {
1816
+ throw new Error('[did-provider-cheqd]: revocation: Invalid status list encoding');
1817
+ }
1818
+ // validate validUntil, if provided
1819
+ if (options?.publishOptions?.statusListValidUntil) {
1820
+ // validate validUntil as string
1821
+ if (typeof options?.publishOptions?.statusListValidUntil !== 'string')
1822
+ throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be string)');
1823
+ // validate validUntil as date
1824
+ if (isNaN(Date.parse(options?.publishOptions?.statusListValidUntil)))
1825
+ throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be date)');
1826
+ // validate validUntil as future date
1827
+ if (new Date(options?.publishOptions?.statusListValidUntil) < new Date())
1828
+ throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be future date)');
1829
+ // validate validUntil towards validFrom
1830
+ if (new Date(options?.publishOptions?.statusListValidUntil) <= new Date(publishedList.StatusList2021.validFrom))
1831
+ throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be after validFrom)');
1832
+ }
1833
+ // define status list content
1834
+ const content = {
1835
+ StatusList2021: {
1836
+ type: publishedList.StatusList2021.type,
1837
+ encodedList: publishedList.metadata.encoding === 'base64url' ? bitstring : toString(fromString(bitstring, 'base64url'), options.publishOptions.statusListEncoding),
1838
+ validFrom: publishedList.StatusList2021.validFrom,
1839
+ validUntil: options?.publishOptions?.statusListValidUntil || publishedList.StatusList2021.validUntil
1840
+ },
1841
+ metadata: {
1842
+ encoding: options?.publishOptions?.statusListEncoding || publishedList.metadata.encoding,
1843
+ encrypted: false,
1844
+ }
1845
+ };
1846
+ // return tuple of publish result and encryption relevant metadata
1847
+ return [
1848
+ await Cheqd.publishStatusList2021(fromString(JSON.stringify(content), 'utf-8'), statusListMetadata, options?.publishOptions),
1849
+ undefined
1850
+ ];
1851
+ }());
1351
1852
  // early exit, if publish failed
1352
1853
  if (!scoped[0])
1353
1854
  throw new Error('[did-provider-cheqd]: revocation: Failed to publish status list 2021');
@@ -1358,7 +1859,7 @@ export class Cheqd {
1358
1859
  return {
1359
1860
  revoked: true,
1360
1861
  published: topArgs?.publish ? true : undefined,
1361
- statusList: topArgs?.returnUpdatedStatusList ? bitstring : undefined,
1862
+ statusList: topArgs?.returnUpdatedStatusList ? await Cheqd.fetchStatusList2021(credential) : undefined,
1362
1863
  encryptedStatusList: topArgs?.returnUpdatedEncryptedStatusList ? await blobToHexString(published?.[1]?.encryptedString) : undefined,
1363
1864
  encryptedSymmetricKey: topArgs?.returnEncryptedSymmetricKey ? published?.[1]?.encryptedSymmetricKey : undefined,
1364
1865
  symmetricKey: topArgs?.returnSymmetricKey ? published?.[1]?.symmetricKey : undefined,
@@ -1366,56 +1867,264 @@ export class Cheqd {
1366
1867
  };
1367
1868
  }
1368
1869
  catch (error) {
1369
- // silent fail + early exit, optimised for parallelisation, use with Promise.allSettled
1870
+ // silent fail + early exit
1370
1871
  console.error(error);
1371
1872
  return { revoked: false, error: error };
1372
1873
  }
1373
1874
  }
1875
+ static async revokeCredentials(credentials, options) {
1876
+ // validate credentials - case: empty
1877
+ if (!credentials.length || credentials.length === 0)
1878
+ throw new Error('[did-provider-cheqd]: revocation: No credentials provided');
1879
+ // validate credentials - case: consistent issuer
1880
+ if (credentials.map((credential) => {
1881
+ return (credential.issuer.id)
1882
+ ? credential.issuer.id
1883
+ : credential.issuer;
1884
+ }).filter((value, _, self) => value && value !== self[0]).length > 0)
1885
+ throw new Error('[did-provider-cheqd]: revocation: Credentials must be issued by the same issuer');
1886
+ // validate credentials - case: status list index
1887
+ if (credentials.map((credential) => credential.credentialStatus.statusListIndex).filter((value, index, self) => self.indexOf(value) !== index).length > 0)
1888
+ throw new Error('[did-provider-cheqd]: revocation: Credentials must have unique status list index');
1889
+ // validate credentials - case: status purpose
1890
+ if (!credentials.every((credential) => credential.credentialStatus?.statusPurpose === 'revocation'))
1891
+ throw new Error('[did-provider-cheqd]: revocation: Invalid status purpose');
1892
+ // validate credentials - case: status list id
1893
+ const remote = credentials[0].credentialStatus?.id
1894
+ ? credentials[0].credentialStatus.id.split('#')[0]
1895
+ : (function () {
1896
+ throw new Error('[did-provider-cheqd]: revocation: Invalid status list id');
1897
+ }());
1898
+ // validate credentials - case: status list id format
1899
+ if (!RemoteListPattern.test(remote))
1900
+ 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>');
1901
+ if (!credentials.every((credential) => {
1902
+ return credential.credentialStatus.id.split('#')[0] === remote;
1903
+ }))
1904
+ throw new Error('[did-provider-cheqd]: revocation: Credentials must belong to the same status list');
1905
+ // validate credentials - case: status list type
1906
+ if (!credentials.every((credential) => credential.credentialStatus?.type === 'StatusList2021Entry'))
1907
+ throw new Error('[did-provider-cheqd]: revocation: Invalid status list type');
1908
+ try {
1909
+ // fetch status list 2021
1910
+ const publishedList = (await Cheqd.fetchStatusList2021(credentials[0]));
1911
+ // early return, if encrypted and no decryption key provided
1912
+ if (publishedList.metadata.encrypted && !options?.topArgs?.symmetricKey)
1913
+ throw new Error('[did-provider-cheqd]: revocation: symmetricKey is required, if status list 2021 is encrypted');
1914
+ // fetch status list 2021 inscribed in credential
1915
+ const statusList2021 = options?.topArgs?.fetchList
1916
+ ? (await async function () {
1917
+ // if not encrypted, return bitstring
1918
+ if (!publishedList.metadata.encrypted)
1919
+ return publishedList.metadata.encoding === 'base64url'
1920
+ ? publishedList.StatusList2021.encodedList
1921
+ : toString(fromString(publishedList.StatusList2021.encodedList, publishedList.metadata.encoding), 'base64url');
1922
+ // otherwise, decrypt and return bitstring
1923
+ const scopedRawBlob = await toBlob(fromString(publishedList.StatusList2021.encodedList, publishedList.metadata.encoding));
1924
+ // decrypt
1925
+ return await LitProtocol.decryptDirect(scopedRawBlob, fromString(options?.topArgs?.symmetricKey, 'hex'));
1926
+ }())
1927
+ : (await async function () {
1928
+ // transcode to base64url, if needed
1929
+ const publishedListTranscoded = publishedList.metadata.encoding === 'base64url'
1930
+ ? publishedList.StatusList2021.encodedList
1931
+ : toString(fromString(publishedList.StatusList2021.encodedList, publishedList.metadata.encoding), 'base64url');
1932
+ // if status list 2021 is not fetched, read from file
1933
+ if (options?.statusListFile) {
1934
+ // if not encrypted, return bitstring
1935
+ if (!publishedList.metadata.encrypted) {
1936
+ // construct encoded status list
1937
+ const encoded = new StatusList({ buffer: await Cheqd.getFile(options.statusListFile) }).encode();
1938
+ // validate against published list
1939
+ if (encoded !== publishedListTranscoded)
1940
+ throw new Error('[did-provider-cheqd]: revocation: statusListFile does not match published status list 2021');
1941
+ // return encoded
1942
+ return encoded;
1943
+ }
1944
+ // otherwise, decrypt and return bitstring
1945
+ const scopedRawBlob = await toBlob(await Cheqd.getFile(options.statusListFile));
1946
+ // decrypt
1947
+ const decrypted = await LitProtocol.decryptDirect(scopedRawBlob, fromString(options?.topArgs?.symmetricKey, 'hex'));
1948
+ // validate against published list
1949
+ if (decrypted !== publishedListTranscoded)
1950
+ throw new Error('[did-provider-cheqd]: revocation: statusListFile does not match published status list 2021');
1951
+ // return decrypted
1952
+ return decrypted;
1953
+ }
1954
+ if (!options?.statusListInlineBitstring)
1955
+ throw new Error('[did-provider-cheqd]: revocation: statusListInlineBitstring is required, if statusListFile is not provided');
1956
+ // validate against published list
1957
+ if (options?.statusListInlineBitstring !== publishedListTranscoded)
1958
+ throw new Error('[did-provider-cheqd]: revocation: statusListInlineBitstring does not match published status list 2021');
1959
+ // otherwise, read from inline bitstring
1960
+ return options?.statusListInlineBitstring;
1961
+ }());
1962
+ // parse status list 2021
1963
+ const statusList = await StatusList.decode({ encodedList: statusList2021 });
1964
+ // initiate bulk revocation
1965
+ const revoked = await Promise.allSettled(credentials.map((credential) => {
1966
+ return async function () {
1967
+ // early return, if no credential status
1968
+ if (!credential.credentialStatus)
1969
+ return { revoked: false };
1970
+ // early exit, if credential is already revoked
1971
+ if (statusList.getStatus(Number(credential.credentialStatus.statusListIndex)))
1972
+ return { revoked: false };
1973
+ // update revocation status
1974
+ statusList.setStatus(Number(credential.credentialStatus.statusListIndex), true);
1975
+ // return revocation status
1976
+ return { revoked: true };
1977
+ }();
1978
+ }));
1979
+ // revert bulk ops, if some failed
1980
+ if (revoked.some((result) => result.status === 'fulfilled' && !result.value.revoked))
1981
+ 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 })))}`);
1982
+ // set in-memory status list ref
1983
+ const bitstring = await statusList.encode();
1984
+ // cast top-level args
1985
+ const topArgs = options?.topArgs;
1986
+ // write status list 2021 to file, if provided
1987
+ if (topArgs?.writeToFile) {
1988
+ await Cheqd.writeFile(fromString(bitstring, 'base64url'), options?.statusListFile);
1989
+ }
1990
+ // publish status list 2021, if provided
1991
+ const published = topArgs?.publish
1992
+ ? (await async function () {
1993
+ // fetch status list 2021 metadata
1994
+ const statusListMetadata = await Cheqd.fetchStatusList2021Metadata(credentials[0]);
1995
+ // publish status list 2021 as new version
1996
+ const scoped = topArgs.publishEncrypted
1997
+ ? (await async function () {
1998
+ // instantiate dkg-threshold client, in which case lit-protocol is used
1999
+ const lit = await LitProtocol.create({
2000
+ chain: options?.topArgs?.bootstrapOptions?.chain,
2001
+ litNetwork: options?.topArgs?.bootstrapOptions?.litNetwork
2002
+ });
2003
+ // encrypt
2004
+ const { encryptedString, encryptedSymmetricKey, symmetricKey } = await lit.encrypt(bitstring, options?.topArgs?.encryptionOptions?.unifiedAccessControlConditions, true);
2005
+ // return tuple of publish result and encryption relevant metadata
2006
+ return [
2007
+ await Cheqd.publishStatusList2021(new Uint8Array(await encryptedString.arrayBuffer()), statusListMetadata, options?.publishOptions),
2008
+ { encryptedString, encryptedSymmetricKey, symmetricKey: toString(symmetricKey, 'hex') }
2009
+ ];
2010
+ }())
2011
+ : (await async function () {
2012
+ // validate encoding, if provided
2013
+ if (options?.publishOptions?.statusListEncoding && !Object.values(DefaultStatusList2021Encodings).includes(options?.publishOptions?.statusListEncoding)) {
2014
+ throw new Error('[did-provider-cheqd]: revocation: Invalid status list encoding');
2015
+ }
2016
+ // validate validUntil, if provided
2017
+ if (options?.publishOptions?.statusListValidUntil) {
2018
+ // validate validUntil as string
2019
+ if (typeof options?.publishOptions?.statusListValidUntil !== 'string')
2020
+ throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be string)');
2021
+ // validate validUntil as date
2022
+ if (isNaN(Date.parse(options?.publishOptions?.statusListValidUntil)))
2023
+ throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be date)');
2024
+ // validate validUntil as future date
2025
+ if (new Date(options?.publishOptions?.statusListValidUntil) < new Date())
2026
+ throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be future date)');
2027
+ // validate validUntil towards validFrom
2028
+ if (new Date(options?.publishOptions?.statusListValidUntil) <= new Date(publishedList.StatusList2021.validFrom))
2029
+ throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be after validFrom)');
2030
+ }
2031
+ // define status list content
2032
+ const content = {
2033
+ StatusList2021: {
2034
+ type: publishedList.StatusList2021.type,
2035
+ encodedList: publishedList.metadata.encoding === 'base64url' ? bitstring : toString(fromString(bitstring, 'base64url'), options.publishOptions.statusListEncoding),
2036
+ validFrom: publishedList.StatusList2021.validFrom,
2037
+ validUntil: options?.publishOptions?.statusListValidUntil || publishedList.StatusList2021.validUntil
2038
+ },
2039
+ metadata: {
2040
+ encoding: options?.publishOptions?.statusListEncoding || publishedList.metadata.encoding,
2041
+ encrypted: false,
2042
+ }
2043
+ };
2044
+ // return tuple of publish result and encryption relevant metadata
2045
+ return [
2046
+ await Cheqd.publishStatusList2021(fromString(JSON.stringify(content), 'utf-8'), statusListMetadata, options?.publishOptions),
2047
+ undefined
2048
+ ];
2049
+ }());
2050
+ // early exit, if publish failed
2051
+ if (!scoped[0])
2052
+ throw new Error('[did-provider-cheqd]: revocation: Failed to publish status list 2021');
2053
+ // return publish result
2054
+ return scoped;
2055
+ }())
2056
+ : undefined;
2057
+ return {
2058
+ revoked: revoked.map((result) => result.status === 'fulfilled' ? result.value.revoked : false),
2059
+ published: topArgs?.publish ? true : undefined,
2060
+ statusList: topArgs?.returnUpdatedStatusList ? await Cheqd.fetchStatusList2021(credentials[0]) : undefined,
2061
+ encryptedStatusList: topArgs?.returnUpdatedEncryptedStatusList ? await blobToHexString(published?.[1]?.encryptedString) : undefined,
2062
+ encryptedSymmetricKey: topArgs?.returnEncryptedSymmetricKey ? published?.[1]?.encryptedSymmetricKey : undefined,
2063
+ symmetricKey: topArgs?.returnSymmetricKey ? published?.[1]?.symmetricKey : undefined,
2064
+ resourceMetadata: topArgs?.returnStatusListMetadata ? await Cheqd.fetchStatusList2021Metadata(credentials[0]) : undefined
2065
+ };
2066
+ }
2067
+ catch (error) {
2068
+ // silent fail + early exit
2069
+ console.error(error);
2070
+ return { revoked: [], error: error };
2071
+ }
2072
+ }
1374
2073
  static async suspendCredential(credential, options) {
1375
2074
  try {
1376
2075
  // validate status purpose
1377
2076
  if (credential?.credentialStatus?.statusPurpose !== 'suspension')
1378
2077
  throw new Error('[did-provider-cheqd]: suspension: Invalid status purpose');
1379
- // fetch status list 2021 metadata
1380
- const metadata = (await Cheqd.fetchStatusList2021Metadata(credential));
1381
- // detect if encrypted
1382
- const isEncrypted = function () {
1383
- switch (metadata.mediaType) {
1384
- case 'application/octet-stream':
1385
- return true;
1386
- case 'application/gzip':
1387
- return false;
1388
- default:
1389
- throw new Error(`[did-provider-cheqd]: suspension: Unsupported media type: ${metadata.mediaType}`);
1390
- }
1391
- }();
2078
+ // fetch status list 2021
2079
+ const publishedList = (await Cheqd.fetchStatusList2021(credential));
1392
2080
  // early return, if encrypted and no decryption key provided
1393
- if (isEncrypted && !options?.topArgs?.symmetricKey)
2081
+ if (publishedList.metadata.encrypted && !options?.topArgs?.symmetricKey)
1394
2082
  throw new Error('[did-provider-cheqd]: suspension: symmetricKey is required, if status list 2021 is encrypted');
1395
2083
  // fetch status list 2021 inscribed in credential
1396
2084
  const statusList2021 = options?.topArgs?.fetchList
1397
2085
  ? (await async function () {
1398
2086
  // if not encrypted, return bitstring
1399
- if (!isEncrypted)
1400
- return await Cheqd.fetchStatusList2021(credential);
2087
+ if (!publishedList.metadata.encrypted)
2088
+ return publishedList.metadata.encoding === 'base64url'
2089
+ ? publishedList.StatusList2021.encodedList
2090
+ : toString(fromString(publishedList.StatusList2021.encodedList, publishedList.metadata.encoding), 'base64url');
1401
2091
  // otherwise, decrypt and return bitstring
1402
2092
  const scopedRawBlob = await toBlob(await Cheqd.fetchStatusList2021(credential, true));
1403
2093
  // decrypt
1404
2094
  return await LitProtocol.decryptDirect(scopedRawBlob, fromString(options?.topArgs?.symmetricKey, 'hex'));
1405
2095
  }())
1406
2096
  : (await async function () {
2097
+ // transcode to base64url, if needed
2098
+ const publishedListTranscoded = publishedList.metadata.encoding === 'base64url'
2099
+ ? publishedList.StatusList2021.encodedList
2100
+ : toString(fromString(publishedList.StatusList2021.encodedList, publishedList.metadata.encoding), 'base64url');
1407
2101
  // if status list 2021 is not fetched, read from file
1408
2102
  if (options?.statusListFile) {
1409
2103
  // if not encrypted, return bitstring
1410
- if (!isEncrypted)
1411
- return new StatusList({ buffer: await Cheqd.getFile(options.statusListFile) }).encode();
2104
+ if (!publishedList.metadata.encrypted) {
2105
+ // construct encoded status list
2106
+ const encoded = new StatusList({ buffer: await Cheqd.getFile(options.statusListFile) }).encode();
2107
+ // validate against published list
2108
+ if (encoded !== publishedListTranscoded)
2109
+ throw new Error('[did-provider-cheqd]: revocation: statusListFile does not match published status list 2021');
2110
+ // return encoded
2111
+ return encoded;
2112
+ }
1412
2113
  // otherwise, decrypt and return bitstring
1413
2114
  const scopedRawBlob = await toBlob(await Cheqd.getFile(options.statusListFile));
1414
2115
  // decrypt
1415
- return await LitProtocol.decryptDirect(scopedRawBlob, fromString(options?.topArgs?.symmetricKey, 'hex'));
2116
+ const decrypted = await LitProtocol.decryptDirect(scopedRawBlob, fromString(options?.topArgs?.symmetricKey, 'hex'));
2117
+ // validate against published list
2118
+ if (decrypted !== publishedListTranscoded)
2119
+ throw new Error('[did-provider-cheqd]: revocation: statusListFile does not match published status list 2021');
2120
+ // return decrypted
2121
+ return decrypted;
1416
2122
  }
1417
2123
  if (!options?.statusListInlineBitstring)
1418
- throw new Error('[did-provider-cheqd]: suspension: statusListInlineBitstring is required, if statusListFile is not provided');
2124
+ throw new Error('[did-provider-cheqd]: revocation: statusListInlineBitstring is required, if statusListFile is not provided');
2125
+ // validate against published list
2126
+ if (options?.statusListInlineBitstring !== publishedListTranscoded)
2127
+ throw new Error('[did-provider-cheqd]: revocation: statusListInlineBitstring does not match published status list 2021');
1419
2128
  // otherwise, read from inline bitstring
1420
2129
  return options?.statusListInlineBitstring;
1421
2130
  }());
@@ -1455,7 +2164,45 @@ export class Cheqd {
1455
2164
  { encryptedString, encryptedSymmetricKey, symmetricKey: toString(symmetricKey, 'hex') }
1456
2165
  ];
1457
2166
  }())
1458
- : [await Cheqd.publishStatusList2021(fromString(bitstring, 'base64url'), statusListMetadata, options?.publishOptions), undefined];
2167
+ : (await async function () {
2168
+ // validate encoding, if provided
2169
+ if (options?.publishOptions?.statusListEncoding && !Object.values(DefaultStatusList2021Encodings).includes(options?.publishOptions?.statusListEncoding)) {
2170
+ throw new Error('[did-provider-cheqd]: revocation: Invalid status list encoding');
2171
+ }
2172
+ // validate validUntil, if provided
2173
+ if (options?.publishOptions?.statusListValidUntil) {
2174
+ // validate validUntil as string
2175
+ if (typeof options?.publishOptions?.statusListValidUntil !== 'string')
2176
+ throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be string)');
2177
+ // validate validUntil as date
2178
+ if (isNaN(Date.parse(options?.publishOptions?.statusListValidUntil)))
2179
+ throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be date)');
2180
+ // validate validUntil as future date
2181
+ if (new Date(options?.publishOptions?.statusListValidUntil) < new Date())
2182
+ throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be future date)');
2183
+ // validate validUntil towards validFrom
2184
+ if (new Date(options?.publishOptions?.statusListValidUntil) <= new Date(publishedList.StatusList2021.validFrom))
2185
+ throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be after validFrom)');
2186
+ }
2187
+ // define status list content
2188
+ const content = {
2189
+ StatusList2021: {
2190
+ type: publishedList.StatusList2021.type,
2191
+ encodedList: publishedList.metadata.encoding === 'base64url' ? bitstring : toString(fromString(bitstring, 'base64url'), options.publishOptions.statusListEncoding),
2192
+ validFrom: publishedList.StatusList2021.validFrom,
2193
+ validUntil: options?.publishOptions?.statusListValidUntil || publishedList.StatusList2021.validUntil
2194
+ },
2195
+ metadata: {
2196
+ encoding: options?.publishOptions?.statusListEncoding || publishedList.metadata.encoding,
2197
+ encrypted: false,
2198
+ }
2199
+ };
2200
+ // return tuple of publish result and encryption relevant metadata
2201
+ return [
2202
+ await Cheqd.publishStatusList2021(fromString(JSON.stringify(content), 'utf-8'), statusListMetadata, options?.publishOptions),
2203
+ undefined
2204
+ ];
2205
+ }());
1459
2206
  // early exit, if publish failed
1460
2207
  if (!scoped[0])
1461
2208
  throw new Error('[did-provider-cheqd]: suspension: Failed to publish status list 2021');
@@ -1466,7 +2213,7 @@ export class Cheqd {
1466
2213
  return {
1467
2214
  suspended: true,
1468
2215
  published: topArgs?.publish ? true : undefined,
1469
- statusList: topArgs?.returnUpdatedStatusList ? bitstring : undefined,
2216
+ statusList: topArgs?.returnUpdatedStatusList ? await Cheqd.fetchStatusList2021(credential) : undefined,
1470
2217
  encryptedStatusList: topArgs?.returnUpdatedEncryptedStatusList ? await blobToHexString(published?.[1]?.encryptedString) : undefined,
1471
2218
  encryptedSymmetricKey: topArgs?.returnEncryptedSymmetricKey ? published?.[1]?.encryptedSymmetricKey : undefined,
1472
2219
  symmetricKey: topArgs?.returnSymmetricKey ? published?.[1]?.symmetricKey : undefined,
@@ -1474,56 +2221,264 @@ export class Cheqd {
1474
2221
  };
1475
2222
  }
1476
2223
  catch (error) {
1477
- // silent fail + early exit, optimised for parallelisation, use with Promise.allSettled
2224
+ // silent fail + early exit
1478
2225
  console.error(error);
1479
2226
  return { suspended: false, error: error };
1480
2227
  }
1481
2228
  }
2229
+ static async suspendCredentials(credentials, options) {
2230
+ // validate credentials - case: empty
2231
+ if (!credentials.length || credentials.length === 0)
2232
+ throw new Error('[did-provider-cheqd]: suspension: No credentials provided');
2233
+ // validate credentials - case: consistent issuer
2234
+ if (credentials.map((credential) => {
2235
+ return (credential.issuer.id)
2236
+ ? credential.issuer.id
2237
+ : credential.issuer;
2238
+ }).filter((value, _, self) => value && value !== self[0]).length > 0)
2239
+ throw new Error('[did-provider-cheqd]: suspension: Credentials must be issued by the same issuer');
2240
+ // validate credentials - case: status list index
2241
+ if (credentials.map((credential) => credential.credentialStatus.statusListIndex).filter((value, index, self) => self.indexOf(value) !== index).length > 0)
2242
+ throw new Error('[did-provider-cheqd]: suspension: Credentials must have unique status list index');
2243
+ // validate credentials - case: status purpose
2244
+ if (!credentials.every((credential) => credential.credentialStatus?.statusPurpose === 'suspension'))
2245
+ throw new Error('[did-provider-cheqd]: suspension: Invalid status purpose');
2246
+ // validate credentials - case: status list id
2247
+ const remote = credentials[0].credentialStatus?.id
2248
+ ? credentials[0].credentialStatus.id.split('#')[0]
2249
+ : (function () {
2250
+ throw new Error('[did-provider-cheqd]: suspension: Invalid status list id');
2251
+ }());
2252
+ // validate credentials - case: status list id format
2253
+ if (!RemoteListPattern.test(remote))
2254
+ 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>');
2255
+ if (!credentials.every((credential) => {
2256
+ return credential.credentialStatus.id.split('#')[0] === remote;
2257
+ }))
2258
+ throw new Error('[did-provider-cheqd]: suspension: Credentials must belong to the same status list');
2259
+ // validate credentials - case: status list type
2260
+ if (!credentials.every((credential) => credential.credentialStatus?.type === 'StatusList2021Entry'))
2261
+ throw new Error('[did-provider-cheqd]: suspension: Invalid status list type');
2262
+ try {
2263
+ // fetch status list 2021
2264
+ const publishedList = (await Cheqd.fetchStatusList2021(credentials[0]));
2265
+ // early return, if encrypted and no decryption key provided
2266
+ if (publishedList.metadata.encrypted && !options?.topArgs?.symmetricKey)
2267
+ throw new Error('[did-provider-cheqd]: suspension: symmetricKey is required, if status list 2021 is encrypted');
2268
+ // fetch status list 2021 inscribed in credential
2269
+ const statusList2021 = options?.topArgs?.fetchList
2270
+ ? (await async function () {
2271
+ // if not encrypted, return bitstring
2272
+ if (!publishedList.metadata.encrypted)
2273
+ return publishedList.metadata.encoding === 'base64url'
2274
+ ? publishedList.StatusList2021.encodedList
2275
+ : toString(fromString(publishedList.StatusList2021.encodedList, publishedList.metadata.encoding), 'base64url');
2276
+ // otherwise, decrypt and return bitstring
2277
+ const scopedRawBlob = await toBlob(await Cheqd.fetchStatusList2021(credentials[0], true));
2278
+ // decrypt
2279
+ return await LitProtocol.decryptDirect(scopedRawBlob, fromString(options?.topArgs?.symmetricKey, 'hex'));
2280
+ }())
2281
+ : (await async function () {
2282
+ // transcode to base64url, if needed
2283
+ const publishedListTranscoded = publishedList.metadata.encoding === 'base64url'
2284
+ ? publishedList.StatusList2021.encodedList
2285
+ : toString(fromString(publishedList.StatusList2021.encodedList, publishedList.metadata.encoding), 'base64url');
2286
+ // if status list 2021 is not fetched, read from file
2287
+ if (options?.statusListFile) {
2288
+ // if not encrypted, return bitstring
2289
+ if (!publishedList.metadata.encrypted) {
2290
+ // construct encoded status list
2291
+ const encoded = new StatusList({ buffer: await Cheqd.getFile(options.statusListFile) }).encode();
2292
+ // validate against published list
2293
+ if (encoded !== publishedListTranscoded)
2294
+ throw new Error('[did-provider-cheqd]: revocation: statusListFile does not match published status list 2021');
2295
+ // return encoded
2296
+ return encoded;
2297
+ }
2298
+ // otherwise, decrypt and return bitstring
2299
+ const scopedRawBlob = await toBlob(await Cheqd.getFile(options.statusListFile));
2300
+ // decrypt
2301
+ const decrypted = await LitProtocol.decryptDirect(scopedRawBlob, fromString(options?.topArgs?.symmetricKey, 'hex'));
2302
+ // validate against published list
2303
+ if (decrypted !== publishedListTranscoded)
2304
+ throw new Error('[did-provider-cheqd]: revocation: statusListFile does not match published status list 2021');
2305
+ // return decrypted
2306
+ return decrypted;
2307
+ }
2308
+ if (!options?.statusListInlineBitstring)
2309
+ throw new Error('[did-provider-cheqd]: revocation: statusListInlineBitstring is required, if statusListFile is not provided');
2310
+ // validate against published list
2311
+ if (options?.statusListInlineBitstring !== publishedListTranscoded)
2312
+ throw new Error('[did-provider-cheqd]: revocation: statusListInlineBitstring does not match published status list 2021');
2313
+ // otherwise, read from inline bitstring
2314
+ return options?.statusListInlineBitstring;
2315
+ }());
2316
+ // parse status list 2021
2317
+ const statusList = await StatusList.decode({ encodedList: statusList2021 });
2318
+ // initiate bulk suspension
2319
+ const suspended = await Promise.allSettled(credentials.map((credential) => {
2320
+ return async function () {
2321
+ // early return, if no credential status
2322
+ if (!credential.credentialStatus)
2323
+ return { suspended: false };
2324
+ // early exit, if credential is already suspended
2325
+ if (statusList.getStatus(Number(credential.credentialStatus.statusListIndex)))
2326
+ return { suspended: false };
2327
+ // update suspension status
2328
+ statusList.setStatus(Number(credential.credentialStatus.statusListIndex), true);
2329
+ // return suspension status
2330
+ return { suspended: true };
2331
+ }();
2332
+ }));
2333
+ // revert bulk ops, if some failed
2334
+ if (suspended.some((result) => result.status === 'fulfilled' && !result.value.suspended))
2335
+ 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 })))}`);
2336
+ // set in-memory status list ref
2337
+ const bitstring = await statusList.encode();
2338
+ // cast top-level args
2339
+ const topArgs = options?.topArgs;
2340
+ // write status list 2021 to file, if provided
2341
+ if (topArgs?.writeToFile) {
2342
+ await Cheqd.writeFile(fromString(bitstring, 'base64url'), options?.statusListFile);
2343
+ }
2344
+ // publish status list 2021, if provided
2345
+ const published = topArgs?.publish
2346
+ ? (await async function () {
2347
+ // fetch status list 2021 metadata
2348
+ const statusListMetadata = await Cheqd.fetchStatusList2021Metadata(credentials[0]);
2349
+ // publish status list 2021 as new version
2350
+ const scoped = topArgs.publishEncrypted
2351
+ ? (await async function () {
2352
+ // instantiate dkg-threshold client, in which case lit-protocol is used
2353
+ const lit = await LitProtocol.create({
2354
+ chain: options?.topArgs?.bootstrapOptions?.chain,
2355
+ litNetwork: options?.topArgs?.bootstrapOptions?.litNetwork
2356
+ });
2357
+ // encrypt
2358
+ const { encryptedString, encryptedSymmetricKey, symmetricKey } = await lit.encrypt(bitstring, options?.topArgs?.encryptionOptions?.unifiedAccessControlConditions, true);
2359
+ // return tuple of publish result and encryption relevant metadata
2360
+ return [
2361
+ await Cheqd.publishStatusList2021(new Uint8Array(await encryptedString.arrayBuffer()), statusListMetadata, options?.publishOptions),
2362
+ { encryptedString, encryptedSymmetricKey, symmetricKey: toString(symmetricKey, 'hex') }
2363
+ ];
2364
+ }())
2365
+ : (await async function () {
2366
+ // validate encoding, if provided
2367
+ if (options?.publishOptions?.statusListEncoding && !Object.values(DefaultStatusList2021Encodings).includes(options?.publishOptions?.statusListEncoding)) {
2368
+ throw new Error('[did-provider-cheqd]: revocation: Invalid status list encoding');
2369
+ }
2370
+ // validate validUntil, if provided
2371
+ if (options?.publishOptions?.statusListValidUntil) {
2372
+ // validate validUntil as string
2373
+ if (typeof options?.publishOptions?.statusListValidUntil !== 'string')
2374
+ throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be string)');
2375
+ // validate validUntil as date
2376
+ if (isNaN(Date.parse(options?.publishOptions?.statusListValidUntil)))
2377
+ throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be date)');
2378
+ // validate validUntil as future date
2379
+ if (new Date(options?.publishOptions?.statusListValidUntil) < new Date())
2380
+ throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be future date)');
2381
+ // validate validUntil towards validFrom
2382
+ if (new Date(options?.publishOptions?.statusListValidUntil) <= new Date(publishedList.StatusList2021.validFrom))
2383
+ throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be after validFrom)');
2384
+ }
2385
+ // define status list content
2386
+ const content = {
2387
+ StatusList2021: {
2388
+ type: publishedList.StatusList2021.type,
2389
+ encodedList: publishedList.metadata.encoding === 'base64url' ? bitstring : toString(fromString(bitstring, 'base64url'), options.publishOptions.statusListEncoding),
2390
+ validFrom: publishedList.StatusList2021.validFrom,
2391
+ validUntil: options?.publishOptions?.statusListValidUntil || publishedList.StatusList2021.validUntil
2392
+ },
2393
+ metadata: {
2394
+ encoding: options?.publishOptions?.statusListEncoding || publishedList.metadata.encoding,
2395
+ encrypted: false,
2396
+ }
2397
+ };
2398
+ // return tuple of publish result and encryption relevant metadata
2399
+ return [
2400
+ await Cheqd.publishStatusList2021(fromString(JSON.stringify(content), 'utf-8'), statusListMetadata, options?.publishOptions),
2401
+ undefined
2402
+ ];
2403
+ }());
2404
+ // early exit, if publish failed
2405
+ if (!scoped[0])
2406
+ throw new Error('[did-provider-cheqd]: suspension: Failed to publish status list 2021');
2407
+ // return publish result
2408
+ return scoped;
2409
+ }())
2410
+ : undefined;
2411
+ return {
2412
+ suspended: suspended.map((result) => result.status === 'fulfilled' ? result.value.suspended : false),
2413
+ published: topArgs?.publish ? true : undefined,
2414
+ statusList: topArgs?.returnUpdatedStatusList ? await Cheqd.fetchStatusList2021(credentials[0]) : undefined,
2415
+ encryptedStatusList: topArgs?.returnUpdatedEncryptedStatusList ? await blobToHexString(published?.[1]?.encryptedString) : undefined,
2416
+ encryptedSymmetricKey: topArgs?.returnEncryptedSymmetricKey ? published?.[1]?.encryptedSymmetricKey : undefined,
2417
+ symmetricKey: topArgs?.returnSymmetricKey ? published?.[1]?.symmetricKey : undefined,
2418
+ resourceMetadata: topArgs?.returnStatusListMetadata ? await Cheqd.fetchStatusList2021Metadata(credentials[0]) : undefined
2419
+ };
2420
+ }
2421
+ catch (error) {
2422
+ // silent fail + early exit
2423
+ console.error(error);
2424
+ return { suspended: [], error: error };
2425
+ }
2426
+ }
1482
2427
  static async unsuspendCredential(credential, options) {
1483
2428
  try {
1484
2429
  // validate status purpose
1485
2430
  if (credential?.credentialStatus?.statusPurpose !== 'suspension')
1486
2431
  throw new Error('[did-provider-cheqd]: unsuspension: Invalid status purpose');
1487
- // fetch status list 2021 metadata
1488
- const metadata = (await Cheqd.fetchStatusList2021Metadata(credential));
1489
- // detect if encrypted
1490
- const isEncrypted = function () {
1491
- switch (metadata.mediaType) {
1492
- case 'application/octet-stream':
1493
- return true;
1494
- case 'application/gzip':
1495
- return false;
1496
- default:
1497
- throw new Error(`[did-provider-cheqd]: unsuspension: Unsupported media type: ${metadata.mediaType}`);
1498
- }
1499
- }();
2432
+ // fetch status list 2021
2433
+ const publishedList = (await Cheqd.fetchStatusList2021(credential));
1500
2434
  // early return, if encrypted and no decryption key provided
1501
- if (isEncrypted && !options?.topArgs?.symmetricKey)
1502
- throw new Error('[did-provider-cheqd]: unsuspension: symmetricKey is required, if status list 2021 is encrypted');
2435
+ if (publishedList.metadata.encrypted && !options?.topArgs?.symmetricKey)
2436
+ throw new Error('[did-provider-cheqd]: suspension: symmetricKey is required, if status list 2021 is encrypted');
1503
2437
  // fetch status list 2021 inscribed in credential
1504
2438
  const statusList2021 = options?.topArgs?.fetchList
1505
2439
  ? (await async function () {
1506
2440
  // if not encrypted, return bitstring
1507
- if (!isEncrypted)
1508
- return await Cheqd.fetchStatusList2021(credential);
2441
+ if (!publishedList.metadata.encrypted)
2442
+ return publishedList.metadata.encoding === 'base64url'
2443
+ ? publishedList.StatusList2021.encodedList
2444
+ : toString(fromString(publishedList.StatusList2021.encodedList, publishedList.metadata.encoding), 'base64url');
1509
2445
  // otherwise, decrypt and return bitstring
1510
2446
  const scopedRawBlob = await toBlob(await Cheqd.fetchStatusList2021(credential, true));
1511
2447
  // decrypt
1512
2448
  return await LitProtocol.decryptDirect(scopedRawBlob, fromString(options?.topArgs?.symmetricKey, 'hex'));
1513
2449
  }())
1514
2450
  : (await async function () {
2451
+ // transcode to base64url, if needed
2452
+ const publishedListTranscoded = publishedList.metadata.encoding === 'base64url'
2453
+ ? publishedList.StatusList2021.encodedList
2454
+ : toString(fromString(publishedList.StatusList2021.encodedList, publishedList.metadata.encoding), 'base64url');
1515
2455
  // if status list 2021 is not fetched, read from file
1516
2456
  if (options?.statusListFile) {
1517
2457
  // if not encrypted, return bitstring
1518
- if (!isEncrypted)
1519
- return new StatusList({ buffer: await Cheqd.getFile(options.statusListFile) }).encode();
2458
+ if (!publishedList.metadata.encrypted) {
2459
+ // construct encoded status list
2460
+ const encoded = new StatusList({ buffer: await Cheqd.getFile(options.statusListFile) }).encode();
2461
+ // validate against published list
2462
+ if (encoded !== publishedListTranscoded)
2463
+ throw new Error('[did-provider-cheqd]: revocation: statusListFile does not match published status list 2021');
2464
+ // return encoded
2465
+ return encoded;
2466
+ }
1520
2467
  // otherwise, decrypt and return bitstring
1521
2468
  const scopedRawBlob = await toBlob(await Cheqd.getFile(options.statusListFile));
1522
2469
  // decrypt
1523
- return await LitProtocol.decryptDirect(scopedRawBlob, fromString(options?.topArgs?.symmetricKey, 'hex'));
2470
+ const decrypted = await LitProtocol.decryptDirect(scopedRawBlob, fromString(options?.topArgs?.symmetricKey, 'hex'));
2471
+ // validate against published list
2472
+ if (decrypted !== publishedListTranscoded)
2473
+ throw new Error('[did-provider-cheqd]: revocation: statusListFile does not match published status list 2021');
2474
+ // return decrypted
2475
+ return decrypted;
1524
2476
  }
1525
2477
  if (!options?.statusListInlineBitstring)
1526
- throw new Error('[did-provider-cheqd]: unsuspension: statusListInlineBitstring is required, if statusListFile is not provided');
2478
+ throw new Error('[did-provider-cheqd]: revocation: statusListInlineBitstring is required, if statusListFile is not provided');
2479
+ // validate against published list
2480
+ if (options?.statusListInlineBitstring !== publishedListTranscoded)
2481
+ throw new Error('[did-provider-cheqd]: revocation: statusListInlineBitstring does not match published status list 2021');
1527
2482
  // otherwise, read from inline bitstring
1528
2483
  return options?.statusListInlineBitstring;
1529
2484
  }());
@@ -1563,7 +2518,45 @@ export class Cheqd {
1563
2518
  { encryptedString, encryptedSymmetricKey, symmetricKey: toString(symmetricKey, 'hex') }
1564
2519
  ];
1565
2520
  }())
1566
- : [await Cheqd.publishStatusList2021(fromString(bitstring, 'base64url'), statusListMetadata, options?.publishOptions), undefined];
2521
+ : (await async function () {
2522
+ // validate encoding, if provided
2523
+ if (options?.publishOptions?.statusListEncoding && !Object.values(DefaultStatusList2021Encodings).includes(options?.publishOptions?.statusListEncoding)) {
2524
+ throw new Error('[did-provider-cheqd]: revocation: Invalid status list encoding');
2525
+ }
2526
+ // validate validUntil, if provided
2527
+ if (options?.publishOptions?.statusListValidUntil) {
2528
+ // validate validUntil as string
2529
+ if (typeof options?.publishOptions?.statusListValidUntil !== 'string')
2530
+ throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be string)');
2531
+ // validate validUntil as date
2532
+ if (isNaN(Date.parse(options?.publishOptions?.statusListValidUntil)))
2533
+ throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be date)');
2534
+ // validate validUntil as future date
2535
+ if (new Date(options?.publishOptions?.statusListValidUntil) < new Date())
2536
+ throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be future date)');
2537
+ // validate validUntil towards validFrom
2538
+ if (new Date(options?.publishOptions?.statusListValidUntil) <= new Date(publishedList.StatusList2021.validFrom))
2539
+ throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be after validFrom)');
2540
+ }
2541
+ // define status list content
2542
+ const content = {
2543
+ StatusList2021: {
2544
+ type: publishedList.StatusList2021.type,
2545
+ encodedList: publishedList.metadata.encoding === 'base64url' ? bitstring : toString(fromString(bitstring, 'base64url'), options.publishOptions.statusListEncoding),
2546
+ validFrom: publishedList.StatusList2021.validFrom,
2547
+ validUntil: options?.publishOptions?.statusListValidUntil || publishedList.StatusList2021.validUntil
2548
+ },
2549
+ metadata: {
2550
+ encoding: options?.publishOptions?.statusListEncoding || publishedList.metadata.encoding,
2551
+ encrypted: false,
2552
+ }
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
+ }());
1567
2560
  // early exit, if publish failed
1568
2561
  if (!scoped[0])
1569
2562
  throw new Error('[did-provider-cheqd]: unsuspension: Failed to publish status list 2021');
@@ -1574,7 +2567,7 @@ export class Cheqd {
1574
2567
  return {
1575
2568
  unsuspended: true,
1576
2569
  published: topArgs?.publish ? true : undefined,
1577
- statusList: topArgs?.returnUpdatedStatusList ? bitstring : undefined,
2570
+ statusList: topArgs?.returnUpdatedStatusList ? await Cheqd.fetchStatusList2021(credential) : undefined,
1578
2571
  encryptedStatusList: topArgs?.returnUpdatedEncryptedStatusList ? await blobToHexString(published?.[1]?.encryptedString) : undefined,
1579
2572
  encryptedSymmetricKey: topArgs?.returnEncryptedSymmetricKey ? published?.[1]?.encryptedSymmetricKey : undefined,
1580
2573
  symmetricKey: topArgs?.returnSymmetricKey ? published?.[1]?.symmetricKey : undefined,
@@ -1582,40 +2575,229 @@ export class Cheqd {
1582
2575
  };
1583
2576
  }
1584
2577
  catch (error) {
1585
- // silent fail + early exit, optimised for parallelisation, use with Promise.allSettled
2578
+ // silent fail + early exit
1586
2579
  console.error(error);
1587
2580
  return { unsuspended: false, error: error };
1588
2581
  }
1589
2582
  }
2583
+ static async unsuspendCredentials(credentials, options) {
2584
+ // validate credentials - case: empty
2585
+ if (!credentials.length || credentials.length === 0)
2586
+ throw new Error('[did-provider-cheqd]: unsuspension: No credentials provided');
2587
+ // validate credentials - case: consistent issuer
2588
+ if (credentials.map((credential) => {
2589
+ return (credential.issuer.id)
2590
+ ? credential.issuer.id
2591
+ : credential.issuer;
2592
+ }).filter((value, _, self) => value && value !== self[0]).length > 0)
2593
+ throw new Error('[did-provider-cheqd]: unsuspension: Credentials must be issued by the same issuer');
2594
+ // validate credentials - case: status list index
2595
+ if (credentials.map((credential) => credential.credentialStatus.statusListIndex).filter((value, index, self) => self.indexOf(value) !== index).length > 0)
2596
+ throw new Error('[did-provider-cheqd]: unsuspension: Credentials must have unique status list index');
2597
+ // validate credentials - case: status purpose
2598
+ if (!credentials.every((credential) => credential.credentialStatus?.statusPurpose === 'suspension'))
2599
+ throw new Error('[did-provider-cheqd]: unsuspension: Invalid status purpose');
2600
+ // validate credentials - case: status list id
2601
+ const remote = credentials[0].credentialStatus?.id
2602
+ ? credentials[0].credentialStatus.id.split('#')[0]
2603
+ : (function () {
2604
+ throw new Error('[did-provider-cheqd]: unsuspension: Invalid status list id');
2605
+ }());
2606
+ // validate credentials - case: status list id format
2607
+ if (!RemoteListPattern.test(remote))
2608
+ 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>');
2609
+ if (!credentials.every((credential) => {
2610
+ return credential.credentialStatus.id.split('#')[0] === remote;
2611
+ }))
2612
+ throw new Error('[did-provider-cheqd]: unsuspension: Credentials must belong to the same status list');
2613
+ // validate credentials - case: status list type
2614
+ if (!credentials.every((credential) => credential.credentialStatus?.type === 'StatusList2021Entry'))
2615
+ throw new Error('[did-provider-cheqd]: unsuspension: Invalid status list type');
2616
+ try {
2617
+ // fetch status list 2021
2618
+ const publishedList = (await Cheqd.fetchStatusList2021(credentials[0]));
2619
+ // early return, if encrypted and no decryption key provided
2620
+ if (publishedList.metadata.encrypted && !options?.topArgs?.symmetricKey)
2621
+ throw new Error('[did-provider-cheqd]: unsuspension: symmetricKey is required, if status list 2021 is encrypted');
2622
+ // fetch status list 2021 inscribed in credential
2623
+ const statusList2021 = options?.topArgs?.fetchList
2624
+ ? (await async function () {
2625
+ // if not encrypted, return bitstring
2626
+ if (!publishedList.metadata.encrypted)
2627
+ return publishedList.metadata.encoding === 'base64url'
2628
+ ? publishedList.StatusList2021.encodedList
2629
+ : toString(fromString(publishedList.StatusList2021.encodedList, publishedList.metadata.encoding), 'base64url');
2630
+ // otherwise, decrypt and return bitstring
2631
+ const scopedRawBlob = await toBlob(await Cheqd.fetchStatusList2021(credentials[0], true));
2632
+ // decrypt
2633
+ return await LitProtocol.decryptDirect(scopedRawBlob, fromString(options?.topArgs?.symmetricKey, 'hex'));
2634
+ }())
2635
+ : (await async function () {
2636
+ // transcode to base64url, if needed
2637
+ const publishedListTranscoded = publishedList.metadata.encoding === 'base64url'
2638
+ ? publishedList.StatusList2021.encodedList
2639
+ : toString(fromString(publishedList.StatusList2021.encodedList, publishedList.metadata.encoding), 'base64url');
2640
+ // if status list 2021 is not fetched, read from file
2641
+ if (options?.statusListFile) {
2642
+ // if not encrypted, return bitstring
2643
+ if (!publishedList.metadata.encrypted) {
2644
+ // construct encoded status list
2645
+ const encoded = new StatusList({ buffer: await Cheqd.getFile(options.statusListFile) }).encode();
2646
+ // validate against published list
2647
+ if (encoded !== publishedListTranscoded)
2648
+ throw new Error('[did-provider-cheqd]: revocation: statusListFile does not match published status list 2021');
2649
+ // return encoded
2650
+ return encoded;
2651
+ }
2652
+ // otherwise, decrypt and return bitstring
2653
+ const scopedRawBlob = await toBlob(await Cheqd.getFile(options.statusListFile));
2654
+ // decrypt
2655
+ const decrypted = await LitProtocol.decryptDirect(scopedRawBlob, fromString(options?.topArgs?.symmetricKey, 'hex'));
2656
+ // validate against published list
2657
+ if (decrypted !== publishedListTranscoded)
2658
+ throw new Error('[did-provider-cheqd]: revocation: statusListFile does not match published status list 2021');
2659
+ // return decrypted
2660
+ return decrypted;
2661
+ }
2662
+ if (!options?.statusListInlineBitstring)
2663
+ throw new Error('[did-provider-cheqd]: revocation: statusListInlineBitstring is required, if statusListFile is not provided');
2664
+ // validate against published list
2665
+ if (options?.statusListInlineBitstring !== publishedListTranscoded)
2666
+ throw new Error('[did-provider-cheqd]: revocation: statusListInlineBitstring does not match published status list 2021');
2667
+ // otherwise, read from inline bitstring
2668
+ return options?.statusListInlineBitstring;
2669
+ }());
2670
+ // parse status list 2021
2671
+ const statusList = await StatusList.decode({ encodedList: statusList2021 });
2672
+ // initiate bulk unsuspension
2673
+ const unsuspended = await Promise.allSettled(credentials.map((credential) => {
2674
+ return async function () {
2675
+ // early return, if no credential status
2676
+ if (!credential.credentialStatus)
2677
+ return { unsuspended: false };
2678
+ // early exit, if credential is already unsuspended
2679
+ if (!statusList.getStatus(Number(credential.credentialStatus.statusListIndex)))
2680
+ return { unsuspended: true };
2681
+ // update unsuspension status
2682
+ statusList.setStatus(Number(credential.credentialStatus.statusListIndex), false);
2683
+ // return unsuspension status
2684
+ return { unsuspended: true };
2685
+ }();
2686
+ }));
2687
+ // revert bulk ops, if some failed
2688
+ if (unsuspended.some((result) => result.status === 'fulfilled' && !result.value.unsuspended))
2689
+ 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 })))}`);
2690
+ // set in-memory status list ref
2691
+ const bitstring = await statusList.encode();
2692
+ // cast top-level args
2693
+ const topArgs = options?.topArgs;
2694
+ // write status list 2021 to file, if provided
2695
+ if (topArgs?.writeToFile) {
2696
+ await Cheqd.writeFile(fromString(bitstring, 'base64url'), options?.statusListFile);
2697
+ }
2698
+ // publish status list 2021, if provided
2699
+ const published = topArgs?.publish
2700
+ ? (await async function () {
2701
+ // fetch status list 2021 metadata
2702
+ const statusListMetadata = await Cheqd.fetchStatusList2021Metadata(credentials[0]);
2703
+ // publish status list 2021 as new version
2704
+ const scoped = topArgs.publishEncrypted
2705
+ ? (await async function () {
2706
+ // instantiate dkg-threshold client, in which case lit-protocol is used
2707
+ const lit = await LitProtocol.create({
2708
+ chain: options?.topArgs?.bootstrapOptions?.chain,
2709
+ litNetwork: options?.topArgs?.bootstrapOptions?.litNetwork
2710
+ });
2711
+ // encrypt
2712
+ const { encryptedString, encryptedSymmetricKey, symmetricKey } = await lit.encrypt(bitstring, options?.topArgs?.encryptionOptions?.unifiedAccessControlConditions, true);
2713
+ // return tuple of publish result and encryption relevant metadata
2714
+ return [
2715
+ await Cheqd.publishStatusList2021(new Uint8Array(await encryptedString.arrayBuffer()), statusListMetadata, options?.publishOptions),
2716
+ { encryptedString, encryptedSymmetricKey, symmetricKey: toString(symmetricKey, 'hex') }
2717
+ ];
2718
+ }())
2719
+ : (await async function () {
2720
+ // validate encoding, if provided
2721
+ if (options?.publishOptions?.statusListEncoding && !Object.values(DefaultStatusList2021Encodings).includes(options?.publishOptions?.statusListEncoding)) {
2722
+ throw new Error('[did-provider-cheqd]: revocation: Invalid status list encoding');
2723
+ }
2724
+ // validate validUntil, if provided
2725
+ if (options?.publishOptions?.statusListValidUntil) {
2726
+ // validate validUntil as string
2727
+ if (typeof options?.publishOptions?.statusListValidUntil !== 'string')
2728
+ throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be string)');
2729
+ // validate validUntil as date
2730
+ if (isNaN(Date.parse(options?.publishOptions?.statusListValidUntil)))
2731
+ throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be date)');
2732
+ // validate validUntil as future date
2733
+ if (new Date(options?.publishOptions?.statusListValidUntil) < new Date())
2734
+ throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be future date)');
2735
+ // validate validUntil towards validFrom
2736
+ if (new Date(options?.publishOptions?.statusListValidUntil) <= new Date(publishedList.StatusList2021.validFrom))
2737
+ throw new Error('[did-provider-cheqd]: revocation: Invalid status list validUntil (must be after validFrom)');
2738
+ }
2739
+ // define status list content
2740
+ const content = {
2741
+ StatusList2021: {
2742
+ type: publishedList.StatusList2021.type,
2743
+ encodedList: publishedList.metadata.encoding === 'base64url' ? bitstring : toString(fromString(bitstring, 'base64url'), options.publishOptions.statusListEncoding),
2744
+ validFrom: publishedList.StatusList2021.validFrom,
2745
+ validUntil: options?.publishOptions?.statusListValidUntil || publishedList.StatusList2021.validUntil
2746
+ },
2747
+ metadata: {
2748
+ encoding: options?.publishOptions?.statusListEncoding || publishedList.metadata.encoding,
2749
+ encrypted: false,
2750
+ }
2751
+ };
2752
+ // return tuple of publish result and encryption relevant metadata
2753
+ return [
2754
+ await Cheqd.publishStatusList2021(fromString(JSON.stringify(content), 'utf-8'), statusListMetadata, options?.publishOptions),
2755
+ undefined
2756
+ ];
2757
+ }());
2758
+ // early exit, if publish failed
2759
+ if (!scoped[0])
2760
+ throw new Error('[did-provider-cheqd]: unsuspension: Failed to publish status list 2021');
2761
+ // return publish result
2762
+ return scoped;
2763
+ }())
2764
+ : undefined;
2765
+ return {
2766
+ unsuspended: unsuspended.map((result) => result.status === 'fulfilled' ? result.value.unsuspended : false),
2767
+ published: topArgs?.publish ? true : undefined,
2768
+ statusList: topArgs?.returnUpdatedStatusList ? await Cheqd.fetchStatusList2021(credentials[0]) : undefined,
2769
+ encryptedStatusList: topArgs?.returnUpdatedEncryptedStatusList ? await blobToHexString(published?.[1]?.encryptedString) : undefined,
2770
+ encryptedSymmetricKey: topArgs?.returnEncryptedSymmetricKey ? published?.[1]?.encryptedSymmetricKey : undefined,
2771
+ symmetricKey: topArgs?.returnSymmetricKey ? published?.[1]?.symmetricKey : undefined,
2772
+ resourceMetadata: topArgs?.returnStatusListMetadata ? await Cheqd.fetchStatusList2021Metadata(credentials[0]) : undefined
2773
+ };
2774
+ }
2775
+ catch (error) {
2776
+ // silent fail + early exit
2777
+ console.error(error);
2778
+ return { unsuspended: [], error: error };
2779
+ }
2780
+ }
1590
2781
  static async checkRevoked(credential, options = { fetchList: true }) {
1591
2782
  // validate status purpose
1592
2783
  if (credential.credentialStatus?.statusPurpose !== 'revocation') {
1593
2784
  throw new Error(`[did-provider-cheqd]: revocation: Unsupported status purpose: ${credential.credentialStatus?.statusPurpose}`);
1594
2785
  }
1595
- // fetch status list 2021 metadata
1596
- const metadata = (await Cheqd.fetchStatusList2021Metadata(credential));
1597
- // detect if encrypted
1598
- const isEncrypted = function () {
1599
- switch (metadata.mediaType) {
1600
- case 'application/octet-stream':
1601
- return true;
1602
- case 'application/gzip':
1603
- return false;
1604
- default:
1605
- throw new Error(`[did-provider-cheqd]: revocation: Unsupported media type: ${metadata.mediaType}`);
1606
- }
1607
- }();
2786
+ // fetch status list 2021
2787
+ const publishedList = (await Cheqd.fetchStatusList2021(credential));
1608
2788
  // early return, if encrypted and decryption key is not provided
1609
- if (isEncrypted && !options?.topArgs?.encryptedSymmetricKey)
2789
+ if (publishedList.metadata.encrypted && !options?.topArgs?.encryptedSymmetricKey)
1610
2790
  throw new Error('[did-provider-cheqd]: revocation: encryptedSymmetricKey is required, if status list 2021 is encrypted');
1611
2791
  // fetch status list 2021 inscribed in credential
1612
2792
  const statusList2021 = options?.topArgs?.fetchList
1613
2793
  ? (await async function () {
1614
2794
  // if not encrypted, return bitstring
1615
- if (!isEncrypted)
1616
- return await Cheqd.fetchStatusList2021(credential);
2795
+ if (!publishedList.metadata.encrypted)
2796
+ return publishedList.metadata.encoding === 'base64url'
2797
+ ? publishedList.StatusList2021.encodedList
2798
+ : toString(fromString(publishedList.StatusList2021.encodedList, publishedList.metadata.encoding), 'base64url');
1617
2799
  // otherwise, decrypt and return bitstring
1618
- const scopedRawBlob = await toBlob(await Cheqd.fetchStatusList2021(credential, true));
2800
+ const scopedRawBlob = await toBlob(fromString(publishedList.StatusList2021.encodedList, publishedList.metadata.encoding));
1619
2801
  // instantiate dkg-threshold client, in which case lit-protocol is used
1620
2802
  const lit = await LitProtocol.create({
1621
2803
  chain: options?.topArgs?.bootstrapOptions?.chain,
@@ -1625,23 +2807,37 @@ export class Cheqd {
1625
2807
  return await lit.decrypt(scopedRawBlob, options?.topArgs?.encryptedSymmetricKey, options?.topArgs?.decryptionOptions?.unifiedAccessControlConditions);
1626
2808
  }())
1627
2809
  : (await async function () {
2810
+ // transcode to base64url, if needed
2811
+ const publishedListTranscoded = publishedList.metadata.encoding === 'base64url'
2812
+ ? publishedList.StatusList2021.encodedList
2813
+ : toString(fromString(publishedList.StatusList2021.encodedList, publishedList.metadata.encoding), 'base64url');
1628
2814
  // if status list 2021 is not fetched, read from file
1629
2815
  if (options?.statusListFile) {
1630
2816
  // if not encrypted, return bitstring
1631
- if (!isEncrypted)
1632
- return new StatusList({ buffer: await Cheqd.getFile(options.statusListFile) }).encode();
2817
+ if (!publishedList.metadata.encrypted) {
2818
+ // construct encoded status list
2819
+ const encoded = new StatusList({ buffer: await Cheqd.getFile(options.statusListFile) }).encode();
2820
+ // validate against published list
2821
+ if (encoded !== publishedListTranscoded)
2822
+ throw new Error('[did-provider-cheqd]: revocation: statusListFile does not match published status list 2021');
2823
+ // return encoded
2824
+ return encoded;
2825
+ }
1633
2826
  // otherwise, decrypt and return bitstring
1634
2827
  const scopedRawBlob = await toBlob(await Cheqd.getFile(options.statusListFile));
1635
- // instantiate dkg-threshold client, in which case lit-protocol is used
1636
- const lit = await LitProtocol.create({
1637
- chain: options?.topArgs?.bootstrapOptions?.chain,
1638
- litNetwork: options?.topArgs?.bootstrapOptions?.litNetwork
1639
- });
1640
2828
  // decrypt
1641
- return await lit.decrypt(scopedRawBlob, options?.topArgs?.encryptedSymmetricKey, options?.topArgs?.decryptionOptions?.unifiedAccessControlConditions);
2829
+ const decrypted = await LitProtocol.decryptDirect(scopedRawBlob, fromString(options?.topArgs?.symmetricKey, 'hex'));
2830
+ // validate against published list
2831
+ if (decrypted !== publishedListTranscoded)
2832
+ throw new Error('[did-provider-cheqd]: revocation: statusListFile does not match published status list 2021');
2833
+ // return decrypted
2834
+ return decrypted;
1642
2835
  }
1643
2836
  if (!options?.statusListInlineBitstring)
1644
- throw new Error(' [did-provider-cheqd]: revocation: statusListInlineBitstring is required, if statusListFile is not provided');
2837
+ throw new Error('[did-provider-cheqd]: revocation: statusListInlineBitstring is required, if statusListFile is not provided');
2838
+ // validate against published list
2839
+ if (options?.statusListInlineBitstring !== publishedListTranscoded)
2840
+ throw new Error('[did-provider-cheqd]: revocation: statusListInlineBitstring does not match published status list 2021');
1645
2841
  // otherwise, read from inline bitstring
1646
2842
  return options?.statusListInlineBitstring;
1647
2843
  }());
@@ -1655,30 +2851,21 @@ export class Cheqd {
1655
2851
  if (credential.credentialStatus?.statusPurpose !== 'suspension') {
1656
2852
  throw new Error(`[did-provider-cheqd]: suspension: Unsupported status purpose: ${credential.credentialStatus?.statusPurpose}`);
1657
2853
  }
1658
- // fetch status list 2021 metadata
1659
- const metadata = (await Cheqd.fetchStatusList2021Metadata(credential));
1660
- // detect if encrypted
1661
- const isEncrypted = function () {
1662
- switch (metadata.mediaType) {
1663
- case 'application/octet-stream':
1664
- return true;
1665
- case 'application/gzip':
1666
- return false;
1667
- default:
1668
- throw new Error(`[did-provider-cheqd]: suspension: Unsupported media type: ${metadata.mediaType}`);
1669
- }
1670
- }();
2854
+ // fetch status list 2021
2855
+ const publishedList = (await Cheqd.fetchStatusList2021(credential));
1671
2856
  // early return, if encrypted and decryption key is not provided
1672
- if (isEncrypted && !options?.topArgs?.encryptedSymmetricKey)
1673
- throw new Error('[did-provider-cheqd]: suspension: encryptedSymmetricKey is required, if status list 2021 is encrypted');
2857
+ if (publishedList.metadata.encrypted && !options?.topArgs?.encryptedSymmetricKey)
2858
+ throw new Error('[did-provider-cheqd]: revocation: encryptedSymmetricKey is required, if status list 2021 is encrypted');
1674
2859
  // fetch status list 2021 inscribed in credential
1675
2860
  const statusList2021 = options?.topArgs?.fetchList
1676
2861
  ? (await async function () {
1677
2862
  // if not encrypted, return bitstring
1678
- if (!isEncrypted)
1679
- return await Cheqd.fetchStatusList2021(credential);
2863
+ if (!publishedList.metadata.encrypted)
2864
+ return publishedList.metadata.encoding === 'base64url'
2865
+ ? publishedList.StatusList2021.encodedList
2866
+ : toString(fromString(publishedList.StatusList2021.encodedList, publishedList.metadata.encoding), 'base64url');
1680
2867
  // otherwise, decrypt and return bitstring
1681
- const scopedRawBlob = await toBlob(await Cheqd.fetchStatusList2021(credential, true));
2868
+ const scopedRawBlob = await toBlob(fromString(publishedList.StatusList2021.encodedList, publishedList.metadata.encoding));
1682
2869
  // instantiate dkg-threshold client, in which case lit-protocol is used
1683
2870
  const lit = await LitProtocol.create({
1684
2871
  chain: options?.topArgs?.bootstrapOptions?.chain,
@@ -1688,23 +2875,37 @@ export class Cheqd {
1688
2875
  return await lit.decrypt(scopedRawBlob, options?.topArgs?.encryptedSymmetricKey, options?.topArgs?.decryptionOptions?.unifiedAccessControlConditions);
1689
2876
  }())
1690
2877
  : (await async function () {
2878
+ // transcode to base64url, if needed
2879
+ const publishedListTranscoded = publishedList.metadata.encoding === 'base64url'
2880
+ ? publishedList.StatusList2021.encodedList
2881
+ : toString(fromString(publishedList.StatusList2021.encodedList, publishedList.metadata.encoding), 'base64url');
1691
2882
  // if status list 2021 is not fetched, read from file
1692
2883
  if (options?.statusListFile) {
1693
2884
  // if not encrypted, return bitstring
1694
- if (!isEncrypted)
1695
- return new StatusList({ buffer: await Cheqd.getFile(options.statusListFile) }).encode();
2885
+ if (!publishedList.metadata.encrypted) {
2886
+ // construct encoded status list
2887
+ const encoded = new StatusList({ buffer: await Cheqd.getFile(options.statusListFile) }).encode();
2888
+ // validate against published list
2889
+ if (encoded !== publishedListTranscoded)
2890
+ throw new Error('[did-provider-cheqd]: revocation: statusListFile does not match published status list 2021');
2891
+ // return encoded
2892
+ return encoded;
2893
+ }
1696
2894
  // otherwise, decrypt and return bitstring
1697
2895
  const scopedRawBlob = await toBlob(await Cheqd.getFile(options.statusListFile));
1698
- // instantiate dkg-threshold client, in which case lit-protocol is used
1699
- const lit = await LitProtocol.create({
1700
- chain: options?.topArgs?.bootstrapOptions?.chain,
1701
- litNetwork: options?.topArgs?.bootstrapOptions?.litNetwork
1702
- });
1703
2896
  // decrypt
1704
- return await lit.decrypt(scopedRawBlob, options?.topArgs?.encryptedSymmetricKey, options?.topArgs?.decryptionOptions?.unifiedAccessControlConditions);
2897
+ const decrypted = await LitProtocol.decryptDirect(scopedRawBlob, fromString(options?.topArgs?.symmetricKey, 'hex'));
2898
+ // validate against published list
2899
+ if (decrypted !== publishedListTranscoded)
2900
+ throw new Error('[did-provider-cheqd]: revocation: statusListFile does not match published status list 2021');
2901
+ // return decrypted
2902
+ return decrypted;
1705
2903
  }
1706
2904
  if (!options?.statusListInlineBitstring)
1707
- throw new Error(' [did-provider-cheqd]: suspension: statusListInlineBitstring is required, if statusListFile is not provided');
2905
+ throw new Error('[did-provider-cheqd]: revocation: statusListInlineBitstring is required, if statusListFile is not provided');
2906
+ // validate against published list
2907
+ if (options?.statusListInlineBitstring !== publishedListTranscoded)
2908
+ throw new Error('[did-provider-cheqd]: revocation: statusListInlineBitstring does not match published status list 2021');
1708
2909
  // otherwise, read from inline bitstring
1709
2910
  return options?.statusListInlineBitstring;
1710
2911
  }());
@@ -1720,10 +2921,11 @@ export class Cheqd {
1720
2921
  id: options?.resourceId || v4(),
1721
2922
  name: statusList2021Metadata.resourceName,
1722
2923
  version: options?.resourceVersion || new Date().toISOString(),
1723
- resourceType: 'StatusList2021',
2924
+ alsoKnownAs: options?.resourceAlsoKnownAs || [],
2925
+ resourceType: statusList2021Metadata.resourceType,
1724
2926
  data: statusList2021Raw
1725
2927
  };
1726
- return await options.context.agent[CreateStatusList2021MethodName]({
2928
+ return await options.context.agent[BroadcastStatusList2021MethodName]({
1727
2929
  kms: (await options.context.agent.keyManagerGetKeyManagementSystems())[0],
1728
2930
  payload,
1729
2931
  network: statusList2021Metadata.resourceURI.split(':')[2],
@@ -1741,25 +2943,26 @@ export class Cheqd {
1741
2943
  // validate credential status list status purpose
1742
2944
  if (credential.credentialStatus.statusPurpose !== 'revocation' && credential.credentialStatus.statusPurpose !== 'suspension')
1743
2945
  throw new Error('[did-provider-cheqd]: fetch status list: Credential status purpose is not valid');
1744
- // validate credential status list status list credential
1745
- if (!credential.credentialStatus.statusListCredential)
1746
- throw new Error('[did-provider-cheqd]: fetch status list: Credential status list credential is not present');
1747
2946
  // fetch status list 2021
1748
- const raw = await (await fetch(credential.credentialStatus.statusListCredential)).arrayBuffer();
2947
+ const content = await (await fetch(credential.credentialStatus.id.split('#')[0])).json();
1749
2948
  // return raw if requested
1750
- if (returnRaw)
1751
- return new Uint8Array(raw);
1752
- // otherwise, parse to bitstring and return
1753
- const bitstring = toString(new Uint8Array(raw), 'base64url');
1754
- return bitstring;
2949
+ if (returnRaw) {
2950
+ return fromString(content.StatusList2021.encodedList, content.metadata.encoding);
2951
+ }
2952
+ // otherwise, return content
2953
+ return content;
1755
2954
  }
1756
2955
  static async fetchStatusList2021Metadata(credential) {
1757
2956
  // get base url
1758
- const baseUrl = new URL(credential.credentialStatus?.statusListCredential);
2957
+ const baseUrl = new URL(credential.credentialStatus.id.split('#')[0]);
1759
2958
  // get resource name
1760
2959
  const resourceName = baseUrl.searchParams.get('resourceName');
2960
+ // get resource type
2961
+ const resourceType = baseUrl.searchParams.get('resourceType');
1761
2962
  // unset resource name
1762
2963
  baseUrl.searchParams.delete('resourceName');
2964
+ // unset resource type
2965
+ baseUrl.searchParams.delete('resourceType');
1763
2966
  // construct metadata url
1764
2967
  const metadataUrl = `${baseUrl.toString()}/metadata`;
1765
2968
  // fetch collection metadata
@@ -1768,7 +2971,7 @@ export class Cheqd {
1768
2971
  if (!collectionMetadata?.contentStream?.linkedResourceMetadata)
1769
2972
  throw new Error('[did-provider-cheqd]: fetch status list metadata: No linked resources found');
1770
2973
  // find relevant resources by resource name
1771
- const resourceVersioning = collectionMetadata.contentStream.linkedResourceMetadata.filter((resource) => resource.resourceName === resourceName);
2974
+ const resourceVersioning = collectionMetadata.contentStream.linkedResourceMetadata.filter((resource) => resource.resourceName === resourceName && resource.resourceType === resourceType);
1772
2975
  // early exit if no relevant resources
1773
2976
  if (!resourceVersioning.length || resourceVersioning.length === 0)
1774
2977
  throw new Error(`[did-provider-cheqd]: fetch status list metadata: No relevant resources found by resource name ${resourceName}`);