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.
- package/dist/{2019-DQdDE6DG.js → 2019-CLMqIAfQ.js} +1722 -1668
- package/dist/{2019-DQdDE6DG.js.map → 2019-CLMqIAfQ.js.map} +1 -1
- package/dist/2019-Cp3uYhyY.cjs +8 -0
- package/dist/{2019-Ew-DTDlI.cjs.map → 2019-Cp3uYhyY.cjs.map} +1 -1
- package/dist/{browser-D2qtVhH5.cjs → browser-D6cNVl0v.cjs} +2 -2
- package/dist/{browser-D2qtVhH5.cjs.map → browser-D6cNVl0v.cjs.map} +1 -1
- package/dist/{browser-CckFyRI9.js → browser-nUQt1cnB.js} +2 -2
- package/dist/{browser-CckFyRI9.js.map → browser-nUQt1cnB.js.map} +1 -1
- package/dist/cjs/holosphere.cjs +1 -1
- package/dist/esm/holosphere.js +1 -1
- package/dist/{index-TDDyakLc.js → index-BN_uoxQK.js} +610 -128
- package/dist/index-BN_uoxQK.js.map +1 -0
- package/dist/{index-DrYM1LOY.js → index-CoAjtqsD.js} +2 -2
- package/dist/{index-DrYM1LOY.js.map → index-CoAjtqsD.js.map} +1 -1
- package/dist/{index-kyf1sjaC.cjs → index-Cp3tI53z.cjs} +2 -2
- package/dist/{index-kyf1sjaC.cjs.map → index-Cp3tI53z.cjs.map} +1 -1
- package/dist/{index-C0ITDyFo.cjs → index-DJjGSwXG.cjs} +2 -2
- package/dist/{index-C0ITDyFo.cjs.map → index-DJjGSwXG.cjs.map} +1 -1
- package/dist/index-V8EHMYEY.cjs +29 -0
- package/dist/index-V8EHMYEY.cjs.map +1 -0
- package/dist/{index-lbSQUoRz.js → index-Z5TstN1e.js} +2 -2
- package/dist/{index-lbSQUoRz.js.map → index-Z5TstN1e.js.map} +1 -1
- package/dist/indexeddb-storage-CZK5A7XH.cjs +2 -0
- package/dist/indexeddb-storage-CZK5A7XH.cjs.map +1 -0
- package/dist/{indexeddb-storage-CXhjqwhA.js → indexeddb-storage-bpA01pAU.js} +39 -2
- package/dist/indexeddb-storage-bpA01pAU.js.map +1 -0
- package/dist/{memory-storage-D1tc1bjk.cjs → memory-storage-B1k8Jszd.cjs} +2 -2
- package/dist/{memory-storage-D1tc1bjk.cjs.map → memory-storage-B1k8Jszd.cjs.map} +1 -1
- package/dist/{memory-storage-DkewsdcM.js → memory-storage-BqhmytP_.js} +2 -2
- package/dist/{memory-storage-DkewsdcM.js.map → memory-storage-BqhmytP_.js.map} +1 -1
- package/package.json +1 -1
- package/src/crypto/secp256k1.js +58 -9
- package/src/federation/card-storage.js +72 -0
- package/src/federation/handshake.js +363 -8
- package/src/federation/hologram.js +31 -3
- package/src/federation/holon-registry.js +22 -1
- package/src/federation/registry.js +54 -4
- package/src/index.js +52 -4
- package/src/storage/indexeddb-storage.js +41 -0
- package/src/storage/nostr-async.js +12 -3
- package/src/storage/nostr-client.js +112 -11
- package/src/storage/nostr-wrapper.js +5 -2
- package/dist/2019-Ew-DTDlI.cjs +0 -8
- package/dist/index-9sqetkAn.cjs +0 -29
- package/dist/index-9sqetkAn.cjs.map +0 -1
- package/dist/index-TDDyakLc.js.map +0 -1
- package/dist/indexeddb-storage-CXhjqwhA.js.map +0 -1
- package/dist/indexeddb-storage-DFESDYIj.cjs +0 -2
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
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
|
|
757
|
-
|
|
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} [
|
|
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,
|
|
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
|
|
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:
|
|
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(
|
|
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:
|
|
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
|
-
|
|
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
|
|
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');
|