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,310 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* RequestScope — Capture layer, request and response sides.
|
|
4
|
+
*
|
|
5
|
+
* Provides:
|
|
6
|
+
* - parseClientIp(req) — extract client IP from headers/socket
|
|
7
|
+
* - bufferRequestBody(req) — consume request stream, buffer ≤50 KB,
|
|
8
|
+
* attempt JSON / URL-encoded parse, return
|
|
9
|
+
* body string + original byte size + client IP.
|
|
10
|
+
* - wrapResponse(res) — monkey-patch res.write/res.end to accumulate
|
|
11
|
+
* response chunks, cap at 100 KB, return wrapper
|
|
12
|
+
* with finish event handler that builds RequestRecord.
|
|
13
|
+
*
|
|
14
|
+
* Uses Node.js built-ins only (no external deps).
|
|
15
|
+
* Compiles under strict: true.
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.ERROR_DATA_SYMBOL = void 0;
|
|
19
|
+
exports.parseClientIp = parseClientIp;
|
|
20
|
+
exports.bufferRequestBody = bufferRequestBody;
|
|
21
|
+
exports.wrapResponse = wrapResponse;
|
|
22
|
+
const node_crypto_1 = require("node:crypto");
|
|
23
|
+
const masker_1 = require("./masker");
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Constants
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
/** Maximum bytes to retain from a request body before truncation. */
|
|
28
|
+
const MAX_BODY_BYTES = 50000;
|
|
29
|
+
/** Maximum bytes to retain from a response body before truncation. */
|
|
30
|
+
const MAX_RESPONSE_BODY_BYTES = 100000;
|
|
31
|
+
/** Suffix appended to a body that was truncated. */
|
|
32
|
+
const TRUNCATED_SUFFIX = '[truncated]';
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Client IP extraction
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
/**
|
|
37
|
+
* Extracts the client IP address from the request.
|
|
38
|
+
*
|
|
39
|
+
* Prefers the first entry of the `X-Forwarded-For` header (proxy-aware),
|
|
40
|
+
* falling back to `req.socket.remoteAddress`. Returns `"0.0.0.0"` when
|
|
41
|
+
* neither is available.
|
|
42
|
+
*
|
|
43
|
+
* @param req Node.js IncomingMessage (or Express Request)
|
|
44
|
+
*/
|
|
45
|
+
function parseClientIp(req) {
|
|
46
|
+
const xff = req.headers['x-forwarded-for'];
|
|
47
|
+
if (xff) {
|
|
48
|
+
const raw = Array.isArray(xff) ? xff[0] : xff;
|
|
49
|
+
// X-Forwarded-For may be comma-separated: "client, proxy1, proxy2"
|
|
50
|
+
const first = raw.split(',')[0].trim();
|
|
51
|
+
if (first) {
|
|
52
|
+
return first;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return req.socket?.remoteAddress ?? '0.0.0.0';
|
|
56
|
+
}
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// URL-encoded body parsing helper
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
/**
|
|
61
|
+
* Attempts to parse `raw` as a URL-encoded query string.
|
|
62
|
+
*
|
|
63
|
+
* Returns a JSON string of the key-value pairs on success, or `null` when
|
|
64
|
+
* the string contains no parseable key-value pairs.
|
|
65
|
+
*/
|
|
66
|
+
function tryParseUrlEncoded(raw) {
|
|
67
|
+
// A URL-encoded body must contain at least one '=' (key=value separator).
|
|
68
|
+
// Without this guard, URLSearchParams treats any bare string as a key with
|
|
69
|
+
// an empty value, producing false positives for plain text / XML bodies.
|
|
70
|
+
if (!raw.includes('=')) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const params = new URLSearchParams(raw);
|
|
75
|
+
const obj = {};
|
|
76
|
+
params.forEach((value, key) => {
|
|
77
|
+
obj[key] = value;
|
|
78
|
+
});
|
|
79
|
+
// Only treat as URL-encoded if we got at least one key-value pair.
|
|
80
|
+
if (Object.keys(obj).length === 0) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
return JSON.stringify(obj);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Request body buffering
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
/**
|
|
93
|
+
* Consumes the `req` stream and buffers up to {@link MAX_BODY_BYTES} bytes.
|
|
94
|
+
*
|
|
95
|
+
* Behaviour:
|
|
96
|
+
* 1. Collects `Buffer` chunks from `data` events; tracks total byte length.
|
|
97
|
+
* 2. On `end`: concatenates chunks, slices to MAX_BODY_BYTES; appends
|
|
98
|
+
* `"[truncated]"` when the original size exceeded the limit.
|
|
99
|
+
* 3. Attempts `JSON.parse` → stores normalised JSON string.
|
|
100
|
+
* 4. On JSON failure, attempts `URLSearchParams` parse → stores JSON map.
|
|
101
|
+
* 5. On both failures, stores the raw string as-is.
|
|
102
|
+
* 6. On stream `error`, resolves with `{ body: '', bodySize: 0, clientIp }`.
|
|
103
|
+
*
|
|
104
|
+
* @param req Node.js IncomingMessage (or Express Request)
|
|
105
|
+
* @returns Resolved promise with body string, original byte size, and IP.
|
|
106
|
+
*/
|
|
107
|
+
async function bufferRequestBody(req) {
|
|
108
|
+
const clientIp = parseClientIp(req);
|
|
109
|
+
return new Promise((resolve) => {
|
|
110
|
+
const chunks = [];
|
|
111
|
+
let totalBytes = 0;
|
|
112
|
+
req.on('data', (chunk) => {
|
|
113
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
114
|
+
totalBytes += buf.byteLength;
|
|
115
|
+
chunks.push(buf);
|
|
116
|
+
});
|
|
117
|
+
req.on('end', () => {
|
|
118
|
+
// Concatenate all chunks into one buffer, then slice to the limit.
|
|
119
|
+
const full = Buffer.concat(chunks);
|
|
120
|
+
const truncated = totalBytes > MAX_BODY_BYTES;
|
|
121
|
+
const kept = truncated ? full.subarray(0, MAX_BODY_BYTES) : full;
|
|
122
|
+
let rawString = kept.toString('utf8');
|
|
123
|
+
if (truncated) {
|
|
124
|
+
rawString += TRUNCATED_SUFFIX;
|
|
125
|
+
}
|
|
126
|
+
// Try JSON first.
|
|
127
|
+
try {
|
|
128
|
+
const parsed = JSON.parse(rawString.replace(TRUNCATED_SUFFIX, ''));
|
|
129
|
+
// Store as normalised JSON string (handles whitespace variation).
|
|
130
|
+
const base = JSON.stringify(parsed);
|
|
131
|
+
resolve({
|
|
132
|
+
body: truncated ? base + TRUNCATED_SUFFIX : base,
|
|
133
|
+
bodySize: totalBytes,
|
|
134
|
+
clientIp,
|
|
135
|
+
});
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// JSON parse failed — try URL-encoded next (only on untruncated or
|
|
140
|
+
// the raw portion; URLSearchParams on a truncated string may be
|
|
141
|
+
// structurally invalid, but we try on the raw slice).
|
|
142
|
+
}
|
|
143
|
+
// Try URL-encoded.
|
|
144
|
+
const rawForUrlParse = truncated
|
|
145
|
+
? full.subarray(0, MAX_BODY_BYTES).toString('utf8')
|
|
146
|
+
: rawString;
|
|
147
|
+
const urlEncoded = tryParseUrlEncoded(rawForUrlParse);
|
|
148
|
+
if (urlEncoded !== null) {
|
|
149
|
+
resolve({
|
|
150
|
+
body: truncated ? urlEncoded + TRUNCATED_SUFFIX : urlEncoded,
|
|
151
|
+
bodySize: totalBytes,
|
|
152
|
+
clientIp,
|
|
153
|
+
});
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// Fall back to raw string (possibly truncated).
|
|
157
|
+
resolve({ body: rawString, bodySize: totalBytes, clientIp });
|
|
158
|
+
});
|
|
159
|
+
req.on('error', () => {
|
|
160
|
+
resolve({ body: '', bodySize: 0, clientIp });
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// Response body wrapping
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
/**
|
|
168
|
+
* Symbol used to attach error data to the request object for the finish handler.
|
|
169
|
+
*/
|
|
170
|
+
exports.ERROR_DATA_SYMBOL = Symbol('requestscope_error_data');
|
|
171
|
+
/**
|
|
172
|
+
* Wraps a ServerResponse to accumulate response chunks.
|
|
173
|
+
*
|
|
174
|
+
* Monkey-patches `res.write` and `res.end` to accumulate response body chunks.
|
|
175
|
+
* Caps the buffer at MAX_RESPONSE_BODY_BYTES; beyond that stores first
|
|
176
|
+
* MAX_RESPONSE_BODY_BYTES bytes + "[truncated]". Records responseBodySize as
|
|
177
|
+
* the original Content-Length or accumulated size.
|
|
178
|
+
*
|
|
179
|
+
* On the `finish` event, builds a RequestRecord with:
|
|
180
|
+
* - UUIDv4 id
|
|
181
|
+
* - process.hrtime-based responseTime
|
|
182
|
+
* - maskObject() applied to request headers and request body
|
|
183
|
+
* - maskObject() applied to response headers
|
|
184
|
+
* - Enqueues the record synchronously via the provided callback
|
|
185
|
+
*
|
|
186
|
+
* If wrapping fails (stream/binary response), sets responseBody = "",
|
|
187
|
+
* reads Content-Length header as responseBodySize, and calls original write/end
|
|
188
|
+
* without disrupting the response.
|
|
189
|
+
*
|
|
190
|
+
* @param res - Node.js ServerResponse (or Express Response)
|
|
191
|
+
* @param req - Node.js IncomingMessage (or Express Request)
|
|
192
|
+
* @param requestBody - Buffered request body string
|
|
193
|
+
* @param requestBodySize - Original request body byte size
|
|
194
|
+
* @param clientIp - Client IP address
|
|
195
|
+
* @param sensitiveFields - Set of sensitive field names to mask
|
|
196
|
+
* @param enqueueRecord - Callback to enqueue the RequestRecord synchronously
|
|
197
|
+
* @returns The wrapped response (same object, with patched methods)
|
|
198
|
+
*/
|
|
199
|
+
function wrapResponse(res, req, requestBody, requestBodySize, clientIp, sensitiveFields, enqueueRecord) {
|
|
200
|
+
const chunks = [];
|
|
201
|
+
let totalBytes = 0;
|
|
202
|
+
const startTime = process.hrtime.bigint();
|
|
203
|
+
// Store original methods
|
|
204
|
+
const originalWrite = res.write;
|
|
205
|
+
const originalEnd = res.end;
|
|
206
|
+
// Helper to accumulate chunks
|
|
207
|
+
const accumulateChunk = (chunk, encoding) => {
|
|
208
|
+
try {
|
|
209
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding);
|
|
210
|
+
totalBytes += buf.byteLength;
|
|
211
|
+
if (totalBytes <= MAX_RESPONSE_BODY_BYTES) {
|
|
212
|
+
chunks.push(buf);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
// Ignore accumulation errors
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
// Patch res.write
|
|
220
|
+
res.write = function (chunk, encoding, cb) {
|
|
221
|
+
accumulateChunk(chunk, encoding);
|
|
222
|
+
return originalWrite.call(this, chunk, encoding, cb);
|
|
223
|
+
};
|
|
224
|
+
// Patch res.end
|
|
225
|
+
res.end = function (chunk, encoding, cb) {
|
|
226
|
+
if (chunk !== undefined) {
|
|
227
|
+
accumulateChunk(chunk, encoding);
|
|
228
|
+
}
|
|
229
|
+
originalEnd.call(this, chunk, encoding, cb);
|
|
230
|
+
};
|
|
231
|
+
// Patch Express-specific methods that might bypass res.write/res.end
|
|
232
|
+
const expressRes = res;
|
|
233
|
+
if (expressRes.send) {
|
|
234
|
+
const originalSend = expressRes.send;
|
|
235
|
+
expressRes.send = function (body) {
|
|
236
|
+
// Capture the body before sending
|
|
237
|
+
if (body !== undefined) {
|
|
238
|
+
const str = typeof body === 'string' ? body : JSON.stringify(body);
|
|
239
|
+
accumulateChunk(str, 'utf8');
|
|
240
|
+
}
|
|
241
|
+
return originalSend.call(this, body);
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
if (expressRes.json) {
|
|
245
|
+
const originalJson = expressRes.json;
|
|
246
|
+
expressRes.json = function (obj) {
|
|
247
|
+
const str = JSON.stringify(obj);
|
|
248
|
+
accumulateChunk(str, 'utf8');
|
|
249
|
+
return originalJson.call(this, obj);
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
// Listen for finish event to build the record
|
|
253
|
+
res.on('finish', () => {
|
|
254
|
+
const endTime = process.hrtime.bigint();
|
|
255
|
+
const responseTime = Number(endTime - startTime) / 1000000; // Convert to milliseconds
|
|
256
|
+
// Build response body
|
|
257
|
+
let responseBody = '';
|
|
258
|
+
let responseBodySize = totalBytes;
|
|
259
|
+
if (chunks.length > 0) {
|
|
260
|
+
const full = Buffer.concat(chunks);
|
|
261
|
+
const truncated = totalBytes > MAX_RESPONSE_BODY_BYTES;
|
|
262
|
+
const kept = truncated ? full.subarray(0, MAX_RESPONSE_BODY_BYTES) : full;
|
|
263
|
+
responseBody = kept.toString('utf8');
|
|
264
|
+
if (truncated) {
|
|
265
|
+
responseBody += TRUNCATED_SUFFIX;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
// No chunks accumulated - try to get size from Content-Length header
|
|
270
|
+
const contentLength = res.getHeader('Content-Length');
|
|
271
|
+
if (typeof contentLength === 'number') {
|
|
272
|
+
responseBodySize = contentLength;
|
|
273
|
+
}
|
|
274
|
+
else if (typeof contentLength === 'string') {
|
|
275
|
+
responseBodySize = parseInt(contentLength, 10) || 0;
|
|
276
|
+
}
|
|
277
|
+
// Log when no chunks are accumulated for debugging
|
|
278
|
+
process.stderr.write(`[RequestScope] No response chunks accumulated for ${req.method} ${req.url}\n`);
|
|
279
|
+
}
|
|
280
|
+
// Get error data from request if present
|
|
281
|
+
const errorData = req[exports.ERROR_DATA_SYMBOL];
|
|
282
|
+
// Build request record
|
|
283
|
+
const record = {
|
|
284
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
285
|
+
method: req.method || 'GET',
|
|
286
|
+
url: req.url || '/',
|
|
287
|
+
route: null, // Will be set by Express if available
|
|
288
|
+
queryParams: {}, // Will be populated by Express
|
|
289
|
+
pathParams: {}, // Will be populated by Express
|
|
290
|
+
requestHeaders: (0, masker_1.maskObject)(Object.fromEntries(Object.entries(req.headers).map(([k, v]) => [k, Array.isArray(v) ? v[0] : v ?? ''])), sensitiveFields),
|
|
291
|
+
requestBody,
|
|
292
|
+
requestBodySize,
|
|
293
|
+
clientIp,
|
|
294
|
+
userAgent: req.headers['user-agent'] || '',
|
|
295
|
+
statusCode: res.statusCode,
|
|
296
|
+
responseHeaders: (0, masker_1.maskObject)(Object.fromEntries(Object.entries(res.getHeaders()).map(([k, v]) => [k, Array.isArray(v) ? v[0] : v ?? ''])), sensitiveFields),
|
|
297
|
+
responseBody,
|
|
298
|
+
responseBodySize,
|
|
299
|
+
responseTime,
|
|
300
|
+
errorMessage: errorData?.message ?? null,
|
|
301
|
+
errorStack: errorData?.stack ?? null,
|
|
302
|
+
errorStatusCode: errorData?.statusCode ?? null,
|
|
303
|
+
timestamp: new Date().toISOString(),
|
|
304
|
+
};
|
|
305
|
+
// Enqueue the record synchronously
|
|
306
|
+
enqueueRecord(record);
|
|
307
|
+
});
|
|
308
|
+
return res;
|
|
309
|
+
}
|
|
310
|
+
//# sourceMappingURL=capture.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capture.js","sourceRoot":"","sources":["../../src/capture.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* RequestScope — Configuration validation, defaults, and sensitive-field set.
|
|
4
|
+
*
|
|
5
|
+
* All validation errors are thrown synchronously so that `requestscope()` fails
|
|
6
|
+
* fast at startup before any middleware is registered.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.applyDefaults = applyDefaults;
|
|
10
|
+
exports.validateConfig = validateConfig;
|
|
11
|
+
exports.buildSensitiveFieldSet = buildSensitiveFieldSet;
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Constants
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
const SUPPORTED_STORAGE_TYPES = ['mongodb', 'mysql', 'postgresql'];
|
|
16
|
+
const SQL_REQUIRED_FIELDS = [
|
|
17
|
+
'host',
|
|
18
|
+
'database',
|
|
19
|
+
'username',
|
|
20
|
+
'password',
|
|
21
|
+
];
|
|
22
|
+
/** Built-in sensitive field names (all compared case-insensitively). */
|
|
23
|
+
const DEFAULT_SENSITIVE_FIELDS = [
|
|
24
|
+
'password',
|
|
25
|
+
'token',
|
|
26
|
+
'authorization',
|
|
27
|
+
'apiKey',
|
|
28
|
+
'secret',
|
|
29
|
+
];
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// applyDefaults
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
/**
|
|
34
|
+
* Fills in missing optional fields with their documented default values.
|
|
35
|
+
* Mutates the config object in place and returns it for chaining.
|
|
36
|
+
*
|
|
37
|
+
* Defaults applied:
|
|
38
|
+
* retentionDays → 30
|
|
39
|
+
* ignore → []
|
|
40
|
+
* maskFields → []
|
|
41
|
+
* storage.poolSize → 5
|
|
42
|
+
* storage.ssl → false
|
|
43
|
+
*/
|
|
44
|
+
function applyDefaults(config) {
|
|
45
|
+
if (config.retentionDays === undefined) {
|
|
46
|
+
config.retentionDays = 30;
|
|
47
|
+
}
|
|
48
|
+
if (config.ignore === undefined) {
|
|
49
|
+
config.ignore = ['/requestscope/api', '/requestscope/assets'];
|
|
50
|
+
}
|
|
51
|
+
if (config.maskFields === undefined) {
|
|
52
|
+
config.maskFields = [];
|
|
53
|
+
}
|
|
54
|
+
if (config.storage.poolSize === undefined) {
|
|
55
|
+
config.storage.poolSize = 5;
|
|
56
|
+
}
|
|
57
|
+
if (config.storage.ssl === undefined) {
|
|
58
|
+
config.storage.ssl = false;
|
|
59
|
+
}
|
|
60
|
+
return config;
|
|
61
|
+
}
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// validateConfig
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
/**
|
|
66
|
+
* Validates the configuration object and throws a synchronous `Error` for
|
|
67
|
+
* any invalid value.
|
|
68
|
+
*
|
|
69
|
+
* Checks (in order):
|
|
70
|
+
* 1. `storage.type` must be one of the supported values.
|
|
71
|
+
* 2. MongoDB requires `storage.uri`.
|
|
72
|
+
* 3. MySQL / PostgreSQL require `host`, `database`, `username`, `password`.
|
|
73
|
+
* 4. `retentionDays` (when provided) must be a positive integer in [1, 365].
|
|
74
|
+
*/
|
|
75
|
+
function validateConfig(config) {
|
|
76
|
+
const { storage, retentionDays } = config;
|
|
77
|
+
// 1. Validate storage.type
|
|
78
|
+
if (!SUPPORTED_STORAGE_TYPES.includes(storage.type)) {
|
|
79
|
+
throw new Error(`Invalid storage type: '${storage.type}'. Supported values are: mongodb, mysql, postgresql`);
|
|
80
|
+
}
|
|
81
|
+
// 2. MongoDB: uri is required
|
|
82
|
+
if (storage.type === 'mongodb') {
|
|
83
|
+
if (!storage.uri) {
|
|
84
|
+
throw new Error("storage.uri is required when storage.type is 'mongodb'");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// 3. MySQL / PostgreSQL: all SQL required fields must be present
|
|
88
|
+
if (storage.type === 'mysql' || storage.type === 'postgresql') {
|
|
89
|
+
const missing = SQL_REQUIRED_FIELDS.filter((field) => storage[field] === undefined || storage[field] === null);
|
|
90
|
+
if (missing.length > 0) {
|
|
91
|
+
throw new Error(`Missing required storage fields: ${missing.join(', ')}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// 4. retentionDays: must be a positive integer in [1, 365] when provided
|
|
95
|
+
if (retentionDays !== undefined) {
|
|
96
|
+
const isValidInteger = typeof retentionDays === 'number' &&
|
|
97
|
+
Number.isInteger(retentionDays) &&
|
|
98
|
+
retentionDays >= 1 &&
|
|
99
|
+
retentionDays <= 365;
|
|
100
|
+
if (!isValidInteger) {
|
|
101
|
+
throw new Error(`retentionDays must be a positive integer between 1 and 365, received: ${retentionDays}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// buildSensitiveFieldSet
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
/**
|
|
109
|
+
* Constructs the `Set<string>` of sensitive field names by forming the union
|
|
110
|
+
* of the built-in defaults and any user-supplied `maskFields`.
|
|
111
|
+
*
|
|
112
|
+
* All names are lowercased so that lookups from `maskObject()` can compare
|
|
113
|
+
* case-insensitively by lowercasing the inspected key.
|
|
114
|
+
*/
|
|
115
|
+
function buildSensitiveFieldSet(config) {
|
|
116
|
+
const names = [
|
|
117
|
+
...DEFAULT_SENSITIVE_FIELDS,
|
|
118
|
+
...(config.maskFields ?? []),
|
|
119
|
+
];
|
|
120
|
+
return new Set(names.map((n) => n.toLowerCase()));
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* RequestScope — Dashboard API handlers.
|
|
4
|
+
*
|
|
5
|
+
* Provides Express route handlers for the dashboard:
|
|
6
|
+
* - GET /api/records — List records with filtering, sorting, and pagination
|
|
7
|
+
* - GET /api/records/:id — Get a single record by ID
|
|
8
|
+
*
|
|
9
|
+
* Requirements: 9.1, 9.2, 9.3, 9.4, 9.5, 9.6
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.getRecords = getRecords;
|
|
13
|
+
exports.getRecordById = getRecordById;
|
|
14
|
+
exports.deleteAllRecords = deleteAllRecords;
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Helper functions
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
/**
|
|
19
|
+
* Parses a query parameter as an integer with a default value.
|
|
20
|
+
*/
|
|
21
|
+
function parseIntParam(value, defaultValue, min = 1) {
|
|
22
|
+
if (value === undefined) {
|
|
23
|
+
return defaultValue;
|
|
24
|
+
}
|
|
25
|
+
const parsed = parseInt(value, 10);
|
|
26
|
+
if (isNaN(parsed) || parsed < min) {
|
|
27
|
+
return defaultValue;
|
|
28
|
+
}
|
|
29
|
+
return parsed;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Validates that a value is one of the allowed options.
|
|
33
|
+
*/
|
|
34
|
+
function validateOption(value, allowed, defaultValue) {
|
|
35
|
+
if (value === undefined) {
|
|
36
|
+
return defaultValue;
|
|
37
|
+
}
|
|
38
|
+
return allowed.includes(value) ? value : defaultValue;
|
|
39
|
+
}
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Route handlers
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
/**
|
|
44
|
+
* GET /api/records handler.
|
|
45
|
+
*
|
|
46
|
+
* Query parameters:
|
|
47
|
+
* - search: string (optional) — case-insensitive substring match on url
|
|
48
|
+
* - startDate: string (optional) — ISO 8601 timestamp, inclusive lower bound
|
|
49
|
+
* - endDate: string (optional) — ISO 8601 timestamp, inclusive upper bound
|
|
50
|
+
* - statusCodeGroup: string (optional) — '2xx', '3xx', '4xx', or '5xx'
|
|
51
|
+
* - statusCode: number (optional) — exact status code match
|
|
52
|
+
* - method: string (optional) — exact HTTP method match (case-insensitive)
|
|
53
|
+
* - sortBy: string (optional) — 'timestamp', 'responseTime', or 'statusCode'
|
|
54
|
+
* - sortOrder: string (optional) — 'asc' or 'desc'
|
|
55
|
+
* - page: number (optional) — page number, default 1
|
|
56
|
+
* - pageSize: number (optional) — page size, default 25
|
|
57
|
+
*
|
|
58
|
+
* Returns:
|
|
59
|
+
* - 200 with { records: RequestRecord[], total: number }
|
|
60
|
+
* - 400 for malformed query parameters
|
|
61
|
+
*/
|
|
62
|
+
async function getRecords(req, res) {
|
|
63
|
+
try {
|
|
64
|
+
const adapter = req.app.get('requestscopeAdapter');
|
|
65
|
+
if (!adapter) {
|
|
66
|
+
res.status(500).json({ error: 'Storage adapter not configured' });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
// Parse and validate query parameters
|
|
70
|
+
const page = parseIntParam(req.query.page, 1);
|
|
71
|
+
const pageSize = parseIntParam(req.query.pageSize, 25);
|
|
72
|
+
const sortBy = validateOption(req.query.sortBy, ['timestamp', 'responseTime', 'statusCode'], 'timestamp');
|
|
73
|
+
const sortOrder = validateOption(req.query.sortOrder, ['asc', 'desc'], 'desc');
|
|
74
|
+
// Build QueryFilters
|
|
75
|
+
const filters = {
|
|
76
|
+
search: req.query.search,
|
|
77
|
+
startDate: req.query.startDate,
|
|
78
|
+
endDate: req.query.endDate,
|
|
79
|
+
statusCodeGroup: (() => {
|
|
80
|
+
const value = req.query.statusCodeGroup;
|
|
81
|
+
if (!value)
|
|
82
|
+
return undefined;
|
|
83
|
+
if (['2xx', '3xx', '4xx', '5xx'].includes(value)) {
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
})(),
|
|
88
|
+
statusCode: req.query.statusCode
|
|
89
|
+
? parseIntParam(req.query.statusCode, 0)
|
|
90
|
+
: undefined,
|
|
91
|
+
method: req.query.method,
|
|
92
|
+
sortBy,
|
|
93
|
+
sortOrder,
|
|
94
|
+
page,
|
|
95
|
+
pageSize,
|
|
96
|
+
};
|
|
97
|
+
// Query the adapter
|
|
98
|
+
const result = await adapter.query(filters, { page, pageSize });
|
|
99
|
+
res.status(200).json(result);
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
103
|
+
res.status(400).json({ error: message });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* GET /api/records/:id handler.
|
|
108
|
+
*
|
|
109
|
+
* Returns:
|
|
110
|
+
* - 200 with the RequestRecord
|
|
111
|
+
* - 404 if the record is not found
|
|
112
|
+
* - 400 for malformed ID
|
|
113
|
+
*/
|
|
114
|
+
async function getRecordById(req, res) {
|
|
115
|
+
try {
|
|
116
|
+
const adapter = req.app.get('requestscopeAdapter');
|
|
117
|
+
if (!adapter) {
|
|
118
|
+
res.status(500).json({ error: 'Storage adapter not configured' });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const id = req.params.id;
|
|
122
|
+
if (!id) {
|
|
123
|
+
res.status(400).json({ error: 'Record ID is required' });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
// Query for the specific record using id filter
|
|
127
|
+
const filters = {
|
|
128
|
+
id,
|
|
129
|
+
page: 1,
|
|
130
|
+
pageSize: 1,
|
|
131
|
+
};
|
|
132
|
+
const result = await adapter.query(filters, { page: 1, pageSize: 1 });
|
|
133
|
+
const record = result.records[0];
|
|
134
|
+
if (!record) {
|
|
135
|
+
res.status(404).json({ error: 'Record not found' });
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
res.status(200).json(record);
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
142
|
+
res.status(400).json({ error: message });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* DELETE /api/records handler.
|
|
147
|
+
*
|
|
148
|
+
* Deletes all records from the database.
|
|
149
|
+
*
|
|
150
|
+
* Returns:
|
|
151
|
+
* - 200 with { deleted: number }
|
|
152
|
+
* - 500 for server errors
|
|
153
|
+
*/
|
|
154
|
+
async function deleteAllRecords(req, res) {
|
|
155
|
+
try {
|
|
156
|
+
const adapter = req.app.get('requestscopeAdapter');
|
|
157
|
+
if (!adapter) {
|
|
158
|
+
res.status(500).json({ error: 'Storage adapter not configured' });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
// Query to get total count before deletion
|
|
162
|
+
const allRecords = await adapter.query({ page: 1, pageSize: 1000000 }, { page: 1, pageSize: 1000000 });
|
|
163
|
+
const totalToDelete = allRecords.total;
|
|
164
|
+
// Delete all records by setting retention to a future date
|
|
165
|
+
await adapter.deleteOlderThan(new Date(Date.now() + 86400000));
|
|
166
|
+
res.status(200).json({ deleted: totalToDelete });
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
170
|
+
res.status(500).json({ error: message });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=api.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../../src/dashboard/api.ts"],"names":[],"mappings":"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"}
|