@vafast/request-logger 0.1.3 → 0.1.5

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 CHANGED
@@ -16,7 +16,7 @@ API request logging middleware for Vafast with automatic sensitive data sanitiza
16
16
  ```bash
17
17
  npm install @vafast/request-logger
18
18
  # or
19
- bun add @vafast/request-logger
19
+ npm install @vafast/request-logger
20
20
  ```
21
21
 
22
22
  ## Quick Start
@@ -0,0 +1,165 @@
1
+ import { Middleware } from "vafast";
2
+
3
+ //#region src/sanitize.d.ts
4
+
5
+ /**
6
+ * 敏感数据清洗工具
7
+ *
8
+ * 用于在记录日志前移除或脱敏敏感信息
9
+ */
10
+ interface SanitizeConfig {
11
+ /** 需要完全移除的字段(小写) */
12
+ removeFields?: string[];
13
+ /** 需要脱敏的字段(小写,部分匹配) */
14
+ maskFields?: string[];
15
+ /** 脱敏占位符 @default '[REDACTED]' */
16
+ placeholder?: string;
17
+ /** 最大递归深度 @default 10 */
18
+ maxDepth?: number;
19
+ }
20
+ /**
21
+ * 深度清洗对象中的敏感数据
22
+ *
23
+ * @example
24
+ * ```typescript
25
+ * const data = { password: '123456', token: 'eyJhbG...' }
26
+ * const sanitized = sanitize(data)
27
+ * // { password: '[REDACTED]', token: 'eyJh****...' }
28
+ * ```
29
+ */
30
+ declare function sanitize<T>(data: T, config?: SanitizeConfig, depth?: number): T;
31
+ /**
32
+ * 清洗 HTTP 请求头
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * const headers = { Authorization: 'Bearer eyJhbG...', Cookie: 'session=xxx' }
37
+ * const sanitized = sanitizeHeaders(headers)
38
+ * // { Authorization: 'Bearer eyJh****...', Cookie: '[REDACTED]' }
39
+ * ```
40
+ */
41
+ declare function sanitizeHeaders(headers: Record<string, string>, config?: SanitizeConfig): Record<string, string>;
42
+ //#endregion
43
+ //#region src/index.d.ts
44
+ interface RequestLog {
45
+ method: string;
46
+ url: string;
47
+ path: string;
48
+ headers: Record<string, string>;
49
+ body: unknown;
50
+ query: Record<string, string>;
51
+ response: {
52
+ success?: boolean;
53
+ message?: string;
54
+ code?: number;
55
+ };
56
+ status: number;
57
+ duration: number;
58
+ userId?: string;
59
+ appId?: string;
60
+ /** 服务标识(区分不同服务,如 auth-server、ones-server) */
61
+ service?: string;
62
+ createdAt: Date;
63
+ }
64
+ interface ResponseLog {
65
+ requestLogId: string;
66
+ success?: boolean;
67
+ message?: string;
68
+ code?: number;
69
+ data?: unknown;
70
+ createdAt: Date;
71
+ }
72
+ /**
73
+ * 存储适配器接口
74
+ */
75
+ interface StorageAdapter {
76
+ /** 存储请求日志 */
77
+ saveRequestLog(log: RequestLog): Promise<string>;
78
+ /** 存储响应详情 */
79
+ saveResponseLog(log: ResponseLog): Promise<void>;
80
+ }
81
+ interface RequestLoggerConfig {
82
+ /** 存储适配器 */
83
+ storage: StorageAdapter;
84
+ /** 排除的路径(支持字符串或正则) */
85
+ excludePaths?: (string | RegExp)[];
86
+ /** 敏感数据清洗配置 */
87
+ sanitize?: SanitizeConfig;
88
+ /** 获取用户 ID 的函数 */
89
+ getUserId?: (req: Request) => string | undefined;
90
+ /** 获取应用 ID 的函数(用于多租户) */
91
+ getAppId?: (req: Request) => string | undefined;
92
+ /** 服务标识(区分不同服务,如 auth-server、ones-server) */
93
+ service?: string;
94
+ /**
95
+ * API 路径前缀,用于在查询路由注册表时匹配正确的路径
96
+ * 例如:'/restfulApi' → 请求路径 '/restfulApi/auth/signIn' 会在注册表中查找 '/auth/signIn'
97
+ */
98
+ pathPrefix?: string;
99
+ /** 错误回调 */
100
+ onError?: (error: Error) => void;
101
+ /** 是否启用 @default true */
102
+ enabled?: boolean;
103
+ }
104
+ /**
105
+ * 创建请求日志中间件
106
+ *
107
+ * 日志排除机制:
108
+ * 1. excludePaths: 手动指定需要排除的路径(字符串或正则)
109
+ * 2. 路由配置 log: false: 在路由定义中设置 log: false,中间件会自动查询 RouteRegistry
110
+ *
111
+ * @example
112
+ * ```typescript
113
+ * import { createRequestLogger, createMongoAdapter } from '@vafast/request-logger'
114
+ * import { mongoDb } from './mongodb'
115
+ *
116
+ * const requestLogger = createRequestLogger({
117
+ * storage: createMongoAdapter(mongoDb, 'logs', 'logsResponse'),
118
+ * // 手动指定排除路径(正则或字符串)
119
+ * excludePaths: ['/health', /^\/metrics/],
120
+ * // API 路径前缀(用于路由注册表查询)
121
+ * pathPrefix: '/restfulApi',
122
+ * getUserId: (req) => getLocals(req)?.userInfo?.id,
123
+ * })
124
+ *
125
+ * server.use(requestLogger)
126
+ * ```
127
+ *
128
+ * 在路由定义中使用 log: false:
129
+ * ```typescript
130
+ * export const healthRoutes: Route[] = [{
131
+ * method: 'GET',
132
+ * path: '/health',
133
+ * log: false, // 此路由不记录日志
134
+ * handler: () => success({ status: 'ok' })
135
+ * }]
136
+ * ```
137
+ */
138
+ declare function createRequestLogger(config: RequestLoggerConfig): Middleware;
139
+ /**
140
+ * 创建 MongoDB 存储适配器
141
+ *
142
+ * @example
143
+ * ```typescript
144
+ * import { Db } from 'mongodb'
145
+ * import { createMongoAdapter } from '@vafast/request-logger'
146
+ *
147
+ * const adapter = createMongoAdapter(db, 'logs', 'logsResponse')
148
+ * ```
149
+ */
150
+ declare function createMongoAdapter(db: {
151
+ collection: (name: string) => {
152
+ insertOne: (doc: any) => Promise<{
153
+ insertedId: {
154
+ toHexString: () => string;
155
+ };
156
+ }>;
157
+ };
158
+ }, logsCollection?: string, logsResponseCollection?: string): StorageAdapter;
159
+ /**
160
+ * 创建控制台存储适配器(用于开发调试)
161
+ */
162
+ declare function createConsoleAdapter(): StorageAdapter;
163
+ //#endregion
164
+ export { RequestLog, RequestLoggerConfig, ResponseLog, type SanitizeConfig, StorageAdapter, createConsoleAdapter, createMongoAdapter, createRequestLogger, createRequestLogger as default, sanitize, sanitizeHeaders };
165
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/sanitize.ts","../src/index.ts"],"sourcesContent":[],"mappings":";;;;;;;AAQA;AA+DA;AAAkC,UA/DjB,cAAA,CA+DiB;EAAY;EAA4B,YAAA,CAAA,EAAA,MAAA,EAAA;EAAC;EA+D3D,UAAA,CAAA,EAAA,MAAe,EAAA;EACpB;EACA,WAAA,CAAA,EAAA,MAAA;EACR;EAAM,QAAA,CAAA,EAAA,MAAA;;;;ACtHT;;;;;AAqBA;AAYA;;AAEmC,iBDiBnB,QCjBmB,CAAA,CAAA,CAAA,CAAA,IAAA,EDiBD,CCjBC,EAAA,MAAA,CAAA,EDiBW,cCjBX,EAAA,KAAA,CAAA,EAAA,MAAA,CAAA,EDiBuC,CCjBvC;;;;AAKnC;;;;;;;AAmByB,iBDwDT,eAAA,CCxDS,OAAA,EDyDd,MCzDc,CAAA,MAAA,EAAA,MAAA,CAAA,EAAA,MAAA,CAAA,ED0Dd,cC1Dc,CAAA,ED2DtB,MC3DsB,CAAA,MAAA,EAAA,MAAA,CAAA;;;UA3DR,UAAA;;EAAA,GAAA,EAAA,MAAA;EAIN,IAAA,EAAA,MAAA;EAEF,OAAA,EAFE,MAEF,CAAA,MAAA,EAAA,MAAA,CAAA;EAYI,IAAA,EAAA,OAAA;EAAI,KAAA,EAZR,MAYQ,CAAA,MAAA,EAAA,MAAA,CAAA;EAGA,QAAA,EAAA;IAYA,OAAA,CAAA,EAAA,OAAc;IAET,OAAA,CAAA,EAAA,MAAA;IAAa,IAAA,CAAA,EAAA,MAAA;EAEZ,CAAA;EAAc,MAAA,EAAA,MAAA;EAAO,QAAA,EAAA,MAAA;EAG3B,MAAA,CAAA,EAAA,MAAA;EAEN,KAAA,CAAA,EAAA,MAAA;EAEgB;EAEd,OAAA,CAAA,EAAA,MAAA;EAEO,SAAA,EA9BP,IA8BO;;AAWA,UAtCH,WAAA,CAsCG;EAAK,YAAA,EAAA,MAAA;EAyCT,OAAA,CAAA,EAAA,OAAA;EAkLA,OAAA,CAAA,EAAA,MAAA;EA+BA,IAAA,CAAA,EAAA,MAAA;;aA1RH;;;;;UAMI,cAAA;;sBAEK,aAAa;;uBAEZ,cAAc;;UAGpB,mBAAA;;WAEN;;2BAEgB;;aAEd;;oBAEO;;mBAED;;;;;;;;;oBASC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAyCJ,mBAAA,SAA4B,sBAAsB;;;;;;;;;;;;iBAkLlD,kBAAA;;6BACiD;;;;;;8DAG9D;;;;iBA2Ba,oBAAA,CAAA,GAAwB"}
package/dist/index.mjs ADDED
@@ -0,0 +1,278 @@
1
+ import { getRoute } from "vafast";
2
+
3
+ //#region src/sanitize.ts
4
+ /** 默认需要完全移除的敏感字段 */
5
+ const DEFAULT_REMOVE_FIELDS = [
6
+ "password",
7
+ "newpassword",
8
+ "oldpassword",
9
+ "confirmpassword",
10
+ "secret",
11
+ "secretkey",
12
+ "privatekey",
13
+ "apisecret",
14
+ "clientsecret"
15
+ ];
16
+ /** 默认需要脱敏的字段(保留部分信息) */
17
+ const DEFAULT_MASK_FIELDS = [
18
+ "token",
19
+ "accesstoken",
20
+ "refreshtoken",
21
+ "authorization",
22
+ "apikey",
23
+ "api_key",
24
+ "x-api-key",
25
+ "idtoken",
26
+ "sessiontoken",
27
+ "bearer"
28
+ ];
29
+ const DEFAULT_PLACEHOLDER = "[REDACTED]";
30
+ const DEFAULT_MAX_DEPTH = 10;
31
+ /**
32
+ * 部分脱敏(保留前4后4位)
33
+ */
34
+ function partialMask(value, placeholder) {
35
+ if (value.length <= 8) return placeholder;
36
+ return value.slice(0, 4) + "****" + value.slice(-4);
37
+ }
38
+ /**
39
+ * 深度清洗对象中的敏感数据
40
+ *
41
+ * @example
42
+ * ```typescript
43
+ * const data = { password: '123456', token: 'eyJhbG...' }
44
+ * const sanitized = sanitize(data)
45
+ * // { password: '[REDACTED]', token: 'eyJh****...' }
46
+ * ```
47
+ */
48
+ function sanitize(data, config, depth = 0) {
49
+ const { removeFields = DEFAULT_REMOVE_FIELDS, maskFields = DEFAULT_MASK_FIELDS, placeholder = DEFAULT_PLACEHOLDER, maxDepth = DEFAULT_MAX_DEPTH } = config ?? {};
50
+ if (depth > maxDepth) return data;
51
+ if (data === null || data === void 0) return data;
52
+ if (Array.isArray(data)) return data.map((item) => sanitize(item, config, depth + 1));
53
+ if (typeof data === "object") {
54
+ const result = {};
55
+ for (const [key, value] of Object.entries(data)) {
56
+ const lowerKey = key.toLowerCase();
57
+ if (removeFields.some((field) => lowerKey === field)) {
58
+ result[key] = placeholder;
59
+ continue;
60
+ }
61
+ if (maskFields.some((field) => lowerKey.includes(field))) {
62
+ if (typeof value === "string") result[key] = partialMask(value, placeholder);
63
+ else result[key] = placeholder;
64
+ continue;
65
+ }
66
+ result[key] = sanitize(value, config, depth + 1);
67
+ }
68
+ return result;
69
+ }
70
+ return data;
71
+ }
72
+ /**
73
+ * 清洗 HTTP 请求头
74
+ *
75
+ * @example
76
+ * ```typescript
77
+ * const headers = { Authorization: 'Bearer eyJhbG...', Cookie: 'session=xxx' }
78
+ * const sanitized = sanitizeHeaders(headers)
79
+ * // { Authorization: 'Bearer eyJh****...', Cookie: '[REDACTED]' }
80
+ * ```
81
+ */
82
+ function sanitizeHeaders(headers, config) {
83
+ const { maskFields = DEFAULT_MASK_FIELDS, placeholder = DEFAULT_PLACEHOLDER } = config ?? {};
84
+ const result = {};
85
+ for (const [key, value] of Object.entries(headers)) {
86
+ const lowerKey = key.toLowerCase();
87
+ if (lowerKey === "authorization") {
88
+ if (value.startsWith("Bearer ")) result[key] = "Bearer " + partialMask(value.slice(7), placeholder);
89
+ else result[key] = partialMask(value, placeholder);
90
+ continue;
91
+ }
92
+ if (lowerKey === "cookie" || lowerKey === "set-cookie") {
93
+ result[key] = placeholder;
94
+ continue;
95
+ }
96
+ if (maskFields.some((field) => lowerKey.includes(field))) {
97
+ result[key] = partialMask(value, placeholder);
98
+ continue;
99
+ }
100
+ result[key] = value;
101
+ }
102
+ return result;
103
+ }
104
+
105
+ //#endregion
106
+ //#region src/index.ts
107
+ /**
108
+ * 创建请求日志中间件
109
+ *
110
+ * 日志排除机制:
111
+ * 1. excludePaths: 手动指定需要排除的路径(字符串或正则)
112
+ * 2. 路由配置 log: false: 在路由定义中设置 log: false,中间件会自动查询 RouteRegistry
113
+ *
114
+ * @example
115
+ * ```typescript
116
+ * import { createRequestLogger, createMongoAdapter } from '@vafast/request-logger'
117
+ * import { mongoDb } from './mongodb'
118
+ *
119
+ * const requestLogger = createRequestLogger({
120
+ * storage: createMongoAdapter(mongoDb, 'logs', 'logsResponse'),
121
+ * // 手动指定排除路径(正则或字符串)
122
+ * excludePaths: ['/health', /^\/metrics/],
123
+ * // API 路径前缀(用于路由注册表查询)
124
+ * pathPrefix: '/restfulApi',
125
+ * getUserId: (req) => getLocals(req)?.userInfo?.id,
126
+ * })
127
+ *
128
+ * server.use(requestLogger)
129
+ * ```
130
+ *
131
+ * 在路由定义中使用 log: false:
132
+ * ```typescript
133
+ * export const healthRoutes: Route[] = [{
134
+ * method: 'GET',
135
+ * path: '/health',
136
+ * log: false, // 此路由不记录日志
137
+ * handler: () => success({ status: 'ok' })
138
+ * }]
139
+ * ```
140
+ */
141
+ function createRequestLogger(config) {
142
+ const { storage, excludePaths = [], sanitize: sanitizeConfig, getUserId, getAppId, service, pathPrefix = "", onError = console.error, enabled = true } = config;
143
+ return async (req, next) => {
144
+ if (!enabled) return next();
145
+ const startTime = Date.now();
146
+ const response = await next();
147
+ recordLog(req, response, startTime, {
148
+ storage,
149
+ excludePaths,
150
+ pathPrefix,
151
+ sanitizeConfig,
152
+ getUserId,
153
+ getAppId,
154
+ service,
155
+ onError
156
+ }).catch(onError);
157
+ return response;
158
+ };
159
+ }
160
+ /**
161
+ * 检查路由是否配置了 log: false
162
+ * 使用 vafast RouteRegistry 查询路由配置
163
+ */
164
+ function shouldSkipLogByRoute(method, path, pathPrefix) {
165
+ try {
166
+ return getRoute(method, pathPrefix ? path.replace(/* @__PURE__ */ new RegExp(`^${pathPrefix}`), "") : path)?.log === false;
167
+ } catch {
168
+ return false;
169
+ }
170
+ }
171
+ async function recordLog(req, response, startTime, options) {
172
+ const { storage, excludePaths, pathPrefix, sanitizeConfig, getUserId, getAppId, service } = options;
173
+ const url = new URL(req.url);
174
+ const path = url.pathname;
175
+ if (excludePaths.some((pattern) => {
176
+ if (typeof pattern === "string") return path.includes(pattern);
177
+ return pattern.test(path);
178
+ })) return;
179
+ if (shouldSkipLogByRoute(req.method, path, pathPrefix)) return;
180
+ let body = null;
181
+ try {
182
+ const clonedReq = req.clone();
183
+ if ((req.headers.get("content-type") || "").includes("application/json")) body = await clonedReq.json();
184
+ } catch {}
185
+ let responseData = {};
186
+ try {
187
+ responseData = await response.clone().json();
188
+ } catch {}
189
+ const headers = {};
190
+ req.headers.forEach((value, key) => {
191
+ headers[key] = value;
192
+ });
193
+ const sanitizedHeaders = sanitizeHeaders(headers, sanitizeConfig);
194
+ const sanitizedBody = sanitize(body, sanitizeConfig);
195
+ const sanitizedResponseData = sanitize(responseData.data, sanitizeConfig);
196
+ const now = /* @__PURE__ */ new Date();
197
+ const duration = Date.now() - startTime;
198
+ const userId = getUserId?.(req);
199
+ const appId = getAppId?.(req);
200
+ const requestLogId = await storage.saveRequestLog({
201
+ method: req.method,
202
+ url: req.url,
203
+ path,
204
+ headers: sanitizedHeaders,
205
+ body: sanitizedBody,
206
+ query: Object.fromEntries(url.searchParams),
207
+ response: {
208
+ success: responseData.success,
209
+ message: responseData.message,
210
+ code: responseData.code
211
+ },
212
+ status: response.status,
213
+ duration,
214
+ userId,
215
+ appId,
216
+ service,
217
+ createdAt: now
218
+ });
219
+ await storage.saveResponseLog({
220
+ requestLogId,
221
+ success: responseData.success,
222
+ message: responseData.message,
223
+ code: responseData.code,
224
+ data: sanitizedResponseData,
225
+ createdAt: now
226
+ });
227
+ }
228
+ /**
229
+ * 创建 MongoDB 存储适配器
230
+ *
231
+ * @example
232
+ * ```typescript
233
+ * import { Db } from 'mongodb'
234
+ * import { createMongoAdapter } from '@vafast/request-logger'
235
+ *
236
+ * const adapter = createMongoAdapter(db, 'logs', 'logsResponse')
237
+ * ```
238
+ */
239
+ function createMongoAdapter(db, logsCollection = "logs", logsResponseCollection = "logsResponse") {
240
+ return {
241
+ async saveRequestLog(log) {
242
+ return (await db.collection(logsCollection).insertOne({
243
+ ...log,
244
+ createAt: log.createdAt,
245
+ updateAt: log.createdAt
246
+ })).insertedId.toHexString();
247
+ },
248
+ async saveResponseLog(log) {
249
+ await db.collection(logsResponseCollection).insertOne({
250
+ logsId: log.requestLogId,
251
+ ...log,
252
+ createAt: log.createdAt,
253
+ updateAt: log.createdAt
254
+ });
255
+ }
256
+ };
257
+ }
258
+ /**
259
+ * 创建控制台存储适配器(用于开发调试)
260
+ */
261
+ function createConsoleAdapter() {
262
+ let idCounter = 0;
263
+ return {
264
+ async saveRequestLog(log) {
265
+ const id = `log_${++idCounter}`;
266
+ console.log(`[REQUEST] ${log.method} ${log.path} ${log.status} ${log.duration}ms`);
267
+ return id;
268
+ },
269
+ async saveResponseLog(log) {
270
+ if (!log.success) console.log(`[RESPONSE ERROR] ${log.message}`);
271
+ }
272
+ };
273
+ }
274
+ var src_default = createRequestLogger;
275
+
276
+ //#endregion
277
+ export { createConsoleAdapter, createMongoAdapter, createRequestLogger, src_default as default, sanitize, sanitizeHeaders };
278
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/sanitize.ts","../src/index.ts"],"sourcesContent":["/**\n * 敏感数据清洗工具\n * \n * 用于在记录日志前移除或脱敏敏感信息\n */\n\n// ============ Types ============\n\nexport interface SanitizeConfig {\n /** 需要完全移除的字段(小写) */\n removeFields?: string[]\n /** 需要脱敏的字段(小写,部分匹配) */\n maskFields?: string[]\n /** 脱敏占位符 @default '[REDACTED]' */\n placeholder?: string\n /** 最大递归深度 @default 10 */\n maxDepth?: number\n}\n\n// ============ Default Config ============\n\n/** 默认需要完全移除的敏感字段 */\nconst DEFAULT_REMOVE_FIELDS = [\n 'password',\n 'newpassword',\n 'oldpassword',\n 'confirmpassword',\n 'secret',\n 'secretkey',\n 'privatekey',\n 'apisecret',\n 'clientsecret',\n]\n\n/** 默认需要脱敏的字段(保留部分信息) */\nconst DEFAULT_MASK_FIELDS = [\n 'token',\n 'accesstoken',\n 'refreshtoken',\n 'authorization',\n 'apikey',\n 'api_key',\n 'x-api-key',\n 'idtoken',\n 'sessiontoken',\n 'bearer',\n]\n\nconst DEFAULT_PLACEHOLDER = '[REDACTED]'\nconst DEFAULT_MAX_DEPTH = 10\n\n// ============ Sanitize Functions ============\n\n/**\n * 部分脱敏(保留前4后4位)\n */\nfunction partialMask(value: string, placeholder: string): string {\n if (value.length <= 8) return placeholder\n return value.slice(0, 4) + '****' + value.slice(-4)\n}\n\n/**\n * 深度清洗对象中的敏感数据\n * \n * @example\n * ```typescript\n * const data = { password: '123456', token: 'eyJhbG...' }\n * const sanitized = sanitize(data)\n * // { password: '[REDACTED]', token: 'eyJh****...' }\n * ```\n */\nexport function sanitize<T>(data: T, config?: SanitizeConfig, depth = 0): T {\n const {\n removeFields = DEFAULT_REMOVE_FIELDS,\n maskFields = DEFAULT_MASK_FIELDS,\n placeholder = DEFAULT_PLACEHOLDER,\n maxDepth = DEFAULT_MAX_DEPTH,\n } = config ?? {}\n\n // 防止无限递归\n if (depth > maxDepth) return data\n \n if (data === null || data === undefined) {\n return data\n }\n\n // 处理数组\n if (Array.isArray(data)) {\n return data.map(item => sanitize(item, config, depth + 1)) as T\n }\n\n // 处理对象\n if (typeof data === 'object') {\n const result: Record<string, unknown> = {}\n \n for (const [key, value] of Object.entries(data)) {\n const lowerKey = key.toLowerCase()\n \n // 完全移除的字段\n if (removeFields.some(field => lowerKey === field)) {\n result[key] = placeholder\n continue\n }\n \n // 部分脱敏的字段\n if (maskFields.some(field => lowerKey.includes(field))) {\n if (typeof value === 'string') {\n result[key] = partialMask(value, placeholder)\n } else {\n result[key] = placeholder\n }\n continue\n }\n \n // 递归处理嵌套对象\n result[key] = sanitize(value, config, depth + 1)\n }\n \n return result as T\n }\n\n return data\n}\n\n/**\n * 清洗 HTTP 请求头\n * \n * @example\n * ```typescript\n * const headers = { Authorization: 'Bearer eyJhbG...', Cookie: 'session=xxx' }\n * const sanitized = sanitizeHeaders(headers)\n * // { Authorization: 'Bearer eyJh****...', Cookie: '[REDACTED]' }\n * ```\n */\nexport function sanitizeHeaders(\n headers: Record<string, string>,\n config?: SanitizeConfig\n): Record<string, string> {\n const {\n maskFields = DEFAULT_MASK_FIELDS,\n placeholder = DEFAULT_PLACEHOLDER,\n } = config ?? {}\n\n const result: Record<string, string> = {}\n \n for (const [key, value] of Object.entries(headers)) {\n const lowerKey = key.toLowerCase()\n \n // Authorization 头部分脱敏\n if (lowerKey === 'authorization') {\n if (value.startsWith('Bearer ')) {\n result[key] = 'Bearer ' + partialMask(value.slice(7), placeholder)\n } else {\n result[key] = partialMask(value, placeholder)\n }\n continue\n }\n \n // Cookie 完全脱敏\n if (lowerKey === 'cookie' || lowerKey === 'set-cookie') {\n result[key] = placeholder\n continue\n }\n \n // API Key 相关头脱敏\n if (maskFields.some(field => lowerKey.includes(field))) {\n result[key] = partialMask(value, placeholder)\n continue\n }\n \n result[key] = value\n }\n \n return result\n}\n\n/**\n * 检查值是否为敏感字段\n */\nexport function isSensitiveField(fieldName: string, config?: SanitizeConfig): boolean {\n const {\n removeFields = DEFAULT_REMOVE_FIELDS,\n maskFields = DEFAULT_MASK_FIELDS,\n } = config ?? {}\n\n const lowerName = fieldName.toLowerCase()\n \n return (\n removeFields.some(field => lowerName === field) ||\n maskFields.some(field => lowerName.includes(field))\n )\n}\n\n","/**\n * @vafast/request-logger - API request logging middleware for Vafast\n * \n * Features:\n * - Automatic sensitive data sanitization\n * - Pluggable storage adapters (MongoDB, custom)\n * - Async logging (non-blocking)\n * - Path exclusion support\n * - Route-level log control (log: false in route definition)\n * \n * Uses vafast RouteRegistry to query route configurations at runtime,\n * similar to @vafast/webhook implementation.\n */\nimport type { Middleware } from 'vafast'\nimport { getRoute } from 'vafast'\nimport { sanitize, sanitizeHeaders, type SanitizeConfig } from './sanitize'\n\n// ============ Types ============\n\nexport interface RequestLog {\n method: string\n url: string\n path: string\n headers: Record<string, string>\n body: unknown\n query: Record<string, string>\n response: {\n success?: boolean\n message?: string\n code?: number\n }\n status: number\n duration: number\n userId?: string\n appId?: string\n /** 服务标识(区分不同服务,如 auth-server、ones-server) */\n service?: string\n createdAt: Date\n}\n\nexport interface ResponseLog {\n requestLogId: string\n success?: boolean\n message?: string\n code?: number\n data?: unknown\n createdAt: Date\n}\n\n/**\n * 存储适配器接口\n */\nexport interface StorageAdapter {\n /** 存储请求日志 */\n saveRequestLog(log: RequestLog): Promise<string>\n /** 存储响应详情 */\n saveResponseLog(log: ResponseLog): Promise<void>\n}\n\nexport interface RequestLoggerConfig {\n /** 存储适配器 */\n storage: StorageAdapter\n /** 排除的路径(支持字符串或正则) */\n excludePaths?: (string | RegExp)[]\n /** 敏感数据清洗配置 */\n sanitize?: SanitizeConfig\n /** 获取用户 ID 的函数 */\n getUserId?: (req: Request) => string | undefined\n /** 获取应用 ID 的函数(用于多租户) */\n getAppId?: (req: Request) => string | undefined\n /** 服务标识(区分不同服务,如 auth-server、ones-server) */\n service?: string\n /** \n * API 路径前缀,用于在查询路由注册表时匹配正确的路径\n * 例如:'/restfulApi' → 请求路径 '/restfulApi/auth/signIn' 会在注册表中查找 '/auth/signIn'\n */\n pathPrefix?: string\n /** 错误回调 */\n onError?: (error: Error) => void\n /** 是否启用 @default true */\n enabled?: boolean\n}\n\n// ============ Middleware Factory ============\n\n/**\n * 创建请求日志中间件\n * \n * 日志排除机制:\n * 1. excludePaths: 手动指定需要排除的路径(字符串或正则)\n * 2. 路由配置 log: false: 在路由定义中设置 log: false,中间件会自动查询 RouteRegistry\n * \n * @example\n * ```typescript\n * import { createRequestLogger, createMongoAdapter } from '@vafast/request-logger'\n * import { mongoDb } from './mongodb'\n * \n * const requestLogger = createRequestLogger({\n * storage: createMongoAdapter(mongoDb, 'logs', 'logsResponse'),\n * // 手动指定排除路径(正则或字符串)\n * excludePaths: ['/health', /^\\/metrics/],\n * // API 路径前缀(用于路由注册表查询)\n * pathPrefix: '/restfulApi',\n * getUserId: (req) => getLocals(req)?.userInfo?.id,\n * })\n * \n * server.use(requestLogger)\n * ```\n * \n * 在路由定义中使用 log: false:\n * ```typescript\n * export const healthRoutes: Route[] = [{\n * method: 'GET',\n * path: '/health',\n * log: false, // 此路由不记录日志\n * handler: () => success({ status: 'ok' })\n * }]\n * ```\n */\nexport function createRequestLogger(config: RequestLoggerConfig): Middleware {\n const {\n storage,\n excludePaths = [],\n sanitize: sanitizeConfig,\n getUserId,\n getAppId,\n service,\n pathPrefix = '',\n onError = console.error,\n enabled = true,\n } = config\n\n return async (req: Request, next: () => Promise<Response>) => {\n if (!enabled) {\n return next()\n }\n\n const startTime = Date.now()\n const response = await next()\n\n // 异步记录日志,不阻塞响应\n recordLog(req, response, startTime, {\n storage,\n excludePaths,\n pathPrefix,\n sanitizeConfig,\n getUserId,\n getAppId,\n service,\n onError,\n }).catch(onError)\n\n return response\n }\n}\n\n// ============ Internal Functions ============\n\ninterface RecordLogOptions {\n storage: StorageAdapter\n excludePaths: (string | RegExp)[]\n pathPrefix: string\n sanitizeConfig?: SanitizeConfig\n getUserId?: (req: Request) => string | undefined\n getAppId?: (req: Request) => string | undefined\n service?: string\n onError: (error: Error) => void\n}\n\n/**\n * 检查路由是否配置了 log: false\n * 使用 vafast RouteRegistry 查询路由配置\n */\nfunction shouldSkipLogByRoute(method: string, path: string, pathPrefix: string): boolean {\n try {\n // 移除 pathPrefix 以匹配路由注册表中的路径\n const routePath = pathPrefix ? path.replace(new RegExp(`^${pathPrefix}`), '') : path\n const route = getRoute<{ log?: boolean }>(method, routePath)\n return route?.log === false\n } catch {\n // RouteRegistry 未初始化时忽略错误\n return false\n }\n}\n\nasync function recordLog(\n req: Request,\n response: Response,\n startTime: number,\n options: RecordLogOptions\n) {\n const { storage, excludePaths, pathPrefix, sanitizeConfig, getUserId, getAppId, service } = options\n\n const url = new URL(req.url)\n const path = url.pathname\n\n // 检查1: 手动配置的 excludePaths\n const shouldExcludeByPath = excludePaths.some(pattern => {\n if (typeof pattern === 'string') {\n return path.includes(pattern)\n }\n return pattern.test(path)\n })\n\n if (shouldExcludeByPath) {\n return\n }\n\n // 检查2: 路由定义中的 log: false(通过 RouteRegistry 查询)\n if (shouldSkipLogByRoute(req.method, path, pathPrefix)) {\n return\n }\n\n // 解析请求体\n let body: unknown = null\n try {\n const clonedReq = req.clone()\n const contentType = req.headers.get('content-type') || ''\n if (contentType.includes('application/json')) {\n body = await clonedReq.json()\n }\n } catch {\n // 忽略解析错误\n }\n\n // 解析响应体\n let responseData: { success?: boolean; message?: string; code?: number; data?: unknown } = {}\n try {\n const clonedRes = response.clone()\n responseData = await clonedRes.json()\n } catch {\n // 忽略解析错误\n }\n\n // 提取请求头\n const headers: Record<string, string> = {}\n req.headers.forEach((value, key) => {\n headers[key] = value\n })\n\n // 清洗敏感数据\n const sanitizedHeaders = sanitizeHeaders(headers, sanitizeConfig)\n const sanitizedBody = sanitize(body, sanitizeConfig)\n const sanitizedResponseData = sanitize(responseData.data, sanitizeConfig)\n\n const now = new Date()\n const duration = Date.now() - startTime\n\n // 获取用户 ID 和应用 ID\n const userId = getUserId?.(req)\n const appId = getAppId?.(req)\n\n // 存储请求日志\n const requestLogId = await storage.saveRequestLog({\n method: req.method,\n url: req.url,\n path,\n headers: sanitizedHeaders,\n body: sanitizedBody,\n query: Object.fromEntries(url.searchParams),\n response: {\n success: responseData.success,\n message: responseData.message,\n code: responseData.code,\n },\n status: response.status,\n duration,\n userId,\n appId,\n service,\n createdAt: now,\n })\n\n // 存储响应详情\n await storage.saveResponseLog({\n requestLogId,\n success: responseData.success,\n message: responseData.message,\n code: responseData.code,\n data: sanitizedResponseData,\n createdAt: now,\n })\n}\n\n// ============ MongoDB Adapter ============\n\n/**\n * 创建 MongoDB 存储适配器\n * \n * @example\n * ```typescript\n * import { Db } from 'mongodb'\n * import { createMongoAdapter } from '@vafast/request-logger'\n * \n * const adapter = createMongoAdapter(db, 'logs', 'logsResponse')\n * ```\n */\nexport function createMongoAdapter(\n db: { collection: (name: string) => { insertOne: (doc: any) => Promise<{ insertedId: { toHexString: () => string } }> } },\n logsCollection: string = 'logs',\n logsResponseCollection: string = 'logsResponse'\n): StorageAdapter {\n return {\n async saveRequestLog(log: RequestLog): Promise<string> {\n const result = await db.collection(logsCollection).insertOne({\n ...log,\n createAt: log.createdAt,\n updateAt: log.createdAt,\n })\n return result.insertedId.toHexString()\n },\n\n async saveResponseLog(log: ResponseLog): Promise<void> {\n await db.collection(logsResponseCollection).insertOne({\n logsId: log.requestLogId,\n ...log,\n createAt: log.createdAt,\n updateAt: log.createdAt,\n })\n },\n }\n}\n\n// ============ Console Adapter (for development) ============\n\n/**\n * 创建控制台存储适配器(用于开发调试)\n */\nexport function createConsoleAdapter(): StorageAdapter {\n let idCounter = 0\n\n return {\n async saveRequestLog(log: RequestLog): Promise<string> {\n const id = `log_${++idCounter}`\n console.log(`[REQUEST] ${log.method} ${log.path} ${log.status} ${log.duration}ms`)\n return id\n },\n\n async saveResponseLog(log: ResponseLog): Promise<void> {\n if (!log.success) {\n console.log(`[RESPONSE ERROR] ${log.message}`)\n }\n },\n }\n}\n\n// ============ Re-exports ============\n\nexport { sanitize, sanitizeHeaders, type SanitizeConfig } from './sanitize'\nexport default createRequestLogger\n\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vafast/request-logger",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "API request logging middleware for Vafast with sensitive data sanitization",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -22,7 +22,7 @@
22
22
  "typescript"
23
23
  ],
24
24
  "scripts": {
25
- "build": "tsup",
25
+ "build": "tsdown",
26
26
  "dev": "npx tsx watch example/index.ts",
27
27
  "test": "npx vitest run",
28
28
  "release": "npm run build && npm run test && npx bumpp && npm publish --access=public"
@@ -38,12 +38,12 @@
38
38
  "@types/node": "^22.15.30",
39
39
  "mongodb": "^6.16.0",
40
40
  "rimraf": "^6.0.1",
41
- "tsup": "^8.5.1",
41
+ "tsdown": "^0.19.0-beta.4",
42
42
  "typescript": "^5.4.5",
43
- "vafast": "^0.3.9"
43
+ "vafast": ">=0.3.11"
44
44
  },
45
45
  "peerDependencies": {
46
- "vafast": ">= 0.3.0",
46
+ "vafast": ">=0.3.11",
47
47
  "mongodb": ">= 6.0.0"
48
48
  },
49
49
  "peerDependenciesMeta": {
package/dist/index.d.ts DELETED
@@ -1,149 +0,0 @@
1
- import { Middleware } from 'vafast';
2
-
3
- /**
4
- * 敏感数据清洗工具
5
- *
6
- * 用于在记录日志前移除或脱敏敏感信息
7
- */
8
- interface SanitizeConfig {
9
- /** 需要完全移除的字段(小写) */
10
- removeFields?: string[];
11
- /** 需要脱敏的字段(小写,部分匹配) */
12
- maskFields?: string[];
13
- /** 脱敏占位符 @default '[REDACTED]' */
14
- placeholder?: string;
15
- /** 最大递归深度 @default 10 */
16
- maxDepth?: number;
17
- }
18
- /**
19
- * 深度清洗对象中的敏感数据
20
- *
21
- * @example
22
- * ```typescript
23
- * const data = { password: '123456', token: 'eyJhbG...' }
24
- * const sanitized = sanitize(data)
25
- * // { password: '[REDACTED]', token: 'eyJh****...' }
26
- * ```
27
- */
28
- declare function sanitize<T>(data: T, config?: SanitizeConfig, depth?: number): T;
29
- /**
30
- * 清洗 HTTP 请求头
31
- *
32
- * @example
33
- * ```typescript
34
- * const headers = { Authorization: 'Bearer eyJhbG...', Cookie: 'session=xxx' }
35
- * const sanitized = sanitizeHeaders(headers)
36
- * // { Authorization: 'Bearer eyJh****...', Cookie: '[REDACTED]' }
37
- * ```
38
- */
39
- declare function sanitizeHeaders(headers: Record<string, string>, config?: SanitizeConfig): Record<string, string>;
40
-
41
- /**
42
- * @vafast/request-logger - API request logging middleware for Vafast
43
- *
44
- * Features:
45
- * - Automatic sensitive data sanitization
46
- * - Pluggable storage adapters (MongoDB, custom)
47
- * - Async logging (non-blocking)
48
- * - Path exclusion support
49
- */
50
-
51
- interface RequestLog {
52
- method: string;
53
- url: string;
54
- path: string;
55
- headers: Record<string, string>;
56
- body: unknown;
57
- query: Record<string, string>;
58
- response: {
59
- success?: boolean;
60
- message?: string;
61
- code?: number;
62
- };
63
- status: number;
64
- duration: number;
65
- userId?: string;
66
- appId?: string;
67
- /** 服务标识(区分不同服务,如 auth-server、ones-server) */
68
- service?: string;
69
- createdAt: Date;
70
- }
71
- interface ResponseLog {
72
- requestLogId: string;
73
- success?: boolean;
74
- message?: string;
75
- code?: number;
76
- data?: unknown;
77
- createdAt: Date;
78
- }
79
- /**
80
- * 存储适配器接口
81
- */
82
- interface StorageAdapter {
83
- /** 存储请求日志 */
84
- saveRequestLog(log: RequestLog): Promise<string>;
85
- /** 存储响应详情 */
86
- saveResponseLog(log: ResponseLog): Promise<void>;
87
- }
88
- interface RequestLoggerConfig {
89
- /** 存储适配器 */
90
- storage: StorageAdapter;
91
- /** 排除的路径(支持字符串或正则) */
92
- excludePaths?: (string | RegExp)[];
93
- /** 敏感数据清洗配置 */
94
- sanitize?: SanitizeConfig;
95
- /** 获取用户 ID 的函数 */
96
- getUserId?: (req: Request) => string | undefined;
97
- /** 获取应用 ID 的函数(用于多租户) */
98
- getAppId?: (req: Request) => string | undefined;
99
- /** 服务标识(区分不同服务,如 auth-server、ones-server) */
100
- service?: string;
101
- /** 错误回调 */
102
- onError?: (error: Error) => void;
103
- /** 是否启用 @default true */
104
- enabled?: boolean;
105
- }
106
- /**
107
- * 创建请求日志中间件
108
- *
109
- * @example
110
- * ```typescript
111
- * import { createRequestLogger, createMongoAdapter } from '@vafast/request-logger'
112
- * import { mongoDb } from './mongodb'
113
- *
114
- * const requestLogger = createRequestLogger({
115
- * storage: createMongoAdapter(mongoDb, 'logs', 'logsResponse'),
116
- * excludePaths: ['/health', '/metrics'],
117
- * getUserId: (req) => getLocals(req)?.userInfo?.id,
118
- * })
119
- *
120
- * server.use(requestLogger)
121
- * ```
122
- */
123
- declare function createRequestLogger(config: RequestLoggerConfig): Middleware;
124
- /**
125
- * 创建 MongoDB 存储适配器
126
- *
127
- * @example
128
- * ```typescript
129
- * import { Db } from 'mongodb'
130
- * import { createMongoAdapter } from '@vafast/request-logger'
131
- *
132
- * const adapter = createMongoAdapter(db, 'logs', 'logsResponse')
133
- * ```
134
- */
135
- declare function createMongoAdapter(db: {
136
- collection: (name: string) => {
137
- insertOne: (doc: any) => Promise<{
138
- insertedId: {
139
- toHexString: () => string;
140
- };
141
- }>;
142
- };
143
- }, logsCollection?: string, logsResponseCollection?: string): StorageAdapter;
144
- /**
145
- * 创建控制台存储适配器(用于开发调试)
146
- */
147
- declare function createConsoleAdapter(): StorageAdapter;
148
-
149
- export { type RequestLog, type RequestLoggerConfig, type ResponseLog, type SanitizeConfig, type StorageAdapter, createConsoleAdapter, createMongoAdapter, createRequestLogger, createRequestLogger as default, sanitize, sanitizeHeaders };
package/dist/index.js DELETED
@@ -1,237 +0,0 @@
1
- // src/sanitize.ts
2
- var DEFAULT_REMOVE_FIELDS = [
3
- "password",
4
- "newpassword",
5
- "oldpassword",
6
- "confirmpassword",
7
- "secret",
8
- "secretkey",
9
- "privatekey",
10
- "apisecret",
11
- "clientsecret"
12
- ];
13
- var DEFAULT_MASK_FIELDS = [
14
- "token",
15
- "accesstoken",
16
- "refreshtoken",
17
- "authorization",
18
- "apikey",
19
- "api_key",
20
- "x-api-key",
21
- "idtoken",
22
- "sessiontoken",
23
- "bearer"
24
- ];
25
- var DEFAULT_PLACEHOLDER = "[REDACTED]";
26
- var DEFAULT_MAX_DEPTH = 10;
27
- function partialMask(value, placeholder) {
28
- if (value.length <= 8) return placeholder;
29
- return value.slice(0, 4) + "****" + value.slice(-4);
30
- }
31
- function sanitize(data, config, depth = 0) {
32
- const {
33
- removeFields = DEFAULT_REMOVE_FIELDS,
34
- maskFields = DEFAULT_MASK_FIELDS,
35
- placeholder = DEFAULT_PLACEHOLDER,
36
- maxDepth = DEFAULT_MAX_DEPTH
37
- } = config ?? {};
38
- if (depth > maxDepth) return data;
39
- if (data === null || data === void 0) {
40
- return data;
41
- }
42
- if (Array.isArray(data)) {
43
- return data.map((item) => sanitize(item, config, depth + 1));
44
- }
45
- if (typeof data === "object") {
46
- const result = {};
47
- for (const [key, value] of Object.entries(data)) {
48
- const lowerKey = key.toLowerCase();
49
- if (removeFields.some((field) => lowerKey === field)) {
50
- result[key] = placeholder;
51
- continue;
52
- }
53
- if (maskFields.some((field) => lowerKey.includes(field))) {
54
- if (typeof value === "string") {
55
- result[key] = partialMask(value, placeholder);
56
- } else {
57
- result[key] = placeholder;
58
- }
59
- continue;
60
- }
61
- result[key] = sanitize(value, config, depth + 1);
62
- }
63
- return result;
64
- }
65
- return data;
66
- }
67
- function sanitizeHeaders(headers, config) {
68
- const {
69
- maskFields = DEFAULT_MASK_FIELDS,
70
- placeholder = DEFAULT_PLACEHOLDER
71
- } = config ?? {};
72
- const result = {};
73
- for (const [key, value] of Object.entries(headers)) {
74
- const lowerKey = key.toLowerCase();
75
- if (lowerKey === "authorization") {
76
- if (value.startsWith("Bearer ")) {
77
- result[key] = "Bearer " + partialMask(value.slice(7), placeholder);
78
- } else {
79
- result[key] = partialMask(value, placeholder);
80
- }
81
- continue;
82
- }
83
- if (lowerKey === "cookie" || lowerKey === "set-cookie") {
84
- result[key] = placeholder;
85
- continue;
86
- }
87
- if (maskFields.some((field) => lowerKey.includes(field))) {
88
- result[key] = partialMask(value, placeholder);
89
- continue;
90
- }
91
- result[key] = value;
92
- }
93
- return result;
94
- }
95
-
96
- // src/index.ts
97
- function createRequestLogger(config) {
98
- const {
99
- storage,
100
- excludePaths = [],
101
- sanitize: sanitizeConfig,
102
- getUserId,
103
- getAppId,
104
- service,
105
- onError = console.error,
106
- enabled = true
107
- } = config;
108
- return async (req, next) => {
109
- if (!enabled) {
110
- return next();
111
- }
112
- const startTime = Date.now();
113
- const response = await next();
114
- recordLog(req, response, startTime, {
115
- storage,
116
- excludePaths,
117
- sanitizeConfig,
118
- getUserId,
119
- getAppId,
120
- service,
121
- onError
122
- }).catch(onError);
123
- return response;
124
- };
125
- }
126
- async function recordLog(req, response, startTime, options) {
127
- const { storage, excludePaths, sanitizeConfig, getUserId, getAppId, service } = options;
128
- const url = new URL(req.url);
129
- const path = url.pathname;
130
- const shouldExclude = excludePaths.some((pattern) => {
131
- if (typeof pattern === "string") {
132
- return path.includes(pattern);
133
- }
134
- return pattern.test(path);
135
- });
136
- if (shouldExclude) {
137
- return;
138
- }
139
- let body = null;
140
- try {
141
- const clonedReq = req.clone();
142
- const contentType = req.headers.get("content-type") || "";
143
- if (contentType.includes("application/json")) {
144
- body = await clonedReq.json();
145
- }
146
- } catch {
147
- }
148
- let responseData = {};
149
- try {
150
- const clonedRes = response.clone();
151
- responseData = await clonedRes.json();
152
- } catch {
153
- }
154
- const headers = {};
155
- req.headers.forEach((value, key) => {
156
- headers[key] = value;
157
- });
158
- const sanitizedHeaders = sanitizeHeaders(headers, sanitizeConfig);
159
- const sanitizedBody = sanitize(body, sanitizeConfig);
160
- const sanitizedResponseData = sanitize(responseData.data, sanitizeConfig);
161
- const now = /* @__PURE__ */ new Date();
162
- const duration = Date.now() - startTime;
163
- const userId = getUserId?.(req);
164
- const appId = getAppId?.(req);
165
- const requestLogId = await storage.saveRequestLog({
166
- method: req.method,
167
- url: req.url,
168
- path,
169
- headers: sanitizedHeaders,
170
- body: sanitizedBody,
171
- query: Object.fromEntries(url.searchParams),
172
- response: {
173
- success: responseData.success,
174
- message: responseData.message,
175
- code: responseData.code
176
- },
177
- status: response.status,
178
- duration,
179
- userId,
180
- appId,
181
- service,
182
- createdAt: now
183
- });
184
- await storage.saveResponseLog({
185
- requestLogId,
186
- success: responseData.success,
187
- message: responseData.message,
188
- code: responseData.code,
189
- data: sanitizedResponseData,
190
- createdAt: now
191
- });
192
- }
193
- function createMongoAdapter(db, logsCollection = "logs", logsResponseCollection = "logsResponse") {
194
- return {
195
- async saveRequestLog(log) {
196
- const result = await db.collection(logsCollection).insertOne({
197
- ...log,
198
- createAt: log.createdAt,
199
- updateAt: log.createdAt
200
- });
201
- return result.insertedId.toHexString();
202
- },
203
- async saveResponseLog(log) {
204
- await db.collection(logsResponseCollection).insertOne({
205
- logsId: log.requestLogId,
206
- ...log,
207
- createAt: log.createdAt,
208
- updateAt: log.createdAt
209
- });
210
- }
211
- };
212
- }
213
- function createConsoleAdapter() {
214
- let idCounter = 0;
215
- return {
216
- async saveRequestLog(log) {
217
- const id = `log_${++idCounter}`;
218
- console.log(`[REQUEST] ${log.method} ${log.path} ${log.status} ${log.duration}ms`);
219
- return id;
220
- },
221
- async saveResponseLog(log) {
222
- if (!log.success) {
223
- console.log(`[RESPONSE ERROR] ${log.message}`);
224
- }
225
- }
226
- };
227
- }
228
- var index_default = createRequestLogger;
229
- export {
230
- createConsoleAdapter,
231
- createMongoAdapter,
232
- createRequestLogger,
233
- index_default as default,
234
- sanitize,
235
- sanitizeHeaders
236
- };
237
- //# sourceMappingURL=index.js.map
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/sanitize.ts","../src/index.ts"],"sourcesContent":["/**\n * 敏感数据清洗工具\n * \n * 用于在记录日志前移除或脱敏敏感信息\n */\n\n// ============ Types ============\n\nexport interface SanitizeConfig {\n /** 需要完全移除的字段(小写) */\n removeFields?: string[]\n /** 需要脱敏的字段(小写,部分匹配) */\n maskFields?: string[]\n /** 脱敏占位符 @default '[REDACTED]' */\n placeholder?: string\n /** 最大递归深度 @default 10 */\n maxDepth?: number\n}\n\n// ============ Default Config ============\n\n/** 默认需要完全移除的敏感字段 */\nconst DEFAULT_REMOVE_FIELDS = [\n 'password',\n 'newpassword',\n 'oldpassword',\n 'confirmpassword',\n 'secret',\n 'secretkey',\n 'privatekey',\n 'apisecret',\n 'clientsecret',\n]\n\n/** 默认需要脱敏的字段(保留部分信息) */\nconst DEFAULT_MASK_FIELDS = [\n 'token',\n 'accesstoken',\n 'refreshtoken',\n 'authorization',\n 'apikey',\n 'api_key',\n 'x-api-key',\n 'idtoken',\n 'sessiontoken',\n 'bearer',\n]\n\nconst DEFAULT_PLACEHOLDER = '[REDACTED]'\nconst DEFAULT_MAX_DEPTH = 10\n\n// ============ Sanitize Functions ============\n\n/**\n * 部分脱敏(保留前4后4位)\n */\nfunction partialMask(value: string, placeholder: string): string {\n if (value.length <= 8) return placeholder\n return value.slice(0, 4) + '****' + value.slice(-4)\n}\n\n/**\n * 深度清洗对象中的敏感数据\n * \n * @example\n * ```typescript\n * const data = { password: '123456', token: 'eyJhbG...' }\n * const sanitized = sanitize(data)\n * // { password: '[REDACTED]', token: 'eyJh****...' }\n * ```\n */\nexport function sanitize<T>(data: T, config?: SanitizeConfig, depth = 0): T {\n const {\n removeFields = DEFAULT_REMOVE_FIELDS,\n maskFields = DEFAULT_MASK_FIELDS,\n placeholder = DEFAULT_PLACEHOLDER,\n maxDepth = DEFAULT_MAX_DEPTH,\n } = config ?? {}\n\n // 防止无限递归\n if (depth > maxDepth) return data\n \n if (data === null || data === undefined) {\n return data\n }\n\n // 处理数组\n if (Array.isArray(data)) {\n return data.map(item => sanitize(item, config, depth + 1)) as T\n }\n\n // 处理对象\n if (typeof data === 'object') {\n const result: Record<string, unknown> = {}\n \n for (const [key, value] of Object.entries(data)) {\n const lowerKey = key.toLowerCase()\n \n // 完全移除的字段\n if (removeFields.some(field => lowerKey === field)) {\n result[key] = placeholder\n continue\n }\n \n // 部分脱敏的字段\n if (maskFields.some(field => lowerKey.includes(field))) {\n if (typeof value === 'string') {\n result[key] = partialMask(value, placeholder)\n } else {\n result[key] = placeholder\n }\n continue\n }\n \n // 递归处理嵌套对象\n result[key] = sanitize(value, config, depth + 1)\n }\n \n return result as T\n }\n\n return data\n}\n\n/**\n * 清洗 HTTP 请求头\n * \n * @example\n * ```typescript\n * const headers = { Authorization: 'Bearer eyJhbG...', Cookie: 'session=xxx' }\n * const sanitized = sanitizeHeaders(headers)\n * // { Authorization: 'Bearer eyJh****...', Cookie: '[REDACTED]' }\n * ```\n */\nexport function sanitizeHeaders(\n headers: Record<string, string>,\n config?: SanitizeConfig\n): Record<string, string> {\n const {\n maskFields = DEFAULT_MASK_FIELDS,\n placeholder = DEFAULT_PLACEHOLDER,\n } = config ?? {}\n\n const result: Record<string, string> = {}\n \n for (const [key, value] of Object.entries(headers)) {\n const lowerKey = key.toLowerCase()\n \n // Authorization 头部分脱敏\n if (lowerKey === 'authorization') {\n if (value.startsWith('Bearer ')) {\n result[key] = 'Bearer ' + partialMask(value.slice(7), placeholder)\n } else {\n result[key] = partialMask(value, placeholder)\n }\n continue\n }\n \n // Cookie 完全脱敏\n if (lowerKey === 'cookie' || lowerKey === 'set-cookie') {\n result[key] = placeholder\n continue\n }\n \n // API Key 相关头脱敏\n if (maskFields.some(field => lowerKey.includes(field))) {\n result[key] = partialMask(value, placeholder)\n continue\n }\n \n result[key] = value\n }\n \n return result\n}\n\n/**\n * 检查值是否为敏感字段\n */\nexport function isSensitiveField(fieldName: string, config?: SanitizeConfig): boolean {\n const {\n removeFields = DEFAULT_REMOVE_FIELDS,\n maskFields = DEFAULT_MASK_FIELDS,\n } = config ?? {}\n\n const lowerName = fieldName.toLowerCase()\n \n return (\n removeFields.some(field => lowerName === field) ||\n maskFields.some(field => lowerName.includes(field))\n )\n}\n\n","/**\n * @vafast/request-logger - API request logging middleware for Vafast\n * \n * Features:\n * - Automatic sensitive data sanitization\n * - Pluggable storage adapters (MongoDB, custom)\n * - Async logging (non-blocking)\n * - Path exclusion support\n */\nimport type { Middleware } from 'vafast'\nimport { sanitize, sanitizeHeaders, type SanitizeConfig } from './sanitize'\n\n// ============ Types ============\n\nexport interface RequestLog {\n method: string\n url: string\n path: string\n headers: Record<string, string>\n body: unknown\n query: Record<string, string>\n response: {\n success?: boolean\n message?: string\n code?: number\n }\n status: number\n duration: number\n userId?: string\n appId?: string\n /** 服务标识(区分不同服务,如 auth-server、ones-server) */\n service?: string\n createdAt: Date\n}\n\nexport interface ResponseLog {\n requestLogId: string\n success?: boolean\n message?: string\n code?: number\n data?: unknown\n createdAt: Date\n}\n\n/**\n * 存储适配器接口\n */\nexport interface StorageAdapter {\n /** 存储请求日志 */\n saveRequestLog(log: RequestLog): Promise<string>\n /** 存储响应详情 */\n saveResponseLog(log: ResponseLog): Promise<void>\n}\n\nexport interface RequestLoggerConfig {\n /** 存储适配器 */\n storage: StorageAdapter\n /** 排除的路径(支持字符串或正则) */\n excludePaths?: (string | RegExp)[]\n /** 敏感数据清洗配置 */\n sanitize?: SanitizeConfig\n /** 获取用户 ID 的函数 */\n getUserId?: (req: Request) => string | undefined\n /** 获取应用 ID 的函数(用于多租户) */\n getAppId?: (req: Request) => string | undefined\n /** 服务标识(区分不同服务,如 auth-server、ones-server) */\n service?: string\n /** 错误回调 */\n onError?: (error: Error) => void\n /** 是否启用 @default true */\n enabled?: boolean\n}\n\n// ============ Middleware Factory ============\n\n/**\n * 创建请求日志中间件\n * \n * @example\n * ```typescript\n * import { createRequestLogger, createMongoAdapter } from '@vafast/request-logger'\n * import { mongoDb } from './mongodb'\n * \n * const requestLogger = createRequestLogger({\n * storage: createMongoAdapter(mongoDb, 'logs', 'logsResponse'),\n * excludePaths: ['/health', '/metrics'],\n * getUserId: (req) => getLocals(req)?.userInfo?.id,\n * })\n * \n * server.use(requestLogger)\n * ```\n */\nexport function createRequestLogger(config: RequestLoggerConfig): Middleware {\n const {\n storage,\n excludePaths = [],\n sanitize: sanitizeConfig,\n getUserId,\n getAppId,\n service,\n onError = console.error,\n enabled = true,\n } = config\n\n return async (req: Request, next: () => Promise<Response>) => {\n if (!enabled) {\n return next()\n }\n\n const startTime = Date.now()\n const response = await next()\n\n // 异步记录日志,不阻塞响应\n recordLog(req, response, startTime, {\n storage,\n excludePaths,\n sanitizeConfig,\n getUserId,\n getAppId,\n service,\n onError,\n }).catch(onError)\n\n return response\n }\n}\n\n// ============ Internal Functions ============\n\ninterface RecordLogOptions {\n storage: StorageAdapter\n excludePaths: (string | RegExp)[]\n sanitizeConfig?: SanitizeConfig\n getUserId?: (req: Request) => string | undefined\n getAppId?: (req: Request) => string | undefined\n service?: string\n onError: (error: Error) => void\n}\n\nasync function recordLog(\n req: Request,\n response: Response,\n startTime: number,\n options: RecordLogOptions\n) {\n const { storage, excludePaths, sanitizeConfig, getUserId, getAppId, service } = options\n\n const url = new URL(req.url)\n const path = url.pathname\n\n // 检查是否需要排除\n const shouldExclude = excludePaths.some(pattern => {\n if (typeof pattern === 'string') {\n return path.includes(pattern)\n }\n return pattern.test(path)\n })\n\n if (shouldExclude) {\n return\n }\n\n // 解析请求体\n let body: unknown = null\n try {\n const clonedReq = req.clone()\n const contentType = req.headers.get('content-type') || ''\n if (contentType.includes('application/json')) {\n body = await clonedReq.json()\n }\n } catch {\n // 忽略解析错误\n }\n\n // 解析响应体\n let responseData: { success?: boolean; message?: string; code?: number; data?: unknown } = {}\n try {\n const clonedRes = response.clone()\n responseData = await clonedRes.json()\n } catch {\n // 忽略解析错误\n }\n\n // 提取请求头\n const headers: Record<string, string> = {}\n req.headers.forEach((value, key) => {\n headers[key] = value\n })\n\n // 清洗敏感数据\n const sanitizedHeaders = sanitizeHeaders(headers, sanitizeConfig)\n const sanitizedBody = sanitize(body, sanitizeConfig)\n const sanitizedResponseData = sanitize(responseData.data, sanitizeConfig)\n\n const now = new Date()\n const duration = Date.now() - startTime\n\n // 获取用户 ID 和应用 ID\n const userId = getUserId?.(req)\n const appId = getAppId?.(req)\n\n // 存储请求日志\n const requestLogId = await storage.saveRequestLog({\n method: req.method,\n url: req.url,\n path,\n headers: sanitizedHeaders,\n body: sanitizedBody,\n query: Object.fromEntries(url.searchParams),\n response: {\n success: responseData.success,\n message: responseData.message,\n code: responseData.code,\n },\n status: response.status,\n duration,\n userId,\n appId,\n service,\n createdAt: now,\n })\n\n // 存储响应详情\n await storage.saveResponseLog({\n requestLogId,\n success: responseData.success,\n message: responseData.message,\n code: responseData.code,\n data: sanitizedResponseData,\n createdAt: now,\n })\n}\n\n// ============ MongoDB Adapter ============\n\n/**\n * 创建 MongoDB 存储适配器\n * \n * @example\n * ```typescript\n * import { Db } from 'mongodb'\n * import { createMongoAdapter } from '@vafast/request-logger'\n * \n * const adapter = createMongoAdapter(db, 'logs', 'logsResponse')\n * ```\n */\nexport function createMongoAdapter(\n db: { collection: (name: string) => { insertOne: (doc: any) => Promise<{ insertedId: { toHexString: () => string } }> } },\n logsCollection: string = 'logs',\n logsResponseCollection: string = 'logsResponse'\n): StorageAdapter {\n return {\n async saveRequestLog(log: RequestLog): Promise<string> {\n const result = await db.collection(logsCollection).insertOne({\n ...log,\n createAt: log.createdAt,\n updateAt: log.createdAt,\n })\n return result.insertedId.toHexString()\n },\n\n async saveResponseLog(log: ResponseLog): Promise<void> {\n await db.collection(logsResponseCollection).insertOne({\n logsId: log.requestLogId,\n ...log,\n createAt: log.createdAt,\n updateAt: log.createdAt,\n })\n },\n }\n}\n\n// ============ Console Adapter (for development) ============\n\n/**\n * 创建控制台存储适配器(用于开发调试)\n */\nexport function createConsoleAdapter(): StorageAdapter {\n let idCounter = 0\n\n return {\n async saveRequestLog(log: RequestLog): Promise<string> {\n const id = `log_${++idCounter}`\n console.log(`[REQUEST] ${log.method} ${log.path} ${log.status} ${log.duration}ms`)\n return id\n },\n\n async saveResponseLog(log: ResponseLog): Promise<void> {\n if (!log.success) {\n console.log(`[RESPONSE ERROR] ${log.message}`)\n }\n },\n }\n}\n\n// ============ Re-exports ============\n\nexport { sanitize, sanitizeHeaders, type SanitizeConfig } from './sanitize'\nexport default createRequestLogger\n\n"],"mappings":"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","names":[]}