@spfn/core 0.2.0-beta.23 → 0.2.0-beta.24

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.
@@ -5,6 +5,19 @@ import { randomBytes } from 'crypto';
5
5
 
6
6
  // src/middleware/error-handler.ts
7
7
  var errorLogger = logger.child("@spfn/core:error-handler");
8
+ function extractCauseMessage(err) {
9
+ const cause = err.cause;
10
+ if (!cause) {
11
+ return void 0;
12
+ }
13
+ if (cause instanceof Error) {
14
+ return cause.message;
15
+ }
16
+ if (typeof cause === "string") {
17
+ return cause;
18
+ }
19
+ return String(cause);
20
+ }
8
21
  var SENSITIVE_HEADERS = /* @__PURE__ */ new Set(["authorization", "cookie", "x-api-key", "x-auth-token"]);
9
22
  function extractHeaders(c) {
10
23
  const headers = {};
@@ -48,12 +61,14 @@ function ErrorHandler(options = {}) {
48
61
  return (err, c) => {
49
62
  const path = c.req.path;
50
63
  const method = c.req.method;
64
+ const causeMessage = extractCauseMessage(err);
51
65
  if (isSerializableError(err)) {
52
66
  const { statusCode: statusCode2 } = err;
53
67
  if (enableLogging) {
54
68
  logError(err, {
55
69
  type: err.constructor.name,
56
70
  message: err.message,
71
+ cause: causeMessage,
57
72
  statusCode: statusCode2,
58
73
  path,
59
74
  method
@@ -75,6 +90,7 @@ function ErrorHandler(options = {}) {
75
90
  logError(err, {
76
91
  type: err.name || "Error",
77
92
  message: err.message,
93
+ cause: causeMessage,
78
94
  statusCode,
79
95
  path,
80
96
  method
@@ -88,6 +104,9 @@ function ErrorHandler(options = {}) {
88
104
  __type: "Error",
89
105
  message: err.message || "Internal Server Error"
90
106
  };
107
+ if (causeMessage) {
108
+ response.cause = causeMessage;
109
+ }
91
110
  if (includeStack && err.stack) {
92
111
  response.stack = err.stack;
93
112
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/middleware/error-handler.ts","../../src/middleware/request-logger.ts"],"names":["statusCode","logger"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Error Handler Middleware\n *\n * Handles SerializableError with automatic serialization and standard errors\n */\nimport type { Context } from 'hono';\nimport type { ContentfulStatusCode } from 'hono/utils/http-status';\nimport { SerializableError } from '@spfn/core/errors';\nimport { logger } from '@spfn/core/logger';\nimport { env } from '@spfn/core/config';\n\nconst errorLogger = logger.child('@spfn/core:error-handler');\n\n/**\n * Options for ErrorHandler middleware\n */\nexport interface ErrorHandlerOptions\n{\n /**\n * Include stack trace in error response\n *\n * Useful for debugging in development, should be disabled in production.\n *\n * @default env.NODE_ENV !== 'production'\n */\n includeStack?: boolean;\n\n /**\n * Enable error logging to console\n *\n * Logs errors with appropriate level (warn for 4xx, error for 5xx).\n *\n * @default true\n */\n enableLogging?: boolean;\n\n /**\n * Callback invoked when an error occurs\n *\n * Called asynchronously without blocking the response.\n * Useful for external error notifications (Slack, PagerDuty, etc.)\n */\n onError?: (\n err: Error,\n context: OnErrorContext\n ) => Promise<void> | void;\n}\n\n/**\n * Context passed to onError callback\n */\nexport interface OnErrorContext\n{\n statusCode: number;\n path: string;\n method: string;\n requestId?: string;\n timestamp: string;\n userId?: string;\n request: {\n headers: Record<string, string>;\n query: Record<string, string>;\n };\n}\n\ninterface ErrorWithStatusCode extends Error\n{\n statusCode?: number;\n details?: Record<string, unknown>;\n}\n\ninterface SerializableErrorLike extends Error\n{\n statusCode: number;\n toJSON(): Record<string, unknown>;\n}\n\ninterface ErrorLogData extends Record<string, unknown>\n{\n type: string;\n message: string;\n statusCode: number;\n path: string;\n method: string;\n}\n\ninterface StandardErrorResponse\n{\n __type: string;\n message: string;\n stack?: string;\n}\n\nconst SENSITIVE_HEADERS = new Set(['authorization', 'cookie', 'x-api-key', 'x-auth-token']);\n\n/**\n * Extract headers from request, masking sensitive values\n */\nfunction extractHeaders(c: Context): Record<string, string>\n{\n const headers: Record<string, string> = {};\n\n c.req.raw.headers.forEach((value, key) =>\n {\n headers[key] = SENSITIVE_HEADERS.has(key.toLowerCase()) ? '***' : value;\n });\n\n return headers;\n}\n\n/**\n * Build onError context from Hono context\n */\nfunction buildOnErrorContext(c: Context, statusCode: number): OnErrorContext\n{\n const auth = c.get('auth') as { userId?: string } | undefined;\n\n return {\n statusCode,\n path: c.req.path,\n method: c.req.method,\n requestId: c.get('requestId') as string | undefined,\n timestamp: new Date().toISOString(),\n userId: auth?.userId,\n request: {\n headers: extractHeaders(c),\n query: c.req.query(),\n },\n };\n}\n\n/**\n * Log error with appropriate level based on status code\n */\nfunction logError(\n err: Error,\n logData: ErrorLogData,\n includeStack: boolean\n): void\n{\n const logLevel = logData.statusCode >= 500 ? 'error' : 'warn';\n\n if (includeStack)\n {\n errorLogger[logLevel]('Error occurred', err, logData);\n }\n else\n {\n errorLogger[logLevel]('Error occurred', logData);\n }\n}\n\n/**\n * Type guard for SerializableError\n *\n * Uses duck typing to handle module duplication issues in dev mode (tsx)\n */\nfunction isSerializableError(err: Error): err is SerializableErrorLike\n{\n return err instanceof SerializableError ||\n (typeof (err as any).toJSON === 'function' &&\n typeof (err as any).statusCode === 'number');\n}\n\n/**\n * Error handler middleware for Hono\n *\n * Handles SerializableError with automatic serialization and standard errors.\n * SerializableError instances are serialized using their toJSON() method,\n * preserving custom fields like `resource`, `fields`, etc.\n *\n * @param options - Configuration options\n * @returns Error handler function for Hono's onError hook\n *\n * @example\n * ```typescript\n * import { Hono } from 'hono';\n * import { ErrorHandler } from '@spfn/core/middleware';\n *\n * const app = new Hono();\n *\n * // Register error handler\n * app.onError(ErrorHandler({\n * includeStack: process.env.NODE_ENV !== 'production',\n * enableLogging: true,\n * }));\n *\n * // Throw SerializableError in routes\n * app.get('/users/:id', (c) => {\n * throw new NotFoundError({ message: 'User not found', resource: 'User' });\n * // Response: { __type: 'NotFoundError', message: 'User not found', resource: 'User' }\n * });\n * ```\n */\nexport function ErrorHandler(options: ErrorHandlerOptions = {}): (err: Error, c: Context) => Response | Promise<Response>\n{\n const {\n includeStack = env.NODE_ENV !== 'production',\n enableLogging = true,\n onError,\n } = options;\n\n return (err: Error, c: Context) =>\n {\n const path = c.req.path;\n const method = c.req.method;\n\n // Handle SerializableError with automatic serialization\n if (isSerializableError(err))\n {\n const { statusCode } = err;\n\n if (enableLogging)\n {\n logError(err, {\n type: err.constructor.name,\n message: err.message,\n statusCode,\n path,\n method,\n }, includeStack);\n }\n\n // Fire onError callback (non-blocking)\n if (onError)\n {\n const ctx = buildOnErrorContext(c, statusCode);\n Promise.resolve(onError(err, ctx))\n .catch(e => errorLogger.warn('onError callback failed', e as Error));\n }\n\n // Use toJSON() for automatic serialization\n const serialized = err.toJSON();\n\n // Add stack trace in development\n if (includeStack && err.stack)\n {\n serialized.stack = err.stack;\n }\n\n return c.json(serialized, statusCode as ContentfulStatusCode);\n }\n\n // Handle standard errors (fallback)\n const errorWithCode = err as ErrorWithStatusCode;\n const statusCode = errorWithCode.statusCode || 500;\n\n if (enableLogging)\n {\n logError(err, {\n type: err.name || 'Error',\n message: err.message,\n statusCode,\n path,\n method,\n }, includeStack);\n }\n\n // Fire onError callback (non-blocking)\n if (onError)\n {\n const ctx = buildOnErrorContext(c, statusCode);\n Promise.resolve(onError(err, ctx))\n .catch(e => errorLogger.warn('onError callback failed', e as Error));\n }\n\n // Standard error response\n const response: StandardErrorResponse = {\n __type: 'Error',\n message: err.message || 'Internal Server Error',\n };\n\n if (includeStack && err.stack)\n {\n response.stack = err.stack;\n }\n\n return c.json(response, statusCode as ContentfulStatusCode);\n };\n}\n","/**\n * Request Logger Middleware\n *\n * Automatic API request/response logging with performance monitoring\n */\nimport { randomBytes } from 'crypto';\nimport type { Context, Next } from 'hono';\nimport { logger } from '@spfn/core/logger';\n\n/**\n * Options for RequestLogger middleware\n */\nexport interface RequestLoggerOptions\n{\n /**\n * Paths to exclude from logging\n *\n * Supports exact match and prefix match (e.g., '/health' excludes '/health/db').\n *\n * @default ['/health', '/ping', '/favicon.ico']\n *\n * @example\n * ```typescript\n * excludePaths: ['/health', '/metrics', '/_next']\n * ```\n */\n excludePaths?: string[];\n\n /**\n * Field names to mask in logged request bodies\n *\n * Case-insensitive partial matching (e.g., 'password' masks 'userPassword').\n *\n * @default ['password', 'token', 'apiKey', 'secret', 'authorization']\n *\n * @example\n * ```typescript\n * sensitiveFields: ['password', 'creditCard', 'ssn']\n * ```\n */\n sensitiveFields?: string[];\n\n /**\n * Threshold in milliseconds for marking requests as slow\n *\n * Slow requests are logged with `slow: true` flag.\n *\n * @default 1000\n */\n slowRequestThreshold?: number;\n}\n\n/**\n * @deprecated Use RequestLoggerOptions instead\n */\nexport type RequestLoggerConfig = RequestLoggerOptions;\n\nconst DEFAULT_CONFIG: Required<RequestLoggerConfig> = {\n excludePaths: ['/health', '/ping', '/favicon.ico'],\n sensitiveFields: ['password', 'token', 'apiKey', 'secret', 'authorization'],\n slowRequestThreshold: 1000,\n};\n\n/**\n * Generate cryptographically secure request ID\n */\nfunction generateRequestId(): string\n{\n const timestamp = Date.now();\n const randomPart = randomBytes(6).toString('hex');\n return `req_${timestamp}_${randomPart}`;\n}\n\n/**\n * Mask sensitive data with circular reference handling\n */\nexport function maskSensitiveData(\n obj: any,\n sensitiveFields: string[],\n seen = new WeakSet()\n): any\n{\n if (!obj || typeof obj !== 'object') return obj;\n\n if (seen.has(obj)) return '[Circular]';\n seen.add(obj);\n\n const lowerFields = sensitiveFields.map(f => f.toLowerCase());\n const masked = Array.isArray(obj) ? [...obj] : { ...obj };\n\n for (const key in masked)\n {\n const lowerKey = key.toLowerCase();\n\n if (lowerFields.some(field => lowerKey.includes(field)))\n {\n masked[key] = '***MASKED***';\n }\n else if (typeof masked[key] === 'object' && masked[key] !== null)\n {\n masked[key] = maskSensitiveData(masked[key], sensitiveFields, seen);\n }\n }\n\n return masked;\n}\n\n/**\n * Request logger middleware for Hono\n *\n * Logs incoming requests with method, path, IP, and user agent.\n * Logs completed requests with status code and duration.\n * Automatically generates unique request IDs and masks sensitive data.\n *\n * @param options - Configuration options\n * @returns Hono middleware function\n *\n * @example\n * ```typescript\n * import { Hono } from 'hono';\n * import { RequestLogger } from '@spfn/core/middleware';\n *\n * const app = new Hono();\n *\n * // Add request logging\n * app.use(RequestLogger({\n * excludePaths: ['/health', '/metrics'],\n * sensitiveFields: ['password', 'token'],\n * slowRequestThreshold: 2000,\n * }));\n *\n * // Access request ID in handlers\n * app.get('/users', (c) => {\n * const requestId = c.get('requestId');\n * return c.json({ requestId });\n * });\n * ```\n */\nexport function RequestLogger(options?: RequestLoggerOptions)\n{\n const cfg = { ...DEFAULT_CONFIG, ...options };\n const apiLogger = logger.child('@spfn/core:api');\n\n return async (c: Context, next: Next) =>\n {\n const path = new URL(c.req.url).pathname;\n\n // Support both exact match and prefix match for excluded paths\n const isExcluded = cfg.excludePaths.some(excludePath =>\n path === excludePath || path.startsWith(excludePath + '/')\n );\n\n if (isExcluded)\n {\n return next();\n }\n\n const requestId = generateRequestId();\n c.set('requestId', requestId);\n\n const method = c.req.method;\n const userAgent = c.req.header('user-agent');\n\n // Extract client IP from proxy chain (first IP is the original client)\n const forwardedFor = c.req.header('x-forwarded-for');\n const ip = forwardedFor?.split(',')[0]?.trim()\n || c.req.header('x-real-ip')\n || 'unknown';\n\n const startTime = Date.now();\n\n apiLogger.info('Request received', {\n requestId,\n method,\n path,\n ip,\n userAgent,\n });\n\n try\n {\n await next();\n\n const duration = Date.now() - startTime;\n const status = c.res.status;\n\n const logData: Record<string, any> = {\n requestId,\n method,\n path,\n status,\n duration,\n };\n\n const isSlowRequest = duration >= cfg.slowRequestThreshold;\n if (isSlowRequest)\n {\n logData.slow = true;\n }\n\n // Add detailed error information for 4xx/5xx responses\n if (status >= 400)\n {\n try\n {\n // Clone response to read body without consuming it\n logData.response = await c.res.clone().json();\n }\n catch\n {\n // Response is not JSON or already consumed - ignore\n }\n\n // Add request body for POST/PUT/PATCH to see what data caused the error\n if (['POST', 'PUT', 'PATCH'].includes(method))\n {\n try\n {\n // Try to get the already parsed body from context\n const requestBody = await c.req.json();\n logData.request = maskSensitiveData(requestBody, cfg.sensitiveFields);\n }\n catch\n {\n // Body is not JSON or already consumed - ignore\n }\n }\n }\n\n const logLevel = status >= 500 ? 'error' : status >= 400 ? 'warn' : 'info';\n apiLogger[logLevel]('Request completed', logData);\n }\n catch (error)\n {\n const duration = Date.now() - startTime;\n\n apiLogger.error('Request failed', error as Error, {\n requestId,\n method,\n path,\n duration,\n });\n\n throw error;\n }\n };\n}"]}
1
+ {"version":3,"sources":["../../src/middleware/error-handler.ts","../../src/middleware/request-logger.ts"],"names":["statusCode","logger"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Error Handler Middleware\n *\n * Handles SerializableError with automatic serialization and standard errors\n */\nimport type { Context } from 'hono';\nimport type { ContentfulStatusCode } from 'hono/utils/http-status';\nimport { SerializableError } from '@spfn/core/errors';\nimport { logger } from '@spfn/core/logger';\nimport { env } from '@spfn/core/config';\n\nconst errorLogger = logger.child('@spfn/core:error-handler');\n\n/**\n * Options for ErrorHandler middleware\n */\nexport interface ErrorHandlerOptions\n{\n /**\n * Include stack trace in error response\n *\n * Useful for debugging in development, should be disabled in production.\n *\n * @default env.NODE_ENV !== 'production'\n */\n includeStack?: boolean;\n\n /**\n * Enable error logging to console\n *\n * Logs errors with appropriate level (warn for 4xx, error for 5xx).\n *\n * @default true\n */\n enableLogging?: boolean;\n\n /**\n * Callback invoked when an error occurs\n *\n * Called asynchronously without blocking the response.\n * Useful for external error notifications (Slack, PagerDuty, etc.)\n */\n onError?: (\n err: Error,\n context: OnErrorContext\n ) => Promise<void> | void;\n}\n\n/**\n * Context passed to onError callback\n */\nexport interface OnErrorContext\n{\n statusCode: number;\n path: string;\n method: string;\n requestId?: string;\n timestamp: string;\n userId?: string;\n request: {\n headers: Record<string, string>;\n query: Record<string, string>;\n };\n}\n\ninterface ErrorWithStatusCode extends Error\n{\n statusCode?: number;\n details?: Record<string, unknown>;\n}\n\ninterface SerializableErrorLike extends Error\n{\n statusCode: number;\n toJSON(): Record<string, unknown>;\n}\n\ninterface ErrorLogData extends Record<string, unknown>\n{\n type: string;\n message: string;\n statusCode: number;\n path: string;\n method: string;\n cause?: string;\n}\n\ninterface StandardErrorResponse\n{\n __type: string;\n message: string;\n cause?: string;\n stack?: string;\n}\n\n/**\n * Extract root cause message from nested Error.cause chain\n */\nfunction extractCauseMessage(err: Error): string | undefined\n{\n const cause = (err as any).cause;\n\n if (!cause)\n {\n return undefined;\n }\n\n if (cause instanceof Error)\n {\n return cause.message;\n }\n\n if (typeof cause === 'string')\n {\n return cause;\n }\n\n return String(cause);\n}\n\nconst SENSITIVE_HEADERS = new Set(['authorization', 'cookie', 'x-api-key', 'x-auth-token']);\n\n/**\n * Extract headers from request, masking sensitive values\n */\nfunction extractHeaders(c: Context): Record<string, string>\n{\n const headers: Record<string, string> = {};\n\n c.req.raw.headers.forEach((value, key) =>\n {\n headers[key] = SENSITIVE_HEADERS.has(key.toLowerCase()) ? '***' : value;\n });\n\n return headers;\n}\n\n/**\n * Build onError context from Hono context\n */\nfunction buildOnErrorContext(c: Context, statusCode: number): OnErrorContext\n{\n const auth = c.get('auth') as { userId?: string } | undefined;\n\n return {\n statusCode,\n path: c.req.path,\n method: c.req.method,\n requestId: c.get('requestId') as string | undefined,\n timestamp: new Date().toISOString(),\n userId: auth?.userId,\n request: {\n headers: extractHeaders(c),\n query: c.req.query(),\n },\n };\n}\n\n/**\n * Log error with appropriate level based on status code\n */\nfunction logError(\n err: Error,\n logData: ErrorLogData,\n includeStack: boolean\n): void\n{\n const logLevel = logData.statusCode >= 500 ? 'error' : 'warn';\n\n if (includeStack)\n {\n errorLogger[logLevel]('Error occurred', err, logData);\n }\n else\n {\n errorLogger[logLevel]('Error occurred', logData);\n }\n}\n\n/**\n * Type guard for SerializableError\n *\n * Uses duck typing to handle module duplication issues in dev mode (tsx)\n */\nfunction isSerializableError(err: Error): err is SerializableErrorLike\n{\n return err instanceof SerializableError ||\n (typeof (err as any).toJSON === 'function' &&\n typeof (err as any).statusCode === 'number');\n}\n\n/**\n * Error handler middleware for Hono\n *\n * Handles SerializableError with automatic serialization and standard errors.\n * SerializableError instances are serialized using their toJSON() method,\n * preserving custom fields like `resource`, `fields`, etc.\n *\n * @param options - Configuration options\n * @returns Error handler function for Hono's onError hook\n *\n * @example\n * ```typescript\n * import { Hono } from 'hono';\n * import { ErrorHandler } from '@spfn/core/middleware';\n *\n * const app = new Hono();\n *\n * // Register error handler\n * app.onError(ErrorHandler({\n * includeStack: process.env.NODE_ENV !== 'production',\n * enableLogging: true,\n * }));\n *\n * // Throw SerializableError in routes\n * app.get('/users/:id', (c) => {\n * throw new NotFoundError({ message: 'User not found', resource: 'User' });\n * // Response: { __type: 'NotFoundError', message: 'User not found', resource: 'User' }\n * });\n * ```\n */\nexport function ErrorHandler(options: ErrorHandlerOptions = {}): (err: Error, c: Context) => Response | Promise<Response>\n{\n const {\n includeStack = env.NODE_ENV !== 'production',\n enableLogging = true,\n onError,\n } = options;\n\n return (err: Error, c: Context) =>\n {\n const path = c.req.path;\n const method = c.req.method;\n\n const causeMessage = extractCauseMessage(err);\n\n // Handle SerializableError with automatic serialization\n if (isSerializableError(err))\n {\n const { statusCode } = err;\n\n if (enableLogging)\n {\n logError(err, {\n type: err.constructor.name,\n message: err.message,\n cause: causeMessage,\n statusCode,\n path,\n method,\n }, includeStack);\n }\n\n // Fire onError callback (non-blocking)\n if (onError)\n {\n const ctx = buildOnErrorContext(c, statusCode);\n Promise.resolve(onError(err, ctx))\n .catch(e => errorLogger.warn('onError callback failed', e as Error));\n }\n\n // Use toJSON() for automatic serialization\n const serialized = err.toJSON();\n\n // Add stack trace in development\n if (includeStack && err.stack)\n {\n serialized.stack = err.stack;\n }\n\n return c.json(serialized, statusCode as ContentfulStatusCode);\n }\n\n // Handle standard errors (fallback)\n const errorWithCode = err as ErrorWithStatusCode;\n const statusCode = errorWithCode.statusCode || 500;\n\n if (enableLogging)\n {\n logError(err, {\n type: err.name || 'Error',\n message: err.message,\n cause: causeMessage,\n statusCode,\n path,\n method,\n }, includeStack);\n }\n\n // Fire onError callback (non-blocking)\n if (onError)\n {\n const ctx = buildOnErrorContext(c, statusCode);\n Promise.resolve(onError(err, ctx))\n .catch(e => errorLogger.warn('onError callback failed', e as Error));\n }\n\n // Standard error response\n const response: StandardErrorResponse = {\n __type: 'Error',\n message: err.message || 'Internal Server Error',\n };\n\n if (causeMessage)\n {\n response.cause = causeMessage;\n }\n\n if (includeStack && err.stack)\n {\n response.stack = err.stack;\n }\n\n return c.json(response, statusCode as ContentfulStatusCode);\n };\n}\n","/**\n * Request Logger Middleware\n *\n * Automatic API request/response logging with performance monitoring\n */\nimport { randomBytes } from 'crypto';\nimport type { Context, Next } from 'hono';\nimport { logger } from '@spfn/core/logger';\n\n/**\n * Options for RequestLogger middleware\n */\nexport interface RequestLoggerOptions\n{\n /**\n * Paths to exclude from logging\n *\n * Supports exact match and prefix match (e.g., '/health' excludes '/health/db').\n *\n * @default ['/health', '/ping', '/favicon.ico']\n *\n * @example\n * ```typescript\n * excludePaths: ['/health', '/metrics', '/_next']\n * ```\n */\n excludePaths?: string[];\n\n /**\n * Field names to mask in logged request bodies\n *\n * Case-insensitive partial matching (e.g., 'password' masks 'userPassword').\n *\n * @default ['password', 'token', 'apiKey', 'secret', 'authorization']\n *\n * @example\n * ```typescript\n * sensitiveFields: ['password', 'creditCard', 'ssn']\n * ```\n */\n sensitiveFields?: string[];\n\n /**\n * Threshold in milliseconds for marking requests as slow\n *\n * Slow requests are logged with `slow: true` flag.\n *\n * @default 1000\n */\n slowRequestThreshold?: number;\n}\n\n/**\n * @deprecated Use RequestLoggerOptions instead\n */\nexport type RequestLoggerConfig = RequestLoggerOptions;\n\nconst DEFAULT_CONFIG: Required<RequestLoggerConfig> = {\n excludePaths: ['/health', '/ping', '/favicon.ico'],\n sensitiveFields: ['password', 'token', 'apiKey', 'secret', 'authorization'],\n slowRequestThreshold: 1000,\n};\n\n/**\n * Generate cryptographically secure request ID\n */\nfunction generateRequestId(): string\n{\n const timestamp = Date.now();\n const randomPart = randomBytes(6).toString('hex');\n return `req_${timestamp}_${randomPart}`;\n}\n\n/**\n * Mask sensitive data with circular reference handling\n */\nexport function maskSensitiveData(\n obj: any,\n sensitiveFields: string[],\n seen = new WeakSet()\n): any\n{\n if (!obj || typeof obj !== 'object') return obj;\n\n if (seen.has(obj)) return '[Circular]';\n seen.add(obj);\n\n const lowerFields = sensitiveFields.map(f => f.toLowerCase());\n const masked = Array.isArray(obj) ? [...obj] : { ...obj };\n\n for (const key in masked)\n {\n const lowerKey = key.toLowerCase();\n\n if (lowerFields.some(field => lowerKey.includes(field)))\n {\n masked[key] = '***MASKED***';\n }\n else if (typeof masked[key] === 'object' && masked[key] !== null)\n {\n masked[key] = maskSensitiveData(masked[key], sensitiveFields, seen);\n }\n }\n\n return masked;\n}\n\n/**\n * Request logger middleware for Hono\n *\n * Logs incoming requests with method, path, IP, and user agent.\n * Logs completed requests with status code and duration.\n * Automatically generates unique request IDs and masks sensitive data.\n *\n * @param options - Configuration options\n * @returns Hono middleware function\n *\n * @example\n * ```typescript\n * import { Hono } from 'hono';\n * import { RequestLogger } from '@spfn/core/middleware';\n *\n * const app = new Hono();\n *\n * // Add request logging\n * app.use(RequestLogger({\n * excludePaths: ['/health', '/metrics'],\n * sensitiveFields: ['password', 'token'],\n * slowRequestThreshold: 2000,\n * }));\n *\n * // Access request ID in handlers\n * app.get('/users', (c) => {\n * const requestId = c.get('requestId');\n * return c.json({ requestId });\n * });\n * ```\n */\nexport function RequestLogger(options?: RequestLoggerOptions)\n{\n const cfg = { ...DEFAULT_CONFIG, ...options };\n const apiLogger = logger.child('@spfn/core:api');\n\n return async (c: Context, next: Next) =>\n {\n const path = new URL(c.req.url).pathname;\n\n // Support both exact match and prefix match for excluded paths\n const isExcluded = cfg.excludePaths.some(excludePath =>\n path === excludePath || path.startsWith(excludePath + '/')\n );\n\n if (isExcluded)\n {\n return next();\n }\n\n const requestId = generateRequestId();\n c.set('requestId', requestId);\n\n const method = c.req.method;\n const userAgent = c.req.header('user-agent');\n\n // Extract client IP from proxy chain (first IP is the original client)\n const forwardedFor = c.req.header('x-forwarded-for');\n const ip = forwardedFor?.split(',')[0]?.trim()\n || c.req.header('x-real-ip')\n || 'unknown';\n\n const startTime = Date.now();\n\n apiLogger.info('Request received', {\n requestId,\n method,\n path,\n ip,\n userAgent,\n });\n\n try\n {\n await next();\n\n const duration = Date.now() - startTime;\n const status = c.res.status;\n\n const logData: Record<string, any> = {\n requestId,\n method,\n path,\n status,\n duration,\n };\n\n const isSlowRequest = duration >= cfg.slowRequestThreshold;\n if (isSlowRequest)\n {\n logData.slow = true;\n }\n\n // Add detailed error information for 4xx/5xx responses\n if (status >= 400)\n {\n try\n {\n // Clone response to read body without consuming it\n logData.response = await c.res.clone().json();\n }\n catch\n {\n // Response is not JSON or already consumed - ignore\n }\n\n // Add request body for POST/PUT/PATCH to see what data caused the error\n if (['POST', 'PUT', 'PATCH'].includes(method))\n {\n try\n {\n // Try to get the already parsed body from context\n const requestBody = await c.req.json();\n logData.request = maskSensitiveData(requestBody, cfg.sensitiveFields);\n }\n catch\n {\n // Body is not JSON or already consumed - ignore\n }\n }\n }\n\n const logLevel = status >= 500 ? 'error' : status >= 400 ? 'warn' : 'info';\n apiLogger[logLevel]('Request completed', logData);\n }\n catch (error)\n {\n const duration = Date.now() - startTime;\n\n apiLogger.error('Request failed', error as Error, {\n requestId,\n method,\n path,\n duration,\n });\n\n throw error;\n }\n };\n}"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spfn/core",
3
- "version": "0.2.0-beta.23",
3
+ "version": "0.2.0-beta.24",
4
4
  "description": "SPFN Framework Core - File-based routing, transactions, repository pattern",
5
5
  "type": "module",
6
6
  "exports": {