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.
- package/dist/{2019-D2OG2idw.js → 2019-CLMqIAfQ.js} +1722 -1668
- package/dist/{2019-D2OG2idw.js.map → 2019-CLMqIAfQ.js.map} +1 -1
- package/dist/2019-Cp3uYhyY.cjs +8 -0
- package/dist/{2019-EION3wKo.cjs.map → 2019-Cp3uYhyY.cjs.map} +1 -1
- package/dist/browser-D6cNVl0v.cjs +2 -0
- package/dist/{browser-Cq59Ij19.cjs.map → browser-D6cNVl0v.cjs.map} +1 -1
- package/dist/{browser-BSniCNqO.js → browser-nUQt1cnB.js} +2 -2
- package/dist/{browser-BSniCNqO.js.map → browser-nUQt1cnB.js.map} +1 -1
- package/dist/cjs/holosphere.cjs +1 -1
- package/dist/esm/holosphere.js +67 -50
- package/dist/{index-D-jZhliX.js → index-BN_uoxQK.js} +20324 -735
- package/dist/index-BN_uoxQK.js.map +1 -0
- package/dist/{index-Bl6rM1NW.js → index-CoAjtqsD.js} +2 -2
- package/dist/{index-Bl6rM1NW.js.map → index-CoAjtqsD.js.map} +1 -1
- package/dist/{index-Bwg3OzRM.cjs → index-Cp3tI53z.cjs} +3 -3
- package/dist/{index-Bwg3OzRM.cjs.map → index-Cp3tI53z.cjs.map} +1 -1
- package/dist/index-DJjGSwXG.cjs +13 -0
- package/dist/index-DJjGSwXG.cjs.map +1 -0
- package/dist/index-V8EHMYEY.cjs +29 -0
- package/dist/index-V8EHMYEY.cjs.map +1 -0
- package/dist/index-Z5TstN1e.js +11663 -0
- package/dist/index-Z5TstN1e.js.map +1 -0
- package/dist/indexeddb-storage-CZK5A7XH.cjs +2 -0
- package/dist/indexeddb-storage-CZK5A7XH.cjs.map +1 -0
- package/dist/{indexeddb-storage-5eiUNsHC.js → indexeddb-storage-bpA01pAU.js} +39 -2
- package/dist/indexeddb-storage-bpA01pAU.js.map +1 -0
- package/dist/{memory-storage-DMt36uZO.cjs → memory-storage-B1k8Jszd.cjs} +2 -2
- package/dist/{memory-storage-DMt36uZO.cjs.map → memory-storage-B1k8Jszd.cjs.map} +1 -1
- package/dist/{memory-storage-CI-gfmuG.js → memory-storage-BqhmytP_.js} +2 -2
- package/dist/{memory-storage-CI-gfmuG.js.map → memory-storage-BqhmytP_.js.map} +1 -1
- package/docs/FEDERATION.md +474 -0
- package/package.json +3 -1
- package/src/crypto/nostr-utils.js +7 -0
- package/src/crypto/secp256k1.js +104 -38
- package/src/federation/capabilities.js +162 -0
- package/src/federation/card-storage.js +376 -0
- package/src/federation/handshake.js +561 -9
- package/src/federation/hologram.js +194 -57
- package/src/federation/holon-registry.js +187 -0
- package/src/federation/index.js +68 -0
- package/src/federation/registry.js +164 -6
- package/src/federation/request-card.js +373 -0
- package/src/hierarchical/upcast.js +19 -3
- package/src/index.js +209 -75
- package/src/lib/federation-methods.js +527 -5
- package/src/storage/indexeddb-storage.js +41 -0
- package/src/storage/nostr-async.js +14 -5
- package/src/storage/nostr-client.js +471 -155
- package/src/storage/nostr-wrapper.js +6 -3
- package/dist/2019-EION3wKo.cjs +0 -8
- package/dist/_commonjsHelpers-C37NGDzP.cjs +0 -2
- package/dist/_commonjsHelpers-C37NGDzP.cjs.map +0 -1
- package/dist/_commonjsHelpers-CUmg6egw.js +0 -7
- package/dist/_commonjsHelpers-CUmg6egw.js.map +0 -1
- package/dist/browser-Cq59Ij19.cjs +0 -2
- package/dist/index-D-jZhliX.js.map +0 -1
- package/dist/index-Dc6Z8Aob.cjs +0 -18
- package/dist/index-Dc6Z8Aob.cjs.map +0 -1
- package/dist/indexeddb-storage-5eiUNsHC.js.map +0 -1
- package/dist/indexeddb-storage-FNFUVvTJ.cjs +0 -2
- package/dist/indexeddb-storage-FNFUVvTJ.cjs.map +0 -1
- package/dist/secp256k1-CEwJNcfV.js +0 -1890
- package/dist/secp256k1-CEwJNcfV.js.map +0 -1
- package/dist/secp256k1-CiEONUnj.cjs +0 -12
- package/dist/secp256k1-CiEONUnj.cjs.map +0 -1
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Federation Registry -
|
|
3
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
73
|
-
const hologram =
|
|
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;
|