@devilsdev/rag-pipeline-utils 2.1.11 → 2.2.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.
Files changed (110) hide show
  1. package/.ragrc.schema.json +218 -0
  2. package/README.md +651 -393
  3. package/bin/cli.js +8 -14
  4. package/dist/index.d.ts +237 -0
  5. package/dist/index.mjs +22 -7
  6. package/package.json +17 -13
  7. package/src/ai/adaptive-retrieval.js +191 -0
  8. package/src/ai/federated-learning.js +214 -0
  9. package/src/ai/federation/federated-learning-coordinator.js +345 -0
  10. package/src/ai/federation.js +76 -0
  11. package/src/ai/index-fixed.js +509 -0
  12. package/src/ai/index.js +33 -0
  13. package/src/ai/model-trainer.js +217 -0
  14. package/src/ai/model-training.js +205 -0
  15. package/src/ai/module-template.js +37 -0
  16. package/src/ai/multimodal/multi-modal-processor.js +345 -0
  17. package/src/ai/multimodal-processing.js +268 -0
  18. package/src/ai/multimodal.js +71 -0
  19. package/src/ai/orchestrator.js +70 -0
  20. package/src/ai/retrieval/adaptive-retrieval-engine.js +398 -0
  21. package/src/ai/retrieval-engine.js +69 -0
  22. package/src/ai/training/model-training-orchestrator.js +228 -0
  23. package/src/ci/workflow-analyzer.js +71 -0
  24. package/src/cli/commands/ai-ml.js +967 -0
  25. package/src/cli/commands/docs.js +228 -0
  26. package/src/cli/commands/dx.js +538 -0
  27. package/src/cli/commands/plugin-hub.js +875 -0
  28. package/src/cli/doctor-command.js +639 -0
  29. package/src/cli/enhanced-cli-commands.js +945 -0
  30. package/src/cli/interactive-wizard.js +997 -0
  31. package/src/cli/plugin-marketplace-commands.js +869 -0
  32. package/src/cli.js +17 -0
  33. package/src/config/enhanced-ragrc-schema.js +632 -0
  34. package/src/config/load-config.js +54 -0
  35. package/src/config/load-plugin-config.js +69 -0
  36. package/src/config/normalize-config.js +105 -0
  37. package/src/config/schema-validator.js +315 -0
  38. package/src/config/validate-plugin-schema.js +35 -0
  39. package/src/config/validate-schema.js +113 -0
  40. package/src/contracts/plugin-contracts.js +33 -0
  41. package/src/core/create-pipeline.js +99 -0
  42. package/src/core/dependency-injection.js +242 -0
  43. package/src/core/observability/event-logger.js +241 -0
  44. package/src/core/observability/instrumented-pipeline.js +609 -0
  45. package/src/core/observability/metrics.js +497 -0
  46. package/src/core/observability/tracing.js +781 -0
  47. package/src/core/performance/benchmark.js +516 -0
  48. package/src/core/performance/parallel-processor.js +240 -0
  49. package/src/core/performance/streaming-safeguards.js +611 -0
  50. package/src/core/pipeline-factory.js +60 -0
  51. package/src/core/plugin-contracts.js +89 -0
  52. package/src/core/plugin-marketplace/plugin-metadata.js +485 -0
  53. package/src/core/plugin-marketplace/plugin-publisher.js +574 -0
  54. package/src/core/plugin-marketplace/plugin-registry-format.js +419 -0
  55. package/src/core/plugin-marketplace/version-resolver.js +61 -0
  56. package/src/core/plugin-registry.js +265 -0
  57. package/src/dag/dag-engine.js +954 -0
  58. package/src/dag/dag-node.js +76 -0
  59. package/src/dag/enhanced-dag-engine.js +822 -0
  60. package/src/dx/index.js +40 -0
  61. package/src/dx/integration-templates.js +665 -0
  62. package/src/dx/performance-profiler.js +968 -0
  63. package/src/dx/realtime-debugger.js +755 -0
  64. package/src/dx/visual-pipeline-builder.js +578 -0
  65. package/src/ecosystem/plugin-analytics-dashboard.js +778 -0
  66. package/src/ecosystem/plugin-certification.js +580 -0
  67. package/src/ecosystem/plugin-hub.js +821 -0
  68. package/src/enterprise/audit-logging.js +967 -0
  69. package/src/enterprise/data-governance.js +408 -0
  70. package/src/enterprise/multi-tenancy.js +496 -0
  71. package/src/enterprise/sso-integration.js +903 -0
  72. package/src/evaluate/evaluator.js +52 -0
  73. package/src/evaluate/scoring.js +91 -0
  74. package/src/index.js +67 -0
  75. package/src/ingest.js +59 -0
  76. package/src/loader/csv-loader.js +38 -0
  77. package/src/loader/directory-loader.js +48 -0
  78. package/src/loader/html-loader.js +62 -0
  79. package/src/loader/markdown-loader.js +67 -0
  80. package/src/mocks/openai-embedder.js +119 -0
  81. package/src/mocks/openai-llm.js +77 -0
  82. package/src/mocks/pdf-loader.js +21 -0
  83. package/src/mocks/pinecone-retriever.js +106 -0
  84. package/src/observability/audit-logger.js +758 -0
  85. package/src/observability/dashboard-config.json +675 -0
  86. package/src/observability/event-logger.js +310 -0
  87. package/src/observability/metrics.js +691 -0
  88. package/src/observability/security-monitor.js +830 -0
  89. package/src/observability/slo-monitor.js +319 -0
  90. package/src/observability/telemetry.js +686 -0
  91. package/src/pipeline/pipeline.js +18 -0
  92. package/src/plugins/sample-embedder.js +53 -0
  93. package/src/plugins/sample-llm.js +30 -0
  94. package/src/plugins/sample-loader.js +24 -0
  95. package/src/plugins/sample-retriever.js +84 -0
  96. package/src/query.js +117 -0
  97. package/src/reranker/llm-reranker.js +85 -0
  98. package/src/scripts/script-utilities.js +36 -0
  99. package/src/security/plugin-sandbox.js +586 -0
  100. package/src/security/plugin-signature-verifier.js +414 -0
  101. package/src/security/validator.js +36 -0
  102. package/src/utils/ci/diagnostic-reporter.js +57 -0
  103. package/src/utils/logger.js +211 -0
  104. package/src/utils/performance-monitor.js +561 -0
  105. package/src/utils/plugin-scaffolder.js +447 -0
  106. package/src/utils/retry-policy.js +443 -0
  107. package/src/utils/retry.js +107 -0
  108. package/src/utils/safe-expression-evaluator.js +579 -0
  109. package/src/utils/structured-logger.js +361 -0
  110. package/src/utils/validate-plugin-contract.js +42 -0
@@ -0,0 +1,821 @@
1
+ /**
2
+ * Community Plugin Hub
3
+ * Public registry with ratings, reviews, downloads, and discovery
4
+ */
5
+
6
+ const fs = require('fs').promises; // eslint-disable-line global-require
7
+ const path = require('path'); // eslint-disable-line global-require
8
+ const crypto = require('crypto'); // eslint-disable-line global-require
9
+ const { EventEmitter } = require('events'); // eslint-disable-line global-require
10
+
11
+ function pickFetch(pref) {
12
+ // Prefer a provided fetch if it is a function; otherwise fall back to globalThis.fetch if available.
13
+ if (typeof pref === 'function') return pref;
14
+ if (typeof globalThis.fetch === 'function') return globalThis.fetch;
15
+ return null;
16
+ }
17
+
18
+ async function parseJsonSafe(res) {
19
+ // Handle both mocked and real fetch responses:
20
+ // some tests use only res.json(); others only res.text(); accommodate both.
21
+ if (res && typeof res.json === 'function') {
22
+ try {
23
+ return await res.json();
24
+ } catch (_) {
25
+ /* fall back */
26
+ }
27
+ }
28
+ if (res && typeof res.text === 'function') {
29
+ const t = await res.text();
30
+ if (!t) return {};
31
+ try {
32
+ return JSON.parse(t);
33
+ } catch (_) {
34
+ return { raw: t };
35
+ }
36
+ }
37
+ // If tests returned a plain object (already "parsed"):
38
+ if (res && typeof res === 'object' && !('ok' in res)) {
39
+ return res;
40
+ }
41
+ return {};
42
+ }
43
+
44
+ class PluginHub extends EventEmitter {
45
+ constructor(_options = {}) {
46
+ super();
47
+
48
+ this._config = {
49
+ registryUrl: _options.registryUrl || 'https://registry.rag-pipeline.dev',
50
+ localCacheDir:
51
+ _options.localCacheDir || path.join(process.cwd(), '.rag-cache'),
52
+ apiKey: _options.apiKey || process.env.RAG_PLUGIN_HUB_API_KEY,
53
+ userAgent: _options.userAgent || 'rag-pipeline-utils/2.1.8',
54
+ timeout: _options.timeout || 30000,
55
+ maxRetries: _options.maxRetries || 3,
56
+ fetch: _options.fetch, // optional injected fetch for tests
57
+ ..._options,
58
+ };
59
+
60
+ this._fetch = pickFetch(this._config.fetch);
61
+ this.cache = new Map();
62
+ this.analytics = new PluginAnalytics(this._config);
63
+ this.sandbox = new PluginSandbox(this._config);
64
+ }
65
+
66
+ /**
67
+ * Search plugins in the community hub
68
+ */
69
+ async searchPlugins(query, _options = {}) {
70
+ const searchParams = {
71
+ q: query,
72
+ category: _options.category,
73
+ tags: _options.tags,
74
+ author: _options.author,
75
+ minRating: _options.minRating || 0,
76
+ verified: _options.verified,
77
+ limit: _options.limit || 20,
78
+ offset: _options.offset || 0,
79
+ sortBy: _options.sortBy || 'relevance', // relevance, downloads, rating, updated
80
+ };
81
+
82
+ try {
83
+ const response = await this._makeRequest(
84
+ 'GET',
85
+ '/plugins/search',
86
+ searchParams,
87
+ );
88
+
89
+ // Track search analytics
90
+ this.analytics.trackSearch(
91
+ query,
92
+ Array.isArray(response.results) ? response.results.length : 0,
93
+ );
94
+
95
+ return {
96
+ results: (response.results || []).map((plugin) =>
97
+ this._normalizePluginInfo(plugin),
98
+ ),
99
+ total: response.total ?? 0,
100
+ hasMore: response.hasMore ?? false,
101
+ facets: response.facets || {},
102
+ };
103
+ } catch (error) {
104
+ this.emit('error', { type: 'search_failed', query, error });
105
+ throw new Error(`Plugin search failed: ${error.message}`);
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Get detailed plugin information
111
+ */
112
+ async getPluginInfo(pluginId) {
113
+ const cacheKey = `plugin:${pluginId}`;
114
+
115
+ // Check cache first
116
+ if (this.cache.has(cacheKey)) {
117
+ const cached = this.cache.get(cacheKey);
118
+ if (Date.now() - cached.timestamp < 300000) {
119
+ // 5 minutes
120
+ return cached.data;
121
+ }
122
+ }
123
+
124
+ try {
125
+ const response = await this._makeRequest('GET', `/plugins/${pluginId}`);
126
+ const pluginInfo = this._normalizePluginInfo(response);
127
+
128
+ // Cache the result
129
+ this.cache.set(cacheKey, {
130
+ data: pluginInfo,
131
+ timestamp: Date.now(),
132
+ });
133
+
134
+ // Track view analytics
135
+ this.analytics.trackPluginView(pluginId);
136
+
137
+ return pluginInfo;
138
+ } catch (error) {
139
+ this.emit('error', { type: 'plugin_info_failed', pluginId, error });
140
+ throw new Error(`Failed to get plugin info: ${error.message}`);
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Install plugin from the hub
146
+ */
147
+ async installPlugin(pluginId, version = 'latest', _options = {}) {
148
+ const installId = crypto.randomUUID();
149
+ const startedAt = Date.now();
150
+
151
+ try {
152
+ this.emit('install_start', { pluginId, version, installId });
153
+
154
+ // Get plugin info and verify
155
+ const pluginInfo = await this.getPluginInfo(pluginId);
156
+ if (!pluginInfo) throw new Error(`Plugin ${pluginId} not found`);
157
+
158
+ // Check certification if required
159
+ if (_options.requireCertified && !pluginInfo.certified) {
160
+ throw new Error(`Plugin ${pluginId} is not certified`);
161
+ }
162
+
163
+ // Security scan in sandbox
164
+ if (_options.securityScan !== false) {
165
+ this.emit('install_progress', { installId, stage: 'security_scan' });
166
+ await this.sandbox.scanPlugin(pluginInfo);
167
+ }
168
+
169
+ // Download plugin
170
+ this.emit('install_progress', { installId, stage: 'downloading' });
171
+ const downloadUrl = await this._getDownloadUrl(pluginId, version);
172
+ const pluginData = await this._downloadPlugin(downloadUrl);
173
+
174
+ // Verify integrity
175
+ this.emit('install_progress', { installId, stage: 'verifying' });
176
+ await this._verifyPluginIntegrity(pluginData, pluginInfo.checksums);
177
+
178
+ // Install in sandbox first
179
+ this.emit('install_progress', { installId, stage: 'sandbox_install' });
180
+ const sandboxResult = await this.sandbox.installPlugin(pluginData, {
181
+ timeout: _options.sandboxTimeout || 30000,
182
+ memoryLimit: _options.memoryLimit || '512MB',
183
+ networkAccess: _options.networkAccess || false,
184
+ });
185
+
186
+ if (!sandboxResult.success) {
187
+ throw new Error(`Sandbox installation failed: ${sandboxResult.error}`);
188
+ }
189
+
190
+ // Install to main system
191
+ this.emit('install_progress', { installId, stage: 'installing' });
192
+ const installPath = await this._installPluginToSystem(
193
+ pluginData,
194
+ pluginInfo,
195
+ );
196
+
197
+ // Track installation analytics
198
+ this.analytics.trackInstallation(pluginId, version, {
199
+ success: true,
200
+ installTime: Date.now() - startedAt,
201
+ });
202
+
203
+ this.emit('install_complete', {
204
+ pluginId,
205
+ version,
206
+ installId,
207
+ installPath,
208
+ pluginInfo,
209
+ });
210
+
211
+ return {
212
+ success: true,
213
+ pluginId,
214
+ version,
215
+ installPath,
216
+ pluginInfo,
217
+ };
218
+ } catch (error) {
219
+ this.analytics.trackInstallation(pluginId, version, {
220
+ success: false,
221
+ error: error.message,
222
+ });
223
+
224
+ this.emit('install_error', { pluginId, version, installId, error });
225
+ throw error;
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Publish plugin to the hub
231
+ */
232
+ async publishPlugin(pluginPath, _options = {}) {
233
+ const publishId = crypto.randomUUID();
234
+
235
+ try {
236
+ this.emit('publish_start', { pluginPath, publishId });
237
+
238
+ // Validate plugin structure
239
+ this.emit('publish_progress', { publishId, stage: 'validation' });
240
+ const pluginInfo = await this._validatePluginForPublish(pluginPath);
241
+
242
+ // Security scan
243
+ this.emit('publish_progress', { publishId, stage: 'security_scan' });
244
+ const securityReport = await this.sandbox.scanPlugin(pluginInfo);
245
+ if (securityReport.risk === 'high') {
246
+ throw new Error(
247
+ `Plugin failed security scan: ${securityReport.issues.join(', ')}`,
248
+ );
249
+ }
250
+
251
+ // Package plugin
252
+ this.emit('publish_progress', { publishId, stage: 'packaging' });
253
+ const packageData = await this._packagePlugin(pluginPath, pluginInfo);
254
+
255
+ // Upload to registry
256
+ this.emit('publish_progress', { publishId, stage: 'uploading' });
257
+ const response = await this._uploadPlugin(packageData, {
258
+ ..._options,
259
+ securityReport,
260
+ });
261
+
262
+ this.emit('publish_complete', {
263
+ publishId,
264
+ pluginId: response.pluginId,
265
+ version: response.version,
266
+ url: response.url,
267
+ });
268
+
269
+ return response;
270
+ } catch (error) {
271
+ this.emit('publish_error', { pluginPath, publishId, error });
272
+ throw error;
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Rate and review a plugin
278
+ */
279
+ async ratePlugin(pluginId, rating, review = null) {
280
+ if (rating < 1 || rating > 5) {
281
+ throw new Error('Rating must be between 1 and 5');
282
+ }
283
+
284
+ try {
285
+ const response = await this._makeRequest(
286
+ 'POST',
287
+ `/plugins/${pluginId}/reviews`,
288
+ {
289
+ rating,
290
+ review,
291
+ version: await this._getInstalledVersion(pluginId),
292
+ },
293
+ );
294
+
295
+ this.analytics.trackReview(pluginId, rating);
296
+ return response;
297
+ } catch (error) {
298
+ this.emit('error', { type: 'review_failed', pluginId, error });
299
+ throw new Error(`Failed to submit review: ${error.message}`);
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Get plugin reviews and ratings
305
+ */
306
+ async getPluginReviews(pluginId, _options = {}) {
307
+ try {
308
+ const response = await this._makeRequest(
309
+ 'GET',
310
+ `/plugins/${pluginId}/reviews`,
311
+ {
312
+ limit: _options.limit || 10,
313
+ offset: _options.offset || 0,
314
+ sortBy: _options.sortBy || 'helpful',
315
+ },
316
+ );
317
+
318
+ return response;
319
+ } catch (error) {
320
+ throw new Error(`Failed to get reviews: ${error.message}`);
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Get trending plugins
326
+ */
327
+ async getTrendingPlugins(_options = {}) {
328
+ try {
329
+ const response = await this._makeRequest('GET', '/plugins/trending', {
330
+ period: _options.period || 'week', // day, week, month
331
+ category: _options.category,
332
+ limit: _options.limit || 20,
333
+ });
334
+
335
+ return (response.results || []).map((plugin) =>
336
+ this._normalizePluginInfo(plugin),
337
+ );
338
+ } catch (error) {
339
+ throw new Error(`Failed to get trending plugins: ${error.message}`);
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Get user's installed plugins
345
+ */
346
+ async getInstalledPlugins() {
347
+ try {
348
+ const pluginsDir = path.join(this._config.localCacheDir, 'plugins');
349
+ const entries = await fs.readdir(pluginsDir, { withFileTypes: true });
350
+
351
+ const installed = [];
352
+
353
+ for (const entry of entries) {
354
+ if (entry.isDirectory()) {
355
+ const metadataPath = path.join(
356
+ pluginsDir,
357
+ entry.name,
358
+ 'metadata.json',
359
+ );
360
+
361
+ try {
362
+ const metadata = JSON.parse(
363
+ await fs.readFile(metadataPath, 'utf8'),
364
+ );
365
+ installed.push({
366
+ ...metadata,
367
+ installPath: path.join(pluginsDir, entry.name),
368
+ lastUsed: await this.analytics.getLastUsed(metadata.id),
369
+ });
370
+ } catch {
371
+ // Skip invalid plugins
372
+ continue; // eslint-disable-line no-continue
373
+ }
374
+ }
375
+ }
376
+
377
+ return installed;
378
+ } catch {
379
+ return [];
380
+ }
381
+ }
382
+
383
+ // ------------------------------
384
+ // Private methods
385
+ // ------------------------------
386
+ async _makeRequest(method, endpoint, params = {}) {
387
+ const doFetch = pickFetch(this._config.fetch);
388
+ if (!doFetch) throw new Error('fetch is not available in this environment');
389
+
390
+ const url = new URL(endpoint, this._config.registryUrl);
391
+
392
+ if (method === 'GET' && Object.keys(params).length > 0) {
393
+ Object.entries(params).forEach(([key, value]) => {
394
+ if (value !== undefined && value !== null) {
395
+ url.searchParams.append(key, String(value));
396
+ }
397
+ });
398
+ }
399
+
400
+ const headers = {
401
+ 'User-Agent': this._config.userAgent,
402
+ Accept: 'application/json',
403
+ 'Content-Type': 'application/json',
404
+ };
405
+ if (this._config.apiKey)
406
+ headers.Authorization = `Bearer ${this._config.apiKey}`;
407
+
408
+ const options = { method, headers };
409
+
410
+ if (method !== 'GET' && Object.keys(params).length > 0) {
411
+ options.body = JSON.stringify(params);
412
+ }
413
+
414
+ let lastError;
415
+ for (let attempt = 0; attempt < this._config.maxRetries; attempt++) {
416
+ const controller =
417
+ typeof AbortController !== 'undefined' ? new AbortController() : null;
418
+ const t = this._config.timeout || 0;
419
+ let timeoutId = null;
420
+ if (controller && t > 0) {
421
+ timeoutId = setTimeout(() => controller.abort(), t);
422
+ // In Node/Jest, this prevents timers from keeping the process alive
423
+ if (typeof timeoutId?.unref === 'function') timeoutId.unref();
424
+ }
425
+
426
+ try {
427
+ const res = await doFetch(url.toString(), {
428
+ ...options,
429
+ signal: controller ? controller.signal : undefined,
430
+ });
431
+
432
+ if (!res || !('ok' in res)) {
433
+ throw new Error('Invalid fetch response');
434
+ }
435
+
436
+ if (!res.ok) {
437
+ // Try to surface server message
438
+ let msg = `HTTP ${res.status}: ${res.statusText}`;
439
+ try {
440
+ const errData = await parseJsonSafe(res);
441
+ if (errData && errData.message) msg = errData.message;
442
+ } catch {
443
+ /* ignore */
444
+ }
445
+ throw new Error(msg);
446
+ }
447
+
448
+ // Success path: parse JSON flexibly
449
+ const data = await parseJsonSafe(res);
450
+ return data;
451
+ } catch (error) {
452
+ lastError = error;
453
+ // Backoff before retry (except last attempt)
454
+ if (attempt < this._config.maxRetries - 1) {
455
+ const wait = Math.pow(2, attempt) * 100; // fast-ish backoff for tests
456
+ await new Promise((r) => setTimeout(r, wait));
457
+ }
458
+ } finally {
459
+ if (timeoutId) clearTimeout(timeoutId);
460
+ }
461
+ }
462
+
463
+ throw lastError;
464
+ }
465
+
466
+ _normalizePluginInfo(plugin) {
467
+ return {
468
+ id: plugin.id,
469
+ name: plugin.name,
470
+ version: plugin.version,
471
+ description: plugin.description,
472
+ author: plugin.author,
473
+ category: plugin.category,
474
+ tags: plugin.tags || [],
475
+ rating: plugin.rating || 0,
476
+ reviewCount: plugin.reviewCount || 0,
477
+ downloadCount: plugin.downloadCount || 0,
478
+ certified: plugin.certified || false,
479
+ certificationId: plugin.certificationId,
480
+ verifiedPublisher: plugin.verifiedPublisher || false,
481
+ lastUpdated: plugin.lastUpdated,
482
+ createdAt: plugin.createdAt,
483
+ homepage: plugin.homepage,
484
+ repository: plugin.repository,
485
+ license: plugin.license,
486
+ dependencies: plugin.dependencies || [],
487
+ compatibility: plugin.compatibility || {},
488
+ screenshots: plugin.screenshots || [],
489
+ documentation: plugin.documentation,
490
+ checksums: plugin.checksums || {},
491
+ };
492
+ }
493
+
494
+ async _getDownloadUrl(pluginId, version) {
495
+ const response = await this._makeRequest(
496
+ 'GET',
497
+ `/plugins/${pluginId}/download`,
498
+ { version },
499
+ );
500
+ return response.downloadUrl;
501
+ }
502
+
503
+ async _downloadPlugin(url) {
504
+ const doFetch = pickFetch(this._config.fetch);
505
+ if (!doFetch) throw new Error('fetch is not available in this environment');
506
+
507
+ const res = await doFetch(url);
508
+ if (!res || !res.ok)
509
+ throw new Error(
510
+ `Download failed: ${res ? res.statusText : 'no response'}`,
511
+ );
512
+
513
+ if (typeof res.arrayBuffer === 'function') {
514
+ const ab = await res.arrayBuffer();
515
+ return Buffer.from(ab);
516
+ }
517
+
518
+ if (typeof res.buffer === 'function') {
519
+ return await res.buffer();
520
+ }
521
+
522
+ // Very mocked case: tests might hand back a plain Buffer-like in res.body
523
+ if (res.body) return Buffer.from(res.body);
524
+
525
+ throw new Error('Unsupported response body for download');
526
+ }
527
+
528
+ async _verifyPluginIntegrity(data, checksums) {
529
+ if (!checksums || !checksums.sha256) {
530
+ throw new Error('No checksums available for verification');
531
+ }
532
+
533
+ const hash = crypto.createHash('sha256');
534
+ hash.update(Buffer.isBuffer(data) ? data : Buffer.from(data));
535
+ const calculatedHash = hash.digest('hex');
536
+
537
+ if (calculatedHash !== checksums.sha256) {
538
+ throw new Error('Plugin integrity verification failed');
539
+ }
540
+ }
541
+
542
+ async _installPluginToSystem(pluginData, pluginInfo) {
543
+ const installDir = path.join(
544
+ this._config.localCacheDir,
545
+ 'plugins',
546
+ pluginInfo.id,
547
+ );
548
+
549
+ // Create installation directory
550
+ await fs.mkdir(installDir, { recursive: true });
551
+
552
+ // Simulated install: write metadata only
553
+ const metadataPath = path.join(installDir, 'metadata.json');
554
+ await fs.writeFile(
555
+ metadataPath,
556
+ JSON.stringify(
557
+ {
558
+ ...pluginInfo,
559
+ installedAt: new Date().toISOString(),
560
+ installPath: installDir,
561
+ size: Buffer.isBuffer(pluginData) ? pluginData.length : 0,
562
+ },
563
+ null,
564
+ 2,
565
+ ),
566
+ );
567
+
568
+ return installDir;
569
+ }
570
+
571
+ async _validatePluginForPublish(pluginPath) {
572
+ // Validate plugin structure and metadata
573
+ const packageJsonPath = path.join(pluginPath, 'package.json');
574
+ const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
575
+
576
+ // Required fields validation
577
+ const required = ['name', 'version', 'description', 'main'];
578
+ for (const field of required) {
579
+ if (!packageJson[field]) {
580
+ throw new Error(`Missing required field: ${field}`);
581
+ }
582
+ }
583
+
584
+ // Plugin-specific validation
585
+ if (!packageJson.ragPlugin) {
586
+ throw new Error('Missing ragPlugin configuration in package.json');
587
+ }
588
+
589
+ return {
590
+ ...packageJson,
591
+ pluginConfig: packageJson.ragPlugin,
592
+ };
593
+ }
594
+
595
+ async _packagePlugin(pluginPath, pluginInfo) {
596
+ // Simulate packaging
597
+ return {
598
+ metadata: pluginInfo,
599
+ files: [], // Would contain actual file data in real implementation
600
+ };
601
+ }
602
+
603
+ async _uploadPlugin(packageData, _options) {
604
+ return await this._makeRequest('POST', '/plugins/publish', {
605
+ ...packageData,
606
+ ..._options,
607
+ });
608
+ }
609
+
610
+ async _getInstalledVersion(pluginId) {
611
+ try {
612
+ const metadataPath = path.join(
613
+ this._config.localCacheDir,
614
+ 'plugins',
615
+ pluginId,
616
+ 'metadata.json',
617
+ );
618
+ const metadata = JSON.parse(await fs.readFile(metadataPath, 'utf8'));
619
+ return metadata.version;
620
+ } catch {
621
+ return null;
622
+ }
623
+ }
624
+ }
625
+
626
+ // Plugin Analytics class for usage tracking
627
+ class PluginAnalytics {
628
+ constructor(_config) {
629
+ this._config = _config;
630
+ this._fetch = pickFetch(this._config.fetch);
631
+ this.events = [];
632
+
633
+ // In tests, avoid background timers that outlive the suite
634
+ if (process.env.NODE_ENV !== 'test') {
635
+ this.flushInterval = setInterval(() => this._flush(), 60000); // Flush every minute
636
+ if (typeof this.flushInterval?.unref === 'function')
637
+ this.flushInterval.unref();
638
+ }
639
+ }
640
+
641
+ trackSearch(query, resultCount) {
642
+ this._track('search', { query, resultCount, timestamp: Date.now() });
643
+ }
644
+
645
+ trackPluginView(pluginId) {
646
+ this._track('plugin_view', { pluginId, timestamp: Date.now() });
647
+ }
648
+
649
+ trackInstallation(pluginId, version, result) {
650
+ this._track('installation', {
651
+ pluginId,
652
+ version,
653
+ success: !!result.success,
654
+ error: result.error,
655
+ installTime: result.installTime,
656
+ timestamp: Date.now(),
657
+ });
658
+ }
659
+
660
+ trackReview(pluginId, rating) {
661
+ this._track('review', { pluginId, rating, timestamp: Date.now() });
662
+ }
663
+
664
+ async getLastUsed(pluginId) {
665
+ const usageEvents = this.events.filter(
666
+ (e) => e.type === 'plugin_usage' && e.data.pluginId === pluginId,
667
+ );
668
+ if (usageEvents.length === 0) return null;
669
+ return Math.max(...usageEvents.map((e) => e.timestamp));
670
+ }
671
+
672
+ _track(type, data) {
673
+ this.events.push({
674
+ type,
675
+ data,
676
+ sessionId: this._getSessionId(),
677
+ timestamp: Date.now(),
678
+ });
679
+
680
+ // Keep only recent events in memory
681
+ if (this.events.length > 1000) {
682
+ this.events = this.events.slice(-500);
683
+ }
684
+ }
685
+
686
+ async _flush() {
687
+ if (this.events.length === 0) return;
688
+
689
+ // capture and clear
690
+ const eventsToFlush = [...this.events];
691
+ this.events = [];
692
+
693
+ // If there's no fetch (e.g., tests didn’t mock global fetch), don't crash.
694
+ const doFetch = pickFetch(this._config.fetch);
695
+ if (!doFetch) return;
696
+
697
+ try {
698
+ await doFetch(
699
+ `${this._config.registryUrl.replace(/\/+$/, '')}/analytics`,
700
+ {
701
+ method: 'POST',
702
+ headers: {
703
+ 'Content-Type': 'application/json',
704
+ 'User-Agent': this._config.userAgent,
705
+ },
706
+ body: JSON.stringify({ events: eventsToFlush }),
707
+ },
708
+ );
709
+ } catch (error) {
710
+ // Silence warnings during tests to avoid "Cannot log after tests are done"
711
+ if (process.env.NODE_ENV !== 'test') {
712
+ // eslint-disable-next-line no-console
713
+ console.warn('Analytics flush failed:', error.message);
714
+ }
715
+ }
716
+ }
717
+
718
+ _getSessionId() {
719
+ if (!this.sessionId) this.sessionId = crypto.randomUUID();
720
+ return this.sessionId;
721
+ }
722
+ }
723
+
724
+ // Plugin Sandbox class for isolated execution
725
+ class PluginSandbox {
726
+ constructor(_config) {
727
+ this._config = _config;
728
+ }
729
+
730
+ async scanPlugin(pluginInfo) {
731
+ // Security scan implementation
732
+ const risks = [];
733
+ const warnings = [];
734
+
735
+ // Check for suspicious patterns
736
+ if (pluginInfo.dependencies) {
737
+ for (const dep of Object.keys(pluginInfo.dependencies)) {
738
+ if (this._isSuspiciousDependency(dep)) {
739
+ risks.push(`Suspicious dependency: ${dep}`);
740
+ }
741
+ }
742
+ }
743
+
744
+ // Check permissions
745
+ if (pluginInfo.pluginConfig?.permissions) {
746
+ for (const permission of pluginInfo.pluginConfig.permissions) {
747
+ if (this._isHighRiskPermission(permission)) {
748
+ risks.push(`High-risk permission: ${permission}`);
749
+ }
750
+ }
751
+ }
752
+
753
+ return {
754
+ risk: risks.length > 0 ? 'high' : warnings.length > 0 ? 'medium' : 'low',
755
+ issues: [...risks, ...warnings],
756
+ recommendations: this._generateRecommendations(risks, warnings),
757
+ };
758
+ }
759
+
760
+ async installPlugin(_pluginData, _options) {
761
+ // Sandbox installation simulation
762
+ const sandboxId = crypto.randomUUID();
763
+
764
+ try {
765
+ const timeout = Math.max(1, Number(_options?.timeout || 30000));
766
+ const result = await this._runInSandbox(
767
+ sandboxId,
768
+ async () => {
769
+ await new Promise((resolve) => setTimeout(resolve, 25)); // faster in tests
770
+ return { success: true };
771
+ },
772
+ { timeout },
773
+ );
774
+
775
+ return result;
776
+ } catch (error) {
777
+ return { success: false, error: error.message };
778
+ }
779
+ }
780
+
781
+ _isSuspiciousDependency(dep) {
782
+ const suspicious = ['eval', 'vm2', 'child_process', 'fs-extra', 'shelljs'];
783
+ return suspicious.some((s) => String(dep).includes(s));
784
+ }
785
+
786
+ _isHighRiskPermission(permission) {
787
+ const highRisk = [
788
+ 'filesystem:write',
789
+ 'network:external',
790
+ 'process:spawn',
791
+ 'system:admin',
792
+ ];
793
+ return highRisk.includes(permission);
794
+ }
795
+
796
+ _generateRecommendations(risks, warnings) {
797
+ const recommendations = [];
798
+ if (risks.length > 0) {
799
+ recommendations.push('Review plugin source code before installation');
800
+ recommendations.push('Install in restricted environment');
801
+ }
802
+ if (warnings.length > 0) {
803
+ recommendations.push('Monitor plugin behavior after installation');
804
+ }
805
+ return recommendations;
806
+ }
807
+
808
+ async _runInSandbox(_sandboxId, fn, _options) {
809
+ const timeout = Math.max(1, Number(_options?.timeout || 30000));
810
+ const timerPromise = new Promise((_, reject) =>
811
+ setTimeout(() => reject(new Error('Sandbox timeout')), timeout),
812
+ );
813
+ return Promise.race([fn(), timerPromise]);
814
+ }
815
+ }
816
+
817
+ module.exports = {
818
+ PluginHub,
819
+ PluginAnalytics,
820
+ PluginSandbox,
821
+ };