holosphere 2.0.0-alpha21 → 2.0.0-alpha23

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 +61 -58
  4. package/dist/{index-B6-8KAQm.js → index-BEkCLOwI.js} +2 -2
  5. package/dist/{index-B6-8KAQm.js.map → index-BEkCLOwI.js.map} +1 -1
  6. package/dist/{index-D2WstuZJ.js → index-BEvX6DxG.js} +2 -2
  7. package/dist/{index-D2WstuZJ.js.map → index-BEvX6DxG.js.map} +1 -1
  8. package/dist/{index--QsHG_gD.cjs → index-BGTOiJ2Y.cjs} +2 -2
  9. package/dist/{index--QsHG_gD.cjs.map → index-BGTOiJ2Y.cjs.map} +1 -1
  10. package/dist/{index-COpLk9gL.cjs → index-BH1woZXL.cjs} +2 -2
  11. package/dist/{index-COpLk9gL.cjs.map → index-BH1woZXL.cjs.map} +1 -1
  12. package/dist/{index-BHptWysv.js → index-Cvxov2jv.js} +2970 -7753
  13. package/dist/index-Cvxov2jv.js.map +1 -0
  14. package/dist/index-vTKI_BAX.cjs +29 -0
  15. package/dist/index-vTKI_BAX.cjs.map +1 -0
  16. package/dist/{indexeddb-storage-wKG4mICM.cjs → indexeddb-storage-BmnCNnSg.cjs} +2 -2
  17. package/dist/{indexeddb-storage-wKG4mICM.cjs.map → indexeddb-storage-BmnCNnSg.cjs.map} +1 -1
  18. package/dist/{indexeddb-storage-kQ53UHEE.js → indexeddb-storage-MIFisaPy.js} +2 -2
  19. package/dist/{indexeddb-storage-kQ53UHEE.js.map → indexeddb-storage-MIFisaPy.js.map} +1 -1
  20. package/dist/{memory-storage-CGC8xM2G.cjs → memory-storage-BJjK3F4r.cjs} +2 -2
  21. package/dist/{memory-storage-CGC8xM2G.cjs.map → memory-storage-BJjK3F4r.cjs.map} +1 -1
  22. package/dist/{memory-storage-DnXCSbBl.js → memory-storage-DhHXdKQ-.js} +2 -2
  23. package/dist/{memory-storage-DnXCSbBl.js.map → memory-storage-DhHXdKQ-.js.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 +4 -393
  30. package/src/federation/discovery.js +7 -75
  31. package/src/federation/handshake.js +69 -202
  32. package/src/federation/hologram.js +222 -298
  33. package/src/federation/index.js +2 -9
  34. package/src/federation/registry.js +67 -1257
  35. package/src/federation/request-card.js +21 -35
  36. package/src/hierarchical/upcast.js +4 -9
  37. package/src/index.js +145 -296
  38. package/src/lib/federation-methods.js +370 -909
  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 -46
  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, normalizeTokenString, isPubkey } 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,40 +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)');
116
+ throw new Error('authorPubKey is required for hologram creation');
114
117
  }
115
118
 
116
- // Normalize capability to plain token string
117
- capability = normalizeTokenString(capability) || capability;
118
-
119
119
  const soul = buildPath(appname, sourceHolon, lensName, dataId);
120
120
 
121
- const hologram = {
121
+ return {
122
122
  id: dataId,
123
123
  hologram: true,
124
124
  soul,
@@ -127,29 +127,25 @@ export function createHologram(sourceHolon, targetHolon, lensName, dataId, appna
127
127
  holonId: sourceHolon,
128
128
  lensName,
129
129
  dataId,
130
- authorPubKey, // Always present in unified model
130
+ authorPubKey,
131
131
  },
132
- capability, // Always the token string (normalized above)
133
132
  _meta: {
134
133
  created: Date.now(),
135
134
  lastUpdated: Date.now(),
136
135
  sourceHolon,
137
- source: sourceHolon, // Alias for compatibility
136
+ source: sourceHolon,
138
137
  sourcePubKey: authorPubKey,
139
- grantedAt: Date.now(),
140
138
  },
141
139
  };
142
-
143
- return hologram;
144
140
  }
145
141
 
146
142
  /**
147
- * Create a hologram with automatic capability issuance - UNIFIED MODEL
143
+ * Create a hologram for a given author.
148
144
  *
149
- * This is the preferred method for creating holograms. It automatically issues
150
- * 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.
151
147
  *
152
- * @param {Object} client - Nostr client instance (must have publicKey and optionally privateKey)
148
+ * @param {Object} client - Nostr client instance (must have publicKey)
153
149
  * @param {string} sourceHolon - Source holon ID
154
150
  * @param {string} targetHolon - Target holon ID
155
151
  * @param {string} lensName - Lens name
@@ -157,103 +153,59 @@ export function createHologram(sourceHolon, targetHolon, lensName, dataId, appna
157
153
  * @param {string} appname - Application namespace
158
154
  * @param {Object} options - Hologram options
159
155
  * @param {string} [options.sourceAuthorPubKey] - Source author's public key (defaults to client.publicKey)
160
- * @param {string} [options.sourceAuthorPrivKey] - Source author's private key for signing capability (defaults to client.privateKey)
161
- * @param {string} [options.targetAuthorPubKey] - Target author's public key (defaults to client.publicKey)
162
- * @param {string} [options.capability] - Pre-issued capability (if not provided, one will be issued)
163
- * @param {string[]} [options.permissions=['read']] - Permissions for auto-issued capability
164
- * @param {number} [options.expiresIn=null] - Capability expiration in ms (null = no expiration)
165
- * @returns {Promise<Object>} Hologram object with unified structure
156
+ * @param {string} [options.capability] - Ignored (kept for backward compat)
157
+ * @returns {Promise<Object>} Hologram object
166
158
  */
167
159
  export async function createHologramWithCapability(client, sourceHolon, targetHolon, lensName, dataId, appname, options = {}) {
168
- const {
169
- sourceAuthorPubKey = client.publicKey,
170
- sourceAuthorPrivKey = client.privateKey, // Allow passing author's private key for signing
171
- targetAuthorPubKey = client.publicKey,
172
- capability: providedCapability = null,
173
- permissions = ['read'],
174
- expiresIn = null,
175
- } = options;
176
-
177
- let capability = providedCapability;
178
-
179
- // Issue capability if not provided
180
- if (!capability) {
181
- const isSelfCapability = sourceAuthorPubKey === targetAuthorPubKey;
182
-
183
- // Issue capability - for self-caps, no expiration by default
184
- capability = await issueCapability(
185
- permissions,
186
- { holonId: sourceHolon, lensName, dataId },
187
- targetAuthorPubKey,
188
- {
189
- expiresIn: expiresIn !== undefined ? expiresIn : null, // Hologram capabilities don't expire — they live as long as the hologram
190
- issuer: sourceAuthorPubKey,
191
- issuerKey: sourceAuthorPrivKey, // Use passed private key (holon's key when federated via holonsbot)
192
- }
193
- );
194
-
195
- // Store the capability in registry for cross-author federation
196
- if (!isSelfCapability && client.privateKey) {
197
- await storeInboundCapability(client, appname, sourceAuthorPubKey, {
198
- token: capability,
199
- scope: { holonId: sourceHolon, lensName, dataId },
200
- permissions,
201
- isSelfCapability: false,
202
- });
203
- }
204
- }
160
+ const sourceAuthorPubKey = options.sourceAuthorPubKey || client.publicKey;
205
161
 
206
162
  return createHologram(sourceHolon, targetHolon, lensName, dataId, appname, {
207
163
  authorPubKey: sourceAuthorPubKey,
208
- capability,
209
164
  });
210
165
  }
211
166
 
212
167
  /**
213
- * Resolve hologram to actual data, merging local overrides - UNIFIED MODEL
168
+ * Resolve hologram to actual data, merging local overrides.
214
169
  *
215
- * In the unified model, ALL holograms have authorPubKey and capability.
216
- * Resolution always:
217
- * 1. Verifies the capability token
218
- * 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
219
174
  *
220
- * 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.
221
177
  *
222
178
  * @param {Object} client - Nostr client instance
223
179
  * @param {Object} hologram - Hologram object
224
180
  * @param {Set} visited - Visited souls (circular reference detection)
225
181
  * @param {Array} chain - Chain of souls for debugging circular references
226
182
  * @param {Object} options - Resolution options
227
- * @param {string} options.appname - Application namespace (for registry lookup)
183
+ * @param {string} options.appname - Application namespace
228
184
  * @param {boolean} options.deleteCircular - If true, delete circular holograms when detected (default: true)
229
185
  * @param {string} options.hologramPath - Path of the hologram being resolved (for deletion)
230
- * @param {boolean} options.skipCapabilityVerification - Skip capability check (for internal use only)
231
186
  * @returns {Promise<Object|null>} Resolved data with _hologram metadata, or null
232
187
  */
233
188
  export async function resolveHologram(client, hologram, visited = new Set(), chain = [], options = {}) {
234
- const { deleteCircular = true, hologramPath = null, skipCapabilityVerification = false } = options;
189
+ const { deleteCircular = true, hologramPath = null } = options;
235
190
 
236
191
  if (!hologram || !hologram.hologram) {
237
192
  return hologram; // Not a hologram, return as-is
238
193
  }
239
194
 
195
+ // Lens holograms expand at read time (in index.js), not during resolution
196
+ if (hologram.lens === true) {
197
+ return null;
198
+ }
199
+
240
200
  const { soul } = hologram;
241
201
  const target = hologram.target || {};
242
202
 
243
203
  // Circular reference detection
244
204
  if (visited.has(soul)) {
245
205
  const chainStr = [...chain, soul].join(' → ');
246
- console.warn(`🔄 Circular reference detected - removing hologram`);
247
- console.warn(` Soul: ${soul}`);
248
- console.warn(` Chain: ${chainStr}`);
249
- console.warn(` Source holon: ${target.holonId || 'unknown'}`);
250
- console.warn(` Lens: ${target.lensName || 'unknown'}`);
251
- console.warn(` Data ID: ${target.dataId || hologram.id || 'unknown'}`);
252
- console.warn(` Resolution depth: ${visited.size}`);
253
-
254
- // Delete the circular hologram
206
+ console.warn(`Circular reference detected: ${chainStr}`);
207
+
255
208
  if (deleteCircular && hologramPath) {
256
- console.info(` 🗑️ Deleting circular hologram at: ${hologramPath}`);
257
209
  await write(client, hologramPath, null);
258
210
  }
259
211
 
@@ -261,102 +213,20 @@ export async function resolveHologram(client, hologram, visited = new Set(), cha
261
213
  }
262
214
 
263
215
  if (visited.size >= MAX_RESOLUTION_DEPTH) {
264
- const chainStr = [...chain, soul].join(' → ');
265
- console.warn(`⚠️ Max resolution depth (${MAX_RESOLUTION_DEPTH}) exceeded - removing hologram`);
266
- console.warn(` Current soul: ${soul}`);
267
- console.warn(` Chain: ${chainStr}`);
268
- console.warn(` Source holon: ${target.holonId || 'unknown'}`);
269
- console.warn(` Lens: ${target.lensName || 'unknown'}`);
270
-
271
- // Delete the problematic hologram
272
216
  if (deleteCircular && hologramPath) {
273
- console.info(` 🗑️ Deleting deep chain hologram at: ${hologramPath}`);
274
217
  await write(client, hologramPath, null);
275
218
  }
276
-
277
219
  return null;
278
220
  }
279
221
 
280
222
  visited.add(soul);
281
223
  chain.push(soul);
282
224
 
283
- let sourceData;
284
-
285
- // UNIFIED MODEL: All holograms have authorPubKey and capability
286
- // Get capability - try embedded first, then registry fallback
287
- let capability = hologram.capability;
225
+ // Fetch source data — use authorPubKey as author filter if present
288
226
  const authorPubKey = target.authorPubKey;
289
-
290
- // Normalize capability to plain token string
291
- if (capability) {
292
- capability = normalizeTokenString(capability) || capability;
293
- }
294
-
295
- if (!capability && options.appname && authorPubKey) {
296
- // Fallback to registry lookup
297
- const capEntry = await getCapabilityForAuthor(
298
- client,
299
- options.appname,
300
- authorPubKey,
301
- {
302
- holonId: target.holonId,
303
- lensName: target.lensName,
304
- dataId: target.dataId,
305
- }
306
- );
307
- if (capEntry) {
308
- // Extract token string from capability entry
309
- capability = capEntry.token || capEntry;
310
- }
311
- }
312
-
313
- // Verify capability (unified model - always verify unless explicitly skipped)
314
- if (!skipCapabilityVerification) {
315
- if (!capability) {
316
- console.warn(`❌ Hologram missing capability: ${soul}`);
317
- return null;
318
- }
319
-
320
- // OBJECT-CAPABILITY SECURITY: Verify capability issuer matches claimed authorPubKey
321
- // This prevents forged capabilities where someone signs a capability for data they don't own
322
- if (authorPubKey) {
323
- const issuerMatch = await verifyCapabilityIssuer(capability, authorPubKey);
324
- if (!issuerMatch) {
325
- console.warn(`❌ Capability issuer does not match authorPubKey for hologram: ${soul}`);
326
- console.warn(` Expected author: ${authorPubKey?.slice(0, 12)}...`);
327
- const decoded = decodeCapability(capability);
328
- if (decoded) {
329
- console.warn(` Actual issuer: ${decoded.issuer?.slice(0, 12)}...`);
330
- }
331
- return null;
332
- }
333
- }
334
-
335
- const isValid = await verifyCapability(
336
- capability,
337
- 'read',
338
- {
339
- holonId: target.holonId,
340
- lensName: target.lensName,
341
- dataId: target.dataId,
342
- }
343
- );
344
-
345
- if (!isValid) {
346
- console.warn(`❌ Capability verification failed for hologram: ${soul}`);
347
- return null;
348
- }
349
- }
350
-
351
- // UNIFIED MODEL: Always fetch with author filter if authorPubKey is present
352
- if (authorPubKey) {
353
- sourceData = await read(client, soul, {
354
- authors: [authorPubKey]
355
- });
356
- } else {
357
- // Legacy fallback for holograms without authorPubKey
358
- sourceData = await read(client, soul);
359
- }
227
+ const sourceData = authorPubKey
228
+ ? await read(client, soul, { authors: [authorPubKey] })
229
+ : await read(client, soul);
360
230
 
361
231
  if (!sourceData) {
362
232
  return null;
@@ -364,7 +234,6 @@ export async function resolveHologram(client, hologram, visited = new Set(), cha
364
234
 
365
235
  // If source data is also a hologram, recursively resolve
366
236
  if (sourceData.hologram) {
367
- // Pass the soul as the hologramPath so circular holograms can be deleted
368
237
  return resolveHologram(client, sourceData, visited, chain, { ...options, hologramPath: soul });
369
238
  }
370
239
 
@@ -373,7 +242,7 @@ export async function resolveHologram(client, hologram, visited = new Set(), cha
373
242
  }
374
243
 
375
244
  /**
376
- * Merge hologram local overrides with source data - UNIFIED MODEL
245
+ * Merge hologram local overrides with source data.
377
246
  * @private
378
247
  * @param {Object} hologram - Hologram object
379
248
  * @param {Object} sourceData - Source data
@@ -455,10 +324,10 @@ export async function setupFederation(
455
324
  }
456
325
 
457
326
  /**
458
- * Propagate data to federated location - UNIFIED MODEL
327
+ * Propagate data to federated location.
459
328
  *
460
- * In the unified model, all holograms require authorPubKey and capability.
461
- * 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.
462
331
  *
463
332
  * @param {Object} client - Nostr client instance (must have publicKey)
464
333
  * @param {string} appname - Application namespace
@@ -469,8 +338,6 @@ export async function setupFederation(
469
338
  * @param {string} mode - 'reference' or 'copy'
470
339
  * @param {Object} options - Propagation options
471
340
  * @param {string} [options.sourceAuthorPubKey] - Source author (defaults to client.publicKey)
472
- * @param {string} [options.sourceAuthorPrivKey] - Source author's private key for signing capability (defaults to client.privateKey)
473
- * @param {string} [options.capability] - Pre-issued capability (auto-issued if not provided)
474
341
  * @returns {Promise<boolean>} Success indicator
475
342
  */
476
343
  export async function propagateData(
@@ -491,97 +358,13 @@ export async function propagateData(
491
358
  return true;
492
359
  }
493
360
 
494
- // Don't propagate holograms back to avoid circular references
495
- // If this data is already a hologram pointing to the target, skip it
361
+ // Don't propagate holograms holograms should only be created explicitly
496
362
  if (data.hologram === true) {
497
- // Check if this hologram would create a circular reference
498
- if (data.target && data.target.holonId === targetHolon) {
499
- console.info(`🔄 Skipping propagation - would create circular reference:`);
500
- console.info(` Data ID: ${dataId}`);
501
- console.info(` Source holon: ${sourceHolon}`);
502
- console.info(` Target holon: ${targetHolon}`);
503
- console.info(` Hologram points to: ${data.target.holonId}`);
504
- console.info(` Lens: ${lensName}`);
505
- console.info(` Original soul: ${data.soul || 'unknown'}`);
506
- return true;
507
- }
508
-
509
- // Check if targetHolon is already in the original source's activeHolograms
510
- // This prevents creating duplicate holograms when the target already has one
511
- if (data.target) {
512
- const originalSourcePath = buildPath(
513
- data.target.appname || appname,
514
- data.target.holonId,
515
- data.target.lensName || lensName,
516
- data.target.dataId || dataId
517
- );
518
- const originalSource = await read(client, originalSourcePath);
519
-
520
- if (originalSource?._meta?.activeHolograms) {
521
- const alreadyHasHologram = originalSource._meta.activeHolograms.some(
522
- h => h.targetHolon === targetHolon
523
- );
524
- if (alreadyHasHologram) {
525
- console.info(`⏭️ Skipping propagation - target already in activeHolograms:`);
526
- console.info(` Data ID: ${dataId}`);
527
- console.info(` Original source: ${data.target.holonId}`);
528
- console.info(` Target holon: ${targetHolon}`);
529
- return true;
530
- }
531
- }
532
- }
533
-
534
- // Deep circular check for hologram copy - ensure the original source doesn't eventually point to target
535
- const originalSourceHolon = data.target.holonId;
536
- const originalDataId = data.target.dataId || dataId;
537
- const originalLens = data.target.lensName || lensName;
538
- const originalAppname = data.target.appname || appname;
539
-
540
- const circularCheck = await wouldCreateCircularHologram(
541
- client, originalAppname, originalSourceHolon, targetHolon, originalLens, originalDataId
542
- );
543
-
544
- if (circularCheck.isCircular) {
545
- const chainStr = circularCheck.chain.map(c => c.holon).join(' → ');
546
- console.warn(`🔄 Preventing circular hologram copy:`);
547
- console.warn(` Data ID: ${dataId}`);
548
- console.warn(` Original source: ${originalSourceHolon}`);
549
- console.warn(` Would copy to: ${targetHolon}`);
550
- console.warn(` Existing chain: ${chainStr}`);
551
- console.warn(` Reason: ${circularCheck.reason}`);
552
- return false;
553
- }
554
-
555
- // When propagating a hologram, copy it instead of creating a hologram of a hologram
556
- // This maintains the reference to the original source
557
- const targetPath = buildPath(appname, targetHolon, lensName, dataId);
558
- const copiedHologram = {
559
- ...data,
560
- _meta: {
561
- ...data._meta,
562
- copiedFrom: sourceHolon,
563
- copiedAt: Date.now()
564
- }
565
- };
566
- const success = await write(client, targetPath, copiedHologram);
567
-
568
- // Add this new location to the original source's activeHolograms
569
- if (success && data.target) {
570
- await addActiveHologram(
571
- client,
572
- data.target.appname || appname,
573
- data.target.holonId,
574
- data.target.lensName || lensName,
575
- data.target.dataId || dataId,
576
- targetHolon
577
- );
578
- console.info(`📋 Copied hologram to ${targetHolon}:`);
579
- console.info(` Data ID: ${dataId}`);
580
- console.info(` Original source: ${data.target.holonId}`);
581
- console.info(` Copied from: ${sourceHolon}`);
582
- }
583
-
584
- 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;
585
368
  }
586
369
 
587
370
  // For non-hologram data, check if targetHolon is already in activeHolograms
@@ -614,13 +397,9 @@ export async function propagateData(
614
397
  return false;
615
398
  }
616
399
 
617
- // UNIFIED MODEL: Create hologram with capability
400
+ // Create hologram — just a pointer with the author's pubkey
618
401
  const sourceAuthorPubKey = options.sourceAuthorPubKey || client.publicKey;
619
402
 
620
- // Determine targetAuthorPubKey - the person who should be able to read the hologram
621
- // If targetHolon is a 64-char hex pubkey, use it directly as the target author
622
- const targetAuthorPubKey = options.targetAuthorPubKey || (isPubkey(targetHolon) ? targetHolon : client.publicKey);
623
-
624
403
  const hologram = await createHologramWithCapability(
625
404
  client,
626
405
  sourceHolon,
@@ -628,13 +407,7 @@ export async function propagateData(
628
407
  lensName,
629
408
  dataId,
630
409
  appname,
631
- {
632
- sourceAuthorPubKey,
633
- sourceAuthorPrivKey: options.sourceAuthorPrivKey, // Pass through for holon-signed capabilities
634
- targetAuthorPubKey,
635
- capability: options.capability,
636
- permissions: ['read'],
637
- }
410
+ { sourceAuthorPubKey }
638
411
  );
639
412
 
640
413
  const targetPath = buildPath(appname, targetHolon, lensName, dataId);
@@ -708,6 +481,157 @@ export function isHologram(data) {
708
481
  return data && data.hologram === true;
709
482
  }
710
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
+
711
635
  /**
712
636
  * Check if data was resolved from a hologram
713
637
  * @param {Object} data - Data to check