@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.
- package/.ragrc.schema.json +218 -0
- package/README.md +651 -393
- package/bin/cli.js +8 -14
- package/dist/index.d.ts +237 -0
- package/dist/index.mjs +22 -7
- package/package.json +17 -13
- package/src/ai/adaptive-retrieval.js +191 -0
- package/src/ai/federated-learning.js +214 -0
- package/src/ai/federation/federated-learning-coordinator.js +345 -0
- package/src/ai/federation.js +76 -0
- package/src/ai/index-fixed.js +509 -0
- package/src/ai/index.js +33 -0
- package/src/ai/model-trainer.js +217 -0
- package/src/ai/model-training.js +205 -0
- package/src/ai/module-template.js +37 -0
- package/src/ai/multimodal/multi-modal-processor.js +345 -0
- package/src/ai/multimodal-processing.js +268 -0
- package/src/ai/multimodal.js +71 -0
- package/src/ai/orchestrator.js +70 -0
- package/src/ai/retrieval/adaptive-retrieval-engine.js +398 -0
- package/src/ai/retrieval-engine.js +69 -0
- package/src/ai/training/model-training-orchestrator.js +228 -0
- package/src/ci/workflow-analyzer.js +71 -0
- package/src/cli/commands/ai-ml.js +967 -0
- package/src/cli/commands/docs.js +228 -0
- package/src/cli/commands/dx.js +538 -0
- package/src/cli/commands/plugin-hub.js +875 -0
- package/src/cli/doctor-command.js +639 -0
- package/src/cli/enhanced-cli-commands.js +945 -0
- package/src/cli/interactive-wizard.js +997 -0
- package/src/cli/plugin-marketplace-commands.js +869 -0
- package/src/cli.js +17 -0
- package/src/config/enhanced-ragrc-schema.js +632 -0
- package/src/config/load-config.js +54 -0
- package/src/config/load-plugin-config.js +69 -0
- package/src/config/normalize-config.js +105 -0
- package/src/config/schema-validator.js +315 -0
- package/src/config/validate-plugin-schema.js +35 -0
- package/src/config/validate-schema.js +113 -0
- package/src/contracts/plugin-contracts.js +33 -0
- package/src/core/create-pipeline.js +99 -0
- package/src/core/dependency-injection.js +242 -0
- package/src/core/observability/event-logger.js +241 -0
- package/src/core/observability/instrumented-pipeline.js +609 -0
- package/src/core/observability/metrics.js +497 -0
- package/src/core/observability/tracing.js +781 -0
- package/src/core/performance/benchmark.js +516 -0
- package/src/core/performance/parallel-processor.js +240 -0
- package/src/core/performance/streaming-safeguards.js +611 -0
- package/src/core/pipeline-factory.js +60 -0
- package/src/core/plugin-contracts.js +89 -0
- package/src/core/plugin-marketplace/plugin-metadata.js +485 -0
- package/src/core/plugin-marketplace/plugin-publisher.js +574 -0
- package/src/core/plugin-marketplace/plugin-registry-format.js +419 -0
- package/src/core/plugin-marketplace/version-resolver.js +61 -0
- package/src/core/plugin-registry.js +265 -0
- package/src/dag/dag-engine.js +954 -0
- package/src/dag/dag-node.js +76 -0
- package/src/dag/enhanced-dag-engine.js +822 -0
- package/src/dx/index.js +40 -0
- package/src/dx/integration-templates.js +665 -0
- package/src/dx/performance-profiler.js +968 -0
- package/src/dx/realtime-debugger.js +755 -0
- package/src/dx/visual-pipeline-builder.js +578 -0
- package/src/ecosystem/plugin-analytics-dashboard.js +778 -0
- package/src/ecosystem/plugin-certification.js +580 -0
- package/src/ecosystem/plugin-hub.js +821 -0
- package/src/enterprise/audit-logging.js +967 -0
- package/src/enterprise/data-governance.js +408 -0
- package/src/enterprise/multi-tenancy.js +496 -0
- package/src/enterprise/sso-integration.js +903 -0
- package/src/evaluate/evaluator.js +52 -0
- package/src/evaluate/scoring.js +91 -0
- package/src/index.js +67 -0
- package/src/ingest.js +59 -0
- package/src/loader/csv-loader.js +38 -0
- package/src/loader/directory-loader.js +48 -0
- package/src/loader/html-loader.js +62 -0
- package/src/loader/markdown-loader.js +67 -0
- package/src/mocks/openai-embedder.js +119 -0
- package/src/mocks/openai-llm.js +77 -0
- package/src/mocks/pdf-loader.js +21 -0
- package/src/mocks/pinecone-retriever.js +106 -0
- package/src/observability/audit-logger.js +758 -0
- package/src/observability/dashboard-config.json +675 -0
- package/src/observability/event-logger.js +310 -0
- package/src/observability/metrics.js +691 -0
- package/src/observability/security-monitor.js +830 -0
- package/src/observability/slo-monitor.js +319 -0
- package/src/observability/telemetry.js +686 -0
- package/src/pipeline/pipeline.js +18 -0
- package/src/plugins/sample-embedder.js +53 -0
- package/src/plugins/sample-llm.js +30 -0
- package/src/plugins/sample-loader.js +24 -0
- package/src/plugins/sample-retriever.js +84 -0
- package/src/query.js +117 -0
- package/src/reranker/llm-reranker.js +85 -0
- package/src/scripts/script-utilities.js +36 -0
- package/src/security/plugin-sandbox.js +586 -0
- package/src/security/plugin-signature-verifier.js +414 -0
- package/src/security/validator.js +36 -0
- package/src/utils/ci/diagnostic-reporter.js +57 -0
- package/src/utils/logger.js +211 -0
- package/src/utils/performance-monitor.js +561 -0
- package/src/utils/plugin-scaffolder.js +447 -0
- package/src/utils/retry-policy.js +443 -0
- package/src/utils/retry.js +107 -0
- package/src/utils/safe-expression-evaluator.js +579 -0
- package/src/utils/structured-logger.js +361 -0
- 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
|
+
};
|