holosphere 1.1.19 → 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 +476 -531
- 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 -1022
- 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 -34549
- package/holosphere-bundle.js +0 -34580
- package/holosphere-bundle.min.js +0 -49
- package/holosphere.d.ts +0 -604
- package/holosphere.js +0 -739
- package/node.js +0 -246
- package/schema.js +0 -139
- package/utils.js +0 -302
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import HoloSphere, { matchScope, createHologram } from '../../../src/index.js';
|
|
3
|
+
|
|
4
|
+
describe('Integration: Scenario 11 - Cross-Holosphere Federation', () => {
|
|
5
|
+
// Two distinct private keys for two separate holospheres
|
|
6
|
+
const privateKeyA = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
|
|
7
|
+
const privateKeyB = 'fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210';
|
|
8
|
+
|
|
9
|
+
let holosphereA;
|
|
10
|
+
let holosphereB;
|
|
11
|
+
let publicKeyA;
|
|
12
|
+
let publicKeyB;
|
|
13
|
+
let testHolon;
|
|
14
|
+
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
// Create two independent holospheres with different private keys and app namespaces
|
|
17
|
+
// Different appNames ensure data isolation in local mode
|
|
18
|
+
holosphereA = new HoloSphere({
|
|
19
|
+
relays: [],
|
|
20
|
+
appName: 'scenario-11-A',
|
|
21
|
+
privateKey: privateKeyA,
|
|
22
|
+
logLevel: 'ERROR'
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
holosphereB = new HoloSphere({
|
|
26
|
+
relays: [],
|
|
27
|
+
appName: 'scenario-11-B',
|
|
28
|
+
privateKey: privateKeyB,
|
|
29
|
+
logLevel: 'ERROR'
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Get public keys
|
|
33
|
+
publicKeyA = holosphereA.client.publicKey;
|
|
34
|
+
publicKeyB = holosphereB.client.publicKey;
|
|
35
|
+
|
|
36
|
+
// Create a test holon (H3 cell)
|
|
37
|
+
testHolon = await holosphereA.toHolon(37.7749, -122.4194, 9);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterEach(async () => {
|
|
41
|
+
if (holosphereA?.client?.close) await holosphereA.client.close();
|
|
42
|
+
if (holosphereB?.client?.close) await holosphereB.client.close();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('Wildcard Scope Matching', () => {
|
|
46
|
+
it('should match exact scope', () => {
|
|
47
|
+
const tokenScope = { holonId: 'abc', lensName: 'quests' };
|
|
48
|
+
const requestedScope = { holonId: 'abc', lensName: 'quests' };
|
|
49
|
+
expect(matchScope(tokenScope, requestedScope)).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should match wildcard holonId', () => {
|
|
53
|
+
const tokenScope = { holonId: '*', lensName: 'quests' };
|
|
54
|
+
const requestedScope = { holonId: 'any-holon-id', lensName: 'quests' };
|
|
55
|
+
expect(matchScope(tokenScope, requestedScope)).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should match wildcard lensName', () => {
|
|
59
|
+
const tokenScope = { holonId: 'abc', lensName: '*' };
|
|
60
|
+
const requestedScope = { holonId: 'abc', lensName: 'any-lens' };
|
|
61
|
+
expect(matchScope(tokenScope, requestedScope)).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should match full wildcard scope', () => {
|
|
65
|
+
const tokenScope = { holonId: '*', lensName: '*' };
|
|
66
|
+
const requestedScope = { holonId: 'any-holon', lensName: 'any-lens' };
|
|
67
|
+
expect(matchScope(tokenScope, requestedScope)).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should match item-level scope', () => {
|
|
71
|
+
const tokenScope = { holonId: 'abc', lensName: 'quests', dataId: 'quest-001' };
|
|
72
|
+
const requestedScope = { holonId: 'abc', lensName: 'quests', dataId: 'quest-001' };
|
|
73
|
+
expect(matchScope(tokenScope, requestedScope)).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should match wildcard dataId', () => {
|
|
77
|
+
const tokenScope = { holonId: 'abc', lensName: 'quests', dataId: '*' };
|
|
78
|
+
const requestedScope = { holonId: 'abc', lensName: 'quests', dataId: 'any-id' };
|
|
79
|
+
expect(matchScope(tokenScope, requestedScope)).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should NOT match different holonId', () => {
|
|
83
|
+
const tokenScope = { holonId: 'abc', lensName: 'quests' };
|
|
84
|
+
const requestedScope = { holonId: 'xyz', lensName: 'quests' };
|
|
85
|
+
expect(matchScope(tokenScope, requestedScope)).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should NOT match different lensName', () => {
|
|
89
|
+
const tokenScope = { holonId: 'abc', lensName: 'quests' };
|
|
90
|
+
const requestedScope = { holonId: 'abc', lensName: 'events' };
|
|
91
|
+
expect(matchScope(tokenScope, requestedScope)).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should NOT match different dataId when specific', () => {
|
|
95
|
+
const tokenScope = { holonId: 'abc', lensName: 'quests', dataId: 'quest-001' };
|
|
96
|
+
const requestedScope = { holonId: 'abc', lensName: 'quests', dataId: 'quest-002' };
|
|
97
|
+
expect(matchScope(tokenScope, requestedScope)).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('Federation Registry', () => {
|
|
102
|
+
it('should add a federated partner', async () => {
|
|
103
|
+
await holosphereA.addFederatedHolosphere(publicKeyB, {
|
|
104
|
+
alias: 'Partner B'
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const partners = await holosphereA.getFederatedHolospheres();
|
|
108
|
+
expect(partners).toContain(publicKeyB);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should remove a federated partner', async () => {
|
|
112
|
+
await holosphereA.addFederatedHolosphere(publicKeyB);
|
|
113
|
+
await holosphereA.removeFederatedHolosphere(publicKeyB);
|
|
114
|
+
|
|
115
|
+
const partners = await holosphereA.getFederatedHolospheres();
|
|
116
|
+
expect(partners).not.toContain(publicKeyB);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should store inbound capabilities', async () => {
|
|
120
|
+
// A issues capability to B
|
|
121
|
+
const capabilityToken = await holosphereA.issueCapability(
|
|
122
|
+
['read'],
|
|
123
|
+
{ holonId: testHolon, lensName: 'shared' },
|
|
124
|
+
publicKeyB,
|
|
125
|
+
{ issuerKey: privateKeyA }
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// B stores the capability they received
|
|
129
|
+
await holosphereB.addFederatedHolosphere(publicKeyA);
|
|
130
|
+
await holosphereB.storeInboundCapability(publicKeyA, {
|
|
131
|
+
token: capabilityToken,
|
|
132
|
+
scope: { holonId: testHolon, lensName: 'shared' },
|
|
133
|
+
permissions: ['read'],
|
|
134
|
+
expires: Date.now() + 3600000
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const registry = await holosphereB.getFederationRegistry();
|
|
138
|
+
const partner = registry.federatedWith.find(p => p.pubKey === publicKeyA);
|
|
139
|
+
expect(partner).toBeDefined();
|
|
140
|
+
expect(partner.inboundCapabilities.length).toBeGreaterThan(0);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('Capability Verification with Wildcards', () => {
|
|
145
|
+
it('should verify wildcard capability for any holon', async () => {
|
|
146
|
+
// Issue wildcard capability
|
|
147
|
+
const token = await holosphereA.issueCapability(
|
|
148
|
+
['read'],
|
|
149
|
+
{ holonId: '*', lensName: 'events' },
|
|
150
|
+
publicKeyB,
|
|
151
|
+
{ issuerKey: privateKeyA }
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// Should be valid for any holon with 'events' lens
|
|
155
|
+
const holon1 = await holosphereA.toHolon(40.7128, -74.0060, 9);
|
|
156
|
+
const holon2 = await holosphereA.toHolon(51.5074, -0.1278, 9);
|
|
157
|
+
|
|
158
|
+
expect(await holosphereA.verifyCapability(token, 'read', { holonId: holon1, lensName: 'events' })).toBe(true);
|
|
159
|
+
expect(await holosphereA.verifyCapability(token, 'read', { holonId: holon2, lensName: 'events' })).toBe(true);
|
|
160
|
+
// But not for different lens
|
|
161
|
+
expect(await holosphereA.verifyCapability(token, 'read', { holonId: holon1, lensName: 'other' })).toBe(false);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should verify full wildcard capability for all data', async () => {
|
|
165
|
+
const token = await holosphereA.issueCapability(
|
|
166
|
+
['read', 'write'],
|
|
167
|
+
{ holonId: '*', lensName: '*' },
|
|
168
|
+
publicKeyB,
|
|
169
|
+
{ issuerKey: privateKeyA }
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const randomHolon = await holosphereA.toHolon(35.6762, 139.6503, 9);
|
|
173
|
+
|
|
174
|
+
expect(await holosphereA.verifyCapability(token, 'read', { holonId: randomHolon, lensName: 'anything' })).toBe(true);
|
|
175
|
+
expect(await holosphereA.verifyCapability(token, 'write', { holonId: randomHolon, lensName: 'something-else' })).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('Cross-Holosphere Hologram Creation', () => {
|
|
180
|
+
it('should create a hologram with embedded capability', () => {
|
|
181
|
+
const capabilityToken = 'mock-capability-token';
|
|
182
|
+
|
|
183
|
+
const hologram = createHologram(
|
|
184
|
+
testHolon, // sourceHolon
|
|
185
|
+
'target-holon', // targetHolon
|
|
186
|
+
'quests', // lensName
|
|
187
|
+
'quest-001', // dataId
|
|
188
|
+
'scenario-11', // appname
|
|
189
|
+
{
|
|
190
|
+
authorPubKey: publicKeyA,
|
|
191
|
+
capability: capabilityToken
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
expect(hologram.hologram).toBe(true);
|
|
196
|
+
expect(hologram.crossHolosphere).toBe(true);
|
|
197
|
+
expect(hologram.target.authorPubKey).toBe(publicKeyA);
|
|
198
|
+
expect(hologram.capability).toBe(capabilityToken);
|
|
199
|
+
expect(hologram._meta.sourcePubKey).toBe(publicKeyA);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should include correct target information', () => {
|
|
203
|
+
const hologram = createHologram(
|
|
204
|
+
testHolon,
|
|
205
|
+
'target-holon',
|
|
206
|
+
'events',
|
|
207
|
+
'event-001',
|
|
208
|
+
'my-app',
|
|
209
|
+
{ authorPubKey: publicKeyA }
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
expect(hologram.target.holonId).toBe(testHolon);
|
|
213
|
+
expect(hologram.target.lensName).toBe('events');
|
|
214
|
+
expect(hologram.target.dataId).toBe('event-001');
|
|
215
|
+
expect(hologram.target.appname).toBe('my-app');
|
|
216
|
+
expect(hologram.target.authorPubKey).toBe(publicKeyA);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe('Issue Capability for Federation', () => {
|
|
221
|
+
it('should issue capability and track in registry', async () => {
|
|
222
|
+
const token = await holosphereA.issueCapabilityForFederation(
|
|
223
|
+
publicKeyB,
|
|
224
|
+
{ holonId: testHolon, lensName: 'shared' },
|
|
225
|
+
['read'],
|
|
226
|
+
{ expiresIn: 3600000, trackInRegistry: true }
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
expect(typeof token).toBe('string');
|
|
230
|
+
|
|
231
|
+
const registry = await holosphereA.getFederationRegistry();
|
|
232
|
+
const partner = registry.federatedWith.find(p => p.pubKey === publicKeyB);
|
|
233
|
+
expect(partner).toBeDefined();
|
|
234
|
+
expect(partner.outboundCapabilities.length).toBeGreaterThan(0);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should issue capability without tracking when disabled', async () => {
|
|
238
|
+
const token = await holosphereA.issueCapabilityForFederation(
|
|
239
|
+
publicKeyB,
|
|
240
|
+
{ holonId: testHolon, lensName: 'private' },
|
|
241
|
+
['read'],
|
|
242
|
+
{ expiresIn: 3600000, trackInRegistry: false }
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
expect(typeof token).toBe('string');
|
|
246
|
+
|
|
247
|
+
// Should still be a valid token
|
|
248
|
+
const isValid = await holosphereA.verifyCapability(
|
|
249
|
+
token,
|
|
250
|
+
'read',
|
|
251
|
+
{ holonId: testHolon, lensName: 'private' }
|
|
252
|
+
);
|
|
253
|
+
expect(isValid).toBe(true);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe('Complete Cross-Holosphere Workflow', () => {
|
|
258
|
+
it('should complete manual federation workflow', async () => {
|
|
259
|
+
// === PHASE 1: A writes data ===
|
|
260
|
+
const secretData = {
|
|
261
|
+
id: 'secret-001',
|
|
262
|
+
title: 'Secret Quest',
|
|
263
|
+
description: 'Only visible with capability',
|
|
264
|
+
_creator: publicKeyA
|
|
265
|
+
};
|
|
266
|
+
await holosphereA.write(testHolon, 'quests', secretData);
|
|
267
|
+
|
|
268
|
+
// Verify A can read their own data
|
|
269
|
+
const aData = await holosphereA.read(testHolon, 'quests', 'secret-001');
|
|
270
|
+
expect(aData).toBeDefined();
|
|
271
|
+
expect(aData.title).toBe('Secret Quest');
|
|
272
|
+
|
|
273
|
+
// B cannot see A's data (different holosphere)
|
|
274
|
+
const bData = await holosphereB.read(testHolon, 'quests', 'secret-001');
|
|
275
|
+
expect(bData).toBeNull();
|
|
276
|
+
|
|
277
|
+
// === PHASE 2: A issues capability to B ===
|
|
278
|
+
const capabilityToken = await holosphereA.issueCapabilityForFederation(
|
|
279
|
+
publicKeyB,
|
|
280
|
+
{ holonId: testHolon, lensName: 'quests' },
|
|
281
|
+
['read'],
|
|
282
|
+
{ expiresIn: 3600000 }
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
expect(typeof capabilityToken).toBe('string');
|
|
286
|
+
|
|
287
|
+
// === PHASE 3: B stores the capability ===
|
|
288
|
+
await holosphereB.addFederatedHolosphere(publicKeyA, {
|
|
289
|
+
alias: 'Holosphere A'
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
await holosphereB.storeInboundCapability(publicKeyA, {
|
|
293
|
+
token: capabilityToken,
|
|
294
|
+
scope: { holonId: testHolon, lensName: 'quests' },
|
|
295
|
+
permissions: ['read'],
|
|
296
|
+
expires: Date.now() + 3600000
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// === PHASE 4: Verify B has access ===
|
|
300
|
+
const isValid = await holosphereB.verifyCapability(
|
|
301
|
+
capabilityToken,
|
|
302
|
+
'read',
|
|
303
|
+
{ holonId: testHolon, lensName: 'quests' }
|
|
304
|
+
);
|
|
305
|
+
expect(isValid).toBe(true);
|
|
306
|
+
|
|
307
|
+
// === PHASE 5: Verify capability is in B's registry ===
|
|
308
|
+
const bRegistry = await holosphereB.getFederationRegistry();
|
|
309
|
+
const partnerA = bRegistry.federatedWith.find(p => p.pubKey === publicKeyA);
|
|
310
|
+
expect(partnerA).toBeDefined();
|
|
311
|
+
expect(partnerA.inboundCapabilities.length).toBeGreaterThan(0);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('should reject operations without valid capability', async () => {
|
|
315
|
+
// A writes data
|
|
316
|
+
await holosphereA.write(testHolon, 'protected', {
|
|
317
|
+
id: 'protected-001',
|
|
318
|
+
content: 'This is protected data'
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// B adds A as partner but without capability
|
|
322
|
+
await holosphereB.addFederatedHolosphere(publicKeyA);
|
|
323
|
+
|
|
324
|
+
// B should not be able to read from A (no capability)
|
|
325
|
+
await expect(
|
|
326
|
+
holosphereB.readFromFederatedSource(publicKeyA, testHolon, 'protected', 'protected-001')
|
|
327
|
+
).rejects.toThrow('No valid capability for federated source');
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('should reject expired capabilities', async () => {
|
|
331
|
+
// Issue already expired capability
|
|
332
|
+
const expiredToken = await holosphereA.issueCapability(
|
|
333
|
+
['read'],
|
|
334
|
+
{ holonId: testHolon, lensName: 'temp' },
|
|
335
|
+
publicKeyB,
|
|
336
|
+
{ expiresIn: -1000, issuerKey: privateKeyA } // Already expired
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
await holosphereB.addFederatedHolosphere(publicKeyA);
|
|
340
|
+
await holosphereB.storeInboundCapability(publicKeyA, {
|
|
341
|
+
token: expiredToken,
|
|
342
|
+
scope: { holonId: testHolon, lensName: 'temp' },
|
|
343
|
+
permissions: ['read'],
|
|
344
|
+
expires: Date.now() - 1000 // Already expired
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// Verification should fail
|
|
348
|
+
const isValid = await holosphereB.verifyCapability(
|
|
349
|
+
expiredToken,
|
|
350
|
+
'read',
|
|
351
|
+
{ holonId: testHolon, lensName: 'temp' }
|
|
352
|
+
);
|
|
353
|
+
expect(isValid).toBe(false);
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
describe('Scope Boundary Enforcement', () => {
|
|
358
|
+
it('should enforce holon boundary', async () => {
|
|
359
|
+
const holon1 = await holosphereA.toHolon(40.7128, -74.0060, 9);
|
|
360
|
+
const holon2 = await holosphereA.toHolon(51.5074, -0.1278, 9);
|
|
361
|
+
|
|
362
|
+
// Issue capability only for holon1
|
|
363
|
+
const token = await holosphereA.issueCapability(
|
|
364
|
+
['read'],
|
|
365
|
+
{ holonId: holon1, lensName: 'events' },
|
|
366
|
+
publicKeyB,
|
|
367
|
+
{ issuerKey: privateKeyA }
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
// Valid for holon1
|
|
371
|
+
expect(await holosphereA.verifyCapability(token, 'read', { holonId: holon1, lensName: 'events' })).toBe(true);
|
|
372
|
+
|
|
373
|
+
// Invalid for holon2
|
|
374
|
+
expect(await holosphereA.verifyCapability(token, 'read', { holonId: holon2, lensName: 'events' })).toBe(false);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('should enforce lens boundary', async () => {
|
|
378
|
+
const token = await holosphereA.issueCapability(
|
|
379
|
+
['read'],
|
|
380
|
+
{ holonId: testHolon, lensName: 'quests' },
|
|
381
|
+
publicKeyB,
|
|
382
|
+
{ issuerKey: privateKeyA }
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
// Valid for 'quests' lens
|
|
386
|
+
expect(await holosphereA.verifyCapability(token, 'read', { holonId: testHolon, lensName: 'quests' })).toBe(true);
|
|
387
|
+
|
|
388
|
+
// Invalid for 'events' lens
|
|
389
|
+
expect(await holosphereA.verifyCapability(token, 'read', { holonId: testHolon, lensName: 'events' })).toBe(false);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('should enforce permission boundary', async () => {
|
|
393
|
+
const readOnlyToken = await holosphereA.issueCapability(
|
|
394
|
+
['read'],
|
|
395
|
+
{ holonId: testHolon, lensName: 'docs' },
|
|
396
|
+
publicKeyB,
|
|
397
|
+
{ issuerKey: privateKeyA }
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
// Has read permission
|
|
401
|
+
expect(await holosphereA.verifyCapability(readOnlyToken, 'read', { holonId: testHolon, lensName: 'docs' })).toBe(true);
|
|
402
|
+
|
|
403
|
+
// Does NOT have write permission
|
|
404
|
+
expect(await holosphereA.verifyCapability(readOnlyToken, 'write', { holonId: testHolon, lensName: 'docs' })).toBe(false);
|
|
405
|
+
|
|
406
|
+
// Does NOT have delete permission
|
|
407
|
+
expect(await holosphereA.verifyCapability(readOnlyToken, 'delete', { holonId: testHolon, lensName: 'docs' })).toBe(false);
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
});
|