@vafast/request-logger 0.1.1 → 0.1.2

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/dist/index.d.ts CHANGED
@@ -63,6 +63,7 @@ interface RequestLog {
63
63
  status: number;
64
64
  duration: number;
65
65
  userId?: string;
66
+ appId?: string;
66
67
  createdAt: Date;
67
68
  }
68
69
  interface ResponseLog {
@@ -91,6 +92,8 @@ interface RequestLoggerConfig {
91
92
  sanitize?: SanitizeConfig;
92
93
  /** 获取用户 ID 的函数 */
93
94
  getUserId?: (req: Request) => string | undefined;
95
+ /** 获取应用 ID 的函数(用于多租户) */
96
+ getAppId?: (req: Request) => string | undefined;
94
97
  /** 错误回调 */
95
98
  onError?: (error: Error) => void;
96
99
  /** 是否启用 @default true */
package/dist/index.js CHANGED
@@ -100,6 +100,7 @@ function createRequestLogger(config) {
100
100
  excludePaths = [],
101
101
  sanitize: sanitizeConfig,
102
102
  getUserId,
103
+ getAppId,
103
104
  onError = console.error,
104
105
  enabled = true
105
106
  } = config;
@@ -114,13 +115,14 @@ function createRequestLogger(config) {
114
115
  excludePaths,
115
116
  sanitizeConfig,
116
117
  getUserId,
118
+ getAppId,
117
119
  onError
118
120
  }).catch(onError);
119
121
  return response;
120
122
  };
121
123
  }
122
124
  async function recordLog(req, response, startTime, options) {
123
- const { storage, excludePaths, sanitizeConfig, getUserId } = options;
125
+ const { storage, excludePaths, sanitizeConfig, getUserId, getAppId } = options;
124
126
  const url = new URL(req.url);
125
127
  const path = url.pathname;
126
128
  const shouldExclude = excludePaths.some((pattern) => {
@@ -157,6 +159,7 @@ async function recordLog(req, response, startTime, options) {
157
159
  const now = /* @__PURE__ */ new Date();
158
160
  const duration = Date.now() - startTime;
159
161
  const userId = getUserId?.(req);
162
+ const appId = getAppId?.(req);
160
163
  const requestLogId = await storage.saveRequestLog({
161
164
  method: req.method,
162
165
  url: req.url,
@@ -172,6 +175,7 @@ async function recordLog(req, response, startTime, options) {
172
175
  status: response.status,
173
176
  duration,
174
177
  userId,
178
+ appId,
175
179
  createdAt: now
176
180
  });
177
181
  await storage.saveResponseLog({
package/dist/index.js.map CHANGED
@@ -1 +1 @@
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 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 /** 错误回调 */\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 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 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 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 } = 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\n const userId = getUserId?.(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 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":[]}
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 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 /** 错误回调 */\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 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 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 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 } = 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 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":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vafast/request-logger",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "API request logging middleware for Vafast with sensitive data sanitization",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",