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,7 +1,13 @@
1
1
  /**
2
- * @fileoverview Federation methods mixin for cross-HoloSphere federation and Nostr discovery.
3
- * Provides federated data access, capability-based authorization, and federation request protocols
4
- * for connecting multiple HoloSphere instances.
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 cross-HoloSphere federation with capability-based access control,
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
- // === Cross-Holosphere Federation ===
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} [kind=30000] - Event kind (default: 30000 for parameterized replaceable)
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, kind = 30000) {
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 result = await client.publish(dataEvent);
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)