holosphere 2.0.0-alpha11 → 2.0.0-alpha12

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 (60) hide show
  1. package/dist/{2019-D2OG2idw.js → 2019-DQdDE6DG.js} +2 -2
  2. package/dist/{2019-D2OG2idw.js.map → 2019-DQdDE6DG.js.map} +1 -1
  3. package/dist/{2019-EION3wKo.cjs → 2019-Ew-DTDlI.cjs} +2 -2
  4. package/dist/{2019-EION3wKo.cjs.map → 2019-Ew-DTDlI.cjs.map} +1 -1
  5. package/dist/{browser-BSniCNqO.js → browser-CckFyRI9.js} +2 -2
  6. package/dist/{browser-BSniCNqO.js.map → browser-CckFyRI9.js.map} +1 -1
  7. package/dist/browser-D2qtVhH5.cjs +2 -0
  8. package/dist/{browser-Cq59Ij19.cjs.map → browser-D2qtVhH5.cjs.map} +1 -1
  9. package/dist/cjs/holosphere.cjs +1 -1
  10. package/dist/esm/holosphere.js +67 -50
  11. package/dist/index-9sqetkAn.cjs +29 -0
  12. package/dist/index-9sqetkAn.cjs.map +1 -0
  13. package/dist/index-C0ITDyFo.cjs +13 -0
  14. package/dist/index-C0ITDyFo.cjs.map +1 -0
  15. package/dist/{index-Bl6rM1NW.js → index-DrYM1LOY.js} +2 -2
  16. package/dist/{index-Bl6rM1NW.js.map → index-DrYM1LOY.js.map} +1 -1
  17. package/dist/{index-D-jZhliX.js → index-TDDyakLc.js} +19827 -720
  18. package/dist/index-TDDyakLc.js.map +1 -0
  19. package/dist/{index-Bwg3OzRM.cjs → index-kyf1sjaC.cjs} +3 -3
  20. package/dist/{index-Bwg3OzRM.cjs.map → index-kyf1sjaC.cjs.map} +1 -1
  21. package/dist/index-lbSQUoRz.js +11663 -0
  22. package/dist/index-lbSQUoRz.js.map +1 -0
  23. package/dist/{indexeddb-storage-5eiUNsHC.js → indexeddb-storage-CXhjqwhA.js} +2 -2
  24. package/dist/{indexeddb-storage-5eiUNsHC.js.map → indexeddb-storage-CXhjqwhA.js.map} +1 -1
  25. package/dist/{indexeddb-storage-FNFUVvTJ.cjs → indexeddb-storage-DFESDYIj.cjs} +2 -2
  26. package/dist/{indexeddb-storage-FNFUVvTJ.cjs.map → indexeddb-storage-DFESDYIj.cjs.map} +1 -1
  27. package/dist/{memory-storage-DMt36uZO.cjs → memory-storage-D1tc1bjk.cjs} +2 -2
  28. package/dist/{memory-storage-DMt36uZO.cjs.map → memory-storage-D1tc1bjk.cjs.map} +1 -1
  29. package/dist/{memory-storage-CI-gfmuG.js → memory-storage-DkewsdcM.js} +2 -2
  30. package/dist/{memory-storage-CI-gfmuG.js.map → memory-storage-DkewsdcM.js.map} +1 -1
  31. package/docs/FEDERATION.md +474 -0
  32. package/package.json +3 -1
  33. package/src/crypto/nostr-utils.js +7 -0
  34. package/src/crypto/secp256k1.js +46 -29
  35. package/src/federation/capabilities.js +162 -0
  36. package/src/federation/card-storage.js +304 -0
  37. package/src/federation/handshake.js +204 -7
  38. package/src/federation/hologram.js +167 -58
  39. package/src/federation/holon-registry.js +166 -0
  40. package/src/federation/index.js +68 -0
  41. package/src/federation/registry.js +110 -2
  42. package/src/federation/request-card.js +373 -0
  43. package/src/hierarchical/upcast.js +19 -3
  44. package/src/index.js +162 -76
  45. package/src/lib/federation-methods.js +527 -5
  46. package/src/storage/nostr-async.js +2 -2
  47. package/src/storage/nostr-client.js +370 -155
  48. package/src/storage/nostr-wrapper.js +1 -1
  49. package/dist/_commonjsHelpers-C37NGDzP.cjs +0 -2
  50. package/dist/_commonjsHelpers-C37NGDzP.cjs.map +0 -1
  51. package/dist/_commonjsHelpers-CUmg6egw.js +0 -7
  52. package/dist/_commonjsHelpers-CUmg6egw.js.map +0 -1
  53. package/dist/browser-Cq59Ij19.cjs +0 -2
  54. package/dist/index-D-jZhliX.js.map +0 -1
  55. package/dist/index-Dc6Z8Aob.cjs +0 -18
  56. package/dist/index-Dc6Z8Aob.cjs.map +0 -1
  57. package/dist/secp256k1-CEwJNcfV.js +0 -1890
  58. package/dist/secp256k1-CEwJNcfV.js.map +0 -1
  59. package/dist/secp256k1-CiEONUnj.cjs +0 -12
  60. package/dist/secp256k1-CiEONUnj.cjs.map +0 -1
@@ -15,8 +15,11 @@ import {
15
15
  createDMEvent,
16
16
  hexToNpub,
17
17
  generateNonce,
18
+ getPublicKey,
18
19
  } from '../crypto/nostr-utils.js';
19
20
  import { addFederatedPartner, storeInboundCapability } from './registry.js';
21
+ import { issueCapability } from '../crypto/secp256k1.js';
22
+ import * as cardStorage from './card-storage.js';
20
23
 
21
24
  // ============================================================================
22
25
  // Types (documented for reference)
@@ -200,17 +203,36 @@ export async function sendFederationResponse(client, privateKey, recipientPubKey
200
203
  * @param {Object} handlers
201
204
  * @param {Function} handlers.onRequest - Called with (request, senderPubKey)
202
205
  * @param {Function} handlers.onResponse - Called with (response, senderPubKey)
206
+ * @param {Object} options - Subscription options
207
+ * @param {string} options.appname - Application namespace (for persistent storage)
203
208
  * @returns {Function} Unsubscribe function
204
209
  */
205
- export function subscribeToFederationDMs(client, privateKey, publicKey, handlers) {
210
+ export function subscribeToFederationDMs(client, privateKey, publicKey, handlers, options = {}) {
206
211
  const { onRequest, onResponse } = handlers;
212
+ const { appname } = options;
207
213
  let isActive = true;
214
+
215
+ // In-memory deduplication for this session
208
216
  const processedEventIds = new Set();
209
217
 
218
+ // Track processed responses persistently to prevent duplicate notifications
219
+ let processedResponseIds = new Set();
220
+
221
+ // Load processed responses from storage if appname is provided
222
+ const loadProcessedResponses = async () => {
223
+ if (appname && client?.client) {
224
+ try {
225
+ processedResponseIds = await cardStorage.getProcessedResponseIds(client.client, appname);
226
+ } catch (err) {
227
+ console.warn('[Handshake] Could not load processed responses:', err.message);
228
+ }
229
+ }
230
+ };
231
+
210
232
  const handleEvent = async (event) => {
211
233
  if (!isActive) return;
212
234
 
213
- // Skip duplicates
235
+ // Skip duplicates (in-memory check for this session)
214
236
  if (processedEventIds.has(event.id)) return;
215
237
  processedEventIds.add(event.id);
216
238
 
@@ -234,9 +256,31 @@ export function subscribeToFederationDMs(client, privateKey, publicKey, handlers
234
256
  const payload = JSON.parse(decrypted);
235
257
 
236
258
  if (payload.type === 'federation_request' && payload.version === '1.0') {
259
+ // Check if this request was already dismissed
260
+ if (appname && client?.client) {
261
+ const isDismissed = await cardStorage.isRequestDismissed(client.client, appname, payload.requestId);
262
+ if (isDismissed) {
263
+ console.log('[Handshake] Skipping dismissed request:', payload.requestId);
264
+ return;
265
+ }
266
+ }
267
+
237
268
  console.log('[Handshake] Received federation request from:', event.pubkey.substring(0, 8) + '...');
238
269
  onRequest?.(payload, event.pubkey);
239
270
  } else if (payload.type === 'federation_response' && payload.version === '1.0') {
271
+ // Check if this response was already processed (persistent check)
272
+ const responseKey = `${payload.requestId}_${event.pubkey}`;
273
+ if (processedResponseIds.has(responseKey)) {
274
+ console.log('[Handshake] Skipping already processed response:', responseKey);
275
+ return;
276
+ }
277
+
278
+ // Mark as processed in persistent storage
279
+ if (appname && client?.client) {
280
+ await cardStorage.markResponseProcessed(client.client, appname, payload.requestId, event.pubkey);
281
+ processedResponseIds.add(responseKey);
282
+ }
283
+
240
284
  console.log('[Handshake] Received federation response from:', event.pubkey.substring(0, 8) + '...');
241
285
  onResponse?.(payload, event.pubkey);
242
286
  }
@@ -250,6 +294,9 @@ export function subscribeToFederationDMs(client, privateKey, publicKey, handlers
250
294
  let subscription = null;
251
295
 
252
296
  const startSubscription = async () => {
297
+ // Load processed responses first
298
+ await loadProcessedResponses();
299
+
253
300
  // Get the NostrClient from HoloSphere instance
254
301
  const nostrClient = client?.client;
255
302
  if (!nostrClient?.subscribe) {
@@ -313,16 +360,39 @@ export async function initiateFederationHandshake(holosphere, privateKey, params
313
360
 
314
361
  try {
315
362
  // Get sender's public key
316
- const { getPublicKey } = await import('../crypto/nostr-utils.js');
317
363
  const senderPubKey = getPublicKey(privateKey);
318
364
 
365
+ // Issue capabilities for outbound lenses (lenses we're sharing with partner)
366
+ const capabilities = [];
367
+ for (const lensName of (lensConfig.outbound || [])) {
368
+ try {
369
+ const token = await issueCapability(
370
+ ['read'],
371
+ { holonId, lensName, dataId: '*' },
372
+ partnerPubKey,
373
+ {
374
+ expiresIn: 365 * 24 * 60 * 60 * 1000, // 1 year
375
+ issuer: senderPubKey,
376
+ issuerKey: privateKey,
377
+ }
378
+ );
379
+ capabilities.push({
380
+ token,
381
+ scope: { holonId, lensName },
382
+ permissions: ['read'],
383
+ });
384
+ } catch (err) {
385
+ console.warn(`[Handshake] Failed to issue capability for ${lensName}:`, err.message);
386
+ }
387
+ }
388
+
319
389
  // Create federation request
320
390
  const request = createFederationRequest({
321
391
  senderHolonId: holonId,
322
392
  senderHolonName: holonName,
323
393
  senderPubKey,
324
394
  lensConfig,
325
- capabilities: [],
395
+ capabilities,
326
396
  message,
327
397
  });
328
398
 
@@ -373,7 +443,6 @@ export async function acceptFederationRequest(holosphere, privateKey, params) {
373
443
 
374
444
  try {
375
445
  // Get responder's public key
376
- const { getPublicKey } = await import('../crypto/nostr-utils.js');
377
446
  const responderPubKey = getPublicKey(privateKey);
378
447
 
379
448
  // Add sender as federated partner in Nostr registry
@@ -399,6 +468,30 @@ export async function acceptFederationRequest(holosphere, privateKey, params) {
399
468
  partnerName: request.senderHolonName
400
469
  });
401
470
 
471
+ // Issue capabilities for our outbound lenses (lenses we're sharing with partner)
472
+ const capabilities = [];
473
+ for (const lensName of (lensConfig.outbound || [])) {
474
+ try {
475
+ const token = await issueCapability(
476
+ ['read'],
477
+ { holonId, lensName, dataId: '*' },
478
+ senderPubKey,
479
+ {
480
+ expiresIn: 365 * 24 * 60 * 60 * 1000, // 1 year
481
+ issuer: responderPubKey,
482
+ issuerKey: privateKey,
483
+ }
484
+ );
485
+ capabilities.push({
486
+ token,
487
+ scope: { holonId, lensName },
488
+ permissions: ['read'],
489
+ });
490
+ } catch (err) {
491
+ console.warn(`[Handshake] Failed to issue capability for ${lensName}:`, err.message);
492
+ }
493
+ }
494
+
402
495
  // Create and send response
403
496
  const response = createFederationResponse({
404
497
  requestId: request.requestId,
@@ -407,14 +500,50 @@ export async function acceptFederationRequest(holosphere, privateKey, params) {
407
500
  responderHolonName: holonName,
408
501
  responderPubKey,
409
502
  lensConfig,
410
- capabilities: [],
503
+ capabilities,
411
504
  message,
412
505
  });
413
506
 
414
507
  const sent = await sendFederationResponse(holosphere.client, privateKey, senderPubKey, response);
415
508
 
416
509
  if (sent) {
417
- return { success: true };
510
+ // Dismiss the request so it won't reappear
511
+ if (holosphere.client && request.requestId) {
512
+ await cardStorage.dismissRequest(holosphere.client, holosphere.config.appName, request.requestId);
513
+ console.log('[Handshake] Request dismissed after acceptance:', request.requestId);
514
+ }
515
+
516
+ // Auto-receive federated lens data as holograms for configured inbound lenses
517
+ // This enables the user to immediately see the partner's data in their holon
518
+ const receivedHolograms = {};
519
+ const inboundLenses = lensConfig.inbound || [];
520
+
521
+ if (inboundLenses.length > 0 && holosphere.receiveFederatedLens) {
522
+ console.log('[Handshake] Receiving federated lens data as holograms...');
523
+
524
+ for (const lensName of inboundLenses) {
525
+ try {
526
+ // The sender's holon ID comes from the request
527
+ const senderHolonId = request.senderHolonId;
528
+
529
+ const result = await holosphere.receiveFederatedLens(
530
+ senderPubKey,
531
+ senderHolonId,
532
+ lensName,
533
+ holonId,
534
+ { overwrite: false }
535
+ );
536
+
537
+ receivedHolograms[lensName] = result;
538
+ console.log(`[Handshake] Received ${result.received} holograms for lens "${lensName}"`);
539
+ } catch (err) {
540
+ console.warn(`[Handshake] Failed to receive holograms for lens "${lensName}":`, err.message);
541
+ receivedHolograms[lensName] = { error: err.message };
542
+ }
543
+ }
544
+ }
545
+
546
+ return { success: true, requestId: request.requestId, receivedHolograms };
418
547
  } else {
419
548
  return { success: false, error: 'Failed to send response DM' };
420
549
  }
@@ -452,6 +581,74 @@ export async function rejectFederationRequest(holosphere, privateKey, params) {
452
581
  }
453
582
  }
454
583
 
584
+ /**
585
+ * Process a federation response (called by initiator when receiving acceptance)
586
+ * Stores capabilities from the responder in the local registry and receives holograms
587
+ * @param {Object} holosphere - HoloSphere instance
588
+ * @param {Object} response - Federation response payload
589
+ * @param {string} responderPubKey - Responder's public key
590
+ * @param {Object} [options={}] - Processing options
591
+ * @param {string} [options.holonId] - Initiator's holon ID (for receiving holograms)
592
+ * @param {string[]} [options.inboundLenses] - Lenses to receive from responder
593
+ * @returns {Promise<Object>} Result with success, stored capability count, and received holograms
594
+ */
595
+ export async function processFederationResponse(holosphere, response, responderPubKey, options = {}) {
596
+ if (!response || response.status !== 'accepted') {
597
+ return { success: false, error: 'Response not accepted', storedCapabilities: 0 };
598
+ }
599
+
600
+ const { holonId, inboundLenses = [] } = options;
601
+ let storedCapabilities = 0;
602
+
603
+ // Store capabilities from the responder
604
+ if (holosphere.client && response.capabilities && response.capabilities.length > 0) {
605
+ for (const cap of response.capabilities) {
606
+ try {
607
+ await storeInboundCapability(holosphere.client, holosphere.config.appName, responderPubKey, cap);
608
+ storedCapabilities++;
609
+ } catch (err) {
610
+ console.warn(`[Handshake] Failed to store capability:`, err.message);
611
+ }
612
+ }
613
+ }
614
+
615
+ // Add responder as federated partner if not already
616
+ if (holosphere.client) {
617
+ await addFederatedPartner(holosphere.client, holosphere.config.appName, responderPubKey, {
618
+ alias: response.responderHolonName,
619
+ addedVia: 'handshake_accepted',
620
+ });
621
+ }
622
+
623
+ // Auto-receive federated lens data as holograms for configured inbound lenses
624
+ const receivedHolograms = {};
625
+ const responderHolonId = response.responderHolonId;
626
+
627
+ if (holonId && responderHolonId && inboundLenses.length > 0 && holosphere.receiveFederatedLens) {
628
+ console.log('[Handshake] Receiving federated lens data as holograms from responder...');
629
+
630
+ for (const lensName of inboundLenses) {
631
+ try {
632
+ const result = await holosphere.receiveFederatedLens(
633
+ responderPubKey,
634
+ responderHolonId,
635
+ lensName,
636
+ holonId,
637
+ { overwrite: false }
638
+ );
639
+
640
+ receivedHolograms[lensName] = result;
641
+ console.log(`[Handshake] Received ${result.received} holograms for lens "${lensName}"`);
642
+ } catch (err) {
643
+ console.warn(`[Handshake] Failed to receive holograms for lens "${lensName}":`, err.message);
644
+ receivedHolograms[lensName] = { error: err.message };
645
+ }
646
+ }
647
+ }
648
+
649
+ return { success: true, storedCapabilities, receivedHolograms };
650
+ }
651
+
455
652
  // ============================================================================
456
653
  // Payload Validation
457
654
  // ============================================================================
@@ -1,17 +1,23 @@
1
1
  /**
2
- * @fileoverview Federation and Hologram (Reference) Management.
2
+ * @fileoverview Unified Federation and Hologram (Reference) Management.
3
3
  *
4
4
  * Provides hologram (lightweight reference) creation, resolution, and management.
5
5
  * Holograms enable data to appear in multiple holons while maintaining a single
6
- * source of truth. Supports circular reference detection and cross-holosphere
7
- * federation with capability-based access control.
6
+ * source of truth.
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)
11
+ *
12
+ * This unifies "in-federation" (same author) and "cross-federation" (different authors)
13
+ * into a single consistent model.
8
14
  *
9
15
  * @module federation/hologram
10
16
  */
11
17
 
12
18
  import { buildPath, write, read, update } from '../storage/unified-storage.js';
13
- import { verifyCapability } from '../crypto/secp256k1.js';
14
- import { getCapabilityForAuthor } from './registry.js';
19
+ import { verifyCapability, issueCapability } from '../crypto/secp256k1.js';
20
+ import { getCapabilityForAuthor, storeInboundCapability } from './registry.js';
15
21
 
16
22
  /** @constant {number} Maximum depth for hologram resolution chain */
17
23
  const MAX_RESOLUTION_DEPTH = 10;
@@ -79,19 +85,32 @@ export async function wouldCreateCircularHologram(client, appname, sourceHolon,
79
85
  }
80
86
 
81
87
  /**
82
- * Create a hologram (lightweight reference)
88
+ * Create a hologram (lightweight reference) - UNIFIED MODEL
89
+ *
90
+ * All holograms now include authorPubKey and capability for consistency.
91
+ * This unifies "in-federation" (same author) and "cross-federation" (different authors).
92
+ *
83
93
  * @param {string} sourceHolon - Source holon ID
84
94
  * @param {string} targetHolon - Target holon ID
85
95
  * @param {string} lensName - Lens name
86
96
  * @param {string} dataId - Data ID
87
97
  * @param {string} appname - Application namespace
88
- * @param {Object} options - Optional cross-holosphere settings
89
- * @param {string} options.authorPubKey - Source author's public key (for cross-holosphere)
90
- * @param {string} options.capability - Capability token (for cross-holosphere)
91
- * @returns {Object} Hologram object
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
92
103
  */
93
104
  export function createHologram(sourceHolon, targetHolon, lensName, dataId, appname, options = {}) {
94
- const { authorPubKey = null, capability = null } = options;
105
+ const { authorPubKey, capability } = options;
106
+
107
+ // Validate required fields for unified model
108
+ if (!authorPubKey) {
109
+ throw new Error('authorPubKey is required for hologram creation (unified model)');
110
+ }
111
+ if (!capability) {
112
+ throw new Error('capability is required for hologram creation (unified model)');
113
+ }
95
114
 
96
115
  const soul = buildPath(appname, sourceHolon, lensName, dataId);
97
116
 
@@ -104,32 +123,95 @@ export function createHologram(sourceHolon, targetHolon, lensName, dataId, appna
104
123
  holonId: sourceHolon,
105
124
  lensName,
106
125
  dataId,
126
+ authorPubKey, // Always present in unified model
107
127
  },
128
+ capability, // Always present in unified model
108
129
  _meta: {
109
130
  created: Date.now(),
110
131
  sourceHolon,
111
132
  source: sourceHolon, // Alias for compatibility
133
+ sourcePubKey: authorPubKey,
134
+ grantedAt: Date.now(),
112
135
  },
113
136
  };
114
137
 
115
- // Cross-holosphere enhancements
116
- if (authorPubKey) {
117
- hologram.crossHolosphere = true;
118
- hologram.target.authorPubKey = authorPubKey;
119
- hologram._meta.sourcePubKey = authorPubKey;
120
- }
138
+ return hologram;
139
+ }
140
+
141
+ /**
142
+ * Create a hologram with automatic capability issuance - UNIFIED MODEL
143
+ *
144
+ * This is the preferred method for creating holograms. It automatically issues
145
+ * a capability token (self-capability if same author, cross-capability otherwise).
146
+ *
147
+ * @param {Object} client - Nostr client instance (must have publicKey and optionally privateKey)
148
+ * @param {string} sourceHolon - Source holon ID
149
+ * @param {string} targetHolon - Target holon ID
150
+ * @param {string} lensName - Lens name
151
+ * @param {string} dataId - Data ID
152
+ * @param {string} appname - Application namespace
153
+ * @param {Object} options - Hologram options
154
+ * @param {string} [options.sourceAuthorPubKey] - Source author's public key (defaults to client.publicKey)
155
+ * @param {string} [options.targetAuthorPubKey] - Target author's public key (defaults to client.publicKey)
156
+ * @param {string} [options.capability] - Pre-issued capability (if not provided, one will be issued)
157
+ * @param {string[]} [options.permissions=['read']] - Permissions for auto-issued capability
158
+ * @param {number} [options.expiresIn] - Capability expiration (null for self-caps = no expiration)
159
+ * @returns {Promise<Object>} Hologram object with unified structure
160
+ */
161
+ export async function createHologramWithCapability(client, sourceHolon, targetHolon, lensName, dataId, appname, options = {}) {
162
+ const {
163
+ sourceAuthorPubKey = client.publicKey,
164
+ targetAuthorPubKey = client.publicKey,
165
+ capability: providedCapability = null,
166
+ permissions = ['read'],
167
+ expiresIn = null,
168
+ } = options;
169
+
170
+ let capability = providedCapability;
171
+
172
+ // Issue capability if not provided
173
+ if (!capability) {
174
+ const isSelfCapability = sourceAuthorPubKey === targetAuthorPubKey;
175
+
176
+ // Issue capability - for self-caps, no expiration by default
177
+ capability = await issueCapability(
178
+ permissions,
179
+ { holonId: sourceHolon, lensName, dataId },
180
+ targetAuthorPubKey,
181
+ {
182
+ expiresIn: expiresIn !== null ? expiresIn : (isSelfCapability ? 365 * 24 * 60 * 60 * 1000 : 3600000), // 1 year for self, 1 hour for cross
183
+ issuer: sourceAuthorPubKey,
184
+ issuerKey: client.privateKey,
185
+ }
186
+ );
121
187
 
122
- if (capability) {
123
- hologram.capability = capability;
124
- hologram._meta.grantedAt = Date.now();
188
+ // Store the capability in registry for cross-author federation
189
+ if (!isSelfCapability && client.privateKey) {
190
+ await storeInboundCapability(client, appname, sourceAuthorPubKey, {
191
+ token: capability,
192
+ scope: { holonId: sourceHolon, lensName, dataId },
193
+ permissions,
194
+ isSelfCapability: false,
195
+ });
196
+ }
125
197
  }
126
198
 
127
- return hologram;
199
+ return createHologram(sourceHolon, targetHolon, lensName, dataId, appname, {
200
+ authorPubKey: sourceAuthorPubKey,
201
+ capability,
202
+ });
128
203
  }
129
204
 
130
205
  /**
131
- * Resolve hologram to actual data, merging local overrides
132
- * Supports cross-holosphere holograms with capability token verification
206
+ * Resolve hologram to actual data, merging local overrides - UNIFIED MODEL
207
+ *
208
+ * In the unified model, ALL holograms have authorPubKey and capability.
209
+ * Resolution always:
210
+ * 1. Verifies the capability token
211
+ * 2. Fetches data with author filter
212
+ *
213
+ * This provides consistent security for both self and cross-author holograms.
214
+ *
133
215
  * @param {Object} client - Nostr client instance
134
216
  * @param {Object} hologram - Hologram object
135
217
  * @param {Set} visited - Visited souls (circular reference detection)
@@ -138,10 +220,11 @@ export function createHologram(sourceHolon, targetHolon, lensName, dataId, appna
138
220
  * @param {string} options.appname - Application namespace (for registry lookup)
139
221
  * @param {boolean} options.deleteCircular - If true, delete circular holograms when detected (default: true)
140
222
  * @param {string} options.hologramPath - Path of the hologram being resolved (for deletion)
223
+ * @param {boolean} options.skipCapabilityVerification - Skip capability check (for internal use only)
141
224
  * @returns {Promise<Object|null>} Resolved data with _hologram metadata, or null
142
225
  */
143
226
  export async function resolveHologram(client, hologram, visited = new Set(), chain = [], options = {}) {
144
- const { deleteCircular = true, hologramPath = null } = options;
227
+ const { deleteCircular = true, hologramPath = null, skipCapabilityVerification = false } = options;
145
228
 
146
229
  if (!hologram || !hologram.hologram) {
147
230
  return hologram; // Not a hologram, return as-is
@@ -192,31 +275,32 @@ export async function resolveHologram(client, hologram, visited = new Set(), cha
192
275
 
193
276
  let sourceData;
194
277
 
195
- // Handle cross-holosphere holograms
196
- if (hologram.crossHolosphere && target.authorPubKey) {
197
- // Get capability - try embedded first, then registry fallback
198
- let capability = hologram.capability;
199
-
200
- if (!capability && options.appname) {
201
- // Fallback to registry lookup
202
- const capEntry = await getCapabilityForAuthor(
203
- client,
204
- options.appname,
205
- target.authorPubKey,
206
- {
207
- holonId: target.holonId,
208
- lensName: target.lensName,
209
- dataId: target.dataId,
210
- }
211
- );
212
- if (capEntry) {
213
- capability = capEntry.token;
278
+ // UNIFIED MODEL: All holograms have authorPubKey and capability
279
+ // Get capability - try embedded first, then registry fallback
280
+ let capability = hologram.capability;
281
+ const authorPubKey = target.authorPubKey;
282
+
283
+ if (!capability && options.appname && authorPubKey) {
284
+ // Fallback to registry lookup
285
+ const capEntry = await getCapabilityForAuthor(
286
+ client,
287
+ options.appname,
288
+ authorPubKey,
289
+ {
290
+ holonId: target.holonId,
291
+ lensName: target.lensName,
292
+ dataId: target.dataId,
214
293
  }
294
+ );
295
+ if (capEntry) {
296
+ capability = capEntry.token;
215
297
  }
298
+ }
216
299
 
217
- // Verify capability before resolving
300
+ // Verify capability (unified model - always verify unless explicitly skipped)
301
+ if (!skipCapabilityVerification) {
218
302
  if (!capability) {
219
- console.warn(`❌ Cross-holosphere hologram missing capability: ${soul}`);
303
+ console.warn(`❌ Hologram missing capability: ${soul}`);
220
304
  return null;
221
305
  }
222
306
 
@@ -231,16 +315,18 @@ export async function resolveHologram(client, hologram, visited = new Set(), cha
231
315
  );
232
316
 
233
317
  if (!isValid) {
234
- console.warn(`❌ Capability verification failed for cross-holosphere hologram: ${soul}`);
318
+ console.warn(`❌ Capability verification failed for hologram: ${soul}`);
235
319
  return null;
236
320
  }
321
+ }
237
322
 
238
- // Read from federated source (different author)
323
+ // UNIFIED MODEL: Always fetch with author filter if authorPubKey is present
324
+ if (authorPubKey) {
239
325
  sourceData = await read(client, soul, {
240
- authors: [target.authorPubKey]
326
+ authors: [authorPubKey]
241
327
  });
242
328
  } else {
243
- // Same-holosphere resolution (original behavior)
329
+ // Legacy fallback for holograms without authorPubKey
244
330
  sourceData = await read(client, soul);
245
331
  }
246
332
 
@@ -259,7 +345,7 @@ export async function resolveHologram(client, hologram, visited = new Set(), cha
259
345
  }
260
346
 
261
347
  /**
262
- * Merge hologram local overrides with source data
348
+ * Merge hologram local overrides with source data - UNIFIED MODEL
263
349
  * @private
264
350
  * @param {Object} hologram - Hologram object
265
351
  * @param {Object} sourceData - Source data
@@ -267,7 +353,7 @@ export async function resolveHologram(client, hologram, visited = new Set(), cha
267
353
  */
268
354
  function mergeHologramWithSource(hologram, sourceData) {
269
355
  // Extract local overrides from the hologram (exclude hologram-specific fields)
270
- const hologramOnlyFields = ['hologram', 'soul', 'target', '_meta', 'id', 'capability', 'crossHolosphere'];
356
+ const hologramOnlyFields = ['hologram', 'soul', 'target', '_meta', 'id', 'capability'];
271
357
  const localOverrides = {};
272
358
  const localOverrideKeys = [];
273
359
 
@@ -280,6 +366,8 @@ function mergeHologramWithSource(hologram, sourceData) {
280
366
 
281
367
  // Merge: source data + local overrides + hologram metadata
282
368
  const sourceHolon = hologram._meta?.sourceHolon || hologram._meta?.source || hologram.target?.holonId;
369
+ const sourcePubKey = hologram.target?.authorPubKey || hologram._meta?.sourcePubKey || null;
370
+
283
371
  const resolved = {
284
372
  ...sourceData, // Original data from source
285
373
  ...localOverrides, // Local overrides (x, y, pinned, etc.)
@@ -288,8 +376,7 @@ function mergeHologramWithSource(hologram, sourceData) {
288
376
  soul: hologram.soul,
289
377
  sourceHolon: sourceHolon,
290
378
  localOverrides: localOverrideKeys,
291
- crossHolosphere: hologram.crossHolosphere || false,
292
- sourcePubKey: hologram._meta?.sourcePubKey || null,
379
+ sourcePubKey: sourcePubKey, // Always present in unified model
293
380
  }
294
381
  };
295
382
 
@@ -340,14 +427,21 @@ export async function setupFederation(
340
427
  }
341
428
 
342
429
  /**
343
- * Propagate data to federated location
344
- * @param {Object} client - Nostr client instance
430
+ * Propagate data to federated location - UNIFIED MODEL
431
+ *
432
+ * In the unified model, all holograms require authorPubKey and capability.
433
+ * This function automatically issues capabilities for propagation.
434
+ *
435
+ * @param {Object} client - Nostr client instance (must have publicKey)
345
436
  * @param {string} appname - Application namespace
346
437
  * @param {Object} data - Data to propagate
347
438
  * @param {string} sourceHolon - Source holon ID
348
439
  * @param {string} targetHolon - Target holon ID
349
440
  * @param {string} lensName - Lens name
350
441
  * @param {string} mode - 'reference' or 'copy'
442
+ * @param {Object} options - Propagation options
443
+ * @param {string} [options.sourceAuthorPubKey] - Source author (defaults to client.publicKey)
444
+ * @param {string} [options.capability] - Pre-issued capability (auto-issued if not provided)
351
445
  * @returns {Promise<boolean>} Success indicator
352
446
  */
353
447
  export async function propagateData(
@@ -357,7 +451,8 @@ export async function propagateData(
357
451
  sourceHolon,
358
452
  targetHolon,
359
453
  lensName,
360
- mode = 'reference'
454
+ mode = 'reference',
455
+ options = {}
361
456
  ) {
362
457
  const dataId = data.id;
363
458
 
@@ -490,8 +585,22 @@ export async function propagateData(
490
585
  return false;
491
586
  }
492
587
 
493
- // Create hologram in target
494
- const hologram = createHologram(sourceHolon, targetHolon, lensName, dataId, appname);
588
+ // UNIFIED MODEL: Create hologram with capability
589
+ const sourceAuthorPubKey = options.sourceAuthorPubKey || client.publicKey;
590
+ const hologram = await createHologramWithCapability(
591
+ client,
592
+ sourceHolon,
593
+ targetHolon,
594
+ lensName,
595
+ dataId,
596
+ appname,
597
+ {
598
+ sourceAuthorPubKey,
599
+ capability: options.capability,
600
+ permissions: ['read'],
601
+ }
602
+ );
603
+
495
604
  const targetPath = buildPath(appname, targetHolon, lensName, dataId);
496
605
  const success = await write(client, targetPath, hologram);
497
606