apitrap 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +200 -0
- package/index.d.ts +117 -0
- package/index.js +395 -0
- package/package.json +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# apitrap
|
|
2
|
+
|
|
3
|
+
**Turn your Express routes into living API documentation — automatically.**
|
|
4
|
+
|
|
5
|
+
Most teams write their API docs manually, or forget to update them after changes.
|
|
6
|
+
`apitrap` captures real API traffic as it happens and sends it to your monitor dashboard, so your collection always reflects what your API _actually does_.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install apitrap
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## How it works
|
|
15
|
+
|
|
16
|
+
Add one middleware per route group. Every time a request hits that route, the library captures the method, path, body, query, response, status code, and duration — then sends it to your monitor server in the background.
|
|
17
|
+
|
|
18
|
+
No manual effort. No extra tools. Just traffic → collection.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Quick start
|
|
23
|
+
|
|
24
|
+
### 1. Initialize (once, in your entry file)
|
|
25
|
+
|
|
26
|
+
```javascript
|
|
27
|
+
const { initApiCapture } = require("apitrap");
|
|
28
|
+
|
|
29
|
+
initApiCapture({
|
|
30
|
+
appName: "my-app",
|
|
31
|
+
monitorUrl: "https://your-monitor-server.com/api/capture",
|
|
32
|
+
monitorApiKey: process.env.MONITOR_API_KEY,
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 2. Add to your routes
|
|
37
|
+
|
|
38
|
+
```javascript
|
|
39
|
+
const { getClient } = require("apitrap");
|
|
40
|
+
|
|
41
|
+
const router = express.Router();
|
|
42
|
+
const auth = getClient().createMiddlewareFactory("Authentication");
|
|
43
|
+
|
|
44
|
+
router.post(
|
|
45
|
+
"/login",
|
|
46
|
+
auth.capture("User login", "Login page"),
|
|
47
|
+
async (req, res) => {
|
|
48
|
+
// your logic here
|
|
49
|
+
res.json({ success: true });
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
router.post(
|
|
54
|
+
"/register",
|
|
55
|
+
auth.capture("New user registration", ["Login page", "Onboarding"]),
|
|
56
|
+
async (req, res) => {
|
|
57
|
+
res.json({ userId: "..." });
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
That's it. Both routes are now tracked in your dashboard under the **Authentication** group.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## TypeScript
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import { initApiCapture, getClient } from "apitrap";
|
|
70
|
+
|
|
71
|
+
initApiCapture({
|
|
72
|
+
appName: "my-ts-app",
|
|
73
|
+
monitorUrl: process.env.MONITOR_URL,
|
|
74
|
+
monitorApiKey: process.env.MONITOR_API_KEY,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const api = getClient().createMiddlewareFactory("Products");
|
|
78
|
+
|
|
79
|
+
router.get("/products", api.capture("List all products"), handler);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Try it without a monitor server
|
|
85
|
+
|
|
86
|
+
Skip `monitorUrl` entirely. The library switches to **debug mode** and prints captured data to your console — perfect for development.
|
|
87
|
+
|
|
88
|
+
```javascript
|
|
89
|
+
initApiCapture({
|
|
90
|
+
appName: "local-test",
|
|
91
|
+
// no monitorUrl needed
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Output:
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
--- [ApiCapture] [200] 12ms ---
|
|
99
|
+
[POST] /api/login
|
|
100
|
+
Desc: User login
|
|
101
|
+
Body: { "email": "user@example.com", "password": "[REDACTED]" }
|
|
102
|
+
Response: { "success": true }
|
|
103
|
+
---------------------------
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Save to a local file
|
|
109
|
+
|
|
110
|
+
Useful for offline inspection or building your initial API collection before setting up a monitor server:
|
|
111
|
+
|
|
112
|
+
```javascript
|
|
113
|
+
initApiCapture({
|
|
114
|
+
appName: "my-app",
|
|
115
|
+
saveLocal: true,
|
|
116
|
+
localPath: "./data/api-capture.json",
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Sensitive data is always masked
|
|
123
|
+
|
|
124
|
+
Passwords, tokens, and secrets are automatically redacted before leaving your server. You can add custom keys too:
|
|
125
|
+
|
|
126
|
+
```javascript
|
|
127
|
+
initApiCapture({
|
|
128
|
+
sensitiveKeys: ["ssn", "tax-id", "card-number"],
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The following keys are masked by default (case-insensitive): `password`, `token`, `secret`, `creditcard`, `pin`, `auth`, `authorization`, `cookie`.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Graceful shutdown
|
|
137
|
+
|
|
138
|
+
If you need to make sure all captured events are flushed before your server stops:
|
|
139
|
+
|
|
140
|
+
```javascript
|
|
141
|
+
process.on("SIGTERM", async () => {
|
|
142
|
+
await getClient().shutdown();
|
|
143
|
+
process.exit(0);
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Configuration
|
|
150
|
+
|
|
151
|
+
| Option | Type | Default | Description |
|
|
152
|
+
| :-------------- | :--------- | :------------------------ | :------------------------------------------------------ |
|
|
153
|
+
| `appName` | `string` | `"Unknown App"` | App name shown in the monitor dashboard |
|
|
154
|
+
| `monitorUrl` | `string` | — | Endpoint of your monitor server |
|
|
155
|
+
| `monitorApiKey` | `string` | — | API key for authenticating with the monitor server |
|
|
156
|
+
| `enabled` | `boolean` | `true` | Master switch — set to `false` to disable all capturing |
|
|
157
|
+
| `debug` | `boolean` | `true` if no `monitorUrl` | Print captured data to console instead of sending |
|
|
158
|
+
| `saveLocal` | `boolean` | `false` | Append captured events to a local JSON file |
|
|
159
|
+
| `localPath` | `string` | `./captured-apis.json` | Path for the local JSON file |
|
|
160
|
+
| `sensitiveKeys` | `string[]` | See above | Additional keys to redact from bodies and queries |
|
|
161
|
+
| `batchSize` | `number` | `10` | Max events per HTTP batch to the monitor server |
|
|
162
|
+
| `batchInterval` | `number` | `3000` | How often (ms) to flush the event queue |
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## What gets captured per request
|
|
167
|
+
|
|
168
|
+
| Field | Description |
|
|
169
|
+
| :----------- | :--------------------------------------------- |
|
|
170
|
+
| `route` | Full URL path including query string |
|
|
171
|
+
| `method` | HTTP method (GET, POST, etc.) |
|
|
172
|
+
| `body` | Request body (sensitive keys redacted) |
|
|
173
|
+
| `query` | Query parameters |
|
|
174
|
+
| `response` | Response body (sensitive keys redacted) |
|
|
175
|
+
| `statusCode` | HTTP status code |
|
|
176
|
+
| `durationMs` | Request processing time in milliseconds |
|
|
177
|
+
| `desc` | Your description for this endpoint |
|
|
178
|
+
| `menus` | Menu/page labels for grouping in the UI |
|
|
179
|
+
| `routeName` | Group name (set via `createMiddlewareFactory`) |
|
|
180
|
+
| `capturedAt` | ISO timestamp |
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Changelog
|
|
185
|
+
|
|
186
|
+
### v1.1.0
|
|
187
|
+
|
|
188
|
+
- **Response capture** — `res.json()` is now intercepted to capture response body
|
|
189
|
+
- **Status code + duration** — every captured event includes HTTP status and response time
|
|
190
|
+
- **Async local save** — file I/O is now non-blocking
|
|
191
|
+
- **Batch sender** — events are queued and sent in batches to reduce HTTP overhead
|
|
192
|
+
- **Deep masking fix** — nested sensitive keys are now correctly redacted
|
|
193
|
+
- **`enabled` config** — replaces `openGen` (still supported for backward compatibility)
|
|
194
|
+
- **`shutdown()` method** — flush pending events before process exit
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## License
|
|
199
|
+
|
|
200
|
+
ISC
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from "express";
|
|
2
|
+
|
|
3
|
+
export interface ApiCaptureConfig {
|
|
4
|
+
/** ชื่อแอปพลิเคชัน */
|
|
5
|
+
appName?: string;
|
|
6
|
+
|
|
7
|
+
/** URL ของ Monitor Server */
|
|
8
|
+
monitorUrl?: string;
|
|
9
|
+
|
|
10
|
+
/** API Key สำหรับ authenticate กับ Monitor Server */
|
|
11
|
+
monitorApiKey?: string;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* เปิด/ปิดการ capture (ค่าเริ่มต้น: true)
|
|
15
|
+
* @alias openGen (deprecated — ใช้ enabled แทน)
|
|
16
|
+
*/
|
|
17
|
+
enabled?: boolean;
|
|
18
|
+
|
|
19
|
+
/** @deprecated ใช้ enabled แทน */
|
|
20
|
+
openGen?: boolean;
|
|
21
|
+
|
|
22
|
+
/** เปิด debug mode — print ข้อมูลลง console แทนการยิง API */
|
|
23
|
+
debug?: boolean;
|
|
24
|
+
|
|
25
|
+
/** บันทึกข้อมูลลงไฟล์ JSON (ค่าเริ่มต้น: false) */
|
|
26
|
+
saveLocal?: boolean;
|
|
27
|
+
|
|
28
|
+
/** path ของไฟล์ที่จะบันทึก (ค่าเริ่มต้น: ./captured-apis.json) */
|
|
29
|
+
localPath?: string;
|
|
30
|
+
|
|
31
|
+
/** Keys เพิ่มเติมที่ต้องการ redact (case-insensitive) */
|
|
32
|
+
sensitiveKeys?: string[];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* จำนวน events ต่อ batch (ค่าเริ่มต้น: 10)
|
|
36
|
+
* เมื่อ queue เต็มจะ flush ทันที
|
|
37
|
+
*/
|
|
38
|
+
batchSize?: number;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* ช่วงเวลา flush อัตโนมัติ เป็น ms (ค่าเริ่มต้น: 3000)
|
|
42
|
+
*/
|
|
43
|
+
batchInterval?: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ApiCaptureParams {
|
|
47
|
+
routeName?: string;
|
|
48
|
+
menus?: string[];
|
|
49
|
+
route?: string;
|
|
50
|
+
method?: string;
|
|
51
|
+
desc?: string;
|
|
52
|
+
body?: any;
|
|
53
|
+
query?: any;
|
|
54
|
+
/** v1.x: response body ที่ถูก intercept */
|
|
55
|
+
responseBody?: any;
|
|
56
|
+
/** v1.x: HTTP status code ของ response */
|
|
57
|
+
statusCode?: number;
|
|
58
|
+
/** v1.x: เวลาที่ใช้ในการ process request (ms) */
|
|
59
|
+
durationMs?: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type MiddlewareFunction = (
|
|
63
|
+
req: Request,
|
|
64
|
+
res: Response,
|
|
65
|
+
next: NextFunction,
|
|
66
|
+
) => void;
|
|
67
|
+
|
|
68
|
+
export interface MiddlewareFactory {
|
|
69
|
+
/**
|
|
70
|
+
* สร้าง Express middleware สำหรับ route นั้นๆ
|
|
71
|
+
* @param desc - คำอธิบาย API
|
|
72
|
+
* @param menus - ชื่อเมนูที่ใช้ API นี้ (optional)
|
|
73
|
+
*/
|
|
74
|
+
capture: (desc?: string, menus?: string | string[]) => MiddlewareFunction;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class ApiCaptureClient {
|
|
78
|
+
serverUrl: string;
|
|
79
|
+
apiKey: string;
|
|
80
|
+
enabled: boolean;
|
|
81
|
+
appName: string;
|
|
82
|
+
debug: boolean;
|
|
83
|
+
saveLocal: boolean;
|
|
84
|
+
localPath: string;
|
|
85
|
+
sensitiveKeys: string[];
|
|
86
|
+
|
|
87
|
+
constructor(config?: ApiCaptureConfig);
|
|
88
|
+
|
|
89
|
+
maskSensitiveData(data: any): any;
|
|
90
|
+
capture(params: ApiCaptureParams): void;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* สร้าง Middleware factory สำหรับกลุ่ม routes
|
|
94
|
+
* @param routeName - ชื่อกลุ่ม route (แสดงใน dashboard)
|
|
95
|
+
* @param routeEnabled - เปิด/ปิด capture สำหรับกลุ่มนี้ (ค่าเริ่มต้น: true)
|
|
96
|
+
*/
|
|
97
|
+
createMiddlewareFactory(
|
|
98
|
+
routeName: string,
|
|
99
|
+
routeEnabled?: boolean,
|
|
100
|
+
): MiddlewareFactory;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Flush ข้อมูลที่ค้างอยู่ใน queue และหยุด timer
|
|
104
|
+
* ควรเรียกก่อน process.exit()
|
|
105
|
+
*/
|
|
106
|
+
shutdown(): Promise<void>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Initialize ApiCapture client แบบ Singleton
|
|
111
|
+
*/
|
|
112
|
+
export function initApiCapture(config?: ApiCaptureConfig): ApiCaptureClient;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* ดึง client instance ที่ init ไว้แล้ว
|
|
116
|
+
*/
|
|
117
|
+
export function getClient(): ApiCaptureClient;
|
package/index.js
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
const http = require("http");
|
|
2
|
+
const https = require("https");
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const fsPromises = require("fs").promises;
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
class ApiCaptureClient {
|
|
8
|
+
constructor(config = {}) {
|
|
9
|
+
this.serverUrl = config.monitorUrl || process.env.MONITOR_URL;
|
|
10
|
+
this.apiKey = config.monitorApiKey || process.env.MONITOR_API_KEY || "";
|
|
11
|
+
this.appName = config.appName || "Unknown App";
|
|
12
|
+
this.debug = config.debug === true || !this.serverUrl;
|
|
13
|
+
|
|
14
|
+
// v1.x: รองรับทั้ง enabled และ openGen (backward compat)
|
|
15
|
+
if (config.enabled !== undefined) {
|
|
16
|
+
this.enabled = config.enabled !== false;
|
|
17
|
+
} else if (config.openGen !== undefined) {
|
|
18
|
+
this.enabled = config.openGen !== false;
|
|
19
|
+
} else {
|
|
20
|
+
this.enabled = true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Local storage config
|
|
24
|
+
this.saveLocal = config.saveLocal === true;
|
|
25
|
+
this.localPath = config.localPath || "./captured-apis.json";
|
|
26
|
+
|
|
27
|
+
// Sensitive keys
|
|
28
|
+
this.sensitiveKeys = [
|
|
29
|
+
"password",
|
|
30
|
+
"token",
|
|
31
|
+
"secret",
|
|
32
|
+
"creditcard",
|
|
33
|
+
"pin",
|
|
34
|
+
"auth",
|
|
35
|
+
"authorization",
|
|
36
|
+
"cookie",
|
|
37
|
+
...(config.sensitiveKeys || []),
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
// v1.x: Batch sender — กันยิง HTTP ทุก request
|
|
41
|
+
this._queue = [];
|
|
42
|
+
this._batchSize = config.batchSize || 10;
|
|
43
|
+
this._batchInterval = config.batchInterval || 3000; // ms
|
|
44
|
+
this._flushTimer = null;
|
|
45
|
+
|
|
46
|
+
if (this.serverUrl) {
|
|
47
|
+
this._startBatchFlush();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!this.serverUrl && !this.debug && !this.saveLocal) {
|
|
51
|
+
this.enabled = false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------
|
|
56
|
+
// v1.x FIX: Deep clone ด้วย structuredClone (ไม่ mutate ข้อมูลหลัก)
|
|
57
|
+
// ---------------------------------------------------------
|
|
58
|
+
maskSensitiveData(data) {
|
|
59
|
+
if (!data || typeof data !== "object") return data;
|
|
60
|
+
|
|
61
|
+
// structuredClone รองรับ Node 17+ / ถ้า env เก่าใช้ JSON fallback
|
|
62
|
+
let cloned;
|
|
63
|
+
try {
|
|
64
|
+
cloned = structuredClone(data);
|
|
65
|
+
} catch {
|
|
66
|
+
cloned = JSON.parse(JSON.stringify(data));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return this._maskDeep(cloned);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
_maskDeep(obj) {
|
|
73
|
+
if (!obj || typeof obj !== "object") return obj;
|
|
74
|
+
|
|
75
|
+
for (const key of Object.keys(obj)) {
|
|
76
|
+
const lowerKey = key.toLowerCase();
|
|
77
|
+
const isSensitive = this.sensitiveKeys.some((s) =>
|
|
78
|
+
lowerKey.includes(s.toLowerCase()),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (isSensitive) {
|
|
82
|
+
obj[key] = "[REDACTED]";
|
|
83
|
+
} else if (typeof obj[key] === "object" && obj[key] !== null) {
|
|
84
|
+
obj[key] = this._maskDeep(obj[key]);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return obj;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------
|
|
92
|
+
// v1.x NEW: Intercept res.json() เพื่อ capture response body
|
|
93
|
+
// ---------------------------------------------------------
|
|
94
|
+
_patchResponse(res) {
|
|
95
|
+
const originalJson = res.json.bind(res);
|
|
96
|
+
let capturedResponseBody = null;
|
|
97
|
+
|
|
98
|
+
res.json = function (body) {
|
|
99
|
+
capturedResponseBody = body;
|
|
100
|
+
return originalJson(body);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
getBody: () => capturedResponseBody,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------
|
|
109
|
+
// v1.x NEW: Async local save (ไม่ block event loop)
|
|
110
|
+
// ---------------------------------------------------------
|
|
111
|
+
async _saveLocalAsync(payloadObj) {
|
|
112
|
+
try {
|
|
113
|
+
const fullPath = path.resolve(this.localPath);
|
|
114
|
+
const dir = path.dirname(fullPath);
|
|
115
|
+
|
|
116
|
+
await fsPromises.mkdir(dir, { recursive: true });
|
|
117
|
+
|
|
118
|
+
let existingData = [];
|
|
119
|
+
try {
|
|
120
|
+
const fileContent = await fsPromises.readFile(fullPath, "utf-8");
|
|
121
|
+
const parsed = JSON.parse(fileContent);
|
|
122
|
+
existingData = Array.isArray(parsed) ? parsed : [];
|
|
123
|
+
} catch {
|
|
124
|
+
existingData = [];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
existingData.push(payloadObj);
|
|
128
|
+
await fsPromises.writeFile(
|
|
129
|
+
fullPath,
|
|
130
|
+
JSON.stringify(existingData, null, 2),
|
|
131
|
+
"utf-8",
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
if (this.debug) {
|
|
135
|
+
console.log(`[ApiCapture] Saved to: ${fullPath}`);
|
|
136
|
+
}
|
|
137
|
+
} catch (err) {
|
|
138
|
+
console.error("[ApiCapture] Failed to save local JSON:", err.message);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------
|
|
143
|
+
// v1.x NEW: Batch flush — ส่ง HTTP เป็น batch ลด overhead
|
|
144
|
+
// ---------------------------------------------------------
|
|
145
|
+
_startBatchFlush() {
|
|
146
|
+
this._flushTimer = setInterval(() => {
|
|
147
|
+
this._flush();
|
|
148
|
+
}, this._batchInterval);
|
|
149
|
+
|
|
150
|
+
// ป้องกัน timer ค้างตอน process ปิด
|
|
151
|
+
if (this._flushTimer.unref) this._flushTimer.unref();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
_flush() {
|
|
155
|
+
if (this._queue.length === 0) return;
|
|
156
|
+
|
|
157
|
+
const batch = this._queue.splice(0, this._batchSize);
|
|
158
|
+
this._sendBatch(batch);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
_sendBatch(batch) {
|
|
162
|
+
const payload = JSON.stringify({ events: batch });
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const url = new URL(this.serverUrl);
|
|
166
|
+
const client = url.protocol === "https:" ? https : http;
|
|
167
|
+
|
|
168
|
+
const options = {
|
|
169
|
+
hostname: url.hostname,
|
|
170
|
+
port: url.port || (url.protocol === "https:" ? 443 : 80),
|
|
171
|
+
path: url.pathname,
|
|
172
|
+
method: "POST",
|
|
173
|
+
headers: {
|
|
174
|
+
"Content-Type": "application/json",
|
|
175
|
+
"Content-Length": Buffer.byteLength(payload),
|
|
176
|
+
"x-api-key": this.apiKey,
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const req = client.request(options, (res) => {
|
|
181
|
+
if (this.debug) {
|
|
182
|
+
let responseData = "";
|
|
183
|
+
res.on("data", (c) => (responseData += c));
|
|
184
|
+
res.on("end", () => {
|
|
185
|
+
if (res.statusCode >= 400) {
|
|
186
|
+
console.error(
|
|
187
|
+
`[ApiCapture] Server error (${res.statusCode}):`,
|
|
188
|
+
responseData,
|
|
189
|
+
);
|
|
190
|
+
} else {
|
|
191
|
+
console.log(
|
|
192
|
+
`[ApiCapture] Sent ${batch.length} event(s) (${res.statusCode})`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
} else {
|
|
197
|
+
res.on("data", () => {});
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
req.on("error", (err) => {
|
|
202
|
+
if (this.debug) {
|
|
203
|
+
console.error(
|
|
204
|
+
"[ApiCapture] Cannot reach monitor server:",
|
|
205
|
+
err.message,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
// v1.x: put failed events back in queue (simple retry)
|
|
209
|
+
this._queue.unshift(...batch);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
req.write(payload);
|
|
213
|
+
req.end();
|
|
214
|
+
} catch (err) {
|
|
215
|
+
if (this.debug) {
|
|
216
|
+
console.error("[ApiCapture] Invalid monitor URL:", err.message);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ---------------------------------------------------------
|
|
222
|
+
// capture() — core method
|
|
223
|
+
// ---------------------------------------------------------
|
|
224
|
+
capture(params) {
|
|
225
|
+
if (!this.enabled) return;
|
|
226
|
+
|
|
227
|
+
const {
|
|
228
|
+
routeName,
|
|
229
|
+
menus,
|
|
230
|
+
route,
|
|
231
|
+
method,
|
|
232
|
+
desc,
|
|
233
|
+
body,
|
|
234
|
+
query,
|
|
235
|
+
responseBody, // v1.x NEW
|
|
236
|
+
statusCode, // v1.x NEW
|
|
237
|
+
durationMs, // v1.x NEW
|
|
238
|
+
} = params;
|
|
239
|
+
|
|
240
|
+
const safeBody = this.maskSensitiveData(body);
|
|
241
|
+
const safeQuery = this.maskSensitiveData(query);
|
|
242
|
+
const safeResponse = this.maskSensitiveData(responseBody);
|
|
243
|
+
|
|
244
|
+
const payloadObj = {
|
|
245
|
+
appName: this.appName,
|
|
246
|
+
routeName: routeName || "unknown",
|
|
247
|
+
menus: Array.isArray(menus) ? menus : [],
|
|
248
|
+
route: route || "/",
|
|
249
|
+
method: (method || "GET").toUpperCase(),
|
|
250
|
+
desc: desc || "",
|
|
251
|
+
body: safeBody || null,
|
|
252
|
+
query: safeQuery || null,
|
|
253
|
+
response: safeResponse || null, // v1.x NEW
|
|
254
|
+
statusCode: statusCode || null, // v1.x NEW
|
|
255
|
+
durationMs: durationMs || null, // v1.x NEW
|
|
256
|
+
capturedAt: new Date().toISOString(),
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// Debug mode
|
|
260
|
+
if (this.debug) {
|
|
261
|
+
const status = statusCode ? ` [${statusCode}]` : "";
|
|
262
|
+
const dur = durationMs ? ` ${durationMs}ms` : "";
|
|
263
|
+
console.log(`\n--- [ApiCapture]${status}${dur} ---`);
|
|
264
|
+
console.log(`[${payloadObj.method}] ${payloadObj.route}`);
|
|
265
|
+
if (payloadObj.desc) console.log(`Desc: ${payloadObj.desc}`);
|
|
266
|
+
if (payloadObj.body)
|
|
267
|
+
console.log("Body:", JSON.stringify(payloadObj.body, null, 2));
|
|
268
|
+
if (payloadObj.query)
|
|
269
|
+
console.log("Query:", JSON.stringify(payloadObj.query, null, 2));
|
|
270
|
+
if (payloadObj.response)
|
|
271
|
+
console.log("Response:", JSON.stringify(payloadObj.response, null, 2));
|
|
272
|
+
console.log("---------------------------\n");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Async local save
|
|
276
|
+
if (this.saveLocal) {
|
|
277
|
+
this._saveLocalAsync(payloadObj);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Push to batch queue
|
|
281
|
+
if (this.serverUrl) {
|
|
282
|
+
this._queue.push(payloadObj);
|
|
283
|
+
|
|
284
|
+
// Flush immediately if batch is full
|
|
285
|
+
if (this._queue.length >= this._batchSize) {
|
|
286
|
+
this._flush();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ---------------------------------------------------------
|
|
292
|
+
// createMiddlewareFactory() — v1.x: now captures response + timing
|
|
293
|
+
// ---------------------------------------------------------
|
|
294
|
+
createMiddlewareFactory(routeName, routeEnabled = true) {
|
|
295
|
+
return {
|
|
296
|
+
capture: (desc = "", menus = []) => {
|
|
297
|
+
if (!routeEnabled || !this.enabled) {
|
|
298
|
+
return (req, res, next) => next();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const menuArray = Array.isArray(menus) ? menus : menus ? [menus] : [];
|
|
302
|
+
|
|
303
|
+
return (req, res, next) => {
|
|
304
|
+
const startTime = Date.now();
|
|
305
|
+
|
|
306
|
+
// v1.x: Patch res.json to intercept response body
|
|
307
|
+
const { getBody } = this._patchResponse(res);
|
|
308
|
+
|
|
309
|
+
res.on("finish", () => {
|
|
310
|
+
this.capture({
|
|
311
|
+
routeName,
|
|
312
|
+
menus: menuArray,
|
|
313
|
+
route: req.originalUrl || req.path,
|
|
314
|
+
method: req.method,
|
|
315
|
+
desc,
|
|
316
|
+
body: req.body,
|
|
317
|
+
query: req.query,
|
|
318
|
+
responseBody: getBody(), // v1.x NEW
|
|
319
|
+
statusCode: res.statusCode, // v1.x NEW
|
|
320
|
+
durationMs: Date.now() - startTime, // v1.x NEW
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
next();
|
|
325
|
+
};
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ---------------------------------------------------------
|
|
331
|
+
// flush() — manual flush ก่อน process ปิด
|
|
332
|
+
// ---------------------------------------------------------
|
|
333
|
+
async shutdown() {
|
|
334
|
+
if (this._flushTimer) clearInterval(this._flushTimer);
|
|
335
|
+
if (this._queue.length > 0) {
|
|
336
|
+
this._flush();
|
|
337
|
+
// รอนิดนึงให้ HTTP request ไปก่อน
|
|
338
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ---------------------------------------------------------
|
|
344
|
+
// Singleton
|
|
345
|
+
// ---------------------------------------------------------
|
|
346
|
+
let clientInstance = null;
|
|
347
|
+
|
|
348
|
+
const initApiCapture = (config = {}) => {
|
|
349
|
+
if (!clientInstance) {
|
|
350
|
+
clientInstance = new ApiCaptureClient(config);
|
|
351
|
+
} else {
|
|
352
|
+
if (config.monitorUrl) clientInstance.serverUrl = config.monitorUrl;
|
|
353
|
+
if (config.monitorApiKey) clientInstance.apiKey = config.monitorApiKey;
|
|
354
|
+
if (config.appName) clientInstance.appName = config.appName;
|
|
355
|
+
if (config.enabled !== undefined)
|
|
356
|
+
clientInstance.enabled = config.enabled !== false;
|
|
357
|
+
else if (config.openGen !== undefined)
|
|
358
|
+
clientInstance.enabled = config.openGen !== false;
|
|
359
|
+
if (config.debug !== undefined)
|
|
360
|
+
clientInstance.debug = config.debug === true;
|
|
361
|
+
if (config.saveLocal !== undefined)
|
|
362
|
+
clientInstance.saveLocal = config.saveLocal === true;
|
|
363
|
+
if (config.localPath) clientInstance.localPath = config.localPath;
|
|
364
|
+
if (config.sensitiveKeys) {
|
|
365
|
+
clientInstance.sensitiveKeys = [
|
|
366
|
+
"password",
|
|
367
|
+
"token",
|
|
368
|
+
"secret",
|
|
369
|
+
"creditcard",
|
|
370
|
+
"pin",
|
|
371
|
+
"auth",
|
|
372
|
+
"authorization",
|
|
373
|
+
"cookie",
|
|
374
|
+
...config.sensitiveKeys,
|
|
375
|
+
];
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return clientInstance;
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const getClient = () => {
|
|
382
|
+
if (!clientInstance) {
|
|
383
|
+
console.warn(
|
|
384
|
+
"[ApiCapture] Warning: getClient() called before initApiCapture(). Using defaults.",
|
|
385
|
+
);
|
|
386
|
+
return initApiCapture();
|
|
387
|
+
}
|
|
388
|
+
return clientInstance;
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
module.exports = {
|
|
392
|
+
ApiCaptureClient,
|
|
393
|
+
initApiCapture,
|
|
394
|
+
getClient,
|
|
395
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "apitrap",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Express middleware that captures API traffic and sends it to your monitor dashboard — turn real requests into living API documentation.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"index.js",
|
|
9
|
+
"index.d.ts",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"keywords": [
|
|
13
|
+
"express",
|
|
14
|
+
"api",
|
|
15
|
+
"capture",
|
|
16
|
+
"monitor",
|
|
17
|
+
"middleware",
|
|
18
|
+
"documentation",
|
|
19
|
+
"traffic",
|
|
20
|
+
"collection"
|
|
21
|
+
],
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/POND9912/apitrap"
|
|
25
|
+
},
|
|
26
|
+
"license": "ISC",
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=16.0.0"
|
|
29
|
+
}
|
|
30
|
+
}
|