holosphere 2.0.0-alpha12 → 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 (49) hide show
  1. package/dist/{2019-DQdDE6DG.js → 2019-CLMqIAfQ.js} +1722 -1668
  2. package/dist/{2019-DQdDE6DG.js.map → 2019-CLMqIAfQ.js.map} +1 -1
  3. package/dist/2019-Cp3uYhyY.cjs +8 -0
  4. package/dist/{2019-Ew-DTDlI.cjs.map → 2019-Cp3uYhyY.cjs.map} +1 -1
  5. package/dist/{browser-D2qtVhH5.cjs → browser-D6cNVl0v.cjs} +2 -2
  6. package/dist/{browser-D2qtVhH5.cjs.map → browser-D6cNVl0v.cjs.map} +1 -1
  7. package/dist/{browser-CckFyRI9.js → browser-nUQt1cnB.js} +2 -2
  8. package/dist/{browser-CckFyRI9.js.map → browser-nUQt1cnB.js.map} +1 -1
  9. package/dist/cjs/holosphere.cjs +1 -1
  10. package/dist/esm/holosphere.js +1 -1
  11. package/dist/{index-TDDyakLc.js → index-BN_uoxQK.js} +610 -128
  12. package/dist/index-BN_uoxQK.js.map +1 -0
  13. package/dist/{index-DrYM1LOY.js → index-CoAjtqsD.js} +2 -2
  14. package/dist/{index-DrYM1LOY.js.map → index-CoAjtqsD.js.map} +1 -1
  15. package/dist/{index-kyf1sjaC.cjs → index-Cp3tI53z.cjs} +2 -2
  16. package/dist/{index-kyf1sjaC.cjs.map → index-Cp3tI53z.cjs.map} +1 -1
  17. package/dist/{index-C0ITDyFo.cjs → index-DJjGSwXG.cjs} +2 -2
  18. package/dist/{index-C0ITDyFo.cjs.map → index-DJjGSwXG.cjs.map} +1 -1
  19. package/dist/index-V8EHMYEY.cjs +29 -0
  20. package/dist/index-V8EHMYEY.cjs.map +1 -0
  21. package/dist/{index-lbSQUoRz.js → index-Z5TstN1e.js} +2 -2
  22. package/dist/{index-lbSQUoRz.js.map → index-Z5TstN1e.js.map} +1 -1
  23. package/dist/indexeddb-storage-CZK5A7XH.cjs +2 -0
  24. package/dist/indexeddb-storage-CZK5A7XH.cjs.map +1 -0
  25. package/dist/{indexeddb-storage-CXhjqwhA.js → indexeddb-storage-bpA01pAU.js} +39 -2
  26. package/dist/indexeddb-storage-bpA01pAU.js.map +1 -0
  27. package/dist/{memory-storage-D1tc1bjk.cjs → memory-storage-B1k8Jszd.cjs} +2 -2
  28. package/dist/{memory-storage-D1tc1bjk.cjs.map → memory-storage-B1k8Jszd.cjs.map} +1 -1
  29. package/dist/{memory-storage-DkewsdcM.js → memory-storage-BqhmytP_.js} +2 -2
  30. package/dist/{memory-storage-DkewsdcM.js.map → memory-storage-BqhmytP_.js.map} +1 -1
  31. package/package.json +1 -1
  32. package/src/crypto/secp256k1.js +58 -9
  33. package/src/federation/card-storage.js +72 -0
  34. package/src/federation/handshake.js +363 -8
  35. package/src/federation/hologram.js +31 -3
  36. package/src/federation/holon-registry.js +22 -1
  37. package/src/federation/registry.js +54 -4
  38. package/src/index.js +52 -4
  39. package/src/storage/indexeddb-storage.js +41 -0
  40. package/src/storage/nostr-async.js +12 -3
  41. package/src/storage/nostr-client.js +112 -11
  42. package/src/storage/nostr-wrapper.js +5 -2
  43. package/dist/2019-Ew-DTDlI.cjs +0 -8
  44. package/dist/index-9sqetkAn.cjs +0 -29
  45. package/dist/index-9sqetkAn.cjs.map +0 -1
  46. package/dist/index-TDDyakLc.js.map +0 -1
  47. package/dist/indexeddb-storage-CXhjqwhA.js.map +0 -1
  48. package/dist/indexeddb-storage-DFESDYIj.cjs +0 -2
  49. package/dist/indexeddb-storage-DFESDYIj.cjs.map +0 -1
@@ -147,22 +147,61 @@ export async function getFederatedPartner(client, appname, partnerPubKey) {
147
147
  */
148
148
  export async function getCapabilityForAuthor(client, appname, authorPubKey, scope) {
149
149
  const registry = await getFederationRegistry(client, appname);
150
+
151
+ console.log('[Registry] 🔍 getCapabilityForAuthor called:', {
152
+ authorPubKey: authorPubKey?.slice(0, 12) + '...',
153
+ scope,
154
+ federatedWithCount: registry.federatedWith?.length || 0,
155
+ federatedPartners: registry.federatedWith?.map(p => p.pubKey?.slice(0, 12) + '...') || []
156
+ });
157
+
150
158
  const partner = registry.federatedWith.find(p => p.pubKey === authorPubKey);
151
159
 
152
- if (!partner || !partner.inboundCapabilities) {
160
+ if (!partner) {
161
+ console.log('[Registry] ❌ Partner not found in federatedWith');
162
+ return null;
163
+ }
164
+
165
+ if (!partner.inboundCapabilities || partner.inboundCapabilities.length === 0) {
166
+ console.log('[Registry] ❌ Partner has no inboundCapabilities:', {
167
+ partnerPubKey: authorPubKey?.slice(0, 12) + '...',
168
+ hasInboundCaps: !!partner.inboundCapabilities,
169
+ inboundCapsLength: partner.inboundCapabilities?.length || 0
170
+ });
153
171
  return null;
154
172
  }
155
173
 
174
+ console.log('[Registry] Partner found with inboundCapabilities:', {
175
+ partnerPubKey: authorPubKey?.slice(0, 12) + '...',
176
+ capsCount: partner.inboundCapabilities.length,
177
+ capScopes: partner.inboundCapabilities.map(c => c.scope)
178
+ });
179
+
156
180
  // Find matching, non-expired capability
157
- return partner.inboundCapabilities.find(cap => {
181
+ const result = partner.inboundCapabilities.find(cap => {
158
182
  // Check expiration
159
183
  if (cap.expires && cap.expires < Date.now()) {
184
+ console.log('[Registry] Capability expired:', { expires: cap.expires, now: Date.now() });
160
185
  return false;
161
186
  }
162
187
 
163
188
  // Check scope match (supports wildcards)
164
- return matchScope(cap.scope, scope);
189
+ const matches = matchScope(cap.scope, scope);
190
+ console.log('[Registry] Scope match check:', {
191
+ capScope: cap.scope,
192
+ requestedScope: scope,
193
+ matches
194
+ });
195
+ return matches;
165
196
  }) || null;
197
+
198
+ if (result) {
199
+ console.log('[Registry] ✅ Matching capability found');
200
+ } else {
201
+ console.log('[Registry] ❌ No matching capability found');
202
+ }
203
+
204
+ return result;
166
205
  }
167
206
 
168
207
  /**
@@ -178,10 +217,17 @@ export async function getCapabilityForAuthor(client, appname, authorPubKey, scop
178
217
  * @returns {Promise<boolean>} Success indicator
179
218
  */
180
219
  export async function storeInboundCapability(client, appname, partnerPubKey, capabilityInfo) {
220
+ console.log('[Registry] 📥 storeInboundCapability called:', {
221
+ partnerPubKey: partnerPubKey?.slice(0, 12) + '...',
222
+ scope: capabilityInfo?.scope,
223
+ hasToken: !!capabilityInfo?.token
224
+ });
225
+
181
226
  const registry = await getFederationRegistry(client, appname);
182
227
  const partner = registry.federatedWith.find(p => p.pubKey === partnerPubKey);
183
228
 
184
229
  if (!partner) {
230
+ console.log('[Registry] Partner not in registry, auto-adding with capability');
185
231
  // Auto-add partner if not exists
186
232
  return addFederatedPartner(client, appname, partnerPubKey, {
187
233
  addedVia: 'capability_received',
@@ -199,12 +245,14 @@ export async function storeInboundCapability(client, appname, partnerPubKey, cap
199
245
  );
200
246
 
201
247
  if (existingIndex >= 0) {
248
+ console.log('[Registry] Updating existing capability at index:', existingIndex);
202
249
  // Update existing capability
203
250
  partner.inboundCapabilities[existingIndex] = {
204
251
  ...capabilityInfo,
205
252
  updatedAt: Date.now()
206
253
  };
207
254
  } else {
255
+ console.log('[Registry] Adding new capability, total will be:', partner.inboundCapabilities.length + 1);
208
256
  // Add new capability
209
257
  partner.inboundCapabilities.push({
210
258
  ...capabilityInfo,
@@ -212,7 +260,9 @@ export async function storeInboundCapability(client, appname, partnerPubKey, cap
212
260
  });
213
261
  }
214
262
 
215
- return saveFederationRegistry(client, appname, registry);
263
+ const result = await saveFederationRegistry(client, appname, registry);
264
+ console.log('[Registry] ✅ Capability stored, registry saved:', result);
265
+ return result;
216
266
  }
217
267
 
218
268
  /**
package/src/index.js CHANGED
@@ -321,6 +321,7 @@ class HoloSphereBase extends HoloSphereCore {
321
321
  * @param {boolean} [options.autoPropagate=true] - Whether to propagate to federated holons
322
322
  * @param {Object} [options.propagationOptions] - Options for propagation
323
323
  * @param {boolean} [options.blocking=false] - If true, wait for relay confirmation before returning
324
+ * @param {string} [options.signingKey] - Private key to sign with (hex format). If not provided, uses holosphere's default key.
324
325
  * @returns {Promise<boolean>} True if write succeeded (or queued for optimistic writes)
325
326
  * @throws {ValidationError} If holonId, lensName, or data is invalid
326
327
  * @throws {AuthorizationError} If capability token is invalid
@@ -427,7 +428,8 @@ class HoloSphereBase extends HoloSphereCore {
427
428
  }
428
429
 
429
430
  // Regular write to relay
430
- await storage.write(this.client, path, data);
431
+ const writeOptions = options.signingKey ? { signingKey: options.signingKey } : {};
432
+ await storage.write(this.client, path, data, writeOptions);
431
433
 
432
434
  const endTime = Date.now();
433
435
  const duration = endTime - startTime;
@@ -688,6 +690,34 @@ class HoloSphereBase extends HoloSphereCore {
688
690
  throw new ValidationError('ValidationError: lensName must be a non-empty string');
689
691
  }
690
692
 
693
+ // OPTIMIZATION: Check local caches first before any network/registry operations
694
+ // This ensures data we just wrote can be read back immediately (important for capability-based writes)
695
+ if (dataId) {
696
+ const earlyPath = storage.buildPath(this.config.appName, holonId, lensName, dataId);
697
+
698
+ // Check delete cache first - if deleted, return null immediately
699
+ if (this._deleteCache.has(earlyPath)) {
700
+ this._log('DEBUG', '🗑️ EARLY CACHE: Deleted item', { path: earlyPath });
701
+ return null;
702
+ }
703
+
704
+ // Check write cache - if this instance wrote data, return it immediately
705
+ const cached = this._writeCache.get(earlyPath);
706
+ if (cached) {
707
+ const cacheAge = Date.now() - cached.timestamp;
708
+ this._log('DEBUG', '⚡ EARLY CACHE HIT: Write cache', {
709
+ path: earlyPath,
710
+ cacheAge: `${cacheAge}ms`
711
+ });
712
+ // Still resolve holograms if needed
713
+ const { resolveHolograms = true } = options;
714
+ if (resolveHolograms && cached.data) {
715
+ return this._resolveHolograms(cached.data);
716
+ }
717
+ return cached.data;
718
+ }
719
+ }
720
+
691
721
  // Resolve holonId to public key (if registered)
692
722
  const targetPubkey = await this._resolveHolonToPubkey(holonId);
693
723
 
@@ -709,11 +739,27 @@ class HoloSphereBase extends HoloSphereCore {
709
739
  }
710
740
  } else if (isOtherAuthor) {
711
741
  // Auto-check capability for other author's data
742
+ this._log('DEBUG', '🔍 Looking up capability for federated author', {
743
+ holonId,
744
+ lensName,
745
+ dataId: dataId || '*',
746
+ targetPubkey: targetPubkey?.slice(0, 12) + '...',
747
+ myPubkey: this.client?.publicKey?.slice(0, 12) + '...'
748
+ });
712
749
  const capability = await this._getCapabilityForAuthor(targetPubkey, { holonId, lensName, dataId });
713
750
  if (!capability) {
714
- this._log('DEBUG', 'No capability for author', { holonId, targetPubkey });
751
+ this._log('WARN', 'No capability found for federated author - returning empty', {
752
+ holonId,
753
+ lensName,
754
+ targetPubkey: targetPubkey?.slice(0, 12) + '...'
755
+ });
715
756
  return dataId ? null : [];
716
757
  }
758
+ this._log('DEBUG', '✅ Capability found for federated author', {
759
+ holonId,
760
+ lensName,
761
+ targetPubkey: targetPubkey?.slice(0, 12) + '...'
762
+ });
717
763
  readOptions.authors = [targetPubkey];
718
764
  }
719
765
 
@@ -753,8 +799,10 @@ class HoloSphereBase extends HoloSphereCore {
753
799
  });
754
800
  result = null;
755
801
  } else {
756
- // Check write cache for optimistic reads (only for own data)
757
- const cached = !isOtherAuthor ? this._writeCache.get(path) : null;
802
+ // Check write cache for optimistic reads
803
+ // Always check write cache first - if this instance wrote data, it should be able to read it back
804
+ // regardless of holon ownership (important for capability-based writes)
805
+ const cached = this._writeCache.get(path);
758
806
  if (cached) {
759
807
  const cacheAge = Date.now() - cached.timestamp;
760
808
  this._log('DEBUG', '⚡ CACHE HIT: Write cache', {
@@ -39,6 +39,7 @@ export class IndexedDBStorage extends PersistentStorage {
39
39
  * Initialize storage with namespace.
40
40
  *
41
41
  * Creates or opens the IndexedDB database and object store.
42
+ * Handles version conflicts by deleting and recreating the database if needed.
42
43
  *
43
44
  * @param {string} namespace - Storage namespace
44
45
  * @returns {Promise<void>}
@@ -46,6 +47,27 @@ export class IndexedDBStorage extends PersistentStorage {
46
47
  async init(namespace) {
47
48
  this.dbName = `holosphere_${namespace}`;
48
49
 
50
+ try {
51
+ await this._openDatabase();
52
+ } catch (error) {
53
+ // Handle version conflict by deleting and recreating the database
54
+ if (error.name === 'VersionError') {
55
+ console.warn(`[IndexedDBStorage] Version conflict detected for ${this.dbName}, recreating database...`);
56
+ await this._deleteDatabase();
57
+ await this._openDatabase();
58
+ } else {
59
+ throw error;
60
+ }
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Open the IndexedDB database.
66
+ *
67
+ * @returns {Promise<void>}
68
+ * @private
69
+ */
70
+ async _openDatabase() {
49
71
  return new Promise((resolve, reject) => {
50
72
  const request = indexedDB.open(this.dbName, 1);
51
73
 
@@ -69,6 +91,25 @@ export class IndexedDBStorage extends PersistentStorage {
69
91
  });
70
92
  }
71
93
 
94
+ /**
95
+ * Delete the IndexedDB database.
96
+ *
97
+ * @returns {Promise<void>}
98
+ * @private
99
+ */
100
+ async _deleteDatabase() {
101
+ return new Promise((resolve, reject) => {
102
+ const request = indexedDB.deleteDatabase(this.dbName);
103
+ request.onsuccess = () => resolve(undefined);
104
+ request.onerror = () => reject(request.error);
105
+ request.onblocked = () => {
106
+ console.warn(`[IndexedDBStorage] Database deletion blocked for ${this.dbName}`);
107
+ // Still resolve - the next open attempt will handle it
108
+ resolve(undefined);
109
+ };
110
+ });
111
+ }
112
+
72
113
  /**
73
114
  * Store an event.
74
115
  *
@@ -42,13 +42,21 @@ const QUERY_DEDUP_WINDOW = 2000;
42
42
  * @param {Object} client - NostrClient instance
43
43
  * @param {string} path - Path identifier (encoded in d-tag)
44
44
  * @param {Object} data - Data to store
45
- * @param {number} [kind=30000] - Event kind (default: 30000 for parameterized replaceable)
45
+ * @param {number|Object} [kindOrOptions=30000] - Event kind (number) or options object
46
+ * @param {number} [kindOrOptions.kind=30000] - Event kind
47
+ * @param {string} [kindOrOptions.signingKey] - Private key to sign with (hex format)
46
48
  * @returns {Promise<Object>} Published event result with relay responses
47
49
  * @example
48
50
  * const result = await nostrPut(client, 'myapp/holon123/items/item1', { name: 'Test' });
49
51
  * console.log(result.event.id); // Event ID
50
52
  */
51
- export async function nostrPut(client, path, data, kind = 30000) {
53
+ export async function nostrPut(client, path, data, kindOrOptions = 30000) {
54
+ // Support both old signature (kind as number) and new signature (options object)
55
+ const options = typeof kindOrOptions === 'number'
56
+ ? { kind: kindOrOptions }
57
+ : kindOrOptions;
58
+ const kind = options.kind || 30000;
59
+
52
60
  const dataEvent = {
53
61
  kind,
54
62
  created_at: Math.floor(Date.now() / 1000),
@@ -58,7 +66,8 @@ export async function nostrPut(client, path, data, kind = 30000) {
58
66
  content: JSON.stringify(data),
59
67
  };
60
68
 
61
- const result = await client.publish(dataEvent);
69
+ const publishOptions = options.signingKey ? { signingKey: options.signingKey } : {};
70
+ const result = await client.publish(dataEvent, publishOptions);
62
71
  return result;
63
72
  }
64
73
 
@@ -369,14 +369,27 @@ export class NostrClient {
369
369
  let cacheAdapter = null;
370
370
 
371
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
+
372
379
  try {
373
380
  // Create NDK cache adapter with app-specific database name
374
- cacheAdapter = new CacheAdapter({
375
- dbName: `holosphere_${this.config.appName}`,
376
- });
381
+ cacheAdapter = new CacheAdapter({ dbName });
377
382
  this._useNDKCache = true;
378
383
  } catch (e) {
379
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
+ }
380
393
  }
381
394
  }
382
395
 
@@ -529,6 +542,83 @@ export class NostrClient {
529
542
  }
530
543
  }
531
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
+
532
622
  /**
533
623
  * Load events from persistent storage into cache
534
624
  * @private
@@ -645,6 +735,7 @@ export class NostrClient {
645
735
  * @param {Object} [options={}] - Publish options
646
736
  * @param {boolean} [options.waitForRelays=false] - Wait for relay confirmation
647
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.
648
739
  * @returns {Promise<Object>} Signed event with relay publish results
649
740
  * @example
650
741
  * const result = await client.publish({
@@ -707,7 +798,7 @@ export class NostrClient {
707
798
  pendingWrites.set(dTagPath, { event, timer, resolve, reject });
708
799
 
709
800
  // Cache immediately for local-first reads (even before relay publish)
710
- const signedEvent = this._createSignedEvent(event);
801
+ const signedEvent = this._createSignedEvent(event, options.signingKey);
711
802
  this._cacheEvent(signedEvent);
712
803
  });
713
804
  }
@@ -716,9 +807,13 @@ export class NostrClient {
716
807
  * Create a signed event using NDK or nostr-tools fallback.
717
808
  * @private
718
809
  * @param {Object} event - Unsigned event
810
+ * @param {string} [signingKey] - Optional private key to sign with (hex format)
719
811
  * @returns {Object} Signed event
720
812
  */
721
- _createSignedEvent(event) {
813
+ _createSignedEvent(event, signingKey = null) {
814
+ const keyToUse = signingKey || this.privateKey;
815
+ const pubkeyToUse = signingKey ? nostrToolsGetPublicKey(hexToBytes(signingKey)) : this.publicKey;
816
+
722
817
  // If NDK is available and we have a signer, use NDKEvent
723
818
  if (this.ndk) {
724
819
  const ndkEvent = new NDKEvent(this.ndk);
@@ -733,7 +828,7 @@ export class NostrClient {
733
828
  content: ndkEvent.content,
734
829
  tags: ndkEvent.tags,
735
830
  created_at: ndkEvent.created_at,
736
- pubkey: this.publicKey,
831
+ pubkey: pubkeyToUse,
737
832
  id: '', // Will be set during actual publish
738
833
  sig: '', // Will be set during actual publish
739
834
  };
@@ -741,12 +836,12 @@ export class NostrClient {
741
836
 
742
837
  // Mock mode - use nostr-tools to create a properly signed event
743
838
  try {
744
- return nostrToolsFinalizeEvent(event, hexToBytes(this.privateKey));
839
+ return nostrToolsFinalizeEvent(event, hexToBytes(keyToUse));
745
840
  } catch (e) {
746
841
  // Return mock signed event if signing fails
747
842
  return {
748
843
  ...event,
749
- pubkey: this.publicKey,
844
+ pubkey: pubkeyToUse,
750
845
  id: Math.random().toString(16).slice(2).padStart(64, '0'),
751
846
  sig: 'mock-signature',
752
847
  created_at: event.created_at || Math.floor(Date.now() / 1000),
@@ -760,6 +855,7 @@ export class NostrClient {
760
855
  */
761
856
  async _doPublish(event, options = {}) {
762
857
  const waitForRelays = options.waitForRelays || false;
858
+ const signingKey = options.signingKey || null;
763
859
 
764
860
  // Check if event is already signed (has id and sig)
765
861
  let signedEvent;
@@ -773,12 +869,17 @@ export class NostrClient {
773
869
  ndkEvent.tags = event.tags || [];
774
870
  ndkEvent.created_at = event.created_at || Math.floor(Date.now() / 1000);
775
871
 
776
- // Sign the event
777
- await ndkEvent.sign(this.signer);
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
+ }
778
879
  signedEvent = ndkEvent.rawEvent();
779
880
  } else {
780
881
  // Mock mode - create mock signed event
781
- signedEvent = this._createSignedEvent(event);
882
+ signedEvent = this._createSignedEvent(event, signingKey);
782
883
  }
783
884
 
784
885
  // 1. Cache the event locally first (this makes reads instant)
@@ -53,11 +53,14 @@ function encodePathComponent(component) {
53
53
  * @param {Object} client - NostrClient instance
54
54
  * @param {string} path - Path
55
55
  * @param {Object} data - Data to write
56
+ * @param {Object} [options={}] - Write options
57
+ * @param {string} [options.signingKey] - Private key to sign with (hex format)
56
58
  * @returns {Promise<boolean>} Success indicator
57
59
  */
58
- export async function write(client, path, data) {
60
+ export async function write(client, path, data, options = {}) {
59
61
  try {
60
- const result = await nostrPut(client, path, data);
62
+ const putOptions = options.signingKey ? { signingKey: options.signingKey } : {};
63
+ const result = await nostrPut(client, path, data, putOptions);
61
64
  // Check if at least one relay accepted the event
62
65
  // If no relays (testing mode), consider it successful if event was created
63
66
  const success = result.results.length === 0 ? true : result.results.some(r => r.status === 'fulfilled');