@ruvector/edge-net 0.4.6 → 0.5.1
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/package.json +13 -2
- package/plugins/SECURITY-AUDIT.md +654 -0
- package/plugins/cli.js +435 -0
- package/plugins/implementations/compression.js +132 -0
- package/plugins/implementations/e2e-encryption.js +205 -0
- package/plugins/implementations/federated-learning.js +249 -0
- package/plugins/implementations/reputation-staking.js +243 -0
- package/plugins/implementations/swarm-intelligence.js +386 -0
- package/plugins/index.js +90 -0
- package/plugins/plugin-loader.js +648 -0
- package/plugins/plugin-manifest.js +702 -0
- package/plugins/plugin-sdk.js +496 -0
- package/tests/plugin-benchmark.js +1239 -0
- package/tests/plugin-system-test.js +382 -0
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edge-Net Secure Plugin Loader
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Ed25519 signature verification
|
|
6
|
+
* - SHA-256 integrity checks
|
|
7
|
+
* - Lazy loading with caching
|
|
8
|
+
* - Capability-based sandboxing
|
|
9
|
+
* - Zero telemetry
|
|
10
|
+
*
|
|
11
|
+
* @module @ruvector/edge-net/plugins/loader
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { EventEmitter } from 'events';
|
|
15
|
+
import { createHash, createVerify, generateKeyPairSync, sign, verify } from 'crypto';
|
|
16
|
+
import { PLUGIN_CATALOG, PLUGIN_BUNDLES, Capability, PluginTier } from './plugin-manifest.js';
|
|
17
|
+
|
|
18
|
+
// ============================================
|
|
19
|
+
// ED25519 SIGNATURE VERIFICATION
|
|
20
|
+
// ============================================
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Built-in trusted public keys for official plugins
|
|
24
|
+
* In production, these would be loaded from a secure registry
|
|
25
|
+
*/
|
|
26
|
+
const TRUSTED_PUBLIC_KEYS = {
|
|
27
|
+
'ruvector': 'MCowBQYDK2VwAyEAMock_ruvector_official_key_replace_in_production_1234567890=',
|
|
28
|
+
'edge-net-official': 'MCowBQYDK2VwAyEAMock_edgenet_official_key_replace_in_production_abcdef12=',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Verify Ed25519 signature
|
|
33
|
+
* @param {Buffer|string} data - The data that was signed
|
|
34
|
+
* @param {string} signature - Base64-encoded signature
|
|
35
|
+
* @param {string} publicKey - PEM or DER formatted public key
|
|
36
|
+
* @returns {boolean} - True if signature is valid
|
|
37
|
+
*/
|
|
38
|
+
function verifyEd25519Signature(data, signature, publicKey) {
|
|
39
|
+
try {
|
|
40
|
+
const signatureBuffer = Buffer.from(signature, 'base64');
|
|
41
|
+
const dataBuffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
42
|
+
|
|
43
|
+
// Handle both PEM and raw key formats
|
|
44
|
+
let keyObject;
|
|
45
|
+
if (publicKey.startsWith('-----BEGIN')) {
|
|
46
|
+
keyObject = publicKey;
|
|
47
|
+
} else {
|
|
48
|
+
// Assume base64-encoded DER format
|
|
49
|
+
keyObject = {
|
|
50
|
+
key: Buffer.from(publicKey, 'base64'),
|
|
51
|
+
format: 'der',
|
|
52
|
+
type: 'spki',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return verify(null, dataBuffer, keyObject, signatureBuffer);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
// Signature verification failed
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ============================================
|
|
64
|
+
// RATE LIMITER
|
|
65
|
+
// ============================================
|
|
66
|
+
|
|
67
|
+
class RateLimiter {
|
|
68
|
+
constructor(options = {}) {
|
|
69
|
+
this.maxRequests = options.maxRequests || 100;
|
|
70
|
+
this.windowMs = options.windowMs || 60000; // 1 minute
|
|
71
|
+
this.requests = new Map(); // key -> { count, windowStart }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Check if action is allowed, returns true if allowed
|
|
76
|
+
*/
|
|
77
|
+
check(key) {
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
let record = this.requests.get(key);
|
|
80
|
+
|
|
81
|
+
if (!record || (now - record.windowStart) > this.windowMs) {
|
|
82
|
+
// New window
|
|
83
|
+
record = { count: 0, windowStart: now };
|
|
84
|
+
this.requests.set(key, record);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (record.count >= this.maxRequests) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
record.count++;
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get remaining requests in current window
|
|
97
|
+
*/
|
|
98
|
+
remaining(key) {
|
|
99
|
+
const record = this.requests.get(key);
|
|
100
|
+
if (!record) return this.maxRequests;
|
|
101
|
+
|
|
102
|
+
const now = Date.now();
|
|
103
|
+
if ((now - record.windowStart) > this.windowMs) {
|
|
104
|
+
return this.maxRequests;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return Math.max(0, this.maxRequests - record.count);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Reset limits for a key
|
|
112
|
+
*/
|
|
113
|
+
reset(key) {
|
|
114
|
+
this.requests.delete(key);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ============================================
|
|
119
|
+
// PLUGIN LOADER
|
|
120
|
+
// ============================================
|
|
121
|
+
|
|
122
|
+
export class PluginLoader extends EventEmitter {
|
|
123
|
+
constructor(options = {}) {
|
|
124
|
+
super();
|
|
125
|
+
|
|
126
|
+
this.options = {
|
|
127
|
+
// Security
|
|
128
|
+
verifySignatures: options.verifySignatures ?? true,
|
|
129
|
+
allowedTiers: options.allowedTiers ?? [PluginTier.STABLE, PluginTier.BETA],
|
|
130
|
+
trustedAuthors: options.trustedAuthors ?? ['ruvector', 'edge-net-official'],
|
|
131
|
+
trustedPublicKeys: options.trustedPublicKeys ?? {},
|
|
132
|
+
|
|
133
|
+
// Loading
|
|
134
|
+
lazyLoad: options.lazyLoad ?? true,
|
|
135
|
+
cachePlugins: options.cachePlugins ?? true,
|
|
136
|
+
pluginPath: options.pluginPath ?? './plugins/implementations',
|
|
137
|
+
|
|
138
|
+
// Permissions
|
|
139
|
+
maxCapabilities: options.maxCapabilities ?? 10,
|
|
140
|
+
deniedCapabilities: options.deniedCapabilities ?? [Capability.SYSTEM_EXEC],
|
|
141
|
+
|
|
142
|
+
// Rate limiting
|
|
143
|
+
rateLimitEnabled: options.rateLimitEnabled ?? true,
|
|
144
|
+
rateLimitRequests: options.rateLimitRequests ?? 100,
|
|
145
|
+
rateLimitWindowMs: options.rateLimitWindowMs ?? 60000,
|
|
146
|
+
...options,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// Plugin state
|
|
150
|
+
this.loadedPlugins = new Map(); // id -> instance
|
|
151
|
+
this.pluginConfigs = new Map(); // id -> config
|
|
152
|
+
this.pendingLoads = new Map(); // id -> Promise
|
|
153
|
+
|
|
154
|
+
// Rate limiter
|
|
155
|
+
this.rateLimiter = new RateLimiter({
|
|
156
|
+
maxRequests: this.options.rateLimitRequests,
|
|
157
|
+
windowMs: this.options.rateLimitWindowMs,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Stats
|
|
161
|
+
this.stats = {
|
|
162
|
+
loaded: 0,
|
|
163
|
+
cached: 0,
|
|
164
|
+
verified: 0,
|
|
165
|
+
rejected: 0,
|
|
166
|
+
rateLimited: 0,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get catalog of available plugins
|
|
172
|
+
*/
|
|
173
|
+
getCatalog() {
|
|
174
|
+
return Object.entries(PLUGIN_CATALOG).map(([id, manifest]) => ({
|
|
175
|
+
id,
|
|
176
|
+
...manifest,
|
|
177
|
+
isLoaded: this.loadedPlugins.has(id),
|
|
178
|
+
isAllowed: this._isPluginAllowed(manifest),
|
|
179
|
+
}));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Get plugin bundles
|
|
184
|
+
*/
|
|
185
|
+
getBundles() {
|
|
186
|
+
return PLUGIN_BUNDLES;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Check if plugin is allowed by security policy
|
|
191
|
+
*/
|
|
192
|
+
_isPluginAllowed(manifest) {
|
|
193
|
+
// Check tier
|
|
194
|
+
if (!this.options.allowedTiers.includes(manifest.tier)) {
|
|
195
|
+
return { allowed: false, reason: `Tier ${manifest.tier} not allowed` };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Check capabilities
|
|
199
|
+
const deniedCaps = manifest.capabilities?.filter(c =>
|
|
200
|
+
this.options.deniedCapabilities.includes(c)
|
|
201
|
+
);
|
|
202
|
+
if (deniedCaps?.length > 0) {
|
|
203
|
+
return { allowed: false, reason: `Denied capabilities: ${deniedCaps.join(', ')}` };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Check capability count
|
|
207
|
+
if (manifest.capabilities?.length > this.options.maxCapabilities) {
|
|
208
|
+
return { allowed: false, reason: 'Too many capabilities requested' };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return { allowed: true };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Verify plugin integrity using Ed25519 signatures
|
|
216
|
+
*/
|
|
217
|
+
async _verifyPlugin(manifest, code) {
|
|
218
|
+
if (!this.options.verifySignatures) {
|
|
219
|
+
return { verified: true, reason: 'Verification disabled' };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Verify checksum first (fast check)
|
|
223
|
+
if (manifest.checksum) {
|
|
224
|
+
const hash = createHash('sha256').update(code).digest('hex');
|
|
225
|
+
if (hash !== manifest.checksum) {
|
|
226
|
+
return { verified: false, reason: 'Checksum mismatch' };
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Verify Ed25519 signature if present
|
|
231
|
+
if (manifest.signature && manifest.author) {
|
|
232
|
+
// Get the public key for this author
|
|
233
|
+
const publicKey = this.options.trustedPublicKeys?.[manifest.author]
|
|
234
|
+
|| TRUSTED_PUBLIC_KEYS[manifest.author];
|
|
235
|
+
|
|
236
|
+
if (!publicKey) {
|
|
237
|
+
return {
|
|
238
|
+
verified: false,
|
|
239
|
+
reason: `Unknown author: ${manifest.author}. No public key registered.`
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Create canonical data for signature verification
|
|
244
|
+
// Include manifest fields that should be signed
|
|
245
|
+
const signedData = JSON.stringify({
|
|
246
|
+
id: manifest.id,
|
|
247
|
+
name: manifest.name,
|
|
248
|
+
version: manifest.version,
|
|
249
|
+
author: manifest.author,
|
|
250
|
+
checksum: manifest.checksum,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Verify the signature using Ed25519
|
|
254
|
+
const isValid = verifyEd25519Signature(signedData, manifest.signature, publicKey);
|
|
255
|
+
|
|
256
|
+
if (!isValid) {
|
|
257
|
+
return {
|
|
258
|
+
verified: false,
|
|
259
|
+
reason: 'Ed25519 signature verification failed'
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return { verified: true, reason: 'Ed25519 signature valid' };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Built-in stable plugins from the catalog don't require external signatures
|
|
267
|
+
// They are verified by code review and included in the package
|
|
268
|
+
if (!manifest.signature && !manifest.checksum) {
|
|
269
|
+
const isBuiltIn = PLUGIN_CATALOG[manifest.id] !== undefined;
|
|
270
|
+
if (isBuiltIn && manifest.tier === PluginTier.STABLE) {
|
|
271
|
+
return { verified: true, reason: 'Built-in stable plugin' };
|
|
272
|
+
}
|
|
273
|
+
return { verified: false, reason: 'No verification metadata for external plugin' };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Checksum passed but no signature - allow for stable tier only
|
|
277
|
+
if (manifest.checksum && !manifest.signature) {
|
|
278
|
+
if (manifest.tier === PluginTier.STABLE) {
|
|
279
|
+
return { verified: true, reason: 'Checksum verified (stable tier)' };
|
|
280
|
+
}
|
|
281
|
+
return { verified: false, reason: 'Non-stable plugins require signature' };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return { verified: true };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Load a plugin by ID
|
|
289
|
+
*/
|
|
290
|
+
async load(pluginId, config = {}) {
|
|
291
|
+
// Rate limit check
|
|
292
|
+
if (this.options.rateLimitEnabled) {
|
|
293
|
+
const rateLimitKey = `load:${pluginId}`;
|
|
294
|
+
if (!this.rateLimiter.check(rateLimitKey)) {
|
|
295
|
+
this.stats.rateLimited++;
|
|
296
|
+
throw new Error(
|
|
297
|
+
`Rate limit exceeded for plugin ${pluginId}. ` +
|
|
298
|
+
`Try again in ${Math.ceil(this.options.rateLimitWindowMs / 1000)}s.`
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Check if already loaded
|
|
304
|
+
if (this.loadedPlugins.has(pluginId)) {
|
|
305
|
+
return this.loadedPlugins.get(pluginId);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Check if loading in progress
|
|
309
|
+
if (this.pendingLoads.has(pluginId)) {
|
|
310
|
+
return this.pendingLoads.get(pluginId);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const loadPromise = this._loadPlugin(pluginId, config);
|
|
314
|
+
this.pendingLoads.set(pluginId, loadPromise);
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
const plugin = await loadPromise;
|
|
318
|
+
this.pendingLoads.delete(pluginId);
|
|
319
|
+
return plugin;
|
|
320
|
+
} catch (error) {
|
|
321
|
+
this.pendingLoads.delete(pluginId);
|
|
322
|
+
throw error;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async _loadPlugin(pluginId, config) {
|
|
327
|
+
const manifest = PLUGIN_CATALOG[pluginId];
|
|
328
|
+
if (!manifest) {
|
|
329
|
+
throw new Error(`Plugin not found: ${pluginId}`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Security check
|
|
333
|
+
const allowed = this._isPluginAllowed(manifest);
|
|
334
|
+
if (!allowed.allowed) {
|
|
335
|
+
this.stats.rejected++;
|
|
336
|
+
throw new Error(`Plugin ${pluginId} not allowed: ${allowed.reason}`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Load dependencies first
|
|
340
|
+
if (manifest.dependencies) {
|
|
341
|
+
for (const depId of manifest.dependencies) {
|
|
342
|
+
if (!this.loadedPlugins.has(depId)) {
|
|
343
|
+
await this.load(depId);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Merge config with defaults
|
|
349
|
+
const finalConfig = {
|
|
350
|
+
...manifest.defaultConfig,
|
|
351
|
+
...config,
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// Create plugin instance
|
|
355
|
+
const plugin = await this._createPluginInstance(manifest, finalConfig);
|
|
356
|
+
|
|
357
|
+
// Store
|
|
358
|
+
this.loadedPlugins.set(pluginId, plugin);
|
|
359
|
+
this.pluginConfigs.set(pluginId, finalConfig);
|
|
360
|
+
this.stats.loaded++;
|
|
361
|
+
|
|
362
|
+
this.emit('plugin:loaded', { pluginId, manifest, config: finalConfig });
|
|
363
|
+
|
|
364
|
+
return plugin;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Create plugin instance with sandbox
|
|
369
|
+
*/
|
|
370
|
+
async _createPluginInstance(manifest, config) {
|
|
371
|
+
// Create sandboxed context with proper isolation
|
|
372
|
+
const sandbox = this._createSandbox(manifest.capabilities || [], manifest);
|
|
373
|
+
|
|
374
|
+
// Return plugin wrapper
|
|
375
|
+
return {
|
|
376
|
+
id: manifest.id,
|
|
377
|
+
name: manifest.name,
|
|
378
|
+
version: manifest.version,
|
|
379
|
+
config,
|
|
380
|
+
sandbox,
|
|
381
|
+
manifest,
|
|
382
|
+
|
|
383
|
+
// Plugin API
|
|
384
|
+
api: this._createPluginAPI(manifest, sandbox),
|
|
385
|
+
|
|
386
|
+
// Lifecycle
|
|
387
|
+
async init() {
|
|
388
|
+
// Plugin-specific initialization
|
|
389
|
+
},
|
|
390
|
+
|
|
391
|
+
async destroy() {
|
|
392
|
+
// Cleanup
|
|
393
|
+
},
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Create capability-based sandbox with proper isolation
|
|
399
|
+
*/
|
|
400
|
+
_createSandbox(capabilities, manifest) {
|
|
401
|
+
const allowedCapabilities = new Set(capabilities);
|
|
402
|
+
const deniedGlobals = new Set([
|
|
403
|
+
'process', 'require', 'eval', 'Function',
|
|
404
|
+
'__dirname', '__filename', 'module', 'exports'
|
|
405
|
+
]);
|
|
406
|
+
|
|
407
|
+
// Create isolated sandbox context
|
|
408
|
+
const sandbox = {
|
|
409
|
+
// Immutable capability set
|
|
410
|
+
get capabilities() {
|
|
411
|
+
return new Set(allowedCapabilities);
|
|
412
|
+
},
|
|
413
|
+
|
|
414
|
+
hasCapability(cap) {
|
|
415
|
+
return allowedCapabilities.has(cap);
|
|
416
|
+
},
|
|
417
|
+
|
|
418
|
+
require(cap) {
|
|
419
|
+
if (!allowedCapabilities.has(cap)) {
|
|
420
|
+
throw new Error(`Missing capability: ${cap}`);
|
|
421
|
+
}
|
|
422
|
+
},
|
|
423
|
+
|
|
424
|
+
// Resource limits
|
|
425
|
+
limits: Object.freeze({
|
|
426
|
+
maxMemoryMB: 128,
|
|
427
|
+
maxCpuTimeMs: 5000,
|
|
428
|
+
maxNetworkConnections: 10,
|
|
429
|
+
maxStorageBytes: 10 * 1024 * 1024, // 10MB
|
|
430
|
+
}),
|
|
431
|
+
|
|
432
|
+
// Execution context (read-only)
|
|
433
|
+
context: Object.freeze({
|
|
434
|
+
pluginId: manifest.id,
|
|
435
|
+
pluginVersion: manifest.version,
|
|
436
|
+
startTime: Date.now(),
|
|
437
|
+
}),
|
|
438
|
+
|
|
439
|
+
// Check if global is allowed
|
|
440
|
+
isGlobalAllowed(name) {
|
|
441
|
+
return !deniedGlobals.has(name);
|
|
442
|
+
},
|
|
443
|
+
|
|
444
|
+
// Secure timer functions (returns cleanup functions)
|
|
445
|
+
setTimeout: (fn, delay) => {
|
|
446
|
+
const maxDelay = 30000; // 30 seconds max
|
|
447
|
+
const safeDelay = Math.min(delay, maxDelay);
|
|
448
|
+
const timer = setTimeout(fn, safeDelay);
|
|
449
|
+
return () => clearTimeout(timer);
|
|
450
|
+
},
|
|
451
|
+
|
|
452
|
+
setInterval: (fn, delay) => {
|
|
453
|
+
const minDelay = 100; // Minimum 100ms
|
|
454
|
+
const safeDelay = Math.max(delay, minDelay);
|
|
455
|
+
const timer = setInterval(fn, safeDelay);
|
|
456
|
+
return () => clearInterval(timer);
|
|
457
|
+
},
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
// Freeze the sandbox to prevent modification
|
|
461
|
+
return Object.freeze(sandbox);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Create plugin API based on capabilities
|
|
466
|
+
*/
|
|
467
|
+
_createPluginAPI(manifest, sandbox) {
|
|
468
|
+
const api = {};
|
|
469
|
+
|
|
470
|
+
// Network API (if permitted)
|
|
471
|
+
if (sandbox.hasCapability(Capability.NETWORK_CONNECT)) {
|
|
472
|
+
api.network = {
|
|
473
|
+
connect: async (url) => {
|
|
474
|
+
sandbox.require(Capability.NETWORK_CONNECT);
|
|
475
|
+
// Implementation delegated to edge-net core
|
|
476
|
+
return { connected: true, url };
|
|
477
|
+
},
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Crypto API (if permitted)
|
|
482
|
+
if (sandbox.hasCapability(Capability.CRYPTO_ENCRYPT)) {
|
|
483
|
+
api.crypto = {
|
|
484
|
+
encrypt: async (data, key) => {
|
|
485
|
+
sandbox.require(Capability.CRYPTO_ENCRYPT);
|
|
486
|
+
// Implementation delegated to WASM crypto
|
|
487
|
+
return { encrypted: true };
|
|
488
|
+
},
|
|
489
|
+
decrypt: async (data, key) => {
|
|
490
|
+
sandbox.require(Capability.CRYPTO_ENCRYPT);
|
|
491
|
+
return { decrypted: true };
|
|
492
|
+
},
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (sandbox.hasCapability(Capability.CRYPTO_SIGN)) {
|
|
497
|
+
api.crypto = api.crypto || {};
|
|
498
|
+
api.crypto.sign = async (data, privateKey) => {
|
|
499
|
+
sandbox.require(Capability.CRYPTO_SIGN);
|
|
500
|
+
return { signed: true };
|
|
501
|
+
};
|
|
502
|
+
api.crypto.verify = async (data, signature, publicKey) => {
|
|
503
|
+
sandbox.require(Capability.CRYPTO_SIGN);
|
|
504
|
+
return { valid: true };
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Storage API (if permitted)
|
|
509
|
+
if (sandbox.hasCapability(Capability.STORAGE_READ)) {
|
|
510
|
+
api.storage = {
|
|
511
|
+
get: async (key) => {
|
|
512
|
+
sandbox.require(Capability.STORAGE_READ);
|
|
513
|
+
return null; // Implementation delegated
|
|
514
|
+
},
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
if (sandbox.hasCapability(Capability.STORAGE_WRITE)) {
|
|
518
|
+
api.storage = api.storage || {};
|
|
519
|
+
api.storage.set = async (key, value) => {
|
|
520
|
+
sandbox.require(Capability.STORAGE_WRITE);
|
|
521
|
+
return true;
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Compute API (if permitted)
|
|
526
|
+
if (sandbox.hasCapability(Capability.COMPUTE_WASM)) {
|
|
527
|
+
api.compute = {
|
|
528
|
+
runWasm: async (module, fn, args) => {
|
|
529
|
+
sandbox.require(Capability.COMPUTE_WASM);
|
|
530
|
+
return { result: null }; // Implementation delegated
|
|
531
|
+
},
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return api;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Unload a plugin
|
|
540
|
+
*/
|
|
541
|
+
async unload(pluginId) {
|
|
542
|
+
const plugin = this.loadedPlugins.get(pluginId);
|
|
543
|
+
if (!plugin) {
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Check for dependents
|
|
548
|
+
for (const [id, p] of this.loadedPlugins) {
|
|
549
|
+
const manifest = PLUGIN_CATALOG[id];
|
|
550
|
+
if (manifest.dependencies?.includes(pluginId)) {
|
|
551
|
+
throw new Error(`Cannot unload ${pluginId}: required by ${id}`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Cleanup
|
|
556
|
+
if (plugin.destroy) {
|
|
557
|
+
await plugin.destroy();
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
this.loadedPlugins.delete(pluginId);
|
|
561
|
+
this.pluginConfigs.delete(pluginId);
|
|
562
|
+
this.stats.loaded--;
|
|
563
|
+
|
|
564
|
+
this.emit('plugin:unloaded', { pluginId });
|
|
565
|
+
return true;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Load a bundle of plugins
|
|
570
|
+
*/
|
|
571
|
+
async loadBundle(bundleName) {
|
|
572
|
+
const bundle = PLUGIN_BUNDLES[bundleName];
|
|
573
|
+
if (!bundle) {
|
|
574
|
+
throw new Error(`Bundle not found: ${bundleName}`);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const results = [];
|
|
578
|
+
for (const pluginId of bundle.plugins) {
|
|
579
|
+
try {
|
|
580
|
+
const plugin = await this.load(pluginId);
|
|
581
|
+
results.push({ pluginId, success: true, plugin });
|
|
582
|
+
} catch (error) {
|
|
583
|
+
results.push({ pluginId, success: false, error: error.message });
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
this.emit('bundle:loaded', { bundleName, results });
|
|
588
|
+
return results;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Get loaded plugin
|
|
593
|
+
*/
|
|
594
|
+
get(pluginId) {
|
|
595
|
+
return this.loadedPlugins.get(pluginId);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Check if plugin is loaded
|
|
600
|
+
*/
|
|
601
|
+
isLoaded(pluginId) {
|
|
602
|
+
return this.loadedPlugins.has(pluginId);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Get all loaded plugins
|
|
607
|
+
*/
|
|
608
|
+
getLoaded() {
|
|
609
|
+
return Array.from(this.loadedPlugins.entries()).map(([id, plugin]) => ({
|
|
610
|
+
id,
|
|
611
|
+
name: plugin.name,
|
|
612
|
+
version: plugin.version,
|
|
613
|
+
config: this.pluginConfigs.get(id),
|
|
614
|
+
}));
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Get loader stats
|
|
619
|
+
*/
|
|
620
|
+
getStats() {
|
|
621
|
+
return {
|
|
622
|
+
...this.stats,
|
|
623
|
+
catalogSize: Object.keys(PLUGIN_CATALOG).length,
|
|
624
|
+
bundleCount: Object.keys(PLUGIN_BUNDLES).length,
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// ============================================
|
|
630
|
+
// PLUGIN MANAGER (Singleton)
|
|
631
|
+
// ============================================
|
|
632
|
+
|
|
633
|
+
export class PluginManager {
|
|
634
|
+
static instance = null;
|
|
635
|
+
|
|
636
|
+
static getInstance(options = {}) {
|
|
637
|
+
if (!PluginManager.instance) {
|
|
638
|
+
PluginManager.instance = new PluginLoader(options);
|
|
639
|
+
}
|
|
640
|
+
return PluginManager.instance;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
static reset() {
|
|
644
|
+
PluginManager.instance = null;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
export default PluginLoader;
|