@spfn/core 0.2.0-beta.52 → 0.2.0-beta.53

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.
@@ -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 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\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}\n"]}
1
+ {"version":3,"sources":["../../src/middleware/error-handler.ts","../../src/middleware/request-logger.ts","../../src/security/proxy-signature.ts","../../src/middleware/proxy-guard.ts"],"names":["statusCode","logger","env"],"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\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}\n","/**\n * Proxy → Backend request signing (HMAC-SHA256) with key rotation\n *\n * Shared between the Next.js RPC proxy (which signs outbound requests) and the\n * backend proxy-guard middleware (which verifies them). Keeping the canonical\n * string and HMAC logic in one pure module guarantees both sides agree.\n *\n * The signature proves a request passed through a trusted proxy that holds the\n * shared secret. It does NOT authenticate the end user — that is the JWT layer's\n * job — and it cannot stop a client from calling the proxy itself. Its role is to\n * stop direct-to-backend calls that bypass the proxy.\n *\n * Keys are identified by a short keyId so secrets can be rotated without downtime:\n * the proxy signs with the active key and stamps its keyId in a header; the\n * backend keeps a set of accepted keys (current + previous) and selects by keyId.\n * Roll a new key in, let both sides pick it up, switch the proxy to it, then drop\n * the old one after a grace period — no request is ever rejected mid-rotation.\n *\n * @module security/proxy-signature\n */\nimport { createHmac, randomBytes, timingSafeEqual, createHash } from 'node:crypto';\n\n// ============================================================================\n// Header names\n// ============================================================================\n\nexport const PROXY_SIGNATURE_HEADER = 'x-spfn-proxy-signature';\nexport const PROXY_TIMESTAMP_HEADER = 'x-spfn-proxy-timestamp';\nexport const PROXY_NONCE_HEADER = 'x-spfn-proxy-nonce';\nexport const PROXY_KEY_ID_HEADER = 'x-spfn-proxy-key-id';\n\n/** keyId used when a raw secret carries no `keyId:` prefix (back-compat). */\nexport const DEFAULT_KEY_ID = 'default';\n\n// ============================================================================\n// Keys\n// ============================================================================\n\n/**\n * A signing key: a short identifier plus its secret. Encoded in env as\n * `<keyId>:<secret>` (e.g. `v2:9f3c...`); a bare secret gets `DEFAULT_KEY_ID`.\n */\nexport interface ProxyKey\n{\n keyId: string;\n secret: string;\n}\n\n/**\n * Parse one `<keyId>:<secret>` (or bare `<secret>`) string into a ProxyKey.\n * keyId is everything before the FIRST colon; the secret keeps any later colons.\n */\nexport function parseProxyKey(raw: string): ProxyKey\n{\n const idx = raw.indexOf(':');\n if (idx === -1)\n {\n return { keyId: DEFAULT_KEY_ID, secret: raw };\n }\n\n // A leading colon (':secret') yields an empty keyId; the backend treats an empty\n // keyId header as \"missing\" and rejects, so normalize it to DEFAULT_KEY_ID.\n return { keyId: raw.slice(0, idx) || DEFAULT_KEY_ID, secret: raw.slice(idx + 1) };\n}\n\n/**\n * Build the backend's accepted key set from raw env values. Each input may be a\n * single key or a comma-separated list (for multiple grace keys). Earlier inputs\n * win on keyId collision, so pass the active secret first.\n */\nexport function parseProxyKeySet(raws: Array<string | undefined | null>): ProxyKey[]\n{\n const keys: ProxyKey[] = [];\n const seen = new Set<string>();\n\n for (const raw of raws)\n {\n if (!raw)\n {\n continue;\n }\n\n for (const part of raw.split(','))\n {\n const trimmed = part.trim();\n if (!trimmed)\n {\n continue;\n }\n\n const key = parseProxyKey(trimmed);\n if (!key.secret || seen.has(key.keyId))\n {\n continue;\n }\n\n seen.add(key.keyId);\n keys.push(key);\n }\n }\n\n return keys;\n}\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * The parts of a request that are bound into the signature.\n */\nexport interface SignatureParts\n{\n /** Resolved backend HTTP method (GET/POST/PUT/...) */\n method: string;\n\n /**\n * Raw (wire) backend path, percent-encoding preserved, leading slash, NO query.\n * Proxy signs its outbound request-target path; the backend reconstructs the\n * SAME bytes from `new URL(c.req.url).pathname` (NOT the decoded `c.req.path`).\n */\n path: string;\n\n /**\n * Raw (wire) query string including leading `?`, or '' when there is none.\n * Both sides use the verbatim search string so query params are authenticated.\n */\n query: string;\n\n /** Unix epoch milliseconds, as a string */\n timestamp: string;\n\n /** Random per-request nonce */\n nonce: string;\n\n /**\n * SHA-256 hex of the raw request body, or empty string when there is no body\n * (GET/HEAD, or multipart uploads which are excluded by design). Bound for ANY\n * non-multipart content-type, not just application/json.\n */\n bodyHash: string;\n}\n\n/**\n * Headers produced by signing — ready to set on the outbound fetch.\n */\nexport interface SignatureHeaders\n{\n [PROXY_SIGNATURE_HEADER]: string;\n [PROXY_TIMESTAMP_HEADER]: string;\n [PROXY_NONCE_HEADER]: string;\n [PROXY_KEY_ID_HEADER]: string;\n}\n\n// ============================================================================\n// Canonical string + HMAC\n// ============================================================================\n\n/**\n * Build the canonical string that gets signed. Field order is fixed and each\n * field is newline-separated, so a captured signature cannot be replayed against\n * a different method, path, or body.\n */\nexport function buildCanonicalString(parts: SignatureParts): string\n{\n return [parts.method.toUpperCase(), parts.path, parts.query, parts.timestamp, parts.nonce, parts.bodyHash].join('\\n');\n}\n\n/**\n * Hash a request body. Accepts a string (proxy side, from JSON.stringify) or a\n * Buffer (backend side, straight from the stream — avoids a bytes→string→bytes\n * round-trip). Returns '' for empty/no body. A utf8 string and its Buffer hash\n * identically, so both sides agree.\n */\nexport function hashBody(body: string | Buffer | undefined | null): string\n{\n if (!body || body.length === 0)\n {\n return '';\n }\n\n return typeof body === 'string'\n ? createHash('sha256').update(body, 'utf8').digest('hex')\n : createHash('sha256').update(body).digest('hex');\n}\n\n/**\n * Compute the HMAC-SHA256 signature (hex) for the given parts.\n */\nexport function computeSignature(secret: string, parts: SignatureParts): string\n{\n return createHmac('sha256', secret).update(buildCanonicalString(parts)).digest('hex');\n}\n\n/**\n * Generate a fresh random nonce (hex).\n */\nexport function generateNonce(): string\n{\n return randomBytes(16).toString('hex');\n}\n\n// ============================================================================\n// Sign (proxy side)\n// ============================================================================\n\nexport interface SignInput\n{\n /** The active signing key. */\n key: ProxyKey;\n method: string;\n /** Raw (wire) path, leading slash, percent-encoding preserved, NO query. */\n path: string;\n /** Raw (wire) query string including leading `?`, or '' / omitted for none. */\n query?: string;\n /** Raw request body string, if any. Omit for GET / multipart uploads. */\n body?: string | null;\n /** Override the timestamp (testing). Defaults to Date.now(). */\n timestamp?: string;\n /** Override the nonce (testing). Defaults to a random nonce. */\n nonce?: string;\n}\n\n/**\n * Sign an outbound proxy request and return the headers to attach (including the\n * keyId so the backend knows which key to verify against).\n */\nexport function signProxyRequest(input: SignInput): SignatureHeaders\n{\n const timestamp = input.timestamp ?? String(Date.now());\n const nonce = input.nonce ?? generateNonce();\n const signature = computeSignature(input.key.secret, {\n method: input.method,\n path: input.path,\n query: input.query ?? '',\n timestamp,\n nonce,\n bodyHash: hashBody(input.body),\n });\n\n return {\n [PROXY_SIGNATURE_HEADER]: signature,\n [PROXY_TIMESTAMP_HEADER]: timestamp,\n [PROXY_NONCE_HEADER]: nonce,\n [PROXY_KEY_ID_HEADER]: input.key.keyId,\n };\n}\n\n// ============================================================================\n// Verify (backend side)\n// ============================================================================\n\nexport type VerifyFailureReason =\n | 'missing-headers'\n | 'stale-timestamp'\n | 'bad-timestamp'\n | 'unknown-key'\n | 'signature-mismatch';\n\n// ('body-read-error' / 'origin-not-allowed' / 'nonce-replay' are guard-level\n// reasons carried separately by the middleware, not produced by verify itself.)\n\nexport interface VerifyResult\n{\n valid: boolean;\n reason?: VerifyFailureReason;\n /** The verified nonce, for optional replay tracking by the caller. */\n nonce?: string;\n /** The keyId the request was verified against (for logging/metrics). */\n keyId?: string;\n}\n\nexport interface VerifyInput\n{\n /** Accepted keys (active + grace). Selected by the request's keyId header. */\n keys: ProxyKey[];\n method: string;\n /** Raw (wire) path from `new URL(c.req.url).pathname` — NOT decoded `c.req.path`. */\n path: string;\n /** Raw (wire) query string from `new URL(c.req.url).search`, or '' for none. */\n query?: string;\n /** Raw request body (string or Buffer) as received, if any. */\n body?: string | Buffer | null;\n signature: string | null | undefined;\n timestamp: string | null | undefined;\n nonce: string | null | undefined;\n keyId: string | null | undefined;\n /** Allowed clock skew / replay window in ms. Default 30s. */\n windowMs?: number;\n /** Current time (testing). Defaults to Date.now(). */\n now?: number;\n}\n\n/**\n * Constant-time hex string comparison. Returns false on any length diff.\n *\n * Buffer.from(_, 'hex') does NOT throw on invalid hex — it silently truncates at\n * the first bad char — so a malformed signature yields a shorter buffer that the\n * length check rejects. No try/catch needed.\n */\nfunction safeEqualHex(a: string, b: string): boolean\n{\n const bufA = Buffer.from(a, 'hex');\n const bufB = Buffer.from(b, 'hex');\n\n if (bufA.length === 0 || bufA.length !== bufB.length)\n {\n return false;\n }\n\n return timingSafeEqual(bufA, bufB);\n}\n\n/**\n * Verify an inbound request's proxy signature against the accepted key set.\n *\n * Checks, in order: headers present, timestamp within window (replay guard),\n * keyId known, and HMAC match. Nonce-level replay rejection is left to the caller\n * (optional, Redis-backed) since not every deployment has a shared store.\n */\nexport function verifyProxyRequest(input: VerifyInput): VerifyResult\n{\n const { signature, timestamp, nonce, keyId } = input;\n\n if (!signature || !timestamp || !nonce || !keyId)\n {\n return { valid: false, reason: 'missing-headers' };\n }\n\n const ts = Number(timestamp);\n if (!Number.isFinite(ts))\n {\n return { valid: false, reason: 'bad-timestamp' };\n }\n\n const now = input.now ?? Date.now();\n const windowMs = input.windowMs ?? 30_000;\n if (Math.abs(now - ts) > windowMs)\n {\n return { valid: false, reason: 'stale-timestamp' };\n }\n\n const key = input.keys.find(k => k.keyId === keyId);\n if (!key)\n {\n return { valid: false, reason: 'unknown-key' };\n }\n\n const expected = computeSignature(key.secret, {\n method: input.method,\n path: input.path,\n query: input.query ?? '',\n timestamp,\n nonce,\n bodyHash: hashBody(input.body),\n });\n\n if (!safeEqualHex(signature, expected))\n {\n return { valid: false, reason: 'signature-mismatch' };\n }\n\n return { valid: true, nonce, keyId };\n}\n","/**\n * Proxy-guard middleware\n *\n * Verifies that an inbound request came through a trusted Next.js RPC proxy\n * (HMAC signature) and/or from an allowed browser origin, then tags the request\n * with a `clientType`. Lets the backend reject direct-to-backend calls that\n * bypass the proxy.\n *\n * This does NOT replace user authentication (JWT) — it answers \"what kind of\n * client is this?\", not \"who is the user?\". It also cannot stop a client from\n * calling the proxy itself; that is the web's inherent limit (see\n * PROXY-BACKEND-AUTH-SPEC.md). Mobile clients call the backend directly and are\n * handled by a future attestation path, OR'd into this same check.\n *\n * @module middleware/proxy-guard\n */\nimport type { Context, MiddlewareHandler, Next } from 'hono';\n\nimport { env } from '@spfn/core/config';\nimport { logger } from '@spfn/core/logger';\n\nimport {\n verifyProxyRequest,\n parseProxyKey,\n parseProxyKeySet,\n PROXY_SIGNATURE_HEADER,\n PROXY_TIMESTAMP_HEADER,\n PROXY_NONCE_HEADER,\n PROXY_KEY_ID_HEADER,\n type VerifyFailureReason,\n} from '../security/proxy-signature';\n\nconst guardLogger = logger.child('@spfn/core:proxy-guard');\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * How the request reached the backend.\n * - `web`: verified via a trusted proxy signature\n * - `untrusted`: could not be verified (only reached handlers in `tag` mode)\n * - future: `ios` | `android` once attestation lands\n */\nexport type ClientType = 'web' | 'untrusted' | (string & {});\n\n/**\n * Enforcement mode.\n * - `off`: middleware is a no-op (default — keeps existing apps working).\n * - `tag`: verify and set `clientType`, but never reject (observe first).\n * - `strict`: reject requests that fail verification.\n */\nexport type ProxyGuardMode = 'off' | 'tag' | 'strict';\n\n/**\n * Optional replay store. Returns `true` when the nonce is fresh (and records it),\n * `false` when it has been seen before. Backed by Redis in multi-instance setups.\n */\nexport interface NonceStore\n{\n checkAndSet(nonce: string, ttlMs: number): Promise<boolean>;\n}\n\nexport interface ProxyGuardConfig\n{\n /**\n * Active shared HMAC secret, as `<keyId>:<secret>` (or a bare secret).\n * Defaults to `env.SPFN_PROXY_SECRET`. Without it (and no previous keys),\n * signatures cannot be verified and every request is tagged `untrusted`.\n */\n secret?: string;\n\n /**\n * Previous (grace) keys still accepted for verification during rotation, as a\n * comma-separated list of `<keyId>:<secret>`. The proxy never signs with\n * these — they exist only so in-flight requests signed with the prior key\n * keep verifying until the rollout settles. Defaults to\n * `env.SPFN_PROXY_SECRET_PREVIOUS`. Backend-only, so it can live in\n * `.env.server` (never exposed to the Next.js process).\n */\n previousSecrets?: string;\n\n /** Enforcement mode. @default 'off' */\n mode?: ProxyGuardMode;\n\n /** Allowed clock skew / replay window in ms. @default 30000 */\n windowMs?: number;\n\n /**\n * Browser origin allowlist. When set, a request carrying an `Origin` header\n * not in this list is rejected (strict) / tagged (tag). Requests without an\n * `Origin` (server-to-server, mobile) skip this check and fall back to the\n * signature.\n */\n allowedOrigins?: string[];\n\n /** Optional Redis-backed nonce store for hard replay rejection (strict mode only). */\n nonceStore?: NonceStore;\n\n /** Paths to skip entirely (e.g. health checks). */\n skipPaths?: string[];\n\n /**\n * Reject (413) once the streamed body exceeds this many bytes. Measured AS IT\n * STREAMS — Content-Length is NOT trusted, so a missing/chunked/under-reported\n * length can't bypass it. Bounds the per-request memory the guard buffers.\n * Undefined = no cap (default). Multipart is exempt (unsigned).\n */\n maxBodyBytes?: number;\n}\n\n// ============================================================================\n// Hono context augmentation\n// ============================================================================\n\ndeclare module 'hono'\n{\n interface ContextVariableMap\n {\n clientType?: ClientType;\n }\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\n/** Sentinel: body exceeded maxBodyBytes while streaming (never fully buffered). */\nconst BODY_OVERSIZE = Symbol('proxy-guard:body-oversize');\n\n/** Sentinel: the body stream errored mid-read (e.g. client abort). */\nconst BODY_READ_ERROR = Symbol('proxy-guard:body-read-error');\n\n/**\n * Read the raw request body without consuming it for downstream handlers.\n * Bound for ANY non-multipart content-type (not just application/json) so the\n * backend hashes exactly what the proxy signed — otherwise the body could be\n * tampered by flipping content-type. Multipart uploads are excluded on both\n * sides (their large bodies are never hashed).\n *\n * When maxBytes is set the body is measured AS IT STREAMS and aborted past the\n * limit — so a missing/chunked/under-reported Content-Length can't bypass the cap\n * (the header is never trusted). Returns BODY_OVERSIZE in that case.\n */\nasync function readRawBody(\n c: Context,\n maxBytes?: number,\n): Promise<Buffer | undefined | typeof BODY_OVERSIZE | typeof BODY_READ_ERROR>\n{\n const method = c.req.method;\n if (method === 'GET' || method === 'HEAD')\n {\n return undefined;\n }\n\n const contentType = c.req.header('content-type') || '';\n if (contentType.includes('multipart/form-data'))\n {\n return undefined;\n }\n\n const stream = c.req.raw.clone().body;\n if (!stream)\n {\n return undefined;\n }\n\n const reader = stream.getReader();\n const chunks: Uint8Array[] = [];\n let total = 0;\n\n try\n {\n for (;;)\n {\n const { done, value } = await reader.read();\n if (done)\n {\n break;\n }\n\n total += value.byteLength;\n if (maxBytes !== undefined && total > maxBytes)\n {\n // Fire-and-forget: awaiting cancel() can hang under some stream\n // implementations, and this is a clone so the original is untouched.\n void reader.cancel().catch(() => undefined);\n\n return BODY_OVERSIZE;\n }\n\n chunks.push(value);\n }\n }\n catch\n {\n // A mid-stream error (client abort, network blip) RETURNS a sentinel — not an\n // empty body (would 403 valid traffic) and not a re-throw (would 500 + page\n // on-call and break tag mode's never-reject contract).\n guardLogger.debug('Request body read failed (client abort?)');\n\n return BODY_READ_ERROR;\n }\n\n return Buffer.concat(chunks);\n}\n\nfunction isOriginAllowed(c: Context, allowedOrigins: string[] | undefined): boolean\n{\n if (!allowedOrigins || allowedOrigins.length === 0)\n {\n return true;\n }\n\n const origin = c.req.header('origin');\n if (!origin)\n {\n // No browser Origin (server-to-server / mobile) — defer to signature.\n return true;\n }\n\n return allowedOrigins.includes(origin);\n}\n\n// ============================================================================\n// Middleware\n// ============================================================================\n\n/**\n * Create the proxy-guard middleware.\n *\n * @example\n * ```typescript\n * export default defineServerConfig()\n * .proxyGuard({ mode: 'strict', allowedOrigins: ['https://app.example.com'] })\n * .routes(appRouter)\n * .build();\n * ```\n */\nexport function createProxyGuard(config: ProxyGuardConfig = {}): MiddlewareHandler\n{\n const mode = config.mode ?? 'off';\n const windowMs = config.windowMs ?? 30_000;\n const allowedOrigins = config.allowedOrigins;\n const nonceStore = config.nonceStore;\n const skipPaths = new Set(config.skipPaths ?? []);\n const maxBodyBytes = config.maxBodyBytes;\n\n // Accepted key set: active secret first (wins on keyId collision), then grace keys.\n const activeRaw = config.secret ?? env.SPFN_PROXY_SECRET;\n const previousRaw = config.previousSecrets ?? env.SPFN_PROXY_SECRET_PREVIOUS;\n const keys = parseProxyKeySet([activeRaw, previousRaw]);\n\n // Fail CLOSED on misconfiguration: strict with no key would otherwise let every\n // request (including direct-to-backend) through. Refuse to start instead.\n if (mode === 'strict' && keys.length === 0)\n {\n throw new Error(\n '[proxy-guard] mode \"strict\" requires a proxy key but none is configured '\n + '(SPFN_PROXY_SECRET is empty/unset). Refusing to start with the guard open — '\n + 'set the secret, or use mode \"tag\" / \"off\".',\n );\n }\n\n // Warn when rotation can't work: bare secrets (no \"keyId:\" prefix) on both active\n // and previous collapse to the same keyId, so the grace key is silently dropped.\n if (activeRaw && previousRaw)\n {\n const activeId = parseProxyKey(activeRaw).keyId;\n const collides = previousRaw.split(',').some(p => p.trim() && parseProxyKey(p.trim()).keyId === activeId);\n if (collides)\n {\n guardLogger.warn(\n 'Previous proxy key shares keyId with the active key (likely bare secrets without a '\n + '\"keyId:\" prefix) — grace key ignored, rotation will drop in-flight requests. '\n + 'Use \"<keyId>:<secret>\" on both keys.',\n { keyId: activeId },\n );\n }\n }\n\n return async (c: Context, next: Next) =>\n {\n if (mode === 'off' || skipPaths.has(c.req.path))\n {\n return next();\n }\n\n // A genuine CORS preflight (OPTIONS + Access-Control-Request-Method) belongs to\n // the CORS layer, not the signature guard — exempt it FIRST so it isn't 403'd\n // here without CORS headers. A NON-preflight OPTIONS (rare OPTIONS-as-API) falls\n // through to normal verification, so exempting preflight is never an unguarded\n // bypass. Tagged 'untrusted' (unsigned) so downstream never sees it unset.\n if (c.req.method === 'OPTIONS' && c.req.header('access-control-request-method'))\n {\n c.set('clientType', 'untrusted');\n\n return next();\n }\n\n // Every gate below is EVALUATED in both modes; only enforcement differs\n // (reject() = 403 in strict, tag clientType='untrusted' + continue in tag).\n // So tag mode observes exactly what strict would reject — no metric skew.\n\n // 1. Origin allowlist (browser cross-origin guard)\n if (!isOriginAllowed(c, allowedOrigins))\n {\n return reject(c, mode, next, 'origin-not-allowed');\n }\n\n // 2. No accepted key — only reachable in tag mode (strict throws at construction).\n if (keys.length === 0)\n {\n c.set('clientType', 'untrusted');\n\n return next();\n }\n\n // 3. Cheap header-presence check BEFORE buffering the body, so unsigned requests\n // (direct-to-backend attacks, tag-mode rollout traffic) reject/tag without\n // paying the body read.\n const signature = c.req.header(PROXY_SIGNATURE_HEADER);\n const timestamp = c.req.header(PROXY_TIMESTAMP_HEADER);\n const nonce = c.req.header(PROXY_NONCE_HEADER);\n const keyId = c.req.header(PROXY_KEY_ID_HEADER);\n if (!signature || !timestamp || !nonce || !keyId)\n {\n return reject(c, mode, next, 'missing-headers');\n }\n\n // 4. Read body with a streaming size cap (Content-Length is never trusted, so a\n // missing/chunked/under-reported length can't bypass the bound).\n const body = await readRawBody(c, maxBodyBytes);\n if (body === BODY_OVERSIZE)\n {\n if (mode === 'strict')\n {\n return c.json({ error: 'Payload Too Large' }, 413);\n }\n\n c.set('clientType', 'untrusted');\n\n return next();\n }\n if (body === BODY_READ_ERROR)\n {\n // Body stream failed (client abort) — can't verify, so don't hash an empty\n // body. Reject in strict / tag untrusted, never a 500.\n return reject(c, mode, next, 'body-read-error');\n }\n\n // 5. HMAC over the wire request-target (raw path + query) and body. Use the raw\n // URL, NOT the decoded c.req.path, so it matches the bytes the proxy signed.\n const url = new URL(c.req.url);\n const result = verifyProxyRequest({\n keys,\n method: c.req.method,\n path: url.pathname,\n query: url.search,\n body,\n signature,\n timestamp,\n nonce,\n keyId,\n windowMs,\n });\n\n if (!result.valid)\n {\n return reject(c, mode, next, result.reason);\n }\n\n // 6. Hard replay rejection via nonce store (both modes — tag observes replays).\n // Degrade to the timestamp window if the store is briefly unavailable rather\n // than 500-ing valid traffic.\n if (nonceStore && result.nonce)\n {\n try\n {\n const fresh = await nonceStore.checkAndSet(result.nonce, windowMs * 2);\n if (!fresh)\n {\n return reject(c, mode, next, 'nonce-replay');\n }\n }\n catch (err)\n {\n guardLogger.warn('Nonce store unavailable — falling back to timestamp window', {\n error: (err as Error).message,\n });\n }\n }\n\n c.set('clientType', 'web');\n\n return next();\n };\n}\n\n// ============================================================================\n// Redis-backed nonce store (optional)\n// ============================================================================\n\n/** Minimal cache client (compatible with ioredis Redis | Cluster). */\ntype CacheClient = {\n set(key: string, value: string, ...args: any[]): Promise<any>;\n};\n\n/**\n * Build a Redis-backed nonce store for hard replay rejection.\n *\n * Uses `SET key 1 PX ttl NX` — an atomic \"set if absent\". A fresh nonce returns\n * 'OK'; a replayed one returns null. TTL keeps the key set bounded (only nonces\n * within the replay window matter).\n */\nexport function createCacheNonceStore(cache: CacheClient, prefix = 'spfn:proxy-nonce:'): NonceStore\n{\n return {\n async checkAndSet(nonce: string, ttlMs: number): Promise<boolean>\n {\n const res = await cache.set(`${prefix}${nonce}`, '1', 'PX', Math.ceil(ttlMs), 'NX');\n\n return res === 'OK';\n },\n };\n}\n\n/**\n * Reject (strict) or tag-and-continue (tag). Keeps the rejection response generic\n * so it leaks no detail about why verification failed.\n */\nfunction reject(\n c: Context,\n mode: ProxyGuardMode,\n next: Next,\n reason: VerifyFailureReason | 'origin-not-allowed' | 'nonce-replay' | 'body-read-error' | undefined,\n)\n{\n if (mode === 'strict')\n {\n guardLogger.warn('Rejected unverified request', { reason, path: c.req.path, method: c.req.method });\n\n return c.json({ error: 'Forbidden', message: 'Request origin could not be verified' }, 403);\n }\n\n // tag mode — observe only, never block\n guardLogger.debug('Unverified request (would reject in strict mode)', { reason, path: c.req.path });\n c.set('clientType', 'untrusted');\n\n return next();\n}\n"]}
@@ -163,6 +163,16 @@ interface RpcProxyConfig extends Omit<TypedProxyConfig, 'onRequest' | 'onRespons
163
163
  * ```
164
164
  */
165
165
  routeMap: RouteMap;
166
+ /**
167
+ * Shared secret for signing proxy→backend requests (HMAC-SHA256).
168
+ *
169
+ * When set, every forwarded request carries an HMAC signature the backend's
170
+ * proxy-guard middleware verifies, letting it reject direct-to-backend calls
171
+ * that bypass this proxy. Set the SAME value here and on the backend.
172
+ *
173
+ * @default process.env.SPFN_PROXY_SECRET (undefined → signing disabled)
174
+ */
175
+ proxySecret?: string;
166
176
  }
167
177
  /**
168
178
  * Create RPC proxy handler for Next.js API Route
@@ -1,8 +1,76 @@
1
1
  import { NextResponse } from 'next/server';
2
2
  import { env } from '@spfn/core/config';
3
3
  import { logger } from '@spfn/core/logger';
4
+ import { randomBytes, createHmac, createHash } from 'crypto';
4
5
 
5
6
  // src/nextjs/proxy/rpc.ts
7
+ var PROXY_SIGNATURE_HEADER = "x-spfn-proxy-signature";
8
+ var PROXY_TIMESTAMP_HEADER = "x-spfn-proxy-timestamp";
9
+ var PROXY_NONCE_HEADER = "x-spfn-proxy-nonce";
10
+ var PROXY_KEY_ID_HEADER = "x-spfn-proxy-key-id";
11
+ var DEFAULT_KEY_ID = "default";
12
+ function parseProxyKey(raw) {
13
+ const idx = raw.indexOf(":");
14
+ if (idx === -1) {
15
+ return { keyId: DEFAULT_KEY_ID, secret: raw };
16
+ }
17
+ return { keyId: raw.slice(0, idx) || DEFAULT_KEY_ID, secret: raw.slice(idx + 1) };
18
+ }
19
+ function parseProxyKeySet(raws) {
20
+ const keys = [];
21
+ const seen = /* @__PURE__ */ new Set();
22
+ for (const raw of raws) {
23
+ if (!raw) {
24
+ continue;
25
+ }
26
+ for (const part of raw.split(",")) {
27
+ const trimmed = part.trim();
28
+ if (!trimmed) {
29
+ continue;
30
+ }
31
+ const key = parseProxyKey(trimmed);
32
+ if (!key.secret || seen.has(key.keyId)) {
33
+ continue;
34
+ }
35
+ seen.add(key.keyId);
36
+ keys.push(key);
37
+ }
38
+ }
39
+ return keys;
40
+ }
41
+ function buildCanonicalString(parts) {
42
+ return [parts.method.toUpperCase(), parts.path, parts.query, parts.timestamp, parts.nonce, parts.bodyHash].join("\n");
43
+ }
44
+ function hashBody(body) {
45
+ if (!body || body.length === 0) {
46
+ return "";
47
+ }
48
+ return typeof body === "string" ? createHash("sha256").update(body, "utf8").digest("hex") : createHash("sha256").update(body).digest("hex");
49
+ }
50
+ function computeSignature(secret, parts) {
51
+ return createHmac("sha256", secret).update(buildCanonicalString(parts)).digest("hex");
52
+ }
53
+ function generateNonce() {
54
+ return randomBytes(16).toString("hex");
55
+ }
56
+ function signProxyRequest(input) {
57
+ const timestamp = input.timestamp ?? String(Date.now());
58
+ const nonce = input.nonce ?? generateNonce();
59
+ const signature = computeSignature(input.key.secret, {
60
+ method: input.method,
61
+ path: input.path,
62
+ query: input.query ?? "",
63
+ timestamp,
64
+ nonce,
65
+ bodyHash: hashBody(input.body)
66
+ });
67
+ return {
68
+ [PROXY_SIGNATURE_HEADER]: signature,
69
+ [PROXY_TIMESTAMP_HEADER]: timestamp,
70
+ [PROXY_NONCE_HEADER]: nonce,
71
+ [PROXY_KEY_ID_HEADER]: input.key.keyId
72
+ };
73
+ }
6
74
 
7
75
  // src/nextjs/shared.ts
8
76
  function buildUrlWithParams(path, params) {
@@ -196,7 +264,10 @@ function buildProxyHeaders(sourceHeaders, defaultHeaders) {
196
264
  "cookie",
197
265
  "user-agent",
198
266
  "accept",
199
- "accept-language"
267
+ "accept-language",
268
+ // Forwarded so the backend proxy-guard can enforce its origin allowlist on
269
+ // the real browser Origin; without this allowedOrigins would be a no-op.
270
+ "origin"
200
271
  ];
201
272
  for (const header of headersToForward2) {
202
273
  const value = sourceHeaders instanceof Headers ? sourceHeaders.get(header) : sourceHeaders[header];
@@ -337,8 +408,21 @@ function createRpcProxy(config) {
337
408
  interceptors,
338
409
  autoDiscoverInterceptors = true,
339
410
  disableAutoInterceptors,
411
+ proxySecret = env.SPFN_PROXY_SECRET,
340
412
  routeMap
341
413
  } = config;
414
+ const proxyKey = proxySecret ? parseProxyKeySet([proxySecret])[0] ?? null : null;
415
+ if (proxyKey) {
416
+ try {
417
+ const basePath = new URL(apiUrl).pathname;
418
+ if (basePath !== "/" && basePath !== "") {
419
+ rpcLogger.warn(
420
+ `SPFN_API_URL has a base path ("${basePath}"). proxy-guard signs route-relative paths, so the backend must receive paths WITHOUT this prefix (e.g. ingress strip) or strict mode will reject all signed requests with 403.`
421
+ );
422
+ }
423
+ } catch {
424
+ }
425
+ }
342
426
  function resolveRoute(routeName) {
343
427
  const entry = routeMap[routeName];
344
428
  if (entry) {
@@ -490,6 +574,19 @@ function createRpcProxy(config) {
490
574
  fetchOptions.body = JSON.stringify(requestCtx.body);
491
575
  }
492
576
  }
577
+ if (proxyKey) {
578
+ const signedBody = typeof fetchOptions.body === "string" ? fetchOptions.body : void 0;
579
+ const signatureHeaders = signProxyRequest({
580
+ key: proxyKey,
581
+ method: targetMethod,
582
+ path: resolvedPath,
583
+ query: queryString,
584
+ body: signedBody
585
+ });
586
+ for (const [headerName, value] of Object.entries(signatureHeaders)) {
587
+ headers.set(headerName, value);
588
+ }
589
+ }
493
590
  const controller = new AbortController();
494
591
  const timeoutId = setTimeout(() => controller.abort(), timeout);
495
592
  try {