holosphere 2.0.0-alpha20 → 2.0.0-alpha22

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