@sparkleideas/plugins 3.0.0-alpha.10
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/README.md +401 -0
- package/__tests__/collection-manager.test.ts +332 -0
- package/__tests__/dependency-graph.test.ts +434 -0
- package/__tests__/enhanced-plugin-registry.test.ts +488 -0
- package/__tests__/plugin-registry.test.ts +368 -0
- package/__tests__/ruvector-bridge.test.ts +2429 -0
- package/__tests__/ruvector-integration.test.ts +1602 -0
- package/__tests__/ruvector-migrations.test.ts +1099 -0
- package/__tests__/ruvector-quantization.test.ts +846 -0
- package/__tests__/ruvector-streaming.test.ts +1088 -0
- package/__tests__/sdk.test.ts +325 -0
- package/__tests__/security.test.ts +348 -0
- package/__tests__/utils/ruvector-test-utils.ts +860 -0
- package/examples/plugin-creator/index.ts +636 -0
- package/examples/plugin-creator/plugin-creator.test.ts +312 -0
- package/examples/ruvector/README.md +288 -0
- package/examples/ruvector/attention-patterns.ts +394 -0
- package/examples/ruvector/basic-usage.ts +288 -0
- package/examples/ruvector/docker-compose.yml +75 -0
- package/examples/ruvector/gnn-analysis.ts +501 -0
- package/examples/ruvector/hyperbolic-hierarchies.ts +557 -0
- package/examples/ruvector/init-db.sql +119 -0
- package/examples/ruvector/quantization.ts +680 -0
- package/examples/ruvector/self-learning.ts +447 -0
- package/examples/ruvector/semantic-search.ts +576 -0
- package/examples/ruvector/streaming-large-data.ts +507 -0
- package/examples/ruvector/transactions.ts +594 -0
- package/examples/ruvector-plugins/hook-pattern-library.ts +486 -0
- package/examples/ruvector-plugins/index.ts +79 -0
- package/examples/ruvector-plugins/intent-router.ts +354 -0
- package/examples/ruvector-plugins/mcp-tool-optimizer.ts +424 -0
- package/examples/ruvector-plugins/reasoning-bank.ts +657 -0
- package/examples/ruvector-plugins/ruvector-plugins.test.ts +518 -0
- package/examples/ruvector-plugins/semantic-code-search.ts +498 -0
- package/examples/ruvector-plugins/shared/index.ts +20 -0
- package/examples/ruvector-plugins/shared/vector-utils.ts +257 -0
- package/examples/ruvector-plugins/sona-learning.ts +445 -0
- package/package.json +97 -0
- package/src/collections/collection-manager.ts +661 -0
- package/src/collections/index.ts +56 -0
- package/src/collections/official/index.ts +1040 -0
- package/src/core/base-plugin.ts +416 -0
- package/src/core/plugin-interface.ts +215 -0
- package/src/hooks/index.ts +685 -0
- package/src/index.ts +378 -0
- package/src/integrations/agentic-flow.ts +743 -0
- package/src/integrations/index.ts +88 -0
- package/src/integrations/ruvector/ARCHITECTURE.md +1245 -0
- package/src/integrations/ruvector/attention-advanced.ts +1040 -0
- package/src/integrations/ruvector/attention-executor.ts +782 -0
- package/src/integrations/ruvector/attention-mechanisms.ts +757 -0
- package/src/integrations/ruvector/attention.ts +1063 -0
- package/src/integrations/ruvector/gnn.ts +3050 -0
- package/src/integrations/ruvector/hyperbolic.ts +1948 -0
- package/src/integrations/ruvector/index.ts +394 -0
- package/src/integrations/ruvector/migrations/001_create_extension.sql +135 -0
- package/src/integrations/ruvector/migrations/002_create_vector_tables.sql +259 -0
- package/src/integrations/ruvector/migrations/003_create_indices.sql +328 -0
- package/src/integrations/ruvector/migrations/004_create_functions.sql +598 -0
- package/src/integrations/ruvector/migrations/005_create_attention_functions.sql +654 -0
- package/src/integrations/ruvector/migrations/006_create_gnn_functions.sql +728 -0
- package/src/integrations/ruvector/migrations/007_create_hyperbolic_functions.sql +762 -0
- package/src/integrations/ruvector/migrations/index.ts +35 -0
- package/src/integrations/ruvector/migrations/migrations.ts +647 -0
- package/src/integrations/ruvector/quantization.ts +2036 -0
- package/src/integrations/ruvector/ruvector-bridge.ts +2000 -0
- package/src/integrations/ruvector/self-learning.ts +2376 -0
- package/src/integrations/ruvector/streaming.ts +1737 -0
- package/src/integrations/ruvector/types.ts +1945 -0
- package/src/providers/index.ts +643 -0
- package/src/registry/dependency-graph.ts +568 -0
- package/src/registry/enhanced-plugin-registry.ts +994 -0
- package/src/registry/plugin-registry.ts +604 -0
- package/src/sdk/index.ts +563 -0
- package/src/security/index.ts +594 -0
- package/src/types/index.ts +446 -0
- package/src/workers/index.ts +700 -0
- package/tmp.json +0 -0
- package/tsconfig.json +25 -0
- package/vitest.config.ts +23 -0
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security Module
|
|
3
|
+
*
|
|
4
|
+
* Provides security utilities for plugin development.
|
|
5
|
+
* Implements best practices for input validation, sanitization, and safe operations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import * as fs from 'fs/promises';
|
|
10
|
+
import * as crypto from 'crypto';
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Input Validation
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Validate and sanitize a string input.
|
|
18
|
+
*/
|
|
19
|
+
export function validateString(
|
|
20
|
+
input: unknown,
|
|
21
|
+
options?: {
|
|
22
|
+
minLength?: number;
|
|
23
|
+
maxLength?: number;
|
|
24
|
+
pattern?: RegExp;
|
|
25
|
+
trim?: boolean;
|
|
26
|
+
lowercase?: boolean;
|
|
27
|
+
uppercase?: boolean;
|
|
28
|
+
}
|
|
29
|
+
): string | null {
|
|
30
|
+
if (typeof input !== 'string') return null;
|
|
31
|
+
|
|
32
|
+
let value = input;
|
|
33
|
+
|
|
34
|
+
if (options?.trim) value = value.trim();
|
|
35
|
+
if (options?.lowercase) value = value.toLowerCase();
|
|
36
|
+
if (options?.uppercase) value = value.toUpperCase();
|
|
37
|
+
|
|
38
|
+
if (options?.minLength !== undefined && value.length < options.minLength) return null;
|
|
39
|
+
if (options?.maxLength !== undefined && value.length > options.maxLength) return null;
|
|
40
|
+
if (options?.pattern && !options.pattern.test(value)) return null;
|
|
41
|
+
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Validate a number input.
|
|
47
|
+
*/
|
|
48
|
+
export function validateNumber(
|
|
49
|
+
input: unknown,
|
|
50
|
+
options?: {
|
|
51
|
+
min?: number;
|
|
52
|
+
max?: number;
|
|
53
|
+
integer?: boolean;
|
|
54
|
+
}
|
|
55
|
+
): number | null {
|
|
56
|
+
const num = typeof input === 'number' ? input : parseFloat(String(input));
|
|
57
|
+
|
|
58
|
+
if (isNaN(num) || !isFinite(num)) return null;
|
|
59
|
+
if (options?.min !== undefined && num < options.min) return null;
|
|
60
|
+
if (options?.max !== undefined && num > options.max) return null;
|
|
61
|
+
if (options?.integer && !Number.isInteger(num)) return null;
|
|
62
|
+
|
|
63
|
+
return num;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Validate a boolean input.
|
|
68
|
+
*/
|
|
69
|
+
export function validateBoolean(input: unknown): boolean | null {
|
|
70
|
+
if (typeof input === 'boolean') return input;
|
|
71
|
+
if (input === 'true' || input === '1' || input === 1) return true;
|
|
72
|
+
if (input === 'false' || input === '0' || input === 0) return false;
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Validate an array input.
|
|
78
|
+
*/
|
|
79
|
+
export function validateArray<T>(
|
|
80
|
+
input: unknown,
|
|
81
|
+
itemValidator: (item: unknown) => T | null,
|
|
82
|
+
options?: {
|
|
83
|
+
minLength?: number;
|
|
84
|
+
maxLength?: number;
|
|
85
|
+
unique?: boolean;
|
|
86
|
+
}
|
|
87
|
+
): T[] | null {
|
|
88
|
+
if (!Array.isArray(input)) return null;
|
|
89
|
+
|
|
90
|
+
if (options?.minLength !== undefined && input.length < options.minLength) return null;
|
|
91
|
+
if (options?.maxLength !== undefined && input.length > options.maxLength) return null;
|
|
92
|
+
|
|
93
|
+
const result: T[] = [];
|
|
94
|
+
for (const item of input) {
|
|
95
|
+
const validated = itemValidator(item);
|
|
96
|
+
if (validated === null) return null;
|
|
97
|
+
result.push(validated);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (options?.unique) {
|
|
101
|
+
const uniqueSet = new Set(result.map(String));
|
|
102
|
+
if (uniqueSet.size !== result.length) return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Validate an enum value.
|
|
110
|
+
*/
|
|
111
|
+
export function validateEnum<T extends string>(
|
|
112
|
+
input: unknown,
|
|
113
|
+
allowedValues: readonly T[]
|
|
114
|
+
): T | null {
|
|
115
|
+
if (typeof input !== 'string') return null;
|
|
116
|
+
if (!allowedValues.includes(input as T)) return null;
|
|
117
|
+
return input as T;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ============================================================================
|
|
121
|
+
// Path Security
|
|
122
|
+
// ============================================================================
|
|
123
|
+
|
|
124
|
+
const MAX_PATH_LENGTH = 4096;
|
|
125
|
+
const BLOCKED_PATH_PATTERNS = [
|
|
126
|
+
/\.\./, // Parent directory traversal
|
|
127
|
+
/^~/, // Home directory expansion
|
|
128
|
+
/^\/etc\//i,
|
|
129
|
+
/^\/var\//i,
|
|
130
|
+
/^\/tmp\//i,
|
|
131
|
+
/^\/proc\//i,
|
|
132
|
+
/^\/sys\//i,
|
|
133
|
+
/^\/dev\//i,
|
|
134
|
+
/^C:\\Windows/i,
|
|
135
|
+
/^C:\\Program Files/i,
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Validate a file path for safety.
|
|
140
|
+
*/
|
|
141
|
+
export function validatePath(
|
|
142
|
+
inputPath: unknown,
|
|
143
|
+
options?: {
|
|
144
|
+
allowedExtensions?: string[];
|
|
145
|
+
blockedPatterns?: RegExp[];
|
|
146
|
+
mustExist?: boolean;
|
|
147
|
+
allowAbsolute?: boolean;
|
|
148
|
+
}
|
|
149
|
+
): string | null {
|
|
150
|
+
if (typeof inputPath !== 'string') return null;
|
|
151
|
+
if (inputPath.length === 0 || inputPath.length > MAX_PATH_LENGTH) return null;
|
|
152
|
+
|
|
153
|
+
// Normalize the path
|
|
154
|
+
const normalized = path.normalize(inputPath);
|
|
155
|
+
|
|
156
|
+
// Check blocked patterns
|
|
157
|
+
const blockedPatterns = [...BLOCKED_PATH_PATTERNS, ...(options?.blockedPatterns ?? [])];
|
|
158
|
+
for (const pattern of blockedPatterns) {
|
|
159
|
+
if (pattern.test(normalized)) return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Check absolute path restriction
|
|
163
|
+
if (!options?.allowAbsolute && path.isAbsolute(normalized)) return null;
|
|
164
|
+
|
|
165
|
+
// Check extension
|
|
166
|
+
if (options?.allowedExtensions) {
|
|
167
|
+
const ext = path.extname(normalized).toLowerCase();
|
|
168
|
+
if (!options.allowedExtensions.includes(ext)) return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return normalized;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Create a safe path relative to a base directory.
|
|
176
|
+
* Prevents path traversal attacks.
|
|
177
|
+
*/
|
|
178
|
+
export function safePath(baseDir: string, ...segments: string[]): string {
|
|
179
|
+
const resolved = path.resolve(baseDir, ...segments);
|
|
180
|
+
const normalizedBase = path.normalize(baseDir);
|
|
181
|
+
|
|
182
|
+
if (!resolved.startsWith(normalizedBase + path.sep) && resolved !== normalizedBase) {
|
|
183
|
+
throw new Error(`Path traversal blocked: ${resolved}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return resolved;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Async version of safePath that uses realpath.
|
|
191
|
+
* More secure as it resolves symlinks.
|
|
192
|
+
*/
|
|
193
|
+
export async function safePathAsync(baseDir: string, ...segments: string[]): Promise<string> {
|
|
194
|
+
const resolved = path.resolve(baseDir, ...segments);
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const realResolved = await fs.realpath(resolved).catch(() => resolved);
|
|
198
|
+
const realBase = await fs.realpath(baseDir).catch(() => baseDir);
|
|
199
|
+
|
|
200
|
+
if (!realResolved.startsWith(realBase + path.sep) && realResolved !== realBase) {
|
|
201
|
+
throw new Error(`Path traversal blocked: ${realResolved}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return realResolved;
|
|
205
|
+
} catch (error) {
|
|
206
|
+
// Handle non-existent files
|
|
207
|
+
const normalizedBase = path.normalize(baseDir);
|
|
208
|
+
if (!resolved.startsWith(normalizedBase + path.sep) && resolved !== normalizedBase) {
|
|
209
|
+
throw new Error(`Path traversal blocked: ${resolved}`);
|
|
210
|
+
}
|
|
211
|
+
return resolved;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ============================================================================
|
|
216
|
+
// JSON Security
|
|
217
|
+
// ============================================================================
|
|
218
|
+
|
|
219
|
+
const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Parse JSON safely, stripping dangerous keys.
|
|
223
|
+
*/
|
|
224
|
+
export function safeJsonParse<T = unknown>(content: string): T {
|
|
225
|
+
return JSON.parse(content, (key, value) => {
|
|
226
|
+
if (DANGEROUS_KEYS.has(key)) {
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
return value;
|
|
230
|
+
}) as T;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Stringify JSON with circular reference detection.
|
|
235
|
+
*/
|
|
236
|
+
export function safeJsonStringify(
|
|
237
|
+
value: unknown,
|
|
238
|
+
options?: {
|
|
239
|
+
space?: number;
|
|
240
|
+
maxDepth?: number;
|
|
241
|
+
replacer?: (key: string, value: unknown) => unknown;
|
|
242
|
+
}
|
|
243
|
+
): string {
|
|
244
|
+
const seen = new WeakSet();
|
|
245
|
+
const maxDepth = options?.maxDepth ?? 100;
|
|
246
|
+
let currentDepth = 0;
|
|
247
|
+
|
|
248
|
+
const replacer = (key: string, val: unknown): unknown => {
|
|
249
|
+
// Apply custom replacer first
|
|
250
|
+
if (options?.replacer) {
|
|
251
|
+
val = options.replacer(key, val);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Strip dangerous keys
|
|
255
|
+
if (DANGEROUS_KEYS.has(key)) {
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Handle circular references
|
|
260
|
+
if (val !== null && typeof val === 'object') {
|
|
261
|
+
if (seen.has(val)) {
|
|
262
|
+
return '[Circular]';
|
|
263
|
+
}
|
|
264
|
+
seen.add(val);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Depth limiting
|
|
268
|
+
if (key !== '') {
|
|
269
|
+
currentDepth++;
|
|
270
|
+
if (currentDepth > maxDepth) {
|
|
271
|
+
return '[Max Depth Exceeded]';
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return val;
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
return JSON.stringify(value, replacer, options?.space);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ============================================================================
|
|
282
|
+
// Command Security
|
|
283
|
+
// ============================================================================
|
|
284
|
+
|
|
285
|
+
const ALLOWED_COMMANDS = new Set([
|
|
286
|
+
'npm', 'npx', 'node', 'git', 'tsc', 'vitest', 'jest',
|
|
287
|
+
'prettier', 'eslint', 'ls', 'cat', 'grep', 'find',
|
|
288
|
+
]);
|
|
289
|
+
|
|
290
|
+
const BLOCKED_COMMANDS = new Set([
|
|
291
|
+
'rm', 'del', 'format', 'dd', 'mkfs', 'fdisk',
|
|
292
|
+
'shutdown', 'reboot', 'poweroff', 'halt',
|
|
293
|
+
'passwd', 'sudo', 'su', 'chmod', 'chown',
|
|
294
|
+
'curl', 'wget', 'nc', 'netcat',
|
|
295
|
+
]);
|
|
296
|
+
|
|
297
|
+
const SHELL_METACHARACTERS = /[|;&$`<>(){}[\]!\\]/;
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Validate a command for safe execution.
|
|
301
|
+
*/
|
|
302
|
+
export function validateCommand(
|
|
303
|
+
command: unknown,
|
|
304
|
+
options?: {
|
|
305
|
+
allowedCommands?: Set<string>;
|
|
306
|
+
blockedCommands?: Set<string>;
|
|
307
|
+
allowShellMetachars?: boolean;
|
|
308
|
+
}
|
|
309
|
+
): { command: string; args: string[] } | null {
|
|
310
|
+
if (typeof command !== 'string') return null;
|
|
311
|
+
|
|
312
|
+
const trimmed = command.trim();
|
|
313
|
+
if (trimmed.length === 0) return null;
|
|
314
|
+
|
|
315
|
+
// Check for shell metacharacters
|
|
316
|
+
if (!options?.allowShellMetachars && SHELL_METACHARACTERS.test(trimmed)) {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Parse command and args
|
|
321
|
+
const parts = trimmed.split(/\s+/);
|
|
322
|
+
const cmd = parts[0].toLowerCase();
|
|
323
|
+
const args = parts.slice(1);
|
|
324
|
+
|
|
325
|
+
// Check allowed/blocked lists
|
|
326
|
+
const allowed = options?.allowedCommands ?? ALLOWED_COMMANDS;
|
|
327
|
+
const blocked = options?.blockedCommands ?? BLOCKED_COMMANDS;
|
|
328
|
+
|
|
329
|
+
if (blocked.has(cmd)) return null;
|
|
330
|
+
if (!allowed.has(cmd) && allowed.size > 0) return null;
|
|
331
|
+
|
|
332
|
+
return { command: cmd, args };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Escape a string for safe shell argument use.
|
|
337
|
+
*/
|
|
338
|
+
export function escapeShellArg(arg: string): string {
|
|
339
|
+
// Empty string
|
|
340
|
+
if (arg.length === 0) return "''";
|
|
341
|
+
|
|
342
|
+
// If no special characters, return as-is
|
|
343
|
+
if (!/[^a-zA-Z0-9_\-=./:@]/.test(arg)) return arg;
|
|
344
|
+
|
|
345
|
+
// Single-quote the argument and escape any single quotes
|
|
346
|
+
return "'" + arg.replace(/'/g, "'\"'\"'") + "'";
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ============================================================================
|
|
350
|
+
// Error Sanitization
|
|
351
|
+
// ============================================================================
|
|
352
|
+
|
|
353
|
+
const SENSITIVE_PATTERNS = [
|
|
354
|
+
/password[=:]\s*\S+/gi,
|
|
355
|
+
/api[_-]?key[=:]\s*\S+/gi,
|
|
356
|
+
/secret[=:]\s*\S+/gi,
|
|
357
|
+
/token[=:]\s*\S+/gi,
|
|
358
|
+
/auth[=:]\s*\S+/gi,
|
|
359
|
+
/bearer\s+\S+/gi,
|
|
360
|
+
/\/\/[^:]+:[^@]+@/g, // Credentials in URLs
|
|
361
|
+
];
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Sanitize error messages to remove sensitive data.
|
|
365
|
+
*/
|
|
366
|
+
export function sanitizeErrorMessage(error: unknown): string {
|
|
367
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
368
|
+
|
|
369
|
+
let sanitized = message;
|
|
370
|
+
for (const pattern of SENSITIVE_PATTERNS) {
|
|
371
|
+
sanitized = sanitized.replace(pattern, '[REDACTED]');
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Truncate very long messages
|
|
375
|
+
if (sanitized.length > 1000) {
|
|
376
|
+
sanitized = sanitized.substring(0, 1000) + '... [truncated]';
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return sanitized;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Create a safe error object for logging/transmission.
|
|
384
|
+
*/
|
|
385
|
+
export function sanitizeError(error: unknown): {
|
|
386
|
+
name: string;
|
|
387
|
+
message: string;
|
|
388
|
+
code?: string;
|
|
389
|
+
} {
|
|
390
|
+
if (error instanceof Error) {
|
|
391
|
+
return {
|
|
392
|
+
name: error.name,
|
|
393
|
+
message: sanitizeErrorMessage(error),
|
|
394
|
+
code: (error as NodeJS.ErrnoException).code,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return {
|
|
399
|
+
name: 'Error',
|
|
400
|
+
message: sanitizeErrorMessage(error),
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ============================================================================
|
|
405
|
+
// Rate Limiting
|
|
406
|
+
// ============================================================================
|
|
407
|
+
|
|
408
|
+
export interface RateLimiter {
|
|
409
|
+
tryAcquire(): boolean;
|
|
410
|
+
getRemaining(): number;
|
|
411
|
+
reset(): void;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Create a token bucket rate limiter.
|
|
416
|
+
*/
|
|
417
|
+
export function createRateLimiter(options: {
|
|
418
|
+
maxTokens: number;
|
|
419
|
+
refillRate: number;
|
|
420
|
+
refillInterval: number;
|
|
421
|
+
}): RateLimiter {
|
|
422
|
+
let tokens = options.maxTokens;
|
|
423
|
+
let lastRefill = Date.now();
|
|
424
|
+
|
|
425
|
+
const refill = () => {
|
|
426
|
+
const now = Date.now();
|
|
427
|
+
const elapsed = now - lastRefill;
|
|
428
|
+
const refillCount = Math.floor(elapsed / options.refillInterval) * options.refillRate;
|
|
429
|
+
|
|
430
|
+
if (refillCount > 0) {
|
|
431
|
+
tokens = Math.min(options.maxTokens, tokens + refillCount);
|
|
432
|
+
lastRefill = now;
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
tryAcquire(): boolean {
|
|
438
|
+
refill();
|
|
439
|
+
if (tokens > 0) {
|
|
440
|
+
tokens--;
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
return false;
|
|
444
|
+
},
|
|
445
|
+
getRemaining(): number {
|
|
446
|
+
refill();
|
|
447
|
+
return tokens;
|
|
448
|
+
},
|
|
449
|
+
reset(): void {
|
|
450
|
+
tokens = options.maxTokens;
|
|
451
|
+
lastRefill = Date.now();
|
|
452
|
+
},
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ============================================================================
|
|
457
|
+
// Crypto Utilities
|
|
458
|
+
// ============================================================================
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Generate a secure random ID.
|
|
462
|
+
*/
|
|
463
|
+
export function generateSecureId(length: number = 32): string {
|
|
464
|
+
return crypto.randomBytes(length).toString('hex');
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Generate a secure random token (URL-safe).
|
|
469
|
+
*/
|
|
470
|
+
export function generateSecureToken(length: number = 32): string {
|
|
471
|
+
return crypto.randomBytes(length).toString('base64url');
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Hash a string securely.
|
|
476
|
+
*/
|
|
477
|
+
export function hashString(input: string, algorithm: string = 'sha256'): string {
|
|
478
|
+
return crypto.createHash(algorithm).update(input).digest('hex');
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Compare two strings in constant time.
|
|
483
|
+
*/
|
|
484
|
+
export function constantTimeCompare(a: string, b: string): boolean {
|
|
485
|
+
if (a.length !== b.length) return false;
|
|
486
|
+
|
|
487
|
+
const bufA = Buffer.from(a);
|
|
488
|
+
const bufB = Buffer.from(b);
|
|
489
|
+
|
|
490
|
+
return crypto.timingSafeEqual(bufA, bufB);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// ============================================================================
|
|
494
|
+
// Resource Limits
|
|
495
|
+
// ============================================================================
|
|
496
|
+
|
|
497
|
+
export interface ResourceLimits {
|
|
498
|
+
maxMemoryMB: number;
|
|
499
|
+
maxCPUPercent: number;
|
|
500
|
+
maxFileSize: number;
|
|
501
|
+
maxOpenFiles: number;
|
|
502
|
+
maxExecutionTime: number;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const DEFAULT_LIMITS: ResourceLimits = {
|
|
506
|
+
maxMemoryMB: 512,
|
|
507
|
+
maxCPUPercent: 80,
|
|
508
|
+
maxFileSize: 10 * 1024 * 1024, // 10MB
|
|
509
|
+
maxOpenFiles: 100,
|
|
510
|
+
maxExecutionTime: 30000, // 30s
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Create a resource limiter.
|
|
515
|
+
*/
|
|
516
|
+
export function createResourceLimiter(limits?: Partial<ResourceLimits>): {
|
|
517
|
+
check(): { ok: boolean; violations: string[] };
|
|
518
|
+
enforce<T>(fn: () => Promise<T>): Promise<T>;
|
|
519
|
+
} {
|
|
520
|
+
const config = { ...DEFAULT_LIMITS, ...limits };
|
|
521
|
+
|
|
522
|
+
return {
|
|
523
|
+
check(): { ok: boolean; violations: string[] } {
|
|
524
|
+
const violations: string[] = [];
|
|
525
|
+
const memUsage = process.memoryUsage();
|
|
526
|
+
const memMB = memUsage.heapUsed / 1024 / 1024;
|
|
527
|
+
|
|
528
|
+
if (memMB > config.maxMemoryMB) {
|
|
529
|
+
violations.push(`Memory usage ${memMB.toFixed(1)}MB exceeds limit ${config.maxMemoryMB}MB`);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return {
|
|
533
|
+
ok: violations.length === 0,
|
|
534
|
+
violations,
|
|
535
|
+
};
|
|
536
|
+
},
|
|
537
|
+
|
|
538
|
+
async enforce<T>(fn: () => Promise<T>): Promise<T> {
|
|
539
|
+
const check = this.check();
|
|
540
|
+
if (!check.ok) {
|
|
541
|
+
throw new Error(`Resource limits exceeded: ${check.violations.join(', ')}`);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return Promise.race([
|
|
545
|
+
fn(),
|
|
546
|
+
new Promise<never>((_, reject) =>
|
|
547
|
+
setTimeout(() => reject(new Error('Execution time limit exceeded')), config.maxExecutionTime)
|
|
548
|
+
),
|
|
549
|
+
]);
|
|
550
|
+
},
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// ============================================================================
|
|
555
|
+
// Export All
|
|
556
|
+
// ============================================================================
|
|
557
|
+
|
|
558
|
+
export const Security = {
|
|
559
|
+
// Validation
|
|
560
|
+
validateString,
|
|
561
|
+
validateNumber,
|
|
562
|
+
validateBoolean,
|
|
563
|
+
validateArray,
|
|
564
|
+
validateEnum,
|
|
565
|
+
validatePath,
|
|
566
|
+
validateCommand,
|
|
567
|
+
|
|
568
|
+
// Path security
|
|
569
|
+
safePath,
|
|
570
|
+
safePathAsync,
|
|
571
|
+
|
|
572
|
+
// JSON security
|
|
573
|
+
safeJsonParse,
|
|
574
|
+
safeJsonStringify,
|
|
575
|
+
|
|
576
|
+
// Command security
|
|
577
|
+
escapeShellArg,
|
|
578
|
+
|
|
579
|
+
// Error sanitization
|
|
580
|
+
sanitizeError,
|
|
581
|
+
sanitizeErrorMessage,
|
|
582
|
+
|
|
583
|
+
// Rate limiting
|
|
584
|
+
createRateLimiter,
|
|
585
|
+
|
|
586
|
+
// Crypto
|
|
587
|
+
generateSecureId,
|
|
588
|
+
generateSecureToken,
|
|
589
|
+
hashString,
|
|
590
|
+
constantTimeCompare,
|
|
591
|
+
|
|
592
|
+
// Resource limits
|
|
593
|
+
createResourceLimiter,
|
|
594
|
+
};
|