faultmesh 1.0.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 (37) hide show
  1. package/README.md +98 -0
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +105 -0
  4. package/dist/dashboard/app.js +998 -0
  5. package/dist/dashboard/index.html +291 -0
  6. package/dist/dashboard/styles.css +1120 -0
  7. package/dist/engine/ControlApi.d.ts +24 -0
  8. package/dist/engine/ControlApi.js +269 -0
  9. package/dist/engine/FaultMeshProxy.d.ts +16 -0
  10. package/dist/engine/FaultMeshProxy.js +167 -0
  11. package/dist/engine/TelemetryHub.d.ts +16 -0
  12. package/dist/engine/TelemetryHub.js +67 -0
  13. package/dist/engine/ToxicPipeline.d.ts +22 -0
  14. package/dist/engine/ToxicPipeline.js +67 -0
  15. package/dist/scorer/ResilienceScorer.d.ts +13 -0
  16. package/dist/scorer/ResilienceScorer.js +330 -0
  17. package/dist/scorer/SecurityAuditor.d.ts +13 -0
  18. package/dist/scorer/SecurityAuditor.js +428 -0
  19. package/dist/scorer/TrafficStormAuditor.d.ts +10 -0
  20. package/dist/scorer/TrafficStormAuditor.js +261 -0
  21. package/dist/server.d.ts +21 -0
  22. package/dist/server.js +169 -0
  23. package/dist/toxics/BandwidthToxic.d.ts +11 -0
  24. package/dist/toxics/BandwidthToxic.js +36 -0
  25. package/dist/toxics/BaseToxic.d.ts +11 -0
  26. package/dist/toxics/BaseToxic.js +19 -0
  27. package/dist/toxics/CorruptToxic.d.ts +11 -0
  28. package/dist/toxics/CorruptToxic.js +53 -0
  29. package/dist/toxics/CutToxic.d.ts +9 -0
  30. package/dist/toxics/CutToxic.js +31 -0
  31. package/dist/toxics/LatencyToxic.d.ts +13 -0
  32. package/dist/toxics/LatencyToxic.js +40 -0
  33. package/dist/toxics/StatusToxic.d.ts +9 -0
  34. package/dist/toxics/StatusToxic.js +22 -0
  35. package/dist/types.d.ts +123 -0
  36. package/dist/types.js +4 -0
  37. package/package.json +35 -0
@@ -0,0 +1,24 @@
1
+ import { ToxicPipeline } from './ToxicPipeline.js';
2
+ import { TelemetryHub } from './TelemetryHub.js';
3
+ import { ResilienceScorer } from '../scorer/ResilienceScorer.js';
4
+ import { SecurityAuditor } from '../scorer/SecurityAuditor.js';
5
+ import { TrafficStormAuditor } from '../scorer/TrafficStormAuditor.js';
6
+ export declare class ControlApi {
7
+ private server;
8
+ private port;
9
+ private pipeline;
10
+ private telemetryHub;
11
+ private scorer;
12
+ private securityAuditor;
13
+ private trafficStormAuditor;
14
+ private dashboardDir;
15
+ private isRunning;
16
+ constructor(port: number, pipeline: ToxicPipeline, telemetryHub: TelemetryHub, scorer: ResilienceScorer, securityAuditor?: SecurityAuditor, trafficStormAuditor?: TrafficStormAuditor, dashboardDir?: string);
17
+ private handleRequest;
18
+ private serveStatic;
19
+ private readBody;
20
+ private json;
21
+ start(): Promise<void>;
22
+ stop(): Promise<void>;
23
+ getPort(): number;
24
+ }
@@ -0,0 +1,269 @@
1
+ import http from 'node:http';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { SecurityAuditor } from '../scorer/SecurityAuditor.js';
6
+ import { TrafficStormAuditor } from '../scorer/TrafficStormAuditor.js';
7
+ export class ControlApi {
8
+ server;
9
+ port = 0;
10
+ pipeline;
11
+ telemetryHub;
12
+ scorer;
13
+ securityAuditor;
14
+ trafficStormAuditor;
15
+ dashboardDir;
16
+ isRunning = false;
17
+ constructor(port, pipeline, telemetryHub, scorer, securityAuditor, trafficStormAuditor, dashboardDir) {
18
+ this.port = port;
19
+ this.pipeline = pipeline;
20
+ this.telemetryHub = telemetryHub;
21
+ this.scorer = scorer;
22
+ this.securityAuditor = securityAuditor || new SecurityAuditor('http://127.0.0.1:3001');
23
+ this.trafficStormAuditor = trafficStormAuditor || new TrafficStormAuditor('http://127.0.0.1:3001');
24
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
25
+ let resolvedDir = dashboardDir || path.resolve(currentDir, '../dashboard');
26
+ if (!fs.existsSync(resolvedDir)) {
27
+ const fallbackDir = path.resolve(currentDir, '../../src/dashboard');
28
+ if (fs.existsSync(fallbackDir)) {
29
+ resolvedDir = fallbackDir;
30
+ }
31
+ }
32
+ this.dashboardDir = resolvedDir;
33
+ this.server = http.createServer((req, res) => this.handleRequest(req, res));
34
+ }
35
+ async handleRequest(req, res) {
36
+ const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
37
+ const pathname = url.pathname;
38
+ // Enable CORS for local dashboards or external runners
39
+ res.setHeader('Access-Control-Allow-Origin', '*');
40
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
41
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
42
+ if (req.method === 'OPTIONS') {
43
+ res.writeHead(204);
44
+ res.end();
45
+ return;
46
+ }
47
+ // 1. SSE Stream
48
+ if (pathname === '/_faultmesh/telemetry/stream' && req.method === 'GET') {
49
+ res.writeHead(200, {
50
+ 'Content-Type': 'text/event-stream',
51
+ 'Cache-Control': 'no-cache',
52
+ 'Connection': 'keep-alive',
53
+ });
54
+ this.telemetryHub.registerSSEClient(res);
55
+ return;
56
+ }
57
+ // 2. Status & Metrics
58
+ if (pathname === '/_faultmesh/status' && req.method === 'GET') {
59
+ const rules = this.pipeline.getRules();
60
+ this.json(res, 200, {
61
+ status: 'online',
62
+ engine: 'FaultMesh v1.0.0',
63
+ metrics: this.telemetryHub.getMetrics(),
64
+ activeRules: rules,
65
+ activeToxics: rules, // backward compatibility
66
+ });
67
+ return;
68
+ }
69
+ // 3. Rules List
70
+ if ((pathname === '/_faultmesh/rules' || pathname === '/_faultmesh/toxics') && req.method === 'GET') {
71
+ this.json(res, 200, this.pipeline.getRules());
72
+ return;
73
+ }
74
+ // 4. Add / Update Rule
75
+ if ((pathname === '/_faultmesh/rules' || pathname === '/_faultmesh/toxics') && req.method === 'POST') {
76
+ const body = await this.readBody(req);
77
+ try {
78
+ const rule = JSON.parse(body);
79
+ if (!rule.id || !rule.type || !rule.config) {
80
+ this.json(res, 400, { error: 'Invalid rule definition. Required: id, type, config' });
81
+ return;
82
+ }
83
+ this.pipeline.addRule(rule);
84
+ this.json(res, 201, { success: true, rule });
85
+ }
86
+ catch (err) {
87
+ this.json(res, 400, { error: 'Malformed JSON payload', details: err.message });
88
+ }
89
+ return;
90
+ }
91
+ // 5. Delete Rule
92
+ const isDeleteSpecific = pathname.startsWith('/_faultmesh/rules/') || pathname.startsWith('/_faultmesh/toxics/');
93
+ if (isDeleteSpecific && req.method === 'DELETE') {
94
+ const prefix = pathname.startsWith('/_faultmesh/rules/') ? '/_faultmesh/rules/' : '/_faultmesh/toxics/';
95
+ const id = pathname.substring(prefix.length);
96
+ const removed = this.pipeline.removeRule(id);
97
+ this.json(res, removed ? 200 : 404, { success: removed, id });
98
+ return;
99
+ }
100
+ if ((pathname === '/_faultmesh/rules' || pathname === '/_faultmesh/toxics') && req.method === 'DELETE') {
101
+ this.pipeline.clearRules();
102
+ this.json(res, 200, { success: true, message: 'All simulation rules cleared' });
103
+ return;
104
+ }
105
+ // 6. Run Diagnostics / Health Check
106
+ const isRunTests = pathname === '/_faultmesh/diagnostics/run' || pathname === '/_faultmesh/tests/run' || pathname === '/_faultmesh/gauntlet/run';
107
+ if (isRunTests && req.method === 'POST') {
108
+ try {
109
+ let profile = 'resilient';
110
+ const queryProfile = url.searchParams.get('profile');
111
+ if (queryProfile === 'fragile' || queryProfile === 'resilient') {
112
+ profile = queryProfile;
113
+ }
114
+ else {
115
+ const body = await this.readBody(req);
116
+ if (body) {
117
+ try {
118
+ const parsed = JSON.parse(body);
119
+ if (parsed.profile === 'fragile' || parsed.profile === 'resilient') {
120
+ profile = parsed.profile;
121
+ }
122
+ }
123
+ catch { }
124
+ }
125
+ }
126
+ const scorecard = await this.scorer.runGauntlet(profile);
127
+ this.json(res, 200, scorecard);
128
+ }
129
+ catch (err) {
130
+ this.json(res, 500, { error: 'Diagnostics execution error', details: err.message });
131
+ }
132
+ return;
133
+ }
134
+ // 7. Run Security & Protocol Audit
135
+ const isRunSecurity = pathname === '/_faultmesh/security/run' || pathname === '/_faultmesh/security/audit';
136
+ if (isRunSecurity && req.method === 'POST') {
137
+ try {
138
+ let profile = 'secure';
139
+ const queryProfile = url.searchParams.get('profile');
140
+ if (queryProfile === 'secure' || queryProfile === 'vulnerable') {
141
+ profile = queryProfile;
142
+ }
143
+ else {
144
+ const body = await this.readBody(req);
145
+ if (body) {
146
+ try {
147
+ const parsed = JSON.parse(body);
148
+ if (parsed.profile === 'secure' || parsed.profile === 'vulnerable') {
149
+ profile = parsed.profile;
150
+ }
151
+ }
152
+ catch { }
153
+ }
154
+ }
155
+ const scorecard = await this.securityAuditor.runAudit(profile);
156
+ this.json(res, 200, scorecard);
157
+ }
158
+ catch (err) {
159
+ this.json(res, 500, { error: 'Security audit execution error', details: err.message });
160
+ }
161
+ return;
162
+ }
163
+ // 8. Run Traffic Storm & DoS Resilience Audit
164
+ const isRunStorm = pathname === '/_faultmesh/storm/run' || pathname === '/_faultmesh/storm/audit';
165
+ if (isRunStorm && req.method === 'POST') {
166
+ try {
167
+ let profile = 'resilient';
168
+ const queryProfile = url.searchParams.get('profile');
169
+ if (queryProfile === 'fragile' || queryProfile === 'resilient') {
170
+ profile = queryProfile;
171
+ }
172
+ else {
173
+ const body = await this.readBody(req);
174
+ if (body) {
175
+ try {
176
+ const parsed = JSON.parse(body);
177
+ if (parsed.profile === 'fragile' || parsed.profile === 'resilient') {
178
+ profile = parsed.profile;
179
+ }
180
+ }
181
+ catch { }
182
+ }
183
+ }
184
+ const scorecard = await this.trafficStormAuditor.runStormSuite(profile);
185
+ this.json(res, 200, scorecard);
186
+ }
187
+ catch (err) {
188
+ this.json(res, 500, { error: 'Traffic storm execution error', details: err.message });
189
+ }
190
+ return;
191
+ }
192
+ // 9. Static Dashboard Serving
193
+ if (req.method === 'GET') {
194
+ this.serveStatic(pathname, res);
195
+ return;
196
+ }
197
+ this.json(res, 404, { error: 'Route not found' });
198
+ }
199
+ 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' });
205
+ return;
206
+ }
207
+ if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
208
+ const ext = path.extname(filePath);
209
+ const contentTypes = {
210
+ '.html': 'text/html',
211
+ '.css': 'text/css',
212
+ '.js': 'application/javascript',
213
+ '.json': 'application/json',
214
+ '.svg': 'image/svg+xml',
215
+ };
216
+ res.writeHead(200, { 'Content-Type': contentTypes[ext] || 'text/plain' });
217
+ fs.createReadStream(filePath).pipe(res);
218
+ }
219
+ 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' });
228
+ }
229
+ }
230
+ }
231
+ readBody(req) {
232
+ return new Promise((resolve, reject) => {
233
+ let data = '';
234
+ req.on('data', chunk => data += chunk);
235
+ req.on('end', () => resolve(data));
236
+ req.on('error', reject);
237
+ });
238
+ }
239
+ json(res, status, data) {
240
+ res.writeHead(status, { 'Content-Type': 'application/json' });
241
+ res.end(JSON.stringify(data));
242
+ }
243
+ start() {
244
+ return new Promise((resolve, reject) => {
245
+ this.server.listen(this.port, () => {
246
+ const address = this.server.address();
247
+ if (address && typeof address === 'object') {
248
+ this.port = address.port;
249
+ }
250
+ this.isRunning = true;
251
+ resolve();
252
+ });
253
+ this.server.once('error', reject);
254
+ });
255
+ }
256
+ stop() {
257
+ return new Promise((resolve) => {
258
+ if (!this.isRunning)
259
+ return resolve();
260
+ this.server.close(() => {
261
+ this.isRunning = false;
262
+ resolve();
263
+ });
264
+ });
265
+ }
266
+ getPort() {
267
+ return this.port;
268
+ }
269
+ }
@@ -0,0 +1,16 @@
1
+ import { ProxyConfig } from '../types.js';
2
+ import { ToxicPipeline } from './ToxicPipeline.js';
3
+ import { TelemetryHub } from './TelemetryHub.js';
4
+ export declare class FaultMeshProxy {
5
+ private server;
6
+ private port;
7
+ private config;
8
+ private toxicPipeline;
9
+ private telemetryHub;
10
+ private isRunning;
11
+ constructor(config: ProxyConfig, toxicPipeline: ToxicPipeline, telemetryHub: TelemetryHub);
12
+ private handleRequest;
13
+ start(): Promise<void>;
14
+ stop(): Promise<void>;
15
+ getPort(): number;
16
+ }
@@ -0,0 +1,167 @@
1
+ import http from 'node:http';
2
+ export class FaultMeshProxy {
3
+ server;
4
+ port = 0;
5
+ config;
6
+ toxicPipeline;
7
+ telemetryHub;
8
+ isRunning = false;
9
+ constructor(config, toxicPipeline, telemetryHub) {
10
+ this.config = config;
11
+ this.toxicPipeline = toxicPipeline;
12
+ this.telemetryHub = telemetryHub;
13
+ this.server = http.createServer((req, res) => this.handleRequest(req, res));
14
+ }
15
+ async handleRequest(req, res) {
16
+ const startTime = Date.now();
17
+ const requestId = `req_${Math.random().toString(36).substring(2, 9)}`;
18
+ this.telemetryHub.recordRequestStart();
19
+ const appliedToxics = [];
20
+ let bytesReceived = 0;
21
+ let bytesSent = 0;
22
+ if (req.method === 'OPTIONS') {
23
+ res.writeHead(204, {
24
+ 'Access-Control-Allow-Origin': '*',
25
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
26
+ 'Access-Control-Allow-Headers': '*',
27
+ });
28
+ res.end();
29
+ return;
30
+ }
31
+ // 1. Check for immediate downstream Status override toxic
32
+ const statusToxic = this.toxicPipeline.getActiveStatusToxic('downstream');
33
+ if (statusToxic) {
34
+ appliedToxics.push(`Status (${statusToxic.statusCode})`);
35
+ const body = statusToxic.responseBody || JSON.stringify({
36
+ error: statusToxic.statusMessage,
37
+ code: statusToxic.statusCode,
38
+ faultMesh: true,
39
+ });
40
+ res.writeHead(statusToxic.statusCode, {
41
+ 'Content-Type': 'application/json',
42
+ 'X-FaultMesh-Injected': 'status',
43
+ 'Access-Control-Allow-Origin': '*',
44
+ });
45
+ res.end(body);
46
+ this.telemetryHub.recordRequestComplete({
47
+ id: requestId,
48
+ timestamp: startTime,
49
+ method: req.method || 'GET',
50
+ path: req.url || '/',
51
+ statusCode: statusToxic.statusCode,
52
+ durationMs: Date.now() - startTime,
53
+ bytesReceived: 0,
54
+ bytesSent: Buffer.byteLength(body),
55
+ appliedToxics,
56
+ });
57
+ return;
58
+ }
59
+ // 2. Prepare Upstream forwarding
60
+ const targetUrl = new URL(req.url || '/', this.config.targetUrl);
61
+ const options = {
62
+ protocol: targetUrl.protocol,
63
+ hostname: targetUrl.hostname,
64
+ port: targetUrl.port,
65
+ path: targetUrl.pathname + targetUrl.search,
66
+ method: req.method,
67
+ headers: { ...req.headers, host: targetUrl.host },
68
+ };
69
+ const upstreamReq = http.request(options, async (upstreamRes) => {
70
+ // 3. Prepare Downstream transformers
71
+ const { transformers: downstreamTransformers, appliedNames } = this.toxicPipeline.createStreamTransformers('downstream');
72
+ appliedToxics.push(...appliedNames);
73
+ const headers = { ...upstreamRes.headers };
74
+ delete headers['content-length']; // Length changes if chunks are compressed, cut, or padded
75
+ headers['x-faultmesh-proxy'] = 'active';
76
+ headers['access-control-allow-origin'] = '*';
77
+ res.writeHead(upstreamRes.statusCode || 200, headers);
78
+ try {
79
+ if (downstreamTransformers.length > 0) {
80
+ // Chain transformers between upstreamRes and client res
81
+ let currentStream = upstreamRes;
82
+ for (const t of downstreamTransformers) {
83
+ currentStream = currentStream.pipe(t);
84
+ }
85
+ currentStream.on('data', (chunk) => {
86
+ bytesSent += chunk.length;
87
+ res.write(chunk);
88
+ });
89
+ currentStream.on('end', () => {
90
+ res.end();
91
+ finishTelemetry(upstreamRes.statusCode || 200);
92
+ });
93
+ currentStream.on('error', (err) => {
94
+ res.destroy(err);
95
+ finishTelemetry(upstreamRes.statusCode || 200, err.message);
96
+ });
97
+ }
98
+ else {
99
+ upstreamRes.on('data', (chunk) => {
100
+ bytesSent += chunk.length;
101
+ res.write(chunk);
102
+ });
103
+ upstreamRes.on('end', () => {
104
+ res.end();
105
+ finishTelemetry(upstreamRes.statusCode || 200);
106
+ });
107
+ }
108
+ }
109
+ catch (err) {
110
+ res.destroy(err);
111
+ finishTelemetry(502, err.message);
112
+ }
113
+ });
114
+ const finishTelemetry = (statusCode, error) => {
115
+ this.telemetryHub.recordRequestComplete({
116
+ id: requestId,
117
+ timestamp: startTime,
118
+ method: req.method || 'GET',
119
+ path: req.url || '/',
120
+ statusCode,
121
+ durationMs: Date.now() - startTime,
122
+ bytesReceived,
123
+ bytesSent,
124
+ appliedToxics,
125
+ error,
126
+ });
127
+ };
128
+ upstreamReq.on('error', (err) => {
129
+ if (!res.headersSent) {
130
+ res.writeHead(502, { 'Content-Type': 'application/json' });
131
+ res.end(JSON.stringify({ error: 'Upstream connection failed', details: err.message }));
132
+ }
133
+ finishTelemetry(502, err.message);
134
+ });
135
+ req.on('data', (chunk) => {
136
+ bytesReceived += chunk.length;
137
+ });
138
+ // Forward request body to upstream
139
+ req.pipe(upstreamReq);
140
+ }
141
+ start() {
142
+ return new Promise((resolve, reject) => {
143
+ this.server.listen(this.config.port, () => {
144
+ const address = this.server.address();
145
+ if (address && typeof address === 'object') {
146
+ this.port = address.port;
147
+ }
148
+ this.isRunning = true;
149
+ resolve();
150
+ });
151
+ this.server.once('error', reject);
152
+ });
153
+ }
154
+ stop() {
155
+ return new Promise((resolve) => {
156
+ if (!this.isRunning)
157
+ return resolve();
158
+ this.server.close(() => {
159
+ this.isRunning = false;
160
+ resolve();
161
+ });
162
+ });
163
+ }
164
+ getPort() {
165
+ return this.port;
166
+ }
167
+ }
@@ -0,0 +1,16 @@
1
+ import { ServerResponse } from 'node:http';
2
+ import { ProxyMetrics, TelemetryEvent } from '../types.js';
3
+ export declare class TelemetryHub {
4
+ private metrics;
5
+ private recentEvents;
6
+ private readonly maxRecentEvents;
7
+ private sseClients;
8
+ private latencySum;
9
+ recordRequestStart(): void;
10
+ recordRequestComplete(event: TelemetryEvent): void;
11
+ getMetrics(): ProxyMetrics;
12
+ getRecentEvents(): TelemetryEvent[];
13
+ registerSSEClient(res: ServerResponse): void;
14
+ private broadcastSSE;
15
+ clear(): void;
16
+ }
@@ -0,0 +1,67 @@
1
+ export class TelemetryHub {
2
+ metrics = {
3
+ totalRequests: 0,
4
+ activeConnections: 0,
5
+ bytesProxied: 0,
6
+ faultsInjected: 0,
7
+ avgLatencyMs: 0,
8
+ };
9
+ recentEvents = [];
10
+ maxRecentEvents = 50;
11
+ sseClients = new Set();
12
+ latencySum = 0;
13
+ recordRequestStart() {
14
+ this.metrics.totalRequests++;
15
+ this.metrics.activeConnections++;
16
+ }
17
+ recordRequestComplete(event) {
18
+ this.metrics.activeConnections = Math.max(0, this.metrics.activeConnections - 1);
19
+ this.metrics.bytesProxied += event.bytesReceived + event.bytesSent;
20
+ this.metrics.faultsInjected += event.appliedToxics.length;
21
+ this.latencySum += event.durationMs;
22
+ this.metrics.avgLatencyMs = Math.round(this.latencySum / this.metrics.totalRequests);
23
+ // Keep ring buffer
24
+ this.recentEvents.unshift(event);
25
+ if (this.recentEvents.length > this.maxRecentEvents) {
26
+ this.recentEvents.pop();
27
+ }
28
+ // Broadcast to live SSE subscribers
29
+ this.broadcastSSE('telemetry', event);
30
+ }
31
+ getMetrics() {
32
+ return { ...this.metrics };
33
+ }
34
+ getRecentEvents() {
35
+ return [...this.recentEvents];
36
+ }
37
+ registerSSEClient(res) {
38
+ this.sseClients.add(res);
39
+ // Send initial snapshot
40
+ res.write(`event: snapshot\ndata: ${JSON.stringify({ metrics: this.metrics, events: this.recentEvents })}\n\n`);
41
+ res.on('close', () => {
42
+ this.sseClients.delete(res);
43
+ });
44
+ }
45
+ broadcastSSE(eventType, data) {
46
+ const payload = `event: ${eventType}\ndata: ${JSON.stringify(data)}\n\n`;
47
+ for (const client of this.sseClients) {
48
+ try {
49
+ client.write(payload);
50
+ }
51
+ catch {
52
+ this.sseClients.delete(client);
53
+ }
54
+ }
55
+ }
56
+ clear() {
57
+ this.metrics = {
58
+ totalRequests: 0,
59
+ activeConnections: 0,
60
+ bytesProxied: 0,
61
+ faultsInjected: 0,
62
+ avgLatencyMs: 0,
63
+ };
64
+ this.latencySum = 0;
65
+ this.recentEvents = [];
66
+ }
67
+ }
@@ -0,0 +1,22 @@
1
+ import { Transform } from 'node:stream';
2
+ import { ToxicRule, ToxicStreamDirection } from '../types.js';
3
+ import { StatusToxic } from '../toxics/StatusToxic.js';
4
+ export declare class ToxicPipeline {
5
+ private rules;
6
+ addRule(rule: ToxicRule): void;
7
+ removeRule(id: string): boolean;
8
+ getRules(): ToxicRule[];
9
+ getRule(id: string): ToxicRule | undefined;
10
+ clearRules(): void;
11
+ /**
12
+ * Check if any active StatusToxic exists for the specified direction
13
+ */
14
+ getActiveStatusToxic(direction: ToxicStreamDirection): StatusToxic | null;
15
+ /**
16
+ * Create an array of streaming transformers for the specified stream direction
17
+ */
18
+ createStreamTransformers(direction: ToxicStreamDirection): {
19
+ transformers: Transform[];
20
+ appliedNames: string[];
21
+ };
22
+ }
@@ -0,0 +1,67 @@
1
+ import { LatencyToxic } from '../toxics/LatencyToxic.js';
2
+ import { BandwidthToxic } from '../toxics/BandwidthToxic.js';
3
+ import { CutToxic } from '../toxics/CutToxic.js';
4
+ import { CorruptToxic } from '../toxics/CorruptToxic.js';
5
+ import { StatusToxic } from '../toxics/StatusToxic.js';
6
+ export class ToxicPipeline {
7
+ rules = new Map();
8
+ addRule(rule) {
9
+ this.rules.set(rule.id, rule);
10
+ }
11
+ removeRule(id) {
12
+ return this.rules.delete(id);
13
+ }
14
+ getRules() {
15
+ return Array.from(this.rules.values());
16
+ }
17
+ getRule(id) {
18
+ return this.rules.get(id);
19
+ }
20
+ clearRules() {
21
+ this.rules.clear();
22
+ }
23
+ /**
24
+ * Check if any active StatusToxic exists for the specified direction
25
+ */
26
+ getActiveStatusToxic(direction) {
27
+ for (const rule of this.rules.values()) {
28
+ if (rule.enabled && rule.type === 'status' && rule.direction === direction) {
29
+ return new StatusToxic(rule.config);
30
+ }
31
+ }
32
+ return null;
33
+ }
34
+ /**
35
+ * Create an array of streaming transformers for the specified stream direction
36
+ */
37
+ createStreamTransformers(direction) {
38
+ const transformers = [];
39
+ const appliedNames = [];
40
+ for (const rule of this.rules.values()) {
41
+ if (!rule.enabled || rule.direction !== direction)
42
+ continue;
43
+ switch (rule.type) {
44
+ case 'latency':
45
+ transformers.push(new LatencyToxic(rule.config));
46
+ appliedNames.push(`Latency (${rule.name})`);
47
+ break;
48
+ case 'bandwidth':
49
+ transformers.push(new BandwidthToxic(rule.config));
50
+ appliedNames.push(`Bandwidth (${rule.name})`);
51
+ break;
52
+ case 'cut':
53
+ transformers.push(new CutToxic(rule.config));
54
+ appliedNames.push(`Cut (${rule.name})`);
55
+ break;
56
+ case 'corrupt':
57
+ transformers.push(new CorruptToxic(rule.config));
58
+ appliedNames.push(`Corrupt (${rule.name})`);
59
+ break;
60
+ case 'status':
61
+ // Status toxics are handled at the HTTP response header layer, not stream byte level
62
+ break;
63
+ }
64
+ }
65
+ return { transformers, appliedNames };
66
+ }
67
+ }
@@ -0,0 +1,13 @@
1
+ import { ResilienceScorecard } from '../types.js';
2
+ import { ToxicPipeline } from '../engine/ToxicPipeline.js';
3
+ export declare class ResilienceScorer {
4
+ private proxyUrl;
5
+ private pipeline;
6
+ constructor(proxyUrl: string, pipeline: ToxicPipeline);
7
+ runGauntlet(profile?: 'resilient' | 'fragile'): Promise<ResilienceScorecard>;
8
+ private testLatencySpike;
9
+ private testBandwidthConstraint;
10
+ private testConnectionCut;
11
+ private testPayloadCorruption;
12
+ private testServiceOutage;
13
+ }