@didcid/keymaster 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +109 -0
  3. package/dist/cjs/db/abstract-base.cjs +25 -0
  4. package/dist/cjs/db/cache.cjs +27 -0
  5. package/dist/cjs/db/chrome.cjs +32 -0
  6. package/dist/cjs/db/json-memory.cjs +24 -0
  7. package/dist/cjs/db/json.cjs +35 -0
  8. package/dist/cjs/db/mongo.cjs +57 -0
  9. package/dist/cjs/db/redis.cjs +55 -0
  10. package/dist/cjs/db/sqlite.cjs +69 -0
  11. package/dist/cjs/db/typeGuards.cjs +11 -0
  12. package/dist/cjs/db/web.cjs +29 -0
  13. package/dist/cjs/encryption.cjs +59 -0
  14. package/dist/cjs/index.cjs +32 -0
  15. package/dist/cjs/keymaster-client.cjs +1139 -0
  16. package/dist/cjs/keymaster.cjs +3787 -0
  17. package/dist/cjs/node.cjs +45 -0
  18. package/dist/cjs/search-client.cjs +87 -0
  19. package/dist/esm/db/abstract-base.js +22 -0
  20. package/dist/esm/db/abstract-base.js.map +1 -0
  21. package/dist/esm/db/cache.js +21 -0
  22. package/dist/esm/db/cache.js.map +1 -0
  23. package/dist/esm/db/chrome.js +26 -0
  24. package/dist/esm/db/chrome.js.map +1 -0
  25. package/dist/esm/db/json-memory.js +18 -0
  26. package/dist/esm/db/json-memory.js.map +1 -0
  27. package/dist/esm/db/json.js +29 -0
  28. package/dist/esm/db/json.js.map +1 -0
  29. package/dist/esm/db/mongo.js +51 -0
  30. package/dist/esm/db/mongo.js.map +1 -0
  31. package/dist/esm/db/redis.js +49 -0
  32. package/dist/esm/db/redis.js.map +1 -0
  33. package/dist/esm/db/sqlite.js +63 -0
  34. package/dist/esm/db/sqlite.js.map +1 -0
  35. package/dist/esm/db/typeGuards.js +7 -0
  36. package/dist/esm/db/typeGuards.js.map +1 -0
  37. package/dist/esm/db/web.js +23 -0
  38. package/dist/esm/db/web.js.map +1 -0
  39. package/dist/esm/encryption.js +55 -0
  40. package/dist/esm/encryption.js.map +1 -0
  41. package/dist/esm/index.js +11 -0
  42. package/dist/esm/index.js.map +1 -0
  43. package/dist/esm/keymaster-client.js +1133 -0
  44. package/dist/esm/keymaster-client.js.map +1 -0
  45. package/dist/esm/keymaster.js +2733 -0
  46. package/dist/esm/keymaster.js.map +1 -0
  47. package/dist/esm/node.js +7 -0
  48. package/dist/esm/node.js.map +1 -0
  49. package/dist/esm/search-client.js +81 -0
  50. package/dist/esm/search-client.js.map +1 -0
  51. package/dist/esm/types.js +2 -0
  52. package/dist/esm/types.js.map +1 -0
  53. package/dist/types/db/abstract-base.d.ts +7 -0
  54. package/dist/types/db/cache.d.ts +9 -0
  55. package/dist/types/db/chrome.d.ts +8 -0
  56. package/dist/types/db/json-memory.d.ts +7 -0
  57. package/dist/types/db/json.d.ts +9 -0
  58. package/dist/types/db/mongo.d.ts +15 -0
  59. package/dist/types/db/redis.d.ts +13 -0
  60. package/dist/types/db/sqlite.d.ts +12 -0
  61. package/dist/types/db/typeGuards.d.ts +3 -0
  62. package/dist/types/db/web.d.ts +8 -0
  63. package/dist/types/encryption.d.ts +10 -0
  64. package/dist/types/index.d.ts +10 -0
  65. package/dist/types/keymaster-client.d.ts +134 -0
  66. package/dist/types/keymaster.d.ts +211 -0
  67. package/dist/types/node.d.ts +6 -0
  68. package/dist/types/search-client.d.ts +9 -0
  69. package/dist/types/types.d.ts +373 -0
  70. package/package.json +171 -0
@@ -0,0 +1,2733 @@
1
+ import { imageSize } from 'image-size';
2
+ import { fileTypeFromBuffer } from 'file-type';
3
+ import { InvalidDIDError, InvalidParameterError, KeymasterError, UnknownIDError } from '@didcid/common/errors';
4
+ import { isWalletEncFile, isWalletFile } from './db/typeGuards.js';
5
+ import { isValidDID } from '@didcid/ipfs/utils';
6
+ import { decMnemonic, encMnemonic } from "./encryption.js";
7
+ const DefaultSchema = {
8
+ "$schema": "http://json-schema.org/draft-07/schema#",
9
+ "type": "object",
10
+ "properties": {
11
+ "propertyName": {
12
+ "type": "string"
13
+ }
14
+ },
15
+ "required": [
16
+ "propertyName"
17
+ ]
18
+ };
19
+ export var DmailTags;
20
+ (function (DmailTags) {
21
+ DmailTags["DMAIL"] = "dmail";
22
+ DmailTags["INBOX"] = "inbox";
23
+ DmailTags["DRAFT"] = "draft";
24
+ DmailTags["SENT"] = "sent";
25
+ DmailTags["ARCHIVED"] = "archived";
26
+ DmailTags["DELETED"] = "deleted";
27
+ DmailTags["UNREAD"] = "unread";
28
+ })(DmailTags || (DmailTags = {}));
29
+ export var NoticeTags;
30
+ (function (NoticeTags) {
31
+ NoticeTags["DMAIL"] = "dmail";
32
+ NoticeTags["BALLOT"] = "ballot";
33
+ NoticeTags["POLL"] = "poll";
34
+ NoticeTags["CREDENTIAL"] = "credential";
35
+ })(NoticeTags || (NoticeTags = {}));
36
+ export default class Keymaster {
37
+ passphrase;
38
+ gatekeeper;
39
+ db;
40
+ cipher;
41
+ searchEngine;
42
+ defaultRegistry;
43
+ ephemeralRegistry;
44
+ maxNameLength;
45
+ maxDataLength;
46
+ _walletCache;
47
+ _hdkeyCache;
48
+ constructor(options) {
49
+ if (!options || !options.gatekeeper || !options.gatekeeper.createDID) {
50
+ throw new InvalidParameterError('options.gatekeeper');
51
+ }
52
+ if (!options.wallet || !options.wallet.loadWallet || !options.wallet.saveWallet) {
53
+ throw new InvalidParameterError('options.wallet');
54
+ }
55
+ if (!options.cipher || !options.cipher.verifySig) {
56
+ throw new InvalidParameterError('options.cipher');
57
+ }
58
+ if (options.search && !options.search.search) {
59
+ throw new InvalidParameterError('options.search');
60
+ }
61
+ if (!options.passphrase) {
62
+ throw new InvalidParameterError('options.passphrase');
63
+ }
64
+ this.passphrase = options.passphrase;
65
+ this.gatekeeper = options.gatekeeper;
66
+ this.db = options.wallet;
67
+ this.cipher = options.cipher;
68
+ this.searchEngine = options.search;
69
+ this.defaultRegistry = options.defaultRegistry || 'hyperswarm';
70
+ this.ephemeralRegistry = 'hyperswarm';
71
+ this.maxNameLength = options.maxNameLength || 32;
72
+ this.maxDataLength = 8 * 1024; // 8 KB max data to store in a JSON object
73
+ }
74
+ async listRegistries() {
75
+ return this.gatekeeper.listRegistries();
76
+ }
77
+ async mutateWallet(mutator) {
78
+ // Create wallet if none and make sure _walletCache is set
79
+ if (!this._walletCache) {
80
+ await this.loadWallet();
81
+ }
82
+ await this.db.updateWallet(async (stored) => {
83
+ const decrypted = this._walletCache;
84
+ const before = JSON.stringify(decrypted);
85
+ await mutator(decrypted);
86
+ const after = JSON.stringify(decrypted);
87
+ if (before === after) {
88
+ return;
89
+ }
90
+ const reenc = await this.encryptWalletForStorage(decrypted);
91
+ Object.assign(stored, reenc);
92
+ this._walletCache = decrypted;
93
+ });
94
+ }
95
+ async loadWallet() {
96
+ if (this._walletCache) {
97
+ return this._walletCache;
98
+ }
99
+ let stored = await this.db.loadWallet();
100
+ if (!stored) {
101
+ stored = await this.newWallet();
102
+ }
103
+ const upgraded = await this.upgradeWallet(stored);
104
+ this._walletCache = await this.decryptWallet(upgraded);
105
+ return this._walletCache;
106
+ }
107
+ async saveWallet(wallet, overwrite = true) {
108
+ let upgraded = await this.upgradeWallet(wallet);
109
+ // Decrypt if encrypted to verify passphrase and get decrypted form
110
+ const decrypted = await this.decryptWallet(upgraded);
111
+ let toStore = await this.encryptWalletForStorage(decrypted);
112
+ const ok = await this.db.saveWallet(toStore, overwrite);
113
+ if (ok) {
114
+ this._walletCache = decrypted;
115
+ }
116
+ return ok;
117
+ }
118
+ async newWallet(mnemonic, overwrite = false) {
119
+ try {
120
+ if (!mnemonic) {
121
+ mnemonic = this.cipher.generateMnemonic();
122
+ }
123
+ this._hdkeyCache = this.cipher.generateHDKey(mnemonic);
124
+ }
125
+ catch (error) {
126
+ throw new InvalidParameterError('mnemonic');
127
+ }
128
+ const mnemonicEnc = await encMnemonic(mnemonic, this.passphrase);
129
+ const wallet = {
130
+ version: 1,
131
+ seed: { mnemonicEnc },
132
+ counter: 0,
133
+ ids: {}
134
+ };
135
+ const ok = await this.saveWallet(wallet, overwrite);
136
+ if (!ok) {
137
+ throw new KeymasterError('save wallet failed');
138
+ }
139
+ return wallet;
140
+ }
141
+ async decryptMnemonic() {
142
+ const wallet = await this.loadWallet();
143
+ return this.getMnemonicForDerivation(wallet);
144
+ }
145
+ async getMnemonicForDerivation(wallet) {
146
+ return decMnemonic(wallet.seed.mnemonicEnc, this.passphrase);
147
+ }
148
+ async checkWallet() {
149
+ const wallet = await this.loadWallet();
150
+ let checked = 0;
151
+ let invalid = 0;
152
+ let deleted = 0;
153
+ // Validate keys
154
+ await this.resolveSeedBank();
155
+ for (const name of Object.keys(wallet.ids)) {
156
+ try {
157
+ const doc = await this.resolveDID(wallet.ids[name].did);
158
+ if (doc.didDocumentMetadata?.deactivated) {
159
+ deleted += 1;
160
+ }
161
+ }
162
+ catch (error) {
163
+ invalid += 1;
164
+ }
165
+ checked += 1;
166
+ }
167
+ for (const id of Object.values(wallet.ids)) {
168
+ if (id.owned) {
169
+ for (const did of id.owned) {
170
+ try {
171
+ const doc = await this.resolveDID(did);
172
+ if (doc.didDocumentMetadata?.deactivated) {
173
+ deleted += 1;
174
+ }
175
+ }
176
+ catch (error) {
177
+ invalid += 1;
178
+ }
179
+ checked += 1;
180
+ }
181
+ }
182
+ if (id.held) {
183
+ for (const did of id.held) {
184
+ try {
185
+ const doc = await this.resolveDID(did);
186
+ if (doc.didDocumentMetadata?.deactivated) {
187
+ deleted += 1;
188
+ }
189
+ }
190
+ catch (error) {
191
+ invalid += 1;
192
+ }
193
+ checked += 1;
194
+ }
195
+ }
196
+ }
197
+ if (wallet.names) {
198
+ for (const name of Object.keys(wallet.names)) {
199
+ try {
200
+ const doc = await this.resolveDID(wallet.names[name]);
201
+ if (doc.didDocumentMetadata?.deactivated) {
202
+ deleted += 1;
203
+ }
204
+ }
205
+ catch (error) {
206
+ invalid += 1;
207
+ }
208
+ checked += 1;
209
+ }
210
+ }
211
+ return { checked, invalid, deleted };
212
+ }
213
+ async fixWallet() {
214
+ let idsRemoved = 0;
215
+ let ownedRemoved = 0;
216
+ let heldRemoved = 0;
217
+ let namesRemoved = 0;
218
+ await this.mutateWallet(async (wallet) => {
219
+ for (const name of Object.keys(wallet.ids)) {
220
+ let remove = false;
221
+ try {
222
+ const doc = await this.resolveDID(wallet.ids[name].did);
223
+ if (doc.didDocumentMetadata?.deactivated) {
224
+ remove = true;
225
+ }
226
+ }
227
+ catch {
228
+ remove = true;
229
+ }
230
+ if (remove) {
231
+ delete wallet.ids[name];
232
+ idsRemoved++;
233
+ }
234
+ }
235
+ for (const id of Object.values(wallet.ids)) {
236
+ if (id.owned) {
237
+ for (let i = 0; i < id.owned.length; i++) {
238
+ let remove = false;
239
+ try {
240
+ const doc = await this.resolveDID(id.owned[i]);
241
+ if (doc.didDocumentMetadata?.deactivated) {
242
+ remove = true;
243
+ }
244
+ }
245
+ catch {
246
+ remove = true;
247
+ }
248
+ if (remove) {
249
+ id.owned.splice(i, 1);
250
+ i--;
251
+ ownedRemoved++;
252
+ }
253
+ }
254
+ }
255
+ if (id.held) {
256
+ for (let i = 0; i < id.held.length; i++) {
257
+ let remove = false;
258
+ try {
259
+ const doc = await this.resolveDID(id.held[i]);
260
+ if (doc.didDocumentMetadata?.deactivated) {
261
+ remove = true;
262
+ }
263
+ }
264
+ catch {
265
+ remove = true;
266
+ }
267
+ if (remove) {
268
+ id.held.splice(i, 1);
269
+ i--;
270
+ heldRemoved++;
271
+ }
272
+ }
273
+ }
274
+ }
275
+ if (wallet.names) {
276
+ for (const name of Object.keys(wallet.names)) {
277
+ let remove = false;
278
+ try {
279
+ const doc = await this.resolveDID(wallet.names[name]);
280
+ if (doc.didDocumentMetadata?.deactivated) {
281
+ remove = true;
282
+ }
283
+ }
284
+ catch {
285
+ remove = true;
286
+ }
287
+ if (remove) {
288
+ delete wallet.names[name];
289
+ namesRemoved++;
290
+ }
291
+ }
292
+ }
293
+ });
294
+ return { idsRemoved, ownedRemoved, heldRemoved, namesRemoved };
295
+ }
296
+ async resolveSeedBank() {
297
+ const keypair = await this.hdKeyPair();
298
+ const operation = {
299
+ type: "create",
300
+ created: new Date(0).toISOString(),
301
+ registration: {
302
+ version: 1,
303
+ type: "agent",
304
+ registry: this.defaultRegistry,
305
+ },
306
+ publicJwk: keypair.publicJwk,
307
+ };
308
+ const msgHash = this.cipher.hashJSON(operation);
309
+ const signature = this.cipher.signHash(msgHash, keypair.privateJwk);
310
+ const signed = {
311
+ ...operation,
312
+ signature: {
313
+ signed: new Date(0).toISOString(),
314
+ hash: msgHash,
315
+ value: signature
316
+ }
317
+ };
318
+ const did = await this.gatekeeper.createDID(signed);
319
+ return this.gatekeeper.resolveDID(did);
320
+ }
321
+ async updateSeedBank(doc) {
322
+ const keypair = await this.hdKeyPair();
323
+ const did = doc.didDocument?.id;
324
+ if (!did) {
325
+ throw new InvalidParameterError('seed bank missing DID');
326
+ }
327
+ const current = await this.gatekeeper.resolveDID(did);
328
+ const previd = current.didDocumentMetadata?.versionId;
329
+ const operation = {
330
+ type: "update",
331
+ did,
332
+ previd,
333
+ doc,
334
+ };
335
+ const msgHash = this.cipher.hashJSON(operation);
336
+ const signature = this.cipher.signHash(msgHash, keypair.privateJwk);
337
+ const signed = {
338
+ ...operation,
339
+ signature: {
340
+ signer: did,
341
+ signed: new Date().toISOString(),
342
+ hash: msgHash,
343
+ value: signature,
344
+ }
345
+ };
346
+ return await this.gatekeeper.updateDID(signed);
347
+ }
348
+ async backupWallet(registry = this.defaultRegistry, wallet) {
349
+ if (!wallet) {
350
+ wallet = await this.loadWallet();
351
+ }
352
+ const keypair = await this.hdKeyPair();
353
+ const seedBank = await this.resolveSeedBank();
354
+ const msg = JSON.stringify(wallet);
355
+ const backup = this.cipher.encryptMessage(keypair.publicJwk, keypair.privateJwk, msg);
356
+ const operation = {
357
+ type: "create",
358
+ created: new Date().toISOString(),
359
+ registration: {
360
+ version: 1,
361
+ type: "asset",
362
+ registry: registry,
363
+ },
364
+ controller: seedBank.didDocument?.id,
365
+ data: { backup: backup },
366
+ };
367
+ const msgHash = this.cipher.hashJSON(operation);
368
+ const signature = this.cipher.signHash(msgHash, keypair.privateJwk);
369
+ const signed = {
370
+ ...operation,
371
+ signature: {
372
+ signer: seedBank.didDocument?.id,
373
+ signed: new Date().toISOString(),
374
+ hash: msgHash,
375
+ value: signature,
376
+ }
377
+ };
378
+ const backupDID = await this.gatekeeper.createDID(signed);
379
+ if (seedBank.didDocumentData && typeof seedBank.didDocumentData === 'object' && !Array.isArray(seedBank.didDocumentData)) {
380
+ const data = seedBank.didDocumentData;
381
+ data.wallet = backupDID;
382
+ await this.updateSeedBank(seedBank);
383
+ }
384
+ return backupDID;
385
+ }
386
+ async recoverWallet(did) {
387
+ try {
388
+ if (!did) {
389
+ const seedBank = await this.resolveSeedBank();
390
+ if (seedBank.didDocumentData && typeof seedBank.didDocumentData === 'object' && !Array.isArray(seedBank.didDocumentData)) {
391
+ const data = seedBank.didDocumentData;
392
+ did = data.wallet;
393
+ }
394
+ if (!did) {
395
+ throw new InvalidParameterError('No backup DID found');
396
+ }
397
+ }
398
+ const keypair = await this.hdKeyPair();
399
+ const data = await this.resolveAsset(did);
400
+ if (!data) {
401
+ throw new InvalidParameterError('No asset data found');
402
+ }
403
+ const castData = data;
404
+ if (typeof castData.backup !== 'string') {
405
+ throw new InvalidParameterError('Asset "backup" is missing or not a string');
406
+ }
407
+ const backup = this.cipher.decryptMessage(keypair.publicJwk, keypair.privateJwk, castData.backup);
408
+ let wallet = JSON.parse(backup);
409
+ if (isWalletFile(wallet)) {
410
+ const mnemonic = await this.decryptMnemonic();
411
+ // Backup might have a different mnemonic passphase so re-encrypt
412
+ wallet.seed.mnemonicEnc = await encMnemonic(mnemonic, this.passphrase);
413
+ }
414
+ await this.mutateWallet(async (current) => {
415
+ // Clear all existing properties from the current wallet
416
+ // This ensures a clean slate before restoring the recovered wallet
417
+ for (const k in current) {
418
+ delete current[k];
419
+ }
420
+ // Upgrade the recovered wallet to the latest version if needed
421
+ wallet = await this.upgradeWallet(wallet);
422
+ // Decrypt the wallet if needed
423
+ wallet = isWalletEncFile(wallet) ? await this.decryptWalletFromStorage(wallet) : wallet;
424
+ // Copy all properties from the recovered wallet into the cleared current wallet
425
+ // This effectively replaces the current wallet with the recovered one
426
+ Object.assign(current, wallet);
427
+ });
428
+ return this.loadWallet();
429
+ }
430
+ catch (error) {
431
+ // If we can't recover the wallet, just return the current one
432
+ return this.loadWallet();
433
+ }
434
+ }
435
+ async listIds() {
436
+ const wallet = await this.loadWallet();
437
+ return Object.keys(wallet.ids);
438
+ }
439
+ async getCurrentId() {
440
+ const wallet = await this.loadWallet();
441
+ return wallet.current;
442
+ }
443
+ async setCurrentId(name) {
444
+ await this.mutateWallet((wallet) => {
445
+ if (!(name in wallet.ids)) {
446
+ throw new UnknownIDError();
447
+ }
448
+ wallet.current = name;
449
+ });
450
+ return true;
451
+ }
452
+ didMatch(did1, did2) {
453
+ const suffix1 = did1.split(':').pop();
454
+ const suffix2 = did2.split(':').pop();
455
+ return (suffix1 === suffix2);
456
+ }
457
+ async fetchIdInfo(id, wallet) {
458
+ // Callers should pass in the wallet if they are going to modify and save it later
459
+ if (!wallet) {
460
+ wallet = await this.loadWallet();
461
+ }
462
+ let idInfo = null;
463
+ if (id) {
464
+ if (id.startsWith('did')) {
465
+ for (const name of Object.keys(wallet.ids)) {
466
+ const info = wallet.ids[name];
467
+ if (this.didMatch(id, info.did)) {
468
+ idInfo = info;
469
+ break;
470
+ }
471
+ }
472
+ }
473
+ else {
474
+ idInfo = wallet.ids[id];
475
+ }
476
+ }
477
+ else {
478
+ if (!wallet.current) {
479
+ throw new KeymasterError('No current ID');
480
+ }
481
+ idInfo = wallet.ids[wallet.current];
482
+ }
483
+ if (!idInfo) {
484
+ throw new UnknownIDError();
485
+ }
486
+ return idInfo;
487
+ }
488
+ async hdKeyPair() {
489
+ const wallet = await this.loadWallet();
490
+ const hdkey = await this.getHDKeyFromCacheOrMnemonic(wallet);
491
+ return this.cipher.generateJwk(hdkey.privateKey);
492
+ }
493
+ getPublicKeyJwk(doc) {
494
+ // TBD Return the right public key, not just the first one
495
+ if (!doc.didDocument) {
496
+ throw new KeymasterError('Missing didDocument.');
497
+ }
498
+ const verificationMethods = doc.didDocument.verificationMethod;
499
+ if (!verificationMethods || verificationMethods.length === 0) {
500
+ throw new KeymasterError('The DID document does not contain any verification methods.');
501
+ }
502
+ const publicKeyJwk = verificationMethods[0].publicKeyJwk;
503
+ if (!publicKeyJwk) {
504
+ throw new KeymasterError('The publicKeyJwk is missing in the first verification method.');
505
+ }
506
+ return publicKeyJwk;
507
+ }
508
+ async fetchKeyPair(name) {
509
+ const wallet = await this.loadWallet();
510
+ const id = await this.fetchIdInfo(name);
511
+ const hdkey = await this.getHDKeyFromCacheOrMnemonic(wallet);
512
+ const doc = await this.resolveDID(id.did, { confirm: true });
513
+ const confirmedPublicKeyJwk = this.getPublicKeyJwk(doc);
514
+ for (let i = id.index; i >= 0; i--) {
515
+ const path = `m/44'/0'/${id.account}'/0/${i}`;
516
+ const didkey = hdkey.derive(path);
517
+ const keypair = this.cipher.generateJwk(didkey.privateKey);
518
+ if (keypair.publicJwk.x === confirmedPublicKeyJwk.x &&
519
+ keypair.publicJwk.y === confirmedPublicKeyJwk.y) {
520
+ return keypair;
521
+ }
522
+ }
523
+ return null;
524
+ }
525
+ async createAsset(data, options = {}) {
526
+ let { registry = this.defaultRegistry, controller, validUntil, name } = options;
527
+ if (validUntil) {
528
+ const validate = new Date(validUntil);
529
+ if (isNaN(validate.getTime())) {
530
+ throw new InvalidParameterError('options.validUntil');
531
+ }
532
+ }
533
+ if (name) {
534
+ const wallet = await this.loadWallet();
535
+ this.validateName(name, wallet);
536
+ }
537
+ if (!data) {
538
+ throw new InvalidParameterError('data');
539
+ }
540
+ const id = await this.fetchIdInfo(controller);
541
+ const block = await this.gatekeeper.getBlock(registry);
542
+ const blockid = block?.hash;
543
+ const operation = {
544
+ type: "create",
545
+ created: new Date().toISOString(),
546
+ blockid,
547
+ registration: {
548
+ version: 1,
549
+ type: "asset",
550
+ registry,
551
+ validUntil
552
+ },
553
+ controller: id.did,
554
+ data,
555
+ };
556
+ const signed = await this.addSignature(operation, controller);
557
+ const did = await this.gatekeeper.createDID(signed);
558
+ // Keep assets that will be garbage-collected out of the owned list
559
+ if (!validUntil) {
560
+ await this.addToOwned(did);
561
+ }
562
+ if (name) {
563
+ await this.addName(name, did);
564
+ }
565
+ return did;
566
+ }
567
+ async cloneAsset(id, options = {}) {
568
+ const assetDoc = await this.resolveDID(id);
569
+ if (assetDoc.didDocumentRegistration?.type !== 'asset') {
570
+ throw new InvalidParameterError('id');
571
+ }
572
+ const assetData = assetDoc.didDocumentData || {};
573
+ const cloneData = { ...assetData, cloned: assetDoc.didDocument.id };
574
+ return this.createAsset(cloneData, options);
575
+ }
576
+ async generateImageAsset(buffer) {
577
+ let metadata;
578
+ try {
579
+ metadata = imageSize(buffer);
580
+ }
581
+ catch (error) {
582
+ throw new InvalidParameterError('buffer');
583
+ }
584
+ const cid = await this.gatekeeper.addData(buffer);
585
+ const image = {
586
+ cid,
587
+ bytes: buffer.length,
588
+ ...metadata,
589
+ type: `image/${metadata.type}`
590
+ };
591
+ return image;
592
+ }
593
+ async createImage(buffer, options = {}) {
594
+ const image = await this.generateImageAsset(buffer);
595
+ return this.createAsset({ image }, options);
596
+ }
597
+ async updateImage(id, buffer) {
598
+ const image = await this.generateImageAsset(buffer);
599
+ return this.updateAsset(id, { image });
600
+ }
601
+ async getImage(id) {
602
+ const asset = await this.resolveAsset(id);
603
+ const image = asset.image;
604
+ if (!image || !image.cid) {
605
+ return null;
606
+ }
607
+ const buffer = await this.gatekeeper.getData(image.cid);
608
+ if (buffer) {
609
+ image.data = buffer;
610
+ }
611
+ return image;
612
+ }
613
+ async testImage(id) {
614
+ try {
615
+ const image = await this.getImage(id);
616
+ return image !== null;
617
+ }
618
+ catch (error) {
619
+ return false;
620
+ }
621
+ }
622
+ async getMimeType(buffer) {
623
+ // Try magic number detection
624
+ const result = await fileTypeFromBuffer(buffer);
625
+ if (result)
626
+ return result.mime;
627
+ // Convert to UTF-8 string if decodable
628
+ const text = buffer.toString('utf8');
629
+ // Check for JSON
630
+ try {
631
+ JSON.parse(text);
632
+ return 'application/json';
633
+ }
634
+ catch { }
635
+ // Default to plain text if printable ASCII
636
+ // eslint-disable-next-line
637
+ if (/^[\x09\x0A\x0D\x20-\x7E]*$/.test(text.replace(/\n/g, ''))) {
638
+ return 'text/plain';
639
+ }
640
+ // Fallback
641
+ return 'application/octet-stream';
642
+ }
643
+ async generateFileAsset(filename, buffer) {
644
+ const cid = await this.gatekeeper.addData(buffer);
645
+ const type = await this.getMimeType(buffer);
646
+ const file = {
647
+ cid,
648
+ filename,
649
+ type,
650
+ bytes: buffer.length,
651
+ };
652
+ return file;
653
+ }
654
+ async createDocument(buffer, options = {}) {
655
+ const filename = options.filename || 'document';
656
+ const document = await this.generateFileAsset(filename, buffer);
657
+ return this.createAsset({ document }, options);
658
+ }
659
+ async updateDocument(id, buffer, options = {}) {
660
+ const filename = options.filename || 'document';
661
+ const document = await this.generateFileAsset(filename, buffer);
662
+ return this.updateAsset(id, { document });
663
+ }
664
+ async getDocument(id) {
665
+ const asset = await this.resolveAsset(id);
666
+ return asset.document ?? null;
667
+ }
668
+ async testDocument(id) {
669
+ try {
670
+ const document = await this.getDocument(id);
671
+ return document !== null;
672
+ }
673
+ catch (error) {
674
+ return false;
675
+ }
676
+ }
677
+ async encryptMessage(msg, receiver, options = {}) {
678
+ const { encryptForSender = true, includeHash = false, } = options;
679
+ const id = await this.fetchIdInfo();
680
+ const senderKeypair = await this.fetchKeyPair();
681
+ if (!senderKeypair) {
682
+ throw new KeymasterError('No valid sender keypair');
683
+ }
684
+ const doc = await this.resolveDID(receiver, { confirm: true });
685
+ const receivePublicJwk = this.getPublicKeyJwk(doc);
686
+ const cipher_sender = encryptForSender ? this.cipher.encryptMessage(senderKeypair.publicJwk, senderKeypair.privateJwk, msg) : null;
687
+ const cipher_receiver = this.cipher.encryptMessage(receivePublicJwk, senderKeypair.privateJwk, msg);
688
+ const cipher_hash = includeHash ? this.cipher.hashMessage(msg) : null;
689
+ const encrypted = {
690
+ sender: id.did,
691
+ created: new Date().toISOString(),
692
+ cipher_hash,
693
+ cipher_sender,
694
+ cipher_receiver,
695
+ };
696
+ return await this.createAsset({ encrypted }, options);
697
+ }
698
+ async decryptWithDerivedKeys(wallet, id, senderPublicJwk, ciphertext) {
699
+ const hdkey = await this.getHDKeyFromCacheOrMnemonic(wallet);
700
+ // Try all private keys for this ID, starting with the most recent and working backward
701
+ let index = id.index;
702
+ while (index >= 0) {
703
+ const path = `m/44'/0'/${id.account}'/0/${index}`;
704
+ const didkey = hdkey.derive(path);
705
+ const receiverKeypair = this.cipher.generateJwk(didkey.privateKey);
706
+ try {
707
+ return this.cipher.decryptMessage(senderPublicJwk, receiverKeypair.privateJwk, ciphertext);
708
+ }
709
+ catch (error) {
710
+ index -= 1;
711
+ }
712
+ }
713
+ throw new KeymasterError("ID can't decrypt ciphertext");
714
+ }
715
+ async decryptMessage(did) {
716
+ const wallet = await this.loadWallet();
717
+ const id = await this.fetchIdInfo();
718
+ const asset = await this.resolveAsset(did);
719
+ if (!asset) {
720
+ throw new InvalidParameterError('did not encrypted');
721
+ }
722
+ const castAsset = asset;
723
+ if (!castAsset.encrypted && !castAsset.cipher_hash) {
724
+ throw new InvalidParameterError('did not encrypted');
725
+ }
726
+ const crypt = (castAsset.encrypted ? castAsset.encrypted : castAsset);
727
+ const doc = await this.resolveDID(crypt.sender, { confirm: true, versionTime: crypt.created });
728
+ const senderPublicJwk = this.getPublicKeyJwk(doc);
729
+ const ciphertext = (crypt.sender === id.did && crypt.cipher_sender) ? crypt.cipher_sender : crypt.cipher_receiver;
730
+ return await this.decryptWithDerivedKeys(wallet, id, senderPublicJwk, ciphertext);
731
+ }
732
+ async encryptJSON(json, did, options = {}) {
733
+ const plaintext = JSON.stringify(json);
734
+ return this.encryptMessage(plaintext, did, options);
735
+ }
736
+ async decryptJSON(did) {
737
+ const plaintext = await this.decryptMessage(did);
738
+ try {
739
+ return JSON.parse(plaintext);
740
+ }
741
+ catch (error) {
742
+ throw new InvalidParameterError('did not encrypted JSON');
743
+ }
744
+ }
745
+ async addSignature(obj, controller) {
746
+ if (obj == null) {
747
+ throw new InvalidParameterError('obj');
748
+ }
749
+ // Fetches current ID if name is missing
750
+ const id = await this.fetchIdInfo(controller);
751
+ const keypair = await this.fetchKeyPair(controller);
752
+ if (!keypair) {
753
+ throw new KeymasterError('addSignature: no keypair');
754
+ }
755
+ try {
756
+ const msgHash = this.cipher.hashJSON(obj);
757
+ const signature = this.cipher.signHash(msgHash, keypair.privateJwk);
758
+ return {
759
+ ...obj,
760
+ signature: {
761
+ signer: id.did,
762
+ signed: new Date().toISOString(),
763
+ hash: msgHash,
764
+ value: signature,
765
+ }
766
+ };
767
+ }
768
+ catch (error) {
769
+ throw new InvalidParameterError('obj');
770
+ }
771
+ }
772
+ async verifySignature(obj) {
773
+ if (!obj?.signature) {
774
+ return false;
775
+ }
776
+ const { signature } = obj;
777
+ if (!signature.signer) {
778
+ return false;
779
+ }
780
+ const jsonCopy = JSON.parse(JSON.stringify(obj));
781
+ delete jsonCopy.signature;
782
+ const msgHash = this.cipher.hashJSON(jsonCopy);
783
+ if (signature.hash && signature.hash !== msgHash) {
784
+ return false;
785
+ }
786
+ const doc = await this.resolveDID(signature.signer, { versionTime: signature.signed });
787
+ const publicJwk = this.getPublicKeyJwk(doc);
788
+ try {
789
+ return this.cipher.verifySig(msgHash, signature.value, publicJwk);
790
+ }
791
+ catch (error) {
792
+ return false;
793
+ }
794
+ }
795
+ async updateDID(id, doc) {
796
+ const did = await this.lookupDID(id);
797
+ const current = await this.resolveDID(did);
798
+ const previd = current.didDocumentMetadata?.versionId;
799
+ // Strip metadata fields from the update doc
800
+ delete doc.didDocumentMetadata;
801
+ delete doc.didResolutionMetadata;
802
+ const block = await this.gatekeeper.getBlock(current.didDocumentRegistration.registry);
803
+ const blockid = block?.hash;
804
+ const operation = {
805
+ type: "update",
806
+ did,
807
+ previd,
808
+ blockid,
809
+ doc,
810
+ };
811
+ let controller;
812
+ if (current.didDocumentRegistration?.type === 'agent') {
813
+ controller = current.didDocument?.id;
814
+ }
815
+ else if (current.didDocumentRegistration?.type === 'asset') {
816
+ controller = current.didDocument?.controller;
817
+ }
818
+ const signed = await this.addSignature(operation, controller);
819
+ return this.gatekeeper.updateDID(signed);
820
+ }
821
+ async revokeDID(id) {
822
+ const did = await this.lookupDID(id);
823
+ const current = await this.resolveDID(did);
824
+ const previd = current.didDocumentMetadata?.versionId;
825
+ const block = await this.gatekeeper.getBlock(current.didDocumentRegistration.registry);
826
+ const blockid = block?.hash;
827
+ const operation = {
828
+ type: "delete",
829
+ did,
830
+ previd,
831
+ blockid
832
+ };
833
+ let controller;
834
+ if (current.didDocumentRegistration?.type === 'agent') {
835
+ controller = current.didDocument?.id;
836
+ }
837
+ else if (current.didDocumentRegistration?.type === 'asset') {
838
+ controller = current.didDocument?.controller;
839
+ }
840
+ const signed = await this.addSignature(operation, controller);
841
+ const ok = await this.gatekeeper.deleteDID(signed);
842
+ if (ok && current.didDocument?.controller) {
843
+ await this.removeFromOwned(did, current.didDocument.controller);
844
+ }
845
+ return ok;
846
+ }
847
+ async addToOwned(did, owner) {
848
+ await this.mutateWallet(async (wallet) => {
849
+ const id = await this.fetchIdInfo(owner, wallet);
850
+ const owned = new Set(id.owned);
851
+ owned.add(did);
852
+ id.owned = Array.from(owned);
853
+ });
854
+ return true;
855
+ }
856
+ async removeFromOwned(did, owner) {
857
+ let ownerFound = false;
858
+ await this.mutateWallet(async (wallet) => {
859
+ const id = await this.fetchIdInfo(owner, wallet);
860
+ if (!id.owned) {
861
+ return;
862
+ }
863
+ ownerFound = true;
864
+ id.owned = id.owned.filter(item => item !== did);
865
+ });
866
+ return ownerFound;
867
+ }
868
+ async addToHeld(did) {
869
+ await this.mutateWallet((wallet) => {
870
+ const id = wallet.ids[wallet.current];
871
+ const held = new Set(id.held);
872
+ held.add(did);
873
+ id.held = Array.from(held);
874
+ });
875
+ return true;
876
+ }
877
+ async removeFromHeld(did) {
878
+ let changed = false;
879
+ await this.mutateWallet((wallet) => {
880
+ const id = wallet.ids[wallet.current];
881
+ const held = new Set(id.held);
882
+ if (held.delete(did)) {
883
+ id.held = Array.from(held);
884
+ changed = true;
885
+ }
886
+ });
887
+ return changed;
888
+ }
889
+ async lookupDID(name) {
890
+ try {
891
+ if (name.startsWith('did:')) {
892
+ return name;
893
+ }
894
+ }
895
+ catch {
896
+ throw new InvalidDIDError();
897
+ }
898
+ const wallet = await this.loadWallet();
899
+ if (wallet.names && name in wallet.names) {
900
+ return wallet.names[name];
901
+ }
902
+ if (wallet.ids && name in wallet.ids) {
903
+ return wallet.ids[name].did;
904
+ }
905
+ throw new UnknownIDError();
906
+ }
907
+ async resolveDID(did, options) {
908
+ const actualDid = await this.lookupDID(did);
909
+ const docs = await this.gatekeeper.resolveDID(actualDid, options);
910
+ if (docs.didResolutionMetadata?.error) {
911
+ if (docs.didResolutionMetadata.error === 'notFound') {
912
+ throw new InvalidDIDError('unknown');
913
+ }
914
+ if (docs.didResolutionMetadata.error === 'invalidDid') {
915
+ throw new InvalidDIDError('bad format');
916
+ }
917
+ }
918
+ const controller = docs.didDocument?.controller || docs.didDocument?.id;
919
+ const isOwned = await this.idInWallet(controller);
920
+ // Augment the DID document metadata with the DID ownership status
921
+ docs.didDocumentMetadata = {
922
+ ...docs.didDocumentMetadata,
923
+ isOwned,
924
+ };
925
+ return docs;
926
+ }
927
+ async idInWallet(did) {
928
+ try {
929
+ await this.fetchIdInfo(did);
930
+ return true;
931
+ }
932
+ catch (error) {
933
+ return false;
934
+ }
935
+ }
936
+ async resolveAsset(did, options) {
937
+ const doc = await this.resolveDID(did, options);
938
+ if (!doc?.didDocument?.controller || !doc?.didDocumentData || doc.didDocumentMetadata?.deactivated) {
939
+ return {};
940
+ }
941
+ return doc.didDocumentData;
942
+ }
943
+ async updateAsset(did, data) {
944
+ const doc = await this.resolveDID(did);
945
+ const currentData = doc.didDocumentData || {};
946
+ const updatedData = {
947
+ ...currentData,
948
+ ...data
949
+ };
950
+ return this.updateDID(did, { didDocumentData: updatedData });
951
+ }
952
+ async transferAsset(id, controller) {
953
+ const assetDoc = await this.resolveDID(id);
954
+ if (assetDoc.didDocumentRegistration?.type !== 'asset') {
955
+ throw new InvalidParameterError('id');
956
+ }
957
+ if (assetDoc.didDocument.controller === controller) {
958
+ return true;
959
+ }
960
+ const agentDoc = await this.resolveDID(controller);
961
+ if (agentDoc.didDocumentRegistration?.type !== 'agent') {
962
+ throw new InvalidParameterError('controller');
963
+ }
964
+ const assetDID = assetDoc.didDocument.id;
965
+ const prevOwner = assetDoc.didDocument.controller;
966
+ const updatedDidDocument = {
967
+ ...assetDoc.didDocument,
968
+ controller: agentDoc.didDocument.id,
969
+ };
970
+ const ok = await this.updateDID(id, { didDocument: updatedDidDocument });
971
+ if (ok && assetDID && prevOwner) {
972
+ await this.removeFromOwned(assetDID, prevOwner);
973
+ try {
974
+ await this.addToOwned(assetDID, controller);
975
+ }
976
+ catch (error) {
977
+ // New controller is not in our wallet
978
+ }
979
+ }
980
+ return ok;
981
+ }
982
+ async listAssets(owner) {
983
+ const id = await this.fetchIdInfo(owner);
984
+ return id.owned || [];
985
+ }
986
+ validateName(name, wallet) {
987
+ if (typeof name !== 'string' || !name.trim()) {
988
+ throw new InvalidParameterError('name must be a non-empty string');
989
+ }
990
+ name = name.trim(); // Remove leading/trailing whitespace
991
+ if (name.length > this.maxNameLength) {
992
+ throw new InvalidParameterError(`name too long`);
993
+ }
994
+ if (/[^\P{Cc}]/u.test(name)) {
995
+ throw new InvalidParameterError('name contains unprintable characters');
996
+ }
997
+ const alreadyUsedError = 'name already used';
998
+ if (wallet && wallet.names && name in wallet.names) {
999
+ throw new InvalidParameterError(alreadyUsedError);
1000
+ }
1001
+ if (wallet && wallet.ids && name in wallet.ids) {
1002
+ throw new InvalidParameterError(alreadyUsedError);
1003
+ }
1004
+ return name;
1005
+ }
1006
+ async createId(name, options = {}) {
1007
+ let did = '';
1008
+ await this.mutateWallet(async (wallet) => {
1009
+ const account = wallet.counter;
1010
+ const index = 0;
1011
+ const signed = await this.createIdOperation(name, account, options);
1012
+ did = await this.gatekeeper.createDID(signed);
1013
+ wallet.ids[name] = { did, account, index };
1014
+ wallet.counter += 1;
1015
+ wallet.current = name;
1016
+ });
1017
+ return did;
1018
+ }
1019
+ async createIdOperation(name, account = 0, options = {}) {
1020
+ const { registry = this.defaultRegistry } = options;
1021
+ const wallet = await this.loadWallet();
1022
+ name = this.validateName(name, wallet);
1023
+ const hdkey = await this.getHDKeyFromCacheOrMnemonic(wallet);
1024
+ const path = `m/44'/0'/${account}'/0/0`;
1025
+ const didkey = hdkey.derive(path);
1026
+ const keypair = this.cipher.generateJwk(didkey.privateKey);
1027
+ const block = await this.gatekeeper.getBlock(registry);
1028
+ const blockid = block?.hash;
1029
+ const operation = {
1030
+ type: 'create',
1031
+ created: new Date().toISOString(),
1032
+ blockid,
1033
+ registration: {
1034
+ version: 1,
1035
+ type: 'agent',
1036
+ registry
1037
+ },
1038
+ publicJwk: keypair.publicJwk,
1039
+ };
1040
+ const msgHash = this.cipher.hashJSON(operation);
1041
+ const signature = this.cipher.signHash(msgHash, keypair.privateJwk);
1042
+ const signed = {
1043
+ ...operation,
1044
+ signature: {
1045
+ signed: new Date().toISOString(),
1046
+ hash: msgHash,
1047
+ value: signature
1048
+ },
1049
+ };
1050
+ return signed;
1051
+ }
1052
+ async removeId(name) {
1053
+ await this.mutateWallet((wallet) => {
1054
+ if (!(name in wallet.ids)) {
1055
+ throw new UnknownIDError();
1056
+ }
1057
+ delete wallet.ids[name];
1058
+ if (wallet.current === name) {
1059
+ wallet.current = Object.keys(wallet.ids)[0] || '';
1060
+ }
1061
+ });
1062
+ return true;
1063
+ }
1064
+ async renameId(id, name) {
1065
+ await this.mutateWallet((wallet) => {
1066
+ name = this.validateName(name);
1067
+ if (!(id in wallet.ids)) {
1068
+ throw new UnknownIDError();
1069
+ }
1070
+ if (name in wallet.ids) {
1071
+ throw new InvalidParameterError('name already used');
1072
+ }
1073
+ wallet.ids[name] = wallet.ids[id];
1074
+ delete wallet.ids[id];
1075
+ if (wallet.current && wallet.current === id) {
1076
+ wallet.current = name;
1077
+ }
1078
+ });
1079
+ return true;
1080
+ }
1081
+ async backupId(id) {
1082
+ // Backs up current ID if id is not provided
1083
+ const wallet = await this.loadWallet();
1084
+ const name = id || wallet.current;
1085
+ if (!name) {
1086
+ throw new InvalidParameterError('no current ID');
1087
+ }
1088
+ const idInfo = await this.fetchIdInfo(name, wallet);
1089
+ const keypair = await this.hdKeyPair();
1090
+ const data = {
1091
+ name: name,
1092
+ id: idInfo,
1093
+ };
1094
+ const msg = JSON.stringify(data);
1095
+ const backup = this.cipher.encryptMessage(keypair.publicJwk, keypair.privateJwk, msg);
1096
+ const doc = await this.resolveDID(idInfo.did);
1097
+ const registry = doc.didDocumentRegistration?.registry;
1098
+ if (!registry) {
1099
+ throw new InvalidParameterError('no registry found for agent DID');
1100
+ }
1101
+ const vaultDid = await this.createAsset({ backup: backup }, { registry, controller: name });
1102
+ if (doc.didDocumentData) {
1103
+ const currentData = doc.didDocumentData;
1104
+ const updatedData = { ...currentData, vault: vaultDid };
1105
+ return this.updateDID(name, { didDocumentData: updatedData });
1106
+ }
1107
+ return false;
1108
+ }
1109
+ async recoverId(did) {
1110
+ try {
1111
+ const keypair = await this.hdKeyPair();
1112
+ const doc = await this.resolveDID(did);
1113
+ const docData = doc.didDocumentData;
1114
+ if (!docData.vault) {
1115
+ throw new InvalidDIDError('didDocumentData missing vault');
1116
+ }
1117
+ const vault = await this.resolveAsset(docData.vault);
1118
+ if (typeof vault.backup !== 'string') {
1119
+ throw new InvalidDIDError('backup not found in vault');
1120
+ }
1121
+ const backup = this.cipher.decryptMessage(keypair.publicJwk, keypair.privateJwk, vault.backup);
1122
+ const data = JSON.parse(backup);
1123
+ await this.mutateWallet((wallet) => {
1124
+ if (wallet.ids[data.name]) {
1125
+ throw new KeymasterError(`${data.name} already exists in wallet`);
1126
+ }
1127
+ wallet.ids[data.name] = data.id;
1128
+ wallet.current = data.name;
1129
+ wallet.counter += 1;
1130
+ });
1131
+ return data.name;
1132
+ }
1133
+ catch (error) {
1134
+ if (error.type === 'Keymaster') {
1135
+ throw error;
1136
+ }
1137
+ else {
1138
+ throw new InvalidDIDError();
1139
+ }
1140
+ }
1141
+ }
1142
+ async rotateKeys() {
1143
+ let ok = false;
1144
+ await this.mutateWallet(async (wallet) => {
1145
+ const id = wallet.ids[wallet.current];
1146
+ const nextIndex = id.index + 1;
1147
+ const hdkey = await this.getHDKeyFromCacheOrMnemonic(wallet);
1148
+ const path = `m/44'/0'/${id.account}'/0/${nextIndex}`;
1149
+ const didkey = hdkey.derive(path);
1150
+ const keypair = this.cipher.generateJwk(didkey.privateKey);
1151
+ const doc = await this.resolveDID(id.did);
1152
+ if (!doc.didDocumentMetadata?.confirmed) {
1153
+ throw new KeymasterError('Cannot rotate keys');
1154
+ }
1155
+ if (!doc.didDocument?.verificationMethod) {
1156
+ throw new KeymasterError('DID Document missing verificationMethod');
1157
+ }
1158
+ const vmethod = { ...doc.didDocument.verificationMethod[0] };
1159
+ vmethod.id = `#key-${nextIndex + 1}`;
1160
+ vmethod.publicKeyJwk = keypair.publicJwk;
1161
+ const updatedDidDocument = {
1162
+ ...doc.didDocument,
1163
+ verificationMethod: [vmethod],
1164
+ authentication: [vmethod.id],
1165
+ };
1166
+ ok = await this.updateDID(id.did, { didDocument: updatedDidDocument });
1167
+ if (!ok) {
1168
+ throw new KeymasterError('Cannot rotate keys');
1169
+ }
1170
+ id.index = nextIndex; // persist in same mutation
1171
+ });
1172
+ return ok;
1173
+ }
1174
+ async listNames(options = {}) {
1175
+ const { includeIDs = false } = options;
1176
+ const wallet = await this.loadWallet();
1177
+ const names = wallet.names || {};
1178
+ if (includeIDs) {
1179
+ for (const [name, id] of Object.entries(wallet.ids || {})) {
1180
+ names[name] = id.did;
1181
+ }
1182
+ }
1183
+ return names;
1184
+ }
1185
+ async addName(name, did) {
1186
+ await this.mutateWallet((wallet) => {
1187
+ if (!wallet.names) {
1188
+ wallet.names = {};
1189
+ }
1190
+ name = this.validateName(name, wallet);
1191
+ wallet.names[name] = did;
1192
+ });
1193
+ return true;
1194
+ }
1195
+ async getName(name) {
1196
+ const wallet = await this.loadWallet();
1197
+ if (wallet.names && name in wallet.names) {
1198
+ return wallet.names[name];
1199
+ }
1200
+ return null;
1201
+ }
1202
+ async removeName(name) {
1203
+ await this.mutateWallet((wallet) => {
1204
+ if (!wallet.names || !(name in wallet.names)) {
1205
+ return;
1206
+ }
1207
+ delete wallet.names[name];
1208
+ });
1209
+ return true;
1210
+ }
1211
+ async testAgent(id) {
1212
+ const doc = await this.resolveDID(id);
1213
+ return doc.didDocumentRegistration?.type === 'agent';
1214
+ }
1215
+ async bindCredential(schemaId, subjectId, options = {}) {
1216
+ let { validFrom, validUntil, credential } = options;
1217
+ if (!validFrom) {
1218
+ validFrom = new Date().toISOString();
1219
+ }
1220
+ const id = await this.fetchIdInfo();
1221
+ const type = await this.lookupDID(schemaId);
1222
+ const subjectDID = await this.lookupDID(subjectId);
1223
+ if (!credential) {
1224
+ const schema = await this.getSchema(type);
1225
+ credential = this.generateSchema(schema);
1226
+ }
1227
+ return {
1228
+ "@context": [
1229
+ "https://www.w3.org/ns/credentials/v2",
1230
+ "https://www.w3.org/ns/credentials/examples/v2"
1231
+ ],
1232
+ type: ["VerifiableCredential", type],
1233
+ issuer: id.did,
1234
+ validFrom,
1235
+ validUntil,
1236
+ credentialSubject: {
1237
+ id: subjectDID,
1238
+ },
1239
+ credential,
1240
+ };
1241
+ }
1242
+ async issueCredential(credential, options = {}) {
1243
+ const id = await this.fetchIdInfo();
1244
+ if (options.schema && options.subject) {
1245
+ credential = await this.bindCredential(options.schema, options.subject, { credential, ...options });
1246
+ }
1247
+ if (credential.issuer !== id.did) {
1248
+ throw new InvalidParameterError('credential.issuer');
1249
+ }
1250
+ const signed = await this.addSignature(credential);
1251
+ return this.encryptJSON(signed, credential.credentialSubject.id, { ...options, includeHash: true });
1252
+ }
1253
+ async sendCredential(did, options = {}) {
1254
+ const vc = await this.getCredential(did);
1255
+ if (!vc) {
1256
+ return null;
1257
+ }
1258
+ const registry = this.ephemeralRegistry;
1259
+ const validUntil = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(); // Default to 7 days
1260
+ const message = {
1261
+ to: [vc.credentialSubject.id],
1262
+ dids: [did],
1263
+ };
1264
+ return this.createNotice(message, { registry, validUntil, ...options });
1265
+ }
1266
+ isVerifiableCredential(obj) {
1267
+ if (typeof obj !== 'object' || !obj) {
1268
+ return false;
1269
+ }
1270
+ const vc = obj;
1271
+ return !(!Array.isArray(vc["@context"]) || !Array.isArray(vc.type) || !vc.issuer || !vc.credentialSubject);
1272
+ }
1273
+ async updateCredential(did, credential) {
1274
+ did = await this.lookupDID(did);
1275
+ const originalVC = await this.decryptJSON(did);
1276
+ if (!this.isVerifiableCredential(originalVC)) {
1277
+ throw new InvalidParameterError("did is not a credential");
1278
+ }
1279
+ if (!credential ||
1280
+ !credential.credential ||
1281
+ !credential.credentialSubject ||
1282
+ !credential.credentialSubject.id) {
1283
+ throw new InvalidParameterError('credential');
1284
+ }
1285
+ delete credential.signature;
1286
+ const signed = await this.addSignature(credential);
1287
+ const msg = JSON.stringify(signed);
1288
+ const id = await this.fetchIdInfo();
1289
+ const senderKeypair = await this.fetchKeyPair();
1290
+ if (!senderKeypair) {
1291
+ throw new KeymasterError('No valid sender keypair');
1292
+ }
1293
+ const holder = credential.credentialSubject.id;
1294
+ const holderDoc = await this.resolveDID(holder, { confirm: true });
1295
+ const receivePublicJwk = this.getPublicKeyJwk(holderDoc);
1296
+ const cipher_sender = this.cipher.encryptMessage(senderKeypair.publicJwk, senderKeypair.privateJwk, msg);
1297
+ const cipher_receiver = this.cipher.encryptMessage(receivePublicJwk, senderKeypair.privateJwk, msg);
1298
+ const msgHash = this.cipher.hashMessage(msg);
1299
+ const encrypted = {
1300
+ sender: id.did,
1301
+ created: new Date().toISOString(),
1302
+ cipher_hash: msgHash,
1303
+ cipher_sender: cipher_sender,
1304
+ cipher_receiver: cipher_receiver,
1305
+ };
1306
+ return this.updateDID(did, { didDocumentData: { encrypted } });
1307
+ }
1308
+ async revokeCredential(credential) {
1309
+ const did = await this.lookupDID(credential);
1310
+ return this.revokeDID(did);
1311
+ }
1312
+ async listIssued(issuer) {
1313
+ const id = await this.fetchIdInfo(issuer);
1314
+ const issued = [];
1315
+ if (id.owned) {
1316
+ for (const did of id.owned) {
1317
+ try {
1318
+ const credential = await this.decryptJSON(did);
1319
+ if (this.isVerifiableCredential(credential) &&
1320
+ credential.issuer === id.did) {
1321
+ issued.push(did);
1322
+ }
1323
+ }
1324
+ catch (error) { }
1325
+ }
1326
+ }
1327
+ return issued;
1328
+ }
1329
+ async acceptCredential(did) {
1330
+ try {
1331
+ const id = await this.fetchIdInfo();
1332
+ const credential = await this.lookupDID(did);
1333
+ const vc = await this.decryptJSON(credential);
1334
+ if (this.isVerifiableCredential(vc) &&
1335
+ vc.credentialSubject?.id !== id.did) {
1336
+ return false;
1337
+ }
1338
+ return this.addToHeld(credential);
1339
+ }
1340
+ catch (error) {
1341
+ return false;
1342
+ }
1343
+ }
1344
+ async getCredential(id) {
1345
+ const did = await this.lookupDID(id);
1346
+ const vc = await this.decryptJSON(did);
1347
+ if (!this.isVerifiableCredential(vc)) {
1348
+ return null;
1349
+ }
1350
+ return vc;
1351
+ }
1352
+ async removeCredential(id) {
1353
+ const did = await this.lookupDID(id);
1354
+ return this.removeFromHeld(did);
1355
+ }
1356
+ async listCredentials(id) {
1357
+ const idInfo = await this.fetchIdInfo(id);
1358
+ return idInfo.held || [];
1359
+ }
1360
+ async publishCredential(did, options = {}) {
1361
+ const { reveal = false } = options;
1362
+ const id = await this.fetchIdInfo();
1363
+ const credential = await this.lookupDID(did);
1364
+ const vc = await this.decryptJSON(credential);
1365
+ if (!this.isVerifiableCredential(vc)) {
1366
+ throw new InvalidParameterError("did is not a credential");
1367
+ }
1368
+ if (vc.credentialSubject?.id !== id.did) {
1369
+ throw new InvalidParameterError('only subject can publish a credential');
1370
+ }
1371
+ const doc = await this.resolveDID(id.did);
1372
+ if (!doc.didDocumentData) {
1373
+ doc.didDocumentData = {};
1374
+ }
1375
+ const data = doc.didDocumentData;
1376
+ if (!data.manifest) {
1377
+ data.manifest = {};
1378
+ }
1379
+ if (!reveal) {
1380
+ // Remove the credential values
1381
+ vc.credential = null;
1382
+ }
1383
+ data.manifest[credential] = vc;
1384
+ const ok = await this.updateDID(id.did, { didDocumentData: doc.didDocumentData });
1385
+ if (ok) {
1386
+ return vc;
1387
+ }
1388
+ throw new KeymasterError('update DID failed');
1389
+ }
1390
+ async unpublishCredential(did) {
1391
+ const id = await this.fetchIdInfo();
1392
+ const doc = await this.resolveDID(id.did);
1393
+ const credential = await this.lookupDID(did);
1394
+ const data = doc.didDocumentData;
1395
+ if (credential && data.manifest && credential in data.manifest) {
1396
+ delete data.manifest[credential];
1397
+ await this.updateDID(id.did, { didDocumentData: doc.didDocumentData });
1398
+ return `OK credential ${did} removed from manifest`;
1399
+ }
1400
+ throw new InvalidParameterError('did');
1401
+ }
1402
+ async createChallenge(challenge = {}, options = {}) {
1403
+ if (!challenge || typeof challenge !== 'object' || Array.isArray(challenge)) {
1404
+ throw new InvalidParameterError('challenge');
1405
+ }
1406
+ if (challenge.credentials && !Array.isArray(challenge.credentials)) {
1407
+ throw new InvalidParameterError('challenge.credentials');
1408
+ // TBD validate each credential spec
1409
+ }
1410
+ if (!options.registry) {
1411
+ options.registry = this.ephemeralRegistry;
1412
+ }
1413
+ if (!options.validUntil) {
1414
+ const expires = new Date();
1415
+ expires.setHours(expires.getHours() + 1); // Add 1 hour
1416
+ options.validUntil = expires.toISOString();
1417
+ }
1418
+ return this.createAsset({ challenge }, options);
1419
+ }
1420
+ async findMatchingCredential(credential) {
1421
+ const id = await this.fetchIdInfo();
1422
+ if (!id.held) {
1423
+ return;
1424
+ }
1425
+ for (let did of id.held) {
1426
+ try {
1427
+ const doc = await this.decryptJSON(did);
1428
+ if (!this.isVerifiableCredential(doc)) {
1429
+ continue;
1430
+ }
1431
+ if (doc.credentialSubject?.id !== id.did) {
1432
+ // This VC is issued by the ID, not held
1433
+ continue;
1434
+ }
1435
+ if (credential.issuers && !credential.issuers.includes(doc.issuer)) {
1436
+ // Attestor not trusted by Verifier
1437
+ continue;
1438
+ }
1439
+ if (doc.type && !doc.type.includes(credential.schema)) {
1440
+ // Wrong type
1441
+ continue;
1442
+ }
1443
+ // TBD test for VC expiry too
1444
+ return did;
1445
+ }
1446
+ catch (error) {
1447
+ // Not encrypted, so can't be a VC
1448
+ }
1449
+ }
1450
+ }
1451
+ async createResponse(challengeDID, options = {}) {
1452
+ let { retries = 0, delay = 1000 } = options;
1453
+ if (!options.registry) {
1454
+ options.registry = this.ephemeralRegistry;
1455
+ }
1456
+ if (!options.validUntil) {
1457
+ const expires = new Date();
1458
+ expires.setHours(expires.getHours() + 1); // Add 1 hour
1459
+ options.validUntil = expires.toISOString();
1460
+ }
1461
+ let doc;
1462
+ while (retries >= 0) {
1463
+ try {
1464
+ doc = await this.resolveDID(challengeDID);
1465
+ break;
1466
+ }
1467
+ catch (error) {
1468
+ if (retries === 0)
1469
+ throw error; // If no retries left, throw the error
1470
+ retries--; // Decrease the retry count
1471
+ await new Promise(resolve => setTimeout(resolve, delay)); // Wait for delay milleseconds
1472
+ }
1473
+ }
1474
+ if (!doc) {
1475
+ throw new InvalidParameterError('challengeDID does not resolve');
1476
+ }
1477
+ const result = await this.resolveAsset(challengeDID);
1478
+ if (!result) {
1479
+ throw new InvalidParameterError('challengeDID');
1480
+ }
1481
+ const challenge = result.challenge;
1482
+ if (!challenge) {
1483
+ throw new InvalidParameterError('challengeDID');
1484
+ }
1485
+ const requestor = doc.didDocument?.controller;
1486
+ if (!requestor) {
1487
+ throw new InvalidParameterError('requestor undefined');
1488
+ }
1489
+ // TBD check challenge isValid for expired?
1490
+ const matches = [];
1491
+ if (challenge.credentials) {
1492
+ for (let credential of challenge.credentials) {
1493
+ const vc = await this.findMatchingCredential(credential);
1494
+ if (vc) {
1495
+ matches.push(vc);
1496
+ }
1497
+ }
1498
+ }
1499
+ const pairs = [];
1500
+ for (let vcDid of matches) {
1501
+ const plaintext = await this.decryptMessage(vcDid);
1502
+ const vpDid = await this.encryptMessage(plaintext, requestor, { ...options, includeHash: true });
1503
+ pairs.push({ vc: vcDid, vp: vpDid });
1504
+ }
1505
+ const requested = challenge.credentials?.length ?? 0;
1506
+ const fulfilled = matches.length;
1507
+ const match = (requested === fulfilled);
1508
+ const response = {
1509
+ challenge: challengeDID,
1510
+ credentials: pairs,
1511
+ requested: requested,
1512
+ fulfilled: fulfilled,
1513
+ match: match
1514
+ };
1515
+ return await this.encryptJSON({ response }, requestor, options);
1516
+ }
1517
+ async verifyResponse(responseDID, options = {}) {
1518
+ let { retries = 0, delay = 1000 } = options;
1519
+ let responseDoc;
1520
+ while (retries >= 0) {
1521
+ try {
1522
+ responseDoc = await this.resolveDID(responseDID);
1523
+ break;
1524
+ }
1525
+ catch (error) {
1526
+ if (retries === 0)
1527
+ throw error; // If no retries left, throw the error
1528
+ retries--; // Decrease the retry count
1529
+ await new Promise(resolve => setTimeout(resolve, delay)); // Wait for delay milliseconds
1530
+ }
1531
+ }
1532
+ if (!responseDoc) {
1533
+ throw new InvalidParameterError('responseDID does not resolve');
1534
+ }
1535
+ const wrapper = await this.decryptJSON(responseDID);
1536
+ if (typeof wrapper !== 'object' || !wrapper || !('response' in wrapper)) {
1537
+ throw new InvalidParameterError('responseDID not a valid challenge response');
1538
+ }
1539
+ const { response } = wrapper;
1540
+ const result = await this.resolveAsset(response.challenge);
1541
+ if (!result) {
1542
+ throw new InvalidParameterError('challenge not found');
1543
+ }
1544
+ const challenge = result.challenge;
1545
+ if (!challenge) {
1546
+ throw new InvalidParameterError('challengeDID');
1547
+ }
1548
+ const vps = [];
1549
+ for (let credential of response.credentials) {
1550
+ const vcData = await this.resolveAsset(credential.vc);
1551
+ const vpData = await this.resolveAsset(credential.vp);
1552
+ const castVCData = vcData;
1553
+ const castVPData = vpData;
1554
+ if (!vcData || !vpData || !castVCData.encrypted || !castVPData.encrypted) {
1555
+ // VC revoked
1556
+ continue;
1557
+ }
1558
+ const vcHash = castVCData.encrypted;
1559
+ const vpHash = castVPData.encrypted;
1560
+ if (vcHash.cipher_hash !== vpHash.cipher_hash) {
1561
+ // can't verify that the contents of VP match the VC
1562
+ continue;
1563
+ }
1564
+ const vp = await this.decryptJSON(credential.vp);
1565
+ const isValid = await this.verifySignature(vp);
1566
+ if (!isValid) {
1567
+ continue;
1568
+ }
1569
+ if (!vp.type || !Array.isArray(vp.type)) {
1570
+ continue;
1571
+ }
1572
+ // Check VP against VCs specified in challenge
1573
+ if (vp.type.length >= 2 && vp.type[1].startsWith('did:')) {
1574
+ const schema = vp.type[1];
1575
+ const credential = challenge.credentials?.find(item => item.schema === schema);
1576
+ if (!credential) {
1577
+ continue;
1578
+ }
1579
+ // Check if issuer of VP is in the trusted issuer list
1580
+ if (credential.issuers && credential.issuers.length > 0 && !credential.issuers.includes(vp.issuer)) {
1581
+ continue;
1582
+ }
1583
+ }
1584
+ vps.push(vp);
1585
+ }
1586
+ response.vps = vps;
1587
+ response.match = vps.length === (challenge.credentials?.length ?? 0);
1588
+ response.responder = responseDoc.didDocument?.controller;
1589
+ return response;
1590
+ }
1591
+ async createGroup(name, options = {}) {
1592
+ const group = {
1593
+ name: name,
1594
+ members: []
1595
+ };
1596
+ return this.createAsset({ group }, options);
1597
+ }
1598
+ async getGroup(id) {
1599
+ const asset = await this.resolveAsset(id);
1600
+ if (!asset) {
1601
+ return null;
1602
+ }
1603
+ // TEMP during did:cid, return old version groups
1604
+ const castOldAsset = asset;
1605
+ if (castOldAsset.members) {
1606
+ return castOldAsset;
1607
+ }
1608
+ const castAsset = asset;
1609
+ if (!castAsset.group) {
1610
+ return null;
1611
+ }
1612
+ return castAsset.group;
1613
+ }
1614
+ async addGroupMember(groupId, memberId) {
1615
+ const groupDID = await this.lookupDID(groupId);
1616
+ const memberDID = await this.lookupDID(memberId);
1617
+ // Can't add a group to itself
1618
+ if (memberDID === groupDID) {
1619
+ throw new InvalidParameterError("can't add a group to itself");
1620
+ }
1621
+ try {
1622
+ // test for valid member DID
1623
+ await this.resolveDID(memberDID);
1624
+ }
1625
+ catch {
1626
+ throw new InvalidParameterError('memberId');
1627
+ }
1628
+ const group = await this.getGroup(groupId);
1629
+ if (!group?.members) {
1630
+ throw new InvalidParameterError('groupId');
1631
+ }
1632
+ // If already a member, return immediately
1633
+ if (group.members.includes(memberDID)) {
1634
+ return true;
1635
+ }
1636
+ // Can't add a mutual membership relation
1637
+ const isMember = await this.testGroup(memberId, groupId);
1638
+ if (isMember) {
1639
+ throw new InvalidParameterError("can't create mutual membership");
1640
+ }
1641
+ const members = new Set(group.members);
1642
+ members.add(memberDID);
1643
+ group.members = Array.from(members);
1644
+ return this.updateAsset(groupDID, { group });
1645
+ }
1646
+ async removeGroupMember(groupId, memberId) {
1647
+ const groupDID = await this.lookupDID(groupId);
1648
+ const memberDID = await this.lookupDID(memberId);
1649
+ const group = await this.getGroup(groupDID);
1650
+ if (!group?.members) {
1651
+ throw new InvalidParameterError('groupId');
1652
+ }
1653
+ try {
1654
+ // test for valid member DID
1655
+ await this.resolveDID(memberDID);
1656
+ }
1657
+ catch {
1658
+ throw new InvalidParameterError('memberId');
1659
+ }
1660
+ // If not already a member, return immediately
1661
+ if (!group.members.includes(memberDID)) {
1662
+ return true;
1663
+ }
1664
+ const members = new Set(group.members);
1665
+ members.delete(memberDID);
1666
+ group.members = Array.from(members);
1667
+ return this.updateAsset(groupDID, { group });
1668
+ }
1669
+ async testGroup(groupId, memberId) {
1670
+ try {
1671
+ const group = await this.getGroup(groupId);
1672
+ if (!group) {
1673
+ return false;
1674
+ }
1675
+ if (!memberId) {
1676
+ return true;
1677
+ }
1678
+ const didMember = await this.lookupDID(memberId);
1679
+ let isMember = group.members.includes(didMember);
1680
+ if (!isMember) {
1681
+ for (const did of group.members) {
1682
+ isMember = await this.testGroup(did, didMember);
1683
+ if (isMember) {
1684
+ break;
1685
+ }
1686
+ }
1687
+ }
1688
+ return isMember;
1689
+ }
1690
+ catch (error) {
1691
+ return false;
1692
+ }
1693
+ }
1694
+ async listGroups(owner) {
1695
+ const assets = await this.listAssets(owner);
1696
+ const groups = [];
1697
+ for (const did of assets) {
1698
+ const isGroup = await this.testGroup(did);
1699
+ if (isGroup) {
1700
+ groups.push(did);
1701
+ }
1702
+ }
1703
+ return groups;
1704
+ }
1705
+ validateSchema(schema) {
1706
+ try {
1707
+ // Attempt to instantiate the schema
1708
+ this.generateSchema(schema);
1709
+ return true;
1710
+ }
1711
+ catch (error) {
1712
+ return false;
1713
+ }
1714
+ }
1715
+ generateSchema(schema) {
1716
+ if (typeof schema !== 'object' ||
1717
+ !schema ||
1718
+ !('$schema' in schema) ||
1719
+ !('properties' in schema)) {
1720
+ throw new InvalidParameterError('schema');
1721
+ }
1722
+ const template = {};
1723
+ const props = schema.properties;
1724
+ for (const property of Object.keys(props)) {
1725
+ template[property] = "TBD";
1726
+ }
1727
+ return template;
1728
+ }
1729
+ async createSchema(schema, options = {}) {
1730
+ if (!schema) {
1731
+ schema = DefaultSchema;
1732
+ }
1733
+ if (!this.validateSchema(schema)) {
1734
+ throw new InvalidParameterError('schema');
1735
+ }
1736
+ return this.createAsset({ schema }, options);
1737
+ }
1738
+ async getSchema(id) {
1739
+ const asset = await this.resolveAsset(id);
1740
+ if (!asset) {
1741
+ return null;
1742
+ }
1743
+ // TEMP during did:cid, return old version schemas
1744
+ const castOldAsset = asset;
1745
+ if (castOldAsset.properties) {
1746
+ return asset;
1747
+ }
1748
+ const castAsset = asset;
1749
+ if (!castAsset.schema) {
1750
+ return null;
1751
+ }
1752
+ return castAsset.schema;
1753
+ }
1754
+ async setSchema(id, schema) {
1755
+ if (!this.validateSchema(schema)) {
1756
+ throw new InvalidParameterError('schema');
1757
+ }
1758
+ return this.updateAsset(id, { schema });
1759
+ }
1760
+ // TBD add optional 2nd parameter that will validate JSON against the schema
1761
+ async testSchema(id) {
1762
+ try {
1763
+ const schema = await this.getSchema(id);
1764
+ // TBD Need a better way because any random object with keys can be a valid schema
1765
+ if (!schema || Object.keys(schema).length === 0) {
1766
+ return false;
1767
+ }
1768
+ return this.validateSchema(schema);
1769
+ }
1770
+ catch (error) {
1771
+ return false;
1772
+ }
1773
+ }
1774
+ async listSchemas(owner) {
1775
+ const assets = await this.listAssets(owner);
1776
+ const schemas = [];
1777
+ for (const did of assets) {
1778
+ const isSchema = await this.testSchema(did);
1779
+ if (isSchema) {
1780
+ schemas.push(did);
1781
+ }
1782
+ }
1783
+ return schemas;
1784
+ }
1785
+ async createTemplate(schemaId) {
1786
+ const isSchema = await this.testSchema(schemaId);
1787
+ if (!isSchema) {
1788
+ throw new InvalidParameterError('schemaId');
1789
+ }
1790
+ const schemaDID = await this.lookupDID(schemaId);
1791
+ const schema = await this.getSchema(schemaDID);
1792
+ const template = this.generateSchema(schema);
1793
+ template['$schema'] = schemaDID;
1794
+ return template;
1795
+ }
1796
+ async pollTemplate() {
1797
+ const now = new Date();
1798
+ const nextWeek = new Date();
1799
+ nextWeek.setDate(now.getDate() + 7);
1800
+ return {
1801
+ type: 'poll',
1802
+ version: 1,
1803
+ description: 'What is this poll about?',
1804
+ roster: 'DID of the eligible voter group',
1805
+ options: ['yes', 'no', 'abstain'],
1806
+ deadline: nextWeek.toISOString(),
1807
+ };
1808
+ }
1809
+ async createPoll(poll, options = {}) {
1810
+ if (poll.type !== 'poll') {
1811
+ throw new InvalidParameterError('poll');
1812
+ }
1813
+ if (poll.version !== 1) {
1814
+ throw new InvalidParameterError('poll.version');
1815
+ }
1816
+ if (!poll.description) {
1817
+ throw new InvalidParameterError('poll.description');
1818
+ }
1819
+ if (!poll.options || !Array.isArray(poll.options) || poll.options.length < 2 || poll.options.length > 10) {
1820
+ throw new InvalidParameterError('poll.options');
1821
+ }
1822
+ if (!poll.roster) {
1823
+ // eslint-disable-next-line
1824
+ throw new InvalidParameterError('poll.roster');
1825
+ }
1826
+ try {
1827
+ const isValidGroup = await this.testGroup(poll.roster);
1828
+ if (!isValidGroup) {
1829
+ throw new InvalidParameterError('poll.roster');
1830
+ }
1831
+ }
1832
+ catch {
1833
+ throw new InvalidParameterError('poll.roster');
1834
+ }
1835
+ if (!poll.deadline) {
1836
+ // eslint-disable-next-line
1837
+ throw new InvalidParameterError('poll.deadline');
1838
+ }
1839
+ const deadline = new Date(poll.deadline);
1840
+ if (isNaN(deadline.getTime())) {
1841
+ throw new InvalidParameterError('poll.deadline');
1842
+ }
1843
+ if (deadline < new Date()) {
1844
+ throw new InvalidParameterError('poll.deadline');
1845
+ }
1846
+ return this.createAsset({ poll }, options);
1847
+ }
1848
+ async getPoll(id) {
1849
+ const asset = await this.resolveAsset(id);
1850
+ // TEMP during did:cid, return old version poll
1851
+ const castOldAsset = asset;
1852
+ if (castOldAsset.options) {
1853
+ return castOldAsset;
1854
+ }
1855
+ const castAsset = asset;
1856
+ if (!castAsset.poll) {
1857
+ return null;
1858
+ }
1859
+ return castAsset.poll;
1860
+ }
1861
+ async testPoll(id) {
1862
+ try {
1863
+ const poll = await this.getPoll(id);
1864
+ return poll !== null;
1865
+ }
1866
+ catch (error) {
1867
+ return false;
1868
+ }
1869
+ }
1870
+ async listPolls(owner) {
1871
+ const assets = await this.listAssets(owner);
1872
+ const polls = [];
1873
+ for (const did of assets) {
1874
+ const isPoll = await this.testPoll(did);
1875
+ if (isPoll) {
1876
+ polls.push(did);
1877
+ }
1878
+ }
1879
+ return polls;
1880
+ }
1881
+ async viewPoll(pollId) {
1882
+ const id = await this.fetchIdInfo();
1883
+ const poll = await this.getPoll(pollId);
1884
+ if (!poll) {
1885
+ throw new InvalidParameterError('pollId');
1886
+ }
1887
+ let hasVoted = false;
1888
+ if (poll.ballots) {
1889
+ hasVoted = !!poll.ballots[id.did];
1890
+ }
1891
+ const voteExpired = Date.now() > new Date(poll.deadline).getTime();
1892
+ const isEligible = await this.testGroup(poll.roster, id.did);
1893
+ const doc = await this.resolveDID(pollId);
1894
+ const view = {
1895
+ description: poll.description,
1896
+ options: poll.options,
1897
+ deadline: poll.deadline,
1898
+ isOwner: (doc.didDocument?.controller === id.did),
1899
+ isEligible: isEligible,
1900
+ voteExpired: voteExpired,
1901
+ hasVoted: hasVoted,
1902
+ };
1903
+ if (id.did === doc.didDocument?.controller) {
1904
+ let voted = 0;
1905
+ const results = {
1906
+ tally: [],
1907
+ ballots: [],
1908
+ };
1909
+ results.tally.push({
1910
+ vote: 0,
1911
+ option: 'spoil',
1912
+ count: 0,
1913
+ });
1914
+ for (let i = 0; i < poll.options.length; i++) {
1915
+ results.tally.push({
1916
+ vote: i + 1,
1917
+ option: poll.options[i],
1918
+ count: 0,
1919
+ });
1920
+ }
1921
+ for (let voter in poll.ballots) {
1922
+ const ballot = poll.ballots[voter];
1923
+ const decrypted = await this.decryptJSON(ballot.ballot);
1924
+ const vote = decrypted.vote;
1925
+ if (results.ballots) {
1926
+ results.ballots.push({
1927
+ ...ballot,
1928
+ voter,
1929
+ vote,
1930
+ option: poll.options[vote - 1],
1931
+ });
1932
+ }
1933
+ voted += 1;
1934
+ results.tally[vote].count += 1;
1935
+ }
1936
+ const roster = await this.getGroup(poll.roster);
1937
+ const total = roster.members.length;
1938
+ results.votes = {
1939
+ eligible: total,
1940
+ received: voted,
1941
+ pending: total - voted,
1942
+ };
1943
+ results.final = voteExpired || (voted === total);
1944
+ view.results = results;
1945
+ }
1946
+ return view;
1947
+ }
1948
+ async votePoll(pollId, vote, options = {}) {
1949
+ const { spoil = false } = options;
1950
+ const id = await this.fetchIdInfo();
1951
+ const didPoll = await this.lookupDID(pollId);
1952
+ const doc = await this.resolveDID(didPoll);
1953
+ const poll = await this.getPoll(pollId);
1954
+ if (!poll) {
1955
+ throw new InvalidParameterError('pollId');
1956
+ }
1957
+ const eligible = await this.testGroup(poll.roster, id.did);
1958
+ const expired = Date.now() > new Date(poll.deadline).getTime();
1959
+ const owner = doc.didDocument?.controller;
1960
+ if (!owner) {
1961
+ throw new KeymasterError('owner mising from poll');
1962
+ }
1963
+ if (!eligible) {
1964
+ throw new InvalidParameterError('voter not in roster');
1965
+ }
1966
+ if (expired) {
1967
+ throw new InvalidParameterError('poll has expired');
1968
+ }
1969
+ let ballot;
1970
+ if (spoil) {
1971
+ ballot = {
1972
+ poll: didPoll,
1973
+ vote: 0,
1974
+ };
1975
+ }
1976
+ else {
1977
+ const max = poll.options.length;
1978
+ if (!Number.isInteger(vote) || vote < 1 || vote > max) {
1979
+ throw new InvalidParameterError('vote');
1980
+ }
1981
+ ballot = {
1982
+ poll: didPoll,
1983
+ vote: vote,
1984
+ };
1985
+ }
1986
+ // Encrypt for receiver only
1987
+ return await this.encryptJSON(ballot, owner, { ...options, encryptForSender: false });
1988
+ }
1989
+ async updatePoll(ballot) {
1990
+ const id = await this.fetchIdInfo();
1991
+ const didBallot = await this.lookupDID(ballot);
1992
+ const docBallot = await this.resolveDID(ballot);
1993
+ const didVoter = docBallot.didDocument.controller;
1994
+ let dataBallot;
1995
+ try {
1996
+ dataBallot = await this.decryptJSON(didBallot);
1997
+ if (!dataBallot.poll || !dataBallot.vote) {
1998
+ throw new InvalidParameterError('ballot');
1999
+ }
2000
+ }
2001
+ catch {
2002
+ throw new InvalidParameterError('ballot');
2003
+ }
2004
+ const didPoll = dataBallot.poll;
2005
+ const docPoll = await this.resolveDID(didPoll);
2006
+ const didOwner = docPoll.didDocument.controller;
2007
+ const poll = await this.getPoll(didPoll);
2008
+ if (!poll) {
2009
+ throw new KeymasterError('Cannot find poll related to ballot');
2010
+ }
2011
+ if (id.did !== didOwner) {
2012
+ throw new InvalidParameterError('only owner can update a poll');
2013
+ }
2014
+ const eligible = await this.testGroup(poll.roster, didVoter);
2015
+ if (!eligible) {
2016
+ throw new InvalidParameterError('voter not in roster');
2017
+ }
2018
+ const expired = Date.now() > new Date(poll.deadline).getTime();
2019
+ if (expired) {
2020
+ throw new InvalidParameterError('poll has expired');
2021
+ }
2022
+ const max = poll.options.length;
2023
+ const vote = dataBallot.vote;
2024
+ if (!vote || vote < 0 || vote > max) {
2025
+ throw new InvalidParameterError('ballot.vote');
2026
+ }
2027
+ if (!poll.ballots) {
2028
+ poll.ballots = {};
2029
+ }
2030
+ poll.ballots[didVoter] = {
2031
+ ballot: didBallot,
2032
+ received: new Date().toISOString(),
2033
+ };
2034
+ return this.updateAsset(didPoll, { poll });
2035
+ }
2036
+ async publishPoll(pollId, options = {}) {
2037
+ const { reveal = false } = options;
2038
+ const id = await this.fetchIdInfo();
2039
+ const doc = await this.resolveDID(pollId);
2040
+ const owner = doc.didDocument?.controller;
2041
+ if (id.did !== owner) {
2042
+ throw new InvalidParameterError('only owner can publish a poll');
2043
+ }
2044
+ const view = await this.viewPoll(pollId);
2045
+ if (!view.results?.final) {
2046
+ throw new InvalidParameterError('poll not final');
2047
+ }
2048
+ if (!reveal && view.results.ballots) {
2049
+ delete view.results.ballots;
2050
+ }
2051
+ const poll = await this.getPoll(pollId);
2052
+ if (!poll) {
2053
+ throw new InvalidParameterError(pollId);
2054
+ }
2055
+ poll.results = view.results;
2056
+ return this.updateAsset(pollId, { poll });
2057
+ }
2058
+ async unpublishPoll(pollId) {
2059
+ const id = await this.fetchIdInfo();
2060
+ const doc = await this.resolveDID(pollId);
2061
+ const owner = doc.didDocument?.controller;
2062
+ if (id.did !== owner) {
2063
+ throw new InvalidParameterError(pollId);
2064
+ }
2065
+ const poll = await this.getPoll(pollId);
2066
+ if (!poll) {
2067
+ throw new InvalidParameterError(pollId);
2068
+ }
2069
+ delete poll.results;
2070
+ return this.updateAsset(pollId, { poll });
2071
+ }
2072
+ async createGroupVault(options = {}) {
2073
+ const id = await this.fetchIdInfo();
2074
+ const idKeypair = await this.fetchKeyPair();
2075
+ // version defaults to 1. To make version undefined (unit testing), set options.version to 0
2076
+ const version = typeof options.version === 'undefined'
2077
+ ? 1
2078
+ : (typeof options.version === 'number' && options.version === 1 ? options.version : undefined);
2079
+ const salt = this.cipher.generateRandomSalt();
2080
+ const vaultKeypair = this.cipher.generateRandomJwk();
2081
+ const keys = {};
2082
+ const config = this.cipher.encryptMessage(idKeypair.publicJwk, vaultKeypair.privateJwk, JSON.stringify(options));
2083
+ const publicJwk = options.secretMembers ? idKeypair.publicJwk : vaultKeypair.publicJwk; // If secret, encrypt for the owner only
2084
+ const members = this.cipher.encryptMessage(publicJwk, vaultKeypair.privateJwk, JSON.stringify({}));
2085
+ const items = this.cipher.encryptMessage(vaultKeypair.publicJwk, vaultKeypair.privateJwk, JSON.stringify({}));
2086
+ const sha256 = this.cipher.hashJSON({});
2087
+ const groupVault = {
2088
+ version,
2089
+ publicJwk: vaultKeypair.publicJwk,
2090
+ salt,
2091
+ config,
2092
+ members,
2093
+ keys,
2094
+ items,
2095
+ sha256,
2096
+ };
2097
+ await this.addMemberKey(groupVault, id.did, vaultKeypair.privateJwk);
2098
+ return this.createAsset({ groupVault }, options);
2099
+ }
2100
+ async getGroupVault(groupVaultId, options) {
2101
+ const asset = await this.resolveAsset(groupVaultId, options);
2102
+ if (!asset.groupVault) {
2103
+ throw new InvalidParameterError('groupVaultId');
2104
+ }
2105
+ return asset.groupVault;
2106
+ }
2107
+ async testGroupVault(id, options) {
2108
+ try {
2109
+ const groupVault = await this.getGroupVault(id, options);
2110
+ return groupVault !== null;
2111
+ }
2112
+ catch (error) {
2113
+ return false;
2114
+ }
2115
+ }
2116
+ generateSaltedId(groupVault, memberDID) {
2117
+ if (!groupVault.version) {
2118
+ return this.cipher.hashMessage(groupVault.salt + memberDID);
2119
+ }
2120
+ const suffix = memberDID.split(':').pop();
2121
+ return this.cipher.hashMessage(groupVault.salt + suffix);
2122
+ }
2123
+ async decryptGroupVault(groupVault) {
2124
+ const wallet = await this.loadWallet();
2125
+ const id = await this.fetchIdInfo();
2126
+ const myMemberId = this.generateSaltedId(groupVault, id.did);
2127
+ const myVaultKey = groupVault.keys[myMemberId];
2128
+ if (!myVaultKey) {
2129
+ throw new KeymasterError('No access to group vault');
2130
+ }
2131
+ const privKeyJSON = await this.decryptWithDerivedKeys(wallet, id, groupVault.publicJwk, myVaultKey);
2132
+ const privateJwk = JSON.parse(privKeyJSON);
2133
+ let config = {};
2134
+ let isOwner = false;
2135
+ try {
2136
+ const configJSON = await this.decryptWithDerivedKeys(wallet, id, groupVault.publicJwk, groupVault.config);
2137
+ config = JSON.parse(configJSON);
2138
+ isOwner = true;
2139
+ }
2140
+ catch (error) {
2141
+ // Can't decrypt config if not the owner
2142
+ }
2143
+ let members = {};
2144
+ if (config.secretMembers) {
2145
+ try {
2146
+ const membersJSON = await this.decryptWithDerivedKeys(wallet, id, groupVault.publicJwk, groupVault.members);
2147
+ members = JSON.parse(membersJSON);
2148
+ }
2149
+ catch (error) {
2150
+ }
2151
+ }
2152
+ else {
2153
+ try {
2154
+ const membersJSON = this.cipher.decryptMessage(groupVault.publicJwk, privateJwk, groupVault.members);
2155
+ members = JSON.parse(membersJSON);
2156
+ }
2157
+ catch (error) {
2158
+ }
2159
+ }
2160
+ const itemsJSON = this.cipher.decryptMessage(groupVault.publicJwk, privateJwk, groupVault.items);
2161
+ const items = JSON.parse(itemsJSON);
2162
+ return {
2163
+ isOwner,
2164
+ privateJwk,
2165
+ config,
2166
+ members,
2167
+ items,
2168
+ };
2169
+ }
2170
+ async checkGroupVaultOwner(vaultId) {
2171
+ const id = await this.fetchIdInfo();
2172
+ const vaultDoc = await this.resolveDID(vaultId);
2173
+ const controller = vaultDoc.didDocument?.controller;
2174
+ if (controller !== id.did) {
2175
+ throw new KeymasterError('Only vault owner can modify the vault');
2176
+ }
2177
+ return controller;
2178
+ }
2179
+ async addMemberKey(groupVault, memberDID, privateJwk) {
2180
+ const memberDoc = await this.resolveDID(memberDID, { confirm: true });
2181
+ const memberPublicJwk = this.getPublicKeyJwk(memberDoc);
2182
+ const memberKey = this.cipher.encryptMessage(memberPublicJwk, privateJwk, JSON.stringify(privateJwk));
2183
+ const memberKeyId = this.generateSaltedId(groupVault, memberDID);
2184
+ groupVault.keys[memberKeyId] = memberKey;
2185
+ }
2186
+ async checkVaultVersion(vaultId, groupVault) {
2187
+ if (groupVault.version === 1) {
2188
+ return;
2189
+ }
2190
+ if (!groupVault.version) {
2191
+ const id = await this.fetchIdInfo();
2192
+ const { privateJwk, members } = await this.decryptGroupVault(groupVault);
2193
+ groupVault.version = 1;
2194
+ groupVault.keys = {};
2195
+ await this.addMemberKey(groupVault, id.did, privateJwk);
2196
+ for (const memberDID of Object.keys(members)) {
2197
+ await this.addMemberKey(groupVault, memberDID, privateJwk);
2198
+ }
2199
+ await this.updateAsset(vaultId, { groupVault });
2200
+ return;
2201
+ }
2202
+ throw new KeymasterError('Unsupported group vault version');
2203
+ }
2204
+ getAgentDID(doc) {
2205
+ if (doc.didDocumentRegistration?.type !== 'agent') {
2206
+ throw new KeymasterError('Document is not an agent');
2207
+ }
2208
+ const did = doc.didDocument?.id;
2209
+ if (!did) {
2210
+ throw new KeymasterError('Agent document does not have a DID');
2211
+ }
2212
+ return did;
2213
+ }
2214
+ async addGroupVaultMember(vaultId, memberId) {
2215
+ const owner = await this.checkGroupVaultOwner(vaultId);
2216
+ const idKeypair = await this.fetchKeyPair();
2217
+ const groupVault = await this.getGroupVault(vaultId);
2218
+ const { privateJwk, config, members } = await this.decryptGroupVault(groupVault);
2219
+ const memberDoc = await this.resolveDID(memberId, { confirm: true });
2220
+ const memberDID = this.getAgentDID(memberDoc);
2221
+ // Don't allow adding the vault owner
2222
+ if (owner === memberDID) {
2223
+ return false;
2224
+ }
2225
+ members[memberDID] = { added: new Date().toISOString() };
2226
+ const publicJwk = config.secretMembers ? idKeypair.publicJwk : groupVault.publicJwk;
2227
+ groupVault.members = this.cipher.encryptMessage(publicJwk, privateJwk, JSON.stringify(members));
2228
+ await this.addMemberKey(groupVault, memberDID, privateJwk);
2229
+ return this.updateAsset(vaultId, { groupVault });
2230
+ }
2231
+ async removeGroupVaultMember(vaultId, memberId) {
2232
+ const owner = await this.checkGroupVaultOwner(vaultId);
2233
+ const idKeypair = await this.fetchKeyPair();
2234
+ const groupVault = await this.getGroupVault(vaultId);
2235
+ const { privateJwk, config, members } = await this.decryptGroupVault(groupVault);
2236
+ const memberDoc = await this.resolveDID(memberId, { confirm: true });
2237
+ const memberDID = this.getAgentDID(memberDoc);
2238
+ // Don't allow removing the vault owner
2239
+ if (owner === memberDID) {
2240
+ return false;
2241
+ }
2242
+ delete members[memberDID];
2243
+ const publicJwk = config.secretMembers ? idKeypair.publicJwk : groupVault.publicJwk;
2244
+ groupVault.members = this.cipher.encryptMessage(publicJwk, privateJwk, JSON.stringify(members));
2245
+ const memberKeyId = this.generateSaltedId(groupVault, memberDID);
2246
+ delete groupVault.keys[memberKeyId];
2247
+ return this.updateAsset(vaultId, { groupVault });
2248
+ }
2249
+ async listGroupVaultMembers(vaultId) {
2250
+ const groupVault = await this.getGroupVault(vaultId);
2251
+ const { members, isOwner } = await this.decryptGroupVault(groupVault);
2252
+ if (isOwner) {
2253
+ await this.checkVaultVersion(vaultId, groupVault);
2254
+ }
2255
+ return members;
2256
+ }
2257
+ async addGroupVaultItem(vaultId, name, buffer) {
2258
+ await this.checkGroupVaultOwner(vaultId);
2259
+ const groupVault = await this.getGroupVault(vaultId);
2260
+ const { privateJwk, items } = await this.decryptGroupVault(groupVault);
2261
+ const validName = this.validateName(name);
2262
+ const encryptedData = this.cipher.encryptBytes(groupVault.publicJwk, privateJwk, buffer);
2263
+ const cid = await this.gatekeeper.addText(encryptedData);
2264
+ const sha256 = this.cipher.hashMessage(buffer);
2265
+ const type = await this.getMimeType(buffer);
2266
+ const data = encryptedData.length < this.maxDataLength ? encryptedData : undefined;
2267
+ items[validName] = {
2268
+ cid,
2269
+ sha256,
2270
+ bytes: buffer.length,
2271
+ type,
2272
+ added: new Date().toISOString(),
2273
+ data,
2274
+ };
2275
+ groupVault.items = this.cipher.encryptMessage(groupVault.publicJwk, privateJwk, JSON.stringify(items));
2276
+ groupVault.sha256 = this.cipher.hashJSON(items);
2277
+ return this.updateAsset(vaultId, { groupVault });
2278
+ }
2279
+ async removeGroupVaultItem(vaultId, name) {
2280
+ await this.checkGroupVaultOwner(vaultId);
2281
+ const groupVault = await this.getGroupVault(vaultId);
2282
+ const { privateJwk, items } = await this.decryptGroupVault(groupVault);
2283
+ delete items[name];
2284
+ groupVault.items = this.cipher.encryptMessage(groupVault.publicJwk, privateJwk, JSON.stringify(items));
2285
+ groupVault.sha256 = this.cipher.hashJSON(items);
2286
+ return this.updateAsset(vaultId, { groupVault });
2287
+ }
2288
+ async listGroupVaultItems(vaultId, options) {
2289
+ const groupVault = await this.getGroupVault(vaultId, options);
2290
+ const { items } = await this.decryptGroupVault(groupVault);
2291
+ return items;
2292
+ }
2293
+ async getGroupVaultItem(vaultId, name, options) {
2294
+ try {
2295
+ const groupVault = await this.getGroupVault(vaultId, options);
2296
+ const { privateJwk, items } = await this.decryptGroupVault(groupVault);
2297
+ if (items[name]) {
2298
+ const encryptedData = items[name].data || await this.gatekeeper.getText(items[name].cid);
2299
+ if (encryptedData) {
2300
+ const bytes = this.cipher.decryptBytes(groupVault.publicJwk, privateJwk, encryptedData);
2301
+ return Buffer.from(bytes);
2302
+ }
2303
+ }
2304
+ return null;
2305
+ }
2306
+ catch (error) {
2307
+ return null;
2308
+ }
2309
+ }
2310
+ async listDmail() {
2311
+ const wallet = await this.loadWallet();
2312
+ const id = await this.fetchIdInfo(undefined, wallet);
2313
+ const list = id.dmail || {};
2314
+ const dmailList = {};
2315
+ const nameList = await this.listNames({ includeIDs: true });
2316
+ const didToName = Object.entries(nameList).reduce((acc, [name, did]) => {
2317
+ acc[did] = name;
2318
+ return acc;
2319
+ }, {});
2320
+ for (const did of Object.keys(list)) {
2321
+ const message = await this.getDmailMessage(did);
2322
+ if (!message) {
2323
+ continue; // Skip if no dmail found for this DID
2324
+ }
2325
+ const tags = list[did].tags ?? [];
2326
+ const docs = await this.resolveDID(did);
2327
+ const controller = docs.didDocument?.controller ?? '';
2328
+ const sender = didToName[controller] ?? controller;
2329
+ const date = docs.didDocumentMetadata?.updated ?? '';
2330
+ const to = message.to.map(did => didToName[did] ?? did);
2331
+ const cc = message.cc.map(did => didToName[did] ?? did);
2332
+ const attachments = await this.listDmailAttachments(did);
2333
+ dmailList[did] = {
2334
+ message,
2335
+ to,
2336
+ cc,
2337
+ tags,
2338
+ sender,
2339
+ date,
2340
+ attachments,
2341
+ docs,
2342
+ };
2343
+ }
2344
+ return dmailList;
2345
+ }
2346
+ verifyTagList(tags) {
2347
+ if (!Array.isArray(tags)) {
2348
+ throw new InvalidParameterError('tags');
2349
+ }
2350
+ const tagSet = new Set();
2351
+ for (const tag of tags) {
2352
+ try {
2353
+ tagSet.add(this.validateName(tag));
2354
+ }
2355
+ catch (error) {
2356
+ throw new InvalidParameterError(`Invalid tag: '${tag}'`);
2357
+ }
2358
+ }
2359
+ return tagSet.size > 0 ? Array.from(tagSet) : [];
2360
+ }
2361
+ async fileDmail(did, tags) {
2362
+ const verifiedTags = this.verifyTagList(tags);
2363
+ await this.mutateWallet(async (wallet) => {
2364
+ const id = await this.fetchIdInfo(undefined, wallet);
2365
+ if (!id.dmail) {
2366
+ id.dmail = {};
2367
+ }
2368
+ id.dmail[did] = { tags: verifiedTags };
2369
+ });
2370
+ return true;
2371
+ }
2372
+ async removeDmail(did) {
2373
+ await this.mutateWallet(async (wallet) => {
2374
+ const id = await this.fetchIdInfo(undefined, wallet);
2375
+ if (!id.dmail || !id.dmail[did]) {
2376
+ return;
2377
+ }
2378
+ delete id.dmail[did];
2379
+ });
2380
+ return true;
2381
+ }
2382
+ async verifyRecipientList(list) {
2383
+ if (!Array.isArray(list)) {
2384
+ throw new InvalidParameterError('list');
2385
+ }
2386
+ const nameList = await this.listNames({ includeIDs: true });
2387
+ let newList = [];
2388
+ for (const id of list) {
2389
+ if (typeof id !== 'string') {
2390
+ throw new InvalidParameterError(`Invalid recipient type: ${typeof id}`);
2391
+ }
2392
+ if (id in nameList) {
2393
+ const did = nameList[id];
2394
+ const isAgent = await this.testAgent(did);
2395
+ if (isAgent) {
2396
+ newList.push(did);
2397
+ continue;
2398
+ }
2399
+ throw new InvalidParameterError(`Invalid recipient: ${id}`);
2400
+ }
2401
+ if (isValidDID(id)) {
2402
+ newList.push(id);
2403
+ continue;
2404
+ }
2405
+ throw new InvalidParameterError(`Invalid recipient: ${id}`);
2406
+ }
2407
+ return newList;
2408
+ }
2409
+ async verifyDmail(message) {
2410
+ const to = await this.verifyRecipientList(message.to);
2411
+ const cc = await this.verifyRecipientList(message.cc);
2412
+ if (to.length === 0) {
2413
+ throw new InvalidParameterError('dmail.to');
2414
+ }
2415
+ if (!message.subject || typeof message.subject !== 'string' || message.subject.trim() === '') {
2416
+ throw new InvalidParameterError('dmail.subject');
2417
+ }
2418
+ if (!message.body || typeof message.body !== 'string' || message.body.trim() === '') {
2419
+ throw new InvalidParameterError('dmail.body');
2420
+ }
2421
+ return {
2422
+ ...message,
2423
+ to,
2424
+ cc,
2425
+ };
2426
+ }
2427
+ async createDmail(message, options = {}) {
2428
+ const dmail = await this.verifyDmail(message);
2429
+ const did = await this.createGroupVault(options);
2430
+ for (const toDID of dmail.to) {
2431
+ await this.addGroupVaultMember(did, toDID);
2432
+ }
2433
+ for (const ccDID of dmail.cc) {
2434
+ await this.addGroupVaultMember(did, ccDID);
2435
+ }
2436
+ const buffer = Buffer.from(JSON.stringify({ dmail }), 'utf-8');
2437
+ await this.addGroupVaultItem(did, DmailTags.DMAIL, buffer);
2438
+ await this.fileDmail(did, [DmailTags.DRAFT]);
2439
+ return did;
2440
+ }
2441
+ async updateDmail(did, message) {
2442
+ const dmail = await this.verifyDmail(message);
2443
+ for (const toDID of dmail.to) {
2444
+ await this.addGroupVaultMember(did, toDID);
2445
+ }
2446
+ for (const ccDID of dmail.cc) {
2447
+ await this.addGroupVaultMember(did, ccDID);
2448
+ }
2449
+ const buffer = Buffer.from(JSON.stringify({ dmail }), 'utf-8');
2450
+ return this.addGroupVaultItem(did, DmailTags.DMAIL, buffer);
2451
+ }
2452
+ async sendDmail(did) {
2453
+ const dmail = await this.getDmailMessage(did);
2454
+ if (!dmail) {
2455
+ return null;
2456
+ }
2457
+ const registry = this.ephemeralRegistry;
2458
+ const validUntil = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(); // Default to 7 days
2459
+ const message = {
2460
+ to: [...dmail.to, ...dmail.cc],
2461
+ dids: [did],
2462
+ };
2463
+ const notice = await this.createNotice(message, { registry, validUntil });
2464
+ if (notice) {
2465
+ await this.fileDmail(did, [DmailTags.SENT]);
2466
+ }
2467
+ return notice;
2468
+ }
2469
+ async getDmailMessage(did, options) {
2470
+ const isGroupVault = await this.testGroupVault(did, options);
2471
+ if (!isGroupVault) {
2472
+ return null;
2473
+ }
2474
+ const buffer = await this.getGroupVaultItem(did, DmailTags.DMAIL, options);
2475
+ if (!buffer) {
2476
+ return null;
2477
+ }
2478
+ try {
2479
+ const data = JSON.parse(buffer.toString('utf-8'));
2480
+ return data.dmail;
2481
+ }
2482
+ catch (error) {
2483
+ return null;
2484
+ }
2485
+ }
2486
+ async listDmailAttachments(did, options) {
2487
+ let items = await this.listGroupVaultItems(did, options);
2488
+ delete items[DmailTags.DMAIL]; // Remove the dmail item itself from attachments
2489
+ return items;
2490
+ }
2491
+ async addDmailAttachment(did, name, buffer) {
2492
+ if (name === DmailTags.DMAIL) {
2493
+ throw new InvalidParameterError('Cannot add attachment with reserved name "dmail"');
2494
+ }
2495
+ return this.addGroupVaultItem(did, name, buffer);
2496
+ }
2497
+ async removeDmailAttachment(did, name) {
2498
+ if (name === DmailTags.DMAIL) {
2499
+ throw new InvalidParameterError('Cannot remove attachment with reserved name "dmail"');
2500
+ }
2501
+ return this.removeGroupVaultItem(did, name);
2502
+ }
2503
+ async getDmailAttachment(did, name) {
2504
+ return this.getGroupVaultItem(did, name);
2505
+ }
2506
+ async importDmail(did) {
2507
+ const dmail = await this.getDmailMessage(did);
2508
+ if (!dmail) {
2509
+ return false;
2510
+ }
2511
+ return this.fileDmail(did, [DmailTags.INBOX, DmailTags.UNREAD]);
2512
+ }
2513
+ async verifyDIDList(didList) {
2514
+ if (!Array.isArray(didList)) {
2515
+ throw new InvalidParameterError('didList');
2516
+ }
2517
+ for (const did of didList) {
2518
+ if (!isValidDID(did)) {
2519
+ throw new InvalidParameterError(`Invalid DID: ${did}`);
2520
+ }
2521
+ }
2522
+ return didList;
2523
+ }
2524
+ async verifyNotice(notice) {
2525
+ const to = await this.verifyRecipientList(notice.to);
2526
+ const dids = await this.verifyDIDList(notice.dids);
2527
+ if (to.length === 0) {
2528
+ throw new InvalidParameterError('notice.to');
2529
+ }
2530
+ if (dids.length === 0) {
2531
+ throw new InvalidParameterError('notice.dids');
2532
+ }
2533
+ return { to, dids };
2534
+ }
2535
+ async createNotice(message, options = {}) {
2536
+ const notice = await this.verifyNotice(message);
2537
+ return this.createAsset({ notice }, options);
2538
+ }
2539
+ async updateNotice(id, message) {
2540
+ const notice = await this.verifyNotice(message);
2541
+ return this.updateAsset(id, { notice });
2542
+ }
2543
+ async addToNotices(did, tags) {
2544
+ const verifiedTags = this.verifyTagList(tags);
2545
+ await this.mutateWallet(async (wallet) => {
2546
+ const id = await this.fetchIdInfo(undefined, wallet);
2547
+ if (!id.notices)
2548
+ id.notices = {};
2549
+ id.notices[did] = { tags: verifiedTags };
2550
+ });
2551
+ return true;
2552
+ }
2553
+ async importNotice(did) {
2554
+ const wallet = await this.loadWallet();
2555
+ const id = await this.fetchIdInfo(undefined, wallet);
2556
+ if (id.notices && id.notices[did]) {
2557
+ return true; // Already imported
2558
+ }
2559
+ const asset = await this.resolveAsset(did);
2560
+ if (!asset || !asset.notice) {
2561
+ return false; // Not a notice
2562
+ }
2563
+ if (!asset.notice.to.includes(id.did)) {
2564
+ return false; // Not for this user
2565
+ }
2566
+ for (const noticeDID of asset.notice.dids) {
2567
+ const dmail = await this.getDmailMessage(noticeDID);
2568
+ if (dmail) {
2569
+ const imported = await this.importDmail(noticeDID);
2570
+ if (imported) {
2571
+ await this.addToNotices(did, [NoticeTags.DMAIL]);
2572
+ }
2573
+ continue;
2574
+ }
2575
+ const isBallot = await this.isBallot(noticeDID);
2576
+ if (isBallot) {
2577
+ let imported = false;
2578
+ try {
2579
+ imported = await this.updatePoll(noticeDID);
2580
+ }
2581
+ catch { }
2582
+ if (imported) {
2583
+ await this.addToNotices(did, [NoticeTags.BALLOT]);
2584
+ }
2585
+ continue;
2586
+ }
2587
+ const poll = await this.getPoll(noticeDID);
2588
+ if (poll) {
2589
+ const names = await this.listNames();
2590
+ if (!Object.values(names).includes(noticeDID)) {
2591
+ await this.addUnnamedPoll(noticeDID);
2592
+ }
2593
+ await this.addToNotices(did, [NoticeTags.POLL]);
2594
+ continue;
2595
+ }
2596
+ const isCredential = await this.acceptCredential(noticeDID);
2597
+ if (isCredential) {
2598
+ await this.addToNotices(did, [NoticeTags.CREDENTIAL]);
2599
+ continue;
2600
+ }
2601
+ return false;
2602
+ }
2603
+ return true;
2604
+ }
2605
+ async searchNotices() {
2606
+ if (!this.searchEngine) {
2607
+ return false; // Search engine not available
2608
+ }
2609
+ const id = await this.fetchIdInfo();
2610
+ if (!id.notices) {
2611
+ id.notices = {};
2612
+ }
2613
+ // Search for all notice DIDs sent to the current ID
2614
+ const where = {
2615
+ "didDocumentData.notice.to[*]": {
2616
+ "$in": [id.did]
2617
+ }
2618
+ };
2619
+ let notices;
2620
+ try {
2621
+ // TBD search engine should not return expired notices
2622
+ notices = await this.searchEngine.search({ where });
2623
+ }
2624
+ catch (error) {
2625
+ throw new KeymasterError('Failed to search for notices');
2626
+ }
2627
+ for (const notice of notices) {
2628
+ if (notice in id.notices) {
2629
+ continue; // Already imported
2630
+ }
2631
+ try {
2632
+ await this.importNotice(notice);
2633
+ }
2634
+ catch (error) {
2635
+ continue; // Skip if notice is expired or invalid
2636
+ }
2637
+ }
2638
+ return true;
2639
+ }
2640
+ async cleanupNotices() {
2641
+ await this.mutateWallet(async (wallet) => {
2642
+ const id = await this.fetchIdInfo(undefined, wallet);
2643
+ if (!id.notices) {
2644
+ return;
2645
+ }
2646
+ for (const nDid of Object.keys(id.notices)) {
2647
+ try {
2648
+ const asset = await this.resolveAsset(nDid);
2649
+ if (!asset || !asset.notice) {
2650
+ delete id.notices[nDid]; // revoked or invalid
2651
+ }
2652
+ }
2653
+ catch {
2654
+ delete id.notices[nDid]; // expired/unresolvable
2655
+ }
2656
+ }
2657
+ });
2658
+ return true;
2659
+ }
2660
+ async refreshNotices() {
2661
+ await this.searchNotices();
2662
+ return this.cleanupNotices();
2663
+ }
2664
+ async exportEncryptedWallet() {
2665
+ const wallet = await this.loadWallet();
2666
+ return this.encryptWalletForStorage(wallet);
2667
+ }
2668
+ async isBallot(ballotDid) {
2669
+ let payload;
2670
+ try {
2671
+ payload = await this.decryptJSON(ballotDid);
2672
+ }
2673
+ catch {
2674
+ return false;
2675
+ }
2676
+ return payload && typeof payload.poll === "string" && typeof payload.vote === "number";
2677
+ }
2678
+ async addUnnamedPoll(did) {
2679
+ const fallbackName = did.slice(-32);
2680
+ try {
2681
+ await this.addName(fallbackName, did);
2682
+ }
2683
+ catch { }
2684
+ }
2685
+ async getHDKeyFromCacheOrMnemonic(wallet) {
2686
+ if (this._hdkeyCache) {
2687
+ return this._hdkeyCache;
2688
+ }
2689
+ const mnemonic = await this.getMnemonicForDerivation(wallet);
2690
+ this._hdkeyCache = this.cipher.generateHDKey(mnemonic);
2691
+ return this._hdkeyCache;
2692
+ }
2693
+ async encryptWalletForStorage(decrypted) {
2694
+ const { version, seed, ...rest } = decrypted;
2695
+ const safeSeed = { mnemonicEnc: seed.mnemonicEnc };
2696
+ const hdkey = await this.getHDKeyFromCacheOrMnemonic(decrypted);
2697
+ const { publicJwk, privateJwk } = this.cipher.generateJwk(hdkey.privateKey);
2698
+ const plaintext = JSON.stringify(rest);
2699
+ const enc = this.cipher.encryptMessage(publicJwk, privateJwk, plaintext);
2700
+ return { version: version, seed: safeSeed, enc };
2701
+ }
2702
+ async decryptWalletFromStorage(stored) {
2703
+ let mnemonic;
2704
+ try {
2705
+ mnemonic = await decMnemonic(stored.seed.mnemonicEnc, this.passphrase);
2706
+ }
2707
+ catch {
2708
+ throw new KeymasterError('Incorrect passphrase.');
2709
+ }
2710
+ this._hdkeyCache = this.cipher.generateHDKey(mnemonic);
2711
+ const { publicJwk, privateJwk } = this.cipher.generateJwk(this._hdkeyCache.privateKey);
2712
+ const plaintext = this.cipher.decryptMessage(publicJwk, privateJwk, stored.enc);
2713
+ const data = JSON.parse(plaintext);
2714
+ const wallet = { version: stored.version, seed: stored.seed, ...data };
2715
+ return wallet;
2716
+ }
2717
+ async decryptWallet(wallet) {
2718
+ if (isWalletEncFile(wallet)) {
2719
+ wallet = await this.decryptWalletFromStorage(wallet);
2720
+ }
2721
+ if (!isWalletFile(wallet)) {
2722
+ throw new KeymasterError("Unsupported wallet version.");
2723
+ }
2724
+ return wallet;
2725
+ }
2726
+ async upgradeWallet(wallet) {
2727
+ if (wallet.version !== 1) {
2728
+ throw new KeymasterError("Unsupported wallet version.");
2729
+ }
2730
+ return wallet;
2731
+ }
2732
+ }
2733
+ //# sourceMappingURL=keymaster.js.map