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.
- package/dist/{2019-D2OG2idw.js → 2019-DQdDE6DG.js} +2 -2
- package/dist/{2019-D2OG2idw.js.map → 2019-DQdDE6DG.js.map} +1 -1
- package/dist/{2019-EION3wKo.cjs → 2019-Ew-DTDlI.cjs} +2 -2
- package/dist/{2019-EION3wKo.cjs.map → 2019-Ew-DTDlI.cjs.map} +1 -1
- package/dist/{browser-BSniCNqO.js → browser-CckFyRI9.js} +2 -2
- package/dist/{browser-BSniCNqO.js.map → browser-CckFyRI9.js.map} +1 -1
- package/dist/browser-D2qtVhH5.cjs +2 -0
- package/dist/{browser-Cq59Ij19.cjs.map → browser-D2qtVhH5.cjs.map} +1 -1
- package/dist/cjs/holosphere.cjs +1 -1
- package/dist/esm/holosphere.js +67 -50
- package/dist/index-9sqetkAn.cjs +29 -0
- package/dist/index-9sqetkAn.cjs.map +1 -0
- package/dist/index-C0ITDyFo.cjs +13 -0
- package/dist/index-C0ITDyFo.cjs.map +1 -0
- package/dist/{index-Bl6rM1NW.js → index-DrYM1LOY.js} +2 -2
- package/dist/{index-Bl6rM1NW.js.map → index-DrYM1LOY.js.map} +1 -1
- package/dist/{index-D-jZhliX.js → index-TDDyakLc.js} +19827 -720
- package/dist/index-TDDyakLc.js.map +1 -0
- package/dist/{index-Bwg3OzRM.cjs → index-kyf1sjaC.cjs} +3 -3
- package/dist/{index-Bwg3OzRM.cjs.map → index-kyf1sjaC.cjs.map} +1 -1
- package/dist/index-lbSQUoRz.js +11663 -0
- package/dist/index-lbSQUoRz.js.map +1 -0
- package/dist/{indexeddb-storage-5eiUNsHC.js → indexeddb-storage-CXhjqwhA.js} +2 -2
- package/dist/{indexeddb-storage-5eiUNsHC.js.map → indexeddb-storage-CXhjqwhA.js.map} +1 -1
- package/dist/{indexeddb-storage-FNFUVvTJ.cjs → indexeddb-storage-DFESDYIj.cjs} +2 -2
- package/dist/{indexeddb-storage-FNFUVvTJ.cjs.map → indexeddb-storage-DFESDYIj.cjs.map} +1 -1
- package/dist/{memory-storage-DMt36uZO.cjs → memory-storage-D1tc1bjk.cjs} +2 -2
- package/dist/{memory-storage-DMt36uZO.cjs.map → memory-storage-D1tc1bjk.cjs.map} +1 -1
- package/dist/{memory-storage-CI-gfmuG.js → memory-storage-DkewsdcM.js} +2 -2
- package/dist/{memory-storage-CI-gfmuG.js.map → memory-storage-DkewsdcM.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 +46 -29
- package/src/federation/capabilities.js +162 -0
- package/src/federation/card-storage.js +304 -0
- package/src/federation/handshake.js +204 -7
- package/src/federation/hologram.js +167 -58
- package/src/federation/holon-registry.js +166 -0
- package/src/federation/index.js +68 -0
- package/src/federation/registry.js +110 -2
- package/src/federation/request-card.js +373 -0
- package/src/hierarchical/upcast.js +19 -3
- package/src/index.js +162 -76
- package/src/lib/federation-methods.js +527 -5
- package/src/storage/nostr-async.js +2 -2
- package/src/storage/nostr-client.js +370 -155
- package/src/storage/nostr-wrapper.js +1 -1
- 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/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,7 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Federation
|
|
3
|
-
*
|
|
4
|
-
* for
|
|
2
|
+
* @fileoverview Unified Federation Methods - UNIFIED MODEL
|
|
3
|
+
*
|
|
4
|
+
* Provides a single, consistent API for all federation operations:
|
|
5
|
+
* - Same-author federation (previously "in-federation")
|
|
6
|
+
* - Cross-author federation (previously "cross-federation")
|
|
7
|
+
*
|
|
8
|
+
* All federation uses capabilities, providing consistent security semantics.
|
|
9
|
+
* The unified `federate()` method handles both cases based on parameters.
|
|
10
|
+
*
|
|
5
11
|
* @module lib/federation-methods
|
|
6
12
|
*/
|
|
7
13
|
|
|
@@ -17,17 +23,242 @@ import { bytesToHex } from '@noble/hashes/utils';
|
|
|
17
23
|
|
|
18
24
|
/**
|
|
19
25
|
* Mixin that adds federation methods to a HoloSphere class.
|
|
20
|
-
* Enables
|
|
26
|
+
* Enables unified federation with capability-based access control,
|
|
21
27
|
* hologram creation, and Nostr-based discovery protocols.
|
|
28
|
+
*
|
|
29
|
+
* UNIFIED MODEL: All federation uses capabilities for consistent security.
|
|
30
|
+
*
|
|
22
31
|
* @param {Class} Base - Base class to extend
|
|
23
32
|
* @returns {Class} Extended class with federation methods
|
|
24
33
|
*/
|
|
25
34
|
export function withFederationMethods(Base) {
|
|
26
35
|
return class extends Base {
|
|
27
|
-
// ===
|
|
36
|
+
// === UNIFIED FEDERATION API ===
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Federation method - UNIFIED MODEL
|
|
40
|
+
*
|
|
41
|
+
* Single entry point for all federation operations.
|
|
42
|
+
* Handles both same-author and cross-author federation consistently.
|
|
43
|
+
*
|
|
44
|
+
* @param {string|Object} source - Source holon. Can be:
|
|
45
|
+
* - string: holonId (implies same author as client)
|
|
46
|
+
* - { holonId, authorPubKey }: explicit author specification
|
|
47
|
+
* @param {string|Object} target - Target holon. Same format as source.
|
|
48
|
+
* @param {string} lensName - Name of the lens to federate
|
|
49
|
+
* @param {Object} [options={}] - Federation options
|
|
50
|
+
* @param {string} [options.direction='outbound'] - 'inbound', 'outbound', or 'bidirectional'
|
|
51
|
+
* @param {string} [options.mode='reference'] - 'reference' (hologram) or 'copy'
|
|
52
|
+
* @param {boolean} [options.propagate=true] - Whether to propagate existing data (false = capability only)
|
|
53
|
+
* @param {string} [options.capability] - Pre-issued capability (auto-generated if not provided)
|
|
54
|
+
* @param {string[]} [options.permissions=['read']] - Permissions for auto-issued capability
|
|
55
|
+
* @param {Function} [options.filter] - Filter function to select which data to federate
|
|
56
|
+
* @returns {Promise<Object>} Federation result with capability info
|
|
57
|
+
* @throws {ValidationError} If trying to federate a holon with itself
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* // Same-author federation (simplified)
|
|
61
|
+
* await hs.federate('holon-a', 'holon-b', 'events');
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* // Cross-author federation (explicit)
|
|
65
|
+
* await hs.federate(
|
|
66
|
+
* { holonId: 'holon-a', authorPubKey: 'abc123...' },
|
|
67
|
+
* { holonId: 'holon-b', authorPubKey: 'def456...' },
|
|
68
|
+
* 'events',
|
|
69
|
+
* { capability: preIssuedToken }
|
|
70
|
+
* );
|
|
71
|
+
*/
|
|
72
|
+
async federate(source, target, lensName, options = {}) {
|
|
73
|
+
const {
|
|
74
|
+
direction = 'outbound',
|
|
75
|
+
mode = 'reference',
|
|
76
|
+
propagate = true,
|
|
77
|
+
capability: providedCapability = null,
|
|
78
|
+
permissions = ['read'],
|
|
79
|
+
filter = null,
|
|
80
|
+
} = options;
|
|
81
|
+
|
|
82
|
+
// Normalize source and target to { holonId, authorPubKey } format
|
|
83
|
+
const normalizedSource = typeof source === 'string'
|
|
84
|
+
? { holonId: source, authorPubKey: this.client.publicKey }
|
|
85
|
+
: { holonId: source.holonId, authorPubKey: source.authorPubKey || this.client.publicKey };
|
|
86
|
+
|
|
87
|
+
const normalizedTarget = typeof target === 'string'
|
|
88
|
+
? { holonId: target, authorPubKey: this.client.publicKey }
|
|
89
|
+
: { holonId: target.holonId, authorPubKey: target.authorPubKey || this.client.publicKey };
|
|
90
|
+
|
|
91
|
+
// Validation
|
|
92
|
+
if (normalizedSource.holonId === normalizedTarget.holonId &&
|
|
93
|
+
normalizedSource.authorPubKey === normalizedTarget.authorPubKey) {
|
|
94
|
+
throw new ValidationError('Cannot federate a holon with itself');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!['inbound', 'outbound', 'bidirectional'].includes(direction)) {
|
|
98
|
+
throw new ValidationError(`Invalid direction: ${direction}. Must be 'inbound', 'outbound', or 'bidirectional'`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Determine if this is self-federation or cross-federation
|
|
102
|
+
const isSelfFederation = normalizedSource.authorPubKey === normalizedTarget.authorPubKey;
|
|
103
|
+
|
|
104
|
+
// Issue or use provided capability
|
|
105
|
+
let capability = providedCapability;
|
|
106
|
+
if (!capability) {
|
|
107
|
+
const expiresIn = isSelfFederation
|
|
108
|
+
? 365 * 24 * 60 * 60 * 1000 // 1 year for self
|
|
109
|
+
: 24 * 60 * 60 * 1000; // 24 hours for cross
|
|
110
|
+
|
|
111
|
+
capability = await crypto.issueCapability(
|
|
112
|
+
permissions,
|
|
113
|
+
{ holonId: normalizedSource.holonId, lensName, dataId: '*' },
|
|
114
|
+
normalizedTarget.authorPubKey,
|
|
115
|
+
{
|
|
116
|
+
expiresIn,
|
|
117
|
+
issuer: normalizedSource.authorPubKey,
|
|
118
|
+
issuerKey: this.client.privateKey,
|
|
119
|
+
isSelfCapability: isSelfFederation,
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// Store capability in registry
|
|
124
|
+
if (isSelfFederation) {
|
|
125
|
+
await registry.storeSelfCapability(this.client, this.config.appName, {
|
|
126
|
+
token: capability,
|
|
127
|
+
scope: { holonId: normalizedSource.holonId, lensName },
|
|
128
|
+
permissions,
|
|
129
|
+
});
|
|
130
|
+
} else {
|
|
131
|
+
await registry.storeInboundCapability(
|
|
132
|
+
this.client,
|
|
133
|
+
this.config.appName,
|
|
134
|
+
normalizedSource.authorPubKey,
|
|
135
|
+
{
|
|
136
|
+
token: capability,
|
|
137
|
+
scope: { holonId: normalizedSource.holonId, lensName },
|
|
138
|
+
permissions,
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Setup federation based on direction
|
|
145
|
+
const results = {
|
|
146
|
+
source: normalizedSource,
|
|
147
|
+
target: normalizedTarget,
|
|
148
|
+
lensName,
|
|
149
|
+
direction,
|
|
150
|
+
mode,
|
|
151
|
+
propagate,
|
|
152
|
+
capability,
|
|
153
|
+
isSelfFederation,
|
|
154
|
+
propagated: { outbound: 0, inbound: 0 },
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// Handle data propagation (skip if propagate: false)
|
|
158
|
+
if (propagate && (direction === 'outbound' || direction === 'bidirectional')) {
|
|
159
|
+
// Propagate existing data from source to target
|
|
160
|
+
const sourceData = await this.read(normalizedSource.holonId, lensName, null, {
|
|
161
|
+
resolveHolograms: false,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
if (sourceData && Array.isArray(sourceData)) {
|
|
165
|
+
for (const item of sourceData) {
|
|
166
|
+
if (filter && !filter(item)) continue;
|
|
167
|
+
|
|
168
|
+
await federation.propagateData(
|
|
169
|
+
this.client,
|
|
170
|
+
this.config.appName,
|
|
171
|
+
item,
|
|
172
|
+
normalizedSource.holonId,
|
|
173
|
+
normalizedTarget.holonId,
|
|
174
|
+
lensName,
|
|
175
|
+
mode,
|
|
176
|
+
{
|
|
177
|
+
sourceAuthorPubKey: normalizedSource.authorPubKey,
|
|
178
|
+
capability,
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
results.propagated.outbound++;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (propagate && (direction === 'inbound' || direction === 'bidirectional')) {
|
|
187
|
+
// Propagate from target to source (need capability from target author)
|
|
188
|
+
const targetData = await this.read(normalizedTarget.holonId, lensName, null, {
|
|
189
|
+
resolveHolograms: false,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
if (targetData && Array.isArray(targetData)) {
|
|
193
|
+
for (const item of targetData) {
|
|
194
|
+
if (filter && !filter(item)) continue;
|
|
195
|
+
|
|
196
|
+
await federation.propagateData(
|
|
197
|
+
this.client,
|
|
198
|
+
this.config.appName,
|
|
199
|
+
item,
|
|
200
|
+
normalizedTarget.holonId,
|
|
201
|
+
normalizedSource.holonId,
|
|
202
|
+
lensName,
|
|
203
|
+
mode,
|
|
204
|
+
{
|
|
205
|
+
sourceAuthorPubKey: normalizedTarget.authorPubKey,
|
|
206
|
+
capability,
|
|
207
|
+
}
|
|
208
|
+
);
|
|
209
|
+
results.propagated.inbound++;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Store federation config
|
|
215
|
+
await federation.setupFederation(
|
|
216
|
+
this.client,
|
|
217
|
+
this.config.appName,
|
|
218
|
+
normalizedSource.holonId,
|
|
219
|
+
normalizedTarget.holonId,
|
|
220
|
+
lensName,
|
|
221
|
+
{ direction, mode }
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
return results;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Unfederate method - UNIFIED MODEL
|
|
229
|
+
*
|
|
230
|
+
* Removes federation between source and target holons.
|
|
231
|
+
*
|
|
232
|
+
* @param {string|Object} source - Source holon (string or { holonId, authorPubKey })
|
|
233
|
+
* @param {string|Object} target - Target holon (string or { holonId, authorPubKey })
|
|
234
|
+
* @param {string} lensName - Lens name to unfederate
|
|
235
|
+
* @returns {Promise<boolean>} True if unfederation succeeded
|
|
236
|
+
*/
|
|
237
|
+
async unfederate(source, target, lensName) {
|
|
238
|
+
const normalizedSource = typeof source === 'string'
|
|
239
|
+
? { holonId: source, authorPubKey: this.client.publicKey }
|
|
240
|
+
: { holonId: source.holonId, authorPubKey: source.authorPubKey || this.client.publicKey };
|
|
241
|
+
|
|
242
|
+
const normalizedTarget = typeof target === 'string'
|
|
243
|
+
? { holonId: target, authorPubKey: this.client.publicKey }
|
|
244
|
+
: { holonId: target.holonId, authorPubKey: target.authorPubKey || this.client.publicKey };
|
|
245
|
+
|
|
246
|
+
// Remove federation config
|
|
247
|
+
const configPath = storage.buildPath(this.config.appName, normalizedSource.holonId, lensName, '_federation');
|
|
248
|
+
try {
|
|
249
|
+
await storage.deleteData(this.client, configPath);
|
|
250
|
+
} catch (e) {
|
|
251
|
+
// Ignore errors - already unfederated or doesn't exist
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// === Legacy Methods (kept for backward compatibility) ===
|
|
28
258
|
|
|
29
259
|
/**
|
|
30
260
|
* Add a federated HoloSphere partner by public key.
|
|
261
|
+
* @deprecated Use federate() with explicit authorPubKey instead
|
|
31
262
|
* @param {string} pubKey - Public key (hex) of the federated HoloSphere
|
|
32
263
|
* @param {Object} [options={}] - Federation options (metadata, capabilities, etc.)
|
|
33
264
|
* @returns {Promise<Object>} Federation registration result
|
|
@@ -339,6 +570,297 @@ export function withFederationMethods(Base) {
|
|
|
339
570
|
async getPendingFederationRequests(options = {}) {
|
|
340
571
|
return discovery.getPendingFederationRequests(this.client, options);
|
|
341
572
|
}
|
|
573
|
+
|
|
574
|
+
// === Federated Lens Reception (Hologram-based) ===
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Receive data from a federated partner's lens as holograms in your own holon.
|
|
578
|
+
* This creates holograms pointing to the partner's data, allowing you to see
|
|
579
|
+
* their data in your holon while the source of truth remains with the partner.
|
|
580
|
+
*
|
|
581
|
+
* @param {string} partnerPubKey - Partner's public key
|
|
582
|
+
* @param {string} sourceHolonId - Partner's holon ID to receive data from
|
|
583
|
+
* @param {string} lensName - Lens name to receive
|
|
584
|
+
* @param {string} targetHolonId - Your holon ID where holograms will be created
|
|
585
|
+
* @param {Object} [options={}] - Reception options
|
|
586
|
+
* @param {Function} [options.filter] - Optional filter function to select which items to receive
|
|
587
|
+
* @param {boolean} [options.overwrite=false] - Whether to overwrite existing holograms
|
|
588
|
+
* @returns {Promise<Object>} Result with count of received holograms and any errors
|
|
589
|
+
* @throws {AuthorizationError} If no valid capability exists for the source
|
|
590
|
+
*
|
|
591
|
+
* @example
|
|
592
|
+
* // Receive events from a federated partner into your holon
|
|
593
|
+
* const result = await hs.receiveFederatedLens(
|
|
594
|
+
* 'partner-pubkey',
|
|
595
|
+
* 'partner-holon-id',
|
|
596
|
+
* 'events',
|
|
597
|
+
* 'my-holon-id'
|
|
598
|
+
* );
|
|
599
|
+
* console.log(`Received ${result.received} holograms`);
|
|
600
|
+
*/
|
|
601
|
+
async receiveFederatedLens(partnerPubKey, sourceHolonId, lensName, targetHolonId, options = {}) {
|
|
602
|
+
const { filter = null, overwrite = false } = options;
|
|
603
|
+
|
|
604
|
+
// Get capability for accessing partner's data
|
|
605
|
+
const capabilityEntry = await registry.getCapabilityForAuthor(
|
|
606
|
+
this.client,
|
|
607
|
+
this.config.appName,
|
|
608
|
+
partnerPubKey,
|
|
609
|
+
{ holonId: sourceHolonId, lensName, dataId: '*' }
|
|
610
|
+
);
|
|
611
|
+
|
|
612
|
+
if (!capabilityEntry) {
|
|
613
|
+
throw new AuthorizationError(
|
|
614
|
+
`No valid capability to access ${sourceHolonId}/${lensName} from ${partnerPubKey}`,
|
|
615
|
+
'read'
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Read all data from partner's lens
|
|
620
|
+
const sourcePath = storage.buildPath(this.config.appName, sourceHolonId, lensName);
|
|
621
|
+
const partnerData = await nostrAsync.nostrGetAll(this.client, sourcePath, 30000, {
|
|
622
|
+
authors: [partnerPubKey],
|
|
623
|
+
includeAuthor: true,
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
if (!partnerData || !Array.isArray(partnerData)) {
|
|
627
|
+
return { received: 0, skipped: 0, errors: [] };
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const result = { received: 0, skipped: 0, errors: [] };
|
|
631
|
+
|
|
632
|
+
for (const item of partnerData) {
|
|
633
|
+
if (!item || !item.id) continue;
|
|
634
|
+
|
|
635
|
+
// Apply filter if provided
|
|
636
|
+
if (filter && !filter(item)) {
|
|
637
|
+
result.skipped++;
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Skip if hologram already exists and overwrite is false
|
|
642
|
+
if (!overwrite) {
|
|
643
|
+
const existingPath = storage.buildPath(this.config.appName, targetHolonId, lensName, item.id);
|
|
644
|
+
const existing = await storage.read(this.client, existingPath);
|
|
645
|
+
if (existing && existing.hologram) {
|
|
646
|
+
result.skipped++;
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
try {
|
|
652
|
+
// Create hologram in target holon pointing to partner's data
|
|
653
|
+
const hologram = await federation.createHologramWithCapability(
|
|
654
|
+
this.client,
|
|
655
|
+
sourceHolonId,
|
|
656
|
+
targetHolonId,
|
|
657
|
+
lensName,
|
|
658
|
+
item.id,
|
|
659
|
+
this.config.appName,
|
|
660
|
+
{
|
|
661
|
+
sourceAuthorPubKey: partnerPubKey,
|
|
662
|
+
targetAuthorPubKey: this.client.publicKey,
|
|
663
|
+
capability: capabilityEntry.token,
|
|
664
|
+
permissions: ['read'],
|
|
665
|
+
}
|
|
666
|
+
);
|
|
667
|
+
|
|
668
|
+
const targetPath = storage.buildPath(this.config.appName, targetHolonId, lensName, item.id);
|
|
669
|
+
await storage.write(this.client, targetPath, hologram);
|
|
670
|
+
result.received++;
|
|
671
|
+
} catch (error) {
|
|
672
|
+
result.errors.push({ id: item.id, error: error.message });
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
return result;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Subscribe to a federated partner's lens for real-time hologram updates.
|
|
681
|
+
* When the partner writes new data, holograms are automatically created in your holon.
|
|
682
|
+
*
|
|
683
|
+
* @param {string} partnerPubKey - Partner's public key
|
|
684
|
+
* @param {string} sourceHolonId - Partner's holon ID to subscribe to
|
|
685
|
+
* @param {string} lensName - Lens name to subscribe to
|
|
686
|
+
* @param {string} targetHolonId - Your holon ID where holograms will be created
|
|
687
|
+
* @param {Object} [options={}] - Subscription options
|
|
688
|
+
* @param {Function} [options.onHologram] - Callback when a new hologram is created (hologram, sourceData) => void
|
|
689
|
+
* @param {Function} [options.onError] - Callback for errors (error) => void
|
|
690
|
+
* @param {Function} [options.filter] - Optional filter function to select which items to receive
|
|
691
|
+
* @returns {Promise<Object>} Subscription object with unsubscribe() method
|
|
692
|
+
*
|
|
693
|
+
* @example
|
|
694
|
+
* // Subscribe to partner's events lens
|
|
695
|
+
* const sub = await hs.subscribeFederatedLens(
|
|
696
|
+
* 'partner-pubkey',
|
|
697
|
+
* 'partner-holon-id',
|
|
698
|
+
* 'events',
|
|
699
|
+
* 'my-holon-id',
|
|
700
|
+
* {
|
|
701
|
+
* onHologram: (hologram, data) => {
|
|
702
|
+
* console.log(`New hologram received: ${data.id}`);
|
|
703
|
+
* }
|
|
704
|
+
* }
|
|
705
|
+
* );
|
|
706
|
+
*
|
|
707
|
+
* // Later, unsubscribe
|
|
708
|
+
* sub.unsubscribe();
|
|
709
|
+
*/
|
|
710
|
+
async subscribeFederatedLens(partnerPubKey, sourceHolonId, lensName, targetHolonId, options = {}) {
|
|
711
|
+
const { onHologram, onError, filter = null } = options;
|
|
712
|
+
|
|
713
|
+
// Get capability for accessing partner's data
|
|
714
|
+
const capabilityEntry = await registry.getCapabilityForAuthor(
|
|
715
|
+
this.client,
|
|
716
|
+
this.config.appName,
|
|
717
|
+
partnerPubKey,
|
|
718
|
+
{ holonId: sourceHolonId, lensName, dataId: '*' }
|
|
719
|
+
);
|
|
720
|
+
|
|
721
|
+
if (!capabilityEntry) {
|
|
722
|
+
throw new AuthorizationError(
|
|
723
|
+
`No valid capability to subscribe to ${sourceHolonId}/${lensName} from ${partnerPubKey}`,
|
|
724
|
+
'read'
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Track processed items to avoid duplicates
|
|
729
|
+
const processedIds = new Set();
|
|
730
|
+
|
|
731
|
+
// Subscribe to partner's lens path
|
|
732
|
+
const sourcePath = storage.buildPath(this.config.appName, sourceHolonId, lensName);
|
|
733
|
+
|
|
734
|
+
const handleEvent = async (data) => {
|
|
735
|
+
if (!data || !data.id) return;
|
|
736
|
+
|
|
737
|
+
// Skip if already processed
|
|
738
|
+
if (processedIds.has(data.id)) return;
|
|
739
|
+
processedIds.add(data.id);
|
|
740
|
+
|
|
741
|
+
// Apply filter if provided
|
|
742
|
+
if (filter && !filter(data)) return;
|
|
743
|
+
|
|
744
|
+
try {
|
|
745
|
+
// Check if hologram already exists
|
|
746
|
+
const existingPath = storage.buildPath(this.config.appName, targetHolonId, lensName, data.id);
|
|
747
|
+
const existing = await storage.read(this.client, existingPath);
|
|
748
|
+
|
|
749
|
+
if (existing && existing.hologram) {
|
|
750
|
+
// Hologram already exists, skip
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Create hologram in target holon
|
|
755
|
+
const hologram = await federation.createHologramWithCapability(
|
|
756
|
+
this.client,
|
|
757
|
+
sourceHolonId,
|
|
758
|
+
targetHolonId,
|
|
759
|
+
lensName,
|
|
760
|
+
data.id,
|
|
761
|
+
this.config.appName,
|
|
762
|
+
{
|
|
763
|
+
sourceAuthorPubKey: partnerPubKey,
|
|
764
|
+
targetAuthorPubKey: this.client.publicKey,
|
|
765
|
+
capability: capabilityEntry.token,
|
|
766
|
+
permissions: ['read'],
|
|
767
|
+
}
|
|
768
|
+
);
|
|
769
|
+
|
|
770
|
+
const targetPath = storage.buildPath(this.config.appName, targetHolonId, lensName, data.id);
|
|
771
|
+
await storage.write(this.client, targetPath, hologram);
|
|
772
|
+
|
|
773
|
+
if (onHologram) {
|
|
774
|
+
onHologram(hologram, data);
|
|
775
|
+
}
|
|
776
|
+
} catch (error) {
|
|
777
|
+
if (onError) {
|
|
778
|
+
onError(error);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
// Subscribe using nostr-async subscription with author filter
|
|
784
|
+
const subscription = await nostrAsync.nostrSubscribe(
|
|
785
|
+
this.client,
|
|
786
|
+
sourcePath,
|
|
787
|
+
handleEvent,
|
|
788
|
+
30000,
|
|
789
|
+
{ authors: [partnerPubKey] }
|
|
790
|
+
);
|
|
791
|
+
|
|
792
|
+
return {
|
|
793
|
+
unsubscribe: () => {
|
|
794
|
+
if (subscription && subscription.unsubscribe) {
|
|
795
|
+
subscription.unsubscribe();
|
|
796
|
+
}
|
|
797
|
+
processedIds.clear();
|
|
798
|
+
},
|
|
799
|
+
processedCount: () => processedIds.size,
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Sync all federated lenses for a holon.
|
|
805
|
+
* Receives holograms from all federated partners for their configured inbound lenses.
|
|
806
|
+
*
|
|
807
|
+
* @param {string} holonId - Your holon ID
|
|
808
|
+
* @param {Object} [options={}] - Sync options
|
|
809
|
+
* @param {boolean} [options.overwrite=false] - Whether to overwrite existing holograms
|
|
810
|
+
* @returns {Promise<Object>} Sync results per partner
|
|
811
|
+
*
|
|
812
|
+
* @example
|
|
813
|
+
* // Sync all federated data for a holon
|
|
814
|
+
* const results = await hs.syncFederatedLenses('my-holon-id');
|
|
815
|
+
* console.log(results);
|
|
816
|
+
*/
|
|
817
|
+
async syncFederatedLenses(holonId, options = {}) {
|
|
818
|
+
const { overwrite = false } = options;
|
|
819
|
+
|
|
820
|
+
// Get federation configuration for this holon
|
|
821
|
+
const federationData = await this.readGlobal('federation', holonId);
|
|
822
|
+
if (!federationData) {
|
|
823
|
+
return { error: 'No federation configuration found for this holon' };
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const results = {};
|
|
827
|
+
|
|
828
|
+
// Get all federated partners
|
|
829
|
+
const partners = await this.getFederatedHolospheres();
|
|
830
|
+
|
|
831
|
+
for (const partnerPubKey of partners) {
|
|
832
|
+
results[partnerPubKey] = { lenses: {}, errors: [] };
|
|
833
|
+
|
|
834
|
+
// Get partner's lens configuration
|
|
835
|
+
const partnerLensConfig = federationData.lensConfig?.[partnerPubKey];
|
|
836
|
+
const inboundLenses = partnerLensConfig?.inbound || [];
|
|
837
|
+
|
|
838
|
+
for (const lensName of inboundLenses) {
|
|
839
|
+
try {
|
|
840
|
+
// For cross-holosphere federation, the sourceHolonId would be the partner's holon
|
|
841
|
+
// We need to determine the partner's holon ID - this might be stored in the federation config
|
|
842
|
+
const partnerHolonId = partnerLensConfig?.holonId || holonId;
|
|
843
|
+
|
|
844
|
+
const result = await this.receiveFederatedLens(
|
|
845
|
+
partnerPubKey,
|
|
846
|
+
partnerHolonId,
|
|
847
|
+
lensName,
|
|
848
|
+
holonId,
|
|
849
|
+
{ overwrite }
|
|
850
|
+
);
|
|
851
|
+
|
|
852
|
+
results[partnerPubKey].lenses[lensName] = result;
|
|
853
|
+
} catch (error) {
|
|
854
|
+
results[partnerPubKey].errors.push({
|
|
855
|
+
lens: lensName,
|
|
856
|
+
error: error.message,
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
return results;
|
|
863
|
+
}
|
|
342
864
|
};
|
|
343
865
|
}
|
|
344
866
|
|
|
@@ -676,7 +676,7 @@ export async function nostrDeleteAll(client, pathPrefix, kind = 30000) {
|
|
|
676
676
|
* });
|
|
677
677
|
* // Later: sub.unsubscribe();
|
|
678
678
|
*/
|
|
679
|
-
export function nostrSubscribe(client, path, callback, options = {}) {
|
|
679
|
+
export async function nostrSubscribe(client, path, callback, options = {}) {
|
|
680
680
|
const kind = options.kind || 30000;
|
|
681
681
|
|
|
682
682
|
// Create unique key for this subscription
|
|
@@ -722,7 +722,7 @@ export function nostrSubscribe(client, path, callback, options = {}) {
|
|
|
722
722
|
limit: 10, // Limit results for single item subscription
|
|
723
723
|
};
|
|
724
724
|
|
|
725
|
-
const subscription = client.subscribe(
|
|
725
|
+
const subscription = await client.subscribe(
|
|
726
726
|
filter,
|
|
727
727
|
(event) => {
|
|
728
728
|
// Verify event is from our public key (relay may not respect author filter)
|