@spfn/core 0.2.0-beta.15 → 0.2.0-beta.17

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.
@@ -237,9 +237,9 @@ var coreEnvSchema = defineEnvSchema({
237
237
  examples: [3e4, 6e4, 12e4]
238
238
  }),
239
239
  SHUTDOWN_TIMEOUT: envNumber({
240
- description: "Graceful shutdown timeout in milliseconds",
241
- default: 3e4,
242
- examples: [1e4, 3e4, 6e4]
240
+ description: "Graceful shutdown timeout in milliseconds (must be less than k8s terminationGracePeriodSeconds minus preStop sleep, with safety margin)",
241
+ default: 28e4,
242
+ examples: [3e4, 12e4, 28e4]
243
243
  }),
244
244
  // ========================================================================
245
245
  // Next.js Integration
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/config/schema.ts","../../src/config/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Core Package Environment Variable Schema\n *\n * Centralized schema definition for all environment variables used in @spfn/core.\n * This provides type safety, validation, and documentation for environment configuration.\n *\n * @module config/schema\n */\n\nimport {\n defineEnvSchema,\n envEnum,\n envNumber,\n envBoolean,\n envUrl,\n envString,\n parsePostgresUrl,\n parseRedisUrl,\n} from '@spfn/core/env';\n\n/**\n * Core package environment variable schema\n *\n * Defines all environment variables with:\n * - Type information\n * - Default values\n * - Validation rules\n * - Documentation\n *\n * @example\n * ```typescript\n * import { coreEnvSchema } from '@spfn/core/config';\n *\n * // Access schema information\n * console.log(coreEnvSchema.DB_POOL_MAX.description);\n * console.log(coreEnvSchema.DB_POOL_MAX.default);\n * ```\n */\nexport const coreEnvSchema = defineEnvSchema({\n // ========================================================================\n // Core Environment\n // ========================================================================\n\n NODE_ENV: envEnum(['local', 'development', 'staging', 'production', 'test'] as const, {\n description: 'Node.js runtime environment',\n default: 'local',\n nextjs: true,\n }),\n\n // ========================================================================\n // Database - Connection\n // ========================================================================\n\n DATABASE_URL: envString({\n description: 'Primary database connection URL',\n required: false,\n sensitive: true,\n validator: parsePostgresUrl,\n examples: ['postgresql://user:password@localhost:5432/dbname'],\n }),\n\n DATABASE_WRITE_URL: envString({\n description: 'Write database URL (master-replica pattern)',\n required: false,\n sensitive: true,\n validator: parsePostgresUrl,\n examples: ['postgresql://user:password@master:5432/dbname'],\n }),\n\n DATABASE_READ_URL: envString({\n description: 'Read database URL (master-replica pattern)',\n required: false,\n sensitive: true,\n validator: parsePostgresUrl,\n examples: ['postgresql://user:password@replica:5432/dbname'],\n }),\n\n\n // ========================================================================\n // Database - Connection Pool\n // ========================================================================\n\n DB_POOL_MAX: envNumber({\n description: 'Maximum number of database connections in pool',\n default: 10,\n examples: [10, 20, 50],\n }),\n\n DB_POOL_IDLE_TIMEOUT: envNumber({\n description: 'Database connection idle timeout in seconds',\n default: 30,\n examples: [20, 30, 60],\n }),\n\n // ========================================================================\n // Database - Retry Configuration\n // ========================================================================\n\n DB_RETRY_MAX: envNumber({\n description: 'Maximum number of database connection retry attempts',\n default: 3,\n examples: [3, 5, 10],\n }),\n\n DB_RETRY_INITIAL_DELAY: envNumber({\n description: 'Initial delay between database retry attempts (milliseconds)',\n default: 100,\n examples: [50, 100, 200],\n }),\n\n DB_RETRY_MAX_DELAY: envNumber({\n description: 'Maximum delay cap for database retry attempts (milliseconds)',\n default: 10000,\n examples: [5000, 10000, 30000],\n }),\n\n DB_RETRY_FACTOR: envNumber({\n description: 'Exponential backoff factor for database retry delays',\n default: 2,\n examples: [2, 1.5, 3],\n }),\n\n // ========================================================================\n // Database - Health Check\n // ========================================================================\n\n DB_HEALTH_CHECK_ENABLED: envBoolean({\n description: 'Enable periodic database health checks',\n default: true,\n examples: [true, false],\n }),\n\n DB_HEALTH_CHECK_INTERVAL: envNumber({\n description: 'Database health check interval (milliseconds)',\n default: 60000,\n examples: [30000, 60000, 120000],\n }),\n\n DB_HEALTH_CHECK_RECONNECT: envBoolean({\n description: 'Reconnect to database on health check failure',\n default: true,\n examples: [true, false],\n }),\n\n DB_HEALTH_CHECK_MAX_RETRIES: envNumber({\n description: 'Maximum health check retry attempts before marking as failed',\n default: 3,\n examples: [3, 5, 10],\n }),\n\n DB_HEALTH_CHECK_RETRY_INTERVAL: envNumber({\n description: 'Interval between health check retry attempts (milliseconds)',\n default: 5000,\n examples: [5000, 10000, 15000],\n }),\n\n // ========================================================================\n // Database - Monitoring\n // ========================================================================\n\n DB_MONITORING_ENABLED: envBoolean({\n description: 'Enable database query performance monitoring',\n default: false,\n examples: [true, false],\n }),\n\n DB_MONITORING_SLOW_THRESHOLD: envNumber({\n description: 'Slow query threshold for monitoring (milliseconds)',\n default: 1000,\n examples: [500, 1000, 2000],\n }),\n\n DB_MONITORING_LOG_QUERIES: envBoolean({\n description: 'Log all database queries (not just slow queries)',\n default: false,\n examples: [true, false],\n }),\n\n // ========================================================================\n // Database - Transaction\n // ========================================================================\n\n TRANSACTION_TIMEOUT: envNumber({\n description: 'Transaction timeout in milliseconds',\n default: 30000,\n examples: [10000, 30000, 60000],\n }),\n\n // ========================================================================\n // Database - Development\n // ========================================================================\n\n DB_DEBUG_TRACE: envBoolean({\n description: 'Enable detailed debug tracing for database operations',\n default: false,\n examples: [true, false],\n }),\n\n // ========================================================================\n // Drizzle ORM\n // ========================================================================\n\n DRIZZLE_SCHEMA_PATH: envString({\n description: 'Path to Drizzle schema configuration',\n required: false,\n default: './src/server/entities/config.ts',\n examples: ['./src/db/schema.ts', './src/server/entities/config.ts'],\n }),\n\n DRIZZLE_OUT_DIR: envString({\n description: 'Output directory for Drizzle migrations',\n required: false,\n default: './drizzle',\n examples: ['./drizzle', './migrations'],\n }),\n\n // ========================================================================\n // Logger - Core\n // ========================================================================\n\n SPFN_LOG_LEVEL: envEnum(['debug', 'info', 'warn', 'error', 'fatal'] as const, {\n description: 'Minimum log level to output',\n default: 'info'\n }),\n\n // ========================================================================\n // Cache (Redis/Valkey)\n // ========================================================================\n\n CACHE_URL: envString({\n description: 'Single Redis/Valkey instance URL',\n required: false,\n sensitive: true,\n validator: parseRedisUrl,\n examples: ['redis://localhost:6379', 'rediss://secure.cache.com:6380'],\n }),\n\n CACHE_WRITE_URL: envString({\n description: 'Master Redis/Valkey URL for writes (master-replica pattern)',\n required: false,\n sensitive: true,\n validator: parseRedisUrl,\n examples: ['redis://master:6379'],\n }),\n\n CACHE_READ_URL: envString({\n description: 'Replica Redis/Valkey URL for reads (master-replica pattern)',\n required: false,\n sensitive: true,\n validator: parseRedisUrl,\n examples: ['redis://replica:6379'],\n }),\n\n CACHE_SENTINEL_HOSTS: envString({\n description: 'Comma-separated Redis Sentinel hosts',\n required: false,\n examples: ['sentinel1:26379,sentinel2:26379'],\n }),\n\n CACHE_CLUSTER_NODES: envString({\n description: 'Comma-separated Redis Cluster nodes',\n required: false,\n examples: ['node1:6379,node2:6379,node3:6379'],\n }),\n\n CACHE_MASTER_NAME: envString({\n description: 'Redis Sentinel master name',\n required: false,\n examples: ['mymaster'],\n }),\n\n CACHE_PASSWORD: envString({\n description: 'Redis/Valkey authentication password',\n required: false,\n sensitive: true,\n examples: ['your-redis-password'],\n }),\n\n CACHE_TLS_REJECT_UNAUTHORIZED: envBoolean({\n description: 'Verify TLS certificates for secure Redis connections',\n default: true,\n examples: [true, false],\n }),\n\n // ========================================================================\n // Server - Core\n // ========================================================================\n\n PORT: envNumber({\n description: 'Server port number',\n default: 4000,\n examples: [3000, 4000, 8080],\n }),\n\n HOST: envString({\n description: 'Server hostname',\n default: 'localhost',\n required: false,\n examples: ['localhost', '0.0.0.0', '127.0.0.1'],\n }),\n\n // ========================================================================\n // Server - Timeout\n // ========================================================================\n\n SERVER_TIMEOUT: envNumber({\n description: 'Request timeout in milliseconds',\n default: 120000,\n examples: [60000, 120000, 300000],\n }),\n\n SERVER_KEEPALIVE_TIMEOUT: envNumber({\n description: 'Keep-alive timeout in milliseconds',\n default: 65000,\n examples: [30000, 65000, 120000],\n }),\n\n SERVER_HEADERS_TIMEOUT: envNumber({\n description: 'Headers timeout in milliseconds',\n default: 60000,\n examples: [30000, 60000, 120000],\n }),\n\n SHUTDOWN_TIMEOUT: envNumber({\n description: 'Graceful shutdown timeout in milliseconds',\n default: 30000,\n examples: [10000, 30000, 60000],\n }),\n\n // ========================================================================\n // Next.js Integration\n // ========================================================================\n\n SPFN_API_URL: envUrl({\n description: 'SPFN API URL (used by Next.js to call backend)',\n required: true,\n nextjs: true,\n examples: ['http://localhost:8790', 'https://api.your-app.com'],\n }),\n\n NEXT_PUBLIC_SPFN_API_URL: envUrl({\n description: 'SPFN API URL (used by Next.js to call backend)',\n required: true,\n nextjs: true,\n examples: ['http://localhost:8790', 'https://api.your-app.com'],\n }),\n\n SPFN_APP_URL: envUrl({\n description: 'Next.js application URL (used by SPFN server)',\n required: false,\n nextjs: true,\n examples: ['http://localhost:3790', 'https://your-app.com'],\n }),\n});","/**\n * Core Package Configuration\n *\n * @example\n * ```typescript\n * import { registry } from '@spfn/core/config';\n *\n * const env = registry.validate();\n * console.log(env.DB_POOL_MAX);\n * ```\n *\n * @module config\n */\n\nimport { createEnvRegistry } from '@spfn/core/env';\nimport { coreEnvSchema } from './schema';\n\n/**\n * Core environment schema\n */\nexport { coreEnvSchema as envSchema } from './schema';\n\n/**\n * Environment registry\n *\n * @example\n * ```typescript\n * // Reset for testing\n * registry.reset();\n * ```\n */\nexport const registry = createEnvRegistry(coreEnvSchema);\n\n/**\n * Validated environment configuration\n */\nexport const env = registry.validate();"]}
1
+ {"version":3,"sources":["../../src/config/schema.ts","../../src/config/index.ts"],"names":[],"mappings":";;;AAsCO,IAAM,gBAAgB,eAAA,CAAgB;AAAA;AAAA;AAAA;AAAA,EAKzC,QAAA,EAAU,QAAQ,CAAC,OAAA,EAAS,eAAe,SAAA,EAAW,YAAA,EAAc,MAAM,CAAA,EAAY;AAAA,IAClF,WAAA,EAAa,6BAAA;AAAA,IACb,OAAA,EAAS,OAAA;AAAA,IACT,MAAA,EAAQ;AAAA,GACX,CAAA;AAAA;AAAA;AAAA;AAAA,EAMD,cAAc,SAAA,CAAU;AAAA,IACpB,WAAA,EAAa,iCAAA;AAAA,IACb,QAAA,EAAU,KAAA;AAAA,IACV,SAAA,EAAW,IAAA;AAAA,IACX,SAAA,EAAW,gBAAA;AAAA,IACX,QAAA,EAAU,CAAC,kDAAkD;AAAA,GAChE,CAAA;AAAA,EAED,oBAAoB,SAAA,CAAU;AAAA,IAC1B,WAAA,EAAa,6CAAA;AAAA,IACb,QAAA,EAAU,KAAA;AAAA,IACV,SAAA,EAAW,IAAA;AAAA,IACX,SAAA,EAAW,gBAAA;AAAA,IACX,QAAA,EAAU,CAAC,+CAA+C;AAAA,GAC7D,CAAA;AAAA,EAED,mBAAmB,SAAA,CAAU;AAAA,IACzB,WAAA,EAAa,4CAAA;AAAA,IACb,QAAA,EAAU,KAAA;AAAA,IACV,SAAA,EAAW,IAAA;AAAA,IACX,SAAA,EAAW,gBAAA;AAAA,IACX,QAAA,EAAU,CAAC,gDAAgD;AAAA,GAC9D,CAAA;AAAA;AAAA;AAAA;AAAA,EAOD,aAAa,SAAA,CAAU;AAAA,IACnB,WAAA,EAAa,gDAAA;AAAA,IACb,OAAA,EAAS,EAAA;AAAA,IACT,QAAA,EAAU,CAAC,EAAA,EAAI,EAAA,EAAI,EAAE;AAAA,GACxB,CAAA;AAAA,EAED,sBAAsB,SAAA,CAAU;AAAA,IAC5B,WAAA,EAAa,6CAAA;AAAA,IACb,OAAA,EAAS,EAAA;AAAA,IACT,QAAA,EAAU,CAAC,EAAA,EAAI,EAAA,EAAI,EAAE;AAAA,GACxB,CAAA;AAAA;AAAA;AAAA;AAAA,EAMD,cAAc,SAAA,CAAU;AAAA,IACpB,WAAA,EAAa,sDAAA;AAAA,IACb,OAAA,EAAS,CAAA;AAAA,IACT,QAAA,EAAU,CAAC,CAAA,EAAG,CAAA,EAAG,EAAE;AAAA,GACtB,CAAA;AAAA,EAED,wBAAwB,SAAA,CAAU;AAAA,IAC9B,WAAA,EAAa,8DAAA;AAAA,IACb,OAAA,EAAS,GAAA;AAAA,IACT,QAAA,EAAU,CAAC,EAAA,EAAI,GAAA,EAAK,GAAG;AAAA,GAC1B,CAAA;AAAA,EAED,oBAAoB,SAAA,CAAU;AAAA,IAC1B,WAAA,EAAa,8DAAA;AAAA,IACb,OAAA,EAAS,GAAA;AAAA,IACT,QAAA,EAAU,CAAC,GAAA,EAAM,GAAA,EAAO,GAAK;AAAA,GAChC,CAAA;AAAA,EAED,iBAAiB,SAAA,CAAU;AAAA,IACvB,WAAA,EAAa,sDAAA;AAAA,IACb,OAAA,EAAS,CAAA;AAAA,IACT,QAAA,EAAU,CAAC,CAAA,EAAG,GAAA,EAAK,CAAC;AAAA,GACvB,CAAA;AAAA;AAAA;AAAA;AAAA,EAMD,yBAAyB,UAAA,CAAW;AAAA,IAChC,WAAA,EAAa,wCAAA;AAAA,IACb,OAAA,EAAS,IAAA;AAAA,IACT,QAAA,EAAU,CAAC,IAAA,EAAM,KAAK;AAAA,GACzB,CAAA;AAAA,EAED,0BAA0B,SAAA,CAAU;AAAA,IAChC,WAAA,EAAa,+CAAA;AAAA,IACb,OAAA,EAAS,GAAA;AAAA,IACT,QAAA,EAAU,CAAC,GAAA,EAAO,GAAA,EAAO,IAAM;AAAA,GAClC,CAAA;AAAA,EAED,2BAA2B,UAAA,CAAW;AAAA,IAClC,WAAA,EAAa,+CAAA;AAAA,IACb,OAAA,EAAS,IAAA;AAAA,IACT,QAAA,EAAU,CAAC,IAAA,EAAM,KAAK;AAAA,GACzB,CAAA;AAAA,EAED,6BAA6B,SAAA,CAAU;AAAA,IACnC,WAAA,EAAa,8DAAA;AAAA,IACb,OAAA,EAAS,CAAA;AAAA,IACT,QAAA,EAAU,CAAC,CAAA,EAAG,CAAA,EAAG,EAAE;AAAA,GACtB,CAAA;AAAA,EAED,gCAAgC,SAAA,CAAU;AAAA,IACtC,WAAA,EAAa,6DAAA;AAAA,IACb,OAAA,EAAS,GAAA;AAAA,IACT,QAAA,EAAU,CAAC,GAAA,EAAM,GAAA,EAAO,IAAK;AAAA,GAChC,CAAA;AAAA;AAAA;AAAA;AAAA,EAMD,uBAAuB,UAAA,CAAW;AAAA,IAC9B,WAAA,EAAa,8CAAA;AAAA,IACb,OAAA,EAAS,KAAA;AAAA,IACT,QAAA,EAAU,CAAC,IAAA,EAAM,KAAK;AAAA,GACzB,CAAA;AAAA,EAED,8BAA8B,SAAA,CAAU;AAAA,IACpC,WAAA,EAAa,oDAAA;AAAA,IACb,OAAA,EAAS,GAAA;AAAA,IACT,QAAA,EAAU,CAAC,GAAA,EAAK,GAAA,EAAM,GAAI;AAAA,GAC7B,CAAA;AAAA,EAED,2BAA2B,UAAA,CAAW;AAAA,IAClC,WAAA,EAAa,kDAAA;AAAA,IACb,OAAA,EAAS,KAAA;AAAA,IACT,QAAA,EAAU,CAAC,IAAA,EAAM,KAAK;AAAA,GACzB,CAAA;AAAA;AAAA;AAAA;AAAA,EAMD,qBAAqB,SAAA,CAAU;AAAA,IAC3B,WAAA,EAAa,qCAAA;AAAA,IACb,OAAA,EAAS,GAAA;AAAA,IACT,QAAA,EAAU,CAAC,GAAA,EAAO,GAAA,EAAO,GAAK;AAAA,GACjC,CAAA;AAAA;AAAA;AAAA;AAAA,EAMD,gBAAgB,UAAA,CAAW;AAAA,IACvB,WAAA,EAAa,uDAAA;AAAA,IACb,OAAA,EAAS,KAAA;AAAA,IACT,QAAA,EAAU,CAAC,IAAA,EAAM,KAAK;AAAA,GACzB,CAAA;AAAA;AAAA;AAAA;AAAA,EAMD,qBAAqB,SAAA,CAAU;AAAA,IAC3B,WAAA,EAAa,sCAAA;AAAA,IACb,QAAA,EAAU,KAAA;AAAA,IACV,OAAA,EAAS,iCAAA;AAAA,IACT,QAAA,EAAU,CAAC,oBAAA,EAAsB,iCAAiC;AAAA,GACrE,CAAA;AAAA,EAED,iBAAiB,SAAA,CAAU;AAAA,IACvB,WAAA,EAAa,yCAAA;AAAA,IACb,QAAA,EAAU,KAAA;AAAA,IACV,OAAA,EAAS,WAAA;AAAA,IACT,QAAA,EAAU,CAAC,WAAA,EAAa,cAAc;AAAA,GACzC,CAAA;AAAA;AAAA;AAAA;AAAA,EAMD,cAAA,EAAgB,QAAQ,CAAC,OAAA,EAAS,QAAQ,MAAA,EAAQ,OAAA,EAAS,OAAO,CAAA,EAAY;AAAA,IAC1E,WAAA,EAAa,6BAAA;AAAA,IACb,OAAA,EAAS;AAAA,GACZ,CAAA;AAAA;AAAA;AAAA;AAAA,EAMD,WAAW,SAAA,CAAU;AAAA,IACjB,WAAA,EAAa,kCAAA;AAAA,IACb,QAAA,EAAU,KAAA;AAAA,IACV,SAAA,EAAW,IAAA;AAAA,IACX,SAAA,EAAW,aAAA;AAAA,IACX,QAAA,EAAU,CAAC,wBAAA,EAA0B,gCAAgC;AAAA,GACxE,CAAA;AAAA,EAED,iBAAiB,SAAA,CAAU;AAAA,IACvB,WAAA,EAAa,6DAAA;AAAA,IACb,QAAA,EAAU,KAAA;AAAA,IACV,SAAA,EAAW,IAAA;AAAA,IACX,SAAA,EAAW,aAAA;AAAA,IACX,QAAA,EAAU,CAAC,qBAAqB;AAAA,GACnC,CAAA;AAAA,EAED,gBAAgB,SAAA,CAAU;AAAA,IACtB,WAAA,EAAa,6DAAA;AAAA,IACb,QAAA,EAAU,KAAA;AAAA,IACV,SAAA,EAAW,IAAA;AAAA,IACX,SAAA,EAAW,aAAA;AAAA,IACX,QAAA,EAAU,CAAC,sBAAsB;AAAA,GACpC,CAAA;AAAA,EAED,sBAAsB,SAAA,CAAU;AAAA,IAC5B,WAAA,EAAa,sCAAA;AAAA,IACb,QAAA,EAAU,KAAA;AAAA,IACV,QAAA,EAAU,CAAC,iCAAiC;AAAA,GAC/C,CAAA;AAAA,EAED,qBAAqB,SAAA,CAAU;AAAA,IAC3B,WAAA,EAAa,qCAAA;AAAA,IACb,QAAA,EAAU,KAAA;AAAA,IACV,QAAA,EAAU,CAAC,kCAAkC;AAAA,GAChD,CAAA;AAAA,EAED,mBAAmB,SAAA,CAAU;AAAA,IACzB,WAAA,EAAa,4BAAA;AAAA,IACb,QAAA,EAAU,KAAA;AAAA,IACV,QAAA,EAAU,CAAC,UAAU;AAAA,GACxB,CAAA;AAAA,EAED,gBAAgB,SAAA,CAAU;AAAA,IACtB,WAAA,EAAa,sCAAA;AAAA,IACb,QAAA,EAAU,KAAA;AAAA,IACV,SAAA,EAAW,IAAA;AAAA,IACX,QAAA,EAAU,CAAC,qBAAqB;AAAA,GACnC,CAAA;AAAA,EAED,+BAA+B,UAAA,CAAW;AAAA,IACtC,WAAA,EAAa,sDAAA;AAAA,IACb,OAAA,EAAS,IAAA;AAAA,IACT,QAAA,EAAU,CAAC,IAAA,EAAM,KAAK;AAAA,GACzB,CAAA;AAAA;AAAA;AAAA;AAAA,EAMD,MAAM,SAAA,CAAU;AAAA,IACZ,WAAA,EAAa,oBAAA;AAAA,IACb,OAAA,EAAS,GAAA;AAAA,IACT,QAAA,EAAU,CAAC,GAAA,EAAM,GAAA,EAAM,IAAI;AAAA,GAC9B,CAAA;AAAA,EAED,MAAM,SAAA,CAAU;AAAA,IACZ,WAAA,EAAa,iBAAA;AAAA,IACb,OAAA,EAAS,WAAA;AAAA,IACT,QAAA,EAAU,KAAA;AAAA,IACV,QAAA,EAAU,CAAC,WAAA,EAAa,SAAA,EAAW,WAAW;AAAA,GACjD,CAAA;AAAA;AAAA;AAAA;AAAA,EAMD,gBAAgB,SAAA,CAAU;AAAA,IACtB,WAAA,EAAa,iCAAA;AAAA,IACb,OAAA,EAAS,IAAA;AAAA,IACT,QAAA,EAAU,CAAC,GAAA,EAAO,IAAA,EAAQ,GAAM;AAAA,GACnC,CAAA;AAAA,EAED,0BAA0B,SAAA,CAAU;AAAA,IAChC,WAAA,EAAa,oCAAA;AAAA,IACb,OAAA,EAAS,IAAA;AAAA,IACT,QAAA,EAAU,CAAC,GAAA,EAAO,IAAA,EAAO,IAAM;AAAA,GAClC,CAAA;AAAA,EAED,wBAAwB,SAAA,CAAU;AAAA,IAC9B,WAAA,EAAa,iCAAA;AAAA,IACb,OAAA,EAAS,GAAA;AAAA,IACT,QAAA,EAAU,CAAC,GAAA,EAAO,GAAA,EAAO,IAAM;AAAA,GAClC,CAAA;AAAA,EAED,kBAAkB,SAAA,CAAU;AAAA,IACxB,WAAA,EAAa,yIAAA;AAAA,IACb,OAAA,EAAS,IAAA;AAAA,IACT,QAAA,EAAU,CAAC,GAAA,EAAO,IAAA,EAAQ,IAAM;AAAA,GACnC,CAAA;AAAA;AAAA;AAAA;AAAA,EAMD,cAAc,MAAA,CAAO;AAAA,IACjB,WAAA,EAAa,gDAAA;AAAA,IACb,QAAA,EAAU,IAAA;AAAA,IACV,MAAA,EAAQ,IAAA;AAAA,IACR,QAAA,EAAU,CAAC,uBAAA,EAAyB,0BAA0B;AAAA,GACjE,CAAA;AAAA,EAED,0BAA0B,MAAA,CAAO;AAAA,IAC7B,WAAA,EAAa,gDAAA;AAAA,IACb,QAAA,EAAU,IAAA;AAAA,IACV,MAAA,EAAQ,IAAA;AAAA,IACR,QAAA,EAAU,CAAC,uBAAA,EAAyB,0BAA0B;AAAA,GACjE,CAAA;AAAA,EAED,cAAc,MAAA,CAAO;AAAA,IACjB,WAAA,EAAa,+CAAA;AAAA,IACb,QAAA,EAAU,KAAA;AAAA,IACV,MAAA,EAAQ,IAAA;AAAA,IACR,QAAA,EAAU,CAAC,uBAAA,EAAyB,sBAAsB;AAAA,GAC7D;AACL,CAAC;;;AClUM,IAAM,QAAA,GAAW,kBAAkB,aAAa;AAKhD,IAAM,GAAA,GAAM,SAAS,QAAA","file":"index.js","sourcesContent":["/**\n * Core Package Environment Variable Schema\n *\n * Centralized schema definition for all environment variables used in @spfn/core.\n * This provides type safety, validation, and documentation for environment configuration.\n *\n * @module config/schema\n */\n\nimport {\n defineEnvSchema,\n envEnum,\n envNumber,\n envBoolean,\n envUrl,\n envString,\n parsePostgresUrl,\n parseRedisUrl,\n} from '@spfn/core/env';\n\n/**\n * Core package environment variable schema\n *\n * Defines all environment variables with:\n * - Type information\n * - Default values\n * - Validation rules\n * - Documentation\n *\n * @example\n * ```typescript\n * import { coreEnvSchema } from '@spfn/core/config';\n *\n * // Access schema information\n * console.log(coreEnvSchema.DB_POOL_MAX.description);\n * console.log(coreEnvSchema.DB_POOL_MAX.default);\n * ```\n */\nexport const coreEnvSchema = defineEnvSchema({\n // ========================================================================\n // Core Environment\n // ========================================================================\n\n NODE_ENV: envEnum(['local', 'development', 'staging', 'production', 'test'] as const, {\n description: 'Node.js runtime environment',\n default: 'local',\n nextjs: true,\n }),\n\n // ========================================================================\n // Database - Connection\n // ========================================================================\n\n DATABASE_URL: envString({\n description: 'Primary database connection URL',\n required: false,\n sensitive: true,\n validator: parsePostgresUrl,\n examples: ['postgresql://user:password@localhost:5432/dbname'],\n }),\n\n DATABASE_WRITE_URL: envString({\n description: 'Write database URL (master-replica pattern)',\n required: false,\n sensitive: true,\n validator: parsePostgresUrl,\n examples: ['postgresql://user:password@master:5432/dbname'],\n }),\n\n DATABASE_READ_URL: envString({\n description: 'Read database URL (master-replica pattern)',\n required: false,\n sensitive: true,\n validator: parsePostgresUrl,\n examples: ['postgresql://user:password@replica:5432/dbname'],\n }),\n\n\n // ========================================================================\n // Database - Connection Pool\n // ========================================================================\n\n DB_POOL_MAX: envNumber({\n description: 'Maximum number of database connections in pool',\n default: 10,\n examples: [10, 20, 50],\n }),\n\n DB_POOL_IDLE_TIMEOUT: envNumber({\n description: 'Database connection idle timeout in seconds',\n default: 30,\n examples: [20, 30, 60],\n }),\n\n // ========================================================================\n // Database - Retry Configuration\n // ========================================================================\n\n DB_RETRY_MAX: envNumber({\n description: 'Maximum number of database connection retry attempts',\n default: 3,\n examples: [3, 5, 10],\n }),\n\n DB_RETRY_INITIAL_DELAY: envNumber({\n description: 'Initial delay between database retry attempts (milliseconds)',\n default: 100,\n examples: [50, 100, 200],\n }),\n\n DB_RETRY_MAX_DELAY: envNumber({\n description: 'Maximum delay cap for database retry attempts (milliseconds)',\n default: 10000,\n examples: [5000, 10000, 30000],\n }),\n\n DB_RETRY_FACTOR: envNumber({\n description: 'Exponential backoff factor for database retry delays',\n default: 2,\n examples: [2, 1.5, 3],\n }),\n\n // ========================================================================\n // Database - Health Check\n // ========================================================================\n\n DB_HEALTH_CHECK_ENABLED: envBoolean({\n description: 'Enable periodic database health checks',\n default: true,\n examples: [true, false],\n }),\n\n DB_HEALTH_CHECK_INTERVAL: envNumber({\n description: 'Database health check interval (milliseconds)',\n default: 60000,\n examples: [30000, 60000, 120000],\n }),\n\n DB_HEALTH_CHECK_RECONNECT: envBoolean({\n description: 'Reconnect to database on health check failure',\n default: true,\n examples: [true, false],\n }),\n\n DB_HEALTH_CHECK_MAX_RETRIES: envNumber({\n description: 'Maximum health check retry attempts before marking as failed',\n default: 3,\n examples: [3, 5, 10],\n }),\n\n DB_HEALTH_CHECK_RETRY_INTERVAL: envNumber({\n description: 'Interval between health check retry attempts (milliseconds)',\n default: 5000,\n examples: [5000, 10000, 15000],\n }),\n\n // ========================================================================\n // Database - Monitoring\n // ========================================================================\n\n DB_MONITORING_ENABLED: envBoolean({\n description: 'Enable database query performance monitoring',\n default: false,\n examples: [true, false],\n }),\n\n DB_MONITORING_SLOW_THRESHOLD: envNumber({\n description: 'Slow query threshold for monitoring (milliseconds)',\n default: 1000,\n examples: [500, 1000, 2000],\n }),\n\n DB_MONITORING_LOG_QUERIES: envBoolean({\n description: 'Log all database queries (not just slow queries)',\n default: false,\n examples: [true, false],\n }),\n\n // ========================================================================\n // Database - Transaction\n // ========================================================================\n\n TRANSACTION_TIMEOUT: envNumber({\n description: 'Transaction timeout in milliseconds',\n default: 30000,\n examples: [10000, 30000, 60000],\n }),\n\n // ========================================================================\n // Database - Development\n // ========================================================================\n\n DB_DEBUG_TRACE: envBoolean({\n description: 'Enable detailed debug tracing for database operations',\n default: false,\n examples: [true, false],\n }),\n\n // ========================================================================\n // Drizzle ORM\n // ========================================================================\n\n DRIZZLE_SCHEMA_PATH: envString({\n description: 'Path to Drizzle schema configuration',\n required: false,\n default: './src/server/entities/config.ts',\n examples: ['./src/db/schema.ts', './src/server/entities/config.ts'],\n }),\n\n DRIZZLE_OUT_DIR: envString({\n description: 'Output directory for Drizzle migrations',\n required: false,\n default: './drizzle',\n examples: ['./drizzle', './migrations'],\n }),\n\n // ========================================================================\n // Logger - Core\n // ========================================================================\n\n SPFN_LOG_LEVEL: envEnum(['debug', 'info', 'warn', 'error', 'fatal'] as const, {\n description: 'Minimum log level to output',\n default: 'info'\n }),\n\n // ========================================================================\n // Cache (Redis/Valkey)\n // ========================================================================\n\n CACHE_URL: envString({\n description: 'Single Redis/Valkey instance URL',\n required: false,\n sensitive: true,\n validator: parseRedisUrl,\n examples: ['redis://localhost:6379', 'rediss://secure.cache.com:6380'],\n }),\n\n CACHE_WRITE_URL: envString({\n description: 'Master Redis/Valkey URL for writes (master-replica pattern)',\n required: false,\n sensitive: true,\n validator: parseRedisUrl,\n examples: ['redis://master:6379'],\n }),\n\n CACHE_READ_URL: envString({\n description: 'Replica Redis/Valkey URL for reads (master-replica pattern)',\n required: false,\n sensitive: true,\n validator: parseRedisUrl,\n examples: ['redis://replica:6379'],\n }),\n\n CACHE_SENTINEL_HOSTS: envString({\n description: 'Comma-separated Redis Sentinel hosts',\n required: false,\n examples: ['sentinel1:26379,sentinel2:26379'],\n }),\n\n CACHE_CLUSTER_NODES: envString({\n description: 'Comma-separated Redis Cluster nodes',\n required: false,\n examples: ['node1:6379,node2:6379,node3:6379'],\n }),\n\n CACHE_MASTER_NAME: envString({\n description: 'Redis Sentinel master name',\n required: false,\n examples: ['mymaster'],\n }),\n\n CACHE_PASSWORD: envString({\n description: 'Redis/Valkey authentication password',\n required: false,\n sensitive: true,\n examples: ['your-redis-password'],\n }),\n\n CACHE_TLS_REJECT_UNAUTHORIZED: envBoolean({\n description: 'Verify TLS certificates for secure Redis connections',\n default: true,\n examples: [true, false],\n }),\n\n // ========================================================================\n // Server - Core\n // ========================================================================\n\n PORT: envNumber({\n description: 'Server port number',\n default: 4000,\n examples: [3000, 4000, 8080],\n }),\n\n HOST: envString({\n description: 'Server hostname',\n default: 'localhost',\n required: false,\n examples: ['localhost', '0.0.0.0', '127.0.0.1'],\n }),\n\n // ========================================================================\n // Server - Timeout\n // ========================================================================\n\n SERVER_TIMEOUT: envNumber({\n description: 'Request timeout in milliseconds',\n default: 120000,\n examples: [60000, 120000, 300000],\n }),\n\n SERVER_KEEPALIVE_TIMEOUT: envNumber({\n description: 'Keep-alive timeout in milliseconds',\n default: 65000,\n examples: [30000, 65000, 120000],\n }),\n\n SERVER_HEADERS_TIMEOUT: envNumber({\n description: 'Headers timeout in milliseconds',\n default: 60000,\n examples: [30000, 60000, 120000],\n }),\n\n SHUTDOWN_TIMEOUT: envNumber({\n description: 'Graceful shutdown timeout in milliseconds (must be less than k8s terminationGracePeriodSeconds minus preStop sleep, with safety margin)',\n default: 280000,\n examples: [30000, 120000, 280000],\n }),\n\n // ========================================================================\n // Next.js Integration\n // ========================================================================\n\n SPFN_API_URL: envUrl({\n description: 'SPFN API URL (used by Next.js to call backend)',\n required: true,\n nextjs: true,\n examples: ['http://localhost:8790', 'https://api.your-app.com'],\n }),\n\n NEXT_PUBLIC_SPFN_API_URL: envUrl({\n description: 'SPFN API URL (used by Next.js to call backend)',\n required: true,\n nextjs: true,\n examples: ['http://localhost:8790', 'https://api.your-app.com'],\n }),\n\n SPFN_APP_URL: envUrl({\n description: 'Next.js application URL (used by SPFN server)',\n required: false,\n nextjs: true,\n examples: ['http://localhost:3790', 'https://your-app.com'],\n }),\n});","/**\n * Core Package Configuration\n *\n * @example\n * ```typescript\n * import { registry } from '@spfn/core/config';\n *\n * const env = registry.validate();\n * console.log(env.DB_POOL_MAX);\n * ```\n *\n * @module config\n */\n\nimport { createEnvRegistry } from '@spfn/core/env';\nimport { coreEnvSchema } from './schema';\n\n/**\n * Core environment schema\n */\nexport { coreEnvSchema as envSchema } from './schema';\n\n/**\n * Environment registry\n *\n * @example\n * ```typescript\n * // Reset for testing\n * registry.reset();\n * ```\n */\nexport const registry = createEnvRegistry(coreEnvSchema);\n\n/**\n * Validated environment configuration\n */\nexport const env = registry.validate();"]}
@@ -26,6 +26,28 @@ interface ErrorHandlerOptions {
26
26
  * @default true
27
27
  */
28
28
  enableLogging?: boolean;
29
+ /**
30
+ * Callback invoked when an error occurs
31
+ *
32
+ * Called asynchronously without blocking the response.
33
+ * Useful for external error notifications (Slack, PagerDuty, etc.)
34
+ */
35
+ onError?: (err: Error, context: OnErrorContext) => Promise<void> | void;
36
+ }
37
+ /**
38
+ * Context passed to onError callback
39
+ */
40
+ interface OnErrorContext {
41
+ statusCode: number;
42
+ path: string;
43
+ method: string;
44
+ requestId?: string;
45
+ timestamp: string;
46
+ userId?: string;
47
+ request: {
48
+ headers: Record<string, string>;
49
+ query: Record<string, string>;
50
+ };
29
51
  }
30
52
  /**
31
53
  * Error handler middleware for Hono
@@ -139,4 +161,4 @@ declare function maskSensitiveData(obj: any, sensitiveFields: string[], seen?: W
139
161
  */
140
162
  declare function RequestLogger(options?: RequestLoggerOptions): (c: Context, next: Next) => Promise<void>;
141
163
 
142
- export { ErrorHandler, type ErrorHandlerOptions, RequestLogger, type RequestLoggerConfig, type RequestLoggerOptions, maskSensitiveData };
164
+ export { ErrorHandler, type ErrorHandlerOptions, type OnErrorContext, RequestLogger, type RequestLoggerConfig, type RequestLoggerOptions, maskSensitiveData };
@@ -5,6 +5,29 @@ import { randomBytes } from 'crypto';
5
5
 
6
6
  // src/middleware/error-handler.ts
7
7
  var errorLogger = logger.child("@spfn/core:error-handler");
8
+ var SENSITIVE_HEADERS = /* @__PURE__ */ new Set(["authorization", "cookie", "x-api-key", "x-auth-token"]);
9
+ function extractHeaders(c) {
10
+ const headers = {};
11
+ c.req.raw.headers.forEach((value, key) => {
12
+ headers[key] = SENSITIVE_HEADERS.has(key.toLowerCase()) ? "***" : value;
13
+ });
14
+ return headers;
15
+ }
16
+ function buildOnErrorContext(c, statusCode) {
17
+ const auth = c.get("auth");
18
+ return {
19
+ statusCode,
20
+ path: c.req.path,
21
+ method: c.req.method,
22
+ requestId: c.get("requestId"),
23
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
24
+ userId: auth?.userId,
25
+ request: {
26
+ headers: extractHeaders(c),
27
+ query: c.req.query()
28
+ }
29
+ };
30
+ }
8
31
  function logError(err, logData, includeStack) {
9
32
  const logLevel = logData.statusCode >= 500 ? "error" : "warn";
10
33
  if (includeStack) {
@@ -19,9 +42,12 @@ function isSerializableError(err) {
19
42
  function ErrorHandler(options = {}) {
20
43
  const {
21
44
  includeStack = env.NODE_ENV !== "production",
22
- enableLogging = true
45
+ enableLogging = true,
46
+ onError
23
47
  } = options;
24
48
  return (err, c) => {
49
+ const path = c.req.path;
50
+ const method = c.req.method;
25
51
  if (isSerializableError(err)) {
26
52
  const { statusCode: statusCode2 } = err;
27
53
  if (enableLogging) {
@@ -29,10 +55,14 @@ function ErrorHandler(options = {}) {
29
55
  type: err.constructor.name,
30
56
  message: err.message,
31
57
  statusCode: statusCode2,
32
- path: c.req.path,
33
- method: c.req.method
58
+ path,
59
+ method
34
60
  }, includeStack);
35
61
  }
62
+ if (onError) {
63
+ const ctx = buildOnErrorContext(c, statusCode2);
64
+ Promise.resolve(onError(err, ctx)).catch((e) => errorLogger.warn("onError callback failed", e));
65
+ }
36
66
  const serialized = err.toJSON();
37
67
  if (includeStack && err.stack) {
38
68
  serialized.stack = err.stack;
@@ -46,10 +76,14 @@ function ErrorHandler(options = {}) {
46
76
  type: err.name || "Error",
47
77
  message: err.message,
48
78
  statusCode,
49
- path: c.req.path,
50
- method: c.req.method
79
+ path,
80
+ method
51
81
  }, includeStack);
52
82
  }
83
+ if (onError) {
84
+ const ctx = buildOnErrorContext(c, statusCode);
85
+ Promise.resolve(onError(err, ctx)).catch((e) => errorLogger.warn("onError callback failed", e));
86
+ }
53
87
  const response = {
54
88
  __type: "Error",
55
89
  message: err.message || "Internal Server Error"
@@ -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\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\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 } = options;\n\n return (err: Error, c: Context) =>\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: c.req.path,\n method: c.req.method,\n }, includeStack);\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: c.req.path,\n method: c.req.method,\n }, includeStack);\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}\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}"]}
@@ -3,6 +3,7 @@ import { MiddlewareHandler, Hono } from 'hono';
3
3
  import { cors } from 'hono/cors';
4
4
  import { serve } from '@hono/node-server';
5
5
  import { NamedMiddleware, Router } from '@spfn/core/route';
6
+ import { OnErrorContext } from '@spfn/core/middleware';
6
7
  import { J as JobRouter, B as BossOptions } from '../boss-DI1r4kTS.js';
7
8
  import { E as EventRouterDef } from '../router-Di7ENoah.js';
8
9
  import { S as SSEHandlerConfig, a as SSEAuthConfig } from '../types-B-lVqv6b.js';
@@ -74,6 +75,21 @@ interface ServerConfig {
74
75
  * Error handler (default: true)
75
76
  */
76
77
  errorHandler?: boolean;
78
+ /**
79
+ * Callback invoked when an error occurs (passed to ErrorHandler)
80
+ *
81
+ * Called asynchronously without blocking the response.
82
+ *
83
+ * @example
84
+ * ```typescript
85
+ * import { createErrorSlackNotifier } from '@spfn/notification/server';
86
+ *
87
+ * middleware: {
88
+ * onError: createErrorSlackNotifier({ minStatusCode: 500 }),
89
+ * }
90
+ * ```
91
+ */
92
+ onError?: (err: Error, context: OnErrorContext) => Promise<void> | void;
77
93
  };
78
94
  /**
79
95
  * Additional custom middleware
@@ -303,9 +319,13 @@ interface ServerConfig {
303
319
  shutdown?: {
304
320
  /**
305
321
  * Graceful shutdown timeout in milliseconds
306
- * Maximum time to wait for ongoing requests and resource cleanup
307
- * After timeout, forces process termination
308
- * @default 30000 (30 seconds)
322
+ * Maximum time to wait for in-flight operations to drain and resource cleanup
323
+ * After timeout, forces process.exit() before k8s SIGKILL
324
+ *
325
+ * Formula: terminationGracePeriodSeconds - preStopSleep - safetyMargin
326
+ * Default: 300s - 5s - 15s = 280s
327
+ *
328
+ * @default 280000 (280 seconds)
309
329
  * @env SHUTDOWN_TIMEOUT
310
330
  */
311
331
  timeout?: number;
@@ -564,6 +584,124 @@ declare function createServer(config?: ServerConfig): Promise<Hono>;
564
584
  */
565
585
  declare function startServer(config?: ServerConfig): Promise<ServerInstance>;
566
586
 
587
+ /**
588
+ * Shutdown Manager
589
+ *
590
+ * Manages graceful shutdown with drain behavior.
591
+ * All tracked operations must complete before shutdown proceeds.
592
+ *
593
+ * Features:
594
+ * - Hook registry: Multiple modules can register independent cleanup handlers
595
+ * - Operation tracking: Long-running tasks are awaited during shutdown (drain)
596
+ * - State management: isShuttingDown() for rejecting new work
597
+ */
598
+ interface ShutdownHookOptions {
599
+ /**
600
+ * Timeout for this hook in milliseconds
601
+ * If the hook exceeds this time, it is skipped and the next hook runs
602
+ * @default 10000 (10s)
603
+ */
604
+ timeout?: number;
605
+ /**
606
+ * Execution order (lower runs first)
607
+ * @default 100
608
+ */
609
+ order?: number;
610
+ }
611
+ declare class ShutdownManager {
612
+ private state;
613
+ private hooks;
614
+ private operations;
615
+ private operationCounter;
616
+ /**
617
+ * Register a shutdown hook
618
+ *
619
+ * Hooks run in order during shutdown, after all tracked operations drain.
620
+ * Each hook has its own timeout — failure does not block subsequent hooks.
621
+ *
622
+ * @example
623
+ * shutdown.onShutdown('ai-service', async () => {
624
+ * await aiService.cancelPending();
625
+ * }, { timeout: 30000, order: 10 });
626
+ */
627
+ onShutdown(name: string, handler: () => Promise<void>, options?: ShutdownHookOptions): void;
628
+ /**
629
+ * Track a long-running operation
630
+ *
631
+ * During shutdown (drain phase), the process waits for ALL tracked
632
+ * operations to complete before proceeding with cleanup.
633
+ *
634
+ * If shutdown has already started, the operation is rejected immediately.
635
+ *
636
+ * @returns The operation result (pass-through)
637
+ *
638
+ * @example
639
+ * const result = await shutdown.trackOperation(
640
+ * 'ai-generate',
641
+ * aiService.generate(prompt)
642
+ * );
643
+ */
644
+ trackOperation<T>(name: string, operation: Promise<T>): Promise<T>;
645
+ /**
646
+ * Whether the server is shutting down
647
+ *
648
+ * Use this to reject new work early (e.g., return 503 in route handlers).
649
+ */
650
+ isShuttingDown(): boolean;
651
+ /**
652
+ * Number of currently active tracked operations
653
+ */
654
+ getActiveOperationCount(): number;
655
+ /**
656
+ * Mark shutdown as started immediately
657
+ *
658
+ * Call this at the very beginning of the shutdown sequence so that:
659
+ * - Health check returns 503 right away
660
+ * - trackOperation() rejects new work
661
+ * - isShuttingDown() returns true
662
+ */
663
+ beginShutdown(): void;
664
+ /**
665
+ * Execute the full shutdown sequence
666
+ *
667
+ * 1. State → draining (reject new operations)
668
+ * 2. Wait for all tracked operations to complete (drain)
669
+ * 3. Run shutdown hooks in order
670
+ * 4. State → closed
671
+ *
672
+ * @param drainTimeout - Max time to wait for operations to drain (ms)
673
+ */
674
+ execute(drainTimeout: number): Promise<void>;
675
+ /**
676
+ * Wait for all tracked operations to complete, up to drainTimeout
677
+ */
678
+ private drain;
679
+ /**
680
+ * Execute registered shutdown hooks in order
681
+ */
682
+ private executeHooks;
683
+ }
684
+ /**
685
+ * Get the global ShutdownManager instance
686
+ *
687
+ * Available after server starts. Use this to register shutdown hooks
688
+ * or track long-running operations.
689
+ *
690
+ * @example
691
+ * import { getShutdownManager } from '@spfn/core/server';
692
+ *
693
+ * const shutdown = getShutdownManager();
694
+ *
695
+ * // Register cleanup
696
+ * shutdown.onShutdown('my-service', async () => {
697
+ * await myService.close();
698
+ * });
699
+ *
700
+ * // Track long operation
701
+ * await shutdown.trackOperation('ai-task', longRunningPromise);
702
+ */
703
+ declare function getShutdownManager(): ShutdownManager;
704
+
567
705
  /**
568
706
  * Server Config Builder
569
707
  *
@@ -754,4 +892,4 @@ declare class ServerConfigBuilder {
754
892
  */
755
893
  declare function defineServerConfig(): ServerConfigBuilder;
756
894
 
757
- export { type AppFactory, type ServerConfig, type ServerInstance, createServer, defineServerConfig, loadEnvFiles, startServer };
895
+ export { type AppFactory, type ServerConfig, type ServerInstance, type ShutdownHookOptions, createServer, defineServerConfig, getShutdownManager, loadEnvFiles, startServer };