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,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
|
|
|
@@ -39,6 +39,7 @@ export class IndexedDBStorage extends PersistentStorage {
|
|
|
39
39
|
* Initialize storage with namespace.
|
|
40
40
|
*
|
|
41
41
|
* Creates or opens the IndexedDB database and object store.
|
|
42
|
+
* Handles version conflicts by deleting and recreating the database if needed.
|
|
42
43
|
*
|
|
43
44
|
* @param {string} namespace - Storage namespace
|
|
44
45
|
* @returns {Promise<void>}
|
|
@@ -46,6 +47,27 @@ export class IndexedDBStorage extends PersistentStorage {
|
|
|
46
47
|
async init(namespace) {
|
|
47
48
|
this.dbName = `holosphere_${namespace}`;
|
|
48
49
|
|
|
50
|
+
try {
|
|
51
|
+
await this._openDatabase();
|
|
52
|
+
} catch (error) {
|
|
53
|
+
// Handle version conflict by deleting and recreating the database
|
|
54
|
+
if (error.name === 'VersionError') {
|
|
55
|
+
console.warn(`[IndexedDBStorage] Version conflict detected for ${this.dbName}, recreating database...`);
|
|
56
|
+
await this._deleteDatabase();
|
|
57
|
+
await this._openDatabase();
|
|
58
|
+
} else {
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Open the IndexedDB database.
|
|
66
|
+
*
|
|
67
|
+
* @returns {Promise<void>}
|
|
68
|
+
* @private
|
|
69
|
+
*/
|
|
70
|
+
async _openDatabase() {
|
|
49
71
|
return new Promise((resolve, reject) => {
|
|
50
72
|
const request = indexedDB.open(this.dbName, 1);
|
|
51
73
|
|
|
@@ -69,6 +91,25 @@ export class IndexedDBStorage extends PersistentStorage {
|
|
|
69
91
|
});
|
|
70
92
|
}
|
|
71
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Delete the IndexedDB database.
|
|
96
|
+
*
|
|
97
|
+
* @returns {Promise<void>}
|
|
98
|
+
* @private
|
|
99
|
+
*/
|
|
100
|
+
async _deleteDatabase() {
|
|
101
|
+
return new Promise((resolve, reject) => {
|
|
102
|
+
const request = indexedDB.deleteDatabase(this.dbName);
|
|
103
|
+
request.onsuccess = () => resolve(undefined);
|
|
104
|
+
request.onerror = () => reject(request.error);
|
|
105
|
+
request.onblocked = () => {
|
|
106
|
+
console.warn(`[IndexedDBStorage] Database deletion blocked for ${this.dbName}`);
|
|
107
|
+
// Still resolve - the next open attempt will handle it
|
|
108
|
+
resolve(undefined);
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
72
113
|
/**
|
|
73
114
|
* Store an event.
|
|
74
115
|
*
|
|
@@ -42,13 +42,21 @@ const QUERY_DEDUP_WINDOW = 2000;
|
|
|
42
42
|
* @param {Object} client - NostrClient instance
|
|
43
43
|
* @param {string} path - Path identifier (encoded in d-tag)
|
|
44
44
|
* @param {Object} data - Data to store
|
|
45
|
-
* @param {number} [
|
|
45
|
+
* @param {number|Object} [kindOrOptions=30000] - Event kind (number) or options object
|
|
46
|
+
* @param {number} [kindOrOptions.kind=30000] - Event kind
|
|
47
|
+
* @param {string} [kindOrOptions.signingKey] - Private key to sign with (hex format)
|
|
46
48
|
* @returns {Promise<Object>} Published event result with relay responses
|
|
47
49
|
* @example
|
|
48
50
|
* const result = await nostrPut(client, 'myapp/holon123/items/item1', { name: 'Test' });
|
|
49
51
|
* console.log(result.event.id); // Event ID
|
|
50
52
|
*/
|
|
51
|
-
export async function nostrPut(client, path, data,
|
|
53
|
+
export async function nostrPut(client, path, data, kindOrOptions = 30000) {
|
|
54
|
+
// Support both old signature (kind as number) and new signature (options object)
|
|
55
|
+
const options = typeof kindOrOptions === 'number'
|
|
56
|
+
? { kind: kindOrOptions }
|
|
57
|
+
: kindOrOptions;
|
|
58
|
+
const kind = options.kind || 30000;
|
|
59
|
+
|
|
52
60
|
const dataEvent = {
|
|
53
61
|
kind,
|
|
54
62
|
created_at: Math.floor(Date.now() / 1000),
|
|
@@ -58,7 +66,8 @@ export async function nostrPut(client, path, data, kind = 30000) {
|
|
|
58
66
|
content: JSON.stringify(data),
|
|
59
67
|
};
|
|
60
68
|
|
|
61
|
-
const
|
|
69
|
+
const publishOptions = options.signingKey ? { signingKey: options.signingKey } : {};
|
|
70
|
+
const result = await client.publish(dataEvent, publishOptions);
|
|
62
71
|
return result;
|
|
63
72
|
}
|
|
64
73
|
|
|
@@ -676,7 +685,7 @@ export async function nostrDeleteAll(client, pathPrefix, kind = 30000) {
|
|
|
676
685
|
* });
|
|
677
686
|
* // Later: sub.unsubscribe();
|
|
678
687
|
*/
|
|
679
|
-
export function nostrSubscribe(client, path, callback, options = {}) {
|
|
688
|
+
export async function nostrSubscribe(client, path, callback, options = {}) {
|
|
680
689
|
const kind = options.kind || 30000;
|
|
681
690
|
|
|
682
691
|
// Create unique key for this subscription
|
|
@@ -722,7 +731,7 @@ export function nostrSubscribe(client, path, callback, options = {}) {
|
|
|
722
731
|
limit: 10, // Limit results for single item subscription
|
|
723
732
|
};
|
|
724
733
|
|
|
725
|
-
const subscription = client.subscribe(
|
|
734
|
+
const subscription = await client.subscribe(
|
|
726
735
|
filter,
|
|
727
736
|
(event) => {
|
|
728
737
|
// Verify event is from our public key (relay may not respect author filter)
|