holosphere 1.1.20 → 2.0.0-alpha1

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.
Files changed (147) hide show
  1. package/.env.example +36 -0
  2. package/.eslintrc.json +16 -0
  3. package/.prettierrc.json +7 -0
  4. package/LICENSE +162 -38
  5. package/README.md +483 -367
  6. package/bin/holosphere-activitypub.js +158 -0
  7. package/cleanup-test-data.js +204 -0
  8. package/examples/demo.html +1333 -0
  9. package/examples/example-bot.js +197 -0
  10. package/package.json +47 -87
  11. package/scripts/check-bundle-size.js +54 -0
  12. package/scripts/check-quest-ids.js +77 -0
  13. package/scripts/import-holons.js +578 -0
  14. package/scripts/publish-to-relay.js +101 -0
  15. package/scripts/read-example.js +186 -0
  16. package/scripts/relay-diagnostic.js +59 -0
  17. package/scripts/relay-example.js +179 -0
  18. package/scripts/resync-to-relay.js +245 -0
  19. package/scripts/revert-import.js +196 -0
  20. package/scripts/test-hybrid-mode.js +108 -0
  21. package/scripts/test-local-storage.js +63 -0
  22. package/scripts/test-nostr-direct.js +55 -0
  23. package/scripts/test-read-data.js +45 -0
  24. package/scripts/test-write-read.js +63 -0
  25. package/scripts/verify-import.js +95 -0
  26. package/scripts/verify-relay-data.js +139 -0
  27. package/src/ai/aggregation.js +319 -0
  28. package/src/ai/breakdown.js +511 -0
  29. package/src/ai/classifier.js +217 -0
  30. package/src/ai/council.js +228 -0
  31. package/src/ai/embeddings.js +279 -0
  32. package/src/ai/federation-ai.js +324 -0
  33. package/src/ai/h3-ai.js +955 -0
  34. package/src/ai/index.js +112 -0
  35. package/src/ai/json-ops.js +225 -0
  36. package/src/ai/llm-service.js +205 -0
  37. package/src/ai/nl-query.js +223 -0
  38. package/src/ai/relationships.js +353 -0
  39. package/src/ai/schema-extractor.js +218 -0
  40. package/src/ai/spatial.js +293 -0
  41. package/src/ai/tts.js +194 -0
  42. package/src/content/social-protocols.js +168 -0
  43. package/src/core/holosphere.js +273 -0
  44. package/src/crypto/secp256k1.js +259 -0
  45. package/src/federation/discovery.js +334 -0
  46. package/src/federation/hologram.js +1042 -0
  47. package/src/federation/registry.js +386 -0
  48. package/src/hierarchical/upcast.js +110 -0
  49. package/src/index.js +2669 -0
  50. package/src/schema/validator.js +91 -0
  51. package/src/spatial/h3-operations.js +110 -0
  52. package/src/storage/backend-factory.js +125 -0
  53. package/src/storage/backend-interface.js +142 -0
  54. package/src/storage/backends/activitypub/server.js +653 -0
  55. package/src/storage/backends/activitypub-backend.js +272 -0
  56. package/src/storage/backends/gundb-backend.js +233 -0
  57. package/src/storage/backends/nostr-backend.js +136 -0
  58. package/src/storage/filesystem-storage-browser.js +41 -0
  59. package/src/storage/filesystem-storage.js +138 -0
  60. package/src/storage/global-tables.js +81 -0
  61. package/src/storage/gun-async.js +281 -0
  62. package/src/storage/gun-wrapper.js +221 -0
  63. package/src/storage/indexeddb-storage.js +122 -0
  64. package/src/storage/key-storage-simple.js +76 -0
  65. package/src/storage/key-storage.js +136 -0
  66. package/src/storage/memory-storage.js +59 -0
  67. package/src/storage/migration.js +338 -0
  68. package/src/storage/nostr-async.js +811 -0
  69. package/src/storage/nostr-client.js +939 -0
  70. package/src/storage/nostr-wrapper.js +211 -0
  71. package/src/storage/outbox-queue.js +208 -0
  72. package/src/storage/persistent-storage.js +109 -0
  73. package/src/storage/sync-service.js +164 -0
  74. package/src/subscriptions/manager.js +142 -0
  75. package/test-ai-real-api.js +202 -0
  76. package/tests/unit/ai/aggregation.test.js +295 -0
  77. package/tests/unit/ai/breakdown.test.js +446 -0
  78. package/tests/unit/ai/classifier.test.js +294 -0
  79. package/tests/unit/ai/council.test.js +262 -0
  80. package/tests/unit/ai/embeddings.test.js +384 -0
  81. package/tests/unit/ai/federation-ai.test.js +344 -0
  82. package/tests/unit/ai/h3-ai.test.js +458 -0
  83. package/tests/unit/ai/index.test.js +304 -0
  84. package/tests/unit/ai/json-ops.test.js +307 -0
  85. package/tests/unit/ai/llm-service.test.js +390 -0
  86. package/tests/unit/ai/nl-query.test.js +383 -0
  87. package/tests/unit/ai/relationships.test.js +311 -0
  88. package/tests/unit/ai/schema-extractor.test.js +384 -0
  89. package/tests/unit/ai/spatial.test.js +279 -0
  90. package/tests/unit/ai/tts.test.js +279 -0
  91. package/tests/unit/content.test.js +332 -0
  92. package/tests/unit/contract/core.test.js +88 -0
  93. package/tests/unit/contract/crypto.test.js +198 -0
  94. package/tests/unit/contract/data.test.js +223 -0
  95. package/tests/unit/contract/federation.test.js +181 -0
  96. package/tests/unit/contract/hierarchical.test.js +113 -0
  97. package/tests/unit/contract/schema.test.js +114 -0
  98. package/tests/unit/contract/social.test.js +217 -0
  99. package/tests/unit/contract/spatial.test.js +110 -0
  100. package/tests/unit/contract/subscriptions.test.js +128 -0
  101. package/tests/unit/contract/utils.test.js +159 -0
  102. package/tests/unit/core.test.js +152 -0
  103. package/tests/unit/crypto.test.js +328 -0
  104. package/tests/unit/federation.test.js +234 -0
  105. package/tests/unit/gun-async.test.js +252 -0
  106. package/tests/unit/hierarchical.test.js +399 -0
  107. package/tests/unit/integration/scenario-01-geographic-storage.test.js +74 -0
  108. package/tests/unit/integration/scenario-02-federation.test.js +76 -0
  109. package/tests/unit/integration/scenario-03-subscriptions.test.js +102 -0
  110. package/tests/unit/integration/scenario-04-validation.test.js +129 -0
  111. package/tests/unit/integration/scenario-05-hierarchy.test.js +125 -0
  112. package/tests/unit/integration/scenario-06-social.test.js +135 -0
  113. package/tests/unit/integration/scenario-07-persistence.test.js +130 -0
  114. package/tests/unit/integration/scenario-08-authorization.test.js +161 -0
  115. package/tests/unit/integration/scenario-09-cross-dimensional.test.js +139 -0
  116. package/tests/unit/integration/scenario-10-cross-holosphere-capabilities.test.js +357 -0
  117. package/tests/unit/integration/scenario-11-cross-holosphere-federation.test.js +410 -0
  118. package/tests/unit/integration/scenario-12-capability-federated-read.test.js +719 -0
  119. package/tests/unit/performance/benchmark.test.js +85 -0
  120. package/tests/unit/schema.test.js +213 -0
  121. package/tests/unit/spatial.test.js +158 -0
  122. package/tests/unit/storage.test.js +195 -0
  123. package/tests/unit/subscriptions.test.js +328 -0
  124. package/tests/unit/test-data-permanence-debug.js +197 -0
  125. package/tests/unit/test-data-permanence.js +340 -0
  126. package/tests/unit/test-key-persistence-fixed.js +148 -0
  127. package/tests/unit/test-key-persistence.js +172 -0
  128. package/tests/unit/test-relay-permanence.js +376 -0
  129. package/tests/unit/test-second-node.js +95 -0
  130. package/tests/unit/test-simple-write.js +89 -0
  131. package/vite.config.js +49 -0
  132. package/vitest.config.js +20 -0
  133. package/FEDERATION.md +0 -213
  134. package/compute.js +0 -298
  135. package/content.js +0 -980
  136. package/federation.js +0 -1234
  137. package/global.js +0 -736
  138. package/hexlib.js +0 -335
  139. package/hologram.js +0 -183
  140. package/holosphere-bundle.esm.js +0 -33256
  141. package/holosphere-bundle.js +0 -33287
  142. package/holosphere-bundle.min.js +0 -39
  143. package/holosphere.d.ts +0 -601
  144. package/holosphere.js +0 -719
  145. package/node.js +0 -246
  146. package/schema.js +0 -139
  147. package/utils.js +0 -302
@@ -0,0 +1,653 @@
1
+ /**
2
+ * Self-hosted ActivityPub server for HoloSphere
3
+ * Provides federation with Mastodon, Pleroma, and other ActivityPub servers
4
+ */
5
+
6
+ import express from 'express';
7
+ import crypto from 'crypto';
8
+ import { createPersistentStorage } from '../../persistent-storage.js';
9
+
10
+ const ACTIVITY_STREAMS_CONTEXT = 'https://www.w3.org/ns/activitystreams';
11
+ const SECURITY_CONTEXT = 'https://w3id.org/security/v1';
12
+
13
+ /**
14
+ * ActivityPub Server for HoloSphere
15
+ */
16
+ export class ActivityPubServer {
17
+ constructor(config = {}) {
18
+ this.config = {
19
+ port: config.port || 3000,
20
+ domain: config.domain || 'localhost',
21
+ dataDir: config.dataDir,
22
+ ...config,
23
+ };
24
+
25
+ this.app = express();
26
+ this.storage = null;
27
+ this.actors = new Map();
28
+ this.subscriptions = new Map();
29
+ this.server = null;
30
+
31
+ // Generate server key pair for signing
32
+ this.serverKeyPair = null;
33
+ }
34
+
35
+ async start() {
36
+ // Initialize storage
37
+ this.storage = await createPersistentStorage('activitypub', {
38
+ dataDir: this.config.dataDir,
39
+ });
40
+
41
+ // Generate or load server keys
42
+ await this._initializeKeys();
43
+
44
+ // Middleware
45
+ this.app.use(express.json({
46
+ type: ['application/json', 'application/activity+json', 'application/ld+json'],
47
+ }));
48
+
49
+ // CORS for federation
50
+ this.app.use((req, res, next) => {
51
+ res.header('Access-Control-Allow-Origin', '*');
52
+ res.header('Access-Control-Allow-Headers', 'Content-Type, Accept, Signature, Date, Digest');
53
+ res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
54
+ if (req.method === 'OPTIONS') {
55
+ return res.sendStatus(200);
56
+ }
57
+ next();
58
+ });
59
+
60
+ // WebFinger endpoint (required for federation discovery)
61
+ this.app.get('/.well-known/webfinger', this._handleWebFinger.bind(this));
62
+
63
+ // NodeInfo endpoints (optional but recommended)
64
+ this.app.get('/.well-known/nodeinfo', this._handleNodeInfoWellKnown.bind(this));
65
+ this.app.get('/nodeinfo/2.0', this._handleNodeInfo.bind(this));
66
+
67
+ // Actor endpoints
68
+ this.app.get('/actor/:name', this._handleGetActor.bind(this));
69
+ this.app.post('/actor/:name/inbox', this._handlePostInbox.bind(this));
70
+ this.app.get('/actor/:name/outbox', this._handleGetOutbox.bind(this));
71
+ this.app.post('/actor/:name/outbox', this._handlePostOutbox.bind(this));
72
+ this.app.get('/actor/:name/followers', this._handleGetFollowers.bind(this));
73
+ this.app.get('/actor/:name/following', this._handleGetFollowing.bind(this));
74
+
75
+ // Object endpoints (for HoloSphere data)
76
+ this.app.get('/objects/:appName/:holonId/:lensName', this._handleGetCollection.bind(this));
77
+ this.app.get('/objects/:appName/:holonId/:lensName/:key', this._handleGetObject.bind(this));
78
+
79
+ // API endpoints (for HoloSphere client)
80
+ this.app.post('/api/actors', this._handleCreateActor.bind(this));
81
+ this.app.post('/api/data', this._handleWriteData.bind(this));
82
+ this.app.get('/api/data/*', this._handleReadData.bind(this));
83
+ this.app.delete('/api/data/*', this._handleDeleteData.bind(this));
84
+
85
+ // SSE for real-time subscriptions
86
+ this.app.get('/subscribe', this._handleSubscribe.bind(this));
87
+
88
+ // Health check
89
+ this.app.get('/health', (req, res) => {
90
+ res.json({ status: 'ok', timestamp: Date.now() });
91
+ });
92
+
93
+ // Start server
94
+ return new Promise((resolve) => {
95
+ this.server = this.app.listen(this.config.port, () => {
96
+ console.log(`ActivityPub server listening on port ${this.config.port}`);
97
+ console.log(`Domain: ${this.config.domain}`);
98
+ resolve();
99
+ });
100
+ });
101
+ }
102
+
103
+ async _initializeKeys() {
104
+ // Try to load existing keys
105
+ const keysKey = '_server_keys';
106
+ const existingKeys = await this.storage.get(keysKey);
107
+
108
+ if (existingKeys) {
109
+ this.serverKeyPair = existingKeys;
110
+ } else {
111
+ // Generate new RSA key pair
112
+ const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
113
+ modulusLength: 2048,
114
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
115
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
116
+ });
117
+
118
+ this.serverKeyPair = { publicKey, privateKey };
119
+ await this.storage.put(keysKey, this.serverKeyPair);
120
+ }
121
+ }
122
+
123
+ // ============ WebFinger ============
124
+
125
+ _handleWebFinger(req, res) {
126
+ const resource = req.query.resource;
127
+ if (!resource) {
128
+ return res.status(400).json({ error: 'resource parameter required' });
129
+ }
130
+
131
+ // Parse acct:user@domain
132
+ const match = resource.match(/^acct:(.+)@(.+)$/);
133
+ if (!match) {
134
+ return res.status(400).json({ error: 'invalid resource format' });
135
+ }
136
+
137
+ const username = match[1];
138
+ const domain = match[2];
139
+
140
+ // Check if domain matches (allow localhost variants)
141
+ const validDomains = [this.config.domain, 'localhost', `localhost:${this.config.port}`];
142
+ if (!validDomains.includes(domain)) {
143
+ return res.status(404).json({ error: 'user not found' });
144
+ }
145
+
146
+ const baseUrl = this._getBaseUrl();
147
+
148
+ res.set('Content-Type', 'application/jrd+json');
149
+ res.json({
150
+ subject: resource,
151
+ aliases: [`${baseUrl}/actor/${username}`],
152
+ links: [
153
+ {
154
+ rel: 'self',
155
+ type: 'application/activity+json',
156
+ href: `${baseUrl}/actor/${username}`,
157
+ },
158
+ ],
159
+ });
160
+ }
161
+
162
+ // ============ NodeInfo ============
163
+
164
+ _handleNodeInfoWellKnown(req, res) {
165
+ const baseUrl = this._getBaseUrl();
166
+ res.json({
167
+ links: [
168
+ {
169
+ rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0',
170
+ href: `${baseUrl}/nodeinfo/2.0`,
171
+ },
172
+ ],
173
+ });
174
+ }
175
+
176
+ _handleNodeInfo(req, res) {
177
+ res.json({
178
+ version: '2.0',
179
+ software: {
180
+ name: 'holosphere-activitypub',
181
+ version: '1.0.0',
182
+ },
183
+ protocols: ['activitypub'],
184
+ usage: {
185
+ users: { total: this.actors.size },
186
+ localPosts: 0,
187
+ },
188
+ openRegistrations: true,
189
+ });
190
+ }
191
+
192
+ // ============ Actor Endpoints ============
193
+
194
+ async _handleGetActor(req, res) {
195
+ const { name } = req.params;
196
+ const baseUrl = this._getBaseUrl();
197
+
198
+ // Load or create actor
199
+ let actor = await this._getOrCreateActor(name);
200
+
201
+ const actorObject = {
202
+ '@context': [ACTIVITY_STREAMS_CONTEXT, SECURITY_CONTEXT],
203
+ id: `${baseUrl}/actor/${name}`,
204
+ type: 'Service',
205
+ preferredUsername: name,
206
+ name: `HoloSphere: ${name}`,
207
+ summary: `HoloSphere application: ${name}`,
208
+ inbox: `${baseUrl}/actor/${name}/inbox`,
209
+ outbox: `${baseUrl}/actor/${name}/outbox`,
210
+ followers: `${baseUrl}/actor/${name}/followers`,
211
+ following: `${baseUrl}/actor/${name}/following`,
212
+ publicKey: {
213
+ id: `${baseUrl}/actor/${name}#main-key`,
214
+ owner: `${baseUrl}/actor/${name}`,
215
+ publicKeyPem: actor.publicKey || this.serverKeyPair.publicKey,
216
+ },
217
+ };
218
+
219
+ res.set('Content-Type', 'application/activity+json');
220
+ res.json(actorObject);
221
+ }
222
+
223
+ async _handlePostInbox(req, res) {
224
+ const { name } = req.params;
225
+ const activity = req.body;
226
+
227
+ // TODO: Verify HTTP Signature for federation security
228
+ // For now, accept all activities
229
+
230
+ try {
231
+ await this._processIncomingActivity(name, activity);
232
+ res.status(202).send();
233
+ } catch (error) {
234
+ console.error('Inbox error:', error);
235
+ res.status(500).json({ error: error.message });
236
+ }
237
+ }
238
+
239
+ async _handleGetOutbox(req, res) {
240
+ const { name } = req.params;
241
+ const baseUrl = this._getBaseUrl();
242
+
243
+ // Get activities from storage
244
+ const activities = await this._getActorActivities(name);
245
+
246
+ const collection = {
247
+ '@context': ACTIVITY_STREAMS_CONTEXT,
248
+ id: `${baseUrl}/actor/${name}/outbox`,
249
+ type: 'OrderedCollection',
250
+ totalItems: activities.length,
251
+ orderedItems: activities,
252
+ };
253
+
254
+ res.set('Content-Type', 'application/activity+json');
255
+ res.json(collection);
256
+ }
257
+
258
+ async _handlePostOutbox(req, res) {
259
+ const { name } = req.params;
260
+ const activity = req.body;
261
+
262
+ // Verify authorization (API key or signature)
263
+ const apiKey = req.headers.authorization?.replace('Bearer ', '');
264
+ const actor = await this._getOrCreateActor(name);
265
+
266
+ if (actor.apiKey && actor.apiKey !== apiKey) {
267
+ return res.status(401).json({ error: 'unauthorized' });
268
+ }
269
+
270
+ try {
271
+ const result = await this._processOutgoingActivity(name, activity);
272
+ res.status(201).json(result);
273
+ } catch (error) {
274
+ console.error('Outbox error:', error);
275
+ res.status(500).json({ error: error.message });
276
+ }
277
+ }
278
+
279
+ async _handleGetFollowers(req, res) {
280
+ const { name } = req.params;
281
+ const baseUrl = this._getBaseUrl();
282
+
283
+ const actor = await this._getOrCreateActor(name);
284
+ const followers = actor.followers || [];
285
+
286
+ res.set('Content-Type', 'application/activity+json');
287
+ res.json({
288
+ '@context': ACTIVITY_STREAMS_CONTEXT,
289
+ id: `${baseUrl}/actor/${name}/followers`,
290
+ type: 'OrderedCollection',
291
+ totalItems: followers.length,
292
+ orderedItems: followers,
293
+ });
294
+ }
295
+
296
+ async _handleGetFollowing(req, res) {
297
+ const { name } = req.params;
298
+ const baseUrl = this._getBaseUrl();
299
+
300
+ const actor = await this._getOrCreateActor(name);
301
+ const following = actor.following || [];
302
+
303
+ res.set('Content-Type', 'application/activity+json');
304
+ res.json({
305
+ '@context': ACTIVITY_STREAMS_CONTEXT,
306
+ id: `${baseUrl}/actor/${name}/following`,
307
+ type: 'OrderedCollection',
308
+ totalItems: following.length,
309
+ orderedItems: following,
310
+ });
311
+ }
312
+
313
+ // ============ Object Endpoints ============
314
+
315
+ async _handleGetCollection(req, res) {
316
+ const { appName, holonId, lensName } = req.params;
317
+ const path = `${appName}/${holonId}/${lensName}`;
318
+ const baseUrl = this._getBaseUrl();
319
+
320
+ const items = await this._getDataByPrefix(path);
321
+
322
+ const collection = {
323
+ '@context': ACTIVITY_STREAMS_CONTEXT,
324
+ id: `${baseUrl}/objects/${path}`,
325
+ type: 'OrderedCollection',
326
+ totalItems: items.length,
327
+ orderedItems: items.map(item => this._dataToObject(item, path)),
328
+ };
329
+
330
+ res.set('Content-Type', 'application/activity+json');
331
+ res.json(collection);
332
+ }
333
+
334
+ async _handleGetObject(req, res) {
335
+ const { appName, holonId, lensName, key } = req.params;
336
+ const path = `${appName}/${holonId}/${lensName}/${key}`;
337
+
338
+ const data = await this.storage.get(`data:${path}`);
339
+ if (!data || data._deleted) {
340
+ return res.status(404).json({ error: 'not found' });
341
+ }
342
+
343
+ res.set('Content-Type', 'application/activity+json');
344
+ res.json(this._dataToObject(data, path));
345
+ }
346
+
347
+ // ============ API Endpoints ============
348
+
349
+ async _handleCreateActor(req, res) {
350
+ const { name, apiKey } = req.body;
351
+
352
+ if (!name) {
353
+ return res.status(400).json({ error: 'name required' });
354
+ }
355
+
356
+ const actor = await this._getOrCreateActor(name);
357
+ if (apiKey) {
358
+ actor.apiKey = apiKey;
359
+ await this.storage.put(`actor:${name}`, actor);
360
+ }
361
+
362
+ res.json({
363
+ name,
364
+ id: `${this._getBaseUrl()}/actor/${name}`,
365
+ publicKey: actor.publicKey,
366
+ });
367
+ }
368
+
369
+ async _handleWriteData(req, res) {
370
+ const { path, data, actorName } = req.body;
371
+
372
+ if (!path || !data) {
373
+ return res.status(400).json({ error: 'path and data required' });
374
+ }
375
+
376
+ // Store data
377
+ const record = {
378
+ ...data,
379
+ _meta: {
380
+ ...(data._meta || {}),
381
+ timestamp: Date.now(),
382
+ path,
383
+ },
384
+ };
385
+
386
+ await this.storage.put(`data:${path}`, record);
387
+
388
+ // Create activity
389
+ if (actorName) {
390
+ await this._createActivity(actorName, 'Create', record, path);
391
+ }
392
+
393
+ // Notify subscribers
394
+ this._notifySubscribers(path, record);
395
+
396
+ res.json({ success: true, path });
397
+ }
398
+
399
+ async _handleReadData(req, res) {
400
+ const path = req.params[0];
401
+
402
+ // Check if this is a prefix query
403
+ const isPrefix = path.split('/').length <= 3;
404
+
405
+ if (isPrefix) {
406
+ const items = await this._getDataByPrefix(path);
407
+ res.json(items);
408
+ } else {
409
+ const data = await this.storage.get(`data:${path}`);
410
+ if (!data || data._deleted) {
411
+ return res.status(404).json({ error: 'not found' });
412
+ }
413
+ res.json(data);
414
+ }
415
+ }
416
+
417
+ async _handleDeleteData(req, res) {
418
+ const path = req.params[0];
419
+ const actorName = req.query.actor;
420
+
421
+ // Tombstone delete
422
+ const existing = await this.storage.get(`data:${path}`);
423
+ if (existing) {
424
+ const tombstone = {
425
+ id: existing.id,
426
+ _deleted: true,
427
+ _deletedAt: Date.now(),
428
+ };
429
+ await this.storage.put(`data:${path}`, tombstone);
430
+
431
+ // Create delete activity
432
+ if (actorName) {
433
+ await this._createActivity(actorName, 'Delete', { id: path }, path);
434
+ }
435
+
436
+ // Notify subscribers
437
+ this._notifySubscribers(path, tombstone);
438
+ }
439
+
440
+ res.json({ success: true });
441
+ }
442
+
443
+ // ============ Subscriptions ============
444
+
445
+ _handleSubscribe(req, res) {
446
+ const path = req.query.path || '';
447
+
448
+ // Set up SSE
449
+ res.writeHead(200, {
450
+ 'Content-Type': 'text/event-stream',
451
+ 'Cache-Control': 'no-cache',
452
+ 'Connection': 'keep-alive',
453
+ });
454
+
455
+ // Send initial ping
456
+ res.write('event: ping\ndata: connected\n\n');
457
+
458
+ // Register subscription
459
+ const subId = crypto.randomUUID();
460
+ this.subscriptions.set(subId, { path, res });
461
+
462
+ // Clean up on close
463
+ req.on('close', () => {
464
+ this.subscriptions.delete(subId);
465
+ });
466
+ }
467
+
468
+ _notifySubscribers(path, data) {
469
+ for (const [id, sub] of this.subscriptions) {
470
+ if (path.startsWith(sub.path)) {
471
+ try {
472
+ sub.res.write(`event: update\ndata: ${JSON.stringify({ path, data })}\n\n`);
473
+ } catch (e) {
474
+ this.subscriptions.delete(id);
475
+ }
476
+ }
477
+ }
478
+ }
479
+
480
+ // ============ Helper Methods ============
481
+
482
+ _getBaseUrl() {
483
+ const protocol = this.config.protocol || 'http';
484
+ const port = this.config.port;
485
+ const domain = this.config.domain;
486
+
487
+ if (domain === 'localhost' || domain.includes(':')) {
488
+ return `${protocol}://${domain}`;
489
+ }
490
+ if (port === 80 || port === 443) {
491
+ return `${protocol}://${domain}`;
492
+ }
493
+ return `${protocol}://${domain}:${port}`;
494
+ }
495
+
496
+ async _getOrCreateActor(name) {
497
+ // Check cache
498
+ if (this.actors.has(name)) {
499
+ return this.actors.get(name);
500
+ }
501
+
502
+ // Check storage
503
+ let actor = await this.storage.get(`actor:${name}`);
504
+ if (!actor) {
505
+ // Create new actor
506
+ const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
507
+ modulusLength: 2048,
508
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
509
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
510
+ });
511
+
512
+ actor = {
513
+ name,
514
+ publicKey,
515
+ privateKey,
516
+ followers: [],
517
+ following: [],
518
+ createdAt: Date.now(),
519
+ };
520
+
521
+ await this.storage.put(`actor:${name}`, actor);
522
+ }
523
+
524
+ this.actors.set(name, actor);
525
+ return actor;
526
+ }
527
+
528
+ async _processIncomingActivity(actorName, activity) {
529
+ const type = activity.type;
530
+
531
+ switch (type) {
532
+ case 'Follow':
533
+ await this._handleFollow(actorName, activity);
534
+ break;
535
+ case 'Undo':
536
+ if (activity.object?.type === 'Follow') {
537
+ await this._handleUnfollow(actorName, activity);
538
+ }
539
+ break;
540
+ case 'Create':
541
+ case 'Update':
542
+ case 'Delete':
543
+ // Store federated content
544
+ await this._storeFederatedActivity(actorName, activity);
545
+ break;
546
+ }
547
+
548
+ // Store activity
549
+ await this.storage.put(`inbox:${actorName}:${Date.now()}`, activity);
550
+ }
551
+
552
+ async _processOutgoingActivity(actorName, activity) {
553
+ const baseUrl = this._getBaseUrl();
554
+
555
+ // Assign ID if not present
556
+ if (!activity.id) {
557
+ activity.id = `${baseUrl}/activities/${crypto.randomUUID()}`;
558
+ }
559
+
560
+ // Set actor if not present
561
+ if (!activity.actor) {
562
+ activity.actor = `${baseUrl}/actor/${actorName}`;
563
+ }
564
+
565
+ // Store activity
566
+ await this.storage.put(`outbox:${actorName}:${Date.now()}`, activity);
567
+
568
+ // Handle based on type
569
+ if (activity.type === 'Create' && activity.object?.['holosphere:data']) {
570
+ const data = activity.object['holosphere:data'];
571
+ const path = activity.object['holosphere:path'];
572
+ if (path) {
573
+ await this.storage.put(`data:${path}`, data);
574
+ this._notifySubscribers(path, data);
575
+ }
576
+ }
577
+
578
+ return activity;
579
+ }
580
+
581
+ async _handleFollow(actorName, activity) {
582
+ const actor = await this._getOrCreateActor(actorName);
583
+ const follower = activity.actor;
584
+
585
+ if (!actor.followers.includes(follower)) {
586
+ actor.followers.push(follower);
587
+ await this.storage.put(`actor:${actorName}`, actor);
588
+ }
589
+
590
+ // Send Accept response
591
+ // TODO: Implement federation delivery
592
+ }
593
+
594
+ async _handleUnfollow(actorName, activity) {
595
+ const actor = await this._getOrCreateActor(actorName);
596
+ const follower = activity.actor;
597
+
598
+ actor.followers = actor.followers.filter(f => f !== follower);
599
+ await this.storage.put(`actor:${actorName}`, actor);
600
+ }
601
+
602
+ async _storeFederatedActivity(actorName, activity) {
603
+ // Store federated content for potential use
604
+ const key = `federated:${actorName}:${Date.now()}`;
605
+ await this.storage.put(key, activity);
606
+ }
607
+
608
+ async _getActorActivities(actorName) {
609
+ const activities = await this.storage.getAll(`outbox:${actorName}:`);
610
+ return activities.sort((a, b) => (b.published || 0) - (a.published || 0));
611
+ }
612
+
613
+ async _getDataByPrefix(prefix) {
614
+ const all = await this.storage.getAll(`data:${prefix}`);
615
+ return all.filter(item => item && !item._deleted);
616
+ }
617
+
618
+ async _createActivity(actorName, type, data, path) {
619
+ const baseUrl = this._getBaseUrl();
620
+ const activity = {
621
+ '@context': ACTIVITY_STREAMS_CONTEXT,
622
+ id: `${baseUrl}/activities/${crypto.randomUUID()}`,
623
+ type,
624
+ actor: `${baseUrl}/actor/${actorName}`,
625
+ published: new Date().toISOString(),
626
+ object: this._dataToObject(data, path),
627
+ };
628
+
629
+ await this.storage.put(`outbox:${actorName}:${Date.now()}`, activity);
630
+ return activity;
631
+ }
632
+
633
+ _dataToObject(data, path) {
634
+ const baseUrl = this._getBaseUrl();
635
+ return {
636
+ '@context': ACTIVITY_STREAMS_CONTEXT,
637
+ id: `${baseUrl}/objects/${path}`,
638
+ type: 'Note',
639
+ content: JSON.stringify(data),
640
+ 'holosphere:data': data,
641
+ 'holosphere:path': path,
642
+ published: data._meta?.timestamp ? new Date(data._meta.timestamp).toISOString() : new Date().toISOString(),
643
+ };
644
+ }
645
+
646
+ stop() {
647
+ if (this.server) {
648
+ this.server.close();
649
+ }
650
+ }
651
+ }
652
+
653
+ export default ActivityPubServer;