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.
Files changed (75) hide show
  1. package/README.md +275 -0
  2. package/dist/cjs/adapters/adapter.interface.js +9 -0
  3. package/dist/cjs/adapters/adapter.interface.js.map +1 -0
  4. package/dist/cjs/adapters/mongo.adapter.js +188 -0
  5. package/dist/cjs/adapters/mongo.adapter.js.map +1 -0
  6. package/dist/cjs/adapters/mysql.adapter.js +243 -0
  7. package/dist/cjs/adapters/mysql.adapter.js.map +1 -0
  8. package/dist/cjs/adapters/pg.adapter.js +334 -0
  9. package/dist/cjs/adapters/pg.adapter.js.map +1 -0
  10. package/dist/cjs/capture.js +310 -0
  11. package/dist/cjs/capture.js.map +1 -0
  12. package/dist/cjs/config.js +122 -0
  13. package/dist/cjs/config.js.map +1 -0
  14. package/dist/cjs/dashboard/api.js +173 -0
  15. package/dist/cjs/dashboard/api.js.map +1 -0
  16. package/dist/cjs/dashboard/router.js +96 -0
  17. package/dist/cjs/dashboard/router.js.map +1 -0
  18. package/dist/cjs/index.js +49 -0
  19. package/dist/cjs/index.js.map +1 -0
  20. package/dist/cjs/masker.js +73 -0
  21. package/dist/cjs/masker.js.map +1 -0
  22. package/dist/cjs/middleware.js +198 -0
  23. package/dist/cjs/middleware.js.map +1 -0
  24. package/dist/cjs/queue.js +114 -0
  25. package/dist/cjs/queue.js.map +1 -0
  26. package/dist/cjs/retention.js +64 -0
  27. package/dist/cjs/retention.js.map +1 -0
  28. package/dist/cjs/types.js +9 -0
  29. package/dist/cjs/types.js.map +1 -0
  30. package/dist/dashboard/assets/index-C0TqFHk6.css +1 -0
  31. package/dist/dashboard/assets/index-MCuAZo4Q.js +67 -0
  32. package/dist/dashboard/index.html +13 -0
  33. package/dist/esm/adapters/adapter.interface.js +8 -0
  34. package/dist/esm/adapters/adapter.interface.js.map +1 -0
  35. package/dist/esm/adapters/mongo.adapter.js +184 -0
  36. package/dist/esm/adapters/mongo.adapter.js.map +1 -0
  37. package/dist/esm/adapters/mysql.adapter.js +236 -0
  38. package/dist/esm/adapters/mysql.adapter.js.map +1 -0
  39. package/dist/esm/adapters/pg.adapter.js +330 -0
  40. package/dist/esm/adapters/pg.adapter.js.map +1 -0
  41. package/dist/esm/capture.js +304 -0
  42. package/dist/esm/capture.js.map +1 -0
  43. package/dist/esm/config.js +117 -0
  44. package/dist/esm/config.js.map +1 -0
  45. package/dist/esm/dashboard/api.js +168 -0
  46. package/dist/esm/dashboard/api.js.map +1 -0
  47. package/dist/esm/dashboard/router.js +90 -0
  48. package/dist/esm/dashboard/router.js.map +1 -0
  49. package/dist/esm/index.js +50 -0
  50. package/dist/esm/index.js.map +1 -0
  51. package/dist/esm/masker.js +70 -0
  52. package/dist/esm/masker.js.map +1 -0
  53. package/dist/esm/middleware.js +193 -0
  54. package/dist/esm/middleware.js.map +1 -0
  55. package/dist/esm/queue.js +110 -0
  56. package/dist/esm/queue.js.map +1 -0
  57. package/dist/esm/retention.js +60 -0
  58. package/dist/esm/retention.js.map +1 -0
  59. package/dist/esm/types.js +8 -0
  60. package/dist/esm/types.js.map +1 -0
  61. package/dist/types/adapters/adapter.interface.d.ts +7 -0
  62. package/dist/types/adapters/mongo.adapter.d.ts +25 -0
  63. package/dist/types/adapters/mysql.adapter.d.ts +24 -0
  64. package/dist/types/adapters/pg.adapter.d.ts +29 -0
  65. package/dist/types/capture.d.ts +88 -0
  66. package/dist/types/config.d.ts +38 -0
  67. package/dist/types/dashboard/api.d.ts +49 -0
  68. package/dist/types/dashboard/router.d.ts +28 -0
  69. package/dist/types/index.d.ts +31 -0
  70. package/dist/types/masker.d.ts +15 -0
  71. package/dist/types/middleware.d.ts +67 -0
  72. package/dist/types/queue.d.ts +49 -0
  73. package/dist/types/retention.d.ts +30 -0
  74. package/dist/types/types.d.ts +101 -0
  75. package/package.json +48 -0
@@ -0,0 +1,117 @@
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
+ // ---------------------------------------------------------------------------
8
+ // Constants
9
+ // ---------------------------------------------------------------------------
10
+ const SUPPORTED_STORAGE_TYPES = ['mongodb', 'mysql', 'postgresql'];
11
+ const SQL_REQUIRED_FIELDS = [
12
+ 'host',
13
+ 'database',
14
+ 'username',
15
+ 'password',
16
+ ];
17
+ /** Built-in sensitive field names (all compared case-insensitively). */
18
+ const DEFAULT_SENSITIVE_FIELDS = [
19
+ 'password',
20
+ 'token',
21
+ 'authorization',
22
+ 'apiKey',
23
+ 'secret',
24
+ ];
25
+ // ---------------------------------------------------------------------------
26
+ // applyDefaults
27
+ // ---------------------------------------------------------------------------
28
+ /**
29
+ * Fills in missing optional fields with their documented default values.
30
+ * Mutates the config object in place and returns it for chaining.
31
+ *
32
+ * Defaults applied:
33
+ * retentionDays → 30
34
+ * ignore → []
35
+ * maskFields → []
36
+ * storage.poolSize → 5
37
+ * storage.ssl → false
38
+ */
39
+ export function applyDefaults(config) {
40
+ if (config.retentionDays === undefined) {
41
+ config.retentionDays = 30;
42
+ }
43
+ if (config.ignore === undefined) {
44
+ config.ignore = ['/requestscope/api', '/requestscope/assets'];
45
+ }
46
+ if (config.maskFields === undefined) {
47
+ config.maskFields = [];
48
+ }
49
+ if (config.storage.poolSize === undefined) {
50
+ config.storage.poolSize = 5;
51
+ }
52
+ if (config.storage.ssl === undefined) {
53
+ config.storage.ssl = false;
54
+ }
55
+ return config;
56
+ }
57
+ // ---------------------------------------------------------------------------
58
+ // validateConfig
59
+ // ---------------------------------------------------------------------------
60
+ /**
61
+ * Validates the configuration object and throws a synchronous `Error` for
62
+ * any invalid value.
63
+ *
64
+ * Checks (in order):
65
+ * 1. `storage.type` must be one of the supported values.
66
+ * 2. MongoDB requires `storage.uri`.
67
+ * 3. MySQL / PostgreSQL require `host`, `database`, `username`, `password`.
68
+ * 4. `retentionDays` (when provided) must be a positive integer in [1, 365].
69
+ */
70
+ export function validateConfig(config) {
71
+ const { storage, retentionDays } = config;
72
+ // 1. Validate storage.type
73
+ if (!SUPPORTED_STORAGE_TYPES.includes(storage.type)) {
74
+ throw new Error(`Invalid storage type: '${storage.type}'. Supported values are: mongodb, mysql, postgresql`);
75
+ }
76
+ // 2. MongoDB: uri is required
77
+ if (storage.type === 'mongodb') {
78
+ if (!storage.uri) {
79
+ throw new Error("storage.uri is required when storage.type is 'mongodb'");
80
+ }
81
+ }
82
+ // 3. MySQL / PostgreSQL: all SQL required fields must be present
83
+ if (storage.type === 'mysql' || storage.type === 'postgresql') {
84
+ const missing = SQL_REQUIRED_FIELDS.filter((field) => storage[field] === undefined || storage[field] === null);
85
+ if (missing.length > 0) {
86
+ throw new Error(`Missing required storage fields: ${missing.join(', ')}`);
87
+ }
88
+ }
89
+ // 4. retentionDays: must be a positive integer in [1, 365] when provided
90
+ if (retentionDays !== undefined) {
91
+ const isValidInteger = typeof retentionDays === 'number' &&
92
+ Number.isInteger(retentionDays) &&
93
+ retentionDays >= 1 &&
94
+ retentionDays <= 365;
95
+ if (!isValidInteger) {
96
+ throw new Error(`retentionDays must be a positive integer between 1 and 365, received: ${retentionDays}`);
97
+ }
98
+ }
99
+ }
100
+ // ---------------------------------------------------------------------------
101
+ // buildSensitiveFieldSet
102
+ // ---------------------------------------------------------------------------
103
+ /**
104
+ * Constructs the `Set<string>` of sensitive field names by forming the union
105
+ * of the built-in defaults and any user-supplied `maskFields`.
106
+ *
107
+ * All names are lowercased so that lookups from `maskObject()` can compare
108
+ * case-insensitively by lowercasing the inspected key.
109
+ */
110
+ export function buildSensitiveFieldSet(config) {
111
+ const names = [
112
+ ...DEFAULT_SENSITIVE_FIELDS,
113
+ ...(config.maskFields ?? []),
114
+ ];
115
+ return new Set(names.map((n) => n.toLowerCase()));
116
+ }
117
+ //# 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,168 @@
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
+ // ---------------------------------------------------------------------------
11
+ // Helper functions
12
+ // ---------------------------------------------------------------------------
13
+ /**
14
+ * Parses a query parameter as an integer with a default value.
15
+ */
16
+ function parseIntParam(value, defaultValue, min = 1) {
17
+ if (value === undefined) {
18
+ return defaultValue;
19
+ }
20
+ const parsed = parseInt(value, 10);
21
+ if (isNaN(parsed) || parsed < min) {
22
+ return defaultValue;
23
+ }
24
+ return parsed;
25
+ }
26
+ /**
27
+ * Validates that a value is one of the allowed options.
28
+ */
29
+ function validateOption(value, allowed, defaultValue) {
30
+ if (value === undefined) {
31
+ return defaultValue;
32
+ }
33
+ return allowed.includes(value) ? value : defaultValue;
34
+ }
35
+ // ---------------------------------------------------------------------------
36
+ // Route handlers
37
+ // ---------------------------------------------------------------------------
38
+ /**
39
+ * GET /api/records handler.
40
+ *
41
+ * Query parameters:
42
+ * - search: string (optional) — case-insensitive substring match on url
43
+ * - startDate: string (optional) — ISO 8601 timestamp, inclusive lower bound
44
+ * - endDate: string (optional) — ISO 8601 timestamp, inclusive upper bound
45
+ * - statusCodeGroup: string (optional) — '2xx', '3xx', '4xx', or '5xx'
46
+ * - statusCode: number (optional) — exact status code match
47
+ * - method: string (optional) — exact HTTP method match (case-insensitive)
48
+ * - sortBy: string (optional) — 'timestamp', 'responseTime', or 'statusCode'
49
+ * - sortOrder: string (optional) — 'asc' or 'desc'
50
+ * - page: number (optional) — page number, default 1
51
+ * - pageSize: number (optional) — page size, default 25
52
+ *
53
+ * Returns:
54
+ * - 200 with { records: RequestRecord[], total: number }
55
+ * - 400 for malformed query parameters
56
+ */
57
+ export async function getRecords(req, res) {
58
+ try {
59
+ const adapter = req.app.get('requestscopeAdapter');
60
+ if (!adapter) {
61
+ res.status(500).json({ error: 'Storage adapter not configured' });
62
+ return;
63
+ }
64
+ // Parse and validate query parameters
65
+ const page = parseIntParam(req.query.page, 1);
66
+ const pageSize = parseIntParam(req.query.pageSize, 25);
67
+ const sortBy = validateOption(req.query.sortBy, ['timestamp', 'responseTime', 'statusCode'], 'timestamp');
68
+ const sortOrder = validateOption(req.query.sortOrder, ['asc', 'desc'], 'desc');
69
+ // Build QueryFilters
70
+ const filters = {
71
+ search: req.query.search,
72
+ startDate: req.query.startDate,
73
+ endDate: req.query.endDate,
74
+ statusCodeGroup: (() => {
75
+ const value = req.query.statusCodeGroup;
76
+ if (!value)
77
+ return undefined;
78
+ if (['2xx', '3xx', '4xx', '5xx'].includes(value)) {
79
+ return value;
80
+ }
81
+ return undefined;
82
+ })(),
83
+ statusCode: req.query.statusCode
84
+ ? parseIntParam(req.query.statusCode, 0)
85
+ : undefined,
86
+ method: req.query.method,
87
+ sortBy,
88
+ sortOrder,
89
+ page,
90
+ pageSize,
91
+ };
92
+ // Query the adapter
93
+ const result = await adapter.query(filters, { page, pageSize });
94
+ res.status(200).json(result);
95
+ }
96
+ catch (err) {
97
+ const message = err instanceof Error ? err.message : String(err);
98
+ res.status(400).json({ error: message });
99
+ }
100
+ }
101
+ /**
102
+ * GET /api/records/:id handler.
103
+ *
104
+ * Returns:
105
+ * - 200 with the RequestRecord
106
+ * - 404 if the record is not found
107
+ * - 400 for malformed ID
108
+ */
109
+ export async function getRecordById(req, res) {
110
+ try {
111
+ const adapter = req.app.get('requestscopeAdapter');
112
+ if (!adapter) {
113
+ res.status(500).json({ error: 'Storage adapter not configured' });
114
+ return;
115
+ }
116
+ const id = req.params.id;
117
+ if (!id) {
118
+ res.status(400).json({ error: 'Record ID is required' });
119
+ return;
120
+ }
121
+ // Query for the specific record using id filter
122
+ const filters = {
123
+ id,
124
+ page: 1,
125
+ pageSize: 1,
126
+ };
127
+ const result = await adapter.query(filters, { page: 1, pageSize: 1 });
128
+ const record = result.records[0];
129
+ if (!record) {
130
+ res.status(404).json({ error: 'Record not found' });
131
+ return;
132
+ }
133
+ res.status(200).json(record);
134
+ }
135
+ catch (err) {
136
+ const message = err instanceof Error ? err.message : String(err);
137
+ res.status(400).json({ error: message });
138
+ }
139
+ }
140
+ /**
141
+ * DELETE /api/records handler.
142
+ *
143
+ * Deletes all records from the database.
144
+ *
145
+ * Returns:
146
+ * - 200 with { deleted: number }
147
+ * - 500 for server errors
148
+ */
149
+ export async function deleteAllRecords(req, res) {
150
+ try {
151
+ const adapter = req.app.get('requestscopeAdapter');
152
+ if (!adapter) {
153
+ res.status(500).json({ error: 'Storage adapter not configured' });
154
+ return;
155
+ }
156
+ // Query to get total count before deletion
157
+ const allRecords = await adapter.query({ page: 1, pageSize: 1000000 }, { page: 1, pageSize: 1000000 });
158
+ const totalToDelete = allRecords.total;
159
+ // Delete all records by setting retention to a future date
160
+ await adapter.deleteOlderThan(new Date(Date.now() + 86400000));
161
+ res.status(200).json({ deleted: totalToDelete });
162
+ }
163
+ catch (err) {
164
+ const message = err instanceof Error ? err.message : String(err);
165
+ res.status(500).json({ error: message });
166
+ }
167
+ }
168
+ //# 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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"}
@@ -0,0 +1,90 @@
1
+ /**
2
+ * RequestScope — Dashboard router.
3
+ *
4
+ * Provides an Express router with:
5
+ * - Optional BasicAuth middleware
6
+ * - API routes for querying records
7
+ * - Static file serving for the React SPA
8
+ * - SPA fallback for client-side routing
9
+ *
10
+ * Requirements: 8.1, 8.2, 8.3, 8.4
11
+ */
12
+ import { Router, static as expressStatic } from 'express';
13
+ import path from 'path';
14
+ import { getRecords, getRecordById, deleteAllRecords } from './api';
15
+ // ---------------------------------------------------------------------------
16
+ // BasicAuth middleware
17
+ // ---------------------------------------------------------------------------
18
+ /**
19
+ * Creates a BasicAuth middleware if auth config is provided.
20
+ *
21
+ * Checks the Authorization header for Basic auth credentials.
22
+ * If credentials are missing or don't match, responds with 401 and
23
+ * WWW-Authenticate header.
24
+ */
25
+ function createBasicAuthMiddleware(auth) {
26
+ if (!auth) {
27
+ return (_req, _res, next) => next();
28
+ }
29
+ const { username, password } = auth;
30
+ const expectedAuth = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
31
+ return (req, res, next) => {
32
+ const authHeader = req.headers.authorization;
33
+ if (!authHeader || authHeader !== expectedAuth) {
34
+ res.setHeader('WWW-Authenticate', 'Basic realm="RequestScope"');
35
+ res.status(401).json({ error: 'Unauthorized' });
36
+ return;
37
+ }
38
+ next();
39
+ };
40
+ }
41
+ // ---------------------------------------------------------------------------
42
+ // Dashboard router factory
43
+ // ---------------------------------------------------------------------------
44
+ /**
45
+ * Creates and returns an Express router for the RequestScope dashboard.
46
+ *
47
+ * The router includes:
48
+ * - BasicAuth middleware (if auth config is provided)
49
+ * - GET /api/records — List records with filtering and pagination
50
+ * - GET /api/records/:id — Get a single record by ID
51
+ * - Static file serving for the React SPA
52
+ * - SPA fallback for client-side routing
53
+ *
54
+ * @param config - Optional dashboard configuration
55
+ * @param adapter - Storage adapter instance (must be set on the app)
56
+ * @returns Express Router
57
+ */
58
+ export function createDashboardRouter(config, adapter) {
59
+ const router = Router();
60
+ // Install BasicAuth middleware if auth config is present
61
+ const authMiddleware = createBasicAuthMiddleware(config?.auth);
62
+ router.use(authMiddleware);
63
+ // Set the adapter on the app for API handlers to access
64
+ router.use((req, res, next) => {
65
+ if (adapter) {
66
+ req.app.set('requestscopeAdapter', adapter);
67
+ }
68
+ next();
69
+ });
70
+ // Mount API routes
71
+ router.get('/api/records', getRecords);
72
+ router.get('/api/records/:id', getRecordById);
73
+ router.delete('/api/records', deleteAllRecords);
74
+ // Serve static files from the dashboard bundle
75
+ // The dashboard is built to dist/dashboard
76
+ // Resolve from project root regardless of where this file is
77
+ const dashboardPath = path.resolve(process.cwd(), 'dist/dashboard');
78
+ // Serve static files from the built dashboard
79
+ router.use(expressStatic(dashboardPath));
80
+ // Explicitly serve index.html for the root path
81
+ router.get('/', (req, res) => {
82
+ res.sendFile(path.join(dashboardPath, 'index.html'));
83
+ });
84
+ // SPA fallback for client-side routing (must be after static files)
85
+ router.get('*', (req, res) => {
86
+ res.sendFile(path.join(dashboardPath, 'index.html'));
87
+ });
88
+ return router;
89
+ }
90
+ //# sourceMappingURL=router.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router.js","sourceRoot":"","sources":["../../../src/dashboard/router.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * RequestScope — Public API entry point.
3
+ *
4
+ * Exports:
5
+ * - Default export: requestscope(config) middleware factory
6
+ * - requestscope.dashboard: dashboard router factory
7
+ * - All public TypeScript interfaces
8
+ *
9
+ * Requirements: 1.1, 12.1, 12.2, 12.3, 12.4
10
+ */
11
+ import { requestscope as requestscopeMiddleware, errorHandler, setup } from './middleware';
12
+ import { createDashboardRouter } from './dashboard/router';
13
+ // ---------------------------------------------------------------------------
14
+ // Main middleware factory
15
+ // ---------------------------------------------------------------------------
16
+ /**
17
+ * Creates and returns an Express middleware that captures HTTP requests.
18
+ *
19
+ * @param config - RequestScope configuration object
20
+ * @returns Express RequestHandler middleware
21
+ */
22
+ export default requestscopeMiddleware;
23
+ // ---------------------------------------------------------------------------
24
+ // Dashboard router factory
25
+ // ---------------------------------------------------------------------------
26
+ /**
27
+ * Creates and returns an Express router for the RequestScope dashboard.
28
+ *
29
+ * @param config - Optional dashboard configuration
30
+ * @param adapter - Optional storage adapter instance
31
+ * @returns Express Router
32
+ */
33
+ export function dashboard(config, adapter) {
34
+ return createDashboardRouter(config, adapter);
35
+ }
36
+ // Attach dashboard as a named property on the default export for convenience
37
+ requestscopeMiddleware.dashboard = dashboard;
38
+ // ---------------------------------------------------------------------------
39
+ // Setup function
40
+ // ---------------------------------------------------------------------------
41
+ export { setup };
42
+ // ---------------------------------------------------------------------------
43
+ // Error middleware
44
+ // ---------------------------------------------------------------------------
45
+ export { errorHandler };
46
+ // ---------------------------------------------------------------------------
47
+ // Named exports for convenience
48
+ // ---------------------------------------------------------------------------
49
+ export { requestscope as requestscopeMiddleware } from './middleware';
50
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EAAE,YAAY,IAAI,sBAAsB,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAC3F,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAW3D,8EAA8E;AAC9E,0BAA0B;AAC1B,8EAA8E;AAE9E;;;;;GAKG;AACH,eAAe,sBAAsB,CAAC;AAEtC,8EAA8E;AAC9E,2BAA2B;AAC3B,8EAA8E;AAE9E;;;;;;GAMG;AACH,MAAM,UAAU,SAAS,CAAC,MAAwB,EAAE,OAAwB;IAC1E,OAAO,qBAAqB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAChD,CAAC;AAED,6EAA6E;AAC5E,sBAA8B,CAAC,SAAS,GAAG,SAAS,CAAC;AAEtD,8EAA8E;AAC9E,iBAAiB;AACjB,8EAA8E;AAE9E,OAAO,EAAE,KAAK,EAAE,CAAC;AAEjB,8EAA8E;AAC9E,mBAAmB;AACnB,8EAA8E;AAE9E,OAAO,EAAE,YAAY,EAAE,CAAC;AAgBxB,8EAA8E;AAC9E,gCAAgC;AAChC,8EAA8E;AAE9E,OAAO,EAAE,YAAY,IAAI,sBAAsB,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * masker.ts — Recursive sensitive field masking
3
+ *
4
+ * Provides a pure function that recursively replaces sensitive field values
5
+ * with "******" at any nesting depth.
6
+ */
7
+ /**
8
+ * Checks if a value is a plain object (not an Array, Date, or class instance).
9
+ *
10
+ * @param val - The value to check
11
+ * @returns True if val is a plain object
12
+ */
13
+ function isPlainObject(val) {
14
+ if (val === null || typeof val !== 'object') {
15
+ return false;
16
+ }
17
+ // Reject arrays
18
+ if (Array.isArray(val)) {
19
+ return false;
20
+ }
21
+ // Check if it's a plain object (created by {} or Object.create(null))
22
+ // using Object.prototype.toString
23
+ const proto = Object.getPrototypeOf(val);
24
+ return proto === null || proto === Object.prototype;
25
+ }
26
+ /**
27
+ * Recursively masks sensitive fields in an object.
28
+ *
29
+ * @param obj - The object to mask
30
+ * @param sensitiveFields - Set of field names to mask (stored in lowercase)
31
+ * @param depth - Current recursion depth (default: 0)
32
+ * @returns A new object with sensitive fields replaced by "******"
33
+ */
34
+ export function maskObject(obj, sensitiveFields, depth = 0) {
35
+ // Prevent infinite recursion
36
+ if (depth > 10) {
37
+ return obj;
38
+ }
39
+ const result = {};
40
+ for (const key in obj) {
41
+ if (!Object.prototype.hasOwnProperty.call(obj, key)) {
42
+ continue;
43
+ }
44
+ const value = obj[key];
45
+ // Check if this key is sensitive (case-insensitive comparison)
46
+ if (sensitiveFields.has(key.toLowerCase())) {
47
+ result[key] = '******';
48
+ continue;
49
+ }
50
+ // Recurse into plain objects
51
+ if (isPlainObject(value)) {
52
+ result[key] = maskObject(value, sensitiveFields, depth + 1);
53
+ continue;
54
+ }
55
+ // Map over arrays: recurse on plain objects, pass primitives through
56
+ if (Array.isArray(value)) {
57
+ result[key] = value.map((element) => {
58
+ if (isPlainObject(element)) {
59
+ return maskObject(element, sensitiveFields, depth + 1);
60
+ }
61
+ return element;
62
+ });
63
+ continue;
64
+ }
65
+ // Pass through all other values (primitives, dates, etc.)
66
+ result[key] = value;
67
+ }
68
+ return result;
69
+ }
70
+ //# sourceMappingURL=masker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"masker.js","sourceRoot":"","sources":["../../src/masker.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;;GAKG;AACH,SAAS,aAAa,CAAC,GAAY;IACjC,IAAI,GAAG,KAAK,IAAI,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QAC5C,OAAO,KAAK,CAAC;IACf,CAAC;IAED,gBAAgB;IAChB,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QACvB,OAAO,KAAK,CAAC;IACf,CAAC;IAED,sEAAsE;IACtE,kCAAkC;IAClC,MAAM,KAAK,GAAG,MAAM,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC;IACzC,OAAO,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,MAAM,CAAC,SAAS,CAAC;AACtD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,UAAU,CACxB,GAA4B,EAC5B,eAA4B,EAC5B,QAAgB,CAAC;IAEjB,6BAA6B;IAC7B,IAAI,KAAK,GAAG,EAAE,EAAE,CAAC;QACf,OAAO,GAAG,CAAC;IACb,CAAC;IAED,MAAM,MAAM,GAA4B,EAAE,CAAC;IAE3C,KAAK,MAAM,GAAG,IAAI,GAAG,EAAE,CAAC;QACtB,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,CAAC;YACpD,SAAS;QACX,CAAC;QAED,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;QAEvB,+DAA+D;QAC/D,IAAI,eAAe,CAAC,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;YAC3C,MAAM,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC;YACvB,SAAS;QACX,CAAC;QAED,6BAA6B;QAC7B,IAAI,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;YACzB,MAAM,CAAC,GAAG,CAAC,GAAG,UAAU,CAAC,KAAK,EAAE,eAAe,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;YAC5D,SAAS;QACX,CAAC;QAED,qEAAqE;QACrE,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACzB,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE;gBAClC,IAAI,aAAa,CAAC,OAAO,CAAC,EAAE,CAAC;oBAC3B,OAAO,UAAU,CAAC,OAAO,EAAE,eAAe,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;gBACzD,CAAC;gBACD,OAAO,OAAO,CAAC;YACjB,CAAC,CAAC,CAAC;YACH,SAAS;QACX,CAAC;QAED,0DAA0D;QAC1D,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;IACtB,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}