phantomback 1.0.3 → 2.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.
package/README.md CHANGED
@@ -284,14 +284,59 @@ curl http://localhost:3777/api/users \
284
284
 
285
285
  ---
286
286
 
287
+ ## Reality Mode — Chaos Engineering
288
+
289
+ Test your frontend's resilience by simulating real-world production failures.
290
+
291
+ Enable in `phantom.config.js`:
292
+
293
+ ```js
294
+ export default {
295
+ // ...
296
+ chaos: {
297
+ enabled: true,
298
+ latency: { min: 200, max: 5000 }, // latency jitter range (ms)
299
+ failureRate: 0.1, // 10% random 5xx responses
300
+ errorCodes: [500, 502, 503, 504],
301
+ connectionDropRate: 0.02, // 2% abrupt connection drops
302
+ corruptionRate: 0.02, // 2% malformed JSON
303
+ timeoutRate: 0.03, // 3% hanging responses
304
+ scenarios: ['latency', 'failure', 'drop', 'corruption', 'timeout'],
305
+ },
306
+ };
307
+ ```
308
+
309
+ Or enable instantly from the CLI:
310
+
311
+ ```bash
312
+ phantomback start --chaos # enable with defaults
313
+ phantomback start --chaos --chaos-failure 0.2 # 20% failure rate
314
+ phantomback start --chaos --chaos-latency 500,3000 # 500–3000 ms jitter
315
+ ```
316
+
317
+ | Scenario | Config Key | Description |
318
+ |---|---|---|
319
+ | `latency` | `latency` | Injects random delay on ~30% of requests |
320
+ | `failure` | `failureRate` | Returns a random 5xx error |
321
+ | `drop` | `connectionDropRate` | Abruptly closes the TCP connection |
322
+ | `corruption` | `corruptionRate` | Sends malformed / partial JSON |
323
+ | `timeout` | `timeoutRate` | Hangs the response for ~30 seconds |
324
+
325
+ > **Full guide →** [phantombackxdocs.vercel.app/docs/reality-mode](https://phantombackxdocs.vercel.app/docs/reality-mode)
326
+
327
+ ---
328
+
287
329
  ## CLI Reference
288
330
 
289
331
  ```bash
290
- phantomback start # start with phantom.config.js
291
- phantomback start --zero # zero-config demo mode
292
- phantomback start --port 4000 # custom port
293
- phantomback start --config ./my-api.config.js
294
- phantomback init # generate starter config
332
+ phantomback start # start with phantom.config.js
333
+ phantomback start --zero # zero-config demo mode
334
+ phantomback start --port 4000 # custom port
335
+ phantomback start --config ./my-api.config.js # custom config path
336
+ phantomback start --chaos # enable Reality Mode
337
+ phantomback start --chaos --chaos-failure 0.2 # 20% failure rate
338
+ phantomback start --chaos --chaos-latency 200,5000 # latency jitter range
339
+ phantomback init # generate starter config
295
340
  phantomback --help
296
341
  ```
297
342
 
@@ -23,6 +23,9 @@ program
23
23
  .option('--prefix <prefix>', 'API route prefix', '/api')
24
24
  .option('-c, --config <path>', 'Path to config file')
25
25
  .option('-z, --zero', 'Zero-config mode: generate a full demo backend')
26
+ .option('--chaos', 'Enable Reality Mode (chaos engineering)')
27
+ .option('--chaos-failure <rate>', 'Failure rate for Reality Mode (0-1)', parseFloat)
28
+ .option('--chaos-latency <range>', 'Latency range in ms (e.g. "200,5000")')
26
29
  .action(async (options) => {
27
30
  const { startCommand } = await import('../src/cli/commands.js');
28
31
  await startCommand(options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phantomback",
3
- "version": "1.0.3",
3
+ "version": "2.0.0",
4
4
  "description": "Instant fake backend generator with smart responses & chaos engineering. Drop in your API schema → get a fully functional, stateful REST server with realistic data, auth, pagination, filtering, and Reality Mode for chaos testing.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -27,6 +27,10 @@
27
27
  "development",
28
28
  "prototyping",
29
29
  "chaos-engineering",
30
+ "reality-mode",
31
+ "chaos-testing",
32
+ "fault-injection",
33
+ "resilience",
30
34
  "testing",
31
35
  "frontend",
32
36
  "faker",
@@ -69,14 +69,24 @@ export default {
69
69
  },
70
70
  },
71
71
 
72
- // Chaos Engineering (Reality Mode) — Phase 2
73
- // chaos: {
74
- // enabled: true,
75
- // jitter: { min: 100, max: 3000 },
76
- // failureRate: 0.1,
77
- // duplicateRate: 0.05,
78
- // connectionDropRate: 0.02,
79
- // },
72
+ // Chaos Engineering (Reality Mode)
73
+ chaos: {
74
+ enabled: false,
75
+ // Latency jitter range (ms) random delays injected on ~30% of requests
76
+ latency: { min: 200, max: 5000 },
77
+ // Probability (0-1) of returning a random 5xx error
78
+ failureRate: 0.1,
79
+ // HTTP error codes used for random failures
80
+ errorCodes: [500, 502, 503, 504],
81
+ // Probability of abruptly dropping the connection
82
+ connectionDropRate: 0.02,
83
+ // Probability of sending malformed/partial JSON
84
+ corruptionRate: 0.02,
85
+ // Probability of request hanging (30s timeout)
86
+ timeoutRate: 0.03,
87
+ // Which chaos scenarios to activate
88
+ scenarios: ['latency', 'failure', 'drop', 'corruption', 'timeout'],
89
+ },
80
90
  };
81
91
  `;
82
92
 
@@ -114,6 +124,31 @@ export async function startCommand(options) {
114
124
  if (options.port) config.port = parseInt(options.port, 10);
115
125
  if (options.prefix) config.prefix = options.prefix;
116
126
 
127
+ // Reality Mode (Chaos) CLI overrides
128
+ if (options.chaos) {
129
+ config.chaos = config.chaos || {};
130
+ config.chaos.enabled = true;
131
+ }
132
+ if (options.chaosFailure !== undefined) {
133
+ config.chaos = config.chaos || {};
134
+ config.chaos.enabled = true;
135
+ const rate = options.chaosFailure;
136
+ if (rate < 0 || rate > 1) {
137
+ logger.warn('--chaos-failure must be between 0 and 1. Clamping to valid range.');
138
+ }
139
+ config.chaos.failureRate = Math.max(0, Math.min(1, rate));
140
+ }
141
+ if (options.chaosLatency) {
142
+ config.chaos = config.chaos || {};
143
+ config.chaos.enabled = true;
144
+ const parts = options.chaosLatency.split(',').map(Number);
145
+ if (parts.length === 2 && !isNaN(parts[0]) && !isNaN(parts[1]) && parts[0] >= 0 && parts[1] >= parts[0]) {
146
+ config.chaos.latency = { min: parts[0], max: parts[1] };
147
+ } else {
148
+ logger.warn('Invalid --chaos-latency format. Expected "min,max" (e.g. "200,5000"). Using defaults.');
149
+ }
150
+ }
151
+
117
152
  // Check if using defaults (no config found and no resources)
118
153
  if (Object.keys(config.resources).length === 0) {
119
154
  logger.warn('No config found and no resources defined. Using zero-config defaults.');
@@ -0,0 +1,468 @@
1
+ /**
2
+ * Reality Mode — Chaos Engineering Middleware for PhantomBack
3
+ *
4
+ * Simulates real-world production instability during development:
5
+ * • Latency spikes (jitter)
6
+ * • Random HTTP failures (5xx errors)
7
+ * • Connection drops (socket destruction)
8
+ * • Response corruption (malformed JSON)
9
+ * • Request timeouts (hanging responses)
10
+ * • Out-of-order response delays
11
+ *
12
+ * Configuration:
13
+ * chaos: {
14
+ * enabled: true,
15
+ * latency: { min: 200, max: 5000 },
16
+ * failureRate: 0.1,
17
+ * errorCodes: [500, 502, 503, 504],
18
+ * connectionDropRate: 0.02,
19
+ * corruptionRate: 0.02,
20
+ * timeoutRate: 0.03,
21
+ * scenarios: ['latency', 'failure', 'drop', 'corruption', 'timeout'],
22
+ * }
23
+ */
24
+
25
+ import { logger } from '../utils/logger.js';
26
+
27
+ // ─── Default Chaos Configuration ─────────────────────────────────────────────
28
+
29
+ const DEFAULT_CHAOS_CONFIG = {
30
+ enabled: false,
31
+ latency: { min: 200, max: 5000 },
32
+ failureRate: 0.1,
33
+ errorCodes: [500, 502, 503, 504],
34
+ connectionDropRate: 0.02,
35
+ corruptionRate: 0.02,
36
+ timeoutRate: 0.03,
37
+ scenarios: ['latency', 'failure', 'drop', 'corruption', 'timeout'],
38
+ };
39
+
40
+ // ─── Chaos Engine ────────────────────────────────────────────────────────────
41
+
42
+ export class ChaosEngine {
43
+ constructor(config = {}) {
44
+ this.config = {
45
+ ...DEFAULT_CHAOS_CONFIG,
46
+ ...config,
47
+ latency: {
48
+ ...DEFAULT_CHAOS_CONFIG.latency,
49
+ ...(config.latency || {}),
50
+ },
51
+ errorCodes: config.errorCodes || DEFAULT_CHAOS_CONFIG.errorCodes,
52
+ scenarios: config.scenarios || DEFAULT_CHAOS_CONFIG.scenarios,
53
+ };
54
+ this.stats = {
55
+ totalRequests: 0,
56
+ chaosApplied: 0,
57
+ latencySpikes: 0,
58
+ failures: 0,
59
+ drops: 0,
60
+ corruptions: 0,
61
+ timeouts: 0,
62
+ startedAt: new Date().toISOString(),
63
+ };
64
+ this.paused = false;
65
+ }
66
+
67
+ /** Check if a specific scenario is enabled */
68
+ isScenarioEnabled(name) {
69
+ return this.config.enabled && !this.paused && this.config.scenarios.includes(name);
70
+ }
71
+
72
+ /** Roll the dice — returns true with the given probability (0-1) */
73
+ shouldTrigger(rate) {
74
+ return Math.random() < rate;
75
+ }
76
+
77
+ /** Generate a random latency value within the configured jitter range */
78
+ getJitter() {
79
+ const { min, max } = this.config.latency;
80
+ return Math.floor(Math.random() * (max - min + 1)) + min;
81
+ }
82
+
83
+ /** Pick a random error code from the configured list */
84
+ getRandomErrorCode() {
85
+ const codes = this.config.errorCodes;
86
+ return codes[Math.floor(Math.random() * codes.length)];
87
+ }
88
+
89
+ /** Enable chaos at runtime */
90
+ enable() {
91
+ this.config.enabled = true;
92
+ this.paused = false;
93
+ logger.chaos('Reality Mode ENABLED');
94
+ }
95
+
96
+ /** Disable chaos at runtime */
97
+ disable() {
98
+ this.config.enabled = false;
99
+ logger.chaos('Reality Mode DISABLED');
100
+ }
101
+
102
+ /** Pause chaos temporarily */
103
+ pause() {
104
+ this.paused = true;
105
+ logger.chaos('Reality Mode PAUSED');
106
+ }
107
+
108
+ /** Resume chaos after pause */
109
+ resume() {
110
+ this.paused = false;
111
+ logger.chaos('Reality Mode RESUMED');
112
+ }
113
+
114
+ /** Update chaos configuration at runtime */
115
+ configure(newConfig) {
116
+ this.config = { ...this.config, ...newConfig };
117
+ logger.chaos('Configuration updated');
118
+ }
119
+
120
+ /** Get current status and stats */
121
+ getStatus() {
122
+ return {
123
+ enabled: this.config.enabled,
124
+ paused: this.paused,
125
+ active: this.config.enabled && !this.paused,
126
+ config: this.config,
127
+ stats: { ...this.stats },
128
+ };
129
+ }
130
+
131
+ /** Reset stats counters */
132
+ resetStats() {
133
+ this.stats = {
134
+ totalRequests: 0,
135
+ chaosApplied: 0,
136
+ latencySpikes: 0,
137
+ failures: 0,
138
+ drops: 0,
139
+ corruptions: 0,
140
+ timeouts: 0,
141
+ startedAt: new Date().toISOString(),
142
+ };
143
+ }
144
+ }
145
+
146
+ // ─── Chaos Scenarios ─────────────────────────────────────────────────────────
147
+
148
+ const ERROR_MESSAGES = {
149
+ 500: 'Internal Server Error — [Reality Mode] Simulated server crash',
150
+ 502: 'Bad Gateway — [Reality Mode] Upstream service unavailable',
151
+ 503: 'Service Unavailable — [Reality Mode] Server overloaded',
152
+ 504: 'Gateway Timeout — [Reality Mode] Upstream request timed out',
153
+ };
154
+
155
+ /**
156
+ * Scenario: Latency Spike
157
+ * Adds a random delay to simulate network jitter or slow backends
158
+ */
159
+ function applyLatencySpike(engine, _req, _res) {
160
+ if (!engine.isScenarioEnabled('latency')) return null;
161
+ if (!engine.shouldTrigger(0.3)) return null; // 30% of requests get jitter
162
+
163
+ const delay = engine.getJitter();
164
+ engine.stats.latencySpikes++;
165
+
166
+ return new Promise((resolve) => {
167
+ logger.chaos(`Latency spike: +${delay}ms`);
168
+ setTimeout(resolve, delay);
169
+ });
170
+ }
171
+
172
+ /**
173
+ * Scenario: Random Failure
174
+ * Returns a random 5xx error response
175
+ */
176
+ function applyFailure(engine, _req, res) {
177
+ if (!engine.isScenarioEnabled('failure')) return false;
178
+ if (!engine.shouldTrigger(engine.config.failureRate)) return false;
179
+
180
+ const code = engine.getRandomErrorCode();
181
+ engine.stats.failures++;
182
+
183
+ logger.chaos(`Random failure: HTTP ${code}`);
184
+ res.status(code).json({
185
+ success: false,
186
+ error: {
187
+ status: code,
188
+ message: ERROR_MESSAGES[code] || `HTTP ${code} — [Reality Mode] Simulated failure`,
189
+ chaos: true,
190
+ },
191
+ });
192
+
193
+ return true;
194
+ }
195
+
196
+ /**
197
+ * Scenario: Connection Drop
198
+ * Destroys the socket mid-request to simulate network issues
199
+ */
200
+ function applyConnectionDrop(engine, req, _res) {
201
+ if (!engine.isScenarioEnabled('drop')) return false;
202
+ if (!engine.shouldTrigger(engine.config.connectionDropRate)) return false;
203
+
204
+ engine.stats.drops++;
205
+ logger.chaos(`Connection drop: ${req.method} ${req.originalUrl}`);
206
+
207
+ // Destroy the underlying socket
208
+ if (req.socket) {
209
+ req.socket.destroy();
210
+ }
211
+
212
+ return true;
213
+ }
214
+
215
+ /**
216
+ * Scenario: Response Corruption
217
+ * Sends back malformed/partial JSON to test error handling
218
+ */
219
+ function applyCorruption(engine, req, res) {
220
+ if (!engine.isScenarioEnabled('corruption')) return false;
221
+ if (!engine.shouldTrigger(engine.config.corruptionRate)) return false;
222
+
223
+ engine.stats.corruptions++;
224
+ logger.chaos(`Response corruption: ${req.method} ${req.originalUrl}`);
225
+
226
+ // Pick a random corruption type
227
+ const corruptions = [
228
+ // Truncated JSON
229
+ () => {
230
+ res.setHeader('Content-Type', 'application/json');
231
+ res.status(200).end('{"success":true,"data":[{"id":"abc","na');
232
+ },
233
+ // Invalid JSON
234
+ () => {
235
+ res.setHeader('Content-Type', 'application/json');
236
+ res.status(200).end('{success: true, data: undefined}');
237
+ },
238
+ // Empty body with 200
239
+ () => {
240
+ res.status(200).end('');
241
+ },
242
+ // Wrong content type
243
+ () => {
244
+ res.setHeader('Content-Type', 'text/html');
245
+ res.status(200).end('<html><body>Unexpected HTML response</body></html>');
246
+ },
247
+ // Partial response with wrong status
248
+ () => {
249
+ res.status(206).json({
250
+ success: true,
251
+ data: null,
252
+ error: { message: '[Reality Mode] Partial response — data truncated' },
253
+ chaos: true,
254
+ });
255
+ },
256
+ ];
257
+
258
+ const corrupt = corruptions[Math.floor(Math.random() * corruptions.length)];
259
+ corrupt();
260
+ return true;
261
+ }
262
+
263
+ /**
264
+ * Scenario: Request Timeout
265
+ * Holds the connection open without responding (simulates hung backend)
266
+ */
267
+ function applyTimeout(engine, req, _res) {
268
+ if (!engine.isScenarioEnabled('timeout')) return false;
269
+ if (!engine.shouldTrigger(engine.config.timeoutRate)) return false;
270
+
271
+ engine.stats.timeouts++;
272
+ logger.chaos(`Request timeout: ${req.method} ${req.originalUrl} (hanging for 30s)`);
273
+
274
+ // Return a promise that resolves after 30s (or until client gives up)
275
+ return new Promise((resolve) => {
276
+ const timer = setTimeout(resolve, 30000);
277
+
278
+ // Clean up if client disconnects
279
+ req.on('close', () => {
280
+ clearTimeout(timer);
281
+ resolve();
282
+ });
283
+ });
284
+ }
285
+
286
+ // ─── Main Chaos Middleware ───────────────────────────────────────────────────
287
+
288
+ /**
289
+ * Create the Reality Mode middleware.
290
+ * This is the main entry point — attach to Express before resource routes.
291
+ *
292
+ * @param {ChaosEngine} engine - Chaos engine instance
293
+ * @returns {Function} Express middleware
294
+ */
295
+ export function chaosMiddleware(engine) {
296
+ return async (req, res, next) => {
297
+ engine.stats.totalRequests++;
298
+
299
+ // Skip if chaos is disabled or paused
300
+ if (!engine.config.enabled || engine.paused) {
301
+ return next();
302
+ }
303
+
304
+ // Skip chaos control endpoints
305
+ if (req.path.includes('/_chaos')) {
306
+ return next();
307
+ }
308
+
309
+ // Skip health check
310
+ if (req.path.includes('/_health')) {
311
+ return next();
312
+ }
313
+
314
+ // Add chaos header so clients know Reality Mode is active
315
+ res.setHeader('X-PhantomBack-Chaos', 'active');
316
+
317
+ // ── Execute scenarios in priority order ──
318
+
319
+ // 1. Connection Drop (highest priority — immediate termination)
320
+ if (applyConnectionDrop(engine, req, res)) {
321
+ engine.stats.chaosApplied++;
322
+ return; // Socket destroyed, nothing more to do
323
+ }
324
+
325
+ // 2. Request Timeout (holds connection)
326
+ const timeoutResult = applyTimeout(engine, req, res);
327
+ if (timeoutResult instanceof Promise) {
328
+ engine.stats.chaosApplied++;
329
+ await timeoutResult;
330
+ // After timeout, destroy the socket (client likely already disconnected)
331
+ if (req.socket && !req.socket.destroyed) {
332
+ req.socket.destroy();
333
+ }
334
+ return;
335
+ }
336
+
337
+ // 3. Random Failure (returns error response)
338
+ if (applyFailure(engine, req, res)) {
339
+ engine.stats.chaosApplied++;
340
+ return;
341
+ }
342
+
343
+ // 4. Response Corruption (sends malformed data)
344
+ if (applyCorruption(engine, req, res)) {
345
+ engine.stats.chaosApplied++;
346
+ return;
347
+ }
348
+
349
+ // 5. Latency Spike (adds delay, then continues to real handler)
350
+ const latencyResult = applyLatencySpike(engine, req, res);
351
+ if (latencyResult instanceof Promise) {
352
+ engine.stats.chaosApplied++;
353
+ await latencyResult;
354
+ }
355
+
356
+ // If no chaos blocked the request, proceed normally
357
+ next();
358
+ };
359
+ }
360
+
361
+ // ─── Chaos Control Routes ────────────────────────────────────────────────────
362
+
363
+ /**
364
+ * Register chaos control endpoints on the Express app.
365
+ * These allow runtime control of Reality Mode.
366
+ *
367
+ * Routes:
368
+ * GET {prefix}/_chaos — Status & stats
369
+ * POST {prefix}/_chaos/enable — Enable chaos
370
+ * POST {prefix}/_chaos/disable — Disable chaos
371
+ * POST {prefix}/_chaos/pause — Pause chaos
372
+ * POST {prefix}/_chaos/resume — Resume chaos
373
+ * POST {prefix}/_chaos/configure — Update config
374
+ * POST {prefix}/_chaos/reset — Reset stats
375
+ */
376
+ export function createChaosRoutes(app, engine, config) {
377
+ const prefix = config.prefix || '/api';
378
+
379
+ // GET /_chaos — status dashboard
380
+ app.get(`${prefix}/_chaos`, (_req, res) => {
381
+ const status = engine.getStatus();
382
+ res.json({
383
+ success: true,
384
+ message: status.active
385
+ ? '🔥 Reality Mode is ACTIVE — chaos is being injected!'
386
+ : '😴 Reality Mode is inactive',
387
+ ...status,
388
+ });
389
+ });
390
+
391
+ // POST /_chaos/enable
392
+ app.post(`${prefix}/_chaos/enable`, (_req, res) => {
393
+ engine.enable();
394
+ res.json({
395
+ success: true,
396
+ message: '🔥 Reality Mode ENABLED — brace yourself!',
397
+ ...engine.getStatus(),
398
+ });
399
+ });
400
+
401
+ // POST /_chaos/disable
402
+ app.post(`${prefix}/_chaos/disable`, (_req, res) => {
403
+ engine.disable();
404
+ res.json({
405
+ success: true,
406
+ message: '😴 Reality Mode DISABLED — back to calm waters',
407
+ ...engine.getStatus(),
408
+ });
409
+ });
410
+
411
+ // POST /_chaos/pause
412
+ app.post(`${prefix}/_chaos/pause`, (_req, res) => {
413
+ engine.pause();
414
+ res.json({
415
+ success: true,
416
+ message: '⏸️ Reality Mode PAUSED',
417
+ ...engine.getStatus(),
418
+ });
419
+ });
420
+
421
+ // POST /_chaos/resume
422
+ app.post(`${prefix}/_chaos/resume`, (_req, res) => {
423
+ engine.resume();
424
+ res.json({
425
+ success: true,
426
+ message: '▶️ Reality Mode RESUMED',
427
+ ...engine.getStatus(),
428
+ });
429
+ });
430
+
431
+ // POST /_chaos/configure — update chaos config at runtime
432
+ app.post(`${prefix}/_chaos/configure`, (req, res) => {
433
+ const updates = req.body;
434
+ if (!updates || typeof updates !== 'object') {
435
+ return res.status(400).json({
436
+ success: false,
437
+ error: { status: 400, message: 'Request body must be a JSON object with chaos config' },
438
+ });
439
+ }
440
+
441
+ engine.configure(updates);
442
+ res.json({
443
+ success: true,
444
+ message: '⚙️ Chaos configuration updated',
445
+ ...engine.getStatus(),
446
+ });
447
+ });
448
+
449
+ // POST /_chaos/reset — reset stats
450
+ app.post(`${prefix}/_chaos/reset`, (_req, res) => {
451
+ engine.resetStats();
452
+ res.json({
453
+ success: true,
454
+ message: '📊 Chaos stats reset',
455
+ ...engine.getStatus(),
456
+ });
457
+ });
458
+
459
+ // Log registered chaos routes
460
+ logger.chaos('Control endpoints registered:');
461
+ logger.route('GET', `${prefix}/_chaos`);
462
+ logger.route('POST', `${prefix}/_chaos/enable`);
463
+ logger.route('POST', `${prefix}/_chaos/disable`);
464
+ logger.route('POST', `${prefix}/_chaos/pause`);
465
+ logger.route('POST', `${prefix}/_chaos/resume`);
466
+ logger.route('POST', `${prefix}/_chaos/configure`);
467
+ logger.route('POST', `${prefix}/_chaos/reset`);
468
+ }
package/src/index.js CHANGED
@@ -64,4 +64,5 @@ export { createServer } from './server/createServer.js';
64
64
  export { parseConfig } from './schema/parser.js';
65
65
  export { DEFAULT_RESOURCES } from './schema/defaults.js';
66
66
  export { DataStore } from './data/store.js';
67
+ export { ChaosEngine, chaosMiddleware, createChaosRoutes } from './features/chaos.js';
67
68
  export { logger } from './utils/logger.js';
@@ -16,6 +16,13 @@ export const DEFAULT_CONFIG = {
16
16
  },
17
17
  chaos: {
18
18
  enabled: false,
19
+ latency: { min: 200, max: 5000 },
20
+ failureRate: 0.1,
21
+ errorCodes: [500, 502, 503, 504],
22
+ connectionDropRate: 0.02,
23
+ corruptionRate: 0.02,
24
+ timeoutRate: 0.03,
25
+ scenarios: ['latency', 'failure', 'drop', 'corruption', 'timeout'],
19
26
  },
20
27
  resources: {},
21
28
  snapshot: false,
@@ -79,6 +86,8 @@ async function findConfigFile() {
79
86
  * Merge user config with defaults
80
87
  */
81
88
  function mergeConfig(userConfig) {
89
+ const userChaos = userConfig.chaos || {};
90
+
82
91
  return {
83
92
  ...DEFAULT_CONFIG,
84
93
  ...userConfig,
@@ -88,7 +97,13 @@ function mergeConfig(userConfig) {
88
97
  },
89
98
  chaos: {
90
99
  ...DEFAULT_CONFIG.chaos,
91
- ...(userConfig.chaos || {}),
100
+ ...userChaos,
101
+ latency: {
102
+ ...DEFAULT_CONFIG.chaos.latency,
103
+ ...(userChaos.latency || {}),
104
+ },
105
+ errorCodes: userChaos.errorCodes || DEFAULT_CONFIG.chaos.errorCodes,
106
+ scenarios: userChaos.scenarios || DEFAULT_CONFIG.chaos.scenarios,
92
107
  },
93
108
  };
94
109
  }
@@ -1,6 +1,7 @@
1
1
  import { createExpressApp, addErrorHandling } from './middleware.js';
2
2
  import { createRouter } from './router.js';
3
3
  import { createAuthRoutes } from '../features/auth.js';
4
+ import { ChaosEngine, chaosMiddleware, createChaosRoutes } from '../features/chaos.js';
4
5
  import { seedAll } from '../data/seeder.js';
5
6
  import { DataStore } from '../data/store.js';
6
7
  import { logger } from '../utils/logger.js';
@@ -15,6 +16,22 @@ export async function createServer(config) {
15
16
  const store = new DataStore();
16
17
  const app = createExpressApp(config);
17
18
 
19
+ // Initialize Reality Mode (Chaos Engine)
20
+ const chaosConfig = config.chaos || {};
21
+ const chaos = new ChaosEngine(chaosConfig);
22
+
23
+ // Register chaos control endpoints (before chaos middleware so they're never affected)
24
+ createChaosRoutes(app, chaos, config);
25
+
26
+ // Always mount chaos middleware so runtime toggling via /_chaos/enable works
27
+ // The middleware itself checks engine.config.enabled internally
28
+ app.use(chaosMiddleware(chaos));
29
+
30
+ // Print chaos banner if enabled at startup
31
+ if (chaosConfig.enabled) {
32
+ logger.chaosBanner(chaosConfig);
33
+ }
34
+
18
35
  // Seed data
19
36
  seedAll(config.resources, store);
20
37
 
@@ -43,6 +60,7 @@ export async function createServer(config) {
43
60
  app,
44
61
  server,
45
62
  store,
63
+ chaos,
46
64
  stop: () =>
47
65
  new Promise((resolve) => {
48
66
  server.close(resolve);
@@ -53,5 +71,6 @@ export async function createServer(config) {
53
71
  logger.info('Store has been reset and re-seeded');
54
72
  },
55
73
  getStore: () => store.toJSON(),
74
+ getChaos: () => chaos.getStatus(),
56
75
  };
57
76
  }
@@ -29,7 +29,7 @@ export const logger = {
29
29
  console.log(chalk.hex('#a78bfa').bold(' ╔═══════════════════════════════════════╗'));
30
30
  console.log(
31
31
  chalk.hex('#a78bfa').bold(' ║ ') +
32
- chalk.white.bold('PhantomBack v1.0.0') +
32
+ chalk.white.bold('PhantomBack v2.0.0') +
33
33
  chalk.hex('#a78bfa').bold(' ║'),
34
34
  );
35
35
  console.log(
@@ -55,4 +55,70 @@ export const logger = {
55
55
  }
56
56
  console.log('');
57
57
  },
58
+
59
+ // ── Reality Mode (Chaos) Logging ──
60
+ chaos: (...args) =>
61
+ console.log(PREFIX, chalk.hex('#ff6b6b').bold('⚡CHAOS'), ...args),
62
+
63
+ chaosBanner: (config) => {
64
+ console.log('');
65
+ console.log(chalk.hex('#ff6b6b').bold(' ┌───────────────────────────────────────┐'));
66
+ console.log(
67
+ chalk.hex('#ff6b6b').bold(' │ ') +
68
+ chalk.white.bold('⚡ Reality Mode ACTIVE ⚡') +
69
+ chalk.hex('#ff6b6b').bold(' │'),
70
+ );
71
+ console.log(
72
+ chalk.hex('#ff6b6b').bold(' │ ') +
73
+ chalk.dim('Chaos is being injected into your API') +
74
+ chalk.hex('#ff6b6b').bold(' │'),
75
+ );
76
+ console.log(chalk.hex('#ff6b6b').bold(' └───────────────────────────────────────┘'));
77
+ console.log('');
78
+ if (config) {
79
+ const scenarios = config.scenarios || [];
80
+ console.log(PREFIX, chalk.hex('#ff6b6b').bold('Active Scenarios:'));
81
+ if (scenarios.includes('latency')) {
82
+ console.log(
83
+ PREFIX,
84
+ chalk.dim(' ├─'),
85
+ chalk.yellow('⏱ Latency Spikes'),
86
+ chalk.dim(`(${config.latency?.min || 200}–${config.latency?.max || 5000}ms)`),
87
+ );
88
+ }
89
+ if (scenarios.includes('failure')) {
90
+ console.log(
91
+ PREFIX,
92
+ chalk.dim(' ├─'),
93
+ chalk.red('💥 Random Failures'),
94
+ chalk.dim(`(${(config.failureRate || 0.1) * 100}% rate)`),
95
+ );
96
+ }
97
+ if (scenarios.includes('drop')) {
98
+ console.log(
99
+ PREFIX,
100
+ chalk.dim(' ├─'),
101
+ chalk.magenta('🔌 Connection Drops'),
102
+ chalk.dim(`(${(config.connectionDropRate || 0.02) * 100}% rate)`),
103
+ );
104
+ }
105
+ if (scenarios.includes('corruption')) {
106
+ console.log(
107
+ PREFIX,
108
+ chalk.dim(' ├─'),
109
+ chalk.hex('#ff9f43')('🧩 Response Corruption'),
110
+ chalk.dim(`(${(config.corruptionRate || 0.02) * 100}% rate)`),
111
+ );
112
+ }
113
+ if (scenarios.includes('timeout')) {
114
+ console.log(
115
+ PREFIX,
116
+ chalk.dim(' ├─'),
117
+ chalk.hex('#ee5a24')('⏳ Request Timeouts'),
118
+ chalk.dim(`(${(config.timeoutRate || 0.03) * 100}% rate)`),
119
+ );
120
+ }
121
+ console.log('');
122
+ }
123
+ },
58
124
  };