• 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

91.89
/keymaster-lib.js
1
import fs from 'fs';
2
import { JSONSchemaFaker } from "json-schema-faker";
3
import * as cipher from './cipher-lib.js';
4

5
let gatekeeper = null;
2✔
6
const dataFolder = 'data';
2✔
7
const walletName = `${dataFolder}/wallet.json`;
2✔
8
const defaultRegistry = 'TESS';
2✔
9
const ephemeralRegistry = 'hyperswarm';
2✔
10

11
export async function start(gk) {
12
    gatekeeper = gk;
328✔
13
}
14

15
export async function stop() {
16
    await gatekeeper.stop();
328✔
17
}
18

19
export async function listRegistries() {
20
    return gatekeeper.listRegistries();
2✔
21
}
22

23
export function saveWallet(wallet) {
24
    // TBD validate wallet before saving
25

26
    if (!fs.existsSync(dataFolder)) {
1,576✔
27
        fs.mkdirSync(dataFolder, { recursive: true });
312✔
28
    }
29

30
    fs.writeFileSync(walletName, JSON.stringify(wallet, null, 4));
1,576✔
31
}
32

33
export function loadWallet() {
34
    if (fs.existsSync(walletName)) {
5,164✔
35
        const walletJson = fs.readFileSync(walletName);
4,854✔
36
        return JSON.parse(walletJson);
4,854✔
37
    }
38

39
    return newWallet();
310✔
40
}
41

42
export function newWallet(mnemonic, overwrite = false) {
157✔
43
    if (fs.existsSync(walletName) && !overwrite) {
324✔
44
        throw "Wallet already exists";
2✔
45
    }
46

47
    try {
322✔
48
        if (!mnemonic) {
322✔
49
            mnemonic = cipher.generateMnemonic();
314✔
50
        }
51
        const hdkey = cipher.generateHDKey(mnemonic);
322✔
52
        const keypair = cipher.generateJwk(hdkey.privateKey);
322✔
53
        const backup = cipher.encryptMessage(keypair.publicJwk, keypair.privateJwk, mnemonic);
322✔
54

55
        const wallet = {
322✔
56
            seed: {
57
                mnemonic: backup,
58
                hdkey: hdkey.toJSON(),
59
            },
60
            counter: 0,
61
            ids: {},
62
        }
63

64
        saveWallet(wallet);
322✔
65
        return wallet;
322✔
66
    }
67
    catch (error) {
68
        throw "Invalid mnemonic";
×
69
    }
70
}
71

72
export function decryptMnemonic() {
73
    const wallet = loadWallet();
10✔
74
    const keypair = hdKeyPair();
10✔
75
    const mnenomic = cipher.decryptMessage(keypair.publicJwk, keypair.privateJwk, wallet.seed.mnemonic);
10✔
76

77
    return mnenomic;
10✔
78
}
79

80
export async function checkWallet() {
81
    const wallet = loadWallet();
2✔
82

83
    let checked = 0;
2✔
84
    let invalid = 0;
2✔
85
    let deleted = 0;
2✔
86

87
    // Validate keys
88
    await resolveSeedBank();
2✔
89

90
    for (const name of Object.keys(wallet.ids)) {
2✔
91
        try {
8✔
92
            const doc = await resolveDID(wallet.ids[name].did);
8✔
93

94
            if (doc.didDocumentMetadata.deactivated) {
8!
95
                deleted += 1;
×
96
            }
97
        }
98
        catch (error) {
99
            invalid += 1;
×
100
        }
101

102
        checked += 1;
8✔
103
    }
104

105
    for (const id of Object.values(wallet.ids)) {
2✔
106
        if (id.owned) {
8✔
107
            for (const did of id.owned) {
4✔
108
                try {
16✔
109
                    const doc = await resolveDID(did);
16✔
110

111
                    if (doc.didDocumentMetadata.deactivated) {
16✔
112
                        deleted += 1;
4✔
113
                    }
114
                }
115
                catch (error) {
116
                    invalid += 1;
×
117
                }
118

119
                checked += 1;
16✔
120
            }
121
        }
122

123
        if (id.held) {
8✔
124
            for (const did of id.held) {
2✔
125
                try {
8✔
126
                    const doc = await resolveDID(did);
8✔
127

128
                    if (doc.didDocumentMetadata.deactivated) {
8✔
129
                        deleted += 1;
4✔
130
                    }
131
                }
132
                catch (error) {
133
                    invalid += 1;
×
134
                }
135

136
                checked += 1;
8✔
137
            }
138
        }
139
    }
140

141
    return { checked, invalid, deleted };
2✔
142
}
143

144
export async function fixWallet() {
145
    const wallet = loadWallet();
2✔
146
    let idsRemoved = 0;
2✔
147
    let ownedRemoved = 0;
2✔
148
    let heldRemoved = 0;
2✔
149

150
    for (const name of Object.keys(wallet.ids)) {
2✔
151
        let remove = false;
8✔
152

153
        try {
8✔
154
            const doc = await resolveDID(wallet.ids[name].did);
8✔
155

156
            if (doc.didDocumentMetadata.deactivated) {
8!
157
                remove = true;
×
158
            }
159
        }
160
        catch (error) {
161
            remove = true;
×
162
        }
163

164
        if (remove) {
8!
165
            delete wallet.ids[name];
×
166
            idsRemoved += 1;
×
167
        }
168
    }
169

170
    for (const id of Object.values(wallet.ids)) {
2✔
171
        if (id.owned) {
8✔
172
            for (let i = 0; i < id.owned.length; i++) {
4✔
173
                let remove = false;
16✔
174

175
                try {
16✔
176
                    const doc = await resolveDID(id.owned[i]);
16✔
177

178
                    if (doc.didDocumentMetadata.deactivated) {
16✔
179
                        remove = true;
4✔
180
                    }
181
                }
182
                catch {
183
                    remove = true;
×
184
                }
185

186
                if (remove) {
16✔
187
                    id.owned.splice(i, 1);
4✔
188
                    i--; // Decrement index to account for the removed item
4✔
189
                    ownedRemoved += 1;
4✔
190
                }
191
            }
192
        }
193

194
        if (id.held) {
8✔
195
            for (let i = 0; i < id.held.length; i++) {
2✔
196
                let remove = false;
8✔
197

198
                try {
8✔
199
                    const doc = await resolveDID(id.held[i]);
8✔
200

201
                    if (doc.didDocumentMetadata.deactivated) {
8✔
202
                        remove = true;
4✔
203
                    }
204
                }
205
                catch {
206
                    remove = true;
×
207
                }
208

209
                if (remove) {
8✔
210
                    id.held.splice(i, 1);
4✔
211
                    i--; // Decrement index to account for the removed item
4✔
212
                    heldRemoved += 1;
4✔
213
                }
214
            }
215
        }
216
    }
217

218
    saveWallet(wallet);
2✔
219

220
    return { idsRemoved, ownedRemoved, heldRemoved };
2✔
221
}
222

223
export async function resolveSeedBank() {
224
    const keypair = hdKeyPair();
18✔
225

226
    const operation = {
18✔
227
        type: "create",
228
        created: new Date(0).toISOString(),
229
        mdip: {
230
            version: 1,
231
            type: "agent",
232
            registry: defaultRegistry,
233
        },
234
        publicJwk: keypair.publicJwk,
235
    };
236

237
    const msgHash = cipher.hashJSON(operation);
18✔
238
    const signature = cipher.signHash(msgHash, keypair.privateJwk);
18✔
239
    const signed = {
18✔
240
        ...operation,
241
        signature: {
242
            signed: new Date(0).toISOString(),
243
            hash: msgHash,
244
            value: signature
245
        }
246
    }
247
    const did = await gatekeeper.createDID(signed);
18✔
248
    const doc = await gatekeeper.resolveDID(did);
18✔
249

250
    return doc;
18✔
251
}
252

253
async function updateSeedBank(doc) {
254
    const keypair = hdKeyPair();
8✔
255
    const did = doc.didDocument.id;
8✔
256
    const current = await gatekeeper.resolveDID(did);
8✔
257
    const prev = cipher.hashJSON(current);
8✔
258

259
    const operation = {
8✔
260
        type: "update",
261
        did: did,
262
        doc: doc,
263
        prev: prev,
264
    };
265

266
    const msgHash = cipher.hashJSON(operation);
8✔
267
    const signature = cipher.signHash(msgHash, keypair.privateJwk);
8✔
268
    const signed = {
8✔
269
        ...operation,
270
        signature: {
271
            signer: did,
272
            signed: new Date().toISOString(),
273
            hash: msgHash,
274
            value: signature,
275
        }
276
    };
277

278
    const ok = await gatekeeper.updateDID(signed);
8✔
279
    return ok;
8✔
280
}
281

282
export async function backupWallet(registry = defaultRegistry) {
4✔
283
    const wallet = loadWallet();
8✔
284
    const keypair = hdKeyPair();
8✔
285
    const seedBank = await resolveSeedBank();
8✔
286
    const msg = JSON.stringify(wallet);
8✔
287
    const backup = cipher.encryptMessage(keypair.publicJwk, keypair.privateJwk, msg);
8✔
288
    const operation = {
8✔
289
        type: "create",
290
        created: new Date().toISOString(),
291
        mdip: {
292
            version: 1,
293
            type: "asset",
294
            registry: registry,
295
        },
296
        controller: seedBank.didDocument.id,
297
        data: { backup: backup },
298
    };
299
    const msgHash = cipher.hashJSON(operation);
8✔
300
    const signature = cipher.signHash(msgHash, keypair.privateJwk);
8✔
301
    const signed = {
8✔
302
        ...operation,
303
        signature: {
304
            signer: seedBank.didDocument.id,
305
            signed: new Date().toISOString(),
306
            hash: msgHash,
307
            value: signature,
308
        }
309
    };
310
    const backupDID = await gatekeeper.createDID(signed);
8✔
311

312
    seedBank.didDocumentData.wallet = backupDID;
8✔
313
    await updateSeedBank(seedBank);
8✔
314

315
    return backupDID;
8✔
316
}
317

318
export async function recoverWallet(did) {
319
    const keypair = hdKeyPair();
4✔
320

321
    if (!did) {
4✔
322
        const seedBank = await resolveSeedBank();
2✔
323
        did = seedBank.didDocumentData.wallet;
2✔
324
    }
325

326
    const data = await resolveAsset(did);
4✔
327
    const backup = cipher.decryptMessage(keypair.publicJwk, keypair.privateJwk, data.backup);
4✔
328
    const wallet = JSON.parse(backup);
4✔
329

330
    saveWallet(wallet);
4✔
331

332
    return wallet;
4✔
333
}
334

335
export function listIds() {
336
    const wallet = loadWallet();
2✔
337
    return Object.keys(wallet.ids);
2✔
338
}
339

340
export function getCurrentId() {
341
    const wallet = loadWallet();
4✔
342
    return wallet.current;
4✔
343
}
344

345
export function setCurrentId(name) {
346
    const wallet = loadWallet();
182✔
347
    if (Object.keys(wallet.ids).includes(name)) {
182✔
348
        wallet.current = name;
178✔
349
        saveWallet(wallet);
178✔
350
    }
351
    else {
352
        throw `Unknown ID`;
4✔
353
    }
354
}
355

356
function fetchId(id) {
357
    const wallet = loadWallet();
2,682✔
358
    let idInfo = null;
2,682✔
359

360
    if (id) {
2,682✔
361
        if (id.startsWith('did')) {
378✔
362
            for (const name of Object.keys(wallet.ids)) {
356✔
363
                const info = wallet.ids[name];
410✔
364

365
                if (info.did === id) {
410✔
366
                    idInfo = info;
354✔
367
                    break;
354✔
368
                }
369
            }
370
        }
371
        else {
372
            idInfo = wallet.ids[id];
22✔
373
        }
374
    }
375
    else {
376
        idInfo = wallet.ids[wallet.current];
2,304✔
377

378
        if (!idInfo) {
2,304✔
379
            throw "No current ID";
6✔
380
        }
381
    }
382

383
    if (!idInfo) {
2,676✔
384
        throw "Unknown ID";
4✔
385
    }
386

387
    return idInfo;
2,672✔
388
}
389

390
function hdKeyPair() {
391
    const wallet = loadWallet();
60✔
392
    const hdkey = cipher.generateHDKeyJSON(wallet.seed.hdkey);
60✔
393
    const keypair = cipher.generateJwk(hdkey.privateKey);
60✔
394

395
    return keypair;
60✔
396
}
397

398
function fetchKeyPair(name = null) {
70✔
399
    const wallet = loadWallet();
834✔
400
    const id = fetchId(name);
834✔
401
    const hdkey = cipher.generateHDKeyJSON(wallet.seed.hdkey);
834✔
402
    const path = `m/44'/0'/${id.account}'/0/${id.index}`;
834✔
403
    const didkey = hdkey.derive(path);
834✔
404
    const keypair = cipher.generateJwk(didkey.privateKey);
834✔
405

406
    return keypair;
834✔
407
}
408

409
export async function encrypt(msg, did, encryptForSender = true, registry = defaultRegistry) {
84✔
410
    const id = fetchId();
140✔
411
    const keypair = fetchKeyPair();
140✔
412
    const doc = await resolveDID(did);
140✔
413
    const publicJwk = doc.didDocument.verificationMethod[0].publicKeyJwk;
140✔
414
    const cipher_sender = encryptForSender ? cipher.encryptMessage(keypair.publicJwk, keypair.privateJwk, msg) : null;
140✔
415
    const cipher_receiver = cipher.encryptMessage(publicJwk, keypair.privateJwk, msg);
140✔
416
    const msgHash = cipher.hashMessage(msg);
140✔
417
    const cipherDid = await createAsset({
140✔
418
        sender: id.did,
419
        created: new Date().toISOString(),
420
        cipher_hash: msgHash,
421
        cipher_sender: cipher_sender,
422
        cipher_receiver: cipher_receiver,
423
    }, registry);
424

425
    return cipherDid;
140✔
426
}
427

428
export async function decrypt(did) {
429
    const wallet = loadWallet();
216✔
430
    const id = fetchId();
216✔
431
    const crypt = await resolveAsset(did);
216✔
432

433
    if (!crypt || !crypt.cipher_hash) {
216✔
434
        throw "DID is not encrypted";
8✔
435
    }
436

437
    const doc = await resolveDID(crypt.sender, crypt.created);
208✔
438
    const publicJwk = doc.didDocument.verificationMethod[0].publicKeyJwk;
208✔
439
    const hdkey = cipher.generateHDKeyJSON(wallet.seed.hdkey);
208✔
440
    const ciphertext = (crypt.sender === id.did && crypt.cipher_sender) ? crypt.cipher_sender : crypt.cipher_receiver;
208✔
441

442
    let index = id.index;
208✔
443
    while (index >= 0) {
208✔
444
        const path = `m/44'/0'/${id.account}'/0/${index}`;
256✔
445
        const didkey = hdkey.derive(path);
256✔
446
        const keypair = cipher.generateJwk(didkey.privateKey);
256✔
447
        try {
256✔
448
            return cipher.decryptMessage(publicJwk, keypair.privateJwk, ciphertext);
256✔
449
        }
450
        catch (error) {
451
            index -= 1;
50✔
452
        }
453
    }
454

455
    throw 'Cannot decrypt';
2✔
456
}
457

458
export async function encryptJSON(json, did, registry = defaultRegistry) {
2✔
459
    const plaintext = JSON.stringify(json);
112✔
460
    return encrypt(plaintext, did, registry);
112✔
461
}
462

463
export async function decryptJSON(did) {
464
    const plaintext = await decrypt(did);
186✔
465
    return JSON.parse(plaintext);
176✔
466
}
467

468
export async function addSignature(obj, controller = null) {
48✔
469
    // Fetches current ID if name is missing
470
    const id = fetchId(controller);
698✔
471
    const keypair = fetchKeyPair(controller);
694✔
472

473
    try {
694✔
474
        const msgHash = cipher.hashJSON(obj);
694✔
475
        const signature = cipher.signHash(msgHash, keypair.privateJwk);
692✔
476

477
        return {
692✔
478
            ...obj,
479
            signature: {
480
                signer: id.did,
481
                signed: new Date().toISOString(),
482
                hash: msgHash,
483
                value: signature,
484
            }
485
        };
486
    }
487
    catch (error) {
488
        throw 'Invalid input';
2✔
489
    }
490
}
491

492
export async function verifySignature(obj) {
493
    if (!obj?.signature) {
40✔
494
        return false;
6✔
495
    }
496

497
    const jsonCopy = JSON.parse(JSON.stringify(obj));
34✔
498
    const signature = jsonCopy.signature;
34✔
499
    delete jsonCopy.signature;
34✔
500
    const msgHash = cipher.hashJSON(jsonCopy);
34✔
501

502
    if (signature.hash && signature.hash !== msgHash) {
34!
503
        return false;
×
504
    }
505

506
    const doc = await resolveDID(signature.signer, signature.signed);
34✔
507

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

511
    try {
34✔
512
        return cipher.verifySig(msgHash, signature.value, publicJwk);
34✔
513
    }
514
    catch (error) {
515
        return false;
2✔
516
    }
517
}
518

519
export async function updateDID(did, doc) {
520
    const current = await resolveDID(did);
144✔
521
    const prev = cipher.hashJSON(current);
144✔
522

523
    const operation = {
144✔
524
        type: "update",
525
        did: did,
526
        doc: doc,
527
        prev: prev,
528
    };
529

530
    const controller = current.didDocument.controller || current.didDocument.id;
144✔
531
    const signed = await addSignature(operation, controller);
144✔
532
    return gatekeeper.updateDID(signed);
144✔
533
}
534

535
export async function revokeDID(did) {
536
    const current = await resolveDID(did);
26✔
537
    const prev = cipher.hashJSON(current);
26✔
538

539
    const operation = {
26✔
540
        type: "delete",
541
        did: did,
542
        prev: prev,
543
    };
544

545
    const controller = current.didDocument.controller || current.didDocument.id;
26✔
546
    const signed = await addSignature(operation, controller);
26✔
547
    const ok = gatekeeper.deleteDID(signed);
24✔
548

549
    if (ok && current.didDocument.controller) {
24✔
550
        removeFromOwned(did, current.didDocument.controller);
22✔
551
    }
552

553
    return ok;
24✔
554
}
555

556
function addToOwned(did) {
557
    const wallet = loadWallet();
496✔
558
    const id = wallet.ids[wallet.current];
496✔
559
    const owned = new Set(id.owned);
496✔
560

561
    owned.add(did);
496✔
562
    id.owned = Array.from(owned);
496✔
563

564
    saveWallet(wallet);
496✔
565
    return true;
496✔
566
}
567

568
function removeFromOwned(did, owner) {
569
    const wallet = loadWallet();
22✔
570
    const id = fetchId(owner);
22✔
571

572
    id.owned = id.owned.filter(item => item !== did);
64✔
573

574
    saveWallet(wallet);
22✔
575
    return true;
22✔
576
}
577

578
function addToHeld(did) {
579
    const wallet = loadWallet();
62✔
580
    const id = wallet.ids[wallet.current];
62✔
581
    const held = new Set(id.held);
62✔
582

583
    held.add(did);
62✔
584
    id.held = Array.from(held);
62✔
585

586
    saveWallet(wallet);
62✔
587
    return true;
62✔
588
}
589

590
function removeFromHeld(did) {
591
    const wallet = loadWallet();
6✔
592
    const id = wallet.ids[wallet.current];
6✔
593
    const held = new Set(id.held);
6✔
594

595
    if (held.delete(did)) {
6✔
596
        id.held = Array.from(held);
4✔
597
        saveWallet(wallet);
4✔
598
        return true;
4✔
599
    }
600

601
    return false;
2✔
602
}
603

604
export async function resolveDID(did, asof, confirm) {
605
    const doc = await gatekeeper.resolveDID(lookupDID(did), asof, confirm);
1,626✔
606
    return doc;
1,612✔
607
}
608

609
export async function resolveAsset(did) {
610
    const doc = await resolveDID(did);
450✔
611

612
    if (doc?.didDocumentMetadata) {
446!
613
        if (!doc.didDocumentMetadata.deactivated) {
446✔
614
            return doc.didDocumentData;
440✔
615
        }
616
    }
617

618
    return null;
6✔
619
}
620

621
export async function createId(name, registry = defaultRegistry) {
200✔
622
    const wallet = loadWallet();
400✔
623
    if (wallet.ids && Object.keys(wallet.ids).includes(name)) {
400✔
624
        throw `Already have an ID named ${name}`;
2✔
625
    }
626

627
    const account = wallet.counter;
398✔
628
    const index = 0;
398✔
629
    const hdkey = cipher.generateHDKeyJSON(wallet.seed.hdkey);
398✔
630
    const path = `m/44'/0'/${account}'/0/${index}`;
398✔
631
    const didkey = hdkey.derive(path);
398✔
632
    const keypair = cipher.generateJwk(didkey.privateKey);
398✔
633

634
    const operation = {
398✔
635
        type: "create",
636
        created: new Date().toISOString(),
637
        mdip: {
638
            version: 1,
639
            type: "agent",
640
            registry: registry,
641
        },
642
        publicJwk: keypair.publicJwk,
643
    };
644

645
    const msgHash = cipher.hashJSON(operation);
398✔
646
    const signature = cipher.signHash(msgHash, keypair.privateJwk);
398✔
647
    const signed = {
398✔
648
        ...operation,
649
        signature: {
650
            signed: new Date().toISOString(),
651
            hash: msgHash,
652
            value: signature
653
        }
654
    }
655
    const did = await gatekeeper.createDID(signed);
398✔
656

657
    const newId = {
398✔
658
        did: did,
659
        account: account,
660
        index: index,
661
    };
662

663
    wallet.ids[name] = newId;
398✔
664
    wallet.counter += 1;
398✔
665
    wallet.current = name;
398✔
666
    saveWallet(wallet);
398✔
667

668
    return did;
398✔
669
}
670

671
export function removeId(name) {
672
    const wallet = loadWallet();
6✔
673
    let ids = Object.keys(wallet.ids);
6✔
674

675
    if (ids.includes(name)) {
6✔
676
        delete wallet.ids[name];
4✔
677

678
        if (wallet.current === name) {
4✔
679
            ids = Object.keys(wallet.ids);
2✔
680
            wallet.current = ids.length > 0 ? ids[0] : '';
2!
681
        }
682

683
        saveWallet(wallet);
4✔
684
        return true;
4✔
685
    }
686
    else {
687
        throw `No ID named ${name}`;
2✔
688
    }
689
}
690

691
export async function resolveId(name) {
692
    const id = fetchId(name);
6✔
693
    return resolveDID(id.did);
6✔
694
}
695

696
export async function backupId(name = null) {
3✔
697
    // Backs up current ID if name is missing
698
    const id = fetchId(name);
8✔
699
    const wallet = loadWallet();
8✔
700
    const keypair = hdKeyPair();
8✔
701
    const data = {
8✔
702
        name: name || wallet.current,
7✔
703
        id: id,
704
    };
705
    const msg = JSON.stringify(data);
8✔
706
    const backup = cipher.encryptMessage(keypair.publicJwk, keypair.privateJwk, msg);
8✔
707
    const doc = await resolveDID(id.did);
8✔
708
    const registry = doc.mdip.registry;
8✔
709
    const vaultDid = await createAsset({ backup: backup }, registry, name);
8✔
710

711
    doc.didDocumentData.vault = vaultDid;
8✔
712
    const ok = await updateDID(id.did, doc);
8✔
713

714
    return ok;
8✔
715
}
716

717
export async function recoverId(did) {
718
    try {
4✔
719
        const wallet = loadWallet();
4✔
720
        const keypair = hdKeyPair();
4✔
721
        const doc = await resolveDID(did);
4✔
722
        const vault = await resolveAsset(doc.didDocumentData.vault);
4✔
723
        const backup = cipher.decryptMessage(keypair.publicJwk, keypair.privateJwk, vault.backup);
4✔
724
        const data = JSON.parse(backup);
2✔
725

726
        // TBD handle the case where name already exists in wallet
727
        wallet.ids[data.name] = data.id;
2✔
728
        wallet.current = data.name;
2✔
729
        wallet.counter += 1;
2✔
730

731
        saveWallet(wallet);
2✔
732

733
        return `Recovered ${data.name}!`;
2✔
734
    }
735
    catch {
736
        throw "Cannot recover ID";
2✔
737
    }
738
}
739

740
export async function rotateKeys() {
741
    const wallet = loadWallet();
46✔
742
    const id = wallet.ids[wallet.current];
46✔
743
    const nextIndex = id.index + 1;
46✔
744
    const hdkey = cipher.generateHDKeyJSON(wallet.seed.hdkey);
46✔
745
    const path = `m/44'/0'/${id.account}'/0/${nextIndex}`;
46✔
746
    const didkey = hdkey.derive(path);
46✔
747
    const keypair = cipher.generateJwk(didkey.privateKey);
46✔
748
    const doc = await resolveDID(id.did);
46✔
749
    const vmethod = doc.didDocument.verificationMethod[0];
46✔
750

751
    vmethod.id = `#key-${nextIndex + 1}`;
46✔
752
    vmethod.publicKeyJwk = keypair.publicJwk;
46✔
753
    doc.didDocument.authentication = [vmethod.id];
46✔
754

755
    const ok = await updateDID(id.did, doc);
46✔
756

757
    if (ok) {
46!
758
        id.index = nextIndex;
46✔
759
        saveWallet(wallet);
46✔
760
        return doc;
46✔
761
    }
762
    else {
763
        throw 'cannot rotate keys';
×
764
    }
765
}
766

767
export function listNames() {
768
    const wallet = loadWallet();
4✔
769

770
    return wallet.names || {};
4✔
771
}
772

773
export function addName(name, did) {
774
    const wallet = loadWallet();
36✔
775

776
    if (!wallet.names) {
36✔
777
        wallet.names = {};
16✔
778
    }
779

780
    if (Object.keys(wallet.names).includes(name)) {
36✔
781
        throw `Name already in use`;
2✔
782
    }
783

784
    wallet.names[name] = did;
34✔
785
    saveWallet(wallet);
34✔
786

787
    return true;
34✔
788
}
789

790
export function removeName(name) {
791
    const wallet = loadWallet();
4✔
792

793
    if (wallet.names) {
4✔
794
        if (Object.keys(wallet.names).includes(name)) {
2!
795
            delete wallet.names[name];
2✔
796
            saveWallet(wallet);
2✔
797
        }
798
    }
799

800
    return true;
4✔
801
}
802

803
export function lookupDID(name) {
804
    try {
2,470✔
805
        if (name.startsWith('did:')) {
2,470✔
806
            return name;
2,402✔
807
        }
808
    }
809
    catch {
810
        throw "Invalid DID";
42✔
811
    }
812

813
    const wallet = loadWallet();
26✔
814

815
    if (wallet.names) {
26✔
816
        if (Object.keys(wallet.names).includes(name)) {
10!
817
            return wallet.names[name];
10✔
818
        }
819
    }
820

821
    if (wallet.ids) {
16!
822
        if (Object.keys(wallet.ids).includes(name)) {
16✔
823
            return wallet.ids[name].did;
2✔
824
        }
825
    }
826

827
    throw "Unknown DID";
14✔
828
}
829

830
export async function createAsset(data, registry = defaultRegistry, name = null) {
354✔
831

832
    function isEmpty(data) {
833
        if (!data) return true;
444✔
834
        if (Array.isArray(data) && data.length === 0) return true;
440✔
835
        if (typeof data === 'object' && Object.keys(data).length === 0) return true;
438✔
836
        return false;
436✔
837
    }
838

839
    if (isEmpty(data)) {
444✔
840
        throw 'Invalid input';
8✔
841
    }
842

843
    const id = fetchId(name);
436✔
844

845
    const operation = {
434✔
846
        type: "create",
847
        created: new Date().toISOString(),
848
        mdip: {
849
            version: 1,
850
            type: "asset",
851
            registry: registry,
852
        },
853
        controller: id.did,
854
        data: data,
855
    };
856

857
    const signed = await addSignature(operation, name);
434✔
858
    const did = await gatekeeper.createDID(signed);
434✔
859

860
    // Keep assets that will be garbage-collected out of the owned list
861
    if (registry !== 'hyperswarm') {
434✔
862
        addToOwned(did);
412✔
863
    }
864

865
    return did;
434✔
866
}
867

868
export async function testAgent(id) {
869
    const doc = await resolveDID(id);
8✔
870
    return doc?.mdip?.type === 'agent';
4✔
871
}
872

873
export async function createCredential(schema) {
874
    // TBD validate schema
875
    return createAsset(schema);
98✔
876
}
877

878
export async function bindCredential(schemaId, subjectId, validUntil = null) {
45✔
879
    const id = fetchId();
90✔
880
    const type = lookupDID(schemaId);
90✔
881
    const schema = await resolveAsset(type);
90✔
882
    const credential = JSONSchemaFaker.generate(schema);
90✔
883

884
    const vc = {
90✔
885
        "@context": [
886
            "https://www.w3.org/ns/credentials/v2",
887
            "https://www.w3.org/ns/credentials/examples/v2"
888
        ],
889
        type: ["VerifiableCredential", type],
890
        issuer: id.did,
891
        validFrom: new Date().toISOString(),
892
        validUntil: validUntil,
893
        credentialSubject: {
894
            id: lookupDID(subjectId),
895
        },
896
        credential: credential,
897
    };
898

899
    return vc;
90✔
900
}
901

902
export async function issueCredential(vc, registry = defaultRegistry) {
43✔
903
    const id = fetchId();
86✔
904

905
    if (vc.issuer !== id.did) {
86✔
906
        throw 'Invalid VC';
2✔
907
    }
908

909
    // Don't allow credentials that will be garbage-collected
910
    if (registry === 'hyperswarm') {
84!
911
        throw 'Invalid VC';
×
912
    }
913

914
    const signed = await addSignature(vc);
84✔
915
    const cipherDid = await encryptJSON(signed, vc.credentialSubject.id, registry);
84✔
916
    addToOwned(cipherDid);
84✔
917
    return cipherDid;
84✔
918
}
919

920
export async function listIssued(issuer) {
921
    const id = fetchId(issuer);
4✔
922
    const issued = [];
4✔
923

924
    if (id.owned) {
4✔
925
        for (const did of id.owned) {
2✔
926
            try {
4✔
927
                const credential = await decryptJSON(did);
4✔
928

929
                if (credential.issuer === id.did) {
2!
930
                    issued.push(did);
2✔
931
                }
932
            }
933
            catch (error) {
934
                continue;
2✔
935
            }
936
        }
937
    }
938

939
    return issued;
4✔
940
}
941

942
export async function revokeCredential(did) {
943
    const credential = lookupDID(did);
22✔
944
    return revokeDID(credential);
22✔
945
}
946

947
export async function acceptCredential(did) {
948
    try {
66✔
949
        const id = fetchId();
66✔
950
        const credential = lookupDID(did);
66✔
951
        const vc = await decryptJSON(credential);
66✔
952

953
        if (vc.credentialSubject.id !== id.did) {
62!
954
            throw 'VC not valid or not assigned to this ID';
×
955
        }
956

957
        return addToHeld(credential);
62✔
958
    }
959
    catch (error) {
960
        return false;
4✔
961
    }
962
}
963

964
export async function getCredential(did) {
965
    return decryptJSON(lookupDID(did));
12✔
966
}
967

968
export async function removeCredential(did) {
969
    return removeFromHeld(lookupDID(did));
10✔
970
}
971

972
export async function listCredentials(id) {
973
    return fetchId(id).held || [];
8✔
974
}
975

976
export async function publishCredential(did, reveal = false) {
×
977
    try {
6✔
978
        const id = fetchId();
6✔
979
        const credential = lookupDID(did);
6✔
980
        const vc = await decryptJSON(credential);
6✔
981

982
        if (vc.credentialSubject.id !== id.did) {
6!
983
            throw 'VC not valid or not assigned to this ID';
×
984
        }
985

986
        const doc = await resolveDID(id.did);
6✔
987

988
        if (!doc.didDocumentData.manifest) {
6!
989
            doc.didDocumentData.manifest = {};
6✔
990
        }
991

992
        if (!reveal) {
6✔
993
            // Remove the credential values
994
            vc.credential = null;
2✔
995
        }
996

997
        doc.didDocumentData.manifest[credential] = vc;
6✔
998

999
        const ok = await updateDID(id.did, doc);
6✔
1000

1001
        if (ok) {
6!
1002
            return vc;
6✔
1003
        }
1004
        else {
1005
            return "Update failed";
×
1006
        }
1007
    }
1008
    catch (error) {
1009
        return error;
×
1010
    }
1011
}
1012

1013
export async function unpublishCredential(did) {
1014
    const id = fetchId();
8✔
1015
    const doc = await resolveDID(id.did);
6✔
1016
    const credential = lookupDID(did);
6✔
1017
    const manifest = doc.didDocumentData.manifest;
6✔
1018

1019
    if (credential && manifest && Object.keys(manifest).includes(credential)) {
6✔
1020
        delete manifest[credential];
2✔
1021
        await updateDID(id.did, doc);
2✔
1022

1023
        return `OK credential ${did} removed from manifest`;
2✔
1024
    }
1025

1026
    throw `Error: credential ${did} not found in manifest`;
4✔
1027
}
1028

1029
export async function createChallenge(challenge) {
1030

1031
    if (!challenge) {
26✔
1032
        challenge = { credentials: [] };
2✔
1033
    }
1034

1035
    // TBD: replace with challenge schema validation
1036

1037
    if (!challenge?.credentials) {
26✔
1038
        throw "Invalid input";
4✔
1039
    }
1040

1041
    if (!Array.isArray(challenge.credentials)) {
22✔
1042
        throw "Invalid input";
2✔
1043
    }
1044

1045
    return createAsset(challenge, ephemeralRegistry);
20✔
1046
}
1047

1048
async function findMatchingCredential(credential) {
1049
    const id = fetchId();
14✔
1050

1051
    if (!id.held) {
14✔
1052
        return;
2✔
1053
    }
1054

1055
    for (let did of id.held) {
12✔
1056
        try {
24✔
1057
            const doc = await decryptJSON(did);
24✔
1058

1059
            // console.log(doc);
1060

1061
            if (!doc.issuer) {
24!
1062
                // Not a VC
1063
                continue;
×
1064
            }
1065

1066
            if (doc.credentialSubject?.id !== id.did) {
24!
1067
                // This VC is issued by the ID, not held
1068
                continue;
×
1069
            }
1070

1071
            if (credential.issuers) {
24✔
1072
                if (!credential.issuers.includes(doc.issuer)) {
22✔
1073
                    // Attestor not trusted by Verifier
1074
                    continue;
8✔
1075
                }
1076
            }
1077

1078
            if (doc.type) {
16!
1079
                if (!doc.type.includes(credential.schema)) {
16✔
1080
                    // Wrong type
1081
                    continue;
4✔
1082
                }
1083
            }
1084

1085
            // TBD test for VC expiry too
1086
            return did;
12✔
1087
        }
1088
        catch (error) {
1089
            // Not encrypted, so can't be a VC
1090
        }
1091
    }
1092
}
1093

1094
export async function createResponse(did) {
1095
    const challenge = lookupDID(did);
14✔
1096

1097
    if (!challenge) {
14!
1098
        throw "Invalid challenge";
×
1099
    }
1100

1101
    const doc = await resolveDID(challenge);
14✔
1102
    const requestor = doc.didDocument.controller;
14✔
1103
    const { credentials } = await resolveAsset(challenge);
14✔
1104

1105
    if (!credentials) {
14!
1106
        throw "Invalid challenge";
×
1107
    }
1108

1109
    const matches = [];
14✔
1110

1111
    for (let credential of credentials) {
14✔
1112
        const vc = await findMatchingCredential(credential);
14✔
1113

1114
        if (vc) {
14✔
1115
            matches.push(vc);
12✔
1116
        }
1117
    }
1118

1119
    if (!matches) {
14!
1120
        throw "VCs don't match challenge";
×
1121
    }
1122

1123
    const pairs = [];
14✔
1124

1125
    for (let vcDid of matches) {
14✔
1126
        const plaintext = await decrypt(vcDid);
12✔
1127
        const vpDid = await encrypt(plaintext, requestor);
12✔
1128
        pairs.push({ vc: vcDid, vp: vpDid });
12✔
1129
    }
1130

1131
    const requested = credentials.length;
14✔
1132
    const fulfilled = matches.length;
14✔
1133
    const match = (requested === fulfilled);
14✔
1134
    const response = {
14✔
1135
        challenge: challenge,
1136
        credentials: pairs,
1137
        requested: requested,
1138
        fulfilled: fulfilled,
1139
        match: match,
1140
    };
1141

1142
    const responseDid = await encryptJSON(response, requestor, ephemeralRegistry);
14✔
1143

1144
    return responseDid;
14✔
1145
}
1146

1147
export async function verifyResponse(responseDID, challengeDID) {
1148
    responseDID = lookupDID(responseDID);
18✔
1149
    challengeDID = lookupDID(challengeDID);
18✔
1150

1151
    if (!responseDID) {
18!
1152
        throw "Invalid response";
×
1153
    }
1154

1155
    const response = await decryptJSON(responseDID);
18✔
1156
    const challenge = await resolveAsset(challengeDID);
18✔
1157

1158
    if (response.challenge !== challengeDID) {
18✔
1159
        response.match = false;
4✔
1160
        return response;
4✔
1161
    }
1162

1163
    const vps = [];
14✔
1164

1165
    for (let credential of response.credentials) {
14✔
1166
        const vcData = await resolveAsset(credential.vc);
34✔
1167
        const vpData = await resolveAsset(credential.vp);
34✔
1168

1169
        if (!vcData) {
34✔
1170
            // revoked
1171
            continue;
6✔
1172
        }
1173

1174
        if (vcData.cipher_hash !== vpData.cipher_hash) {
28!
1175
            continue;
×
1176
        }
1177

1178
        const vp = await decryptJSON(credential.vp);
28✔
1179
        const isValid = await verifySignature(vp);
28✔
1180

1181
        if (!isValid) {
28!
1182
            continue;
×
1183
        }
1184

1185
        // Check VP against VCs specified in challenge
1186
        if (vp.type.length > 1 && vp.type[1].startsWith('did:')) {
28!
1187
            const schema = vp.type[1];
28✔
1188
            const credential = challenge.credentials.find(item => item.schema === schema);
72✔
1189

1190
            if (!credential) {
28!
1191
                continue;
×
1192
            }
1193

1194
            // Check if issuer of VP is in the trusted issuer list
1195
            if (credential.issuers && credential.issuers.length > 0) {
28✔
1196
                if (!credential.issuers.includes(vp.issuer)) {
26!
1197
                    continue;
×
1198
                }
1199
            }
1200
        }
1201

1202
        vps.push(vp);
28✔
1203
    }
1204

1205
    response.vps = vps;
14✔
1206
    response.match = vps.length === challenge.credentials.length;
14✔
1207

1208
    return response;
14✔
1209
}
1210

1211
export async function exportDID(did) {
1212
    return gatekeeper.exportDID(lookupDID(did));
6✔
1213
}
1214

1215
export async function importDID(events) {
1216
    return gatekeeper.importBatch(events);
6✔
1217
}
1218

1219
export async function createGroup(name) {
1220
    const group = {
76✔
1221
        name: name,
1222
        members: []
1223
    };
1224

1225
    return createAsset(group);
76✔
1226
}
1227

1228
export async function getGroup(id) {
1229
    const isGroup = await groupTest(id);
6✔
1230

1231
    if (!isGroup) {
4✔
1232
        throw "Invalid group";
2✔
1233
    }
1234

1235
    return resolveAsset(id);
2✔
1236
}
1237

1238
export async function groupAdd(groupId, memberId) {
1239
    const groupDID = lookupDID(groupId);
92✔
1240
    const doc = await resolveDID(groupDID);
82✔
1241
    const data = doc.didDocumentData;
82✔
1242

1243
    if (!data.members || !Array.isArray(data.members)) {
82✔
1244
        throw "Invalid group";
4✔
1245
    }
1246

1247
    const memberDID = lookupDID(memberId);
78✔
1248

1249
    try {
68✔
1250
        // test for valid member DID
1251
        await resolveDID(memberDID);
68✔
1252
    }
1253
    catch {
1254
        throw "Invalid DID";
2✔
1255
    }
1256

1257
    // If already a member, return immediately
1258
    if (data.members.includes(memberDID)) {
66✔
1259
        return data;
8✔
1260
    }
1261

1262
    // Can't add a group to itself
1263
    if (memberDID === groupDID) {
58✔
1264
        throw "Invalid member";
2✔
1265
    }
1266

1267
    // Can't add a mutual membership relation
1268
    const isMember = await groupTest(memberId, groupId);
56✔
1269

1270
    if (isMember) {
56✔
1271
        throw "Invalid member";
2✔
1272
    }
1273

1274
    const members = new Set(data.members);
54✔
1275
    members.add(memberDID);
54✔
1276
    data.members = Array.from(members);
54✔
1277

1278
    const ok = await updateDID(groupDID, doc);
54✔
1279

1280
    if (!ok) {
54!
1281
        throw `Error: can't update ${groupId}`
×
1282
    }
1283

1284
    return data;
54✔
1285
}
1286

1287
export async function groupRemove(groupId, memberId) {
1288
    const groupDID = lookupDID(groupId);
32✔
1289
    const doc = await resolveDID(groupDID);
22✔
1290
    const data = doc.didDocumentData;
22✔
1291

1292
    if (!data.members) {
22✔
1293
        throw "Invalid group";
4✔
1294
    }
1295

1296
    const memberDID = lookupDID(memberId);
18✔
1297

1298
    try {
10✔
1299
        // test for valid member DID
1300
        await resolveDID(memberDID);
10✔
1301
    }
1302
    catch {
1303
        throw "Invalid DID";
2✔
1304
    }
1305

1306
    // If not already a member, return immediately
1307
    if (!data.members.includes(memberDID)) {
8✔
1308
        return data;
4✔
1309
    }
1310

1311
    const members = new Set(data.members);
4✔
1312
    members.delete(memberDID);
4✔
1313
    data.members = Array.from(members);
4✔
1314

1315
    const ok = await updateDID(groupDID, doc);
4✔
1316

1317
    if (!ok) {
4!
1318
        throw `Error: can't update ${groupId}`
×
1319
    }
1320

1321
    return data;
4✔
1322
}
1323

1324
export async function groupTest(group, member) {
1325
    const didGroup = lookupDID(group);
148✔
1326

1327
    if (!didGroup) {
146!
1328
        return false;
×
1329
    }
1330

1331
    const doc = await resolveDID(didGroup);
146✔
1332

1333
    if (!doc) {
146!
1334
        return false;
×
1335
    }
1336

1337
    const data = doc.didDocumentData;
146✔
1338

1339
    if (!data) {
146!
1340
        return false;
×
1341
    }
1342

1343
    if (!Array.isArray(data.members)) {
146✔
1344
        return false;
46✔
1345
    }
1346

1347
    if (!member) {
100✔
1348
        return true;
30✔
1349
    }
1350

1351
    const didMember = lookupDID(member);
70✔
1352
    let isMember = data.members.includes(didMember);
70✔
1353

1354
    if (!isMember) {
70✔
1355
        for (const did of data.members) {
30✔
1356
            isMember = await groupTest(did, didMember);
14✔
1357

1358
            if (isMember) {
14!
1359
                break;
14✔
1360
            }
1361
        }
1362
    }
1363

1364
    return isMember;
70✔
1365
}
1366

1367
export const defaultSchema = {
2✔
1368
    "$schema": "http://json-schema.org/draft-07/schema#",
1369
    "type": "object",
1370
    "properties": {
1371
        "propertyName": {
1372
            "type": "string"
1373
        }
1374
    },
1375
    "required": [
1376
        "propertyName"
1377
    ]
1378
};
1379

1380
function validateSchema(schema) {
1381
    try {
22✔
1382
        if (!Object.keys(schema).includes('$schema')) {
22✔
1383
            throw "Invalid schema";
4✔
1384
        }
1385

1386
        // Attempt to instantiate the schema
1387
        JSONSchemaFaker.generate(schema);
18✔
1388
    }
1389
    catch {
1390
        throw "Invalid schema";
4✔
1391
    }
1392

1393
    return true;
18✔
1394
}
1395

1396
export async function createSchema(schema) {
1397
    if (!schema) {
14✔
1398
        schema = defaultSchema;
8✔
1399
    }
1400

1401
    validateSchema(schema);
14✔
1402

1403
    return createAsset(schema);
12✔
1404
}
1405

1406
export async function getSchema(id) {
1407
    return resolveAsset(id);
12✔
1408
}
1409

1410
export async function setSchema(id, newSchema) {
1411
    validateSchema(newSchema);
6✔
1412

1413
    const doc = await resolveDID(id);
4✔
1414
    const did = doc.didDocument.id;
4✔
1415
    doc.didDocumentData = newSchema;
4✔
1416

1417
    return updateDID(did, doc);
4✔
1418
}
1419

1420
// TBD add optional 2nd parameter that will validate JSON against the schema
1421
export async function testSchema(id) {
1422
    const schema = await getSchema(id);
6✔
1423

1424
    // TBD Need a better way because any random object with keys can be a valid schema
1425
    if (Object.keys(schema).length === 0) {
4✔
1426
        return false;
2✔
1427
    }
1428

1429
    return validateSchema(schema);
2✔
1430
}
1431

1432
export async function createTemplate(schemaId) {
1433
    const schemaDID = lookupDID(schemaId);
×
1434
    const schema = await resolveAsset(schemaDID);
×
1435
    const template = JSONSchemaFaker.generate(schema);
×
1436

1437
    template['$schema'] = schemaDID;
×
1438

1439
    return template;
×
1440
}
1441

1442
export async function pollTemplate() {
1443
    const now = new Date();
24✔
1444
    const nextWeek = new Date();
24✔
1445
    nextWeek.setDate(now.getDate() + 7);
24✔
1446

1447
    return {
24✔
1448
        type: 'poll',
1449
        version: 1,
1450
        description: 'What is this poll about?',
1451
        roster: 'DID of the eligible voter group',
1452
        options: ['yes', 'no', 'abstain'],
1453
        deadline: nextWeek.toISOString(),
1454
    };
1455
}
1456

1457
export async function createPoll(poll) {
1458
    if (poll.type !== 'poll') {
42✔
1459
        throw "Invalid poll type";
2✔
1460
    }
1461

1462
    if (poll.version !== 1) {
40✔
1463
        throw "Invalid poll version";
2✔
1464
    }
1465

1466
    if (!poll.description) {
38✔
1467
        throw "Invalid poll description";
2✔
1468
    }
1469

1470
    if (!poll.options || !Array.isArray(poll.options) || poll.options.length < 2 || poll.options.length > 10) {
36✔
1471
        throw "Invalid poll options";
8✔
1472
    }
1473

1474
    if (!poll.roster) {
28✔
1475
        throw "Invalid poll roster";
2✔
1476
    }
1477

1478
    try {
26✔
1479
        const isValidGroup = await groupTest(poll.roster);
26✔
1480

1481
        if (!isValidGroup) {
26!
UNCOV
1482
            throw "Invalid poll roster";
×
1483
        }
1484
    }
1485
    catch {
UNCOV
1486
        throw "Invalid poll roster";
×
1487
    }
1488

1489
    if (!poll.deadline) {
26✔
1490
        throw "Invalid poll deadline";
2✔
1491
    }
1492

1493
    const deadline = new Date(poll.deadline);
24✔
1494

1495
    if (isNaN(deadline.getTime())) {
24✔
1496
        throw "Invalid poll deadline";
2✔
1497
    }
1498

1499
    if (deadline < new Date()) {
22✔
1500
        throw "Invalid poll deadline";
2✔
1501
    }
1502

1503
    return createAsset(poll);
20✔
1504
}
1505

1506
export async function viewPoll(poll) {
1507
    const id = fetchId();
8✔
1508
    const didPoll = lookupDID(poll);
8✔
1509
    const doc = await resolveDID(didPoll);
8✔
1510
    const data = doc.didDocumentData;
8✔
1511

1512
    if (!data || !data.options || !data.deadline) {
8!
UNCOV
1513
        throw "Invalid poll";
×
1514
    }
1515

1516
    let hasVoted = false;
8✔
1517

1518
    if (data.ballots) {
8✔
1519
        hasVoted = !!data.ballots[id.did];
6✔
1520
    }
1521

1522
    const voteExpired = Date(data.deadline) > new Date();
8✔
1523
    const isEligible = await groupTest(data.roster, id.did);
8✔
1524

1525
    const view = {
8✔
1526
        description: data.description,
1527
        options: data.options,
1528
        deadline: data.deadline,
1529
        isOwner: (doc.didDocument.controller === id.did),
1530
        isEligible: isEligible,
1531
        voteExpired: voteExpired,
1532
        hasVoted: hasVoted,
1533
    };
1534

1535
    if (id.did === doc.didDocument.controller) {
8!
1536
        let voted = 0;
8✔
1537

1538
        const results = {
8✔
1539
            tally: [],
1540
            ballots: [],
1541
        }
1542

1543
        results.tally.push({
8✔
1544
            vote: 0,
1545
            option: 'spoil',
1546
            count: 0,
1547
        });
1548

1549
        for (let i = 0; i < data.options.length; i++) {
8✔
1550
            results.tally.push({
24✔
1551
                vote: i + 1,
1552
                option: data.options[i],
1553
                count: 0,
1554
            });
1555
        }
1556

1557
        for (let voter in data.ballots) {
8✔
1558
            const ballot = data.ballots[voter];
6✔
1559
            const decrypted = await decryptJSON(ballot.ballot);
6✔
1560
            const vote = decrypted.vote;
6✔
1561
            results.ballots.push({
6✔
1562
                ...ballot,
1563
                voter: voter,
1564
                vote: vote,
1565
                option: data.options[vote - 1],
1566
            });
1567
            voted += 1;
6✔
1568
            results.tally[vote].count += 1;
6✔
1569
        }
1570

1571
        const roster = await resolveAsset(data.roster);
8✔
1572
        const total = roster.members.length;
8✔
1573

1574
        results.votes = {
8✔
1575
            eligible: total,
1576
            received: voted,
1577
            pending: total - voted,
1578
        };
1579
        results.final = voteExpired || (voted === total);
8✔
1580

1581
        view.results = results;
8✔
1582
    }
1583

1584
    return view;
8✔
1585
}
1586

1587
export async function votePoll(poll, vote, spoil = false) {
7✔
1588
    const id = fetchId();
14✔
1589
    const didPoll = lookupDID(poll);
14✔
1590
    const doc = await resolveDID(didPoll);
14✔
1591
    const data = doc.didDocumentData;
14✔
1592
    const eligible = await groupTest(data.roster, id.did);
14✔
1593
    const expired = (Date(data.deadline) > new Date());
14✔
1594
    const owner = doc.didDocument.controller;
14✔
1595

1596
    if (!eligible) {
14✔
1597
        throw "Not eligible to vote on this poll";
2✔
1598
    }
1599

1600
    if (expired) {
12!
UNCOV
1601
        throw "The deadline to vote has passed for this poll";
×
1602
    }
1603

1604
    let ballot;
1605

1606
    if (spoil) {
12!
UNCOV
1607
        ballot = {
×
1608
            poll: didPoll,
1609
            vote: 0,
1610
        };
1611
    }
1612
    else {
1613
        const max = data.options.length;
12✔
1614
        vote = parseInt(vote);
12✔
1615

1616
        if (!Number.isInteger(vote) || vote < 1 || vote > max) {
12✔
1617
            throw `Vote must be a number between 1 and ${max}`;
2✔
1618
        }
1619

1620
        ballot = {
10✔
1621
            poll: didPoll,
1622
            vote: vote,
1623
        };
1624
    }
1625

1626
    // Encrypt for receiver only
1627
    const didBallot = await encryptJSON(ballot, owner, false);
10✔
1628

1629
    return didBallot;
10✔
1630
}
1631

1632
export async function updatePoll(ballot) {
1633
    const id = fetchId();
10✔
1634

1635
    const didBallot = lookupDID(ballot);
10✔
1636
    const docBallot = await resolveDID(didBallot);
10✔
1637
    const didVoter = docBallot.didDocument.controller;
10✔
1638
    let dataBallot;
1639

1640
    try {
10✔
1641
        dataBallot = await decryptJSON(didBallot);
10✔
1642

1643
        if (!dataBallot.poll || !dataBallot.vote) {
8!
UNCOV
1644
            throw "Invalid ballot";
×
1645
        }
1646
    }
1647
    catch {
1648
        throw "Invalid ballot";
2✔
1649
    }
1650

1651
    const didPoll = lookupDID(dataBallot.poll);
8✔
1652
    const docPoll = await resolveDID(didPoll);
8✔
1653
    const dataPoll = docPoll.didDocumentData;
8✔
1654
    const didOwner = docPoll.didDocument.controller;
8✔
1655

1656
    if (id.did !== didOwner) {
8!
UNCOV
1657
        throw "Only poll owners can add a ballot";
×
1658
    }
1659

1660
    const eligible = await groupTest(dataPoll.roster, didVoter);
8✔
1661

1662
    if (!eligible) {
8!
UNCOV
1663
        throw "Voter not eligible to vote on this poll";
×
1664
    }
1665

1666
    const expired = (Date(dataPoll.deadline) > new Date());
8✔
1667

1668
    if (expired) {
8!
UNCOV
1669
        throw "The deadline to vote has passed for this poll";
×
1670
    }
1671

1672
    const max = dataPoll.options.length;
8✔
1673
    const vote = parseInt(dataBallot.vote);
8✔
1674

1675
    if (!vote || vote < 0 || vote > max) {
8!
UNCOV
1676
        throw "Invalid ballot vote";
×
1677
    }
1678

1679
    if (!dataPoll.ballots) {
8!
1680
        dataPoll.ballots = {};
8✔
1681
    }
1682

1683
    dataPoll.ballots[didVoter] = {
8✔
1684
        ballot: didBallot,
1685
        received: new Date().toISOString(),
1686
    };
1687

1688
    const ok = await updateDID(didPoll, docPoll);
8✔
1689

1690
    return ok;
8✔
1691
}
1692

1693
export async function publishPoll(poll, reveal = false) {
2✔
1694
    const id = fetchId();
6✔
1695
    const didPoll = lookupDID(poll);
6✔
1696
    const doc = await resolveDID(didPoll);
6✔
1697
    const owner = doc.didDocument.controller;
6✔
1698

1699
    if (id.did !== owner) {
6!
UNCOV
1700
        throw "Only poll owners can publish";
×
1701
    }
1702

1703
    const view = await viewPoll(poll);
6✔
1704

1705
    if (!view.results.final) {
6!
UNCOV
1706
        throw "Poll can be published only when results are final";
×
1707
    }
1708

1709
    if (!reveal) {
6✔
1710
        delete view.results.ballots;
4✔
1711
    }
1712

1713
    doc.didDocumentData.results = view.results;
6✔
1714

1715
    const ok = await updateDID(didPoll, doc);
6✔
1716

1717
    return ok;
6✔
1718
}
1719

1720
export async function unpublishPoll(poll) {
1721
    const id = fetchId();
2✔
1722
    const didPoll = lookupDID(poll);
2✔
1723
    const doc = await resolveDID(didPoll);
2✔
1724
    const owner = doc.didDocument.controller;
2✔
1725

1726
    if (id.did !== owner) {
2!
UNCOV
1727
        throw "Only poll owners can unpublish";
×
1728
    }
1729

1730
    delete doc.didDocumentData.results;
2✔
1731

1732
    const ok = await updateDID(didPoll, doc);
2✔
1733

1734
    return ok;
2✔
1735
}
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