@didcid/gatekeeper 0.1.3
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/LICENSE +21 -0
- package/README.md +65 -0
- package/dist/cjs/abstract-json-BJMq-iEa.cjs +216 -0
- package/dist/cjs/db/json-cache.cjs +75 -0
- package/dist/cjs/db/json-memory.cjs +29 -0
- package/dist/cjs/db/json.cjs +36 -0
- package/dist/cjs/db/mongo.cjs +242 -0
- package/dist/cjs/db/redis.cjs +291 -0
- package/dist/cjs/db/sqlite.cjs +330 -0
- package/dist/cjs/errors-DQaog-FG.cjs +34 -0
- package/dist/cjs/gatekeeper-client.cjs +369 -0
- package/dist/cjs/gatekeeper.cjs +42212 -0
- package/dist/cjs/index.cjs +12 -0
- package/dist/cjs/node.cjs +40 -0
- package/dist/esm/db/abstract-json.js +212 -0
- package/dist/esm/db/abstract-json.js.map +1 -0
- package/dist/esm/db/json-cache.js +68 -0
- package/dist/esm/db/json-cache.js.map +1 -0
- package/dist/esm/db/json-memory.js +22 -0
- package/dist/esm/db/json-memory.js.map +1 -0
- package/dist/esm/db/json.js +29 -0
- package/dist/esm/db/json.js.map +1 -0
- package/dist/esm/db/mongo.js +236 -0
- package/dist/esm/db/mongo.js.map +1 -0
- package/dist/esm/db/redis.js +285 -0
- package/dist/esm/db/redis.js.map +1 -0
- package/dist/esm/db/sqlite.js +305 -0
- package/dist/esm/db/sqlite.js.map +1 -0
- package/dist/esm/gatekeeper-client.js +363 -0
- package/dist/esm/gatekeeper-client.js.map +1 -0
- package/dist/esm/gatekeeper.js +1090 -0
- package/dist/esm/gatekeeper.js.map +1 -0
- package/dist/esm/index.js +4 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/node.js +8 -0
- package/dist/esm/node.js.map +1 -0
- package/dist/esm/types.js +2 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/types/db/abstract-json.d.ts +26 -0
- package/dist/types/db/json-cache.d.ts +14 -0
- package/dist/types/db/json-memory.d.ts +9 -0
- package/dist/types/db/json.d.ts +8 -0
- package/dist/types/db/mongo.d.ts +23 -0
- package/dist/types/db/redis.d.ts +29 -0
- package/dist/types/db/sqlite.d.ts +30 -0
- package/dist/types/gatekeeper-client.d.ts +40 -0
- package/dist/types/gatekeeper.d.ts +67 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/node.d.ts +7 -0
- package/dist/types/types.d.ts +226 -0
- package/package.json +128 -0
|
@@ -0,0 +1,1090 @@
|
|
|
1
|
+
import CipherNode from '@didcid/cipher/node';
|
|
2
|
+
import { copyJSON, compareOrdinals } from '@didcid/common/utils';
|
|
3
|
+
import { isValidDID, generateCID } from '@didcid/ipfs/utils';
|
|
4
|
+
import { InvalidParameterError, InvalidOperationError } from '@didcid/common/errors';
|
|
5
|
+
const ValidVersions = [1];
|
|
6
|
+
const ValidTypes = ['agent', 'asset'];
|
|
7
|
+
// Registries that are considered valid when importing DIDs from the network
|
|
8
|
+
const ValidRegistries = [
|
|
9
|
+
'local',
|
|
10
|
+
'hyperswarm',
|
|
11
|
+
'BTC:mainnet',
|
|
12
|
+
'BTC:testnet4',
|
|
13
|
+
'BTC:signet',
|
|
14
|
+
'FTC:testnet5',
|
|
15
|
+
];
|
|
16
|
+
var ImportStatus;
|
|
17
|
+
(function (ImportStatus) {
|
|
18
|
+
ImportStatus["ADDED"] = "added";
|
|
19
|
+
ImportStatus["MERGED"] = "merged";
|
|
20
|
+
ImportStatus["REJECTED"] = "rejected";
|
|
21
|
+
ImportStatus["DEFERRED"] = "deferred";
|
|
22
|
+
})(ImportStatus || (ImportStatus = {}));
|
|
23
|
+
export default class Gatekeeper {
|
|
24
|
+
db;
|
|
25
|
+
eventsQueue;
|
|
26
|
+
eventsSeen;
|
|
27
|
+
verifiedDIDs;
|
|
28
|
+
isProcessingEvents;
|
|
29
|
+
ipfs;
|
|
30
|
+
cipher;
|
|
31
|
+
didPrefix;
|
|
32
|
+
maxOpBytes;
|
|
33
|
+
maxQueueSize;
|
|
34
|
+
supportedRegistries;
|
|
35
|
+
didLocks = new Map();
|
|
36
|
+
constructor(options) {
|
|
37
|
+
if (!options || !options.db) {
|
|
38
|
+
throw new InvalidParameterError('missing options.db');
|
|
39
|
+
}
|
|
40
|
+
this.db = options.db;
|
|
41
|
+
// Only used for unit testing
|
|
42
|
+
// TBD replace console with a real logging package
|
|
43
|
+
if (options.console) {
|
|
44
|
+
// eslint-disable-next-line
|
|
45
|
+
console = options.console;
|
|
46
|
+
}
|
|
47
|
+
this.eventsQueue = [];
|
|
48
|
+
this.eventsSeen = {};
|
|
49
|
+
this.verifiedDIDs = {};
|
|
50
|
+
this.isProcessingEvents = false;
|
|
51
|
+
this.ipfs = options.ipfs;
|
|
52
|
+
this.cipher = new CipherNode();
|
|
53
|
+
this.didPrefix = options.didPrefix || 'did:cid';
|
|
54
|
+
this.maxOpBytes = options.maxOpBytes || 64 * 1024; // 64KB
|
|
55
|
+
this.maxQueueSize = options.maxQueueSize || 100;
|
|
56
|
+
// Only DIDs registered on supported registries will be created by this node
|
|
57
|
+
this.supportedRegistries = options.registries || ['local', 'hyperswarm'];
|
|
58
|
+
for (const registry of this.supportedRegistries) {
|
|
59
|
+
if (!ValidRegistries.includes(registry)) {
|
|
60
|
+
throw new InvalidParameterError(`registry=${registry}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async withDidLock(did, fn) {
|
|
65
|
+
const prev = this.didLocks.get(did) ?? Promise.resolve();
|
|
66
|
+
let release = () => { };
|
|
67
|
+
const gate = new Promise(r => (release = r));
|
|
68
|
+
this.didLocks.set(did, prev.then(() => gate, () => gate));
|
|
69
|
+
try {
|
|
70
|
+
await prev;
|
|
71
|
+
return await fn();
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
release();
|
|
75
|
+
if (this.didLocks.get(did) === gate) {
|
|
76
|
+
this.didLocks.delete(did);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async verifyDb(options) {
|
|
81
|
+
const chatty = options?.chatty ?? true;
|
|
82
|
+
const dids = await this.getDIDs();
|
|
83
|
+
const total = dids.length;
|
|
84
|
+
let n = 0;
|
|
85
|
+
let expired = 0;
|
|
86
|
+
let invalid = 0;
|
|
87
|
+
let verified = Object.keys(this.verifiedDIDs).length;
|
|
88
|
+
if (chatty) {
|
|
89
|
+
console.time('verifyDb');
|
|
90
|
+
}
|
|
91
|
+
for (const did of dids) {
|
|
92
|
+
n += 1;
|
|
93
|
+
if (this.verifiedDIDs[did]) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
let validUntil = null;
|
|
97
|
+
try {
|
|
98
|
+
const doc = await this.resolveDID(did, { verify: true });
|
|
99
|
+
validUntil = doc.didDocumentRegistration?.validUntil;
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (chatty) {
|
|
103
|
+
console.log(`removing ${n}/${total} ${did} invalid`);
|
|
104
|
+
}
|
|
105
|
+
invalid += 1;
|
|
106
|
+
await this.db.deleteEvents(did);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (validUntil) {
|
|
110
|
+
const expires = new Date(validUntil);
|
|
111
|
+
const now = new Date();
|
|
112
|
+
if (expires < now) {
|
|
113
|
+
if (chatty) {
|
|
114
|
+
console.log(`removing ${n}/${total} ${did} expired`);
|
|
115
|
+
}
|
|
116
|
+
await this.db.deleteEvents(did);
|
|
117
|
+
expired += 1;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
const minutesLeft = Math.round((expires.getTime() - now.getTime()) / 60 / 1000);
|
|
121
|
+
if (chatty) {
|
|
122
|
+
console.log(`expiring ${n}/${total} ${did} in ${minutesLeft} minutes`);
|
|
123
|
+
}
|
|
124
|
+
verified += 1;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
if (chatty) {
|
|
129
|
+
console.log(`verifying ${n}/${total} ${did} OK`);
|
|
130
|
+
}
|
|
131
|
+
this.verifiedDIDs[did] = true;
|
|
132
|
+
verified += 1;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Clear queue of permanently invalid events
|
|
136
|
+
this.eventsQueue = [];
|
|
137
|
+
if (chatty) {
|
|
138
|
+
console.timeEnd('verifyDb');
|
|
139
|
+
}
|
|
140
|
+
return { total, verified, expired, invalid };
|
|
141
|
+
}
|
|
142
|
+
async checkDIDs(options) {
|
|
143
|
+
const chatty = options?.chatty ?? false;
|
|
144
|
+
let dids = options?.dids;
|
|
145
|
+
if (!dids) {
|
|
146
|
+
dids = await this.getDIDs();
|
|
147
|
+
}
|
|
148
|
+
const total = dids.length;
|
|
149
|
+
let n = 0;
|
|
150
|
+
let agents = 0;
|
|
151
|
+
let assets = 0;
|
|
152
|
+
let confirmed = 0;
|
|
153
|
+
let unconfirmed = 0;
|
|
154
|
+
let ephemeral = 0;
|
|
155
|
+
let invalid = 0;
|
|
156
|
+
const byRegistry = {};
|
|
157
|
+
const byVersion = {};
|
|
158
|
+
for (const did of dids) {
|
|
159
|
+
n += 1;
|
|
160
|
+
try {
|
|
161
|
+
const doc = await this.resolveDID(did);
|
|
162
|
+
if (doc.didResolutionMetadata?.error) {
|
|
163
|
+
invalid += 1;
|
|
164
|
+
if (chatty) {
|
|
165
|
+
console.log(`can't resolve ${n}/${total} ${did} ${doc.didResolutionMetadata.error}`);
|
|
166
|
+
}
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (chatty) {
|
|
170
|
+
console.log(`resolved ${n}/${total} ${did} OK`);
|
|
171
|
+
}
|
|
172
|
+
if (doc.didDocumentRegistration?.type === 'agent') {
|
|
173
|
+
agents += 1;
|
|
174
|
+
}
|
|
175
|
+
if (doc.didDocumentRegistration?.type === 'asset') {
|
|
176
|
+
assets += 1;
|
|
177
|
+
}
|
|
178
|
+
if (doc.didDocumentMetadata?.confirmed) {
|
|
179
|
+
confirmed += 1;
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
unconfirmed += 1;
|
|
183
|
+
}
|
|
184
|
+
if (doc.didDocumentRegistration?.validUntil) {
|
|
185
|
+
ephemeral += 1;
|
|
186
|
+
}
|
|
187
|
+
const registry = doc.didDocumentRegistration?.registry;
|
|
188
|
+
if (registry) {
|
|
189
|
+
byRegistry[registry] = (byRegistry[registry] || 0) + 1;
|
|
190
|
+
}
|
|
191
|
+
const version = doc.didDocumentMetadata?.version;
|
|
192
|
+
if (version != null) {
|
|
193
|
+
const versionNum = parseInt(version, 10);
|
|
194
|
+
if (!isNaN(versionNum)) {
|
|
195
|
+
byVersion[versionNum] = (byVersion[versionNum] || 0) + 1;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const byType = { agents, assets, confirmed, unconfirmed, ephemeral, invalid };
|
|
203
|
+
const eventsQueue = this.eventsQueue;
|
|
204
|
+
return { total, byType, byRegistry, byVersion, eventsQueue };
|
|
205
|
+
}
|
|
206
|
+
async listRegistries() {
|
|
207
|
+
return this.supportedRegistries;
|
|
208
|
+
}
|
|
209
|
+
// For testing purposes
|
|
210
|
+
async resetDb() {
|
|
211
|
+
await this.db.resetDb();
|
|
212
|
+
this.verifiedDIDs = {};
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
async generateCID(operation, save = false) {
|
|
216
|
+
const canonical = this.cipher.canonicalizeJSON(operation);
|
|
217
|
+
if (save) {
|
|
218
|
+
return this.ipfs.addJSON(JSON.parse(canonical));
|
|
219
|
+
}
|
|
220
|
+
return generateCID(JSON.parse(canonical));
|
|
221
|
+
}
|
|
222
|
+
async generateDID(operation) {
|
|
223
|
+
const cid = await this.generateCID(operation);
|
|
224
|
+
const prefix = operation.registration?.prefix || this.didPrefix;
|
|
225
|
+
return `${prefix}:${cid}`;
|
|
226
|
+
}
|
|
227
|
+
async verifyOperation(operation) {
|
|
228
|
+
if (operation.type === 'create') {
|
|
229
|
+
return this.verifyCreateOperation(operation);
|
|
230
|
+
}
|
|
231
|
+
if (operation.type === 'update' || operation.type === 'delete') {
|
|
232
|
+
const doc = await this.resolveDID(operation.did);
|
|
233
|
+
return this.verifyUpdateOperation(operation, doc);
|
|
234
|
+
}
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
verifyDIDFormat(did) {
|
|
238
|
+
return did.startsWith('did:');
|
|
239
|
+
}
|
|
240
|
+
verifyDateFormat(time) {
|
|
241
|
+
if (!time) {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
const date = new Date(time);
|
|
245
|
+
return !isNaN(date.getTime());
|
|
246
|
+
}
|
|
247
|
+
verifyHashFormat(hash) {
|
|
248
|
+
// Check if hash is a hexadecimal string of length 64
|
|
249
|
+
const hex64Regex = /^[a-f0-9]{64}$/i;
|
|
250
|
+
return hex64Regex.test(hash);
|
|
251
|
+
}
|
|
252
|
+
verifySignatureFormat(signature) {
|
|
253
|
+
if (!signature) {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
if (!this.verifyDateFormat(signature.signed)) {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
if (!this.verifyHashFormat(signature.hash)) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
// eslint-disable-next-line
|
|
263
|
+
if (signature.signer && !this.verifyDIDFormat(signature.signer)) {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
async verifyCreateOperation(operation) {
|
|
269
|
+
if (!operation) {
|
|
270
|
+
throw new InvalidOperationError('missing');
|
|
271
|
+
}
|
|
272
|
+
if (JSON.stringify(operation).length > this.maxOpBytes) {
|
|
273
|
+
throw new InvalidOperationError('size');
|
|
274
|
+
}
|
|
275
|
+
if (operation.type !== "create") {
|
|
276
|
+
throw new InvalidOperationError(`type=${operation.type}`);
|
|
277
|
+
}
|
|
278
|
+
if (!this.verifyDateFormat(operation.created)) {
|
|
279
|
+
// TBD ensure valid timestamp format
|
|
280
|
+
throw new InvalidOperationError(`created=${operation.created}`);
|
|
281
|
+
}
|
|
282
|
+
if (!operation.registration) {
|
|
283
|
+
throw new InvalidOperationError('registration');
|
|
284
|
+
}
|
|
285
|
+
if (!ValidVersions.includes(operation.registration.version)) {
|
|
286
|
+
throw new InvalidOperationError(`registration.version=${operation.registration.version}`);
|
|
287
|
+
}
|
|
288
|
+
if (!ValidTypes.includes(operation.registration.type)) {
|
|
289
|
+
throw new InvalidOperationError(`registration.type=${operation.registration.type}`);
|
|
290
|
+
}
|
|
291
|
+
if (!ValidRegistries.includes(operation.registration.registry)) {
|
|
292
|
+
throw new InvalidOperationError(`registration.registry=${operation.registration.registry}`);
|
|
293
|
+
}
|
|
294
|
+
if (!this.verifySignatureFormat(operation.signature)) {
|
|
295
|
+
throw new InvalidOperationError('signature');
|
|
296
|
+
}
|
|
297
|
+
if (operation.registration.validUntil && !this.verifyDateFormat(operation.registration.validUntil)) {
|
|
298
|
+
throw new InvalidOperationError(`registration.validUntil=${operation.registration.validUntil}`);
|
|
299
|
+
}
|
|
300
|
+
if (operation.registration.type === 'agent') {
|
|
301
|
+
if (!operation.publicJwk) {
|
|
302
|
+
throw new InvalidOperationError('publicJwk');
|
|
303
|
+
}
|
|
304
|
+
const operationCopy = copyJSON(operation);
|
|
305
|
+
delete operationCopy.signature;
|
|
306
|
+
const msgHash = this.cipher.hashJSON(operationCopy);
|
|
307
|
+
return this.cipher.verifySig(msgHash, operation.signature.value, operation.publicJwk);
|
|
308
|
+
}
|
|
309
|
+
if (operation.registration.type === 'asset') {
|
|
310
|
+
if (operation.controller !== operation.signature?.signer) {
|
|
311
|
+
throw new InvalidOperationError('signer is not controller');
|
|
312
|
+
}
|
|
313
|
+
const doc = await this.resolveDID(operation.signature.signer, { confirm: true, versionTime: operation.signature.signed });
|
|
314
|
+
if (doc.didDocumentRegistration && doc.didDocumentRegistration.registry === 'local' && operation.registration.registry !== 'local') {
|
|
315
|
+
throw new InvalidOperationError(`non-local registry=${operation.registration.registry}`);
|
|
316
|
+
}
|
|
317
|
+
const operationCopy = copyJSON(operation);
|
|
318
|
+
delete operationCopy.signature;
|
|
319
|
+
const msgHash = this.cipher.hashJSON(operationCopy);
|
|
320
|
+
if (!doc.didDocument ||
|
|
321
|
+
!doc.didDocument.verificationMethod ||
|
|
322
|
+
doc.didDocument.verificationMethod.length === 0 ||
|
|
323
|
+
!doc.didDocument.verificationMethod[0].publicKeyJwk) {
|
|
324
|
+
throw new InvalidOperationError('didDocument missing verificationMethod');
|
|
325
|
+
}
|
|
326
|
+
// TBD select the right key here, not just the first one
|
|
327
|
+
const publicJwk = doc.didDocument.verificationMethod[0].publicKeyJwk;
|
|
328
|
+
return this.cipher.verifySig(msgHash, operation.signature.value, publicJwk);
|
|
329
|
+
}
|
|
330
|
+
throw new InvalidOperationError(`registration.type=${operation.registration.type}`);
|
|
331
|
+
}
|
|
332
|
+
async verifyUpdateOperation(operation, doc) {
|
|
333
|
+
if (JSON.stringify(operation).length > this.maxOpBytes) {
|
|
334
|
+
throw new InvalidOperationError('size');
|
|
335
|
+
}
|
|
336
|
+
if (!this.verifySignatureFormat(operation.signature)) {
|
|
337
|
+
throw new InvalidOperationError('signature');
|
|
338
|
+
}
|
|
339
|
+
if (!doc?.didDocument) {
|
|
340
|
+
throw new InvalidOperationError('doc.didDocument');
|
|
341
|
+
}
|
|
342
|
+
if (doc.didDocumentMetadata?.deactivated) {
|
|
343
|
+
throw new InvalidOperationError('DID deactivated');
|
|
344
|
+
}
|
|
345
|
+
if (doc.didDocument.controller) {
|
|
346
|
+
// This DID is an asset, verify with controller's keys
|
|
347
|
+
const controllerDoc = await this.resolveDID(doc.didDocument.controller, { confirm: true, versionTime: operation.signature.signed });
|
|
348
|
+
return this.verifyUpdateOperation(operation, controllerDoc);
|
|
349
|
+
}
|
|
350
|
+
if (!doc.didDocument.verificationMethod) {
|
|
351
|
+
throw new InvalidOperationError('doc.didDocument.verificationMethod');
|
|
352
|
+
}
|
|
353
|
+
const signature = operation.signature;
|
|
354
|
+
const jsonCopy = copyJSON(operation);
|
|
355
|
+
delete jsonCopy.signature;
|
|
356
|
+
const msgHash = this.cipher.hashJSON(jsonCopy);
|
|
357
|
+
if (signature.hash && signature.hash !== msgHash) {
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
if (doc.didDocument.verificationMethod.length === 0 ||
|
|
361
|
+
!doc.didDocument.verificationMethod[0].publicKeyJwk) {
|
|
362
|
+
throw new InvalidOperationError('didDocument missing verificationMethod');
|
|
363
|
+
}
|
|
364
|
+
// TBD get the right signature, not just the first one
|
|
365
|
+
const publicJwk = doc.didDocument.verificationMethod[0].publicKeyJwk;
|
|
366
|
+
return this.cipher.verifySig(msgHash, signature.value, publicJwk);
|
|
367
|
+
}
|
|
368
|
+
async queueOperation(registry, operation) {
|
|
369
|
+
// Don't distribute local DIDs
|
|
370
|
+
if (registry === 'local') {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
// Always distribute on hyperswarm
|
|
374
|
+
await this.db.queueOperation('hyperswarm', operation);
|
|
375
|
+
// Distribute on specified registry
|
|
376
|
+
if (registry !== 'hyperswarm') {
|
|
377
|
+
const queueSize = await this.db.queueOperation(registry, operation);
|
|
378
|
+
if (queueSize >= this.maxQueueSize) {
|
|
379
|
+
this.supportedRegistries = this.supportedRegistries.filter(reg => reg !== registry);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
async createDID(operation) {
|
|
384
|
+
const valid = await this.verifyCreateOperation(operation);
|
|
385
|
+
if (!valid) {
|
|
386
|
+
throw new InvalidOperationError('signature');
|
|
387
|
+
}
|
|
388
|
+
const registry = operation.registration.registry;
|
|
389
|
+
// Reject operations with unsupported registries
|
|
390
|
+
if (!registry || !this.supportedRegistries.includes(registry)) {
|
|
391
|
+
throw new InvalidOperationError(`registry ${registry} not supported`);
|
|
392
|
+
}
|
|
393
|
+
const did = await this.generateDID(operation);
|
|
394
|
+
return this.withDidLock(did, async () => {
|
|
395
|
+
const ops = await this.exportDID(did);
|
|
396
|
+
// Check to see if we already have this DID in the db
|
|
397
|
+
if (ops.length > 0) {
|
|
398
|
+
return did;
|
|
399
|
+
}
|
|
400
|
+
const opid = await this.generateCID(operation, true);
|
|
401
|
+
await this.db.addEvent(did, {
|
|
402
|
+
registry: 'local',
|
|
403
|
+
time: operation.created,
|
|
404
|
+
ordinal: [0],
|
|
405
|
+
operation,
|
|
406
|
+
opid,
|
|
407
|
+
did
|
|
408
|
+
});
|
|
409
|
+
await this.queueOperation(registry, operation);
|
|
410
|
+
return did;
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
async generateDoc(anchor, defaultDID) {
|
|
414
|
+
let doc = {};
|
|
415
|
+
try {
|
|
416
|
+
if (!anchor?.registration) {
|
|
417
|
+
return {};
|
|
418
|
+
}
|
|
419
|
+
if (!ValidVersions.includes(anchor.registration.version)) {
|
|
420
|
+
return {};
|
|
421
|
+
}
|
|
422
|
+
if (!ValidTypes.includes(anchor.registration.type)) {
|
|
423
|
+
return {};
|
|
424
|
+
}
|
|
425
|
+
if (!ValidRegistries.includes(anchor.registration.registry)) {
|
|
426
|
+
return {};
|
|
427
|
+
}
|
|
428
|
+
const did = defaultDID ?? await this.generateDID(anchor);
|
|
429
|
+
if (anchor.registration.type === 'agent') {
|
|
430
|
+
// TBD support different key types?
|
|
431
|
+
doc = {
|
|
432
|
+
"didDocument": {
|
|
433
|
+
"@context": ["https://www.w3.org/ns/did/v1"],
|
|
434
|
+
"id": did,
|
|
435
|
+
"verificationMethod": [
|
|
436
|
+
{
|
|
437
|
+
"id": "#key-1",
|
|
438
|
+
"controller": did,
|
|
439
|
+
"type": "EcdsaSecp256k1VerificationKey2019",
|
|
440
|
+
"publicKeyJwk": anchor.publicJwk,
|
|
441
|
+
}
|
|
442
|
+
],
|
|
443
|
+
"authentication": [
|
|
444
|
+
"#key-1"
|
|
445
|
+
],
|
|
446
|
+
},
|
|
447
|
+
"didDocumentMetadata": {
|
|
448
|
+
"created": anchor.created,
|
|
449
|
+
},
|
|
450
|
+
"didDocumentData": {},
|
|
451
|
+
"didDocumentRegistration": anchor.registration,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
if (anchor.registration.type === 'asset') {
|
|
455
|
+
doc = {
|
|
456
|
+
"didDocument": {
|
|
457
|
+
"@context": ["https://www.w3.org/ns/did/v1"],
|
|
458
|
+
"id": did,
|
|
459
|
+
"controller": anchor.controller,
|
|
460
|
+
},
|
|
461
|
+
"didDocumentMetadata": {
|
|
462
|
+
"created": anchor.created,
|
|
463
|
+
},
|
|
464
|
+
"didDocumentData": anchor.data,
|
|
465
|
+
"didDocumentRegistration": anchor.registration,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
if (doc.didDocumentMetadata && anchor.registration.prefix) {
|
|
469
|
+
doc.didDocumentMetadata.canonicalId = did;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
catch (error) {
|
|
473
|
+
// console.error(error);
|
|
474
|
+
}
|
|
475
|
+
return doc;
|
|
476
|
+
}
|
|
477
|
+
async resolveDID(did, options) {
|
|
478
|
+
const { versionTime, versionSequence, confirm = false, verify = false } = options || {};
|
|
479
|
+
if (!did || !isValidDID(did)) {
|
|
480
|
+
return {
|
|
481
|
+
didResolutionMetadata: {
|
|
482
|
+
error: "invalidDid"
|
|
483
|
+
},
|
|
484
|
+
didDocument: {},
|
|
485
|
+
didDocumentMetadata: {}
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
const events = await this.db.getEvents(did);
|
|
489
|
+
if (events.length === 0) {
|
|
490
|
+
return {
|
|
491
|
+
didResolutionMetadata: {
|
|
492
|
+
error: "notFound"
|
|
493
|
+
},
|
|
494
|
+
didDocument: {},
|
|
495
|
+
didDocumentMetadata: {}
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
const anchor = events[0];
|
|
499
|
+
let doc = await this.generateDoc(anchor.operation, did);
|
|
500
|
+
if (versionTime && doc.didDocumentRegistration?.created && new Date(doc.didDocumentRegistration.created) > new Date(versionTime)) {
|
|
501
|
+
// TBD What to return if DID was created after specified time?
|
|
502
|
+
}
|
|
503
|
+
function generateStandardDatetime(time) {
|
|
504
|
+
const date = new Date(time);
|
|
505
|
+
// Remove milliseconds for standardization
|
|
506
|
+
return date.toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
507
|
+
}
|
|
508
|
+
const created = generateStandardDatetime(doc.didDocumentMetadata?.created);
|
|
509
|
+
const canonicalId = doc.didDocumentMetadata?.canonicalId;
|
|
510
|
+
let versionNum = 1; // initial version is version 1 by definition
|
|
511
|
+
let confirmed = true; // create event is always confirmed by definition
|
|
512
|
+
for (const { time, operation, registry, registration: blockchain } of events) {
|
|
513
|
+
const versionId = await this.generateCID(operation);
|
|
514
|
+
const updated = generateStandardDatetime(time);
|
|
515
|
+
let timestamp;
|
|
516
|
+
if (doc.didDocumentRegistration?.registry) {
|
|
517
|
+
let lowerBound;
|
|
518
|
+
let upperBound;
|
|
519
|
+
if (operation.blockid) {
|
|
520
|
+
const lowerBlock = await this.db.getBlock(doc.didDocumentRegistration.registry, operation.blockid);
|
|
521
|
+
if (lowerBlock) {
|
|
522
|
+
lowerBound = {
|
|
523
|
+
time: lowerBlock.time,
|
|
524
|
+
timeISO: new Date(lowerBlock.time * 1000).toISOString(),
|
|
525
|
+
blockid: lowerBlock.hash,
|
|
526
|
+
height: lowerBlock.height,
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (blockchain) {
|
|
531
|
+
const upperBlock = await this.db.getBlock(doc.didDocumentRegistration.registry, blockchain.height);
|
|
532
|
+
if (upperBlock) {
|
|
533
|
+
upperBound = {
|
|
534
|
+
time: upperBlock.time,
|
|
535
|
+
timeISO: new Date(upperBlock.time * 1000).toISOString(),
|
|
536
|
+
blockid: upperBlock.hash,
|
|
537
|
+
height: upperBlock.height,
|
|
538
|
+
txid: blockchain.txid,
|
|
539
|
+
txidx: blockchain.index,
|
|
540
|
+
batchid: blockchain.batch,
|
|
541
|
+
opidx: blockchain.opidx,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
if (lowerBound || upperBound) {
|
|
546
|
+
timestamp = {
|
|
547
|
+
chain: doc.didDocumentRegistration.registry,
|
|
548
|
+
opid: versionId,
|
|
549
|
+
lowerBound,
|
|
550
|
+
upperBound,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
if (operation.type === 'create') {
|
|
555
|
+
if (verify) {
|
|
556
|
+
const valid = await this.verifyCreateOperation(operation);
|
|
557
|
+
if (!valid) {
|
|
558
|
+
throw new InvalidOperationError('signature');
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
doc.didDocumentMetadata = {
|
|
562
|
+
created,
|
|
563
|
+
canonicalId,
|
|
564
|
+
versionId,
|
|
565
|
+
version: versionNum.toString(),
|
|
566
|
+
confirmed,
|
|
567
|
+
timestamp,
|
|
568
|
+
};
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
if (versionTime && new Date(time) > new Date(versionTime)) {
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
if (versionSequence && versionNum === versionSequence) {
|
|
575
|
+
break;
|
|
576
|
+
}
|
|
577
|
+
confirmed = confirmed && doc.didDocumentRegistration?.registry === registry;
|
|
578
|
+
if (confirm && !confirmed) {
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
581
|
+
if (verify) {
|
|
582
|
+
const valid = await this.verifyUpdateOperation(operation, doc);
|
|
583
|
+
if (!valid) {
|
|
584
|
+
throw new InvalidOperationError('signature');
|
|
585
|
+
}
|
|
586
|
+
if (!operation.previd || operation.previd !== doc.didDocumentMetadata?.versionId) {
|
|
587
|
+
throw new InvalidOperationError('previd');
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
if (operation.type === 'update') {
|
|
591
|
+
versionNum += 1;
|
|
592
|
+
const nextDoc = operation.doc || {};
|
|
593
|
+
// Merge update: carry forward fields not provided in the update
|
|
594
|
+
if (nextDoc.didDocument !== undefined) {
|
|
595
|
+
doc.didDocument = nextDoc.didDocument;
|
|
596
|
+
}
|
|
597
|
+
if (nextDoc.didDocumentData !== undefined) {
|
|
598
|
+
doc.didDocumentData = nextDoc.didDocumentData;
|
|
599
|
+
}
|
|
600
|
+
if (nextDoc.didDocumentRegistration !== undefined) {
|
|
601
|
+
doc.didDocumentRegistration = nextDoc.didDocumentRegistration;
|
|
602
|
+
}
|
|
603
|
+
doc.didDocumentMetadata = {
|
|
604
|
+
created,
|
|
605
|
+
updated,
|
|
606
|
+
canonicalId,
|
|
607
|
+
versionId,
|
|
608
|
+
version: versionNum.toString(),
|
|
609
|
+
confirmed,
|
|
610
|
+
timestamp,
|
|
611
|
+
};
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
if (operation.type === 'delete') {
|
|
615
|
+
versionNum += 1;
|
|
616
|
+
doc.didDocument = { id: did };
|
|
617
|
+
doc.didDocumentData = {};
|
|
618
|
+
doc.didDocumentMetadata = {
|
|
619
|
+
deactivated: true,
|
|
620
|
+
created,
|
|
621
|
+
deleted: updated,
|
|
622
|
+
canonicalId,
|
|
623
|
+
versionId,
|
|
624
|
+
version: versionNum.toString(),
|
|
625
|
+
confirmed,
|
|
626
|
+
timestamp,
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
doc.didResolutionMetadata = {
|
|
631
|
+
// We'll deliberately use millisecond precision here to avoid intermittent unit test failures
|
|
632
|
+
retrieved: new Date().toISOString(),
|
|
633
|
+
};
|
|
634
|
+
// Remove deprecated fields
|
|
635
|
+
delete doc['@context'];
|
|
636
|
+
if (doc.didDocumentRegistration) {
|
|
637
|
+
delete doc.didDocumentRegistration.opid; // Replaced by didDocumentMetadata.versionId
|
|
638
|
+
delete doc.didDocumentRegistration.registration; // Replaced by didDocumentMetadata.timestamp
|
|
639
|
+
}
|
|
640
|
+
return copyJSON(doc);
|
|
641
|
+
}
|
|
642
|
+
async updateDID(operation) {
|
|
643
|
+
if (!operation.did) {
|
|
644
|
+
throw new InvalidOperationError('missing operation.did');
|
|
645
|
+
}
|
|
646
|
+
const doc = await this.resolveDID(operation.did);
|
|
647
|
+
const updateValid = await this.verifyUpdateOperation(operation, doc);
|
|
648
|
+
if (!updateValid) {
|
|
649
|
+
return false;
|
|
650
|
+
}
|
|
651
|
+
const registry = doc.didDocumentRegistration?.registry;
|
|
652
|
+
// Reject operations with unsupported registries
|
|
653
|
+
if (!registry || !this.supportedRegistries.includes(registry)) {
|
|
654
|
+
throw new InvalidOperationError(`registry ${registry} not supported`);
|
|
655
|
+
}
|
|
656
|
+
return this.withDidLock(operation.did, async () => {
|
|
657
|
+
const opid = await this.generateCID(operation, true);
|
|
658
|
+
await this.db.addEvent(operation.did, {
|
|
659
|
+
registry: 'local',
|
|
660
|
+
time: operation.signature?.signed || '',
|
|
661
|
+
ordinal: [0],
|
|
662
|
+
operation,
|
|
663
|
+
opid,
|
|
664
|
+
did: operation.did
|
|
665
|
+
});
|
|
666
|
+
await this.queueOperation(registry, operation);
|
|
667
|
+
return true;
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
async deleteDID(operation) {
|
|
671
|
+
return this.updateDID(operation);
|
|
672
|
+
}
|
|
673
|
+
async getDIDs(options) {
|
|
674
|
+
let { dids, updatedAfter, updatedBefore, confirm, verify, resolve } = options || {};
|
|
675
|
+
if (!dids) {
|
|
676
|
+
const keys = await this.db.getAllKeys();
|
|
677
|
+
dids = keys.map(key => `${this.didPrefix}:${key}`);
|
|
678
|
+
}
|
|
679
|
+
if (updatedAfter || updatedBefore || resolve) {
|
|
680
|
+
const start = updatedAfter ? new Date(updatedAfter) : null;
|
|
681
|
+
const end = updatedBefore ? new Date(updatedBefore) : null;
|
|
682
|
+
const docList = [];
|
|
683
|
+
const didList = [];
|
|
684
|
+
for (const did of dids) {
|
|
685
|
+
try {
|
|
686
|
+
const doc = await this.resolveDID(did, { confirm, verify });
|
|
687
|
+
const updatedStr = doc.didDocumentMetadata?.updated ?? doc.didDocumentMetadata?.created ?? 0;
|
|
688
|
+
const updated = new Date(updatedStr);
|
|
689
|
+
if (start && updated <= start) {
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
if (end && updated >= end) {
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
if (resolve) {
|
|
696
|
+
docList.push(doc);
|
|
697
|
+
}
|
|
698
|
+
else {
|
|
699
|
+
didList.push(did);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
catch {
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
if (resolve) {
|
|
707
|
+
return docList;
|
|
708
|
+
}
|
|
709
|
+
return didList;
|
|
710
|
+
}
|
|
711
|
+
return dids;
|
|
712
|
+
}
|
|
713
|
+
async exportDID(did) {
|
|
714
|
+
return this.db.getEvents(did);
|
|
715
|
+
}
|
|
716
|
+
async exportDIDs(dids) {
|
|
717
|
+
if (!dids) {
|
|
718
|
+
dids = await this.getDIDs();
|
|
719
|
+
}
|
|
720
|
+
const batch = [];
|
|
721
|
+
for (const did of dids) {
|
|
722
|
+
batch.push(await this.exportDID(did));
|
|
723
|
+
}
|
|
724
|
+
return batch;
|
|
725
|
+
}
|
|
726
|
+
async importDIDs(dids) {
|
|
727
|
+
return this.importBatch(dids.flat());
|
|
728
|
+
}
|
|
729
|
+
async removeDIDs(dids) {
|
|
730
|
+
if (!Array.isArray(dids)) {
|
|
731
|
+
throw new InvalidParameterError('dids');
|
|
732
|
+
}
|
|
733
|
+
for (const did of dids) {
|
|
734
|
+
await this.db.deleteEvents(did);
|
|
735
|
+
}
|
|
736
|
+
return true;
|
|
737
|
+
}
|
|
738
|
+
async importEvent(event) {
|
|
739
|
+
try {
|
|
740
|
+
if (!event.did) {
|
|
741
|
+
if (event.operation.did) {
|
|
742
|
+
event.did = event.operation.did;
|
|
743
|
+
}
|
|
744
|
+
else {
|
|
745
|
+
event.did = await this.generateDID(event.operation);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
const did = event.did;
|
|
749
|
+
const expectedRegistryForIndex = (events, index) => {
|
|
750
|
+
if (index <= 0) {
|
|
751
|
+
return events[0]?.operation?.registration?.registry;
|
|
752
|
+
}
|
|
753
|
+
let registry = events[0]?.operation?.registration?.registry;
|
|
754
|
+
for (let i = 1; i < index; i++) {
|
|
755
|
+
const op = events[i]?.operation;
|
|
756
|
+
if (op?.type === 'update') {
|
|
757
|
+
const nextRegistry = op.doc?.didDocumentRegistration?.registry;
|
|
758
|
+
if (nextRegistry) {
|
|
759
|
+
registry = nextRegistry;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
return registry;
|
|
764
|
+
};
|
|
765
|
+
return await this.withDidLock(did, async () => {
|
|
766
|
+
const currentEvents = await this.db.getEvents(did);
|
|
767
|
+
for (const e of currentEvents) {
|
|
768
|
+
if (!e.opid) {
|
|
769
|
+
e.opid = await this.generateCID(e.operation, true);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
if (!event.opid) {
|
|
773
|
+
event.opid = await this.generateCID(event.operation, true);
|
|
774
|
+
}
|
|
775
|
+
const opMatch = currentEvents.find(item => item.operation.signature?.value === event.operation.signature?.value);
|
|
776
|
+
if (opMatch) {
|
|
777
|
+
const index = currentEvents.indexOf(opMatch);
|
|
778
|
+
const expectedRegistry = expectedRegistryForIndex(currentEvents, index);
|
|
779
|
+
if (expectedRegistry && opMatch.registry === expectedRegistry) {
|
|
780
|
+
// Already confirmed on the expected registry for this version
|
|
781
|
+
return ImportStatus.MERGED;
|
|
782
|
+
}
|
|
783
|
+
if (expectedRegistry && event.registry === expectedRegistry) {
|
|
784
|
+
// Import is confirmed on the expected registry for this version, replace existing event
|
|
785
|
+
currentEvents[index] = event;
|
|
786
|
+
await this.db.setEvents(did, currentEvents);
|
|
787
|
+
return ImportStatus.ADDED;
|
|
788
|
+
}
|
|
789
|
+
return ImportStatus.MERGED;
|
|
790
|
+
}
|
|
791
|
+
else {
|
|
792
|
+
// Reject update/delete operations without previd (check before expensive signature verification)
|
|
793
|
+
if (currentEvents.length > 0 && !event.operation.previd) {
|
|
794
|
+
return ImportStatus.REJECTED;
|
|
795
|
+
}
|
|
796
|
+
const ok = await this.verifyOperation(event.operation);
|
|
797
|
+
if (!ok) {
|
|
798
|
+
return ImportStatus.REJECTED;
|
|
799
|
+
}
|
|
800
|
+
if (currentEvents.length === 0) {
|
|
801
|
+
await this.db.addEvent(did, event);
|
|
802
|
+
return ImportStatus.ADDED;
|
|
803
|
+
}
|
|
804
|
+
const idMatch = currentEvents.find(item => item.opid === event.operation.previd);
|
|
805
|
+
if (!idMatch) {
|
|
806
|
+
return ImportStatus.DEFERRED;
|
|
807
|
+
}
|
|
808
|
+
const index = currentEvents.indexOf(idMatch);
|
|
809
|
+
if (index === currentEvents.length - 1) {
|
|
810
|
+
await this.db.addEvent(did, event);
|
|
811
|
+
return ImportStatus.ADDED;
|
|
812
|
+
}
|
|
813
|
+
const expectedRegistry = expectedRegistryForIndex(currentEvents, index + 1);
|
|
814
|
+
if (expectedRegistry && event.registry === expectedRegistry) {
|
|
815
|
+
const nextEvent = currentEvents[index + 1];
|
|
816
|
+
if (nextEvent.registry !== event.registry ||
|
|
817
|
+
(event.ordinal && nextEvent.ordinal && compareOrdinals(event.ordinal, nextEvent.ordinal) < 0)) {
|
|
818
|
+
// reorg event, discard the rest of the operation sequence and replace with this event
|
|
819
|
+
const newSequence = [...currentEvents.slice(0, index + 1), event];
|
|
820
|
+
await this.db.setEvents(did, newSequence);
|
|
821
|
+
return ImportStatus.ADDED;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
return ImportStatus.REJECTED;
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
catch (error) {
|
|
829
|
+
if (error.type === 'Invalid operation') {
|
|
830
|
+
// Could be an event with a controller DID that hasn't been imported yet
|
|
831
|
+
return ImportStatus.DEFERRED;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return ImportStatus.REJECTED;
|
|
835
|
+
}
|
|
836
|
+
async importEvents() {
|
|
837
|
+
let tempQueue = this.eventsQueue;
|
|
838
|
+
const total = tempQueue.length;
|
|
839
|
+
let event = tempQueue.shift();
|
|
840
|
+
let i = 0;
|
|
841
|
+
let added = 0;
|
|
842
|
+
let merged = 0;
|
|
843
|
+
let rejected = 0;
|
|
844
|
+
this.eventsQueue = [];
|
|
845
|
+
while (event) {
|
|
846
|
+
i += 1;
|
|
847
|
+
const status = await this.importEvent(event);
|
|
848
|
+
if (status === ImportStatus.ADDED) {
|
|
849
|
+
added += 1;
|
|
850
|
+
console.log(`import ${i}/${total}: added event for ${event.did}`);
|
|
851
|
+
}
|
|
852
|
+
else if (status === ImportStatus.MERGED) {
|
|
853
|
+
merged += 1;
|
|
854
|
+
console.log(`import ${i}/${total}: merged event for ${event.did}`);
|
|
855
|
+
}
|
|
856
|
+
else if (status === ImportStatus.REJECTED) {
|
|
857
|
+
rejected += 1;
|
|
858
|
+
console.log(`import ${i}/${total}: rejected event for ${event.did}`);
|
|
859
|
+
}
|
|
860
|
+
else if (status === ImportStatus.DEFERRED) {
|
|
861
|
+
this.eventsQueue.push(event);
|
|
862
|
+
console.log(`import ${i}/${total}: deferred event for ${event.did}`);
|
|
863
|
+
}
|
|
864
|
+
event = tempQueue.shift();
|
|
865
|
+
}
|
|
866
|
+
return { added, merged, rejected };
|
|
867
|
+
}
|
|
868
|
+
async processEvents() {
|
|
869
|
+
if (this.isProcessingEvents) {
|
|
870
|
+
return { busy: true };
|
|
871
|
+
}
|
|
872
|
+
let added = 0;
|
|
873
|
+
let merged = 0;
|
|
874
|
+
let rejected = 0;
|
|
875
|
+
let done = false;
|
|
876
|
+
try {
|
|
877
|
+
this.isProcessingEvents = true;
|
|
878
|
+
while (!done) {
|
|
879
|
+
const response = await this.importEvents();
|
|
880
|
+
added += response.added;
|
|
881
|
+
merged += response.merged;
|
|
882
|
+
rejected += response.rejected;
|
|
883
|
+
done = (response.added === 0 && response.merged === 0);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
catch (error) {
|
|
887
|
+
console.log(error);
|
|
888
|
+
this.eventsQueue = [];
|
|
889
|
+
}
|
|
890
|
+
finally {
|
|
891
|
+
this.isProcessingEvents = false;
|
|
892
|
+
}
|
|
893
|
+
//console.log(JSON.stringify(eventsQueue, null, 4));
|
|
894
|
+
const pending = this.eventsQueue.length;
|
|
895
|
+
const response = { added, merged, rejected, pending };
|
|
896
|
+
console.log(`processEvents: ${JSON.stringify(response)}`);
|
|
897
|
+
return response;
|
|
898
|
+
}
|
|
899
|
+
async verifyEvent(event) {
|
|
900
|
+
if (!event.registry || !event.time || !event.operation) {
|
|
901
|
+
return false;
|
|
902
|
+
}
|
|
903
|
+
const eventTime = new Date(event.time).getTime();
|
|
904
|
+
if (isNaN(eventTime)) {
|
|
905
|
+
return false;
|
|
906
|
+
}
|
|
907
|
+
const operation = event.operation;
|
|
908
|
+
if (JSON.stringify(operation).length > this.maxOpBytes) {
|
|
909
|
+
return false;
|
|
910
|
+
}
|
|
911
|
+
if (!this.verifySignatureFormat(operation.signature)) {
|
|
912
|
+
return false;
|
|
913
|
+
}
|
|
914
|
+
if (operation.type === 'create') {
|
|
915
|
+
if (!operation.created) {
|
|
916
|
+
return false;
|
|
917
|
+
}
|
|
918
|
+
if (!operation.registration) {
|
|
919
|
+
return false;
|
|
920
|
+
}
|
|
921
|
+
if (!ValidVersions.includes(operation.registration.version)) {
|
|
922
|
+
return false;
|
|
923
|
+
}
|
|
924
|
+
if (!ValidTypes.includes(operation.registration.type)) {
|
|
925
|
+
return false;
|
|
926
|
+
}
|
|
927
|
+
if (!ValidRegistries.includes(operation.registration.registry)) {
|
|
928
|
+
return false;
|
|
929
|
+
}
|
|
930
|
+
// eslint-disable-next-line
|
|
931
|
+
if (operation.registration.type === 'agent') {
|
|
932
|
+
if (!operation.publicJwk) {
|
|
933
|
+
return false;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
// eslint-disable-next-line
|
|
937
|
+
if (operation.registration.type === 'asset') {
|
|
938
|
+
if (operation.controller !== operation.signature?.signer) {
|
|
939
|
+
return false;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
else if (operation.type === 'update') {
|
|
944
|
+
const doc = operation.doc;
|
|
945
|
+
// Keymaster intentionally omits didDocumentMetadata/didResolutionMetadata from update operations.
|
|
946
|
+
// Gatekeeper re-materializes didDocumentMetadata during resolution, and didDocumentRegistration
|
|
947
|
+
// can be carried forward from the previous version.
|
|
948
|
+
// Update must include any or all of: didDocument, didDocumentData, didDocumentRegistration
|
|
949
|
+
if (!doc || (!doc.didDocument && !doc.didDocumentData && !doc.didDocumentRegistration)) {
|
|
950
|
+
return false;
|
|
951
|
+
}
|
|
952
|
+
if (doc.didDocument?.id && operation.did && doc.didDocument.id !== operation.did) {
|
|
953
|
+
return false;
|
|
954
|
+
}
|
|
955
|
+
if (!operation.did) {
|
|
956
|
+
return false;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
else if (operation.type === 'delete') {
|
|
960
|
+
if (!operation.did) {
|
|
961
|
+
return false;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
else {
|
|
965
|
+
return false;
|
|
966
|
+
}
|
|
967
|
+
return true;
|
|
968
|
+
}
|
|
969
|
+
async importBatch(batch) {
|
|
970
|
+
if (!batch || !Array.isArray(batch) || batch.length < 1) {
|
|
971
|
+
throw new InvalidParameterError('batch');
|
|
972
|
+
}
|
|
973
|
+
let queued = 0;
|
|
974
|
+
let rejected = 0;
|
|
975
|
+
let processed = 0;
|
|
976
|
+
for (let i = 0; i < batch.length; i++) {
|
|
977
|
+
const event = batch[i];
|
|
978
|
+
const ok = await this.verifyEvent(event);
|
|
979
|
+
if (ok) {
|
|
980
|
+
const eventKey = `${event.registry}/${event.operation.signature?.hash}`;
|
|
981
|
+
if (!this.eventsSeen[eventKey]) {
|
|
982
|
+
this.eventsSeen[eventKey] = true;
|
|
983
|
+
this.eventsQueue.push(event);
|
|
984
|
+
queued += 1;
|
|
985
|
+
}
|
|
986
|
+
else {
|
|
987
|
+
processed += 1;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
else {
|
|
991
|
+
rejected += 1;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
return {
|
|
995
|
+
queued,
|
|
996
|
+
processed,
|
|
997
|
+
rejected,
|
|
998
|
+
total: this.eventsQueue.length
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
async importBatchByCids(cids, metadata) {
|
|
1002
|
+
if (!cids || !Array.isArray(cids) || cids.length < 1) {
|
|
1003
|
+
throw new InvalidParameterError('cids');
|
|
1004
|
+
}
|
|
1005
|
+
if (!metadata || !metadata.registry || !metadata.time || !metadata.ordinal) {
|
|
1006
|
+
throw new InvalidParameterError('metadata');
|
|
1007
|
+
}
|
|
1008
|
+
const events = [];
|
|
1009
|
+
for (let i = 0; i < cids.length; i++) {
|
|
1010
|
+
const cid = cids[i];
|
|
1011
|
+
let op = await this.db.getOperation(cid);
|
|
1012
|
+
if (!op) {
|
|
1013
|
+
op = await this.ipfs.getJSON(cid);
|
|
1014
|
+
if (op) {
|
|
1015
|
+
await this.db.addOperation(cid, op);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
if (op) {
|
|
1019
|
+
events.push({
|
|
1020
|
+
registry: metadata.registry,
|
|
1021
|
+
time: metadata.time,
|
|
1022
|
+
ordinal: [...metadata.ordinal, i],
|
|
1023
|
+
operation: op,
|
|
1024
|
+
opid: cid,
|
|
1025
|
+
registration: metadata.registration,
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
return this.importBatch(events);
|
|
1030
|
+
}
|
|
1031
|
+
async exportBatch(dids) {
|
|
1032
|
+
const allDIDs = await this.exportDIDs(dids);
|
|
1033
|
+
const nonlocalDIDs = allDIDs.filter(events => {
|
|
1034
|
+
if (events.length > 0) {
|
|
1035
|
+
const create = events[0];
|
|
1036
|
+
const registry = create.operation?.registration?.registry;
|
|
1037
|
+
return registry && registry !== 'local';
|
|
1038
|
+
}
|
|
1039
|
+
return false;
|
|
1040
|
+
});
|
|
1041
|
+
const events = nonlocalDIDs.flat();
|
|
1042
|
+
return events.sort((a, b) => new Date(a.operation.signature?.signed ?? 0).getTime() - new Date(b.operation.signature?.signed ?? 0).getTime());
|
|
1043
|
+
}
|
|
1044
|
+
async getQueue(registry) {
|
|
1045
|
+
if (!ValidRegistries.includes(registry)) {
|
|
1046
|
+
throw new InvalidParameterError(`registry=${registry}`);
|
|
1047
|
+
}
|
|
1048
|
+
if (!this.supportedRegistries.includes(registry)) {
|
|
1049
|
+
this.supportedRegistries.push(registry);
|
|
1050
|
+
}
|
|
1051
|
+
return this.db.getQueue(registry);
|
|
1052
|
+
}
|
|
1053
|
+
async clearQueue(registry, events) {
|
|
1054
|
+
if (!ValidRegistries.includes(registry)) {
|
|
1055
|
+
throw new InvalidParameterError(`registry=${registry}`);
|
|
1056
|
+
}
|
|
1057
|
+
return this.db.clearQueue(registry, events);
|
|
1058
|
+
}
|
|
1059
|
+
async getBlock(registry, block) {
|
|
1060
|
+
if (!ValidRegistries.includes(registry)) {
|
|
1061
|
+
throw new InvalidParameterError(`registry=${registry}`);
|
|
1062
|
+
}
|
|
1063
|
+
return this.db.getBlock(registry, block);
|
|
1064
|
+
}
|
|
1065
|
+
async addBlock(registry, block) {
|
|
1066
|
+
if (!ValidRegistries.includes(registry)) {
|
|
1067
|
+
throw new InvalidParameterError(`registry=${registry}`);
|
|
1068
|
+
}
|
|
1069
|
+
return this.db.addBlock(registry, block);
|
|
1070
|
+
}
|
|
1071
|
+
async addText(text) {
|
|
1072
|
+
return this.ipfs.addText(text);
|
|
1073
|
+
}
|
|
1074
|
+
async getText(cid) {
|
|
1075
|
+
return this.ipfs.getText(cid);
|
|
1076
|
+
}
|
|
1077
|
+
async addData(data) {
|
|
1078
|
+
return this.ipfs.addData(data);
|
|
1079
|
+
}
|
|
1080
|
+
async getData(cid) {
|
|
1081
|
+
return this.ipfs.getData(cid);
|
|
1082
|
+
}
|
|
1083
|
+
async addJSON(json) {
|
|
1084
|
+
return this.ipfs.addJSON(json);
|
|
1085
|
+
}
|
|
1086
|
+
async getJSON(cid) {
|
|
1087
|
+
return this.ipfs.getJSON(cid);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
//# sourceMappingURL=gatekeeper.js.map
|