• 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

94.1
/packages/keymaster/src/keymaster-lib.js
1
import { JSONSchemaFaker } from "json-schema-faker";
2
import * as exceptions from '@mdip/exceptions';
3

4
let gatekeeper = null;
2✔
5
let db = null;
2✔
6
let cipher = null;
2✔
7

8
const defaultRegistry = 'TESS';
2✔
9
const ephemeralRegistry = 'hyperswarm';
2✔
10

11
export async function start(gatekeeperDep, dbDep, cipherDep) {
12
    if (!gatekeeperDep?.createDID || !dbDep?.loadWallet || !cipherDep?.verifySig) {
362!
13
        throw new Error(exceptions.INVALID_PARAMETER);
×
14
    }
15

16
    gatekeeper = gatekeeperDep;
362✔
17
    db = dbDep;
362✔
18
    cipher = cipherDep;
362✔
19
}
20

21
export async function stop() {
22
    await gatekeeper.stop();
362✔
23
}
24

25
export async function listRegistries() {
26
    return gatekeeper.listRegistries();
2✔
27
}
28

29
export function loadWallet() {
30
    return db.loadWallet() || newWallet();
5,336✔
31
}
32

33
export function saveWallet(wallet) {
34
    // TBD validate wallet before saving
35
    return db.saveWallet(wallet, true);
1,284✔
36
}
37

38
export function newWallet(mnemonic, overwrite = false) {
173✔
39
    let wallet;
40

41
    try {
360✔
42
        if (!mnemonic) {
360✔
43
            mnemonic = cipher.generateMnemonic();
346✔
44
        }
45
        const hdkey = cipher.generateHDKey(mnemonic);
360✔
46
        const keypair = cipher.generateJwk(hdkey.privateKey);
358✔
47
        const backup = cipher.encryptMessage(keypair.publicJwk, keypair.privateJwk, mnemonic);
358✔
48

49
        wallet = {
358✔
50
            seed: {
51
                mnemonic: backup,
52
                hdkey: hdkey.toJSON(),
53
            },
54
            counter: 0,
55
            ids: {},
56
        }
57
    }
58
    catch (error) {
59
        throw new Error(exceptions.INVALID_PARAMETER);
2✔
60
    }
61

62
    if (!db.saveWallet(wallet, overwrite)) {
358✔
63
        throw new Error(exceptions.UPDATE_FAILED);
2✔
64
    }
65

66
    return wallet;
356✔
67
}
68

69
export function decryptMnemonic() {
70
    const wallet = loadWallet();
14✔
71
    const keypair = hdKeyPair();
14✔
72

73
    return cipher.decryptMessage(keypair.publicJwk, keypair.privateJwk, wallet.seed.mnemonic);
14✔
74
}
75

76
export async function checkWallet() {
77
    const wallet = loadWallet();
10✔
78

79
    let checked = 0;
10✔
80
    let invalid = 0;
10✔
81
    let deleted = 0;
10✔
82

83
    // Validate keys
84
    await resolveSeedBank();
10✔
85

86
    for (const name of Object.keys(wallet.ids)) {
10✔
87
        try {
14✔
88
            const doc = await resolveDID(wallet.ids[name].did);
14✔
89

90
            if (doc.didDocumentMetadata.deactivated) {
12✔
91
                deleted += 1;
2✔
92
            }
93
        }
94
        catch (error) {
95
            invalid += 1;
2✔
96
        }
97

98
        checked += 1;
14✔
99
    }
100

101
    for (const id of Object.values(wallet.ids)) {
10✔
102
        if (id.owned) {
14✔
103
            for (const did of id.owned) {
6✔
104
                try {
18✔
105
                    const doc = await resolveDID(did);
18✔
106

107
                    if (doc.didDocumentMetadata.deactivated) {
16✔
108
                        deleted += 1;
4✔
109
                    }
110
                }
111
                catch (error) {
112
                    invalid += 1;
2✔
113
                }
114

115
                checked += 1;
18✔
116
            }
117
        }
118

119
        if (id.held) {
14✔
120
            for (const did of id.held) {
2✔
121
                try {
8✔
122
                    const doc = await resolveDID(did);
8✔
123

124
                    if (doc.didDocumentMetadata.deactivated) {
8✔
125
                        deleted += 1;
4✔
126
                    }
127
                }
128
                catch (error) {
129
                    invalid += 1;
×
130
                }
131

132
                checked += 1;
8✔
133
            }
134
        }
135
    }
136

137
    if (wallet.names) {
10✔
138
        for (const name of Object.keys(wallet.names)) {
4✔
139
            try {
6✔
140
                const doc = await resolveDID(wallet.names[name]);
6✔
141

142
                if (doc.didDocumentMetadata.deactivated) {
4!
143
                    deleted += 1;
4✔
144
                }
145
            }
146
            catch (error) {
147
                invalid += 1;
2✔
148
            }
149

150
            checked += 1;
6✔
151
        }
152
    }
153

154
    return { checked, invalid, deleted };
10✔
155
}
156

157
export async function fixWallet() {
158
    const wallet = loadWallet();
10✔
159
    let idsRemoved = 0;
10✔
160
    let ownedRemoved = 0;
10✔
161
    let heldRemoved = 0;
10✔
162
    let namesRemoved = 0;
10✔
163

164
    for (const name of Object.keys(wallet.ids)) {
10✔
165
        let remove = false;
14✔
166

167
        try {
14✔
168
            const doc = await resolveDID(wallet.ids[name].did);
14✔
169

170
            if (doc.didDocumentMetadata.deactivated) {
12✔
171
                remove = true;
2✔
172
            }
173
        }
174
        catch (error) {
175
            remove = true;
2✔
176
        }
177

178
        if (remove) {
14✔
179
            delete wallet.ids[name];
4✔
180
            idsRemoved += 1;
4✔
181
        }
182
    }
183

184
    for (const id of Object.values(wallet.ids)) {
10✔
185
        if (id.owned) {
10✔
186
            for (let i = 0; i < id.owned.length; i++) {
4✔
187
                let remove = false;
16✔
188

189
                try {
16✔
190
                    const doc = await resolveDID(id.owned[i]);
16✔
191

192
                    if (doc.didDocumentMetadata.deactivated) {
16✔
193
                        remove = true;
4✔
194
                    }
195
                }
196
                catch {
197
                    remove = true;
×
198
                }
199

200
                if (remove) {
16✔
201
                    id.owned.splice(i, 1);
4✔
202
                    i--; // Decrement index to account for the removed item
4✔
203
                    ownedRemoved += 1;
4✔
204
                }
205
            }
206
        }
207

208
        if (id.held) {
10✔
209
            for (let i = 0; i < id.held.length; i++) {
2✔
210
                let remove = false;
8✔
211

212
                try {
8✔
213
                    const doc = await resolveDID(id.held[i]);
8✔
214

215
                    if (doc.didDocumentMetadata.deactivated) {
8✔
216
                        remove = true;
4✔
217
                    }
218
                }
219
                catch {
220
                    remove = true;
×
221
                }
222

223
                if (remove) {
8✔
224
                    id.held.splice(i, 1);
4✔
225
                    i--; // Decrement index to account for the removed item
4✔
226
                    heldRemoved += 1;
4✔
227
                }
228
            }
229
        }
230
    }
231

232
    if (wallet.names) {
10✔
233
        for (const name of Object.keys(wallet.names)) {
4✔
234
            let remove = false;
6✔
235

236
            try {
6✔
237
                const doc = await resolveDID(wallet.names[name]);
6✔
238

239
                if (doc.didDocumentMetadata.deactivated) {
4!
240
                    remove = true;
4✔
241
                }
242
            }
243
            catch (error) {
244
                remove = true;
2✔
245
            }
246

247
            if (remove) {
6!
248
                delete wallet.names[name];
6✔
249
                namesRemoved += 1;
6✔
250
            }
251
        }
252
    }
253

254
    saveWallet(wallet);
10✔
255

256
    return { idsRemoved, ownedRemoved, heldRemoved, namesRemoved };
10✔
257
}
258

259
export async function resolveSeedBank() {
260
    const keypair = hdKeyPair();
28✔
261

262
    const operation = {
28✔
263
        type: "create",
264
        created: new Date(0).toISOString(),
265
        mdip: {
266
            version: 1,
267
            type: "agent",
268
            registry: defaultRegistry,
269
        },
270
        publicJwk: keypair.publicJwk,
271
    };
272

273
    const msgHash = cipher.hashJSON(operation);
28✔
274
    const signature = cipher.signHash(msgHash, keypair.privateJwk);
28✔
275
    const signed = {
28✔
276
        ...operation,
277
        signature: {
278
            signed: new Date(0).toISOString(),
279
            hash: msgHash,
280
            value: signature
281
        }
282
    }
283
    const did = await gatekeeper.createDID(signed);
28✔
284
    return await gatekeeper.resolveDID(did);
28✔
285
}
286

287
async function updateSeedBank(doc) {
288
    const keypair = hdKeyPair();
8✔
289
    const did = doc.didDocument.id;
8✔
290
    const current = await gatekeeper.resolveDID(did);
8✔
291
    const prev = cipher.hashJSON(current);
8✔
292

293
    const operation = {
8✔
294
        type: "update",
295
        did: did,
296
        doc: doc,
297
        prev: prev,
298
    };
299

300
    const msgHash = cipher.hashJSON(operation);
8✔
301
    const signature = cipher.signHash(msgHash, keypair.privateJwk);
8✔
302
    const signed = {
8✔
303
        ...operation,
304
        signature: {
305
            signer: did,
306
            signed: new Date().toISOString(),
307
            hash: msgHash,
308
            value: signature,
309
        }
310
    };
311

312
    return await gatekeeper.updateDID(signed);
8✔
313
}
314

315
export async function backupWallet(registry = defaultRegistry) {
4✔
316
    const wallet = loadWallet();
8✔
317
    const keypair = hdKeyPair();
8✔
318
    const seedBank = await resolveSeedBank();
8✔
319
    const msg = JSON.stringify(wallet);
8✔
320
    const backup = cipher.encryptMessage(keypair.publicJwk, keypair.privateJwk, msg);
8✔
321
    const operation = {
8✔
322
        type: "create",
323
        created: new Date().toISOString(),
324
        mdip: {
325
            version: 1,
326
            type: "asset",
327
            registry: registry,
328
        },
329
        controller: seedBank.didDocument.id,
330
        data: { backup: backup },
331
    };
332
    const msgHash = cipher.hashJSON(operation);
8✔
333
    const signature = cipher.signHash(msgHash, keypair.privateJwk);
8✔
334
    const signed = {
8✔
335
        ...operation,
336
        signature: {
337
            signer: seedBank.didDocument.id,
338
            signed: new Date().toISOString(),
339
            hash: msgHash,
340
            value: signature,
341
        }
342
    };
343
    const backupDID = await gatekeeper.createDID(signed);
8✔
344

345
    seedBank.didDocumentData.wallet = backupDID;
8✔
346
    await updateSeedBank(seedBank);
8✔
347

348
    return backupDID;
8✔
349
}
350

351
export async function recoverWallet(did) {
352
    try {
8✔
353
        if (!did) {
8✔
354
            const seedBank = await resolveSeedBank();
4✔
355
            did = seedBank.didDocumentData.wallet;
4✔
356
        }
357

358
        const keypair = hdKeyPair();
8✔
359
        const data = await resolveAsset(did);
8✔
360
        const backup = cipher.decryptMessage(keypair.publicJwk, keypair.privateJwk, data.backup);
6✔
361
        const wallet = JSON.parse(backup);
4✔
362

363
        saveWallet(wallet);
4✔
364
        return wallet;
4✔
365
    }
366
    catch (error) {
367
        // If we can't recover the wallet, just return the current one
368
        return loadWallet();
4✔
369
    }
370
}
371

372
export function listIds() {
373
    const wallet = loadWallet();
2✔
374
    return Object.keys(wallet.ids);
2✔
375
}
376

377
export function getCurrentId() {
378
    const wallet = loadWallet();
4✔
379
    return wallet.current;
4✔
380
}
381

382
export function setCurrentId(name) {
383
    const wallet = loadWallet();
168✔
384
    if (Object.keys(wallet.ids).includes(name)) {
168✔
385
        wallet.current = name;
164✔
386
        saveWallet(wallet);
164✔
387
    }
388
    else {
389
        throw new Error(exceptions.UNKNOWN_ID);
4✔
390
    }
391
}
392

393
function fetchId(id) {
394
    const wallet = loadWallet();
2,744✔
395
    let idInfo = null;
2,744✔
396

397
    if (id) {
2,744✔
398
        if (id.startsWith('did')) {
364✔
399
            for (const name of Object.keys(wallet.ids)) {
342✔
400
                const info = wallet.ids[name];
396✔
401

402
                if (info.did === id) {
396✔
403
                    idInfo = info;
340✔
404
                    break;
340✔
405
                }
406
            }
407
        }
408
        else {
409
            idInfo = wallet.ids[id];
22✔
410
        }
411
    }
412
    else {
413
        idInfo = wallet.ids[wallet.current];
2,380✔
414

415
        if (!idInfo) {
2,380✔
416
            throw new Error(exceptions.NO_CURRENT_ID);
6✔
417
        }
418
    }
419

420
    if (!idInfo) {
2,738✔
421
        throw new Error(exceptions.UNKNOWN_ID);
4✔
422
    }
423

424
    return idInfo;
2,734✔
425
}
426

427
function hdKeyPair() {
428
    const wallet = loadWallet();
78✔
429
    const hdkey = cipher.generateHDKeyJSON(wallet.seed.hdkey);
78✔
430

431
    return cipher.generateJwk(hdkey.privateKey);
78✔
432
}
433

434
async function fetchKeyPair(name = null) {
74✔
435
    const wallet = loadWallet();
848✔
436
    const id = fetchId(name);
848✔
437
    const hdkey = cipher.generateHDKeyJSON(wallet.seed.hdkey);
848✔
438
    const doc = await resolveDID(id.did, { confirm: true });
848✔
439
    const confirmedPublicKeyJwk = doc.didDocument.verificationMethod[0].publicKeyJwk;
848✔
440

441
    for (let i = id.index; i >= 0; i--) {
848✔
442
        const path = `m/44'/0'/${id.account}'/0/${i}`;
852✔
443
        const didkey = hdkey.derive(path);
852✔
444
        const keypair = cipher.generateJwk(didkey.privateKey);
852✔
445

446
        if (keypair.publicJwk.x === confirmedPublicKeyJwk.x &&
852✔
447
            keypair.publicJwk.y === confirmedPublicKeyJwk.y
448
        ) {
449
            return keypair;
848✔
450
        }
451
    }
452

453
    return null;
×
454
}
455

456
export async function createAsset(data, options = {}) {
44✔
457
    let { registry, controller, ephemeral } = options;
452✔
458

459
    if (!registry) {
452✔
460
        registry = defaultRegistry;
314✔
461
    }
462

463
    function isEmpty(data) {
464
        return (
452✔
465
            !data ||
896✔
466
            (Array.isArray(data) && data.length === 0) ||
467
            (typeof data === 'object' && Object.keys(data).length === 0)
468
        );
469
    }
470

471
    if (isEmpty(data)) {
452✔
472
        throw new Error(exceptions.INVALID_PARAMETER);
8✔
473
    }
474

475
    const id = fetchId(controller);
444✔
476

477
    const operation = {
442✔
478
        type: "create",
479
        created: new Date().toISOString(),
480
        mdip: {
481
            version: 1,
482
            type: "asset",
483
            registry,
484
            ephemeral
485
        },
486
        controller: id.did,
487
        data,
488
    };
489

490
    const signed = await addSignature(operation, controller);
442✔
491
    const did = await gatekeeper.createDID(signed);
442✔
492

493
    // Keep assets that will be garbage-collected out of the owned list
494
    if (registry !== 'hyperswarm') {
442✔
495
        addToOwned(did);
426✔
496
    }
497

498
    return did;
442✔
499
}
500

501
export async function encryptMessage(msg, receiver, options = {}) {
6✔
502
    let { encryptForSender } = options;
146✔
503

504
    if (typeof encryptForSender === 'undefined') {
146✔
505
        encryptForSender = true;
142✔
506
    }
507

508
    const id = fetchId();
146✔
509
    const senderKeypair = await fetchKeyPair();
146✔
510
    const doc = await resolveDID(receiver, { confirm: true });
146✔
511
    const receivePublicJwk = doc.didDocument.verificationMethod[0].publicKeyJwk;
146✔
512
    const cipher_sender = encryptForSender ? cipher.encryptMessage(senderKeypair.publicJwk, senderKeypair.privateJwk, msg) : null;
146!
513
    const cipher_receiver = cipher.encryptMessage(receivePublicJwk, senderKeypair.privateJwk, msg);
146✔
514
    const msgHash = cipher.hashMessage(msg);
146✔
515

516
    return await createAsset({
146✔
517
        encrypted: {
518
            sender: id.did,
519
            created: new Date().toISOString(),
520
            cipher_hash: msgHash,
521
            cipher_sender: cipher_sender,
522
            cipher_receiver: cipher_receiver,
523
        }
524
    }, options);
525
}
526

527
export async function decryptMessage(did) {
528
    const wallet = loadWallet();
240✔
529
    const id = fetchId();
240✔
530
    const asset = await resolveAsset(did);
240✔
531

532
    if (!asset || (!asset.encrypted && !asset.cipher_hash)) {
240✔
533
        throw new Error(exceptions.INVALID_PARAMETER);
12✔
534
    }
535

536
    const crypt = asset.encrypted ? asset.encrypted : asset;
228✔
537
    const doc = await resolveDID(crypt.sender, { confirm: true, atTime: crypt.created });
228✔
538
    const senderPublicJwk = doc.didDocument.verificationMethod[0].publicKeyJwk;
228✔
539
    const hdkey = cipher.generateHDKeyJSON(wallet.seed.hdkey);
228✔
540
    const ciphertext = (crypt.sender === id.did && crypt.cipher_sender) ? crypt.cipher_sender : crypt.cipher_receiver;
228✔
541

542
    // Try all private keys for this ID, starting with the most recent and working backward
543
    let index = id.index;
228✔
544
    while (index >= 0) {
228✔
545
        const path = `m/44'/0'/${id.account}'/0/${index}`;
280✔
546
        const didkey = hdkey.derive(path);
280✔
547
        const receiverKeypair = cipher.generateJwk(didkey.privateKey);
280✔
548
        try {
280✔
549
            return cipher.decryptMessage(senderPublicJwk, receiverKeypair.privateJwk, ciphertext);
280✔
550
        }
551
        catch (error) {
552
            index -= 1;
54✔
553
        }
554
    }
555

556
    throw new Error('Cannot decrypt');
2✔
557
}
558

559
export async function encryptJSON(json, did, options = {}) {
3✔
560
    const plaintext = JSON.stringify(json);
112✔
561
    return encryptMessage(plaintext, did, options);
112✔
562
}
563

564
export async function decryptJSON(did) {
565
    const plaintext = await decryptMessage(did);
206✔
566

567
    try {
192✔
568
        return JSON.parse(plaintext);
192✔
569
    }
570
    catch (error) {
571
        throw new Error(exceptions.INVALID_PARAMETER);
2✔
572
    }
573
}
574

575
export async function addSignature(obj, controller = null) {
266✔
576
    // Fetches current ID if name is missing
577
    const id = fetchId(controller);
704✔
578
    const keypair = await fetchKeyPair(controller);
700✔
579

580
    try {
700✔
581
        const msgHash = cipher.hashJSON(obj);
700✔
582
        const signature = cipher.signHash(msgHash, keypair.privateJwk);
698✔
583

584
        return {
698✔
585
            ...obj,
586
            signature: {
587
                signer: id.did,
588
                signed: new Date().toISOString(),
589
                hash: msgHash,
590
                value: signature,
591
            }
592
        };
593
    }
594
    catch (error) {
595
        throw new Error(exceptions.INVALID_PARAMETER);
2✔
596
    }
597
}
598

599
export async function verifySignature(obj) {
600
    if (!obj?.signature) {
40✔
601
        return false;
6✔
602
    }
603

604
    const jsonCopy = JSON.parse(JSON.stringify(obj));
34✔
605
    const signature = jsonCopy.signature;
34✔
606
    delete jsonCopy.signature;
34✔
607
    const msgHash = cipher.hashJSON(jsonCopy);
34✔
608

609
    if (signature.hash && signature.hash !== msgHash) {
34!
UNCOV
610
        return false;
×
611
    }
612

613
    const doc = await resolveDID(signature.signer, { atTime: signature.signed });
34✔
614

615
    // TBD get the right signature, not just the first one
616
    const publicJwk = doc.didDocument.verificationMethod[0].publicKeyJwk;
34✔
617

618
    try {
34✔
619
        return cipher.verifySig(msgHash, signature.value, publicJwk);
34✔
620
    }
621
    catch (error) {
622
        return false;
2✔
623
    }
624
}
625

626
export async function updateDID(did, doc) {
627
    const current = await resolveDID(did);
136✔
628
    const prev = cipher.hashJSON(current);
136✔
629

630
    const operation = {
136✔
631
        type: "update",
632
        did: did,
633
        doc: doc,
634
        prev: prev,
635
    };
636

637
    const controller = current.didDocument.controller || current.didDocument.id;
136✔
638
    const signed = await addSignature(operation, controller);
136✔
639
    return gatekeeper.updateDID(signed);
136✔
640
}
641

642
export async function revokeDID(did) {
643
    const current = await resolveDID(did);
28✔
644
    const prev = cipher.hashJSON(current);
28✔
645

646
    const operation = {
28✔
647
        type: "delete",
648
        did: did,
649
        prev: prev,
650
    };
651

652
    const controller = current.didDocument.controller || current.didDocument.id;
28✔
653
    const signed = await addSignature(operation, controller);
28✔
654
    const ok = gatekeeper.deleteDID(signed);
26✔
655

656
    if (ok && current.didDocument.controller) {
26✔
657
        removeFromOwned(did, current.didDocument.controller);
20✔
658
    }
659

660
    return ok;
26✔
661
}
662

663
function addToOwned(did) {
664
    const wallet = loadWallet();
512✔
665
    const id = wallet.ids[wallet.current];
512✔
666
    const owned = new Set(id.owned);
512✔
667

668
    owned.add(did);
512✔
669
    id.owned = Array.from(owned);
512✔
670

671
    saveWallet(wallet);
512✔
672
    return true;
512✔
673
}
674

675
function removeFromOwned(did, owner) {
676
    const wallet = loadWallet();
20✔
677
    const id = fetchId(owner);
20✔
678

679
    id.owned = id.owned.filter(item => item !== did);
60✔
680

681
    saveWallet(wallet);
20✔
682
    return true;
20✔
683
}
684

685
function addToHeld(did) {
686
    const wallet = loadWallet();
62✔
687
    const id = wallet.ids[wallet.current];
62✔
688
    const held = new Set(id.held);
62✔
689

690
    held.add(did);
62✔
691
    id.held = Array.from(held);
62✔
692

693
    saveWallet(wallet);
62✔
694
    return true;
62✔
695
}
696

697
function removeFromHeld(did) {
698
    const wallet = loadWallet();
6✔
699
    const id = wallet.ids[wallet.current];
6✔
700
    const held = new Set(id.held);
6✔
701

702
    if (held.delete(did)) {
6✔
703
        id.held = Array.from(held);
4✔
704
        saveWallet(wallet);
4✔
705
        return true;
4✔
706
    }
707

708
    return false;
2✔
709
}
710

711
export async function resolveDID(did, options = {}) {
680✔
712
    return await gatekeeper.resolveDID(lookupDID(did), options);
2,618✔
713
}
714

715
export async function resolveAsset(did) {
716
    const doc = await resolveDID(did);
480✔
717

718
    if (doc?.didDocumentMetadata && !doc.didDocumentMetadata.deactivated) {
472✔
719
        return doc.didDocumentData;
466✔
720
    }
721

722
    return null;
6✔
723
}
724

725
export async function createId(name, options = {}) {
201✔
726
    let { registry } = options;
422✔
727

728
    if (!registry) {
422✔
729
        registry = defaultRegistry;
406✔
730
    }
731
    
732
    const wallet = loadWallet();
422✔
733
    if (wallet.ids && Object.keys(wallet.ids).includes(name)) {
422✔
734
        throw new Error(exceptions.INVALID_PARAMETER);
2✔
735
    }
736

737
    const account = wallet.counter;
420✔
738
    const index = 0;
420✔
739
    const hdkey = cipher.generateHDKeyJSON(wallet.seed.hdkey);
420✔
740
    const path = `m/44'/0'/${account}'/0/${index}`;
420✔
741
    const didkey = hdkey.derive(path);
420✔
742
    const keypair = cipher.generateJwk(didkey.privateKey);
420✔
743

744
    const operation = {
420✔
745
        type: "create",
746
        created: new Date().toISOString(),
747
        mdip: {
748
            version: 1,
749
            type: "agent",
750
            registry: registry,
751
        },
752
        publicJwk: keypair.publicJwk,
753
    };
754

755
    const msgHash = cipher.hashJSON(operation);
420✔
756
    const signature = cipher.signHash(msgHash, keypair.privateJwk);
420✔
757
    const signed = {
420✔
758
        ...operation,
759
        signature: {
760
            signed: new Date().toISOString(),
761
            hash: msgHash,
762
            value: signature
763
        }
764
    }
765
    const did = await gatekeeper.createDID(signed);
420✔
766

767
    const newId = {
420✔
768
        did: did,
769
        account: account,
770
        index: index,
771
    };
772

773
    wallet.ids[name] = newId;
420✔
774
    wallet.counter += 1;
420✔
775
    wallet.current = name;
420✔
776
    saveWallet(wallet);
420✔
777

778
    return did;
420✔
779
}
780

781
export function removeId(name) {
782
    const wallet = loadWallet();
6✔
783
    let ids = Object.keys(wallet.ids);
6✔
784

785
    if (ids.includes(name)) {
6✔
786
        delete wallet.ids[name];
4✔
787

788
        if (wallet.current === name) {
4✔
789
            ids = Object.keys(wallet.ids);
2✔
790
            wallet.current = ids.length > 0 ? ids[0] : '';
2!
791
        }
792

793
        saveWallet(wallet);
4✔
794
        return true;
4✔
795
    }
796
    else {
797
        throw new Error(exceptions.UNKNOWN_ID);
2✔
798
    }
799
}
800

801
export async function resolveId(name) {
802
    const id = fetchId(name);
6✔
803
    return resolveDID(id.did);
6✔
804
}
805

806
export async function backupId(controller = null) {
3✔
807
    // Backs up current ID if name is missing
808
    const id = fetchId(controller);
8✔
809
    const wallet = loadWallet();
8✔
810
    const keypair = hdKeyPair();
8✔
811
    const data = {
8✔
812
        name: controller || wallet.current,
7✔
813
        id: id,
814
    };
815
    const msg = JSON.stringify(data);
8✔
816
    const backup = cipher.encryptMessage(keypair.publicJwk, keypair.privateJwk, msg);
8✔
817
    const doc = await resolveDID(id.did);
8✔
818
    const registry = doc.mdip.registry;
8✔
819
    const vaultDid = await createAsset({ backup: backup }, { registry, controller });
8✔
820

821
    doc.didDocumentData.vault = vaultDid;
8✔
822
    return await updateDID(id.did, doc);
8✔
823
}
824

825
export async function recoverId(did) {
826
    try {
4✔
827
        const wallet = loadWallet();
4✔
828
        const keypair = hdKeyPair();
4✔
829
        const doc = await resolveDID(did);
4✔
830
        const vault = await resolveAsset(doc.didDocumentData.vault);
4✔
831
        const backup = cipher.decryptMessage(keypair.publicJwk, keypair.privateJwk, vault.backup);
4✔
832
        const data = JSON.parse(backup);
2✔
833

834
        // TBD handle the case where name already exists in wallet
835
        wallet.ids[data.name] = data.id;
2✔
836
        wallet.current = data.name;
2✔
837
        wallet.counter += 1;
2✔
838

839
        saveWallet(wallet);
2✔
840

841
        return `Recovered ${data.name}!`;
2✔
842
    }
843
    catch {
844
        throw new Error(exceptions.INVALID_PARAMETER);
2✔
845
    }
846
}
847

848
export async function rotateKeys() {
849
    const wallet = loadWallet();
36✔
850
    const id = wallet.ids[wallet.current];
36✔
851
    const nextIndex = id.index + 1;
36✔
852
    const hdkey = cipher.generateHDKeyJSON(wallet.seed.hdkey);
36✔
853
    const path = `m/44'/0'/${id.account}'/0/${nextIndex}`;
36✔
854
    const didkey = hdkey.derive(path);
36✔
855
    const keypair = cipher.generateJwk(didkey.privateKey);
36✔
856
    const doc = await resolveDID(id.did);
36✔
857

858
    if (!doc.didDocumentMetadata.confirmed) {
36✔
859
        throw new Error('Cannot rotate keys');
2✔
860
    }
861

862
    const vmethod = doc.didDocument.verificationMethod[0];
34✔
863

864
    vmethod.id = `#key-${nextIndex + 1}`;
34✔
865
    vmethod.publicKeyJwk = keypair.publicJwk;
34✔
866
    doc.didDocument.authentication = [vmethod.id];
34✔
867

868
    const ok = await updateDID(id.did, doc);
34✔
869

870
    if (ok) {
34!
871
        id.index = nextIndex;
34✔
872
        saveWallet(wallet);
34✔
873
        return doc;
34✔
874
    }
875
    else {
UNCOV
876
        throw new Error('Cannot rotate keys');
×
877
    }
878
}
879

880
export function listNames() {
881
    const wallet = loadWallet();
4✔
882

883
    return wallet.names || {};
4✔
884
}
885

886
export function addName(name, did) {
887
    const wallet = loadWallet();
50✔
888

889
    if (!wallet.names) {
50✔
890
        wallet.names = {};
26✔
891
    }
892

893
    if (Object.keys(wallet.names).includes(name)) {
50✔
894
        throw new Error(exceptions.INVALID_PARAMETER);
2✔
895
    }
896

897
    if (Object.keys(wallet.ids).includes(name)) {
48✔
898
        throw new Error(exceptions.INVALID_PARAMETER);
2✔
899
    }
900

901
    wallet.names[name] = did;
46✔
902
    saveWallet(wallet);
46✔
903

904
    return true;
46✔
905
}
906

907
export function removeName(name) {
908
    const wallet = loadWallet();
4✔
909

910
    if (wallet.names && Object.keys(wallet.names).includes(name)) {
4✔
911
        delete wallet.names[name];
2✔
912
        saveWallet(wallet);
2✔
913
    }
914

915
    return true;
4✔
916
}
917

918
export function lookupDID(name) {
919
    try {
3,434✔
920
        if (name.startsWith('did:')) {
3,434✔
921
            return name;
3,352✔
922
        }
923
    }
924
    catch {
925
        throw new Error(exceptions.INVALID_DID);
52✔
926
    }
927

928
    const wallet = loadWallet();
30✔
929

930
    if (wallet.names && Object.keys(wallet.names).includes(name)) {
30✔
931
        return wallet.names[name];
10✔
932
    }
933

934
    if (wallet.ids && Object.keys(wallet.ids).includes(name)) {
20✔
935
        return wallet.ids[name].did;
2✔
936
    }
937

938
    throw new Error(exceptions.UNKNOWN_ID);
18✔
939
}
940

941
export async function testAgent(id) {
942
    const doc = await resolveDID(id);
8✔
943
    return doc?.mdip?.type === 'agent';
4✔
944
}
945

946
export async function createCredential(schema, registry) {
947
    // TBD validate schema
948
    return createAsset(schema, { registry });
100✔
949
}
950

951
export async function bindCredential(schemaId, subjectId, validUntil = null) {
46✔
952
    const id = fetchId();
92✔
953
    const type = lookupDID(schemaId);
92✔
954
    const schema = await resolveAsset(type);
92✔
955
    const credential = JSONSchemaFaker.generate(schema);
92✔
956

957
    return {
92✔
958
        "@context": [
959
            "https://www.w3.org/ns/credentials/v2",
960
            "https://www.w3.org/ns/credentials/examples/v2"
961
        ],
962
        type: ["VerifiableCredential", type],
963
        issuer: id.did,
964
        validFrom: new Date().toISOString(),
965
        validUntil: validUntil,
966
        credentialSubject: {
967
            id: lookupDID(subjectId),
968
        },
969
        credential: credential,
970
    };
971
}
972

973
export async function issueCredential(credential, registry = defaultRegistry) {
40✔
974
    const id = fetchId();
88✔
975

976
    if (credential.issuer !== id.did) {
88✔
977
        throw new Error(exceptions.INVALID_PARAMETER);
2✔
978
    }
979

980
    const signed = await addSignature(credential);
86✔
981
    const cipherDid = await encryptJSON(signed, credential.credentialSubject.id, { registry });
86✔
982
    addToOwned(cipherDid);
86✔
983
    return cipherDid;
86✔
984
}
985

986
export async function updateCredential(did, credential) {
987
    did = lookupDID(did);
18✔
988
    const originalVC = await decryptJSON(did);
16✔
989

990
    if (!originalVC.credential) {
12✔
991
        throw new Error(exceptions.INVALID_PARAMETER);
2✔
992
    }
993

994
    if (!credential?.credential || !credential?.credentialSubject?.id) {
10✔
995
        throw new Error(exceptions.INVALID_PARAMETER);
8✔
996
    }
997

998
    delete credential.signature;
2✔
999
    const signed = await addSignature(credential);
2✔
1000
    const msg = JSON.stringify(signed);
2✔
1001

1002
    const id = fetchId();
2✔
1003
    const senderKeypair = await fetchKeyPair();
2✔
1004
    const holder = credential.credentialSubject.id;
2✔
1005
    const holderDoc = await resolveDID(holder, { confirm: true });
2✔
1006
    const receivePublicJwk = holderDoc.didDocument.verificationMethod[0].publicKeyJwk;
2✔
1007
    const cipher_sender = cipher.encryptMessage(senderKeypair.publicJwk, senderKeypair.privateJwk, msg);
2✔
1008
    const cipher_receiver = cipher.encryptMessage(receivePublicJwk, senderKeypair.privateJwk, msg);
2✔
1009
    const msgHash = cipher.hashMessage(msg);
2✔
1010

1011
    const doc = await resolveDID(did);
2✔
1012
    doc.didDocumentData = {
2✔
1013
        sender: id.did,
1014
        created: new Date().toISOString(),
1015
        cipher_hash: msgHash,
1016
        cipher_sender: cipher_sender,
1017
        cipher_receiver: cipher_receiver,
1018
    };
1019

1020
    return updateDID(did, doc);
2✔
1021
}
1022

1023
export async function revokeCredential(credential) {
1024
    const did = lookupDID(credential);
20✔
1025
    return revokeDID(did);
20✔
1026
}
1027

1028
export async function listIssued(issuer) {
1029
    const id = fetchId(issuer);
4✔
1030
    const issued = [];
4✔
1031

1032
    if (id.owned) {
4✔
1033
        for (const did of id.owned) {
2✔
1034
            try {
4✔
1035
                const credential = await decryptJSON(did);
4✔
1036

1037
                if (credential.issuer === id.did) {
2!
1038
                    issued.push(did);
2✔
1039
                }
1040
            }
1041
            catch (error) {
1042
                continue;
2✔
1043
            }
1044
        }
1045
    }
1046

1047
    return issued;
4✔
1048
}
1049

1050
export async function acceptCredential(did) {
1051
    try {
66✔
1052
        const id = fetchId();
66✔
1053
        const credential = lookupDID(did);
66✔
1054
        const vc = await decryptJSON(credential);
66✔
1055

1056
        if (vc.credentialSubject.id !== id.did) {
62!
UNCOV
1057
            throw new Error(exceptions.INVALID_PARAMETER);
×
1058
        }
1059

1060
        return addToHeld(credential);
62✔
1061
    }
1062
    catch (error) {
1063
        return false;
4✔
1064
    }
1065
}
1066

1067
export async function getCredential(did) {
1068
    return decryptJSON(lookupDID(did));
18✔
1069
}
1070

1071
export async function removeCredential(did) {
1072
    return removeFromHeld(lookupDID(did));
10✔
1073
}
1074

1075
export async function listCredentials(id) {
1076
    return fetchId(id).held || [];
8✔
1077
}
1078

1079
export async function publishCredential(did, reveal = false) {
×
1080
    const id = fetchId();
6✔
1081
    const credential = lookupDID(did);
6✔
1082
    const vc = await decryptJSON(credential);
6✔
1083

1084
    if (vc.credentialSubject.id !== id.did) {
6!
UNCOV
1085
        throw new Error(exceptions.INVALID_PARAMETER);
×
1086
    }
1087

1088
    const doc = await resolveDID(id.did);
6✔
1089

1090
    if (!doc.didDocumentData.manifest) {
6!
1091
        doc.didDocumentData.manifest = {};
6✔
1092
    }
1093

1094
    if (!reveal) {
6✔
1095
        // Remove the credential values
1096
        vc.credential = null;
2✔
1097
    }
1098

1099
    doc.didDocumentData.manifest[credential] = vc;
6✔
1100

1101
    const ok = await updateDID(id.did, doc);
6✔
1102

1103
    if (ok) {
6!
1104
        return vc;
6✔
1105
    }
1106
    else {
UNCOV
1107
        throw new Error(exceptions.UPDATE_FAILED);
×
1108
    }
1109
}
1110

1111
export async function unpublishCredential(did) {
1112
    const id = fetchId();
8✔
1113
    const doc = await resolveDID(id.did);
6✔
1114
    const credential = lookupDID(did);
6✔
1115
    const manifest = doc.didDocumentData.manifest;
6✔
1116

1117
    if (credential && manifest && Object.keys(manifest).includes(credential)) {
6✔
1118
        delete manifest[credential];
2✔
1119
        await updateDID(id.did, doc);
2✔
1120

1121
        return `OK credential ${did} removed from manifest`;
2✔
1122
    }
1123

1124
    throw new Error(exceptions.INVALID_PARAMETER);
4✔
1125
}
1126

1127
export async function createChallenge(challengeSpec, options = {}) {
8✔
1128

1129
    let { registry } = options;
18✔
1130

1131
    if (!registry) {
18✔
1132
        registry = ephemeralRegistry;
16✔
1133
    }
1134

1135
    if (!challengeSpec) {
18✔
1136
        challengeSpec = {};
2✔
1137
    }
1138

1139
    if (typeof challengeSpec !== 'object' || Array.isArray(challengeSpec)) {
18✔
1140
        throw new Error(exceptions.INVALID_PARAMETER);
2✔
1141
    }
1142

1143
    if (!challengeSpec.challenge) {
16✔
1144
        challengeSpec.challenge = {};
4✔
1145
    }
1146

1147
    if (!challengeSpec.ephemeral) {
16!
1148
        const expires = new Date();
16✔
1149
        expires.setHours(expires.getHours() + 1); // Add 1 hour
16✔
1150
        challengeSpec.ephemeral = { validUntil: expires.toISOString() };
16✔
1151
    }
1152

1153
    if (!challengeSpec.challenge.credentials) {
16✔
1154
        challengeSpec.challenge.credentials = [];
4✔
1155
    }
1156

1157
    if (!Array.isArray(challengeSpec.challenge.credentials)) {
16✔
1158
        throw new Error(exceptions.INVALID_PARAMETER);
2✔
1159
    }
1160

1161
    return createAsset(challengeSpec, { registry });
14✔
1162
}
1163

1164
async function findMatchingCredential(credential) {
1165
    const id = fetchId();
14✔
1166

1167
    if (!id.held) {
14✔
1168
        return;
2✔
1169
    }
1170

1171
    for (let did of id.held) {
12✔
1172
        try {
24✔
1173
            const doc = await decryptJSON(did);
24✔
1174

1175
            // console.log(doc);
1176

1177
            if (!doc.issuer) {
24!
1178
                // Not a VC
UNCOV
1179
                continue;
×
1180
            }
1181

1182
            if (doc.credentialSubject?.id !== id.did) {
24!
1183
                // This VC is issued by the ID, not held
UNCOV
1184
                continue;
×
1185
            }
1186

1187
            if (credential.issuers && !credential.issuers.includes(doc.issuer)) {
24✔
1188
                // Attestor not trusted by Verifier
1189
                continue;
8✔
1190
            }
1191

1192
            if (doc.type && !doc.type.includes(credential.schema)) {
16✔
1193
                // Wrong type
1194
                continue;
4✔
1195
            }
1196

1197
            // TBD test for VC expiry too
1198
            return did;
12✔
1199
        }
1200
        catch (error) {
1201
            // Not encrypted, so can't be a VC
1202
        }
1203
    }
1204
}
1205

1206
export async function createResponse(challengeDID, options = {}) {
8✔
1207
    let { registry, retries, delay } = options;
20✔
1208

1209
    if (!registry) {
20✔
1210
        registry = ephemeralRegistry;
18✔
1211
    }
1212

1213
    if (!retries) {
20✔
1214
        retries = 0;
18✔
1215
    }
1216

1217
    if (!delay) {
20✔
1218
        delay = 1000;
18✔
1219
    }
1220

1221
    let doc;
1222

1223
    while (retries >= 0) {
20✔
1224
        try {
40✔
1225
            doc = await resolveDID(challengeDID);
40✔
1226
            break;
12✔
1227
        } catch (error) {
1228
            if (retries === 0) throw error; // If no retries left, throw the error
28✔
1229
            retries--; // Decrease the retry count
20✔
1230
            await new Promise(resolve => setTimeout(resolve, delay)); // Wait for delay milleseconds
20✔
1231
        }
1232
    }
1233

1234
    const requestor = doc.didDocument.controller;
12✔
1235
    const { challenge } = await resolveAsset(challengeDID);
12✔
1236

1237
    if (!challenge?.credentials) {
12✔
1238
        throw new Error(exceptions.INVALID_PARAMETER);
2✔
1239
    }
1240

1241
    // TBD check challenge.ephemeral for expired?
1242

1243
    const matches = [];
10✔
1244

1245
    for (let credential of challenge.credentials) {
10✔
1246
        const vc = await findMatchingCredential(credential);
14✔
1247

1248
        if (vc) {
14✔
1249
            matches.push(vc);
12✔
1250
        }
1251
    }
1252

1253
    if (!matches) {
10!
UNCOV
1254
        throw new Error(exceptions.INVALID_PARAMETER);
×
1255
    }
1256

1257
    const pairs = [];
10✔
1258

1259
    for (let vcDid of matches) {
10✔
1260
        const plaintext = await decryptMessage(vcDid);
12✔
1261
        const vpDid = await encryptMessage(plaintext, requestor, options);
12✔
1262
        pairs.push({ vc: vcDid, vp: vpDid });
12✔
1263
    }
1264

1265
    const requested = challenge.credentials.length;
10✔
1266
    const fulfilled = matches.length;
10✔
1267
    const match = (requested === fulfilled);
10✔
1268
    const expires = new Date();
10✔
1269
    expires.setHours(expires.getHours() + 1); // Add 1 hour
10✔
1270

1271
    const response = {
10✔
1272
        response: {
1273
            challenge: challengeDID,
1274
            credentials: pairs,
1275
            requested: requested,
1276
            fulfilled: fulfilled,
1277
            match: match,
1278
        },
1279
        ephemeral: {
1280
            validUntil: expires.toISOString()
1281
        }
1282
    };
1283

1284
    return await encryptJSON(response, requestor, options);
10✔
1285
}
1286

1287
export async function verifyResponse(responseDID, options = {}) {
11✔
1288
    let { retries, delay } = options;
24✔
1289

1290
    if (!retries) {
24✔
1291
        retries = 0;
22✔
1292
    }
1293

1294
    if (!delay) {
24✔
1295
        delay = 1000;
22✔
1296
    }
1297

1298
    let responseDoc;
1299

1300
    while (retries >= 0) {
24✔
1301
        try {
44✔
1302
            responseDoc = await resolveDID(responseDID);
44✔
1303
            break;
16✔
1304
        } catch (error) {
1305
            if (retries === 0) throw error; // If no retries left, throw the error
28✔
1306
            retries--; // Decrease the retry count
20✔
1307
            await new Promise(resolve => setTimeout(resolve, delay)); // Wait for delay milliseconds
20✔
1308
        }
1309
    }
1310

1311
    const { response } = await decryptJSON(responseDID);
16✔
1312
    const { challenge } = await resolveAsset(response.challenge);
14✔
1313

1314
    const vps = [];
14✔
1315

1316
    for (let credential of response.credentials) {
14✔
1317
        const vcData = await resolveAsset(credential.vc);
34✔
1318
        const vpData = await resolveAsset(credential.vp);
34✔
1319

1320
        if (!vcData) {
34✔
1321
            // revoked
1322
            continue;
6✔
1323
        }
1324

1325
        if (vcData.cipher_hash !== vpData.cipher_hash) {
28!
UNCOV
1326
            continue;
×
1327
        }
1328

1329
        const vp = await decryptJSON(credential.vp);
28✔
1330
        const isValid = await verifySignature(vp);
28✔
1331

1332
        if (!isValid) {
28!
UNCOV
1333
            continue;
×
1334
        }
1335

1336
        // Check VP against VCs specified in challenge
1337
        if (vp.type.length > 1 && vp.type[1].startsWith('did:')) {
28!
1338
            const schema = vp.type[1];
28✔
1339
            const credential = challenge.credentials.find(item => item.schema === schema);
72✔
1340

1341
            if (!credential) {
28!
1342
                continue;
×
1343
            }
1344

1345
            // Check if issuer of VP is in the trusted issuer list
1346
            if (credential.issuers && credential.issuers.length > 0 && !credential.issuers.includes(vp.issuer)) {
28!
UNCOV
1347
                continue;
×
1348
            }
1349
        }
1350

1351
        vps.push(vp);
28✔
1352
    }
1353

1354
    response.vps = vps;
14✔
1355
    response.match = vps.length === challenge.credentials.length;
14✔
1356
    response.responder = responseDoc.didDocument.controller;
14✔
1357

1358
    return response;
14✔
1359
}
1360

1361
export async function createGroup(name, registry) {
1362
    const group = {
76✔
1363
        name: name,
1364
        members: []
1365
    };
1366

1367
    return createAsset(group, { registry });
76✔
1368
}
1369

1370
export async function getGroup(id) {
1371
    const isGroup = await groupTest(id);
6✔
1372

1373
    if (!isGroup) {
4✔
1374
        throw new Error(exceptions.INVALID_PARAMETER);
2✔
1375
    }
1376

1377
    return resolveAsset(id);
2✔
1378
}
1379

1380
export async function groupAdd(groupId, memberId) {
1381
    const groupDID = lookupDID(groupId);
92✔
1382
    const doc = await resolveDID(groupDID);
82✔
1383
    const data = doc.didDocumentData;
82✔
1384

1385
    if (!data.members || !Array.isArray(data.members)) {
82✔
1386
        throw new Error(exceptions.INVALID_PARAMETER);
4✔
1387
    }
1388

1389
    const memberDID = lookupDID(memberId);
78✔
1390

1391
    try {
68✔
1392
        // test for valid member DID
1393
        await resolveDID(memberDID);
68✔
1394
    }
1395
    catch {
1396
        throw new Error(exceptions.INVALID_DID);
2✔
1397
    }
1398

1399
    // If already a member, return immediately
1400
    if (data.members.includes(memberDID)) {
66✔
1401
        return data;
8✔
1402
    }
1403

1404
    // Can't add a group to itself
1405
    if (memberDID === groupDID) {
58✔
1406
        throw new Error(exceptions.INVALID_PARAMETER);
2✔
1407
    }
1408

1409
    // Can't add a mutual membership relation
1410
    const isMember = await groupTest(memberId, groupId);
56✔
1411

1412
    if (isMember) {
56✔
1413
        throw new Error(exceptions.INVALID_PARAMETER);
2✔
1414
    }
1415

1416
    const members = new Set(data.members);
54✔
1417
    members.add(memberDID);
54✔
1418
    data.members = Array.from(members);
54✔
1419

1420
    const ok = await updateDID(groupDID, doc);
54✔
1421

1422
    if (!ok) {
54!
UNCOV
1423
        throw new Error(exceptions.UPDATE_FAILED);
×
1424
    }
1425

1426
    return data;
54✔
1427
}
1428

1429
export async function groupRemove(groupId, memberId) {
1430
    const groupDID = lookupDID(groupId);
32✔
1431
    const doc = await resolveDID(groupDID);
22✔
1432
    const data = doc.didDocumentData;
22✔
1433

1434
    if (!data.members) {
22✔
1435
        throw new Error(exceptions.INVALID_PARAMETER);
4✔
1436
    }
1437

1438
    const memberDID = lookupDID(memberId);
18✔
1439

1440
    try {
10✔
1441
        // test for valid member DID
1442
        await resolveDID(memberDID);
10✔
1443
    }
1444
    catch {
1445
        throw new Error(exceptions.INVALID_DID);
2✔
1446
    }
1447

1448
    // If not already a member, return immediately
1449
    if (!data.members.includes(memberDID)) {
8✔
1450
        return data;
4✔
1451
    }
1452

1453
    const members = new Set(data.members);
4✔
1454
    members.delete(memberDID);
4✔
1455
    data.members = Array.from(members);
4✔
1456

1457
    const ok = await updateDID(groupDID, doc);
4✔
1458

1459
    if (!ok) {
4!
UNCOV
1460
        throw new Error(exceptions.UPDATE_FAILED);
×
1461
    }
1462

1463
    return data;
4✔
1464
}
1465

1466
export async function groupTest(group, member) {
1467
    const didGroup = lookupDID(group);
148✔
1468

1469
    if (!didGroup) {
146!
UNCOV
1470
        return false;
×
1471
    }
1472

1473
    const doc = await resolveDID(didGroup);
146✔
1474

1475
    if (!doc) {
146!
UNCOV
1476
        return false;
×
1477
    }
1478

1479
    const data = doc.didDocumentData;
146✔
1480

1481
    if (!data) {
146!
UNCOV
1482
        return false;
×
1483
    }
1484

1485
    if (!Array.isArray(data.members)) {
146✔
1486
        return false;
46✔
1487
    }
1488

1489
    if (!member) {
100✔
1490
        return true;
30✔
1491
    }
1492

1493
    const didMember = lookupDID(member);
70✔
1494
    let isMember = data.members.includes(didMember);
70✔
1495

1496
    if (!isMember) {
70✔
1497
        for (const did of data.members) {
30✔
1498
            isMember = await groupTest(did, didMember);
14✔
1499

1500
            if (isMember) {
14!
1501
                break;
14✔
1502
            }
1503
        }
1504
    }
1505

1506
    return isMember;
70✔
1507
}
1508

1509
export const defaultSchema = {
2✔
1510
    "$schema": "http://json-schema.org/draft-07/schema#",
1511
    "type": "object",
1512
    "properties": {
1513
        "propertyName": {
1514
            "type": "string"
1515
        }
1516
    },
1517
    "required": [
1518
        "propertyName"
1519
    ]
1520
};
1521

1522
function validateSchema(schema) {
1523
    try {
32✔
1524
        if (!Object.keys(schema).includes('$schema')) {
32✔
1525
            throw new Error(exceptions.INVALID_PARAMETER);
4✔
1526
        }
1527

1528
        // Attempt to instantiate the schema
1529
        JSONSchemaFaker.generate(schema);
28✔
1530
    }
1531
    catch {
1532
        throw new Error(exceptions.INVALID_PARAMETER);
4✔
1533
    }
1534

1535
    return true;
28✔
1536
}
1537

1538
export async function createSchema(schema, registry) {
1539
    if (!schema) {
20✔
1540
        schema = defaultSchema;
14✔
1541
    }
1542

1543
    validateSchema(schema);
20✔
1544

1545
    return createAsset(schema, { registry });
18✔
1546
}
1547

1548
export async function getSchema(id) {
1549
    return resolveAsset(id);
16✔
1550
}
1551

1552
export async function setSchema(id, newSchema) {
1553
    validateSchema(newSchema);
8✔
1554

1555
    const doc = await resolveDID(id);
6✔
1556
    const did = doc.didDocument.id;
6✔
1557
    doc.didDocumentData = newSchema;
6✔
1558

1559
    return updateDID(did, doc);
6✔
1560
}
1561

1562
// TBD add optional 2nd parameter that will validate JSON against the schema
1563
export async function testSchema(id) {
1564
    const schema = await getSchema(id);
10✔
1565

1566
    // TBD Need a better way because any random object with keys can be a valid schema
1567
    if (Object.keys(schema).length === 0) {
6✔
1568
        return false;
2✔
1569
    }
1570

1571
    return validateSchema(schema);
4✔
1572
}
1573

1574
export async function createTemplate(id) {
1575
    //JSONSchemaFaker.option("alwaysFakeOptionals", true);
1576

1577
    const isSchema = await testSchema(id);
4✔
1578

1579
    if (!isSchema) {
2!
UNCOV
1580
        throw new Error(exceptions.INVALID_PARAMETER);
×
1581
    }
1582

1583
    const schemaDID = lookupDID(id);
2✔
1584
    const schema = await resolveAsset(schemaDID);
2✔
1585
    const template = JSONSchemaFaker.generate(schema);
2✔
1586

1587
    template['$schema'] = schemaDID;
2✔
1588

1589
    return template;
2✔
1590
}
1591

1592
export async function pollTemplate() {
1593
    const now = new Date();
24✔
1594
    const nextWeek = new Date();
24✔
1595
    nextWeek.setDate(now.getDate() + 7);
24✔
1596

1597
    return {
24✔
1598
        type: 'poll',
1599
        version: 1,
1600
        description: 'What is this poll about?',
1601
        roster: 'DID of the eligible voter group',
1602
        options: ['yes', 'no', 'abstain'],
1603
        deadline: nextWeek.toISOString(),
1604
    };
1605
}
1606

1607
export async function createPoll(poll) {
1608
    if (poll.type !== 'poll') {
42✔
1609
        throw new Error(exceptions.INVALID_PARAMETER);
2✔
1610
    }
1611

1612
    if (poll.version !== 1) {
40✔
1613
        throw new Error(exceptions.INVALID_PARAMETER);
2✔
1614
    }
1615

1616
    if (!poll.description) {
38✔
1617
        throw new Error(exceptions.INVALID_PARAMETER);
2✔
1618
    }
1619

1620
    if (!poll.options || !Array.isArray(poll.options) || poll.options.length < 2 || poll.options.length > 10) {
36✔
1621
        throw new Error(exceptions.INVALID_PARAMETER);
8✔
1622
    }
1623

1624
    if (!poll.roster) {
28✔
1625
        throw new Error(exceptions.INVALID_PARAMETER);
2✔
1626
    }
1627

1628
    try {
26✔
1629
        const isValidGroup = await groupTest(poll.roster);
26✔
1630

1631
        if (!isValidGroup) {
26!
UNCOV
1632
            throw new Error(exceptions.INVALID_PARAMETER);
×
1633
        }
1634
    }
1635
    catch {
UNCOV
1636
        throw new Error(exceptions.INVALID_PARAMETER);
×
1637
    }
1638

1639
    if (!poll.deadline) {
26✔
1640
        throw new Error(exceptions.INVALID_PARAMETER);
2✔
1641
    }
1642

1643
    const deadline = new Date(poll.deadline);
24✔
1644

1645
    if (isNaN(deadline.getTime())) {
24✔
1646
        throw new Error(exceptions.INVALID_PARAMETER);
2✔
1647
    }
1648

1649
    if (deadline < new Date()) {
22✔
1650
        throw new Error(exceptions.INVALID_PARAMETER);
2✔
1651
    }
1652

1653
    return createAsset(poll);
20✔
1654
}
1655

1656
export async function viewPoll(poll) {
1657
    const id = fetchId();
8✔
1658
    const didPoll = lookupDID(poll);
8✔
1659
    const doc = await resolveDID(didPoll);
8✔
1660
    const data = doc.didDocumentData;
8✔
1661

1662
    if (!data || !data.options || !data.deadline) {
8!
UNCOV
1663
        throw new Error(exceptions.INVALID_PARAMETER);
×
1664
    }
1665

1666
    let hasVoted = false;
8✔
1667

1668
    if (data.ballots) {
8✔
1669
        hasVoted = !!data.ballots[id.did];
6✔
1670
    }
1671

1672
    const voteExpired = Date(data.deadline) > new Date();
8✔
1673
    const isEligible = await groupTest(data.roster, id.did);
8✔
1674

1675
    const view = {
8✔
1676
        description: data.description,
1677
        options: data.options,
1678
        deadline: data.deadline,
1679
        isOwner: (doc.didDocument.controller === id.did),
1680
        isEligible: isEligible,
1681
        voteExpired: voteExpired,
1682
        hasVoted: hasVoted,
1683
    };
1684

1685
    if (id.did === doc.didDocument.controller) {
8!
1686
        let voted = 0;
8✔
1687

1688
        const results = {
8✔
1689
            tally: [],
1690
            ballots: [],
1691
        }
1692

1693
        results.tally.push({
8✔
1694
            vote: 0,
1695
            option: 'spoil',
1696
            count: 0,
1697
        });
1698

1699
        for (let i = 0; i < data.options.length; i++) {
8✔
1700
            results.tally.push({
24✔
1701
                vote: i + 1,
1702
                option: data.options[i],
1703
                count: 0,
1704
            });
1705
        }
1706

1707
        for (let voter in data.ballots) {
8✔
1708
            const ballot = data.ballots[voter];
6✔
1709
            const decrypted = await decryptJSON(ballot.ballot);
6✔
1710
            const vote = decrypted.vote;
6✔
1711
            results.ballots.push({
6✔
1712
                ...ballot,
1713
                voter: voter,
1714
                vote: vote,
1715
                option: data.options[vote - 1],
1716
            });
1717
            voted += 1;
6✔
1718
            results.tally[vote].count += 1;
6✔
1719
        }
1720

1721
        const roster = await resolveAsset(data.roster);
8✔
1722
        const total = roster.members.length;
8✔
1723

1724
        results.votes = {
8✔
1725
            eligible: total,
1726
            received: voted,
1727
            pending: total - voted,
1728
        };
1729
        results.final = voteExpired || (voted === total);
8✔
1730

1731
        view.results = results;
8✔
1732
    }
1733

1734
    return view;
8✔
1735
}
1736

1737
export async function votePoll(poll, vote, spoil = false) {
7✔
1738
    const id = fetchId();
14✔
1739
    const didPoll = lookupDID(poll);
14✔
1740
    const doc = await resolveDID(didPoll);
14✔
1741
    const data = doc.didDocumentData;
14✔
1742
    const eligible = await groupTest(data.roster, id.did);
14✔
1743
    const expired = (Date(data.deadline) > new Date());
14✔
1744
    const owner = doc.didDocument.controller;
14✔
1745

1746
    if (!eligible) {
14✔
1747
        throw new Error(exceptions.INVALID_PARAMETER);
2✔
1748
    }
1749

1750
    if (expired) {
12!
UNCOV
1751
        throw new Error(exceptions.INVALID_PARAMETER);
×
1752
    }
1753

1754
    let ballot;
1755

1756
    if (spoil) {
12!
UNCOV
1757
        ballot = {
×
1758
            poll: didPoll,
1759
            vote: 0,
1760
        };
1761
    }
1762
    else {
1763
        const max = data.options.length;
12✔
1764
        vote = parseInt(vote);
12✔
1765

1766
        if (!Number.isInteger(vote) || vote < 1 || vote > max) {
12✔
1767
            throw new Error(exceptions.INVALID_PARAMETER);
2✔
1768
        }
1769

1770
        ballot = {
10✔
1771
            poll: didPoll,
1772
            vote: vote,
1773
        };
1774
    }
1775

1776
    // Encrypt for receiver only
1777
    // TBD which registry?
1778
    return await encryptJSON(ballot, owner, false);
10✔
1779
}
1780

1781
export async function updatePoll(ballot) {
1782
    const id = fetchId();
10✔
1783

1784
    const didBallot = lookupDID(ballot);
10✔
1785
    const docBallot = await resolveDID(didBallot);
10✔
1786
    const didVoter = docBallot.didDocument.controller;
10✔
1787
    let dataBallot;
1788

1789
    try {
10✔
1790
        dataBallot = await decryptJSON(didBallot);
10✔
1791

1792
        if (!dataBallot.poll || !dataBallot.vote) {
8!
UNCOV
1793
            throw new Error(exceptions.INVALID_PARAMETER);
×
1794
        }
1795
    }
1796
    catch {
1797
        throw new Error(exceptions.INVALID_PARAMETER);
2✔
1798
    }
1799

1800
    const didPoll = lookupDID(dataBallot.poll);
8✔
1801
    const docPoll = await resolveDID(didPoll);
8✔
1802
    const dataPoll = docPoll.didDocumentData;
8✔
1803
    const didOwner = docPoll.didDocument.controller;
8✔
1804

1805
    if (id.did !== didOwner) {
8!
UNCOV
1806
        throw new Error(exceptions.INVALID_PARAMETER);
×
1807
    }
1808

1809
    const eligible = await groupTest(dataPoll.roster, didVoter);
8✔
1810

1811
    if (!eligible) {
8!
UNCOV
1812
        throw new Error(exceptions.INVALID_PARAMETER);
×
1813
    }
1814

1815
    const expired = (Date(dataPoll.deadline) > new Date());
8✔
1816

1817
    if (expired) {
8!
UNCOV
1818
        throw new Error(exceptions.INVALID_PARAMETER);
×
1819
    }
1820

1821
    const max = dataPoll.options.length;
8✔
1822
    const vote = parseInt(dataBallot.vote);
8✔
1823

1824
    if (!vote || vote < 0 || vote > max) {
8!
UNCOV
1825
        throw new Error(exceptions.INVALID_PARAMETER);
×
1826
    }
1827

1828
    if (!dataPoll.ballots) {
8!
1829
        dataPoll.ballots = {};
8✔
1830
    }
1831

1832
    dataPoll.ballots[didVoter] = {
8✔
1833
        ballot: didBallot,
1834
        received: new Date().toISOString(),
1835
    };
1836

1837
    return await updateDID(didPoll, docPoll);
8✔
1838
}
1839

1840
export async function publishPoll(poll, reveal = false) {
2✔
1841
    const id = fetchId();
6✔
1842
    const didPoll = lookupDID(poll);
6✔
1843
    const doc = await resolveDID(didPoll);
6✔
1844
    const owner = doc.didDocument.controller;
6✔
1845

1846
    if (id.did !== owner) {
6!
UNCOV
1847
        throw new Error(exceptions.INVALID_PARAMETER);
×
1848
    }
1849

1850
    const view = await viewPoll(poll);
6✔
1851

1852
    if (!view.results.final) {
6!
UNCOV
1853
        throw new Error(exceptions.INVALID_PARAMETER);
×
1854
    }
1855

1856
    if (!reveal) {
6✔
1857
        delete view.results.ballots;
4✔
1858
    }
1859

1860
    doc.didDocumentData.results = view.results;
6✔
1861

1862
    return await updateDID(didPoll, doc);
6✔
1863
}
1864

1865
export async function unpublishPoll(poll) {
1866
    const id = fetchId();
2✔
1867
    const didPoll = lookupDID(poll);
2✔
1868
    const doc = await resolveDID(didPoll);
2✔
1869
    const owner = doc.didDocument.controller;
2✔
1870

1871
    if (id.did !== owner) {
2!
UNCOV
1872
        throw new Error(exceptions.INVALID_PARAMETER);
×
1873
    }
1874

1875
    delete doc.didDocumentData.results;
2✔
1876

1877
    return await updateDID(didPoll, doc);
2✔
1878
}
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