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,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
|
+
}
|