holosphere 2.0.0-alpha21 → 2.0.0-alpha23

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