@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.
- package/build/cjs/agent/ICheqd.d.ts +175 -22
- package/build/cjs/agent/ICheqd.d.ts.map +1 -1
- package/build/cjs/agent/ICheqd.js +1424 -221
- package/build/cjs/agent/ICheqd.js.map +1 -1
- package/build/cjs/did-manager/cheqd-did-provider.d.ts +20 -3
- package/build/cjs/did-manager/cheqd-did-provider.d.ts.map +1 -1
- package/build/cjs/did-manager/cheqd-did-provider.js +15 -1
- package/build/cjs/did-manager/cheqd-did-provider.js.map +1 -1
- package/build/esm/agent/ICheqd.d.ts +175 -22
- package/build/esm/agent/ICheqd.d.ts.map +1 -1
- package/build/esm/agent/ICheqd.js +1424 -221
- package/build/esm/agent/ICheqd.js.map +1 -1
- package/build/esm/did-manager/cheqd-did-provider.d.ts +20 -3
- package/build/esm/did-manager/cheqd-did-provider.d.ts.map +1 -1
- package/build/esm/did-manager/cheqd-did-provider.js +14 -0
- package/build/esm/did-manager/cheqd-did-provider.js.map +1 -1
- package/build/tsconfig.cjs.tsbuildinfo +1 -1
- package/build/tsconfig.esm.tsbuildinfo +1 -1
- package/build/tsconfig.types.tsbuildinfo +1 -1
- package/build/types/agent/ICheqd.d.ts +175 -22
- package/build/types/agent/ICheqd.d.ts.map +1 -1
- package/build/types/did-manager/cheqd-did-provider.d.ts +20 -3
- package/build/types/did-manager/cheqd-did-provider.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/agent/ICheqd.ts +1820 -363
- package/src/did-manager/cheqd-did-provider.ts +26 -3
|
@@ -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
|
-
|
|
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
|
|
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": "
|
|
170
|
+
"type": "object"
|
|
140
171
|
}
|
|
141
172
|
},
|
|
142
|
-
"
|
|
143
|
-
"description": "
|
|
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
|
|
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
|
-
[
|
|
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.
|
|
634
|
-
throw new Error('[did-provider-cheqd]:
|
|
664
|
+
if (typeof args.issuerDid !== 'string' || !args.issuerDid) {
|
|
665
|
+
throw new Error('[did-provider-cheqd]: issuerDid is required');
|
|
635
666
|
}
|
|
636
|
-
if (typeof args.
|
|
637
|
-
throw new Error('[did-provider-cheqd]:
|
|
667
|
+
if (typeof args.statusListName !== 'string' || !args.statusListName) {
|
|
668
|
+
throw new Error('[did-provider-cheqd]: statusListName is required');
|
|
638
669
|
}
|
|
639
|
-
if (args
|
|
640
|
-
|
|
670
|
+
if (typeof args.statusPurpose !== 'string' || !args.statusPurpose) {
|
|
671
|
+
throw new Error('[did-provider-cheqd]: statusPurpose is required');
|
|
641
672
|
}
|
|
642
|
-
if (typeof args
|
|
643
|
-
|
|
673
|
+
if (typeof args.encrypted === 'undefined') {
|
|
674
|
+
throw new Error('[did-provider-cheqd]: encrypted is required');
|
|
644
675
|
}
|
|
645
|
-
//
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
677
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1070
|
-
|
|
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
|
-
//
|
|
1109
|
-
|
|
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
|
-
//
|
|
1148
|
-
|
|
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
|
|
1272
|
-
const
|
|
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 (
|
|
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 (!
|
|
1292
|
-
return
|
|
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(
|
|
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 (!
|
|
1303
|
-
|
|
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
|
-
|
|
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
|
-
:
|
|
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 ?
|
|
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
|
|
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
|
|
1380
|
-
const
|
|
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 (
|
|
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 (!
|
|
1400
|
-
return
|
|
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 (!
|
|
1411
|
-
|
|
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
|
-
|
|
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]:
|
|
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
|
-
:
|
|
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 ?
|
|
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
|
|
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
|
|
1488
|
-
const
|
|
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 (
|
|
1502
|
-
throw new Error('[did-provider-cheqd]:
|
|
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 (!
|
|
1508
|
-
return
|
|
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 (!
|
|
1519
|
-
|
|
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
|
-
|
|
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]:
|
|
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
|
-
:
|
|
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 ?
|
|
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
|
|
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
|
|
1596
|
-
const
|
|
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 (
|
|
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 (!
|
|
1616
|
-
return
|
|
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(
|
|
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 (!
|
|
1632
|
-
|
|
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
|
-
|
|
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('
|
|
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
|
|
1659
|
-
const
|
|
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 (
|
|
1673
|
-
throw new Error('[did-provider-cheqd]:
|
|
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 (!
|
|
1679
|
-
return
|
|
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(
|
|
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 (!
|
|
1695
|
-
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
2924
|
+
alsoKnownAs: options?.resourceAlsoKnownAs || [],
|
|
2925
|
+
resourceType: statusList2021Metadata.resourceType,
|
|
1724
2926
|
data: statusList2021Raw
|
|
1725
2927
|
};
|
|
1726
|
-
return await options.context.agent[
|
|
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
|
|
2947
|
+
const content = await (await fetch(credential.credentialStatus.id.split('#')[0])).json();
|
|
1749
2948
|
// return raw if requested
|
|
1750
|
-
if (returnRaw)
|
|
1751
|
-
return
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
return
|
|
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
|
|
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}`);
|