• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

KeychainMDIP / kc / 12657192105

07 Jan 2025 06:32PM UTC coverage: 95.778% (+0.04%) from 95.741%
12657192105

Pull #514

github

macterra
Added DbMongo class
Pull Request #514: refactor: Gatekeeper classes

1036 of 1103 branches covered (93.93%)

Branch coverage included in aggregate %.

62 of 67 new or added lines in 1 file covered. (92.54%)

7 existing lines in 1 file now uncovered.

1482 of 1526 relevant lines covered (97.12%)

445.28 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.45
/packages/gatekeeper/src/gatekeeper-lib.js
1
import canonicalize from 'canonicalize';
2
import * as cipher from '@mdip/cipher/node';
3
import { copyJSON } from '@mdip/common/utils';
4
import {
5
    InvalidDIDError,
6
    InvalidParameterError,
7
    InvalidOperationError
8
} from '@mdip/common/errors';
9
import IPFS from '@mdip/ipfs';
10
import config from './config.js';
11

12
const validVersions = [1];
4✔
13
const validTypes = ['agent', 'asset'];
4✔
14
// We'll leave TESS here so existing TESS DIDs are not deleted
15
// Remove TESS when we switch to did:mdip
16
const validRegistries = ['local', 'hyperswarm', 'TESS', 'TBTC', 'TFTC'];
4✔
17
let supportedRegistries = null;
4✔
18

19
let db = null;
4✔
20
let eventsQueue = [];
4✔
21
let eventsSeen = {};
4✔
22
let verifiedDIDs = {};
4✔
23
let isProcessingEvents = false;
4✔
24
const ipfs = new IPFS({ minimal: true });
4✔
25

26
export async function start(options = {}) {
1✔
27
    if (options.db) {
442✔
28
        db = options.db;
440✔
29
    }
30
    else {
31
        throw new InvalidParameterError('missing options.db');
2✔
32
    }
33

34
    // Only used for unit testing
35
    if (options.console) {
440✔
36
        // eslint-disable-next-line
37
        console = options.console;
2✔
38
    }
39

40
    return ipfs.start();
440✔
41
}
42

43
export async function stop() {
44
    return ipfs.stop();
440✔
45
}
46

47
export async function verifyDb(options = {}) {
6✔
48
    const { chatty = true } = options;
14✔
49

50
    const dids = await getDIDs();
14✔
51
    const total = dids.length;
14✔
52
    let n = 0;
14✔
53
    let expired = 0;
14✔
54
    let invalid = 0;
14✔
55
    let verified = Object.keys(verifiedDIDs).length;
14✔
56

57
    if (chatty) {
14✔
58
        console.time('verifyDb');
12✔
59
    }
60

61
    for (const did of dids) {
14✔
62
        n += 1;
28✔
63

64
        if (verifiedDIDs[did]) {
28✔
65
            continue;
8✔
66
        }
67

68
        let validUntil = null;
20✔
69

70
        try {
20✔
71
            const doc = await resolveDID(did, { verify: true });
20✔
72
            validUntil = doc.mdip.validUntil;
18✔
73
        }
74
        catch (error) {
75
            if (chatty) {
2!
76
                console.log(`removing ${n}/${total} ${did} invalid`);
2✔
77
            }
78
            invalid += 1;
2✔
79
            await db.deleteEvents(did);
2✔
80
            continue;
2✔
81
        }
82

83
        if (validUntil) {
18✔
84
            const expires = new Date(validUntil);
4✔
85
            const now = new Date();
4✔
86

87
            if (expires < now) {
4✔
88
                if (chatty) {
2!
89
                    console.log(`removing ${n}/${total} ${did} expired`);
2✔
90
                }
91
                await db.deleteEvents(did);
2✔
92
                expired += 1;
2✔
93
            }
94
            else {
95
                const minutesLeft = Math.round((expires.getTime() - now.getTime()) / 60 / 1000);
2✔
96

97
                if (chatty) {
2!
98
                    console.log(`expiring ${n}/${total} ${did} in ${minutesLeft} minutes`);
2✔
99
                }
100
                verified += 1;
2✔
101
            }
102
        }
103
        else {
104
            if (chatty) {
14!
105
                console.log(`verifying ${n}/${total} ${did} OK`);
14✔
106
            }
107
            verifiedDIDs[did] = true;
14✔
108
            verified += 1;
14✔
109
        }
110
    }
111

112
    if (chatty) {
14✔
113
        console.timeEnd('verifyDb');
12✔
114
    }
115

116
    return { total, verified, expired, invalid };
14✔
117
}
118

119
export async function checkDIDs(options = {}) {
1✔
120
    let { chatty = false, dids } = options;
8✔
121

122
    if (!dids) {
8✔
123
        dids = await getDIDs();
6✔
124
    }
125

126
    const total = dids.length;
8✔
127
    let n = 0;
8✔
128
    let agents = 0;
8✔
129
    let assets = 0;
8✔
130
    let confirmed = 0;
8✔
131
    let unconfirmed = 0;
8✔
132
    let ephemeral = 0;
8✔
133
    let invalid = 0;
8✔
134
    const byRegistry = {};
8✔
135
    const byVersion = {};
8✔
136

137
    for (const did of dids) {
8✔
138
        n += 1;
14✔
139
        try {
14✔
140
            const doc = await resolveDID(did);
14✔
141
            if (chatty) {
12!
142
                console.log(`resolved ${n}/${total} ${did} OK`);
12✔
143
            }
144

145
            if (doc.mdip.type === 'agent') {
12✔
146
                agents += 1;
6✔
147
            }
148

149
            if (doc.mdip.type === 'asset') {
12✔
150
                assets += 1;
6✔
151
            }
152

153
            if (doc.didDocumentMetadata.confirmed) {
12✔
154
                confirmed += 1;
10✔
155
            }
156
            else {
157
                unconfirmed += 1;
2✔
158
            }
159

160
            if (doc.mdip.validUntil) {
12✔
161
                ephemeral += 1;
2✔
162
            }
163

164
            const registry = doc.mdip.registry;
12✔
165
            byRegistry[registry] = (byRegistry[registry] || 0) + 1;
12✔
166

167
            const version = doc.didDocumentMetadata.version;
12✔
168
            byVersion[version] = (byVersion[version] || 0) + 1;
12✔
169
        }
170
        catch (error) {
171
            invalid += 1;
2✔
172
            if (chatty) {
2!
173
                console.log(`can't resolve ${n}/${total} ${did} ${error}`);
2✔
174
            }
175
        }
176
    }
177

178
    const byType = { agents, assets, confirmed, unconfirmed, ephemeral, invalid };
8✔
179
    return { total, byType, byRegistry, byVersion };
8✔
180
}
181

182
export async function initRegistries(csvRegistries) {
183
    if (!csvRegistries) {
12✔
184
        supportedRegistries = validRegistries;
4✔
185
    }
186
    else {
187
        const registries = csvRegistries.split(',').map(registry => registry.trim());
18✔
188
        supportedRegistries = [];
8✔
189

190
        for (const registry of registries) {
8✔
191
            if (validRegistries.includes(registry)) {
18✔
192
                supportedRegistries.push(registry);
16✔
193
            }
194
            else {
195
                throw new InvalidParameterError(`registry=${registry}`);
2✔
196
            }
197
        }
198
    }
199

200
    return supportedRegistries;
10✔
201
}
202

203
export async function listRegistries() {
204
    return supportedRegistries || validRegistries;
6✔
205
}
206

207
// For testing purposes
208
export async function resetDb() {
209
    await db.resetDb();
258✔
210
    verifiedDIDs = {};
258✔
211
}
212

213
export async function generateCID(operation) {
214
    return ipfs.add(JSON.parse(canonicalize(operation)));
13,614✔
215
}
216

217
export async function generateDID(operation) {
218
    const cid = await generateCID(operation);
6,098✔
219
    return `${config.didPrefix}:${cid}`;
6,098✔
220
}
221

222
export async function verifyOperation(operation) {
223
    try {
88✔
224
        if (operation.type === 'create') {
88✔
225
            return verifyCreateOperation(operation);
30✔
226
        }
227

228
        if (operation.type === 'update' || operation.type === 'delete') {
58!
229
            const doc = await resolveDID(operation.did);
58✔
230
            return verifyUpdateOperation(operation, doc);
52✔
231
        }
232
    }
233
    catch (error) {
234
        return false;
6✔
235
    }
236
}
237

238
function verifyDIDFormat(did) {
239
    return did && typeof did === 'string' && did.startsWith('did:');
1,332✔
240
}
241

242
function verifyDateFormat(time) {
243
    const date = new Date(time);
3,614✔
244
    return !isNaN(date.getTime());
3,614✔
245
}
246

247
function verifyHashFormat(hash) {
248
    // Check if hash is a hexadecimal string of length 64
249
    const hex64Regex = /^[a-f0-9]{64}$/i;
2,124✔
250
    return hex64Regex.test(hash);
2,124✔
251
}
252

253
function verifySignatureFormat(signature) {
254
    if (!signature) {
2,136✔
255
        return false;
10✔
256
    }
257

258
    if (!verifyDateFormat(signature.signed)) {
2,126✔
259
        return false;
2✔
260
    }
261

262
    if (!verifyHashFormat(signature.hash)) {
2,124✔
263
        return false;
2✔
264
    }
265

266
    // eslint-disable-next-line
267
    if (signature.signer && !verifyDIDFormat(signature.signer)) {
2,122✔
268
        return false;
2✔
269
    }
270

271
    return true;
2,120✔
272
}
273

274
async function verifyCreateOperation(operation) {
275
    if (!operation) {
1,432✔
276
        throw new InvalidOperationError('missing');
2✔
277
    }
278

279
    if (operation.type !== "create") {
1,430✔
280
        throw new InvalidOperationError(`type=${operation.type}`);
2✔
281
    }
282

283
    if (!verifyDateFormat(operation.created)) {
1,428✔
284
        // TBD ensure valid timestamp format
285
        throw new InvalidOperationError(`created=${operation.created}`);
2✔
286
    }
287

288
    if (!operation.mdip) {
1,426✔
289
        throw new InvalidOperationError('mdip');
2✔
290
    }
291

292
    if (!validVersions.includes(operation.mdip.version)) {
1,424✔
293
        throw new InvalidOperationError(`mdip.version=${operation.mdip.version}`);
2✔
294
    }
295

296
    if (!validTypes.includes(operation.mdip.type)) {
1,422✔
297
        throw new InvalidOperationError(`mdip.type=${operation.mdip.type}`);
2✔
298
    }
299

300
    if (!validRegistries.includes(operation.mdip.registry)) {
1,420✔
301
        throw new InvalidOperationError(`mdip.registry=${operation.mdip.registry}`);
2✔
302
    }
303

304
    if (!verifySignatureFormat(operation.signature)) {
1,418✔
305
        throw new InvalidOperationError('signature');
4✔
306
    }
307

308
    if (operation.mdip.validUntil && !verifyDateFormat(operation.mdip.validUntil)) {
1,414✔
309
        throw new InvalidOperationError(`mdip.validUntil=${operation.mdip.validUntil}`);
2✔
310
    }
311

312
    if (operation.mdip.type === 'agent') {
1,412✔
313
        if (!operation.publicJwk) {
730✔
314
            throw new InvalidOperationError('publicJwk');
2✔
315
        }
316

317
        const operationCopy = copyJSON(operation);
728✔
318
        delete operationCopy.signature;
728✔
319

320
        const msgHash = cipher.hashJSON(operationCopy);
728✔
321
        return cipher.verifySig(msgHash, operation.signature.value, operation.publicJwk);
728✔
322
    }
323

324
    if (operation.mdip.type === 'asset') {
682!
325
        if (operation.controller !== operation.signature?.signer) {
682✔
326
            throw new InvalidOperationError('signer is not controller');
2✔
327
        }
328

329
        const doc = await resolveDID(operation.signature.signer, { confirm: true, atTime: operation.signature.signed });
680✔
330

331
        if (doc.mdip.registry === 'local' && operation.mdip.registry !== 'local') {
676✔
332
            throw new InvalidOperationError(`non-local registry=${operation.mdip.registry}`);
2✔
333
        }
334

335
        const operationCopy = copyJSON(operation);
674✔
336
        delete operationCopy.signature;
674✔
337
        const msgHash = cipher.hashJSON(operationCopy);
674✔
338
        // TBD select the right key here, not just the first one
339
        const publicJwk = doc.didDocument.verificationMethod[0].publicKeyJwk;
674✔
340
        return cipher.verifySig(msgHash, operation.signature.value, publicJwk);
674✔
341
    }
342

UNCOV
343
    throw new InvalidOperationError(`mdip.type=${operation.mdip.type}`);
×
344
}
345

346
async function verifyUpdateOperation(operation, doc) {
347
    if (!verifySignatureFormat(operation.signature)) {
582✔
348
        throw new InvalidOperationError('signature');
2✔
349
    }
350

351
    if (!doc?.didDocument) {
580!
UNCOV
352
        throw new InvalidOperationError('doc.didDocument');
×
353
    }
354

355
    if (doc.didDocumentMetadata?.deactivated) {
580✔
356
        throw new InvalidOperationError('DID deactivated');
2✔
357
    }
358

359
    if (doc.didDocument.controller) {
578✔
360
        // This DID is an asset, verify with controller's keys
361
        const controllerDoc = await resolveDID(doc.didDocument.controller, { confirm: true, atTime: operation.signature.signed });
124✔
362
        return verifyUpdateOperation(operation, controllerDoc);
124✔
363
    }
364

365
    if (!doc.didDocument.verificationMethod) {
454!
UNCOV
366
        throw new InvalidOperationError('doc.didDocument.verificationMethod');
×
367
    }
368

369
    const signature = operation.signature;
454✔
370
    const jsonCopy = copyJSON(operation);
454✔
371
    delete jsonCopy.signature;
454✔
372
    const msgHash = cipher.hashJSON(jsonCopy);
454✔
373

374
    if (signature.hash && signature.hash !== msgHash) {
454✔
375
        return false;
4✔
376
    }
377

378
    // TBD get the right signature, not just the first one
379
    const publicJwk = doc.didDocument.verificationMethod[0].publicKeyJwk;
450✔
380
    return cipher.verifySig(msgHash, signature.value, publicJwk);
450✔
381
}
382

383
export async function createDID(operation) {
384
    const valid = await verifyCreateOperation(operation);
1,372✔
385

386
    if (valid) {
1,346✔
387
        const did = await generateDID(operation);
1,344✔
388
        const ops = await exportDID(did);
1,344✔
389

390
        // Check to see if we already have this DID in the db
391
        if (ops.length === 0) {
1,344✔
392
            await db.addEvent(did, {
1,338✔
393
                registry: 'local',
394
                time: operation.created,
395
                ordinal: 0,
396
                operation,
397
                did
398
            });
399

400
            // Create events are distributed only by hyperswarm
401
            // (because the DID's registry specifies where to look for *update* events)
402
            // Don't distribute local DIDs
403
            if (operation.mdip.registry !== 'local') {
1,338✔
404
                await db.queueOperation('hyperswarm', operation);
1,016✔
405
            }
406
        }
407

408
        return did;
1,344✔
409
    }
410
    else {
411
        throw new InvalidOperationError('signature');
2✔
412
    }
413
}
414

415
export async function generateDoc(anchor) {
416
    let doc = {};
4,752✔
417
    try {
4,752✔
418
        if (!anchor?.mdip) {
4,752✔
419
            return {};
2✔
420
        }
421

422
        if (!validVersions.includes(anchor.mdip.version)) {
4,750✔
423
            return {};
2✔
424
        }
425

426
        if (!validTypes.includes(anchor.mdip.type)) {
4,748✔
427
            return {};
2✔
428
        }
429

430
        if (!validRegistries.includes(anchor.mdip.registry)) {
4,746✔
431
            return {};
2✔
432
        }
433

434
        const did = await generateDID(anchor);
4,744✔
435

436
        if (anchor.mdip.type === 'agent') {
4,744✔
437
            // TBD support different key types?
438
            doc = {
3,220✔
439
                "@context": "https://w3id.org/did-resolution/v1",
440
                "didDocument": {
441
                    "@context": ["https://www.w3.org/ns/did/v1"],
442
                    "id": did,
443
                    "verificationMethod": [
444
                        {
445
                            "id": "#key-1",
446
                            "controller": did,
447
                            "type": "EcdsaSecp256k1VerificationKey2019",
448
                            "publicKeyJwk": anchor.publicJwk,
449
                        }
450
                    ],
451
                    "authentication": [
452
                        "#key-1"
453
                    ],
454
                },
455
                "didDocumentMetadata": {
456
                    "created": anchor.created,
457
                },
458
                "didDocumentData": {},
459
                "mdip": anchor.mdip,
460
            };
461
        }
462

463
        if (anchor.mdip.type === 'asset') {
4,744✔
464
            doc = {
1,524✔
465
                "@context": "https://w3id.org/did-resolution/v1",
466
                "didDocument": {
467
                    "@context": ["https://www.w3.org/ns/did/v1"],
468
                    "id": did,
469
                    "controller": anchor.controller,
470
                },
471
                "didDocumentMetadata": {
472
                    "created": anchor.created,
473
                },
474
                "didDocumentData": anchor.data,
475
                "mdip": anchor.mdip,
476
            };
477
        }
478
    }
479
    catch (error) {
480
        // console.error(error);
481
    }
482

483
    return doc;
4,744✔
484
}
485

486
export async function resolveDID(did, options = {}) {
472✔
487
    const { atTime, atVersion, confirm = false, verify = false } = options;
4,844✔
488

489
    const events = await db.getEvents(did);
4,844✔
490

491
    if (events.length === 0) {
4,844✔
492
        throw new InvalidDIDError();
104✔
493
    }
494

495
    const anchor = events[0];
4,740✔
496
    let doc = await generateDoc(anchor.operation);
4,740✔
497

498
    if (atTime && new Date(doc.mdip.created) > new Date(atTime)) {
4,740!
499
        // TBD What to return if DID was created after specified time?
500
    }
501

502
    let version = 1; // initial version is version 1 by definition
4,740✔
503
    let confirmed = true; // create event is always confirmed by definition
4,740✔
504

505
    doc.didDocumentMetadata.version = version;
4,740✔
506
    doc.didDocumentMetadata.confirmed = confirmed;
4,740✔
507

508
    for (const { time, operation, registry, blockchain } of events) {
4,740✔
509
        const opid = await generateCID(operation);
7,470✔
510

511
        if (operation.type === 'create') {
7,470✔
512
            if (verify) {
4,740✔
513
                const valid = await verifyCreateOperation(operation);
30✔
514

515
                if (!valid) {
28✔
516
                    throw new InvalidOperationError('signature');
2✔
517
                }
518
            }
519
            doc.mdip.opid = opid;
4,736✔
520
            continue;
4,736✔
521
        }
522

523
        if (atTime && new Date(time) > new Date(atTime)) {
2,730✔
524
            break;
62✔
525
        }
526

527
        if (atVersion && version === atVersion) {
2,668✔
528
            break;
22✔
529
        }
530

531
        confirmed = confirmed && doc.mdip.registry === registry;
2,646✔
532

533
        if (confirm && !confirmed) {
2,646✔
534
            break;
24✔
535
        }
536

537
        if (verify) {
2,622✔
538
            const valid = await verifyUpdateOperation(operation, doc);
26✔
539

540
            if (!valid) {
26✔
541
                throw new InvalidOperationError('signature');
2✔
542
            }
543

544
            // TEMP during did:test, operation.previd is optional
545
            if (operation.previd && operation.previd !== doc.mdip.opid) {
24✔
546
                throw new InvalidOperationError('previd');
2✔
547
            }
548
        }
549

550
        if (operation.type === 'update') {
2,618✔
551
            // Increment version
552
            version += 1;
2,572✔
553

554
            // TBD if registry change in operation.doc.didDocumentMetadata.mdip,
555
            // fetch updates from new registry and search for same operation
556
            doc = operation.doc;
2,572✔
557
            doc.didDocumentMetadata.updated = time;
2,572✔
558
            doc.didDocumentMetadata.version = version;
2,572✔
559
            doc.didDocumentMetadata.confirmed = confirmed;
2,572✔
560
            doc.mdip.opid = opid;
2,572✔
561

562
            if (blockchain) {
2,572!
UNCOV
563
                doc.mdip.registration = blockchain;
×
564
            }
565
            else {
566
                delete doc.mdip.registration;
2,572✔
567
            }
568
        }
569
        else if (operation.type === 'delete') {
46!
570
            doc.didDocument = {};
46✔
571
            doc.didDocumentData = {};
46✔
572
            doc.didDocumentMetadata.deactivated = true;
46✔
573
            doc.didDocumentMetadata.deleted = time;
46✔
574
            doc.didDocumentMetadata.confirmed = confirmed;
46✔
575
            doc.mdip.opid = opid;
46✔
576

577
            if (blockchain) {
46!
578
                doc.mdip.registration = blockchain;
×
579
            }
580
            else {
581
                delete doc.mdip.registration;
46✔
582
            }
583
        }
584
        else {
UNCOV
585
            if (verify) {
×
UNCOV
586
                throw new InvalidOperationError('signature');
×
587
            }
588

589
            // console.error(`unknown type ${operation.type}`);
590
        }
591
    }
592

593
    return copyJSON(doc);
4,732✔
594
}
595

596
export async function updateDID(operation) {
597
    const doc = await resolveDID(operation.did);
380✔
598
    const updateValid = await verifyUpdateOperation(operation, doc);
380✔
599

600
    if (!updateValid) {
376✔
601
        return false;
2✔
602
    }
603

604
    const registry = doc.mdip.registry;
374✔
605

606
    await db.addEvent(operation.did, {
374✔
607
        registry: 'local',
608
        time: operation.signature.signed,
609
        ordinal: 0,
610
        operation,
611
        did: operation.did
612
    });
613

614
    if (registry === 'local') {
374✔
615
        return true;
186✔
616
    }
617

618
    await db.queueOperation(registry, operation);
188✔
619

620
    if (registry !== 'hyperswarm') {
188✔
621
        await db.queueOperation('hyperswarm', operation);
176✔
622
    }
623

624
    return true;
188✔
625
}
626

627
export async function deleteDID(operation) {
628
    return updateDID(operation);
28✔
629
}
630

631
export async function getDIDs(options = {}) {
18✔
632
    let { dids, updatedAfter, updatedBefore, confirm, verify, resolve } = options;
50✔
633
    if (!dids) {
50✔
634
        const keys = await db.getAllKeys();
48✔
635
        dids = keys.map(key => `${config.didPrefix}:${key}`);
164✔
636
    }
637

638
    if (updatedAfter || updatedBefore || resolve) {
50✔
639
        const start = updatedAfter ? new Date(updatedAfter) : null;
14✔
640
        const end = updatedBefore ? new Date(updatedBefore) : null;
14✔
641
        const response = [];
14✔
642

643
        for (const did of dids) {
14✔
644
            const doc = await resolveDID(did, { confirm, verify });
98✔
645
            const updated = new Date(doc.didDocumentMetadata.updated || doc.didDocumentMetadata.created);
98✔
646

647
            if (start && updated <= start) {
98✔
648
                continue;
22✔
649
            }
650

651
            if (end && updated >= end) {
76✔
652
                continue;
14✔
653
            }
654

655
            response.push(resolve ? doc : did);
62✔
656
        }
657

658
        return response;
14✔
659
    }
660

661
    return dids;
36✔
662
}
663

664
export async function exportDID(did) {
665
    return db.getEvents(did);
1,466✔
666
}
667

668
export async function exportDIDs(dids) {
669
    if (!dids) {
28✔
670
        dids = await getDIDs();
12✔
671
    }
672

673
    const batch = [];
28✔
674

675
    for (const did of dids) {
28✔
676
        batch.push(await exportDID(did));
60✔
677
    }
678

679
    return batch;
28✔
680
}
681

682
export async function importDIDs(dids) {
683
    return importBatch(dids.flat());
2✔
684
}
685

686
export async function removeDIDs(dids) {
687
    if (!Array.isArray(dids)) {
14✔
688
        throw new InvalidParameterError('dids');
2✔
689
    }
690

691
    for (const did of dids) {
12✔
692
        await db.deleteEvents(did);
14✔
693
    }
694

695
    return true;
12✔
696
}
697

698
async function importEvent(event) {
699
    if (!event.did) {
110✔
700
        if (event.operation.did) {
6✔
701
            event.did = event.operation.did;
2✔
702
        }
703
        else {
704
            event.did = await generateDID(event.operation);
4✔
705
        }
706
    }
707

708
    const did = event.did;
110✔
709
    const currentEvents = await db.getEvents(did);
110✔
710

711
    const match = currentEvents.find(item => item.operation.signature.value === event.operation.signature.value);
182✔
712

713
    if (match) {
110✔
714
        const first = currentEvents[0];
42✔
715
        const nativeRegistry = first.operation.mdip.registry;
42✔
716

717
        if (match.registry === nativeRegistry) {
42✔
718
            // If this event is already confirmed on the native registry, no need to update
719
            return false;
12✔
720
        }
721

722
        if (event.registry === nativeRegistry) {
30✔
723
            // If this import is on the native registry, replace the current one
724
            const index = currentEvents.indexOf(match);
18✔
725
            currentEvents[index] = event;
18✔
726
            await db.setEvents(did, currentEvents);
18✔
727
            return true;
18✔
728
        }
729

730
        return false;
12✔
731
    }
732
    else {
733
        const ok = await verifyOperation(event.operation);
68✔
734

735
        if (ok) {
66✔
736
            // TEMP during did:test, operation.previd is optional
737
            if (currentEvents.length > 0 && event.operation.previd) {
60✔
738
                const lastEvent = currentEvents[currentEvents.length - 1];
32✔
739
                const opid = await generateCID(lastEvent.operation);
32✔
740

741
                if (opid !== event.operation.previd) {
32!
UNCOV
742
                    throw new InvalidOperationError('previd');
×
743
                }
744
            }
745

746
            await db.addEvent(did, event);
60✔
747
            return true;
60✔
748
        }
749
        else {
750
            throw new InvalidOperationError('signature');
6✔
751
        }
752
    }
753
}
754

755
async function importEvents() {
756
    let tempQueue = eventsQueue;
68✔
757
    const total = tempQueue.length;
68✔
758
    let event = tempQueue.shift();
68✔
759
    let i = 0;
68✔
760
    let added = 0;
68✔
761
    let merged = 0;
68✔
762

763
    eventsQueue = [];
68✔
764

765
    while (event) {
68✔
766
        //console.time('importEvent');
767
        i += 1;
110✔
768
        try {
110✔
769
            const imported = await importEvent(event);
110✔
770

771
            if (imported) {
102✔
772
                added += 1;
78✔
773
                console.log(`import ${i}/${total}: added event for ${event.did}`);
78✔
774
            }
775
            else {
776
                merged += 1;
24✔
777
                console.log(`import ${i}/${total}: merged event for ${event.did}`);
24✔
778
            }
779
        }
780
        catch (error) {
781
            eventsQueue.push(event);
8✔
782
            console.log(`import ${i}/${total}: deferred event for ${event.did}`);
8✔
783
        }
784
        //console.timeEnd('importEvent');
785
        event = tempQueue.shift();
110✔
786
    }
787

788
    return { added, merged };
68✔
789
}
790

791
export async function processEvents() {
792
    if (isProcessingEvents) {
32!
793
        return { busy: true };
×
794
    }
795

796
    let added = 0;
32✔
797
    let merged = 0;
32✔
798
    let done = false;
32✔
799

800
    try {
32✔
801
        isProcessingEvents = true;
32✔
802

803
        while (!done) {
32✔
804
            //console.time('importEvents');
805
            const response = await importEvents();
68✔
806
            //console.timeEnd('importEvents');
807

808
            added += response.added;
68✔
809
            merged += response.merged;
68✔
810

811
            done = (response.added === 0 && response.merged === 0);
68✔
812
        }
813
    }
814
    catch (error) {
815
        console.log(error);
×
816
    }
817
    finally {
818
        isProcessingEvents = false;
32✔
819
    }
820

821
    //console.log(JSON.stringify(eventsQueue, null, 4));
822
    const pending = eventsQueue.length;
32✔
823
    const response = { added, merged, pending };
32✔
824

825
    console.log(`processEvents: ${JSON.stringify(response)}`);
32✔
826

827
    return response;
32✔
828
}
829

830
export async function verifyEvent(event) {
831
    if (!event.registry || !event.time || !event.operation) {
144✔
832
        return false;
6✔
833
    }
834

835
    const eventTime = new Date(event.time).getTime();
138✔
836

837
    if (isNaN(eventTime)) {
138✔
838
        return false;
2✔
839
    }
840

841
    const operation = event.operation;
136✔
842

843
    if (!verifySignatureFormat(operation.signature)) {
136✔
844
        return false;
10✔
845
    }
846

847
    if (operation.type === 'create') {
126✔
848
        if (!operation.created) {
72✔
849
            return false;
2✔
850
        }
851

852
        if (!operation.mdip) {
70✔
853
            return false;
2✔
854
        }
855

856
        if (!validVersions.includes(operation.mdip.version)) {
68✔
857
            return false;
2✔
858
        }
859

860
        if (!validTypes.includes(operation.mdip.type)) {
66✔
861
            return false;
2✔
862
        }
863

864
        if (!validRegistries.includes(operation.mdip.registry)) {
64✔
865
            return false;
2✔
866
        }
867

868
        // eslint-disable-next-line
869
        if (operation.mdip.type === 'agent') {
62✔
870
            if (!operation.publicJwk) {
48✔
871
                return false;
2✔
872
            }
873
        }
874

875
        // eslint-disable-next-line
876
        if (operation.mdip.type === 'asset') {
60✔
877
            if (operation.controller !== operation.signature?.signer) {
14✔
878
                return false;
2✔
879
            }
880
        }
881
    }
882
    else if (operation.type === 'update') {
54✔
883
        const doc = operation.doc;
50✔
884

885
        if (!doc || !doc.didDocument || !doc.didDocumentMetadata || !doc.didDocumentData || !doc.mdip) {
50✔
886
            return false;
2✔
887
        }
888

889
        if (!operation.did) {
48✔
890
            return false;
2✔
891
        }
892
    }
893
    else if (operation.type === 'delete') {
4✔
894
        if (!operation.did) {
2!
895
            return false;
2✔
896
        }
897
    }
898
    else {
899
        return false;
2✔
900
    }
901

902
    return true;
104✔
903
}
904

905
export async function importBatch(batch) {
906
    if (!batch || !Array.isArray(batch) || batch.length < 1) {
80✔
907
        throw new InvalidParameterError('batch');
6✔
908
    }
909

910
    let queued = 0;
74✔
911
    let rejected = 0;
74✔
912
    let processed = 0;
74✔
913

914
    for (let i = 0; i < batch.length; i++) {
74✔
915
        const event = batch[i];
144✔
916
        const ok = await verifyEvent(event);
144✔
917

918
        if (ok) {
144✔
919
            const eventKey = `${event.registry}/${event.operation.signature.hash}`;
104✔
920
            if (!eventsSeen[eventKey]) {
104✔
921
                eventsSeen[eventKey] = true;
102✔
922
                eventsQueue.push(event);
102✔
923
                queued += 1;
102✔
924
            }
925
            else {
926
                processed += 1;
2✔
927
            }
928
        }
929
        else {
930
            rejected += 1;
40✔
931
        }
932
    }
933

934
    return {
74✔
935
        queued,
936
        processed,
937
        rejected,
938
        total: eventsQueue.length
939
    };
940
}
941

942
export async function exportBatch(dids) {
943
    const allDIDs = await exportDIDs(dids);
12✔
944
    const nonlocalDIDs = allDIDs.filter(events => {
12✔
945
        if (events.length > 0) {
26✔
946
            const create = events[0];
24✔
947
            const registry = create.operation?.mdip?.registry;
24✔
948
            return registry && registry !== 'local'
24✔
949
        }
950
        return false;
2✔
951
    });
952

953
    const events = nonlocalDIDs.flat();
12✔
954
    return events.sort((a, b) => new Date(a.operation.signature.signed) - new Date(b.operation.signature.signed));
18✔
955
}
956

957
export async function getQueue(registry) {
958
    if (!validRegistries.includes(registry)) {
18✔
959
        throw new InvalidParameterError(`registry=${registry}`);
2✔
960
    }
961

962
    return db.getQueue(registry);
16✔
963
}
964

965
export async function clearQueue(registry, events) {
966
    if (!validRegistries.includes(registry)) {
12✔
967
        throw new InvalidParameterError(`registry=${registry}`);
2✔
968
    }
969

970
    return db.clearQueue(registry, events);
10✔
971
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc