holosphere 2.0.0-alpha11 → 2.0.0-alpha13

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 (65) hide show
  1. package/dist/{2019-D2OG2idw.js → 2019-CLMqIAfQ.js} +1722 -1668
  2. package/dist/{2019-D2OG2idw.js.map → 2019-CLMqIAfQ.js.map} +1 -1
  3. package/dist/2019-Cp3uYhyY.cjs +8 -0
  4. package/dist/{2019-EION3wKo.cjs.map → 2019-Cp3uYhyY.cjs.map} +1 -1
  5. package/dist/browser-D6cNVl0v.cjs +2 -0
  6. package/dist/{browser-Cq59Ij19.cjs.map → browser-D6cNVl0v.cjs.map} +1 -1
  7. package/dist/{browser-BSniCNqO.js → browser-nUQt1cnB.js} +2 -2
  8. package/dist/{browser-BSniCNqO.js.map → browser-nUQt1cnB.js.map} +1 -1
  9. package/dist/cjs/holosphere.cjs +1 -1
  10. package/dist/esm/holosphere.js +67 -50
  11. package/dist/{index-D-jZhliX.js → index-BN_uoxQK.js} +20324 -735
  12. package/dist/index-BN_uoxQK.js.map +1 -0
  13. package/dist/{index-Bl6rM1NW.js → index-CoAjtqsD.js} +2 -2
  14. package/dist/{index-Bl6rM1NW.js.map → index-CoAjtqsD.js.map} +1 -1
  15. package/dist/{index-Bwg3OzRM.cjs → index-Cp3tI53z.cjs} +3 -3
  16. package/dist/{index-Bwg3OzRM.cjs.map → index-Cp3tI53z.cjs.map} +1 -1
  17. package/dist/index-DJjGSwXG.cjs +13 -0
  18. package/dist/index-DJjGSwXG.cjs.map +1 -0
  19. package/dist/index-V8EHMYEY.cjs +29 -0
  20. package/dist/index-V8EHMYEY.cjs.map +1 -0
  21. package/dist/index-Z5TstN1e.js +11663 -0
  22. package/dist/index-Z5TstN1e.js.map +1 -0
  23. package/dist/indexeddb-storage-CZK5A7XH.cjs +2 -0
  24. package/dist/indexeddb-storage-CZK5A7XH.cjs.map +1 -0
  25. package/dist/{indexeddb-storage-5eiUNsHC.js → indexeddb-storage-bpA01pAU.js} +39 -2
  26. package/dist/indexeddb-storage-bpA01pAU.js.map +1 -0
  27. package/dist/{memory-storage-DMt36uZO.cjs → memory-storage-B1k8Jszd.cjs} +2 -2
  28. package/dist/{memory-storage-DMt36uZO.cjs.map → memory-storage-B1k8Jszd.cjs.map} +1 -1
  29. package/dist/{memory-storage-CI-gfmuG.js → memory-storage-BqhmytP_.js} +2 -2
  30. package/dist/{memory-storage-CI-gfmuG.js.map → memory-storage-BqhmytP_.js.map} +1 -1
  31. package/docs/FEDERATION.md +474 -0
  32. package/package.json +3 -1
  33. package/src/crypto/nostr-utils.js +7 -0
  34. package/src/crypto/secp256k1.js +104 -38
  35. package/src/federation/capabilities.js +162 -0
  36. package/src/federation/card-storage.js +376 -0
  37. package/src/federation/handshake.js +561 -9
  38. package/src/federation/hologram.js +194 -57
  39. package/src/federation/holon-registry.js +187 -0
  40. package/src/federation/index.js +68 -0
  41. package/src/federation/registry.js +164 -6
  42. package/src/federation/request-card.js +373 -0
  43. package/src/hierarchical/upcast.js +19 -3
  44. package/src/index.js +209 -75
  45. package/src/lib/federation-methods.js +527 -5
  46. package/src/storage/indexeddb-storage.js +41 -0
  47. package/src/storage/nostr-async.js +14 -5
  48. package/src/storage/nostr-client.js +471 -155
  49. package/src/storage/nostr-wrapper.js +6 -3
  50. package/dist/2019-EION3wKo.cjs +0 -8
  51. package/dist/_commonjsHelpers-C37NGDzP.cjs +0 -2
  52. package/dist/_commonjsHelpers-C37NGDzP.cjs.map +0 -1
  53. package/dist/_commonjsHelpers-CUmg6egw.js +0 -7
  54. package/dist/_commonjsHelpers-CUmg6egw.js.map +0 -1
  55. package/dist/browser-Cq59Ij19.cjs +0 -2
  56. package/dist/index-D-jZhliX.js.map +0 -1
  57. package/dist/index-Dc6Z8Aob.cjs +0 -18
  58. package/dist/index-Dc6Z8Aob.cjs.map +0 -1
  59. package/dist/indexeddb-storage-5eiUNsHC.js.map +0 -1
  60. package/dist/indexeddb-storage-FNFUVvTJ.cjs +0 -2
  61. package/dist/indexeddb-storage-FNFUVvTJ.cjs.map +0 -1
  62. package/dist/secp256k1-CEwJNcfV.js +0 -1890
  63. package/dist/secp256k1-CEwJNcfV.js.map +0 -1
  64. package/dist/secp256k1-CiEONUnj.cjs +0 -12
  65. package/dist/secp256k1-CiEONUnj.cjs.map +0 -1
@@ -1,46 +1,98 @@
1
1
  /**
2
- * @fileoverview Nostr Client - SimplePool wrapper for relay management.
2
+ * @fileoverview Nostr Client - NDK-based wrapper for relay management.
3
3
  *
4
4
  * Provides connection pooling, relay management, event caching, persistent storage,
5
5
  * outbox queue for guaranteed delivery, and background sync services.
6
+ * Uses @nostr-dev-kit/ndk for improved relay handling and NIP support.
6
7
  *
7
8
  * @module storage/nostr-client
8
9
  */
9
10
 
10
- import { SimplePool, finalizeEvent, getPublicKey } from 'nostr-tools';
11
+ import NDK, { NDKEvent, NDKPrivateKeySigner, NDKSubscriptionCacheUsage } from '@nostr-dev-kit/ndk';
12
+ import { getPublicKey as nostrToolsGetPublicKey, finalizeEvent as nostrToolsFinalizeEvent } from 'nostr-tools';
11
13
  import { OutboxQueue } from './outbox-queue.js';
12
14
  import { SyncService } from './sync-service.js';
13
15
 
14
16
  /**
15
- * Global pool singleton - reuse connections across NostrClient instances.
17
+ * Lazy-load NDK cache adapter for browser environments.
18
+ * Falls back to null if not available (Node.js or package not installed).
16
19
  * @private
17
20
  */
18
- let globalPool = null;
21
+ let NDKCacheAdapterDexie = null;
22
+ let ndkCacheAdapterLoaded = false;
23
+
24
+ async function loadNDKCacheAdapter() {
25
+ if (ndkCacheAdapterLoaded) return NDKCacheAdapterDexie;
26
+ ndkCacheAdapterLoaded = true;
27
+
28
+ // Only try to load in browser environment with IndexedDB support
29
+ if (typeof window !== 'undefined' && typeof indexedDB !== 'undefined') {
30
+ try {
31
+ const module = await import('@nostr-dev-kit/ndk-cache-dexie');
32
+ NDKCacheAdapterDexie = module.default || module.NDKCacheAdapterDexie;
33
+ } catch (e) {
34
+ // Package not available, use fallback caching
35
+ console.debug('[nostr] NDK cache adapter not available, using LRU cache');
36
+ }
37
+ }
38
+ return NDKCacheAdapterDexie;
39
+ }
19
40
 
20
41
  /**
21
- * Set of relays used by the global pool.
42
+ * Global NDK singleton - reuse connections across NostrClient instances.
22
43
  * @private
23
44
  */
24
- let globalPoolRelays = new Set();
45
+ let globalNDK = null;
25
46
 
26
47
  /**
27
- * Get or create global SimplePool singleton.
48
+ * Set of relays used by the global NDK.
49
+ * @private
50
+ */
51
+ let globalNDKRelays = new Set();
52
+
53
+ /**
54
+ * NDK cache adapter instance (shared across clients).
55
+ * @private
56
+ */
57
+ let globalNDKCacheAdapter = null;
58
+
59
+ /**
60
+ * Get or create global NDK singleton.
28
61
  * This ensures WebSocket connections are reused across all operations.
29
62
  *
30
63
  * @private
31
- * @param {Object} [config={}] - Pool configuration
32
- * @param {boolean} [config.enableReconnect=true] - Enable auto-reconnect
33
- * @param {boolean} [config.enablePing=true] - Enable ping/pong
34
- * @returns {SimplePool} The global SimplePool instance
64
+ * @param {Object} [config={}] - NDK configuration
65
+ * @param {string[]} [config.relays=[]] - Array of relay URLs
66
+ * @param {NDKPrivateKeySigner} [config.signer] - NDK signer instance
67
+ * @param {Object} [config.cacheAdapter] - Optional cache adapter instance
68
+ * @returns {NDK} The global NDK instance
35
69
  */
36
- function getGlobalPool(config = {}) {
37
- if (!globalPool) {
38
- globalPool = new SimplePool({
39
- enableReconnect: config.enableReconnect !== false,
40
- enablePing: config.enablePing !== false,
41
- });
70
+ function getGlobalNDK(config = {}) {
71
+ if (!globalNDK) {
72
+ const ndkOptions = {
73
+ explicitRelayUrls: config.relays || [],
74
+ signer: config.signer,
75
+ autoConnectUserRelays: false,
76
+ autoFetchUserMutelist: false,
77
+ };
78
+
79
+ // Use cache adapter if provided
80
+ if (config.cacheAdapter) {
81
+ ndkOptions.cacheAdapter = config.cacheAdapter;
82
+ globalNDKCacheAdapter = config.cacheAdapter;
83
+ }
84
+
85
+ globalNDK = new NDK(ndkOptions);
86
+ } else if (config.relays) {
87
+ // Add any new relays to existing NDK
88
+ for (const relay of config.relays) {
89
+ if (!globalNDKRelays.has(relay)) {
90
+ globalNDK.addExplicitRelay(relay);
91
+ globalNDKRelays.add(relay);
92
+ }
93
+ }
42
94
  }
43
- return globalPool;
95
+ return globalNDK;
44
96
  }
45
97
 
46
98
  /**
@@ -211,7 +263,33 @@ function ensureWebSocket() {
211
263
  import { createPersistentStorage } from './persistent-storage.js';
212
264
 
213
265
  /**
214
- * NostrClient - Manages connections to Nostr relays.
266
+ * Convert hex string to Uint8Array.
267
+ * @private
268
+ * @param {string} hex - Hexadecimal string
269
+ * @returns {Uint8Array} Byte array
270
+ */
271
+ function hexToBytes(hex) {
272
+ const bytes = new Uint8Array(hex.length / 2);
273
+ for (let i = 0; i < hex.length; i += 2) {
274
+ bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
275
+ }
276
+ return bytes;
277
+ }
278
+
279
+ /**
280
+ * Convert Uint8Array to hex string.
281
+ * @private
282
+ * @param {Uint8Array} bytes - Byte array
283
+ * @returns {string} Hexadecimal string
284
+ */
285
+ function bytesToHex(bytes) {
286
+ return Array.from(bytes)
287
+ .map((b) => b.toString(16).padStart(2, '0'))
288
+ .join('');
289
+ }
290
+
291
+ /**
292
+ * NostrClient - Manages connections to Nostr relays using NDK.
215
293
  *
216
294
  * Provides event publishing, querying, subscription management, persistent caching,
217
295
  * and guaranteed delivery via outbox queue.
@@ -252,7 +330,14 @@ export class NostrClient {
252
330
  // IMPORTANT: If you want keys to persist across restarts, you MUST provide
253
331
  // a privateKey in config. Otherwise, a new key is generated each time.
254
332
  this.privateKey = config.privateKey || this._generatePrivateKey();
255
- this.publicKey = getPublicKey(this.privateKey);
333
+
334
+ // Create NDK signer
335
+ this.signer = new NDKPrivateKeySigner(this.privateKey);
336
+
337
+ // Set public key synchronously for immediate access
338
+ // This uses nostr-tools which is synchronous, unlike NDK's async signer.user()
339
+ this.publicKey = nostrToolsGetPublicKey(this.privateKey);
340
+
256
341
  this.config = config;
257
342
 
258
343
  this._subscriptions = new Map();
@@ -265,38 +350,72 @@ export class NostrClient {
265
350
  this._persistTimer = null;
266
351
  this._persistBatchMs = config.persistBatchMs || 100; // Batch writes within 100ms window
267
352
 
268
- // Initialize pool and storage asynchronously
353
+ // Initialize NDK and storage asynchronously
269
354
  this._initReady = this._initialize();
270
355
  }
271
356
 
272
357
  /**
273
- * Initialize WebSocket polyfill and pool.
358
+ * Initialize WebSocket polyfill and NDK.
274
359
  *
275
360
  * @private
276
361
  * @returns {Promise<void>}
277
362
  */
278
363
  async _initialize() {
279
- // Ensure WebSocket is available before initializing pool
364
+ // Ensure WebSocket is available before initializing NDK
280
365
  await ensureWebSocket();
281
366
 
282
- // Initialize SimplePool with options (only if relays exist)
367
+ // Try to load NDK cache adapter for browser environments
368
+ const CacheAdapter = await loadNDKCacheAdapter();
369
+ let cacheAdapter = null;
370
+
371
+ if (CacheAdapter && this.config.appName) {
372
+ // Use distinct name from IndexedDBStorage to avoid conflicts
373
+ const dbName = `holosphere_ndk_${this.config.appName}`;
374
+
375
+ // Pre-emptively check for and clear incompatible NDK cache database
376
+ // This prevents async Dexie UpgradeError crashes during background cache operations
377
+ await this._clearIncompatibleNDKCache(dbName);
378
+
379
+ try {
380
+ // Create NDK cache adapter with app-specific database name
381
+ cacheAdapter = new CacheAdapter({ dbName });
382
+ this._useNDKCache = true;
383
+ } catch (e) {
384
+ console.warn('[nostr] Failed to initialize NDK cache adapter:', e.message);
385
+ // Try once more after clearing the database
386
+ await this._deleteDexieDatabase(dbName);
387
+ try {
388
+ cacheAdapter = new CacheAdapter({ dbName });
389
+ this._useNDKCache = true;
390
+ } catch (retryError) {
391
+ console.warn('[nostr] Failed to initialize NDK cache adapter after reset:', retryError.message);
392
+ }
393
+ }
394
+ }
395
+
396
+ // Initialize NDK with options (only if relays exist)
283
397
  if (this.relays.length > 0) {
284
- // Use global pool singleton to reuse WebSocket connections
285
- this.pool = getGlobalPool(this.config);
398
+ // Use global NDK singleton to reuse WebSocket connections
399
+ this.ndk = getGlobalNDK({
400
+ relays: this.relays,
401
+ signer: this.signer,
402
+ cacheAdapter,
403
+ });
286
404
 
287
405
  // Track relays used by this client
288
- this.relays.forEach(r => globalPoolRelays.add(r));
406
+ this.relays.forEach(r => globalNDKRelays.add(r));
407
+
408
+ // Connect to relays
409
+ await this.ndk.connect();
410
+
411
+ // Note: publicKey is already set synchronously in constructor using nostr-tools
289
412
  } else {
290
- // Mock pool for testing - returns mock promise that resolves immediately
291
- this.pool = {
292
- publish: (relays, event) => [Promise.resolve()],
293
- querySync: (relays, filter, options) => Promise.resolve([]),
294
- subscribeMany: (relays, filters, opts) => ({ close: () => {} }),
295
- close: (relays) => {},
296
- };
413
+ // Mock NDK for testing - returns mock promises that resolve immediately
414
+ this.ndk = null;
415
+ // Note: publicKey is already set synchronously in constructor using nostr-tools
297
416
  }
298
417
 
299
- // Initialize persistent storage
418
+ // Initialize persistent storage (used for outbox queue and fallback caching)
300
419
  await this._initPersistentStorage();
301
420
 
302
421
  // Start long-lived subscription for real-time cache updates
@@ -339,28 +458,29 @@ export class NostrClient {
339
458
  authors: [this.publicKey],
340
459
  };
341
460
 
342
- const sub = this.pool.subscribeMany(
343
- this.relays,
344
- [filter],
345
- {
346
- onevent: (event) => {
347
- // Verify author (relay may not respect filter)
348
- if (event.pubkey !== this.publicKey) {
349
- return;
350
- }
461
+ const sub = this.ndk.subscribe(filter, {
462
+ closeOnEose: false,
463
+ cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY,
464
+ });
351
465
 
352
- // Cache the event - this keeps our local cache in sync
353
- this._cacheEvent(event);
354
- },
355
- oneose: () => {
356
- // End of stored events - initial load complete
357
- if (!subInfo.initialized) {
358
- subInfo.initialized = true;
359
- subInfo.initResolve();
360
- }
361
- },
466
+ sub.on('event', (ndkEvent) => {
467
+ // Verify author (relay may not respect filter)
468
+ if (ndkEvent.pubkey !== this.publicKey) {
469
+ return;
362
470
  }
363
- );
471
+
472
+ // Convert NDKEvent to raw event format and cache it
473
+ const event = ndkEvent.rawEvent();
474
+ this._cacheEvent(event);
475
+ });
476
+
477
+ sub.on('eose', () => {
478
+ // End of stored events - initial load complete
479
+ if (!subInfo.initialized) {
480
+ subInfo.initialized = true;
481
+ subInfo.initResolve();
482
+ }
483
+ });
364
484
 
365
485
  subInfo.subscription = sub;
366
486
 
@@ -422,6 +542,83 @@ export class NostrClient {
422
542
  }
423
543
  }
424
544
 
545
+ /**
546
+ * Check if NDK cache database exists with potentially incompatible schema.
547
+ * NDK cache adapter uses Dexie which can't handle primary key changes.
548
+ * We detect this by checking if the database exists and clearing it if needed.
549
+ * @param {string} dbName - The database name to check
550
+ * @returns {Promise<void>}
551
+ * @private
552
+ */
553
+ async _clearIncompatibleNDKCache(dbName) {
554
+ if (typeof indexedDB === 'undefined') return;
555
+
556
+ // Check if database exists and get its version
557
+ const databases = await indexedDB.databases?.() || [];
558
+ const existingDb = databases.find(db => db.name === dbName);
559
+
560
+ if (existingDb) {
561
+ // NDK cache adapter expects specific schema versions
562
+ // If the database exists but was created with an incompatible version,
563
+ // we need to delete it to prevent UpgradeError crashes
564
+ // The NDK cache adapter will recreate it with the correct schema
565
+ try {
566
+ await new Promise((resolve, reject) => {
567
+ const request = indexedDB.open(dbName);
568
+ request.onsuccess = () => {
569
+ const db = request.result;
570
+ const storeNames = Array.from(db.objectStoreNames);
571
+ db.close();
572
+
573
+ // Check for known incompatible schemas (missing required stores or wrong structure)
574
+ // NDK cache adapter expects: events, eventTags, users, nip05, profiles, etc.
575
+ const requiredStores = ['events', 'eventTags'];
576
+ const hasRequiredStores = requiredStores.every(name => storeNames.includes(name));
577
+
578
+ if (!hasRequiredStores && storeNames.length > 0) {
579
+ // Database exists but doesn't have the right structure
580
+ console.warn(`[nostr] NDK cache database ${dbName} has incompatible schema, clearing...`);
581
+ this._deleteDexieDatabase(dbName).then(resolve).catch(resolve);
582
+ } else {
583
+ resolve();
584
+ }
585
+ };
586
+ request.onerror = () => resolve(); // Ignore errors, continue without cache
587
+ request.onblocked = () => resolve();
588
+ });
589
+ } catch {
590
+ // Ignore errors during compatibility check
591
+ }
592
+ }
593
+ }
594
+
595
+ /**
596
+ * Delete a Dexie database by name.
597
+ * Used to recover from schema upgrade errors.
598
+ * @param {string} dbName - The database name to delete
599
+ * @returns {Promise<void>}
600
+ * @private
601
+ */
602
+ async _deleteDexieDatabase(dbName) {
603
+ if (typeof indexedDB === 'undefined') return;
604
+
605
+ return new Promise((resolve) => {
606
+ const request = indexedDB.deleteDatabase(dbName);
607
+ request.onsuccess = () => {
608
+ console.log(`[nostr] Deleted incompatible database: ${dbName}`);
609
+ resolve();
610
+ };
611
+ request.onerror = () => {
612
+ console.warn(`[nostr] Failed to delete database: ${dbName}`);
613
+ resolve(); // Resolve anyway to continue initialization
614
+ };
615
+ request.onblocked = () => {
616
+ console.warn(`[nostr] Database deletion blocked: ${dbName}`);
617
+ resolve(); // Resolve anyway to continue initialization
618
+ };
619
+ });
620
+ }
621
+
425
622
  /**
426
623
  * Load events from persistent storage into cache
427
624
  * @private
@@ -538,6 +735,7 @@ export class NostrClient {
538
735
  * @param {Object} [options={}] - Publish options
539
736
  * @param {boolean} [options.waitForRelays=false] - Wait for relay confirmation
540
737
  * @param {boolean} [options.debounce=true] - Debounce rapid writes to same d-tag
738
+ * @param {string} [options.signingKey] - Private key to sign with (hex format). If not provided, uses client's default key.
541
739
  * @returns {Promise<Object>} Signed event with relay publish results
542
740
  * @example
543
741
  * const result = await client.publish({
@@ -600,23 +798,89 @@ export class NostrClient {
600
798
  pendingWrites.set(dTagPath, { event, timer, resolve, reject });
601
799
 
602
800
  // Cache immediately for local-first reads (even before relay publish)
603
- const signedEvent = finalizeEvent(event, this.privateKey);
801
+ const signedEvent = this._createSignedEvent(event, options.signingKey);
604
802
  this._cacheEvent(signedEvent);
605
803
  });
606
804
  }
607
805
 
806
+ /**
807
+ * Create a signed event using NDK or nostr-tools fallback.
808
+ * @private
809
+ * @param {Object} event - Unsigned event
810
+ * @param {string} [signingKey] - Optional private key to sign with (hex format)
811
+ * @returns {Object} Signed event
812
+ */
813
+ _createSignedEvent(event, signingKey = null) {
814
+ const keyToUse = signingKey || this.privateKey;
815
+ const pubkeyToUse = signingKey ? nostrToolsGetPublicKey(hexToBytes(signingKey)) : this.publicKey;
816
+
817
+ // If NDK is available and we have a signer, use NDKEvent
818
+ if (this.ndk) {
819
+ const ndkEvent = new NDKEvent(this.ndk);
820
+ ndkEvent.kind = event.kind;
821
+ ndkEvent.content = event.content;
822
+ ndkEvent.tags = event.tags || [];
823
+ ndkEvent.created_at = event.created_at || Math.floor(Date.now() / 1000);
824
+
825
+ // NDK will sign when publishing, for now return raw event format
826
+ return {
827
+ kind: ndkEvent.kind,
828
+ content: ndkEvent.content,
829
+ tags: ndkEvent.tags,
830
+ created_at: ndkEvent.created_at,
831
+ pubkey: pubkeyToUse,
832
+ id: '', // Will be set during actual publish
833
+ sig: '', // Will be set during actual publish
834
+ };
835
+ }
836
+
837
+ // Mock mode - use nostr-tools to create a properly signed event
838
+ try {
839
+ return nostrToolsFinalizeEvent(event, hexToBytes(keyToUse));
840
+ } catch (e) {
841
+ // Return mock signed event if signing fails
842
+ return {
843
+ ...event,
844
+ pubkey: pubkeyToUse,
845
+ id: Math.random().toString(16).slice(2).padStart(64, '0'),
846
+ sig: 'mock-signature',
847
+ created_at: event.created_at || Math.floor(Date.now() / 1000),
848
+ };
849
+ }
850
+ }
851
+
608
852
  /**
609
853
  * Internal publish implementation
610
854
  * @private
611
855
  */
612
856
  async _doPublish(event, options = {}) {
613
857
  const waitForRelays = options.waitForRelays || false;
858
+ const signingKey = options.signingKey || null;
614
859
 
615
860
  // Check if event is already signed (has id and sig)
616
- // If so, use it as-is; otherwise sign it
617
- const signedEvent = (event.id && event.sig)
618
- ? event
619
- : finalizeEvent(event, this.privateKey);
861
+ let signedEvent;
862
+ if (event.id && event.sig) {
863
+ signedEvent = event;
864
+ } else if (this.ndk) {
865
+ // Use NDK to create and sign the event
866
+ const ndkEvent = new NDKEvent(this.ndk);
867
+ ndkEvent.kind = event.kind;
868
+ ndkEvent.content = event.content;
869
+ ndkEvent.tags = event.tags || [];
870
+ ndkEvent.created_at = event.created_at || Math.floor(Date.now() / 1000);
871
+
872
+ // Sign the event - use different signer if signingKey provided
873
+ if (signingKey) {
874
+ const tempSigner = new NDKPrivateKeySigner(signingKey);
875
+ await ndkEvent.sign(tempSigner);
876
+ } else {
877
+ await ndkEvent.sign(this.signer);
878
+ }
879
+ signedEvent = ndkEvent.rawEvent();
880
+ } else {
881
+ // Mock mode - create mock signed event
882
+ signedEvent = this._createSignedEvent(event, signingKey);
883
+ }
620
884
 
621
885
  // 1. Cache the event locally first (this makes reads instant)
622
886
  await this._cacheEvent(signedEvent);
@@ -627,28 +891,37 @@ export class NostrClient {
627
891
  }
628
892
 
629
893
  // 3. Publish to relays
630
- if (waitForRelays) {
631
- // Wait for relay confirmation if explicitly requested
632
- const deliveryResult = await this._attemptDelivery(signedEvent, this.relays);
633
-
634
- return {
635
- event: signedEvent,
636
- results: deliveryResult.results,
637
- queued: !!this.outboxQueue,
638
- };
639
- } else {
640
- // Fire and forget - publish in background, return immediately
641
- this._attemptDelivery(signedEvent, this.relays).catch(() => {
642
- // Silent - event is cached locally and queued for retry
643
- });
894
+ if (this.ndk && this.relays.length > 0) {
895
+ if (waitForRelays) {
896
+ // Wait for relay confirmation if explicitly requested
897
+ const deliveryResult = await this._attemptDelivery(signedEvent, this.relays);
898
+
899
+ return {
900
+ event: signedEvent,
901
+ results: deliveryResult.results,
902
+ queued: !!this.outboxQueue,
903
+ };
904
+ } else {
905
+ // Fire and forget - publish in background, return immediately
906
+ this._attemptDelivery(signedEvent, this.relays).catch(() => {
907
+ // Silent - event is cached locally and queued for retry
908
+ });
644
909
 
645
- // Return immediately (data is in local cache and queued for delivery)
646
- return {
647
- event: signedEvent,
648
- results: [],
649
- queued: !!this.outboxQueue,
650
- };
910
+ // Return immediately (data is in local cache and queued for delivery)
911
+ return {
912
+ event: signedEvent,
913
+ results: [],
914
+ queued: !!this.outboxQueue,
915
+ };
916
+ }
651
917
  }
918
+
919
+ // No relays configured - just return the signed event
920
+ return {
921
+ event: signedEvent,
922
+ results: [],
923
+ queued: !!this.outboxQueue,
924
+ };
652
925
  }
653
926
 
654
927
  /**
@@ -659,28 +932,50 @@ export class NostrClient {
659
932
  * @returns {Promise<Object>} Delivery result with successful/failed relays
660
933
  */
661
934
  async _attemptDelivery(event, relays) {
662
- const results = await Promise.allSettled(
663
- this.pool.publish(relays, event)
664
- );
935
+ if (!this.ndk) {
936
+ return { successful: [], failed: relays, results: [] };
937
+ }
938
+
939
+ const ndkEvent = new NDKEvent(this.ndk, event);
665
940
 
666
941
  const successful = [];
667
942
  const failed = [];
668
943
  const formattedResults = [];
669
944
 
670
- results.forEach((r, i) => {
671
- formattedResults.push({
672
- relay: relays[i],
673
- status: r.status,
674
- value: r.value,
675
- reason: r.reason,
676
- });
677
-
678
- if (r.status === 'fulfilled') {
679
- successful.push(relays[i]);
680
- } else {
681
- failed.push(relays[i]);
945
+ try {
946
+ // Publish using NDK
947
+ const publishedRelays = await ndkEvent.publish();
948
+
949
+ // Track results
950
+ for (const relay of relays) {
951
+ const wasPublished = publishedRelays.has(this.ndk.pool.getRelay(relay));
952
+ if (wasPublished) {
953
+ successful.push(relay);
954
+ formattedResults.push({
955
+ relay,
956
+ status: 'fulfilled',
957
+ value: true,
958
+ });
959
+ } else {
960
+ failed.push(relay);
961
+ formattedResults.push({
962
+ relay,
963
+ status: 'rejected',
964
+ reason: 'Not published to relay',
965
+ });
966
+ }
682
967
  }
683
- });
968
+ } catch (error) {
969
+ // All failed
970
+ for (const relay of relays) {
971
+ failed.push(relay);
972
+ formattedResults.push({
973
+ relay,
974
+ status: 'rejected',
975
+ reason: error.message,
976
+ });
977
+ }
978
+ }
684
979
 
685
980
  // Update outbox queue with delivery status
686
981
  if (this.outboxQueue) {
@@ -689,16 +984,12 @@ export class NostrClient {
689
984
 
690
985
  // Log failures only at debug level (common during network issues)
691
986
  if (failed.length > 0 && successful.length === 0) {
692
- // Only warn if ALL relays failed (likely a real issue)
693
- const reasons = results
987
+ const reasons = formattedResults
694
988
  .filter(r => r.status === 'rejected')
695
- .map(f => f.reason?.message || f.reason || 'unknown')
989
+ .map(f => f.reason || 'unknown')
696
990
  .join(', ');
697
- // Use debug level for timeout issues (very common)
698
- if (reasons.includes('timed out')) {
699
- // Silent - event is queued for retry anyway
700
- } else {
701
- console.warn(`[nostr] All relays failed for ${event.id.slice(0, 8)}: ${reasons}`);
991
+ if (!reasons.includes('timed out')) {
992
+ console.warn(`[nostr] All relays failed for ${event.id?.slice(0, 8)}: ${reasons}`);
702
993
  }
703
994
  }
704
995
 
@@ -732,7 +1023,7 @@ export class NostrClient {
732
1023
  const forceRelay = options.forceRelay === true;
733
1024
 
734
1025
  // If no relays, query from cache only
735
- if (this.relays.length === 0) {
1026
+ if (this.relays.length === 0 || !this.ndk) {
736
1027
  const matchingEvents = this._getMatchingCachedEvents(filter);
737
1028
  return matchingEvents;
738
1029
  }
@@ -812,7 +1103,17 @@ export class NostrClient {
812
1103
  // Create the query promise
813
1104
  const queryPromise = (async () => {
814
1105
  try {
815
- let events = await this.pool.querySync(this.relays, filter, { timeout });
1106
+ let events = [];
1107
+
1108
+ if (this.ndk) {
1109
+ // Use NDK to fetch events
1110
+ const fetchedEvents = await this.ndk.fetchEvents(filter, {
1111
+ closeOnEose: true,
1112
+ });
1113
+
1114
+ // Convert NDKEvent Set to raw event array
1115
+ events = Array.from(fetchedEvents).map(e => e.rawEvent());
1116
+ }
816
1117
 
817
1118
  // CRITICAL: Filter out events from other authors (relay may not respect filter)
818
1119
  if (filter.authors && filter.authors.length > 0) {
@@ -925,7 +1226,7 @@ export class NostrClient {
925
1226
  * @private
926
1227
  */
927
1228
  async _doBackgroundPathRefresh(path, kind, options) {
928
- if (this.relays.length === 0) return;
1229
+ if (this.relays.length === 0 || !this.ndk) return;
929
1230
 
930
1231
  // Throttle: Skip if we've refreshed this path recently
931
1232
  const lastRefresh = backgroundRefreshThrottle.get(path);
@@ -954,7 +1255,7 @@ export class NostrClient {
954
1255
  };
955
1256
  const cacheKey = JSON.stringify(filter);
956
1257
 
957
- // Use our query deduplication by calling query() instead of pool.querySync() directly
1258
+ // Use our query deduplication by calling query() instead of direct fetch
958
1259
  const timeout = options.timeout || 30000;
959
1260
  const events = await this._queryRelaysAndCache(filter, cacheKey, timeout);
960
1261
 
@@ -988,7 +1289,7 @@ export class NostrClient {
988
1289
  * @private
989
1290
  */
990
1291
  async _doBackgroundPrefixRefresh(prefix, kind, options) {
991
- if (this.relays.length === 0) return;
1292
+ if (this.relays.length === 0 || !this.ndk) return;
992
1293
 
993
1294
  // Throttle: Skip if we've refreshed this prefix recently
994
1295
  const throttleKey = `prefix:${prefix}`;
@@ -1047,14 +1348,17 @@ export class NostrClient {
1047
1348
  const localEvents = this._getMatchingCachedEvents(filter);
1048
1349
 
1049
1350
  // If no relays configured, return local only
1050
- if (this.relays.length === 0) {
1351
+ if (this.relays.length === 0 || !this.ndk) {
1051
1352
  return localEvents;
1052
1353
  }
1053
1354
 
1054
1355
  // Step 2: Query relays
1055
1356
  let relayEvents = [];
1056
1357
  try {
1057
- relayEvents = await this.pool.querySync(this.relays, filter, { timeout });
1358
+ const fetchedEvents = await this.ndk.fetchEvents(filter, {
1359
+ closeOnEose: true,
1360
+ });
1361
+ relayEvents = Array.from(fetchedEvents).map(e => e.rawEvent());
1058
1362
 
1059
1363
  // Filter out events from other authors
1060
1364
  if (filter.authors && filter.authors.length > 0) {
@@ -1142,7 +1446,7 @@ export class NostrClient {
1142
1446
  const filterKey = JSON.stringify(filter);
1143
1447
 
1144
1448
  // If no relays, check cache for matching events and trigger callbacks
1145
- if (this.relays.length === 0) {
1449
+ if (this.relays.length === 0 || !this.ndk) {
1146
1450
  // Trigger with cached events that match the filter
1147
1451
  const matchingEvents = this._getMatchingCachedEvents(filter);
1148
1452
 
@@ -1192,8 +1496,8 @@ export class NostrClient {
1192
1496
 
1193
1497
  // Only close actual subscription when no more callbacks
1194
1498
  if (existing.refCount === 0) {
1195
- if (existing.subscription && existing.subscription.close) {
1196
- existing.subscription.close();
1499
+ if (existing.subscription && existing.subscription.stop) {
1500
+ existing.subscription.stop();
1197
1501
  }
1198
1502
  activeSubscriptions.delete(filterKey);
1199
1503
  }
@@ -1213,39 +1517,41 @@ export class NostrClient {
1213
1517
  // Store before creating subscription to handle race conditions
1214
1518
  activeSubscriptions.set(filterKey, subscriptionInfo);
1215
1519
 
1216
- const sub = this.pool.subscribeMany(
1217
- this.relays,
1218
- [filter],
1219
- {
1220
- onevent: (event) => {
1221
- // CRITICAL: Filter out events from other authors immediately
1222
- // Many relays don't respect the authors filter, so we must do it client-side
1223
- if (filter.authors && filter.authors.length > 0) {
1224
- if (!filter.authors.includes(event.pubkey)) {
1225
- // Silently reject events from other authors
1226
- return;
1227
- }
1228
- }
1520
+ const sub = this.ndk.subscribe(filter, {
1521
+ closeOnEose: false,
1522
+ cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY,
1523
+ });
1229
1524
 
1230
- this._cacheEvent(event);
1525
+ sub.on('event', (ndkEvent) => {
1526
+ const event = ndkEvent.rawEvent();
1231
1527
 
1232
- // Dispatch to ALL registered callbacks for this subscription
1233
- const subInfo = activeSubscriptions.get(filterKey);
1234
- if (subInfo) {
1235
- for (const cb of subInfo.callbacks) {
1236
- try {
1237
- cb(event);
1238
- } catch (err) {
1239
- console.warn('[nostr] Subscription callback error:', err.message);
1240
- }
1241
- }
1528
+ // CRITICAL: Filter out events from other authors immediately
1529
+ // Many relays don't respect the authors filter, so we must do it client-side
1530
+ if (filter.authors && filter.authors.length > 0) {
1531
+ if (!filter.authors.includes(event.pubkey)) {
1532
+ // Silently reject events from other authors
1533
+ return;
1534
+ }
1535
+ }
1536
+
1537
+ this._cacheEvent(event);
1538
+
1539
+ // Dispatch to ALL registered callbacks for this subscription
1540
+ const subInfo = activeSubscriptions.get(filterKey);
1541
+ if (subInfo) {
1542
+ for (const cb of subInfo.callbacks) {
1543
+ try {
1544
+ cb(event);
1545
+ } catch (err) {
1546
+ console.warn('[nostr] Subscription callback error:', err.message);
1242
1547
  }
1243
- },
1244
- oneose: () => {
1245
- if (options.onEOSE) options.onEOSE();
1246
- },
1548
+ }
1247
1549
  }
1248
- );
1550
+ });
1551
+
1552
+ sub.on('eose', () => {
1553
+ if (options.onEOSE) options.onEOSE();
1554
+ });
1249
1555
 
1250
1556
  // Store the actual subscription object
1251
1557
  subscriptionInfo.subscription = sub;
@@ -1259,7 +1565,7 @@ export class NostrClient {
1259
1565
 
1260
1566
  // Only close actual subscription when no more callbacks
1261
1567
  if (subscriptionInfo.refCount === 0) {
1262
- if (sub.close) sub.close();
1568
+ if (sub.stop) sub.stop();
1263
1569
  activeSubscriptions.delete(filterKey);
1264
1570
  }
1265
1571
  this._subscriptions.delete(subId);
@@ -1379,7 +1685,7 @@ export class NostrClient {
1379
1685
  }
1380
1686
 
1381
1687
  // Trigger any active subscriptions that match this event
1382
- if (this.relays.length === 0) {
1688
+ if (this.relays.length === 0 || !this.ndk) {
1383
1689
  for (const sub of this._subscriptions.values()) {
1384
1690
  if (sub.active && this._eventMatchesFilter(event, sub.filter)) {
1385
1691
  // Trigger callback asynchronously, but check active status again
@@ -1559,16 +1865,16 @@ export class NostrClient {
1559
1865
  // Close long-lived author subscription
1560
1866
  const authorSub = authorSubscriptions.get(this.publicKey);
1561
1867
  if (authorSub && authorSub.subscription) {
1562
- if (authorSub.subscription.close) {
1563
- authorSub.subscription.close();
1868
+ if (authorSub.subscription.stop) {
1869
+ authorSub.subscription.stop();
1564
1870
  }
1565
1871
  authorSubscriptions.delete(this.publicKey);
1566
1872
  }
1567
1873
 
1568
1874
  // Close all subscriptions
1569
1875
  for (const sub of this._subscriptions.values()) {
1570
- if (sub.close) {
1571
- sub.close();
1876
+ if (sub.stop) {
1877
+ sub.stop();
1572
1878
  } else if (sub.active !== undefined) {
1573
1879
  // Mock subscription
1574
1880
  sub.active = false;
@@ -1576,8 +1882,8 @@ export class NostrClient {
1576
1882
  }
1577
1883
  this._subscriptions.clear();
1578
1884
 
1579
- // Close pool
1580
- this.pool.close(this.relays);
1885
+ // Note: We don't close the global NDK as it may be used by other clients
1886
+ // The pool connections will be cleaned up when the process exits
1581
1887
 
1582
1888
  // Clear cache and index
1583
1889
  this._eventCache.clear();
@@ -1589,10 +1895,20 @@ export class NostrClient {
1589
1895
  * @returns {Array} Relay connection statuses
1590
1896
  */
1591
1897
  getRelayStatus() {
1592
- return this.relays.map(relay => ({
1593
- url: relay,
1594
- connected: true, // SimplePool manages this internally
1595
- }));
1898
+ if (!this.ndk) {
1899
+ return this.relays.map(relay => ({
1900
+ url: relay,
1901
+ connected: false,
1902
+ }));
1903
+ }
1904
+
1905
+ return this.relays.map(relay => {
1906
+ const ndkRelay = this.ndk.pool.getRelay(relay);
1907
+ return {
1908
+ url: relay,
1909
+ connected: ndkRelay?.status === 1, // WebSocket.OPEN
1910
+ };
1911
+ });
1596
1912
  }
1597
1913
  }
1598
1914