@umeshindu222/apisnap 1.1.3 → 1.1.4
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 +211 -67
- package/dist/core/runner.js +335 -110
- package/dist/core/runner.js.map +1 -1
- package/dist/middleware/index.d.ts +8 -2
- package/dist/middleware/index.d.ts.map +1 -1
- package/dist/middleware/index.js +44 -22
- package/dist/middleware/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,28 +7,31 @@
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## Why APISnap?
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
**The Solution:** APISnap plugs directly into your Express app, **automatically discovers every route you've registered**, and then health-checks all of them in seconds with zero configuration.
|
|
12
|
+
Every time you change your Express backend, manually testing 20+ endpoints in Postman is slow and error-prone. APISnap **auto-discovers every route** and health-checks all of them in seconds — with zero config.
|
|
15
13
|
|
|
16
14
|
---
|
|
17
15
|
|
|
18
|
-
##
|
|
16
|
+
## Features
|
|
19
17
|
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
18
|
+
- 🔍 **Auto Route Discovery** — Scans your full Express router stack including sub-routers
|
|
19
|
+
- 🔐 **Full Auth Support** — JWT, API Keys, Cookies, multiple headers simultaneously
|
|
20
|
+
- 🔁 **Retry Logic** — Auto-retry failed requests with exponential backoff
|
|
21
|
+
- ⚡ **Slow Route Detection** — Flags endpoints exceeding your threshold
|
|
22
|
+
- 📊 **HTML Reports** — Beautiful visual reports for sharing/archiving
|
|
23
|
+
- 💾 **JSON Export** — Structured output for CI/CD pipelines
|
|
24
|
+
- ⚙️ **Config File** — Persist options in `.apisnaprc.json`
|
|
25
|
+
- 🎯 **Method Filter** — Test only GET, POST, etc.
|
|
26
|
+
- 🧠 **Smart Params** — Auto-replaces `:id`, `:slug`, `:uuid` with safe defaults
|
|
27
|
+
- 🚨 **Auth Hints** — Tells you exactly how to fix 401/403 errors
|
|
28
|
+
- 🏗️ **CI/CD Ready** — Exit code 1 on failures for pipeline integration
|
|
26
29
|
|
|
27
30
|
---
|
|
28
31
|
|
|
29
|
-
##
|
|
32
|
+
## Quick Start
|
|
30
33
|
|
|
31
|
-
### Step 1
|
|
34
|
+
### Step 1 — Install & add middleware
|
|
32
35
|
|
|
33
36
|
```bash
|
|
34
37
|
npm install @umeshindu222/apisnap
|
|
@@ -40,27 +43,118 @@ const apisnap = require('@umeshindu222/apisnap');
|
|
|
40
43
|
|
|
41
44
|
const app = express();
|
|
42
45
|
|
|
43
|
-
// Your routes here
|
|
46
|
+
// ✅ Your routes go here
|
|
44
47
|
app.get('/users', (req, res) => res.json({ users: [] }));
|
|
45
48
|
app.post('/users', (req, res) => res.json({ message: 'Created' }));
|
|
46
49
|
|
|
47
|
-
//
|
|
50
|
+
// ✅ APISnap goes AFTER your routes (so it can discover them)
|
|
51
|
+
// ✅ APISnap goes BEFORE global auth middleware (to allow discovery)
|
|
48
52
|
apisnap.init(app);
|
|
49
53
|
|
|
54
|
+
// ⚠️ If you use global auth middleware, place it AFTER apisnap.init():
|
|
55
|
+
// app.use(authMiddleware); ← AFTER init, not before
|
|
56
|
+
|
|
50
57
|
app.listen(3000);
|
|
51
58
|
```
|
|
52
59
|
|
|
53
|
-
### Step 2
|
|
60
|
+
### Step 2 — Run
|
|
54
61
|
|
|
55
62
|
```bash
|
|
56
63
|
npx @umeshindu222/apisnap --port 3000
|
|
57
64
|
```
|
|
58
65
|
|
|
59
|
-
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 🔐 Fixing 401 / 403 Errors
|
|
69
|
+
|
|
70
|
+
This is the most common issue. APISnap sends real HTTP requests, so **protected routes require credentials** just like any client would.
|
|
71
|
+
|
|
72
|
+
### JWT / Bearer Token
|
|
73
|
+
```bash
|
|
74
|
+
npx @umeshindu222/apisnap -H "Authorization: Bearer eyJhbGci..."
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### API Key header
|
|
78
|
+
```bash
|
|
79
|
+
npx @umeshindu222/apisnap -H "x-api-key: my-secret-key"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Cookie / Session auth
|
|
83
|
+
```bash
|
|
84
|
+
npx @umeshindu222/apisnap --cookie "sessionId=abc123; connect.sid=xyz"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Multiple headers at once (`-H` can be repeated)
|
|
88
|
+
```bash
|
|
89
|
+
npx @umeshindu222/apisnap \
|
|
90
|
+
-H "Authorization: Bearer TOKEN" \
|
|
91
|
+
-H "x-tenant-id: acme" \
|
|
92
|
+
-H "x-api-version: 2"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Skip specific protected routes
|
|
96
|
+
```javascript
|
|
97
|
+
// In your server — skip routes you don't want tested:
|
|
98
|
+
apisnap.init(app, {
|
|
99
|
+
skip: ['/admin', '/internal', '/webhooks']
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Use a config file (recommended for teams)
|
|
104
|
+
|
|
105
|
+
Create `.apisnaprc.json` in your project root:
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"port": "3000",
|
|
110
|
+
"slow": "300",
|
|
111
|
+
"headers": [
|
|
112
|
+
"Authorization: Bearer YOUR_DEV_TOKEN",
|
|
113
|
+
"x-api-key: YOUR_KEY"
|
|
114
|
+
],
|
|
115
|
+
"cookie": "sessionId=dev-session-abc",
|
|
116
|
+
"params": {
|
|
117
|
+
"id": "42",
|
|
118
|
+
"slug": "hello-world",
|
|
119
|
+
"uuid": "550e8400-e29b-41d4-a716-446655440000"
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Then just run:
|
|
125
|
+
```bash
|
|
126
|
+
npx @umeshindu222/apisnap
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Middleware Placement (Important!)
|
|
132
|
+
|
|
133
|
+
The order of middleware in Express matters:
|
|
134
|
+
|
|
135
|
+
```javascript
|
|
136
|
+
// ✅ CORRECT — apisnap can bypass auth because it registers first
|
|
137
|
+
app.use(express.json());
|
|
138
|
+
apisnap.init(app); // ← BEFORE auth middleware
|
|
139
|
+
app.use(authMiddleware); // ← AFTER apisnap.init
|
|
140
|
+
|
|
141
|
+
// ❌ WRONG — auth blocks the discovery endpoint
|
|
142
|
+
app.use(authMiddleware); // ← BEFORE apisnap
|
|
143
|
+
apisnap.init(app); // discovery endpoint gets blocked!
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
If you **must** put auth before apisnap, manually whitelist the discovery path:
|
|
147
|
+
|
|
148
|
+
```javascript
|
|
149
|
+
app.use((req, res, next) => {
|
|
150
|
+
if (req.path === '/__apisnap_discovery') return next(); // bypass
|
|
151
|
+
return authMiddleware(req, res, next);
|
|
152
|
+
});
|
|
153
|
+
```
|
|
60
154
|
|
|
61
155
|
---
|
|
62
156
|
|
|
63
|
-
##
|
|
157
|
+
## CLI Reference
|
|
64
158
|
|
|
65
159
|
```bash
|
|
66
160
|
npx @umeshindu222/apisnap [options]
|
|
@@ -68,114 +162,164 @@ npx @umeshindu222/apisnap [options]
|
|
|
68
162
|
|
|
69
163
|
| Option | Description | Default |
|
|
70
164
|
|--------|-------------|---------|
|
|
71
|
-
| `-p, --port <
|
|
72
|
-
| `-H, --header <
|
|
73
|
-
| `-
|
|
74
|
-
| `-
|
|
75
|
-
| `-
|
|
76
|
-
| `-
|
|
165
|
+
| `-p, --port <n>` | Port your server runs on | `3000` |
|
|
166
|
+
| `-H, --header <str>` | Add auth header (repeatable) | — |
|
|
167
|
+
| `-c, --cookie <str>` | Cookie string for session auth | — |
|
|
168
|
+
| `-s, --slow <n>` | Slow threshold in ms | `200` |
|
|
169
|
+
| `-t, --timeout <n>` | Request timeout in ms | `5000` |
|
|
170
|
+
| `-r, --retry <n>` | Retry failed requests N times | `0` |
|
|
171
|
+
| `-e, --export <file>` | Export JSON report | — |
|
|
172
|
+
| `--html <file>` | Export HTML report | — |
|
|
173
|
+
| `--only <methods>` | Filter methods (e.g. `GET,POST`) | — |
|
|
174
|
+
| `--base-url <url>` | Override base URL (for staging) | `localhost` |
|
|
175
|
+
| `--params <json>` | Path param overrides as JSON | — |
|
|
176
|
+
| `--fail-on-slow` | Exit code 1 if slow routes found | `false` |
|
|
77
177
|
|
|
78
178
|
---
|
|
79
179
|
|
|
80
|
-
##
|
|
180
|
+
## Examples
|
|
81
181
|
|
|
82
|
-
### Basic
|
|
182
|
+
### Basic
|
|
83
183
|
```bash
|
|
84
184
|
npx @umeshindu222/apisnap --port 3000
|
|
85
185
|
```
|
|
86
186
|
|
|
87
|
-
### With
|
|
187
|
+
### With JWT auth
|
|
88
188
|
```bash
|
|
89
|
-
npx @umeshindu222/apisnap
|
|
189
|
+
npx @umeshindu222/apisnap -p 3000 -H "Authorization: Bearer eyJhbGci..."
|
|
90
190
|
```
|
|
91
191
|
|
|
92
|
-
### Custom
|
|
192
|
+
### Custom path params (for routes like `/users/:id/posts/:postId`)
|
|
93
193
|
```bash
|
|
94
|
-
npx @umeshindu222/apisnap --
|
|
194
|
+
npx @umeshindu222/apisnap --params '{"id":"42","postId":"7"}'
|
|
95
195
|
```
|
|
96
196
|
|
|
97
|
-
###
|
|
197
|
+
### Test only GET routes
|
|
98
198
|
```bash
|
|
99
|
-
npx @umeshindu222/apisnap --
|
|
100
|
-
# Creates: my-report.json
|
|
199
|
+
npx @umeshindu222/apisnap --only GET
|
|
101
200
|
```
|
|
102
201
|
|
|
103
|
-
###
|
|
202
|
+
### Test staging server
|
|
104
203
|
```bash
|
|
105
|
-
npx @umeshindu222/apisnap --
|
|
204
|
+
npx @umeshindu222/apisnap --base-url https://staging.myapp.com -H "Authorization: Bearer TOKEN"
|
|
106
205
|
```
|
|
107
206
|
|
|
108
|
-
|
|
207
|
+
### Generate HTML report
|
|
208
|
+
```bash
|
|
209
|
+
npx @umeshindu222/apisnap --html report
|
|
210
|
+
|
|
211
|
+
# Mac
|
|
212
|
+
# open report.html
|
|
109
213
|
|
|
110
|
-
|
|
214
|
+
# Windows
|
|
215
|
+
# start report.html
|
|
111
216
|
|
|
217
|
+
# Linux
|
|
218
|
+
# xdg-open report.html
|
|
112
219
|
```
|
|
113
|
-
📸 APISnap v1.0.0
|
|
114
|
-
Slow threshold: 200ms
|
|
115
220
|
|
|
116
|
-
|
|
221
|
+
### CI/CD — fail pipeline on any broken endpoint
|
|
222
|
+
```bash
|
|
223
|
+
npx @umeshindu222/apisnap --export ci-report && echo "All healthy!"
|
|
224
|
+
# Exit code 1 if any endpoint fails
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Full power
|
|
228
|
+
```bash
|
|
229
|
+
npx @umeshindu222/apisnap \
|
|
230
|
+
-p 5000 \
|
|
231
|
+
-H "Authorization: Bearer TOKEN" \
|
|
232
|
+
-H "x-api-key: SECRET" \
|
|
233
|
+
--cookie "sessionId=abc" \
|
|
234
|
+
--slow 300 \
|
|
235
|
+
--retry 2 \
|
|
236
|
+
--html report \
|
|
237
|
+
--export report \
|
|
238
|
+
--fail-on-slow
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
---
|
|
117
242
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
243
|
+
## Sample Output
|
|
244
|
+
|
|
245
|
+
```
|
|
246
|
+
📸 APISnap v2.0.0
|
|
247
|
+
Target: http://localhost:3000
|
|
248
|
+
Slow: >200ms
|
|
249
|
+
Timeout: 5000ms
|
|
250
|
+
Headers: {"Authorization":"Bearer ••••••"}
|
|
251
|
+
|
|
252
|
+
✔ Connected! Found 6 endpoints to test.
|
|
253
|
+
|
|
254
|
+
✔ GET /health [200] 3ms
|
|
255
|
+
✔ GET /users [200] 12ms
|
|
256
|
+
✔ POST /users [200] 8ms
|
|
257
|
+
✔ GET /users/1 [200] 15ms
|
|
258
|
+
⚠️ GET /reports [200] 543ms ← slow!
|
|
259
|
+
✖ DELETE /users/1 [401]
|
|
260
|
+
💡 Hint: 401 Unauthorized — try adding -H "Authorization: Bearer YOUR_TOKEN"
|
|
124
261
|
|
|
125
262
|
📊 Summary:
|
|
126
263
|
✅ Passed: 5
|
|
127
264
|
❌ Failed: 1
|
|
128
265
|
⚠️ Slow: 1 (>200ms)
|
|
266
|
+
⏱ Avg: 100ms
|
|
267
|
+
🕐 Total: 600ms
|
|
129
268
|
|
|
130
269
|
⚠️ Some endpoints are unhealthy!
|
|
131
270
|
```
|
|
132
271
|
|
|
133
272
|
---
|
|
134
273
|
|
|
135
|
-
##
|
|
274
|
+
## HTML Report
|
|
136
275
|
|
|
137
|
-
|
|
276
|
+
`--html report` generates a beautiful standalone HTML file:
|
|
277
|
+
|
|
278
|
+
- Pass rate progress bar
|
|
279
|
+
- Color-coded result table
|
|
280
|
+
- Per-endpoint timing, status, retry count
|
|
281
|
+
- No external dependencies — works offline
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## JSON Report Format
|
|
138
286
|
|
|
139
287
|
```json
|
|
140
288
|
{
|
|
141
289
|
"tool": "APISnap",
|
|
142
|
-
"
|
|
290
|
+
"version": "2.0.0",
|
|
291
|
+
"generatedAt": "2026-03-08T10:00:00.000Z",
|
|
143
292
|
"config": { "port": "3000", "slowThreshold": 200 },
|
|
144
|
-
"summary": {
|
|
293
|
+
"summary": {
|
|
294
|
+
"total": 6, "passed": 5, "failed": 1,
|
|
295
|
+
"slow": 1, "avgDuration": 100, "totalDuration": 600
|
|
296
|
+
},
|
|
145
297
|
"results": [
|
|
146
|
-
{ "method": "GET", "path": "/
|
|
147
|
-
{ "method": "GET", "path": "/users", "status": 200, "duration": 12, "success": true, "slow": false },
|
|
148
|
-
{ "method": "GET", "path": "/reports", "status": 200, "duration": 543,"success": true, "slow": true }
|
|
298
|
+
{ "method": "GET", "path": "/users", "status": 200, "duration": 12, "success": true, "slow": false, "retries": 0 }
|
|
149
299
|
]
|
|
150
300
|
}
|
|
151
301
|
```
|
|
152
302
|
|
|
153
|
-
> **CI/CD tip:**
|
|
303
|
+
> **CI/CD tip:** Check `summary.failed > 0` to fail your build.
|
|
154
304
|
|
|
155
305
|
---
|
|
156
306
|
|
|
157
|
-
##
|
|
307
|
+
## How It Works
|
|
158
308
|
|
|
159
|
-
|
|
309
|
+
1. **Middleware** — `apisnap.init(app)` registers `/__apisnap_discovery` and patches `app.use` so global auth middleware skips the discovery path automatically.
|
|
160
310
|
|
|
161
|
-
|
|
311
|
+
2. **CLI** — Calls the discovery endpoint, gets the full route map, then pings each route with your headers/cookies. Smart defaults replace `:id` → `1`, `:uuid` → a valid UUID, `:slug` → `"example"`, etc.
|
|
162
312
|
|
|
163
|
-
|
|
313
|
+
3. **Reports** — Results are collected and can be exported as JSON (for CI/CD) or a self-contained HTML file (for humans).
|
|
164
314
|
|
|
165
315
|
---
|
|
166
316
|
|
|
167
|
-
##
|
|
168
|
-
|
|
169
|
-
Contributions, issues and feature requests are welcome!
|
|
317
|
+
## Contributing
|
|
170
318
|
|
|
171
|
-
1. Fork
|
|
172
|
-
2. Create your feature branch: `git checkout -b feat/amazing-feature`
|
|
173
|
-
3. Commit your changes: `git commit -m 'feat: add amazing feature'`
|
|
174
|
-
4. Push to the branch: `git push origin feat/amazing-feature`
|
|
175
|
-
5. Open a Pull Request
|
|
319
|
+
1. Fork → `git checkout -b feat/amazing` → commit → push → PR
|
|
176
320
|
|
|
177
321
|
---
|
|
178
322
|
|
|
179
|
-
##
|
|
323
|
+
## License
|
|
180
324
|
|
|
181
325
|
MIT © [Umesh Induranga](https://github.com/Umeshinduranga)
|
package/dist/core/runner.js
CHANGED
|
@@ -4,157 +4,382 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
7
8
|
const axios_1 = __importDefault(require("axios"));
|
|
8
9
|
const chalk_1 = __importDefault(require("chalk"));
|
|
9
10
|
const ora_1 = __importDefault(require("ora"));
|
|
10
11
|
const commander_1 = require("commander");
|
|
11
12
|
const program = new commander_1.Command();
|
|
12
13
|
const { version } = require('../../package.json');
|
|
14
|
+
// ─── Config File Loader ───────────────────────────────────────────────────────
|
|
15
|
+
function loadConfigFile() {
|
|
16
|
+
const configNames = ['.apisnaprc', '.apisnaprc.json', 'apisnap.config.json'];
|
|
17
|
+
for (const name of configNames) {
|
|
18
|
+
const filePath = path_1.default.resolve(process.cwd(), name);
|
|
19
|
+
if (fs_1.default.existsSync(filePath)) {
|
|
20
|
+
try {
|
|
21
|
+
// Strip BOM if present (fixes Windows PowerShell encoding issue)
|
|
22
|
+
let raw = fs_1.default.readFileSync(filePath, 'utf-8');
|
|
23
|
+
raw = raw.replace(/^\uFEFF/, '');
|
|
24
|
+
raw = raw.trim();
|
|
25
|
+
console.log(chalk_1.default.gray(` Config: ${name}\n`));
|
|
26
|
+
return JSON.parse(raw);
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
console.warn(chalk_1.default.yellow(`⚠️ Could not parse config file: ${name}`));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
// ─── Header Parser ─────────────────────────────────────────────────────────
|
|
36
|
+
function parseHeaders(headerArgs) {
|
|
37
|
+
const headers = {};
|
|
38
|
+
for (const h of headerArgs) {
|
|
39
|
+
const colonIdx = h.indexOf(':');
|
|
40
|
+
if (colonIdx > 0) {
|
|
41
|
+
const key = h.slice(0, colonIdx).trim();
|
|
42
|
+
const value = h.slice(colonIdx + 1).trim();
|
|
43
|
+
headers[key] = value;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
console.warn(chalk_1.default.yellow(`⚠️ Skipping malformed header: "${h}" (expected "Key: Value")`));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return headers;
|
|
50
|
+
}
|
|
51
|
+
// ─── Smart Path Param Replacement ────────────────────────────────────────────
|
|
52
|
+
function replacePath(rawPath, paramMap = {}) {
|
|
53
|
+
return rawPath.replace(/:([a-zA-Z0-9_]+)/g, (_, param) => {
|
|
54
|
+
if (paramMap[param])
|
|
55
|
+
return paramMap[param];
|
|
56
|
+
// Smart defaults based on param name
|
|
57
|
+
if (/id$/i.test(param))
|
|
58
|
+
return '1';
|
|
59
|
+
if (/slug$/i.test(param))
|
|
60
|
+
return 'example';
|
|
61
|
+
if (/uuid$/i.test(param))
|
|
62
|
+
return '00000000-0000-0000-0000-000000000001';
|
|
63
|
+
if (/name$/i.test(param))
|
|
64
|
+
return 'test';
|
|
65
|
+
if (/token$/i.test(param))
|
|
66
|
+
return 'abc123';
|
|
67
|
+
if (/page$/i.test(param))
|
|
68
|
+
return '1';
|
|
69
|
+
if (/limit$/i.test(param))
|
|
70
|
+
return '10';
|
|
71
|
+
return '1'; // fallback
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
// ─── HTML Report Generator ────────────────────────────────────────────────────
|
|
75
|
+
function generateHTMLReport(data) {
|
|
76
|
+
const passRate = data.summary.total > 0
|
|
77
|
+
? Math.round((data.summary.passed / data.summary.total) * 100)
|
|
78
|
+
: 0;
|
|
79
|
+
const rowColor = (r) => {
|
|
80
|
+
if (!r.success)
|
|
81
|
+
return '#fee2e2';
|
|
82
|
+
if (r.slow)
|
|
83
|
+
return '#fef9c3';
|
|
84
|
+
return '#f0fdf4';
|
|
85
|
+
};
|
|
86
|
+
const rows = data.results.map(r => `
|
|
87
|
+
<tr style="background:${rowColor(r)}">
|
|
88
|
+
<td><span class="badge badge-${r.method.toLowerCase()}">${r.method}</span></td>
|
|
89
|
+
<td><code>${r.path}</code></td>
|
|
90
|
+
<td>${r.success
|
|
91
|
+
? `<span class="ok">✔ ${r.status}</span>`
|
|
92
|
+
: `<span class="fail">✖ ${r.status || 'ERR'}</span>`}</td>
|
|
93
|
+
<td>${r.slow ? `<span class="slow">⚠️ ${r.duration}ms</span>` : `${r.duration}ms`}</td>
|
|
94
|
+
<td>${r.retries > 0 ? `${r.retries} retry` : '—'}</td>
|
|
95
|
+
<td>${r.error ? `<span class="errtext">${r.error}</span>` : '—'}</td>
|
|
96
|
+
</tr>
|
|
97
|
+
`).join('');
|
|
98
|
+
return `<!DOCTYPE html>
|
|
99
|
+
<html lang="en">
|
|
100
|
+
<head>
|
|
101
|
+
<meta charset="UTF-8"/>
|
|
102
|
+
<title>APISnap Report — ${data.generatedAt}</title>
|
|
103
|
+
<style>
|
|
104
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
105
|
+
body{font-family:'Segoe UI',system-ui,sans-serif;background:#f8fafc;color:#1e293b;padding:2rem}
|
|
106
|
+
h1{font-size:1.8rem;margin-bottom:.25rem}
|
|
107
|
+
.sub{color:#64748b;font-size:.9rem;margin-bottom:2rem}
|
|
108
|
+
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:1rem;margin-bottom:2rem}
|
|
109
|
+
.card{background:#fff;border-radius:12px;padding:1.2rem;box-shadow:0 1px 4px rgba(0,0,0,.08);text-align:center}
|
|
110
|
+
.card .num{font-size:2rem;font-weight:700}
|
|
111
|
+
.card .lbl{font-size:.8rem;color:#64748b;margin-top:.25rem}
|
|
112
|
+
.green{color:#16a34a}.red{color:#dc2626}.yellow{color:#ca8a04}.blue{color:#2563eb}
|
|
113
|
+
table{width:100%;border-collapse:collapse;background:#fff;border-radius:12px;overflow:hidden;box-shadow:0 1px 4px rgba(0,0,0,.08)}
|
|
114
|
+
th{background:#1e293b;color:#f8fafc;padding:.8rem 1rem;text-align:left;font-size:.85rem}
|
|
115
|
+
td{padding:.75rem 1rem;font-size:.875rem;border-bottom:1px solid #f1f5f9}
|
|
116
|
+
.badge{display:inline-block;padding:.15rem .5rem;border-radius:6px;font-weight:600;font-size:.75rem;color:#fff}
|
|
117
|
+
.badge-get{background:#2563eb}.badge-post{background:#16a34a}.badge-put{background:#d97706}
|
|
118
|
+
.badge-delete{background:#dc2626}.badge-patch{background:#7c3aed}
|
|
119
|
+
.ok{color:#16a34a;font-weight:600}.fail{color:#dc2626;font-weight:600}.slow{color:#ca8a04;font-weight:600}
|
|
120
|
+
.errtext{color:#dc2626;font-size:.8rem}
|
|
121
|
+
.progress{background:#e2e8f0;border-radius:999px;height:10px;margin:1rem 0}
|
|
122
|
+
.progress-bar{background:#16a34a;height:10px;border-radius:999px;transition:width .3s}
|
|
123
|
+
footer{margin-top:2rem;color:#94a3b8;font-size:.8rem;text-align:center}
|
|
124
|
+
</style>
|
|
125
|
+
</head>
|
|
126
|
+
<body>
|
|
127
|
+
<h1>📸 APISnap Health Report</h1>
|
|
128
|
+
<p class="sub">Generated: ${data.generatedAt} | Port: ${data.config.port} | v${data.version}</p>
|
|
129
|
+
|
|
130
|
+
<div class="cards">
|
|
131
|
+
<div class="card"><div class="num blue">${data.summary.total}</div><div class="lbl">Total Endpoints</div></div>
|
|
132
|
+
<div class="card"><div class="num green">${data.summary.passed}</div><div class="lbl">Passed</div></div>
|
|
133
|
+
<div class="card"><div class="num red">${data.summary.failed}</div><div class="lbl">Failed</div></div>
|
|
134
|
+
<div class="card"><div class="num yellow">${data.summary.slow}</div><div class="lbl">Slow (>${data.config.slowThreshold}ms)</div></div>
|
|
135
|
+
<div class="card"><div class="num blue">${data.summary.avgDuration}ms</div><div class="lbl">Avg Response</div></div>
|
|
136
|
+
<div class="card"><div class="num ${passRate === 100 ? 'green' : passRate >= 80 ? 'yellow' : 'red'}">${passRate}%</div><div class="lbl">Pass Rate</div></div>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<div class="progress"><div class="progress-bar" style="width:${passRate}%"></div></div>
|
|
140
|
+
|
|
141
|
+
<table>
|
|
142
|
+
<thead>
|
|
143
|
+
<tr><th>Method</th><th>Path</th><th>Status</th><th>Duration</th><th>Retries</th><th>Error</th></tr>
|
|
144
|
+
</thead>
|
|
145
|
+
<tbody>${rows}</tbody>
|
|
146
|
+
</table>
|
|
147
|
+
|
|
148
|
+
<footer>APISnap v${data.version} — MIT License</footer>
|
|
149
|
+
</body>
|
|
150
|
+
</html>`;
|
|
151
|
+
}
|
|
152
|
+
// ─── Main CLI ─────────────────────────────────────────────────────────────────
|
|
13
153
|
program
|
|
14
154
|
.name('apisnap')
|
|
15
155
|
.description('Instant API health-check CLI for Express.js')
|
|
16
156
|
.version(version)
|
|
17
|
-
.option('-p, --port <number>', '
|
|
18
|
-
.option('-H, --header <string>', '
|
|
19
|
-
.option('-
|
|
20
|
-
.option('-
|
|
157
|
+
.option('-p, --port <number>', 'Port your server is running on')
|
|
158
|
+
.option('-H, --header <string>', 'Custom header — can be used multiple times (e.g. -H "Authorization: Bearer TOKEN" -H "x-api-key: SECRET")', collect, [])
|
|
159
|
+
.option('-c, --cookie <string>', 'Cookie string (e.g. "sessionId=abc; token=xyz")')
|
|
160
|
+
.option('-s, --slow <number>', 'Slow response threshold in ms')
|
|
161
|
+
.option('-t, --timeout <number>', 'Request timeout in ms')
|
|
162
|
+
.option('-r, --retry <number>', 'Retry failed requests N times')
|
|
163
|
+
.option('-e, --export <filename>', 'Export JSON report (e.g. report)')
|
|
164
|
+
.option('--html <filename>', 'Export HTML report (e.g. report)')
|
|
165
|
+
.option('--only <methods>', 'Only test specific methods (e.g. "GET,POST")')
|
|
166
|
+
.option('--base-url <url>', 'Override base URL (e.g. https://staging.myapp.com)')
|
|
167
|
+
.option('--params <json>', 'JSON map of param overrides (e.g. \'{"id":"42"}\')')
|
|
168
|
+
.option('--fail-on-slow', 'Exit with code 1 if any slow routes are found')
|
|
21
169
|
.action(async (options) => {
|
|
22
|
-
|
|
23
|
-
const
|
|
170
|
+
// Merge config file with CLI options (CLI takes precedence)
|
|
171
|
+
const fileConfig = loadConfigFile();
|
|
172
|
+
const mergedOptions = { ...fileConfig, ...options };
|
|
173
|
+
const port = mergedOptions.port || '3000';
|
|
174
|
+
const slowThreshold = parseInt(mergedOptions.slow || '200');
|
|
175
|
+
const timeout = parseInt(mergedOptions.timeout || '5000');
|
|
176
|
+
const retryCount = parseInt(mergedOptions.retry || '0');
|
|
177
|
+
const onlyMethods = mergedOptions.only
|
|
178
|
+
? mergedOptions.only.split(',').map((m) => m.trim().toUpperCase())
|
|
179
|
+
: null;
|
|
180
|
+
const paramOverrides = mergedOptions.params
|
|
181
|
+
? JSON.parse(mergedOptions.params)
|
|
182
|
+
: (fileConfig.params || {});
|
|
183
|
+
const baseUrl = mergedOptions.baseUrl || mergedOptions['base-url'] || `http://localhost:${port}`;
|
|
24
184
|
const discoveryUrl = `http://localhost:${port}/__apisnap_discovery`;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
185
|
+
// Build headers
|
|
186
|
+
const headerArgs = [
|
|
187
|
+
...(Array.isArray(mergedOptions.header) ? mergedOptions.header : []),
|
|
188
|
+
...(Array.isArray(fileConfig.headers) ? fileConfig.headers : []),
|
|
189
|
+
];
|
|
190
|
+
const customHeaders = {
|
|
191
|
+
...parseHeaders(headerArgs),
|
|
192
|
+
'User-Agent': `APISnap/${version}`,
|
|
193
|
+
};
|
|
194
|
+
if (mergedOptions.cookie) {
|
|
195
|
+
customHeaders['Cookie'] = mergedOptions.cookie;
|
|
33
196
|
}
|
|
197
|
+
// ── Banner ──────────────────────────────────────────────────────────────
|
|
34
198
|
console.log(chalk_1.default.bold.cyan(`\n📸 APISnap v${version}`));
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (
|
|
41
|
-
|
|
199
|
+
console.log(chalk_1.default.gray(` Target: ${baseUrl}`));
|
|
200
|
+
console.log(chalk_1.default.gray(` Slow: >${slowThreshold}ms`));
|
|
201
|
+
console.log(chalk_1.default.gray(` Timeout: ${timeout}ms`));
|
|
202
|
+
if (retryCount > 0)
|
|
203
|
+
console.log(chalk_1.default.gray(` Retries: ${retryCount}`));
|
|
204
|
+
if (Object.keys(customHeaders).filter(k => k !== 'User-Agent').length > 0) {
|
|
205
|
+
const safeHeaders = { ...customHeaders };
|
|
206
|
+
// Mask auth tokens in output
|
|
207
|
+
Object.keys(safeHeaders).forEach(k => {
|
|
208
|
+
if (/auth|token|key|secret|cookie/i.test(k)) {
|
|
209
|
+
safeHeaders[k] = safeHeaders[k].slice(0, 8) + '••••••';
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
delete safeHeaders['User-Agent'];
|
|
213
|
+
console.log(chalk_1.default.gray(` Headers: ${JSON.stringify(safeHeaders)}`));
|
|
42
214
|
}
|
|
215
|
+
if (onlyMethods)
|
|
216
|
+
console.log(chalk_1.default.gray(` Filter: ${onlyMethods.join(', ')}`));
|
|
43
217
|
console.log();
|
|
44
|
-
const spinner = (0, ora_1.default)('Connecting to
|
|
218
|
+
const spinner = (0, ora_1.default)('Connecting to discovery endpoint...').start();
|
|
219
|
+
const results = [];
|
|
45
220
|
try {
|
|
46
|
-
//
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
221
|
+
// ── Discovery ───────────────────────────────────────────────────────
|
|
222
|
+
const discovery = await axios_1.default.get(discoveryUrl, { timeout: 5000 });
|
|
223
|
+
let { endpoints } = discovery.data;
|
|
224
|
+
// Filter by method if --only flag provided
|
|
225
|
+
if (onlyMethods) {
|
|
226
|
+
endpoints = endpoints.filter((e) => e.methods.some((m) => onlyMethods.includes(m)));
|
|
227
|
+
}
|
|
228
|
+
spinner.succeed(chalk_1.default.green(`Connected! Found ${endpoints.length} endpoint${endpoints.length !== 1 ? 's' : ''} to test.\n`));
|
|
229
|
+
let passed = 0, failed = 0, slow = 0;
|
|
230
|
+
const allDurations = [];
|
|
231
|
+
// ── Test Each Endpoint ───────────────────────────────────────────────
|
|
55
232
|
for (const endpoint of endpoints) {
|
|
56
|
-
const method = endpoint.methods[0];
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
path = path.replace(/:[a-zA-Z0-9]+/g, '1');
|
|
61
|
-
}
|
|
62
|
-
const fullUrl = `http://localhost:${port}${path}`;
|
|
63
|
-
const testSpinner = (0, ora_1.default)(`Testing ${chalk_1.default.bold(method)} ${path}`).start();
|
|
64
|
-
// Step 8: Initialize result object for this endpoint
|
|
233
|
+
const method = endpoint.methods[0]; // primary method
|
|
234
|
+
const rawPath = endpoint.path;
|
|
235
|
+
const resolvedPath = replacePath(rawPath, paramOverrides);
|
|
236
|
+
const fullUrl = `${baseUrl}${resolvedPath}`;
|
|
65
237
|
const testResult = {
|
|
66
|
-
method,
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
status: 0,
|
|
70
|
-
duration: 0,
|
|
71
|
-
success: false,
|
|
72
|
-
slow: false,
|
|
238
|
+
method, path: rawPath, fullUrl,
|
|
239
|
+
status: 0, statusText: '', duration: 0,
|
|
240
|
+
success: false, slow: false, retries: 0,
|
|
73
241
|
};
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
242
|
+
const testSpinner = (0, ora_1.default)({ text: `${chalk_1.default.bold(method.padEnd(7))} ${chalk_1.default.dim(rawPath)}`, prefixText: ' ' }).start();
|
|
243
|
+
let lastError = null;
|
|
244
|
+
let attempt = 0;
|
|
245
|
+
while (attempt <= retryCount) {
|
|
246
|
+
try {
|
|
247
|
+
const start = Date.now();
|
|
248
|
+
const res = await (0, axios_1.default)({
|
|
249
|
+
method,
|
|
250
|
+
url: fullUrl,
|
|
251
|
+
headers: customHeaders,
|
|
252
|
+
timeout,
|
|
253
|
+
validateStatus: () => true, // Don't throw on 4xx/5xx — we judge ourselves
|
|
254
|
+
});
|
|
255
|
+
const duration = Date.now() - start;
|
|
256
|
+
testResult.duration = duration;
|
|
257
|
+
testResult.status = res.status;
|
|
258
|
+
testResult.statusText = res.statusText;
|
|
259
|
+
testResult.retries = attempt;
|
|
260
|
+
testResult.success = res.status < 400;
|
|
261
|
+
testResult.slow = duration > slowThreshold;
|
|
262
|
+
allDurations.push(duration);
|
|
263
|
+
if (testResult.success) {
|
|
264
|
+
const durationStr = testResult.slow
|
|
265
|
+
? chalk_1.default.yellow.bold(`${duration}ms ← slow!`)
|
|
266
|
+
: chalk_1.default.gray(`${duration}ms`);
|
|
267
|
+
const msg = `${chalk_1.default.bold(method.padEnd(7))} ${chalk_1.default.white(rawPath.padEnd(35))} ` +
|
|
268
|
+
`${chalk_1.default.green(`[${res.status}]`)} ${durationStr}`;
|
|
269
|
+
if (testResult.slow) {
|
|
270
|
+
testSpinner.warn(msg);
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
testSpinner.succeed(msg);
|
|
274
|
+
}
|
|
275
|
+
passed++;
|
|
276
|
+
if (testResult.slow)
|
|
277
|
+
slow++;
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
testSpinner.fail(`${chalk_1.default.bold(method.padEnd(7))} ${chalk_1.default.white(rawPath.padEnd(35))} ` +
|
|
281
|
+
`${chalk_1.default.red(`[${res.status} ${res.statusText}]`)} ${chalk_1.default.gray(`${duration}ms`)}`);
|
|
282
|
+
// Helpful hint for auth errors
|
|
283
|
+
if (res.status === 401) {
|
|
284
|
+
console.log(chalk_1.default.yellow(` 💡 Hint: 401 Unauthorized — try adding -H "Authorization: Bearer YOUR_TOKEN" or --cookie "sessionId=abc"`));
|
|
285
|
+
}
|
|
286
|
+
else if (res.status === 403) {
|
|
287
|
+
console.log(chalk_1.default.yellow(` 💡 Hint: 403 Forbidden — your token may lack permission for this route`));
|
|
288
|
+
}
|
|
289
|
+
else if (res.status === 404) {
|
|
290
|
+
console.log(chalk_1.default.yellow(` 💡 Hint: 404 Not Found — path param replacement may need --params '{"id":"YOUR_ID"}'`));
|
|
291
|
+
}
|
|
292
|
+
failed++;
|
|
293
|
+
}
|
|
294
|
+
lastError = null;
|
|
295
|
+
break; // success, stop retrying
|
|
296
|
+
}
|
|
297
|
+
catch (err) {
|
|
298
|
+
lastError = err;
|
|
299
|
+
attempt++;
|
|
300
|
+
if (attempt <= retryCount) {
|
|
301
|
+
await new Promise(r => setTimeout(r, 500 * attempt)); // backoff
|
|
302
|
+
}
|
|
98
303
|
}
|
|
99
|
-
testSpinner.succeed(`${statusIcon} ${chalk_1.default.bold(method)} ${chalk_1.default.white(path)} ` +
|
|
100
|
-
`${chalk_1.default.green(`[${res.status} OK]`)} ` +
|
|
101
|
-
`${durationColor(`${duration}ms`)}`);
|
|
102
|
-
passed++;
|
|
103
304
|
}
|
|
104
|
-
|
|
105
|
-
testResult.status = err.response?.status || 500;
|
|
305
|
+
if (lastError) {
|
|
106
306
|
testResult.success = false;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
307
|
+
testResult.retries = attempt - 1;
|
|
308
|
+
testResult.error = lastError.code === 'ECONNABORTED' ? 'Timeout' : lastError.message;
|
|
309
|
+
testSpinner.fail(`${chalk_1.default.bold(method.padEnd(7))} ${chalk_1.default.white(rawPath.padEnd(35))} ` +
|
|
310
|
+
chalk_1.default.red(`[${testResult.error}]`));
|
|
110
311
|
failed++;
|
|
111
312
|
}
|
|
112
|
-
results.push(testResult);
|
|
313
|
+
results.push(testResult);
|
|
113
314
|
}
|
|
114
|
-
// Summary
|
|
315
|
+
// ── Summary ──────────────────────────────────────────────────────────
|
|
316
|
+
const avgDuration = allDurations.length > 0
|
|
317
|
+
? Math.round(allDurations.reduce((a, b) => a + b, 0) / allDurations.length)
|
|
318
|
+
: 0;
|
|
319
|
+
const totalDuration = allDurations.reduce((a, b) => a + b, 0);
|
|
115
320
|
console.log(chalk_1.default.bold('\n📊 Summary:'));
|
|
116
|
-
console.log(chalk_1.default.green(
|
|
117
|
-
console.log(chalk_1.default.red(
|
|
118
|
-
console.log(chalk_1.default.yellow(
|
|
321
|
+
console.log(` ${chalk_1.default.green('✅ Passed: ')} ${chalk_1.default.bold(passed)}`);
|
|
322
|
+
console.log(` ${chalk_1.default.red('❌ Failed: ')} ${chalk_1.default.bold(failed)}`);
|
|
323
|
+
console.log(` ${chalk_1.default.yellow('⚠️ Slow: ')} ${chalk_1.default.bold(slow)} (>${slowThreshold}ms)`);
|
|
324
|
+
console.log(` ${chalk_1.default.cyan('⏱ Avg: ')} ${chalk_1.default.bold(avgDuration + 'ms')}`);
|
|
325
|
+
console.log(` ${chalk_1.default.cyan('🕐 Total: ')} ${chalk_1.default.bold(totalDuration + 'ms')}`);
|
|
119
326
|
if (failed > 0) {
|
|
120
327
|
console.log(chalk_1.default.red.bold('\n⚠️ Some endpoints are unhealthy!'));
|
|
121
328
|
}
|
|
122
329
|
else if (slow > 0) {
|
|
123
|
-
console.log(chalk_1.default.yellow.bold('\n🐢 All
|
|
330
|
+
console.log(chalk_1.default.yellow.bold('\n🐢 All alive, but some routes are slow!'));
|
|
124
331
|
}
|
|
125
332
|
else {
|
|
126
333
|
console.log(chalk_1.default.green.bold('\n✨ All systems nominal!'));
|
|
127
334
|
}
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
335
|
+
// ── Auth Troubleshooting Summary ─────────────────────────────────────
|
|
336
|
+
const authFailures = results.filter(r => r.status === 401 || r.status === 403);
|
|
337
|
+
if (authFailures.length > 0 && !headerArgs.length && !mergedOptions.cookie) {
|
|
338
|
+
console.log(chalk_1.default.bgYellow.black.bold('\n🔐 Auth Help'));
|
|
339
|
+
console.log(chalk_1.default.yellow(' You have ' + authFailures.length + ' auth failure(s) and no credentials were provided.'));
|
|
340
|
+
console.log(chalk_1.default.yellow(' Solutions:'));
|
|
341
|
+
console.log(chalk_1.default.gray(' JWT: apisnap -H "Authorization: Bearer YOUR_JWT_TOKEN"'));
|
|
342
|
+
console.log(chalk_1.default.gray(' API Key: apisnap -H "x-api-key: YOUR_KEY"'));
|
|
343
|
+
console.log(chalk_1.default.gray(' Cookie: apisnap --cookie "sessionId=abc123"'));
|
|
344
|
+
console.log(chalk_1.default.gray(' Multi: apisnap -H "Authorization: Bearer TOKEN" -H "x-tenant: acme"'));
|
|
345
|
+
console.log(chalk_1.default.gray(' Config: create .apisnaprc.json (see README)\n'));
|
|
346
|
+
}
|
|
347
|
+
// ── Exports ──────────────────────────────────────────────────────────
|
|
348
|
+
const reportData = {
|
|
349
|
+
tool: 'APISnap', version,
|
|
350
|
+
generatedAt: new Date().toISOString(),
|
|
351
|
+
config: { port, baseUrl, slowThreshold, timeout, headers: Object.keys(customHeaders).filter(k => k !== 'User-Agent') },
|
|
352
|
+
summary: { total: endpoints.length, passed, failed, slow, avgDuration, totalDuration },
|
|
353
|
+
results,
|
|
354
|
+
};
|
|
355
|
+
if (mergedOptions.export) {
|
|
356
|
+
const filePath = mergedOptions.export.endsWith('.json') ? mergedOptions.export : `${mergedOptions.export}.json`;
|
|
149
357
|
fs_1.default.writeFileSync(filePath, JSON.stringify(reportData, null, 2));
|
|
150
|
-
console.log(chalk_1.default.cyan
|
|
358
|
+
console.log(chalk_1.default.cyan(`\n💾 JSON report → ${chalk_1.default.white(filePath)}`));
|
|
359
|
+
}
|
|
360
|
+
if (mergedOptions.html) {
|
|
361
|
+
const filePath = mergedOptions.html.endsWith('.html') ? mergedOptions.html : `${mergedOptions.html}.html`;
|
|
362
|
+
fs_1.default.writeFileSync(filePath, generateHTMLReport(reportData));
|
|
363
|
+
console.log(chalk_1.default.cyan(`🌐 HTML report → ${chalk_1.default.white(filePath)}`));
|
|
151
364
|
}
|
|
365
|
+
console.log();
|
|
366
|
+
// Exit codes for CI/CD
|
|
367
|
+
const shouldFail = failed > 0 || (mergedOptions['fail-on-slow'] && slow > 0);
|
|
368
|
+
process.exit(shouldFail ? 1 : 0);
|
|
152
369
|
}
|
|
153
370
|
catch (error) {
|
|
154
|
-
spinner.fail(chalk_1.default.red(`
|
|
155
|
-
console.log(chalk_1.default.yellow('
|
|
371
|
+
spinner.fail(chalk_1.default.red(`Cannot reach discovery endpoint: ${discoveryUrl}`));
|
|
372
|
+
console.log(chalk_1.default.yellow('\n Checklist:'));
|
|
373
|
+
console.log(chalk_1.default.gray(' 1. Is your server running? (e.g. node server.js)'));
|
|
374
|
+
console.log(chalk_1.default.gray(' 2. Did you call apisnap.init(app) in your server?'));
|
|
375
|
+
console.log(chalk_1.default.gray(' 3. Is the port correct? (default 3000, use -p PORT)'));
|
|
376
|
+
console.log(chalk_1.default.gray(' 4. Is apisnap.init(app) placed AFTER your routes?\n'));
|
|
156
377
|
process.exit(1);
|
|
157
378
|
}
|
|
158
379
|
});
|
|
380
|
+
// Allows -H to be used multiple times
|
|
381
|
+
function collect(val, prev) {
|
|
382
|
+
return prev.concat([val]);
|
|
383
|
+
}
|
|
159
384
|
program.parse(process.argv);
|
|
160
385
|
//# sourceMappingURL=runner.js.map
|
package/dist/core/runner.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.js","sourceRoot":"","sources":["../../src/core/runner.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"runner.js","sourceRoot":"","sources":["../../src/core/runner.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
export
|
|
1
|
+
export interface APISnapOptions {
|
|
2
|
+
/** Routes to skip during health checks (e.g. ['/admin', '/internal']) */
|
|
3
|
+
skip?: string[];
|
|
4
|
+
/** Custom name shown in discovery response */
|
|
5
|
+
name?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare const init: (app: any, options?: APISnapOptions) => void;
|
|
2
8
|
declare const _default: {
|
|
3
|
-
init: (app: any) => void;
|
|
9
|
+
init: (app: any, options?: APISnapOptions) => void;
|
|
4
10
|
};
|
|
5
11
|
export default _default;
|
|
6
12
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/middleware/index.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,IAAI,GAAI,KAAK,GAAG,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/middleware/index.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,cAAc;IAC3B,yEAAyE;IACzE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,8CAA8C;IAC9C,IAAI,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,eAAO,MAAM,IAAI,GAAI,KAAK,GAAG,EAAE,UAAS,cAAmB,SA6F1D,CAAC;;gBA7FwB,GAAG,YAAW,cAAc;;AA+FtD,wBAAwB"}
|
package/dist/middleware/index.js
CHANGED
|
@@ -1,30 +1,33 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.init = void 0;
|
|
4
|
-
const init = (app) => {
|
|
4
|
+
const init = (app, options = {}) => {
|
|
5
5
|
const DISCOVERY_PATH = '/__apisnap_discovery';
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
const skipList = options.skip || [];
|
|
7
|
+
// ─── Recursive Route Extractor ────────────────────────────────────────────
|
|
8
|
+
const extractRoutes = (stack, prefix = '') => {
|
|
8
9
|
let routes = [];
|
|
9
10
|
stack.forEach((layer) => {
|
|
10
11
|
if (layer.route) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
const path = (prefix + layer.route.path).replace('//', '/');
|
|
13
|
+
// Skip the discovery endpoint itself and any user-skipped paths
|
|
14
|
+
if (path === DISCOVERY_PATH)
|
|
15
|
+
return;
|
|
16
|
+
if (skipList.some((s) => path.startsWith(s)))
|
|
17
|
+
return;
|
|
18
|
+
const methods = Object.keys(layer.route.methods)
|
|
19
|
+
.filter((m) => m !== '_all')
|
|
20
|
+
.map((m) => m.toUpperCase());
|
|
21
|
+
routes.push({ path, methods });
|
|
15
22
|
}
|
|
16
23
|
else if (layer.handle && layer.handle.stack) {
|
|
17
|
-
// Nested Router - GO DEEPER
|
|
18
|
-
// Extract the prefix from the regexp (e.g. /api or /api/community)
|
|
19
24
|
let rStr = layer.regexp.toString();
|
|
20
25
|
let routerPrefix = '';
|
|
21
|
-
// Try to match standard Express router regexp: /^\/api\/?(?=\/|$)/i
|
|
22
26
|
const standardMatch = rStr.match(/^\/\^\\\/(.*?)\\\/\?\(\?\=\\\/\|\$\)\/i$/);
|
|
23
27
|
if (standardMatch) {
|
|
24
28
|
routerPrefix = standardMatch[1];
|
|
25
29
|
}
|
|
26
30
|
else {
|
|
27
|
-
// Fallback for custom or older regexes
|
|
28
31
|
const fallbackMatch = rStr.match(/^\/\^\\?(.*?)\\?\/?(?:\(\?=\\\/\|\$\))?\//);
|
|
29
32
|
routerPrefix = fallbackMatch ? fallbackMatch[1] : '';
|
|
30
33
|
}
|
|
@@ -32,32 +35,31 @@ const init = (app) => {
|
|
|
32
35
|
if (routerPrefix && !routerPrefix.startsWith('/')) {
|
|
33
36
|
routerPrefix = '/' + routerPrefix;
|
|
34
37
|
}
|
|
35
|
-
// Avoid double slashes in concatenation
|
|
36
38
|
const newPrefix = (prefix + routerPrefix).replace(/\/\//g, '/');
|
|
37
|
-
routes = routes.concat(
|
|
39
|
+
routes = routes.concat(extractRoutes(layer.handle.stack, newPrefix));
|
|
38
40
|
}
|
|
39
41
|
});
|
|
40
42
|
return routes;
|
|
41
43
|
};
|
|
44
|
+
// ─── Discovery Endpoint ───────────────────────────────────────────────────
|
|
45
|
+
// IMPORTANT: This is registered FIRST so auth middleware added later won't
|
|
46
|
+
// wrap it. If your auth is global (app.use), see the bypass middleware below.
|
|
42
47
|
app.get(DISCOVERY_PATH, (req, res) => {
|
|
43
48
|
try {
|
|
44
|
-
// Safely get Express router (v4 uses _router, v5 uses router)
|
|
45
49
|
let router = app._router;
|
|
46
50
|
if (!router) {
|
|
47
51
|
try {
|
|
48
52
|
router = app.router;
|
|
49
53
|
}
|
|
50
|
-
catch (
|
|
51
|
-
// Ignore getter deprecation errors from Express 4
|
|
52
|
-
}
|
|
54
|
+
catch (_) { }
|
|
53
55
|
}
|
|
54
56
|
if (!router) {
|
|
55
|
-
res.status(500).json({ error: 'Router not initialized yet' });
|
|
56
|
-
return;
|
|
57
|
+
return res.status(500).json({ error: 'Router not initialized yet. Make sure apisnap.init(app) is called after your routes.' });
|
|
57
58
|
}
|
|
58
|
-
const allRoutes =
|
|
59
|
+
const allRoutes = extractRoutes(router.stack);
|
|
59
60
|
res.json({
|
|
60
|
-
|
|
61
|
+
tool: 'APISnap',
|
|
62
|
+
appName: options.name || 'Express App',
|
|
61
63
|
timestamp: new Date().toISOString(),
|
|
62
64
|
total: allRoutes.length,
|
|
63
65
|
endpoints: allRoutes,
|
|
@@ -67,7 +69,27 @@ const init = (app) => {
|
|
|
67
69
|
res.status(500).json({ error: 'Failed to parse routes', detail: e.message });
|
|
68
70
|
}
|
|
69
71
|
});
|
|
70
|
-
|
|
72
|
+
// ─── Auth Bypass Middleware ───────────────────────────────────────────────
|
|
73
|
+
// This intercepts requests to the discovery path and short-circuits any
|
|
74
|
+
// downstream auth middleware the user may have added globally.
|
|
75
|
+
// Works by monkey-patching app.use to detect auth-style middleware.
|
|
76
|
+
const originalUse = app.use.bind(app);
|
|
77
|
+
app.use = function (...args) {
|
|
78
|
+
// If it's a global middleware (no path), wrap it to skip discovery route
|
|
79
|
+
if (typeof args[0] === 'function') {
|
|
80
|
+
const originalMiddleware = args[0];
|
|
81
|
+
args[0] = (req, res, next) => {
|
|
82
|
+
if (req.path === DISCOVERY_PATH)
|
|
83
|
+
return next();
|
|
84
|
+
return originalMiddleware(req, res, next);
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return originalUse(...args);
|
|
88
|
+
};
|
|
89
|
+
console.log(`\x1b[32m✅ [APISnap] Discovery active → http://localhost:PORT${DISCOVERY_PATH}\x1b[0m`);
|
|
90
|
+
if (skipList.length > 0) {
|
|
91
|
+
console.log(`\x1b[33m⏭ [APISnap] Skipping: ${skipList.join(', ')}\x1b[0m`);
|
|
92
|
+
}
|
|
71
93
|
};
|
|
72
94
|
exports.init = init;
|
|
73
95
|
exports.default = { init: exports.init };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/middleware/index.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/middleware/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED