holosphere 2.0.0-alpha11 → 2.0.0-alpha13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/{2019-D2OG2idw.js → 2019-CLMqIAfQ.js} +1722 -1668
  2. package/dist/{2019-D2OG2idw.js.map → 2019-CLMqIAfQ.js.map} +1 -1
  3. package/dist/2019-Cp3uYhyY.cjs +8 -0
  4. package/dist/{2019-EION3wKo.cjs.map → 2019-Cp3uYhyY.cjs.map} +1 -1
  5. package/dist/browser-D6cNVl0v.cjs +2 -0
  6. package/dist/{browser-Cq59Ij19.cjs.map → browser-D6cNVl0v.cjs.map} +1 -1
  7. package/dist/{browser-BSniCNqO.js → browser-nUQt1cnB.js} +2 -2
  8. package/dist/{browser-BSniCNqO.js.map → browser-nUQt1cnB.js.map} +1 -1
  9. package/dist/cjs/holosphere.cjs +1 -1
  10. package/dist/esm/holosphere.js +67 -50
  11. package/dist/{index-D-jZhliX.js → index-BN_uoxQK.js} +20324 -735
  12. package/dist/index-BN_uoxQK.js.map +1 -0
  13. package/dist/{index-Bl6rM1NW.js → index-CoAjtqsD.js} +2 -2
  14. package/dist/{index-Bl6rM1NW.js.map → index-CoAjtqsD.js.map} +1 -1
  15. package/dist/{index-Bwg3OzRM.cjs → index-Cp3tI53z.cjs} +3 -3
  16. package/dist/{index-Bwg3OzRM.cjs.map → index-Cp3tI53z.cjs.map} +1 -1
  17. package/dist/index-DJjGSwXG.cjs +13 -0
  18. package/dist/index-DJjGSwXG.cjs.map +1 -0
  19. package/dist/index-V8EHMYEY.cjs +29 -0
  20. package/dist/index-V8EHMYEY.cjs.map +1 -0
  21. package/dist/index-Z5TstN1e.js +11663 -0
  22. package/dist/index-Z5TstN1e.js.map +1 -0
  23. package/dist/indexeddb-storage-CZK5A7XH.cjs +2 -0
  24. package/dist/indexeddb-storage-CZK5A7XH.cjs.map +1 -0
  25. package/dist/{indexeddb-storage-5eiUNsHC.js → indexeddb-storage-bpA01pAU.js} +39 -2
  26. package/dist/indexeddb-storage-bpA01pAU.js.map +1 -0
  27. package/dist/{memory-storage-DMt36uZO.cjs → memory-storage-B1k8Jszd.cjs} +2 -2
  28. package/dist/{memory-storage-DMt36uZO.cjs.map → memory-storage-B1k8Jszd.cjs.map} +1 -1
  29. package/dist/{memory-storage-CI-gfmuG.js → memory-storage-BqhmytP_.js} +2 -2
  30. package/dist/{memory-storage-CI-gfmuG.js.map → memory-storage-BqhmytP_.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 +104 -38
  35. package/src/federation/capabilities.js +162 -0
  36. package/src/federation/card-storage.js +376 -0
  37. package/src/federation/handshake.js +561 -9
  38. package/src/federation/hologram.js +194 -57
  39. package/src/federation/holon-registry.js +187 -0
  40. package/src/federation/index.js +68 -0
  41. package/src/federation/registry.js +164 -6
  42. package/src/federation/request-card.js +373 -0
  43. package/src/hierarchical/upcast.js +19 -3
  44. package/src/index.js +209 -75
  45. package/src/lib/federation-methods.js +527 -5
  46. package/src/storage/indexeddb-storage.js +41 -0
  47. package/src/storage/nostr-async.js +14 -5
  48. package/src/storage/nostr-client.js +471 -155
  49. package/src/storage/nostr-wrapper.js +6 -3
  50. package/dist/2019-EION3wKo.cjs +0 -8
  51. package/dist/_commonjsHelpers-C37NGDzP.cjs +0 -2
  52. package/dist/_commonjsHelpers-C37NGDzP.cjs.map +0 -1
  53. package/dist/_commonjsHelpers-CUmg6egw.js +0 -7
  54. package/dist/_commonjsHelpers-CUmg6egw.js.map +0 -1
  55. package/dist/browser-Cq59Ij19.cjs +0 -2
  56. package/dist/index-D-jZhliX.js.map +0 -1
  57. package/dist/index-Dc6Z8Aob.cjs +0 -18
  58. package/dist/index-Dc6Z8Aob.cjs.map +0 -1
  59. package/dist/indexeddb-storage-5eiUNsHC.js.map +0 -1
  60. package/dist/indexeddb-storage-FNFUVvTJ.cjs +0 -2
  61. package/dist/indexeddb-storage-FNFUVvTJ.cjs.map +0 -1
  62. package/dist/secp256k1-CEwJNcfV.js +0 -1890
  63. package/dist/secp256k1-CEwJNcfV.js.map +0 -1
  64. package/dist/secp256k1-CiEONUnj.cjs +0 -12
  65. package/dist/secp256k1-CiEONUnj.cjs.map +0 -1
@@ -1,6 +1,12 @@
1
1
  /**
2
- * Federation Registry - Manages cross-holosphere partnerships
3
- * Tracks federated partners and their capability tokens
2
+ * Federation Registry - UNIFIED MODEL
3
+ *
4
+ * Manages federation partnerships and capability tokens for both:
5
+ * - Self-federation (same author, different holons)
6
+ * - Cross-federation (different authors)
7
+ *
8
+ * In the unified model, all federation uses capabilities, including self-federation.
9
+ * This provides consistent security semantics across all federation types.
4
10
  */
5
11
 
6
12
  import { writeGlobal, readGlobal } from '../storage/global-tables.js';
@@ -141,22 +147,61 @@ export async function getFederatedPartner(client, appname, partnerPubKey) {
141
147
  */
142
148
  export async function getCapabilityForAuthor(client, appname, authorPubKey, scope) {
143
149
  const registry = await getFederationRegistry(client, appname);
150
+
151
+ console.log('[Registry] 🔍 getCapabilityForAuthor called:', {
152
+ authorPubKey: authorPubKey?.slice(0, 12) + '...',
153
+ scope,
154
+ federatedWithCount: registry.federatedWith?.length || 0,
155
+ federatedPartners: registry.federatedWith?.map(p => p.pubKey?.slice(0, 12) + '...') || []
156
+ });
157
+
144
158
  const partner = registry.federatedWith.find(p => p.pubKey === authorPubKey);
145
159
 
146
- if (!partner || !partner.inboundCapabilities) {
160
+ if (!partner) {
161
+ console.log('[Registry] ❌ Partner not found in federatedWith');
162
+ return null;
163
+ }
164
+
165
+ if (!partner.inboundCapabilities || partner.inboundCapabilities.length === 0) {
166
+ console.log('[Registry] ❌ Partner has no inboundCapabilities:', {
167
+ partnerPubKey: authorPubKey?.slice(0, 12) + '...',
168
+ hasInboundCaps: !!partner.inboundCapabilities,
169
+ inboundCapsLength: partner.inboundCapabilities?.length || 0
170
+ });
147
171
  return null;
148
172
  }
149
173
 
174
+ console.log('[Registry] Partner found with inboundCapabilities:', {
175
+ partnerPubKey: authorPubKey?.slice(0, 12) + '...',
176
+ capsCount: partner.inboundCapabilities.length,
177
+ capScopes: partner.inboundCapabilities.map(c => c.scope)
178
+ });
179
+
150
180
  // Find matching, non-expired capability
151
- return partner.inboundCapabilities.find(cap => {
181
+ const result = partner.inboundCapabilities.find(cap => {
152
182
  // Check expiration
153
183
  if (cap.expires && cap.expires < Date.now()) {
184
+ console.log('[Registry] Capability expired:', { expires: cap.expires, now: Date.now() });
154
185
  return false;
155
186
  }
156
187
 
157
188
  // Check scope match (supports wildcards)
158
- return matchScope(cap.scope, scope);
189
+ const matches = matchScope(cap.scope, scope);
190
+ console.log('[Registry] Scope match check:', {
191
+ capScope: cap.scope,
192
+ requestedScope: scope,
193
+ matches
194
+ });
195
+ return matches;
159
196
  }) || null;
197
+
198
+ if (result) {
199
+ console.log('[Registry] ✅ Matching capability found');
200
+ } else {
201
+ console.log('[Registry] ❌ No matching capability found');
202
+ }
203
+
204
+ return result;
160
205
  }
161
206
 
162
207
  /**
@@ -172,10 +217,17 @@ export async function getCapabilityForAuthor(client, appname, authorPubKey, scop
172
217
  * @returns {Promise<boolean>} Success indicator
173
218
  */
174
219
  export async function storeInboundCapability(client, appname, partnerPubKey, capabilityInfo) {
220
+ console.log('[Registry] 📥 storeInboundCapability called:', {
221
+ partnerPubKey: partnerPubKey?.slice(0, 12) + '...',
222
+ scope: capabilityInfo?.scope,
223
+ hasToken: !!capabilityInfo?.token
224
+ });
225
+
175
226
  const registry = await getFederationRegistry(client, appname);
176
227
  const partner = registry.federatedWith.find(p => p.pubKey === partnerPubKey);
177
228
 
178
229
  if (!partner) {
230
+ console.log('[Registry] Partner not in registry, auto-adding with capability');
179
231
  // Auto-add partner if not exists
180
232
  return addFederatedPartner(client, appname, partnerPubKey, {
181
233
  addedVia: 'capability_received',
@@ -193,12 +245,14 @@ export async function storeInboundCapability(client, appname, partnerPubKey, cap
193
245
  );
194
246
 
195
247
  if (existingIndex >= 0) {
248
+ console.log('[Registry] Updating existing capability at index:', existingIndex);
196
249
  // Update existing capability
197
250
  partner.inboundCapabilities[existingIndex] = {
198
251
  ...capabilityInfo,
199
252
  updatedAt: Date.now()
200
253
  };
201
254
  } else {
255
+ console.log('[Registry] Adding new capability, total will be:', partner.inboundCapabilities.length + 1);
202
256
  // Add new capability
203
257
  partner.inboundCapabilities.push({
204
258
  ...capabilityInfo,
@@ -206,7 +260,9 @@ export async function storeInboundCapability(client, appname, partnerPubKey, cap
206
260
  });
207
261
  }
208
262
 
209
- return saveFederationRegistry(client, appname, registry);
263
+ const result = await saveFederationRegistry(client, appname, registry);
264
+ console.log('[Registry] ✅ Capability stored, registry saved:', result);
265
+ return result;
210
266
  }
211
267
 
212
268
  /**
@@ -384,3 +440,105 @@ export async function cleanupExpiredCapabilities(client, appname) {
384
440
 
385
441
  return { inboundRemoved, outboundRemoved };
386
442
  }
443
+
444
+ /**
445
+ * Store a self-capability (for same-author federation) - UNIFIED MODEL
446
+ *
447
+ * In the unified model, self-capabilities are stored similarly to cross-capabilities
448
+ * but marked as self-issued. This provides consistent capability management.
449
+ *
450
+ * @param {Object} client - NostrClient instance
451
+ * @param {string} appname - Application namespace
452
+ * @param {Object} capabilityInfo - Capability information
453
+ * @param {string} capabilityInfo.token - The capability token
454
+ * @param {Object} capabilityInfo.scope - Token scope
455
+ * @param {string[]} capabilityInfo.permissions - Granted permissions
456
+ * @returns {Promise<boolean>} Success indicator
457
+ */
458
+ export async function storeSelfCapability(client, appname, capabilityInfo) {
459
+ const registry = await getFederationRegistry(client, appname);
460
+
461
+ // Self-capabilities are stored under the user's own pubKey
462
+ const selfPubKey = client.publicKey;
463
+
464
+ // Find or create self-entry
465
+ let selfEntry = registry.federatedWith.find(p => p.pubKey === selfPubKey);
466
+
467
+ if (!selfEntry) {
468
+ selfEntry = {
469
+ pubKey: selfPubKey,
470
+ alias: 'self',
471
+ addedAt: Date.now(),
472
+ addedVia: 'self_capability',
473
+ isSelf: true,
474
+ inboundCapabilities: [],
475
+ outboundCapabilities: [],
476
+ selfCapabilities: [], // Special array for self-capabilities
477
+ };
478
+ registry.federatedWith.push(selfEntry);
479
+ }
480
+
481
+ if (!selfEntry.selfCapabilities) {
482
+ selfEntry.selfCapabilities = [];
483
+ }
484
+
485
+ // Check if we already have this capability (by scope match)
486
+ const existingIndex = selfEntry.selfCapabilities.findIndex(cap =>
487
+ JSON.stringify(cap.scope) === JSON.stringify(capabilityInfo.scope)
488
+ );
489
+
490
+ if (existingIndex >= 0) {
491
+ // Update existing capability
492
+ selfEntry.selfCapabilities[existingIndex] = {
493
+ ...capabilityInfo,
494
+ updatedAt: Date.now(),
495
+ isSelfCapability: true,
496
+ };
497
+ } else {
498
+ // Add new capability
499
+ selfEntry.selfCapabilities.push({
500
+ ...capabilityInfo,
501
+ issuedAt: Date.now(),
502
+ isSelfCapability: true,
503
+ });
504
+ }
505
+
506
+ return saveFederationRegistry(client, appname, registry);
507
+ }
508
+
509
+ /**
510
+ * Get self-capability for a scope - UNIFIED MODEL
511
+ *
512
+ * @param {Object} client - NostrClient instance
513
+ * @param {string} appname - Application namespace
514
+ * @param {Object} scope - Requested scope { holonId, lensName, dataId? }
515
+ * @returns {Promise<Object|null>} Self-capability or null
516
+ */
517
+ export async function getSelfCapability(client, appname, scope) {
518
+ const registry = await getFederationRegistry(client, appname);
519
+ const selfEntry = registry.federatedWith.find(p => p.pubKey === client.publicKey && p.isSelf);
520
+
521
+ if (!selfEntry || !selfEntry.selfCapabilities) {
522
+ return null;
523
+ }
524
+
525
+ // Find matching, non-expired self-capability
526
+ return selfEntry.selfCapabilities.find(cap => {
527
+ // Check expiration
528
+ if (cap.expires && cap.expires < Date.now()) {
529
+ return false;
530
+ }
531
+
532
+ // Check scope match (supports wildcards)
533
+ return matchScope(cap.scope, scope);
534
+ }) || null;
535
+ }
536
+
537
+ /**
538
+ * Check if a capability is a self-capability
539
+ * @param {Object} capabilityInfo - Capability info object
540
+ * @returns {boolean} True if self-capability
541
+ */
542
+ export function isSelfCapability(capabilityInfo) {
543
+ return capabilityInfo && capabilityInfo.isSelfCapability === true;
544
+ }
@@ -0,0 +1,373 @@
1
+ /**
2
+ * @fileoverview Federation Request Card Configuration
3
+ *
4
+ * Provides a card-based configuration system for federation requests.
5
+ * After inputting a public key, displays a card with all available lenses
6
+ * that can be configured for the federation request.
7
+ *
8
+ * @module federation/request-card
9
+ */
10
+
11
+ import { issueCapabilitiesForLenses } from './capabilities.js';
12
+
13
+ /**
14
+ * @typedef {Object} LensConfig
15
+ * @property {string} name - Lens name
16
+ * @property {boolean} enabled - Whether lens is enabled for federation
17
+ * @property {string} direction - 'inbound', 'outbound', or 'bidirectional'
18
+ * @property {string[]} permissions - Permissions to grant ('read', 'write')
19
+ */
20
+
21
+ /**
22
+ * @typedef {Object} FederationCardConfig
23
+ * @property {string} partnerPubKey - Partner's public key
24
+ * @property {string} partnerName - Partner's display name (optional)
25
+ * @property {string} holonId - Local holon ID
26
+ * @property {string} holonName - Local holon display name
27
+ * @property {LensConfig[]} lenses - Lens configurations
28
+ * @property {string} message - Optional message to partner
29
+ * @property {number} createdAt - Card creation timestamp
30
+ * @property {string} status - 'pending', 'sent', 'accepted', 'rejected', 'dismissed'
31
+ */
32
+
33
+ /**
34
+ * @typedef {Object} CardDisplayOptions
35
+ * @property {number} initialLensCount - Number of lenses to show initially (default: 3)
36
+ * @property {boolean} expanded - Whether card is expanded to show all lenses
37
+ * @property {boolean} showPermissions - Whether to show permission toggles
38
+ */
39
+
40
+ // Default number of lenses to show before "Show more" button
41
+ const DEFAULT_INITIAL_LENS_COUNT = 3;
42
+
43
+ /**
44
+ * Create a federation request card configuration
45
+ * @param {Object} params - Card parameters
46
+ * @param {string} params.partnerPubKey - Partner's public key
47
+ * @param {string} params.partnerName - Partner's display name
48
+ * @param {string} params.holonId - Local holon ID
49
+ * @param {string} params.holonName - Local holon display name
50
+ * @param {string[]} params.availableLenses - All available lens names
51
+ * @param {Object} params.defaultConfig - Default lens configuration
52
+ * @returns {FederationCardConfig} Card configuration
53
+ */
54
+ export function createFederationCard({
55
+ partnerPubKey,
56
+ partnerName = null,
57
+ holonId,
58
+ holonName,
59
+ availableLenses = [],
60
+ defaultConfig = {},
61
+ }) {
62
+ const {
63
+ defaultDirection = 'bidirectional',
64
+ defaultPermissions = ['read'],
65
+ enabledByDefault = true,
66
+ } = defaultConfig;
67
+
68
+ // Create lens configurations for all available lenses
69
+ const lenses = availableLenses.map(name => ({
70
+ name,
71
+ enabled: enabledByDefault,
72
+ direction: defaultDirection,
73
+ permissions: [...defaultPermissions],
74
+ }));
75
+
76
+ return {
77
+ id: `fedcard_${partnerPubKey.substring(0, 8)}_${Date.now()}`,
78
+ partnerPubKey,
79
+ partnerName,
80
+ holonId,
81
+ holonName,
82
+ lenses,
83
+ message: '',
84
+ createdAt: Date.now(),
85
+ status: 'pending',
86
+ expanded: false,
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Update a lens configuration in a card
92
+ * @param {FederationCardConfig} card - Card configuration
93
+ * @param {string} lensName - Lens name to update
94
+ * @param {Partial<LensConfig>} updates - Updates to apply
95
+ * @returns {FederationCardConfig} Updated card
96
+ */
97
+ export function updateLensConfig(card, lensName, updates) {
98
+ const lensIndex = card.lenses.findIndex(l => l.name === lensName);
99
+ if (lensIndex === -1) return card;
100
+
101
+ const updatedLenses = [...card.lenses];
102
+ updatedLenses[lensIndex] = {
103
+ ...updatedLenses[lensIndex],
104
+ ...updates,
105
+ };
106
+
107
+ return {
108
+ ...card,
109
+ lenses: updatedLenses,
110
+ };
111
+ }
112
+
113
+ /**
114
+ * Toggle a lens enabled/disabled state
115
+ * @param {FederationCardConfig} card - Card configuration
116
+ * @param {string} lensName - Lens name to toggle
117
+ * @returns {FederationCardConfig} Updated card
118
+ */
119
+ export function toggleLens(card, lensName) {
120
+ const lens = card.lenses.find(l => l.name === lensName);
121
+ if (!lens) return card;
122
+
123
+ return updateLensConfig(card, lensName, { enabled: !lens.enabled });
124
+ }
125
+
126
+ /**
127
+ * Enable all lenses in a card
128
+ * @param {FederationCardConfig} card - Card configuration
129
+ * @returns {FederationCardConfig} Updated card
130
+ */
131
+ export function enableAllLenses(card) {
132
+ return {
133
+ ...card,
134
+ lenses: card.lenses.map(l => ({ ...l, enabled: true })),
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Disable all lenses in a card
140
+ * @param {FederationCardConfig} card - Card configuration
141
+ * @returns {FederationCardConfig} Updated card
142
+ */
143
+ export function disableAllLenses(card) {
144
+ return {
145
+ ...card,
146
+ lenses: card.lenses.map(l => ({ ...l, enabled: false })),
147
+ };
148
+ }
149
+
150
+ /**
151
+ * Toggle card expansion to show all lenses
152
+ * @param {FederationCardConfig} card - Card configuration
153
+ * @returns {FederationCardConfig} Updated card
154
+ */
155
+ export function toggleCardExpansion(card) {
156
+ return {
157
+ ...card,
158
+ expanded: !card.expanded,
159
+ };
160
+ }
161
+
162
+ /**
163
+ * Get lenses to display based on expansion state
164
+ * @param {FederationCardConfig} card - Card configuration
165
+ * @param {number} initialCount - Number of lenses to show when collapsed
166
+ * @returns {Object} { visibleLenses, hiddenCount, isExpanded }
167
+ */
168
+ export function getVisibleLenses(card, initialCount = DEFAULT_INITIAL_LENS_COUNT) {
169
+ const allLenses = card.lenses;
170
+ const isExpanded = card.expanded;
171
+
172
+ if (isExpanded || allLenses.length <= initialCount) {
173
+ return {
174
+ visibleLenses: allLenses,
175
+ hiddenCount: 0,
176
+ isExpanded: true,
177
+ };
178
+ }
179
+
180
+ return {
181
+ visibleLenses: allLenses.slice(0, initialCount),
182
+ hiddenCount: allLenses.length - initialCount,
183
+ isExpanded: false,
184
+ };
185
+ }
186
+
187
+ /**
188
+ * Get enabled lenses from a card
189
+ * @param {FederationCardConfig} card - Card configuration
190
+ * @returns {LensConfig[]} Enabled lenses
191
+ */
192
+ export function getEnabledLenses(card) {
193
+ return card.lenses.filter(l => l.enabled);
194
+ }
195
+
196
+ /**
197
+ * Get lens configuration for handshake (inbound/outbound arrays)
198
+ * @param {FederationCardConfig} card - Card configuration
199
+ * @returns {Object} { inbound: string[], outbound: string[] }
200
+ */
201
+ export function getLensConfigForHandshake(card) {
202
+ const inbound = [];
203
+ const outbound = [];
204
+
205
+ for (const lens of card.lenses) {
206
+ if (!lens.enabled) continue;
207
+
208
+ if (lens.direction === 'inbound' || lens.direction === 'bidirectional') {
209
+ inbound.push(lens.name);
210
+ }
211
+ if (lens.direction === 'outbound' || lens.direction === 'bidirectional') {
212
+ outbound.push(lens.name);
213
+ }
214
+ }
215
+
216
+ return { inbound, outbound };
217
+ }
218
+
219
+ /**
220
+ * Update card status
221
+ * @param {FederationCardConfig} card - Card configuration
222
+ * @param {string} status - New status
223
+ * @returns {FederationCardConfig} Updated card
224
+ */
225
+ export function updateCardStatus(card, status) {
226
+ return {
227
+ ...card,
228
+ status,
229
+ updatedAt: Date.now(),
230
+ };
231
+ }
232
+
233
+ /**
234
+ * Dismiss a card (mark as dismissed so it doesn't reappear)
235
+ * @param {FederationCardConfig} card - Card configuration
236
+ * @returns {FederationCardConfig} Updated card with dismissed status
237
+ */
238
+ export function dismissCard(card) {
239
+ return updateCardStatus(card, 'dismissed');
240
+ }
241
+
242
+ /**
243
+ * Check if a card should be displayed
244
+ * @param {FederationCardConfig} card - Card configuration
245
+ * @returns {boolean} True if card should be displayed
246
+ */
247
+ export function shouldDisplayCard(card) {
248
+ return card.status !== 'dismissed' && card.status !== 'rejected';
249
+ }
250
+
251
+ /**
252
+ * Convert card configuration to handshake request params
253
+ * @param {FederationCardConfig} card - Card configuration
254
+ * @returns {Object} Params for initiateFederationHandshake
255
+ */
256
+ export function cardToHandshakeParams(card) {
257
+ const lensConfig = getLensConfigForHandshake(card);
258
+
259
+ return {
260
+ partnerPubKey: card.partnerPubKey,
261
+ holonId: card.holonId,
262
+ holonName: card.holonName,
263
+ lensConfig,
264
+ message: card.message,
265
+ };
266
+ }
267
+
268
+ /**
269
+ * Generate capabilities for a card's outbound lenses
270
+ * @param {Object} client - NostrClient instance
271
+ * @param {FederationCardConfig} card - Card configuration
272
+ * @returns {Promise<Object[]>} Array of capability info objects
273
+ */
274
+ export async function generateCapabilitiesForCard(client, card) {
275
+ const { outbound } = getLensConfigForHandshake(card);
276
+
277
+ if (outbound.length === 0) {
278
+ return [];
279
+ }
280
+
281
+ return issueCapabilitiesForLenses(
282
+ client,
283
+ card.holonId,
284
+ outbound,
285
+ card.partnerPubKey,
286
+ { permissions: ['read'] }
287
+ );
288
+ }
289
+
290
+ /**
291
+ * Render card as a simple text representation (for CLI/logs)
292
+ * @param {FederationCardConfig} card - Card configuration
293
+ * @param {CardDisplayOptions} options - Display options
294
+ * @returns {string} Text representation
295
+ */
296
+ export function renderCardAsText(card, options = {}) {
297
+ const { initialLensCount = DEFAULT_INITIAL_LENS_COUNT } = options;
298
+ const { visibleLenses, hiddenCount, isExpanded } = getVisibleLenses(card, initialLensCount);
299
+ const { inbound, outbound } = getLensConfigForHandshake(card);
300
+
301
+ const lines = [
302
+ `┌─────────────────────────────────────────────────────┐`,
303
+ `│ Federation Request │`,
304
+ `├─────────────────────────────────────────────────────┤`,
305
+ `│ Partner: ${(card.partnerName || card.partnerPubKey.substring(0, 16) + '...').padEnd(40)}│`,
306
+ `│ Status: ${card.status.padEnd(40)}│`,
307
+ `├─────────────────────────────────────────────────────┤`,
308
+ `│ Lenses: │`,
309
+ ];
310
+
311
+ for (const lens of visibleLenses) {
312
+ const status = lens.enabled ? '✓' : '○';
313
+ const dir = lens.direction === 'bidirectional' ? '↔' :
314
+ lens.direction === 'inbound' ? '←' : '→';
315
+ lines.push(`│ ${status} ${lens.name.padEnd(20)} ${dir.padEnd(22)}│`);
316
+ }
317
+
318
+ if (hiddenCount > 0) {
319
+ lines.push(`│ ... and ${hiddenCount} more lenses (click to expand) │`);
320
+ }
321
+
322
+ lines.push(`├─────────────────────────────────────────────────────┤`);
323
+ lines.push(`│ Sharing: ${outbound.length} lenses | Receiving: ${inbound.length} lenses`.padEnd(52) + `│`);
324
+ lines.push(`└─────────────────────────────────────────────────────┘`);
325
+
326
+ return lines.join('\n');
327
+ }
328
+
329
+ /**
330
+ * Serialize card for storage
331
+ * @param {FederationCardConfig} card - Card configuration
332
+ * @returns {Object} Serialized card
333
+ */
334
+ export function serializeCard(card) {
335
+ return {
336
+ ...card,
337
+ serializedAt: Date.now(),
338
+ };
339
+ }
340
+
341
+ /**
342
+ * Deserialize card from storage
343
+ * @param {Object} data - Serialized card data
344
+ * @returns {FederationCardConfig} Card configuration
345
+ */
346
+ export function deserializeCard(data) {
347
+ return {
348
+ ...data,
349
+ expanded: data.expanded || false,
350
+ lenses: data.lenses || [],
351
+ };
352
+ }
353
+
354
+ export default {
355
+ createFederationCard,
356
+ updateLensConfig,
357
+ toggleLens,
358
+ enableAllLenses,
359
+ disableAllLenses,
360
+ toggleCardExpansion,
361
+ getVisibleLenses,
362
+ getEnabledLenses,
363
+ getLensConfigForHandshake,
364
+ updateCardStatus,
365
+ dismissCard,
366
+ shouldDisplayCard,
367
+ cardToHandshakeParams,
368
+ generateCapabilitiesForCard,
369
+ renderCardAsText,
370
+ serializeCard,
371
+ deserializeCard,
372
+ DEFAULT_INITIAL_LENS_COUNT,
373
+ };
@@ -1,11 +1,14 @@
1
1
  /**
2
2
  * @fileoverview Hierarchical aggregation (upcast) operations for propagating data to parent holons.
3
3
  * Implements upcast operations (FR-025 to FR-027) for H3-based geographic hierarchies.
4
+ *
5
+ * UNIFIED MODEL: Uses createHologramWithCapability for consistent capability-based federation.
6
+ *
4
7
  * @module hierarchical/upcast
5
8
  */
6
9
 
7
10
  import { getParents, isValidH3 } from '../spatial/h3-operations.js';
8
- import { createHologram } from '../federation/hologram.js';
11
+ import { createHologramWithCapability } from '../federation/hologram.js';
9
12
  import { write } from '../storage/unified-storage.js';
10
13
 
11
14
  /**
@@ -50,6 +53,7 @@ export async function upcast(hs, holonId, lensName, dataId, options = {}) {
50
53
 
51
54
  /**
52
55
  * Propagate data to a parent holon by creating a hologram reference.
56
+ * UNIFIED MODEL: Uses createHologramWithCapability for consistent capability-based federation.
53
57
  * @private
54
58
  * @param {Object} client - Nostr client instance
55
59
  * @param {string} appname - Application name
@@ -69,8 +73,20 @@ async function propagateToParent(
69
73
  dataId,
70
74
  operation
71
75
  ) {
72
- // Create hologram reference
73
- const hologram = createHologram(sourceHolon, parentHolon, lensName, dataId, appname);
76
+ // UNIFIED MODEL: Create hologram with capability
77
+ const hologram = await createHologramWithCapability(
78
+ client,
79
+ sourceHolon,
80
+ parentHolon,
81
+ lensName,
82
+ dataId,
83
+ appname,
84
+ {
85
+ // Use client's public key as source author (self-federation for upcast)
86
+ sourceAuthorPubKey: client.publicKey,
87
+ permissions: ['read'],
88
+ }
89
+ );
74
90
 
75
91
  // Add operation metadata
76
92
  hologram._meta.operation = operation;