holosphere 1.1.20 → 2.0.0-alpha0
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/.env.example +36 -0
- package/.eslintrc.json +16 -0
- package/.prettierrc.json +7 -0
- package/README.md +483 -367
- package/bin/holosphere-activitypub.js +158 -0
- package/cleanup-test-data.js +204 -0
- package/examples/demo.html +1333 -0
- package/examples/example-bot.js +197 -0
- package/package.json +47 -87
- package/scripts/check-bundle-size.js +54 -0
- package/scripts/check-quest-ids.js +77 -0
- package/scripts/import-holons.js +578 -0
- package/scripts/publish-to-relay.js +101 -0
- package/scripts/read-example.js +186 -0
- package/scripts/relay-diagnostic.js +59 -0
- package/scripts/relay-example.js +179 -0
- package/scripts/resync-to-relay.js +245 -0
- package/scripts/revert-import.js +196 -0
- package/scripts/test-hybrid-mode.js +108 -0
- package/scripts/test-local-storage.js +63 -0
- package/scripts/test-nostr-direct.js +55 -0
- package/scripts/test-read-data.js +45 -0
- package/scripts/test-write-read.js +63 -0
- package/scripts/verify-import.js +95 -0
- package/scripts/verify-relay-data.js +139 -0
- package/src/ai/aggregation.js +319 -0
- package/src/ai/breakdown.js +511 -0
- package/src/ai/classifier.js +217 -0
- package/src/ai/council.js +228 -0
- package/src/ai/embeddings.js +279 -0
- package/src/ai/federation-ai.js +324 -0
- package/src/ai/h3-ai.js +955 -0
- package/src/ai/index.js +112 -0
- package/src/ai/json-ops.js +225 -0
- package/src/ai/llm-service.js +205 -0
- package/src/ai/nl-query.js +223 -0
- package/src/ai/relationships.js +353 -0
- package/src/ai/schema-extractor.js +218 -0
- package/src/ai/spatial.js +293 -0
- package/src/ai/tts.js +194 -0
- package/src/content/social-protocols.js +168 -0
- package/src/core/holosphere.js +273 -0
- package/src/crypto/secp256k1.js +259 -0
- package/src/federation/discovery.js +334 -0
- package/src/federation/hologram.js +1042 -0
- package/src/federation/registry.js +386 -0
- package/src/hierarchical/upcast.js +110 -0
- package/src/index.js +2669 -0
- package/src/schema/validator.js +91 -0
- package/src/spatial/h3-operations.js +110 -0
- package/src/storage/backend-factory.js +125 -0
- package/src/storage/backend-interface.js +142 -0
- package/src/storage/backends/activitypub/server.js +653 -0
- package/src/storage/backends/activitypub-backend.js +272 -0
- package/src/storage/backends/gundb-backend.js +233 -0
- package/src/storage/backends/nostr-backend.js +136 -0
- package/src/storage/filesystem-storage-browser.js +41 -0
- package/src/storage/filesystem-storage.js +138 -0
- package/src/storage/global-tables.js +81 -0
- package/src/storage/gun-async.js +281 -0
- package/src/storage/gun-wrapper.js +221 -0
- package/src/storage/indexeddb-storage.js +122 -0
- package/src/storage/key-storage-simple.js +76 -0
- package/src/storage/key-storage.js +136 -0
- package/src/storage/memory-storage.js +59 -0
- package/src/storage/migration.js +338 -0
- package/src/storage/nostr-async.js +811 -0
- package/src/storage/nostr-client.js +939 -0
- package/src/storage/nostr-wrapper.js +211 -0
- package/src/storage/outbox-queue.js +208 -0
- package/src/storage/persistent-storage.js +109 -0
- package/src/storage/sync-service.js +164 -0
- package/src/subscriptions/manager.js +142 -0
- package/test-ai-real-api.js +202 -0
- package/tests/unit/ai/aggregation.test.js +295 -0
- package/tests/unit/ai/breakdown.test.js +446 -0
- package/tests/unit/ai/classifier.test.js +294 -0
- package/tests/unit/ai/council.test.js +262 -0
- package/tests/unit/ai/embeddings.test.js +384 -0
- package/tests/unit/ai/federation-ai.test.js +344 -0
- package/tests/unit/ai/h3-ai.test.js +458 -0
- package/tests/unit/ai/index.test.js +304 -0
- package/tests/unit/ai/json-ops.test.js +307 -0
- package/tests/unit/ai/llm-service.test.js +390 -0
- package/tests/unit/ai/nl-query.test.js +383 -0
- package/tests/unit/ai/relationships.test.js +311 -0
- package/tests/unit/ai/schema-extractor.test.js +384 -0
- package/tests/unit/ai/spatial.test.js +279 -0
- package/tests/unit/ai/tts.test.js +279 -0
- package/tests/unit/content.test.js +332 -0
- package/tests/unit/contract/core.test.js +88 -0
- package/tests/unit/contract/crypto.test.js +198 -0
- package/tests/unit/contract/data.test.js +223 -0
- package/tests/unit/contract/federation.test.js +181 -0
- package/tests/unit/contract/hierarchical.test.js +113 -0
- package/tests/unit/contract/schema.test.js +114 -0
- package/tests/unit/contract/social.test.js +217 -0
- package/tests/unit/contract/spatial.test.js +110 -0
- package/tests/unit/contract/subscriptions.test.js +128 -0
- package/tests/unit/contract/utils.test.js +159 -0
- package/tests/unit/core.test.js +152 -0
- package/tests/unit/crypto.test.js +328 -0
- package/tests/unit/federation.test.js +234 -0
- package/tests/unit/gun-async.test.js +252 -0
- package/tests/unit/hierarchical.test.js +399 -0
- package/tests/unit/integration/scenario-01-geographic-storage.test.js +74 -0
- package/tests/unit/integration/scenario-02-federation.test.js +76 -0
- package/tests/unit/integration/scenario-03-subscriptions.test.js +102 -0
- package/tests/unit/integration/scenario-04-validation.test.js +129 -0
- package/tests/unit/integration/scenario-05-hierarchy.test.js +125 -0
- package/tests/unit/integration/scenario-06-social.test.js +135 -0
- package/tests/unit/integration/scenario-07-persistence.test.js +130 -0
- package/tests/unit/integration/scenario-08-authorization.test.js +161 -0
- package/tests/unit/integration/scenario-09-cross-dimensional.test.js +139 -0
- package/tests/unit/integration/scenario-10-cross-holosphere-capabilities.test.js +357 -0
- package/tests/unit/integration/scenario-11-cross-holosphere-federation.test.js +410 -0
- package/tests/unit/integration/scenario-12-capability-federated-read.test.js +719 -0
- package/tests/unit/performance/benchmark.test.js +85 -0
- package/tests/unit/schema.test.js +213 -0
- package/tests/unit/spatial.test.js +158 -0
- package/tests/unit/storage.test.js +195 -0
- package/tests/unit/subscriptions.test.js +328 -0
- package/tests/unit/test-data-permanence-debug.js +197 -0
- package/tests/unit/test-data-permanence.js +340 -0
- package/tests/unit/test-key-persistence-fixed.js +148 -0
- package/tests/unit/test-key-persistence.js +172 -0
- package/tests/unit/test-relay-permanence.js +376 -0
- package/tests/unit/test-second-node.js +95 -0
- package/tests/unit/test-simple-write.js +89 -0
- package/vite.config.js +49 -0
- package/vitest.config.js +20 -0
- package/FEDERATION.md +0 -213
- package/compute.js +0 -298
- package/content.js +0 -980
- package/federation.js +0 -1234
- package/global.js +0 -736
- package/hexlib.js +0 -335
- package/hologram.js +0 -183
- package/holosphere-bundle.esm.js +0 -33256
- package/holosphere-bundle.js +0 -33287
- package/holosphere-bundle.min.js +0 -39
- package/holosphere.d.ts +0 -601
- package/holosphere.js +0 -719
- package/node.js +0 -246
- package/schema.js +0 -139
- package/utils.js +0 -302
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Federation Registry - Manages cross-holosphere partnerships
|
|
3
|
+
* Tracks federated partners and their capability tokens
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { writeGlobal, readGlobal } from '../storage/global-tables.js';
|
|
7
|
+
import { matchScope } from '../crypto/secp256k1.js';
|
|
8
|
+
|
|
9
|
+
const FEDERATION_TABLE = 'federations';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get the federation registry for this holosphere
|
|
13
|
+
* @param {Object} client - NostrClient instance
|
|
14
|
+
* @param {string} appname - Application namespace
|
|
15
|
+
* @returns {Promise<Object>} Federation registry
|
|
16
|
+
*/
|
|
17
|
+
export async function getFederationRegistry(client, appname) {
|
|
18
|
+
const registry = await readGlobal(client, appname, FEDERATION_TABLE, client.publicKey);
|
|
19
|
+
|
|
20
|
+
return registry || {
|
|
21
|
+
id: client.publicKey,
|
|
22
|
+
federatedWith: [],
|
|
23
|
+
discoveryEnabled: false,
|
|
24
|
+
autoAccept: false,
|
|
25
|
+
defaultScope: { holonId: '*', lensName: '*' },
|
|
26
|
+
defaultPermissions: ['read'],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Save the federation registry
|
|
32
|
+
* @param {Object} client - NostrClient instance
|
|
33
|
+
* @param {string} appname - Application namespace
|
|
34
|
+
* @param {Object} registry - Registry to save
|
|
35
|
+
* @returns {Promise<boolean>} Success indicator
|
|
36
|
+
*/
|
|
37
|
+
export async function saveFederationRegistry(client, appname, registry) {
|
|
38
|
+
return writeGlobal(client, appname, FEDERATION_TABLE, registry);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Add a federated partner
|
|
43
|
+
* @param {Object} client - NostrClient instance
|
|
44
|
+
* @param {string} appname - Application namespace
|
|
45
|
+
* @param {string} partnerPubKey - Partner's public key
|
|
46
|
+
* @param {Object} options - Partner options
|
|
47
|
+
* @param {string} options.alias - Human-readable name
|
|
48
|
+
* @param {string} options.addedVia - How the partner was added ('manual' or 'nostr_discovery')
|
|
49
|
+
* @param {Object[]} options.inboundCapabilities - Capabilities they've granted us
|
|
50
|
+
* @returns {Promise<boolean>} Success indicator
|
|
51
|
+
*/
|
|
52
|
+
export async function addFederatedPartner(client, appname, partnerPubKey, options = {}) {
|
|
53
|
+
const registry = await getFederationRegistry(client, appname);
|
|
54
|
+
|
|
55
|
+
// Check if already exists
|
|
56
|
+
const existingIndex = registry.federatedWith.findIndex(p => p.pubKey === partnerPubKey);
|
|
57
|
+
|
|
58
|
+
if (existingIndex >= 0) {
|
|
59
|
+
// Update existing entry
|
|
60
|
+
const existing = registry.federatedWith[existingIndex];
|
|
61
|
+
|
|
62
|
+
if (options.alias) {
|
|
63
|
+
existing.alias = options.alias;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (options.inboundCapabilities) {
|
|
67
|
+
existing.inboundCapabilities = [
|
|
68
|
+
...(existing.inboundCapabilities || []),
|
|
69
|
+
...options.inboundCapabilities
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
existing.updatedAt = Date.now();
|
|
74
|
+
registry.federatedWith[existingIndex] = existing;
|
|
75
|
+
} else {
|
|
76
|
+
// Add new partner
|
|
77
|
+
registry.federatedWith.push({
|
|
78
|
+
pubKey: partnerPubKey,
|
|
79
|
+
alias: options.alias || null,
|
|
80
|
+
addedAt: Date.now(),
|
|
81
|
+
addedVia: options.addedVia || 'manual',
|
|
82
|
+
inboundCapabilities: options.inboundCapabilities || [],
|
|
83
|
+
outboundCapabilities: [],
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return saveFederationRegistry(client, appname, registry);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Remove a federated partner
|
|
92
|
+
* @param {Object} client - NostrClient instance
|
|
93
|
+
* @param {string} appname - Application namespace
|
|
94
|
+
* @param {string} partnerPubKey - Partner's public key to remove
|
|
95
|
+
* @returns {Promise<boolean>} Success indicator
|
|
96
|
+
*/
|
|
97
|
+
export async function removeFederatedPartner(client, appname, partnerPubKey) {
|
|
98
|
+
const registry = await getFederationRegistry(client, appname);
|
|
99
|
+
|
|
100
|
+
const initialLength = registry.federatedWith.length;
|
|
101
|
+
registry.federatedWith = registry.federatedWith.filter(p => p.pubKey !== partnerPubKey);
|
|
102
|
+
|
|
103
|
+
if (registry.federatedWith.length === initialLength) {
|
|
104
|
+
return false; // Partner not found
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return saveFederationRegistry(client, appname, registry);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get all federated author public keys
|
|
112
|
+
* @param {Object} client - NostrClient instance
|
|
113
|
+
* @param {string} appname - Application namespace
|
|
114
|
+
* @returns {Promise<string[]>} Array of federated public keys
|
|
115
|
+
*/
|
|
116
|
+
export async function getFederatedAuthors(client, appname) {
|
|
117
|
+
const registry = await getFederationRegistry(client, appname);
|
|
118
|
+
return registry.federatedWith.map(p => p.pubKey);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get a specific federated partner
|
|
123
|
+
* @param {Object} client - NostrClient instance
|
|
124
|
+
* @param {string} appname - Application namespace
|
|
125
|
+
* @param {string} partnerPubKey - Partner's public key
|
|
126
|
+
* @returns {Promise<Object|null>} Partner info or null
|
|
127
|
+
*/
|
|
128
|
+
export async function getFederatedPartner(client, appname, partnerPubKey) {
|
|
129
|
+
const registry = await getFederationRegistry(client, appname);
|
|
130
|
+
return registry.federatedWith.find(p => p.pubKey === partnerPubKey) || null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get capability for a specific author and scope
|
|
135
|
+
* Finds a valid, non-expired capability that covers the requested scope
|
|
136
|
+
* @param {Object} client - NostrClient instance
|
|
137
|
+
* @param {string} appname - Application namespace
|
|
138
|
+
* @param {string} authorPubKey - Author's public key
|
|
139
|
+
* @param {Object} scope - Requested scope { holonId, lensName, dataId? }
|
|
140
|
+
* @returns {Promise<Object|null>} Capability entry or null
|
|
141
|
+
*/
|
|
142
|
+
export async function getCapabilityForAuthor(client, appname, authorPubKey, scope) {
|
|
143
|
+
const registry = await getFederationRegistry(client, appname);
|
|
144
|
+
const partner = registry.federatedWith.find(p => p.pubKey === authorPubKey);
|
|
145
|
+
|
|
146
|
+
if (!partner || !partner.inboundCapabilities) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Find matching, non-expired capability
|
|
151
|
+
return partner.inboundCapabilities.find(cap => {
|
|
152
|
+
// Check expiration
|
|
153
|
+
if (cap.expires && cap.expires < Date.now()) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Check scope match (supports wildcards)
|
|
158
|
+
return matchScope(cap.scope, scope);
|
|
159
|
+
}) || null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Store an inbound capability (one we received from a partner)
|
|
164
|
+
* @param {Object} client - NostrClient instance
|
|
165
|
+
* @param {string} appname - Application namespace
|
|
166
|
+
* @param {string} partnerPubKey - Partner's public key
|
|
167
|
+
* @param {Object} capabilityInfo - Capability information
|
|
168
|
+
* @param {string} capabilityInfo.token - The capability token
|
|
169
|
+
* @param {Object} capabilityInfo.scope - Token scope
|
|
170
|
+
* @param {string[]} capabilityInfo.permissions - Granted permissions
|
|
171
|
+
* @param {number} capabilityInfo.expires - Expiration timestamp
|
|
172
|
+
* @returns {Promise<boolean>} Success indicator
|
|
173
|
+
*/
|
|
174
|
+
export async function storeInboundCapability(client, appname, partnerPubKey, capabilityInfo) {
|
|
175
|
+
const registry = await getFederationRegistry(client, appname);
|
|
176
|
+
const partner = registry.federatedWith.find(p => p.pubKey === partnerPubKey);
|
|
177
|
+
|
|
178
|
+
if (!partner) {
|
|
179
|
+
// Auto-add partner if not exists
|
|
180
|
+
return addFederatedPartner(client, appname, partnerPubKey, {
|
|
181
|
+
addedVia: 'capability_received',
|
|
182
|
+
inboundCapabilities: [capabilityInfo]
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!partner.inboundCapabilities) {
|
|
187
|
+
partner.inboundCapabilities = [];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Check if we already have this capability (by scope match)
|
|
191
|
+
const existingIndex = partner.inboundCapabilities.findIndex(cap =>
|
|
192
|
+
JSON.stringify(cap.scope) === JSON.stringify(capabilityInfo.scope)
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
if (existingIndex >= 0) {
|
|
196
|
+
// Update existing capability
|
|
197
|
+
partner.inboundCapabilities[existingIndex] = {
|
|
198
|
+
...capabilityInfo,
|
|
199
|
+
updatedAt: Date.now()
|
|
200
|
+
};
|
|
201
|
+
} else {
|
|
202
|
+
// Add new capability
|
|
203
|
+
partner.inboundCapabilities.push({
|
|
204
|
+
...capabilityInfo,
|
|
205
|
+
receivedAt: Date.now()
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return saveFederationRegistry(client, appname, registry);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Store an outbound capability (one we issued to a partner)
|
|
214
|
+
* We only store the hash, not the full token
|
|
215
|
+
* @param {Object} client - NostrClient instance
|
|
216
|
+
* @param {string} appname - Application namespace
|
|
217
|
+
* @param {string} partnerPubKey - Partner's public key
|
|
218
|
+
* @param {Object} capabilityInfo - Capability information
|
|
219
|
+
* @param {string} capabilityInfo.tokenHash - SHA-256 hash of the token
|
|
220
|
+
* @param {Object} capabilityInfo.scope - Token scope
|
|
221
|
+
* @param {string[]} capabilityInfo.permissions - Granted permissions
|
|
222
|
+
* @param {number} capabilityInfo.expires - Expiration timestamp
|
|
223
|
+
* @returns {Promise<boolean>} Success indicator
|
|
224
|
+
*/
|
|
225
|
+
export async function storeOutboundCapability(client, appname, partnerPubKey, capabilityInfo) {
|
|
226
|
+
const registry = await getFederationRegistry(client, appname);
|
|
227
|
+
const partner = registry.federatedWith.find(p => p.pubKey === partnerPubKey);
|
|
228
|
+
|
|
229
|
+
if (!partner) {
|
|
230
|
+
throw new Error(`Partner ${partnerPubKey} not found in federation registry`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (!partner.outboundCapabilities) {
|
|
234
|
+
partner.outboundCapabilities = [];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
partner.outboundCapabilities.push({
|
|
238
|
+
tokenHash: capabilityInfo.tokenHash,
|
|
239
|
+
scope: capabilityInfo.scope,
|
|
240
|
+
permissions: capabilityInfo.permissions,
|
|
241
|
+
issuedAt: Date.now(),
|
|
242
|
+
expires: capabilityInfo.expires,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
return saveFederationRegistry(client, appname, registry);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Revoke an outbound capability (by token hash)
|
|
250
|
+
* @param {Object} client - NostrClient instance
|
|
251
|
+
* @param {string} appname - Application namespace
|
|
252
|
+
* @param {string} partnerPubKey - Partner's public key
|
|
253
|
+
* @param {string} tokenHash - Hash of the token to revoke
|
|
254
|
+
* @returns {Promise<boolean>} Success indicator
|
|
255
|
+
*/
|
|
256
|
+
export async function revokeOutboundCapability(client, appname, partnerPubKey, tokenHash) {
|
|
257
|
+
const registry = await getFederationRegistry(client, appname);
|
|
258
|
+
const partner = registry.federatedWith.find(p => p.pubKey === partnerPubKey);
|
|
259
|
+
|
|
260
|
+
if (!partner || !partner.outboundCapabilities) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const initialLength = partner.outboundCapabilities.length;
|
|
265
|
+
partner.outboundCapabilities = partner.outboundCapabilities.filter(
|
|
266
|
+
cap => cap.tokenHash !== tokenHash
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
if (partner.outboundCapabilities.length === initialLength) {
|
|
270
|
+
return false; // Token not found
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return saveFederationRegistry(client, appname, registry);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Update federation discovery settings
|
|
278
|
+
* @param {Object} client - NostrClient instance
|
|
279
|
+
* @param {string} appname - Application namespace
|
|
280
|
+
* @param {Object} settings - Discovery settings
|
|
281
|
+
* @param {boolean} settings.discoveryEnabled - Enable Nostr discovery
|
|
282
|
+
* @param {boolean} settings.autoAccept - Auto-accept federation requests
|
|
283
|
+
* @param {Object} settings.defaultScope - Default scope for auto-accepted federations
|
|
284
|
+
* @param {string[]} settings.defaultPermissions - Default permissions for auto-accepted federations
|
|
285
|
+
* @returns {Promise<boolean>} Success indicator
|
|
286
|
+
*/
|
|
287
|
+
export async function updateDiscoverySettings(client, appname, settings) {
|
|
288
|
+
const registry = await getFederationRegistry(client, appname);
|
|
289
|
+
|
|
290
|
+
if (settings.discoveryEnabled !== undefined) {
|
|
291
|
+
registry.discoveryEnabled = settings.discoveryEnabled;
|
|
292
|
+
}
|
|
293
|
+
if (settings.autoAccept !== undefined) {
|
|
294
|
+
registry.autoAccept = settings.autoAccept;
|
|
295
|
+
}
|
|
296
|
+
if (settings.defaultScope !== undefined) {
|
|
297
|
+
registry.defaultScope = settings.defaultScope;
|
|
298
|
+
}
|
|
299
|
+
if (settings.defaultPermissions !== undefined) {
|
|
300
|
+
registry.defaultPermissions = settings.defaultPermissions;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return saveFederationRegistry(client, appname, registry);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Get all federated authors that have granted capabilities for a given scope
|
|
308
|
+
* Returns pubKeys of partners who have given us access to read/write their data
|
|
309
|
+
* @param {Object} client - NostrClient instance
|
|
310
|
+
* @param {string} appname - Application namespace
|
|
311
|
+
* @param {Object} scope - Requested scope { holonId?, lensName?, dataId? }
|
|
312
|
+
* @param {string} permission - Required permission ('read' or 'write')
|
|
313
|
+
* @returns {Promise<Array>} Array of { pubKey, capability } for matching partners
|
|
314
|
+
*/
|
|
315
|
+
export async function getFederatedAuthorsForScope(client, appname, scope = {}, permission = 'read') {
|
|
316
|
+
const registry = await getFederationRegistry(client, appname);
|
|
317
|
+
const now = Date.now();
|
|
318
|
+
const results = [];
|
|
319
|
+
|
|
320
|
+
for (const partner of registry.federatedWith) {
|
|
321
|
+
if (!partner.inboundCapabilities || partner.inboundCapabilities.length === 0) {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Find a valid capability that covers this scope
|
|
326
|
+
for (const cap of partner.inboundCapabilities) {
|
|
327
|
+
// Check expiration
|
|
328
|
+
if (cap.expires && cap.expires < now) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Check permission
|
|
333
|
+
if (cap.permissions && !cap.permissions.includes(permission)) {
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Check scope match (supports wildcards)
|
|
338
|
+
if (matchScope(cap.scope, scope)) {
|
|
339
|
+
results.push({
|
|
340
|
+
pubKey: partner.pubKey,
|
|
341
|
+
capability: cap,
|
|
342
|
+
});
|
|
343
|
+
break; // One valid capability per partner is enough
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return results;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Clean up expired capabilities from the registry
|
|
353
|
+
* @param {Object} client - NostrClient instance
|
|
354
|
+
* @param {string} appname - Application namespace
|
|
355
|
+
* @returns {Promise<Object>} Cleanup result { inboundRemoved, outboundRemoved }
|
|
356
|
+
*/
|
|
357
|
+
export async function cleanupExpiredCapabilities(client, appname) {
|
|
358
|
+
const registry = await getFederationRegistry(client, appname);
|
|
359
|
+
const now = Date.now();
|
|
360
|
+
let inboundRemoved = 0;
|
|
361
|
+
let outboundRemoved = 0;
|
|
362
|
+
|
|
363
|
+
for (const partner of registry.federatedWith) {
|
|
364
|
+
if (partner.inboundCapabilities) {
|
|
365
|
+
const beforeCount = partner.inboundCapabilities.length;
|
|
366
|
+
partner.inboundCapabilities = partner.inboundCapabilities.filter(
|
|
367
|
+
cap => !cap.expires || cap.expires > now
|
|
368
|
+
);
|
|
369
|
+
inboundRemoved += beforeCount - partner.inboundCapabilities.length;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (partner.outboundCapabilities) {
|
|
373
|
+
const beforeCount = partner.outboundCapabilities.length;
|
|
374
|
+
partner.outboundCapabilities = partner.outboundCapabilities.filter(
|
|
375
|
+
cap => !cap.expires || cap.expires > now
|
|
376
|
+
);
|
|
377
|
+
outboundRemoved += beforeCount - partner.outboundCapabilities.length;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (inboundRemoved > 0 || outboundRemoved > 0) {
|
|
382
|
+
await saveFederationRegistry(client, appname, registry);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return { inboundRemoved, outboundRemoved };
|
|
386
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hierarchical Aggregation (Upcast Operations)
|
|
3
|
+
* FR-025 to FR-027
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getParents, isValidH3 } from '../spatial/h3-operations.js';
|
|
7
|
+
import { createHologram } from '../federation/hologram.js';
|
|
8
|
+
import { write } from '../storage/nostr-wrapper.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Upcast data to parent holons in hierarchy
|
|
12
|
+
* @param {Object} client - Nostr client instance
|
|
13
|
+
* @param {string} appname - Application namespace
|
|
14
|
+
* @param {string} holonId - Source holon ID (must be H3)
|
|
15
|
+
* @param {string} lensName - Lens name
|
|
16
|
+
* @param {string} dataId - Data ID
|
|
17
|
+
* @param {Object} options - Upcast options
|
|
18
|
+
* @param {number} options.maxLevel - Maximum levels to propagate (default: 3)
|
|
19
|
+
* @param {string} options.operation - 'summarize', 'aggregate', or 'concatenate'
|
|
20
|
+
* @returns {Promise<boolean>} Success indicator
|
|
21
|
+
*/
|
|
22
|
+
export async function upcast(client, appname, holonId, lensName, dataId, options = {}) {
|
|
23
|
+
const { maxLevel = 3, operation = 'summarize' } = options;
|
|
24
|
+
|
|
25
|
+
// Validate H3 format
|
|
26
|
+
if (!isValidH3(holonId)) {
|
|
27
|
+
throw new Error('Upcast only supported for geographic (H3) holons, not noospheric holons');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Get parent holons
|
|
31
|
+
const parents = getParents(holonId);
|
|
32
|
+
const targetParents = parents.slice(0, maxLevel);
|
|
33
|
+
|
|
34
|
+
if (targetParents.length === 0) {
|
|
35
|
+
return true; // Already at root
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Create holograms in parent holons
|
|
39
|
+
const promises = targetParents.map(async (parentHolon) => {
|
|
40
|
+
return propagateToParent(client, appname, holonId, parentHolon, lensName, dataId, operation);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
await Promise.all(promises);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Propagate data to parent holon
|
|
49
|
+
* @private
|
|
50
|
+
*/
|
|
51
|
+
async function propagateToParent(
|
|
52
|
+
client,
|
|
53
|
+
appname,
|
|
54
|
+
sourceHolon,
|
|
55
|
+
parentHolon,
|
|
56
|
+
lensName,
|
|
57
|
+
dataId,
|
|
58
|
+
operation
|
|
59
|
+
) {
|
|
60
|
+
// Create hologram reference
|
|
61
|
+
const hologram = createHologram(sourceHolon, parentHolon, lensName, dataId, appname);
|
|
62
|
+
|
|
63
|
+
// Add operation metadata
|
|
64
|
+
hologram._meta.operation = operation;
|
|
65
|
+
hologram._meta.upcast = true;
|
|
66
|
+
|
|
67
|
+
// Write to parent holon
|
|
68
|
+
const path = `${appname}/${parentHolon}/${lensName}/${dataId}`;
|
|
69
|
+
return write(client, path, hologram);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Summarize operation (count aggregation)
|
|
74
|
+
* @param {Object[]} items - Data items
|
|
75
|
+
* @returns {Object} Summary object
|
|
76
|
+
*/
|
|
77
|
+
export function summarize(items) {
|
|
78
|
+
return {
|
|
79
|
+
operation: 'summarize',
|
|
80
|
+
count: items.length,
|
|
81
|
+
summary: true,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Aggregate operation (merge objects)
|
|
87
|
+
* @param {Object[]} items - Data items
|
|
88
|
+
* @returns {Object} Aggregated object
|
|
89
|
+
*/
|
|
90
|
+
export function aggregate(items) {
|
|
91
|
+
const result = { operation: 'aggregate' };
|
|
92
|
+
|
|
93
|
+
for (const item of items) {
|
|
94
|
+
Object.assign(result, item);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Concatenate operation (array merge)
|
|
102
|
+
* @param {Array[]} items - Array items
|
|
103
|
+
* @returns {Array} Concatenated array
|
|
104
|
+
*/
|
|
105
|
+
export function concatenate(items) {
|
|
106
|
+
return {
|
|
107
|
+
operation: 'concatenate',
|
|
108
|
+
items: items.flat(),
|
|
109
|
+
};
|
|
110
|
+
}
|