phantomback 1.0.2 β 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 +194 -198
- package/bin/phantomback.js +3 -0
- package/package.json +5 -1
- package/src/cli/commands.js +43 -8
- package/src/features/chaos.js +468 -0
- package/src/index.js +1 -0
- package/src/schema/parser.js +16 -1
- package/src/server/createServer.js +19 -0
- package/src/utils/logger.js +67 -1
package/README.md
CHANGED
|
@@ -1,85 +1,76 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
+
<br />
|
|
4
|
+
|
|
3
5
|
# π» PhantomBack
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
**Instant Fake Backend Generator with Smart Responses**
|
|
6
8
|
|
|
7
9
|
[](https://www.npmjs.com/package/phantomback)
|
|
10
|
+
[](https://www.npmjs.com/package/phantomback)
|
|
8
11
|
[](LICENSE)
|
|
9
12
|
[](package.json)
|
|
10
|
-
[](https://phantombackxdocs.vercel.app)
|
|
11
|
-
|
|
12
|
-
**Stop waiting for the backend. Start building now.**
|
|
13
|
-
|
|
14
|
-
Drop in your API schema β get a fully functional REST server with realistic data,
|
|
15
|
-
JWT auth, pagination, filtering, sorting, search, and nested routes β in seconds.
|
|
16
13
|
|
|
17
|
-
|
|
14
|
+
Stop waiting for the backend. Drop in your API schema β get a fully functional REST server with
|
|
15
|
+
realistic data, JWT auth, pagination, filtering, sorting, search, and nested routes β in seconds.
|
|
18
16
|
|
|
19
|
-
[Documentation](https://phantombackxdocs.vercel.app) Β·
|
|
17
|
+
[Documentation](https://phantombackxdocs.vercel.app) Β·
|
|
18
|
+
[Getting Started](https://phantombackxdocs.vercel.app/docs/getting-started) Β·
|
|
19
|
+
[API Reference](https://phantombackxdocs.vercel.app/docs/api-reference) Β·
|
|
20
|
+
[Playground](https://phantombackxdocs.vercel.app/docs/playground) Β·
|
|
21
|
+
[GitHub](https://github.com/madhavxchaturvedi/npm-phantomback)
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
<br />
|
|
22
24
|
|
|
23
25
|
</div>
|
|
24
26
|
|
|
25
27
|
---
|
|
26
28
|
|
|
27
|
-
##
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
- π **Relations & Nested Routes** β `GET /users/:id/posts` auto-detected from foreign keys
|
|
37
|
-
- π **JWT Auth** β register, login, protected routes with Bearer tokens
|
|
38
|
-
- β±οΈ **Response Delay** β simulate slow networks with fixed or random latency
|
|
39
|
-
- β
**Validation** β required fields, type checks, unique constraints, email format
|
|
40
|
-
- π₯οΈ **CLI + Library** β use as a CLI tool or import in your code
|
|
41
|
-
- π§ **Smart Defaults** β sensible conventions, override only what you need
|
|
29
|
+
## Why PhantomBack?
|
|
30
|
+
|
|
31
|
+
| Pain point | PhantomBack fix |
|
|
32
|
+
|---|---|
|
|
33
|
+
| Backend not ready yet | Full REST API in one command |
|
|
34
|
+
| Static JSON mocks feel fake | Stateful CRUD with realistic Faker data |
|
|
35
|
+
| No pagination / filtering in mocks | Full query support out of the box |
|
|
36
|
+
| Auth testing is painful | JWT auth simulation built-in |
|
|
37
|
+
| Mock server setup takes time | One command or one line of code |
|
|
42
38
|
|
|
43
39
|
---
|
|
44
40
|
|
|
45
|
-
##
|
|
41
|
+
## Quick Start
|
|
46
42
|
|
|
47
|
-
|
|
48
|
-
# Global install (recommended for CLI)
|
|
49
|
-
npm install -g phantomback
|
|
43
|
+
### Install
|
|
50
44
|
|
|
51
|
-
|
|
52
|
-
npm install
|
|
45
|
+
```bash
|
|
46
|
+
npm install -g phantomback # CLI (global)
|
|
47
|
+
npm install --save-dev phantomback # Library (project)
|
|
53
48
|
```
|
|
54
49
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
## π Quick Start
|
|
58
|
-
|
|
59
|
-
### One command β full API:
|
|
50
|
+
### Zero-config β one command, full API
|
|
60
51
|
|
|
61
52
|
```bash
|
|
62
53
|
phantomback start --zero
|
|
63
54
|
```
|
|
64
55
|
|
|
65
|
-
|
|
66
|
-
- π€ 25 Users (protected with auth)
|
|
67
|
-
- π 50 Posts
|
|
68
|
-
- π¬ 100 Comments
|
|
69
|
-
- π¦ 30 Products
|
|
70
|
-
- β
40 Todos
|
|
56
|
+
You now have a REST API at **`http://localhost:3777`** with:
|
|
71
57
|
|
|
72
|
-
|
|
58
|
+
| Resource | Count | Auth |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| `/api/users` | 25 records | π Protected |
|
|
61
|
+
| `/api/posts` | 50 records | β |
|
|
62
|
+
| `/api/comments` | 100 records | β |
|
|
63
|
+
| `/api/products` | 30 records | β |
|
|
64
|
+
| `/api/todos` | 40 records | β |
|
|
73
65
|
|
|
74
|
-
|
|
75
|
-
# Generate a starter config
|
|
76
|
-
phantomback init
|
|
66
|
+
### Or bring your own schema
|
|
77
67
|
|
|
78
|
-
|
|
79
|
-
phantomback
|
|
68
|
+
```bash
|
|
69
|
+
phantomback init # generates phantom.config.js
|
|
70
|
+
phantomback start # reads config and starts server
|
|
80
71
|
```
|
|
81
72
|
|
|
82
|
-
### Or as a library
|
|
73
|
+
### Or use as a library
|
|
83
74
|
|
|
84
75
|
```js
|
|
85
76
|
import { createPhantom } from 'phantomback';
|
|
@@ -100,10 +91,10 @@ const server = await createPhantom({
|
|
|
100
91
|
|
|
101
92
|
// server.stop() β shut down
|
|
102
93
|
// server.reset() β re-seed all data
|
|
103
|
-
// server.getStore() β export current state
|
|
94
|
+
// server.getStore() β export current state
|
|
104
95
|
```
|
|
105
96
|
|
|
106
|
-
One
|
|
97
|
+
One-liner for zero-config:
|
|
107
98
|
|
|
108
99
|
```js
|
|
109
100
|
import { createPhantomZero } from 'phantomback';
|
|
@@ -112,18 +103,16 @@ await createPhantomZero(); // Full demo API on port 3777
|
|
|
112
103
|
|
|
113
104
|
---
|
|
114
105
|
|
|
115
|
-
##
|
|
106
|
+
## Configuration
|
|
116
107
|
|
|
117
|
-
Create
|
|
108
|
+
Create **`phantom.config.js`** in your project root:
|
|
118
109
|
|
|
119
110
|
```js
|
|
120
111
|
export default {
|
|
121
112
|
port: 3777,
|
|
122
113
|
prefix: '/api',
|
|
123
|
-
|
|
124
|
-
//
|
|
125
|
-
// latency: 500,
|
|
126
|
-
// latency: [200, 800], // random range
|
|
114
|
+
// latency: 500, // fixed delay (ms)
|
|
115
|
+
// latency: [200, 800], // random range
|
|
127
116
|
|
|
128
117
|
auth: {
|
|
129
118
|
secret: 'my-secret-key',
|
|
@@ -133,23 +122,23 @@ export default {
|
|
|
133
122
|
resources: {
|
|
134
123
|
users: {
|
|
135
124
|
fields: {
|
|
136
|
-
name:
|
|
137
|
-
email:
|
|
138
|
-
age:
|
|
139
|
-
role:
|
|
140
|
-
avatar:
|
|
125
|
+
name: { type: 'name', required: true },
|
|
126
|
+
email: { type: 'email', unique: true },
|
|
127
|
+
age: { type: 'number', min: 18, max: 65 },
|
|
128
|
+
role: { type: 'enum', values: ['admin', 'user', 'moderator'] },
|
|
129
|
+
avatar: { type: 'avatar' },
|
|
141
130
|
isActive: { type: 'boolean' },
|
|
142
131
|
},
|
|
143
|
-
seed: 25,
|
|
132
|
+
seed: 25,
|
|
144
133
|
auth: true, // protect with JWT
|
|
145
134
|
},
|
|
146
135
|
|
|
147
136
|
posts: {
|
|
148
137
|
fields: {
|
|
149
|
-
title:
|
|
150
|
-
body:
|
|
138
|
+
title: { type: 'title', required: true },
|
|
139
|
+
body: { type: 'paragraphs', count: 3 },
|
|
151
140
|
userId: { type: 'relation', resource: 'users' },
|
|
152
|
-
views:
|
|
141
|
+
views: { type: 'number', min: 0, max: 10000 },
|
|
153
142
|
},
|
|
154
143
|
seed: 50,
|
|
155
144
|
},
|
|
@@ -157,43 +146,29 @@ export default {
|
|
|
157
146
|
};
|
|
158
147
|
```
|
|
159
148
|
|
|
160
|
-
|
|
149
|
+
> **Full config reference β** [phantombackxdocs.vercel.app/docs/configuration](https://phantombackxdocs.vercel.app/docs/configuration)
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Supported Field Types
|
|
161
154
|
|
|
162
155
|
| Type | Generates | Options |
|
|
163
|
-
|
|
164
|
-
| `name`
|
|
165
|
-
| `
|
|
166
|
-
| `lastName` | Last name | β |
|
|
167
|
-
| `username` | Username | β |
|
|
168
|
-
| `email` | Email address | `unique: true` |
|
|
169
|
-
| `avatar` | Avatar URL | β |
|
|
156
|
+
|---|---|---|
|
|
157
|
+
| `name` `firstName` `lastName` `username` | Names | β |
|
|
158
|
+
| `email` | Email | `unique` |
|
|
170
159
|
| `phone` | Phone number | β |
|
|
171
|
-
| `
|
|
172
|
-
| `jobTitle` |
|
|
173
|
-
| `sentence` |
|
|
174
|
-
| `
|
|
175
|
-
| `
|
|
176
|
-
| `
|
|
177
|
-
| `
|
|
178
|
-
| `
|
|
179
|
-
| `
|
|
180
|
-
| `
|
|
181
|
-
| `price` | Price string | β |
|
|
182
|
-
| `rating` | 1.0 β 5.0 | β |
|
|
183
|
-
| `boolean` | true/false | β |
|
|
184
|
-
| `date` | ISO date | β |
|
|
185
|
-
| `pastDate` | Past date | β |
|
|
186
|
-
| `futureDate` | Future date | β |
|
|
187
|
-
| `url` | URL | β |
|
|
188
|
-
| `image` | Image URL | β |
|
|
189
|
-
| `color` | Color name | β |
|
|
190
|
-
| `address` | Street address | β |
|
|
191
|
-
| `city` | City name | β |
|
|
192
|
-
| `country` | Country name | β |
|
|
193
|
-
| `product` | Product name | β |
|
|
194
|
-
| `company` | Company name | β |
|
|
160
|
+
| `avatar` | Avatar URL | β |
|
|
161
|
+
| `bio` `jobTitle` | Profile text | β |
|
|
162
|
+
| `sentence` `paragraph` `paragraphs` | Text blocks | `count` |
|
|
163
|
+
| `title` `description` `slug` | Content | β |
|
|
164
|
+
| `number` `float` `price` `rating` | Numbers | `min` `max` `precision` |
|
|
165
|
+
| `boolean` | true / false | β |
|
|
166
|
+
| `date` `pastDate` `futureDate` | ISO dates | β |
|
|
167
|
+
| `url` `image` `color` | Misc | β |
|
|
168
|
+
| `address` `city` `country` | Location | β |
|
|
169
|
+
| `product` `company` | Business | β |
|
|
195
170
|
| `enum` | Random from list | `values: [...]` |
|
|
196
|
-
| `relation` | Foreign key | `resource: '
|
|
171
|
+
| `relation` | Foreign key | `resource: '...'` |
|
|
197
172
|
| `uuid` | UUID string | β |
|
|
198
173
|
|
|
199
174
|
### Field Options
|
|
@@ -201,60 +176,81 @@ export default {
|
|
|
201
176
|
```js
|
|
202
177
|
{
|
|
203
178
|
type: 'email',
|
|
204
|
-
required: true, //
|
|
205
|
-
unique: true, //
|
|
206
|
-
min: 0, //
|
|
207
|
-
max: 100, //
|
|
179
|
+
required: true, // must be present on create
|
|
180
|
+
unique: true, // no duplicates allowed
|
|
181
|
+
min: 0, // number minimum
|
|
182
|
+
max: 100, // number maximum
|
|
208
183
|
}
|
|
209
184
|
```
|
|
210
185
|
|
|
211
186
|
---
|
|
212
187
|
|
|
213
|
-
##
|
|
188
|
+
## Auto-Generated Routes
|
|
214
189
|
|
|
215
|
-
|
|
190
|
+
Every resource gets full CRUD automatically:
|
|
216
191
|
|
|
217
192
|
| Method | Endpoint | Description |
|
|
218
|
-
|
|
193
|
+
|---|---|---|
|
|
219
194
|
| `GET` | `/api/users` | List all (paginated) |
|
|
220
|
-
| `GET` | `/api/users/:id` | Get one
|
|
221
|
-
| `POST` | `/api/users` | Create
|
|
195
|
+
| `GET` | `/api/users/:id` | Get one |
|
|
196
|
+
| `POST` | `/api/users` | Create |
|
|
222
197
|
| `PUT` | `/api/users/:id` | Full update |
|
|
223
198
|
| `PATCH` | `/api/users/:id` | Partial update |
|
|
224
199
|
| `DELETE` | `/api/users/:id` | Delete |
|
|
225
200
|
|
|
226
|
-
### Nested Routes
|
|
201
|
+
### Nested Routes
|
|
227
202
|
|
|
228
|
-
If `posts` has `userId: { type: 'relation', resource: 'users' }`, you get:
|
|
203
|
+
If `posts` has `userId: { type: 'relation', resource: 'users' }`, you automatically get:
|
|
229
204
|
|
|
230
205
|
```
|
|
231
|
-
GET /api/users/:id/posts
|
|
206
|
+
GET /api/users/:id/posts β all posts by this user
|
|
232
207
|
```
|
|
233
208
|
|
|
234
209
|
### Special Routes
|
|
235
210
|
|
|
236
211
|
| Method | Endpoint | Description |
|
|
237
|
-
|
|
212
|
+
|---|---|---|
|
|
238
213
|
| `GET` | `/api` | List all endpoints |
|
|
239
|
-
| `GET` | `/api/_health` |
|
|
240
|
-
| `POST` | `/api/auth/register` | Register
|
|
241
|
-
| `POST` | `/api/auth/login` | Login
|
|
242
|
-
| `GET` | `/api/auth/me` | Current user (
|
|
214
|
+
| `GET` | `/api/_health` | Health check |
|
|
215
|
+
| `POST` | `/api/auth/register` | Register |
|
|
216
|
+
| `POST` | `/api/auth/login` | Login β JWT |
|
|
217
|
+
| `GET` | `/api/auth/me` | Current user (token required) |
|
|
243
218
|
|
|
244
219
|
---
|
|
245
220
|
|
|
246
|
-
##
|
|
247
|
-
|
|
248
|
-
### Pagination
|
|
221
|
+
## Query Parameters
|
|
249
222
|
|
|
250
223
|
```bash
|
|
224
|
+
# Pagination
|
|
251
225
|
GET /api/users?page=2&limit=10
|
|
252
226
|
GET /api/users?offset=20&limit=10
|
|
227
|
+
|
|
228
|
+
# Filtering
|
|
229
|
+
GET /api/users?role=admin # exact match
|
|
230
|
+
GET /api/users?age_gte=18 # β₯
|
|
231
|
+
GET /api/users?age_lte=30 # β€
|
|
232
|
+
GET /api/users?age_gt=18&age_lt=30 # range
|
|
233
|
+
GET /api/users?role_ne=admin # not equal
|
|
234
|
+
GET /api/users?name_like=john # contains
|
|
235
|
+
|
|
236
|
+
# Sorting
|
|
237
|
+
GET /api/users?sort=name # ascending
|
|
238
|
+
GET /api/users?sort=-name # descending
|
|
239
|
+
GET /api/users?sort=role,-age # multi-field
|
|
240
|
+
|
|
241
|
+
# Search
|
|
242
|
+
GET /api/users?q=john # full-text across all fields
|
|
243
|
+
|
|
244
|
+
# Field Selection
|
|
245
|
+
GET /api/users?fields=name,email,role
|
|
253
246
|
```
|
|
254
247
|
|
|
255
|
-
Response includes:
|
|
248
|
+
Response includes pagination metadata:
|
|
249
|
+
|
|
256
250
|
```json
|
|
257
251
|
{
|
|
252
|
+
"success": true,
|
|
253
|
+
"data": [ ... ],
|
|
258
254
|
"meta": {
|
|
259
255
|
"page": 2,
|
|
260
256
|
"limit": 10,
|
|
@@ -266,91 +262,92 @@ Response includes:
|
|
|
266
262
|
}
|
|
267
263
|
```
|
|
268
264
|
|
|
269
|
-
### Filtering
|
|
270
|
-
|
|
271
|
-
```bash
|
|
272
|
-
GET /api/users?role=admin # exact match
|
|
273
|
-
GET /api/users?age_gte=18 # greater than or equal
|
|
274
|
-
GET /api/users?age_lte=30 # less than or equal
|
|
275
|
-
GET /api/users?age_gt=18 # greater than
|
|
276
|
-
GET /api/users?age_lt=30 # less than
|
|
277
|
-
GET /api/users?role_ne=admin # not equal
|
|
278
|
-
GET /api/users?name_like=john # contains (case-insensitive)
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
### Sorting
|
|
282
|
-
|
|
283
|
-
```bash
|
|
284
|
-
GET /api/users?sort=name # ascending
|
|
285
|
-
GET /api/users?sort=-name # descending
|
|
286
|
-
GET /api/users?sort=role,-age # multi-field
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
### Search
|
|
290
|
-
|
|
291
|
-
```bash
|
|
292
|
-
GET /api/users?q=john # search across all fields
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
### Field Selection
|
|
296
|
-
|
|
297
|
-
```bash
|
|
298
|
-
GET /api/users?fields=name,email,role # only return these fields
|
|
299
|
-
```
|
|
300
|
-
|
|
301
265
|
---
|
|
302
266
|
|
|
303
|
-
##
|
|
267
|
+
## Authentication
|
|
304
268
|
|
|
305
|
-
1. **Register:**
|
|
306
269
|
```bash
|
|
270
|
+
# 1. Register
|
|
307
271
|
curl -X POST http://localhost:3777/api/auth/register \
|
|
308
272
|
-H "Content-Type: application/json" \
|
|
309
|
-
-d '{"email": "user@
|
|
310
|
-
```
|
|
273
|
+
-d '{"email": "user@test.com", "password": "secret123", "name": "John"}'
|
|
311
274
|
|
|
312
|
-
2.
|
|
313
|
-
```bash
|
|
275
|
+
# 2. Login
|
|
314
276
|
curl -X POST http://localhost:3777/api/auth/login \
|
|
315
277
|
-H "Content-Type: application/json" \
|
|
316
|
-
-d '{"email": "user@
|
|
317
|
-
```
|
|
278
|
+
-d '{"email": "user@test.com", "password": "secret123"}'
|
|
318
279
|
|
|
319
|
-
3.
|
|
320
|
-
```bash
|
|
280
|
+
# 3. Use the token
|
|
321
281
|
curl http://localhost:3777/api/users \
|
|
322
282
|
-H "Authorization: Bearer <your-token>"
|
|
323
283
|
```
|
|
324
284
|
|
|
325
285
|
---
|
|
326
286
|
|
|
327
|
-
##
|
|
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:
|
|
328
310
|
|
|
329
311
|
```bash
|
|
330
|
-
#
|
|
331
|
-
phantomback start
|
|
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
|
+
```
|
|
332
316
|
|
|
333
|
-
|
|
334
|
-
|
|
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 |
|
|
335
324
|
|
|
336
|
-
|
|
337
|
-
phantomback start --port 4000
|
|
325
|
+
> **Full guide β** [phantombackxdocs.vercel.app/docs/reality-mode](https://phantombackxdocs.vercel.app/docs/reality-mode)
|
|
338
326
|
|
|
339
|
-
|
|
340
|
-
phantomback start --config ./my-api.config.js
|
|
327
|
+
---
|
|
341
328
|
|
|
342
|
-
|
|
343
|
-
phantomback init
|
|
329
|
+
## CLI Reference
|
|
344
330
|
|
|
345
|
-
|
|
331
|
+
```bash
|
|
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
|
|
346
340
|
phantomback --help
|
|
347
341
|
```
|
|
348
342
|
|
|
343
|
+
> **Full CLI docs β** [phantombackxdocs.vercel.app/docs/cli](https://phantombackxdocs.vercel.app/docs/cli)
|
|
344
|
+
|
|
349
345
|
---
|
|
350
346
|
|
|
351
|
-
##
|
|
347
|
+
## Real-World Examples
|
|
352
348
|
|
|
353
|
-
|
|
349
|
+
<details>
|
|
350
|
+
<summary><strong>π₯ Hospital Management</strong></summary>
|
|
354
351
|
|
|
355
352
|
```js
|
|
356
353
|
export default {
|
|
@@ -376,8 +373,10 @@ export default {
|
|
|
376
373
|
},
|
|
377
374
|
};
|
|
378
375
|
```
|
|
376
|
+
</details>
|
|
379
377
|
|
|
380
|
-
|
|
378
|
+
<details>
|
|
379
|
+
<summary><strong>π E-Commerce</strong></summary>
|
|
381
380
|
|
|
382
381
|
```js
|
|
383
382
|
export default {
|
|
@@ -403,12 +402,13 @@ export default {
|
|
|
403
402
|
},
|
|
404
403
|
};
|
|
405
404
|
```
|
|
405
|
+
</details>
|
|
406
406
|
|
|
407
|
-
|
|
407
|
+
> **More examples β** [phantombackxdocs.vercel.app/docs/examples](https://phantombackxdocs.vercel.app/docs/examples)
|
|
408
408
|
|
|
409
|
-
|
|
409
|
+
---
|
|
410
410
|
|
|
411
|
-
|
|
411
|
+
## Response Format
|
|
412
412
|
|
|
413
413
|
**Success:**
|
|
414
414
|
```json
|
|
@@ -423,10 +423,7 @@ All responses follow a consistent format:
|
|
|
423
423
|
```json
|
|
424
424
|
{
|
|
425
425
|
"success": false,
|
|
426
|
-
"error": {
|
|
427
|
-
"status": 404,
|
|
428
|
-
"message": "users with id \"abc\" not found"
|
|
429
|
-
}
|
|
426
|
+
"error": { "status": 404, "message": "users with id \"abc\" not found" }
|
|
430
427
|
}
|
|
431
428
|
```
|
|
432
429
|
|
|
@@ -437,28 +434,27 @@ All responses follow a consistent format:
|
|
|
437
434
|
"error": {
|
|
438
435
|
"status": 400,
|
|
439
436
|
"message": "Validation failed",
|
|
440
|
-
"details": [
|
|
441
|
-
{ "field": "email", "message": "\"email\" is required" }
|
|
442
|
-
]
|
|
437
|
+
"details": [{ "field": "email", "message": "\"email\" is required" }]
|
|
443
438
|
}
|
|
444
439
|
}
|
|
445
440
|
```
|
|
446
441
|
|
|
447
442
|
---
|
|
448
443
|
|
|
449
|
-
##
|
|
444
|
+
## License
|
|
450
445
|
|
|
451
|
-
|
|
452
|
-
|---------|---------------------|
|
|
453
|
-
| Backend not ready yet | Start frontend dev instantly |
|
|
454
|
-
| Static JSON mocks are unrealistic | Stateful CRUD with realistic Faker data |
|
|
455
|
-
| No pagination/filtering in mocks | Full query support out of the box |
|
|
456
|
-
| Auth testing is painful | JWT auth simulation built-in |
|
|
457
|
-
| Setting up mock servers takes time | One command / one line of code |
|
|
458
|
-
| Different projects need different schemas | Define any resource with a config file |
|
|
446
|
+
MIT Β© [Madhav Chaturvedi](https://github.com/madhavxchaturvedi)
|
|
459
447
|
|
|
460
448
|
---
|
|
461
449
|
|
|
462
|
-
|
|
450
|
+
<div align="center">
|
|
463
451
|
|
|
464
|
-
|
|
452
|
+
[Documentation](https://phantombackxdocs.vercel.app) Β·
|
|
453
|
+
[npm](https://www.npmjs.com/package/phantomback) Β·
|
|
454
|
+
[GitHub](https://github.com/madhavxchaturvedi/npm-phantomback) Β·
|
|
455
|
+
[CLI Reference](https://phantombackxdocs.vercel.app/docs/cli) Β·
|
|
456
|
+
[Playground](https://phantombackxdocs.vercel.app/docs/playground)
|
|
457
|
+
|
|
458
|
+
Made with β€οΈ by [Madhav Chaturvedi](https://madhavxchaturvedi.vercel.app) Β· [LinkedIn](https://www.linkedin.com/in/madhavxchaturvedi/) Β· [Instagram](https://www.instagram.com/madhavxchaturvedi)
|
|
459
|
+
|
|
460
|
+
</div>
|
package/bin/phantomback.js
CHANGED
|
@@ -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": "
|
|
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",
|
package/src/cli/commands.js
CHANGED
|
@@ -69,14 +69,24 @@ export default {
|
|
|
69
69
|
},
|
|
70
70
|
},
|
|
71
71
|
|
|
72
|
-
// Chaos Engineering (Reality Mode)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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';
|
package/src/schema/parser.js
CHANGED
|
@@ -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
|
-
...
|
|
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
|
}
|
package/src/utils/logger.js
CHANGED
|
@@ -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
|
|
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
|
};
|