holosphere 2.0.0-alpha13 → 2.0.0-alpha15
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.
- package/dist/2019-ATLjawsU.cjs +8 -0
- package/dist/{2019-Cp3uYhyY.cjs.map → 2019-ATLjawsU.cjs.map} +1 -1
- package/dist/{2019-CLMqIAfQ.js → 2019-BfjzDRje.js} +1667 -1721
- package/dist/{2019-CLMqIAfQ.js.map → 2019-BfjzDRje.js.map} +1 -1
- package/dist/{browser-nUQt1cnB.js → browser-CKTczilW.js} +2 -2
- package/dist/{browser-nUQt1cnB.js.map → browser-CKTczilW.js.map} +1 -1
- package/dist/{browser-D6cNVl0v.cjs → browser-GOg6KKOV.cjs} +2 -2
- package/dist/{browser-D6cNVl0v.cjs.map → browser-GOg6KKOV.cjs.map} +1 -1
- package/dist/cjs/holosphere.cjs +1 -1
- package/dist/esm/holosphere.js +25 -21
- package/dist/{index-CoAjtqsD.js → index-BdnrGafX.js} +2 -2
- package/dist/{index-CoAjtqsD.js.map → index-BdnrGafX.js.map} +1 -1
- package/dist/{index-BN_uoxQK.js → index-C3Cag0SV.js} +2558 -495
- package/dist/index-C3Cag0SV.js.map +1 -0
- package/dist/index-ChpSfdYS.cjs +29 -0
- package/dist/index-ChpSfdYS.cjs.map +1 -0
- package/dist/{index-DJjGSwXG.cjs → index-D_QecZNu.cjs} +2 -2
- package/dist/{index-DJjGSwXG.cjs.map → index-D_QecZNu.cjs.map} +1 -1
- package/dist/{index-Z5TstN1e.js → index-nMC3dWZ5.js} +2 -2
- package/dist/{index-Z5TstN1e.js.map → index-nMC3dWZ5.js.map} +1 -1
- package/dist/{index-Cp3tI53z.cjs → index-z5HWfWMu.cjs} +2 -2
- package/dist/{index-Cp3tI53z.cjs.map → index-z5HWfWMu.cjs.map} +1 -1
- package/dist/{indexeddb-storage-CZK5A7XH.cjs → indexeddb-storage-DWSeL-YF.cjs} +2 -2
- package/dist/{indexeddb-storage-CZK5A7XH.cjs.map → indexeddb-storage-DWSeL-YF.cjs.map} +1 -1
- package/dist/{indexeddb-storage-bpA01pAU.js → indexeddb-storage-dx01N0ET.js} +2 -2
- package/dist/{indexeddb-storage-bpA01pAU.js.map → indexeddb-storage-dx01N0ET.js.map} +1 -1
- package/dist/{memory-storage-BqhmytP_.js → memory-storage-BPIfkpcf.js} +2 -2
- package/dist/{memory-storage-BqhmytP_.js.map → memory-storage-BPIfkpcf.js.map} +1 -1
- package/dist/{memory-storage-B1k8Jszd.cjs → memory-storage-CKUGDq2d.cjs} +2 -2
- package/dist/{memory-storage-B1k8Jszd.cjs.map → memory-storage-CKUGDq2d.cjs.map} +1 -1
- package/package.json +3 -1
- package/scripts/test-ndk-direct.js +104 -0
- package/src/crypto/key-store.js +356 -0
- package/src/crypto/lens-keys.js +205 -0
- package/src/crypto/secp256k1.js +181 -18
- package/src/federation/handshake.js +317 -23
- package/src/federation/hologram.js +25 -17
- package/src/federation/registry.js +779 -59
- package/src/index.js +416 -27
- package/src/lib/federation-methods.js +308 -4
- package/src/storage/nostr-async.js +144 -86
- package/src/storage/nostr-client.js +77 -18
- package/src/storage/nostr-wrapper.js +4 -1
- package/src/storage/unified-storage.js +5 -4
- package/src/subscriptions/manager.js +1 -1
- package/vitest.config.js +6 -1
- package/dist/2019-Cp3uYhyY.cjs +0 -8
- package/dist/index-BN_uoxQK.js.map +0 -1
- package/dist/index-V8EHMYEY.cjs +0 -29
- package/dist/index-V8EHMYEY.cjs.map +0 -1
|
@@ -14,16 +14,81 @@ import { matchScope } from '../crypto/secp256k1.js';
|
|
|
14
14
|
|
|
15
15
|
const FEDERATION_TABLE = 'federations';
|
|
16
16
|
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Registry Cache - Avoids repeated storage reads
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
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}` */
|
|
29
|
+
const registryCache = new Map();
|
|
30
|
+
|
|
31
|
+
/** Cache TTL in milliseconds (10 seconds) */
|
|
32
|
+
const CACHE_TTL_MS = 10000;
|
|
33
|
+
|
|
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
|
+
function buildPartnerIndex(federatedWith) {
|
|
40
|
+
const index = new Map();
|
|
41
|
+
for (const partner of federatedWith) {
|
|
42
|
+
if (partner.pubKey) {
|
|
43
|
+
index.set(partner.pubKey, partner);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return index;
|
|
47
|
+
}
|
|
48
|
+
|
|
17
49
|
/**
|
|
18
|
-
* Get
|
|
50
|
+
* Get cache key for a client/appname combination
|
|
19
51
|
* @param {Object} client - NostrClient instance
|
|
20
52
|
* @param {string} appname - Application namespace
|
|
53
|
+
* @returns {string} Cache key
|
|
54
|
+
*/
|
|
55
|
+
function getCacheKey(client, appname) {
|
|
56
|
+
return `${client.publicKey}:${appname}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
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
|
+
export function invalidateRegistryCache(client, appname) {
|
|
66
|
+
const key = getCacheKey(client, appname);
|
|
67
|
+
registryCache.delete(key);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get the federation registry for this holosphere (with caching)
|
|
72
|
+
* @param {Object} client - NostrClient instance
|
|
73
|
+
* @param {string} appname - Application namespace
|
|
74
|
+
* @param {Object} [options] - Options
|
|
75
|
+
* @param {boolean} [options.skipCache=false] - Force fresh read from storage
|
|
21
76
|
* @returns {Promise<Object>} Federation registry
|
|
22
77
|
*/
|
|
23
|
-
export async function getFederationRegistry(client, appname) {
|
|
24
|
-
const
|
|
78
|
+
export async function getFederationRegistry(client, appname, options = {}) {
|
|
79
|
+
const cacheKey = getCacheKey(client, appname);
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
|
|
82
|
+
// Check cache first (unless skipCache is set)
|
|
83
|
+
if (!options.skipCache) {
|
|
84
|
+
const cached = registryCache.get(cacheKey);
|
|
85
|
+
if (cached && (now - cached.timestamp) < CACHE_TTL_MS) {
|
|
86
|
+
return cached.registry;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
25
89
|
|
|
26
|
-
|
|
90
|
+
// Read from storage
|
|
91
|
+
const registry = await readGlobal(client, appname, FEDERATION_TABLE, client.publicKey) || {
|
|
27
92
|
id: client.publicKey,
|
|
28
93
|
federatedWith: [],
|
|
29
94
|
discoveryEnabled: false,
|
|
@@ -31,6 +96,37 @@ export async function getFederationRegistry(client, appname) {
|
|
|
31
96
|
defaultScope: { holonId: '*', lensName: '*' },
|
|
32
97
|
defaultPermissions: ['read'],
|
|
33
98
|
};
|
|
99
|
+
|
|
100
|
+
// Build partner index and cache
|
|
101
|
+
const partnerIndex = buildPartnerIndex(registry.federatedWith || []);
|
|
102
|
+
registryCache.set(cacheKey, {
|
|
103
|
+
registry,
|
|
104
|
+
partnerIndex,
|
|
105
|
+
timestamp: now,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return registry;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get partner from cache index (O(1) lookup)
|
|
113
|
+
* @param {Object} client - NostrClient instance
|
|
114
|
+
* @param {string} appname - Application namespace
|
|
115
|
+
* @param {string} partnerPubKey - Partner's public key
|
|
116
|
+
* @returns {Promise<Object|null>} Partner object or null
|
|
117
|
+
*/
|
|
118
|
+
async function getPartnerFromIndex(client, appname, partnerPubKey) {
|
|
119
|
+
const cacheKey = getCacheKey(client, appname);
|
|
120
|
+
|
|
121
|
+
// Ensure registry is loaded and cached
|
|
122
|
+
await getFederationRegistry(client, appname);
|
|
123
|
+
|
|
124
|
+
const cached = registryCache.get(cacheKey);
|
|
125
|
+
if (cached && cached.partnerIndex) {
|
|
126
|
+
return cached.partnerIndex.get(partnerPubKey) || null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return null;
|
|
34
130
|
}
|
|
35
131
|
|
|
36
132
|
/**
|
|
@@ -41,6 +137,8 @@ export async function getFederationRegistry(client, appname) {
|
|
|
41
137
|
* @returns {Promise<boolean>} Success indicator
|
|
42
138
|
*/
|
|
43
139
|
export async function saveFederationRegistry(client, appname, registry) {
|
|
140
|
+
// Invalidate cache before write so next read gets fresh data
|
|
141
|
+
invalidateRegistryCache(client, appname);
|
|
44
142
|
return writeGlobal(client, appname, FEDERATION_TABLE, registry);
|
|
45
143
|
}
|
|
46
144
|
|
|
@@ -70,10 +168,18 @@ export async function addFederatedPartner(client, appname, partnerPubKey, option
|
|
|
70
168
|
}
|
|
71
169
|
|
|
72
170
|
if (options.inboundCapabilities) {
|
|
73
|
-
|
|
171
|
+
// Merge and deduplicate capabilities by scope to prevent accumulation
|
|
172
|
+
const allCaps = [
|
|
74
173
|
...(existing.inboundCapabilities || []),
|
|
75
174
|
...options.inboundCapabilities
|
|
76
175
|
];
|
|
176
|
+
// Keep only unique capabilities (by scope), preferring newer ones (last in array)
|
|
177
|
+
const seen = new Map();
|
|
178
|
+
for (const cap of allCaps) {
|
|
179
|
+
const scopeKey = `${cap.scope?.holonId}:${cap.scope?.lensName}:${cap.scope?.dataId}`;
|
|
180
|
+
seen.set(scopeKey, cap); // Later entries overwrite earlier (newer wins)
|
|
181
|
+
}
|
|
182
|
+
existing.inboundCapabilities = Array.from(seen.values());
|
|
77
183
|
}
|
|
78
184
|
|
|
79
185
|
existing.updatedAt = Date.now();
|
|
@@ -125,20 +231,20 @@ export async function getFederatedAuthors(client, appname) {
|
|
|
125
231
|
}
|
|
126
232
|
|
|
127
233
|
/**
|
|
128
|
-
* Get a specific federated partner
|
|
234
|
+
* Get a specific federated partner (O(1) lookup via index)
|
|
129
235
|
* @param {Object} client - NostrClient instance
|
|
130
236
|
* @param {string} appname - Application namespace
|
|
131
237
|
* @param {string} partnerPubKey - Partner's public key
|
|
132
238
|
* @returns {Promise<Object|null>} Partner info or null
|
|
133
239
|
*/
|
|
134
240
|
export async function getFederatedPartner(client, appname, partnerPubKey) {
|
|
135
|
-
|
|
136
|
-
return registry.federatedWith.find(p => p.pubKey === partnerPubKey) || null;
|
|
241
|
+
return getPartnerFromIndex(client, appname, partnerPubKey);
|
|
137
242
|
}
|
|
138
243
|
|
|
139
244
|
/**
|
|
140
245
|
* Get capability for a specific author and scope
|
|
141
246
|
* Finds a valid, non-expired capability that covers the requested scope
|
|
247
|
+
* Uses O(1) partner lookup via index
|
|
142
248
|
* @param {Object} client - NostrClient instance
|
|
143
249
|
* @param {string} appname - Application namespace
|
|
144
250
|
* @param {string} authorPubKey - Author's public key
|
|
@@ -146,62 +252,35 @@ export async function getFederatedPartner(client, appname, partnerPubKey) {
|
|
|
146
252
|
* @returns {Promise<Object|null>} Capability entry or null
|
|
147
253
|
*/
|
|
148
254
|
export async function getCapabilityForAuthor(client, appname, authorPubKey, scope) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
console.log('[Registry] 🔍 getCapabilityForAuthor called:', {
|
|
152
|
-
authorPubKey: authorPubKey?.slice(0, 12) + '...',
|
|
153
|
-
scope,
|
|
154
|
-
federatedWithCount: registry.federatedWith?.length || 0,
|
|
155
|
-
federatedPartners: registry.federatedWith?.map(p => p.pubKey?.slice(0, 12) + '...') || []
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
const partner = registry.federatedWith.find(p => p.pubKey === authorPubKey);
|
|
255
|
+
// Use O(1) index lookup instead of array.find()
|
|
256
|
+
const partner = await getPartnerFromIndex(client, appname, authorPubKey);
|
|
159
257
|
|
|
160
258
|
if (!partner) {
|
|
161
|
-
console.log('[Registry] ❌ Partner not found in federatedWith');
|
|
162
259
|
return null;
|
|
163
260
|
}
|
|
164
261
|
|
|
165
262
|
if (!partner.inboundCapabilities || partner.inboundCapabilities.length === 0) {
|
|
166
|
-
console.log('[Registry] ❌ Partner has no inboundCapabilities:', {
|
|
167
|
-
partnerPubKey: authorPubKey?.slice(0, 12) + '...',
|
|
168
|
-
hasInboundCaps: !!partner.inboundCapabilities,
|
|
169
|
-
inboundCapsLength: partner.inboundCapabilities?.length || 0
|
|
170
|
-
});
|
|
171
263
|
return null;
|
|
172
264
|
}
|
|
173
265
|
|
|
174
|
-
|
|
175
|
-
partnerPubKey: authorPubKey?.slice(0, 12) + '...',
|
|
176
|
-
capsCount: partner.inboundCapabilities.length,
|
|
177
|
-
capScopes: partner.inboundCapabilities.map(c => c.scope)
|
|
178
|
-
});
|
|
266
|
+
const now = Date.now();
|
|
179
267
|
|
|
180
268
|
// Find matching, non-expired capability
|
|
181
|
-
|
|
269
|
+
// Note: We still iterate through capabilities here, but there are typically
|
|
270
|
+
// only a few per partner (often just one wildcard capability)
|
|
271
|
+
for (const cap of partner.inboundCapabilities) {
|
|
182
272
|
// Check expiration
|
|
183
|
-
if (cap.expires && cap.expires <
|
|
184
|
-
|
|
185
|
-
return false;
|
|
273
|
+
if (cap.expires && cap.expires < now) {
|
|
274
|
+
continue;
|
|
186
275
|
}
|
|
187
276
|
|
|
188
277
|
// Check scope match (supports wildcards)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
requestedScope: scope,
|
|
193
|
-
matches
|
|
194
|
-
});
|
|
195
|
-
return matches;
|
|
196
|
-
}) || null;
|
|
197
|
-
|
|
198
|
-
if (result) {
|
|
199
|
-
console.log('[Registry] ✅ Matching capability found');
|
|
200
|
-
} else {
|
|
201
|
-
console.log('[Registry] ❌ No matching capability found');
|
|
278
|
+
if (matchScope(cap.scope, scope)) {
|
|
279
|
+
return cap;
|
|
280
|
+
}
|
|
202
281
|
}
|
|
203
282
|
|
|
204
|
-
return
|
|
283
|
+
return null;
|
|
205
284
|
}
|
|
206
285
|
|
|
207
286
|
/**
|
|
@@ -217,17 +296,10 @@ export async function getCapabilityForAuthor(client, appname, authorPubKey, scop
|
|
|
217
296
|
* @returns {Promise<boolean>} Success indicator
|
|
218
297
|
*/
|
|
219
298
|
export async function storeInboundCapability(client, appname, partnerPubKey, capabilityInfo) {
|
|
220
|
-
console.log('[Registry] 📥 storeInboundCapability called:', {
|
|
221
|
-
partnerPubKey: partnerPubKey?.slice(0, 12) + '...',
|
|
222
|
-
scope: capabilityInfo?.scope,
|
|
223
|
-
hasToken: !!capabilityInfo?.token
|
|
224
|
-
});
|
|
225
|
-
|
|
226
299
|
const registry = await getFederationRegistry(client, appname);
|
|
227
300
|
const partner = registry.federatedWith.find(p => p.pubKey === partnerPubKey);
|
|
228
301
|
|
|
229
302
|
if (!partner) {
|
|
230
|
-
console.log('[Registry] Partner not in registry, auto-adding with capability');
|
|
231
303
|
// Auto-add partner if not exists
|
|
232
304
|
return addFederatedPartner(client, appname, partnerPubKey, {
|
|
233
305
|
addedVia: 'capability_received',
|
|
@@ -245,14 +317,12 @@ export async function storeInboundCapability(client, appname, partnerPubKey, cap
|
|
|
245
317
|
);
|
|
246
318
|
|
|
247
319
|
if (existingIndex >= 0) {
|
|
248
|
-
console.log('[Registry] Updating existing capability at index:', existingIndex);
|
|
249
320
|
// Update existing capability
|
|
250
321
|
partner.inboundCapabilities[existingIndex] = {
|
|
251
322
|
...capabilityInfo,
|
|
252
323
|
updatedAt: Date.now()
|
|
253
324
|
};
|
|
254
325
|
} else {
|
|
255
|
-
console.log('[Registry] Adding new capability, total will be:', partner.inboundCapabilities.length + 1);
|
|
256
326
|
// Add new capability
|
|
257
327
|
partner.inboundCapabilities.push({
|
|
258
328
|
...capabilityInfo,
|
|
@@ -260,9 +330,7 @@ export async function storeInboundCapability(client, appname, partnerPubKey, cap
|
|
|
260
330
|
});
|
|
261
331
|
}
|
|
262
332
|
|
|
263
|
-
|
|
264
|
-
console.log('[Registry] ✅ Capability stored, registry saved:', result);
|
|
265
|
-
return result;
|
|
333
|
+
return saveFederationRegistry(client, appname, registry);
|
|
266
334
|
}
|
|
267
335
|
|
|
268
336
|
/**
|
|
@@ -542,3 +610,655 @@ export async function getSelfCapability(client, appname, scope) {
|
|
|
542
610
|
export function isSelfCapability(capabilityInfo) {
|
|
543
611
|
return capabilityInfo && capabilityInfo.isSelfCapability === true;
|
|
544
612
|
}
|
|
613
|
+
|
|
614
|
+
// ============================================================================
|
|
615
|
+
// Write Grant Functions (Cross-Holon Write Access)
|
|
616
|
+
// ============================================================================
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Grant write access to a holon for a specific lens.
|
|
620
|
+
* This allows all members of the granted holon to write to the specified lens.
|
|
621
|
+
*
|
|
622
|
+
* @param {Object} client - NostrClient instance
|
|
623
|
+
* @param {string} appname - Application namespace
|
|
624
|
+
* @param {string} holonId - The holon (pubKey) being granted write access
|
|
625
|
+
* @param {string} lensName - The lens to grant write access to (or '*' for all lenses)
|
|
626
|
+
* @param {Object} [options={}] - Grant options
|
|
627
|
+
* @param {number} [options.expiresAt] - Optional expiration timestamp
|
|
628
|
+
* @returns {Promise<boolean>} Success indicator
|
|
629
|
+
*/
|
|
630
|
+
export async function grantWriteAccessToHolon(client, appname, holonId, lensName, options = {}) {
|
|
631
|
+
console.warn('[Deprecated] grantWriteAccessToHolon() is deprecated. Use grantAccess() instead.');
|
|
632
|
+
const registry = await getFederationRegistry(client, appname);
|
|
633
|
+
|
|
634
|
+
// Find or create partner entry
|
|
635
|
+
let partner = registry.federatedWith.find(p => p.pubKey === holonId);
|
|
636
|
+
|
|
637
|
+
if (!partner) {
|
|
638
|
+
// Auto-add partner if not exists
|
|
639
|
+
partner = {
|
|
640
|
+
pubKey: holonId,
|
|
641
|
+
alias: null,
|
|
642
|
+
addedAt: Date.now(),
|
|
643
|
+
addedVia: 'write_grant',
|
|
644
|
+
inboundCapabilities: [],
|
|
645
|
+
outboundCapabilities: [],
|
|
646
|
+
writeGrants: [],
|
|
647
|
+
};
|
|
648
|
+
registry.federatedWith.push(partner);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (!partner.writeGrants) {
|
|
652
|
+
partner.writeGrants = [];
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Check if grant already exists
|
|
656
|
+
const existingIndex = partner.writeGrants.findIndex(g => g.lensName === lensName);
|
|
657
|
+
|
|
658
|
+
const grant = {
|
|
659
|
+
lensName,
|
|
660
|
+
grantedAt: Date.now(),
|
|
661
|
+
expiresAt: options.expiresAt || null,
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
if (existingIndex >= 0) {
|
|
665
|
+
// Update existing grant
|
|
666
|
+
partner.writeGrants[existingIndex] = grant;
|
|
667
|
+
} else {
|
|
668
|
+
// Add new grant
|
|
669
|
+
partner.writeGrants.push(grant);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return saveFederationRegistry(client, appname, registry);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Revoke write access from a holon for a specific lens.
|
|
677
|
+
*
|
|
678
|
+
* @param {Object} client - NostrClient instance
|
|
679
|
+
* @param {string} appname - Application namespace
|
|
680
|
+
* @param {string} holonId - The holon to revoke write access from
|
|
681
|
+
* @param {string} lensName - The lens to revoke write access for
|
|
682
|
+
* @returns {Promise<boolean>} Success indicator
|
|
683
|
+
*/
|
|
684
|
+
export async function revokeWriteAccess(client, appname, holonId, lensName) {
|
|
685
|
+
console.warn('[Deprecated] revokeWriteAccess() is deprecated. Use revokeAccess() instead.');
|
|
686
|
+
const registry = await getFederationRegistry(client, appname);
|
|
687
|
+
|
|
688
|
+
const partner = registry.federatedWith.find(p => p.pubKey === holonId);
|
|
689
|
+
|
|
690
|
+
if (!partner || !partner.writeGrants) {
|
|
691
|
+
return false; // No grants to revoke
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const initialLength = partner.writeGrants.length;
|
|
695
|
+
partner.writeGrants = partner.writeGrants.filter(g => g.lensName !== lensName);
|
|
696
|
+
|
|
697
|
+
if (partner.writeGrants.length === initialLength) {
|
|
698
|
+
return false; // Grant not found
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
return saveFederationRegistry(client, appname, registry);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Get all write grants for a specific holon.
|
|
706
|
+
*
|
|
707
|
+
* @param {Object} client - NostrClient instance
|
|
708
|
+
* @param {string} appname - Application namespace
|
|
709
|
+
* @param {string} holonId - The holon to get write grants for
|
|
710
|
+
* @returns {Promise<Array>} Array of write grants { lensName, grantedAt, expiresAt }
|
|
711
|
+
*/
|
|
712
|
+
export async function getWriteGrantsForHolon(client, appname, holonId) {
|
|
713
|
+
console.warn('[Deprecated] getWriteGrantsForHolon() is deprecated. Use getAccessGrants() instead.');
|
|
714
|
+
const registry = await getFederationRegistry(client, appname);
|
|
715
|
+
|
|
716
|
+
const partner = registry.federatedWith.find(p => p.pubKey === holonId);
|
|
717
|
+
|
|
718
|
+
if (!partner || !partner.writeGrants) {
|
|
719
|
+
return [];
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Filter out expired grants
|
|
723
|
+
const now = Date.now();
|
|
724
|
+
return partner.writeGrants.filter(g => !g.expiresAt || g.expiresAt > now);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Check if a holon has write access to a specific lens.
|
|
729
|
+
*
|
|
730
|
+
* @param {Object} client - NostrClient instance
|
|
731
|
+
* @param {string} appname - Application namespace
|
|
732
|
+
* @param {string} holonId - The holon to check
|
|
733
|
+
* @param {string} lensName - The lens to check access for
|
|
734
|
+
* @returns {Promise<boolean>} True if holon has write access
|
|
735
|
+
*/
|
|
736
|
+
export async function hasWriteAccess(client, appname, holonId, lensName) {
|
|
737
|
+
console.warn('[Deprecated] hasWriteAccess() is deprecated. Use findAccessGrant() instead.');
|
|
738
|
+
const grants = await getWriteGrantsForHolon(client, appname, holonId);
|
|
739
|
+
|
|
740
|
+
// Check for exact lens match or wildcard
|
|
741
|
+
return grants.some(g => g.lensName === lensName || g.lensName === '*');
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Get all holons that have been granted write access to any lens.
|
|
746
|
+
*
|
|
747
|
+
* @param {Object} client - NostrClient instance
|
|
748
|
+
* @param {string} appname - Application namespace
|
|
749
|
+
* @returns {Promise<Array>} Array of { holonId, writeGrants }
|
|
750
|
+
*/
|
|
751
|
+
export async function getAllWriteGrants(client, appname) {
|
|
752
|
+
console.warn('[Deprecated] getAllWriteGrants() is deprecated. Use getAccessGrants() instead.');
|
|
753
|
+
const registry = await getFederationRegistry(client, appname);
|
|
754
|
+
const now = Date.now();
|
|
755
|
+
const results = [];
|
|
756
|
+
|
|
757
|
+
for (const partner of registry.federatedWith) {
|
|
758
|
+
if (partner.writeGrants && partner.writeGrants.length > 0) {
|
|
759
|
+
// Filter out expired grants
|
|
760
|
+
const validGrants = partner.writeGrants.filter(g => !g.expiresAt || g.expiresAt > now);
|
|
761
|
+
|
|
762
|
+
if (validGrants.length > 0) {
|
|
763
|
+
results.push({
|
|
764
|
+
holonId: partner.pubKey,
|
|
765
|
+
alias: partner.alias,
|
|
766
|
+
writeGrants: validGrants,
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
return results;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// ============================================================================
|
|
776
|
+
// UNIFIED ACCESS GRANTS (New Unified Model)
|
|
777
|
+
// ============================================================================
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* @typedef {Object} AccessGrant
|
|
781
|
+
* @property {Object} scope - Grant scope
|
|
782
|
+
* @property {string} [scope.holonId] - Holon ID pattern (or '*' for all)
|
|
783
|
+
* @property {string} [scope.lensName] - Lens name pattern (or '*' for all)
|
|
784
|
+
* @property {string[]} permissions - Array of permissions ('read', 'write')
|
|
785
|
+
* @property {number} grantedAt - When the grant was created
|
|
786
|
+
* @property {number|null} expiresAt - When the grant expires (null for never)
|
|
787
|
+
* @property {string} [capabilityToken] - Optional capability token for cryptographic verification
|
|
788
|
+
* @property {'inbound'|'outbound'} direction - Direction of the grant
|
|
789
|
+
*/
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Grant unified access to a partner.
|
|
793
|
+
* This is the new unified method that replaces separate read/write grant functions.
|
|
794
|
+
*
|
|
795
|
+
* @param {Object} client - NostrClient instance
|
|
796
|
+
* @param {string} appname - Application namespace
|
|
797
|
+
* @param {string} partnerPubKey - Partner's public key
|
|
798
|
+
* @param {Object} scope - Access scope { holonId, lensName }
|
|
799
|
+
* @param {string[]} permissions - Array of permissions ('read', 'write')
|
|
800
|
+
* @param {Object} [options={}] - Grant options
|
|
801
|
+
* @param {number} [options.expiresAt] - Expiration timestamp
|
|
802
|
+
* @param {string} [options.capabilityToken] - Capability token for verification
|
|
803
|
+
* @param {'inbound'|'outbound'} [options.direction='inbound'] - Grant direction
|
|
804
|
+
* @returns {Promise<boolean>} Success indicator
|
|
805
|
+
*/
|
|
806
|
+
export async function grantAccess(client, appname, partnerPubKey, scope, permissions, options = {}) {
|
|
807
|
+
const registry = await getFederationRegistry(client, appname);
|
|
808
|
+
const { expiresAt = null, capabilityToken = null, direction = 'inbound' } = options;
|
|
809
|
+
|
|
810
|
+
// Find or create partner entry
|
|
811
|
+
let partner = registry.federatedWith.find(p => p.pubKey === partnerPubKey);
|
|
812
|
+
|
|
813
|
+
if (!partner) {
|
|
814
|
+
partner = {
|
|
815
|
+
pubKey: partnerPubKey,
|
|
816
|
+
alias: null,
|
|
817
|
+
addedAt: Date.now(),
|
|
818
|
+
addedVia: 'access_grant',
|
|
819
|
+
inboundCapabilities: [],
|
|
820
|
+
outboundCapabilities: [],
|
|
821
|
+
writeGrants: [],
|
|
822
|
+
accessGrants: [], // New unified array
|
|
823
|
+
};
|
|
824
|
+
registry.federatedWith.push(partner);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Ensure accessGrants array exists
|
|
828
|
+
if (!partner.accessGrants) {
|
|
829
|
+
partner.accessGrants = [];
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// Normalize scope
|
|
833
|
+
const normalizedScope = {
|
|
834
|
+
holonId: scope.holonId || '*',
|
|
835
|
+
lensName: scope.lensName || '*',
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
// Check if grant already exists for this scope and direction
|
|
839
|
+
const existingIndex = partner.accessGrants.findIndex(g =>
|
|
840
|
+
g.scope.holonId === normalizedScope.holonId &&
|
|
841
|
+
g.scope.lensName === normalizedScope.lensName &&
|
|
842
|
+
g.direction === direction
|
|
843
|
+
);
|
|
844
|
+
|
|
845
|
+
const grant = {
|
|
846
|
+
scope: normalizedScope,
|
|
847
|
+
permissions: [...permissions],
|
|
848
|
+
grantedAt: Date.now(),
|
|
849
|
+
expiresAt,
|
|
850
|
+
capabilityToken,
|
|
851
|
+
direction,
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
if (existingIndex >= 0) {
|
|
855
|
+
// Update existing grant
|
|
856
|
+
partner.accessGrants[existingIndex] = grant;
|
|
857
|
+
} else {
|
|
858
|
+
// Add new grant
|
|
859
|
+
partner.accessGrants.push(grant);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
return saveFederationRegistry(client, appname, registry);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* Revoke access from a partner.
|
|
867
|
+
* Removes specific permissions from an existing grant.
|
|
868
|
+
*
|
|
869
|
+
* @param {Object} client - NostrClient instance
|
|
870
|
+
* @param {string} appname - Application namespace
|
|
871
|
+
* @param {string} partnerPubKey - Partner's public key
|
|
872
|
+
* @param {Object} scope - Access scope { holonId, lensName }
|
|
873
|
+
* @param {string[]} [permissions] - Permissions to revoke (if omitted, revokes entire grant)
|
|
874
|
+
* @param {Object} [options={}] - Revoke options
|
|
875
|
+
* @param {'inbound'|'outbound'} [options.direction='inbound'] - Grant direction
|
|
876
|
+
* @returns {Promise<boolean>} Success indicator
|
|
877
|
+
*/
|
|
878
|
+
export async function revokeAccess(client, appname, partnerPubKey, scope, permissions = null, options = {}) {
|
|
879
|
+
const registry = await getFederationRegistry(client, appname);
|
|
880
|
+
const { direction = 'inbound' } = options;
|
|
881
|
+
|
|
882
|
+
const partner = registry.federatedWith.find(p => p.pubKey === partnerPubKey);
|
|
883
|
+
|
|
884
|
+
if (!partner || !partner.accessGrants) {
|
|
885
|
+
return false;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const normalizedScope = {
|
|
889
|
+
holonId: scope.holonId || '*',
|
|
890
|
+
lensName: scope.lensName || '*',
|
|
891
|
+
};
|
|
892
|
+
|
|
893
|
+
const grantIndex = partner.accessGrants.findIndex(g =>
|
|
894
|
+
g.scope.holonId === normalizedScope.holonId &&
|
|
895
|
+
g.scope.lensName === normalizedScope.lensName &&
|
|
896
|
+
g.direction === direction
|
|
897
|
+
);
|
|
898
|
+
|
|
899
|
+
if (grantIndex < 0) {
|
|
900
|
+
return false;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
if (permissions === null) {
|
|
904
|
+
// Remove entire grant
|
|
905
|
+
partner.accessGrants.splice(grantIndex, 1);
|
|
906
|
+
} else {
|
|
907
|
+
// Remove specific permissions
|
|
908
|
+
const grant = partner.accessGrants[grantIndex];
|
|
909
|
+
grant.permissions = grant.permissions.filter(p => !permissions.includes(p));
|
|
910
|
+
|
|
911
|
+
// If no permissions left, remove the grant entirely
|
|
912
|
+
if (grant.permissions.length === 0) {
|
|
913
|
+
partner.accessGrants.splice(grantIndex, 1);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
return saveFederationRegistry(client, appname, registry);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* Find an access grant for a partner matching the given scope and permission.
|
|
922
|
+
* Checks unified accessGrants first, then falls back to legacy structures for backward compatibility.
|
|
923
|
+
*
|
|
924
|
+
* @param {Object} client - NostrClient instance
|
|
925
|
+
* @param {string} appname - Application namespace
|
|
926
|
+
* @param {string} partnerPubKey - Partner's public key
|
|
927
|
+
* @param {Object} scope - Access scope { holonId, lensName }
|
|
928
|
+
* @param {string} permission - Permission to check ('read' or 'write')
|
|
929
|
+
* @param {Object} [options={}] - Options
|
|
930
|
+
* @param {'inbound'|'outbound'} [options.direction='inbound'] - Grant direction
|
|
931
|
+
* @returns {Promise<AccessGrant|null>} Matching grant or null
|
|
932
|
+
*/
|
|
933
|
+
export async function findAccessGrant(client, appname, partnerPubKey, scope, permission, options = {}) {
|
|
934
|
+
const registry = await getFederationRegistry(client, appname);
|
|
935
|
+
const { direction = 'inbound' } = options;
|
|
936
|
+
const now = Date.now();
|
|
937
|
+
|
|
938
|
+
const partner = registry.federatedWith.find(p => p.pubKey === partnerPubKey);
|
|
939
|
+
|
|
940
|
+
if (!partner) {
|
|
941
|
+
return null;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// Check unified accessGrants first
|
|
945
|
+
if (partner.accessGrants && partner.accessGrants.length > 0) {
|
|
946
|
+
for (const grant of partner.accessGrants) {
|
|
947
|
+
// Check expiration
|
|
948
|
+
if (grant.expiresAt && grant.expiresAt < now) {
|
|
949
|
+
continue;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// Check direction
|
|
953
|
+
if (grant.direction !== direction) {
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Check permission
|
|
958
|
+
if (!grant.permissions.includes(permission)) {
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// Check scope match
|
|
963
|
+
if (matchScope(grant.scope, scope)) {
|
|
964
|
+
return grant;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// Fall back to legacy structures for backward compatibility
|
|
970
|
+
if (permission === 'read' && direction === 'inbound') {
|
|
971
|
+
// Check inboundCapabilities
|
|
972
|
+
if (partner.inboundCapabilities) {
|
|
973
|
+
for (const cap of partner.inboundCapabilities) {
|
|
974
|
+
if (cap.expires && cap.expires < now) continue;
|
|
975
|
+
if (cap.permissions && !cap.permissions.includes('read')) continue;
|
|
976
|
+
if (matchScope(cap.scope, scope)) {
|
|
977
|
+
return {
|
|
978
|
+
scope: cap.scope,
|
|
979
|
+
permissions: cap.permissions || ['read'],
|
|
980
|
+
grantedAt: cap.receivedAt || Date.now(),
|
|
981
|
+
expiresAt: cap.expires || null,
|
|
982
|
+
capabilityToken: cap.token,
|
|
983
|
+
direction: 'inbound',
|
|
984
|
+
_legacy: true,
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
if (permission === 'write' && direction === 'inbound') {
|
|
992
|
+
// Check writeGrants
|
|
993
|
+
if (partner.writeGrants) {
|
|
994
|
+
for (const wg of partner.writeGrants) {
|
|
995
|
+
if (wg.expiresAt && wg.expiresAt < now) continue;
|
|
996
|
+
if (wg.lensName === scope.lensName || wg.lensName === '*') {
|
|
997
|
+
return {
|
|
998
|
+
scope: { holonId: '*', lensName: wg.lensName },
|
|
999
|
+
permissions: ['write'],
|
|
1000
|
+
grantedAt: wg.grantedAt || Date.now(),
|
|
1001
|
+
expiresAt: wg.expiresAt || null,
|
|
1002
|
+
direction: 'inbound',
|
|
1003
|
+
_legacy: true,
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
return null;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* Get all access grants for a partner.
|
|
1015
|
+
*
|
|
1016
|
+
* @param {Object} client - NostrClient instance
|
|
1017
|
+
* @param {string} appname - Application namespace
|
|
1018
|
+
* @param {string} partnerPubKey - Partner's public key
|
|
1019
|
+
* @param {Object} [options={}] - Options
|
|
1020
|
+
* @param {'inbound'|'outbound'|'all'} [options.direction='all'] - Filter by direction
|
|
1021
|
+
* @param {boolean} [options.includeLegacy=true] - Include legacy grants
|
|
1022
|
+
* @returns {Promise<AccessGrant[]>} Array of access grants
|
|
1023
|
+
*/
|
|
1024
|
+
export async function getAccessGrants(client, appname, partnerPubKey, options = {}) {
|
|
1025
|
+
const registry = await getFederationRegistry(client, appname);
|
|
1026
|
+
const { direction = 'all', includeLegacy = true } = options;
|
|
1027
|
+
const now = Date.now();
|
|
1028
|
+
|
|
1029
|
+
const partner = registry.federatedWith.find(p => p.pubKey === partnerPubKey);
|
|
1030
|
+
|
|
1031
|
+
if (!partner) {
|
|
1032
|
+
return [];
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
const results = [];
|
|
1036
|
+
|
|
1037
|
+
// Add unified accessGrants
|
|
1038
|
+
if (partner.accessGrants) {
|
|
1039
|
+
for (const grant of partner.accessGrants) {
|
|
1040
|
+
// Filter expired
|
|
1041
|
+
if (grant.expiresAt && grant.expiresAt < now) continue;
|
|
1042
|
+
// Filter by direction
|
|
1043
|
+
if (direction !== 'all' && grant.direction !== direction) continue;
|
|
1044
|
+
results.push(grant);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// Add legacy grants if requested
|
|
1049
|
+
if (includeLegacy) {
|
|
1050
|
+
// Convert inboundCapabilities
|
|
1051
|
+
if (partner.inboundCapabilities && (direction === 'all' || direction === 'inbound')) {
|
|
1052
|
+
for (const cap of partner.inboundCapabilities) {
|
|
1053
|
+
if (cap.expires && cap.expires < now) continue;
|
|
1054
|
+
results.push({
|
|
1055
|
+
scope: cap.scope,
|
|
1056
|
+
permissions: cap.permissions || ['read'],
|
|
1057
|
+
grantedAt: cap.receivedAt || Date.now(),
|
|
1058
|
+
expiresAt: cap.expires || null,
|
|
1059
|
+
capabilityToken: cap.token,
|
|
1060
|
+
direction: 'inbound',
|
|
1061
|
+
_legacy: true,
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// Convert writeGrants
|
|
1067
|
+
if (partner.writeGrants && (direction === 'all' || direction === 'inbound')) {
|
|
1068
|
+
for (const wg of partner.writeGrants) {
|
|
1069
|
+
if (wg.expiresAt && wg.expiresAt < now) continue;
|
|
1070
|
+
results.push({
|
|
1071
|
+
scope: { holonId: '*', lensName: wg.lensName },
|
|
1072
|
+
permissions: ['write'],
|
|
1073
|
+
grantedAt: wg.grantedAt || Date.now(),
|
|
1074
|
+
expiresAt: wg.expiresAt || null,
|
|
1075
|
+
direction: 'inbound',
|
|
1076
|
+
_legacy: true,
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
return results;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/**
|
|
1086
|
+
* Migrate legacy grants to unified accessGrants structure.
|
|
1087
|
+
* This should be called during registry load to auto-migrate old data.
|
|
1088
|
+
*
|
|
1089
|
+
* @param {Object} client - NostrClient instance
|
|
1090
|
+
* @param {string} appname - Application namespace
|
|
1091
|
+
* @returns {Promise<Object>} Migration result { migratedPartners, migratedGrants }
|
|
1092
|
+
*/
|
|
1093
|
+
export async function migrateToUnifiedGrants(client, appname) {
|
|
1094
|
+
const registry = await getFederationRegistry(client, appname);
|
|
1095
|
+
let migratedPartners = 0;
|
|
1096
|
+
let migratedGrants = 0;
|
|
1097
|
+
|
|
1098
|
+
for (const partner of registry.federatedWith) {
|
|
1099
|
+
let partnerModified = false;
|
|
1100
|
+
|
|
1101
|
+
// Initialize accessGrants if not present
|
|
1102
|
+
if (!partner.accessGrants) {
|
|
1103
|
+
partner.accessGrants = [];
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// Migrate inboundCapabilities
|
|
1107
|
+
if (partner.inboundCapabilities && partner.inboundCapabilities.length > 0) {
|
|
1108
|
+
for (const cap of partner.inboundCapabilities) {
|
|
1109
|
+
// Check if already migrated
|
|
1110
|
+
const alreadyMigrated = partner.accessGrants.some(g =>
|
|
1111
|
+
g.scope.holonId === cap.scope?.holonId &&
|
|
1112
|
+
g.scope.lensName === cap.scope?.lensName &&
|
|
1113
|
+
g.direction === 'inbound' &&
|
|
1114
|
+
g._migratedFrom === 'inboundCapability'
|
|
1115
|
+
);
|
|
1116
|
+
|
|
1117
|
+
if (!alreadyMigrated && cap.scope) {
|
|
1118
|
+
partner.accessGrants.push({
|
|
1119
|
+
scope: { holonId: cap.scope.holonId || '*', lensName: cap.scope.lensName || '*' },
|
|
1120
|
+
permissions: cap.permissions || ['read'],
|
|
1121
|
+
grantedAt: cap.receivedAt || Date.now(),
|
|
1122
|
+
expiresAt: cap.expires || null,
|
|
1123
|
+
capabilityToken: cap.token,
|
|
1124
|
+
direction: 'inbound',
|
|
1125
|
+
_migratedFrom: 'inboundCapability',
|
|
1126
|
+
});
|
|
1127
|
+
migratedGrants++;
|
|
1128
|
+
partnerModified = true;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// Migrate writeGrants
|
|
1134
|
+
if (partner.writeGrants && partner.writeGrants.length > 0) {
|
|
1135
|
+
for (const wg of partner.writeGrants) {
|
|
1136
|
+
// Check if already migrated
|
|
1137
|
+
const alreadyMigrated = partner.accessGrants.some(g =>
|
|
1138
|
+
g.scope.lensName === wg.lensName &&
|
|
1139
|
+
g.permissions.includes('write') &&
|
|
1140
|
+
g.direction === 'inbound' &&
|
|
1141
|
+
g._migratedFrom === 'writeGrant'
|
|
1142
|
+
);
|
|
1143
|
+
|
|
1144
|
+
if (!alreadyMigrated) {
|
|
1145
|
+
partner.accessGrants.push({
|
|
1146
|
+
scope: { holonId: '*', lensName: wg.lensName },
|
|
1147
|
+
permissions: ['write'],
|
|
1148
|
+
grantedAt: wg.grantedAt || Date.now(),
|
|
1149
|
+
expiresAt: wg.expiresAt || null,
|
|
1150
|
+
direction: 'inbound',
|
|
1151
|
+
_migratedFrom: 'writeGrant',
|
|
1152
|
+
});
|
|
1153
|
+
migratedGrants++;
|
|
1154
|
+
partnerModified = true;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
if (partnerModified) {
|
|
1160
|
+
migratedPartners++;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
if (migratedGrants > 0) {
|
|
1165
|
+
await saveFederationRegistry(client, appname, registry);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
return { migratedPartners, migratedGrants };
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
/**
|
|
1172
|
+
* Convert legacy lensConfig format (v1.0) to unified permissions format (v2.0).
|
|
1173
|
+
*
|
|
1174
|
+
* @param {Object} legacyConfig - Legacy lens config { inbound, outbound, writeInbound, writeOutbound }
|
|
1175
|
+
* @returns {Object} Unified permissions format { version, permissions }
|
|
1176
|
+
*/
|
|
1177
|
+
export function convertLegacyLensConfig(legacyConfig) {
|
|
1178
|
+
if (!legacyConfig) return { version: '2.0', permissions: {} };
|
|
1179
|
+
|
|
1180
|
+
const permissions = {};
|
|
1181
|
+
|
|
1182
|
+
// Process inbound (read receive)
|
|
1183
|
+
for (const lens of (legacyConfig.inbound || [])) {
|
|
1184
|
+
if (!permissions[lens]) {
|
|
1185
|
+
permissions[lens] = { receive: [], share: [] };
|
|
1186
|
+
}
|
|
1187
|
+
if (!permissions[lens].receive.includes('read')) {
|
|
1188
|
+
permissions[lens].receive.push('read');
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// Process outbound (read share)
|
|
1193
|
+
for (const lens of (legacyConfig.outbound || [])) {
|
|
1194
|
+
if (!permissions[lens]) {
|
|
1195
|
+
permissions[lens] = { receive: [], share: [] };
|
|
1196
|
+
}
|
|
1197
|
+
if (!permissions[lens].share.includes('read')) {
|
|
1198
|
+
permissions[lens].share.push('read');
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// Process writeInbound (write receive)
|
|
1203
|
+
for (const lens of (legacyConfig.writeInbound || [])) {
|
|
1204
|
+
if (!permissions[lens]) {
|
|
1205
|
+
permissions[lens] = { receive: [], share: [] };
|
|
1206
|
+
}
|
|
1207
|
+
if (!permissions[lens].receive.includes('write')) {
|
|
1208
|
+
permissions[lens].receive.push('write');
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// Process writeOutbound (write share)
|
|
1213
|
+
for (const lens of (legacyConfig.writeOutbound || [])) {
|
|
1214
|
+
if (!permissions[lens]) {
|
|
1215
|
+
permissions[lens] = { receive: [], share: [] };
|
|
1216
|
+
}
|
|
1217
|
+
if (!permissions[lens].share.includes('write')) {
|
|
1218
|
+
permissions[lens].share.push('write');
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
return { version: '2.0', permissions };
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
/**
|
|
1226
|
+
* Convert unified permissions format (v2.0) to legacy lensConfig format (v1.0).
|
|
1227
|
+
* Useful for backward compatibility with older clients.
|
|
1228
|
+
*
|
|
1229
|
+
* @param {Object} unifiedConfig - Unified permissions { version, permissions }
|
|
1230
|
+
* @returns {Object} Legacy lens config { inbound, outbound, writeInbound, writeOutbound }
|
|
1231
|
+
*/
|
|
1232
|
+
export function convertToLegacyLensConfig(unifiedConfig) {
|
|
1233
|
+
if (!unifiedConfig || !unifiedConfig.permissions) {
|
|
1234
|
+
return { inbound: [], outbound: [], writeInbound: [], writeOutbound: [] };
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
const legacy = {
|
|
1238
|
+
inbound: [],
|
|
1239
|
+
outbound: [],
|
|
1240
|
+
writeInbound: [],
|
|
1241
|
+
writeOutbound: [],
|
|
1242
|
+
};
|
|
1243
|
+
|
|
1244
|
+
for (const [lensName, perms] of Object.entries(unifiedConfig.permissions)) {
|
|
1245
|
+
// Read receive → inbound
|
|
1246
|
+
if (perms.receive?.includes('read')) {
|
|
1247
|
+
legacy.inbound.push(lensName);
|
|
1248
|
+
}
|
|
1249
|
+
// Read share → outbound
|
|
1250
|
+
if (perms.share?.includes('read')) {
|
|
1251
|
+
legacy.outbound.push(lensName);
|
|
1252
|
+
}
|
|
1253
|
+
// Write receive → writeInbound
|
|
1254
|
+
if (perms.receive?.includes('write')) {
|
|
1255
|
+
legacy.writeInbound.push(lensName);
|
|
1256
|
+
}
|
|
1257
|
+
// Write share → writeOutbound
|
|
1258
|
+
if (perms.share?.includes('write')) {
|
|
1259
|
+
legacy.writeOutbound.push(lensName);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
return legacy;
|
|
1264
|
+
}
|