epistery 1.0.0

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.
@@ -0,0 +1,663 @@
1
+ /*
2
+ * Witness - Browser client for Epistery
3
+ *
4
+ * This is the browser-side client that connects to the Epistery server
5
+ * and provides local wallet functionality for signing data
6
+ */
7
+
8
+ import { Wallet, Web3Wallet, BrowserWallet } from './wallet.js';
9
+
10
+ // Global ethers variable - will be loaded dynamically if needed
11
+ let ethers;
12
+
13
+ // Function to ensure ethers is loaded
14
+ async function ensureEthers() {
15
+ if (ethers) return ethers;
16
+
17
+ if (typeof window !== 'undefined' && window.ethers) {
18
+ ethers = window.ethers;
19
+ return ethers;
20
+ }
21
+
22
+ // Dynamically import ethers from the epistery lib endpoint
23
+ try {
24
+ const ethersModule = await import('/.well-known/epistery/lib/ethers.js');
25
+ ethers = ethersModule.ethers || ethersModule.default || ethersModule;
26
+ // Make it available globally for future use
27
+ if (typeof window !== 'undefined') {
28
+ window.ethers = ethers;
29
+ }
30
+ return ethers;
31
+ } catch (error) {
32
+ console.error('Failed to load ethers.js:', error);
33
+ throw new Error('ethers.js is required but not available');
34
+ }
35
+ }
36
+
37
+ export default class Witness {
38
+ constructor() {
39
+ if (Witness.instance) return Witness.instance;
40
+ Witness.instance = this;
41
+ this.wallet = null;
42
+ this.server = null;
43
+ return this;
44
+ }
45
+
46
+ save() {
47
+ const storageData = this.loadStorageData();
48
+
49
+ // If current wallet exists, update or add it to the wallets array
50
+ if (this.wallet) {
51
+ const walletData = {
52
+ id: this.wallet.id || this.generateWalletId(this.wallet.source),
53
+ wallet: this.wallet.toJSON(),
54
+ label: this.wallet.label || (this.wallet.source === 'web3' ? 'Web3 Wallet' : 'Browser Wallet'),
55
+ createdAt: this.wallet.createdAt || Date.now(),
56
+ lastUsed: Date.now()
57
+ };
58
+
59
+ // Store the ID back on the wallet object
60
+ this.wallet.id = walletData.id;
61
+ this.wallet.label = walletData.label;
62
+ this.wallet.createdAt = walletData.createdAt;
63
+
64
+ // Update or add wallet in the array
65
+ const existingIndex = storageData.wallets.findIndex(w => w.id === walletData.id);
66
+ if (existingIndex >= 0) {
67
+ storageData.wallets[existingIndex] = walletData;
68
+ } else {
69
+ storageData.wallets.push(walletData);
70
+ }
71
+
72
+ // Set as default if no default exists
73
+ if (!storageData.defaultWalletId) {
74
+ storageData.defaultWalletId = walletData.id;
75
+ }
76
+ }
77
+
78
+ storageData.server = this.server;
79
+
80
+ localStorage.setItem('epistery', JSON.stringify(storageData));
81
+ }
82
+
83
+ loadStorageData() {
84
+ const data = localStorage.getItem('epistery');
85
+ if (!data) {
86
+ return { wallets: [], defaultWalletId: null, server: null };
87
+ }
88
+
89
+ try {
90
+ const parsed = JSON.parse(data);
91
+
92
+ // Migrate old single-wallet format to new multi-wallet format
93
+ if (parsed.wallet && !parsed.wallets) {
94
+ const migratedWalletId = this.generateWalletId(parsed.wallet.source);
95
+ return {
96
+ wallets: [{
97
+ id: migratedWalletId,
98
+ wallet: parsed.wallet,
99
+ label: parsed.wallet.source === 'web3' ? 'Web3 Wallet' : 'Browser Wallet',
100
+ createdAt: Date.now(),
101
+ lastUsed: Date.now()
102
+ }],
103
+ defaultWalletId: migratedWalletId,
104
+ server: parsed.server
105
+ };
106
+ }
107
+
108
+ return {
109
+ wallets: parsed.wallets || [],
110
+ defaultWalletId: parsed.defaultWalletId || null,
111
+ server: parsed.server || null
112
+ };
113
+ } catch (error) {
114
+ console.error('Failed to parse epistery data:', error);
115
+ return { wallets: [], defaultWalletId: null, server: null };
116
+ }
117
+ }
118
+
119
+ async load() {
120
+ const storageData = this.loadStorageData();
121
+
122
+ this.server = storageData.server;
123
+
124
+ // Check if migration happened and persist it immediately to avoid data loss
125
+ const currentData = localStorage.getItem('epistery');
126
+ if (currentData) {
127
+ const parsed = JSON.parse(currentData);
128
+ // If we migrated from old format (had wallet but no wallets), save the migration
129
+ if (parsed.wallet && !parsed.wallets && storageData.wallets.length > 0) {
130
+ console.log('[epistery] Migrating old wallet format to multi-wallet format');
131
+ localStorage.setItem('epistery', JSON.stringify(storageData));
132
+ console.log('[epistery] Migration complete - wallet preserved');
133
+ }
134
+ }
135
+
136
+ // Load the default wallet if it exists (maintains backward compatibility)
137
+ if (storageData.defaultWalletId && ethers) {
138
+ const walletData = storageData.wallets.find(w => w.id === storageData.defaultWalletId);
139
+ if (walletData) {
140
+ this.wallet = await Wallet.fromJSON(walletData.wallet, ethers);
141
+ this.wallet.id = walletData.id;
142
+ this.wallet.label = walletData.label;
143
+ this.wallet.createdAt = walletData.createdAt;
144
+ }
145
+ }
146
+ }
147
+
148
+ generateWalletId(source) {
149
+ const prefix = source === 'web3' ? 'web3-wallet' : 'browser-wallet';
150
+ return prefix + '-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
151
+ }
152
+
153
+ static async connect(options = {}) {
154
+ let witness = new Witness();
155
+
156
+ try {
157
+ // Ensure ethers is loaded first
158
+ ethers = await ensureEthers();
159
+
160
+ // Load existing data (now that ethers is available)
161
+ await witness.load();
162
+
163
+ // Get server info to check chain compatibility
164
+ await witness.fetchServerInfo();
165
+
166
+ // Initialize wallet if needed
167
+ if (!witness.wallet) {
168
+ await witness.initialize();
169
+ }
170
+
171
+ // Verify chain compatibility and switch if needed
172
+ await witness.ensureChainCompatibility();
173
+
174
+ // Perform key exchange (skip if skipKeyExchange option is true)
175
+ if (!options.skipKeyExchange) {
176
+ await witness.performKeyExchange();
177
+ }
178
+
179
+ } catch (e) {
180
+ console.error('Failed to connect to Epistery server:', e);
181
+ // For unclaimed domains, wallet discovery might succeed even if key exchange fails
182
+ if (!options.skipKeyExchange) {
183
+ throw e;
184
+ }
185
+ }
186
+
187
+ return witness;
188
+ }
189
+
190
+ async initialize() {
191
+ try {
192
+ // Try Web3 first, then fall back to browser wallet
193
+ this.wallet = await Web3Wallet.create(ethers);
194
+
195
+ if (!this.wallet) {
196
+ this.wallet = await BrowserWallet.create(ethers);
197
+ }
198
+
199
+ if (this.wallet) {
200
+ console.log(`Wallet initialized: ${this.wallet.source} (${this.wallet.address})`);
201
+ this.save();
202
+ } else {
203
+ throw new Error('Failed to create any wallet type');
204
+ }
205
+ } catch (e) {
206
+ console.error('Failed to initialize wallet:', e);
207
+ }
208
+ }
209
+
210
+ async fetchServerInfo() {
211
+ try {
212
+ const response = await fetch('/.well-known/epistery');
213
+ if (response.ok) {
214
+ const serverInfo = await response.json();
215
+ this.serverInfo = serverInfo.server;
216
+ } else {
217
+ throw new Error('Failed to fetch server info');
218
+ }
219
+ } catch (e) {
220
+ console.error('Failed to fetch server info:', e);
221
+ }
222
+ }
223
+
224
+ async ensureChainCompatibility() {
225
+ if (!this.wallet || this.wallet.source !== 'web3' || !this.serverInfo) {
226
+ return;
227
+ }
228
+
229
+ try {
230
+ const targetChainId = this.serverInfo.chainId;
231
+ const targetRpc = this.serverInfo.rpc;
232
+
233
+ // Get current chain ID
234
+ const currentNetwork = await this.wallet.provider.getNetwork();
235
+
236
+ // Parse server chain ID (remove comma if present)
237
+ const expectedChainId = parseInt(targetChainId.toString().replace(',', ''));
238
+
239
+ if (currentNetwork.chainId !== expectedChainId) {
240
+ await this.requestChainSwitch(expectedChainId, targetRpc, this.serverInfo.provider);
241
+ }
242
+ } catch (e) {
243
+ console.warn('Chain compatibility check failed:', e);
244
+ }
245
+ }
246
+
247
+ async requestChainSwitch(chainId, rpcUrl, networkName) {
248
+ if (!window.ethereum) {
249
+ throw new Error('No Web3 provider available for chain switching');
250
+ }
251
+
252
+ try {
253
+ // First, try to switch to the chain
254
+ await window.ethereum.request({
255
+ method: 'wallet_switchEthereumChain',
256
+ params: [{ chainId: `0x${chainId.toString(16)}` }],
257
+ });
258
+
259
+ } catch (switchError) {
260
+ // If the chain hasn't been added to MetaMask, add it
261
+ if (switchError.code === 4902) {
262
+ try {
263
+ // Build nativeCurrency from serverInfo with sensible defaults
264
+ const nativeCurrency = this.serverInfo?.nativeCurrency || {
265
+ name: 'ETH',
266
+ symbol: 'ETH',
267
+ decimals: 18
268
+ };
269
+
270
+ await window.ethereum.request({
271
+ method: 'wallet_addEthereumChain',
272
+ params: [{
273
+ chainId: `0x${chainId.toString(16)}`,
274
+ chainName: networkName || `Chain ${chainId}`,
275
+ rpcUrls: [rpcUrl],
276
+ nativeCurrency: nativeCurrency,
277
+ }],
278
+ });
279
+
280
+ } catch (addError) {
281
+ console.error('Failed to add chain:', addError);
282
+ throw addError;
283
+ }
284
+ } else {
285
+ console.error('Failed to switch chain:', switchError);
286
+ throw switchError;
287
+ }
288
+ }
289
+
290
+ // Recreate the provider connection after chain switch
291
+ if (this.wallet && this.wallet.source === 'web3') {
292
+ this.wallet.provider = new ethers.providers.Web3Provider(window.ethereum);
293
+ this.wallet.signer = this.wallet.provider.getSigner();
294
+ }
295
+ }
296
+
297
+ generateChallenge() {
298
+ // Generate a random 32-byte challenge for key exchange
299
+ return ethers.utils.hexlify(ethers.utils.randomBytes(32));
300
+ }
301
+
302
+ async performKeyExchange() {
303
+ try {
304
+ if (!this.wallet) {
305
+ throw new Error('No wallet available for key exchange');
306
+ }
307
+
308
+ // Create a message to sign for identity proof
309
+ const challenge = this.generateChallenge();
310
+ const message = `Epistery Key Exchange - ${this.wallet.address} - ${challenge}`;
311
+
312
+ // Sign the message using the wallet
313
+ const signature = await this.wallet.sign(message, ethers);
314
+
315
+ // Get the updated public key (especially important for Web3 wallets)
316
+ const publicKey = this.wallet.publicKey;
317
+
318
+ // Send key exchange request to server
319
+ const keyExchangeData = {
320
+ clientAddress: this.wallet.address,
321
+ clientPublicKey: publicKey,
322
+ challenge: challenge,
323
+ message: message,
324
+ signature: signature,
325
+ walletSource: this.wallet.source
326
+ };
327
+
328
+ const response = await fetch('/.well-known/epistery/connect', {
329
+ method: 'POST',
330
+ credentials: 'include',
331
+ headers: {'Content-Type': 'application/json'},
332
+ body: JSON.stringify(keyExchangeData)
333
+ });
334
+
335
+ if (response.ok) {
336
+ const serverResponse = await response.json();
337
+
338
+ // Verify server's identity by checking signature
339
+ if (this.verifyServerIdentity(serverResponse)) {
340
+ this.server = {
341
+ address: serverResponse.serverAddress,
342
+ publicKey: serverResponse.serverPublicKey,
343
+ services: serverResponse.services,
344
+ challenge: serverResponse.challenge,
345
+ signature: serverResponse.signature,
346
+ identified: true,
347
+ provider: this.serverInfo?.provider,
348
+ chainId: this.serverInfo?.chainId,
349
+ rpc: this.serverInfo?.rpc,
350
+ nativeCurrency: this.serverInfo?.nativeCurrency
351
+ };
352
+
353
+ this.save();
354
+ console.log('Key exchange completed successfully');
355
+ console.log('Server address:', this.server.address);
356
+ console.log('Available services:', this.server.services);
357
+ } else {
358
+ throw new Error('Server identity verification failed');
359
+ }
360
+ } else {
361
+ const errorResponse = await response.json();
362
+ throw new Error(`Key exchange failed with status: ${response.status} - ${errorResponse.error || 'Unknown error'}`);
363
+ }
364
+ } catch (e) {
365
+ console.error('Key exchange failed:', e);
366
+ throw e;
367
+ }
368
+ }
369
+
370
+ verifyServerIdentity(serverResponse) {
371
+ try {
372
+ // Reconstruct the message the server should have signed
373
+ const expectedMessage = `Epistery Server Response - ${serverResponse.serverAddress} - ${serverResponse.challenge}`;
374
+
375
+ // Verify the signature matches the server's public key
376
+ const recoveredAddress = ethers.utils.verifyMessage(expectedMessage, serverResponse.signature);
377
+ return recoveredAddress === serverResponse.serverAddress;
378
+ } catch (e) {
379
+ console.error('Server identity verification error:', e);
380
+ return false;
381
+ }
382
+ }
383
+
384
+ async transferOwnershipEvent(futureOwnerWalletAddress) {
385
+ if (!this.wallet) {
386
+ throw new Error('Wallet not initialized');
387
+ }
388
+
389
+ try {
390
+ const clientWalletInfo = {
391
+ address: this.wallet.address,
392
+ publicKey: this.wallet.publicKey,
393
+ mnemonic: this.wallet.mnemonic || '', // Only available for browser wallets
394
+ privateKey: this.wallet.privateKey || '', // Only available for browser wallets
395
+ };
396
+
397
+ let options = {
398
+ method: 'PUT',
399
+ credentials: 'include',
400
+ headers: {'Content-Type': 'application/json'}
401
+ };
402
+ options.body = JSON.stringify({
403
+ clientWalletInfo: clientWalletInfo,
404
+ futureOwnerWalletAddress: futureOwnerWalletAddress
405
+ });
406
+
407
+ let result = await fetch('/.well-known/epistery/data/ownership', options);
408
+
409
+ if (result.ok) {
410
+ return await result.json();
411
+ }
412
+ else {
413
+ throw new Error(`Transfer ownership failed with status: ${result.status}`);
414
+ }
415
+ } catch (e) {
416
+ console.error('Failed to execute transfer ownership event:', e);
417
+ throw e;
418
+ }
419
+ }
420
+
421
+ async readEvent() {
422
+ if (!this.wallet) {
423
+ throw new Error('Wallet not initialized');
424
+ }
425
+
426
+ try {
427
+ const clientWalletInfo = {
428
+ address: this.wallet.address,
429
+ publicKey: this.wallet.publicKey,
430
+ mnemonic: this.wallet.mnemonic || '', // Only available for browser wallets
431
+ privateKey: this.wallet.privateKey || '', // Only available for browser wallets
432
+ };
433
+
434
+ let options = {
435
+ method: 'POST',
436
+ credentials: 'include',
437
+ headers: {'Content-Type': 'application/json'}
438
+ };
439
+ options.body = JSON.stringify({
440
+ clientWalletInfo: clientWalletInfo,
441
+ });
442
+
443
+ let result = await fetch('/.well-known/epistery/data/read', options);
444
+ if (result.status === 204) {
445
+ return null;
446
+ }
447
+
448
+ if (result.ok) {
449
+ return await result.json();
450
+ }
451
+ else {
452
+ throw new Error(`Read failed with status: ${result.status}`);
453
+ }
454
+ } catch (e) {
455
+ console.error('Failed to execute read event:', e);
456
+ throw e;
457
+ }
458
+ }
459
+
460
+ async writeEvent(data) {
461
+ if (!this.wallet) {
462
+ throw new Error('Wallet not initialized');
463
+ }
464
+
465
+ try {
466
+ // Convert wallet to the format expected by the server
467
+ const clientWalletInfo = {
468
+ address: this.wallet.address,
469
+ publicKey: this.wallet.publicKey,
470
+ mnemonic: this.wallet.mnemonic || '', // Only available for browser wallets
471
+ privateKey: this.wallet.privateKey || '', // Only available for browser wallets
472
+ };
473
+
474
+ let options = {
475
+ method: 'POST',
476
+ credentials: 'include',
477
+ headers: {'Content-Type': 'application/json'}
478
+ };
479
+ options.body = JSON.stringify({
480
+ clientWalletInfo: clientWalletInfo,
481
+ data: data
482
+ });
483
+
484
+ let result = await fetch('/.well-known/epistery/data/write', options);
485
+
486
+ if (result.ok) {
487
+ return await result.json();
488
+ } else {
489
+ throw new Error(`Write failed with status: ${result.status}`);
490
+ }
491
+ } catch (e) {
492
+ console.error('Failed to write event:', e);
493
+ throw e;
494
+ }
495
+ }
496
+
497
+ // Wallet management methods for multi-wallet support
498
+
499
+ getWallets() {
500
+ const storageData = this.loadStorageData();
501
+ return {
502
+ wallets: storageData.wallets.map(w => ({
503
+ id: w.id,
504
+ address: w.wallet.address,
505
+ source: w.wallet.source,
506
+ label: w.label,
507
+ createdAt: w.createdAt,
508
+ lastUsed: w.lastUsed,
509
+ isDefault: w.id === storageData.defaultWalletId
510
+ })),
511
+ defaultWalletId: storageData.defaultWalletId
512
+ };
513
+ }
514
+
515
+ async addWeb3Wallet(label = null) {
516
+ await ensureEthers();
517
+ const newWallet = await Web3Wallet.create(ethers);
518
+
519
+ if (!newWallet) {
520
+ throw new Error('Failed to connect Web3 wallet. User may have cancelled or no Web3 provider available.');
521
+ }
522
+
523
+ newWallet.label = label || 'Web3 Wallet';
524
+
525
+ // Temporarily set as active wallet to save it
526
+ const previousWallet = this.wallet;
527
+ this.wallet = newWallet;
528
+ this.save();
529
+
530
+ // Restore previous wallet if there was one
531
+ if (previousWallet) {
532
+ this.wallet = previousWallet;
533
+ }
534
+
535
+ return {
536
+ id: newWallet.id,
537
+ address: newWallet.address,
538
+ source: newWallet.source,
539
+ label: newWallet.label
540
+ };
541
+ }
542
+
543
+ async addBrowserWallet(label = null) {
544
+ await ensureEthers();
545
+ const newWallet = await BrowserWallet.create(ethers);
546
+
547
+ if (!newWallet) {
548
+ throw new Error('Failed to create browser wallet');
549
+ }
550
+
551
+ newWallet.label = label || 'Browser Wallet';
552
+
553
+ // Temporarily set as active wallet to save it
554
+ const previousWallet = this.wallet;
555
+ this.wallet = newWallet;
556
+ this.save();
557
+
558
+ // Restore previous wallet if there was one
559
+ if (previousWallet) {
560
+ this.wallet = previousWallet;
561
+ }
562
+
563
+ return {
564
+ id: newWallet.id,
565
+ address: newWallet.address,
566
+ source: newWallet.source,
567
+ label: newWallet.label
568
+ };
569
+ }
570
+
571
+ async setDefaultWallet(walletId) {
572
+ const storageData = this.loadStorageData();
573
+ const walletData = storageData.wallets.find(w => w.id === walletId);
574
+
575
+ if (!walletData) {
576
+ throw new Error(`Wallet with ID ${walletId} not found`);
577
+ }
578
+
579
+ await ensureEthers();
580
+
581
+ // Load the wallet
582
+ this.wallet = await Wallet.fromJSON(walletData.wallet, ethers);
583
+ this.wallet.id = walletData.id;
584
+ this.wallet.label = walletData.label;
585
+ this.wallet.createdAt = walletData.createdAt;
586
+
587
+ // Update default in storage
588
+ storageData.defaultWalletId = walletId;
589
+ storageData.wallets = storageData.wallets.map(w => {
590
+ if (w.id === walletId) {
591
+ w.lastUsed = Date.now();
592
+ }
593
+ return w;
594
+ });
595
+
596
+ localStorage.setItem('epistery', JSON.stringify(storageData));
597
+
598
+ console.log(`Switched to wallet: ${this.wallet.source} (${this.wallet.address})`);
599
+
600
+ return {
601
+ id: this.wallet.id,
602
+ address: this.wallet.address,
603
+ source: this.wallet.source,
604
+ label: this.wallet.label
605
+ };
606
+ }
607
+
608
+ removeWallet(walletId) {
609
+ const storageData = this.loadStorageData();
610
+
611
+ // Don't allow removing the default wallet if it's the only one
612
+ if (storageData.wallets.length === 1) {
613
+ throw new Error('Cannot remove the only wallet');
614
+ }
615
+
616
+ // Don't allow removing the default wallet without switching first
617
+ if (storageData.defaultWalletId === walletId) {
618
+ throw new Error('Cannot remove default wallet. Switch to another wallet first.');
619
+ }
620
+
621
+ const walletIndex = storageData.wallets.findIndex(w => w.id === walletId);
622
+ if (walletIndex === -1) {
623
+ throw new Error(`Wallet with ID ${walletId} not found`);
624
+ }
625
+
626
+ storageData.wallets.splice(walletIndex, 1);
627
+ localStorage.setItem('epistery', JSON.stringify(storageData));
628
+
629
+ return true;
630
+ }
631
+
632
+ updateWalletLabel(walletId, newLabel) {
633
+ const storageData = this.loadStorageData();
634
+ const walletData = storageData.wallets.find(w => w.id === walletId);
635
+
636
+ if (!walletData) {
637
+ throw new Error(`Wallet with ID ${walletId} not found`);
638
+ }
639
+
640
+ walletData.label = newLabel;
641
+
642
+ // If this is the current wallet, update it too
643
+ if (this.wallet && this.wallet.id === walletId) {
644
+ this.wallet.label = newLabel;
645
+ }
646
+
647
+ localStorage.setItem('epistery', JSON.stringify(storageData));
648
+
649
+ return true;
650
+ }
651
+
652
+ getStatus() {
653
+ return {
654
+ client: this.wallet ? {
655
+ address: this.wallet.address,
656
+ publicKey: this.wallet.publicKey,
657
+ source: this.wallet.source
658
+ } : null,
659
+ server: this.server,
660
+ connected: !!(this.wallet && this.server)
661
+ };
662
+ }
663
+ }