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.
Files changed (146) hide show
  1. package/.env.example +36 -0
  2. package/.eslintrc.json +16 -0
  3. package/.prettierrc.json +7 -0
  4. package/README.md +483 -367
  5. package/bin/holosphere-activitypub.js +158 -0
  6. package/cleanup-test-data.js +204 -0
  7. package/examples/demo.html +1333 -0
  8. package/examples/example-bot.js +197 -0
  9. package/package.json +47 -87
  10. package/scripts/check-bundle-size.js +54 -0
  11. package/scripts/check-quest-ids.js +77 -0
  12. package/scripts/import-holons.js +578 -0
  13. package/scripts/publish-to-relay.js +101 -0
  14. package/scripts/read-example.js +186 -0
  15. package/scripts/relay-diagnostic.js +59 -0
  16. package/scripts/relay-example.js +179 -0
  17. package/scripts/resync-to-relay.js +245 -0
  18. package/scripts/revert-import.js +196 -0
  19. package/scripts/test-hybrid-mode.js +108 -0
  20. package/scripts/test-local-storage.js +63 -0
  21. package/scripts/test-nostr-direct.js +55 -0
  22. package/scripts/test-read-data.js +45 -0
  23. package/scripts/test-write-read.js +63 -0
  24. package/scripts/verify-import.js +95 -0
  25. package/scripts/verify-relay-data.js +139 -0
  26. package/src/ai/aggregation.js +319 -0
  27. package/src/ai/breakdown.js +511 -0
  28. package/src/ai/classifier.js +217 -0
  29. package/src/ai/council.js +228 -0
  30. package/src/ai/embeddings.js +279 -0
  31. package/src/ai/federation-ai.js +324 -0
  32. package/src/ai/h3-ai.js +955 -0
  33. package/src/ai/index.js +112 -0
  34. package/src/ai/json-ops.js +225 -0
  35. package/src/ai/llm-service.js +205 -0
  36. package/src/ai/nl-query.js +223 -0
  37. package/src/ai/relationships.js +353 -0
  38. package/src/ai/schema-extractor.js +218 -0
  39. package/src/ai/spatial.js +293 -0
  40. package/src/ai/tts.js +194 -0
  41. package/src/content/social-protocols.js +168 -0
  42. package/src/core/holosphere.js +273 -0
  43. package/src/crypto/secp256k1.js +259 -0
  44. package/src/federation/discovery.js +334 -0
  45. package/src/federation/hologram.js +1042 -0
  46. package/src/federation/registry.js +386 -0
  47. package/src/hierarchical/upcast.js +110 -0
  48. package/src/index.js +2669 -0
  49. package/src/schema/validator.js +91 -0
  50. package/src/spatial/h3-operations.js +110 -0
  51. package/src/storage/backend-factory.js +125 -0
  52. package/src/storage/backend-interface.js +142 -0
  53. package/src/storage/backends/activitypub/server.js +653 -0
  54. package/src/storage/backends/activitypub-backend.js +272 -0
  55. package/src/storage/backends/gundb-backend.js +233 -0
  56. package/src/storage/backends/nostr-backend.js +136 -0
  57. package/src/storage/filesystem-storage-browser.js +41 -0
  58. package/src/storage/filesystem-storage.js +138 -0
  59. package/src/storage/global-tables.js +81 -0
  60. package/src/storage/gun-async.js +281 -0
  61. package/src/storage/gun-wrapper.js +221 -0
  62. package/src/storage/indexeddb-storage.js +122 -0
  63. package/src/storage/key-storage-simple.js +76 -0
  64. package/src/storage/key-storage.js +136 -0
  65. package/src/storage/memory-storage.js +59 -0
  66. package/src/storage/migration.js +338 -0
  67. package/src/storage/nostr-async.js +811 -0
  68. package/src/storage/nostr-client.js +939 -0
  69. package/src/storage/nostr-wrapper.js +211 -0
  70. package/src/storage/outbox-queue.js +208 -0
  71. package/src/storage/persistent-storage.js +109 -0
  72. package/src/storage/sync-service.js +164 -0
  73. package/src/subscriptions/manager.js +142 -0
  74. package/test-ai-real-api.js +202 -0
  75. package/tests/unit/ai/aggregation.test.js +295 -0
  76. package/tests/unit/ai/breakdown.test.js +446 -0
  77. package/tests/unit/ai/classifier.test.js +294 -0
  78. package/tests/unit/ai/council.test.js +262 -0
  79. package/tests/unit/ai/embeddings.test.js +384 -0
  80. package/tests/unit/ai/federation-ai.test.js +344 -0
  81. package/tests/unit/ai/h3-ai.test.js +458 -0
  82. package/tests/unit/ai/index.test.js +304 -0
  83. package/tests/unit/ai/json-ops.test.js +307 -0
  84. package/tests/unit/ai/llm-service.test.js +390 -0
  85. package/tests/unit/ai/nl-query.test.js +383 -0
  86. package/tests/unit/ai/relationships.test.js +311 -0
  87. package/tests/unit/ai/schema-extractor.test.js +384 -0
  88. package/tests/unit/ai/spatial.test.js +279 -0
  89. package/tests/unit/ai/tts.test.js +279 -0
  90. package/tests/unit/content.test.js +332 -0
  91. package/tests/unit/contract/core.test.js +88 -0
  92. package/tests/unit/contract/crypto.test.js +198 -0
  93. package/tests/unit/contract/data.test.js +223 -0
  94. package/tests/unit/contract/federation.test.js +181 -0
  95. package/tests/unit/contract/hierarchical.test.js +113 -0
  96. package/tests/unit/contract/schema.test.js +114 -0
  97. package/tests/unit/contract/social.test.js +217 -0
  98. package/tests/unit/contract/spatial.test.js +110 -0
  99. package/tests/unit/contract/subscriptions.test.js +128 -0
  100. package/tests/unit/contract/utils.test.js +159 -0
  101. package/tests/unit/core.test.js +152 -0
  102. package/tests/unit/crypto.test.js +328 -0
  103. package/tests/unit/federation.test.js +234 -0
  104. package/tests/unit/gun-async.test.js +252 -0
  105. package/tests/unit/hierarchical.test.js +399 -0
  106. package/tests/unit/integration/scenario-01-geographic-storage.test.js +74 -0
  107. package/tests/unit/integration/scenario-02-federation.test.js +76 -0
  108. package/tests/unit/integration/scenario-03-subscriptions.test.js +102 -0
  109. package/tests/unit/integration/scenario-04-validation.test.js +129 -0
  110. package/tests/unit/integration/scenario-05-hierarchy.test.js +125 -0
  111. package/tests/unit/integration/scenario-06-social.test.js +135 -0
  112. package/tests/unit/integration/scenario-07-persistence.test.js +130 -0
  113. package/tests/unit/integration/scenario-08-authorization.test.js +161 -0
  114. package/tests/unit/integration/scenario-09-cross-dimensional.test.js +139 -0
  115. package/tests/unit/integration/scenario-10-cross-holosphere-capabilities.test.js +357 -0
  116. package/tests/unit/integration/scenario-11-cross-holosphere-federation.test.js +410 -0
  117. package/tests/unit/integration/scenario-12-capability-federated-read.test.js +719 -0
  118. package/tests/unit/performance/benchmark.test.js +85 -0
  119. package/tests/unit/schema.test.js +213 -0
  120. package/tests/unit/spatial.test.js +158 -0
  121. package/tests/unit/storage.test.js +195 -0
  122. package/tests/unit/subscriptions.test.js +328 -0
  123. package/tests/unit/test-data-permanence-debug.js +197 -0
  124. package/tests/unit/test-data-permanence.js +340 -0
  125. package/tests/unit/test-key-persistence-fixed.js +148 -0
  126. package/tests/unit/test-key-persistence.js +172 -0
  127. package/tests/unit/test-relay-permanence.js +376 -0
  128. package/tests/unit/test-second-node.js +95 -0
  129. package/tests/unit/test-simple-write.js +89 -0
  130. package/vite.config.js +49 -0
  131. package/vitest.config.js +20 -0
  132. package/FEDERATION.md +0 -213
  133. package/compute.js +0 -298
  134. package/content.js +0 -980
  135. package/federation.js +0 -1234
  136. package/global.js +0 -736
  137. package/hexlib.js +0 -335
  138. package/hologram.js +0 -183
  139. package/holosphere-bundle.esm.js +0 -33256
  140. package/holosphere-bundle.js +0 -33287
  141. package/holosphere-bundle.min.js +0 -39
  142. package/holosphere.d.ts +0 -601
  143. package/holosphere.js +0 -719
  144. package/node.js +0 -246
  145. package/schema.js +0 -139
  146. package/utils.js +0 -302
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Social Protocol Adapters (Nostr, ActivityPub)
3
+ */
4
+
5
+ // Import ValidationError from the validator module
6
+ import { ValidationError } from '../schema/validator.js';
7
+
8
+ /**
9
+ * Validate Nostr NIP-01 event format
10
+ * @param {Object} event - Nostr event
11
+ * @param {boolean} partial - Allow partial events (missing id, pubkey, sig)
12
+ * @returns {boolean} True if valid
13
+ * @throws {ValidationError} If strict validation fails
14
+ */
15
+ export function validateNostrEvent(event, partial = true, throwOnError = false) {
16
+ if (!event || typeof event !== 'object') {
17
+ if (throwOnError) throw new ValidationError('ValidationError: Invalid Nostr event format');
18
+ return false;
19
+ }
20
+
21
+ // Required fields always
22
+ if (typeof event.kind !== 'number') {
23
+ if (throwOnError) throw new ValidationError('ValidationError: Invalid Nostr event format - kind must be a number');
24
+ return false;
25
+ }
26
+ if (!Array.isArray(event.tags)) {
27
+ if (throwOnError) throw new ValidationError('ValidationError: Invalid Nostr event format - tags must be an array');
28
+ return false;
29
+ }
30
+ if (typeof event.content !== 'string') {
31
+ if (throwOnError) throw new ValidationError('ValidationError: Invalid Nostr event format - content must be a string');
32
+ return false;
33
+ }
34
+ if (typeof event.created_at !== 'number') {
35
+ if (throwOnError) throw new ValidationError('ValidationError: Invalid Nostr event format - created_at must be a number');
36
+ return false;
37
+ }
38
+
39
+ // Optional for partial events (will be added by system)
40
+ if (!partial) {
41
+ if (typeof event.id !== 'string') {
42
+ if (throwOnError) throw new ValidationError('ValidationError: Invalid Nostr event format - id must be a string');
43
+ return false;
44
+ }
45
+ if (typeof event.pubkey !== 'string') {
46
+ if (throwOnError) throw new ValidationError('ValidationError: Invalid Nostr event format - pubkey must be a string');
47
+ return false;
48
+ }
49
+ if (typeof event.sig !== 'string') {
50
+ if (throwOnError) throw new ValidationError('ValidationError: Invalid Nostr event format - sig must be a string');
51
+ return false;
52
+ }
53
+ }
54
+
55
+ return true;
56
+ }
57
+
58
+ /**
59
+ * Validate ActivityPub object format
60
+ * @param {Object} object - ActivityPub object
61
+ * @param {boolean} throwOnError - Throw ValidationError on failure
62
+ * @param {boolean} requireId - Require id field (default: false, will be auto-generated)
63
+ * @returns {boolean} True if valid
64
+ * @throws {ValidationError} If strict validation fails
65
+ */
66
+ export function validateActivityPubObject(object, throwOnError = false, requireId = false) {
67
+ if (!object || typeof object !== 'object') {
68
+ if (throwOnError) throw new ValidationError('ValidationError: Invalid ActivityPub object format');
69
+ return false;
70
+ }
71
+ if (!object['@context']) {
72
+ if (throwOnError) throw new ValidationError('ValidationError: Invalid ActivityPub object format - @context is required');
73
+ return false;
74
+ }
75
+ if (typeof object.type !== 'string') {
76
+ if (throwOnError) throw new ValidationError('ValidationError: Invalid ActivityPub object format - type must be a string');
77
+ return false;
78
+ }
79
+ // ID is optional - will be auto-generated if not provided
80
+ if (requireId && typeof object.id !== 'string') {
81
+ if (throwOnError) throw new ValidationError('ValidationError: Invalid ActivityPub object format - id must be a string');
82
+ return false;
83
+ }
84
+ return true;
85
+ }
86
+
87
+ /**
88
+ * Transform Nostr event to HoloSphere format
89
+ * @param {Object} event - Nostr event
90
+ * @returns {Object} HoloSphere data object
91
+ */
92
+ export function transformNostrEvent(event) {
93
+ // Generate missing fields if needed
94
+ const id = event.id || `nostr-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
95
+ const pubkey = event.pubkey || 'anonymous';
96
+ const sig = event.sig || '';
97
+
98
+ return {
99
+ id,
100
+ protocol: 'nostr',
101
+ originalFormat: 'nip-01',
102
+ pubkey,
103
+ sig, // Keep original field name for compatibility
104
+ created_at: event.created_at,
105
+ kind: event.kind,
106
+ // Gun doesn't support arrays, so serialize as JSON string
107
+ tags: JSON.stringify(event.tags),
108
+ content: event.content,
109
+ signature: sig, // Also include as signature for clarity
110
+ _meta: {
111
+ timestamp: event.created_at * 1000,
112
+ protocol: 'nostr',
113
+ },
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Transform ActivityPub object to HoloSphere format
119
+ * @param {Object} object - ActivityPub object
120
+ * @returns {Object} HoloSphere data object
121
+ */
122
+ export function transformActivityPubObject(object) {
123
+ // Generate ID if not provided
124
+ const id = object.id || `ap-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
125
+
126
+ return {
127
+ id,
128
+ protocol: 'activitypub',
129
+ type: object.type,
130
+ actor: object.actor,
131
+ published: object.published,
132
+ content: object.content || object.name,
133
+ originalObject: { ...object, id }, // Add generated ID to original object
134
+ _meta: {
135
+ timestamp: object.published ? new Date(object.published).getTime() : Date.now(),
136
+ protocol: 'activitypub',
137
+ },
138
+ };
139
+ }
140
+
141
+ /**
142
+ * Filter social content by access level
143
+ * @param {Object[]} content - Social content array
144
+ * @param {string} accessLevel - Access level filter
145
+ * @returns {Object[]} Filtered content
146
+ */
147
+ export function filterByAccessLevel(content, accessLevel) {
148
+ if (!accessLevel) return content;
149
+
150
+ return content.filter((item) => {
151
+ return !item.accessLevel || item.accessLevel === accessLevel;
152
+ });
153
+ }
154
+
155
+ /**
156
+ * Filter social content by protocol
157
+ * @param {Object[]} content - Social content array
158
+ * @param {string} protocol - Protocol filter ('nostr' or 'activitypub')
159
+ * @returns {Object[]} Filtered content
160
+ */
161
+ export function filterByProtocol(content, protocol) {
162
+ if (!protocol || protocol === 'all') return content;
163
+ if (!Array.isArray(content)) return [];
164
+
165
+ return content.filter((item) => {
166
+ return item.protocol === protocol;
167
+ });
168
+ }
@@ -0,0 +1,273 @@
1
+ import { createClient } from '../storage/nostr-client.js';
2
+ import { BackendFactory } from '../storage/backend-factory.js';
3
+ import pkg from '../../package.json' with { type: 'json' };
4
+
5
+ /**
6
+ * Get environment variable (works in Node.js, returns undefined in browser)
7
+ * @param {string} name - Environment variable name
8
+ * @returns {string|undefined}
9
+ */
10
+ function getEnv(name) {
11
+ if (typeof process !== 'undefined' && process.env) {
12
+ return process.env[name];
13
+ }
14
+ return undefined;
15
+ }
16
+
17
+ /**
18
+ * Parse relay URLs from environment variable or use defaults
19
+ * @returns {string[]}
20
+ */
21
+ function getDefaultRelays() {
22
+ const envRelays = getEnv('HOLOSPHERE_RELAYS');
23
+ if (envRelays) {
24
+ return envRelays.split(',').map(r => r.trim()).filter(r => r);
25
+ }
26
+ return ['wss://relay.holons.io'];
27
+ }
28
+
29
+ /**
30
+ * HoloSphere - Holonic Geospatial Communication Infrastructure
31
+ * Combines H3 hexagonal indexing with distributed P2P storage
32
+ * Supports multiple storage backends: nostr, gundb, activitypub
33
+ */
34
+ export class HoloSphere {
35
+ /**
36
+ * @param {Object} config - Configuration options
37
+ * @param {string} config.appName - Application namespace (default: 'holosphere')
38
+ * @param {string} config.backend - Storage backend: 'nostr' | 'gundb' | 'activitypub' (default: 'nostr')
39
+ * @param {string[]} config.relays - Nostr relay URLs (default from HOLOSPHERE_RELAYS env or ['wss://relay.holons.io', 'wss://relay.nostr.band'])
40
+ * @param {string} config.privateKey - Private key for signing (hex format, optional)
41
+ * @param {string} config.logLevel - Log verbosity: ERROR|WARN|INFO|DEBUG (default: 'WARN')
42
+ * @param {boolean} config.hybridMode - Enable hybrid mode (local + relay queries) (default: true)
43
+ * @param {Object} config.gundb - GunDB-specific configuration
44
+ * @param {Object} config.activitypub - ActivityPub-specific configuration
45
+ */
46
+ constructor(config = {}) {
47
+ // Allow string as shorthand for appName (backward compatibility)
48
+ if (typeof config === 'string') {
49
+ config = { appName: config };
50
+ }
51
+
52
+ // Validate config
53
+ if (config && typeof config !== 'object') {
54
+ throw new TypeError('Config must be an object');
55
+ }
56
+
57
+ // Validate individual config properties
58
+ if (config.appName !== undefined && typeof config.appName !== 'string') {
59
+ throw new TypeError('config.appName must be a string');
60
+ }
61
+ if (config.relays !== undefined && !Array.isArray(config.relays)) {
62
+ throw new TypeError('config.relays must be an array');
63
+ }
64
+ if (config.logLevel !== undefined && !['ERROR', 'WARN', 'INFO', 'DEBUG'].includes(config.logLevel)) {
65
+ throw new TypeError('config.logLevel must be one of: ERROR, WARN, INFO, DEBUG');
66
+ }
67
+ if (config.backend !== undefined && !BackendFactory.isAvailable(config.backend)) {
68
+ throw new TypeError(`config.backend must be one of: ${BackendFactory.getAvailableBackends().join(', ')}`);
69
+ }
70
+
71
+ // Set defaults (with environment variable fallbacks)
72
+ this.config = {
73
+ appName: config.appName || getEnv('HOLOSPHERE_APP_NAME') || 'holosphere',
74
+ backend: config.backend || 'nostr',
75
+ relays: config.relays || getDefaultRelays(),
76
+ privateKey: config.privateKey || getEnv('HOLOSPHERE_PRIVATE_KEY'),
77
+ logLevel: config.logLevel || getEnv('HOLOSPHERE_LOG_LEVEL') || 'WARN',
78
+ hybridMode: config.hybridMode !== false, // Enable by default
79
+ };
80
+
81
+ // Store raw config for backend initialization
82
+ this._rawConfig = config;
83
+
84
+ // Initialize logging
85
+ this.logLevels = { ERROR: 0, WARN: 1, INFO: 2, DEBUG: 3 };
86
+ this.currentLogLevel = this.logLevels[this.config.logLevel] || 1;
87
+
88
+ // Backend instance (set during initialization)
89
+ this._backend = null;
90
+
91
+ // For backward compatibility with Nostr, initialize synchronously
92
+ // Other backends use async initialization via _backendReady
93
+ if (this.config.backend === 'nostr') {
94
+ this._initNostrSync(config);
95
+ } else {
96
+ // Async initialization for other backends
97
+ this._backendReady = this._initBackendAsync(config);
98
+ }
99
+
100
+ // Metrics tracking
101
+ this._metrics = {
102
+ writes: 0,
103
+ reads: 0,
104
+ subscriptions: 0,
105
+ };
106
+ }
107
+
108
+ /**
109
+ * Initialize Nostr backend synchronously (for backward compatibility)
110
+ * @private
111
+ */
112
+ _initNostrSync(config) {
113
+ try {
114
+ this.client = createClient({
115
+ relays: this.config.relays,
116
+ privateKey: this.config.privateKey,
117
+ enableReconnect: config.enableReconnect !== false,
118
+ enablePing: config.enablePing !== false,
119
+ appName: this.config.appName,
120
+ radisk: config.radisk !== false,
121
+ persistence: config.persistence !== false,
122
+ dataDir: config.dataDir,
123
+ });
124
+
125
+ // Log startup information
126
+ this._logStartup({
127
+ version: pkg.version,
128
+ appName: this.config.appName,
129
+ backend: 'nostr',
130
+ relays: this.config.relays,
131
+ publicKey: this.client.publicKey,
132
+ logLevel: this.config.logLevel,
133
+ persistence: config.persistence !== false,
134
+ reconnect: config.enableReconnect !== false,
135
+ dataDir: config.dataDir,
136
+ });
137
+
138
+ // Backend ready immediately for Nostr
139
+ this._backendReady = Promise.resolve();
140
+ } catch (error) {
141
+ this._log('ERROR', 'Nostr client initialization failed', { error: error.message });
142
+ throw new Error(`Nostr client initialization failed: ${error.message}`);
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Initialize non-Nostr backends asynchronously
148
+ * @private
149
+ */
150
+ async _initBackendAsync(config) {
151
+ const backendType = this.config.backend;
152
+ const backendSpecificConfig = config[backendType] || {};
153
+
154
+ try {
155
+ // Merge common config with backend-specific config
156
+ const backendConfig = {
157
+ appName: this.config.appName,
158
+ privateKey: this.config.privateKey,
159
+ persistence: config.persistence !== false,
160
+ dataDir: config.dataDir,
161
+ ...backendSpecificConfig,
162
+ };
163
+
164
+ this._backend = await BackendFactory.create(backendType, backendConfig);
165
+
166
+ // For compatibility, also set client if backend has one
167
+ this.client = this._backend.client || this._backend;
168
+
169
+ // Log startup information
170
+ this._logStartup({
171
+ version: pkg.version,
172
+ appName: this.config.appName,
173
+ backend: backendType,
174
+ publicKey: this._backend.publicKey,
175
+ logLevel: this.config.logLevel,
176
+ persistence: config.persistence !== false,
177
+ ...this._backend.getStatus(),
178
+ });
179
+ } catch (error) {
180
+ this._log('ERROR', `${backendType} backend initialization failed`, { error: error.message });
181
+ throw new Error(`${backendType} backend initialization failed: ${error.message}`);
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Ensure backend is ready before operations
187
+ * @returns {Promise<void>}
188
+ */
189
+ async ready() {
190
+ if (this._backendReady) {
191
+ await this._backendReady;
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Get the storage backend instance
197
+ * @returns {StorageBackend|null}
198
+ */
199
+ get backend() {
200
+ return this._backend;
201
+ }
202
+
203
+ /**
204
+ * Internal structured logging
205
+ * @private
206
+ */
207
+ _log(level, message, data = {}) {
208
+ if (this.logLevels[level] <= this.currentLogLevel) {
209
+ const logEntry = {
210
+ timestamp: Date.now(),
211
+ level,
212
+ message,
213
+ app: this.config.appName,
214
+ ...data,
215
+ };
216
+ console.log(JSON.stringify(logEntry));
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Log startup information in a user-friendly format
222
+ * @private
223
+ */
224
+ _logStartup(info) {
225
+ // Only log if INFO or DEBUG level
226
+ if (this.currentLogLevel >= this.logLevels.INFO) {
227
+ console.log('\n' + '='.repeat(60));
228
+ console.log(' HoloSphere - Holonic Geospatial Infrastructure');
229
+ console.log('='.repeat(60));
230
+ console.log(` Version: ${info.version}`);
231
+ console.log(` App Name: ${info.appName}`);
232
+ console.log(` Backend: ${info.backend || 'nostr'}`);
233
+ console.log(` Public Key: ${info.publicKey ? info.publicKey.substring(0, 16) + '...' : 'N/A'}`);
234
+ console.log(` Log Level: ${info.logLevel}`);
235
+ console.log(` Persistence: ${info.persistence ? 'Enabled' : 'Disabled'}${info.dataDir ? ` (${info.dataDir})` : ''}`);
236
+
237
+ // Backend-specific info
238
+ if (info.backend === 'nostr' || !info.backend) {
239
+ console.log(` Auto-reconnect: ${info.reconnect ? 'Enabled' : 'Disabled'}`);
240
+ console.log('\n Connected Relays:');
241
+ if (!info.relays || info.relays.length === 0) {
242
+ console.log(' (none - running in local mode)');
243
+ } else {
244
+ info.relays.forEach((relay, idx) => {
245
+ console.log(` ${idx + 1}. ${relay}`);
246
+ });
247
+ }
248
+ } else if (info.backend === 'gundb') {
249
+ console.log('\n Gun Peers:');
250
+ if (!info.peers || info.peers.length === 0) {
251
+ console.log(' (none - running in local mode)');
252
+ } else {
253
+ info.peers.forEach((peer, idx) => {
254
+ console.log(` ${idx + 1}. ${peer}`);
255
+ });
256
+ }
257
+ } else if (info.backend === 'activitypub') {
258
+ console.log(` Server URL: ${info.serverUrl || 'N/A'}`);
259
+ console.log(` Actor ID: ${info.actorId || 'N/A'}`);
260
+ }
261
+
262
+ console.log('='.repeat(60) + '\n');
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Get metrics
268
+ * @returns {Object} Operation counts
269
+ */
270
+ metrics() {
271
+ return { ...this._metrics };
272
+ }
273
+ }
@@ -0,0 +1,259 @@
1
+ /**
2
+ * Cryptographic Operations (secp256k1)
3
+ * Lazy-loaded for performance
4
+ */
5
+
6
+ import { sha256 } from '@noble/hashes/sha256';
7
+ import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
8
+
9
+ let secp256k1 = null;
10
+
11
+ /**
12
+ * Lazy load secp256k1 module
13
+ * @private
14
+ */
15
+ async function loadCrypto() {
16
+ if (!secp256k1) {
17
+ const module = await import('@noble/curves/secp256k1');
18
+ secp256k1 = module.secp256k1;
19
+ }
20
+ return secp256k1;
21
+ }
22
+
23
+ /**
24
+ * Get public key from private key
25
+ * @param {string} privateKey - Private key (hex string)
26
+ * @returns {Promise<string>} Public key (hex string)
27
+ */
28
+ export async function getPublicKey(privateKey) {
29
+ const crypto = await loadCrypto();
30
+ const pubKey = crypto.getPublicKey(privateKey);
31
+ return bytesToHex(pubKey);
32
+ }
33
+
34
+ /**
35
+ * Sign content with private key
36
+ * @param {string} content - Content to sign (will be hashed)
37
+ * @param {string} privateKey - Private key (hex string)
38
+ * @returns {Promise<string>} Signature (hex string)
39
+ */
40
+ export async function sign(content, privateKey) {
41
+ try {
42
+ const crypto = await loadCrypto();
43
+
44
+ // Hash content
45
+ const msgHash = hashContent(content);
46
+
47
+ // Sign - secp256k1.sign returns Signature object
48
+ const signature = crypto.sign(msgHash, privateKey);
49
+ return signature.toCompactHex();
50
+ } catch (error) {
51
+ throw new Error(`Signature generation failed: ${error.message}`);
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Verify signature
57
+ * @param {string} content - Original content
58
+ * @param {string} signature - Signature (hex string)
59
+ * @param {string} publicKey - Public key (hex string)
60
+ * @returns {Promise<boolean>} True if valid
61
+ */
62
+ export async function verify(content, signature, publicKey) {
63
+ try {
64
+ // Validate public key format
65
+ if (!publicKey || typeof publicKey !== 'string' || publicKey.length < 64) {
66
+ throw new Error('Invalid public key format');
67
+ }
68
+
69
+ const crypto = await loadCrypto();
70
+
71
+ const msgHash = hashContent(content);
72
+ const isValid = crypto.verify(signature, msgHash, publicKey);
73
+ return isValid;
74
+ } catch (error) {
75
+ // Invalid signatures or keys throw errors
76
+ // For test compatibility, rethrow if it's a validation error
77
+ if (error.message === 'Invalid public key format') {
78
+ throw error;
79
+ }
80
+ // Otherwise return false for invalid signatures
81
+ return false;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Match token scope against requested scope with wildcard support
87
+ * Supports:
88
+ * - Exact match: { holonId: "abc", lensName: "quests" }
89
+ * - Item-level: { holonId: "abc", lensName: "quests", dataId: "quest-001" }
90
+ * - Wildcards: { holonId: "*", lensName: "*" } matches everything
91
+ * @param {Object} tokenScope - Scope from capability token
92
+ * @param {Object} requestedScope - Scope being requested
93
+ * @returns {boolean} True if token scope covers requested scope
94
+ */
95
+ export function matchScope(tokenScope, requestedScope) {
96
+ // Handle string scopes (legacy support)
97
+ if (typeof tokenScope === 'string' || typeof requestedScope === 'string') {
98
+ const tokenStr = typeof tokenScope === 'string' ? tokenScope : JSON.stringify(tokenScope);
99
+ const reqStr = typeof requestedScope === 'string' ? requestedScope : JSON.stringify(requestedScope);
100
+ return tokenStr === reqStr;
101
+ }
102
+
103
+ // Handle holonId
104
+ if (tokenScope.holonId !== '*' && tokenScope.holonId !== requestedScope.holonId) {
105
+ return false;
106
+ }
107
+
108
+ // Handle lensName
109
+ if (tokenScope.lensName !== '*' && tokenScope.lensName !== requestedScope.lensName) {
110
+ return false;
111
+ }
112
+
113
+ // Handle optional dataId (item-level scope)
114
+ // If token has specific dataId (not wildcard), it must match requested dataId
115
+ if (tokenScope.dataId && tokenScope.dataId !== '*') {
116
+ if (requestedScope.dataId && tokenScope.dataId !== requestedScope.dataId) {
117
+ return false;
118
+ }
119
+ }
120
+
121
+ return true;
122
+ }
123
+
124
+ /**
125
+ * Issue capability token
126
+ * @param {string[]} permissions - Permissions array (e.g., ['read', 'write', 'delete'])
127
+ * @param {Object|string} scope - Scope (holon/lens path or object). Supports wildcards: { holonId: "*", lensName: "*" }
128
+ * @param {string} recipient - Recipient public key
129
+ * @param {Object} options - Options
130
+ * @param {number} options.expiresIn - Expiration in milliseconds (default: 1 hour)
131
+ * @param {string} options.issuer - Issuer ID
132
+ * @param {string} options.issuerKey - Issuer private key for signing
133
+ * @returns {Promise<string>} Capability token (base64-encoded JWT-like)
134
+ */
135
+ export async function issueCapability(permissions, scope, recipient, options = {}) {
136
+ const { expiresIn = 3600000, issuer = 'holosphere', issuerKey } = options;
137
+
138
+ // Validate permissions
139
+ if (!Array.isArray(permissions) || permissions.length === 0) {
140
+ throw new Error('Permissions array cannot be empty');
141
+ }
142
+
143
+ // Validate scope - now supports wildcards
144
+ if (typeof scope === 'object') {
145
+ // holonId is required (can be '*' for wildcard)
146
+ if (scope.holonId === undefined || scope.holonId === '') {
147
+ throw new Error('Invalid scope: holonId is required (use "*" for wildcard)');
148
+ }
149
+ // lensName is required (can be '*' for wildcard)
150
+ if (scope.lensName === undefined || scope.lensName === '') {
151
+ throw new Error('Invalid scope: lensName is required (use "*" for wildcard)');
152
+ }
153
+ // dataId is optional (can be specific value, '*', or omitted)
154
+ } else if (typeof scope === 'string' && scope === '') {
155
+ throw new Error('Invalid scope: cannot be empty string');
156
+ }
157
+
158
+ // Validate issuerKey if provided
159
+ if (issuerKey && (typeof issuerKey !== 'string' || issuerKey.length < 32)) {
160
+ throw new Error('Invalid issuer key');
161
+ }
162
+
163
+ const token = {
164
+ type: 'capability',
165
+ permissions,
166
+ scope,
167
+ recipient,
168
+ issuer,
169
+ nonce: generateNonce(),
170
+ issued: Date.now(),
171
+ expires: Date.now() + expiresIn,
172
+ };
173
+
174
+ // Encode token as base64
175
+ const payload = JSON.stringify(token);
176
+ const encoded = Buffer.from ?
177
+ Buffer.from(payload).toString('base64') :
178
+ btoa(payload);
179
+
180
+ // If issuerKey provided, sign the token
181
+ if (issuerKey) {
182
+ const signature = await sign(payload, issuerKey);
183
+ return `${encoded}.${signature}`;
184
+ }
185
+
186
+ return encoded;
187
+ }
188
+
189
+ /**
190
+ * Verify capability token
191
+ * @param {string|Object} token - Capability token (string or object)
192
+ * @param {string} requiredPermission - Required permission
193
+ * @param {Object|string} scope - Scope to check (supports wildcards in token scope)
194
+ * @returns {Promise<boolean>} True if valid
195
+ */
196
+ export async function verifyCapability(token, requiredPermission, scope) {
197
+ try {
198
+ let tokenObj;
199
+
200
+ // Decode if string
201
+ if (typeof token === 'string') {
202
+ const parts = token.split('.');
203
+ const payload = parts[0];
204
+ const decoded = Buffer.from ?
205
+ Buffer.from(payload, 'base64').toString('utf8') :
206
+ atob(payload);
207
+ tokenObj = JSON.parse(decoded);
208
+
209
+ // TODO: Verify signature if present (parts[1])
210
+ } else {
211
+ tokenObj = token;
212
+ }
213
+
214
+ if (!tokenObj || tokenObj.type !== 'capability') {
215
+ return false;
216
+ }
217
+
218
+ // Check expiration
219
+ if (Date.now() > tokenObj.expires) {
220
+ return false;
221
+ }
222
+
223
+ // Check scope using matchScope (supports wildcards)
224
+ if (!matchScope(tokenObj.scope, scope)) {
225
+ return false;
226
+ }
227
+
228
+ // Check permission
229
+ if (!tokenObj.permissions.includes(requiredPermission)) {
230
+ return false;
231
+ }
232
+
233
+ return true;
234
+ } catch (error) {
235
+ return false;
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Generate unique nonce
241
+ * @private
242
+ */
243
+ function generateNonce() {
244
+ return (
245
+ Date.now().toString(36) + Math.random().toString(36).substring(2, 15)
246
+ );
247
+ }
248
+
249
+ /**
250
+ * Hash content using SHA-256
251
+ * @private
252
+ */
253
+ function hashContent(content) {
254
+ const str = typeof content === 'string' ? content : JSON.stringify(content);
255
+ const encoder = new TextEncoder();
256
+ const data = encoder.encode(str);
257
+ const hash = sha256(data);
258
+ return bytesToHex(hash);
259
+ }