holosphere 2.0.0-alpha7 → 2.0.0-alpha8

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 (40) hide show
  1. package/dist/cjs/holosphere.cjs +1 -1
  2. package/dist/esm/holosphere.js +1 -1
  3. package/dist/{index-d6f4RJBM.js → index-4XHHKe6S.js} +356 -58
  4. package/dist/index-4XHHKe6S.js.map +1 -0
  5. package/dist/{index-jmTHEbR2.js → index-BjP1TXGz.js} +2 -2
  6. package/dist/{index-jmTHEbR2.js.map → index-BjP1TXGz.js.map} +1 -1
  7. package/dist/{index-C-IlLYlk.cjs → index-CKffQDmQ.cjs} +2 -2
  8. package/dist/{index-C-IlLYlk.cjs.map → index-CKffQDmQ.cjs.map} +1 -1
  9. package/dist/index-Dz5kOZMI.cjs +5 -0
  10. package/dist/index-Dz5kOZMI.cjs.map +1 -0
  11. package/dist/{indexeddb-storage-a8GipaDr.cjs → indexeddb-storage-DD7EFBVc.cjs} +2 -2
  12. package/dist/{indexeddb-storage-a8GipaDr.cjs.map → indexeddb-storage-DD7EFBVc.cjs.map} +1 -1
  13. package/dist/{indexeddb-storage-D8kOl0oK.js → indexeddb-storage-lExjjFlV.js} +2 -2
  14. package/dist/{indexeddb-storage-D8kOl0oK.js.map → indexeddb-storage-lExjjFlV.js.map} +1 -1
  15. package/dist/{memory-storage-DBQK622V.js → memory-storage-C68adso2.js} +2 -2
  16. package/dist/{memory-storage-DBQK622V.js.map → memory-storage-C68adso2.js.map} +1 -1
  17. package/dist/{memory-storage-gfRovk2O.cjs → memory-storage-DD_6yyXT.cjs} +2 -2
  18. package/dist/{memory-storage-gfRovk2O.cjs.map → memory-storage-DD_6yyXT.cjs.map} +1 -1
  19. package/dist/{secp256k1-BCAPF45D.cjs → secp256k1-DYELiqgx.cjs} +2 -2
  20. package/dist/{secp256k1-BCAPF45D.cjs.map → secp256k1-DYELiqgx.cjs.map} +1 -1
  21. package/dist/{secp256k1-DYm_CMqW.js → secp256k1-OM8siPyy.js} +2 -2
  22. package/dist/{secp256k1-DYm_CMqW.js.map → secp256k1-OM8siPyy.js.map} +1 -1
  23. package/examples/holosphere-widget.js +1242 -0
  24. package/examples/widget-demo.html +274 -0
  25. package/examples/widget.html +703 -0
  26. package/package.json +3 -1
  27. package/src/cdn-entry.js +22 -0
  28. package/src/contracts/queries.js +16 -1
  29. package/src/core/holosphere.js +2 -2
  30. package/src/crypto/nostr-utils.js +36 -2
  31. package/src/federation/handshake.js +16 -4
  32. package/src/index.js +16 -2
  33. package/src/storage/backends/gundb-backend.js +293 -9
  34. package/src/storage/gun-wrapper.js +64 -16
  35. package/src/storage/nostr-async.js +40 -25
  36. package/src/storage/unified-storage.js +31 -1
  37. package/vite.config.cdn.js +60 -0
  38. package/dist/index-Bvwyvd0T.cjs +0 -5
  39. package/dist/index-Bvwyvd0T.cjs.map +0 -1
  40. package/dist/index-d6f4RJBM.js.map +0 -1
@@ -54,15 +54,32 @@ function encodePathComponent(component) {
54
54
  return encodeURIComponent(component).replace(/%2F/g, '/');
55
55
  }
56
56
 
57
+ /**
58
+ * Navigate to a Gun path using chained .get() calls
59
+ * Gun treats 'a/b/c' as a literal key, not a path.
60
+ * This function splits the path and chains .get() calls properly.
61
+ * @private
62
+ * @param {Object} gun - Gun instance
63
+ * @param {string} path - Path string like "appname/holon/lens/key"
64
+ * @returns {Object} Gun chain reference at the path
65
+ */
66
+ function getGunPath(gun, path) {
67
+ const parts = path.split('/').filter(p => p.length > 0);
68
+ let ref = gun;
69
+ for (const part of parts) {
70
+ ref = ref.get(part);
71
+ }
72
+ return ref;
73
+ }
74
+
57
75
  /**
58
76
  * Serialize data for GunDB storage
59
- * Wraps data in { _json: string } format for Gun compatibility
60
- * Gun requires an object at graph roots, so we can't store raw JSON strings
61
- * The deserialization handles both this format and raw JSON strings for reading old data
77
+ * Stores data as raw JSON string for compatibility with holosphere original
78
+ * This matches the format used in holosphere v1 for better interoperability
62
79
  * @private
63
80
  */
64
81
  function serializeForGun(data) {
65
- return { _json: JSON.stringify(data) };
82
+ return JSON.stringify(data);
66
83
  }
67
84
 
68
85
  /**
@@ -152,11 +169,21 @@ function deserializeFromGun(data) {
152
169
  export async function write(gun, path, data) {
153
170
  try {
154
171
  const serialized = serializeForGun(data);
155
- await gunPut(gun.get(path), serialized, 2000);
156
- // Delay to allow Gun to propagate the write (50ms for better reliability)
157
- await new Promise(resolve => setTimeout(resolve, 50));
158
- return true;
172
+ const parts = path.split('/').filter(p => p.length > 0);
173
+ console.log('[gun-wrapper] write:', { path, parts, dataId: data?.id });
174
+ const ref = getGunPath(gun, path);
175
+ console.log('[gun-wrapper] write ref soul:', ref?._.get);
176
+ const ack = await gunPut(ref, serialized, 5000); // Increased timeout from 2s to 5s
177
+ console.log('[gun-wrapper] write ack:', { ok: ack.ok, timeout: ack.timeout });
178
+ if (ack.timeout) {
179
+ console.warn('[gun-wrapper] write timed out (data may not be persisted):', path);
180
+ }
181
+ console.log('[gun-wrapper] write complete:', path);
182
+
183
+ // Return ack info so caller can handle timeouts
184
+ return { ok: true, timeout: ack.timeout || false };
159
185
  } catch (error) {
186
+ console.error('[gun-wrapper] write error:', error);
160
187
  throw error;
161
188
  }
162
189
  }
@@ -168,7 +195,12 @@ export async function write(gun, path, data) {
168
195
  * @returns {Promise<Object|null>} Data or null if not found
169
196
  */
170
197
  export async function read(gun, path) {
171
- const rawData = await gunPromise(gun.get(path), 2000);
198
+ const parts = path.split('/').filter(p => p.length > 0);
199
+ console.log('[gun-wrapper] read:', { path, parts });
200
+ const ref = getGunPath(gun, path);
201
+ console.log('[gun-wrapper] read ref soul:', ref?._.get);
202
+ const rawData = await gunPromise(ref, 2000);
203
+ console.log('[gun-wrapper] read rawData:', rawData ? (typeof rawData === 'string' ? rawData.substring(0, 100) : 'object') : 'null');
172
204
 
173
205
  if (!rawData) {
174
206
  return null;
@@ -193,18 +225,23 @@ export async function read(gun, path) {
193
225
  * @returns {Promise<Object[]>} Array of data objects
194
226
  */
195
227
  export async function readAll(gun, path, timeout = 5000) {
228
+ const parts = path.split('/').filter(p => p.length > 0);
229
+ console.log('[gun-wrapper] readAll:', { path, parts });
230
+
196
231
  return new Promise((resolve) => {
197
232
  const output = new Map();
198
233
  let settled = false;
199
234
  let expectedCount = 0;
200
235
  let receivedCount = 0;
201
236
 
202
- const ref = gun.get(path);
237
+ const ref = getGunPath(gun, path);
238
+ console.log('[gun-wrapper] readAll ref soul:', ref?._.get);
203
239
 
204
240
  const tryResolve = () => {
205
241
  if (settled) return;
206
242
  if (expectedCount > 0 && receivedCount >= expectedCount) {
207
243
  settled = true;
244
+ console.log('[gun-wrapper] readAll resolved with', output.size, 'items');
208
245
  resolve(Array.from(output.values()));
209
246
  }
210
247
  };
@@ -216,18 +253,24 @@ export async function readAll(gun, path, timeout = 5000) {
216
253
  // Step 1: Get the parent data to count expected items
217
254
  ref.once((parentData) => {
218
255
  if (settled) return;
256
+ console.log('[gun-wrapper] readAll parentData:', parentData);
257
+ console.log('[gun-wrapper] readAll parentData keys:', parentData ? Object.keys(parentData).filter(k => k !== '_') : 'null');
258
+ console.log('[gun-wrapper] readAll parentData type:', typeof parentData);
219
259
 
220
260
  if (!parentData) {
221
261
  settled = true;
262
+ console.log('[gun-wrapper] readAll: no parent data, returning empty');
222
263
  resolve([]);
223
264
  return;
224
265
  }
225
266
 
226
267
  // Get all keys except Gun metadata
227
268
  const keys = Object.keys(parentData).filter(k => k !== '_');
269
+ console.log('[gun-wrapper] readAll keys:', keys);
228
270
 
229
271
  if (keys.length === 0) {
230
272
  settled = true;
273
+ console.log('[gun-wrapper] readAll: no keys, returning empty');
231
274
  resolve([]);
232
275
  return;
233
276
  }
@@ -298,7 +341,7 @@ export async function readAll(gun, path, timeout = 5000) {
298
341
  * @returns {Promise<boolean>} Success indicator
299
342
  */
300
343
  export async function update(gun, path, updates) {
301
- const rawData = await gunPromise(gun.get(path));
344
+ const rawData = await gunPromise(getGunPath(gun, path));
302
345
 
303
346
  if (!rawData) {
304
347
  return false; // Not found
@@ -315,7 +358,9 @@ export async function update(gun, path, updates) {
315
358
 
316
359
  try {
317
360
  const serialized = serializeForGun(merged);
318
- await gunPut(gun.get(path), serialized);
361
+ await gunPut(getGunPath(gun, path), serialized, 2000);
362
+ // Add delay for propagation
363
+ await new Promise(resolve => setTimeout(resolve, 200));
319
364
  return true;
320
365
  } catch (error) {
321
366
  throw error;
@@ -331,7 +376,7 @@ export async function update(gun, path, updates) {
331
376
  export async function deleteData(gun, path) {
332
377
  try {
333
378
  // First read existing data to get the id
334
- const rawData = await gunPromise(gun.get(path));
379
+ const rawData = await gunPromise(getGunPath(gun, path));
335
380
  if (!rawData) {
336
381
  return true; // Already deleted/doesn't exist
337
382
  }
@@ -345,7 +390,9 @@ export async function deleteData(gun, path) {
345
390
  _deletedAt: Date.now()
346
391
  };
347
392
 
348
- await gunPut(gun.get(path), serializeForGun(tombstone));
393
+ await gunPut(getGunPath(gun, path), serializeForGun(tombstone), 2000);
394
+ // Add delay for propagation
395
+ await new Promise(resolve => setTimeout(resolve, 200));
349
396
  return true;
350
397
  } catch (error) {
351
398
  throw error;
@@ -389,7 +436,7 @@ export function subscribe(gun, path, callback, options = {}) {
389
436
 
390
437
  if (isPrefix) {
391
438
  // Subscribe to all items under this prefix
392
- const ref = gun.get(path);
439
+ const ref = getGunPath(gun, path);
393
440
 
394
441
  ref.map().on((data, key) => {
395
442
  if (data && !key.startsWith('_')) {
@@ -411,7 +458,7 @@ export function subscribe(gun, path, callback, options = {}) {
411
458
  };
412
459
  } else {
413
460
  // Subscribe to single item
414
- const listener = gun.get(path).on((data, key) => {
461
+ const listener = getGunPath(gun, path).on((data, key) => {
415
462
  if (data) {
416
463
  const deserialized = deserializeFromGun(data);
417
464
  if (deserialized && !deserialized._deleted) {
@@ -450,6 +497,7 @@ export async function writeGlobal(gun, appname, tableName, data) {
450
497
  throw new Error('writeGlobal: data must have an id field');
451
498
  }
452
499
  const path = buildGlobalPath(appname, tableName, data.id);
500
+ // Use write function which includes the propagation delay
453
501
  return write(gun, path, data);
454
502
  }
455
503
 
@@ -65,28 +65,33 @@ export async function nostrGet(client, path, kind = 30000, options = {}) {
65
65
  if (!options.skipPersistent && client.persistentGet) {
66
66
  const persistedEvent = await client.persistentGet(path);
67
67
  if (persistedEvent && persistedEvent.content) {
68
- try {
69
- const data = JSON.parse(persistedEvent.content);
68
+ // Verify author is in requested authors list (persistent storage may have cached events from other authors)
69
+ if (!authors.includes(persistedEvent.pubkey)) {
70
+ // Author mismatch - fall through to relay query
71
+ } else {
72
+ try {
73
+ const data = JSON.parse(persistedEvent.content);
70
74
 
71
- // Skip deleted items
72
- if (data._deleted) {
73
- return null;
74
- }
75
+ // Skip deleted items
76
+ if (data._deleted) {
77
+ return null;
78
+ }
75
79
 
76
- // Optionally include author information
77
- if (options.includeAuthor) {
78
- data._author = persistedEvent.pubkey;
79
- }
80
+ // Optionally include author information
81
+ if (options.includeAuthor) {
82
+ data._author = persistedEvent.pubkey;
83
+ }
80
84
 
81
- // Trigger background refresh from relays (fire-and-forget)
82
- if (client.refreshPathInBackground) {
83
- client.refreshPathInBackground(path, kind, { authors, timeout });
84
- }
85
+ // Trigger background refresh from relays (fire-and-forget)
86
+ if (client.refreshPathInBackground) {
87
+ client.refreshPathInBackground(path, kind, { authors, timeout });
88
+ }
85
89
 
86
- return data;
87
- } catch (error) {
88
- // Fall through to relay query if parsing fails
89
- console.warn('[nostrGet] Failed to parse persisted event:', error);
90
+ return data;
91
+ } catch (error) {
92
+ // Fall through to relay query if parsing fails
93
+ console.warn('[nostrGet] Failed to parse persisted event:', error);
94
+ }
90
95
  }
91
96
  }
92
97
  }
@@ -136,12 +141,15 @@ async function _executeNostrGet(client, path, kind, authors, timeout, options) {
136
141
 
137
142
  const events = await client.query(filter, { timeout });
138
143
 
139
- if (events.length === 0) {
144
+ // Filter by author (relays may not respect authors filter)
145
+ const authoredEvents = events.filter(event => authors.includes(event.pubkey));
146
+
147
+ if (authoredEvents.length === 0) {
140
148
  return null;
141
149
  }
142
150
 
143
- // Get most recent event (across all authors)
144
- const event = events.sort((a, b) => b.created_at - a.created_at)[0];
151
+ // Get most recent event (across allowed authors)
152
+ const event = authoredEvents.sort((a, b) => b.created_at - a.created_at)[0];
145
153
 
146
154
  try {
147
155
  const data = JSON.parse(event.content);
@@ -187,6 +195,9 @@ export async function nostrGetAll(client, pathPrefix, kind = 30000, options = {}
187
195
  for (const event of persistedEvents) {
188
196
  if (!event || !event.tags) continue;
189
197
 
198
+ // Verify author is in requested authors list (persistent storage may have cached events from other authors)
199
+ if (!authors.includes(event.pubkey)) continue;
200
+
190
201
  const dTag = event.tags.find(t => t[0] === 'd');
191
202
  if (!dTag || !dTag[1] || !dTag[1].startsWith(pathPrefix)) continue;
192
203
 
@@ -267,10 +278,12 @@ async function _executeNostrGetAll(client, pathPrefix, kind, authors, timeout, l
267
278
 
268
279
  const events = await client.query(filter, { timeout });
269
280
 
270
- // Filter by path prefix in application layer
281
+ // Filter by path prefix AND verify author (relays may not respect authors filter)
271
282
  const matching = events.filter(event => {
272
283
  const dTag = event.tags.find(t => t[0] === 'd');
273
- return dTag && dTag[1] && dTag[1].startsWith(pathPrefix);
284
+ const pathMatches = dTag && dTag[1] && dTag[1].startsWith(pathPrefix);
285
+ const authorAllowed = authors.includes(event.pubkey);
286
+ return pathMatches && authorAllowed;
274
287
  });
275
288
 
276
289
  // Parse content and group by d-tag (keep latest only, across all authors)
@@ -331,10 +344,12 @@ export async function nostrGetAllHybrid(client, pathPrefix, kind = 30000, option
331
344
 
332
345
  const events = await queryMethod.call(client, filter, { timeout });
333
346
 
334
- // Filter by path prefix in application layer
347
+ // Filter by path prefix AND verify author (relays may not respect authors filter)
335
348
  const matching = events.filter(event => {
336
349
  const dTag = event.tags.find(t => t[0] === 'd');
337
- return dTag && dTag[1] && dTag[1].startsWith(pathPrefix);
350
+ const pathMatches = dTag && dTag[1] && dTag[1].startsWith(pathPrefix);
351
+ const authorAllowed = authors.includes(event.pubkey);
352
+ return pathMatches && authorAllowed;
338
353
  });
339
354
 
340
355
  // Parse content and group by d-tag (keep latest only)
@@ -27,7 +27,11 @@ export function buildPath(appName, holonId, lensName, key = null) {
27
27
  * @returns {Promise<boolean>} Success indicator
28
28
  */
29
29
  export async function write(client, path, data) {
30
- // Check if this is a GunDB client
30
+ // Check if this is a GunDB client with backend methods (preferred - has write cache)
31
+ if (client.write && client.gun) {
32
+ return client.write(path, data);
33
+ }
34
+ // Fallback to direct gunWrapper (no write cache)
31
35
  if (client.gun) {
32
36
  return gunWrapper.write(client.gun, path, data);
33
37
  }
@@ -43,6 +47,11 @@ export async function write(client, path, data) {
43
47
  * @returns {Promise<Object|null>} Data or null
44
48
  */
45
49
  export async function read(client, path, options = {}) {
50
+ // Check if this is a GunDB client with backend methods (preferred - has write cache)
51
+ if (client.read && client.gun) {
52
+ return client.read(path, options);
53
+ }
54
+ // Fallback to direct gunWrapper
46
55
  if (client.gun) {
47
56
  return gunWrapper.read(client.gun, path);
48
57
  }
@@ -57,6 +66,11 @@ export async function read(client, path, options = {}) {
57
66
  * @returns {Promise<Object[]>} Array of data objects
58
67
  */
59
68
  export async function readAll(client, path, options = {}) {
69
+ // Check if this is a GunDB client with backend methods (preferred - has write cache)
70
+ if (client.readAll && client.gun) {
71
+ return client.readAll(path, options);
72
+ }
73
+ // Fallback to direct gunWrapper
60
74
  if (client.gun) {
61
75
  return gunWrapper.readAll(client.gun, path);
62
76
  }
@@ -71,6 +85,11 @@ export async function readAll(client, path, options = {}) {
71
85
  * @returns {Promise<boolean>} Success indicator
72
86
  */
73
87
  export async function update(client, path, updates) {
88
+ // Check if this is a GunDB client with backend methods (preferred - has write cache)
89
+ if (client.update && client.gun) {
90
+ return client.update(path, updates);
91
+ }
92
+ // Fallback to direct gunWrapper
74
93
  if (client.gun) {
75
94
  return gunWrapper.update(client.gun, path, updates);
76
95
  }
@@ -84,6 +103,11 @@ export async function update(client, path, updates) {
84
103
  * @returns {Promise<boolean>} Success indicator
85
104
  */
86
105
  export async function deleteData(client, path) {
106
+ // Check if this is a GunDB client with backend methods (preferred - has write cache)
107
+ if (client.delete && client.gun) {
108
+ return client.delete(path);
109
+ }
110
+ // Fallback to direct gunWrapper
87
111
  if (client.gun) {
88
112
  return gunWrapper.deleteData(client.gun, path);
89
113
  }
@@ -97,6 +121,7 @@ export async function deleteData(client, path) {
97
121
  * @returns {Promise<Object>} Deletion results
98
122
  */
99
123
  export async function deleteAll(client, path) {
124
+ // Fallback to direct gunWrapper (deleteAll not typically in client interface)
100
125
  if (client.gun) {
101
126
  return gunWrapper.deleteAll(client.gun, path);
102
127
  }
@@ -112,6 +137,11 @@ export async function deleteAll(client, path) {
112
137
  * @returns {Object} Subscription with unsubscribe method
113
138
  */
114
139
  export function subscribe(client, path, callback, options = {}) {
140
+ // Check if this is a GunDB client with backend methods
141
+ if (client.subscribe && client.gun) {
142
+ return client.subscribe(path, callback, options);
143
+ }
144
+ // Fallback to direct gunWrapper
115
145
  if (client.gun) {
116
146
  return gunWrapper.subscribe(client.gun, path, callback, options);
117
147
  }
@@ -0,0 +1,60 @@
1
+ import { defineConfig } from 'vite';
2
+ import { resolve } from 'path';
3
+
4
+ /**
5
+ * CDN Build Configuration
6
+ * Creates a self-contained bundle for browser usage via CDN
7
+ * All dependencies are bundled (no externals)
8
+ */
9
+ export default defineConfig({
10
+ build: {
11
+ target: 'es2020',
12
+ outDir: 'dist/cdn',
13
+ lib: {
14
+ entry: resolve(__dirname, 'src/cdn-entry.js'),
15
+ name: 'HoloSphere',
16
+ formats: ['iife'],
17
+ fileName: () => 'holosphere.min.js',
18
+ },
19
+ rollupOptions: {
20
+ // Bundle all dependencies for CDN
21
+ external: [],
22
+ output: {
23
+ // Use default export so HoloSphere is directly accessible
24
+ exports: 'default',
25
+ // Extend window with all exports
26
+ extend: true,
27
+ // Provide globals for any remaining externals
28
+ globals: {},
29
+ // Inline dynamic imports
30
+ inlineDynamicImports: true,
31
+ },
32
+ },
33
+ sourcemap: true,
34
+ minify: 'terser',
35
+ terserOptions: {
36
+ compress: {
37
+ drop_console: false,
38
+ passes: 2,
39
+ },
40
+ mangle: {
41
+ safari10: true,
42
+ },
43
+ },
44
+ // Increase chunk size warning limit for CDN bundle
45
+ chunkSizeWarningLimit: 2000,
46
+ },
47
+ resolve: {
48
+ alias: [
49
+ {
50
+ find: './filesystem-storage.js',
51
+ replacement: resolve(__dirname, 'src/storage/filesystem-storage-browser.js'),
52
+ },
53
+ ],
54
+ },
55
+ define: {
56
+ 'process.versions.node': JSON.stringify(undefined),
57
+ 'process.env.NODE_ENV': JSON.stringify('production'),
58
+ 'global': 'globalThis',
59
+ },
60
+ });