shogun-core 3.0.4 → 3.0.6

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 (93) hide show
  1. package/dist/browser/shogun-core.js +91002 -0
  2. package/dist/browser/shogun-core.js.map +1 -0
  3. package/dist/config/simplified-config.js +230 -0
  4. package/dist/core.js +338 -0
  5. package/dist/gundb/crypto.js +268 -0
  6. package/dist/gundb/db.js +1829 -0
  7. package/dist/gundb/derive.js +229 -0
  8. package/dist/gundb/errors.js +66 -0
  9. package/dist/gundb/index.js +6 -0
  10. package/dist/gundb/rxjs.js +445 -0
  11. package/dist/gundb/simple-api.js +438 -0
  12. package/dist/gundb/types.js +4 -0
  13. package/dist/index.js +16 -0
  14. package/dist/interfaces/common.js +1 -0
  15. package/dist/interfaces/events.js +36 -0
  16. package/dist/interfaces/plugin.js +1 -0
  17. package/dist/interfaces/shogun.js +34 -0
  18. package/dist/managers/AuthManager.js +225 -0
  19. package/dist/managers/CoreInitializer.js +227 -0
  20. package/dist/managers/EventManager.js +67 -0
  21. package/dist/managers/PluginManager.js +296 -0
  22. package/dist/migration-test.js +91 -0
  23. package/dist/plugins/base.js +47 -0
  24. package/dist/plugins/index.js +15 -0
  25. package/dist/plugins/nostr/index.js +4 -0
  26. package/dist/plugins/nostr/nostrConnector.js +413 -0
  27. package/dist/plugins/nostr/nostrConnectorPlugin.js +446 -0
  28. package/dist/plugins/nostr/nostrSigner.js +313 -0
  29. package/dist/plugins/nostr/types.js +1 -0
  30. package/dist/plugins/oauth/index.js +3 -0
  31. package/dist/plugins/oauth/oauthConnector.js +753 -0
  32. package/dist/plugins/oauth/oauthPlugin.js +396 -0
  33. package/dist/plugins/oauth/types.js +1 -0
  34. package/dist/plugins/web3/index.js +4 -0
  35. package/dist/plugins/web3/types.js +1 -0
  36. package/dist/plugins/web3/web3Connector.js +528 -0
  37. package/dist/plugins/web3/web3ConnectorPlugin.js +448 -0
  38. package/dist/plugins/web3/web3Signer.js +308 -0
  39. package/dist/plugins/webauthn/index.js +3 -0
  40. package/dist/plugins/webauthn/types.js +11 -0
  41. package/dist/plugins/webauthn/webauthn.js +478 -0
  42. package/dist/plugins/webauthn/webauthnPlugin.js +398 -0
  43. package/dist/plugins/webauthn/webauthnSigner.js +304 -0
  44. package/dist/storage/storage.js +147 -0
  45. package/dist/types/config/simplified-config.d.ts +114 -0
  46. package/dist/types/core.d.ts +305 -0
  47. package/dist/types/gundb/crypto.d.ts +95 -0
  48. package/dist/types/gundb/db.d.ts +401 -0
  49. package/dist/types/gundb/derive.d.ts +21 -0
  50. package/dist/types/gundb/errors.d.ts +42 -0
  51. package/dist/types/gundb/index.d.ts +3 -0
  52. package/dist/types/gundb/rxjs.d.ts +110 -0
  53. package/dist/types/gundb/simple-api.d.ts +90 -0
  54. package/dist/types/gundb/types.d.ts +264 -0
  55. package/dist/types/index.d.ts +14 -0
  56. package/dist/types/interfaces/common.d.ts +85 -0
  57. package/dist/types/interfaces/events.d.ts +131 -0
  58. package/dist/types/interfaces/plugin.d.ts +162 -0
  59. package/dist/types/interfaces/shogun.d.ts +215 -0
  60. package/dist/types/managers/AuthManager.d.ts +72 -0
  61. package/dist/types/managers/CoreInitializer.d.ts +40 -0
  62. package/dist/types/managers/EventManager.d.ts +49 -0
  63. package/dist/types/managers/PluginManager.d.ts +145 -0
  64. package/dist/types/migration-test.d.ts +16 -0
  65. package/dist/types/plugins/base.d.ts +35 -0
  66. package/dist/types/plugins/index.d.ts +14 -0
  67. package/dist/types/plugins/nostr/index.d.ts +4 -0
  68. package/dist/types/plugins/nostr/nostrConnector.d.ts +119 -0
  69. package/dist/types/plugins/nostr/nostrConnectorPlugin.d.ts +163 -0
  70. package/dist/types/plugins/nostr/nostrSigner.d.ts +105 -0
  71. package/dist/types/plugins/nostr/types.d.ts +122 -0
  72. package/dist/types/plugins/oauth/index.d.ts +3 -0
  73. package/dist/types/plugins/oauth/oauthConnector.d.ts +110 -0
  74. package/dist/types/plugins/oauth/oauthPlugin.d.ts +91 -0
  75. package/dist/types/plugins/oauth/types.d.ts +114 -0
  76. package/dist/types/plugins/web3/index.d.ts +4 -0
  77. package/dist/types/plugins/web3/types.d.ts +107 -0
  78. package/dist/types/plugins/web3/web3Connector.d.ts +129 -0
  79. package/dist/types/plugins/web3/web3ConnectorPlugin.d.ts +160 -0
  80. package/dist/types/plugins/web3/web3Signer.d.ts +114 -0
  81. package/dist/types/plugins/webauthn/index.d.ts +3 -0
  82. package/dist/types/plugins/webauthn/types.d.ts +162 -0
  83. package/dist/types/plugins/webauthn/webauthn.d.ts +129 -0
  84. package/dist/types/plugins/webauthn/webauthnPlugin.d.ts +158 -0
  85. package/dist/types/plugins/webauthn/webauthnSigner.d.ts +91 -0
  86. package/dist/types/storage/storage.d.ts +50 -0
  87. package/dist/types/utils/errorHandler.d.ts +119 -0
  88. package/dist/types/utils/eventEmitter.d.ts +39 -0
  89. package/dist/types/utils/validation.d.ts +27 -0
  90. package/dist/utils/errorHandler.js +241 -0
  91. package/dist/utils/eventEmitter.js +76 -0
  92. package/dist/utils/validation.js +72 -0
  93. package/package.json +1 -1
@@ -0,0 +1,1829 @@
1
+ /**
2
+ * GunDB class with enhanced features:
3
+ * - Dynamic peer linking
4
+ * - Support for remove/unset operations
5
+ * - Direct authentication through Gun.user()
6
+ */
7
+ import Gun from "gun/gun";
8
+ import SEA from "gun/sea";
9
+ import "gun/lib/then";
10
+ import "gun/lib/radix";
11
+ import "gun/lib/radisk";
12
+ import "gun/lib/store";
13
+ import "gun/lib/rindexed";
14
+ import "gun/lib/webrtc";
15
+ import derive from "./derive";
16
+ import { ErrorHandler, ErrorType } from "../utils/errorHandler";
17
+ import { EventEmitter } from "../utils/eventEmitter";
18
+ import { RxJS } from "./rxjs";
19
+ import * as GunErrors from "./errors";
20
+ import * as crypto from "./crypto";
21
+ /**
22
+ * Configuration constants for timeouts and security
23
+ */
24
+ const CONFIG = {
25
+ TIMEOUTS: {
26
+ USER_DATA_OPERATION: 5000,
27
+ },
28
+ PASSWORD: {
29
+ MIN_LENGTH: 8,
30
+ REQUIRE_UPPERCASE: false,
31
+ REQUIRE_LOWERCASE: false,
32
+ REQUIRE_NUMBERS: false,
33
+ REQUIRE_SPECIAL_CHARS: false,
34
+ },
35
+ };
36
+ class DataBase {
37
+ gun;
38
+ user = null;
39
+ crypto;
40
+ sea;
41
+ node;
42
+ onAuthCallbacks = [];
43
+ eventEmitter;
44
+ // Integrated modules
45
+ _rxjs;
46
+ constructor(gun, appScope = "shogun") {
47
+ // Initialize event emitter
48
+ this.eventEmitter = new EventEmitter();
49
+ // Validate Gun instance
50
+ if (!gun) {
51
+ throw new Error("Gun instance is required but was not provided");
52
+ }
53
+ if (typeof gun !== "object") {
54
+ throw new Error(`Gun instance must be an object, received: ${typeof gun}`);
55
+ }
56
+ if (typeof gun.user !== "function") {
57
+ throw new Error(`Gun instance is invalid: gun.user is not a function. Received gun.user type: ${typeof gun.user}`);
58
+ }
59
+ if (typeof gun.get !== "function") {
60
+ throw new Error(`Gun instance is invalid: gun.get is not a function. Received gun.get type: ${typeof gun.get}`);
61
+ }
62
+ if (typeof gun.on !== "function") {
63
+ throw new Error(`Gun instance is invalid: gun.on is not a function. Received gun.on type: ${typeof gun.on}`);
64
+ }
65
+ this.gun = gun;
66
+ this.user = this.gun.user().recall({ sessionStorage: true });
67
+ this.subscribeToAuthEvents();
68
+ this.crypto = crypto;
69
+ this.sea = SEA;
70
+ this.node = null;
71
+ }
72
+ /**
73
+ * Initialize the GunInstance asynchronously
74
+ * This method should be called after construction to perform async operations
75
+ */
76
+ async initialize(appScope = "shogun") {
77
+ try {
78
+ const sessionResult = this.restoreSession();
79
+ this.node = this.gun.get(appScope);
80
+ if (sessionResult.success) {
81
+ // Session automatically restored
82
+ }
83
+ else {
84
+ // No previous session to restore
85
+ }
86
+ }
87
+ catch (error) {
88
+ console.error("Error during automatic session restoration:", error);
89
+ }
90
+ }
91
+ subscribeToAuthEvents() {
92
+ this.gun.on("auth", (ack) => {
93
+ // Auth event received
94
+ if (ack.err) {
95
+ ErrorHandler.handle(ErrorType.GUN, "AUTH_EVENT_ERROR", ack.err, new Error(ack.err));
96
+ }
97
+ else {
98
+ this.notifyAuthListeners(ack.sea?.pub || "");
99
+ }
100
+ });
101
+ }
102
+ notifyAuthListeners(pub) {
103
+ const user = this.gun.user();
104
+ this.onAuthCallbacks.forEach((cb) => cb(user));
105
+ }
106
+ /**
107
+ * Adds a new peer to the network
108
+ * @param peer URL of the peer to add
109
+ */
110
+ addPeer(peer) {
111
+ this.gun.opt({ peers: [peer] });
112
+ console.log(`Added new peer: ${peer}`);
113
+ }
114
+ /**
115
+ * Removes a peer from the network
116
+ * @param peer URL of the peer to remove
117
+ */
118
+ removePeer(peer) {
119
+ try {
120
+ // Get current peers from Gun instance
121
+ const gunOpts = this.gun._.opt;
122
+ if (gunOpts && gunOpts.peers) {
123
+ // Remove the peer from the peers object
124
+ delete gunOpts.peers[peer];
125
+ // Also try to close the connection if it exists
126
+ const peerConnection = gunOpts.peers[peer];
127
+ if (peerConnection && typeof peerConnection.close === "function") {
128
+ peerConnection.close();
129
+ }
130
+ console.log(`Removed peer: ${peer}`);
131
+ }
132
+ else {
133
+ console.log(`Peer not found in current connections: ${peer}`);
134
+ }
135
+ }
136
+ catch (error) {
137
+ console.error(`Error removing peer ${peer}:`, error);
138
+ }
139
+ }
140
+ /**
141
+ * Gets the list of currently connected peers
142
+ * @returns Array of peer URLs
143
+ */
144
+ getCurrentPeers() {
145
+ try {
146
+ const gunOpts = this.gun._.opt;
147
+ if (gunOpts && gunOpts.peers) {
148
+ return Object.keys(gunOpts.peers).filter((peer) => {
149
+ const peerObj = gunOpts.peers[peer];
150
+ // Check if peer is actually connected (not just configured)
151
+ return peerObj && peerObj.wire && peerObj.wire.hied !== "bye";
152
+ });
153
+ }
154
+ return [];
155
+ }
156
+ catch (error) {
157
+ console.error("Error getting current peers:", error);
158
+ return [];
159
+ }
160
+ }
161
+ /**
162
+ * Gets the list of all configured peers (connected and disconnected)
163
+ * @returns Array of peer URLs
164
+ */
165
+ getAllConfiguredPeers() {
166
+ try {
167
+ const gunOpts = this.gun._.opt;
168
+ if (gunOpts && gunOpts.peers) {
169
+ return Object.keys(gunOpts.peers);
170
+ }
171
+ return [];
172
+ }
173
+ catch (error) {
174
+ console.error("Error getting configured peers:", error);
175
+ return [];
176
+ }
177
+ }
178
+ /**
179
+ * Gets detailed information about all peers
180
+ * @returns Object with peer information
181
+ */
182
+ getPeerInfo() {
183
+ try {
184
+ const gunOpts = this.gun._.opt;
185
+ const peerInfo = {};
186
+ if (gunOpts && gunOpts.peers) {
187
+ Object.keys(gunOpts.peers).forEach((peer) => {
188
+ const peerObj = gunOpts.peers[peer];
189
+ const isConnected = peerObj && peerObj.wire && peerObj.wire.hied !== "bye";
190
+ const status = isConnected
191
+ ? "connected"
192
+ : peerObj && peerObj.wire
193
+ ? "disconnected"
194
+ : "not_initialized";
195
+ peerInfo[peer] = {
196
+ connected: isConnected,
197
+ status: status,
198
+ };
199
+ });
200
+ }
201
+ return peerInfo;
202
+ }
203
+ catch (error) {
204
+ console.error("Error getting peer info:", error);
205
+ return {};
206
+ }
207
+ }
208
+ /**
209
+ * Reconnects to a specific peer
210
+ * @param peer URL of the peer to reconnect
211
+ */
212
+ reconnectToPeer(peer) {
213
+ try {
214
+ // First remove the peer
215
+ this.removePeer(peer);
216
+ // Add it back immediately instead of with timeout
217
+ this.addPeer(peer);
218
+ console.log(`Reconnected to peer: ${peer}`);
219
+ }
220
+ catch (error) {
221
+ console.error(`Error reconnecting to peer ${peer}:`, error);
222
+ }
223
+ }
224
+ /**
225
+ * Clears all peers and optionally adds new ones
226
+ * @param newPeers Optional array of new peers to add
227
+ */
228
+ resetPeers(newPeers) {
229
+ try {
230
+ const gunOpts = this.gun._.opt;
231
+ if (gunOpts && gunOpts.peers) {
232
+ // Clear all existing peers
233
+ Object.keys(gunOpts.peers).forEach((peer) => {
234
+ this.removePeer(peer);
235
+ });
236
+ // Add new peers if provided
237
+ if (newPeers && newPeers.length > 0) {
238
+ newPeers.forEach((peer) => {
239
+ this.addPeer(peer);
240
+ });
241
+ }
242
+ console.log(`Gun database reset with ${newPeers ? newPeers.length : 0} peers: ${newPeers ? newPeers.join(", ") : "none"}`);
243
+ }
244
+ }
245
+ catch (error) {
246
+ console.error("Error resetting peers:", error);
247
+ }
248
+ }
249
+ /**
250
+ * Registers an authentication callback
251
+ * @param callback Function to call on auth events
252
+ * @returns Function to unsubscribe the callback
253
+ */
254
+ onAuth(callback) {
255
+ this.onAuthCallbacks.push(callback);
256
+ const user = this.gun.user();
257
+ if (user && user.is)
258
+ callback(user);
259
+ return () => {
260
+ const i = this.onAuthCallbacks.indexOf(callback);
261
+ if (i !== -1)
262
+ this.onAuthCallbacks.splice(i, 1);
263
+ };
264
+ }
265
+ /**
266
+ * Helper method to navigate to a nested path by splitting and chaining .get() calls
267
+ * @param node Starting Gun node
268
+ * @param path Path string (e.g., "test/data/marco")
269
+ * @returns Gun node at the specified path
270
+ */
271
+ navigateToPath(node, path) {
272
+ if (!path || typeof path !== "string")
273
+ return node;
274
+ // Sanitize path to remove any control characters or invalid characters
275
+ const sanitizedPath = path
276
+ .replace(/[\x00-\x1F\x7F]/g, "") // Remove control characters
277
+ .replace(/[^\w\-._/]/g, "") // Only allow alphanumeric, hyphens, dots, underscores, and slashes
278
+ .trim();
279
+ if (!sanitizedPath)
280
+ return node;
281
+ // Split path by '/' and filter out empty segments
282
+ const pathSegments = sanitizedPath
283
+ .split("/")
284
+ .filter((segment) => segment.length > 0)
285
+ .map((segment) => segment.trim())
286
+ .filter((segment) => segment.length > 0);
287
+ // Chain .get() calls for each path segment
288
+ return pathSegments.reduce((currentNode, segment) => {
289
+ return currentNode.get(segment);
290
+ }, node);
291
+ }
292
+ /**
293
+ * Gets the Gun instance
294
+ * @returns Gun instance
295
+ */
296
+ getGun() {
297
+ return this.gun;
298
+ }
299
+ /**
300
+ * Gets the current user
301
+ * @returns Current user object or null
302
+ */
303
+ getCurrentUser() {
304
+ try {
305
+ const user = this.gun.user();
306
+ const pub = user?.is?.pub;
307
+ return pub ? { pub, user } : null;
308
+ }
309
+ catch (error) {
310
+ console.error("Error getting current user:", error);
311
+ return null;
312
+ }
313
+ }
314
+ /**
315
+ * Gets the current user instance
316
+ * @returns User instance
317
+ */
318
+ getUser() {
319
+ return this.gun.user();
320
+ }
321
+ /**
322
+ * Gets a node at the specified path
323
+ * @param path Path to the node
324
+ * @returns Gun node
325
+ */
326
+ get(path) {
327
+ return this.navigateToPath(this.gun, path);
328
+ }
329
+ /**
330
+ * Gets data at the specified path (one-time read)
331
+ * @param path Path to get the data from
332
+ * @returns Promise resolving to the data
333
+ */
334
+ async getData(path) {
335
+ const node = await this.navigateToPath(this.gun, path).then();
336
+ return node;
337
+ }
338
+ /**
339
+ * Puts data at the specified path
340
+ * @param path Path to store data
341
+ * @param data Data to store
342
+ * @returns Promise resolving to operation result
343
+ */
344
+ async put(path, data) {
345
+ const ack = await this.navigateToPath(this.gun, path).put(data).then();
346
+ const result = ack.err
347
+ ? { success: false, error: ack.err }
348
+ : { success: true };
349
+ return result;
350
+ }
351
+ /**
352
+ * Sets data at the specified path
353
+ * @param path Path to store data
354
+ * @param data Data to store
355
+ * @returns Promise resolving to operation result
356
+ */
357
+ async set(path, data) {
358
+ const ack = await this.navigateToPath(this.gun, path).set(data).then();
359
+ const result = ack.err
360
+ ? { success: false, error: ack.err }
361
+ : { success: true };
362
+ return result;
363
+ }
364
+ /**
365
+ * Removes data at the specified path
366
+ * @param path Path to remove
367
+ * @returns Promise resolving to operation result
368
+ */
369
+ async remove(path) {
370
+ const ack = await this.navigateToPath(this.gun, path).put(null).then();
371
+ const result = ack.err
372
+ ? { success: false, error: ack.err }
373
+ : { success: true };
374
+ return result;
375
+ }
376
+ /**
377
+ * Checks if a user is currently logged in
378
+ * @returns True if logged in
379
+ */
380
+ isLoggedIn() {
381
+ try {
382
+ const user = this.gun.user();
383
+ return !!(user && user.is && user.is.pub);
384
+ }
385
+ catch (error) {
386
+ console.error("Error checking login status:", error);
387
+ return false;
388
+ }
389
+ }
390
+ /**
391
+ * Attempts to restore user session from local storage
392
+ * @returns Promise resolving to session restoration result
393
+ */
394
+ restoreSession() {
395
+ try {
396
+ if (typeof localStorage === "undefined") {
397
+ return { success: false, error: "localStorage not available" };
398
+ }
399
+ const sessionInfo = localStorage.getItem("gun/session");
400
+ const pairInfo = localStorage.getItem("gun/pair");
401
+ if (!sessionInfo || !pairInfo) {
402
+ // No saved session found
403
+ return { success: false, error: "No saved session" };
404
+ }
405
+ let session, pair;
406
+ try {
407
+ session = JSON.parse(sessionInfo);
408
+ pair = JSON.parse(pairInfo);
409
+ }
410
+ catch (parseError) {
411
+ console.error("Error parsing session data:", parseError);
412
+ // Clear corrupted data
413
+ localStorage.removeItem("gun/session");
414
+ localStorage.removeItem("gun/pair");
415
+ return { success: false, error: "Corrupted session data" };
416
+ }
417
+ // Validate session data structure
418
+ if (!session.username || !session.pair || !session.userPub) {
419
+ // Invalid session data, clearing storage
420
+ localStorage.removeItem("gun/session");
421
+ localStorage.removeItem("gun/pair");
422
+ return { success: false, error: "Incomplete session data" };
423
+ }
424
+ // Check if session is expired
425
+ if (session.expiresAt && Date.now() > session.expiresAt) {
426
+ // Session expired, clearing storage
427
+ localStorage.removeItem("gun/session");
428
+ localStorage.removeItem("gun/pair");
429
+ return { success: false, error: "Session expired" };
430
+ }
431
+ // Attempt to restore user session
432
+ try {
433
+ const userInstance = this.gun.user();
434
+ if (!userInstance) {
435
+ console.error("Gun user instance not available");
436
+ localStorage.removeItem("gun/session");
437
+ localStorage.removeItem("gun/pair");
438
+ return { success: false, error: "Gun user instance not available" };
439
+ }
440
+ // Set the user pair
441
+ try {
442
+ userInstance._ = { ...userInstance._, sea: session.pair };
443
+ }
444
+ catch (pairError) {
445
+ console.error("Error setting user pair:", pairError);
446
+ }
447
+ // Attempt to recall user session
448
+ try {
449
+ const recallResult = userInstance.recall({ sessionStorage: true });
450
+ // console.log("recallResult", recallResult);
451
+ }
452
+ catch (recallError) {
453
+ console.error("Error during recall:", recallError);
454
+ }
455
+ // Verify session restoration success
456
+ if (userInstance.is && userInstance.is.pub === session.userPub) {
457
+ this.user = userInstance;
458
+ // Session restored successfully for user
459
+ return {
460
+ success: true,
461
+ userPub: session.userPub,
462
+ };
463
+ }
464
+ else {
465
+ // Session restoration verification failed
466
+ localStorage.removeItem("gun/session");
467
+ localStorage.removeItem("gun/pair");
468
+ return { success: false, error: "Session verification failed" };
469
+ }
470
+ }
471
+ catch (error) {
472
+ console.error(`Error restoring session: ${error}`);
473
+ return {
474
+ success: false,
475
+ error: `Session restoration failed: ${error}`,
476
+ };
477
+ }
478
+ }
479
+ catch (mainError) {
480
+ console.error(`Error in restoreSession: ${mainError}`);
481
+ return {
482
+ success: false,
483
+ error: `Session restoration failed: ${mainError}`,
484
+ };
485
+ }
486
+ return { success: false, error: "No session data available" };
487
+ }
488
+ logout() {
489
+ try {
490
+ const currentUser = this.gun.user();
491
+ if (!currentUser || !currentUser.is) {
492
+ console.log("No user logged in, skipping logout");
493
+ return;
494
+ }
495
+ // Log out user
496
+ try {
497
+ currentUser.leave();
498
+ }
499
+ catch (gunError) {
500
+ console.error("Error during Gun logout:", gunError);
501
+ }
502
+ // Clear user reference
503
+ this.user = null;
504
+ // Clear local session data
505
+ try {
506
+ // Clear session data if needed
507
+ }
508
+ catch (error) {
509
+ console.error("Error clearing local session data:", error);
510
+ }
511
+ // Clear session storage
512
+ try {
513
+ if (typeof sessionStorage !== "undefined") {
514
+ sessionStorage.removeItem("gunSessionData");
515
+ // Session storage cleared
516
+ }
517
+ }
518
+ catch (error) {
519
+ console.error("Error clearing session storage:", error);
520
+ }
521
+ // Logout completed successfully
522
+ }
523
+ catch (error) {
524
+ console.error("Error during logout:", error);
525
+ }
526
+ }
527
+ /**
528
+ * Accesses the RxJS module for reactive programming
529
+ * @returns GunRxJS instance
530
+ */
531
+ rx() {
532
+ if (!this._rxjs) {
533
+ this._rxjs = new RxJS(this.gun);
534
+ }
535
+ return this._rxjs;
536
+ }
537
+ /**
538
+ * Validates password strength according to security requirements
539
+ */
540
+ validatePasswordStrength(password) {
541
+ if (password.length < CONFIG.PASSWORD.MIN_LENGTH) {
542
+ return {
543
+ valid: false,
544
+ error: `Password must be at least ${CONFIG.PASSWORD.MIN_LENGTH} characters long`,
545
+ };
546
+ }
547
+ const validations = [];
548
+ if (CONFIG.PASSWORD.REQUIRE_UPPERCASE && !/[A-Z]/.test(password)) {
549
+ validations.push("uppercase letter");
550
+ }
551
+ if (CONFIG.PASSWORD.REQUIRE_LOWERCASE && !/[a-z]/.test(password)) {
552
+ validations.push("lowercase letter");
553
+ }
554
+ if (CONFIG.PASSWORD.REQUIRE_NUMBERS && !/\d/.test(password)) {
555
+ validations.push("number");
556
+ }
557
+ if (CONFIG.PASSWORD.REQUIRE_SPECIAL_CHARS &&
558
+ !/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
559
+ validations.push("special character");
560
+ }
561
+ if (validations.length > 0) {
562
+ return {
563
+ valid: false,
564
+ error: `Password must contain at least one: ${validations.join(", ")}`,
565
+ };
566
+ }
567
+ return { valid: true };
568
+ }
569
+ /**
570
+ * Validates signup credentials with enhanced security
571
+ */
572
+ validateSignupCredentials(username, password, pair) {
573
+ // Validate username
574
+ if (!username || username.length < 1) {
575
+ return {
576
+ valid: false,
577
+ error: "Username must be more than 0 characters long!",
578
+ };
579
+ }
580
+ // Validate username format (alphanumeric and some special chars only)
581
+ if (!/^[a-zA-Z0-9._-]+$/.test(username)) {
582
+ return {
583
+ valid: false,
584
+ error: "Username can only contain letters, numbers, dots, underscores, and hyphens",
585
+ };
586
+ }
587
+ // If using pair authentication, skip password validation
588
+ if (pair) {
589
+ if (!pair.pub ||
590
+ !pair.priv ||
591
+ !pair.epub ||
592
+ !pair.epriv) {
593
+ return {
594
+ valid: false,
595
+ error: "Invalid pair provided",
596
+ };
597
+ }
598
+ if (!pair.pub || !pair.priv || !pair.epub || !pair.epriv) {
599
+ return {
600
+ valid: false,
601
+ error: "Invalid pair provided",
602
+ };
603
+ }
604
+ return { valid: true };
605
+ }
606
+ // Validate password strength
607
+ return this.validatePasswordStrength(password);
608
+ }
609
+ /**
610
+ * Creates a new user in Gun
611
+ */
612
+ async createNewUser(username, password) {
613
+ return new Promise((resolve) => {
614
+ // Validate inputs before creating user
615
+ if (!username ||
616
+ typeof username !== "string" ||
617
+ username.trim().length === 0) {
618
+ resolve({ success: false, error: "Invalid username provided" });
619
+ return;
620
+ }
621
+ if (!password ||
622
+ typeof password !== "string" ||
623
+ password.length === 0) {
624
+ resolve({ success: false, error: "Invalid password provided" });
625
+ return;
626
+ }
627
+ // Normalize username
628
+ const normalizedUsername = username.trim().toLowerCase();
629
+ if (normalizedUsername.length === 0) {
630
+ resolve({
631
+ success: false,
632
+ error: "Username cannot be empty",
633
+ });
634
+ return;
635
+ }
636
+ this.gun.user().create(normalizedUsername, password, (ack) => {
637
+ if (ack.err) {
638
+ console.error(`User creation error: ${ack.err}`);
639
+ resolve({ success: false, error: ack.err });
640
+ }
641
+ else {
642
+ // Validate that we got a userPub from creation
643
+ const userPub = ack.pub;
644
+ if (!userPub ||
645
+ typeof userPub !== "string" ||
646
+ userPub.trim().length === 0) {
647
+ console.error("User creation successful but no userPub returned:", ack);
648
+ resolve({
649
+ success: false,
650
+ error: "User creation successful but no userPub returned",
651
+ });
652
+ }
653
+ else {
654
+ console.log(`User created successfully with userPub: ${userPub}`);
655
+ resolve({ success: true, userPub: userPub });
656
+ }
657
+ }
658
+ });
659
+ });
660
+ }
661
+ /**
662
+ * Authenticates user after creation
663
+ */
664
+ async authenticateNewUser(username, password, pair) {
665
+ return new Promise((resolve) => {
666
+ // Validate inputs before authentication
667
+ if (!username ||
668
+ typeof username !== "string" ||
669
+ username.trim().length === 0) {
670
+ resolve({ success: false, error: "Invalid username provided" });
671
+ return;
672
+ }
673
+ // Skip password validation when using pair authentication
674
+ if (!pair &&
675
+ (!password || typeof password !== "string" || password.length === 0)) {
676
+ resolve({ success: false, error: "Invalid password provided" });
677
+ return;
678
+ }
679
+ // Normalize username to match what was used in creation
680
+ const normalizedUsername = username.trim().toLowerCase();
681
+ if (normalizedUsername.length === 0) {
682
+ resolve({
683
+ success: false,
684
+ error: "Username cannot be empty",
685
+ });
686
+ return;
687
+ }
688
+ if (pair) {
689
+ this.gun.user().auth(pair, (ack) => {
690
+ console.log(`Pair authentication after creation result:`, ack);
691
+ if (ack.err) {
692
+ console.error(`Authentication after creation failed: ${ack.err}`);
693
+ resolve({ success: false, error: ack.err });
694
+ }
695
+ else {
696
+ // Add a small delay to ensure user state is properly set
697
+ setTimeout(() => {
698
+ // Extract userPub from multiple possible sources
699
+ const userPub = ack.pub || this.gun.user().is?.pub || ack.user?.pub;
700
+ console.log(`Extracted userPub after pair auth: ${userPub}`);
701
+ console.log(`User object after pair auth:`, this.gun.user());
702
+ console.log(`User.is after pair auth:`, this.gun.user().is);
703
+ if (!userPub) {
704
+ console.error("Authentication successful but no userPub found");
705
+ resolve({
706
+ success: false,
707
+ error: "No userPub returned from authentication",
708
+ });
709
+ }
710
+ else {
711
+ resolve({ success: true, userPub: userPub });
712
+ }
713
+ }, 100);
714
+ }
715
+ });
716
+ }
717
+ else {
718
+ this.gun.user().auth(normalizedUsername, password, (ack) => {
719
+ console.log(`Password authentication after creation result:`, ack);
720
+ if (ack.err) {
721
+ console.error(`Authentication after creation failed: ${ack.err}`);
722
+ resolve({ success: false, error: ack.err });
723
+ }
724
+ else {
725
+ // Add a small delay to ensure user state is properly set
726
+ setTimeout(() => {
727
+ // Extract userPub from multiple possible sources
728
+ const userPub = ack.pub || this.gun.user().is?.pub || ack.user?.pub;
729
+ console.log(`Extracted userPub after password auth: ${userPub}`);
730
+ console.log(`User object after password auth:`, this.gun.user());
731
+ console.log(`User.is after password auth:`, this.gun.user().is);
732
+ if (!userPub) {
733
+ console.error("Authentication successful but no userPub found");
734
+ resolve({
735
+ success: false,
736
+ error: "No userPub returned from authentication",
737
+ });
738
+ }
739
+ else {
740
+ resolve({ success: true, userPub: userPub });
741
+ }
742
+ }, 100);
743
+ }
744
+ });
745
+ }
746
+ });
747
+ }
748
+ /**
749
+ * Signs up a new user using direct Gun authentication
750
+ * @param username Username
751
+ * @param password Password
752
+ * @param pair Optional SEA pair for Web3 login
753
+ * @returns Promise resolving to signup result
754
+ */
755
+ async signUp(username, password, pair) {
756
+ try {
757
+ // Validate credentials with enhanced security
758
+ const validation = this.validateSignupCredentials(username, password, pair);
759
+ if (!validation.valid) {
760
+ return { success: false, error: validation.error };
761
+ }
762
+ let createResult;
763
+ if (pair) {
764
+ // For Web3/plugin authentication, use pair-based creation
765
+ createResult = await this.createNewUserWithPair(username, pair);
766
+ }
767
+ else {
768
+ // For password authentication, use standard creation
769
+ createResult = await this.createNewUser(username, password);
770
+ }
771
+ if (!createResult.success) {
772
+ return { success: false, error: createResult.error };
773
+ }
774
+ // Add a small delay to ensure user is properly registered
775
+ await new Promise((resolve) => setTimeout(resolve, 100));
776
+ // Authenticate the newly created user
777
+ const authResult = await this.authenticateNewUser(username, password, pair);
778
+ if (!authResult.success) {
779
+ return { success: false, error: authResult.error };
780
+ }
781
+ // Validate that we have a userPub
782
+ if (!authResult.userPub ||
783
+ typeof authResult.userPub !== "string" ||
784
+ authResult.userPub.trim().length === 0) {
785
+ console.error("Authentication successful but no valid userPub returned:", authResult);
786
+ return {
787
+ success: false,
788
+ error: "Authentication successful but no valid userPub returned",
789
+ };
790
+ }
791
+ // Set the user instance
792
+ this.user = this.gun.user();
793
+ // Run post-authentication tasks
794
+ try {
795
+ console.log(`Running post-auth setup with userPub: ${authResult.userPub}`);
796
+ const postAuthResult = await this.runPostAuthOnAuthResult(username, authResult.userPub, authResult);
797
+ // Return the post-auth result which includes the complete user data
798
+ return postAuthResult;
799
+ }
800
+ catch (postAuthError) {
801
+ console.error(`Post-auth error: ${postAuthError}`);
802
+ // Even if post-auth fails, the user was created and authenticated successfully
803
+ return {
804
+ success: true,
805
+ userPub: authResult.userPub,
806
+ username: username,
807
+ isNewUser: true,
808
+ sea: this.gun.user()?._?.sea
809
+ ? {
810
+ pub: this.gun.user()._?.sea.pub,
811
+ priv: this.gun.user()._?.sea.priv,
812
+ epub: this.gun.user()._?.sea.epub,
813
+ epriv: this.gun.user()._?.sea.epriv,
814
+ }
815
+ : undefined,
816
+ };
817
+ }
818
+ }
819
+ catch (error) {
820
+ console.error(`Exception during signup for ${username}: ${error}`);
821
+ return { success: false, error: `Signup failed: ${error}` };
822
+ }
823
+ }
824
+ /**
825
+ * Creates a new user in Gun with pair-based authentication (for Web3/plugins)
826
+ */
827
+ async createNewUserWithPair(username, pair) {
828
+ return new Promise((resolve) => {
829
+ // Validate inputs before creating user
830
+ if (!username ||
831
+ typeof username !== "string" ||
832
+ username.trim().length === 0) {
833
+ resolve({ success: false, error: "Invalid username provided" });
834
+ return;
835
+ }
836
+ if (!pair || !pair.pub || !pair.priv) {
837
+ resolve({ success: false, error: "Invalid pair provided" });
838
+ return;
839
+ }
840
+ // Normalize username
841
+ const normalizedUsername = username.trim().toLowerCase();
842
+ if (normalizedUsername.length === 0) {
843
+ resolve({
844
+ success: false,
845
+ error: "Username cannot be empty",
846
+ });
847
+ return;
848
+ }
849
+ // For pair-based authentication, we don't need to call gun.user().create()
850
+ // because the pair already contains the cryptographic credentials
851
+ // We just need to validate that the pair is valid and return success
852
+ console.log(`User created successfully with pair for: ${normalizedUsername}`);
853
+ resolve({ success: true, userPub: pair.pub });
854
+ });
855
+ }
856
+ async runPostAuthOnAuthResult(username, userPub, authResult) {
857
+ // Setting up user profile after authentication
858
+ try {
859
+ // Validate required parameters
860
+ if (!username ||
861
+ typeof username !== "string" ||
862
+ username.trim().length === 0) {
863
+ throw new Error("Invalid username provided");
864
+ }
865
+ if (!userPub ||
866
+ typeof userPub !== "string" ||
867
+ userPub.trim().length === 0) {
868
+ console.error("Invalid userPub provided:", {
869
+ userPub,
870
+ type: typeof userPub,
871
+ authResult,
872
+ });
873
+ throw new Error("Invalid userPub provided");
874
+ }
875
+ // Additional validation for userPub format
876
+ if (!userPub.includes(".") || userPub.length < 10) {
877
+ console.error("Invalid userPub format:", userPub);
878
+ throw new Error("Invalid userPub format");
879
+ }
880
+ // Normalize username to prevent path issues
881
+ const normalizedUsername = username.trim().toLowerCase();
882
+ if (normalizedUsername.length === 0) {
883
+ throw new Error("Username cannot be empty");
884
+ }
885
+ console.log(`Setting up user profile for ${normalizedUsername} with userPub: ${userPub}`);
886
+ const existingUser = await this.gun.get(userPub).once().then();
887
+ const isNewUser = !existingUser || !existingUser.username;
888
+ // Get user's encryption public key (epub) for comprehensive tracking
889
+ const userInstance = this.gun.user();
890
+ const userSea = userInstance?._?.sea;
891
+ const epub = userSea?.epub || null;
892
+ // Enhanced user tracking system
893
+ await this.setupComprehensiveUserTracking(normalizedUsername, userPub, epub, isNewUser);
894
+ return {
895
+ success: true,
896
+ userPub: userPub,
897
+ username: normalizedUsername,
898
+ isNewUser: isNewUser,
899
+ // Get the SEA pair from the user object
900
+ sea: userSea
901
+ ? {
902
+ pub: userSea.pub,
903
+ priv: userSea.priv,
904
+ epub: userSea.epub,
905
+ epriv: userSea.epriv,
906
+ }
907
+ : undefined,
908
+ };
909
+ }
910
+ catch (error) {
911
+ console.error(`Error in post-authentication setup: ${error}`);
912
+ return {
913
+ success: false,
914
+ error: `Post-authentication setup failed: ${error}`,
915
+ };
916
+ }
917
+ }
918
+ /**
919
+ * Sets up comprehensive user tracking system for agile user lookup
920
+ * Creates multiple indexes for efficient user discovery
921
+ */
922
+ async setupComprehensiveUserTracking(username, userPub, epub, isNewUser) {
923
+ try {
924
+ // 1. Create alias index: ~@alias -> userPub (for GunDB compatibility)
925
+ await this.createAliasIndex(username, userPub);
926
+ // 2. Create username mapping: usernames/alias -> userPub
927
+ await this.createUsernameMapping(username, userPub);
928
+ // 3. Create user registry: users/userPub -> user data
929
+ await this.createUserRegistry(username, userPub, epub);
930
+ // 4. Create reverse lookup: userPub -> alias
931
+ await this.createReverseLookup(username, userPub);
932
+ // 5. Create epub index: epubKeys/epub -> userPub (for encryption lookups)
933
+ if (epub) {
934
+ await this.createEpubIndex(epub, userPub);
935
+ }
936
+ // 6. Create user metadata in user's own node
937
+ await this.createUserMetadata(username, userPub, epub);
938
+ console.log(`Comprehensive user tracking setup completed for ${username}`);
939
+ }
940
+ catch (error) {
941
+ console.error(`Error in comprehensive user tracking setup: ${error}`);
942
+ // Don't throw - continue with other operations
943
+ }
944
+ }
945
+ /**
946
+ * Creates alias index following GunDB pattern: ~@alias -> userPub
947
+ */
948
+ async createAliasIndex(username, userPub) {
949
+ try {
950
+ const aliasNode = this.gun.get(`~@${username}`);
951
+ // For Gun.js alias validation to pass, the data must be exactly equal to the key
952
+ // The key is `~@${username}`, so we store that as the data
953
+ const ack = await aliasNode.put(`~@${username}`).then();
954
+ if (ack.err) {
955
+ console.error(`Error creating alias index: ${ack.err}`);
956
+ }
957
+ else {
958
+ console.log(`Alias index created: ~@${username} -> ${userPub}`);
959
+ }
960
+ }
961
+ catch (error) {
962
+ console.error(`Error creating alias index: ${error}`);
963
+ }
964
+ }
965
+ /**
966
+ * Creates username mapping: usernames/alias -> userPub
967
+ */
968
+ async createUsernameMapping(username, userPub) {
969
+ try {
970
+ const ack = await this.node
971
+ .get("usernames")
972
+ .get(username)
973
+ .put(userPub)
974
+ .then();
975
+ if (ack.err) {
976
+ console.error(`Error creating username mapping: ${ack.err}`);
977
+ }
978
+ else {
979
+ console.log(`Username mapping created: ${username} -> ${userPub}`);
980
+ }
981
+ }
982
+ catch (error) {
983
+ console.error(`Error creating username mapping: ${error}`);
984
+ }
985
+ }
986
+ /**
987
+ * Creates user registry: users/userPub -> user data
988
+ */
989
+ async createUserRegistry(username, userPub, epub) {
990
+ try {
991
+ const userData = {
992
+ username: username,
993
+ userPub: userPub,
994
+ epub: epub,
995
+ registeredAt: Date.now(),
996
+ lastSeen: Date.now(),
997
+ };
998
+ const ack = await this.node
999
+ .get("users")
1000
+ .get(userPub)
1001
+ .put(userData)
1002
+ .then();
1003
+ if (ack.err) {
1004
+ console.error(`Error creating user registry: ${ack.err}`);
1005
+ }
1006
+ else {
1007
+ console.log(`User registry created: ${userPub}`);
1008
+ }
1009
+ }
1010
+ catch (error) {
1011
+ console.error(`Error creating user registry: ${error}`);
1012
+ }
1013
+ }
1014
+ /**
1015
+ * Creates reverse lookup: userPub -> alias
1016
+ */
1017
+ async createReverseLookup(username, userPub) {
1018
+ try {
1019
+ const ack = await this.node
1020
+ .get("userAliases")
1021
+ .get(userPub)
1022
+ .put(username)
1023
+ .then();
1024
+ if (ack.err) {
1025
+ console.error(`Error creating reverse lookup: ${ack.err}`);
1026
+ }
1027
+ else {
1028
+ console.log(`Reverse lookup created: ${userPub} -> ${username}`);
1029
+ }
1030
+ }
1031
+ catch (error) {
1032
+ console.error(`Error creating reverse lookup: ${error}`);
1033
+ }
1034
+ }
1035
+ /**
1036
+ * Creates epub index: epubKeys/epub -> userPub
1037
+ */
1038
+ async createEpubIndex(epub, userPub) {
1039
+ try {
1040
+ const ack = await this.node.get("epubKeys").get(epub).put(userPub).then();
1041
+ if (ack.err) {
1042
+ console.error(`Error creating epub index: ${ack.err}`);
1043
+ }
1044
+ else {
1045
+ console.log(`Epub index created: ${epub} -> ${userPub}`);
1046
+ }
1047
+ }
1048
+ catch (error) {
1049
+ console.error(`Error creating epub index: ${error}`);
1050
+ }
1051
+ }
1052
+ /**
1053
+ * Creates user metadata in user's own node
1054
+ */
1055
+ async createUserMetadata(username, userPub, epub) {
1056
+ try {
1057
+ const userMetadata = {
1058
+ username: username,
1059
+ epub: epub,
1060
+ registeredAt: Date.now(),
1061
+ lastSeen: Date.now(),
1062
+ };
1063
+ const ack = await this.gun.get(userPub).put(userMetadata).then();
1064
+ if (ack.err) {
1065
+ console.error(`Error creating user metadata: ${ack.err}`);
1066
+ }
1067
+ else {
1068
+ console.log(`User metadata created for ${userPub}`);
1069
+ }
1070
+ }
1071
+ catch (error) {
1072
+ console.error(`Error creating user metadata: ${error}`);
1073
+ }
1074
+ }
1075
+ /**
1076
+ * Gets user information by alias using the comprehensive tracking system
1077
+ * @param alias Username/alias to lookup
1078
+ * @returns Promise resolving to user information or null if not found
1079
+ */
1080
+ async getUserByAlias(alias) {
1081
+ try {
1082
+ const normalizedAlias = alias.trim().toLowerCase();
1083
+ if (!normalizedAlias) {
1084
+ return null;
1085
+ }
1086
+ // Method 1: Try GunDB standard alias lookup (~@alias)
1087
+ try {
1088
+ const aliasData = await this.gun
1089
+ .get(`~@${normalizedAlias}`)
1090
+ .once()
1091
+ .then();
1092
+ if (aliasData && aliasData["~pubKeyOfUser"]) {
1093
+ const userPub = aliasData["~pubKeyOfUser"]["#"] || aliasData["~pubKeyOfUser"];
1094
+ if (userPub) {
1095
+ const userData = await this.getUserDataByPub(userPub);
1096
+ if (userData) {
1097
+ return userData;
1098
+ }
1099
+ }
1100
+ }
1101
+ }
1102
+ catch (error) {
1103
+ console.log(`GunDB alias lookup failed for ${normalizedAlias}:`, error);
1104
+ }
1105
+ // Method 2: Try username mapping (usernames/alias -> userPub)
1106
+ try {
1107
+ const userPub = await this.node
1108
+ .get("usernames")
1109
+ .get(normalizedAlias)
1110
+ .once()
1111
+ .then();
1112
+ if (userPub) {
1113
+ const userData = await this.getUserDataByPub(userPub);
1114
+ if (userData) {
1115
+ return userData;
1116
+ }
1117
+ }
1118
+ }
1119
+ catch (error) {
1120
+ console.log(`Username mapping lookup failed for ${normalizedAlias}:`, error);
1121
+ }
1122
+ return null;
1123
+ }
1124
+ catch (error) {
1125
+ console.error(`Error looking up user by alias ${alias}:`, error);
1126
+ return null;
1127
+ }
1128
+ }
1129
+ /**
1130
+ * Gets user information by public key
1131
+ * @param userPub User's public key
1132
+ * @returns Promise resolving to user information or null if not found
1133
+ */
1134
+ async getUserDataByPub(userPub) {
1135
+ try {
1136
+ if (!userPub || typeof userPub !== "string") {
1137
+ return null;
1138
+ }
1139
+ // Method 1: Try user registry (users/userPub -> user data)
1140
+ try {
1141
+ const userData = await this.node
1142
+ .get("users")
1143
+ .get(userPub)
1144
+ .once()
1145
+ .then();
1146
+ if (userData && userData.username) {
1147
+ return {
1148
+ userPub: userData.userPub || userPub,
1149
+ epub: userData.epub || null,
1150
+ username: userData.username,
1151
+ registeredAt: userData.registeredAt || 0,
1152
+ lastSeen: userData.lastSeen || 0,
1153
+ };
1154
+ }
1155
+ }
1156
+ catch (error) {
1157
+ console.log(`User registry lookup failed for ${userPub}:`, error);
1158
+ }
1159
+ // Method 2: Try user's own node
1160
+ try {
1161
+ const userNodeData = await this.gun.get(userPub).once().then();
1162
+ if (userNodeData && userNodeData.username) {
1163
+ return {
1164
+ userPub: userPub,
1165
+ epub: userNodeData.epub || null,
1166
+ username: userNodeData.username,
1167
+ registeredAt: userNodeData.registeredAt || 0,
1168
+ lastSeen: userNodeData.lastSeen || 0,
1169
+ };
1170
+ }
1171
+ }
1172
+ catch (error) {
1173
+ console.log(`User node lookup failed for ${userPub}:`, error);
1174
+ }
1175
+ return null;
1176
+ }
1177
+ catch (error) {
1178
+ console.error(`Error looking up user data by pub ${userPub}:`, error);
1179
+ return null;
1180
+ }
1181
+ }
1182
+ /**
1183
+ * Gets user public key by encryption public key (epub)
1184
+ * @param epub User's encryption public key
1185
+ * @returns Promise resolving to user public key or null if not found
1186
+ */
1187
+ async getUserPubByEpub(epub) {
1188
+ try {
1189
+ if (!epub || typeof epub !== "string") {
1190
+ return null;
1191
+ }
1192
+ const userPub = await this.node.get("epubKeys").get(epub).once().then();
1193
+ return userPub || null;
1194
+ }
1195
+ catch (error) {
1196
+ console.error(`Error looking up user pub by epub ${epub}:`, error);
1197
+ return null;
1198
+ }
1199
+ }
1200
+ /**
1201
+ * Gets user alias by public key
1202
+ * @param userPub User's public key
1203
+ * @returns Promise resolving to user alias or null if not found
1204
+ */
1205
+ async getUserAliasByPub(userPub) {
1206
+ try {
1207
+ if (!userPub || typeof userPub !== "string") {
1208
+ return null;
1209
+ }
1210
+ const alias = await this.node
1211
+ .get("userAliases")
1212
+ .get(userPub)
1213
+ .once()
1214
+ .then();
1215
+ return alias || null;
1216
+ }
1217
+ catch (error) {
1218
+ console.error(`Error looking up user alias by pub ${userPub}:`, error);
1219
+ return null;
1220
+ }
1221
+ }
1222
+ /**
1223
+ * Gets all registered users (for admin purposes)
1224
+ * @returns Promise resolving to array of user information
1225
+ */
1226
+ async getAllRegisteredUsers() {
1227
+ try {
1228
+ const users = [];
1229
+ // Get all users from the users registry
1230
+ const usersNode = this.node.get("users");
1231
+ // Note: This is a simplified approach. In a real implementation,
1232
+ // you might want to use Gun's map functionality or iterate through
1233
+ // known user public keys
1234
+ return users;
1235
+ }
1236
+ catch (error) {
1237
+ console.error(`Error getting all registered users:`, error);
1238
+ return [];
1239
+ }
1240
+ }
1241
+ /**
1242
+ * Updates user's last seen timestamp
1243
+ * @param userPub User's public key
1244
+ */
1245
+ async updateUserLastSeen(userPub) {
1246
+ try {
1247
+ if (!userPub || typeof userPub !== "string") {
1248
+ return;
1249
+ }
1250
+ const timestamp = Date.now();
1251
+ // Update in user registry
1252
+ try {
1253
+ await this.node
1254
+ .get("users")
1255
+ .get(userPub)
1256
+ .get("lastSeen")
1257
+ .put(timestamp)
1258
+ .then();
1259
+ }
1260
+ catch (error) {
1261
+ console.log(`Failed to update lastSeen in user registry:`, error);
1262
+ }
1263
+ // Update in user's own node
1264
+ try {
1265
+ await this.gun.get(userPub).get("lastSeen").put(timestamp).then();
1266
+ }
1267
+ catch (error) {
1268
+ console.log(`Failed to update lastSeen in user node:`, error);
1269
+ }
1270
+ }
1271
+ catch (error) {
1272
+ console.error(`Error updating user last seen for ${userPub}:`, error);
1273
+ }
1274
+ }
1275
+ /**
1276
+ * Performs authentication with Gun
1277
+ */
1278
+ async performAuthentication(username, password, pair) {
1279
+ return new Promise((resolve) => {
1280
+ console.log(`Attempting authentication for user: ${username}`);
1281
+ if (pair) {
1282
+ this.gun.user().auth(pair, (ack) => {
1283
+ console.log(`Pair authentication result:`, ack);
1284
+ if (ack.err) {
1285
+ console.error(`Login error for ${username}: ${ack.err}`);
1286
+ resolve({ success: false, error: ack.err });
1287
+ }
1288
+ else {
1289
+ resolve({ success: true, ack });
1290
+ }
1291
+ });
1292
+ }
1293
+ else {
1294
+ this.gun.user().auth(username, password, (ack) => {
1295
+ console.log(`Password authentication result:`, ack);
1296
+ if (ack.err) {
1297
+ console.error(`Login error for ${username}: ${ack.err}`);
1298
+ resolve({ success: false, error: ack.err });
1299
+ }
1300
+ else {
1301
+ resolve({ success: true, ack });
1302
+ }
1303
+ });
1304
+ }
1305
+ });
1306
+ }
1307
+ /**
1308
+ * Builds login result object
1309
+ */
1310
+ buildLoginResult(username, userPub) {
1311
+ // Get the SEA pair from the user object
1312
+ const seaPair = this.gun.user()?._?.sea;
1313
+ return {
1314
+ success: true,
1315
+ userPub,
1316
+ username,
1317
+ // Include SEA pair for consistency with AuthResult interface
1318
+ sea: seaPair
1319
+ ? {
1320
+ pub: seaPair.pub,
1321
+ priv: seaPair.priv,
1322
+ epub: seaPair.epub,
1323
+ epriv: seaPair.epriv,
1324
+ }
1325
+ : undefined,
1326
+ };
1327
+ }
1328
+ async login(username, password, pair) {
1329
+ try {
1330
+ const loginResult = await this.performAuthentication(username, password, pair);
1331
+ if (!loginResult.success) {
1332
+ return {
1333
+ success: false,
1334
+ error: `User '${username}' not found. Please check your username or register first.`,
1335
+ };
1336
+ }
1337
+ // Add a small delay to ensure user state is properly set
1338
+ await new Promise((resolve) => setTimeout(resolve, 100));
1339
+ const userPub = this.gun.user().is?.pub;
1340
+ console.log(`Login authentication successful, extracted userPub: ${userPub}`);
1341
+ console.log(`User object:`, this.gun.user());
1342
+ console.log(`User.is:`, this.gun.user().is);
1343
+ if (!userPub) {
1344
+ return {
1345
+ success: false,
1346
+ error: "Authentication failed: No user pub returned.",
1347
+ };
1348
+ }
1349
+ // Pass the userPub to runPostAuthOnAuthResult
1350
+ try {
1351
+ await this.runPostAuthOnAuthResult(username, userPub, {
1352
+ success: true,
1353
+ userPub: userPub,
1354
+ });
1355
+ }
1356
+ catch (postAuthError) {
1357
+ console.error(`Post-auth error during login: ${postAuthError}`);
1358
+ // Continue with login even if post-auth fails
1359
+ }
1360
+ // Update user's last seen timestamp
1361
+ try {
1362
+ await this.updateUserLastSeen(userPub);
1363
+ }
1364
+ catch (lastSeenError) {
1365
+ console.error(`Error updating last seen: ${lastSeenError}`);
1366
+ // Continue with login even if last seen update fails
1367
+ }
1368
+ // Save credentials for future sessions
1369
+ try {
1370
+ const userInfo = {
1371
+ username,
1372
+ pair: pair ?? null,
1373
+ userPub: userPub,
1374
+ };
1375
+ this.saveCredentials(userInfo);
1376
+ }
1377
+ catch (saveError) {
1378
+ console.error(`Error saving credentials:`, saveError);
1379
+ }
1380
+ return this.buildLoginResult(username, userPub);
1381
+ }
1382
+ catch (error) {
1383
+ console.error(`Exception during login for ${username}: ${error}`);
1384
+ return { success: false, error: String(error) };
1385
+ }
1386
+ }
1387
+ /**
1388
+ * Encrypts session data before storage
1389
+ */
1390
+ async encryptSessionData(data) {
1391
+ try {
1392
+ // Use a derived key from device fingerprint for encryption
1393
+ const deviceInfo = navigator.userAgent +
1394
+ (typeof screen !== "undefined"
1395
+ ? screen.width + "x" + screen.height
1396
+ : "");
1397
+ const encryptionKey = await SEA.work(deviceInfo, null, null, {
1398
+ name: "SHA-256",
1399
+ });
1400
+ if (!encryptionKey) {
1401
+ throw new Error("Failed to generate encryption key");
1402
+ }
1403
+ const encryptedData = await SEA.encrypt(JSON.stringify(data), encryptionKey);
1404
+ if (!encryptedData) {
1405
+ throw new Error("Failed to encrypt session data");
1406
+ }
1407
+ return encryptedData;
1408
+ }
1409
+ catch (error) {
1410
+ console.error("Error encrypting session data:", error);
1411
+ throw error;
1412
+ }
1413
+ }
1414
+ /**
1415
+ * Decrypts session data from storage
1416
+ */
1417
+ async decryptSessionData(encryptedData) {
1418
+ try {
1419
+ // Use the same device fingerprint for decryption
1420
+ const deviceInfo = navigator.userAgent +
1421
+ (typeof screen !== "undefined"
1422
+ ? screen.width + "x" + screen.height
1423
+ : "");
1424
+ const encryptionKey = await SEA.work(deviceInfo, null, null, {
1425
+ name: "SHA-256",
1426
+ });
1427
+ if (!encryptionKey) {
1428
+ throw new Error("Failed to generate decryption key");
1429
+ }
1430
+ const decryptedData = await SEA.decrypt(encryptedData, encryptionKey);
1431
+ if (decryptedData === undefined) {
1432
+ throw new Error("Failed to decrypt session data");
1433
+ }
1434
+ return JSON.parse(decryptedData);
1435
+ }
1436
+ catch (error) {
1437
+ console.error("Error decrypting session data:", error);
1438
+ throw error;
1439
+ }
1440
+ }
1441
+ saveCredentials(userInfo) {
1442
+ try {
1443
+ const sessionInfo = {
1444
+ username: userInfo.username,
1445
+ pair: userInfo.pair,
1446
+ userPub: userInfo.userPub,
1447
+ timestamp: Date.now(),
1448
+ expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000, // 7 days
1449
+ };
1450
+ if (typeof sessionStorage !== "undefined") {
1451
+ // Encrypt session data before storage
1452
+ this.encryptSessionData(sessionInfo)
1453
+ .then((encryptedData) => {
1454
+ sessionStorage.setItem("gunSessionData", encryptedData);
1455
+ })
1456
+ .catch((error) => {
1457
+ console.error("Failed to encrypt and save session data:", error);
1458
+ // Fallback to unencrypted storage (less secure)
1459
+ sessionStorage.setItem("gunSessionData", JSON.stringify(sessionInfo));
1460
+ });
1461
+ }
1462
+ }
1463
+ catch (error) {
1464
+ console.error(`Error saving credentials: ${error}`);
1465
+ }
1466
+ }
1467
+ /**
1468
+ * Sets up security questions and password hint
1469
+ * @param username Username
1470
+ * @param password Current password
1471
+ * @param hint Password hint
1472
+ * @param securityQuestions Array of security questions
1473
+ * @param securityAnswers Array of answers to security questions
1474
+ * @returns Promise resolving with the operation result
1475
+ */
1476
+ async setPasswordHintWithSecurity(username, password, hint, securityQuestions, securityAnswers) {
1477
+ // Setting password hint for
1478
+ // Verify that the user is authenticated with password
1479
+ const loginResult = await this.login(username, password);
1480
+ if (!loginResult.success) {
1481
+ return { success: false, error: "Authentication failed" };
1482
+ }
1483
+ // Check if user was authenticated with password (not with other methods)
1484
+ const currentUser = this.getCurrentUser();
1485
+ if (!currentUser || !currentUser.pub) {
1486
+ return { success: false, error: "User not authenticated" };
1487
+ }
1488
+ try {
1489
+ // Generate a proof of work from security question answers
1490
+ const answersText = securityAnswers.join("|");
1491
+ let proofOfWork;
1492
+ try {
1493
+ // Use SEA directly if available
1494
+ if (SEA && SEA.work) {
1495
+ proofOfWork = await SEA.work(answersText, null, null, {
1496
+ name: "SHA-256",
1497
+ });
1498
+ }
1499
+ else if (this.crypto && this.crypto.hashText) {
1500
+ proofOfWork = await this.crypto.hashText(answersText);
1501
+ }
1502
+ else {
1503
+ throw new Error("Cryptographic functions not available");
1504
+ }
1505
+ if (!proofOfWork) {
1506
+ throw new Error("Failed to generate hash");
1507
+ }
1508
+ }
1509
+ catch (hashError) {
1510
+ console.error("Error generating hash:", hashError);
1511
+ return { success: false, error: "Failed to generate security hash" };
1512
+ }
1513
+ // Encrypt the password hint with the proof of work
1514
+ let encryptedHint;
1515
+ try {
1516
+ if (SEA && SEA.encrypt) {
1517
+ encryptedHint = await SEA.encrypt(hint, proofOfWork);
1518
+ }
1519
+ else if (this.crypto && this.crypto.encrypt) {
1520
+ encryptedHint = await this.crypto.encrypt(hint, proofOfWork);
1521
+ }
1522
+ else {
1523
+ throw new Error("Encryption functions not available");
1524
+ }
1525
+ if (!encryptedHint) {
1526
+ throw new Error("Failed to encrypt hint");
1527
+ }
1528
+ }
1529
+ catch (encryptError) {
1530
+ console.error("Error encrypting hint:", encryptError);
1531
+ return { success: false, error: "Failed to encrypt password hint" };
1532
+ }
1533
+ // Save to the public graph, readable by anyone but only decryptable with the right answers.
1534
+ const userPub = currentUser.pub;
1535
+ const securityPayload = {
1536
+ questions: JSON.stringify(securityQuestions),
1537
+ hint: encryptedHint,
1538
+ };
1539
+ const ack = await this.node.get(userPub)
1540
+ .get("security")
1541
+ .put(securityPayload)
1542
+ .then();
1543
+ if (ack.err) {
1544
+ console.error("Error saving security data to public graph:", ack.err);
1545
+ throw new Error(ack.err);
1546
+ }
1547
+ return { success: true };
1548
+ }
1549
+ catch (error) {
1550
+ console.error("Error setting password hint:", error);
1551
+ return { success: false, error: String(error) };
1552
+ }
1553
+ }
1554
+ /**
1555
+ * Recovers password hint using security question answers
1556
+ * @param username Username
1557
+ * @param securityAnswers Array of answers to security questions
1558
+ * @returns Promise resolving with the password hint
1559
+ */
1560
+ async forgotPassword(username, securityAnswers) {
1561
+ // Attempting password recovery for
1562
+ try {
1563
+ // Find the user's data using direct lookup
1564
+ const normalizedUsername = username.trim().toLowerCase();
1565
+ const userPub = (await this.node
1566
+ .get("usernames")
1567
+ .get(normalizedUsername)
1568
+ .once()
1569
+ .then()) || null;
1570
+ if (!userPub) {
1571
+ return { success: false, error: "User not found" };
1572
+ }
1573
+ // console.log(`Found user public key for password recovery: ${userPub}`);
1574
+ // Access the user's security data directly from their public key node
1575
+ const securityData = await this.node.get(userPub)
1576
+ .get("security")
1577
+ .once()
1578
+ .then();
1579
+ if (!securityData || !securityData.hint) {
1580
+ return {
1581
+ success: false,
1582
+ error: "No password hint found",
1583
+ };
1584
+ }
1585
+ // Generate hash from security answers
1586
+ const answersText = securityAnswers.join("|");
1587
+ let proofOfWork;
1588
+ try {
1589
+ // Use SEA directly if available
1590
+ if (SEA && SEA.work) {
1591
+ proofOfWork = await SEA.work(answersText, null, null, {
1592
+ name: "SHA-256",
1593
+ });
1594
+ }
1595
+ else if (this.crypto && this.crypto.hashText) {
1596
+ proofOfWork = await this.crypto.hashText(answersText);
1597
+ }
1598
+ else {
1599
+ throw new Error("Cryptographic functions not available");
1600
+ }
1601
+ if (!proofOfWork) {
1602
+ throw new Error("Failed to generate hash");
1603
+ }
1604
+ }
1605
+ catch (hashError) {
1606
+ console.error("Error generating hash:", hashError);
1607
+ return { success: false, error: "Failed to generate security hash" };
1608
+ }
1609
+ // Decrypt the password hint with the proof of work
1610
+ let hint;
1611
+ try {
1612
+ if (SEA && SEA.decrypt) {
1613
+ hint = await SEA.decrypt(securityData.hint, proofOfWork);
1614
+ }
1615
+ else if (this.crypto && this.crypto.decrypt) {
1616
+ hint = await this.crypto.decrypt(securityData.hint, proofOfWork);
1617
+ }
1618
+ else {
1619
+ throw new Error("Decryption functions not available");
1620
+ }
1621
+ }
1622
+ catch (decryptError) {
1623
+ return {
1624
+ success: false,
1625
+ error: "Incorrect answers to security questions",
1626
+ };
1627
+ }
1628
+ if (hint === undefined) {
1629
+ return {
1630
+ success: false,
1631
+ error: "Incorrect answers to security questions",
1632
+ };
1633
+ }
1634
+ return { success: true, hint: hint };
1635
+ }
1636
+ catch (error) {
1637
+ console.error("Error recovering password hint:", error);
1638
+ return { success: false, error: String(error) };
1639
+ }
1640
+ }
1641
+ /**
1642
+ * Saves user data at the specified path
1643
+ * @param path Path to save the data (supports nested paths like "test/data/marco")
1644
+ * @param data Data to save
1645
+ * @returns Promise that resolves when the data is saved
1646
+ */
1647
+ async putUserData(path, data) {
1648
+ const user = this.gun.user();
1649
+ if (!user.is) {
1650
+ throw new Error("User not authenticated");
1651
+ }
1652
+ const ack = await this.navigateToPath(user, path).put(data).then();
1653
+ if (ack.err) {
1654
+ throw new Error(ack.err);
1655
+ }
1656
+ }
1657
+ /**
1658
+ * Gets user data from the specified path
1659
+ * @param path Path to get the data from (supports nested paths like "test/data/marco")
1660
+ * @returns Promise that resolves with the data
1661
+ */
1662
+ async getUserData(path) {
1663
+ // Validazione del path
1664
+ if (!path || typeof path !== "string") {
1665
+ throw new Error("Path must be a non-empty string");
1666
+ }
1667
+ const user = this.gun.user();
1668
+ if (!user.is) {
1669
+ throw new Error("User not authenticated");
1670
+ }
1671
+ try {
1672
+ const data = await this.navigateToPath(user, path).once().then();
1673
+ // Gestisci i riferimenti GunDB
1674
+ if (data && typeof data === "object" && data["#"]) {
1675
+ // È un riferimento GunDB, carica i dati effettivi
1676
+ const referencePath = data["#"];
1677
+ const actualData = await this.navigateToPath(this.gun, referencePath)
1678
+ .once()
1679
+ .then();
1680
+ return actualData;
1681
+ }
1682
+ else {
1683
+ // Dati diretti, restituisci così come sono
1684
+ return data;
1685
+ }
1686
+ }
1687
+ catch (error) {
1688
+ const errorMsg = error instanceof Error ? error.message : "Unknown error";
1689
+ throw new Error(errorMsg);
1690
+ }
1691
+ }
1692
+ // Errors
1693
+ static Errors = GunErrors;
1694
+ /**
1695
+ * Adds an event listener
1696
+ * @param event Event name
1697
+ * @param listener Event listener function
1698
+ */
1699
+ on(event, listener) {
1700
+ this.eventEmitter.on(event, listener);
1701
+ }
1702
+ /**
1703
+ * Removes an event listener
1704
+ * @param event Event name
1705
+ * @param listener Event listener function
1706
+ */
1707
+ off(event, listener) {
1708
+ this.eventEmitter.off(event, listener);
1709
+ }
1710
+ /**
1711
+ * Adds a one-time event listener
1712
+ * @param event Event name
1713
+ * @param listener Event listener function
1714
+ */
1715
+ once(event, listener) {
1716
+ this.eventEmitter.once(event, listener);
1717
+ }
1718
+ /**
1719
+ * Emits an event
1720
+ * @param event Event name
1721
+ * @param data Event data
1722
+ */
1723
+ emit(event, data) {
1724
+ return this.eventEmitter.emit(event, data);
1725
+ }
1726
+ /**
1727
+ * Recall user session
1728
+ */
1729
+ recall() {
1730
+ if (this.user) {
1731
+ this.user.recall({ sessionStorage: true });
1732
+ }
1733
+ }
1734
+ /**
1735
+ * Leave user session
1736
+ */
1737
+ leave() {
1738
+ if (this.user) {
1739
+ this.user.leave();
1740
+ }
1741
+ }
1742
+ /**
1743
+ * Set user data
1744
+ */
1745
+ setUserData(data) {
1746
+ if (this.user) {
1747
+ this.user.put(data);
1748
+ }
1749
+ }
1750
+ /**
1751
+ * Set password hint
1752
+ */
1753
+ setPasswordHint(hint) {
1754
+ if (this.user) {
1755
+ try {
1756
+ this.user.get("passwordHint").put(hint);
1757
+ }
1758
+ catch (error) {
1759
+ // Handle case where user.get returns undefined
1760
+ console.warn("Could not set password hint:", error);
1761
+ }
1762
+ }
1763
+ }
1764
+ /**
1765
+ * Get password hint
1766
+ */
1767
+ getPasswordHint() {
1768
+ if (this.user) {
1769
+ // Access passwordHint from user data, not from is object
1770
+ return this.user.passwordHint || null;
1771
+ }
1772
+ return null;
1773
+ }
1774
+ /**
1775
+ * Save session to storage
1776
+ */
1777
+ saveSession(session) {
1778
+ if (this.user) {
1779
+ this.user.recall({ sessionStorage: true });
1780
+ }
1781
+ }
1782
+ /**
1783
+ * Load session from storage
1784
+ */
1785
+ loadSession() {
1786
+ if (this.user) {
1787
+ return this.user.recall({ sessionStorage: true });
1788
+ }
1789
+ return null;
1790
+ }
1791
+ /**
1792
+ * Clear session
1793
+ */
1794
+ clearSession() {
1795
+ if (this.user) {
1796
+ this.user.leave();
1797
+ }
1798
+ }
1799
+ /**
1800
+ * Get app scope
1801
+ */
1802
+ getAppScope() {
1803
+ return this.node?._?.soul || "shogun";
1804
+ }
1805
+ /**
1806
+ * Get user public key
1807
+ */
1808
+ getUserPub() {
1809
+ if (this.user) {
1810
+ return this.user.is?.pub || null;
1811
+ }
1812
+ return null;
1813
+ }
1814
+ /**
1815
+ * Check if user is authenticated
1816
+ */
1817
+ isAuthenticated() {
1818
+ return this.user?.is?.pub ? true : false;
1819
+ }
1820
+ }
1821
+ const createGun = (config) => {
1822
+ console.log("Creating Gun instance with config:", config);
1823
+ console.log("Config peers:", config?.peers);
1824
+ const gunInstance = Gun(config);
1825
+ console.log("Created Gun instance:", gunInstance);
1826
+ return gunInstance;
1827
+ };
1828
+ export { Gun, DataBase, SEA, RxJS, crypto, GunErrors, derive, createGun };
1829
+ export default Gun;