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,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RequestScope — Core middleware factory.
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - requestscope(config) — Express middleware factory that validates config,
|
|
6
|
+
* instantiates the appropriate storage adapter, creates AsyncQueue and
|
|
7
|
+
* RetentionScheduler, and returns a request handler.
|
|
8
|
+
* - errorHandler — 4-arity error middleware that attaches error data to
|
|
9
|
+
* the request for capture by the finish handler.
|
|
10
|
+
*
|
|
11
|
+
* Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 2.2, 4.1, 4.2, 4.3, 5.1
|
|
12
|
+
*/
|
|
13
|
+
import { validateConfig, applyDefaults, buildSensitiveFieldSet } from './config';
|
|
14
|
+
import { MongoAdapter } from './adapters/mongo.adapter';
|
|
15
|
+
import { MySQLAdapter } from './adapters/mysql.adapter';
|
|
16
|
+
import { PgAdapter } from './adapters/pg.adapter';
|
|
17
|
+
import { AsyncQueue } from './queue';
|
|
18
|
+
import { RetentionScheduler } from './retention';
|
|
19
|
+
import { bufferRequestBody, wrapResponse, ERROR_DATA_SYMBOL } from './capture';
|
|
20
|
+
import { createDashboardRouter } from './dashboard/router';
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Factory function
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
/**
|
|
25
|
+
* Creates and returns an Express middleware that captures HTTP requests.
|
|
26
|
+
*
|
|
27
|
+
* Validation and initialization:
|
|
28
|
+
* 1. Calls `validateConfig()`, `applyDefaults()`, and `buildSensitiveFieldSet()`
|
|
29
|
+
* synchronously; throws on any validation error.
|
|
30
|
+
* 2. Instantiates the correct `StorageAdapter` based on `storage.type` (unless adapter is provided).
|
|
31
|
+
* 3. Calls `adapter.initialize()` asynchronously; logs errors to `stderr`
|
|
32
|
+
* without throwing (middleware continues to function even if storage fails).
|
|
33
|
+
* 4. Creates `AsyncQueue` and `RetentionScheduler`; starts both.
|
|
34
|
+
* 5. Attaches queue `drain()` to `process.on('SIGTERM')` and
|
|
35
|
+
* `process.on('SIGINT')` for graceful shutdown.
|
|
36
|
+
*
|
|
37
|
+
* Request handling:
|
|
38
|
+
* - Checks `ignore` list; skips capture and calls `next()` if matched.
|
|
39
|
+
* - Buffers request body via `bufferRequestBody()`.
|
|
40
|
+
* - Wraps response via `wrapResponse()` to accumulate response chunks.
|
|
41
|
+
* - On response finish, builds `RequestRecord` and enqueues it.
|
|
42
|
+
*
|
|
43
|
+
* @param config - RequestScope configuration object
|
|
44
|
+
* @param adapter - Optional storage adapter instance (for sharing with dashboard)
|
|
45
|
+
* @returns Express RequestHandler middleware
|
|
46
|
+
*/
|
|
47
|
+
export function requestscope(config, adapter) {
|
|
48
|
+
// 1. Validate and apply defaults synchronously
|
|
49
|
+
validateConfig(config);
|
|
50
|
+
applyDefaults(config);
|
|
51
|
+
const sensitiveFields = buildSensitiveFieldSet(config);
|
|
52
|
+
// 2. Instantiate the appropriate storage adapter (if not provided)
|
|
53
|
+
if (!adapter) {
|
|
54
|
+
switch (config.storage.type) {
|
|
55
|
+
case 'mongodb':
|
|
56
|
+
adapter = new MongoAdapter(config.storage);
|
|
57
|
+
break;
|
|
58
|
+
case 'mysql':
|
|
59
|
+
adapter = new MySQLAdapter(config.storage);
|
|
60
|
+
break;
|
|
61
|
+
case 'postgresql':
|
|
62
|
+
adapter = new PgAdapter(config.storage);
|
|
63
|
+
break;
|
|
64
|
+
default:
|
|
65
|
+
// This should never happen due to validateConfig, but TypeScript needs it
|
|
66
|
+
throw new Error(`Unsupported storage type: ${config.storage.type}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// 3. Initialize adapter asynchronously (log errors, don't throw)
|
|
70
|
+
void adapter.initialize().catch((err) => {
|
|
71
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
72
|
+
process.stderr.write(`[RequestScope] Failed to initialize storage adapter: ${message}\n`);
|
|
73
|
+
});
|
|
74
|
+
// 4. Create and start AsyncQueue and RetentionScheduler
|
|
75
|
+
const queue = new AsyncQueue(adapter);
|
|
76
|
+
const retentionDays = config.retentionDays ?? 30;
|
|
77
|
+
const retentionScheduler = new RetentionScheduler(adapter, retentionDays);
|
|
78
|
+
retentionScheduler.start();
|
|
79
|
+
// 5. Attach graceful shutdown handlers
|
|
80
|
+
const shutdown = async () => {
|
|
81
|
+
retentionScheduler.stop();
|
|
82
|
+
await queue.drain(10000);
|
|
83
|
+
queue.destroy();
|
|
84
|
+
};
|
|
85
|
+
process.on('SIGTERM', () => {
|
|
86
|
+
void shutdown();
|
|
87
|
+
});
|
|
88
|
+
process.on('SIGINT', () => {
|
|
89
|
+
void shutdown();
|
|
90
|
+
});
|
|
91
|
+
// 6. Return the request handler middleware
|
|
92
|
+
return async (req, res, next) => {
|
|
93
|
+
// Check ignore list
|
|
94
|
+
const ignoreList = config.ignore ?? [];
|
|
95
|
+
const url = req.originalUrl || req.url || '/';
|
|
96
|
+
// Always ignore dashboard routes
|
|
97
|
+
if (url.startsWith('/requestscope')) {
|
|
98
|
+
next();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (ignoreList.some((pattern) => url === pattern || url.startsWith(pattern))) {
|
|
102
|
+
next();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
// Buffer request body
|
|
107
|
+
const { body: requestBody, bodySize: requestBodySize, clientIp } = await bufferRequestBody(req);
|
|
108
|
+
// Wrap response to capture response data
|
|
109
|
+
wrapResponse(res, req, requestBody, requestBodySize, clientIp, sensitiveFields, (record) => queue.enqueue(record));
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
// If capture fails, log and continue without disrupting the request
|
|
113
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
114
|
+
process.stderr.write(`[RequestScope] Capture error: ${message}\n`);
|
|
115
|
+
}
|
|
116
|
+
next();
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Setup function
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
/**
|
|
123
|
+
* Sets up RequestScope middleware and automatically mounts the dashboard at /requestscope.
|
|
124
|
+
*
|
|
125
|
+
* This function:
|
|
126
|
+
* 1. Creates the storage adapter
|
|
127
|
+
* 2. Sets up the request capture middleware
|
|
128
|
+
* 3. Automatically mounts the dashboard at /requestscope
|
|
129
|
+
*
|
|
130
|
+
* @param app - Express application instance
|
|
131
|
+
* @param config - RequestScope configuration object
|
|
132
|
+
*/
|
|
133
|
+
export function setup(app, config) {
|
|
134
|
+
// Validate and apply defaults
|
|
135
|
+
validateConfig(config);
|
|
136
|
+
applyDefaults(config);
|
|
137
|
+
// Instantiate the appropriate storage adapter
|
|
138
|
+
let adapter;
|
|
139
|
+
switch (config.storage.type) {
|
|
140
|
+
case 'mongodb':
|
|
141
|
+
adapter = new MongoAdapter(config.storage);
|
|
142
|
+
break;
|
|
143
|
+
case 'mysql':
|
|
144
|
+
adapter = new MySQLAdapter(config.storage);
|
|
145
|
+
break;
|
|
146
|
+
case 'postgresql':
|
|
147
|
+
adapter = new PgAdapter(config.storage);
|
|
148
|
+
break;
|
|
149
|
+
default:
|
|
150
|
+
throw new Error(`Unsupported storage type: ${config.storage.type}`);
|
|
151
|
+
}
|
|
152
|
+
// Initialize adapter asynchronously
|
|
153
|
+
void adapter.initialize().catch((err) => {
|
|
154
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
155
|
+
process.stderr.write(`[RequestScope] Failed to initialize storage adapter: ${message}\n`);
|
|
156
|
+
});
|
|
157
|
+
// Set adapter on app for dashboard API to use
|
|
158
|
+
app.set('requestscopeAdapter', adapter);
|
|
159
|
+
// Use the middleware with the shared adapter
|
|
160
|
+
app.use(requestscope(config, adapter));
|
|
161
|
+
// Mount dashboard at /requestscope
|
|
162
|
+
app.use('/requestscope', createDashboardRouter(config.dashboard, adapter));
|
|
163
|
+
}
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// Error middleware
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
/**
|
|
168
|
+
* 4-arity error middleware that attaches error data to the request.
|
|
169
|
+
*
|
|
170
|
+
* The error data (message, stack, statusCode) is attached via a Symbol-keyed
|
|
171
|
+
* property so the response finish handler can include it in the RequestRecord.
|
|
172
|
+
*
|
|
173
|
+
* Usage:
|
|
174
|
+
* app.use(requestscope(config));
|
|
175
|
+
* app.use(errorHandler);
|
|
176
|
+
* // ... route handlers that may throw
|
|
177
|
+
*
|
|
178
|
+
* @param err - Error object
|
|
179
|
+
* @param req - Express Request
|
|
180
|
+
* @param res - Express Response
|
|
181
|
+
* @param next - Express NextFunction
|
|
182
|
+
*/
|
|
183
|
+
export function errorHandler(err, req, res, next) {
|
|
184
|
+
const errorData = {
|
|
185
|
+
message: err.message,
|
|
186
|
+
stack: err.stack || '',
|
|
187
|
+
statusCode: err.statusCode || 500,
|
|
188
|
+
};
|
|
189
|
+
req[ERROR_DATA_SYMBOL] = errorData;
|
|
190
|
+
// Pass error to next error handler in the chain
|
|
191
|
+
next(err);
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=middleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../src/middleware.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AsyncQueue — In-process batch write queue.
|
|
3
|
+
*
|
|
4
|
+
* Decouples the hot-path enqueue (synchronous, ≤1 ms) from database writes,
|
|
5
|
+
* which happen asynchronously in configurable batches. Fault isolation is
|
|
6
|
+
* total: adapter failures are caught, logged to stderr, and discarded so
|
|
7
|
+
* they never propagate to the caller.
|
|
8
|
+
*/
|
|
9
|
+
export class AsyncQueue {
|
|
10
|
+
constructor(adapter, batchSize = 50, flushIntervalMs = 5000, maxCapacity = 10000) {
|
|
11
|
+
this.adapter = adapter;
|
|
12
|
+
this.batchSize = batchSize;
|
|
13
|
+
this.flushIntervalMs = flushIntervalMs;
|
|
14
|
+
this.maxCapacity = maxCapacity;
|
|
15
|
+
this.items = [];
|
|
16
|
+
this.flushTimer = null;
|
|
17
|
+
this.isFlushing = false;
|
|
18
|
+
// Start the periodic flush timer immediately on construction.
|
|
19
|
+
this.flushTimer = setInterval(() => {
|
|
20
|
+
void this.flush();
|
|
21
|
+
}, this.flushIntervalMs);
|
|
22
|
+
// Allow the Node.js process to exit even if this timer is still active.
|
|
23
|
+
if (this.flushTimer.unref) {
|
|
24
|
+
this.flushTimer.unref();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Synchronously adds a record to the queue.
|
|
29
|
+
*
|
|
30
|
+
* If the queue is at capacity, the record is discarded and a warning is
|
|
31
|
+
* written to stderr. If the queue has reached `batchSize` after this push,
|
|
32
|
+
* a flush is triggered immediately (fire-and-forget).
|
|
33
|
+
*/
|
|
34
|
+
enqueue(record) {
|
|
35
|
+
if (this.items.length >= this.maxCapacity) {
|
|
36
|
+
process.stderr.write(`[RequestScope] Queue capacity exceeded (max ${this.maxCapacity}). ` +
|
|
37
|
+
`Record for ${record.method} ${record.url} discarded.\n`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
this.items.push(record);
|
|
41
|
+
if (this.items.length >= this.batchSize) {
|
|
42
|
+
void this.flush();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Flushes up to `batchSize` items from the front of the queue.
|
|
47
|
+
*
|
|
48
|
+
* A guard (`isFlushing`) prevents concurrent flushes. The splice is
|
|
49
|
+
* performed atomically before the async adapter call so no records are
|
|
50
|
+
* lost even if the adapter rejects. On failure the batch is discarded
|
|
51
|
+
* and the error is written to stderr.
|
|
52
|
+
*/
|
|
53
|
+
async flush() {
|
|
54
|
+
if (this.isFlushing || this.items.length === 0) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
this.isFlushing = true;
|
|
58
|
+
// Atomically remove up to batchSize items from the front of the array.
|
|
59
|
+
const batch = this.items.splice(0, this.batchSize);
|
|
60
|
+
try {
|
|
61
|
+
await this.adapter.insert(batch);
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
65
|
+
process.stderr.write(`[RequestScope] adapter.insert() failed — batch of ${batch.length} record(s) discarded. ` +
|
|
66
|
+
`Error: ${message}\n`);
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
this.isFlushing = false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Flushes all remaining items in the queue, waiting at most `timeoutMs`
|
|
74
|
+
* milliseconds. Any items that could not be flushed before the timeout
|
|
75
|
+
* are logged to stderr.
|
|
76
|
+
*
|
|
77
|
+
* Used during graceful shutdown (SIGTERM / SIGINT).
|
|
78
|
+
*/
|
|
79
|
+
async drain(timeoutMs = 10000) {
|
|
80
|
+
const deadline = Date.now() + timeoutMs;
|
|
81
|
+
while (this.items.length > 0) {
|
|
82
|
+
if (Date.now() >= deadline) {
|
|
83
|
+
const remaining = this.items.splice(0);
|
|
84
|
+
process.stderr.write(`[RequestScope] drain() timed out — ${remaining.length} record(s) could not be flushed:\n` +
|
|
85
|
+
remaining
|
|
86
|
+
.map((r) => ` ${r.method} ${r.url} @ ${r.timestamp}`)
|
|
87
|
+
.join('\n') +
|
|
88
|
+
'\n');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// Wait for any in-progress flush to complete before starting the next.
|
|
92
|
+
if (this.isFlushing) {
|
|
93
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
await this.flush();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Clears the periodic flush timer. Call this to allow the Node.js process
|
|
101
|
+
* to exit cleanly, or when the queue is no longer needed.
|
|
102
|
+
*/
|
|
103
|
+
destroy() {
|
|
104
|
+
if (this.flushTimer !== null) {
|
|
105
|
+
clearInterval(this.flushTimer);
|
|
106
|
+
this.flushTimer = null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=queue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queue.js","sourceRoot":"","sources":["../../src/queue.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RetentionScheduler — automatically purges records older than the configured
|
|
3
|
+
* retention window by calling `adapter.deleteOlderThan()` once per day.
|
|
4
|
+
*
|
|
5
|
+
* Requirements: 7.1, 7.2, 7.4, 7.5
|
|
6
|
+
*/
|
|
7
|
+
const TWENTY_FOUR_HOURS_MS = 24 * 60 * 60 * 1000; // 86_400_000 ms
|
|
8
|
+
export class RetentionScheduler {
|
|
9
|
+
constructor(adapter, retentionDays) {
|
|
10
|
+
this.adapter = adapter;
|
|
11
|
+
this.retentionDays = retentionDays;
|
|
12
|
+
this.intervalHandle = null;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Runs the retention job immediately, then schedules it to repeat every 24 h.
|
|
16
|
+
* Calling `start()` more than once has no additional effect until `stop()` is
|
|
17
|
+
* called first (the previous interval is replaced by the new one).
|
|
18
|
+
*/
|
|
19
|
+
start() {
|
|
20
|
+
// Run an initial sweep right away so old records are not kept waiting up
|
|
21
|
+
// to 24 h after the server first starts.
|
|
22
|
+
void this.runOnce();
|
|
23
|
+
// Replace any previously-running interval.
|
|
24
|
+
if (this.intervalHandle !== null) {
|
|
25
|
+
clearInterval(this.intervalHandle);
|
|
26
|
+
}
|
|
27
|
+
this.intervalHandle = setInterval(() => {
|
|
28
|
+
void this.runOnce();
|
|
29
|
+
}, TWENTY_FOUR_HOURS_MS);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Stops the scheduled retention job. Inflight `runOnce()` calls are allowed
|
|
33
|
+
* to complete naturally because they are fire-and-forget.
|
|
34
|
+
*/
|
|
35
|
+
stop() {
|
|
36
|
+
if (this.intervalHandle !== null) {
|
|
37
|
+
clearInterval(this.intervalHandle);
|
|
38
|
+
this.intervalHandle = null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Computes the cutoff date from `retentionDays` and asks the adapter to
|
|
43
|
+
* delete every record older than that date. Any error is logged to `stderr`
|
|
44
|
+
* but never rethrown so the scheduler stays alive for the next tick.
|
|
45
|
+
*/
|
|
46
|
+
async runOnce() {
|
|
47
|
+
const cutoff = new Date(Date.now() - this.retentionDays * 86400000);
|
|
48
|
+
try {
|
|
49
|
+
const deleted = await this.adapter.deleteOlderThan(cutoff);
|
|
50
|
+
if (deleted > 0) {
|
|
51
|
+
process.stderr.write(`[RequestScope] RetentionScheduler: deleted ${deleted} record(s) older than ${cutoff.toISOString()}\n`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
56
|
+
process.stderr.write(`[RequestScope] RetentionScheduler error: ${message}\n`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=retention.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retention.js","sourceRoot":"","sources":["../../src/retention.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,MAAM,oBAAoB,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,gBAAgB;AAElE,MAAM,OAAO,kBAAkB;IAG7B,YACmB,OAAuB,EACvB,aAAqB;QADrB,YAAO,GAAP,OAAO,CAAgB;QACvB,kBAAa,GAAb,aAAa,CAAQ;QAJhC,mBAAc,GAA0C,IAAI,CAAC;IAKlE,CAAC;IAEJ;;;;OAIG;IACH,KAAK;QACH,yEAAyE;QACzE,yCAAyC;QACzC,KAAK,IAAI,CAAC,OAAO,EAAE,CAAC;QAEpB,2CAA2C;QAC3C,IAAI,IAAI,CAAC,cAAc,KAAK,IAAI,EAAE,CAAC;YACjC,aAAa,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;QACrC,CAAC;QAED,IAAI,CAAC,cAAc,GAAG,WAAW,CAAC,GAAG,EAAE;YACrC,KAAK,IAAI,CAAC,OAAO,EAAE,CAAC;QACtB,CAAC,EAAE,oBAAoB,CAAC,CAAC;IAC3B,CAAC;IAED;;;OAGG;IACH,IAAI;QACF,IAAI,IAAI,CAAC,cAAc,KAAK,IAAI,EAAE,CAAC;YACjC,aAAa,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;YACnC,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;QAC7B,CAAC;IACH,CAAC;IAED;;;;OAIG;IACK,KAAK,CAAC,OAAO;QACnB,MAAM,MAAM,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,aAAa,GAAG,QAAU,CAAC,CAAC;QACtE,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC;YAC3D,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;gBAChB,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,8CAA8C,OAAO,yBAAyB,MAAM,CAAC,WAAW,EAAE,IAAI,CACvG,CAAC;YACJ,CAAC;QACH,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACtB,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACjE,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,4CAA4C,OAAO,IAAI,CACxD,CAAC;QACJ,CAAC;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RequestScope — All exported TypeScript interfaces.
|
|
3
|
+
*
|
|
4
|
+
* This file is the single source of truth for every public type in the library.
|
|
5
|
+
* All interfaces are written to compile under `strict: true` with zero errors.
|
|
6
|
+
*/
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StorageAdapter interface — the contract all storage adapters must implement.
|
|
3
|
+
*
|
|
4
|
+
* Adapters should import from this file so they stay decoupled from the
|
|
5
|
+
* top-level types module while still satisfying the shared contract.
|
|
6
|
+
*/
|
|
7
|
+
export type { StorageAdapter, RequestRecord, QueryFilters } from '../types';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MongoAdapter — MongoDB implementation of the StorageAdapter interface.
|
|
3
|
+
*
|
|
4
|
+
* Collection: `requestscope_records`
|
|
5
|
+
* `_id` is set to the record's UUID string for direct look-ups.
|
|
6
|
+
* A descending index on `timestamp` is created at initialization for sort
|
|
7
|
+
* performance and retention queries.
|
|
8
|
+
*/
|
|
9
|
+
import type { StorageAdapter, RequestRecord, QueryFilters, StorageConfig } from '../types';
|
|
10
|
+
export declare class MongoAdapter implements StorageAdapter {
|
|
11
|
+
private readonly config;
|
|
12
|
+
private client;
|
|
13
|
+
private collection;
|
|
14
|
+
constructor(config: StorageConfig);
|
|
15
|
+
initialize(): Promise<void>;
|
|
16
|
+
insert(records: RequestRecord[]): Promise<void>;
|
|
17
|
+
query(filters: QueryFilters, pagination: {
|
|
18
|
+
page: number;
|
|
19
|
+
pageSize: number;
|
|
20
|
+
}): Promise<{
|
|
21
|
+
records: RequestRecord[];
|
|
22
|
+
total: number;
|
|
23
|
+
}>;
|
|
24
|
+
deleteOlderThan(date: Date): Promise<number>;
|
|
25
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MySQLAdapter — StorageAdapter implementation for MySQL using mysql2/promise.
|
|
3
|
+
*
|
|
4
|
+
* Compiles under `strict: true` with zero errors.
|
|
5
|
+
* Requirements: 1.6, 1.7, 6.1, 6.3, 6.5, 7.4
|
|
6
|
+
*/
|
|
7
|
+
import type { StorageAdapter, RequestRecord, QueryFilters } from '../types';
|
|
8
|
+
import type { StorageConfig } from '../types';
|
|
9
|
+
export declare class MySQLAdapter implements StorageAdapter {
|
|
10
|
+
private pool;
|
|
11
|
+
private readonly config;
|
|
12
|
+
constructor(config: StorageConfig);
|
|
13
|
+
initialize(): Promise<void>;
|
|
14
|
+
insert(records: RequestRecord[]): Promise<void>;
|
|
15
|
+
query(filters: QueryFilters, pagination: {
|
|
16
|
+
page: number;
|
|
17
|
+
pageSize: number;
|
|
18
|
+
}): Promise<{
|
|
19
|
+
records: RequestRecord[];
|
|
20
|
+
total: number;
|
|
21
|
+
}>;
|
|
22
|
+
deleteOlderThan(date: Date): Promise<number>;
|
|
23
|
+
private getPool;
|
|
24
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
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 type { StorageAdapter, RequestRecord, QueryFilters, StorageConfig } from '../types';
|
|
8
|
+
export declare class PgAdapter implements StorageAdapter {
|
|
9
|
+
private pool;
|
|
10
|
+
private readonly config;
|
|
11
|
+
constructor(config: StorageConfig);
|
|
12
|
+
initialize(): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Batch-inserts records using unnest arrays for efficient multi-row inserts.
|
|
15
|
+
*
|
|
16
|
+
* INSERT INTO requestscope_records (col1, col2, ...)
|
|
17
|
+
* SELECT * FROM unnest($1::varchar[], $2::text[], ...)
|
|
18
|
+
*/
|
|
19
|
+
insert(records: RequestRecord[]): Promise<void>;
|
|
20
|
+
query(filters: QueryFilters, pagination: {
|
|
21
|
+
page: number;
|
|
22
|
+
pageSize: number;
|
|
23
|
+
}): Promise<{
|
|
24
|
+
records: RequestRecord[];
|
|
25
|
+
total: number;
|
|
26
|
+
}>;
|
|
27
|
+
deleteOlderThan(date: Date): Promise<number>;
|
|
28
|
+
private getPool;
|
|
29
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
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 { IncomingMessage, ServerResponse } from 'http';
|
|
17
|
+
import type { RequestRecord } from './types';
|
|
18
|
+
/**
|
|
19
|
+
* Extracts the client IP address from the request.
|
|
20
|
+
*
|
|
21
|
+
* Prefers the first entry of the `X-Forwarded-For` header (proxy-aware),
|
|
22
|
+
* falling back to `req.socket.remoteAddress`. Returns `"0.0.0.0"` when
|
|
23
|
+
* neither is available.
|
|
24
|
+
*
|
|
25
|
+
* @param req Node.js IncomingMessage (or Express Request)
|
|
26
|
+
*/
|
|
27
|
+
export declare function parseClientIp(req: IncomingMessage): string;
|
|
28
|
+
/**
|
|
29
|
+
* Consumes the `req` stream and buffers up to {@link MAX_BODY_BYTES} bytes.
|
|
30
|
+
*
|
|
31
|
+
* Behaviour:
|
|
32
|
+
* 1. Collects `Buffer` chunks from `data` events; tracks total byte length.
|
|
33
|
+
* 2. On `end`: concatenates chunks, slices to MAX_BODY_BYTES; appends
|
|
34
|
+
* `"[truncated]"` when the original size exceeded the limit.
|
|
35
|
+
* 3. Attempts `JSON.parse` → stores normalised JSON string.
|
|
36
|
+
* 4. On JSON failure, attempts `URLSearchParams` parse → stores JSON map.
|
|
37
|
+
* 5. On both failures, stores the raw string as-is.
|
|
38
|
+
* 6. On stream `error`, resolves with `{ body: '', bodySize: 0, clientIp }`.
|
|
39
|
+
*
|
|
40
|
+
* @param req Node.js IncomingMessage (or Express Request)
|
|
41
|
+
* @returns Resolved promise with body string, original byte size, and IP.
|
|
42
|
+
*/
|
|
43
|
+
export declare function bufferRequestBody(req: IncomingMessage): Promise<{
|
|
44
|
+
body: string;
|
|
45
|
+
bodySize: number;
|
|
46
|
+
clientIp: string;
|
|
47
|
+
}>;
|
|
48
|
+
/**
|
|
49
|
+
* Symbol used to attach error data to the request object for the finish handler.
|
|
50
|
+
*/
|
|
51
|
+
export declare const ERROR_DATA_SYMBOL: unique symbol;
|
|
52
|
+
/**
|
|
53
|
+
* Error data attached to the request via the error middleware.
|
|
54
|
+
*/
|
|
55
|
+
export interface ErrorData {
|
|
56
|
+
message: string;
|
|
57
|
+
stack: string;
|
|
58
|
+
statusCode: number;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Wraps a ServerResponse to accumulate response chunks.
|
|
62
|
+
*
|
|
63
|
+
* Monkey-patches `res.write` and `res.end` to accumulate response body chunks.
|
|
64
|
+
* Caps the buffer at MAX_RESPONSE_BODY_BYTES; beyond that stores first
|
|
65
|
+
* MAX_RESPONSE_BODY_BYTES bytes + "[truncated]". Records responseBodySize as
|
|
66
|
+
* the original Content-Length or accumulated size.
|
|
67
|
+
*
|
|
68
|
+
* On the `finish` event, builds a RequestRecord with:
|
|
69
|
+
* - UUIDv4 id
|
|
70
|
+
* - process.hrtime-based responseTime
|
|
71
|
+
* - maskObject() applied to request headers and request body
|
|
72
|
+
* - maskObject() applied to response headers
|
|
73
|
+
* - Enqueues the record synchronously via the provided callback
|
|
74
|
+
*
|
|
75
|
+
* If wrapping fails (stream/binary response), sets responseBody = "",
|
|
76
|
+
* reads Content-Length header as responseBodySize, and calls original write/end
|
|
77
|
+
* without disrupting the response.
|
|
78
|
+
*
|
|
79
|
+
* @param res - Node.js ServerResponse (or Express Response)
|
|
80
|
+
* @param req - Node.js IncomingMessage (or Express Request)
|
|
81
|
+
* @param requestBody - Buffered request body string
|
|
82
|
+
* @param requestBodySize - Original request body byte size
|
|
83
|
+
* @param clientIp - Client IP address
|
|
84
|
+
* @param sensitiveFields - Set of sensitive field names to mask
|
|
85
|
+
* @param enqueueRecord - Callback to enqueue the RequestRecord synchronously
|
|
86
|
+
* @returns The wrapped response (same object, with patched methods)
|
|
87
|
+
*/
|
|
88
|
+
export declare function wrapResponse(res: ServerResponse, req: IncomingMessage, requestBody: string, requestBodySize: number, clientIp: string, sensitiveFields: Set<string>, enqueueRecord: (record: RequestRecord) => void): ServerResponse;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RequestScope — Configuration validation, defaults, and sensitive-field set.
|
|
3
|
+
*
|
|
4
|
+
* All validation errors are thrown synchronously so that `requestscope()` fails
|
|
5
|
+
* fast at startup before any middleware is registered.
|
|
6
|
+
*/
|
|
7
|
+
import type { RequestScopeConfig } from './types';
|
|
8
|
+
/**
|
|
9
|
+
* Fills in missing optional fields with their documented default values.
|
|
10
|
+
* Mutates the config object in place and returns it for chaining.
|
|
11
|
+
*
|
|
12
|
+
* Defaults applied:
|
|
13
|
+
* retentionDays → 30
|
|
14
|
+
* ignore → []
|
|
15
|
+
* maskFields → []
|
|
16
|
+
* storage.poolSize → 5
|
|
17
|
+
* storage.ssl → false
|
|
18
|
+
*/
|
|
19
|
+
export declare function applyDefaults(config: RequestScopeConfig): RequestScopeConfig;
|
|
20
|
+
/**
|
|
21
|
+
* Validates the configuration object and throws a synchronous `Error` for
|
|
22
|
+
* any invalid value.
|
|
23
|
+
*
|
|
24
|
+
* Checks (in order):
|
|
25
|
+
* 1. `storage.type` must be one of the supported values.
|
|
26
|
+
* 2. MongoDB requires `storage.uri`.
|
|
27
|
+
* 3. MySQL / PostgreSQL require `host`, `database`, `username`, `password`.
|
|
28
|
+
* 4. `retentionDays` (when provided) must be a positive integer in [1, 365].
|
|
29
|
+
*/
|
|
30
|
+
export declare function validateConfig(config: RequestScopeConfig): void;
|
|
31
|
+
/**
|
|
32
|
+
* Constructs the `Set<string>` of sensitive field names by forming the union
|
|
33
|
+
* of the built-in defaults and any user-supplied `maskFields`.
|
|
34
|
+
*
|
|
35
|
+
* All names are lowercased so that lookups from `maskObject()` can compare
|
|
36
|
+
* case-insensitively by lowercasing the inspected key.
|
|
37
|
+
*/
|
|
38
|
+
export declare function buildSensitiveFieldSet(config: RequestScopeConfig): Set<string>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RequestScope — Dashboard API handlers.
|
|
3
|
+
*
|
|
4
|
+
* Provides Express route handlers for the dashboard:
|
|
5
|
+
* - GET /api/records — List records with filtering, sorting, and pagination
|
|
6
|
+
* - GET /api/records/:id — Get a single record by ID
|
|
7
|
+
*
|
|
8
|
+
* Requirements: 9.1, 9.2, 9.3, 9.4, 9.5, 9.6
|
|
9
|
+
*/
|
|
10
|
+
import type { Request, Response } from 'express';
|
|
11
|
+
/**
|
|
12
|
+
* GET /api/records handler.
|
|
13
|
+
*
|
|
14
|
+
* Query parameters:
|
|
15
|
+
* - search: string (optional) — case-insensitive substring match on url
|
|
16
|
+
* - startDate: string (optional) — ISO 8601 timestamp, inclusive lower bound
|
|
17
|
+
* - endDate: string (optional) — ISO 8601 timestamp, inclusive upper bound
|
|
18
|
+
* - statusCodeGroup: string (optional) — '2xx', '3xx', '4xx', or '5xx'
|
|
19
|
+
* - statusCode: number (optional) — exact status code match
|
|
20
|
+
* - method: string (optional) — exact HTTP method match (case-insensitive)
|
|
21
|
+
* - sortBy: string (optional) — 'timestamp', 'responseTime', or 'statusCode'
|
|
22
|
+
* - sortOrder: string (optional) — 'asc' or 'desc'
|
|
23
|
+
* - page: number (optional) — page number, default 1
|
|
24
|
+
* - pageSize: number (optional) — page size, default 25
|
|
25
|
+
*
|
|
26
|
+
* Returns:
|
|
27
|
+
* - 200 with { records: RequestRecord[], total: number }
|
|
28
|
+
* - 400 for malformed query parameters
|
|
29
|
+
*/
|
|
30
|
+
export declare function getRecords(req: Request, res: Response): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* GET /api/records/:id handler.
|
|
33
|
+
*
|
|
34
|
+
* Returns:
|
|
35
|
+
* - 200 with the RequestRecord
|
|
36
|
+
* - 404 if the record is not found
|
|
37
|
+
* - 400 for malformed ID
|
|
38
|
+
*/
|
|
39
|
+
export declare function getRecordById(req: Request, res: Response): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* DELETE /api/records handler.
|
|
42
|
+
*
|
|
43
|
+
* Deletes all records from the database.
|
|
44
|
+
*
|
|
45
|
+
* Returns:
|
|
46
|
+
* - 200 with { deleted: number }
|
|
47
|
+
* - 500 for server errors
|
|
48
|
+
*/
|
|
49
|
+
export declare function deleteAllRecords(req: Request, res: Response): Promise<void>;
|