@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.
Files changed (80) hide show
  1. package/README.md +401 -0
  2. package/__tests__/collection-manager.test.ts +332 -0
  3. package/__tests__/dependency-graph.test.ts +434 -0
  4. package/__tests__/enhanced-plugin-registry.test.ts +488 -0
  5. package/__tests__/plugin-registry.test.ts +368 -0
  6. package/__tests__/ruvector-bridge.test.ts +2429 -0
  7. package/__tests__/ruvector-integration.test.ts +1602 -0
  8. package/__tests__/ruvector-migrations.test.ts +1099 -0
  9. package/__tests__/ruvector-quantization.test.ts +846 -0
  10. package/__tests__/ruvector-streaming.test.ts +1088 -0
  11. package/__tests__/sdk.test.ts +325 -0
  12. package/__tests__/security.test.ts +348 -0
  13. package/__tests__/utils/ruvector-test-utils.ts +860 -0
  14. package/examples/plugin-creator/index.ts +636 -0
  15. package/examples/plugin-creator/plugin-creator.test.ts +312 -0
  16. package/examples/ruvector/README.md +288 -0
  17. package/examples/ruvector/attention-patterns.ts +394 -0
  18. package/examples/ruvector/basic-usage.ts +288 -0
  19. package/examples/ruvector/docker-compose.yml +75 -0
  20. package/examples/ruvector/gnn-analysis.ts +501 -0
  21. package/examples/ruvector/hyperbolic-hierarchies.ts +557 -0
  22. package/examples/ruvector/init-db.sql +119 -0
  23. package/examples/ruvector/quantization.ts +680 -0
  24. package/examples/ruvector/self-learning.ts +447 -0
  25. package/examples/ruvector/semantic-search.ts +576 -0
  26. package/examples/ruvector/streaming-large-data.ts +507 -0
  27. package/examples/ruvector/transactions.ts +594 -0
  28. package/examples/ruvector-plugins/hook-pattern-library.ts +486 -0
  29. package/examples/ruvector-plugins/index.ts +79 -0
  30. package/examples/ruvector-plugins/intent-router.ts +354 -0
  31. package/examples/ruvector-plugins/mcp-tool-optimizer.ts +424 -0
  32. package/examples/ruvector-plugins/reasoning-bank.ts +657 -0
  33. package/examples/ruvector-plugins/ruvector-plugins.test.ts +518 -0
  34. package/examples/ruvector-plugins/semantic-code-search.ts +498 -0
  35. package/examples/ruvector-plugins/shared/index.ts +20 -0
  36. package/examples/ruvector-plugins/shared/vector-utils.ts +257 -0
  37. package/examples/ruvector-plugins/sona-learning.ts +445 -0
  38. package/package.json +97 -0
  39. package/src/collections/collection-manager.ts +661 -0
  40. package/src/collections/index.ts +56 -0
  41. package/src/collections/official/index.ts +1040 -0
  42. package/src/core/base-plugin.ts +416 -0
  43. package/src/core/plugin-interface.ts +215 -0
  44. package/src/hooks/index.ts +685 -0
  45. package/src/index.ts +378 -0
  46. package/src/integrations/agentic-flow.ts +743 -0
  47. package/src/integrations/index.ts +88 -0
  48. package/src/integrations/ruvector/ARCHITECTURE.md +1245 -0
  49. package/src/integrations/ruvector/attention-advanced.ts +1040 -0
  50. package/src/integrations/ruvector/attention-executor.ts +782 -0
  51. package/src/integrations/ruvector/attention-mechanisms.ts +757 -0
  52. package/src/integrations/ruvector/attention.ts +1063 -0
  53. package/src/integrations/ruvector/gnn.ts +3050 -0
  54. package/src/integrations/ruvector/hyperbolic.ts +1948 -0
  55. package/src/integrations/ruvector/index.ts +394 -0
  56. package/src/integrations/ruvector/migrations/001_create_extension.sql +135 -0
  57. package/src/integrations/ruvector/migrations/002_create_vector_tables.sql +259 -0
  58. package/src/integrations/ruvector/migrations/003_create_indices.sql +328 -0
  59. package/src/integrations/ruvector/migrations/004_create_functions.sql +598 -0
  60. package/src/integrations/ruvector/migrations/005_create_attention_functions.sql +654 -0
  61. package/src/integrations/ruvector/migrations/006_create_gnn_functions.sql +728 -0
  62. package/src/integrations/ruvector/migrations/007_create_hyperbolic_functions.sql +762 -0
  63. package/src/integrations/ruvector/migrations/index.ts +35 -0
  64. package/src/integrations/ruvector/migrations/migrations.ts +647 -0
  65. package/src/integrations/ruvector/quantization.ts +2036 -0
  66. package/src/integrations/ruvector/ruvector-bridge.ts +2000 -0
  67. package/src/integrations/ruvector/self-learning.ts +2376 -0
  68. package/src/integrations/ruvector/streaming.ts +1737 -0
  69. package/src/integrations/ruvector/types.ts +1945 -0
  70. package/src/providers/index.ts +643 -0
  71. package/src/registry/dependency-graph.ts +568 -0
  72. package/src/registry/enhanced-plugin-registry.ts +994 -0
  73. package/src/registry/plugin-registry.ts +604 -0
  74. package/src/sdk/index.ts +563 -0
  75. package/src/security/index.ts +594 -0
  76. package/src/types/index.ts +446 -0
  77. package/src/workers/index.ts +700 -0
  78. package/tmp.json +0 -0
  79. package/tsconfig.json +25 -0
  80. package/vitest.config.ts +23 -0
@@ -0,0 +1,325 @@
1
+ /**
2
+ * SDK Builder Tests
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import {
7
+ PluginBuilder,
8
+ MCPToolBuilder,
9
+ HookBuilder,
10
+ WorkerBuilder,
11
+ createToolPlugin,
12
+ createHooksPlugin,
13
+ createWorkerPlugin,
14
+ } from '../src/sdk/index.js';
15
+ import { HookEvent, HookPriority } from '../src/types/index.js';
16
+
17
+ describe('PluginBuilder', () => {
18
+ it('should create a basic plugin', () => {
19
+ const plugin = new PluginBuilder('test-plugin', '1.0.0')
20
+ .withDescription('A test plugin')
21
+ .build();
22
+
23
+ expect(plugin.metadata.name).toBe('test-plugin');
24
+ expect(plugin.metadata.version).toBe('1.0.0');
25
+ expect(plugin.metadata.description).toBe('A test plugin');
26
+ expect(plugin.state).toBe('uninitialized');
27
+ });
28
+
29
+ it('should add all metadata fields', () => {
30
+ const plugin = new PluginBuilder('full-plugin', '2.0.0')
31
+ .withDescription('Full metadata test')
32
+ .withAuthor('Test Author')
33
+ .withLicense('MIT')
34
+ .withRepository('https://github.com/test/repo')
35
+ .withDependencies(['dep1', 'dep2'])
36
+ .withTags(['test', 'demo'])
37
+ .withMinCoreVersion('3.0.0')
38
+ .build();
39
+
40
+ expect(plugin.metadata.author).toBe('Test Author');
41
+ expect(plugin.metadata.license).toBe('MIT');
42
+ expect(plugin.metadata.repository).toBe('https://github.com/test/repo');
43
+ expect(plugin.metadata.dependencies).toEqual(['dep1', 'dep2']);
44
+ expect(plugin.metadata.tags).toEqual(['test', 'demo']);
45
+ expect(plugin.metadata.minCoreVersion).toBe('3.0.0');
46
+ });
47
+
48
+ it('should add MCP tools', () => {
49
+ const plugin = new PluginBuilder('tool-plugin', '1.0.0')
50
+ .withMCPTools([
51
+ {
52
+ name: 'test-tool',
53
+ description: 'A test tool',
54
+ inputSchema: { type: 'object', properties: {} },
55
+ handler: async () => ({ content: [{ type: 'text', text: 'done' }] }),
56
+ },
57
+ ])
58
+ .build();
59
+
60
+ const tools = plugin.registerMCPTools?.();
61
+ expect(tools).toHaveLength(1);
62
+ expect(tools?.[0].name).toBe('test-tool');
63
+ });
64
+
65
+ it('should add hooks', () => {
66
+ const plugin = new PluginBuilder('hook-plugin', '1.0.0')
67
+ .withHooks([
68
+ {
69
+ event: HookEvent.PostTaskComplete,
70
+ handler: async () => ({ success: true }),
71
+ priority: HookPriority.High,
72
+ },
73
+ ])
74
+ .build();
75
+
76
+ const hooks = plugin.registerHooks?.();
77
+ expect(hooks).toHaveLength(1);
78
+ expect(hooks?.[0].event).toBe(HookEvent.PostTaskComplete);
79
+ });
80
+
81
+ it('should add workers', () => {
82
+ const plugin = new PluginBuilder('worker-plugin', '1.0.0')
83
+ .withWorkers([
84
+ {
85
+ type: 'coder',
86
+ name: 'test-coder',
87
+ capabilities: ['code-generation'],
88
+ },
89
+ ])
90
+ .build();
91
+
92
+ const workers = plugin.registerWorkers?.();
93
+ expect(workers).toHaveLength(1);
94
+ expect(workers?.[0].name).toBe('test-coder');
95
+ });
96
+
97
+ it('should call lifecycle handlers', async () => {
98
+ let initCalled = false;
99
+ let shutdownCalled = false;
100
+
101
+ const plugin = new PluginBuilder('lifecycle-plugin', '1.0.0')
102
+ .onInitialize(async () => {
103
+ initCalled = true;
104
+ })
105
+ .onShutdown(async () => {
106
+ shutdownCalled = true;
107
+ })
108
+ .build();
109
+
110
+ // Create minimal context
111
+ const context = {
112
+ config: { enabled: true, priority: 50, settings: {} },
113
+ eventBus: { emit: () => {}, on: () => () => {}, off: () => {}, once: () => () => {} },
114
+ logger: { debug: () => {}, info: () => {}, warn: () => {}, error: () => {}, child: () => ({} as any) },
115
+ services: { get: () => undefined, set: () => {}, has: () => false, delete: () => false },
116
+ coreVersion: '3.0.0',
117
+ dataDir: '/tmp',
118
+ };
119
+
120
+ await plugin.initialize(context);
121
+ expect(initCalled).toBe(true);
122
+
123
+ await plugin.shutdown();
124
+ expect(shutdownCalled).toBe(true);
125
+ });
126
+ });
127
+
128
+ describe('MCPToolBuilder', () => {
129
+ it('should build a tool with parameters', async () => {
130
+ const tool = new MCPToolBuilder('greet')
131
+ .withDescription('Greet someone')
132
+ .addStringParam('name', 'The name to greet', { required: true })
133
+ .addNumberParam('times', 'How many times to greet', { default: 1, minimum: 1 })
134
+ .addBooleanParam('formal', 'Use formal greeting', { default: false })
135
+ .withHandler(async (input) => ({
136
+ content: [{ type: 'text', text: `Hello, ${input.name}!` }],
137
+ }))
138
+ .build();
139
+
140
+ expect(tool.name).toBe('greet');
141
+ expect(tool.description).toBe('Greet someone');
142
+ expect(tool.inputSchema.properties.name).toBeDefined();
143
+ expect(tool.inputSchema.required).toContain('name');
144
+
145
+ const result = await tool.handler({ name: 'World' });
146
+ expect(result.content[0].text).toBe('Hello, World!');
147
+ });
148
+
149
+ it('should support array parameters', () => {
150
+ const tool = new MCPToolBuilder('process')
151
+ .withDescription('Process items')
152
+ .addArrayParam('items', 'Items to process', { type: 'string' }, { required: true })
153
+ .withHandler(async () => ({ content: [{ type: 'text', text: 'done' }] }))
154
+ .build();
155
+
156
+ expect(tool.inputSchema.properties.items.type).toBe('array');
157
+ expect(tool.inputSchema.required).toContain('items');
158
+ });
159
+
160
+ it('should support object parameters', () => {
161
+ const tool = new MCPToolBuilder('configure')
162
+ .withDescription('Configure settings')
163
+ .addObjectParam(
164
+ 'settings',
165
+ 'Configuration settings',
166
+ {
167
+ type: 'object',
168
+ properties: {
169
+ debug: { type: 'boolean' },
170
+ level: { type: 'number' },
171
+ },
172
+ },
173
+ { required: true }
174
+ )
175
+ .withHandler(async () => ({ content: [{ type: 'text', text: 'configured' }] }))
176
+ .build();
177
+
178
+ expect(tool.inputSchema.properties.settings).toBeDefined();
179
+ });
180
+
181
+ it('should throw without handler', () => {
182
+ expect(() => {
183
+ new MCPToolBuilder('no-handler')
184
+ .withDescription('Missing handler')
185
+ .build();
186
+ }).toThrow('requires a handler');
187
+ });
188
+ });
189
+
190
+ describe('HookBuilder', () => {
191
+ it('should build a hook with conditions', async () => {
192
+ let executed = false;
193
+
194
+ const hook = new HookBuilder(HookEvent.PostTaskComplete)
195
+ .withName('conditional-hook')
196
+ .withDescription('Conditional hook test')
197
+ .withPriority(HookPriority.High)
198
+ .when((ctx) => (ctx.data as any)?.shouldRun === true)
199
+ .handle(async () => {
200
+ executed = true;
201
+ return { success: true };
202
+ })
203
+ .build();
204
+
205
+ // Should not execute when condition is false
206
+ await hook.handler({
207
+ event: HookEvent.PostTaskComplete,
208
+ data: { shouldRun: false },
209
+ timestamp: new Date(),
210
+ });
211
+ expect(executed).toBe(false);
212
+
213
+ // Should execute when condition is true
214
+ await hook.handler({
215
+ event: HookEvent.PostTaskComplete,
216
+ data: { shouldRun: true },
217
+ timestamp: new Date(),
218
+ });
219
+ expect(executed).toBe(true);
220
+ });
221
+
222
+ it('should apply transformers', async () => {
223
+ const hook = new HookBuilder(HookEvent.PreTaskExecute)
224
+ .withName('transform-hook')
225
+ .transform((data) => ({ ...(data as object), transformed: true }))
226
+ .handle(async (ctx) => ({
227
+ success: true,
228
+ data: ctx.data,
229
+ modified: true,
230
+ }))
231
+ .build();
232
+
233
+ const result = await hook.handler({
234
+ event: HookEvent.PreTaskExecute,
235
+ data: { original: true },
236
+ timestamp: new Date(),
237
+ });
238
+
239
+ expect((result.data as any).transformed).toBe(true);
240
+ expect((result.data as any).original).toBe(true);
241
+ });
242
+
243
+ it('should support synchronous mode', () => {
244
+ const hook = new HookBuilder(HookEvent.PostCommand)
245
+ .synchronous()
246
+ .handle(() => ({ success: true }))
247
+ .build();
248
+
249
+ expect(hook.async).toBe(false);
250
+ });
251
+ });
252
+
253
+ describe('WorkerBuilder', () => {
254
+ it('should build a worker definition', () => {
255
+ const worker = new WorkerBuilder('coder', 'main-coder')
256
+ .withDescription('Primary coder worker')
257
+ .withCapabilities(['code-generation', 'refactoring', 'debugging'])
258
+ .withMaxConcurrentTasks(5)
259
+ .withTimeout(60000)
260
+ .withPriority(75)
261
+ .withMetadata({ language: 'typescript' })
262
+ .build();
263
+
264
+ expect(worker.type).toBe('coder');
265
+ expect(worker.name).toBe('main-coder');
266
+ expect(worker.description).toBe('Primary coder worker');
267
+ expect(worker.capabilities).toContain('code-generation');
268
+ expect(worker.maxConcurrentTasks).toBe(5);
269
+ expect(worker.timeout).toBe(60000);
270
+ expect(worker.priority).toBe(75);
271
+ expect(worker.metadata?.language).toBe('typescript');
272
+ });
273
+
274
+ it('should support specialization vector', () => {
275
+ const specialization = new Float32Array([0.1, 0.2, 0.3]);
276
+
277
+ const worker = new WorkerBuilder('specialized', 'ml-worker')
278
+ .withSpecialization(specialization)
279
+ .withCapabilities(['ml-training'])
280
+ .build();
281
+
282
+ expect(worker.specialization).toBe(specialization);
283
+ });
284
+ });
285
+
286
+ describe('Quick Plugin Creators', () => {
287
+ it('should create tool-only plugin', () => {
288
+ const plugin = createToolPlugin('quick-tools', '1.0.0', [
289
+ {
290
+ name: 'quick-tool',
291
+ description: 'Quick tool',
292
+ inputSchema: { type: 'object', properties: {} },
293
+ handler: async () => ({ content: [{ type: 'text', text: '' }] }),
294
+ },
295
+ ]);
296
+
297
+ expect(plugin.metadata.name).toBe('quick-tools');
298
+ expect(plugin.registerMCPTools?.()).toHaveLength(1);
299
+ });
300
+
301
+ it('should create hooks-only plugin', () => {
302
+ const plugin = createHooksPlugin('quick-hooks', '1.0.0', [
303
+ {
304
+ event: HookEvent.SessionStart,
305
+ handler: async () => ({ success: true }),
306
+ },
307
+ ]);
308
+
309
+ expect(plugin.metadata.name).toBe('quick-hooks');
310
+ expect(plugin.registerHooks?.()).toHaveLength(1);
311
+ });
312
+
313
+ it('should create worker plugin', () => {
314
+ const plugin = createWorkerPlugin('quick-workers', '1.0.0', [
315
+ {
316
+ type: 'tester',
317
+ name: 'quick-tester',
318
+ capabilities: ['testing'],
319
+ },
320
+ ]);
321
+
322
+ expect(plugin.metadata.name).toBe('quick-workers');
323
+ expect(plugin.registerWorkers?.()).toHaveLength(1);
324
+ });
325
+ });
@@ -0,0 +1,348 @@
1
+ /**
2
+ * Security Module Tests
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import {
7
+ validateString,
8
+ validateNumber,
9
+ validateBoolean,
10
+ validateArray,
11
+ validateEnum,
12
+ validatePath,
13
+ validateCommand,
14
+ safePath,
15
+ safeJsonParse,
16
+ safeJsonStringify,
17
+ escapeShellArg,
18
+ sanitizeErrorMessage,
19
+ sanitizeError,
20
+ createRateLimiter,
21
+ generateSecureId,
22
+ generateSecureToken,
23
+ hashString,
24
+ constantTimeCompare,
25
+ createResourceLimiter,
26
+ } from '../src/security/index.js';
27
+ import * as path from 'path';
28
+
29
+ describe('Input Validation', () => {
30
+ describe('validateString', () => {
31
+ it('should validate basic strings', () => {
32
+ expect(validateString('hello')).toBe('hello');
33
+ expect(validateString(123)).toBeNull();
34
+ expect(validateString(null)).toBeNull();
35
+ });
36
+
37
+ it('should apply length constraints', () => {
38
+ expect(validateString('ab', { minLength: 3 })).toBeNull();
39
+ expect(validateString('abc', { minLength: 3 })).toBe('abc');
40
+ expect(validateString('abcdef', { maxLength: 5 })).toBeNull();
41
+ expect(validateString('abcde', { maxLength: 5 })).toBe('abcde');
42
+ });
43
+
44
+ it('should apply pattern matching', () => {
45
+ expect(validateString('abc123', { pattern: /^[a-z]+$/ })).toBeNull();
46
+ expect(validateString('abc', { pattern: /^[a-z]+$/ })).toBe('abc');
47
+ });
48
+
49
+ it('should apply transformations', () => {
50
+ expect(validateString(' hello ', { trim: true })).toBe('hello');
51
+ expect(validateString('Hello', { lowercase: true })).toBe('hello');
52
+ expect(validateString('hello', { uppercase: true })).toBe('HELLO');
53
+ });
54
+ });
55
+
56
+ describe('validateNumber', () => {
57
+ it('should validate numbers', () => {
58
+ expect(validateNumber(42)).toBe(42);
59
+ expect(validateNumber('42')).toBe(42);
60
+ expect(validateNumber('abc')).toBeNull();
61
+ expect(validateNumber(NaN)).toBeNull();
62
+ expect(validateNumber(Infinity)).toBeNull();
63
+ });
64
+
65
+ it('should apply range constraints', () => {
66
+ expect(validateNumber(5, { min: 10 })).toBeNull();
67
+ expect(validateNumber(15, { min: 10 })).toBe(15);
68
+ expect(validateNumber(15, { max: 10 })).toBeNull();
69
+ expect(validateNumber(5, { max: 10 })).toBe(5);
70
+ });
71
+
72
+ it('should validate integers', () => {
73
+ expect(validateNumber(5.5, { integer: true })).toBeNull();
74
+ expect(validateNumber(5, { integer: true })).toBe(5);
75
+ });
76
+ });
77
+
78
+ describe('validateBoolean', () => {
79
+ it('should validate booleans', () => {
80
+ expect(validateBoolean(true)).toBe(true);
81
+ expect(validateBoolean(false)).toBe(false);
82
+ expect(validateBoolean('true')).toBe(true);
83
+ expect(validateBoolean('false')).toBe(false);
84
+ expect(validateBoolean('1')).toBe(true);
85
+ expect(validateBoolean('0')).toBe(false);
86
+ expect(validateBoolean(1)).toBe(true);
87
+ expect(validateBoolean(0)).toBe(false);
88
+ expect(validateBoolean('maybe')).toBeNull();
89
+ });
90
+ });
91
+
92
+ describe('validateArray', () => {
93
+ it('should validate arrays with item validator', () => {
94
+ expect(validateArray([1, 2, 3], (x) => validateNumber(x))).toEqual([1, 2, 3]);
95
+ expect(validateArray([1, 'a', 3], (x) => validateNumber(x))).toBeNull();
96
+ });
97
+
98
+ it('should apply length constraints', () => {
99
+ expect(validateArray([1, 2], (x) => validateNumber(x), { minLength: 3 })).toBeNull();
100
+ expect(validateArray([1, 2, 3, 4], (x) => validateNumber(x), { maxLength: 3 })).toBeNull();
101
+ });
102
+
103
+ it('should enforce uniqueness', () => {
104
+ expect(validateArray([1, 2, 2], (x) => validateNumber(x), { unique: true })).toBeNull();
105
+ expect(validateArray([1, 2, 3], (x) => validateNumber(x), { unique: true })).toEqual([1, 2, 3]);
106
+ });
107
+ });
108
+
109
+ describe('validateEnum', () => {
110
+ it('should validate enum values', () => {
111
+ const allowed = ['a', 'b', 'c'] as const;
112
+ expect(validateEnum('a', allowed)).toBe('a');
113
+ expect(validateEnum('d', allowed)).toBeNull();
114
+ expect(validateEnum(123, allowed)).toBeNull();
115
+ });
116
+ });
117
+ });
118
+
119
+ describe('Path Security', () => {
120
+ describe('validatePath', () => {
121
+ it('should reject path traversal', () => {
122
+ expect(validatePath('../etc/passwd')).toBeNull();
123
+ expect(validatePath('/etc/../etc/passwd')).toBeNull();
124
+ });
125
+
126
+ it('should reject absolute paths by default', () => {
127
+ expect(validatePath('/absolute/path')).toBeNull();
128
+ expect(validatePath('/absolute/path', { allowAbsolute: true })).not.toBeNull();
129
+ });
130
+
131
+ it('should validate extensions', () => {
132
+ expect(validatePath('file.txt', { allowedExtensions: ['.js'] })).toBeNull();
133
+ expect(validatePath('file.js', { allowedExtensions: ['.js'] })).not.toBeNull();
134
+ });
135
+
136
+ it('should reject dangerous paths', () => {
137
+ expect(validatePath('/etc/passwd')).toBeNull();
138
+ expect(validatePath('/var/log/syslog')).toBeNull();
139
+ expect(validatePath('C:\\Windows\\System32')).toBeNull();
140
+ });
141
+ });
142
+
143
+ describe('safePath', () => {
144
+ it('should create safe paths within base', () => {
145
+ const base = '/project';
146
+ const result = safePath(base, 'src', 'index.ts');
147
+ expect(result).toBe(path.resolve(base, 'src', 'index.ts'));
148
+ });
149
+
150
+ it('should block path traversal attempts', () => {
151
+ const base = '/project';
152
+ expect(() => safePath(base, '..', 'etc', 'passwd')).toThrow('Path traversal blocked');
153
+ expect(() => safePath(base, 'src', '..', '..', 'etc')).toThrow('Path traversal blocked');
154
+ });
155
+ });
156
+ });
157
+
158
+ describe('JSON Security', () => {
159
+ describe('safeJsonParse', () => {
160
+ it('should parse valid JSON', () => {
161
+ expect(safeJsonParse('{"a": 1}')).toEqual({ a: 1 });
162
+ expect(safeJsonParse('[1, 2, 3]')).toEqual([1, 2, 3]);
163
+ });
164
+
165
+ it('should strip dangerous keys', () => {
166
+ const malicious = '{"__proto__": {"polluted": true}, "normal": 1}';
167
+ const result = safeJsonParse<Record<string, unknown>>(malicious);
168
+ expect(result.normal).toBe(1);
169
+ expect(result.__proto__).toBeUndefined();
170
+ });
171
+
172
+ it('should throw on invalid JSON', () => {
173
+ expect(() => safeJsonParse('not json')).toThrow();
174
+ });
175
+ });
176
+
177
+ describe('safeJsonStringify', () => {
178
+ it('should stringify objects', () => {
179
+ expect(safeJsonStringify({ a: 1 })).toBe('{"a":1}');
180
+ });
181
+
182
+ it('should handle circular references', () => {
183
+ const obj: Record<string, unknown> = { a: 1 };
184
+ obj.self = obj;
185
+ expect(() => safeJsonStringify(obj)).not.toThrow();
186
+ expect(safeJsonStringify(obj)).toContain('[Circular]');
187
+ });
188
+
189
+ it('should respect max depth', () => {
190
+ const deep = { a: { b: { c: { d: { e: 1 } } } } };
191
+ const result = safeJsonStringify(deep, { maxDepth: 3 });
192
+ expect(result).toContain('[Max Depth Exceeded]');
193
+ });
194
+ });
195
+ });
196
+
197
+ describe('Command Security', () => {
198
+ describe('validateCommand', () => {
199
+ it('should validate allowed commands', () => {
200
+ const result = validateCommand('npm install');
201
+ expect(result?.command).toBe('npm');
202
+ expect(result?.args).toEqual(['install']);
203
+ });
204
+
205
+ it('should reject blocked commands', () => {
206
+ expect(validateCommand('rm -rf /')).toBeNull();
207
+ expect(validateCommand('sudo apt install')).toBeNull();
208
+ });
209
+
210
+ it('should reject shell metacharacters', () => {
211
+ expect(validateCommand('npm install; rm -rf /')).toBeNull();
212
+ expect(validateCommand('npm install | cat')).toBeNull();
213
+ expect(validateCommand('npm install && rm')).toBeNull();
214
+ });
215
+ });
216
+
217
+ describe('escapeShellArg', () => {
218
+ it('should escape special characters', () => {
219
+ expect(escapeShellArg('hello')).toBe('hello');
220
+ expect(escapeShellArg('hello world')).toBe("'hello world'");
221
+ expect(escapeShellArg("it's")).toBe("'it'\"'\"'s'");
222
+ expect(escapeShellArg('')).toBe("''");
223
+ });
224
+ });
225
+ });
226
+
227
+ describe('Error Sanitization', () => {
228
+ describe('sanitizeErrorMessage', () => {
229
+ it('should remove sensitive data', () => {
230
+ expect(sanitizeErrorMessage('password=secret123')).not.toContain('secret123');
231
+ expect(sanitizeErrorMessage('api_key: abc123')).not.toContain('abc123');
232
+ expect(sanitizeErrorMessage('Bearer token123')).not.toContain('token123');
233
+ expect(sanitizeErrorMessage('http://user:pass@host.com')).not.toContain('pass');
234
+ });
235
+
236
+ it('should truncate long messages', () => {
237
+ const longMessage = 'a'.repeat(2000);
238
+ const result = sanitizeErrorMessage(longMessage);
239
+ expect(result.length).toBeLessThan(1100);
240
+ expect(result).toContain('[truncated]');
241
+ });
242
+ });
243
+
244
+ describe('sanitizeError', () => {
245
+ it('should create safe error objects', () => {
246
+ const error = new Error('password=secret');
247
+ const result = sanitizeError(error);
248
+
249
+ expect(result.name).toBe('Error');
250
+ expect(result.message).not.toContain('secret');
251
+ });
252
+
253
+ it('should handle non-Error inputs', () => {
254
+ const result = sanitizeError('string error');
255
+ expect(result.name).toBe('Error');
256
+ expect(result.message).toBe('string error');
257
+ });
258
+ });
259
+ });
260
+
261
+ describe('Rate Limiting', () => {
262
+ it('should limit requests', () => {
263
+ const limiter = createRateLimiter({
264
+ maxTokens: 3,
265
+ refillRate: 1,
266
+ refillInterval: 1000,
267
+ });
268
+
269
+ expect(limiter.tryAcquire()).toBe(true);
270
+ expect(limiter.tryAcquire()).toBe(true);
271
+ expect(limiter.tryAcquire()).toBe(true);
272
+ expect(limiter.tryAcquire()).toBe(false);
273
+ expect(limiter.getRemaining()).toBe(0);
274
+ });
275
+
276
+ it('should reset tokens', () => {
277
+ const limiter = createRateLimiter({
278
+ maxTokens: 2,
279
+ refillRate: 1,
280
+ refillInterval: 1000,
281
+ });
282
+
283
+ limiter.tryAcquire();
284
+ limiter.tryAcquire();
285
+ expect(limiter.getRemaining()).toBe(0);
286
+
287
+ limiter.reset();
288
+ expect(limiter.getRemaining()).toBe(2);
289
+ });
290
+ });
291
+
292
+ describe('Crypto Utilities', () => {
293
+ it('should generate secure IDs', () => {
294
+ const id1 = generateSecureId();
295
+ const id2 = generateSecureId();
296
+
297
+ expect(id1).toHaveLength(64); // 32 bytes = 64 hex chars
298
+ expect(id1).not.toBe(id2);
299
+ expect(/^[a-f0-9]+$/.test(id1)).toBe(true);
300
+ });
301
+
302
+ it('should generate secure tokens', () => {
303
+ const token = generateSecureToken();
304
+
305
+ expect(token.length).toBeGreaterThan(20);
306
+ // URL-safe base64
307
+ expect(/^[A-Za-z0-9_-]+$/.test(token)).toBe(true);
308
+ });
309
+
310
+ it('should hash strings consistently', () => {
311
+ const hash1 = hashString('hello');
312
+ const hash2 = hashString('hello');
313
+ const hash3 = hashString('world');
314
+
315
+ expect(hash1).toBe(hash2);
316
+ expect(hash1).not.toBe(hash3);
317
+ expect(hash1).toHaveLength(64); // SHA-256 = 64 hex chars
318
+ });
319
+
320
+ it('should compare strings in constant time', () => {
321
+ expect(constantTimeCompare('abc', 'abc')).toBe(true);
322
+ expect(constantTimeCompare('abc', 'abd')).toBe(false);
323
+ expect(constantTimeCompare('abc', 'abcd')).toBe(false);
324
+ });
325
+ });
326
+
327
+ describe('Resource Limiting', () => {
328
+ it('should check resource usage', () => {
329
+ const limiter = createResourceLimiter({
330
+ maxMemoryMB: 2048, // High limit to pass
331
+ });
332
+
333
+ const result = limiter.check();
334
+ expect(result.ok).toBe(true);
335
+ });
336
+
337
+ it('should enforce execution time limits', async () => {
338
+ const limiter = createResourceLimiter({
339
+ maxExecutionTime: 100,
340
+ });
341
+
342
+ await expect(
343
+ limiter.enforce(async () => {
344
+ await new Promise(resolve => setTimeout(resolve, 200));
345
+ })
346
+ ).rejects.toThrow('Execution time limit');
347
+ });
348
+ });