holosphere 2.0.0-alpha20 → 2.0.0-alpha22

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 (69) hide show
  1. package/README.md +1 -2
  2. package/dist/cjs/holosphere.cjs +1 -1
  3. package/dist/esm/holosphere.js +43 -41
  4. package/dist/{index-COpLk9gL.cjs → index-B4xe-N5-.cjs} +2 -2
  5. package/dist/{index-COpLk9gL.cjs.map → index-B4xe-N5-.cjs.map} +1 -1
  6. package/dist/{index-D2WstuZJ.js → index-Bug_CGNq.js} +2 -2
  7. package/dist/{index-D2WstuZJ.js.map → index-Bug_CGNq.js.map} +1 -1
  8. package/dist/index-CaCPzdlv.cjs +29 -0
  9. package/dist/index-CaCPzdlv.cjs.map +1 -0
  10. package/dist/{index-B6-8KAQm.js → index-D76zMgwU.js} +2 -2
  11. package/dist/{index-B6-8KAQm.js.map → index-D76zMgwU.js.map} +1 -1
  12. package/dist/{index--QsHG_gD.cjs → index-DuOuk96g.cjs} +2 -2
  13. package/dist/{index--QsHG_gD.cjs.map → index-DuOuk96g.cjs.map} +1 -1
  14. package/dist/{index-BHptWysv.js → index-bYHRpACA.js} +2951 -7736
  15. package/dist/index-bYHRpACA.js.map +1 -0
  16. package/dist/{indexeddb-storage-kQ53UHEE.js → indexeddb-storage-BrIwr42m.js} +2 -2
  17. package/dist/{indexeddb-storage-kQ53UHEE.js.map → indexeddb-storage-BrIwr42m.js.map} +1 -1
  18. package/dist/{indexeddb-storage-wKG4mICM.cjs → indexeddb-storage-CFWfkdX9.cjs} +2 -2
  19. package/dist/{indexeddb-storage-wKG4mICM.cjs.map → indexeddb-storage-CFWfkdX9.cjs.map} +1 -1
  20. package/dist/{memory-storage-DnXCSbBl.js → memory-storage-BDQRj-2j.js} +2 -2
  21. package/dist/{memory-storage-DnXCSbBl.js.map → memory-storage-BDQRj-2j.js.map} +1 -1
  22. package/dist/{memory-storage-CGC8xM2G.cjs → memory-storage-bkatDnuR.cjs} +2 -2
  23. package/dist/{memory-storage-CGC8xM2G.cjs.map → memory-storage-bkatDnuR.cjs.map} +1 -1
  24. package/examples/demo.html +2 -29
  25. package/package.json +3 -8
  26. package/src/content/social-protocols.js +3 -59
  27. package/src/core/holosphere.js +16 -554
  28. package/src/crypto/nostr-utils.js +98 -1
  29. package/src/crypto/secp256k1.js +13 -411
  30. package/src/federation/discovery.js +7 -75
  31. package/src/federation/handshake.js +69 -202
  32. package/src/federation/hologram.js +275 -460
  33. package/src/federation/index.js +2 -9
  34. package/src/federation/registry.js +65 -1169
  35. package/src/federation/request-card.js +21 -35
  36. package/src/hierarchical/upcast.js +4 -9
  37. package/src/index.js +142 -296
  38. package/src/lib/federation-methods.js +388 -886
  39. package/src/storage/global-tables.js +1 -1
  40. package/src/storage/nostr-wrapper.js +9 -5
  41. package/src/subscriptions/manager.js +1 -1
  42. package/types/index.d.ts +145 -37
  43. package/bin/holosphere-activitypub.js +0 -158
  44. package/dist/2019-BzVkRcax.js +0 -6680
  45. package/dist/2019-BzVkRcax.js.map +0 -1
  46. package/dist/2019-C1hPR_Os.cjs +0 -8
  47. package/dist/2019-C1hPR_Os.cjs.map +0 -1
  48. package/dist/browser-BcmACE3G.js +0 -3058
  49. package/dist/browser-BcmACE3G.js.map +0 -1
  50. package/dist/browser-DaqYUTcG.cjs +0 -2
  51. package/dist/browser-DaqYUTcG.cjs.map +0 -1
  52. package/dist/index-BHptWysv.js.map +0 -1
  53. package/dist/index-CDlhzxT2.cjs +0 -29
  54. package/dist/index-CDlhzxT2.cjs.map +0 -1
  55. package/src/federation/capabilities.js +0 -162
  56. package/src/storage/backend-factory.js +0 -130
  57. package/src/storage/backend-interface.js +0 -161
  58. package/src/storage/backends/activitypub/server.js +0 -675
  59. package/src/storage/backends/activitypub-backend.js +0 -295
  60. package/src/storage/backends/gundb-backend.js +0 -875
  61. package/src/storage/backends/nostr-backend.js +0 -251
  62. package/src/storage/gun-async.js +0 -341
  63. package/src/storage/gun-auth.js +0 -373
  64. package/src/storage/gun-federation.js +0 -785
  65. package/src/storage/gun-references.js +0 -209
  66. package/src/storage/gun-schema.js +0 -306
  67. package/src/storage/gun-wrapper.js +0 -642
  68. package/src/storage/migration.js +0 -351
  69. package/src/storage/unified-storage.js +0 -161
@@ -1,23 +1,31 @@
1
1
  /**
2
- * @fileoverview Unified Federation and Hologram (Reference) Management.
2
+ * @fileoverview Hologram the core federation infrastructure.
3
3
  *
4
- * Provides hologram (lightweight reference) creation, resolution, and management.
5
- * Holograms enable data to appear in multiple holons while maintaining a single
6
- * source of truth.
4
+ * A hologram is a lightweight reference (pointer) from one holon to data
5
+ * in another holon. It enables data to appear in multiple holons while
6
+ * maintaining a single source of truth.
7
7
  *
8
- * UNIFIED MODEL: All federation is capability-based. Every hologram includes:
9
- * - authorPubKey: The public key of the data owner (can be self)
10
- * - capability: A capability token (self-issued for same-author federation)
8
+ * SIMPLIFIED MODEL (v2):
9
+ * - authorPubKey: identifies WHO wrote the source data
10
+ * - No capability tokens federation membership (pubkey list) controls visibility
11
+ * - Anyone can write anywhere (Nostr is open-write); reads are filtered by federation
11
12
  *
12
- * This unifies "in-federation" (same author) and "cross-federation" (different authors)
13
- * into a single consistent model.
13
+ * The hologram is the core infrastructure of federation:
14
+ * 1. It points to source data (soul / target)
15
+ * 2. It knows the author (authorPubKey)
16
+ * 3. It resolves to the actual data on read
17
+ * 4. It supports local overrides (x, y, pinned, etc.)
14
18
  *
15
19
  * @module federation/hologram
16
20
  */
17
21
 
18
- import { buildPath, write, read, update } from '../storage/unified-storage.js';
19
- import { verifyCapability, issueCapability, verifyCapabilityIssuer, decodeCapability } from '../crypto/secp256k1.js';
20
- import { getCapabilityForAuthor, storeInboundCapability } from './registry.js';
22
+ import { buildPath, write, read, update } from '../storage/nostr-wrapper.js';
23
+ import { isPubkey } from '../crypto/secp256k1.js';
24
+ import {
25
+ encryptNIP44,
26
+ createDMEvent,
27
+ generateNonce,
28
+ } from '../crypto/nostr-utils.js';
21
29
 
22
30
  /** @constant {number} Maximum depth for hologram resolution chain */
23
31
  const MAX_RESOLUTION_DEPTH = 10;
@@ -85,42 +93,32 @@ export async function wouldCreateCircularHologram(client, appname, sourceHolon,
85
93
  }
86
94
 
87
95
  /**
88
- * Create a hologram (lightweight reference) - UNIFIED MODEL
96
+ * Create a hologram (lightweight reference).
89
97
  *
90
- * All holograms now include authorPubKey and capability for consistency.
91
- * This unifies "in-federation" (same author) and "cross-federation" (different authors).
98
+ * A hologram is a pointer it says "the real data lives at soul,
99
+ * written by authorPubKey." No capability tokens needed; federation
100
+ * membership (a pubkey list) controls who gets to see what on read.
92
101
  *
93
102
  * @param {string} sourceHolon - Source holon ID
94
103
  * @param {string} targetHolon - Target holon ID
95
104
  * @param {string} lensName - Lens name
96
105
  * @param {string} dataId - Data ID
97
106
  * @param {string} appname - Application namespace
98
- * @param {Object} options - Hologram options (authorPubKey and capability are required)
99
- * @param {string} options.authorPubKey - Source author's public key (REQUIRED - can be self)
100
- * @param {string} options.capability - Capability token (REQUIRED - self-issued for same author)
101
- * @returns {Object} Hologram object with unified structure
102
- * @throws {Error} If authorPubKey or capability is missing
107
+ * @param {Object} options - Hologram options
108
+ * @param {string} options.authorPubKey - Source author's public key (REQUIRED)
109
+ * @returns {Object} Hologram object
110
+ * @throws {Error} If authorPubKey is missing
103
111
  */
104
112
  export function createHologram(sourceHolon, targetHolon, lensName, dataId, appname, options = {}) {
105
113
  const { authorPubKey } = options;
106
- let { capability } = options;
107
114
 
108
- // Validate required fields for unified model
109
115
  if (!authorPubKey) {
110
- throw new Error('authorPubKey is required for hologram creation (unified model)');
111
- }
112
- if (!capability) {
113
- throw new Error('capability is required for hologram creation (unified model)');
114
- }
115
-
116
- // Normalize capability - ensure it's the token string, not a capability object
117
- if (typeof capability === 'object' && capability.token) {
118
- capability = capability.token;
116
+ throw new Error('authorPubKey is required for hologram creation');
119
117
  }
120
118
 
121
119
  const soul = buildPath(appname, sourceHolon, lensName, dataId);
122
120
 
123
- const hologram = {
121
+ return {
124
122
  id: dataId,
125
123
  hologram: true,
126
124
  soul,
@@ -129,29 +127,25 @@ export function createHologram(sourceHolon, targetHolon, lensName, dataId, appna
129
127
  holonId: sourceHolon,
130
128
  lensName,
131
129
  dataId,
132
- authorPubKey, // Always present in unified model
130
+ authorPubKey,
133
131
  },
134
- capability, // Always the token string (normalized above)
135
132
  _meta: {
136
133
  created: Date.now(),
137
134
  lastUpdated: Date.now(),
138
135
  sourceHolon,
139
- source: sourceHolon, // Alias for compatibility
136
+ source: sourceHolon,
140
137
  sourcePubKey: authorPubKey,
141
- grantedAt: Date.now(),
142
138
  },
143
139
  };
144
-
145
- return hologram;
146
140
  }
147
141
 
148
142
  /**
149
- * Create a hologram with automatic capability issuance - UNIFIED MODEL
143
+ * Create a hologram for a given author.
150
144
  *
151
- * This is the preferred method for creating holograms. It automatically issues
152
- * a capability token (self-capability if same author, cross-capability otherwise).
145
+ * This is the preferred method for creating holograms. It just needs the
146
+ * source author's public key — no capability tokens are issued or stored.
153
147
  *
154
- * @param {Object} client - Nostr client instance (must have publicKey and optionally privateKey)
148
+ * @param {Object} client - Nostr client instance (must have publicKey)
155
149
  * @param {string} sourceHolon - Source holon ID
156
150
  * @param {string} targetHolon - Target holon ID
157
151
  * @param {string} lensName - Lens name
@@ -159,103 +153,59 @@ export function createHologram(sourceHolon, targetHolon, lensName, dataId, appna
159
153
  * @param {string} appname - Application namespace
160
154
  * @param {Object} options - Hologram options
161
155
  * @param {string} [options.sourceAuthorPubKey] - Source author's public key (defaults to client.publicKey)
162
- * @param {string} [options.sourceAuthorPrivKey] - Source author's private key for signing capability (defaults to client.privateKey)
163
- * @param {string} [options.targetAuthorPubKey] - Target author's public key (defaults to client.publicKey)
164
- * @param {string} [options.capability] - Pre-issued capability (if not provided, one will be issued)
165
- * @param {string[]} [options.permissions=['read']] - Permissions for auto-issued capability
166
- * @param {number} [options.expiresIn=null] - Capability expiration in ms (null = no expiration)
167
- * @returns {Promise<Object>} Hologram object with unified structure
156
+ * @param {string} [options.capability] - Ignored (kept for backward compat)
157
+ * @returns {Promise<Object>} Hologram object
168
158
  */
169
159
  export async function createHologramWithCapability(client, sourceHolon, targetHolon, lensName, dataId, appname, options = {}) {
170
- const {
171
- sourceAuthorPubKey = client.publicKey,
172
- sourceAuthorPrivKey = client.privateKey, // Allow passing author's private key for signing
173
- targetAuthorPubKey = client.publicKey,
174
- capability: providedCapability = null,
175
- permissions = ['read'],
176
- expiresIn = null,
177
- } = options;
178
-
179
- let capability = providedCapability;
180
-
181
- // Issue capability if not provided
182
- if (!capability) {
183
- const isSelfCapability = sourceAuthorPubKey === targetAuthorPubKey;
184
-
185
- // Issue capability - for self-caps, no expiration by default
186
- capability = await issueCapability(
187
- permissions,
188
- { holonId: sourceHolon, lensName, dataId },
189
- targetAuthorPubKey,
190
- {
191
- expiresIn: expiresIn !== undefined ? expiresIn : null, // Hologram capabilities don't expire — they live as long as the hologram
192
- issuer: sourceAuthorPubKey,
193
- issuerKey: sourceAuthorPrivKey, // Use passed private key (holon's key when federated via holonsbot)
194
- }
195
- );
196
-
197
- // Store the capability in registry for cross-author federation
198
- if (!isSelfCapability && client.privateKey) {
199
- await storeInboundCapability(client, appname, sourceAuthorPubKey, {
200
- token: capability,
201
- scope: { holonId: sourceHolon, lensName, dataId },
202
- permissions,
203
- isSelfCapability: false,
204
- });
205
- }
206
- }
160
+ const sourceAuthorPubKey = options.sourceAuthorPubKey || client.publicKey;
207
161
 
208
162
  return createHologram(sourceHolon, targetHolon, lensName, dataId, appname, {
209
163
  authorPubKey: sourceAuthorPubKey,
210
- capability,
211
164
  });
212
165
  }
213
166
 
214
167
  /**
215
- * Resolve hologram to actual data, merging local overrides - UNIFIED MODEL
168
+ * Resolve hologram to actual data, merging local overrides.
216
169
  *
217
- * In the unified model, ALL holograms have authorPubKey and capability.
218
- * Resolution always:
219
- * 1. Verifies the capability token
220
- * 2. Fetches data with author filter
170
+ * Resolution is simple:
171
+ * 1. Detect circular references
172
+ * 2. Fetch source data using authorPubKey as author filter
173
+ * 3. Merge with local overrides
221
174
  *
222
- * This provides consistent security for both self and cross-author holograms.
175
+ * No capability verification federation membership controls visibility
176
+ * at the read() layer, not at hologram resolution.
223
177
  *
224
178
  * @param {Object} client - Nostr client instance
225
179
  * @param {Object} hologram - Hologram object
226
180
  * @param {Set} visited - Visited souls (circular reference detection)
227
181
  * @param {Array} chain - Chain of souls for debugging circular references
228
182
  * @param {Object} options - Resolution options
229
- * @param {string} options.appname - Application namespace (for registry lookup)
183
+ * @param {string} options.appname - Application namespace
230
184
  * @param {boolean} options.deleteCircular - If true, delete circular holograms when detected (default: true)
231
185
  * @param {string} options.hologramPath - Path of the hologram being resolved (for deletion)
232
- * @param {boolean} options.skipCapabilityVerification - Skip capability check (for internal use only)
233
186
  * @returns {Promise<Object|null>} Resolved data with _hologram metadata, or null
234
187
  */
235
188
  export async function resolveHologram(client, hologram, visited = new Set(), chain = [], options = {}) {
236
- const { deleteCircular = true, hologramPath = null, skipCapabilityVerification = false } = options;
189
+ const { deleteCircular = true, hologramPath = null } = options;
237
190
 
238
191
  if (!hologram || !hologram.hologram) {
239
192
  return hologram; // Not a hologram, return as-is
240
193
  }
241
194
 
195
+ // Lens holograms expand at read time (in index.js), not during resolution
196
+ if (hologram.lens === true) {
197
+ return null;
198
+ }
199
+
242
200
  const { soul } = hologram;
243
201
  const target = hologram.target || {};
244
202
 
245
203
  // Circular reference detection
246
204
  if (visited.has(soul)) {
247
205
  const chainStr = [...chain, soul].join(' → ');
248
- console.warn(`🔄 Circular reference detected - removing hologram`);
249
- console.warn(` Soul: ${soul}`);
250
- console.warn(` Chain: ${chainStr}`);
251
- console.warn(` Source holon: ${target.holonId || 'unknown'}`);
252
- console.warn(` Lens: ${target.lensName || 'unknown'}`);
253
- console.warn(` Data ID: ${target.dataId || hologram.id || 'unknown'}`);
254
- console.warn(` Resolution depth: ${visited.size}`);
255
-
256
- // Delete the circular hologram
206
+ console.warn(`Circular reference detected: ${chainStr}`);
207
+
257
208
  if (deleteCircular && hologramPath) {
258
- console.info(` 🗑️ Deleting circular hologram at: ${hologramPath}`);
259
209
  await write(client, hologramPath, null);
260
210
  }
261
211
 
@@ -263,102 +213,20 @@ export async function resolveHologram(client, hologram, visited = new Set(), cha
263
213
  }
264
214
 
265
215
  if (visited.size >= MAX_RESOLUTION_DEPTH) {
266
- const chainStr = [...chain, soul].join(' → ');
267
- console.warn(`⚠️ Max resolution depth (${MAX_RESOLUTION_DEPTH}) exceeded - removing hologram`);
268
- console.warn(` Current soul: ${soul}`);
269
- console.warn(` Chain: ${chainStr}`);
270
- console.warn(` Source holon: ${target.holonId || 'unknown'}`);
271
- console.warn(` Lens: ${target.lensName || 'unknown'}`);
272
-
273
- // Delete the problematic hologram
274
216
  if (deleteCircular && hologramPath) {
275
- console.info(` 🗑️ Deleting deep chain hologram at: ${hologramPath}`);
276
217
  await write(client, hologramPath, null);
277
218
  }
278
-
279
219
  return null;
280
220
  }
281
221
 
282
222
  visited.add(soul);
283
223
  chain.push(soul);
284
224
 
285
- let sourceData;
286
-
287
- // UNIFIED MODEL: All holograms have authorPubKey and capability
288
- // Get capability - try embedded first, then registry fallback
289
- let capability = hologram.capability;
225
+ // Fetch source data — use authorPubKey as author filter if present
290
226
  const authorPubKey = target.authorPubKey;
291
-
292
- // Normalize capability - extract token string if it's a capability object
293
- if (capability && typeof capability === 'object' && capability.token) {
294
- capability = capability.token;
295
- }
296
-
297
- if (!capability && options.appname && authorPubKey) {
298
- // Fallback to registry lookup
299
- const capEntry = await getCapabilityForAuthor(
300
- client,
301
- options.appname,
302
- authorPubKey,
303
- {
304
- holonId: target.holonId,
305
- lensName: target.lensName,
306
- dataId: target.dataId,
307
- }
308
- );
309
- if (capEntry) {
310
- // Extract token string from capability entry
311
- capability = capEntry.token || capEntry;
312
- }
313
- }
314
-
315
- // Verify capability (unified model - always verify unless explicitly skipped)
316
- if (!skipCapabilityVerification) {
317
- if (!capability) {
318
- console.warn(`❌ Hologram missing capability: ${soul}`);
319
- return null;
320
- }
321
-
322
- // OBJECT-CAPABILITY SECURITY: Verify capability issuer matches claimed authorPubKey
323
- // This prevents forged capabilities where someone signs a capability for data they don't own
324
- if (authorPubKey) {
325
- const issuerMatch = await verifyCapabilityIssuer(capability, authorPubKey);
326
- if (!issuerMatch) {
327
- console.warn(`❌ Capability issuer does not match authorPubKey for hologram: ${soul}`);
328
- console.warn(` Expected author: ${authorPubKey?.slice(0, 12)}...`);
329
- const decoded = decodeCapability(capability);
330
- if (decoded) {
331
- console.warn(` Actual issuer: ${decoded.issuer?.slice(0, 12)}...`);
332
- }
333
- return null;
334
- }
335
- }
336
-
337
- const isValid = await verifyCapability(
338
- capability,
339
- 'read',
340
- {
341
- holonId: target.holonId,
342
- lensName: target.lensName,
343
- dataId: target.dataId,
344
- }
345
- );
346
-
347
- if (!isValid) {
348
- console.warn(`❌ Capability verification failed for hologram: ${soul}`);
349
- return null;
350
- }
351
- }
352
-
353
- // UNIFIED MODEL: Always fetch with author filter if authorPubKey is present
354
- if (authorPubKey) {
355
- sourceData = await read(client, soul, {
356
- authors: [authorPubKey]
357
- });
358
- } else {
359
- // Legacy fallback for holograms without authorPubKey
360
- sourceData = await read(client, soul);
361
- }
227
+ const sourceData = authorPubKey
228
+ ? await read(client, soul, { authors: [authorPubKey] })
229
+ : await read(client, soul);
362
230
 
363
231
  if (!sourceData) {
364
232
  return null;
@@ -366,7 +234,6 @@ export async function resolveHologram(client, hologram, visited = new Set(), cha
366
234
 
367
235
  // If source data is also a hologram, recursively resolve
368
236
  if (sourceData.hologram) {
369
- // Pass the soul as the hologramPath so circular holograms can be deleted
370
237
  return resolveHologram(client, sourceData, visited, chain, { ...options, hologramPath: soul });
371
238
  }
372
239
 
@@ -375,7 +242,7 @@ export async function resolveHologram(client, hologram, visited = new Set(), cha
375
242
  }
376
243
 
377
244
  /**
378
- * Merge hologram local overrides with source data - UNIFIED MODEL
245
+ * Merge hologram local overrides with source data.
379
246
  * @private
380
247
  * @param {Object} hologram - Hologram object
381
248
  * @param {Object} sourceData - Source data
@@ -457,10 +324,10 @@ export async function setupFederation(
457
324
  }
458
325
 
459
326
  /**
460
- * Propagate data to federated location - UNIFIED MODEL
327
+ * Propagate data to federated location.
461
328
  *
462
- * In the unified model, all holograms require authorPubKey and capability.
463
- * This function automatically issues capabilities for propagation.
329
+ * Creates a hologram (reference) or copy in the target holon.
330
+ * No capability tokens needed just the author's pubkey.
464
331
  *
465
332
  * @param {Object} client - Nostr client instance (must have publicKey)
466
333
  * @param {string} appname - Application namespace
@@ -471,8 +338,6 @@ export async function setupFederation(
471
338
  * @param {string} mode - 'reference' or 'copy'
472
339
  * @param {Object} options - Propagation options
473
340
  * @param {string} [options.sourceAuthorPubKey] - Source author (defaults to client.publicKey)
474
- * @param {string} [options.sourceAuthorPrivKey] - Source author's private key for signing capability (defaults to client.privateKey)
475
- * @param {string} [options.capability] - Pre-issued capability (auto-issued if not provided)
476
341
  * @returns {Promise<boolean>} Success indicator
477
342
  */
478
343
  export async function propagateData(
@@ -493,97 +358,13 @@ export async function propagateData(
493
358
  return true;
494
359
  }
495
360
 
496
- // Don't propagate holograms back to avoid circular references
497
- // If this data is already a hologram pointing to the target, skip it
361
+ // Don't propagate holograms holograms should only be created explicitly
498
362
  if (data.hologram === true) {
499
- // Check if this hologram would create a circular reference
500
- if (data.target && data.target.holonId === targetHolon) {
501
- console.info(`🔄 Skipping propagation - would create circular reference:`);
502
- console.info(` Data ID: ${dataId}`);
503
- console.info(` Source holon: ${sourceHolon}`);
504
- console.info(` Target holon: ${targetHolon}`);
505
- console.info(` Hologram points to: ${data.target.holonId}`);
506
- console.info(` Lens: ${lensName}`);
507
- console.info(` Original soul: ${data.soul || 'unknown'}`);
508
- return true;
509
- }
510
-
511
- // Check if targetHolon is already in the original source's activeHolograms
512
- // This prevents creating duplicate holograms when the target already has one
513
- if (data.target) {
514
- const originalSourcePath = buildPath(
515
- data.target.appname || appname,
516
- data.target.holonId,
517
- data.target.lensName || lensName,
518
- data.target.dataId || dataId
519
- );
520
- const originalSource = await read(client, originalSourcePath);
521
-
522
- if (originalSource?._meta?.activeHolograms) {
523
- const alreadyHasHologram = originalSource._meta.activeHolograms.some(
524
- h => h.targetHolon === targetHolon
525
- );
526
- if (alreadyHasHologram) {
527
- console.info(`⏭️ Skipping propagation - target already in activeHolograms:`);
528
- console.info(` Data ID: ${dataId}`);
529
- console.info(` Original source: ${data.target.holonId}`);
530
- console.info(` Target holon: ${targetHolon}`);
531
- return true;
532
- }
533
- }
534
- }
535
-
536
- // Deep circular check for hologram copy - ensure the original source doesn't eventually point to target
537
- const originalSourceHolon = data.target.holonId;
538
- const originalDataId = data.target.dataId || dataId;
539
- const originalLens = data.target.lensName || lensName;
540
- const originalAppname = data.target.appname || appname;
541
-
542
- const circularCheck = await wouldCreateCircularHologram(
543
- client, originalAppname, originalSourceHolon, targetHolon, originalLens, originalDataId
544
- );
545
-
546
- if (circularCheck.isCircular) {
547
- const chainStr = circularCheck.chain.map(c => c.holon).join(' → ');
548
- console.warn(`🔄 Preventing circular hologram copy:`);
549
- console.warn(` Data ID: ${dataId}`);
550
- console.warn(` Original source: ${originalSourceHolon}`);
551
- console.warn(` Would copy to: ${targetHolon}`);
552
- console.warn(` Existing chain: ${chainStr}`);
553
- console.warn(` Reason: ${circularCheck.reason}`);
554
- return false;
555
- }
556
-
557
- // When propagating a hologram, copy it instead of creating a hologram of a hologram
558
- // This maintains the reference to the original source
559
- const targetPath = buildPath(appname, targetHolon, lensName, dataId);
560
- const copiedHologram = {
561
- ...data,
562
- _meta: {
563
- ...data._meta,
564
- copiedFrom: sourceHolon,
565
- copiedAt: Date.now()
566
- }
567
- };
568
- const success = await write(client, targetPath, copiedHologram);
569
-
570
- // Add this new location to the original source's activeHolograms
571
- if (success && data.target) {
572
- await addActiveHologram(
573
- client,
574
- data.target.appname || appname,
575
- data.target.holonId,
576
- data.target.lensName || lensName,
577
- data.target.dataId || dataId,
578
- targetHolon
579
- );
580
- console.info(`📋 Copied hologram to ${targetHolon}:`);
581
- console.info(` Data ID: ${dataId}`);
582
- console.info(` Original source: ${data.target.holonId}`);
583
- console.info(` Copied from: ${sourceHolon}`);
584
- }
585
-
586
- return success;
363
+ console.info(`⏭️ Skipping propagation - data is already a hologram (no transitive copy):`);
364
+ console.info(` Data ID: ${dataId}`);
365
+ console.info(` Source holon: ${sourceHolon}`);
366
+ console.info(` Target holon: ${targetHolon}`);
367
+ return true;
587
368
  }
588
369
 
589
370
  // For non-hologram data, check if targetHolon is already in activeHolograms
@@ -616,15 +397,9 @@ export async function propagateData(
616
397
  return false;
617
398
  }
618
399
 
619
- // UNIFIED MODEL: Create hologram with capability
400
+ // Create hologram — just a pointer with the author's pubkey
620
401
  const sourceAuthorPubKey = options.sourceAuthorPubKey || client.publicKey;
621
402
 
622
- // Determine targetAuthorPubKey - the person who should be able to read the hologram
623
- // If targetHolon is a 64-char hex pubkey, use it directly as the target author
624
- // Otherwise, this needs to be resolved via holon registry (TODO: add resolution)
625
- const isPubkey = typeof targetHolon === 'string' && /^[0-9a-f]{64}$/i.test(targetHolon);
626
- const targetAuthorPubKey = options.targetAuthorPubKey || (isPubkey ? targetHolon : client.publicKey);
627
-
628
403
  const hologram = await createHologramWithCapability(
629
404
  client,
630
405
  sourceHolon,
@@ -632,13 +407,7 @@ export async function propagateData(
632
407
  lensName,
633
408
  dataId,
634
409
  appname,
635
- {
636
- sourceAuthorPubKey,
637
- sourceAuthorPrivKey: options.sourceAuthorPrivKey, // Pass through for holon-signed capabilities
638
- targetAuthorPubKey,
639
- capability: options.capability,
640
- permissions: ['read'],
641
- }
410
+ { sourceAuthorPubKey }
642
411
  );
643
412
 
644
413
  const targetPath = buildPath(appname, targetHolon, lensName, dataId);
@@ -712,6 +481,157 @@ export function isHologram(data) {
712
481
  return data && data.hologram === true;
713
482
  }
714
483
 
484
+ // ============================================================================
485
+ // Lens Holograms — "share this entire lens from this author"
486
+ // ============================================================================
487
+
488
+ /**
489
+ * Create a lens hologram — a reference meaning "all data in this lens from this author".
490
+ *
491
+ * Unlike an item hologram (which points at a single data item), a lens hologram
492
+ * expands at read time: the reader fetches every item in the source lens and merges
493
+ * them into the local results.
494
+ *
495
+ * @param {string} sourceHolon - Source holon ID (where data lives)
496
+ * @param {string} lensName - Lens name
497
+ * @param {string} appname - Application namespace
498
+ * @param {Object} options
499
+ * @param {string} options.author - Public key of the author whose data this points to
500
+ * @returns {Object} Lens hologram object
501
+ */
502
+ export function createLensHologram(sourceHolon, lensName, appname, { author }) {
503
+ if (!author) {
504
+ throw new Error('author is required for lens hologram creation');
505
+ }
506
+
507
+ const soul = buildPath(appname, sourceHolon, lensName);
508
+
509
+ return {
510
+ id: `_lens_${author}`,
511
+ hologram: true,
512
+ lens: true, // distinguishes from item hologram
513
+ soul,
514
+ target: {
515
+ app: appname,
516
+ holon: sourceHolon,
517
+ lens: lensName,
518
+ author,
519
+ },
520
+ _meta: {
521
+ created: Date.now(),
522
+ lastUpdated: Date.now(),
523
+ sourceHolon,
524
+ sourcePubKey: author,
525
+ },
526
+ };
527
+ }
528
+
529
+ /**
530
+ * Check if data is a lens hologram (all-items reference).
531
+ * @param {Object} data - Data to check
532
+ * @returns {boolean} True if this is a lens hologram
533
+ */
534
+ export function isLensHologram(data) {
535
+ return !!(data && data.hologram === true && data.lens === true);
536
+ }
537
+
538
+ // ============================================================================
539
+ // DM Helpers — send share / share_ack payloads via NIP-44 encrypted DM
540
+ // ============================================================================
541
+
542
+ /**
543
+ * Send a 'share' DM to a target pubkey.
544
+ *
545
+ * @param {Object} client - NostrClient instance with publish method
546
+ * @param {string} privateKey - Sender's hex private key
547
+ * @param {string} targetPubKey - Recipient's hex public key
548
+ * @param {Object} payload - Share payload
549
+ * @param {string} payload.source - Source holon ID
550
+ * @param {string} payload.lens - Lens name
551
+ * @param {string|null} payload.item - Data ID (null for lens-level share)
552
+ * @param {string} payload.author - Author public key
553
+ * @param {Array} [payload.keys] - Optional bundled lens encryption keys
554
+ * @param {string} [payload.msg] - Optional message
555
+ * @returns {Promise<boolean>} Success indicator
556
+ */
557
+ export async function sendShare(client, privateKey, targetPubKey, payload) {
558
+ try {
559
+ const dm = {
560
+ type: 'share',
561
+ id: generateNonce(),
562
+ source: payload.source,
563
+ lens: payload.lens,
564
+ item: payload.item || null,
565
+ author: payload.author,
566
+ keys: payload.keys || [],
567
+ msg: payload.msg || '',
568
+ timestamp: Date.now(),
569
+ };
570
+
571
+ const content = JSON.stringify(dm);
572
+ const encrypted = encryptNIP44(privateKey, targetPubKey, content);
573
+ const event = createDMEvent(targetPubKey, encrypted, privateKey);
574
+
575
+ if (client?.publish) {
576
+ await client.publish(event);
577
+ console.log('[Share] Share DM sent to:', targetPubKey.substring(0, 8) + '...');
578
+ return true;
579
+ }
580
+ console.error('[Share] No publish method on client');
581
+ return false;
582
+ } catch (error) {
583
+ console.error('[Share] Failed to send share DM:', error);
584
+ return false;
585
+ }
586
+ }
587
+
588
+ /**
589
+ * Send a 'share_ack' DM (accept or reject response).
590
+ *
591
+ * @param {Object} client - NostrClient instance with publish method
592
+ * @param {string} privateKey - Sender's hex private key
593
+ * @param {string} targetPubKey - Recipient's hex public key
594
+ * @param {Object} payload - Ack payload
595
+ * @param {string} payload.id - Original share id being acknowledged
596
+ * @param {string} payload.source - Source holon ID
597
+ * @param {string} payload.lens - Lens name
598
+ * @param {string|null} payload.item - Data ID (null for lens-level)
599
+ * @param {string} payload.author - Author public key
600
+ * @param {'accepted'|'rejected'} payload.status - Acceptance status
601
+ * @param {string} [payload.msg] - Optional message
602
+ * @returns {Promise<boolean>} Success indicator
603
+ */
604
+ export async function sendAck(client, privateKey, targetPubKey, payload) {
605
+ try {
606
+ const dm = {
607
+ type: 'share_ack',
608
+ id: payload.id,
609
+ source: payload.source,
610
+ lens: payload.lens,
611
+ item: payload.item || null,
612
+ author: payload.author,
613
+ status: payload.status,
614
+ msg: payload.msg || '',
615
+ timestamp: Date.now(),
616
+ };
617
+
618
+ const content = JSON.stringify(dm);
619
+ const encrypted = encryptNIP44(privateKey, targetPubKey, content);
620
+ const event = createDMEvent(targetPubKey, encrypted, privateKey);
621
+
622
+ if (client?.publish) {
623
+ await client.publish(event);
624
+ console.log('[Share] Ack DM sent to:', targetPubKey.substring(0, 8) + '...', 'status:', payload.status);
625
+ return true;
626
+ }
627
+ console.error('[Share] No publish method on client');
628
+ return false;
629
+ } catch (error) {
630
+ console.error('[Share] Failed to send ack DM:', error);
631
+ return false;
632
+ }
633
+ }
634
+
715
635
  /**
716
636
  * Check if data was resolved from a hologram
717
637
  * @param {Object} data - Data to check
@@ -738,36 +658,31 @@ export function getHologramSource(data) {
738
658
  }
739
659
 
740
660
  /**
741
- * Add a hologram reference to the source data's _meta.activeHolograms
742
- * This tracks all holograms pointing to this specific data item
743
- * If the source is itself a hologram, follows the chain to find the real source
661
+ * Follow a hologram chain to find the real (non-hologram) source data.
662
+ * If the data at the given location is a hologram, follows .target references
663
+ * until real data is found or max depth is reached.
664
+ * @private
744
665
  * @param {Object} client - Nostr client instance
745
666
  * @param {string} appname - Application namespace
746
- * @param {string} sourceHolon - Source holon ID (where original data lives)
747
- * @param {string} lensName - Lens name
667
+ * @param {string} holon - Starting holon ID
668
+ * @param {string} lens - Lens name
748
669
  * @param {string} dataId - Data ID
749
- * @param {string} targetHolon - Target holon ID (where hologram lives)
750
- * @returns {Promise<boolean>} Success indicator
670
+ * @param {number} [maxDepth=10] - Maximum chain depth
671
+ * @returns {Promise<{ sourcePath: string, sourceData: Object }|null>} Real source or null
751
672
  */
752
- export async function addActiveHologram(client, appname, sourceHolon, lensName, dataId, targetHolon) {
673
+ async function followToRealSource(client, appname, holon, lens, dataId, maxDepth = 10) {
753
674
  let currentAppname = appname;
754
- let currentHolon = sourceHolon;
755
- let currentLens = lensName;
675
+ let currentHolon = holon;
676
+ let currentLens = lens;
756
677
  let currentDataId = dataId;
757
678
  let sourcePath = buildPath(currentAppname, currentHolon, currentLens, currentDataId);
758
679
 
759
- // Read existing source data
760
- // Note: May return null if called immediately after write (before relay persistence)
761
- // This is expected - the hologram still works, we just can't track activeHolograms
762
680
  let sourceData = await read(client, sourcePath);
763
681
  if (!sourceData) {
764
- return false;
682
+ return null;
765
683
  }
766
684
 
767
- // If source is a hologram, follow the chain to find the real source
768
- // This ensures activeHolograms is always tracked on the actual data, not on holograms
769
685
  let depth = 0;
770
- const maxDepth = 10;
771
686
  while (sourceData.hologram === true && sourceData.target && depth < maxDepth) {
772
687
  depth++;
773
688
  currentAppname = sourceData.target.appname || currentAppname;
@@ -778,10 +693,34 @@ export async function addActiveHologram(client, appname, sourceHolon, lensName,
778
693
 
779
694
  sourceData = await read(client, sourcePath);
780
695
  if (!sourceData) {
781
- return false;
696
+ return null;
782
697
  }
783
698
  }
784
699
 
700
+ return { sourcePath, sourceData };
701
+ }
702
+
703
+ /**
704
+ * Add a hologram reference to the source data's _meta.activeHolograms
705
+ * This tracks all holograms pointing to this specific data item
706
+ * If the source is itself a hologram, follows the chain to find the real source
707
+ * @param {Object} client - Nostr client instance
708
+ * @param {string} appname - Application namespace
709
+ * @param {string} sourceHolon - Source holon ID (where original data lives)
710
+ * @param {string} lensName - Lens name
711
+ * @param {string} dataId - Data ID
712
+ * @param {string} targetHolon - Target holon ID (where hologram lives)
713
+ * @returns {Promise<boolean>} Success indicator
714
+ */
715
+ export async function addActiveHologram(client, appname, sourceHolon, lensName, dataId, targetHolon) {
716
+ // Note: May return null if called immediately after write (before relay persistence)
717
+ // This is expected - the hologram still works, we just can't track activeHolograms
718
+ const result = await followToRealSource(client, appname, sourceHolon, lensName, dataId);
719
+ if (!result) {
720
+ return false;
721
+ }
722
+
723
+ const { sourcePath, sourceData } = result;
785
724
 
786
725
  // Initialize _meta and activeHolograms if needed
787
726
  if (!sourceData._meta) {
@@ -827,34 +766,12 @@ export async function addActiveHologram(client, appname, sourceHolon, lensName,
827
766
  * @returns {Promise<boolean>} Success indicator
828
767
  */
829
768
  export async function removeActiveHologram(client, appname, sourceHolon, lensName, dataId, targetHolon) {
830
- let currentAppname = appname;
831
- let currentHolon = sourceHolon;
832
- let currentLens = lensName;
833
- let currentDataId = dataId;
834
- let sourcePath = buildPath(currentAppname, currentHolon, currentLens, currentDataId);
835
-
836
- // Read existing source data
837
- let sourceData = await read(client, sourcePath);
838
- if (!sourceData) {
769
+ const result = await followToRealSource(client, appname, sourceHolon, lensName, dataId);
770
+ if (!result) {
839
771
  return false;
840
772
  }
841
773
 
842
- // If source is a hologram, follow the chain to find the real source
843
- let depth = 0;
844
- const maxDepth = 10;
845
- while (sourceData.hologram === true && sourceData.target && depth < maxDepth) {
846
- depth++;
847
- currentAppname = sourceData.target.appname || currentAppname;
848
- currentHolon = sourceData.target.holonId;
849
- currentLens = sourceData.target.lensName || currentLens;
850
- currentDataId = sourceData.target.dataId || currentDataId;
851
- sourcePath = buildPath(currentAppname, currentHolon, currentLens, currentDataId);
852
-
853
- sourceData = await read(client, sourcePath);
854
- if (!sourceData) {
855
- return false;
856
- }
857
- }
774
+ const { sourcePath, sourceData } = result;
858
775
 
859
776
  if (!sourceData._meta || !Array.isArray(sourceData._meta.activeHolograms)) {
860
777
  return false;
@@ -887,35 +804,12 @@ export async function removeActiveHologram(client, appname, sourceHolon, lensNam
887
804
  * @returns {Promise<boolean>} Success indicator
888
805
  */
889
806
  export async function updateActiveHologramPlatform(client, appname, sourceHolon, lensName, dataId, targetHolon, platform, platformData) {
890
- let currentAppname = appname;
891
- let currentHolon = sourceHolon;
892
- let currentLens = lensName;
893
- let currentDataId = dataId;
894
- let sourcePath = buildPath(currentAppname, currentHolon, currentLens, currentDataId);
895
-
896
- // Read existing source data
897
- // Note: May return null if called immediately after write (before relay persistence)
898
- let sourceData = await read(client, sourcePath);
899
- if (!sourceData) {
807
+ const result = await followToRealSource(client, appname, sourceHolon, lensName, dataId);
808
+ if (!result) {
900
809
  return false;
901
810
  }
902
811
 
903
- // If source is a hologram, follow the chain to find the real source
904
- let depth = 0;
905
- const maxDepth = 10;
906
- while (sourceData.hologram === true && sourceData.target && depth < maxDepth) {
907
- depth++;
908
- currentAppname = sourceData.target.appname || currentAppname;
909
- currentHolon = sourceData.target.holonId;
910
- currentLens = sourceData.target.lensName || currentLens;
911
- currentDataId = sourceData.target.dataId || currentDataId;
912
- sourcePath = buildPath(currentAppname, currentHolon, currentLens, currentDataId);
913
-
914
- sourceData = await read(client, sourcePath);
915
- if (!sourceData) {
916
- return false;
917
- }
918
- }
812
+ const { sourcePath, sourceData } = result;
919
813
 
920
814
  if (!sourceData._meta || !Array.isArray(sourceData._meta.activeHolograms)) {
921
815
  console.warn(`No activeHolograms found for ${sourcePath}`);
@@ -1132,96 +1026,17 @@ export async function deleteHologram(client, appname, holonId, lensName, dataId,
1132
1026
  * @returns {Promise<Object>} Result with cleanup info
1133
1027
  */
1134
1028
  export async function cleanupCircularHolograms(client, appname, holonId, lensName, options = {}) {
1135
- const { dryRun = false } = options;
1136
- const result = {
1137
- scanned: 0,
1138
- circularFound: 0,
1139
- deleted: 0,
1140
- errors: [],
1141
- details: []
1142
- };
1143
-
1144
- console.info(`🧹 ${dryRun ? '[DRY RUN] ' : ''}Cleaning up circular holograms in ${holonId}/${lensName}...`);
1145
-
1146
- try {
1147
- // Get all data in this lens
1148
- const basePath = buildPath(appname, holonId, lensName);
1149
- // We need to iterate through all items - this requires listing capability
1150
- // For now, we'll rely on the caller to provide a list of IDs to check
1151
- // or we can use a different approach
1152
-
1153
- // Alternative: Read the lens index if it exists
1154
- const lensIndexPath = buildPath(appname, holonId, lensName, '_index');
1155
- const lensIndex = await read(client, lensIndexPath);
1156
-
1157
- if (!lensIndex || !Array.isArray(lensIndex.items)) {
1158
- console.warn(` No index found for ${holonId}/${lensName}, cannot scan for circular references`);
1159
- console.info(` Tip: Provide specific IDs to check using cleanupCircularHologramsByIds()`);
1160
- return result;
1161
- }
1162
-
1163
- for (const itemId of lensIndex.items) {
1164
- result.scanned++;
1165
- const itemPath = buildPath(appname, holonId, lensName, itemId);
1166
- const item = await read(client, itemPath);
1167
-
1168
- if (!item || item.hologram !== true || !item.target) {
1169
- continue; // Not a hologram, skip
1170
- }
1171
-
1172
- // This is a hologram - check if it creates a circular reference
1173
- const sourceHolon = item.target.holonId;
1174
- const sourceDataId = item.target.dataId || itemId;
1175
- const sourceLens = item.target.lensName || lensName;
1176
-
1177
- // Read the source data
1178
- const sourcePath = buildPath(item.target.appname || appname, sourceHolon, sourceLens, sourceDataId);
1179
- const sourceData = await read(client, sourcePath);
1180
-
1181
- if (sourceData && sourceData.hologram === true && sourceData.target) {
1182
- // Source is also a hologram - check if it points back
1183
- if (sourceData.target.holonId === holonId) {
1184
- result.circularFound++;
1185
- const detail = {
1186
- itemId,
1187
- path: itemPath,
1188
- pointsTo: `${sourceHolon}/${sourceLens}/${sourceDataId}`,
1189
- circularWith: `${sourceData.target.holonId}/${sourceData.target.lensName}/${sourceData.target.dataId}`
1190
- };
1191
- result.details.push(detail);
1192
-
1193
- console.warn(` 🔄 Found circular hologram:`);
1194
- console.warn(` ${holonId}/${lensName}/${itemId} → ${sourceHolon}/${sourceLens}/${sourceDataId} → ${holonId}`);
1195
-
1196
- if (!dryRun) {
1197
- // Delete this hologram
1198
- const deleteResult = await deleteHologram(client, appname, holonId, lensName, itemId);
1199
- if (deleteResult.success) {
1200
- result.deleted++;
1201
- console.info(` ✓ Deleted circular hologram`);
1202
- } else {
1203
- result.errors.push({ itemId, error: deleteResult.error });
1204
- console.error(` ❌ Failed to delete: ${deleteResult.error}`);
1205
- }
1206
- }
1207
- }
1208
- }
1209
- }
1210
-
1211
- console.info(`🧹 Cleanup complete for ${holonId}/${lensName}:`);
1212
- console.info(` Scanned: ${result.scanned}`);
1213
- console.info(` Circular found: ${result.circularFound}`);
1214
- console.info(` Deleted: ${result.deleted}`);
1215
- if (result.errors.length > 0) {
1216
- console.warn(` Errors: ${result.errors.length}`);
1217
- }
1218
-
1219
- return result;
1220
- } catch (error) {
1221
- console.error(`❌ Error during cleanup:`, error);
1222
- result.errors.push({ error: error.message });
1223
- return result;
1029
+ // Read the lens index to get all item IDs
1030
+ const lensIndexPath = buildPath(appname, holonId, lensName, '_index');
1031
+ const lensIndex = await read(client, lensIndexPath);
1032
+
1033
+ if (!lensIndex || !Array.isArray(lensIndex.items)) {
1034
+ console.warn(` No index found for ${holonId}/${lensName}, cannot scan for circular references`);
1035
+ console.info(` Tip: Provide specific IDs to check using cleanupCircularHologramsByIds()`);
1036
+ return { scanned: 0, circularFound: 0, deleted: 0, errors: [], details: [] };
1224
1037
  }
1038
+
1039
+ return cleanupCircularHologramsByIds(client, appname, holonId, lensName, lensIndex.items, options);
1225
1040
  }
1226
1041
 
1227
1042
  /**