request-scope-api 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +275 -0
- package/dist/cjs/adapters/adapter.interface.js +9 -0
- package/dist/cjs/adapters/adapter.interface.js.map +1 -0
- package/dist/cjs/adapters/mongo.adapter.js +188 -0
- package/dist/cjs/adapters/mongo.adapter.js.map +1 -0
- package/dist/cjs/adapters/mysql.adapter.js +243 -0
- package/dist/cjs/adapters/mysql.adapter.js.map +1 -0
- package/dist/cjs/adapters/pg.adapter.js +334 -0
- package/dist/cjs/adapters/pg.adapter.js.map +1 -0
- package/dist/cjs/capture.js +310 -0
- package/dist/cjs/capture.js.map +1 -0
- package/dist/cjs/config.js +122 -0
- package/dist/cjs/config.js.map +1 -0
- package/dist/cjs/dashboard/api.js +173 -0
- package/dist/cjs/dashboard/api.js.map +1 -0
- package/dist/cjs/dashboard/router.js +96 -0
- package/dist/cjs/dashboard/router.js.map +1 -0
- package/dist/cjs/index.js +49 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/masker.js +73 -0
- package/dist/cjs/masker.js.map +1 -0
- package/dist/cjs/middleware.js +198 -0
- package/dist/cjs/middleware.js.map +1 -0
- package/dist/cjs/queue.js +114 -0
- package/dist/cjs/queue.js.map +1 -0
- package/dist/cjs/retention.js +64 -0
- package/dist/cjs/retention.js.map +1 -0
- package/dist/cjs/types.js +9 -0
- package/dist/cjs/types.js.map +1 -0
- package/dist/dashboard/assets/index-C0TqFHk6.css +1 -0
- package/dist/dashboard/assets/index-MCuAZo4Q.js +67 -0
- package/dist/dashboard/index.html +13 -0
- package/dist/esm/adapters/adapter.interface.js +8 -0
- package/dist/esm/adapters/adapter.interface.js.map +1 -0
- package/dist/esm/adapters/mongo.adapter.js +184 -0
- package/dist/esm/adapters/mongo.adapter.js.map +1 -0
- package/dist/esm/adapters/mysql.adapter.js +236 -0
- package/dist/esm/adapters/mysql.adapter.js.map +1 -0
- package/dist/esm/adapters/pg.adapter.js +330 -0
- package/dist/esm/adapters/pg.adapter.js.map +1 -0
- package/dist/esm/capture.js +304 -0
- package/dist/esm/capture.js.map +1 -0
- package/dist/esm/config.js +117 -0
- package/dist/esm/config.js.map +1 -0
- package/dist/esm/dashboard/api.js +168 -0
- package/dist/esm/dashboard/api.js.map +1 -0
- package/dist/esm/dashboard/router.js +90 -0
- package/dist/esm/dashboard/router.js.map +1 -0
- package/dist/esm/index.js +50 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/masker.js +70 -0
- package/dist/esm/masker.js.map +1 -0
- package/dist/esm/middleware.js +193 -0
- package/dist/esm/middleware.js.map +1 -0
- package/dist/esm/queue.js +110 -0
- package/dist/esm/queue.js.map +1 -0
- package/dist/esm/retention.js +60 -0
- package/dist/esm/retention.js.map +1 -0
- package/dist/esm/types.js +8 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/types/adapters/adapter.interface.d.ts +7 -0
- package/dist/types/adapters/mongo.adapter.d.ts +25 -0
- package/dist/types/adapters/mysql.adapter.d.ts +24 -0
- package/dist/types/adapters/pg.adapter.d.ts +29 -0
- package/dist/types/capture.d.ts +88 -0
- package/dist/types/config.d.ts +38 -0
- package/dist/types/dashboard/api.d.ts +49 -0
- package/dist/types/dashboard/router.d.ts +28 -0
- package/dist/types/index.d.ts +31 -0
- package/dist/types/masker.d.ts +15 -0
- package/dist/types/middleware.d.ts +67 -0
- package/dist/types/queue.d.ts +49 -0
- package/dist/types/retention.d.ts +30 -0
- package/dist/types/types.d.ts +101 -0
- package/package.json +48 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PgAdapter — StorageAdapter implementation for PostgreSQL using pg.
|
|
3
|
+
*
|
|
4
|
+
* Compiles under `strict: true` with zero errors.
|
|
5
|
+
* Requirements: 1.6, 1.7, 6.1, 6.4, 6.5, 7.4
|
|
6
|
+
*/
|
|
7
|
+
import { Pool } from 'pg';
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Column name mapping: JS camelCase → SQL snake_case
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
const SORT_COLUMN_MAP = {
|
|
12
|
+
timestamp: 'timestamp',
|
|
13
|
+
responseTime: 'response_time',
|
|
14
|
+
statusCode: 'status_code',
|
|
15
|
+
};
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// DDL
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
const DDL_TABLE = `
|
|
20
|
+
CREATE TABLE IF NOT EXISTS requestscope_records (
|
|
21
|
+
id VARCHAR(36) NOT NULL PRIMARY KEY,
|
|
22
|
+
method VARCHAR(10) NOT NULL,
|
|
23
|
+
url TEXT NOT NULL,
|
|
24
|
+
route TEXT,
|
|
25
|
+
query_params JSONB NOT NULL,
|
|
26
|
+
path_params JSONB NOT NULL,
|
|
27
|
+
request_headers JSONB NOT NULL,
|
|
28
|
+
request_body TEXT NOT NULL,
|
|
29
|
+
request_body_size INTEGER NOT NULL,
|
|
30
|
+
client_ip VARCHAR(45) NOT NULL,
|
|
31
|
+
user_agent TEXT NOT NULL,
|
|
32
|
+
status_code SMALLINT NOT NULL,
|
|
33
|
+
response_headers JSONB NOT NULL,
|
|
34
|
+
response_body TEXT NOT NULL,
|
|
35
|
+
response_body_size INTEGER NOT NULL,
|
|
36
|
+
response_time REAL NOT NULL,
|
|
37
|
+
error_message TEXT,
|
|
38
|
+
error_stack TEXT,
|
|
39
|
+
error_status_code SMALLINT,
|
|
40
|
+
timestamp VARCHAR(30) NOT NULL
|
|
41
|
+
)
|
|
42
|
+
`.trim();
|
|
43
|
+
const DDL_IDX_TIMESTAMP = `CREATE INDEX IF NOT EXISTS idx_rs_timestamp ON requestscope_records (timestamp DESC)`;
|
|
44
|
+
const DDL_IDX_STATUS = `CREATE INDEX IF NOT EXISTS idx_rs_status ON requestscope_records (status_code)`;
|
|
45
|
+
const DDL_IDX_METHOD = `CREATE INDEX IF NOT EXISTS idx_rs_method ON requestscope_records (method)`;
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Helpers
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
/**
|
|
50
|
+
* Ensure a JSONB column value (which pg already parses to an object) is
|
|
51
|
+
* returned as `Record<string, string>`. Falls back to `{}` on any error.
|
|
52
|
+
*/
|
|
53
|
+
function parseJsonb(value) {
|
|
54
|
+
if (value === null || value === undefined)
|
|
55
|
+
return {};
|
|
56
|
+
if (typeof value === 'object')
|
|
57
|
+
return value;
|
|
58
|
+
if (typeof value === 'string') {
|
|
59
|
+
try {
|
|
60
|
+
return JSON.parse(value);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return {};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return {};
|
|
67
|
+
}
|
|
68
|
+
/** Map a database row back to a `RequestRecord`. */
|
|
69
|
+
function rowToRecord(row) {
|
|
70
|
+
return {
|
|
71
|
+
id: row.id,
|
|
72
|
+
method: row.method,
|
|
73
|
+
url: row.url,
|
|
74
|
+
route: row.route,
|
|
75
|
+
queryParams: parseJsonb(row.query_params),
|
|
76
|
+
pathParams: parseJsonb(row.path_params),
|
|
77
|
+
requestHeaders: parseJsonb(row.request_headers),
|
|
78
|
+
requestBody: row.request_body,
|
|
79
|
+
requestBodySize: Number(row.request_body_size),
|
|
80
|
+
clientIp: row.client_ip,
|
|
81
|
+
userAgent: row.user_agent,
|
|
82
|
+
statusCode: Number(row.status_code),
|
|
83
|
+
responseHeaders: parseJsonb(row.response_headers),
|
|
84
|
+
responseBody: row.response_body,
|
|
85
|
+
responseBodySize: Number(row.response_body_size),
|
|
86
|
+
responseTime: Number(row.response_time),
|
|
87
|
+
errorMessage: row.error_message,
|
|
88
|
+
errorStack: row.error_stack,
|
|
89
|
+
errorStatusCode: row.error_status_code !== null ? Number(row.error_status_code) : null,
|
|
90
|
+
timestamp: row.timestamp,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/** Resolve the `statusCodeGroup` filter to a `[min, max)` pair. */
|
|
94
|
+
function statusGroupRange(group) {
|
|
95
|
+
const base = parseInt(group[0], 10) * 100;
|
|
96
|
+
return [base, base + 100];
|
|
97
|
+
}
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// PgAdapter
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
export class PgAdapter {
|
|
102
|
+
constructor(config) {
|
|
103
|
+
this.pool = null;
|
|
104
|
+
this.config = config;
|
|
105
|
+
}
|
|
106
|
+
// -------------------------------------------------------------------------
|
|
107
|
+
// initialize
|
|
108
|
+
// -------------------------------------------------------------------------
|
|
109
|
+
async initialize() {
|
|
110
|
+
const poolConfig = {
|
|
111
|
+
host: this.config.host ?? 'localhost',
|
|
112
|
+
port: this.config.port ?? 5432,
|
|
113
|
+
database: this.config.database,
|
|
114
|
+
user: this.config.username,
|
|
115
|
+
password: this.config.password,
|
|
116
|
+
max: this.config.poolSize ?? 5,
|
|
117
|
+
ssl: this.config.ssl ? { rejectUnauthorized: true } : undefined,
|
|
118
|
+
};
|
|
119
|
+
this.pool = new Pool(poolConfig);
|
|
120
|
+
const client = await this.pool.connect();
|
|
121
|
+
try {
|
|
122
|
+
await client.query(DDL_TABLE);
|
|
123
|
+
await client.query(DDL_IDX_TIMESTAMP);
|
|
124
|
+
await client.query(DDL_IDX_STATUS);
|
|
125
|
+
await client.query(DDL_IDX_METHOD);
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
client.release();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// -------------------------------------------------------------------------
|
|
132
|
+
// insert
|
|
133
|
+
// -------------------------------------------------------------------------
|
|
134
|
+
/**
|
|
135
|
+
* Batch-inserts records using unnest arrays for efficient multi-row inserts.
|
|
136
|
+
*
|
|
137
|
+
* INSERT INTO requestscope_records (col1, col2, ...)
|
|
138
|
+
* SELECT * FROM unnest($1::varchar[], $2::text[], ...)
|
|
139
|
+
*/
|
|
140
|
+
async insert(records) {
|
|
141
|
+
if (records.length === 0)
|
|
142
|
+
return;
|
|
143
|
+
const pool = this.getPool();
|
|
144
|
+
// Collect per-column arrays
|
|
145
|
+
const ids = [];
|
|
146
|
+
const methods = [];
|
|
147
|
+
const urls = [];
|
|
148
|
+
const routes = [];
|
|
149
|
+
const queryParams = [];
|
|
150
|
+
const pathParams = [];
|
|
151
|
+
const requestHeaders = [];
|
|
152
|
+
const requestBodies = [];
|
|
153
|
+
const requestBodySizes = [];
|
|
154
|
+
const clientIps = [];
|
|
155
|
+
const userAgents = [];
|
|
156
|
+
const statusCodes = [];
|
|
157
|
+
const responseHeaders = [];
|
|
158
|
+
const responseBodies = [];
|
|
159
|
+
const responseBodySizes = [];
|
|
160
|
+
const responseTimes = [];
|
|
161
|
+
const errorMessages = [];
|
|
162
|
+
const errorStacks = [];
|
|
163
|
+
const errorStatusCodes = [];
|
|
164
|
+
const timestamps = [];
|
|
165
|
+
for (const r of records) {
|
|
166
|
+
ids.push(r.id);
|
|
167
|
+
methods.push(r.method);
|
|
168
|
+
urls.push(r.url);
|
|
169
|
+
routes.push(r.route);
|
|
170
|
+
queryParams.push(JSON.stringify(r.queryParams));
|
|
171
|
+
pathParams.push(JSON.stringify(r.pathParams));
|
|
172
|
+
requestHeaders.push(JSON.stringify(r.requestHeaders));
|
|
173
|
+
requestBodies.push(r.requestBody);
|
|
174
|
+
requestBodySizes.push(r.requestBodySize);
|
|
175
|
+
clientIps.push(r.clientIp);
|
|
176
|
+
userAgents.push(r.userAgent);
|
|
177
|
+
statusCodes.push(r.statusCode);
|
|
178
|
+
responseHeaders.push(JSON.stringify(r.responseHeaders));
|
|
179
|
+
responseBodies.push(r.responseBody);
|
|
180
|
+
responseBodySizes.push(r.responseBodySize);
|
|
181
|
+
responseTimes.push(r.responseTime);
|
|
182
|
+
errorMessages.push(r.errorMessage);
|
|
183
|
+
errorStacks.push(r.errorStack);
|
|
184
|
+
errorStatusCodes.push(r.errorStatusCode);
|
|
185
|
+
timestamps.push(r.timestamp);
|
|
186
|
+
}
|
|
187
|
+
const sql = `
|
|
188
|
+
INSERT INTO requestscope_records (
|
|
189
|
+
id, method, url, route,
|
|
190
|
+
query_params, path_params, request_headers,
|
|
191
|
+
request_body, request_body_size,
|
|
192
|
+
client_ip, user_agent,
|
|
193
|
+
status_code, response_headers,
|
|
194
|
+
response_body, response_body_size,
|
|
195
|
+
response_time,
|
|
196
|
+
error_message, error_stack, error_status_code,
|
|
197
|
+
timestamp
|
|
198
|
+
)
|
|
199
|
+
SELECT * FROM unnest(
|
|
200
|
+
$1::varchar[],
|
|
201
|
+
$2::varchar[],
|
|
202
|
+
$3::text[],
|
|
203
|
+
$4::text[],
|
|
204
|
+
$5::jsonb[],
|
|
205
|
+
$6::jsonb[],
|
|
206
|
+
$7::jsonb[],
|
|
207
|
+
$8::text[],
|
|
208
|
+
$9::integer[],
|
|
209
|
+
$10::varchar[],
|
|
210
|
+
$11::text[],
|
|
211
|
+
$12::smallint[],
|
|
212
|
+
$13::jsonb[],
|
|
213
|
+
$14::text[],
|
|
214
|
+
$15::integer[],
|
|
215
|
+
$16::real[],
|
|
216
|
+
$17::text[],
|
|
217
|
+
$18::text[],
|
|
218
|
+
$19::smallint[],
|
|
219
|
+
$20::varchar[]
|
|
220
|
+
)
|
|
221
|
+
`.trim();
|
|
222
|
+
await pool.query(sql, [
|
|
223
|
+
ids,
|
|
224
|
+
methods,
|
|
225
|
+
urls,
|
|
226
|
+
routes,
|
|
227
|
+
queryParams,
|
|
228
|
+
pathParams,
|
|
229
|
+
requestHeaders,
|
|
230
|
+
requestBodies,
|
|
231
|
+
requestBodySizes,
|
|
232
|
+
clientIps,
|
|
233
|
+
userAgents,
|
|
234
|
+
statusCodes,
|
|
235
|
+
responseHeaders,
|
|
236
|
+
responseBodies,
|
|
237
|
+
responseBodySizes,
|
|
238
|
+
responseTimes,
|
|
239
|
+
errorMessages,
|
|
240
|
+
errorStacks,
|
|
241
|
+
errorStatusCodes,
|
|
242
|
+
timestamps,
|
|
243
|
+
]);
|
|
244
|
+
}
|
|
245
|
+
// -------------------------------------------------------------------------
|
|
246
|
+
// query
|
|
247
|
+
// -------------------------------------------------------------------------
|
|
248
|
+
async query(filters, pagination) {
|
|
249
|
+
const pool = this.getPool();
|
|
250
|
+
const conditions = [];
|
|
251
|
+
const params = [];
|
|
252
|
+
let paramIdx = 1;
|
|
253
|
+
// Helper to add a parameter and return its $N placeholder
|
|
254
|
+
const addParam = (value) => {
|
|
255
|
+
params.push(value);
|
|
256
|
+
return `$${paramIdx++}`;
|
|
257
|
+
};
|
|
258
|
+
// id — exact match
|
|
259
|
+
if (filters.id !== undefined) {
|
|
260
|
+
conditions.push(`id = ${addParam(filters.id)}`);
|
|
261
|
+
}
|
|
262
|
+
// search — url ILIKE $N
|
|
263
|
+
if (filters.search !== undefined) {
|
|
264
|
+
conditions.push(`url ILIKE ${addParam(`%${filters.search}%`)}`);
|
|
265
|
+
}
|
|
266
|
+
// startDate — timestamp >= $N
|
|
267
|
+
if (filters.startDate !== undefined) {
|
|
268
|
+
conditions.push(`timestamp >= ${addParam(filters.startDate)}`);
|
|
269
|
+
}
|
|
270
|
+
// endDate — timestamp <= $N
|
|
271
|
+
if (filters.endDate !== undefined) {
|
|
272
|
+
conditions.push(`timestamp <= ${addParam(filters.endDate)}`);
|
|
273
|
+
}
|
|
274
|
+
// statusCode (exact, takes precedence over group)
|
|
275
|
+
if (filters.statusCode !== undefined) {
|
|
276
|
+
conditions.push(`status_code = ${addParam(filters.statusCode)}`);
|
|
277
|
+
}
|
|
278
|
+
else if (filters.statusCodeGroup !== undefined) {
|
|
279
|
+
const [min, max] = statusGroupRange(filters.statusCodeGroup);
|
|
280
|
+
conditions.push(`status_code >= ${addParam(min)} AND status_code < ${addParam(max)}`);
|
|
281
|
+
}
|
|
282
|
+
// method — case-insensitive
|
|
283
|
+
if (filters.method !== undefined) {
|
|
284
|
+
conditions.push(`LOWER(method) = LOWER(${addParam(filters.method)})`);
|
|
285
|
+
}
|
|
286
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
287
|
+
// Sort
|
|
288
|
+
const sortColRaw = filters.sortBy ?? 'timestamp';
|
|
289
|
+
const sortCol = SORT_COLUMN_MAP[sortColRaw] ?? 'timestamp';
|
|
290
|
+
const sortDir = (filters.sortOrder ?? 'desc').toUpperCase() === 'ASC' ? 'ASC' : 'DESC';
|
|
291
|
+
const orderClause = `ORDER BY ${sortCol} ${sortDir}`;
|
|
292
|
+
// COUNT query (reuses same params)
|
|
293
|
+
const countSql = `SELECT COUNT(*) AS total FROM requestscope_records ${whereClause}`;
|
|
294
|
+
const countResult = await pool.query(countSql, params);
|
|
295
|
+
const total = Number(countResult.rows[0]?.total ?? 0);
|
|
296
|
+
// Page slice — add LIMIT and OFFSET as the next params
|
|
297
|
+
const { page, pageSize } = pagination;
|
|
298
|
+
const offset = (Math.max(1, page) - 1) * pageSize;
|
|
299
|
+
const limitPlaceholder = addParam(pageSize);
|
|
300
|
+
const offsetPlaceholder = addParam(offset);
|
|
301
|
+
const dataSql = `
|
|
302
|
+
SELECT * FROM requestscope_records
|
|
303
|
+
${whereClause}
|
|
304
|
+
${orderClause}
|
|
305
|
+
LIMIT ${limitPlaceholder} OFFSET ${offsetPlaceholder}
|
|
306
|
+
`.trim();
|
|
307
|
+
const dataResult = await pool.query(dataSql, params);
|
|
308
|
+
const records = dataResult.rows.map(rowToRecord);
|
|
309
|
+
return { records, total };
|
|
310
|
+
}
|
|
311
|
+
// -------------------------------------------------------------------------
|
|
312
|
+
// deleteOlderThan
|
|
313
|
+
// -------------------------------------------------------------------------
|
|
314
|
+
async deleteOlderThan(date) {
|
|
315
|
+
const pool = this.getPool();
|
|
316
|
+
const iso = date.toISOString();
|
|
317
|
+
const result = await pool.query('DELETE FROM requestscope_records WHERE timestamp < $1', [iso]);
|
|
318
|
+
return result.rowCount ?? 0;
|
|
319
|
+
}
|
|
320
|
+
// -------------------------------------------------------------------------
|
|
321
|
+
// Private helpers
|
|
322
|
+
// -------------------------------------------------------------------------
|
|
323
|
+
getPool() {
|
|
324
|
+
if (this.pool === null) {
|
|
325
|
+
throw new Error('PgAdapter: call initialize() before using the adapter');
|
|
326
|
+
}
|
|
327
|
+
return this.pool;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
//# sourceMappingURL=pg.adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pg.adapter.js","sourceRoot":"","sources":["../../../src/adapters/pg.adapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RequestScope — Capture layer, request and response sides.
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - parseClientIp(req) — extract client IP from headers/socket
|
|
6
|
+
* - bufferRequestBody(req) — consume request stream, buffer ≤50 KB,
|
|
7
|
+
* attempt JSON / URL-encoded parse, return
|
|
8
|
+
* body string + original byte size + client IP.
|
|
9
|
+
* - wrapResponse(res) — monkey-patch res.write/res.end to accumulate
|
|
10
|
+
* response chunks, cap at 100 KB, return wrapper
|
|
11
|
+
* with finish event handler that builds RequestRecord.
|
|
12
|
+
*
|
|
13
|
+
* Uses Node.js built-ins only (no external deps).
|
|
14
|
+
* Compiles under strict: true.
|
|
15
|
+
*/
|
|
16
|
+
import { randomUUID } from 'node:crypto';
|
|
17
|
+
import { maskObject } from './masker';
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Constants
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
/** Maximum bytes to retain from a request body before truncation. */
|
|
22
|
+
const MAX_BODY_BYTES = 50000;
|
|
23
|
+
/** Maximum bytes to retain from a response body before truncation. */
|
|
24
|
+
const MAX_RESPONSE_BODY_BYTES = 100000;
|
|
25
|
+
/** Suffix appended to a body that was truncated. */
|
|
26
|
+
const TRUNCATED_SUFFIX = '[truncated]';
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Client IP extraction
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
/**
|
|
31
|
+
* Extracts the client IP address from the request.
|
|
32
|
+
*
|
|
33
|
+
* Prefers the first entry of the `X-Forwarded-For` header (proxy-aware),
|
|
34
|
+
* falling back to `req.socket.remoteAddress`. Returns `"0.0.0.0"` when
|
|
35
|
+
* neither is available.
|
|
36
|
+
*
|
|
37
|
+
* @param req Node.js IncomingMessage (or Express Request)
|
|
38
|
+
*/
|
|
39
|
+
export function parseClientIp(req) {
|
|
40
|
+
const xff = req.headers['x-forwarded-for'];
|
|
41
|
+
if (xff) {
|
|
42
|
+
const raw = Array.isArray(xff) ? xff[0] : xff;
|
|
43
|
+
// X-Forwarded-For may be comma-separated: "client, proxy1, proxy2"
|
|
44
|
+
const first = raw.split(',')[0].trim();
|
|
45
|
+
if (first) {
|
|
46
|
+
return first;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return req.socket?.remoteAddress ?? '0.0.0.0';
|
|
50
|
+
}
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// URL-encoded body parsing helper
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
/**
|
|
55
|
+
* Attempts to parse `raw` as a URL-encoded query string.
|
|
56
|
+
*
|
|
57
|
+
* Returns a JSON string of the key-value pairs on success, or `null` when
|
|
58
|
+
* the string contains no parseable key-value pairs.
|
|
59
|
+
*/
|
|
60
|
+
function tryParseUrlEncoded(raw) {
|
|
61
|
+
// A URL-encoded body must contain at least one '=' (key=value separator).
|
|
62
|
+
// Without this guard, URLSearchParams treats any bare string as a key with
|
|
63
|
+
// an empty value, producing false positives for plain text / XML bodies.
|
|
64
|
+
if (!raw.includes('=')) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const params = new URLSearchParams(raw);
|
|
69
|
+
const obj = {};
|
|
70
|
+
params.forEach((value, key) => {
|
|
71
|
+
obj[key] = value;
|
|
72
|
+
});
|
|
73
|
+
// Only treat as URL-encoded if we got at least one key-value pair.
|
|
74
|
+
if (Object.keys(obj).length === 0) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return JSON.stringify(obj);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Request body buffering
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
/**
|
|
87
|
+
* Consumes the `req` stream and buffers up to {@link MAX_BODY_BYTES} bytes.
|
|
88
|
+
*
|
|
89
|
+
* Behaviour:
|
|
90
|
+
* 1. Collects `Buffer` chunks from `data` events; tracks total byte length.
|
|
91
|
+
* 2. On `end`: concatenates chunks, slices to MAX_BODY_BYTES; appends
|
|
92
|
+
* `"[truncated]"` when the original size exceeded the limit.
|
|
93
|
+
* 3. Attempts `JSON.parse` → stores normalised JSON string.
|
|
94
|
+
* 4. On JSON failure, attempts `URLSearchParams` parse → stores JSON map.
|
|
95
|
+
* 5. On both failures, stores the raw string as-is.
|
|
96
|
+
* 6. On stream `error`, resolves with `{ body: '', bodySize: 0, clientIp }`.
|
|
97
|
+
*
|
|
98
|
+
* @param req Node.js IncomingMessage (or Express Request)
|
|
99
|
+
* @returns Resolved promise with body string, original byte size, and IP.
|
|
100
|
+
*/
|
|
101
|
+
export async function bufferRequestBody(req) {
|
|
102
|
+
const clientIp = parseClientIp(req);
|
|
103
|
+
return new Promise((resolve) => {
|
|
104
|
+
const chunks = [];
|
|
105
|
+
let totalBytes = 0;
|
|
106
|
+
req.on('data', (chunk) => {
|
|
107
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
108
|
+
totalBytes += buf.byteLength;
|
|
109
|
+
chunks.push(buf);
|
|
110
|
+
});
|
|
111
|
+
req.on('end', () => {
|
|
112
|
+
// Concatenate all chunks into one buffer, then slice to the limit.
|
|
113
|
+
const full = Buffer.concat(chunks);
|
|
114
|
+
const truncated = totalBytes > MAX_BODY_BYTES;
|
|
115
|
+
const kept = truncated ? full.subarray(0, MAX_BODY_BYTES) : full;
|
|
116
|
+
let rawString = kept.toString('utf8');
|
|
117
|
+
if (truncated) {
|
|
118
|
+
rawString += TRUNCATED_SUFFIX;
|
|
119
|
+
}
|
|
120
|
+
// Try JSON first.
|
|
121
|
+
try {
|
|
122
|
+
const parsed = JSON.parse(rawString.replace(TRUNCATED_SUFFIX, ''));
|
|
123
|
+
// Store as normalised JSON string (handles whitespace variation).
|
|
124
|
+
const base = JSON.stringify(parsed);
|
|
125
|
+
resolve({
|
|
126
|
+
body: truncated ? base + TRUNCATED_SUFFIX : base,
|
|
127
|
+
bodySize: totalBytes,
|
|
128
|
+
clientIp,
|
|
129
|
+
});
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// JSON parse failed — try URL-encoded next (only on untruncated or
|
|
134
|
+
// the raw portion; URLSearchParams on a truncated string may be
|
|
135
|
+
// structurally invalid, but we try on the raw slice).
|
|
136
|
+
}
|
|
137
|
+
// Try URL-encoded.
|
|
138
|
+
const rawForUrlParse = truncated
|
|
139
|
+
? full.subarray(0, MAX_BODY_BYTES).toString('utf8')
|
|
140
|
+
: rawString;
|
|
141
|
+
const urlEncoded = tryParseUrlEncoded(rawForUrlParse);
|
|
142
|
+
if (urlEncoded !== null) {
|
|
143
|
+
resolve({
|
|
144
|
+
body: truncated ? urlEncoded + TRUNCATED_SUFFIX : urlEncoded,
|
|
145
|
+
bodySize: totalBytes,
|
|
146
|
+
clientIp,
|
|
147
|
+
});
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
// Fall back to raw string (possibly truncated).
|
|
151
|
+
resolve({ body: rawString, bodySize: totalBytes, clientIp });
|
|
152
|
+
});
|
|
153
|
+
req.on('error', () => {
|
|
154
|
+
resolve({ body: '', bodySize: 0, clientIp });
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// Response body wrapping
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
/**
|
|
162
|
+
* Symbol used to attach error data to the request object for the finish handler.
|
|
163
|
+
*/
|
|
164
|
+
export const ERROR_DATA_SYMBOL = Symbol('requestscope_error_data');
|
|
165
|
+
/**
|
|
166
|
+
* Wraps a ServerResponse to accumulate response chunks.
|
|
167
|
+
*
|
|
168
|
+
* Monkey-patches `res.write` and `res.end` to accumulate response body chunks.
|
|
169
|
+
* Caps the buffer at MAX_RESPONSE_BODY_BYTES; beyond that stores first
|
|
170
|
+
* MAX_RESPONSE_BODY_BYTES bytes + "[truncated]". Records responseBodySize as
|
|
171
|
+
* the original Content-Length or accumulated size.
|
|
172
|
+
*
|
|
173
|
+
* On the `finish` event, builds a RequestRecord with:
|
|
174
|
+
* - UUIDv4 id
|
|
175
|
+
* - process.hrtime-based responseTime
|
|
176
|
+
* - maskObject() applied to request headers and request body
|
|
177
|
+
* - maskObject() applied to response headers
|
|
178
|
+
* - Enqueues the record synchronously via the provided callback
|
|
179
|
+
*
|
|
180
|
+
* If wrapping fails (stream/binary response), sets responseBody = "",
|
|
181
|
+
* reads Content-Length header as responseBodySize, and calls original write/end
|
|
182
|
+
* without disrupting the response.
|
|
183
|
+
*
|
|
184
|
+
* @param res - Node.js ServerResponse (or Express Response)
|
|
185
|
+
* @param req - Node.js IncomingMessage (or Express Request)
|
|
186
|
+
* @param requestBody - Buffered request body string
|
|
187
|
+
* @param requestBodySize - Original request body byte size
|
|
188
|
+
* @param clientIp - Client IP address
|
|
189
|
+
* @param sensitiveFields - Set of sensitive field names to mask
|
|
190
|
+
* @param enqueueRecord - Callback to enqueue the RequestRecord synchronously
|
|
191
|
+
* @returns The wrapped response (same object, with patched methods)
|
|
192
|
+
*/
|
|
193
|
+
export function wrapResponse(res, req, requestBody, requestBodySize, clientIp, sensitiveFields, enqueueRecord) {
|
|
194
|
+
const chunks = [];
|
|
195
|
+
let totalBytes = 0;
|
|
196
|
+
const startTime = process.hrtime.bigint();
|
|
197
|
+
// Store original methods
|
|
198
|
+
const originalWrite = res.write;
|
|
199
|
+
const originalEnd = res.end;
|
|
200
|
+
// Helper to accumulate chunks
|
|
201
|
+
const accumulateChunk = (chunk, encoding) => {
|
|
202
|
+
try {
|
|
203
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding);
|
|
204
|
+
totalBytes += buf.byteLength;
|
|
205
|
+
if (totalBytes <= MAX_RESPONSE_BODY_BYTES) {
|
|
206
|
+
chunks.push(buf);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
// Ignore accumulation errors
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
// Patch res.write
|
|
214
|
+
res.write = function (chunk, encoding, cb) {
|
|
215
|
+
accumulateChunk(chunk, encoding);
|
|
216
|
+
return originalWrite.call(this, chunk, encoding, cb);
|
|
217
|
+
};
|
|
218
|
+
// Patch res.end
|
|
219
|
+
res.end = function (chunk, encoding, cb) {
|
|
220
|
+
if (chunk !== undefined) {
|
|
221
|
+
accumulateChunk(chunk, encoding);
|
|
222
|
+
}
|
|
223
|
+
originalEnd.call(this, chunk, encoding, cb);
|
|
224
|
+
};
|
|
225
|
+
// Patch Express-specific methods that might bypass res.write/res.end
|
|
226
|
+
const expressRes = res;
|
|
227
|
+
if (expressRes.send) {
|
|
228
|
+
const originalSend = expressRes.send;
|
|
229
|
+
expressRes.send = function (body) {
|
|
230
|
+
// Capture the body before sending
|
|
231
|
+
if (body !== undefined) {
|
|
232
|
+
const str = typeof body === 'string' ? body : JSON.stringify(body);
|
|
233
|
+
accumulateChunk(str, 'utf8');
|
|
234
|
+
}
|
|
235
|
+
return originalSend.call(this, body);
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
if (expressRes.json) {
|
|
239
|
+
const originalJson = expressRes.json;
|
|
240
|
+
expressRes.json = function (obj) {
|
|
241
|
+
const str = JSON.stringify(obj);
|
|
242
|
+
accumulateChunk(str, 'utf8');
|
|
243
|
+
return originalJson.call(this, obj);
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
// Listen for finish event to build the record
|
|
247
|
+
res.on('finish', () => {
|
|
248
|
+
const endTime = process.hrtime.bigint();
|
|
249
|
+
const responseTime = Number(endTime - startTime) / 1000000; // Convert to milliseconds
|
|
250
|
+
// Build response body
|
|
251
|
+
let responseBody = '';
|
|
252
|
+
let responseBodySize = totalBytes;
|
|
253
|
+
if (chunks.length > 0) {
|
|
254
|
+
const full = Buffer.concat(chunks);
|
|
255
|
+
const truncated = totalBytes > MAX_RESPONSE_BODY_BYTES;
|
|
256
|
+
const kept = truncated ? full.subarray(0, MAX_RESPONSE_BODY_BYTES) : full;
|
|
257
|
+
responseBody = kept.toString('utf8');
|
|
258
|
+
if (truncated) {
|
|
259
|
+
responseBody += TRUNCATED_SUFFIX;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
// No chunks accumulated - try to get size from Content-Length header
|
|
264
|
+
const contentLength = res.getHeader('Content-Length');
|
|
265
|
+
if (typeof contentLength === 'number') {
|
|
266
|
+
responseBodySize = contentLength;
|
|
267
|
+
}
|
|
268
|
+
else if (typeof contentLength === 'string') {
|
|
269
|
+
responseBodySize = parseInt(contentLength, 10) || 0;
|
|
270
|
+
}
|
|
271
|
+
// Log when no chunks are accumulated for debugging
|
|
272
|
+
process.stderr.write(`[RequestScope] No response chunks accumulated for ${req.method} ${req.url}\n`);
|
|
273
|
+
}
|
|
274
|
+
// Get error data from request if present
|
|
275
|
+
const errorData = req[ERROR_DATA_SYMBOL];
|
|
276
|
+
// Build request record
|
|
277
|
+
const record = {
|
|
278
|
+
id: randomUUID(),
|
|
279
|
+
method: req.method || 'GET',
|
|
280
|
+
url: req.url || '/',
|
|
281
|
+
route: null, // Will be set by Express if available
|
|
282
|
+
queryParams: {}, // Will be populated by Express
|
|
283
|
+
pathParams: {}, // Will be populated by Express
|
|
284
|
+
requestHeaders: maskObject(Object.fromEntries(Object.entries(req.headers).map(([k, v]) => [k, Array.isArray(v) ? v[0] : v ?? ''])), sensitiveFields),
|
|
285
|
+
requestBody,
|
|
286
|
+
requestBodySize,
|
|
287
|
+
clientIp,
|
|
288
|
+
userAgent: req.headers['user-agent'] || '',
|
|
289
|
+
statusCode: res.statusCode,
|
|
290
|
+
responseHeaders: maskObject(Object.fromEntries(Object.entries(res.getHeaders()).map(([k, v]) => [k, Array.isArray(v) ? v[0] : v ?? ''])), sensitiveFields),
|
|
291
|
+
responseBody,
|
|
292
|
+
responseBodySize,
|
|
293
|
+
responseTime,
|
|
294
|
+
errorMessage: errorData?.message ?? null,
|
|
295
|
+
errorStack: errorData?.stack ?? null,
|
|
296
|
+
errorStatusCode: errorData?.statusCode ?? null,
|
|
297
|
+
timestamp: new Date().toISOString(),
|
|
298
|
+
};
|
|
299
|
+
// Enqueue the record synchronously
|
|
300
|
+
enqueueRecord(record);
|
|
301
|
+
});
|
|
302
|
+
return res;
|
|
303
|
+
}
|
|
304
|
+
//# sourceMappingURL=capture.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capture.js","sourceRoot":"","sources":["../../src/capture.ts"],"names":[],"mappings":"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"}
|