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