holosphere 2.0.0-alpha20 → 2.0.0-alpha22

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 (69) hide show
  1. package/README.md +1 -2
  2. package/dist/cjs/holosphere.cjs +1 -1
  3. package/dist/esm/holosphere.js +43 -41
  4. package/dist/{index-COpLk9gL.cjs → index-B4xe-N5-.cjs} +2 -2
  5. package/dist/{index-COpLk9gL.cjs.map → index-B4xe-N5-.cjs.map} +1 -1
  6. package/dist/{index-D2WstuZJ.js → index-Bug_CGNq.js} +2 -2
  7. package/dist/{index-D2WstuZJ.js.map → index-Bug_CGNq.js.map} +1 -1
  8. package/dist/index-CaCPzdlv.cjs +29 -0
  9. package/dist/index-CaCPzdlv.cjs.map +1 -0
  10. package/dist/{index-B6-8KAQm.js → index-D76zMgwU.js} +2 -2
  11. package/dist/{index-B6-8KAQm.js.map → index-D76zMgwU.js.map} +1 -1
  12. package/dist/{index--QsHG_gD.cjs → index-DuOuk96g.cjs} +2 -2
  13. package/dist/{index--QsHG_gD.cjs.map → index-DuOuk96g.cjs.map} +1 -1
  14. package/dist/{index-BHptWysv.js → index-bYHRpACA.js} +2951 -7736
  15. package/dist/index-bYHRpACA.js.map +1 -0
  16. package/dist/{indexeddb-storage-kQ53UHEE.js → indexeddb-storage-BrIwr42m.js} +2 -2
  17. package/dist/{indexeddb-storage-kQ53UHEE.js.map → indexeddb-storage-BrIwr42m.js.map} +1 -1
  18. package/dist/{indexeddb-storage-wKG4mICM.cjs → indexeddb-storage-CFWfkdX9.cjs} +2 -2
  19. package/dist/{indexeddb-storage-wKG4mICM.cjs.map → indexeddb-storage-CFWfkdX9.cjs.map} +1 -1
  20. package/dist/{memory-storage-DnXCSbBl.js → memory-storage-BDQRj-2j.js} +2 -2
  21. package/dist/{memory-storage-DnXCSbBl.js.map → memory-storage-BDQRj-2j.js.map} +1 -1
  22. package/dist/{memory-storage-CGC8xM2G.cjs → memory-storage-bkatDnuR.cjs} +2 -2
  23. package/dist/{memory-storage-CGC8xM2G.cjs.map → memory-storage-bkatDnuR.cjs.map} +1 -1
  24. package/examples/demo.html +2 -29
  25. package/package.json +3 -8
  26. package/src/content/social-protocols.js +3 -59
  27. package/src/core/holosphere.js +16 -554
  28. package/src/crypto/nostr-utils.js +98 -1
  29. package/src/crypto/secp256k1.js +13 -411
  30. package/src/federation/discovery.js +7 -75
  31. package/src/federation/handshake.js +69 -202
  32. package/src/federation/hologram.js +275 -460
  33. package/src/federation/index.js +2 -9
  34. package/src/federation/registry.js +65 -1169
  35. package/src/federation/request-card.js +21 -35
  36. package/src/hierarchical/upcast.js +4 -9
  37. package/src/index.js +142 -296
  38. package/src/lib/federation-methods.js +388 -886
  39. package/src/storage/global-tables.js +1 -1
  40. package/src/storage/nostr-wrapper.js +9 -5
  41. package/src/subscriptions/manager.js +1 -1
  42. package/types/index.d.ts +145 -37
  43. package/bin/holosphere-activitypub.js +0 -158
  44. package/dist/2019-BzVkRcax.js +0 -6680
  45. package/dist/2019-BzVkRcax.js.map +0 -1
  46. package/dist/2019-C1hPR_Os.cjs +0 -8
  47. package/dist/2019-C1hPR_Os.cjs.map +0 -1
  48. package/dist/browser-BcmACE3G.js +0 -3058
  49. package/dist/browser-BcmACE3G.js.map +0 -1
  50. package/dist/browser-DaqYUTcG.cjs +0 -2
  51. package/dist/browser-DaqYUTcG.cjs.map +0 -1
  52. package/dist/index-BHptWysv.js.map +0 -1
  53. package/dist/index-CDlhzxT2.cjs +0 -29
  54. package/dist/index-CDlhzxT2.cjs.map +0 -1
  55. package/src/federation/capabilities.js +0 -162
  56. package/src/storage/backend-factory.js +0 -130
  57. package/src/storage/backend-interface.js +0 -161
  58. package/src/storage/backends/activitypub/server.js +0 -675
  59. package/src/storage/backends/activitypub-backend.js +0 -295
  60. package/src/storage/backends/gundb-backend.js +0 -875
  61. package/src/storage/backends/nostr-backend.js +0 -251
  62. package/src/storage/gun-async.js +0 -341
  63. package/src/storage/gun-auth.js +0 -373
  64. package/src/storage/gun-federation.js +0 -785
  65. package/src/storage/gun-references.js +0 -209
  66. package/src/storage/gun-schema.js +0 -306
  67. package/src/storage/gun-wrapper.js +0 -642
  68. package/src/storage/migration.js +0 -351
  69. package/src/storage/unified-storage.js +0 -161
@@ -1,12 +1,18 @@
1
1
  /**
2
- * @fileoverview Unified Federation Methods - UNIFIED MODEL
2
+ * @fileoverview Federation Methods Simplified.
3
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")
4
+ * Federation = exchanging pubkeys.
5
+ * No capability tokens, no complex handshake.
7
6
  *
8
- * All federation uses capabilities, providing consistent security semantics.
9
- * The unified `federate()` method handles both cases based on parameters.
7
+ * The hologram is the core infrastructure:
8
+ * - federate() adds a partner to the registry + stores config (no auto-hologram creation)
9
+ * - Reads are filtered by federation membership
10
+ * - Everyone can write everywhere; non-federated data is invisible
11
+ * - Holograms are only created explicitly via propagateData()
12
+ *
13
+ * For cryptographic privacy: encrypted lenses + key sharing via NIP-44 DMs.
14
+ * - shareLensKeys() shares symmetric keys with a federation partner
15
+ * - Only holders of the key can decrypt the data on the relay
10
16
  *
11
17
  * @module lib/federation-methods
12
18
  */
@@ -14,207 +20,109 @@
14
20
  import * as registry from '../federation/registry.js';
15
21
  import * as discovery from '../federation/discovery.js';
16
22
  import * as federation from '../federation/hologram.js';
17
- import * as storage from '../storage/unified-storage.js';
23
+ import * as storage from '../storage/nostr-wrapper.js';
18
24
  import * as nostrAsync from '../storage/nostr-async.js';
19
- import * as crypto from '../crypto/secp256k1.js';
25
+ import { isPubkey } from '../crypto/secp256k1.js';
26
+ import { sendLensKeyShare } from '../federation/handshake.js';
27
+ import { buildLensPath } from '../crypto/key-store.js';
28
+ import { registerHolon } from '../federation/holon-registry.js';
20
29
  import { ValidationError, AuthorizationError } from './errors.js';
21
- import { sha256 } from '@noble/hashes/sha256';
22
- import { bytesToHex } from '@noble/hashes/utils';
30
+
31
+ /**
32
+ * Normalize a holon target (string or object) to { holonId, authorPubKey }.
33
+ */
34
+ function normalizeHolonTarget(input, defaultPubKey) {
35
+ if (typeof input === 'string') {
36
+ return { holonId: input, authorPubKey: isPubkey(input) ? input : defaultPubKey };
37
+ }
38
+ return {
39
+ holonId: input.holonId,
40
+ authorPubKey: input.authorPubKey || (isPubkey(input.holonId) ? input.holonId : defaultPubKey),
41
+ };
42
+ }
23
43
 
24
44
  /**
25
45
  * Mixin that adds federation methods to a HoloSphere class.
26
- * Enables unified federation with capability-based access control,
27
- * hologram creation, and Nostr-based discovery protocols.
28
46
  *
29
- * UNIFIED MODEL: All federation uses capabilities for consistent security.
47
+ * SIMPLIFIED MODEL: Federation = pubkey list + holograms.
48
+ * No capability tokens. No complex access control.
30
49
  *
31
50
  * @param {Class} Base - Base class to extend
32
51
  * @returns {Class} Extended class with federation methods
33
52
  */
34
53
  export function withFederationMethods(Base) {
35
54
  return class extends Base {
36
- // === UNIFIED FEDERATION API ===
37
55
 
38
56
  /**
39
- * Federation method - UNIFIED MODEL
57
+ * Federate two holons the core operation.
40
58
  *
41
- * Single entry point for all federation operations.
42
- * Handles both same-author and cross-author federation consistently.
59
+ * This does two things:
60
+ * 1. Adds the partner to the federation registry (pubkey list)
61
+ * 2. Stores federation config
43
62
  *
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
63
+ * Holograms are NOT automatically created. Use propagateData() explicitly
64
+ * to create holograms when needed.
65
+ *
66
+ * @param {string|Object} source - Source holon
67
+ * @param {string|Object} target - Target holon
68
+ * @param {string} lensName - Lens name to federate
49
69
  * @param {Object} [options={}] - Federation options
50
70
  * @param {string} [options.direction='outbound'] - 'inbound', 'outbound', or 'bidirectional'
51
71
  * @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
- * );
72
+ * @param {boolean} [options.shareKeys=false] - Share encrypted lens keys with partner via NIP-44 DM
73
+ * @returns {Promise<Object>} Federation result
71
74
  */
72
75
  async federate(source, target, lensName, options = {}) {
73
76
  const {
74
77
  direction = 'outbound',
75
78
  mode = 'reference',
76
- propagate = true,
77
- capability: providedCapability = null,
78
- permissions = ['read'],
79
- filter = null,
79
+ shareKeys = false, // Share encrypted lens keys with partner
80
80
  } = options;
81
81
 
82
- // Normalize source and target to { holonId, authorPubKey } format
83
- // Helper function to detect if a string is a pubkey (64-char hex)
84
- const isPubkey = (str) => typeof str === 'string' && /^[0-9a-f]{64}$/i.test(str);
85
-
86
- // When the holon ID is a pubkey, use it as the authorPubKey for cross-holosphere federation
87
- const normalizedSource = typeof source === 'string'
88
- ? { holonId: source, authorPubKey: isPubkey(source) ? source : this.client.publicKey }
89
- : { holonId: source.holonId, authorPubKey: source.authorPubKey || (isPubkey(source.holonId) ? source.holonId : this.client.publicKey) };
90
-
91
- const normalizedTarget = typeof target === 'string'
92
- ? { holonId: target, authorPubKey: isPubkey(target) ? target : this.client.publicKey }
93
- : { holonId: target.holonId, authorPubKey: target.authorPubKey || (isPubkey(target.holonId) ? target.holonId : this.client.publicKey) };
82
+ const normalizedSource = normalizeHolonTarget(source, this.client.publicKey);
83
+ const normalizedTarget = normalizeHolonTarget(target, this.client.publicKey);
94
84
 
95
- // Validation
96
85
  if (normalizedSource.holonId === normalizedTarget.holonId &&
97
86
  normalizedSource.authorPubKey === normalizedTarget.authorPubKey) {
98
87
  throw new ValidationError('Cannot federate a holon with itself');
99
88
  }
100
89
 
101
90
  if (!['inbound', 'outbound', 'bidirectional'].includes(direction)) {
102
- throw new ValidationError(`Invalid direction: ${direction}. Must be 'inbound', 'outbound', or 'bidirectional'`);
91
+ throw new ValidationError(`Invalid direction: ${direction}`);
103
92
  }
104
93
 
105
- // Determine if this is self-federation or cross-federation
106
94
  const isSelfFederation = normalizedSource.authorPubKey === normalizedTarget.authorPubKey;
107
95
 
108
- // Issue or use provided capability
109
- let capability = providedCapability;
110
- if (!capability) {
111
- const expiresIn = isSelfFederation
112
- ? 365 * 24 * 60 * 60 * 1000 // 1 year for self
113
- : 24 * 60 * 60 * 1000; // 24 hours for cross
114
-
115
- capability = await crypto.issueCapability(
116
- permissions,
117
- { holonId: normalizedSource.holonId, lensName, dataId: '*' },
118
- normalizedTarget.authorPubKey,
119
- {
120
- expiresIn,
121
- issuer: normalizedSource.authorPubKey,
122
- issuerKey: this.client.privateKey,
123
- isSelfCapability: isSelfFederation,
124
- }
125
- );
96
+ // Add partner to federation list (if cross-author)
97
+ if (!isSelfFederation) {
98
+ const needsOutbound = direction === 'outbound' || direction === 'bidirectional';
99
+ const needsInbound = direction === 'inbound' || direction === 'bidirectional';
126
100
 
127
- // Store capability in registry
128
- if (isSelfFederation) {
129
- await registry.storeSelfCapability(this.client, this.config.appName, {
130
- token: capability,
131
- scope: { holonId: normalizedSource.holonId, lensName },
132
- permissions,
133
- });
134
- } else {
135
- await registry.storeInboundCapability(
136
- this.client,
137
- this.config.appName,
101
+ if (needsOutbound) {
102
+ await registry.addFederatedPartner(
103
+ this.client, this.config.appName,
104
+ normalizedTarget.authorPubKey,
105
+ { addedVia: 'federate' }
106
+ );
107
+ }
108
+ if (needsInbound) {
109
+ await registry.addFederatedPartner(
110
+ this.client, this.config.appName,
138
111
  normalizedSource.authorPubKey,
139
- {
140
- token: capability,
141
- scope: { holonId: normalizedSource.holonId, lensName },
142
- permissions,
143
- }
112
+ { addedVia: 'federate' }
144
113
  );
145
114
  }
146
115
  }
147
116
 
148
- // Setup federation based on direction
149
117
  const results = {
150
118
  source: normalizedSource,
151
119
  target: normalizedTarget,
152
120
  lensName,
153
121
  direction,
154
122
  mode,
155
- propagate,
156
- capability,
157
123
  isSelfFederation,
158
- propagated: { outbound: 0, inbound: 0 },
159
124
  };
160
125
 
161
- // Handle data propagation (skip if propagate: false)
162
- if (propagate && (direction === 'outbound' || direction === 'bidirectional')) {
163
- // Propagate existing data from source to target
164
- const sourceData = await this.read(normalizedSource.holonId, lensName, null, {
165
- resolveHolograms: false,
166
- });
167
-
168
- if (sourceData && Array.isArray(sourceData)) {
169
- for (const item of sourceData) {
170
- if (filter && !filter(item)) continue;
171
-
172
- await federation.propagateData(
173
- this.client,
174
- this.config.appName,
175
- item,
176
- normalizedSource.holonId,
177
- normalizedTarget.holonId,
178
- lensName,
179
- mode,
180
- {
181
- sourceAuthorPubKey: normalizedSource.authorPubKey,
182
- capability,
183
- }
184
- );
185
- results.propagated.outbound++;
186
- }
187
- }
188
- }
189
-
190
- if (propagate && (direction === 'inbound' || direction === 'bidirectional')) {
191
- // Propagate from target to source (need capability from target author)
192
- const targetData = await this.read(normalizedTarget.holonId, lensName, null, {
193
- resolveHolograms: false,
194
- });
195
-
196
- if (targetData && Array.isArray(targetData)) {
197
- for (const item of targetData) {
198
- if (filter && !filter(item)) continue;
199
-
200
- await federation.propagateData(
201
- this.client,
202
- this.config.appName,
203
- item,
204
- normalizedTarget.holonId,
205
- normalizedSource.holonId,
206
- lensName,
207
- mode,
208
- {
209
- sourceAuthorPubKey: normalizedTarget.authorPubKey,
210
- capability,
211
- }
212
- );
213
- results.propagated.inbound++;
214
- }
215
- }
216
- }
217
-
218
126
  // Store federation config
219
127
  await federation.setupFederation(
220
128
  this.client,
@@ -225,140 +133,79 @@ export function withFederationMethods(Base) {
225
133
  { direction, mode }
226
134
  );
227
135
 
136
+ // Share encrypted lens keys with partner (if requested and cross-author)
137
+ if (shareKeys && !isSelfFederation && this.client?.privateKey && this.keyStore) {
138
+ try {
139
+ const keyResult = await this.shareLensKeys(
140
+ normalizedTarget.authorPubKey,
141
+ [lensName],
142
+ { holonId: normalizedSource.holonId }
143
+ );
144
+ results.keysShared = keyResult.shared;
145
+ } catch (err) {
146
+ results.keysShared = 0;
147
+ results.keyShareError = err.message;
148
+ }
149
+ }
150
+
228
151
  return results;
229
152
  }
230
153
 
231
154
  /**
232
- * Unfederate method - UNIFIED MODEL
233
- *
234
- * Removes federation between source and target holons.
235
- *
236
- * @param {string|Object} source - Source holon (string or { holonId, authorPubKey })
237
- * @param {string|Object} target - Target holon (string or { holonId, authorPubKey })
238
- * @param {string} lensName - Lens name to unfederate
239
- * @returns {Promise<boolean>} True if unfederation succeeded
155
+ * Unfederate remove federation between source and target holons.
240
156
  */
241
157
  async unfederate(source, target, lensName) {
242
- const normalizedSource = typeof source === 'string'
243
- ? { holonId: source, authorPubKey: this.client.publicKey }
244
- : { holonId: source.holonId, authorPubKey: source.authorPubKey || this.client.publicKey };
158
+ const normalizedSource = normalizeHolonTarget(source, this.client.publicKey);
159
+ const normalizedTarget = normalizeHolonTarget(target, this.client.publicKey);
245
160
 
246
- const normalizedTarget = typeof target === 'string'
247
- ? { holonId: target, authorPubKey: this.client.publicKey }
248
- : { holonId: target.holonId, authorPubKey: target.authorPubKey || this.client.publicKey };
249
-
250
- // Remove federation config
251
161
  const configPath = storage.buildPath(this.config.appName, normalizedSource.holonId, lensName, '_federation');
252
162
  try {
253
163
  await storage.deleteData(this.client, configPath);
254
164
  } catch (e) {
255
- // Ignore errors - already unfederated or doesn't exist
165
+ // Ignore already unfederated
256
166
  }
257
167
 
258
168
  return true;
259
169
  }
260
170
 
261
- // === Legacy Methods (kept for backward compatibility) ===
171
+ // === Federation Registry Methods ===
262
172
 
263
- /**
264
- * Add a federated HoloSphere partner by public key.
265
- * @deprecated Use federate() with explicit authorPubKey instead
266
- * @param {string} pubKey - Public key (hex) of the federated HoloSphere
267
- * @param {Object} [options={}] - Federation options (metadata, capabilities, etc.)
268
- * @returns {Promise<Object>} Federation registration result
269
- * @throws {ValidationError} If pubKey is invalid
270
- */
271
173
  async addFederatedHolosphere(pubKey, options = {}) {
272
174
  if (!pubKey || typeof pubKey !== 'string') {
273
175
  throw new ValidationError('pubKey must be a valid public key string');
274
176
  }
275
- return registry.addFederatedPartner(
276
- this.client,
277
- this.config.appName,
278
- pubKey,
279
- options
280
- );
177
+ return registry.addFederatedPartner(this.client, this.config.appName, pubKey, options);
281
178
  }
282
179
 
283
- /**
284
- * Remove a federated HoloSphere partner.
285
- * @param {string} pubKey - Public key of the partner to remove
286
- * @returns {Promise<void>}
287
- */
288
180
  async removeFederatedHolosphere(pubKey) {
289
- return registry.removeFederatedPartner(
290
- this.client,
291
- this.config.appName,
292
- pubKey
293
- );
181
+ return registry.removeFederatedPartner(this.client, this.config.appName, pubKey);
294
182
  }
295
183
 
296
- /**
297
- * Get all federated HoloSphere public keys.
298
- * @returns {Promise<string[]>} Array of federated public keys
299
- */
300
184
  async getFederatedHolospheres() {
301
- return registry.getFederatedAuthors(
302
- this.client,
303
- this.config.appName
304
- );
185
+ return registry.getFederatedAuthors(this.client, this.config.appName);
305
186
  }
306
187
 
307
- /**
308
- * Get the complete federation registry.
309
- * @returns {Promise<Object>} Federation registry with all partners and capabilities
310
- */
311
188
  async getFederationRegistry() {
312
- return registry.getFederationRegistry(
313
- this.client,
314
- this.config.appName
315
- );
189
+ return registry.getFederationRegistry(this.client, this.config.appName);
316
190
  }
317
191
 
318
192
  /**
319
- * Store an inbound capability token from a federated partner.
320
- * @param {string} partnerPubKey - Partner's public key
321
- * @param {Object} capabilityInfo - Capability token and metadata
322
- * @returns {Promise<void>}
193
+ * Check if a pubkey is in this holosphere's federation.
194
+ * @param {string} pubKey - Public key to check
195
+ * @returns {Promise<boolean>}
323
196
  */
324
- async storeInboundCapability(partnerPubKey, capabilityInfo) {
325
- return registry.storeInboundCapability(
326
- this.client,
327
- this.config.appName,
328
- partnerPubKey,
329
- capabilityInfo
330
- );
197
+ async isFederated(pubKey) {
198
+ return registry.isFederated(this.client, this.config.appName, pubKey);
331
199
  }
332
200
 
333
201
  /**
334
- * Read data from a federated source with capability verification.
335
- * @param {string} sourcePubKey - Source HoloSphere public key
336
- * @param {string} holonId - Holon ID to read from
337
- * @param {string} lensName - Lens name to read from
338
- * @param {string|null} [dataId=null] - Optional specific data ID (null for all)
339
- * @returns {Promise<Object|Object[]>} Data from federated source
340
- * @throws {AuthorizationError} If capability verification fails
202
+ * Read data from a federated source.
203
+ * Simplified: just checks federation membership, then reads with author filter.
341
204
  */
342
205
  async readFromFederatedSource(sourcePubKey, holonId, lensName, dataId = null) {
343
- const capabilityEntry = await registry.getCapabilityForAuthor(
344
- this.client,
345
- this.config.appName,
346
- sourcePubKey,
347
- { holonId, lensName, dataId }
348
- );
349
-
350
- if (!capabilityEntry) {
351
- throw new AuthorizationError('No valid capability for federated source', 'read');
352
- }
353
-
354
- const isValid = await this.verifyCapability(
355
- capabilityEntry.token,
356
- 'read',
357
- { holonId, lensName, dataId }
358
- );
359
-
360
- if (!isValid) {
361
- throw new AuthorizationError('Capability verification failed', 'read');
206
+ const federated = await this.isFederated(sourcePubKey);
207
+ if (!federated) {
208
+ throw new AuthorizationError('Source is not in the federation', 'read');
362
209
  }
363
210
 
364
211
  if (dataId) {
@@ -377,545 +224,137 @@ export function withFederationMethods(Base) {
377
224
  }
378
225
 
379
226
  /**
380
- * Create a cross-HoloSphere hologram reference.
381
- * Links data from another HoloSphere into this one using capability-based access.
382
- * @param {string} sourcePubKey - Source HoloSphere public key
383
- * @param {string} sourceHolon - Source holon ID
384
- * @param {string} lensName - Lens name
385
- * @param {string} dataId - Data ID
386
- * @param {string} targetHolon - Target holon in this HoloSphere
387
- * @param {Object} [options={}] - Hologram options
388
- * @param {boolean} [options.embedCapability=true] - Embed capability in hologram
389
- * @returns {Promise<Object>} Created hologram
390
- * @throws {AuthorizationError} If no valid capability exists
227
+ * Create a cross-holosphere hologram reference.
391
228
  */
392
- async createCrossHolosphereHologram(sourcePubKey, sourceHolon, lensName, dataId, targetHolon, options = {}) {
393
- const { embedCapability = true } = options;
394
-
395
- const capabilityEntry = await registry.getCapabilityForAuthor(
396
- this.client,
397
- this.config.appName,
398
- sourcePubKey,
399
- { holonId: sourceHolon, lensName, dataId }
400
- );
401
-
402
- if (!capabilityEntry) {
403
- throw new AuthorizationError('No valid capability for cross-holosphere hologram', 'read');
404
- }
405
-
229
+ async createCrossHolosphereHologram(sourcePubKey, sourceHolon, lensName, dataId, targetHolon, _options = {}) {
406
230
  const hologram = federation.createHologram(
407
- sourceHolon,
408
- targetHolon,
409
- lensName,
410
- dataId,
231
+ sourceHolon, targetHolon, lensName, dataId,
411
232
  this.config.appName,
412
- {
413
- authorPubKey: sourcePubKey,
414
- capability: embedCapability ? capabilityEntry.token : null,
415
- }
233
+ { authorPubKey: sourcePubKey }
416
234
  );
417
235
 
418
236
  const targetPath = storage.buildPath(this.config.appName, targetHolon, lensName, dataId);
419
237
  await storage.write(this.client, targetPath, hologram);
420
-
421
238
  return hologram;
422
239
  }
423
240
 
424
- /**
425
- * Issue a capability token for federated access.
426
- * Grants another HoloSphere permission to access data with specified scope.
427
- * @param {string} targetPubKey - Target HoloSphere public key to grant access to
428
- * @param {Object} scope - Access scope (holonId, lensName, dataId patterns)
429
- * @param {string[]} permissions - Array of permissions ('read', 'write', etc.)
430
- * @param {Object} [options={}] - Capability options
431
- * @param {number} [options.expiresIn=3600000] - Expiration time in milliseconds
432
- * @param {boolean} [options.trackInRegistry=true] - Store in federation registry
433
- * @returns {Promise<string>} Capability token
434
- */
435
- async issueCapabilityForFederation(targetPubKey, scope, permissions, options = {}) {
436
- const { expiresIn = 3600000, trackInRegistry = true } = options;
437
-
438
- const token = await crypto.issueCapability(
439
- permissions,
440
- scope,
441
- targetPubKey,
442
- {
443
- expiresIn,
444
- issuerKey: this.client.privateKey,
445
- }
446
- );
447
-
448
- if (trackInRegistry) {
449
- const partner = await registry.getFederatedPartner(
450
- this.client,
451
- this.config.appName,
452
- targetPubKey
453
- );
454
-
455
- if (!partner) {
456
- await registry.addFederatedPartner(
457
- this.client,
458
- this.config.appName,
459
- targetPubKey,
460
- { addedVia: 'capability_issued' }
461
- );
462
- }
463
-
464
- await registry.storeOutboundCapability(
465
- this.client,
466
- this.config.appName,
467
- targetPubKey,
468
- {
469
- tokenHash: await this._hashToken(token),
470
- scope,
471
- permissions,
472
- expires: Date.now() + expiresIn,
473
- }
474
- );
475
- }
476
-
477
- return token;
478
- }
479
-
480
- /**
481
- * Hash a capability token for storage.
482
- * @private
483
- * @param {string} token - Capability token to hash
484
- * @returns {Promise<string>} SHA256 hash of token
485
- */
486
- async _hashToken(token) {
487
- const encoder = new TextEncoder();
488
- return bytesToHex(sha256(encoder.encode(token)));
489
- }
490
-
491
241
  // === Nostr Discovery Protocol ===
492
242
 
493
- /**
494
- * Send a federation request to another HoloSphere via Nostr.
495
- * @param {string} targetPubKey - Target HoloSphere public key
496
- * @param {Object} [options={}] - Request options (message, metadata, etc.)
497
- * @returns {Promise<Object>} Request event
498
- */
499
243
  async sendFederationRequest(targetPubKey, options = {}) {
500
- return discovery.sendFederationRequest(
501
- this.client,
502
- this.config.appName,
503
- targetPubKey,
504
- options
505
- );
244
+ return discovery.sendFederationRequest(this.client, this.config.appName, targetPubKey, options);
506
245
  }
507
246
 
508
- /**
509
- * Subscribe to incoming federation requests.
510
- * @param {Function} callback - Callback function (request) => void
511
- * @returns {Promise<Object>} Subscription object with unsubscribe method
512
- */
513
247
  async subscribeFederationRequests(callback) {
514
248
  return discovery.subscribeFederationRequests(this.client, callback);
515
249
  }
516
250
 
517
- /**
518
- * Accept a federation request and establish partnership.
519
- * @param {Object} request - Federation request object
520
- * @param {Object} [options={}] - Acceptance options (capabilities to grant, etc.)
521
- * @returns {Promise<Object>} Acceptance event
522
- */
523
251
  async acceptFederationRequest(request, options = {}) {
524
- return discovery.acceptFederationRequest(
525
- this.client,
526
- this.config.appName,
527
- request,
528
- options
529
- );
252
+ return discovery.acceptFederationRequest(this.client, this.config.appName, request, options);
530
253
  }
531
254
 
532
- /**
533
- * Decline a federation request.
534
- * @param {Object} request - Federation request object
535
- * @param {string} [reason=''] - Optional reason for declining
536
- * @returns {Promise<Object>} Decline event
537
- */
538
255
  async declineFederationRequest(request, reason = '') {
539
- return discovery.declineFederationRequest(
540
- this.client,
541
- this.config.appName,
542
- request,
543
- reason
544
- );
256
+ return discovery.declineFederationRequest(this.client, this.config.appName, request, reason);
545
257
  }
546
258
 
547
- /**
548
- * Subscribe to federation request acceptances.
549
- * @param {Function} callback - Callback function (acceptance) => void
550
- * @returns {Promise<Object>} Subscription object with unsubscribe method
551
- */
552
259
  async subscribeFederationAcceptances(callback) {
553
- return discovery.subscribeFederationAcceptances(
554
- this.client,
555
- this.config.appName,
556
- callback
557
- );
260
+ return discovery.subscribeFederationAcceptances(this.client, this.config.appName, callback);
558
261
  }
559
262
 
560
- /**
561
- * Subscribe to federation request declines.
562
- * @param {Function} callback - Callback function (decline) => void
563
- * @returns {Promise<Object>} Subscription object with unsubscribe method
564
- */
565
263
  async subscribeFederationDeclines(callback) {
566
264
  return discovery.subscribeFederationDeclines(this.client, callback);
567
265
  }
568
266
 
569
- /**
570
- * Get all pending federation requests.
571
- * @param {Object} [options={}] - Query options (limit, since, etc.)
572
- * @returns {Promise<Object[]>} Array of pending requests
573
- */
574
267
  async getPendingFederationRequests(options = {}) {
575
268
  return discovery.getPendingFederationRequests(this.client, options);
576
269
  }
577
270
 
578
- // === Federated Lens Reception (Hologram-based) ===
271
+ // === Encrypted Federation (Key Sharing via NIP-44 DMs) ===
579
272
 
580
273
  /**
581
- * Receive data from a federated partner's lens as holograms in your own holon.
582
- * This creates holograms pointing to the partner's data, allowing you to see
583
- * their data in your holon while the source of truth remains with the partner.
274
+ * Share lens encryption keys with a federation partner.
584
275
  *
585
- * @param {string} partnerPubKey - Partner's public key
586
- * @param {string} sourceHolonId - Partner's holon ID to receive data from
587
- * @param {string} lensName - Lens name to receive
588
- * @param {string} targetHolonId - Your holon ID where holograms will be created
589
- * @param {Object} [options={}] - Reception options
590
- * @param {Function} [options.filter] - Optional filter function to select which items to receive
591
- * @param {boolean} [options.overwrite=false] - Whether to overwrite existing holograms
592
- * @returns {Promise<Object>} Result with count of received holograms and any errors
593
- * @throws {AuthorizationError} If no valid capability exists for the source
276
+ * This is how you make your data readable ONLY by your federation:
277
+ * 1. Your lenses are encrypted with symmetric keys (AES-256-GCM)
278
+ * 2. This method wraps each key for the partner using NIP-44
279
+ * 3. Sends the wrapped key via encrypted DM (kind 4)
280
+ * 4. Partner unwraps with their private key → can now decrypt your data
281
+ *
282
+ * Non-federation members see encrypted blobs they can't decrypt.
594
283
  *
595
- * @example
596
- * // Receive events from a federated partner into your holon
597
- * const result = await hs.receiveFederatedLens(
598
- * 'partner-pubkey',
599
- * 'partner-holon-id',
600
- * 'events',
601
- * 'my-holon-id'
602
- * );
603
- * console.log(`Received ${result.received} holograms`);
284
+ * @param {string} partnerPubKey - Partner's public key
285
+ * @param {string[]} [lensNames] - Specific lenses to share (default: all encrypted lenses)
286
+ * @param {Object} [options={}] - Options
287
+ * @param {string} [options.holonId] - Specific holon (default: client.publicKey)
288
+ * @returns {Promise<Object>} { shared: number, failed: number, lenses: string[] }
604
289
  */
605
- async receiveFederatedLens(partnerPubKey, sourceHolonId, lensName, targetHolonId, options = {}) {
606
- const { filter = null, overwrite = false } = options;
607
-
608
- // Get capability for accessing partner's data
609
- const capabilityEntry = await registry.getCapabilityForAuthor(
610
- this.client,
611
- this.config.appName,
612
- partnerPubKey,
613
- { holonId: sourceHolonId, lensName, dataId: '*' }
614
- );
615
-
616
- if (!capabilityEntry) {
617
- throw new AuthorizationError(
618
- `No valid capability to access ${sourceHolonId}/${lensName} from ${partnerPubKey}`,
619
- 'read'
620
- );
621
- }
622
-
623
- // Read all data from partner's lens
624
- const sourcePath = storage.buildPath(this.config.appName, sourceHolonId, lensName);
625
- const partnerData = await nostrAsync.nostrGetAll(this.client, sourcePath, 30000, {
626
- authors: [partnerPubKey],
627
- includeAuthor: true,
628
- });
629
-
630
- if (!partnerData || !Array.isArray(partnerData)) {
631
- return { received: 0, skipped: 0, errors: [] };
290
+ async shareLensKeys(partnerPubKey, lensNames = null, options = {}) {
291
+ if (!this.client?.privateKey) {
292
+ throw new ValidationError('Private key required to share lens keys');
632
293
  }
633
294
 
634
- const result = { received: 0, skipped: 0, errors: [] };
295
+ const holonId = options.holonId || this.client.publicKey;
296
+ const result = { shared: 0, failed: 0, lenses: [] };
635
297
 
636
- for (const item of partnerData) {
637
- if (!item || !item.id) continue;
298
+ // Get all own keys from the key store
299
+ const ownKeys = this.keyStore.ownKeys;
638
300
 
639
- // Apply filter if provided
640
- if (filter && !filter(item)) {
641
- result.skipped++;
642
- continue;
301
+ for (const [lensPath, keyData] of ownKeys) {
302
+ // If specific lenses requested, filter
303
+ if (lensNames) {
304
+ const matchesLens = lensNames.some(name => lensPath.includes(`/${name}`));
305
+ if (!matchesLens) continue;
643
306
  }
644
307
 
645
- // Skip if hologram already exists and overwrite is false
646
- if (!overwrite) {
647
- const existingPath = storage.buildPath(this.config.appName, targetHolonId, lensName, item.id);
648
- const existing = await storage.read(this.client, existingPath);
649
- if (existing && existing.hologram) {
650
- result.skipped++;
651
- continue;
652
- }
308
+ // If specific holonId, filter
309
+ if (holonId && !lensPath.includes(`/${holonId}/`) && !lensPath.startsWith(`${this.config.appName}/${holonId}/`)) {
310
+ continue;
653
311
  }
654
312
 
655
313
  try {
656
- // Create hologram in target holon pointing to partner's data
657
- const hologram = await federation.createHologramWithCapability(
658
- this.client,
659
- sourceHolonId,
660
- targetHolonId,
661
- lensName,
662
- item.id,
663
- this.config.appName,
664
- {
665
- sourceAuthorPubKey: partnerPubKey,
666
- targetAuthorPubKey: this.client.publicKey,
667
- capability: capabilityEntry.token,
668
- permissions: ['read'],
669
- }
314
+ // Wrap the key for this partner
315
+ const wrappedKey = this.keyStore.wrapKeyForPartner(
316
+ lensPath,
317
+ this.client.privateKey,
318
+ partnerPubKey
670
319
  );
671
320
 
672
- const targetPath = storage.buildPath(this.config.appName, targetHolonId, lensName, item.id);
673
- await storage.write(this.client, targetPath, hologram);
674
- result.received++;
675
- } catch (error) {
676
- result.errors.push({ id: item.id, error: error.message });
677
- }
678
- }
679
-
680
- return result;
681
- }
321
+ if (!wrappedKey) continue;
682
322
 
683
- /**
684
- * Subscribe to a federated partner's lens for real-time hologram updates.
685
- * When the partner writes new data, holograms are automatically created in your holon.
686
- *
687
- * @param {string} partnerPubKey - Partner's public key
688
- * @param {string} sourceHolonId - Partner's holon ID to subscribe to
689
- * @param {string} lensName - Lens name to subscribe to
690
- * @param {string} targetHolonId - Your holon ID where holograms will be created
691
- * @param {Object} [options={}] - Subscription options
692
- * @param {Function} [options.onHologram] - Callback when a new hologram is created (hologram, sourceData) => void
693
- * @param {Function} [options.onError] - Callback for errors (error) => void
694
- * @param {Function} [options.filter] - Optional filter function to select which items to receive
695
- * @returns {Promise<Object>} Subscription object with unsubscribe() method
696
- *
697
- * @example
698
- * // Subscribe to partner's events lens
699
- * const sub = await hs.subscribeFederatedLens(
700
- * 'partner-pubkey',
701
- * 'partner-holon-id',
702
- * 'events',
703
- * 'my-holon-id',
704
- * {
705
- * onHologram: (hologram, data) => {
706
- * console.log(`New hologram received: ${data.id}`);
707
- * }
708
- * }
709
- * );
710
- *
711
- * // Later, unsubscribe
712
- * sub.unsubscribe();
713
- */
714
- async subscribeFederatedLens(partnerPubKey, sourceHolonId, lensName, targetHolonId, options = {}) {
715
- const { onHologram, onError, filter = null } = options;
716
-
717
- // Get capability for accessing partner's data
718
- const capabilityEntry = await registry.getCapabilityForAuthor(
719
- this.client,
720
- this.config.appName,
721
- partnerPubKey,
722
- { holonId: sourceHolonId, lensName, dataId: '*' }
723
- );
724
-
725
- if (!capabilityEntry) {
726
- throw new AuthorizationError(
727
- `No valid capability to subscribe to ${sourceHolonId}/${lensName} from ${partnerPubKey}`,
728
- 'read'
729
- );
730
- }
731
-
732
- // Track processed items to avoid duplicates
733
- const processedIds = new Set();
734
-
735
- // Subscribe to partner's lens path
736
- const sourcePath = storage.buildPath(this.config.appName, sourceHolonId, lensName);
737
-
738
- const handleEvent = async (data) => {
739
- if (!data || !data.id) return;
740
-
741
- // Skip if already processed
742
- if (processedIds.has(data.id)) return;
743
- processedIds.add(data.id);
744
-
745
- // Apply filter if provided
746
- if (filter && !filter(data)) return;
747
-
748
- try {
749
- // Check if hologram already exists
750
- const existingPath = storage.buildPath(this.config.appName, targetHolonId, lensName, data.id);
751
- const existing = await storage.read(this.client, existingPath);
752
-
753
- if (existing && existing.hologram) {
754
- // Hologram already exists, skip
755
- return;
756
- }
757
-
758
- // Create hologram in target holon
759
- const hologram = await federation.createHologramWithCapability(
323
+ // Send via NIP-44 encrypted DM
324
+ await sendLensKeyShare(
760
325
  this.client,
761
- sourceHolonId,
762
- targetHolonId,
763
- lensName,
764
- data.id,
765
- this.config.appName,
766
- {
767
- sourceAuthorPubKey: partnerPubKey,
768
- targetAuthorPubKey: this.client.publicKey,
769
- capability: capabilityEntry.token,
770
- permissions: ['read'],
771
- }
326
+ this.client.privateKey,
327
+ partnerPubKey,
328
+ { lensPath, wrappedKey }
772
329
  );
773
330
 
774
- const targetPath = storage.buildPath(this.config.appName, targetHolonId, lensName, data.id);
775
- await storage.write(this.client, targetPath, hologram);
776
-
777
- if (onHologram) {
778
- onHologram(hologram, data);
779
- }
331
+ result.shared++;
332
+ result.lenses.push(lensPath);
780
333
  } catch (error) {
781
- if (onError) {
782
- onError(error);
783
- }
334
+ result.failed++;
784
335
  }
785
- };
786
-
787
- // Subscribe using nostr-async subscription with author filter
788
- const subscription = await nostrAsync.nostrSubscribe(
789
- this.client,
790
- sourcePath,
791
- handleEvent,
792
- 30000,
793
- { authors: [partnerPubKey] }
794
- );
795
-
796
- return {
797
- unsubscribe: () => {
798
- if (subscription && subscription.unsubscribe) {
799
- subscription.unsubscribe();
800
- }
801
- processedIds.clear();
802
- },
803
- processedCount: () => processedIds.size,
804
- };
805
- }
806
-
807
- /**
808
- * Sync all federated lenses for a holon.
809
- * Receives holograms from all federated partners for their configured inbound lenses.
810
- *
811
- * @param {string} holonId - Your holon ID
812
- * @param {Object} [options={}] - Sync options
813
- * @param {boolean} [options.overwrite=false] - Whether to overwrite existing holograms
814
- * @returns {Promise<Object>} Sync results per partner
815
- *
816
- * @example
817
- * // Sync all federated data for a holon
818
- * const results = await hs.syncFederatedLenses('my-holon-id');
819
- * console.log(results);
820
- */
821
- async syncFederatedLenses(holonId, options = {}) {
822
- const { overwrite = false } = options;
823
-
824
- // Get federation configuration for this holon
825
- const federationData = await this.readGlobal('federation', holonId);
826
- if (!federationData) {
827
- return { error: 'No federation configuration found for this holon' };
828
336
  }
829
337
 
830
- const results = {};
831
-
832
- // Get all federated partners
833
- const partners = await this.getFederatedHolospheres();
834
-
835
- for (const partnerPubKey of partners) {
836
- results[partnerPubKey] = { lenses: {}, errors: [] };
837
-
838
- // Get partner's lens configuration
839
- const partnerLensConfig = federationData.lensConfig?.[partnerPubKey];
840
- const inboundLenses = partnerLensConfig?.inbound || [];
841
-
842
- for (const lensName of inboundLenses) {
843
- try {
844
- // For cross-holosphere federation, the sourceHolonId would be the partner's holon
845
- // We need to determine the partner's holon ID - this might be stored in the federation config
846
- const partnerHolonId = partnerLensConfig?.holonId || holonId;
847
-
848
- const result = await this.receiveFederatedLens(
849
- partnerPubKey,
850
- partnerHolonId,
851
- lensName,
852
- holonId,
853
- { overwrite }
854
- );
855
-
856
- results[partnerPubKey].lenses[lensName] = result;
857
- } catch (error) {
858
- results[partnerPubKey].errors.push({
859
- lens: lensName,
860
- error: error.message,
861
- });
862
- }
863
- }
864
- }
865
-
866
- return results;
338
+ return result;
867
339
  }
868
340
 
869
- // === UNIFIED ACCESS CONTROL ===
341
+ // === Access Control (Simplified) ===
870
342
 
871
343
  /**
872
- * Unified access verification method.
873
- * Single entry point for checking all access permissions (read, write, etc.).
874
- * Replaces and unifies the previous separate access checking methods.
875
- *
876
- * @param {string} targetHolonId - The holon being accessed
877
- * @param {string} lensName - The lens being accessed
878
- * @param {string} actorPubKey - The actor's public key
879
- * @param {string} permission - Permission to check ('read' or 'write')
880
- * @param {Object} [options={}] - Options
881
- * @param {string} [options.actingAsHolon] - The holon the actor is acting on behalf of
882
- * @param {string} [options.capabilityToken] - Explicit capability token to verify
883
- * @returns {Promise<Object>} { allowed: boolean, via: string, reason?: string, grant?: Object }
884
- *
885
- * @example
886
- * // Check if current user can read from a holon
887
- * const result = await hs.canAccess(targetHolonId, 'quests', myPubKey, 'read');
888
- * if (result.allowed) {
889
- * console.log('Access granted via:', result.via);
890
- * }
891
- *
892
- * @example
893
- * // Check write access with explicit capability
894
- * const result = await hs.canAccess(targetHolonId, 'quests', myPubKey, 'write', {
895
- * capabilityToken: myCapToken
896
- * });
344
+ * Unified access check.
345
+ * Simplified: owner always has access; federated partners always have access.
346
+ * No capability tokens needed.
897
347
  */
898
348
  async canAccess(targetHolonId, lensName, actorPubKey, permission, options = {}) {
899
- const { actingAsHolon = null, capabilityToken = null } = options;
900
-
901
- // 1. Owner check - actor owns the target holon
349
+ // 1. Owner check
902
350
  if (actorPubKey === targetHolonId) {
903
351
  return { allowed: true, via: 'owner' };
904
352
  }
905
353
 
906
- // 2. Explicit capability token verification
907
- if (capabilityToken) {
908
- try {
909
- const valid = await this.verifyCapability(capabilityToken, permission, {
910
- holonId: targetHolonId,
911
- lensName,
912
- });
913
- if (valid) {
914
- return { allowed: true, via: 'capability' };
915
- }
916
- } catch (err) {
917
- console.log('[canAccess] Capability verification failed:', err.message);
918
- }
354
+ // 2. Federation check
355
+ const federated = await this.isFederated(actorPubKey);
356
+ if (federated) {
357
+ return { allowed: true, via: 'federation' };
919
358
  }
920
359
 
921
360
  // 3. Membership in target holon
@@ -923,248 +362,311 @@ export function withFederationMethods(Base) {
923
362
  const members = await this.get(targetHolonId, 'users');
924
363
  if (members && Array.isArray(members)) {
925
364
  const isMember = members.some(m =>
926
- m.pubKey === actorPubKey ||
927
- m.id === actorPubKey ||
928
- m.nostrPubKey === actorPubKey
365
+ m.pubKey === actorPubKey || m.id === actorPubKey || m.nostrPubKey === actorPubKey
929
366
  );
930
367
  if (isMember) {
931
368
  return { allowed: true, via: 'membership' };
932
369
  }
933
370
  }
934
- } catch (err) {
935
- // Users lens might not exist or be empty - continue to check grants
936
- console.log('[canAccess] Could not read users lens:', err.message);
937
- }
938
-
939
- // 4. Access grant via federation (unified check)
940
- // First check using the new unified findAccessGrant
941
- const scope = { holonId: targetHolonId, lensName };
942
-
943
- // Check if actingAsHolon is specified
944
- if (actingAsHolon) {
945
- // Verify the actor is a member of the acting holon
946
- const actorHolons = await this.getHolonsForPubKey(actorPubKey);
947
- const isMemberOfActingHolon = actorHolons.some(h => h.id === actingAsHolon);
948
-
949
- if (!isMemberOfActingHolon) {
950
- return { allowed: false, via: 'none', reason: 'Not a member of the acting holon' };
951
- }
952
-
953
- // Check if actingAsHolon has an access grant
954
- const grant = await registry.findAccessGrant(
955
- this.client,
956
- this.config.appName,
957
- actingAsHolon,
958
- scope,
959
- permission,
960
- { direction: 'inbound' }
961
- );
962
-
963
- if (grant) {
964
- return {
965
- allowed: true,
966
- via: 'federation',
967
- grant,
968
- reason: `Acting as ${actingAsHolon} with ${permission} grant`,
969
- };
970
- }
971
- } else {
972
- // Check all holons the actor is a member of
973
- const actorHolons = await this.getHolonsForPubKey(actorPubKey);
974
-
975
- for (const holon of actorHolons) {
976
- const grant = await registry.findAccessGrant(
977
- this.client,
978
- this.config.appName,
979
- holon.id,
980
- scope,
981
- permission,
982
- { direction: 'inbound' }
983
- );
984
-
985
- if (grant) {
986
- return {
987
- allowed: true,
988
- via: 'federation',
989
- grant,
990
- reason: `Member of ${holon.name || holon.id} which has ${permission} grant`,
991
- viaHolon: holon.id,
992
- };
993
- }
994
- }
371
+ } catch (_err) {
372
+ // Users lens might not exist
995
373
  }
996
374
 
997
375
  return { allowed: false, via: 'none', reason: 'No access' };
998
376
  }
999
377
 
1000
- // === CROSS-HOLON WRITE ACCESS ===
1001
-
1002
378
  /**
1003
- * Check if a writer can write to a target holon's lens.
1004
- * This is a backward-compatible wrapper that delegates to canAccess().
1005
- *
1006
- * @param {string} targetHolonId - The holon being written to
1007
- * @param {string} lensName - The lens being written to
1008
- * @param {string} writerPubKey - The writer's public key
1009
- * @param {Object} [options={}] - Options
1010
- * @param {string} [options.actingAsHolon] - The holon the writer is acting on behalf of
1011
- * @param {string} [options.capabilityToken] - Explicit capability token to verify
1012
- * @returns {Promise<Object>} { canWrite: boolean, reason: string, accessType: string }
1013
- *
1014
- * @example
1015
- * // Check if current user can write to another holon
1016
- * const result = await hs.canWrite(targetHolonId, 'quests', myPubKey);
1017
- * if (result.canWrite) {
1018
- * console.log('Access granted:', result.accessType);
1019
- * }
379
+ * Check write access (delegates to canAccess).
1020
380
  */
1021
381
  async canWrite(targetHolonId, lensName, writerPubKey, options = {}) {
1022
- // Delegate to unified canAccess method
1023
382
  const result = await this.canAccess(targetHolonId, lensName, writerPubKey, 'write', options);
1024
-
1025
- // Convert to legacy format for backward compatibility
1026
383
  return {
1027
384
  canWrite: result.allowed,
1028
385
  reason: result.reason || result.via,
1029
386
  accessType: result.via,
1030
- viaHolon: result.viaHolon,
1031
- grant: result.grant,
1032
387
  };
1033
388
  }
1034
389
 
1035
390
  /**
1036
- * Check if an actor can read from a target holon's lens.
1037
- * Convenience wrapper around canAccess() for read operations.
1038
- *
1039
- * @param {string} targetHolonId - The holon being read from
1040
- * @param {string} lensName - The lens being read from
1041
- * @param {string} readerPubKey - The reader's public key
1042
- * @param {Object} [options={}] - Options
1043
- * @param {string} [options.actingAsHolon] - The holon the reader is acting on behalf of
1044
- * @param {string} [options.capabilityToken] - Explicit capability token to verify
1045
- * @returns {Promise<Object>} { canRead: boolean, reason: string, accessType: string }
391
+ * Check read access (delegates to canAccess).
1046
392
  */
1047
393
  async canRead(targetHolonId, lensName, readerPubKey, options = {}) {
1048
- // Delegate to unified canAccess method
1049
394
  const result = await this.canAccess(targetHolonId, lensName, readerPubKey, 'read', options);
1050
-
1051
395
  return {
1052
396
  canRead: result.allowed,
1053
397
  reason: result.reason || result.via,
1054
398
  accessType: result.via,
1055
- viaHolon: result.viaHolon,
1056
- grant: result.grant,
1057
399
  };
1058
400
  }
1059
401
 
1060
402
  /**
1061
403
  * Get all holons that a public key is a member of.
1062
- * Searches federation registry and users lenses.
1063
- *
1064
- * @param {string} pubKey - The public key to look up
1065
- * @returns {Promise<Array>} Array of { id, name } for each holon
1066
404
  */
1067
405
  async getHolonsForPubKey(pubKey) {
1068
- const results = [];
1069
-
1070
- // The user's own holon (their pubkey is also a holon)
1071
- results.push({ id: pubKey, name: 'Personal Holon' });
406
+ const results = [{ id: pubKey, name: 'Personal Holon' }];
1072
407
 
1073
- // Check federation registry for holons where user has membership
1074
408
  try {
1075
409
  const reg = await registry.getFederationRegistry(this.client, this.config.appName);
1076
410
  if (reg && reg.federatedWith) {
1077
411
  for (const partner of reg.federatedWith) {
1078
- // Check if this partner's holon has the user as a member
1079
412
  try {
1080
413
  const members = await this.get(partner.pubKey, 'users');
1081
414
  if (members && Array.isArray(members)) {
1082
415
  const isMember = members.some(m =>
1083
- m.pubKey === pubKey ||
1084
- m.id === pubKey ||
1085
- m.nostrPubKey === pubKey
416
+ m.pubKey === pubKey || m.id === pubKey || m.nostrPubKey === pubKey
1086
417
  );
1087
418
  if (isMember) {
1088
419
  results.push({ id: partner.pubKey, name: partner.alias || partner.pubKey });
1089
420
  }
1090
421
  }
1091
- } catch (err) {
1092
- // Skip holons we can't read
1093
- }
422
+ } catch (_err) { /* skip */ }
1094
423
  }
1095
424
  }
1096
- } catch (err) {
1097
- console.warn('[getHolonsForPubKey] Error reading federation registry:', err.message);
1098
- }
425
+ } catch (_err) { /* skip */ }
1099
426
 
1100
427
  return results;
1101
428
  }
1102
429
 
1103
- /**
1104
- * Grant write access to a federated holon for a specific lens.
1105
- * Members of the granted holon will be able to write to this lens.
1106
- *
1107
- * @param {string} holonId - The holon to grant write access to
1108
- * @param {string} lensName - The lens to grant write access for (or '*' for all)
1109
- * @param {Object} [options={}] - Grant options
1110
- * @param {number} [options.expiresAt] - Optional expiration timestamp
1111
- * @returns {Promise<boolean>} Success indicator
1112
- *
1113
- * @example
1114
- * // Grant holon B write access to quests lens
1115
- * await hs.grantWriteAccess('holon-b-pubkey', 'quests');
1116
- */
430
+ // === Write Grant Backward Compat ===
431
+
1117
432
  async grantWriteAccess(holonId, lensName, options = {}) {
1118
- return registry.grantWriteAccessToHolon(
1119
- this.client,
1120
- this.config.appName,
1121
- holonId,
1122
- lensName,
1123
- options
1124
- );
433
+ return registry.addFederatedPartner(this.client, this.config.appName, holonId, {
434
+ addedVia: 'write_grant',
435
+ });
1125
436
  }
1126
437
 
438
+ // === Share Protocol (Hologram DM as Federation) ===
439
+
1127
440
  /**
1128
- * Revoke write access from a federated holon for a specific lens.
441
+ * Share data with another user via NIP-44 DM.
442
+ *
443
+ * - If `item` is provided → share one item (creates an item hologram on accept)
444
+ * - If `item` is omitted → share an entire lens (creates a lens hologram on accept)
445
+ *
446
+ * This replaces `federate()` + `propagateData()` + `initiateFederationHandshake()`
447
+ * for cross-author cases. Same-author (H3 hierarchical) still uses propagateData().
1129
448
  *
1130
- * @param {string} holonId - The holon to revoke write access from
1131
- * @param {string} lensName - The lens to revoke write access for
1132
- * @returns {Promise<boolean>} Success indicator
449
+ * @param {string} target - Recipient's hex public key
450
+ * @param {string} lens - Lens name to share
451
+ * @param {string|null} [item=null] - Specific data ID, or null for lens-level share
452
+ * @param {Object} [opts={}] - Options
453
+ * @param {Array} [opts.keys] - Bundled lens encryption keys [{ path, key }]
454
+ * @param {string} [opts.msg] - Optional message
455
+ * @param {string} [opts.sourceHolon] - Source holon ID (defaults to client.publicKey)
456
+ * @returns {Promise<Object>} { success, shareId?, error? }
1133
457
  */
1134
- async revokeWriteAccess(holonId, lensName) {
1135
- return registry.revokeWriteAccess(
458
+ async share(target, lens, item = null, opts = {}) {
459
+ if (!target || typeof target !== 'string') {
460
+ throw new ValidationError('target must be a valid public key string');
461
+ }
462
+ if (!lens || typeof lens !== 'string') {
463
+ throw new ValidationError('lens must be a non-empty string');
464
+ }
465
+ if (!this.client?.privateKey) {
466
+ throw new ValidationError('Private key required to send share');
467
+ }
468
+
469
+ const sourceHolon = opts.sourceHolon || this.client.publicKey;
470
+ const author = this.client.publicKey;
471
+
472
+ // Bundle lens encryption keys if requested or if we have them
473
+ let keys = opts.keys || [];
474
+ if (keys.length === 0 && this.keyStore) {
475
+ const lensPath = buildLensPath(this.config.appName, sourceHolon, lens);
476
+ const lensKey = this.keyStore.getKey(lensPath);
477
+ if (lensKey && this.client.privateKey) {
478
+ try {
479
+ const wrappedKey = this.keyStore.wrapKeyForPartner(
480
+ lensPath,
481
+ this.client.privateKey,
482
+ target
483
+ );
484
+ if (wrappedKey) {
485
+ keys = [{ path: lensPath, key: wrappedKey }];
486
+ }
487
+ } catch (_err) {
488
+ // Key wrapping failed — share without keys
489
+ }
490
+ }
491
+ }
492
+
493
+ const sent = await federation.sendShare(
1136
494
  this.client,
1137
- this.config.appName,
1138
- holonId,
1139
- lensName
495
+ this.client.privateKey,
496
+ target,
497
+ {
498
+ source: sourceHolon,
499
+ lens,
500
+ item,
501
+ author,
502
+ keys,
503
+ msg: opts.msg,
504
+ }
1140
505
  );
506
+
507
+ if (sent) {
508
+ // Add target to federation registry proactively
509
+ await registry.addFederatedPartner(
510
+ this.client, this.config.appName, target,
511
+ { addedVia: 'share_sent' }
512
+ );
513
+ return { success: true };
514
+ }
515
+ return { success: false, error: 'Failed to send share DM' };
1141
516
  }
1142
517
 
1143
518
  /**
1144
- * Get write grants for a specific holon.
519
+ * Accept an incoming share.
520
+ *
521
+ * Stores the hologram locally, adds the sender to the federation registry,
522
+ * unwraps any bundled keys, and sends a share_ack DM.
1145
523
  *
1146
- * @param {string} holonId - The holon to get write grants for
1147
- * @returns {Promise<Array>} Array of write grants { lensName, grantedAt, expiresAt }
524
+ * @param {Object} sharePayload - The share DM payload
525
+ * @param {string} sharePayload.id - Share nonce
526
+ * @param {string} sharePayload.source - Source holon ID
527
+ * @param {string} sharePayload.lens - Lens name
528
+ * @param {string|null} sharePayload.item - Data ID (null for lens-level)
529
+ * @param {string} sharePayload.author - Author's public key
530
+ * @param {Array} [sharePayload.keys] - Bundled encryption keys
531
+ * @param {string} senderPubKey - The pubkey of who sent the share
532
+ * @param {Object} [opts={}] - Options
533
+ * @param {string} [opts.holonId] - Local holon to store hologram in (defaults to client.publicKey)
534
+ * @param {string} [opts.msg] - Optional message in ack
535
+ * @returns {Promise<Object>} { success, error? }
1148
536
  */
1149
- async getWriteGrantsForHolon(holonId) {
1150
- return registry.getWriteGrantsForHolon(
1151
- this.client,
1152
- this.config.appName,
1153
- holonId
1154
- );
537
+ async accept(sharePayload, senderPubKey, opts = {}) {
538
+ if (!sharePayload || !sharePayload.lens || !sharePayload.author) {
539
+ throw new ValidationError('Invalid share payload');
540
+ }
541
+ if (!this.client?.privateKey) {
542
+ throw new ValidationError('Private key required to accept share');
543
+ }
544
+
545
+ const localHolon = opts.holonId || this.client.publicKey;
546
+ const { source, lens, item, author, keys = [] } = sharePayload;
547
+
548
+ try {
549
+ // 1. Create and store the hologram locally
550
+ let hologram;
551
+ if (item) {
552
+ // Item-level hologram
553
+ hologram = federation.createHologram(
554
+ source, localHolon, lens, item, this.config.appName,
555
+ { authorPubKey: author }
556
+ );
557
+ } else {
558
+ // Lens-level hologram
559
+ hologram = federation.createLensHologram(
560
+ source, lens, this.config.appName,
561
+ { author }
562
+ );
563
+ }
564
+
565
+ const hologramPath = storage.buildPath(
566
+ this.config.appName, localHolon, lens, hologram.id
567
+ );
568
+ await storage.write(this.client, hologramPath, hologram);
569
+
570
+ // 2. Add sender to federation registry
571
+ await registry.addFederatedPartner(
572
+ this.client, this.config.appName, senderPubKey,
573
+ { addedVia: 'share_accepted' }
574
+ );
575
+
576
+ // Also add the author if different from sender
577
+ if (author !== senderPubKey) {
578
+ await registry.addFederatedPartner(
579
+ this.client, this.config.appName, author,
580
+ { addedVia: 'share_accepted_author' }
581
+ );
582
+ }
583
+
584
+ // Register the source holon -> author pubkey mapping
585
+ if (source) {
586
+ await registerHolon(this.client, this.config.appName, source, author, {
587
+ alias: null,
588
+ });
589
+ }
590
+
591
+ // 3. Unwrap bundled encryption keys
592
+ if (keys.length > 0 && this.keyStore && this.client.privateKey) {
593
+ for (const { path: kPath, key: wrappedKey } of keys) {
594
+ try {
595
+ this.keyStore.receiveKey(
596
+ kPath,
597
+ wrappedKey,
598
+ this.client.privateKey,
599
+ senderPubKey
600
+ );
601
+ } catch (_err) {
602
+ // Key unwrap failed for this entry — continue
603
+ }
604
+ }
605
+ }
606
+
607
+ // 4. Send ack DM
608
+ await federation.sendAck(
609
+ this.client,
610
+ this.client.privateKey,
611
+ senderPubKey,
612
+ {
613
+ id: sharePayload.id,
614
+ source,
615
+ lens,
616
+ item,
617
+ author,
618
+ status: 'accepted',
619
+ msg: opts.msg,
620
+ }
621
+ );
622
+
623
+ return { success: true };
624
+ } catch (error) {
625
+ return { success: false, error: error.message };
626
+ }
1155
627
  }
1156
628
 
1157
629
  /**
1158
- * Get all write grants issued to federated holons.
630
+ * Reject an incoming share.
631
+ *
632
+ * Sends a share_ack DM with status 'rejected'. No local state is changed.
1159
633
  *
1160
- * @returns {Promise<Array>} Array of { holonId, alias, writeGrants }
634
+ * @param {Object} sharePayload - The share DM payload
635
+ * @param {string} senderPubKey - The pubkey of who sent the share
636
+ * @param {Object} [opts={}] - Options
637
+ * @param {string} [opts.msg] - Optional rejection message
638
+ * @returns {Promise<Object>} { success, error? }
1161
639
  */
1162
- async getAllWriteGrants() {
1163
- return registry.getAllWriteGrants(
1164
- this.client,
1165
- this.config.appName
1166
- );
640
+ async reject(sharePayload, senderPubKey, opts = {}) {
641
+ if (!sharePayload || !sharePayload.id) {
642
+ throw new ValidationError('Invalid share payload');
643
+ }
644
+ if (!this.client?.privateKey) {
645
+ throw new ValidationError('Private key required to reject share');
646
+ }
647
+
648
+ try {
649
+ await federation.sendAck(
650
+ this.client,
651
+ this.client.privateKey,
652
+ senderPubKey,
653
+ {
654
+ id: sharePayload.id,
655
+ source: sharePayload.source,
656
+ lens: sharePayload.lens,
657
+ item: sharePayload.item,
658
+ author: sharePayload.author,
659
+ status: 'rejected',
660
+ msg: opts.msg,
661
+ }
662
+ );
663
+
664
+ return { success: true };
665
+ } catch (error) {
666
+ return { success: false, error: error.message };
667
+ }
1167
668
  }
669
+
1168
670
  };
1169
671
  }
1170
672