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

KeychainMDIP / kc / 11114847373

30 Sep 2024 10:04PM UTC coverage: 93.859% (+0.01%) from 93.848%
11114847373

Pull #340

github

macterra
Refactored createId
Pull Request #340: fix: response DIDs should be ephemeral

766 of 834 branches covered (91.85%)

Branch coverage included in aggregate %.

70 of 71 new or added lines in 2 files covered. (98.59%)

48 existing lines in 2 files now uncovered.

1221 of 1283 relevant lines covered (95.17%)

357.68 hits per line

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

93.29
/packages/gatekeeper/src/gatekeeper-lib.js
1
import { json } from '@helia/json';
2
import { base58btc } from 'multiformats/bases/base58';
3
import canonicalize from 'canonicalize';
4
import { createHelia } from 'helia';
5
import * as cipher from '@mdip/cipher/node';
6
import * as exceptions from '@mdip/exceptions';
7
import config from './config.js';
8

9
const validVersions = [1];
4✔
10
const validTypes = ['agent', 'asset'];
4✔
11
const validRegistries = ['local', 'hyperswarm', 'TESS', 'TBTC', 'TFTC'];
4✔
12
let supportedRegistries = null;
4✔
13

14
let db = null;
4✔
15
let helia = null;
4✔
16
let ipfs = null;
4✔
17
let eventsCache = {};
4✔
18

19
export function copyJSON(json) {
20
    return JSON.parse(JSON.stringify(json))
10,910✔
21
}
22

23
export async function start(injectedDb) {
24
    if (!ipfs) {
540✔
25
        helia = await createHelia();
4✔
26
        ipfs = json(helia);
4✔
27
    }
28

29
    db = injectedDb;
540✔
30
}
31

32
export async function stop() {
33
    helia.stop();
540✔
34
    await db.stop();
540✔
35
}
36

37
export async function verifyDID(did) {
38
    await resolveDID(did, { verify: true });
6✔
39
}
40

41
export async function verifyDb(chatty = true) {
×
42
    if (chatty) {
4✔
NEW
43
        console.time('verifyDb');
×
44
    }
45

46
    const keys = await db.getAllKeys();
4✔
47
    const dids = keys.map(key => `${config.didPrefix}:${key}`);
6✔
48
    let n = 0;
4✔
49
    let invalid = 0;
4✔
50

51
    // prime the cache
52
    try {
4✔
53
        const allEvents = await db.getAllEvents();
4✔
54
        for (const key of Object.keys(allEvents)) {
4✔
55
            eventsCache[`${config.didPrefix}:${key}`] = allEvents[key];
6✔
56
        }
57
    }
58
    catch (error) {
UNCOV
59
        if (chatty) {
×
UNCOV
60
            console.error(error);
×
61
        }
62
    }
63

64
    for (const did of dids) {
4✔
65
        n += 1;
6✔
66
        try {
6✔
67
            await verifyDID(did);
6✔
68
            if (chatty) {
4✔
UNCOV
69
                console.log(`${n} ${did} OK`);
×
70
            }
71
        }
72
        catch (error) {
73
            if (chatty) {
2✔
UNCOV
74
                console.log(`${n} ${did} ${error}`);
×
75
            }
76
            invalid += 1;
2✔
77
            await db.deleteEvents(did);
2✔
78
            delete eventsCache[did];
2✔
79
        }
80
    }
81

82
    if (chatty) {
4✔
UNCOV
83
        console.timeEnd('verifyDb');
×
84
    }
85

86
    return invalid;
4✔
87
}
88

89
export async function initRegistries(csvRegistries) {
90
    if (!csvRegistries) {
12✔
91
        supportedRegistries = validRegistries;
4✔
92
    }
93
    else {
94
        const registries = csvRegistries.split(',').map(registry => registry.trim());
18✔
95
        supportedRegistries = [];
8✔
96

97
        for (const registry of registries) {
8✔
98
            if (validRegistries.includes(registry)) {
18✔
99
                supportedRegistries.push(registry);
16✔
100
            }
101
            else {
102
                throw new Error(exceptions.INVALID_REGISTRY);
2✔
103
            }
104
        }
105
    }
106

107
    return supportedRegistries;
10✔
108
}
109

110
export async function listRegistries() {
111
    return supportedRegistries || validRegistries;
6✔
112
}
113

114
// For testing purposes
115
export async function resetDb() {
116
    await db.resetDb();
6✔
117
    eventsCache = {};
6✔
118
}
119

120
export async function anchorSeed(seed) {
121
    const cid = await ipfs.add(JSON.parse(canonicalize(seed)));
5,216✔
122
    return `${config.didPrefix}:${cid.toString(base58btc)}`;
5,216✔
123
}
124

125
async function verifyCreateAgent(operation) {
126
    if (!operation.signature) {
578✔
127
        throw new Error(exceptions.INVALID_OPERATION);
2✔
128
    }
129

130
    if (!operation.publicJwk) {
576✔
131
        throw new Error(exceptions.INVALID_OPERATION);
2✔
132
    }
133

134
    const operationCopy = copyJSON(operation);
574✔
135
    delete operationCopy.signature;
574✔
136

137
    const msgHash = cipher.hashJSON(operationCopy);
574✔
138
    return cipher.verifySig(msgHash, operation.signature.value, operation.publicJwk);
574✔
139
}
140

141
async function verifyCreateAsset(operation) {
142
    if (operation.controller !== operation.signature?.signer) {
576✔
143
        throw new Error(exceptions.INVALID_OPERATION);
4✔
144
    }
145

146
    const doc = await resolveDID(operation.signature.signer, { confirm: true, atTime: operation.signature.signed });
572✔
147

148
    if (doc.mdip.registry === 'local' && operation.mdip.registry !== 'local') {
572✔
149
        throw new Error(exceptions.INVALID_REGISTRY);
2✔
150
    }
151

152
    const operationCopy = copyJSON(operation);
570✔
153
    delete operationCopy.signature;
570✔
154
    const msgHash = cipher.hashJSON(operationCopy);
570✔
155
    // TBD select the right key here, not just the first one
156
    const publicJwk = doc.didDocument.verificationMethod[0].publicKeyJwk;
570✔
157
    return cipher.verifySig(msgHash, operation.signature.value, publicJwk);
570✔
158
}
159

160
async function verifyCreate(operation) {
161
    if (operation?.type !== "create") {
1,168✔
162
        throw new Error(exceptions.INVALID_OPERATION);
4✔
163
    }
164

165
    if (!operation.created) {
1,164✔
166
        // TBD ensure valid timestamp format
167
        throw new Error(exceptions.INVALID_OPERATION);
2✔
168
    }
169

170
    if (!operation.mdip) {
1,162✔
171
        throw new Error(exceptions.INVALID_OPERATION);
2✔
172
    }
173

174
    if (!validVersions.includes(operation.mdip.version)) {
1,160✔
175
        throw new Error(exceptions.INVALID_VERSION);
2✔
176
    }
177

178
    if (!validTypes.includes(operation.mdip.type)) {
1,158✔
179
        throw new Error(exceptions.INVALID_TYPE);
2✔
180
    }
181

182
    if (!validRegistries.includes(operation.mdip.registry)) {
1,156✔
183
        throw new Error(exceptions.INVALID_REGISTRY);
2✔
184
    }
185

186
    if (operation.mdip.type === 'agent') {
1,154✔
187
        return verifyCreateAgent(operation);
578✔
188
    }
189

190
    if (operation.mdip.type === 'asset') {
576!
191
        return verifyCreateAsset(operation);
576✔
192
    }
193

UNCOV
194
    throw new Error(exceptions.INVALID_OPERATION);
×
195
}
196

197
export async function createDID(operation) {
198
    const valid = await verifyCreate(operation);
1,158✔
199

200
    if (valid) {
1,134!
201
        const did = await anchorSeed(operation);
1,134✔
202
        const ops = await exportDID(did);
1,134✔
203

204
        // Check to see if we already have this DID in the db
205
        if (ops.length === 0) {
1,134✔
206
            await db.addEvent(did, {
1,130✔
207
                registry: 'local',
208
                time: operation.created,
209
                ordinal: 0,
210
                operation: operation
211
            });
212

213
            // Create events are distributed only by hyperswarm
214
            // (because the DID's registry specifies where to look for *update* events)
215
            // Don't distribute local DIDs
216
            if (operation.mdip.registry !== 'local') {
1,130✔
217
                await db.queueOperation('hyperswarm', operation);
882✔
218
            }
219
        }
220

221
        return did;
1,134✔
222
    }
223
    else {
UNCOV
224
        throw new Error(exceptions.INVALID_OPERATION);
×
225
    }
226
}
227

228
export async function generateDoc(anchor) {
229
    let doc = {};
4,052✔
230
    try {
4,052✔
231
        if (!anchor?.mdip) {
4,052✔
232
            return {};
2✔
233
        }
234

235
        if (!validVersions.includes(anchor.mdip.version)) {
4,050✔
236
            return {};
2✔
237
        }
238

239
        if (!validTypes.includes(anchor.mdip.type)) {
4,048✔
240
            return {};
2✔
241
        }
242

243
        if (!validRegistries.includes(anchor.mdip.registry)) {
4,046✔
244
            return {};
2✔
245
        }
246

247
        const did = await anchorSeed(anchor);
4,044✔
248

249
        if (anchor.mdip.type === 'agent') {
4,044✔
250
            // TBD support different key types?
251
            doc = {
2,748✔
252
                "@context": "https://w3id.org/did-resolution/v1",
253
                "didDocument": {
254
                    "@context": ["https://www.w3.org/ns/did/v1"],
255
                    "id": did,
256
                    "verificationMethod": [
257
                        {
258
                            "id": "#key-1",
259
                            "controller": did,
260
                            "type": "EcdsaSecp256k1VerificationKey2019",
261
                            "publicKeyJwk": anchor.publicJwk,
262
                        }
263
                    ],
264
                    "authentication": [
265
                        "#key-1"
266
                    ],
267
                },
268
                "didDocumentMetadata": {
269
                    "created": anchor.created,
270
                },
271
                "didDocumentData": {},
272
                "mdip": anchor.mdip,
273
            };
274
        }
275

276
        if (anchor.mdip.type === 'asset') {
4,044✔
277
            doc = {
1,296✔
278
                "@context": "https://w3id.org/did-resolution/v1",
279
                "didDocument": {
280
                    "@context": ["https://www.w3.org/ns/did/v1"],
281
                    "id": did,
282
                    "controller": anchor.controller,
283
                },
284
                "didDocumentMetadata": {
285
                    "created": anchor.created,
286
                },
287
                "didDocumentData": anchor.data,
288
                "mdip": anchor.mdip,
289
            };
290
        }
291
    }
292
    catch (error) {
293
        // console.error(error);
294
    }
295

296
    return doc;
4,044✔
297
}
298

299
async function verifyUpdate(operation, doc) {
300

301
    if (!doc?.didDocument) {
446!
UNCOV
302
        return false;
×
303
    }
304

305
    if (doc.didDocument.controller) {
446✔
306
        const controllerDoc = await resolveDID(doc.didDocument.controller, { confirm: true, atTime: operation.signature.signed });
110✔
307
        return verifyUpdate(operation, controllerDoc);
108✔
308
    }
309

310
    if (!doc.didDocument.verificationMethod) {
336✔
311
        return false;
2✔
312
    }
313

314
    const jsonCopy = copyJSON(operation);
334✔
315

316
    const signature = jsonCopy.signature;
334✔
317
    delete jsonCopy.signature;
334✔
318
    const msgHash = cipher.hashJSON(jsonCopy);
334✔
319

320
    if (signature.hash && signature.hash !== msgHash) {
334✔
321
        return false;
2✔
322
    }
323

324
    // TBD get the right signature, not just the first one
325
    const publicJwk = doc.didDocument.verificationMethod[0].publicKeyJwk;
332✔
326
    return cipher.verifySig(msgHash, signature.value, publicJwk);
332✔
327
}
328

329
async function getEvents(did) {
330
    let events = eventsCache[did];
5,390✔
331

332
    if (!events) {
5,390✔
333
        events = await db.getEvents(did);
2,522✔
334

335
        if (events.length > 0) {
2,522✔
336
            eventsCache[did] = events;
1,292✔
337
        }
338
    }
339

340
    return copyJSON(events);
5,390✔
341
}
342

343
export async function resolveDID(did, { atTime, atVersion, confirm, verify } = {}) {
355✔
344
    const events = await getEvents(did);
4,124✔
345

346
    if (events.length === 0) {
4,124✔
347
        throw new Error(exceptions.INVALID_DID);
84✔
348
    }
349

350
    const anchor = events[0];
4,040✔
351
    let doc = await generateDoc(anchor.operation);
4,040✔
352
    let mdip = doc?.mdip;
4,040✔
353

354
    if (!mdip) {
4,040!
UNCOV
355
        throw new Error(exceptions.INVALID_DID);
×
356
    }
357

358
    if (atTime && new Date(mdip.created) > new Date(atTime)) {
4,040!
359
        // TBD What to return if DID was created after specified time?
360
    }
361

362
    let version = 1; // initial version is version 1 by definition
4,040✔
363
    let confirmed = true; // create event is always confirmed by definition
4,040✔
364

365
    doc.didDocumentMetadata.version = version;
4,040✔
366
    doc.didDocumentMetadata.confirmed = confirmed;
4,040✔
367

368
    for (const { time, operation, registry, blockchain } of events) {
4,040✔
369
        if (operation.type === 'create') {
5,996✔
370
            continue;
4,044✔
371
        }
372

373
        if (atTime && new Date(time) > new Date(atTime)) {
1,952✔
374
            break;
62✔
375
        }
376

377
        confirmed = confirmed && mdip.registry === registry;
1,890✔
378

379
        if (confirm && !confirmed) {
1,890✔
380
            break;
20✔
381
        }
382

383
        if (verify) {
1,870✔
384
            const valid = await verifyUpdate(operation, doc);
4✔
385

386
            if (!valid) {
2!
UNCOV
387
                throw new Error(exceptions.INVALID_OPERATION);
×
388
            }
389
        }
390

391
        if (operation.type === 'update') {
1,868✔
392
            // Increment version
393
            version += 1;
1,822✔
394

395
            // Maintain mdip metadata across versions
396
            mdip = doc.mdip;
1,822✔
397

398
            // TBD if registry change in operation.doc.didDocumentMetadata.mdip,
399
            // fetch updates from new registry and search for same operation
400
            doc = operation.doc;
1,822✔
401
            doc.didDocumentMetadata.updated = time;
1,822✔
402
            doc.didDocumentMetadata.version = version;
1,822✔
403
            doc.didDocumentMetadata.confirmed = confirmed;
1,822✔
404
            doc.mdip = mdip;
1,822✔
405

406
            if (blockchain) {
1,822!
407
                doc.mdip.registration = blockchain;
×
408
            }
409
            else {
410
                delete doc.mdip.registration;
1,822✔
411
            }
412
        }
413
        else if (operation.type === 'delete') {
46!
414
            doc.didDocument = {};
46✔
415
            doc.didDocumentData = {};
46✔
416
            doc.didDocumentMetadata.deactivated = true;
46✔
417
            doc.didDocumentMetadata.deleted = time;
46✔
418
            doc.didDocumentMetadata.confirmed = confirmed;
46✔
419
        }
420
        else {
UNCOV
421
            if (verify) {
×
UNCOV
422
                throw new Error(exceptions.INVALID_OPERATION);
×
423
            }
424

425
            // console.error(`unknown type ${operation.type}`);
426
        }
427

428
        if (atVersion && version === atVersion) {
1,868✔
429
            break;
2✔
430
        }
431
    }
432

433
    return copyJSON(doc);
4,038✔
434
}
435

436
export async function updateDID(operation) {
437
    try {
312✔
438
        const doc = await resolveDID(operation.did);
312✔
439
        const updateValid = await verifyUpdate(operation, doc);
312✔
440

441
        if (!updateValid) {
312✔
442
            return false;
4✔
443
        }
444

445
        const registry = doc.mdip.registry;
308✔
446

447
        await db.addEvent(operation.did, {
308✔
448
            registry: 'local',
449
            time: operation.signature.signed,
450
            ordinal: 0,
451
            operation: operation
452
        });
453

454
        delete eventsCache[operation.did];
308✔
455

456
        if (registry === 'local') {
308✔
457
            return true;
130✔
458
        }
459

460
        await db.queueOperation(registry, operation);
178✔
461

462
        if (registry !== 'hyperswarm') {
178✔
463
            await db.queueOperation('hyperswarm', operation);
170✔
464
        }
465

466
        return true;
178✔
467
    }
468
    catch (error) {
469
        // console.error(error);
UNCOV
470
        return false;
×
471
    }
472
}
473

474
export async function deleteDID(operation) {
475
    return updateDID(operation);
26✔
476
}
477

478
export async function getDIDs({ dids, updatedAfter, updatedBefore, confirm, resolve } = {}) {
3✔
479
    if (!dids) {
20✔
480
        const keys = await db.getAllKeys();
18✔
481
        dids = keys.map(key => `${config.didPrefix}:${key}`);
106✔
482
    }
483

484
    if (updatedAfter || updatedBefore || resolve) {
20✔
485
        const start = updatedAfter ? new Date(updatedAfter) : null;
14✔
486
        const end = updatedBefore ? new Date(updatedBefore) : null;
14✔
487
        const response = [];
14✔
488

489
        for (const did of dids) {
14✔
490
            const doc = await resolveDID(did, { confirm: confirm });
98✔
491
            const updated = new Date(doc.didDocumentMetadata.updated || doc.didDocumentMetadata.created);
98✔
492

493
            if (start && updated <= start) {
98✔
494
                continue;
22✔
495
            }
496

497
            if (end && updated >= end) {
76✔
498
                continue;
14✔
499
            }
500

501
            response.push(resolve ? doc : did);
62✔
502
        }
503

504
        return response;
14✔
505
    }
506

507
    return dids;
6✔
508
}
509

510
export async function exportDID(did) {
511
    return getEvents(did);
1,266✔
512
}
513

514
export async function exportDIDs(dids) {
515
    if (!dids) {
20✔
516
        dids = await getDIDs();
4✔
517
    }
518

519
    const batch = [];
20✔
520

521
    for (const did of dids) {
20✔
522
        batch.push(await exportDID(did));
42✔
523
    }
524

525
    return batch;
20✔
526
}
527

528
export async function importDIDs(dids) {
529
    return importBatch(dids.flat());
2✔
530
}
531

532
export async function removeDIDs(dids) {
533
    if (!Array.isArray(dids)) {
14✔
534
        throw new Error(exceptions.INVALID_PARAMETER);
2✔
535
    }
536

537
    for (const did of dids) {
12✔
538
        await db.deleteEvents(did);
14✔
539
        delete eventsCache[did];
14✔
540
    }
541

542
    return true;
12✔
543
}
544

545
async function importCreateEvent(event) {
546
    try {
10✔
547
        const valid = await verifyCreate(event.operation);
10✔
548

549
        if (valid) {
6!
550
            const did = await anchorSeed(event.operation);
6✔
551
            await db.addEvent(did, event);
6✔
552
            return true;
6✔
553
        }
554

UNCOV
555
        return false;
×
556
    }
557
    catch {
558
        return false;
4✔
559
    }
560
}
561

562
async function importUpdateEvent(event) {
563
    try {
22✔
564
        const did = event.operation.did;
22✔
565
        const doc = await resolveDID(did);
22✔
566
        const updateValid = await verifyUpdate(event.operation, doc);
22✔
567

568
        if (!updateValid) {
22!
UNCOV
569
            return false;
×
570
        }
571

572
        await db.addEvent(did, event);
22✔
573
        return true;
22✔
574
    }
575
    catch (error) {
576
        // console.error(error);
UNCOV
577
        return false;
×
578
    }
579
}
580

581
export async function importEvent(event) {
582

583
    if (!event.registry || !event.time || !event.operation) {
68✔
584
        throw new Error(exceptions.INVALID_PARAMETER);
6✔
585
    }
586

587
    let did;
588

589
    try {
62✔
590
        if (event.operation.type === 'create') {
62✔
591
            did = await anchorSeed(event.operation);
26✔
592
        }
593
        else {
594
            did = event.operation.did;
36✔
595
        }
596

597
        if (!did) {
62✔
598
            throw new Error(exceptions.INVALID_OPERATION);
4✔
599
        }
600
    }
601
    catch {
602
        throw new Error(exceptions.INVALID_OPERATION);
4✔
603
    }
604

605
    const current = await exportDID(did);
58✔
606

607
    if (current.length === 0) {
58✔
608
        const ok = await importCreateEvent(event);
10✔
609

610
        if (!ok) {
10✔
611
            throw new Error(exceptions.INVALID_OPERATION);
4✔
612
        }
613

614
        return true;
6✔
615
    }
616

617
    const create = current[0];
48✔
618
    const registry = create.operation.mdip.registry;
48✔
619
    const match = current.find(item => item.operation.signature.value === event.operation.signature.value);
148✔
620

621
    if (match) {
48✔
622
        if (match.registry === registry) {
26✔
623
            // Don't update if this op has already been validated on its native registry
624
            return false;
14✔
625
        }
626

627
        if (event.registry === registry) {
12!
628
            // If this import is on the native registry, replace the current one
629
            const index = current.indexOf(match);
12✔
630
            current[index] = event;
12✔
631

632
            db.setEvents(did, current);
12✔
633
            delete eventsCache[did];
12✔
634
            return true;
12✔
635
        }
636

UNCOV
637
        return false;
×
638
    }
639

640
    const ok = await importUpdateEvent(event);
22✔
641

642
    if (!ok) {
22!
UNCOV
643
        throw new Error(exceptions.INVALID_OPERATION);
×
644
    }
645

646
    delete eventsCache[did];
22✔
647

648
    return true;
22✔
649
}
650

651
export async function importBatch(batch) {
652
    if (!batch || !Array.isArray(batch) || batch.length < 1) {
38✔
653
        throw new Error(exceptions.INVALID_PARAMETER);
6✔
654
    }
655

656
    let verified = 0;
32✔
657
    let updated = 0;
32✔
658
    let failed = 0;
32✔
659

660
    for (const event of batch) {
32✔
661
        try {
68✔
662
            const imported = await importEvent(event);
68✔
663

664
            if (imported) {
54✔
665
                updated += 1;
40✔
666
            }
667
            else {
668
                verified += 1;
14✔
669
            }
670
        }
671
        catch (error) {
672
            failed += 1;
14✔
673
        }
674
    }
675

676
    return {
32✔
677
        verified: verified,
678
        updated: updated,
679
        failed: failed,
680
    };
681
}
682

683
export async function exportBatch(dids) {
684
    const allDIDs = await exportDIDs(dids);
8✔
685
    const nonlocalDIDs = allDIDs.filter(events => {
8✔
686
        if (events.length > 0) {
18✔
687
            const create = events[0];
16✔
688
            const registry = create.operation?.mdip?.registry;
16✔
689
            return registry && registry !== 'local'
16✔
690
        }
691
        return false;
2✔
692
    });
693

694
    const events = nonlocalDIDs.flat();
8✔
695
    return events.sort((a, b) => new Date(a.operation.signature.signed) - new Date(b.operation.signature.signed));
12✔
696
}
697

698
export async function getQueue(registry) {
699
    if (!validRegistries.includes(registry)) {
16✔
700
        throw new Error(exceptions.INVALID_REGISTRY);
2✔
701
    }
702

703
    return db.getQueue(registry);
14✔
704
}
705

706
export async function clearQueue(registry, events) {
707
    if (!validRegistries.includes(registry)) {
12✔
708
        throw new Error(exceptions.INVALID_REGISTRY);
2✔
709
    }
710

711
    return db.clearQueue(registry, events);
10✔
712
}
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