spider-watch 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +274 -0
  2. package/dist/config/defaults.d.ts +5 -0
  3. package/dist/config/defaults.d.ts.map +1 -0
  4. package/dist/config/defaults.js +70 -0
  5. package/dist/config/env-loader.d.ts +3 -0
  6. package/dist/config/env-loader.d.ts.map +1 -0
  7. package/dist/config/env-loader.js +29 -0
  8. package/dist/config/validate.d.ts +3 -0
  9. package/dist/config/validate.d.ts.map +1 -0
  10. package/dist/config/validate.js +19 -0
  11. package/dist/create-monitoring.d.ts +3 -0
  12. package/dist/create-monitoring.d.ts.map +1 -0
  13. package/dist/create-monitoring.js +73 -0
  14. package/dist/index.d.ts +4 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +2 -0
  17. package/dist/middleware/auth-basic.d.ts +4 -0
  18. package/dist/middleware/auth-basic.d.ts.map +1 -0
  19. package/dist/middleware/auth-basic.js +34 -0
  20. package/dist/middleware/capture.d.ts +7 -0
  21. package/dist/middleware/capture.d.ts.map +1 -0
  22. package/dist/middleware/capture.js +68 -0
  23. package/dist/middleware/error.d.ts +4 -0
  24. package/dist/middleware/error.d.ts.map +1 -0
  25. package/dist/middleware/error.js +27 -0
  26. package/dist/repository/monitoring-repository.d.ts +4 -0
  27. package/dist/repository/monitoring-repository.d.ts.map +1 -0
  28. package/dist/repository/monitoring-repository.js +239 -0
  29. package/dist/repository/sqlite-db.d.ts +7 -0
  30. package/dist/repository/sqlite-db.d.ts.map +1 -0
  31. package/dist/repository/sqlite-db.js +91 -0
  32. package/dist/router/async-handler.d.ts +3 -0
  33. package/dist/router/async-handler.d.ts.map +1 -0
  34. package/dist/router/async-handler.js +5 -0
  35. package/dist/router/monitoring-router.d.ts +4 -0
  36. package/dist/router/monitoring-router.d.ts.map +1 -0
  37. package/dist/router/monitoring-router.js +109 -0
  38. package/dist/services/console-hook.d.ts +12 -0
  39. package/dist/services/console-hook.d.ts.map +1 -0
  40. package/dist/services/console-hook.js +61 -0
  41. package/dist/services/context.d.ts +7 -0
  42. package/dist/services/context.d.ts.map +1 -0
  43. package/dist/services/context.js +22 -0
  44. package/dist/services/http-client.d.ts +7 -0
  45. package/dist/services/http-client.d.ts.map +1 -0
  46. package/dist/services/http-client.js +56 -0
  47. package/dist/services/instrumentation.d.ts +8 -0
  48. package/dist/services/instrumentation.d.ts.map +1 -0
  49. package/dist/services/instrumentation.js +9 -0
  50. package/dist/services/recorder.d.ts +5 -0
  51. package/dist/services/recorder.d.ts.map +1 -0
  52. package/dist/services/recorder.js +23 -0
  53. package/dist/services/retention.d.ts +12 -0
  54. package/dist/services/retention.d.ts.map +1 -0
  55. package/dist/services/retention.js +36 -0
  56. package/dist/services/runtime.d.ts +3 -0
  57. package/dist/services/runtime.d.ts.map +1 -0
  58. package/dist/services/runtime.js +9 -0
  59. package/dist/types.d.ts +133 -0
  60. package/dist/types.d.ts.map +1 -0
  61. package/dist/types.js +1 -0
  62. package/dist/ui/app.js +382 -0
  63. package/dist/ui/index.html +610 -0
  64. package/dist/utils/masking.d.ts +7 -0
  65. package/dist/utils/masking.d.ts.map +1 -0
  66. package/dist/utils/masking.js +79 -0
  67. package/package.json +71 -0
package/README.md ADDED
@@ -0,0 +1,274 @@
1
+ # spider-watch
2
+
3
+ `spider-watch` is a reusable monitoring toolkit for Node.js Express apps, designed for **API-only backends** (no server-rendered views required).
4
+
5
+ It comes from a common production need: monitor incoming API requests, exceptions, outbound HTTP calls, and operational events in one place, with a built-in dashboard UI (labeled **Spider Watch**) and JSON APIs.
6
+
7
+ ## What it provides
8
+
9
+ - Request capture (`REQUEST` events): method, path, status, duration, IP, headers/body (optional), response headers/body (optional)
10
+ - Exception capture (`EXCEPTION` events) through Express error middleware
11
+ - Outbound HTTP capture (`EXTERNAL_HTTP` events) via a monitored Axios instance
12
+ - Optional console capture (`LOG` events)
13
+ - Manual domain instrumentation (`DB_QUERY`, `JOB`, `NOTIFICATION`, `MAIL`, `CACHE_OP`)
14
+ - SQLite persistence with retention cleanup
15
+ - Built-in dashboard + API endpoints under a configurable route prefix
16
+ - Basic Auth protection for the monitoring surface
17
+ - Sensitive data masking for headers and payloads
18
+
19
+ ## Requirements
20
+
21
+ - Node.js `>=18`
22
+ - Express `^4.18.0 || ^5.0.0` (peer dependency)
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ pnpm add express-monitoring
28
+ # or: npm install express-monitoring
29
+ # or: yarn add express-monitoring
30
+ ```
31
+
32
+ ## Quick start (JavaScript)
33
+
34
+ ```js
35
+ import express from "express";
36
+ import { createMonitoring, loadMonitoringConfigFromEnv } from "express-monitoring";
37
+
38
+ const app = express();
39
+
40
+ const monitoring = createMonitoring(loadMonitoringConfigFromEnv(process.env));
41
+
42
+ app.use(express.json());
43
+
44
+ // Required order:
45
+ app.use(monitoring.captureMiddleware);
46
+ app.use(monitoring.router);
47
+ app.use(monitoring.errorMiddleware);
48
+
49
+ monitoring.start();
50
+
51
+ const server = app.listen(3000, () => {
52
+ console.log("API listening on :3000");
53
+ });
54
+
55
+ process.on("SIGTERM", () => {
56
+ monitoring.stop();
57
+ server.close(() => process.exit(0));
58
+ });
59
+ ```
60
+
61
+ ## Core integration rules
62
+
63
+ 1. Use `captureMiddleware` before your monitoring router and error middleware.
64
+ 2. Keep `errorMiddleware` after your application routes/middlewares so thrown errors are captured.
65
+ 3. Call `monitoring.start()` during bootstrap and `monitoring.stop()` during shutdown.
66
+ 4. If monitoring is enabled and auth is enabled, credentials are required.
67
+
68
+ ## Configuration
69
+
70
+ You can configure through environment variables or pass options directly.
71
+
72
+ ### Defaults
73
+
74
+ ```js
75
+ {
76
+ enabled: false,
77
+ routePrefix: "/monitoring",
78
+ dbPath: "./data/monitoring.sqlite",
79
+ retentionDays: 30,
80
+ captureLogs: false,
81
+ captureBodies: true,
82
+ sampleRate: 1,
83
+ auth: {
84
+ type: "basic",
85
+ user: "",
86
+ pass: "",
87
+ realm: "Monitoring",
88
+ enabled: true
89
+ },
90
+ masking: {
91
+ enabled: true,
92
+ sensitiveKeys: [
93
+ "password", "token", "authorization", "api_key", "access_token", "refresh_token",
94
+ "credentials", "cvv", "card_number", "ssn", "private_key"
95
+ ],
96
+ maxBodySize: 8192
97
+ }
98
+ }
99
+ ```
100
+
101
+ ### Programmatic config example
102
+
103
+ ```js
104
+ import { createMonitoring } from "express-monitoring";
105
+
106
+ const monitoring = createMonitoring({
107
+ enabled: true,
108
+ routePrefix: "/ops/monitoring",
109
+ dbPath: "./var/monitoring.sqlite",
110
+ retentionDays: 14,
111
+ captureLogs: true,
112
+ captureBodies: true,
113
+ sampleRate: 1,
114
+ auth: {
115
+ type: "basic",
116
+ enabled: true,
117
+ user: "admin",
118
+ pass: "change-me",
119
+ realm: "Spider Watch",
120
+ },
121
+ masking: {
122
+ enabled: true,
123
+ sensitiveKeys: ["authorization", "password", "token", "api_key"],
124
+ maxBodySize: 4096,
125
+ },
126
+ });
127
+ ```
128
+
129
+ ### Environment config example
130
+
131
+ ```bash
132
+ MONITORING_ENABLED=true
133
+ MONITORING_ROUTE_PREFIX=/monitoring
134
+ MONITORING_DB_PATH=./data/monitoring.sqlite
135
+ MONITORING_RETENTION_DAYS=30
136
+ MONITORING_CAPTURE_LOGS=true
137
+ MONITORING_CAPTURE_BODIES=true
138
+ MONITORING_SAMPLE_RATE=1
139
+
140
+ MONITORING_AUTH_ENABLED=true
141
+ MONITORING_AUTH_USER=admin
142
+ MONITORING_AUTH_PASS=change-me
143
+ MONITORING_AUTH_REALM="Spider Watch"
144
+
145
+ MONITORING_MASKING_ENABLED=true
146
+ MONITORING_MAX_BODY_SIZE=8192
147
+ MONITORING_SENSITIVE_KEYS=authorization,password,token,api_key
148
+ ```
149
+
150
+ ```js
151
+ import { createMonitoring, loadMonitoringConfigFromEnv } from "express-monitoring";
152
+
153
+ const monitoring = createMonitoring(loadMonitoringConfigFromEnv(process.env));
154
+ ```
155
+
156
+ ### Validation rules
157
+
158
+ - `sampleRate` must be between `0` and `1`
159
+ - `routePrefix` must start with `/`
160
+ - `retentionDays` and `masking.maxBodySize` must be positive integers
161
+ - If `enabled=true` and `auth.enabled=true`, `auth.user` and `auth.pass` are required
162
+
163
+ ## Monitoring surface
164
+
165
+ Assuming `routePrefix=/monitoring`:
166
+
167
+ - `GET /monitoring` → Spider Watch dashboard UI
168
+ - `GET /monitoring/app.js` → dashboard script
169
+ - `GET /monitoring/api/events` → paginated event list with filters
170
+ - `GET /monitoring/api/events/:id` → event detail (request/exceptions/external calls when available)
171
+ - `DELETE /monitoring/api/events` → delete all data (requires JSON body `{"scope":"all"}`)
172
+ - `GET /monitoring/api/requests` → request-only view
173
+ - `GET /monitoring/api/requests/:id` → request detail with linked external calls/exceptions
174
+
175
+ If monitoring is disabled, requests under the route prefix return HTTP `503`.
176
+
177
+ ## Event model (high-level)
178
+
179
+ - `REQUEST`: captured by `captureMiddleware`
180
+ - `EXCEPTION`: captured by `errorMiddleware`
181
+ - `EXTERNAL_HTTP`: captured by monitored Axios
182
+ - `LOG`: captured when `captureLogs=true`
183
+ - `SCHEDULED_TASK`: retention cleanup events
184
+ - Instrumentation events: `DB_QUERY`, `JOB`, `NOTIFICATION`, `MAIL`, `CACHE_OP`
185
+
186
+ Events are linked through a correlation ID generated per captured request.
187
+
188
+ ## Outbound HTTP monitoring (Axios)
189
+
190
+ Use the monitored client returned by `createMonitoredAxios()`:
191
+
192
+ ```js
193
+ const http = monitoring.createMonitoredAxios();
194
+
195
+ await http.get("https://api.example.com/v1/users", {
196
+ headers: { Authorization: "Bearer super-secret-token" },
197
+ });
198
+ ```
199
+
200
+ When monitoring is enabled, outbound requests are stored as `EXTERNAL_HTTP` with method, URL, status, duration, masked headers/body, and optional error summary.
201
+
202
+ ## Manual instrumentation
203
+
204
+ Use `monitoring.instrumentation` to emit custom operational events:
205
+
206
+ ```js
207
+ monitoring.instrumentation.recordDbQuery("SELECT users by tenant", { tenantId: "t-123" });
208
+ monitoring.instrumentation.recordJobRun("daily-report", { tookMs: 482 });
209
+ monitoring.instrumentation.recordNotification("Slack alert sent", { channel: "#ops" });
210
+ monitoring.instrumentation.recordMail("Welcome email", { userId: "u-42" });
211
+ monitoring.instrumentation.recordCacheOp("users:list cache hit", { key: "users:tenant:t-123" });
212
+ ```
213
+
214
+ ## Security and sensitive data
215
+
216
+ - Protect monitoring routes with Basic Auth in production.
217
+ - Keep credentials out of source code (prefer environment variables/secrets managers).
218
+ - Masking is enabled by default for known sensitive keys.
219
+ - Payload strings are truncated to `masking.maxBodySize`.
220
+ - Binary bodies are stored as `[binary data]`.
221
+
222
+ ## Sampling, storage, and retention
223
+
224
+ - `sampleRate` controls request capture probability (`1` = all, `0.1` = ~10%).
225
+ - Data is stored in SQLite at `dbPath`.
226
+ - Retention runs immediately on `start()` and then hourly.
227
+ - Old events are deleted according to `retentionDays`.
228
+
229
+ ## Troubleshooting
230
+
231
+ ### `auth.user and auth.pass are required when auth is enabled`
232
+
233
+ Set both credentials, or disable auth explicitly:
234
+
235
+ ```bash
236
+ MONITORING_AUTH_ENABLED=false
237
+ ```
238
+
239
+ ### Dashboard returns 503
240
+
241
+ Monitoring is disabled. Set:
242
+
243
+ ```bash
244
+ MONITORING_ENABLED=true
245
+ ```
246
+
247
+ ### No outbound HTTP events
248
+
249
+ Use `monitoring.createMonitoredAxios()` for calls you want tracked.
250
+
251
+ ### Missing request payloads
252
+
253
+ Ensure `captureBodies=true`. If disabled, bodies and captured headers are intentionally omitted.
254
+
255
+ ## Public API
256
+
257
+ ```js
258
+ import { createMonitoring, loadMonitoringConfigFromEnv } from "express-monitoring";
259
+ ```
260
+
261
+ - `createMonitoring(options?)` → monitoring instance
262
+ - `loadMonitoringConfigFromEnv(env?)` → normalized config object
263
+
264
+ Returned monitoring instance:
265
+
266
+ - `captureMiddleware`
267
+ - `router`
268
+ - `errorMiddleware`
269
+ - `start()`
270
+ - `stop()`
271
+ - `recordEvent(eventInput)`
272
+ - `instrumentation`
273
+ - `createMonitoredAxios()`
274
+ - `config`
@@ -0,0 +1,5 @@
1
+ import type { MonitoringOptions } from "../types.js";
2
+ export declare const DEFAULT_SENSITIVE_KEYS: readonly ["password", "passwd", "pass", "secret", "token", "authorization", "auth", "apikey", "api_key", "accesstoken", "access_token", "refreshtoken", "refresh_token", "credential", "credentials", "pin", "cvv", "cvc", "cardnumber", "card_number", "ssn", "private_key", "privatekey"];
3
+ export declare const DEFAULT_OPTIONS: MonitoringOptions;
4
+ export declare function normalizeOptions(partial?: Partial<MonitoringOptions>): MonitoringOptions;
5
+ //# sourceMappingURL=defaults.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../../src/config/defaults.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAErD,eAAO,MAAM,sBAAsB,6RAwBzB,CAAC;AAEX,eAAO,MAAM,eAAe,EAAE,iBAoB7B,CAAC;AASF,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,OAAO,CAAC,iBAAiB,CAAM,GAAG,iBAAiB,CAkB5F"}
@@ -0,0 +1,70 @@
1
+ export const DEFAULT_SENSITIVE_KEYS = [
2
+ "password",
3
+ "passwd",
4
+ "pass",
5
+ "secret",
6
+ "token",
7
+ "authorization",
8
+ "auth",
9
+ "apikey",
10
+ "api_key",
11
+ "accesstoken",
12
+ "access_token",
13
+ "refreshtoken",
14
+ "refresh_token",
15
+ "credential",
16
+ "credentials",
17
+ "pin",
18
+ "cvv",
19
+ "cvc",
20
+ "cardnumber",
21
+ "card_number",
22
+ "ssn",
23
+ "private_key",
24
+ "privatekey",
25
+ ];
26
+ export const DEFAULT_OPTIONS = {
27
+ enabled: false,
28
+ routePrefix: "/monitoring",
29
+ dbPath: "./data/monitoring.sqlite",
30
+ retentionDays: 30,
31
+ captureLogs: false,
32
+ captureBodies: true,
33
+ sampleRate: 1,
34
+ auth: {
35
+ type: "basic",
36
+ user: "",
37
+ pass: "",
38
+ realm: "Monitoring",
39
+ enabled: true,
40
+ },
41
+ masking: {
42
+ enabled: true,
43
+ sensitiveKeys: [...DEFAULT_SENSITIVE_KEYS],
44
+ maxBodySize: 8192,
45
+ },
46
+ };
47
+ function normalizeRoutePrefix(value) {
48
+ const trimmed = value.trim();
49
+ if (!trimmed)
50
+ return "/monitoring";
51
+ const withLeadingSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
52
+ return withLeadingSlash.replace(/\/+$/, "") || "/monitoring";
53
+ }
54
+ export function normalizeOptions(partial = {}) {
55
+ const auth = {
56
+ ...DEFAULT_OPTIONS.auth,
57
+ ...(partial.auth ?? {}),
58
+ };
59
+ const masking = {
60
+ ...DEFAULT_OPTIONS.masking,
61
+ ...(partial.masking ?? {}),
62
+ };
63
+ return {
64
+ ...DEFAULT_OPTIONS,
65
+ ...partial,
66
+ routePrefix: normalizeRoutePrefix(partial.routePrefix ?? DEFAULT_OPTIONS.routePrefix),
67
+ auth,
68
+ masking,
69
+ };
70
+ }
@@ -0,0 +1,3 @@
1
+ import type { MonitoringOptions } from "../types.js";
2
+ export declare function loadMonitoringConfigFromEnv(env?: NodeJS.ProcessEnv): MonitoringOptions;
3
+ //# sourceMappingURL=env-loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env-loader.d.ts","sourceRoot":"","sources":["../../src/config/env-loader.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAErD,wBAAgB,2BAA2B,CACzC,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,iBAAiB,CA6BnB"}
@@ -0,0 +1,29 @@
1
+ import { normalizeOptions } from "./defaults.js";
2
+ export function loadMonitoringConfigFromEnv(env = process.env) {
3
+ const masking = {
4
+ enabled: env.MONITORING_MASKING_ENABLED !== "false",
5
+ maxBodySize: parseInt(env.MONITORING_MAX_BODY_SIZE ?? "8192", 10),
6
+ };
7
+ if (env.MONITORING_SENSITIVE_KEYS) {
8
+ masking.sensitiveKeys = env.MONITORING_SENSITIVE_KEYS.split(",")
9
+ .map((v) => v.trim())
10
+ .filter(Boolean);
11
+ }
12
+ return normalizeOptions({
13
+ enabled: env.MONITORING_ENABLED === "true",
14
+ routePrefix: env.MONITORING_ROUTE_PREFIX ?? "/monitoring",
15
+ dbPath: env.MONITORING_DB_PATH ?? "./data/monitoring.sqlite",
16
+ retentionDays: parseInt(env.MONITORING_RETENTION_DAYS ?? "30", 10),
17
+ captureLogs: env.MONITORING_CAPTURE_LOGS === "true",
18
+ captureBodies: env.MONITORING_CAPTURE_BODIES !== "false",
19
+ sampleRate: parseFloat(env.MONITORING_SAMPLE_RATE ?? "1"),
20
+ auth: {
21
+ type: "basic",
22
+ user: env.MONITORING_AUTH_USER ?? "",
23
+ pass: env.MONITORING_AUTH_PASS ?? "",
24
+ realm: env.MONITORING_AUTH_REALM ?? "Monitoring",
25
+ enabled: env.MONITORING_AUTH_ENABLED !== "false",
26
+ },
27
+ masking,
28
+ });
29
+ }
@@ -0,0 +1,3 @@
1
+ import type { MonitoringOptions } from "../types.js";
2
+ export declare function validateOptions(options: MonitoringOptions): void;
3
+ //# sourceMappingURL=validate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/config/validate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAErD,wBAAgB,eAAe,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI,CAsBhE"}
@@ -0,0 +1,19 @@
1
+ export function validateOptions(options) {
2
+ if (!Number.isFinite(options.sampleRate) || options.sampleRate < 0 || options.sampleRate > 1) {
3
+ throw new Error("[monitoring] sampleRate must be a number between 0 and 1");
4
+ }
5
+ if (!options.routePrefix || !options.routePrefix.startsWith("/")) {
6
+ throw new Error("[monitoring] routePrefix must start with '/'");
7
+ }
8
+ if (!Number.isInteger(options.retentionDays) || options.retentionDays <= 0) {
9
+ throw new Error("[monitoring] retentionDays must be a positive integer");
10
+ }
11
+ if (!Number.isInteger(options.masking.maxBodySize) || options.masking.maxBodySize <= 0) {
12
+ throw new Error("[monitoring] masking.maxBodySize must be a positive integer");
13
+ }
14
+ if (options.enabled && options.auth.enabled) {
15
+ if (!options.auth.user || !options.auth.pass) {
16
+ throw new Error("[monitoring] auth.user and auth.pass are required when auth is enabled");
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,3 @@
1
+ import type { MonitoringInstance, MonitoringOptions } from "./types.js";
2
+ export declare function createMonitoring(options?: Partial<MonitoringOptions>): MonitoringInstance;
3
+ //# sourceMappingURL=create-monitoring.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-monitoring.d.ts","sourceRoot":"","sources":["../src/create-monitoring.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAGxE,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,OAAO,CAAC,iBAAiB,CAAM,GAAG,kBAAkB,CAiE7F"}
@@ -0,0 +1,73 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { normalizeOptions } from "./config/defaults.js";
4
+ import { validateOptions } from "./config/validate.js";
5
+ import { createBasicAuthMiddleware } from "./middleware/auth-basic.js";
6
+ import { createCaptureMiddleware } from "./middleware/capture.js";
7
+ import { createErrorMiddleware } from "./middleware/error.js";
8
+ import { createMonitoringRepository } from "./repository/monitoring-repository.js";
9
+ import { createSqliteDb } from "./repository/sqlite-db.js";
10
+ import { createMonitoringRouter } from "./router/monitoring-router.js";
11
+ import { createConsoleHookService } from "./services/console-hook.js";
12
+ import { getCorrelationId } from "./services/context.js";
13
+ import { createMonitoredAxios } from "./services/http-client.js";
14
+ import { createInstrumentation } from "./services/instrumentation.js";
15
+ import { createRecorder } from "./services/recorder.js";
16
+ import { createRetentionService } from "./services/retention.js";
17
+ import { createRuntimeState } from "./services/runtime.js";
18
+ import { createMaskingUtils } from "./utils/masking.js";
19
+ export function createMonitoring(options = {}) {
20
+ const config = normalizeOptions(options);
21
+ validateOptions(config);
22
+ const dbAdapter = createSqliteDb(config.dbPath);
23
+ const repository = createMonitoringRepository(dbAdapter);
24
+ const runtime = createRuntimeState();
25
+ const masking = createMaskingUtils(config.masking);
26
+ const authMiddleware = createBasicAuthMiddleware(config);
27
+ const recorder = createRecorder(repository, getCorrelationId);
28
+ const retention = createRetentionService(config, repository, recorder.recordEvent);
29
+ const consoleHook = createConsoleHookService(config, recorder.recordEvent);
30
+ const instrumentation = createInstrumentation(recorder.recordEvent);
31
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
32
+ const uiDir = path.join(__dirname, "ui");
33
+ const captureMiddleware = createCaptureMiddleware(config, repository, masking);
34
+ const errorMiddleware = createErrorMiddleware(config, repository);
35
+ const router = createMonitoringRouter(config, repository, authMiddleware, uiDir);
36
+ function start() {
37
+ if (runtime.status === "running")
38
+ return;
39
+ runtime.status = "running";
40
+ runtime.startedAt = new Date().toISOString();
41
+ if (!config.enabled)
42
+ return;
43
+ dbAdapter.getDb();
44
+ runtime.dbConnected = true;
45
+ consoleHook.install();
46
+ runtime.consoleHookActive = consoleHook.isInstalled();
47
+ retention.start();
48
+ runtime.retentionJobActive = retention.isActive();
49
+ }
50
+ function stop() {
51
+ if (runtime.status === "stopped" || runtime.status === "stopping")
52
+ return;
53
+ runtime.status = "stopping";
54
+ retention.stop();
55
+ runtime.retentionJobActive = false;
56
+ consoleHook.uninstall();
57
+ runtime.consoleHookActive = false;
58
+ dbAdapter.closeDb();
59
+ runtime.dbConnected = false;
60
+ runtime.status = "stopped";
61
+ }
62
+ return {
63
+ captureMiddleware,
64
+ errorMiddleware,
65
+ router,
66
+ start,
67
+ stop,
68
+ recordEvent: recorder.recordEvent,
69
+ instrumentation,
70
+ createMonitoredAxios: () => createMonitoredAxios(config, repository, masking, getCorrelationId),
71
+ config,
72
+ };
73
+ }
@@ -0,0 +1,4 @@
1
+ export { createMonitoring } from "./create-monitoring.js";
2
+ export { loadMonitoringConfigFromEnv } from "./config/env-loader.js";
3
+ export type { MonitoringAuthOptions, MonitoringEvent, MonitoringEventInput, MonitoringInstance, MonitoringMaskingOptions, MonitoringOptions, MonitoringRuntime, } from "./types.js";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,2BAA2B,EAAE,MAAM,wBAAwB,CAAC;AAErE,YAAY,EACV,qBAAqB,EACrB,eAAe,EACf,oBAAoB,EACpB,kBAAkB,EAClB,wBAAwB,EACxB,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { createMonitoring } from "./create-monitoring.js";
2
+ export { loadMonitoringConfigFromEnv } from "./config/env-loader.js";
@@ -0,0 +1,4 @@
1
+ import type { RequestHandler } from "express";
2
+ import type { MonitoringOptions } from "../types.js";
3
+ export declare function createBasicAuthMiddleware(config: MonitoringOptions): RequestHandler;
4
+ //# sourceMappingURL=auth-basic.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-basic.d.ts","sourceRoot":"","sources":["../../src/middleware/auth-basic.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAQrD,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,iBAAiB,GAAG,cAAc,CA8BnF"}
@@ -0,0 +1,34 @@
1
+ import { createHash, timingSafeEqual } from "node:crypto";
2
+ function safeCompare(a, b) {
3
+ const ha = createHash("sha256").update(a).digest();
4
+ const hb = createHash("sha256").update(b).digest();
5
+ return timingSafeEqual(ha, hb);
6
+ }
7
+ export function createBasicAuthMiddleware(config) {
8
+ return (req, res, next) => {
9
+ if (!config.auth.enabled)
10
+ return next();
11
+ const authHeader = req.headers.authorization ?? "";
12
+ if (!authHeader.startsWith("Basic ")) {
13
+ res.set("WWW-Authenticate", `Basic realm="${config.auth.realm}"`);
14
+ return res.status(401).json({ error: "Authentication required" });
15
+ }
16
+ try {
17
+ const decoded = Buffer.from(authHeader.slice(6), "base64").toString("utf8");
18
+ const colonIndex = decoded.indexOf(":");
19
+ const user = decoded.substring(0, colonIndex);
20
+ const pass = decoded.substring(colonIndex + 1);
21
+ const validUser = safeCompare(user, config.auth.user);
22
+ const validPass = safeCompare(pass, config.auth.pass);
23
+ if (!validUser || !validPass) {
24
+ res.set("WWW-Authenticate", `Basic realm="${config.auth.realm}"`);
25
+ return res.status(401).json({ error: "Invalid credentials" });
26
+ }
27
+ return next();
28
+ }
29
+ catch {
30
+ res.set("WWW-Authenticate", `Basic realm="${config.auth.realm}"`);
31
+ return res.status(401).json({ error: "Invalid credentials" });
32
+ }
33
+ };
34
+ }
@@ -0,0 +1,7 @@
1
+ import type { RequestHandler } from "express";
2
+ import type { MonitoringOptions, MonitoringRepository } from "../types.js";
3
+ export declare function createCaptureMiddleware(config: MonitoringOptions, repository: MonitoringRepository, masking: {
4
+ maskBody: (body: unknown) => string | null;
5
+ maskHeaders: (headers: unknown) => Record<string, unknown>;
6
+ }): RequestHandler;
7
+ //# sourceMappingURL=capture.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capture.d.ts","sourceRoot":"","sources":["../../src/middleware/capture.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAG9C,OAAO,KAAK,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAE3E,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,iBAAiB,EACzB,UAAU,EAAE,oBAAoB,EAChC,OAAO,EAAE;IACP,QAAQ,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,MAAM,GAAG,IAAI,CAAC;IAC3C,WAAW,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC5D,GACA,cAAc,CA+EhB"}
@@ -0,0 +1,68 @@
1
+ import { createContext, runWithContext } from "../services/context.js";
2
+ export function createCaptureMiddleware(config, repository, masking) {
3
+ return (req, res, next) => {
4
+ if (!config.enabled)
5
+ return next();
6
+ if (Math.random() > config.sampleRate)
7
+ return next();
8
+ if (req.path.startsWith(config.routePrefix)) {
9
+ return next();
10
+ }
11
+ const context = createContext();
12
+ const startTime = Date.now();
13
+ const responseChunks = [];
14
+ const originalWrite = res.write.bind(res);
15
+ const originalEnd = res.end.bind(res);
16
+ res.write = ((chunk, ...args) => {
17
+ if (chunk) {
18
+ responseChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
19
+ }
20
+ return originalWrite(chunk, ...args);
21
+ });
22
+ res.end = ((chunk, ...args) => {
23
+ if (chunk) {
24
+ responseChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
25
+ }
26
+ return originalEnd(chunk, ...args);
27
+ });
28
+ runWithContext(context, () => {
29
+ res.on("finish", () => {
30
+ try {
31
+ const durationMs = Date.now() - startTime;
32
+ const rawResponseBody = responseChunks.length
33
+ ? Buffer.concat(responseChunks).toString("utf8")
34
+ : null;
35
+ const requestBody = config.captureBodies ? masking.maskBody(req.body) : null;
36
+ const responseBody = config.captureBodies ? masking.maskBody(rawResponseBody) : null;
37
+ const ipAddress = req.headers["x-forwarded-for"]?.toString().split(",")[0]?.trim() ??
38
+ req.socket.remoteAddress ??
39
+ null;
40
+ const summary = `${req.method} ${req.path} ${res.statusCode}`;
41
+ const severity = res.statusCode >= 500 ? "ERROR" : res.statusCode >= 400 ? "WARN" : "INFO";
42
+ const eventId = repository.insertEvent({
43
+ eventType: "REQUEST",
44
+ severity,
45
+ correlationId: context.correlationId,
46
+ summary,
47
+ data: { requestId: context.requestId },
48
+ });
49
+ repository.insertRequest(eventId, {
50
+ method: req.method,
51
+ path: req.path,
52
+ statusCode: res.statusCode,
53
+ durationMs,
54
+ ipAddress,
55
+ requestHeaders: config.captureBodies ? masking.maskHeaders(req.headers) : {},
56
+ requestBody,
57
+ responseHeaders: config.captureBodies ? masking.maskHeaders(res.getHeaders()) : {},
58
+ responseBody,
59
+ });
60
+ }
61
+ catch (err) {
62
+ console.error("[monitoring] Failed to capture request:", err instanceof Error ? err.message : String(err));
63
+ }
64
+ });
65
+ next();
66
+ });
67
+ };
68
+ }
@@ -0,0 +1,4 @@
1
+ import type { ErrorRequestHandler } from "express";
2
+ import type { MonitoringOptions, MonitoringRepository } from "../types.js";
3
+ export declare function createErrorMiddleware(config: MonitoringOptions, repository: MonitoringRepository): ErrorRequestHandler;
4
+ //# sourceMappingURL=error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../../src/middleware/error.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAGnD,OAAO,KAAK,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAE3E,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,iBAAiB,EACzB,UAAU,EAAE,oBAAoB,GAC/B,mBAAmB,CA6BrB"}