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.
- package/README.md +1 -2
- package/dist/cjs/holosphere.cjs +1 -1
- package/dist/esm/holosphere.js +43 -41
- package/dist/{index-COpLk9gL.cjs → index-B4xe-N5-.cjs} +2 -2
- package/dist/{index-COpLk9gL.cjs.map → index-B4xe-N5-.cjs.map} +1 -1
- package/dist/{index-D2WstuZJ.js → index-Bug_CGNq.js} +2 -2
- package/dist/{index-D2WstuZJ.js.map → index-Bug_CGNq.js.map} +1 -1
- package/dist/index-CaCPzdlv.cjs +29 -0
- package/dist/index-CaCPzdlv.cjs.map +1 -0
- package/dist/{index-B6-8KAQm.js → index-D76zMgwU.js} +2 -2
- package/dist/{index-B6-8KAQm.js.map → index-D76zMgwU.js.map} +1 -1
- package/dist/{index--QsHG_gD.cjs → index-DuOuk96g.cjs} +2 -2
- package/dist/{index--QsHG_gD.cjs.map → index-DuOuk96g.cjs.map} +1 -1
- package/dist/{index-BHptWysv.js → index-bYHRpACA.js} +2951 -7736
- package/dist/index-bYHRpACA.js.map +1 -0
- package/dist/{indexeddb-storage-kQ53UHEE.js → indexeddb-storage-BrIwr42m.js} +2 -2
- package/dist/{indexeddb-storage-kQ53UHEE.js.map → indexeddb-storage-BrIwr42m.js.map} +1 -1
- package/dist/{indexeddb-storage-wKG4mICM.cjs → indexeddb-storage-CFWfkdX9.cjs} +2 -2
- package/dist/{indexeddb-storage-wKG4mICM.cjs.map → indexeddb-storage-CFWfkdX9.cjs.map} +1 -1
- package/dist/{memory-storage-DnXCSbBl.js → memory-storage-BDQRj-2j.js} +2 -2
- package/dist/{memory-storage-DnXCSbBl.js.map → memory-storage-BDQRj-2j.js.map} +1 -1
- package/dist/{memory-storage-CGC8xM2G.cjs → memory-storage-bkatDnuR.cjs} +2 -2
- package/dist/{memory-storage-CGC8xM2G.cjs.map → memory-storage-bkatDnuR.cjs.map} +1 -1
- package/examples/demo.html +2 -29
- package/package.json +3 -8
- package/src/content/social-protocols.js +3 -59
- package/src/core/holosphere.js +16 -554
- package/src/crypto/nostr-utils.js +98 -1
- package/src/crypto/secp256k1.js +13 -411
- package/src/federation/discovery.js +7 -75
- package/src/federation/handshake.js +69 -202
- package/src/federation/hologram.js +275 -460
- package/src/federation/index.js +2 -9
- package/src/federation/registry.js +65 -1169
- package/src/federation/request-card.js +21 -35
- package/src/hierarchical/upcast.js +4 -9
- package/src/index.js +142 -296
- package/src/lib/federation-methods.js +388 -886
- package/src/storage/global-tables.js +1 -1
- package/src/storage/nostr-wrapper.js +9 -5
- package/src/subscriptions/manager.js +1 -1
- package/types/index.d.ts +145 -37
- package/bin/holosphere-activitypub.js +0 -158
- package/dist/2019-BzVkRcax.js +0 -6680
- package/dist/2019-BzVkRcax.js.map +0 -1
- package/dist/2019-C1hPR_Os.cjs +0 -8
- package/dist/2019-C1hPR_Os.cjs.map +0 -1
- package/dist/browser-BcmACE3G.js +0 -3058
- package/dist/browser-BcmACE3G.js.map +0 -1
- package/dist/browser-DaqYUTcG.cjs +0 -2
- package/dist/browser-DaqYUTcG.cjs.map +0 -1
- package/dist/index-BHptWysv.js.map +0 -1
- package/dist/index-CDlhzxT2.cjs +0 -29
- package/dist/index-CDlhzxT2.cjs.map +0 -1
- package/src/federation/capabilities.js +0 -162
- package/src/storage/backend-factory.js +0 -130
- package/src/storage/backend-interface.js +0 -161
- package/src/storage/backends/activitypub/server.js +0 -675
- package/src/storage/backends/activitypub-backend.js +0 -295
- package/src/storage/backends/gundb-backend.js +0 -875
- package/src/storage/backends/nostr-backend.js +0 -251
- package/src/storage/gun-async.js +0 -341
- package/src/storage/gun-auth.js +0 -373
- package/src/storage/gun-federation.js +0 -785
- package/src/storage/gun-references.js +0 -209
- package/src/storage/gun-schema.js +0 -306
- package/src/storage/gun-wrapper.js +0 -642
- package/src/storage/migration.js +0 -351
- package/src/storage/unified-storage.js +0 -161
|
@@ -1,41 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Federation Registry
|
|
2
|
+
* Federation Registry — Simplified.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
*
|
|
9
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
102
|
-
if (!registry.
|
|
103
|
-
|
|
104
|
-
}
|
|
75
|
+
// Ensure arrays exist
|
|
76
|
+
if (!registry.federatedWith) registry.federatedWith = [];
|
|
77
|
+
if (!registry.capabilityWriters) registry.capabilityWriters = [];
|
|
105
78
|
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
|
170
|
-
const existing = registry.federatedWith[existingIndex];
|
|
171
|
-
|
|
109
|
+
// Update alias if provided
|
|
172
110
|
if (options.alias) {
|
|
173
|
-
|
|
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;
|
|
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
|
-
//
|
|
216
|
+
// Partner Lookup
|
|
686
217
|
// ============================================================================
|
|
687
218
|
|
|
688
219
|
/**
|
|
689
|
-
*
|
|
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
|
|
949
|
-
|
|
950
|
-
const
|
|
951
|
-
|
|
952
|
-
|
|
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
|
-
}
|