faultmesh 1.0.0 → 1.1.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 (43) hide show
  1. package/README.md +61 -19
  2. package/dist/dashboard/app.js +1906 -139
  3. package/dist/dashboard/index.html +393 -99
  4. package/dist/dashboard/styles.css +1191 -23
  5. package/dist/engine/ControlApi.d.ts +13 -1
  6. package/dist/engine/ControlApi.js +304 -14
  7. package/dist/engine/FaultMeshProxy.d.ts +2 -0
  8. package/dist/engine/FaultMeshProxy.js +41 -5
  9. package/dist/engine/TelemetryHub.d.ts +1 -0
  10. package/dist/engine/TelemetryHub.js +3 -0
  11. package/dist/engine/ToxicPipeline.d.ts +5 -4
  12. package/dist/engine/ToxicPipeline.js +17 -4
  13. package/dist/healer/AiHealer.d.ts +30 -0
  14. package/dist/healer/AiHealer.js +188 -0
  15. package/dist/healer/AutoHealer.d.ts +24 -0
  16. package/dist/healer/AutoHealer.js +503 -0
  17. package/dist/healer/DiffGenerator.d.ts +10 -0
  18. package/dist/healer/DiffGenerator.js +63 -0
  19. package/dist/healer/DiffUtil.d.ts +6 -0
  20. package/dist/healer/DiffUtil.js +67 -0
  21. package/dist/healer/transformers/ExpressTransformers.d.ts +43 -0
  22. package/dist/healer/transformers/ExpressTransformers.js +228 -0
  23. package/dist/healer/transformers/FastApiTransformers.d.ts +19 -0
  24. package/dist/healer/transformers/FastApiTransformers.js +93 -0
  25. package/dist/healer/transformers/GoTransformers.d.ts +19 -0
  26. package/dist/healer/transformers/GoTransformers.js +106 -0
  27. package/dist/healer/types.d.ts +59 -0
  28. package/dist/healer/types.js +1 -0
  29. package/dist/redteam/EccAgentBridge.d.ts +20 -0
  30. package/dist/redteam/EccAgentBridge.js +303 -0
  31. package/dist/redteam/RedTeamEngine.d.ts +56 -0
  32. package/dist/redteam/RedTeamEngine.js +709 -0
  33. package/dist/redteam/types.d.ts +117 -0
  34. package/dist/redteam/types.js +5 -0
  35. package/dist/scorer/ResilienceScorer.d.ts +5 -0
  36. package/dist/scorer/ResilienceScorer.js +323 -7
  37. package/dist/scorer/SecurityAuditor.d.ts +8 -0
  38. package/dist/scorer/SecurityAuditor.js +471 -69
  39. package/dist/scorer/TrafficStormAuditor.d.ts +5 -0
  40. package/dist/scorer/TrafficStormAuditor.js +328 -69
  41. package/dist/server.js +17 -10
  42. package/dist/types.d.ts +7 -3
  43. package/package.json +2 -1
@@ -3,6 +3,9 @@ import { TelemetryHub } from './TelemetryHub.js';
3
3
  import { ResilienceScorer } from '../scorer/ResilienceScorer.js';
4
4
  import { SecurityAuditor } from '../scorer/SecurityAuditor.js';
5
5
  import { TrafficStormAuditor } from '../scorer/TrafficStormAuditor.js';
6
+ import { FaultMeshProxy } from './FaultMeshProxy.js';
7
+ import { EccAgentBridge } from '../redteam/EccAgentBridge.js';
8
+ import { RedTeamEngine } from '../redteam/RedTeamEngine.js';
6
9
  export declare class ControlApi {
7
10
  private server;
8
11
  private port;
@@ -12,12 +15,21 @@ export declare class ControlApi {
12
15
  private securityAuditor;
13
16
  private trafficStormAuditor;
14
17
  private dashboardDir;
18
+ private proxy?;
19
+ private eccBridge;
20
+ private redTeamEngine;
15
21
  private isRunning;
16
- constructor(port: number, pipeline: ToxicPipeline, telemetryHub: TelemetryHub, scorer: ResilienceScorer, securityAuditor?: SecurityAuditor, trafficStormAuditor?: TrafficStormAuditor, dashboardDir?: string);
22
+ constructor(port: number, pipeline: ToxicPipeline, telemetryHub: TelemetryHub, scorer: ResilienceScorer, securityAuditor?: SecurityAuditor, trafficStormAuditor?: TrafficStormAuditor, dashboardDir?: string, proxy?: FaultMeshProxy, redTeamEngine?: RedTeamEngine, eccBridge?: EccAgentBridge);
17
23
  private handleRequest;
24
+ setProxy(proxy: FaultMeshProxy): void;
25
+ getRedTeamEngine(): RedTeamEngine;
26
+ getEccBridge(): EccAgentBridge;
18
27
  private serveStatic;
19
28
  private readBody;
20
29
  private json;
30
+ private sampleProcess?;
31
+ private restartSampleBackend;
32
+ private killPort;
21
33
  start(): Promise<void>;
22
34
  stop(): Promise<void>;
23
35
  getPort(): number;
@@ -2,8 +2,12 @@ import http from 'node:http';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
+ import { execSync, spawn } from 'node:child_process';
5
6
  import { SecurityAuditor } from '../scorer/SecurityAuditor.js';
6
7
  import { TrafficStormAuditor } from '../scorer/TrafficStormAuditor.js';
8
+ import { AutoHealer } from '../healer/AutoHealer.js';
9
+ import { EccAgentBridge } from '../redteam/EccAgentBridge.js';
10
+ import { RedTeamEngine } from '../redteam/RedTeamEngine.js';
7
11
  export class ControlApi {
8
12
  server;
9
13
  port = 0;
@@ -13,14 +17,20 @@ export class ControlApi {
13
17
  securityAuditor;
14
18
  trafficStormAuditor;
15
19
  dashboardDir;
20
+ proxy;
21
+ eccBridge;
22
+ redTeamEngine;
16
23
  isRunning = false;
17
- constructor(port, pipeline, telemetryHub, scorer, securityAuditor, trafficStormAuditor, dashboardDir) {
24
+ constructor(port, pipeline, telemetryHub, scorer, securityAuditor, trafficStormAuditor, dashboardDir, proxy, redTeamEngine, eccBridge) {
18
25
  this.port = port;
19
26
  this.pipeline = pipeline;
20
27
  this.telemetryHub = telemetryHub;
21
28
  this.scorer = scorer;
22
29
  this.securityAuditor = securityAuditor || new SecurityAuditor('http://127.0.0.1:3001');
23
30
  this.trafficStormAuditor = trafficStormAuditor || new TrafficStormAuditor('http://127.0.0.1:3001');
31
+ this.proxy = proxy;
32
+ this.eccBridge = eccBridge || new EccAgentBridge();
33
+ this.redTeamEngine = redTeamEngine || new RedTeamEngine(this.pipeline, this.telemetryHub, this.eccBridge);
24
34
  const currentDir = path.dirname(fileURLToPath(import.meta.url));
25
35
  let resolvedDir = dashboardDir || path.resolve(currentDir, '../dashboard');
26
36
  if (!fs.existsSync(resolvedDir)) {
@@ -33,6 +43,11 @@ export class ControlApi {
33
43
  this.server = http.createServer((req, res) => this.handleRequest(req, res));
34
44
  }
35
45
  async handleRequest(req, res) {
46
+ const rawUrl = req.url || '/';
47
+ if (rawUrl.includes('/..') || rawUrl.includes('..\\') || rawUrl.toLowerCase().includes('%2e%2e') || rawUrl.includes('\0')) {
48
+ this.json(res, 403, { error: 'Access Denied: Path Traversal Prohibited' });
49
+ return;
50
+ }
36
51
  const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
37
52
  const pathname = url.pathname;
38
53
  // Enable CORS for local dashboards or external runners
@@ -189,6 +204,186 @@ export class ControlApi {
189
204
  }
190
205
  return;
191
206
  }
207
+ // 8.5 Dynamic Target API Configuration
208
+ if (pathname === '/_faultmesh/config/target') {
209
+ if (req.method === 'GET') {
210
+ const currentTarget = this.proxy ? this.proxy.getTargetUrl() : 'http://127.0.0.1:4000';
211
+ this.json(res, 200, { targetUrl: currentTarget });
212
+ return;
213
+ }
214
+ if (req.method === 'POST') {
215
+ const raw = await this.readBody(req);
216
+ try {
217
+ const body = JSON.parse(raw);
218
+ if (!body.targetUrl || typeof body.targetUrl !== 'string') {
219
+ this.json(res, 400, { error: 'targetUrl is required and must be a string' });
220
+ return;
221
+ }
222
+ // Validate URL format
223
+ new URL(body.targetUrl);
224
+ if (this.proxy) {
225
+ this.proxy.setTargetUrl(body.targetUrl);
226
+ }
227
+ if (this.securityAuditor) {
228
+ this.securityAuditor.setTargetUrl(body.targetUrl);
229
+ }
230
+ if (this.trafficStormAuditor) {
231
+ this.trafficStormAuditor.setTargetUrl(body.targetUrl);
232
+ }
233
+ this.json(res, 200, { success: true, targetUrl: body.targetUrl });
234
+ }
235
+ catch (err) {
236
+ this.json(res, 400, { error: 'Invalid target URL format', details: err.message });
237
+ }
238
+ return;
239
+ }
240
+ }
241
+ // 8.6 Auto-Healer Endpoints
242
+ if (pathname === '/_faultmesh/healer/scan' && req.method === 'POST') {
243
+ try {
244
+ const raw = await this.readBody(req);
245
+ const body = raw ? JSON.parse(raw) : {};
246
+ const projectDir = body.projectDir || '.';
247
+ const failedChecks = body.failedChecks || [];
248
+ const aiConfig = body.aiConfig;
249
+ const engineMode = body.engineMode;
250
+ const scanResult = await AutoHealer.scan({ projectDir, failedChecks, aiConfig, engineMode });
251
+ this.json(res, 200, scanResult);
252
+ }
253
+ catch (err) {
254
+ this.json(res, 500, { error: 'AutoHealer scan error', details: err.message });
255
+ }
256
+ return;
257
+ }
258
+ if (pathname === '/_faultmesh/healer/apply' && req.method === 'POST') {
259
+ try {
260
+ const raw = await this.readBody(req);
261
+ const body = raw ? JSON.parse(raw) : {};
262
+ const projectDir = body.projectDir || '.';
263
+ const patchIds = body.patchIds;
264
+ const failedChecks = body.failedChecks;
265
+ const createBackup = body.createBackup !== false;
266
+ const aiConfig = body.aiConfig;
267
+ const engineMode = body.engineMode;
268
+ const applyResult = await AutoHealer.apply({ projectDir, patchIds, failedChecks, createBackup, aiConfig, engineMode });
269
+ // If healing the sample backend, automatically restart it so memory updates immediately and switch proxy target
270
+ if (applyResult.success && projectDir.includes('vulnerable-backend')) {
271
+ await this.restartSampleBackend();
272
+ if (this.proxy) {
273
+ this.proxy.setTargetUrl('http://127.0.0.1:5050');
274
+ }
275
+ if (this.securityAuditor) {
276
+ this.securityAuditor.setTargetUrl('http://127.0.0.1:5050');
277
+ }
278
+ if (this.trafficStormAuditor) {
279
+ this.trafficStormAuditor.setTargetUrl('http://127.0.0.1:5050');
280
+ }
281
+ }
282
+ this.json(res, 200, applyResult);
283
+ }
284
+ catch (err) {
285
+ this.json(res, 500, { error: 'AutoHealer apply error', details: err.message });
286
+ }
287
+ return;
288
+ }
289
+ if (pathname === '/_faultmesh/healer/rollback' && req.method === 'POST') {
290
+ try {
291
+ const raw = await this.readBody(req);
292
+ const body = raw ? JSON.parse(raw) : {};
293
+ if (!body.backupDir) {
294
+ this.json(res, 400, { error: 'backupDir is required for rollback' });
295
+ return;
296
+ }
297
+ const projectDir = body.projectDir || '.';
298
+ const rollbackResult = await AutoHealer.rollback({ projectDir, backupDir: body.backupDir });
299
+ if (rollbackResult.success && projectDir.includes('vulnerable-backend')) {
300
+ await this.restartSampleBackend();
301
+ }
302
+ this.json(res, 200, rollbackResult);
303
+ }
304
+ catch (err) {
305
+ this.json(res, 500, { error: 'AutoHealer rollback error', details: err.message });
306
+ }
307
+ return;
308
+ }
309
+ if (pathname === '/_faultmesh/sample/restart' && req.method === 'POST') {
310
+ try {
311
+ await this.restartSampleBackend();
312
+ this.json(res, 200, { success: true, message: 'Sample backend restarted on port 5050' });
313
+ }
314
+ catch (err) {
315
+ this.json(res, 500, { error: 'Failed to restart sample backend', details: err.message });
316
+ }
317
+ return;
318
+ }
319
+ if (pathname === '/_faultmesh/sample/reset' && req.method === 'POST') {
320
+ try {
321
+ const baselinePath = path.resolve(process.cwd(), 'examples/vulnerable-backend/baseline-vulnerable.js');
322
+ const targetPath = path.resolve(process.cwd(), 'examples/vulnerable-backend/server.js');
323
+ if (fs.existsSync(baselinePath)) {
324
+ fs.copyFileSync(baselinePath, targetPath);
325
+ }
326
+ await this.restartSampleBackend();
327
+ if (this.proxy) {
328
+ this.proxy.setTargetUrl('http://127.0.0.1:5050');
329
+ }
330
+ this.json(res, 200, { success: true, message: 'Sample backend restored to baseline vulnerable state on port 5050' });
331
+ }
332
+ catch (err) {
333
+ this.json(res, 500, { error: 'Failed to reset sample backend', details: err.message });
334
+ }
335
+ return;
336
+ }
337
+ // 8.8 Autonomous Red Chaos Team Engine
338
+ if (pathname === '/_faultmesh/redteam/personas' && req.method === 'GET') {
339
+ try {
340
+ const personas = await this.eccBridge.getPersonas();
341
+ this.json(res, 200, { success: true, personas });
342
+ }
343
+ catch (err) {
344
+ this.json(res, 500, { error: 'Failed to load agent personas', details: err.message });
345
+ }
346
+ return;
347
+ }
348
+ if (pathname === '/_faultmesh/redteam/start' && req.method === 'POST') {
349
+ try {
350
+ const raw = await this.readBody(req);
351
+ const options = raw ? JSON.parse(raw) : {};
352
+ if (options.targetUrl && options.targetUrl.includes(':5050')) {
353
+ if (this.proxy) {
354
+ this.proxy.setTargetUrl('http://127.0.0.1:5050');
355
+ }
356
+ }
357
+ if (!options.targetUrl || options.targetUrl.includes(':4000') || options.targetUrl.includes(':5050')) {
358
+ options.targetUrl = 'http://127.0.0.1:3001';
359
+ }
360
+ // Start campaign asynchronously in background
361
+ const campaignPromise = this.redTeamEngine.startCampaign(options);
362
+ campaignPromise.catch(err => {
363
+ console.error('Red Team background campaign error:', err.message);
364
+ });
365
+ const initialStatus = this.redTeamEngine.getStatus();
366
+ this.json(res, 202, { success: true, message: 'Red Team campaign initiated', status: initialStatus });
367
+ }
368
+ catch (err) {
369
+ this.json(res, 400, { error: 'Failed to start Red Team campaign', details: err.message });
370
+ }
371
+ return;
372
+ }
373
+ if (pathname === '/_faultmesh/redteam/abort' && req.method === 'POST') {
374
+ const result = this.redTeamEngine.abortCampaign();
375
+ this.json(res, 200, result);
376
+ return;
377
+ }
378
+ if (pathname === '/_faultmesh/redteam/status' && req.method === 'GET') {
379
+ this.json(res, 200, this.redTeamEngine.getStatus());
380
+ return;
381
+ }
382
+ if (pathname === '/_faultmesh/redteam/report' && req.method === 'GET') {
383
+ const report = this.redTeamEngine.getReport();
384
+ this.json(res, 200, report || { message: 'No campaign report recorded yet' });
385
+ return;
386
+ }
192
387
  // 9. Static Dashboard Serving
193
388
  if (req.method === 'GET') {
194
389
  this.serveStatic(pathname, res);
@@ -196,14 +391,40 @@ export class ControlApi {
196
391
  }
197
392
  this.json(res, 404, { error: 'Route not found' });
198
393
  }
394
+ setProxy(proxy) {
395
+ this.proxy = proxy;
396
+ }
397
+ getRedTeamEngine() {
398
+ return this.redTeamEngine;
399
+ }
400
+ getEccBridge() {
401
+ return this.eccBridge;
402
+ }
199
403
  serveStatic(pathname, res) {
200
- const safePath = pathname === '/' ? 'index.html' : pathname.replace(/^\//, '');
201
- const filePath = path.join(this.dashboardDir, safePath);
202
- // Prevent directory traversal
203
- if (!filePath.startsWith(this.dashboardDir)) {
204
- this.json(res, 403, { error: 'Forbidden' });
404
+ const normalizedDashboardDir = path.resolve(this.dashboardDir);
405
+ let decodedPath = '';
406
+ try {
407
+ decodedPath = decodeURIComponent(pathname);
408
+ }
409
+ catch {
410
+ this.json(res, 400, { error: 'Invalid URL encoding' });
205
411
  return;
206
412
  }
413
+ // Explicit Traversal Guard: reject relative parent indicators or backslashes
414
+ if (decodedPath.includes('..') || decodedPath.includes('\\')) {
415
+ this.json(res, 403, { error: 'Access Denied: Path Traversal Prohibited' });
416
+ return;
417
+ }
418
+ const safePath = decodedPath === '/' ? 'index.html' : decodedPath.replace(/^\/+/, '');
419
+ const resolvedPath = path.resolve(normalizedDashboardDir, safePath);
420
+ // Strict Path Jailing: prevent directory traversal outside dashboardDir
421
+ const isWithinDashboard = resolvedPath.startsWith(normalizedDashboardDir + path.sep) ||
422
+ resolvedPath === path.join(normalizedDashboardDir, 'index.html');
423
+ if (!isWithinDashboard) {
424
+ this.json(res, 403, { error: 'Access Denied: Path Traversal Prohibited' });
425
+ return;
426
+ }
427
+ const filePath = resolvedPath;
207
428
  if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
208
429
  const ext = path.extname(filePath);
209
430
  const contentTypes = {
@@ -217,15 +438,17 @@ export class ControlApi {
217
438
  fs.createReadStream(filePath).pipe(res);
218
439
  }
219
440
  else {
220
- // Fallback to index.html for SPA if exists
221
- const indexPath = path.join(this.dashboardDir, 'index.html');
222
- if (fs.existsSync(indexPath)) {
223
- res.writeHead(200, { 'Content-Type': 'text/html' });
224
- fs.createReadStream(indexPath).pipe(res);
225
- }
226
- else {
227
- this.json(res, 404, { error: 'File not found' });
441
+ // Fallback to index.html for SPA only if requesting an extensionless path (e.g. client routes)
442
+ const ext = path.extname(filePath);
443
+ if (!ext) {
444
+ const indexPath = path.join(this.dashboardDir, 'index.html');
445
+ if (fs.existsSync(indexPath)) {
446
+ res.writeHead(200, { 'Content-Type': 'text/html' });
447
+ fs.createReadStream(indexPath).pipe(res);
448
+ return;
449
+ }
228
450
  }
451
+ this.json(res, 404, { error: 'File not found' });
229
452
  }
230
453
  }
231
454
  readBody(req) {
@@ -240,6 +463,65 @@ export class ControlApi {
240
463
  res.writeHead(status, { 'Content-Type': 'application/json' });
241
464
  res.end(JSON.stringify(data));
242
465
  }
466
+ sampleProcess;
467
+ async restartSampleBackend() {
468
+ if (this.sampleProcess) {
469
+ try {
470
+ this.sampleProcess.kill();
471
+ }
472
+ catch { }
473
+ this.sampleProcess = undefined;
474
+ }
475
+ this.killPort(5050);
476
+ await new Promise((r) => setTimeout(r, 400));
477
+ const sampleScript = path.resolve(process.cwd(), 'examples/vulnerable-backend/server.js');
478
+ if (!fs.existsSync(sampleScript)) {
479
+ return;
480
+ }
481
+ const child = spawn(process.execPath, [sampleScript], {
482
+ cwd: path.dirname(sampleScript),
483
+ stdio: 'ignore',
484
+ detached: false,
485
+ });
486
+ this.sampleProcess = child;
487
+ const deadline = Date.now() + 6000;
488
+ while (Date.now() < deadline) {
489
+ try {
490
+ const res = await fetch('http://127.0.0.1:5050/api/health', {
491
+ signal: AbortSignal.timeout(500),
492
+ });
493
+ if (res.ok) {
494
+ return;
495
+ }
496
+ }
497
+ catch { }
498
+ await new Promise((r) => setTimeout(r, 200));
499
+ }
500
+ }
501
+ killPort(port) {
502
+ try {
503
+ if (process.platform === 'win32') {
504
+ const output = execSync(`netstat -ano | findstr :${port}`, { encoding: 'utf8' }).trim();
505
+ if (output) {
506
+ const lines = output.split('\n');
507
+ for (const line of lines) {
508
+ const parts = line.trim().split(/\s+/);
509
+ const pid = parts[parts.length - 1];
510
+ if (pid && pid !== '0' && pid !== String(process.pid)) {
511
+ try {
512
+ execSync(`taskkill /F /PID ${pid}`, { stdio: 'ignore' });
513
+ }
514
+ catch { }
515
+ }
516
+ }
517
+ }
518
+ }
519
+ else {
520
+ execSync(`fuser -k ${port}/tcp 2>/dev/null || true`, { stdio: 'ignore' });
521
+ }
522
+ }
523
+ catch { }
524
+ }
243
525
  start() {
244
526
  return new Promise((resolve, reject) => {
245
527
  this.server.listen(this.port, () => {
@@ -254,6 +536,14 @@ export class ControlApi {
254
536
  });
255
537
  }
256
538
  stop() {
539
+ if (this.sampleProcess) {
540
+ try {
541
+ this.sampleProcess.kill();
542
+ }
543
+ catch { }
544
+ this.sampleProcess = undefined;
545
+ }
546
+ this.killPort(5050);
257
547
  return new Promise((resolve) => {
258
548
  if (!this.isRunning)
259
549
  return resolve();
@@ -13,4 +13,6 @@ export declare class FaultMeshProxy {
13
13
  start(): Promise<void>;
14
14
  stop(): Promise<void>;
15
15
  getPort(): number;
16
+ setTargetUrl(url: string): void;
17
+ getTargetUrl(): string;
16
18
  }
@@ -28,8 +28,9 @@ export class FaultMeshProxy {
28
28
  res.end();
29
29
  return;
30
30
  }
31
+ const requestPath = req.url || '/';
31
32
  // 1. Check for immediate downstream Status override toxic
32
- const statusToxic = this.toxicPipeline.getActiveStatusToxic('downstream');
33
+ const statusToxic = this.toxicPipeline.getActiveStatusToxic('downstream', requestPath);
33
34
  if (statusToxic) {
34
35
  appliedToxics.push(`Status (${statusToxic.statusCode})`);
35
36
  const body = statusToxic.responseBody || JSON.stringify({
@@ -37,11 +38,32 @@ export class FaultMeshProxy {
37
38
  code: statusToxic.statusCode,
38
39
  faultMesh: true,
39
40
  });
40
- res.writeHead(statusToxic.statusCode, {
41
+ // Check if there is also an active downstream latency toxic
42
+ const latencyRule = this.toxicPipeline.getRules().find(r => r.enabled &&
43
+ r.type === 'latency' &&
44
+ r.direction === 'downstream' &&
45
+ (!r.pathPattern || !r.pathPattern.trim() || requestPath.includes(r.pathPattern.trim())));
46
+ if (latencyRule && latencyRule.config) {
47
+ const lCfg = latencyRule.config;
48
+ let delay = Math.max(0, lCfg.latencyMs || 0);
49
+ if (lCfg.jitterMs) {
50
+ const jitter = (Math.random() * 2 - 1) * lCfg.jitterMs;
51
+ delay = Math.max(0, Math.round(delay + jitter));
52
+ }
53
+ if (delay > 0) {
54
+ appliedToxics.push(`Latency (${delay}ms)`);
55
+ await new Promise(resolve => setTimeout(resolve, delay));
56
+ }
57
+ }
58
+ const headers = {
41
59
  'Content-Type': 'application/json',
42
60
  'X-FaultMesh-Injected': 'status',
43
61
  'Access-Control-Allow-Origin': '*',
44
- });
62
+ };
63
+ if (statusToxic.statusCode === 429) {
64
+ headers['Retry-After'] = '15';
65
+ }
66
+ res.writeHead(statusToxic.statusCode, headers);
45
67
  res.end(body);
46
68
  this.telemetryHub.recordRequestComplete({
47
69
  id: requestId,
@@ -58,17 +80,25 @@ export class FaultMeshProxy {
58
80
  }
59
81
  // 2. Prepare Upstream forwarding
60
82
  const targetUrl = new URL(req.url || '/', this.config.targetUrl);
83
+ const clientHost = req.headers['host'];
84
+ const forwardedHost = clientHost && !clientHost.includes('3001')
85
+ ? clientHost
86
+ : targetUrl.host;
61
87
  const options = {
62
88
  protocol: targetUrl.protocol,
63
89
  hostname: targetUrl.hostname,
64
90
  port: targetUrl.port,
65
91
  path: targetUrl.pathname + targetUrl.search,
66
92
  method: req.method,
67
- headers: { ...req.headers, host: targetUrl.host },
93
+ headers: {
94
+ ...req.headers,
95
+ host: forwardedHost,
96
+ 'x-forwarded-host': clientHost || targetUrl.host,
97
+ },
68
98
  };
69
99
  const upstreamReq = http.request(options, async (upstreamRes) => {
70
100
  // 3. Prepare Downstream transformers
71
- const { transformers: downstreamTransformers, appliedNames } = this.toxicPipeline.createStreamTransformers('downstream');
101
+ const { transformers: downstreamTransformers, appliedNames } = this.toxicPipeline.createStreamTransformers('downstream', requestPath);
72
102
  appliedToxics.push(...appliedNames);
73
103
  const headers = { ...upstreamRes.headers };
74
104
  delete headers['content-length']; // Length changes if chunks are compressed, cut, or padded
@@ -164,4 +194,10 @@ export class FaultMeshProxy {
164
194
  getPort() {
165
195
  return this.port;
166
196
  }
197
+ setTargetUrl(url) {
198
+ this.config.targetUrl = url;
199
+ }
200
+ getTargetUrl() {
201
+ return this.config.targetUrl;
202
+ }
167
203
  }
@@ -11,6 +11,7 @@ export declare class TelemetryHub {
11
11
  getMetrics(): ProxyMetrics;
12
12
  getRecentEvents(): TelemetryEvent[];
13
13
  registerSSEClient(res: ServerResponse): void;
14
+ broadcastCustomEvent(eventType: string, data: any): void;
14
15
  private broadcastSSE;
15
16
  clear(): void;
16
17
  }
@@ -42,6 +42,9 @@ export class TelemetryHub {
42
42
  this.sseClients.delete(res);
43
43
  });
44
44
  }
45
+ broadcastCustomEvent(eventType, data) {
46
+ this.broadcastSSE(eventType, data);
47
+ }
45
48
  broadcastSSE(eventType, data) {
46
49
  const payload = `event: ${eventType}\ndata: ${JSON.stringify(data)}\n\n`;
47
50
  for (const client of this.sseClients) {
@@ -8,14 +8,15 @@ export declare class ToxicPipeline {
8
8
  getRules(): ToxicRule[];
9
9
  getRule(id: string): ToxicRule | undefined;
10
10
  clearRules(): void;
11
+ private matchesPath;
11
12
  /**
12
- * Check if any active StatusToxic exists for the specified direction
13
+ * Check if any active StatusToxic exists for the specified direction and request path
13
14
  */
14
- getActiveStatusToxic(direction: ToxicStreamDirection): StatusToxic | null;
15
+ getActiveStatusToxic(direction: ToxicStreamDirection, requestPath?: string): StatusToxic | null;
15
16
  /**
16
- * Create an array of streaming transformers for the specified stream direction
17
+ * Create an array of streaming transformers for the specified stream direction and request path
17
18
  */
18
- createStreamTransformers(direction: ToxicStreamDirection): {
19
+ createStreamTransformers(direction: ToxicStreamDirection, requestPath?: string): {
19
20
  transformers: Transform[];
20
21
  appliedNames: string[];
21
22
  };
@@ -20,26 +20,39 @@ export class ToxicPipeline {
20
20
  clearRules() {
21
21
  this.rules.clear();
22
22
  }
23
+ matchesPath(rule, requestPath) {
24
+ if (!rule.pathPattern || !rule.pathPattern.trim()) {
25
+ return true;
26
+ }
27
+ if (!requestPath) {
28
+ return true;
29
+ }
30
+ return requestPath.includes(rule.pathPattern.trim());
31
+ }
23
32
  /**
24
- * Check if any active StatusToxic exists for the specified direction
33
+ * Check if any active StatusToxic exists for the specified direction and request path
25
34
  */
26
- getActiveStatusToxic(direction) {
35
+ getActiveStatusToxic(direction, requestPath) {
27
36
  for (const rule of this.rules.values()) {
28
37
  if (rule.enabled && rule.type === 'status' && rule.direction === direction) {
38
+ if (!this.matchesPath(rule, requestPath))
39
+ continue;
29
40
  return new StatusToxic(rule.config);
30
41
  }
31
42
  }
32
43
  return null;
33
44
  }
34
45
  /**
35
- * Create an array of streaming transformers for the specified stream direction
46
+ * Create an array of streaming transformers for the specified stream direction and request path
36
47
  */
37
- createStreamTransformers(direction) {
48
+ createStreamTransformers(direction, requestPath) {
38
49
  const transformers = [];
39
50
  const appliedNames = [];
40
51
  for (const rule of this.rules.values()) {
41
52
  if (!rule.enabled || rule.direction !== direction)
42
53
  continue;
54
+ if (!this.matchesPath(rule, requestPath))
55
+ continue;
43
56
  switch (rule.type) {
44
57
  case 'latency':
45
58
  transformers.push(new LatencyToxic(rule.config));
@@ -0,0 +1,30 @@
1
+ import { AiProviderConfig, BackendFramework } from './types.js';
2
+ export interface AiRemediationResult {
3
+ success: boolean;
4
+ content: string;
5
+ description: string;
6
+ error?: string;
7
+ }
8
+ export declare class AiHealer {
9
+ /**
10
+ * Checks if an AI provider is reachable or configured
11
+ */
12
+ static isAvailable(config?: AiProviderConfig): Promise<{
13
+ available: boolean;
14
+ provider?: string;
15
+ }>;
16
+ /**
17
+ * Generates a surgical code remediation using the configured or detected LLM provider
18
+ */
19
+ static generateRemediation(options: {
20
+ filePath: string;
21
+ originalContent: string;
22
+ framework: BackendFramework;
23
+ failedChecks: string[];
24
+ config?: AiProviderConfig;
25
+ }): Promise<AiRemediationResult>;
26
+ /**
27
+ * Safely strips enclosing markdown code fences from model outputs
28
+ */
29
+ private static extractCodeBlock;
30
+ }