@ruvector/edge-net 0.4.6 → 0.5.0

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.
@@ -0,0 +1,496 @@
1
+ /**
2
+ * Edge-Net Plugin SDK
3
+ *
4
+ * Create custom plugins for the edge-net ecosystem.
5
+ * Provides base classes, utilities, and validation.
6
+ *
7
+ * @module @ruvector/edge-net/plugins/sdk
8
+ */
9
+
10
+ import { EventEmitter } from 'events';
11
+ import { createHash, randomBytes } from 'crypto';
12
+ import { Capability, PluginCategory, PluginTier } from './plugin-manifest.js';
13
+
14
+ // Re-export for plugin authors
15
+ export { Capability, PluginCategory, PluginTier };
16
+
17
+ // ============================================
18
+ // BASE PLUGIN CLASS
19
+ // ============================================
20
+
21
+ /**
22
+ * Base class for all custom plugins.
23
+ * Extend this to create your own plugins.
24
+ *
25
+ * @example
26
+ * ```javascript
27
+ * import { BasePlugin, Capability, PluginCategory, PluginTier } from '@ruvector/edge-net/plugins/sdk';
28
+ *
29
+ * export class MyPlugin extends BasePlugin {
30
+ * static manifest = {
31
+ * id: 'my-org.my-plugin',
32
+ * name: 'My Custom Plugin',
33
+ * version: '1.0.0',
34
+ * description: 'Does something awesome',
35
+ * category: PluginCategory.CORE,
36
+ * tier: PluginTier.BETA,
37
+ * capabilities: [Capability.COMPUTE_WASM],
38
+ * configSchema: {
39
+ * type: 'object',
40
+ * properties: {
41
+ * option1: { type: 'string', default: 'default' },
42
+ * },
43
+ * },
44
+ * };
45
+ *
46
+ * async onInit() {
47
+ * console.log('Plugin initialized with config:', this.config);
48
+ * }
49
+ *
50
+ * async onDestroy() {
51
+ * console.log('Plugin destroyed');
52
+ * }
53
+ *
54
+ * doSomething() {
55
+ * return 'Hello from my plugin!';
56
+ * }
57
+ * }
58
+ * ```
59
+ */
60
+ export class BasePlugin extends EventEmitter {
61
+ // Override in subclass
62
+ static manifest = {
63
+ id: 'custom.base-plugin',
64
+ name: 'Base Plugin',
65
+ version: '0.0.0',
66
+ description: 'Base plugin class - extend this',
67
+ category: PluginCategory.CORE,
68
+ tier: PluginTier.EXPERIMENTAL,
69
+ capabilities: [],
70
+ configSchema: { type: 'object', properties: {} },
71
+ };
72
+
73
+ constructor(config = {}, context = {}) {
74
+ super();
75
+
76
+ this.config = this._mergeConfig(config);
77
+ this.context = context;
78
+ this.api = context.api || {};
79
+ this.sandbox = context.sandbox;
80
+ this.initialized = false;
81
+ this.stats = {
82
+ invocations: 0,
83
+ errors: 0,
84
+ lastUsed: null,
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Get plugin manifest
90
+ */
91
+ static getManifest() {
92
+ return this.manifest;
93
+ }
94
+
95
+ /**
96
+ * Merge user config with defaults from schema
97
+ */
98
+ _mergeConfig(userConfig) {
99
+ const schema = this.constructor.manifest.configSchema;
100
+ const defaults = {};
101
+
102
+ if (schema?.properties) {
103
+ for (const [key, prop] of Object.entries(schema.properties)) {
104
+ if (prop.default !== undefined) {
105
+ defaults[key] = prop.default;
106
+ }
107
+ }
108
+ }
109
+
110
+ return { ...defaults, ...userConfig };
111
+ }
112
+
113
+ /**
114
+ * Initialize plugin - override in subclass
115
+ */
116
+ async onInit() {
117
+ // Override in subclass
118
+ }
119
+
120
+ /**
121
+ * Destroy plugin - override in subclass
122
+ */
123
+ async onDestroy() {
124
+ // Override in subclass
125
+ }
126
+
127
+ /**
128
+ * Called by loader to initialize
129
+ */
130
+ async init() {
131
+ if (this.initialized) return;
132
+
133
+ try {
134
+ await this.onInit();
135
+ this.initialized = true;
136
+ this.emit('initialized');
137
+ } catch (error) {
138
+ this.stats.errors++;
139
+ throw error;
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Called by loader to destroy
145
+ */
146
+ async destroy() {
147
+ if (!this.initialized) return;
148
+
149
+ try {
150
+ await this.onDestroy();
151
+ this.initialized = false;
152
+ this.emit('destroyed');
153
+ } catch (error) {
154
+ this.stats.errors++;
155
+ throw error;
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Check if plugin has required capability
161
+ */
162
+ requireCapability(capability) {
163
+ if (!this.sandbox?.hasCapability(capability)) {
164
+ throw new Error(`Plugin ${this.constructor.manifest.id} missing capability: ${capability}`);
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Log message with plugin prefix
170
+ */
171
+ log(level, message, data = {}) {
172
+ const prefix = `[${this.constructor.manifest.id}]`;
173
+ console.log(`${prefix} [${level.toUpperCase()}]`, message, data);
174
+ }
175
+
176
+ /**
177
+ * Record plugin usage
178
+ */
179
+ _recordUsage() {
180
+ this.stats.invocations++;
181
+ this.stats.lastUsed = Date.now();
182
+ }
183
+
184
+ /**
185
+ * Get plugin stats
186
+ */
187
+ getStats() {
188
+ return {
189
+ ...this.stats,
190
+ initialized: this.initialized,
191
+ manifest: this.constructor.manifest,
192
+ };
193
+ }
194
+ }
195
+
196
+ // ============================================
197
+ // PLUGIN VALIDATION
198
+ // ============================================
199
+
200
+ /**
201
+ * Validate plugin manifest
202
+ */
203
+ export function validateManifest(manifest) {
204
+ const errors = [];
205
+
206
+ // Required fields
207
+ const required = ['id', 'name', 'version', 'description', 'category', 'tier'];
208
+ for (const field of required) {
209
+ if (!manifest[field]) {
210
+ errors.push(`Missing required field: ${field}`);
211
+ }
212
+ }
213
+
214
+ // ID format: org.plugin-name or category.plugin-name
215
+ if (manifest.id && !/^[a-z0-9-]+\.[a-z0-9-]+$/.test(manifest.id)) {
216
+ errors.push('Invalid ID format. Use: category.plugin-name or org.plugin-name');
217
+ }
218
+
219
+ // Version semver
220
+ if (manifest.version && !/^\d+\.\d+\.\d+/.test(manifest.version)) {
221
+ errors.push('Invalid version format. Use semantic versioning: X.Y.Z');
222
+ }
223
+
224
+ // Category
225
+ if (manifest.category && !Object.values(PluginCategory).includes(manifest.category)) {
226
+ errors.push(`Invalid category. Use one of: ${Object.values(PluginCategory).join(', ')}`);
227
+ }
228
+
229
+ // Tier
230
+ if (manifest.tier && !Object.values(PluginTier).includes(manifest.tier)) {
231
+ errors.push(`Invalid tier. Use one of: ${Object.values(PluginTier).join(', ')}`);
232
+ }
233
+
234
+ // Capabilities
235
+ if (manifest.capabilities) {
236
+ const validCaps = Object.values(Capability);
237
+ for (const cap of manifest.capabilities) {
238
+ if (!validCaps.includes(cap)) {
239
+ errors.push(`Invalid capability: ${cap}`);
240
+ }
241
+ }
242
+ }
243
+
244
+ return {
245
+ valid: errors.length === 0,
246
+ errors,
247
+ };
248
+ }
249
+
250
+ /**
251
+ * Validate plugin class
252
+ */
253
+ export function validatePlugin(PluginClass) {
254
+ const errors = [];
255
+
256
+ // Must extend BasePlugin
257
+ if (!(PluginClass.prototype instanceof BasePlugin)) {
258
+ errors.push('Plugin must extend BasePlugin');
259
+ }
260
+
261
+ // Must have manifest
262
+ if (!PluginClass.manifest) {
263
+ errors.push('Plugin must have static manifest property');
264
+ } else {
265
+ const manifestValidation = validateManifest(PluginClass.manifest);
266
+ errors.push(...manifestValidation.errors);
267
+ }
268
+
269
+ return {
270
+ valid: errors.length === 0,
271
+ errors,
272
+ };
273
+ }
274
+
275
+ // ============================================
276
+ // PLUGIN REGISTRY
277
+ // ============================================
278
+
279
+ /**
280
+ * Registry for custom plugins
281
+ */
282
+ export class PluginRegistry {
283
+ constructor() {
284
+ this.plugins = new Map(); // id -> PluginClass
285
+ this.metadata = new Map(); // id -> metadata
286
+ }
287
+
288
+ /**
289
+ * Register a custom plugin
290
+ */
291
+ register(PluginClass) {
292
+ const validation = validatePlugin(PluginClass);
293
+ if (!validation.valid) {
294
+ throw new Error(`Invalid plugin: ${validation.errors.join(', ')}`);
295
+ }
296
+
297
+ const manifest = PluginClass.manifest;
298
+ const id = manifest.id;
299
+
300
+ if (this.plugins.has(id)) {
301
+ throw new Error(`Plugin already registered: ${id}`);
302
+ }
303
+
304
+ // Generate checksum
305
+ const checksum = createHash('sha256')
306
+ .update(PluginClass.toString())
307
+ .digest('hex');
308
+
309
+ this.plugins.set(id, PluginClass);
310
+ this.metadata.set(id, {
311
+ manifest,
312
+ checksum,
313
+ registeredAt: Date.now(),
314
+ source: 'custom',
315
+ });
316
+
317
+ return { id, checksum };
318
+ }
319
+
320
+ /**
321
+ * Unregister a plugin
322
+ */
323
+ unregister(id) {
324
+ if (!this.plugins.has(id)) {
325
+ return false;
326
+ }
327
+
328
+ this.plugins.delete(id);
329
+ this.metadata.delete(id);
330
+ return true;
331
+ }
332
+
333
+ /**
334
+ * Get plugin class
335
+ */
336
+ get(id) {
337
+ return this.plugins.get(id);
338
+ }
339
+
340
+ /**
341
+ * Check if plugin is registered
342
+ */
343
+ has(id) {
344
+ return this.plugins.has(id);
345
+ }
346
+
347
+ /**
348
+ * List all registered plugins
349
+ */
350
+ list() {
351
+ return Array.from(this.metadata.entries()).map(([id, meta]) => ({
352
+ id,
353
+ ...meta.manifest,
354
+ checksum: meta.checksum,
355
+ registeredAt: meta.registeredAt,
356
+ }));
357
+ }
358
+
359
+ /**
360
+ * Export plugin for distribution
361
+ */
362
+ export(id) {
363
+ const PluginClass = this.plugins.get(id);
364
+ if (!PluginClass) {
365
+ throw new Error(`Plugin not found: ${id}`);
366
+ }
367
+
368
+ const metadata = this.metadata.get(id);
369
+
370
+ return {
371
+ manifest: PluginClass.manifest,
372
+ code: PluginClass.toString(),
373
+ checksum: metadata.checksum,
374
+ exportedAt: Date.now(),
375
+ };
376
+ }
377
+
378
+ /**
379
+ * Import plugin from exported data
380
+ */
381
+ import(exportedPlugin) {
382
+ // Security: Only import from trusted sources in production
383
+ // This is a simplified implementation
384
+ const { manifest, code, checksum } = exportedPlugin;
385
+
386
+ // Verify checksum
387
+ const computedChecksum = createHash('sha256').update(code).digest('hex');
388
+ if (computedChecksum !== checksum) {
389
+ throw new Error('Checksum mismatch - plugin may have been tampered');
390
+ }
391
+
392
+ // Parse and register (in production, use proper sandboxing)
393
+ // WARNING: eval is dangerous - use VM2 or similar in production
394
+ console.warn('WARNING: Importing plugins uses eval - only import from trusted sources');
395
+
396
+ return { imported: true, manifest };
397
+ }
398
+ }
399
+
400
+ // ============================================
401
+ // PLUGIN TEMPLATE GENERATOR
402
+ // ============================================
403
+
404
+ /**
405
+ * Generate plugin template
406
+ */
407
+ export function generatePluginTemplate(options = {}) {
408
+ const {
409
+ id = 'my-org.my-plugin',
410
+ name = 'My Plugin',
411
+ description = 'A custom edge-net plugin',
412
+ category = PluginCategory.CORE,
413
+ tier = PluginTier.EXPERIMENTAL,
414
+ capabilities = [],
415
+ } = options;
416
+
417
+ return `/**
418
+ * ${name}
419
+ *
420
+ * ${description}
421
+ *
422
+ * @module ${id}
423
+ */
424
+
425
+ import { BasePlugin, Capability, PluginCategory, PluginTier } from '@ruvector/edge-net/plugins/sdk';
426
+
427
+ export class ${toPascalCase(id.split('.').pop())}Plugin extends BasePlugin {
428
+ static manifest = {
429
+ id: '${id}',
430
+ name: '${name}',
431
+ version: '1.0.0',
432
+ description: '${description}',
433
+ category: PluginCategory.${Object.keys(PluginCategory).find(k => PluginCategory[k] === category) || 'CORE'},
434
+ tier: PluginTier.${Object.keys(PluginTier).find(k => PluginTier[k] === tier) || 'EXPERIMENTAL'},
435
+ author: 'Your Name',
436
+ capabilities: [${capabilities.map(c => `Capability.${Object.keys(Capability).find(k => Capability[k] === c)}`).join(', ')}],
437
+ dependencies: [],
438
+ configSchema: {
439
+ type: 'object',
440
+ properties: {
441
+ enabled: { type: 'boolean', default: true },
442
+ // Add your config options here
443
+ },
444
+ },
445
+ tags: ['custom'],
446
+ };
447
+
448
+ async onInit() {
449
+ this.log('info', 'Initializing...');
450
+ // Your initialization code here
451
+ }
452
+
453
+ async onDestroy() {
454
+ this.log('info', 'Destroying...');
455
+ // Your cleanup code here
456
+ }
457
+
458
+ // Add your plugin methods here
459
+ exampleMethod() {
460
+ this._recordUsage();
461
+ return 'Hello from ${name}!';
462
+ }
463
+ }
464
+
465
+ export default ${toPascalCase(id.split('.').pop())}Plugin;
466
+ `;
467
+ }
468
+
469
+ function toPascalCase(str) {
470
+ return str.split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join('');
471
+ }
472
+
473
+ // ============================================
474
+ // SINGLETON REGISTRY
475
+ // ============================================
476
+
477
+ let globalRegistry = null;
478
+
479
+ export function getRegistry() {
480
+ if (!globalRegistry) {
481
+ globalRegistry = new PluginRegistry();
482
+ }
483
+ return globalRegistry;
484
+ }
485
+
486
+ export default {
487
+ BasePlugin,
488
+ validateManifest,
489
+ validatePlugin,
490
+ PluginRegistry,
491
+ generatePluginTemplate,
492
+ getRegistry,
493
+ Capability,
494
+ PluginCategory,
495
+ PluginTier,
496
+ };