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,272 @@
1
+ /**
2
+ * ActivityPub Storage Backend
3
+ * Client adapter for connecting to an ActivityPub server
4
+ */
5
+
6
+ import { StorageBackend } from '../backend-interface.js';
7
+
8
+ export class ActivityPubBackend extends StorageBackend {
9
+ constructor(config) {
10
+ super(config);
11
+ this.serverUrl = config.serverUrl;
12
+ this.actorName = config.appName || 'holosphere';
13
+ this.apiKey = config.apiKey;
14
+ this.actorId = null;
15
+ }
16
+
17
+ async init() {
18
+ if (!this.serverUrl) {
19
+ throw new Error('ActivityPub backend requires serverUrl in config');
20
+ }
21
+
22
+ // Ensure server URL doesn't have trailing slash
23
+ this.serverUrl = this.serverUrl.replace(/\/$/, '');
24
+
25
+ // Try to fetch or create actor
26
+ try {
27
+ const response = await fetch(`${this.serverUrl}/actor/${this.actorName}`, {
28
+ headers: { 'Accept': 'application/activity+json' },
29
+ });
30
+
31
+ if (response.ok) {
32
+ const actor = await response.json();
33
+ this.actorId = actor.id;
34
+ this.publicKey = actor.publicKey?.id || actor.id;
35
+ } else {
36
+ // Try to create actor
37
+ await this._createActor();
38
+ }
39
+ } catch (error) {
40
+ // Server might not be running yet, try to create actor
41
+ await this._createActor();
42
+ }
43
+ }
44
+
45
+ async _createActor() {
46
+ try {
47
+ const response = await this._fetch(`${this.serverUrl}/api/actors`, {
48
+ method: 'POST',
49
+ headers: { 'Content-Type': 'application/json' },
50
+ body: JSON.stringify({
51
+ name: this.actorName,
52
+ apiKey: this.apiKey,
53
+ }),
54
+ });
55
+
56
+ if (response.ok) {
57
+ const result = await response.json();
58
+ this.actorId = result.id;
59
+ this.publicKey = result.publicKey || result.id;
60
+ } else {
61
+ throw new Error(`Failed to create actor: ${response.status}`);
62
+ }
63
+ } catch (error) {
64
+ // If server is not available, set placeholder values
65
+ this.actorId = `${this.serverUrl}/actor/${this.actorName}`;
66
+ this.publicKey = this.actorId;
67
+ }
68
+ }
69
+
70
+ buildPath(appName, holonId, lensName, key = null) {
71
+ // Use same path format as other backends
72
+ const encodedHolon = encodeURIComponent(holonId);
73
+ const encodedLens = encodeURIComponent(lensName);
74
+
75
+ if (key) {
76
+ const encodedKey = encodeURIComponent(key);
77
+ return `${appName}/${encodedHolon}/${encodedLens}/${encodedKey}`;
78
+ }
79
+ return `${appName}/${encodedHolon}/${encodedLens}`;
80
+ }
81
+
82
+ async write(path, data, options = {}) {
83
+ const response = await this._fetch(`${this.serverUrl}/api/data`, {
84
+ method: 'POST',
85
+ headers: { 'Content-Type': 'application/json' },
86
+ body: JSON.stringify({
87
+ path,
88
+ data,
89
+ actorName: this.actorName,
90
+ }),
91
+ });
92
+
93
+ return response.ok;
94
+ }
95
+
96
+ async read(path, options = {}) {
97
+ const response = await this._fetch(`${this.serverUrl}/api/data/${path}`);
98
+
99
+ if (!response.ok) {
100
+ if (response.status === 404) return null;
101
+ throw new Error(`Read failed: ${response.status}`);
102
+ }
103
+
104
+ const data = await response.json();
105
+ return data._deleted ? null : data;
106
+ }
107
+
108
+ async readAll(path, options = {}) {
109
+ const response = await this._fetch(`${this.serverUrl}/api/data/${path}`);
110
+
111
+ if (!response.ok) {
112
+ if (response.status === 404) return [];
113
+ throw new Error(`ReadAll failed: ${response.status}`);
114
+ }
115
+
116
+ const data = await response.json();
117
+ return Array.isArray(data) ? data.filter(item => !item._deleted) : [data];
118
+ }
119
+
120
+ async update(path, updates) {
121
+ // Read existing, merge, write back
122
+ const existing = await this.read(path);
123
+ if (!existing) return false;
124
+
125
+ const merged = { ...existing, ...updates };
126
+ return this.write(path, merged);
127
+ }
128
+
129
+ async delete(path) {
130
+ const response = await this._fetch(
131
+ `${this.serverUrl}/api/data/${path}?actor=${this.actorName}`,
132
+ { method: 'DELETE' }
133
+ );
134
+
135
+ return response.ok;
136
+ }
137
+
138
+ async deleteAll(path) {
139
+ const items = await this.readAll(path);
140
+ let count = 0;
141
+
142
+ for (const item of items) {
143
+ if (item && item.id) {
144
+ await this.delete(`${path}/${item.id}`);
145
+ count++;
146
+ }
147
+ }
148
+
149
+ return { success: true, count };
150
+ }
151
+
152
+ async subscribe(path, callback, options = {}) {
153
+ // Use Server-Sent Events for real-time updates
154
+ const url = `${this.serverUrl}/subscribe?path=${encodeURIComponent(path)}`;
155
+
156
+ // Use EventSource if available (browser) or polyfill
157
+ let eventSource;
158
+
159
+ if (typeof EventSource !== 'undefined') {
160
+ eventSource = new EventSource(url);
161
+ } else {
162
+ // In Node.js, we need to use a different approach
163
+ // For now, return a polling-based subscription
164
+ return this._pollSubscribe(path, callback, options);
165
+ }
166
+
167
+ eventSource.addEventListener('update', (event) => {
168
+ try {
169
+ const { path: eventPath, data } = JSON.parse(event.data);
170
+ const key = eventPath.split('/').pop();
171
+ callback(data, key);
172
+ } catch (error) {
173
+ console.error('ActivityPub subscription error:', error);
174
+ }
175
+ });
176
+
177
+ eventSource.onerror = (error) => {
178
+ console.error('ActivityPub SSE error:', error);
179
+ };
180
+
181
+ return {
182
+ unsubscribe: () => eventSource.close(),
183
+ };
184
+ }
185
+
186
+ async _pollSubscribe(path, callback, options = {}) {
187
+ // Fallback polling for Node.js environments
188
+ const interval = options.pollInterval || 5000;
189
+ let lastTimestamp = Date.now();
190
+ let running = true;
191
+
192
+ const poll = async () => {
193
+ while (running) {
194
+ try {
195
+ const items = await this.readAll(path);
196
+ for (const item of items) {
197
+ if (item._meta?.timestamp > lastTimestamp) {
198
+ const key = item.id || item._meta?.path?.split('/').pop();
199
+ callback(item, key);
200
+ lastTimestamp = item._meta.timestamp;
201
+ }
202
+ }
203
+ } catch (error) {
204
+ console.error('Polling error:', error);
205
+ }
206
+
207
+ await new Promise(resolve => setTimeout(resolve, interval));
208
+ }
209
+ };
210
+
211
+ // Start polling
212
+ poll();
213
+
214
+ return {
215
+ unsubscribe: () => { running = false; },
216
+ };
217
+ }
218
+
219
+ async exportData(pathPrefix = '') {
220
+ const items = await this.readAll(pathPrefix || this.actorName);
221
+
222
+ return items.map(item => ({
223
+ path: item._meta?.path || `${pathPrefix}/${item.id}`,
224
+ data: item,
225
+ timestamp: item._meta?.timestamp || Date.now(),
226
+ }));
227
+ }
228
+
229
+ async importData(records, options = {}) {
230
+ const results = { success: 0, failed: 0, errors: [] };
231
+
232
+ for (const record of records) {
233
+ try {
234
+ await this.write(record.path, record.data);
235
+ results.success++;
236
+ } catch (error) {
237
+ results.failed++;
238
+ results.errors.push({ path: record.path, error: error.message });
239
+ }
240
+ }
241
+
242
+ return results;
243
+ }
244
+
245
+ async _fetch(url, options = {}) {
246
+ const headers = options.headers || {};
247
+
248
+ // Add authorization if we have an API key
249
+ if (this.apiKey) {
250
+ headers['Authorization'] = `Bearer ${this.apiKey}`;
251
+ }
252
+
253
+ return fetch(url, { ...options, headers });
254
+ }
255
+
256
+ close() {
257
+ // Nothing to close for HTTP client
258
+ }
259
+
260
+ getStatus() {
261
+ return {
262
+ type: 'activitypub',
263
+ serverUrl: this.serverUrl,
264
+ actorId: this.actorId,
265
+ actorName: this.actorName,
266
+ publicKey: this.publicKey,
267
+ connected: true,
268
+ };
269
+ }
270
+ }
271
+
272
+ export default ActivityPubBackend;
@@ -0,0 +1,233 @@
1
+ /**
2
+ * GunDB Storage Backend
3
+ * Wraps existing gun-wrapper.js as a StorageBackend
4
+ */
5
+
6
+ import { StorageBackend } from '../backend-interface.js';
7
+ import * as wrapper from '../gun-wrapper.js';
8
+ import { gunPromise, gunPut, gunMap, gunCollect } from '../gun-async.js';
9
+
10
+ export class GunDBBackend extends StorageBackend {
11
+ constructor(config) {
12
+ super(config);
13
+ this.gun = null;
14
+ this.keyPair = null;
15
+ }
16
+
17
+ async init() {
18
+ // Dynamically import Gun to avoid issues if not installed
19
+ let Gun;
20
+ try {
21
+ const gunModule = await import('gun');
22
+ Gun = gunModule.default || gunModule;
23
+ } catch (error) {
24
+ throw new Error(
25
+ 'GunDB backend requires the "gun" package. Install it with: npm install gun'
26
+ );
27
+ }
28
+
29
+ const gunConfig = {
30
+ peers: this.config.peers || [],
31
+ radisk: this.config.radisk !== false,
32
+ localStorage: this.config.localStorage !== false,
33
+ file: this.config.dataDir,
34
+ };
35
+
36
+ this.gun = Gun(gunConfig);
37
+
38
+ // Generate or use provided key pair using Gun's SEA
39
+ try {
40
+ const SEA = Gun.SEA;
41
+ if (SEA) {
42
+ if (this.config.privateKey) {
43
+ // Try to restore from existing key
44
+ this.keyPair = { priv: this.config.privateKey, pub: this.config.publicKey };
45
+ this.publicKey = this.config.publicKey || this.config.privateKey.substring(0, 32);
46
+ } else {
47
+ // Generate new key pair
48
+ this.keyPair = await SEA.pair();
49
+ this.publicKey = this.keyPair.pub;
50
+ }
51
+ } else {
52
+ // SEA not available, use simple identifier
53
+ this.publicKey = this.config.appName || 'gundb-user';
54
+ }
55
+ } catch (error) {
56
+ // SEA might not be loaded
57
+ this.publicKey = this.config.appName || 'gundb-user';
58
+ }
59
+ }
60
+
61
+ buildPath(appName, holonId, lensName, key = null) {
62
+ return wrapper.buildPath(appName, holonId, lensName, key);
63
+ }
64
+
65
+ async write(path, data, options = {}) {
66
+ return wrapper.write(this.gun, path, data);
67
+ }
68
+
69
+ async read(path, options = {}) {
70
+ return wrapper.read(this.gun, path);
71
+ }
72
+
73
+ async readAll(path, options = {}) {
74
+ return wrapper.readAll(this.gun, path);
75
+ }
76
+
77
+ async update(path, updates) {
78
+ return wrapper.update(this.gun, path, updates);
79
+ }
80
+
81
+ async delete(path) {
82
+ return wrapper.deleteData(this.gun, path);
83
+ }
84
+
85
+ async deleteAll(path) {
86
+ // Gun doesn't have a native deleteAll, so we iterate and delete
87
+ const items = await this.readAll(path);
88
+ let count = 0;
89
+
90
+ for (const item of items) {
91
+ if (item && item.id) {
92
+ const itemPath = `${path}/${item.id}`;
93
+ await wrapper.deleteData(this.gun, itemPath);
94
+ count++;
95
+ }
96
+ }
97
+
98
+ return { success: true, count };
99
+ }
100
+
101
+ async subscribe(path, callback, options = {}) {
102
+ // Determine if this is a prefix subscription (no key) or single item
103
+ const pathParts = path.split('/');
104
+ const isPrefix = pathParts.length <= 3;
105
+
106
+ if (isPrefix) {
107
+ // Subscribe to all items under this prefix
108
+ const ref = this.gun.get(path);
109
+ const handlers = [];
110
+
111
+ ref.map().on((data, key) => {
112
+ if (data && !key.startsWith('_') && !data._deleted) {
113
+ callback(data, key);
114
+ }
115
+ });
116
+
117
+ // Store reference for cleanup
118
+ handlers.push(ref);
119
+
120
+ return {
121
+ unsubscribe: () => {
122
+ handlers.forEach(h => {
123
+ try {
124
+ h.off();
125
+ } catch (e) {
126
+ // Ignore cleanup errors
127
+ }
128
+ });
129
+ },
130
+ };
131
+ } else {
132
+ // Subscribe to single item
133
+ const unsubFn = wrapper.subscribe(this.gun, path, callback);
134
+ return {
135
+ unsubscribe: typeof unsubFn === 'function' ? unsubFn : () => {},
136
+ };
137
+ }
138
+ }
139
+
140
+ async exportData(pathPrefix = '') {
141
+ const records = [];
142
+
143
+ // Get the app-level data
144
+ const appName = this.config.appName || 'holosphere';
145
+ const basePath = pathPrefix || appName;
146
+
147
+ try {
148
+ const items = await gunMap(this.gun.get(basePath), 2000);
149
+
150
+ for (const [key, data] of Object.entries(items)) {
151
+ if (!key.startsWith('_') && data && typeof data === 'object' && !data._deleted) {
152
+ // Recursively collect nested data
153
+ await this._collectRecords(basePath, key, data, records);
154
+ }
155
+ }
156
+ } catch (error) {
157
+ console.warn('Export data error:', error);
158
+ }
159
+
160
+ return records;
161
+ }
162
+
163
+ async _collectRecords(basePath, key, data, records) {
164
+ const currentPath = `${basePath}/${key}`;
165
+
166
+ if (data.id) {
167
+ // This is a leaf data node
168
+ records.push({
169
+ path: currentPath,
170
+ data: this._cleanGunData(data),
171
+ timestamp: data._meta?.timestamp || Date.now(),
172
+ });
173
+ } else {
174
+ // This might be a nested structure, explore further
175
+ try {
176
+ const nested = await gunMap(this.gun.get(currentPath), 500);
177
+ for (const [nestedKey, nestedData] of Object.entries(nested)) {
178
+ if (!nestedKey.startsWith('_') && nestedData) {
179
+ await this._collectRecords(currentPath, nestedKey, nestedData, records);
180
+ }
181
+ }
182
+ } catch (e) {
183
+ // Ignore nested exploration errors
184
+ }
185
+ }
186
+ }
187
+
188
+ _cleanGunData(data) {
189
+ // Remove Gun's internal metadata
190
+ const cleaned = { ...data };
191
+ delete cleaned['_'];
192
+ return cleaned;
193
+ }
194
+
195
+ async importData(records, options = {}) {
196
+ const results = { success: 0, failed: 0, errors: [] };
197
+
198
+ for (const record of records) {
199
+ try {
200
+ await wrapper.write(this.gun, record.path, record.data);
201
+ results.success++;
202
+ } catch (error) {
203
+ results.failed++;
204
+ results.errors.push({ path: record.path, error: error.message });
205
+ }
206
+ }
207
+
208
+ return results;
209
+ }
210
+
211
+ close() {
212
+ // Gun doesn't have explicit close, but we can try to clean up
213
+ if (this.gun) {
214
+ try {
215
+ // Attempt to close any connections
216
+ this.gun.off();
217
+ } catch (e) {
218
+ // Ignore cleanup errors
219
+ }
220
+ }
221
+ }
222
+
223
+ getStatus() {
224
+ return {
225
+ type: 'gundb',
226
+ publicKey: this.publicKey,
227
+ peers: this.config.peers || [],
228
+ connected: !!this.gun,
229
+ };
230
+ }
231
+ }
232
+
233
+ export default GunDBBackend;
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Nostr Storage Backend
3
+ * Wraps existing nostr-wrapper.js and nostr-client.js as a StorageBackend
4
+ */
5
+
6
+ import { StorageBackend } from '../backend-interface.js';
7
+ import { createClient } from '../nostr-client.js';
8
+ import * as wrapper from '../nostr-wrapper.js';
9
+
10
+ export class NostrBackend extends StorageBackend {
11
+ constructor(config) {
12
+ super(config);
13
+ this.client = null;
14
+ }
15
+
16
+ async init() {
17
+ this.client = createClient({
18
+ relays: this.config.relays || ['wss://relay.holons.io'],
19
+ privateKey: this.config.privateKey,
20
+ enableReconnect: this.config.enableReconnect !== false,
21
+ enablePing: this.config.enablePing !== false,
22
+ appName: this.config.appName,
23
+ radisk: this.config.radisk !== false,
24
+ persistence: this.config.persistence !== false,
25
+ dataDir: this.config.dataDir,
26
+ backgroundSync: this.config.backgroundSync,
27
+ syncInterval: this.config.syncInterval,
28
+ });
29
+
30
+ // Wait for client initialization
31
+ await this.client._initReady;
32
+ this.publicKey = this.client.publicKey;
33
+ }
34
+
35
+ buildPath(appName, holonId, lensName, key = null) {
36
+ return wrapper.buildPath(appName, holonId, lensName, key);
37
+ }
38
+
39
+ async write(path, data, options = {}) {
40
+ return wrapper.write(this.client, path, data);
41
+ }
42
+
43
+ async read(path, options = {}) {
44
+ return wrapper.read(this.client, path, options);
45
+ }
46
+
47
+ async readAll(path, options = {}) {
48
+ return wrapper.readAll(this.client, path, options);
49
+ }
50
+
51
+ async update(path, updates) {
52
+ return wrapper.update(this.client, path, updates);
53
+ }
54
+
55
+ async delete(path) {
56
+ return wrapper.deleteData(this.client, path);
57
+ }
58
+
59
+ async deleteAll(path) {
60
+ return wrapper.deleteAll(this.client, path);
61
+ }
62
+
63
+ async subscribe(path, callback, options = {}) {
64
+ return wrapper.subscribe(this.client, path, callback, options);
65
+ }
66
+
67
+ async exportData(pathPrefix = '') {
68
+ // Wait for client to be ready
69
+ await this.client._initReady;
70
+
71
+ // Query all events for this author
72
+ const events = await this.client.query({
73
+ kinds: [30000],
74
+ authors: [this.publicKey],
75
+ limit: 10000,
76
+ });
77
+
78
+ return events
79
+ .filter(e => {
80
+ const dTag = e.tags.find(t => t[0] === 'd');
81
+ if (!dTag) return false;
82
+ // Filter by prefix if provided
83
+ if (pathPrefix && !dTag[1].startsWith(pathPrefix)) return false;
84
+ return true;
85
+ })
86
+ .map(e => {
87
+ const dTag = e.tags.find(t => t[0] === 'd');
88
+ let data;
89
+ try {
90
+ data = JSON.parse(e.content);
91
+ } catch {
92
+ data = { content: e.content };
93
+ }
94
+ return {
95
+ path: dTag[1],
96
+ data,
97
+ timestamp: e.created_at * 1000,
98
+ author: e.pubkey,
99
+ };
100
+ })
101
+ .filter(record => !record.data._deleted); // Filter out deleted items
102
+ }
103
+
104
+ async importData(records, options = {}) {
105
+ const results = { success: 0, failed: 0, errors: [] };
106
+
107
+ for (const record of records) {
108
+ try {
109
+ await wrapper.write(this.client, record.path, record.data);
110
+ results.success++;
111
+ } catch (error) {
112
+ results.failed++;
113
+ results.errors.push({ path: record.path, error: error.message });
114
+ }
115
+ }
116
+
117
+ return results;
118
+ }
119
+
120
+ close() {
121
+ if (this.client) {
122
+ this.client.close();
123
+ }
124
+ }
125
+
126
+ getStatus() {
127
+ return {
128
+ type: 'nostr',
129
+ publicKey: this.publicKey,
130
+ relays: this.config.relays || [],
131
+ connected: !!this.client,
132
+ };
133
+ }
134
+ }
135
+
136
+ export default NostrBackend;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Browser stub for FileSystemStorage
3
+ * Throws an error if used in browser environment
4
+ */
5
+
6
+ import { PersistentStorage } from './persistent-storage.js';
7
+
8
+ export class FileSystemStorage extends PersistentStorage {
9
+ constructor(baseDir = null) {
10
+ super();
11
+ throw new Error('FileSystemStorage is not available in browser environments. Use IndexedDBStorage or MemoryStorage instead.');
12
+ }
13
+
14
+ async init(namespace) {
15
+ throw new Error('FileSystemStorage is not available in browser environments');
16
+ }
17
+
18
+ async put(key, event) {
19
+ throw new Error('FileSystemStorage is not available in browser environments');
20
+ }
21
+
22
+ async get(key) {
23
+ throw new Error('FileSystemStorage is not available in browser environments');
24
+ }
25
+
26
+ async getAll(prefix) {
27
+ throw new Error('FileSystemStorage is not available in browser environments');
28
+ }
29
+
30
+ async delete(key) {
31
+ throw new Error('FileSystemStorage is not available in browser environments');
32
+ }
33
+
34
+ async clear() {
35
+ throw new Error('FileSystemStorage is not available in browser environments');
36
+ }
37
+
38
+ async close() {
39
+ throw new Error('FileSystemStorage is not available in browser environments');
40
+ }
41
+ }