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,41 +1,30 @@
1
1
  /**
2
- * Federation Registry - UNIFIED MODEL
2
+ * Federation Registry Simplified.
3
3
  *
4
- * Manages federation partnerships and capability tokens for both:
5
- * - Self-federation (same author, different holons)
6
- * - Cross-federation (different authors)
4
+ * Federation = a list of pubkeys whose data I want to see.
5
+ * That's it. No capability tokens, no scoped permissions, no expiration.
7
6
  *
8
- * In the unified model, all federation uses capabilities, including self-federation.
9
- * This provides consistent security semantics across all federation types.
7
+ * Everyone can write everywhere (Nostr is open-write).
8
+ * Non-federated writes are simply invisible on read (filtered out).
9
+ *
10
+ * The registry stores:
11
+ * - federatedWith: [{ pubKey, alias, addedAt, addedVia }]
12
+ * - capabilityWriters: [{ pubKey, addedAt }] (external writers, e.g. QR code)
10
13
  */
11
14
 
12
15
  import { writeGlobal, readGlobal } from '../storage/global-tables.js';
13
- import { matchScope, issueCapability as cryptoIssueCapability } from '../crypto/secp256k1.js';
14
16
 
15
17
  const FEDERATION_TABLE = 'federations';
16
18
 
17
19
  // ============================================================================
18
- // Registry Cache - Avoids repeated storage reads
20
+ // Registry Cache
19
21
  // ============================================================================
20
22
 
21
- /**
22
- * @typedef {Object} CachedRegistry
23
- * @property {Object} registry - The federation registry data
24
- * @property {Map<string, Object>} partnerIndex - Index of partners by pubKey for O(1) lookup
25
- * @property {number} timestamp - When the cache was created
26
- */
27
-
28
- /** @type {Map<string, CachedRegistry>} Cache keyed by `${clientPubKey}:${appname}` */
23
+ /** @type {Map<string, { registry: Object, partnerIndex: Map, timestamp: number }>} */
29
24
  const registryCache = new Map();
30
25
 
31
- /** Cache TTL in milliseconds (10 seconds) */
32
26
  const CACHE_TTL_MS = 10000;
33
27
 
34
- /**
35
- * Build an index of partners by pubKey for O(1) lookups
36
- * @param {Object[]} federatedWith - Array of partner objects
37
- * @returns {Map<string, Object>} Partner index
38
- */
39
28
  function buildPartnerIndex(federatedWith) {
40
29
  const index = new Map();
41
30
  for (const partner of federatedWith) {
@@ -46,40 +35,30 @@ function buildPartnerIndex(federatedWith) {
46
35
  return index;
47
36
  }
48
37
 
49
- /**
50
- * Get cache key for a client/appname combination
51
- * @param {Object} client - NostrClient instance
52
- * @param {string} appname - Application namespace
53
- * @returns {string} Cache key
54
- */
55
38
  function getCacheKey(client, appname) {
56
39
  return `${client.publicKey}:${appname}`;
57
40
  }
58
41
 
59
- /**
60
- * Invalidate the registry cache for a specific client/appname
61
- * Call this after any write operation
62
- * @param {Object} client - NostrClient instance
63
- * @param {string} appname - Application namespace
64
- */
65
42
  export function invalidateRegistryCache(client, appname) {
66
- const key = getCacheKey(client, appname);
67
- registryCache.delete(key);
43
+ registryCache.delete(getCacheKey(client, appname));
68
44
  }
69
45
 
46
+ // ============================================================================
47
+ // Core Registry CRUD
48
+ // ============================================================================
49
+
70
50
  /**
71
- * Get the federation registry for this holosphere (with caching)
51
+ * Get the federation registry (with caching).
72
52
  * @param {Object} client - NostrClient instance
73
53
  * @param {string} appname - Application namespace
74
54
  * @param {Object} [options] - Options
75
- * @param {boolean} [options.skipCache=false] - Force fresh read from storage
55
+ * @param {boolean} [options.skipCache=false] - Force fresh read
76
56
  * @returns {Promise<Object>} Federation registry
77
57
  */
78
58
  export async function getFederationRegistry(client, appname, options = {}) {
79
59
  const cacheKey = getCacheKey(client, appname);
80
60
  const now = Date.now();
81
61
 
82
- // Check cache first (unless skipCache is set)
83
62
  if (!options.skipCache) {
84
63
  const cached = registryCache.get(cacheKey);
85
64
  if (cached && (now - cached.timestamp) < CACHE_TTL_MS) {
@@ -87,139 +66,57 @@ export async function getFederationRegistry(client, appname, options = {}) {
87
66
  }
88
67
  }
89
68
 
90
- // Read from storage
91
69
  const registry = await readGlobal(client, appname, FEDERATION_TABLE, client.publicKey) || {
92
70
  id: client.publicKey,
93
71
  federatedWith: [],
94
72
  capabilityWriters: [],
95
- discoveryEnabled: false,
96
- autoAccept: false,
97
- defaultScope: { holonId: '*', lensName: '*' },
98
- defaultPermissions: ['read'],
99
73
  };
100
74
 
101
- // Ensure capabilityWriters array exists (for registries created before this field)
102
- if (!registry.capabilityWriters) {
103
- registry.capabilityWriters = [];
104
- }
75
+ // Ensure arrays exist
76
+ if (!registry.federatedWith) registry.federatedWith = [];
77
+ if (!registry.capabilityWriters) registry.capabilityWriters = [];
105
78
 
106
- // Build partner index and cache
107
- const partnerIndex = buildPartnerIndex(registry.federatedWith || []);
108
- registryCache.set(cacheKey, {
109
- registry,
110
- partnerIndex,
111
- timestamp: now,
112
- });
79
+ const partnerIndex = buildPartnerIndex(registry.federatedWith);
80
+ registryCache.set(cacheKey, { registry, partnerIndex, timestamp: now });
113
81
 
114
82
  return registry;
115
83
  }
116
84
 
117
85
  /**
118
- * Get partner from cache index (O(1) lookup)
119
- * @param {Object} client - NostrClient instance
120
- * @param {string} appname - Application namespace
121
- * @param {string} partnerPubKey - Partner's public key
122
- * @returns {Promise<Object|null>} Partner object or null
123
- */
124
- async function getPartnerFromIndex(client, appname, partnerPubKey) {
125
- const cacheKey = getCacheKey(client, appname);
126
-
127
- // Ensure registry is loaded and cached
128
- await getFederationRegistry(client, appname);
129
-
130
- const cached = registryCache.get(cacheKey);
131
- if (cached && cached.partnerIndex) {
132
- return cached.partnerIndex.get(partnerPubKey) || null;
133
- }
134
-
135
- return null;
136
- }
137
-
138
- /**
139
- * Find a partner in the registry using the cached O(1) index.
140
- * Must be called AFTER getFederationRegistry() has been called (which populates the cache).
141
- * Falls back to array.find() if cache is not available.
142
- * @private
143
- * @param {Object} client - NostrClient instance
144
- * @param {string} appname - Application namespace
145
- * @param {Object} registryObj - The already-loaded registry object
146
- * @param {string} partnerPubKey - Partner's public key
147
- * @returns {Object|null} Partner object or null
148
- */
149
- function findPartner(client, appname, registryObj, partnerPubKey) {
150
- const cacheKey = getCacheKey(client, appname);
151
- const cached = registryCache.get(cacheKey);
152
- if (cached && cached.partnerIndex) {
153
- return cached.partnerIndex.get(partnerPubKey) || null;
154
- }
155
- // Fallback to linear scan if index not available
156
- return registryObj.federatedWith.find(p => p.pubKey === partnerPubKey) || null;
157
- }
158
-
159
- /**
160
- * Save the federation registry
161
- * @param {Object} client - NostrClient instance
162
- * @param {string} appname - Application namespace
163
- * @param {Object} registry - Registry to save
164
- * @returns {Promise<boolean>} Success indicator
86
+ * Save the federation registry.
165
87
  */
166
88
  export async function saveFederationRegistry(client, appname, registry) {
167
- // Invalidate cache before write so next read gets fresh data
168
89
  invalidateRegistryCache(client, appname);
169
90
  return writeGlobal(client, appname, FEDERATION_TABLE, registry);
170
91
  }
171
92
 
172
93
  /**
173
- * Add a federated partner
94
+ * Add a federated partner (just a pubkey).
174
95
  * @param {Object} client - NostrClient instance
175
96
  * @param {string} appname - Application namespace
176
97
  * @param {string} partnerPubKey - Partner's public key
177
98
  * @param {Object} options - Partner options
178
- * @param {string} options.alias - Human-readable name
179
- * @param {string} options.addedVia - How the partner was added ('manual' or 'nostr_discovery')
180
- * @param {Object[]} options.inboundCapabilities - Capabilities they've granted us
99
+ * @param {string} [options.alias] - Human-readable name
100
+ * @param {string} [options.addedVia] - How the partner was added
181
101
  * @returns {Promise<boolean>} Success indicator
182
102
  */
183
103
  export async function addFederatedPartner(client, appname, partnerPubKey, options = {}) {
184
104
  const registry = await getFederationRegistry(client, appname);
185
105
 
186
- // Check if already exists
187
106
  const existingIndex = registry.federatedWith.findIndex(p => p.pubKey === partnerPubKey);
188
107
 
189
108
  if (existingIndex >= 0) {
190
- // Update existing entry
191
- const existing = registry.federatedWith[existingIndex];
192
-
109
+ // Update alias if provided
193
110
  if (options.alias) {
194
- existing.alias = options.alias;
111
+ registry.federatedWith[existingIndex].alias = options.alias;
195
112
  }
196
-
197
- if (options.inboundCapabilities) {
198
- // Merge and deduplicate capabilities by scope to prevent accumulation
199
- const allCaps = [
200
- ...(existing.inboundCapabilities || []),
201
- ...options.inboundCapabilities
202
- ];
203
- // Keep only unique capabilities (by scope), preferring newer ones (last in array)
204
- const seen = new Map();
205
- for (const cap of allCaps) {
206
- const scopeKey = `${cap.scope?.holonId}:${cap.scope?.lensName}:${cap.scope?.dataId}`;
207
- seen.set(scopeKey, cap); // Later entries overwrite earlier (newer wins)
208
- }
209
- existing.inboundCapabilities = Array.from(seen.values());
210
- }
211
-
212
- existing.updatedAt = Date.now();
213
- registry.federatedWith[existingIndex] = existing;
113
+ registry.federatedWith[existingIndex].updatedAt = Date.now();
214
114
  } else {
215
- // Add new partner
216
115
  registry.federatedWith.push({
217
116
  pubKey: partnerPubKey,
218
117
  alias: options.alias || null,
219
118
  addedAt: Date.now(),
220
119
  addedVia: options.addedVia || 'manual',
221
- inboundCapabilities: options.inboundCapabilities || [],
222
- outboundCapabilities: [],
223
120
  });
224
121
  }
225
122
 
@@ -227,58 +124,62 @@ export async function addFederatedPartner(client, appname, partnerPubKey, option
227
124
  }
228
125
 
229
126
  /**
230
- * Remove a federated partner
231
- * @param {Object} client - NostrClient instance
232
- * @param {string} appname - Application namespace
233
- * @param {string} partnerPubKey - Partner's public key to remove
234
- * @returns {Promise<boolean>} Success indicator
127
+ * Remove a federated partner.
235
128
  */
236
129
  export async function removeFederatedPartner(client, appname, partnerPubKey) {
237
130
  const registry = await getFederationRegistry(client, appname);
238
-
239
131
  const initialLength = registry.federatedWith.length;
240
132
  registry.federatedWith = registry.federatedWith.filter(p => p.pubKey !== partnerPubKey);
241
133
 
242
134
  if (registry.federatedWith.length === initialLength) {
243
- return false; // Partner not found
135
+ return false;
244
136
  }
245
137
 
246
138
  return saveFederationRegistry(client, appname, registry);
247
139
  }
248
140
 
249
141
  /**
250
- * Get all federated author public keys
251
- * @param {Object} client - NostrClient instance
252
- * @param {string} appname - Application namespace
253
- * @returns {Promise<string[]>} Array of federated public keys
142
+ * Get all federated author public keys.
254
143
  */
255
144
  export async function getFederatedAuthors(client, appname) {
256
145
  const registry = await getFederationRegistry(client, appname);
257
146
  return registry.federatedWith.map(p => p.pubKey);
258
147
  }
259
148
 
149
+ /**
150
+ * Check if a pubkey is in the federation.
151
+ * This is THE core federation check — O(1) via cached index.
152
+ *
153
+ * @param {Object} client - NostrClient instance
154
+ * @param {string} appname - Application namespace
155
+ * @param {string} pubKey - Public key to check
156
+ * @returns {Promise<boolean>} True if pubkey is federated
157
+ */
158
+ export async function isFederated(client, appname, pubKey) {
159
+ // Self is always "federated"
160
+ if (pubKey === client.publicKey) return true;
161
+
162
+ await getFederationRegistry(client, appname);
163
+ const cacheKey = getCacheKey(client, appname);
164
+ const cached = registryCache.get(cacheKey);
165
+ if (cached && cached.partnerIndex) {
166
+ return cached.partnerIndex.has(pubKey);
167
+ }
168
+ return false;
169
+ }
170
+
260
171
  // ============================================================================
261
172
  // Capability Writers (External writers registered via app-level authorization)
262
173
  // ============================================================================
263
174
 
264
175
  /**
265
- * Add a capability writer — an external pubkey authorized to write to this holon
266
- * via app-level capability tokens (e.g., QR codes). Their pubkey is recorded so
267
- * their data is included in future read queries.
268
- *
269
- * @param {Object} client - NostrClient instance
270
- * @param {string} appname - Application namespace
271
- * @param {string} writerPubKey - Writer's public key to register
272
- * @returns {Promise<boolean>} Success indicator
176
+ * Add a capability writer — an external pubkey authorized to write to this holon.
273
177
  */
274
178
  export async function addCapabilityWriter(client, appname, writerPubKey) {
275
179
  const registry = await getFederationRegistry(client, appname);
276
180
 
277
- // Deduplicate
278
181
  const exists = registry.capabilityWriters.some(w => w.pubKey === writerPubKey);
279
- if (exists) {
280
- return true; // Already registered
281
- }
182
+ if (exists) return true;
282
183
 
283
184
  registry.capabilityWriters.push({
284
185
  pubKey: writerPubKey,
@@ -290,10 +191,6 @@ export async function addCapabilityWriter(client, appname, writerPubKey) {
290
191
 
291
192
  /**
292
193
  * Get all capability writer public keys.
293
- *
294
- * @param {Object} client - NostrClient instance
295
- * @param {string} appname - Application namespace
296
- * @returns {Promise<string[]>} Array of capability writer pubkeys
297
194
  */
298
195
  export async function getCapabilityWriters(client, appname) {
299
196
  const registry = await getFederationRegistry(client, appname);
@@ -302,1119 +199,32 @@ export async function getCapabilityWriters(client, appname) {
302
199
 
303
200
  /**
304
201
  * Remove a capability writer.
305
- *
306
- * @param {Object} client - NostrClient instance
307
- * @param {string} appname - Application namespace
308
- * @param {string} writerPubKey - Writer's public key to remove
309
- * @returns {Promise<boolean>} Success indicator
310
202
  */
311
203
  export async function removeCapabilityWriter(client, appname, writerPubKey) {
312
204
  const registry = await getFederationRegistry(client, appname);
313
-
314
205
  const initialLength = registry.capabilityWriters.length;
315
206
  registry.capabilityWriters = registry.capabilityWriters.filter(w => w.pubKey !== writerPubKey);
316
207
 
317
208
  if (registry.capabilityWriters.length === initialLength) {
318
- return false; // Writer not found
319
- }
320
-
321
- return saveFederationRegistry(client, appname, registry);
322
- }
323
-
324
- /**
325
- * Get a specific federated partner (O(1) lookup via index)
326
- * @param {Object} client - NostrClient instance
327
- * @param {string} appname - Application namespace
328
- * @param {string} partnerPubKey - Partner's public key
329
- * @returns {Promise<Object|null>} Partner info or null
330
- */
331
- export async function getFederatedPartner(client, appname, partnerPubKey) {
332
- return getPartnerFromIndex(client, appname, partnerPubKey);
333
- }
334
-
335
- /**
336
- * Get capability for a specific author and scope
337
- * Finds a valid, non-expired capability that covers the requested scope
338
- * Uses O(1) partner lookup via index
339
- * @param {Object} client - NostrClient instance
340
- * @param {string} appname - Application namespace
341
- * @param {string} authorPubKey - Author's public key
342
- * @param {Object} scope - Requested scope { holonId, lensName, dataId? }
343
- * @returns {Promise<Object|null>} Capability entry or null
344
- */
345
- export async function getCapabilityForAuthor(client, appname, authorPubKey, scope) {
346
- // Use O(1) index lookup instead of array.find()
347
- const partner = await getPartnerFromIndex(client, appname, authorPubKey);
348
-
349
- if (!partner) {
350
- return null;
351
- }
352
-
353
- if (!partner.inboundCapabilities || partner.inboundCapabilities.length === 0) {
354
- return null;
355
- }
356
-
357
- const now = Date.now();
358
-
359
- // Find matching, non-expired capability
360
- // Note: We still iterate through capabilities here, but there are typically
361
- // only a few per partner (often just one wildcard capability)
362
- for (const cap of partner.inboundCapabilities) {
363
- // Check expiration
364
- if (cap.expires && cap.expires < now) {
365
- continue;
366
- }
367
-
368
- // Check scope match (supports wildcards)
369
- if (matchScope(cap.scope, scope)) {
370
- return cap;
371
- }
372
- }
373
-
374
- return null;
375
- }
376
-
377
- /**
378
- * Store an inbound capability (one we received from a partner)
379
- * @param {Object} client - NostrClient instance
380
- * @param {string} appname - Application namespace
381
- * @param {string} partnerPubKey - Partner's public key
382
- * @param {Object} capabilityInfo - Capability information
383
- * @param {string} capabilityInfo.token - The capability token
384
- * @param {Object} capabilityInfo.scope - Token scope
385
- * @param {string[]} capabilityInfo.permissions - Granted permissions
386
- * @param {number} capabilityInfo.expires - Expiration timestamp
387
- * @returns {Promise<boolean>} Success indicator
388
- */
389
- export async function storeInboundCapability(client, appname, partnerPubKey, capabilityInfo) {
390
- const registry = await getFederationRegistry(client, appname);
391
- const partner = findPartner(client, appname, registry, partnerPubKey);
392
-
393
- if (!partner) {
394
- // Auto-add partner if not exists
395
- return addFederatedPartner(client, appname, partnerPubKey, {
396
- addedVia: 'capability_received',
397
- inboundCapabilities: [capabilityInfo]
398
- });
399
- }
400
-
401
- if (!partner.inboundCapabilities) {
402
- partner.inboundCapabilities = [];
403
- }
404
-
405
- // Check if we already have this capability (by scope match)
406
- const existingIndex = partner.inboundCapabilities.findIndex(cap =>
407
- JSON.stringify(cap.scope) === JSON.stringify(capabilityInfo.scope)
408
- );
409
-
410
- if (existingIndex >= 0) {
411
- // Update existing capability
412
- partner.inboundCapabilities[existingIndex] = {
413
- ...capabilityInfo,
414
- updatedAt: Date.now()
415
- };
416
- } else {
417
- // Add new capability
418
- partner.inboundCapabilities.push({
419
- ...capabilityInfo,
420
- receivedAt: Date.now()
421
- });
422
- }
423
-
424
- return saveFederationRegistry(client, appname, registry);
425
- }
426
-
427
- /**
428
- * Store an outbound capability (one we issued to a partner)
429
- * We only store the hash, not the full token
430
- * @param {Object} client - NostrClient instance
431
- * @param {string} appname - Application namespace
432
- * @param {string} partnerPubKey - Partner's public key
433
- * @param {Object} capabilityInfo - Capability information
434
- * @param {string} capabilityInfo.tokenHash - SHA-256 hash of the token
435
- * @param {Object} capabilityInfo.scope - Token scope
436
- * @param {string[]} capabilityInfo.permissions - Granted permissions
437
- * @param {number} capabilityInfo.expires - Expiration timestamp
438
- * @returns {Promise<boolean>} Success indicator
439
- */
440
- export async function storeOutboundCapability(client, appname, partnerPubKey, capabilityInfo) {
441
- const registry = await getFederationRegistry(client, appname);
442
- const partner = findPartner(client, appname, registry, partnerPubKey);
443
-
444
- if (!partner) {
445
- throw new Error(`Partner ${partnerPubKey} not found in federation registry`);
446
- }
447
-
448
- if (!partner.outboundCapabilities) {
449
- partner.outboundCapabilities = [];
450
- }
451
-
452
- partner.outboundCapabilities.push({
453
- tokenHash: capabilityInfo.tokenHash,
454
- scope: capabilityInfo.scope,
455
- permissions: capabilityInfo.permissions,
456
- issuedAt: Date.now(),
457
- expires: capabilityInfo.expires,
458
- });
459
-
460
- return saveFederationRegistry(client, appname, registry);
461
- }
462
-
463
- /**
464
- * Revoke an outbound capability (by token hash)
465
- * @param {Object} client - NostrClient instance
466
- * @param {string} appname - Application namespace
467
- * @param {string} partnerPubKey - Partner's public key
468
- * @param {string} tokenHash - Hash of the token to revoke
469
- * @returns {Promise<boolean>} Success indicator
470
- */
471
- export async function revokeOutboundCapability(client, appname, partnerPubKey, tokenHash) {
472
- const registry = await getFederationRegistry(client, appname);
473
- const partner = findPartner(client, appname, registry, partnerPubKey);
474
-
475
- if (!partner || !partner.outboundCapabilities) {
476
209
  return false;
477
210
  }
478
211
 
479
- const initialLength = partner.outboundCapabilities.length;
480
- partner.outboundCapabilities = partner.outboundCapabilities.filter(
481
- cap => cap.tokenHash !== tokenHash
482
- );
483
-
484
- if (partner.outboundCapabilities.length === initialLength) {
485
- return false; // Token not found
486
- }
487
-
488
- return saveFederationRegistry(client, appname, registry);
489
- }
490
-
491
- /**
492
- * Update federation discovery settings
493
- * @param {Object} client - NostrClient instance
494
- * @param {string} appname - Application namespace
495
- * @param {Object} settings - Discovery settings
496
- * @param {boolean} settings.discoveryEnabled - Enable Nostr discovery
497
- * @param {boolean} settings.autoAccept - Auto-accept federation requests
498
- * @param {Object} settings.defaultScope - Default scope for auto-accepted federations
499
- * @param {string[]} settings.defaultPermissions - Default permissions for auto-accepted federations
500
- * @returns {Promise<boolean>} Success indicator
501
- */
502
- export async function updateDiscoverySettings(client, appname, settings) {
503
- const registry = await getFederationRegistry(client, appname);
504
-
505
- if (settings.discoveryEnabled !== undefined) {
506
- registry.discoveryEnabled = settings.discoveryEnabled;
507
- }
508
- if (settings.autoAccept !== undefined) {
509
- registry.autoAccept = settings.autoAccept;
510
- }
511
- if (settings.defaultScope !== undefined) {
512
- registry.defaultScope = settings.defaultScope;
513
- }
514
- if (settings.defaultPermissions !== undefined) {
515
- registry.defaultPermissions = settings.defaultPermissions;
516
- }
517
-
518
212
  return saveFederationRegistry(client, appname, registry);
519
213
  }
520
214
 
521
- /**
522
- * Get all federated authors that have granted capabilities for a given scope
523
- * Returns pubKeys of partners who have given us access to read/write their data
524
- * @param {Object} client - NostrClient instance
525
- * @param {string} appname - Application namespace
526
- * @param {Object} scope - Requested scope { holonId?, lensName?, dataId? }
527
- * @param {string} permission - Required permission ('read' or 'write')
528
- * @returns {Promise<Array>} Array of { pubKey, capability } for matching partners
529
- */
530
- export async function getFederatedAuthorsForScope(client, appname, scope = {}, permission = 'read') {
531
- const registry = await getFederationRegistry(client, appname);
532
- const now = Date.now();
533
- const results = [];
534
-
535
- for (const partner of registry.federatedWith) {
536
- if (!partner.inboundCapabilities || partner.inboundCapabilities.length === 0) {
537
- continue;
538
- }
539
-
540
- // Find a valid capability that covers this scope
541
- for (const cap of partner.inboundCapabilities) {
542
- // Check expiration
543
- if (cap.expires && cap.expires < now) {
544
- continue;
545
- }
546
-
547
- // Check permission
548
- if (cap.permissions && !cap.permissions.includes(permission)) {
549
- continue;
550
- }
551
-
552
- // Check scope match (supports wildcards)
553
- if (matchScope(cap.scope, scope)) {
554
- results.push({
555
- pubKey: partner.pubKey,
556
- capability: cap,
557
- });
558
- break; // One valid capability per partner is enough
559
- }
560
- }
561
- }
562
-
563
- return results;
564
- }
565
-
566
- /**
567
- * Clean up expired capabilities from the registry
568
- * @param {Object} client - NostrClient instance
569
- * @param {string} appname - Application namespace
570
- * @returns {Promise<Object>} Cleanup result { inboundRemoved, outboundRemoved }
571
- */
572
- export async function cleanupExpiredCapabilities(client, appname) {
573
- const registry = await getFederationRegistry(client, appname);
574
- const now = Date.now();
575
- let inboundRemoved = 0;
576
- let outboundRemoved = 0;
577
-
578
- for (const partner of registry.federatedWith) {
579
- if (partner.inboundCapabilities) {
580
- const beforeCount = partner.inboundCapabilities.length;
581
- partner.inboundCapabilities = partner.inboundCapabilities.filter(
582
- cap => !cap.expires || cap.expires > now
583
- );
584
- inboundRemoved += beforeCount - partner.inboundCapabilities.length;
585
- }
586
-
587
- if (partner.outboundCapabilities) {
588
- const beforeCount = partner.outboundCapabilities.length;
589
- partner.outboundCapabilities = partner.outboundCapabilities.filter(
590
- cap => !cap.expires || cap.expires > now
591
- );
592
- outboundRemoved += beforeCount - partner.outboundCapabilities.length;
593
- }
594
- }
595
-
596
- if (inboundRemoved > 0 || outboundRemoved > 0) {
597
- await saveFederationRegistry(client, appname, registry);
598
- }
599
-
600
- return { inboundRemoved, outboundRemoved };
601
- }
602
-
603
- /**
604
- * Store a self-capability (for same-author federation) - UNIFIED MODEL
605
- *
606
- * In the unified model, self-capabilities are stored similarly to cross-capabilities
607
- * but marked as self-issued. This provides consistent capability management.
608
- *
609
- * @param {Object} client - NostrClient instance
610
- * @param {string} appname - Application namespace
611
- * @param {Object} capabilityInfo - Capability information
612
- * @param {string} capabilityInfo.token - The capability token
613
- * @param {Object} capabilityInfo.scope - Token scope
614
- * @param {string[]} capabilityInfo.permissions - Granted permissions
615
- * @returns {Promise<boolean>} Success indicator
616
- */
617
- export async function storeSelfCapability(client, appname, capabilityInfo) {
618
- const registry = await getFederationRegistry(client, appname);
619
-
620
- // Self-capabilities are stored under the user's own pubKey
621
- const selfPubKey = client.publicKey;
622
-
623
- // Find or create self-entry
624
- let selfEntry = registry.federatedWith.find(p => p.pubKey === selfPubKey);
625
-
626
- if (!selfEntry) {
627
- selfEntry = {
628
- pubKey: selfPubKey,
629
- alias: 'self',
630
- addedAt: Date.now(),
631
- addedVia: 'self_capability',
632
- isSelf: true,
633
- inboundCapabilities: [],
634
- outboundCapabilities: [],
635
- selfCapabilities: [], // Special array for self-capabilities
636
- };
637
- registry.federatedWith.push(selfEntry);
638
- }
639
-
640
- if (!selfEntry.selfCapabilities) {
641
- selfEntry.selfCapabilities = [];
642
- }
643
-
644
- // Check if we already have this capability (by scope match)
645
- const existingIndex = selfEntry.selfCapabilities.findIndex(cap =>
646
- JSON.stringify(cap.scope) === JSON.stringify(capabilityInfo.scope)
647
- );
648
-
649
- if (existingIndex >= 0) {
650
- // Update existing capability
651
- selfEntry.selfCapabilities[existingIndex] = {
652
- ...capabilityInfo,
653
- updatedAt: Date.now(),
654
- isSelfCapability: true,
655
- };
656
- } else {
657
- // Add new capability
658
- selfEntry.selfCapabilities.push({
659
- ...capabilityInfo,
660
- issuedAt: Date.now(),
661
- isSelfCapability: true,
662
- });
663
- }
664
-
665
- return saveFederationRegistry(client, appname, registry);
666
- }
215
+ // ============================================================================
216
+ // Partner Lookup
217
+ // ============================================================================
667
218
 
668
219
  /**
669
- * Get self-capability for a scope - UNIFIED MODEL
670
- *
671
- * @param {Object} client - NostrClient instance
672
- * @param {string} appname - Application namespace
673
- * @param {Object} scope - Requested scope { holonId, lensName, dataId? }
674
- * @returns {Promise<Object|null>} Self-capability or null
220
+ * Get a specific federated partner (O(1) lookup via index).
675
221
  */
676
- export async function getSelfCapability(client, appname, scope) {
677
- const registry = await getFederationRegistry(client, appname);
678
- const selfEntry = registry.federatedWith.find(p => p.pubKey === client.publicKey && p.isSelf);
679
-
680
- if (!selfEntry || !selfEntry.selfCapabilities) {
681
- return null;
222
+ export async function getFederatedPartner(client, appname, partnerPubKey) {
223
+ await getFederationRegistry(client, appname);
224
+ const cacheKey = getCacheKey(client, appname);
225
+ const cached = registryCache.get(cacheKey);
226
+ if (cached && cached.partnerIndex) {
227
+ return cached.partnerIndex.get(partnerPubKey) || null;
682
228
  }
683
-
684
- // Find matching, non-expired self-capability
685
- return selfEntry.selfCapabilities.find(cap => {
686
- // Check expiration
687
- if (cap.expires && cap.expires < Date.now()) {
688
- return false;
689
- }
690
-
691
- // Check scope match (supports wildcards)
692
- return matchScope(cap.scope, scope);
693
- }) || null;
694
- }
695
-
696
- /**
697
- * Check if a capability is a self-capability
698
- * @param {Object} capabilityInfo - Capability info object
699
- * @returns {boolean} True if self-capability
700
- */
701
- export function isSelfCapability(capabilityInfo) {
702
- return capabilityInfo && capabilityInfo.isSelfCapability === true;
703
- }
704
-
705
- // ============================================================================
706
- // Write Grant Functions (Cross-Holon Write Access)
707
- // ============================================================================
708
-
709
- /**
710
- * Grant write access to a holon for a specific lens.
711
- * This allows all members of the granted holon to write to the specified lens.
712
- *
713
- * @param {Object} client - NostrClient instance
714
- * @param {string} appname - Application namespace
715
- * @param {string} holonId - The holon (pubKey) being granted write access
716
- * @param {string} lensName - The lens to grant write access to (or '*' for all lenses)
717
- * @param {Object} [options={}] - Grant options
718
- * @param {number} [options.expiresAt] - Optional expiration timestamp
719
- * @returns {Promise<boolean>} Success indicator
720
- */
721
- export async function grantWriteAccessToHolon(client, appname, holonId, lensName, options = {}) {
722
- console.warn('[Deprecated] grantWriteAccessToHolon() is deprecated. Use grantAccess() instead.');
723
- const registry = await getFederationRegistry(client, appname);
724
-
725
- // Find or create partner entry
726
- let partner = findPartner(client, appname, registry, holonId);
727
-
728
- if (!partner) {
729
- // Auto-add partner if not exists
730
- partner = {
731
- pubKey: holonId,
732
- alias: null,
733
- addedAt: Date.now(),
734
- addedVia: 'write_grant',
735
- inboundCapabilities: [],
736
- outboundCapabilities: [],
737
- writeGrants: [],
738
- };
739
- registry.federatedWith.push(partner);
740
- }
741
-
742
- if (!partner.writeGrants) {
743
- partner.writeGrants = [];
744
- }
745
-
746
- // Check if grant already exists
747
- const existingIndex = partner.writeGrants.findIndex(g => g.lensName === lensName);
748
-
749
- const grant = {
750
- lensName,
751
- grantedAt: Date.now(),
752
- expiresAt: options.expiresAt || null,
753
- };
754
-
755
- if (existingIndex >= 0) {
756
- // Update existing grant
757
- partner.writeGrants[existingIndex] = grant;
758
- } else {
759
- // Add new grant
760
- partner.writeGrants.push(grant);
761
- }
762
-
763
- return saveFederationRegistry(client, appname, registry);
764
- }
765
-
766
- /**
767
- * Revoke write access from a holon for a specific lens.
768
- *
769
- * @param {Object} client - NostrClient instance
770
- * @param {string} appname - Application namespace
771
- * @param {string} holonId - The holon to revoke write access from
772
- * @param {string} lensName - The lens to revoke write access for
773
- * @returns {Promise<boolean>} Success indicator
774
- */
775
- export async function revokeWriteAccess(client, appname, holonId, lensName) {
776
- console.warn('[Deprecated] revokeWriteAccess() is deprecated. Use revokeAccess() instead.');
777
- const registry = await getFederationRegistry(client, appname);
778
-
779
- const partner = findPartner(client, appname, registry, holonId);
780
-
781
- if (!partner || !partner.writeGrants) {
782
- return false; // No grants to revoke
783
- }
784
-
785
- const initialLength = partner.writeGrants.length;
786
- partner.writeGrants = partner.writeGrants.filter(g => g.lensName !== lensName);
787
-
788
- if (partner.writeGrants.length === initialLength) {
789
- return false; // Grant not found
790
- }
791
-
792
- return saveFederationRegistry(client, appname, registry);
793
- }
794
-
795
- /**
796
- * Get all write grants for a specific holon.
797
- *
798
- * @param {Object} client - NostrClient instance
799
- * @param {string} appname - Application namespace
800
- * @param {string} holonId - The holon to get write grants for
801
- * @returns {Promise<Array>} Array of write grants { lensName, grantedAt, expiresAt }
802
- */
803
- export async function getWriteGrantsForHolon(client, appname, holonId) {
804
- console.warn('[Deprecated] getWriteGrantsForHolon() is deprecated. Use getAccessGrants() instead.');
805
- const registry = await getFederationRegistry(client, appname);
806
-
807
- const partner = findPartner(client, appname, registry, holonId);
808
-
809
- if (!partner || !partner.writeGrants) {
810
- return [];
811
- }
812
-
813
- // Filter out expired grants
814
- const now = Date.now();
815
- return partner.writeGrants.filter(g => !g.expiresAt || g.expiresAt > now);
816
- }
817
-
818
- /**
819
- * Check if a holon has write access to a specific lens.
820
- *
821
- * @param {Object} client - NostrClient instance
822
- * @param {string} appname - Application namespace
823
- * @param {string} holonId - The holon to check
824
- * @param {string} lensName - The lens to check access for
825
- * @returns {Promise<boolean>} True if holon has write access
826
- */
827
- export async function hasWriteAccess(client, appname, holonId, lensName) {
828
- console.warn('[Deprecated] hasWriteAccess() is deprecated. Use findAccessGrant() instead.');
829
- const grants = await getWriteGrantsForHolon(client, appname, holonId);
830
-
831
- // Check for exact lens match or wildcard
832
- return grants.some(g => g.lensName === lensName || g.lensName === '*');
833
- }
834
-
835
- /**
836
- * Get all holons that have been granted write access to any lens.
837
- *
838
- * @param {Object} client - NostrClient instance
839
- * @param {string} appname - Application namespace
840
- * @returns {Promise<Array>} Array of { holonId, writeGrants }
841
- */
842
- export async function getAllWriteGrants(client, appname) {
843
- console.warn('[Deprecated] getAllWriteGrants() is deprecated. Use getAccessGrants() instead.');
844
- const registry = await getFederationRegistry(client, appname);
845
- const now = Date.now();
846
- const results = [];
847
-
848
- for (const partner of registry.federatedWith) {
849
- if (partner.writeGrants && partner.writeGrants.length > 0) {
850
- // Filter out expired grants
851
- const validGrants = partner.writeGrants.filter(g => !g.expiresAt || g.expiresAt > now);
852
-
853
- if (validGrants.length > 0) {
854
- results.push({
855
- holonId: partner.pubKey,
856
- alias: partner.alias,
857
- writeGrants: validGrants,
858
- });
859
- }
860
- }
861
- }
862
-
863
- return results;
864
- }
865
-
866
- // ============================================================================
867
- // UNIFIED ACCESS GRANTS (New Unified Model)
868
- // ============================================================================
869
-
870
- /**
871
- * @typedef {Object} AccessGrant
872
- * @property {Object} scope - Grant scope
873
- * @property {string} [scope.holonId] - Holon ID pattern (or '*' for all)
874
- * @property {string} [scope.lensName] - Lens name pattern (or '*' for all)
875
- * @property {string[]} permissions - Array of permissions ('read', 'write')
876
- * @property {number} grantedAt - When the grant was created
877
- * @property {number|null} expiresAt - When the grant expires (null for never)
878
- * @property {string} [capabilityToken] - Optional capability token for cryptographic verification
879
- * @property {'inbound'|'outbound'} direction - Direction of the grant
880
- */
881
-
882
- /**
883
- * Grant unified access to a partner.
884
- * This is the new unified method that replaces separate read/write grant functions.
885
- *
886
- * @param {Object} client - NostrClient instance
887
- * @param {string} appname - Application namespace
888
- * @param {string} partnerPubKey - Partner's public key
889
- * @param {Object} scope - Access scope { holonId, lensName }
890
- * @param {string[]} permissions - Array of permissions ('read', 'write')
891
- * @param {Object} [options={}] - Grant options
892
- * @param {number} [options.expiresAt] - Expiration timestamp
893
- * @param {string} [options.capabilityToken] - Capability token for verification
894
- * @param {'inbound'|'outbound'} [options.direction='inbound'] - Grant direction
895
- * @returns {Promise<boolean>} Success indicator
896
- */
897
- export async function grantAccess(client, appname, partnerPubKey, scope, permissions, options = {}) {
898
- const registry = await getFederationRegistry(client, appname);
899
- const { expiresAt = null, capabilityToken = null, direction = 'inbound' } = options;
900
-
901
- // Find or create partner entry
902
- let partner = findPartner(client, appname, registry, partnerPubKey);
903
-
904
- if (!partner) {
905
- partner = {
906
- pubKey: partnerPubKey,
907
- alias: null,
908
- addedAt: Date.now(),
909
- addedVia: 'access_grant',
910
- inboundCapabilities: [],
911
- outboundCapabilities: [],
912
- writeGrants: [],
913
- accessGrants: [], // New unified array
914
- };
915
- registry.federatedWith.push(partner);
916
- }
917
-
918
- // Ensure accessGrants array exists
919
- if (!partner.accessGrants) {
920
- partner.accessGrants = [];
921
- }
922
-
923
- // Normalize scope
924
- const normalizedScope = {
925
- holonId: scope.holonId || '*',
926
- lensName: scope.lensName || '*',
927
- };
928
-
929
- // Check if grant already exists for this scope and direction
930
- const existingIndex = partner.accessGrants.findIndex(g =>
931
- g.scope.holonId === normalizedScope.holonId &&
932
- g.scope.lensName === normalizedScope.lensName &&
933
- g.direction === direction
934
- );
935
-
936
- const grant = {
937
- scope: normalizedScope,
938
- permissions: [...permissions],
939
- grantedAt: Date.now(),
940
- expiresAt,
941
- capabilityToken,
942
- direction,
943
- };
944
-
945
- if (existingIndex >= 0) {
946
- // Update existing grant
947
- partner.accessGrants[existingIndex] = grant;
948
- } else {
949
- // Add new grant
950
- partner.accessGrants.push(grant);
951
- }
952
-
953
- return saveFederationRegistry(client, appname, registry);
954
- }
955
-
956
- /**
957
- * Revoke access from a partner.
958
- * Removes specific permissions from an existing grant.
959
- *
960
- * @param {Object} client - NostrClient instance
961
- * @param {string} appname - Application namespace
962
- * @param {string} partnerPubKey - Partner's public key
963
- * @param {Object} scope - Access scope { holonId, lensName }
964
- * @param {string[]} [permissions] - Permissions to revoke (if omitted, revokes entire grant)
965
- * @param {Object} [options={}] - Revoke options
966
- * @param {'inbound'|'outbound'} [options.direction='inbound'] - Grant direction
967
- * @returns {Promise<boolean>} Success indicator
968
- */
969
- export async function revokeAccess(client, appname, partnerPubKey, scope, permissions = null, options = {}) {
970
- const registry = await getFederationRegistry(client, appname);
971
- const { direction = 'inbound' } = options;
972
-
973
- const partner = findPartner(client, appname, registry, partnerPubKey);
974
-
975
- if (!partner || !partner.accessGrants) {
976
- return false;
977
- }
978
-
979
- const normalizedScope = {
980
- holonId: scope.holonId || '*',
981
- lensName: scope.lensName || '*',
982
- };
983
-
984
- const grantIndex = partner.accessGrants.findIndex(g =>
985
- g.scope.holonId === normalizedScope.holonId &&
986
- g.scope.lensName === normalizedScope.lensName &&
987
- g.direction === direction
988
- );
989
-
990
- if (grantIndex < 0) {
991
- return false;
992
- }
993
-
994
- if (permissions === null) {
995
- // Remove entire grant
996
- partner.accessGrants.splice(grantIndex, 1);
997
- } else {
998
- // Remove specific permissions
999
- const grant = partner.accessGrants[grantIndex];
1000
- grant.permissions = grant.permissions.filter(p => !permissions.includes(p));
1001
-
1002
- // If no permissions left, remove the grant entirely
1003
- if (grant.permissions.length === 0) {
1004
- partner.accessGrants.splice(grantIndex, 1);
1005
- }
1006
- }
1007
-
1008
- return saveFederationRegistry(client, appname, registry);
1009
- }
1010
-
1011
- /**
1012
- * Find an access grant for a partner matching the given scope and permission.
1013
- * Checks unified accessGrants first, then falls back to legacy structures for backward compatibility.
1014
- *
1015
- * @param {Object} client - NostrClient instance
1016
- * @param {string} appname - Application namespace
1017
- * @param {string} partnerPubKey - Partner's public key
1018
- * @param {Object} scope - Access scope { holonId, lensName }
1019
- * @param {string} permission - Permission to check ('read' or 'write')
1020
- * @param {Object} [options={}] - Options
1021
- * @param {'inbound'|'outbound'} [options.direction='inbound'] - Grant direction
1022
- * @returns {Promise<AccessGrant|null>} Matching grant or null
1023
- */
1024
- export async function findAccessGrant(client, appname, partnerPubKey, scope, permission, options = {}) {
1025
- const registry = await getFederationRegistry(client, appname);
1026
- const { direction = 'inbound' } = options;
1027
- const now = Date.now();
1028
-
1029
- const partner = findPartner(client, appname, registry, partnerPubKey);
1030
-
1031
- if (!partner) {
1032
- return null;
1033
- }
1034
-
1035
- // Check unified accessGrants first
1036
- if (partner.accessGrants && partner.accessGrants.length > 0) {
1037
- for (const grant of partner.accessGrants) {
1038
- // Check expiration
1039
- if (grant.expiresAt && grant.expiresAt < now) {
1040
- continue;
1041
- }
1042
-
1043
- // Check direction
1044
- if (grant.direction !== direction) {
1045
- continue;
1046
- }
1047
-
1048
- // Check permission
1049
- if (!grant.permissions.includes(permission)) {
1050
- continue;
1051
- }
1052
-
1053
- // Check scope match
1054
- if (matchScope(grant.scope, scope)) {
1055
- return grant;
1056
- }
1057
- }
1058
- }
1059
-
1060
- // Fall back to legacy structures for backward compatibility
1061
- if (permission === 'read' && direction === 'inbound') {
1062
- // Check inboundCapabilities
1063
- if (partner.inboundCapabilities) {
1064
- for (const cap of partner.inboundCapabilities) {
1065
- if (cap.expires && cap.expires < now) continue;
1066
- if (cap.permissions && !cap.permissions.includes('read')) continue;
1067
- if (matchScope(cap.scope, scope)) {
1068
- return {
1069
- scope: cap.scope,
1070
- permissions: cap.permissions || ['read'],
1071
- grantedAt: cap.receivedAt || Date.now(),
1072
- expiresAt: cap.expires || null,
1073
- capabilityToken: cap.token,
1074
- direction: 'inbound',
1075
- _legacy: true,
1076
- };
1077
- }
1078
- }
1079
- }
1080
- }
1081
-
1082
- if (permission === 'write' && direction === 'inbound') {
1083
- // Check writeGrants
1084
- if (partner.writeGrants) {
1085
- for (const wg of partner.writeGrants) {
1086
- if (wg.expiresAt && wg.expiresAt < now) continue;
1087
- if (wg.lensName === scope.lensName || wg.lensName === '*') {
1088
- return {
1089
- scope: { holonId: '*', lensName: wg.lensName },
1090
- permissions: ['write'],
1091
- grantedAt: wg.grantedAt || Date.now(),
1092
- expiresAt: wg.expiresAt || null,
1093
- direction: 'inbound',
1094
- _legacy: true,
1095
- };
1096
- }
1097
- }
1098
- }
1099
- }
1100
-
1101
229
  return null;
1102
230
  }
1103
-
1104
- /**
1105
- * Get all access grants for a partner.
1106
- *
1107
- * @param {Object} client - NostrClient instance
1108
- * @param {string} appname - Application namespace
1109
- * @param {string} partnerPubKey - Partner's public key
1110
- * @param {Object} [options={}] - Options
1111
- * @param {'inbound'|'outbound'|'all'} [options.direction='all'] - Filter by direction
1112
- * @param {boolean} [options.includeLegacy=true] - Include legacy grants
1113
- * @returns {Promise<AccessGrant[]>} Array of access grants
1114
- */
1115
- export async function getAccessGrants(client, appname, partnerPubKey, options = {}) {
1116
- const registry = await getFederationRegistry(client, appname);
1117
- const { direction = 'all', includeLegacy = true } = options;
1118
- const now = Date.now();
1119
-
1120
- const partner = findPartner(client, appname, registry, partnerPubKey);
1121
-
1122
- if (!partner) {
1123
- return [];
1124
- }
1125
-
1126
- const results = [];
1127
-
1128
- // Add unified accessGrants
1129
- if (partner.accessGrants) {
1130
- for (const grant of partner.accessGrants) {
1131
- // Filter expired
1132
- if (grant.expiresAt && grant.expiresAt < now) continue;
1133
- // Filter by direction
1134
- if (direction !== 'all' && grant.direction !== direction) continue;
1135
- results.push(grant);
1136
- }
1137
- }
1138
-
1139
- // Add legacy grants if requested
1140
- if (includeLegacy) {
1141
- // Convert inboundCapabilities
1142
- if (partner.inboundCapabilities && (direction === 'all' || direction === 'inbound')) {
1143
- for (const cap of partner.inboundCapabilities) {
1144
- if (cap.expires && cap.expires < now) continue;
1145
- results.push({
1146
- scope: cap.scope,
1147
- permissions: cap.permissions || ['read'],
1148
- grantedAt: cap.receivedAt || Date.now(),
1149
- expiresAt: cap.expires || null,
1150
- capabilityToken: cap.token,
1151
- direction: 'inbound',
1152
- _legacy: true,
1153
- });
1154
- }
1155
- }
1156
-
1157
- // Convert writeGrants
1158
- if (partner.writeGrants && (direction === 'all' || direction === 'inbound')) {
1159
- for (const wg of partner.writeGrants) {
1160
- if (wg.expiresAt && wg.expiresAt < now) continue;
1161
- results.push({
1162
- scope: { holonId: '*', lensName: wg.lensName },
1163
- permissions: ['write'],
1164
- grantedAt: wg.grantedAt || Date.now(),
1165
- expiresAt: wg.expiresAt || null,
1166
- direction: 'inbound',
1167
- _legacy: true,
1168
- });
1169
- }
1170
- }
1171
- }
1172
-
1173
- return results;
1174
- }
1175
-
1176
- /**
1177
- * Migrate legacy grants to unified accessGrants structure.
1178
- * This should be called during registry load to auto-migrate old data.
1179
- *
1180
- * @param {Object} client - NostrClient instance
1181
- * @param {string} appname - Application namespace
1182
- * @returns {Promise<Object>} Migration result { migratedPartners, migratedGrants }
1183
- */
1184
- export async function migrateToUnifiedGrants(client, appname) {
1185
- const registry = await getFederationRegistry(client, appname);
1186
- let migratedPartners = 0;
1187
- let migratedGrants = 0;
1188
-
1189
- for (const partner of registry.federatedWith) {
1190
- let partnerModified = false;
1191
-
1192
- // Initialize accessGrants if not present
1193
- if (!partner.accessGrants) {
1194
- partner.accessGrants = [];
1195
- }
1196
-
1197
- // Migrate inboundCapabilities
1198
- if (partner.inboundCapabilities && partner.inboundCapabilities.length > 0) {
1199
- for (const cap of partner.inboundCapabilities) {
1200
- // Check if already migrated
1201
- const alreadyMigrated = partner.accessGrants.some(g =>
1202
- g.scope.holonId === cap.scope?.holonId &&
1203
- g.scope.lensName === cap.scope?.lensName &&
1204
- g.direction === 'inbound' &&
1205
- g._migratedFrom === 'inboundCapability'
1206
- );
1207
-
1208
- if (!alreadyMigrated && cap.scope) {
1209
- partner.accessGrants.push({
1210
- scope: { holonId: cap.scope.holonId || '*', lensName: cap.scope.lensName || '*' },
1211
- permissions: cap.permissions || ['read'],
1212
- grantedAt: cap.receivedAt || Date.now(),
1213
- expiresAt: cap.expires || null,
1214
- capabilityToken: cap.token,
1215
- direction: 'inbound',
1216
- _migratedFrom: 'inboundCapability',
1217
- });
1218
- migratedGrants++;
1219
- partnerModified = true;
1220
- }
1221
- }
1222
- }
1223
-
1224
- // Migrate writeGrants
1225
- if (partner.writeGrants && partner.writeGrants.length > 0) {
1226
- for (const wg of partner.writeGrants) {
1227
- // Check if already migrated
1228
- const alreadyMigrated = partner.accessGrants.some(g =>
1229
- g.scope.lensName === wg.lensName &&
1230
- g.permissions.includes('write') &&
1231
- g.direction === 'inbound' &&
1232
- g._migratedFrom === 'writeGrant'
1233
- );
1234
-
1235
- if (!alreadyMigrated) {
1236
- partner.accessGrants.push({
1237
- scope: { holonId: '*', lensName: wg.lensName },
1238
- permissions: ['write'],
1239
- grantedAt: wg.grantedAt || Date.now(),
1240
- expiresAt: wg.expiresAt || null,
1241
- direction: 'inbound',
1242
- _migratedFrom: 'writeGrant',
1243
- });
1244
- migratedGrants++;
1245
- partnerModified = true;
1246
- }
1247
- }
1248
- }
1249
-
1250
- if (partnerModified) {
1251
- migratedPartners++;
1252
- }
1253
- }
1254
-
1255
- if (migratedGrants > 0) {
1256
- await saveFederationRegistry(client, appname, registry);
1257
- }
1258
-
1259
- return { migratedPartners, migratedGrants };
1260
- }
1261
-
1262
- /**
1263
- * Convert legacy lensConfig format (v1.0) to unified permissions format (v2.0).
1264
- *
1265
- * @param {Object} legacyConfig - Legacy lens config { inbound, outbound, writeInbound, writeOutbound }
1266
- * @returns {Object} Unified permissions format { version, permissions }
1267
- */
1268
- export function convertLegacyLensConfig(legacyConfig) {
1269
- if (!legacyConfig) return { version: '2.0', permissions: {} };
1270
-
1271
- const permissions = {};
1272
-
1273
- // Process inbound (read receive)
1274
- for (const lens of (legacyConfig.inbound || [])) {
1275
- if (!permissions[lens]) {
1276
- permissions[lens] = { receive: [], share: [] };
1277
- }
1278
- if (!permissions[lens].receive.includes('read')) {
1279
- permissions[lens].receive.push('read');
1280
- }
1281
- }
1282
-
1283
- // Process outbound (read share)
1284
- for (const lens of (legacyConfig.outbound || [])) {
1285
- if (!permissions[lens]) {
1286
- permissions[lens] = { receive: [], share: [] };
1287
- }
1288
- if (!permissions[lens].share.includes('read')) {
1289
- permissions[lens].share.push('read');
1290
- }
1291
- }
1292
-
1293
- // Process writeInbound (write receive)
1294
- for (const lens of (legacyConfig.writeInbound || [])) {
1295
- if (!permissions[lens]) {
1296
- permissions[lens] = { receive: [], share: [] };
1297
- }
1298
- if (!permissions[lens].receive.includes('write')) {
1299
- permissions[lens].receive.push('write');
1300
- }
1301
- }
1302
-
1303
- // Process writeOutbound (write share)
1304
- for (const lens of (legacyConfig.writeOutbound || [])) {
1305
- if (!permissions[lens]) {
1306
- permissions[lens] = { receive: [], share: [] };
1307
- }
1308
- if (!permissions[lens].share.includes('write')) {
1309
- permissions[lens].share.push('write');
1310
- }
1311
- }
1312
-
1313
- return { version: '2.0', permissions };
1314
- }
1315
-
1316
- /**
1317
- * Convert unified permissions format (v2.0) to legacy lensConfig format (v1.0).
1318
- * Useful for backward compatibility with older clients.
1319
- *
1320
- * @param {Object} unifiedConfig - Unified permissions { version, permissions }
1321
- * @returns {Object} Legacy lens config { inbound, outbound, writeInbound, writeOutbound }
1322
- */
1323
- export function convertToLegacyLensConfig(unifiedConfig) {
1324
- if (!unifiedConfig || !unifiedConfig.permissions) {
1325
- return { inbound: [], outbound: [], writeInbound: [], writeOutbound: [] };
1326
- }
1327
-
1328
- const legacy = {
1329
- inbound: [],
1330
- outbound: [],
1331
- writeInbound: [],
1332
- writeOutbound: [],
1333
- };
1334
-
1335
- for (const [lensName, perms] of Object.entries(unifiedConfig.permissions)) {
1336
- // Read receive → inbound
1337
- if (perms.receive?.includes('read')) {
1338
- legacy.inbound.push(lensName);
1339
- }
1340
- // Read share → outbound
1341
- if (perms.share?.includes('read')) {
1342
- legacy.outbound.push(lensName);
1343
- }
1344
- // Write receive → writeInbound
1345
- if (perms.receive?.includes('write')) {
1346
- legacy.writeInbound.push(lensName);
1347
- }
1348
- // Write share → writeOutbound
1349
- if (perms.share?.includes('write')) {
1350
- legacy.writeOutbound.push(lensName);
1351
- }
1352
- }
1353
-
1354
- return legacy;
1355
- }
1356
-
1357
- // ============================================================================
1358
- // Capability Issuance Helpers (moved from capabilities.js)
1359
- // ============================================================================
1360
-
1361
- /**
1362
- * Issue capability for a specific lens
1363
- * @param {Object} client - NostrClient instance
1364
- * @param {string} holonId - Holon ID
1365
- * @param {string} lensName - Lens name
1366
- * @param {string} targetPubKey - Target public key
1367
- * @param {Object} options - Options
1368
- * @returns {Promise<Object>} Capability info { token, scope, permissions }
1369
- */
1370
- export async function issueCapabilityForLens(client, holonId, lensName, targetPubKey, options = {}) {
1371
- const {
1372
- permissions = ['read'],
1373
- expiresIn = 365 * 24 * 60 * 60 * 1000, // 1 year default
1374
- } = options;
1375
-
1376
- const scope = { holonId, lensName, dataId: '*' };
1377
-
1378
- const token = await cryptoIssueCapability(
1379
- permissions,
1380
- scope,
1381
- targetPubKey,
1382
- {
1383
- expiresIn,
1384
- issuer: client.publicKey,
1385
- issuerKey: client.privateKey,
1386
- isSelfCapability: client.publicKey === targetPubKey,
1387
- }
1388
- );
1389
-
1390
- return {
1391
- token,
1392
- scope,
1393
- permissions,
1394
- expiresAt: Date.now() + expiresIn,
1395
- };
1396
- }
1397
-
1398
- /**
1399
- * Issue capabilities for multiple lenses
1400
- * @param {Object} client - NostrClient instance
1401
- * @param {string} holonId - Holon ID
1402
- * @param {string[]} lensNames - Array of lens names
1403
- * @param {string} targetPubKey - Target public key
1404
- * @param {Object} options - Options
1405
- * @returns {Promise<Object[]>} Array of capability info objects
1406
- */
1407
- export async function issueCapabilitiesForLenses(client, holonId, lensNames, targetPubKey, options = {}) {
1408
- const capabilities = [];
1409
-
1410
- for (const lensName of lensNames) {
1411
- try {
1412
- const capInfo = await issueCapabilityForLens(client, holonId, lensName, targetPubKey, options);
1413
- capabilities.push(capInfo);
1414
- } catch (err) {
1415
- console.warn(`[Registry] Failed to issue capability for ${lensName}:`, err.message);
1416
- }
1417
- }
1418
-
1419
- return capabilities;
1420
- }