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

KeychainMDIP / kc / 9716015453

28 Jun 2024 04:35PM UTC coverage: 89.361% (+10.1%) from 79.24%
9716015453

Pull #205

github

macterra
Added unit tests for testSchema
Pull Request #205: Improve test coverage

651 of 756 branches covered (86.11%)

Branch coverage included in aggregate %.

18 of 22 new or added lines in 3 files covered. (81.82%)

13 existing lines in 1 file now uncovered.

1054 of 1152 relevant lines covered (91.49%)

361.87 hits per line

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

82.7
/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 './cipher-lib.js';
6
import config from './config.js';
7

8
const validVersions = [1];
4✔
9
const validTypes = ['agent', 'asset'];
4✔
10
const validRegistries = ['local', 'hyperswarm', 'TESS'];
4✔
11

12
let db = null;
4✔
13
let helia = null;
4✔
14
let ipfs = null;
4✔
15

16
export async function listRegistries() {
17
    return validRegistries;
4✔
18
}
19

20
export async function start(injectedDb) {
21
    if (!ipfs) {
428✔
22
        helia = await createHelia();
4✔
23
        ipfs = json(helia);
4✔
24
    }
25

26
    db = injectedDb;
428✔
27
}
28

29
export async function stop() {
30
    helia.stop();
428✔
31
    await db.stop();
428✔
32
}
33

34
export async function verifyDID(did) {
35
    const doc = await resolveDID(did, null, false, true);
4✔
36

37
    if (doc.didDocument.controller) {
4✔
38
        const controller = await resolveDID(doc.didDocument.controller);
2✔
39

40
        if (controller.mdip.registry === 'local' && doc.mdip.registry !== 'local') {
2!
41
            throw "Registry mistmatch";
×
42
        }
43
    }
44
}
45

46
export async function verifyDb(chatty = true) {
×
47
    const dids = await db.getAllKeys();
2✔
48
    let n = 0;
2✔
49
    let invalid = 0;
2✔
50

51
    for (const did of dids) {
2✔
52
        n += 1;
4✔
53
        try {
4✔
54
            await verifyDID(did);
4✔
55
            if (chatty) {
4!
NEW
56
                console.log(`${n} ${did} OK`);
×
57
            }
58
        }
59
        catch (error) {
NEW
60
            if (chatty) {
×
NEW
61
                console.log(`${n} ${did} ${error}`);
×
62
            }
63
            invalid += 1;
×
64
            await db.deleteEvents(did);
×
65
        }
66
    }
67

68
    return invalid;
2✔
69
}
70

71
// For testing purposes
72
export async function resetDb() {
73
    await db.resetDb();
10✔
74
}
75

76
export async function anchorSeed(seed) {
77
    const cid = await ipfs.add(JSON.parse(canonicalize(seed)));
3,942✔
78
    const did = `${config.didPrefix}:${cid.toString(base58btc)}`;
3,942✔
79
    return did;
3,942✔
80
}
81

82
async function verifyCreateAgent(operation) {
83
    if (!operation.signature) {
490!
84
        throw "Invalid operation";
×
85
    }
86

87
    if (!operation.publicJwk) {
490!
88
        throw "Invalid operation";
×
89
    }
90

91
    const operationCopy = JSON.parse(JSON.stringify(operation));
490✔
92
    delete operationCopy.signature;
490✔
93

94
    const msgHash = cipher.hashJSON(operationCopy);
490✔
95
    const isValid = cipher.verifySig(msgHash, operation.signature.value, operation.publicJwk);
490✔
96

97
    return isValid;
490✔
98
}
99

100
async function verifyCreateAsset(operation) {
101
    if (operation.controller !== operation.signature.signer) {
456!
102
        throw "Invalid operation";
×
103
    }
104

105
    const doc = await resolveDID(operation.signature.signer, operation.signature.signed);
456✔
106

107
    if (doc.mdip.registry === 'local' && operation.mdip.registry !== 'local') {
456!
108
        throw "Invalid operation";
×
109
    }
110

111
    const operationCopy = JSON.parse(JSON.stringify(operation));
456✔
112
    delete operationCopy.signature;
456✔
113
    const msgHash = cipher.hashJSON(operationCopy);
456✔
114
    // TBD select the right key here, not just the first one
115
    const publicJwk = doc.didDocument.verificationMethod[0].publicKeyJwk;
456✔
116
    const isValid = cipher.verifySig(msgHash, operation.signature.value, publicJwk);
456✔
117

118
    return isValid;
456✔
119
}
120

121
async function verifyCreate(operation) {
122
    if (operation?.type !== "create") {
950!
123
        throw "Invalid operation";
×
124
    }
125

126
    if (!operation.created) {
950!
127
        // TBD ensure valid timestamp format
128
        throw "Invalid operation";
×
129
    }
130

131
    if (!operation.mdip) {
950!
132
        throw "Invalid operation";
×
133
    }
134

135
    if (!validVersions.includes(operation.mdip.version)) {
950✔
136
        throw `Valid versions include: ${validVersions}`;
2✔
137
    }
138

139
    if (!validTypes.includes(operation.mdip.type)) {
948!
140
        throw `Valid types include: ${validTypes}`;
×
141
    }
142

143
    if (!validRegistries.includes(operation.mdip.registry)) {
948✔
144
        throw `Valid registries include: ${validRegistries}`;
2✔
145
    }
146

147
    if (operation.mdip.type === 'agent') {
946✔
148
        return verifyCreateAgent(operation);
490✔
149
    }
150

151
    if (operation.mdip.type === 'asset') {
456!
152
        return verifyCreateAsset(operation);
456✔
153
    }
154

155
    throw "Invalid operation";
×
156
}
157

158
export async function createDID(operation) {
159
    const valid = await verifyCreate(operation);
938✔
160

161
    if (valid) {
934!
162
        const did = await anchorSeed(operation);
934✔
163
        const ops = await exportDID(did);
934✔
164

165
        // Check to see if we already have this DID in the db
166
        if (ops.length === 0) {
934✔
167
            await db.addEvent(did, {
930✔
168
                registry: 'local',
169
                time: operation.created,
170
                ordinal: 0,
171
                operation: operation
172
            });
173

174
            // Create events are distributed only by hyperswarm
175
            // (because the DID's registry specifies where to look for *update* events)
176
            // Don't distribute local DIDs
177
            if (operation.mdip.registry !== 'local') {
930✔
178
                await db.queueOperation('hyperswarm', operation);
872✔
179
            }
180
        }
181

182
        return did;
934✔
183
    }
184
    else {
185
        throw "Invalid operation";
×
186
    }
187
}
188

189
async function generateDoc(anchor, asofTime) {
190
    try {
2,964✔
191
        if (!anchor?.mdip) {
2,964!
192
            return {};
×
193
        }
194

195
        if (asofTime && new Date(anchor.created) < new Date(asofTime)) {
2,964!
196
            return {}; // DID was not yet created
×
197
        }
198

199
        if (!validVersions.includes(anchor.mdip.version)) {
2,964!
200
            return {};
×
201
        }
202

203
        if (!validTypes.includes(anchor.mdip.type)) {
2,964!
204
            return {};
×
205
        }
206

207
        if (!validRegistries.includes(anchor.mdip.registry)) {
2,964!
208
            return {};
×
209
        }
210

211
        const did = await anchorSeed(anchor);
2,964✔
212

213
        if (anchor.mdip.type === 'agent') {
2,964✔
214
            // TBD support different key types?
215
            const doc = {
1,842✔
216
                "@context": "https://w3id.org/did-resolution/v1",
217
                "didDocument": {
218
                    "@context": ["https://www.w3.org/ns/did/v1"],
219
                    "id": did,
220
                    "verificationMethod": [
221
                        {
222
                            "id": "#key-1",
223
                            "controller": did,
224
                            "type": "EcdsaSecp256k1VerificationKey2019",
225
                            "publicKeyJwk": anchor.publicJwk,
226
                        }
227
                    ],
228
                    "authentication": [
229
                        "#key-1"
230
                    ],
231
                },
232
                "didDocumentMetadata": {
233
                    "created": anchor.created,
234
                },
235
                "didDocumentData": {},
236
                "mdip": anchor.mdip,
237
            };
238

239
            return doc;
1,842✔
240
        }
241

242
        if (anchor.mdip.type === 'asset') {
1,122!
243
            const doc = {
1,122✔
244
                "@context": "https://w3id.org/did-resolution/v1",
245
                "didDocument": {
246
                    "@context": ["https://www.w3.org/ns/did/v1"],
247
                    "id": did,
248
                    "controller": anchor.controller,
249
                },
250
                "didDocumentMetadata": {
251
                    "created": anchor.created,
252
                },
253
                "didDocumentData": anchor.data,
254
                "mdip": anchor.mdip,
255
            };
256

257
            return doc;
1,122✔
258
        }
259
    }
260
    catch (error) {
261
        // console.error(error);
262
    }
263

264
    return {}; // TBD unknown type error
×
265
}
266

267
async function verifyUpdate(operation, doc) {
268

269
    if (!doc?.didDocument) {
2,276!
270
        return false;
×
271
    }
272

273
    if (doc.didDocument.controller) {
2,276✔
274
        const controllerDoc = await resolveDID(doc.didDocument.controller, operation.signature.signed);
374✔
275
        return verifyUpdate(operation, controllerDoc);
374✔
276
    }
277

278
    if (!doc.didDocument.verificationMethod) {
1,902✔
279
        return false;
2✔
280
    }
281

282
    const jsonCopy = JSON.parse(JSON.stringify(operation));
1,900✔
283

284
    const signature = jsonCopy.signature;
1,900✔
285
    delete jsonCopy.signature;
1,900✔
286
    const msgHash = cipher.hashJSON(jsonCopy);
1,900✔
287

288
    if (signature.hash && signature.hash !== msgHash) {
1,900✔
289
        return false;
2✔
290
    }
291

292
    // TBD get the right signature, not just the first one
293
    const publicJwk = doc.didDocument.verificationMethod[0].publicKeyJwk;
1,898✔
294
    const isValid = cipher.verifySig(msgHash, signature.value, publicJwk);
1,898✔
295

296
    return isValid;
1,898✔
297
}
298

299
export async function resolveDID(did, asOfTime = null, confirm = false, verify = false) {
3,938✔
300
    const events = await db.getEvents(did);
2,988✔
301

302
    if (events.length === 0) {
2,988✔
303
        throw "Invalid DID";
24✔
304
    }
305

306
    const anchor = events[0];
2,964✔
307
    let doc = await generateDoc(anchor.operation);
2,964✔
308
    let mdip = doc?.mdip;
2,964✔
309

310
    if (!mdip) {
2,964!
311
        throw "Invalid DID";
×
312
    }
313

314
    if (asOfTime && new Date(mdip.created) > new Date(asOfTime)) {
2,964!
315
        // TBD What to return if DID was created after specified time?
316
    }
317

318
    let version = 1; // initial version is version 1 by definition
2,964✔
319
    let confirmed = true; // create event is always confirmed by definition
2,964✔
320

321
    doc.didDocumentMetadata.version = version;
2,964✔
322
    doc.didDocumentMetadata.confirmed = confirmed;
2,964✔
323

324
    for (const { time, operation, registry } of events) {
2,964✔
325
        if (operation.type === 'create') {
4,620✔
326
            continue;
2,968✔
327
        }
328

329
        if (asOfTime && new Date(time) > new Date(asOfTime)) {
1,652✔
330
            break;
58✔
331
        }
332

333
        confirmed = confirmed && mdip.registry === registry;
1,594✔
334

335
        if (confirm && !confirmed) {
1,594✔
336
            break;
2✔
337
        }
338

339
        const hash = cipher.hashJSON(doc);
1,592✔
340

341
        if (hash !== operation.prev) {
1,592!
342
            // hash mismatch
343
            // if (verify) {
344
            //     throw "Invalid hash";
345
            // }
346
            // !!! This fails on key rotation #3 (!?), disabling for now
347
            // continue;
348
        }
349

350
        const valid = await verifyUpdate(operation, doc);
1,592✔
351

352
        if (!valid) {
1,592!
353
            if (verify) {
×
354
                throw "Invalid update";
×
355
            }
356

357
            continue;
×
358
        }
359

360
        if (operation.type === 'update') {
1,592✔
361
            // Increment version
362
            version += 1;
1,558✔
363

364
            // Maintain mdip metadata across versions
365
            mdip = doc.mdip;
1,558✔
366

367
            // TBD if registry change in operation.doc.didDocumentMetadata.mdip,
368
            // fetch updates from new registry and search for same operation
369
            doc = operation.doc;
1,558✔
370
            doc.didDocumentMetadata.updated = time;
1,558✔
371
            doc.didDocumentMetadata.version = version;
1,558✔
372
            doc.didDocumentMetadata.confirmed = confirmed;
1,558✔
373
            doc.mdip = mdip;
1,558✔
374
        }
375
        else if (operation.type === 'delete') {
34!
376
            doc.didDocument = {};
34✔
377
            doc.didDocumentData = {};
34✔
378
            doc.didDocumentMetadata.deactivated = true;
34✔
379
            doc.didDocumentMetadata.deleted = time;
34✔
380
            doc.didDocumentMetadata.confirmed = confirmed;
34✔
381
        }
382
        else {
383
            if (verify) {
×
384
                throw "Invalid operation";
×
385
            }
386

387
            console.error(`unknown type ${operation.type}`);
×
388
        }
389
    }
390

391
    return doc;
2,964✔
392
}
393

394
export async function updateDID(operation) {
395
    try {
266✔
396
        const doc = await resolveDID(operation.did);
266✔
397
        const updateValid = await verifyUpdate(operation, doc);
266✔
398

399
        if (!updateValid) {
266✔
400
            return false;
4✔
401
        }
402

403
        const registry = doc.mdip.registry;
262✔
404

405
        await db.addEvent(operation.did, {
262✔
406
            registry: 'local',
407
            time: operation.signature.signed,
408
            ordinal: 0,
409
            operation: operation
410
        });
411

412
        if (registry === 'local') {
262✔
413
            return true;
52✔
414
        }
415

416
        await db.queueOperation(registry, operation);
210✔
417

418
        if (registry !== 'hyperswarm') {
210✔
419
            await db.queueOperation('hyperswarm', operation);
206✔
420
        }
421

422
        return true;
210✔
423
    }
424
    catch (error) {
425
        console.error(error);
×
426
        return false;
×
427
    }
428
}
429

430
export async function deleteDID(operation) {
431
    return updateDID(operation);
24✔
432
}
433

434
export async function getDIDs() {
435
    const keys = await db.getAllKeys();
2✔
436
    const dids = keys.map(key => `${config.didPrefix}:${key}`);
4✔
437
    return dids;
2✔
438
}
439

440
export async function exportDID(did) {
441
    return await db.getEvents(did);
1,044✔
442
}
443

444
export async function exportDIDs(dids) {
445
    const batch = [];
8✔
446

447
    for (const did of dids) {
8✔
448
        batch.push(await db.getEvents(did));
10✔
449
    }
450

451
    return batch;
8✔
452
}
453

454
export async function removeDIDs(dids) {
455
    if (!Array.isArray(dids)) {
8✔
456
        throw "Invalid array";
2✔
457
    }
458

459
    for (const did of dids) {
6✔
460
        await db.deleteEvents(did);
4✔
461
    }
462

463
    return true;
6✔
464
}
465

466
async function importCreateEvent(event) {
467
    try {
12✔
468
        const valid = await verifyCreate(event.operation);
12✔
469

470
        if (valid) {
12!
471
            const did = await anchorSeed(event.operation);
12✔
472
            await db.addEvent(did, event);
12✔
473
            return true;
12✔
474
        }
475

476
        return false;
×
477
    }
478
    catch {
479
        return false;
×
480
    }
481
}
482

483
async function importUpdateEvent(event) {
484
    try {
44✔
485
        const did = event.operation.did;
44✔
486
        const doc = await resolveDID(did);
44✔
487
        const updateValid = await verifyUpdate(event.operation, doc);
44✔
488

489
        if (!updateValid) {
44!
490
            return false;
×
491
        }
492

493
        await db.addEvent(did, event);
44✔
494
        return true;
44✔
495
    }
496
    catch (error) {
497
        //console.error(error);
498
        return false;
×
499
    }
500
}
501

502
export async function importEvent(event) {
503

504
    if (!event.registry || !event.time || !event.operation) {
86✔
505
        throw "Invalid import";
6✔
506
    }
507

508
    let did;
509

510
    try {
80✔
511
        if (event.operation.type === 'create') {
80✔
512
            did = await anchorSeed(event.operation);
26✔
513
        }
514
        else {
515
            did = event.operation.did;
54✔
516
        }
517

518
        if (!did) {
80!
519
            throw "Invalid operation";
×
520
        }
521
    }
522
    catch {
523
        throw "Invalid operation";
×
524
    }
525

526
    const current = await exportDID(did);
80✔
527

528
    if (current.length === 0) {
80✔
529
        const ok = await importCreateEvent(event);
12✔
530

531
        if (!ok) {
12!
532
            throw "Invalid operation";
×
533
        }
534

535
        return true;
12✔
536
    }
537

538
    const create = current[0];
68✔
539
    const registry = create.operation.mdip.registry;
68✔
540
    const match = current.find(item => item.operation.signature.value === event.operation.signature.value);
258✔
541

542
    if (match) {
68✔
543
        if (match.registry === registry) {
24✔
544
            // Don't update if this op has already been validated on its native registry
545
            return false;
12✔
546
        }
547

548
        if (event.registry === registry) {
12!
549
            // If this import is on the native registry, replace the current one
550
            const index = current.indexOf(match);
12✔
551
            current[index] = event;
12✔
552

553
            db.setEvents(did, current);
12✔
554
            return true;
12✔
555
        }
556

557
        return false;
×
558
    }
559

560
    const ok = await importUpdateEvent(event);
44✔
561

562
    if (!ok) {
44!
563
        throw "Invalid operation";
×
564
    }
565

566
    return true;
44✔
567
}
568

569
export async function importBatch(batch) {
570
    if (!batch || !Array.isArray(batch) || batch.length < 1) {
34✔
571
        throw "Invalid import";
6✔
572
    }
573

574
    let verified = 0;
28✔
575
    let updated = 0;
28✔
576
    let failed = 0;
28✔
577

578
    for (const event of batch) {
28✔
579
        //console.time('importEvent');
580
        try {
86✔
581
            const imported = await importEvent(event);
86✔
582

583
            if (imported) {
80✔
584
                updated += 1;
68✔
585
            }
586
            else {
587
                verified += 1;
12✔
588
            }
589
        }
590
        catch (error) {
591
            //console.error(error);
592
            failed += 1;
6✔
593
        }
594
        //console.timeEnd('importEvent');
595
    }
596

597
    return {
28✔
598
        verified: verified,
599
        updated: updated,
600
        failed: failed,
601
    };
602
}
603

604
export async function getQueue(registry) {
605
    if (!validRegistries.includes(registry)) {
16✔
606
        throw `Invalid registry`;
2✔
607
    }
608

609
    const queue = db.getQueue(registry);
14✔
610
    return queue;
14✔
611
}
612

613
export async function clearQueue(registry, events) {
614
    if (!validRegistries.includes(registry)) {
12✔
615
        throw `Invalid registry`;
2✔
616
    }
617

618
    const ok = db.clearQueue(registry, events);
10✔
619
    return ok;
10✔
620
}
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