@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.
- package/dist/config/index.d.ts +72 -0
- package/dist/config/index.js +18 -1
- package/dist/config/index.js.map +1 -1
- package/dist/middleware/index.d.ts +109 -2
- package/dist/middleware/index.js +244 -2
- package/dist/middleware/index.js.map +1 -1
- package/dist/nextjs/server.d.ts +10 -0
- package/dist/nextjs/server.js +98 -1
- package/dist/nextjs/server.js.map +1 -1
- package/dist/server/index.d.ts +27 -1
- package/dist/server/index.js +50 -0
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/dist/config/index.d.ts
CHANGED
|
@@ -469,6 +469,30 @@ declare const coreEnvSchema: {
|
|
|
469
469
|
} & {
|
|
470
470
|
key: "RPC_PROXY_TIMEOUT";
|
|
471
471
|
};
|
|
472
|
+
SPFN_PROXY_SECRET: {
|
|
473
|
+
description: string;
|
|
474
|
+
required: boolean;
|
|
475
|
+
sensitive: boolean;
|
|
476
|
+
nextjs: boolean;
|
|
477
|
+
examples: string[];
|
|
478
|
+
} & {
|
|
479
|
+
type: "string";
|
|
480
|
+
validator: (value: string) => string;
|
|
481
|
+
} & {
|
|
482
|
+
key: "SPFN_PROXY_SECRET";
|
|
483
|
+
};
|
|
484
|
+
SPFN_PROXY_SECRET_PREVIOUS: {
|
|
485
|
+
description: string;
|
|
486
|
+
required: boolean;
|
|
487
|
+
sensitive: boolean;
|
|
488
|
+
nextjs: boolean;
|
|
489
|
+
examples: string[];
|
|
490
|
+
} & {
|
|
491
|
+
type: "string";
|
|
492
|
+
validator: (value: string) => string;
|
|
493
|
+
} & {
|
|
494
|
+
key: "SPFN_PROXY_SECRET_PREVIOUS";
|
|
495
|
+
};
|
|
472
496
|
};
|
|
473
497
|
|
|
474
498
|
/**
|
|
@@ -930,6 +954,30 @@ declare const registry: _spfn_core_env.EnvRegistry<{
|
|
|
930
954
|
} & {
|
|
931
955
|
key: "RPC_PROXY_TIMEOUT";
|
|
932
956
|
};
|
|
957
|
+
SPFN_PROXY_SECRET: {
|
|
958
|
+
description: string;
|
|
959
|
+
required: boolean;
|
|
960
|
+
sensitive: boolean;
|
|
961
|
+
nextjs: boolean;
|
|
962
|
+
examples: string[];
|
|
963
|
+
} & {
|
|
964
|
+
type: "string";
|
|
965
|
+
validator: (value: string) => string;
|
|
966
|
+
} & {
|
|
967
|
+
key: "SPFN_PROXY_SECRET";
|
|
968
|
+
};
|
|
969
|
+
SPFN_PROXY_SECRET_PREVIOUS: {
|
|
970
|
+
description: string;
|
|
971
|
+
required: boolean;
|
|
972
|
+
sensitive: boolean;
|
|
973
|
+
nextjs: boolean;
|
|
974
|
+
examples: string[];
|
|
975
|
+
} & {
|
|
976
|
+
type: "string";
|
|
977
|
+
validator: (value: string) => string;
|
|
978
|
+
} & {
|
|
979
|
+
key: "SPFN_PROXY_SECRET_PREVIOUS";
|
|
980
|
+
};
|
|
933
981
|
}>;
|
|
934
982
|
/**
|
|
935
983
|
* Validated environment configuration
|
|
@@ -1384,6 +1432,30 @@ declare const env: _spfn_core_env.InferEnvType<{
|
|
|
1384
1432
|
} & {
|
|
1385
1433
|
key: "RPC_PROXY_TIMEOUT";
|
|
1386
1434
|
};
|
|
1435
|
+
SPFN_PROXY_SECRET: {
|
|
1436
|
+
description: string;
|
|
1437
|
+
required: boolean;
|
|
1438
|
+
sensitive: boolean;
|
|
1439
|
+
nextjs: boolean;
|
|
1440
|
+
examples: string[];
|
|
1441
|
+
} & {
|
|
1442
|
+
type: "string";
|
|
1443
|
+
validator: (value: string) => string;
|
|
1444
|
+
} & {
|
|
1445
|
+
key: "SPFN_PROXY_SECRET";
|
|
1446
|
+
};
|
|
1447
|
+
SPFN_PROXY_SECRET_PREVIOUS: {
|
|
1448
|
+
description: string;
|
|
1449
|
+
required: boolean;
|
|
1450
|
+
sensitive: boolean;
|
|
1451
|
+
nextjs: boolean;
|
|
1452
|
+
examples: string[];
|
|
1453
|
+
} & {
|
|
1454
|
+
type: "string";
|
|
1455
|
+
validator: (value: string) => string;
|
|
1456
|
+
} & {
|
|
1457
|
+
key: "SPFN_PROXY_SECRET_PREVIOUS";
|
|
1458
|
+
};
|
|
1387
1459
|
}>;
|
|
1388
1460
|
|
|
1389
1461
|
export { env, coreEnvSchema as envSchema, registry };
|
package/dist/config/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineEnvSchema, envNumber, envUrl,
|
|
1
|
+
import { defineEnvSchema, envString, envNumber, envUrl, envBoolean, envEnum, parseRedisUrl, parsePostgresUrl, createEnvRegistry } from '@spfn/core/env';
|
|
2
2
|
|
|
3
3
|
// src/config/index.ts
|
|
4
4
|
var coreEnvSchema = defineEnvSchema({
|
|
@@ -285,6 +285,23 @@ var coreEnvSchema = defineEnvSchema({
|
|
|
285
285
|
default: 12e4,
|
|
286
286
|
nextjs: true,
|
|
287
287
|
examples: [6e4, 12e4, 28e4]
|
|
288
|
+
}),
|
|
289
|
+
// ========================================================================
|
|
290
|
+
// Proxy → Backend trust (HMAC signing)
|
|
291
|
+
// ========================================================================
|
|
292
|
+
SPFN_PROXY_SECRET: envString({
|
|
293
|
+
description: "Shared secret for signing proxy\u2192backend requests (HMAC-SHA256). Read by BOTH processes \u2014 the Next.js proxy (to sign) and the SPFN backend (to verify) \u2014 so it belongs in .env.local (loaded by both; the backend reads it via loadEnv, Next.js reads it server-side without exposing it to the browser). Set the SAME value on both. Leave unset to disable proxy-guard signing.",
|
|
294
|
+
required: false,
|
|
295
|
+
sensitive: true,
|
|
296
|
+
nextjs: true,
|
|
297
|
+
examples: ["<32+ byte random hex>", "v2:<32+ byte random hex>"]
|
|
298
|
+
}),
|
|
299
|
+
SPFN_PROXY_SECRET_PREVIOUS: envString({
|
|
300
|
+
description: "Previous (grace) proxy keys still accepted for verification during rotation \u2014 comma-separated <keyId>:<secret>. The proxy never signs with these; they only keep requests signed with the prior key verifying until a rollout settles. Backend-only (verification), so it belongs in .env.server, NOT exposed to the Next.js process.",
|
|
301
|
+
required: false,
|
|
302
|
+
sensitive: true,
|
|
303
|
+
nextjs: false,
|
|
304
|
+
examples: ["v1:<old secret>", "v1:<old>,v0:<older>"]
|
|
288
305
|
})
|
|
289
306
|
});
|
|
290
307
|
|
package/dist/config/index.js.map
CHANGED
|
@@ -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 // 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 // Fetch (Node.js undici) - outbound HTTP request timeout\n // ========================================================================\n\n FETCH_CONNECT_TIMEOUT: envNumber({\n description: 'Fetch TCP connection timeout in milliseconds (time to establish socket connection to upstream server)',\n default: 10000,\n examples: [5000, 10000, 30000],\n }),\n\n FETCH_HEADERS_TIMEOUT: envNumber({\n description: 'Fetch headers timeout in milliseconds (time to receive response headers after request sent)',\n default: 300000,\n examples: [120000, 300000, 600000],\n }),\n\n FETCH_BODY_TIMEOUT: envNumber({\n description: 'Fetch body timeout in milliseconds (time between body data chunks from upstream server)',\n default: 300000,\n examples: [120000, 300000, 600000],\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 RPC_PROXY_TIMEOUT: envNumber({\n description: 'RPC proxy request timeout in milliseconds (AbortController timeout for proxied requests to backend, should be shorter than FETCH_HEADERS_TIMEOUT)',\n default: 120000,\n nextjs: true,\n examples: [60000, 120000, 280000],\n }),\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();\n"]}
|
|
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,EAMD,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,uBAAuB,SAAA,CAAU;AAAA,IAC7B,WAAA,EAAa,uGAAA;AAAA,IACb,OAAA,EAAS,GAAA;AAAA,IACT,QAAA,EAAU,CAAC,GAAA,EAAM,GAAA,EAAO,GAAK;AAAA,GAChC,CAAA;AAAA,EAED,uBAAuB,SAAA,CAAU;AAAA,IAC7B,WAAA,EAAa,6FAAA;AAAA,IACb,OAAA,EAAS,GAAA;AAAA,IACT,QAAA,EAAU,CAAC,IAAA,EAAQ,GAAA,EAAQ,GAAM;AAAA,GACpC,CAAA;AAAA,EAED,oBAAoB,SAAA,CAAU;AAAA,IAC1B,WAAA,EAAa,yFAAA;AAAA,IACb,OAAA,EAAS,GAAA;AAAA,IACT,QAAA,EAAU,CAAC,IAAA,EAAQ,GAAA,EAAQ,GAAM;AAAA,GACpC,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,CAAA;AAAA,EAED,mBAAmB,SAAA,CAAU;AAAA,IACzB,WAAA,EAAa,mJAAA;AAAA,IACb,OAAA,EAAS,IAAA;AAAA,IACT,MAAA,EAAQ,IAAA;AAAA,IACR,QAAA,EAAU,CAAC,GAAA,EAAO,IAAA,EAAQ,IAAM;AAAA,GACnC,CAAA;AAAA;AAAA;AAAA;AAAA,EAMD,mBAAmB,SAAA,CAAU;AAAA,IACzB,WAAA,EAAa,iYAAA;AAAA,IACb,QAAA,EAAU,KAAA;AAAA,IACV,SAAA,EAAW,IAAA;AAAA,IACX,MAAA,EAAQ,IAAA;AAAA,IACR,QAAA,EAAU,CAAC,uBAAA,EAAyB,0BAA0B;AAAA,GACjE,CAAA;AAAA,EAED,4BAA4B,SAAA,CAAU;AAAA,IAClC,WAAA,EAAa,4UAAA;AAAA,IACb,QAAA,EAAU,KAAA;AAAA,IACV,SAAA,EAAW,IAAA;AAAA,IACX,MAAA,EAAQ,KAAA;AAAA,IACR,QAAA,EAAU,CAAC,iBAAA,EAAmB,qBAAqB;AAAA,GACtD;AACL,CAAC;;;AClXM,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 // 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 // Fetch (Node.js undici) - outbound HTTP request timeout\n // ========================================================================\n\n FETCH_CONNECT_TIMEOUT: envNumber({\n description: 'Fetch TCP connection timeout in milliseconds (time to establish socket connection to upstream server)',\n default: 10000,\n examples: [5000, 10000, 30000],\n }),\n\n FETCH_HEADERS_TIMEOUT: envNumber({\n description: 'Fetch headers timeout in milliseconds (time to receive response headers after request sent)',\n default: 300000,\n examples: [120000, 300000, 600000],\n }),\n\n FETCH_BODY_TIMEOUT: envNumber({\n description: 'Fetch body timeout in milliseconds (time between body data chunks from upstream server)',\n default: 300000,\n examples: [120000, 300000, 600000],\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 RPC_PROXY_TIMEOUT: envNumber({\n description: 'RPC proxy request timeout in milliseconds (AbortController timeout for proxied requests to backend, should be shorter than FETCH_HEADERS_TIMEOUT)',\n default: 120000,\n nextjs: true,\n examples: [60000, 120000, 280000],\n }),\n\n // ========================================================================\n // Proxy → Backend trust (HMAC signing)\n // ========================================================================\n\n SPFN_PROXY_SECRET: envString({\n description: 'Shared secret for signing proxy→backend requests (HMAC-SHA256). Read by BOTH processes — the Next.js proxy (to sign) and the SPFN backend (to verify) — so it belongs in .env.local (loaded by both; the backend reads it via loadEnv, Next.js reads it server-side without exposing it to the browser). Set the SAME value on both. Leave unset to disable proxy-guard signing.',\n required: false,\n sensitive: true,\n nextjs: true,\n examples: ['<32+ byte random hex>', 'v2:<32+ byte random hex>'],\n }),\n\n SPFN_PROXY_SECRET_PREVIOUS: envString({\n description: 'Previous (grace) proxy keys still accepted for verification during rotation — comma-separated <keyId>:<secret>. The proxy never signs with these; they only keep requests signed with the prior key verifying until a rollout settles. Backend-only (verification), so it belongs in .env.server, NOT exposed to the Next.js process.',\n required: false,\n sensitive: true,\n nextjs: false,\n examples: ['v1:<old secret>', 'v1:<old>,v0:<older>'],\n }),\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();\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Context, Next } from 'hono';
|
|
1
|
+
import { Context, Next, MiddlewareHandler } from 'hono';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Error Handler Middleware
|
|
@@ -161,4 +161,111 @@ declare function maskSensitiveData(obj: any, sensitiveFields: string[], seen?: W
|
|
|
161
161
|
*/
|
|
162
162
|
declare function RequestLogger(options?: RequestLoggerOptions): (c: Context, next: Next) => Promise<void>;
|
|
163
163
|
|
|
164
|
-
|
|
164
|
+
/**
|
|
165
|
+
* Proxy-guard middleware
|
|
166
|
+
*
|
|
167
|
+
* Verifies that an inbound request came through a trusted Next.js RPC proxy
|
|
168
|
+
* (HMAC signature) and/or from an allowed browser origin, then tags the request
|
|
169
|
+
* with a `clientType`. Lets the backend reject direct-to-backend calls that
|
|
170
|
+
* bypass the proxy.
|
|
171
|
+
*
|
|
172
|
+
* This does NOT replace user authentication (JWT) — it answers "what kind of
|
|
173
|
+
* client is this?", not "who is the user?". It also cannot stop a client from
|
|
174
|
+
* calling the proxy itself; that is the web's inherent limit (see
|
|
175
|
+
* PROXY-BACKEND-AUTH-SPEC.md). Mobile clients call the backend directly and are
|
|
176
|
+
* handled by a future attestation path, OR'd into this same check.
|
|
177
|
+
*
|
|
178
|
+
* @module middleware/proxy-guard
|
|
179
|
+
*/
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* How the request reached the backend.
|
|
183
|
+
* - `web`: verified via a trusted proxy signature
|
|
184
|
+
* - `untrusted`: could not be verified (only reached handlers in `tag` mode)
|
|
185
|
+
* - future: `ios` | `android` once attestation lands
|
|
186
|
+
*/
|
|
187
|
+
type ClientType = 'web' | 'untrusted' | (string & {});
|
|
188
|
+
/**
|
|
189
|
+
* Enforcement mode.
|
|
190
|
+
* - `off`: middleware is a no-op (default — keeps existing apps working).
|
|
191
|
+
* - `tag`: verify and set `clientType`, but never reject (observe first).
|
|
192
|
+
* - `strict`: reject requests that fail verification.
|
|
193
|
+
*/
|
|
194
|
+
type ProxyGuardMode = 'off' | 'tag' | 'strict';
|
|
195
|
+
/**
|
|
196
|
+
* Optional replay store. Returns `true` when the nonce is fresh (and records it),
|
|
197
|
+
* `false` when it has been seen before. Backed by Redis in multi-instance setups.
|
|
198
|
+
*/
|
|
199
|
+
interface NonceStore {
|
|
200
|
+
checkAndSet(nonce: string, ttlMs: number): Promise<boolean>;
|
|
201
|
+
}
|
|
202
|
+
interface ProxyGuardConfig {
|
|
203
|
+
/**
|
|
204
|
+
* Active shared HMAC secret, as `<keyId>:<secret>` (or a bare secret).
|
|
205
|
+
* Defaults to `env.SPFN_PROXY_SECRET`. Without it (and no previous keys),
|
|
206
|
+
* signatures cannot be verified and every request is tagged `untrusted`.
|
|
207
|
+
*/
|
|
208
|
+
secret?: string;
|
|
209
|
+
/**
|
|
210
|
+
* Previous (grace) keys still accepted for verification during rotation, as a
|
|
211
|
+
* comma-separated list of `<keyId>:<secret>`. The proxy never signs with
|
|
212
|
+
* these — they exist only so in-flight requests signed with the prior key
|
|
213
|
+
* keep verifying until the rollout settles. Defaults to
|
|
214
|
+
* `env.SPFN_PROXY_SECRET_PREVIOUS`. Backend-only, so it can live in
|
|
215
|
+
* `.env.server` (never exposed to the Next.js process).
|
|
216
|
+
*/
|
|
217
|
+
previousSecrets?: string;
|
|
218
|
+
/** Enforcement mode. @default 'off' */
|
|
219
|
+
mode?: ProxyGuardMode;
|
|
220
|
+
/** Allowed clock skew / replay window in ms. @default 30000 */
|
|
221
|
+
windowMs?: number;
|
|
222
|
+
/**
|
|
223
|
+
* Browser origin allowlist. When set, a request carrying an `Origin` header
|
|
224
|
+
* not in this list is rejected (strict) / tagged (tag). Requests without an
|
|
225
|
+
* `Origin` (server-to-server, mobile) skip this check and fall back to the
|
|
226
|
+
* signature.
|
|
227
|
+
*/
|
|
228
|
+
allowedOrigins?: string[];
|
|
229
|
+
/** Optional Redis-backed nonce store for hard replay rejection (strict mode only). */
|
|
230
|
+
nonceStore?: NonceStore;
|
|
231
|
+
/** Paths to skip entirely (e.g. health checks). */
|
|
232
|
+
skipPaths?: string[];
|
|
233
|
+
/**
|
|
234
|
+
* Reject (413) once the streamed body exceeds this many bytes. Measured AS IT
|
|
235
|
+
* STREAMS — Content-Length is NOT trusted, so a missing/chunked/under-reported
|
|
236
|
+
* length can't bypass it. Bounds the per-request memory the guard buffers.
|
|
237
|
+
* Undefined = no cap (default). Multipart is exempt (unsigned).
|
|
238
|
+
*/
|
|
239
|
+
maxBodyBytes?: number;
|
|
240
|
+
}
|
|
241
|
+
declare module 'hono' {
|
|
242
|
+
interface ContextVariableMap {
|
|
243
|
+
clientType?: ClientType;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Create the proxy-guard middleware.
|
|
248
|
+
*
|
|
249
|
+
* @example
|
|
250
|
+
* ```typescript
|
|
251
|
+
* export default defineServerConfig()
|
|
252
|
+
* .proxyGuard({ mode: 'strict', allowedOrigins: ['https://app.example.com'] })
|
|
253
|
+
* .routes(appRouter)
|
|
254
|
+
* .build();
|
|
255
|
+
* ```
|
|
256
|
+
*/
|
|
257
|
+
declare function createProxyGuard(config?: ProxyGuardConfig): MiddlewareHandler;
|
|
258
|
+
/** Minimal cache client (compatible with ioredis Redis | Cluster). */
|
|
259
|
+
type CacheClient = {
|
|
260
|
+
set(key: string, value: string, ...args: any[]): Promise<any>;
|
|
261
|
+
};
|
|
262
|
+
/**
|
|
263
|
+
* Build a Redis-backed nonce store for hard replay rejection.
|
|
264
|
+
*
|
|
265
|
+
* Uses `SET key 1 PX ttl NX` — an atomic "set if absent". A fresh nonce returns
|
|
266
|
+
* 'OK'; a replayed one returns null. TTL keeps the key set bounded (only nonces
|
|
267
|
+
* within the replay window matter).
|
|
268
|
+
*/
|
|
269
|
+
declare function createCacheNonceStore(cache: CacheClient, prefix?: string): NonceStore;
|
|
270
|
+
|
|
271
|
+
export { type ClientType, ErrorHandler, type ErrorHandlerOptions, type NonceStore, type OnErrorContext, type ProxyGuardConfig, type ProxyGuardMode, RequestLogger, type RequestLoggerConfig, type RequestLoggerOptions, createCacheNonceStore, createProxyGuard, maskSensitiveData };
|
package/dist/middleware/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { SerializableError } from '@spfn/core/errors';
|
|
2
2
|
import { logger } from '@spfn/core/logger';
|
|
3
3
|
import { env } from '@spfn/core/config';
|
|
4
|
-
import { randomBytes } from 'crypto';
|
|
4
|
+
import { randomBytes, createHmac, createHash, timingSafeEqual } from 'crypto';
|
|
5
5
|
|
|
6
6
|
// src/middleware/error-handler.ts
|
|
7
7
|
var errorLogger = logger.child("@spfn/core:error-handler");
|
|
@@ -206,7 +206,249 @@ function RequestLogger(options) {
|
|
|
206
206
|
}
|
|
207
207
|
};
|
|
208
208
|
}
|
|
209
|
+
var PROXY_SIGNATURE_HEADER = "x-spfn-proxy-signature";
|
|
210
|
+
var PROXY_TIMESTAMP_HEADER = "x-spfn-proxy-timestamp";
|
|
211
|
+
var PROXY_NONCE_HEADER = "x-spfn-proxy-nonce";
|
|
212
|
+
var PROXY_KEY_ID_HEADER = "x-spfn-proxy-key-id";
|
|
213
|
+
var DEFAULT_KEY_ID = "default";
|
|
214
|
+
function parseProxyKey(raw) {
|
|
215
|
+
const idx = raw.indexOf(":");
|
|
216
|
+
if (idx === -1) {
|
|
217
|
+
return { keyId: DEFAULT_KEY_ID, secret: raw };
|
|
218
|
+
}
|
|
219
|
+
return { keyId: raw.slice(0, idx) || DEFAULT_KEY_ID, secret: raw.slice(idx + 1) };
|
|
220
|
+
}
|
|
221
|
+
function parseProxyKeySet(raws) {
|
|
222
|
+
const keys = [];
|
|
223
|
+
const seen = /* @__PURE__ */ new Set();
|
|
224
|
+
for (const raw of raws) {
|
|
225
|
+
if (!raw) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
for (const part of raw.split(",")) {
|
|
229
|
+
const trimmed = part.trim();
|
|
230
|
+
if (!trimmed) {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const key = parseProxyKey(trimmed);
|
|
234
|
+
if (!key.secret || seen.has(key.keyId)) {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
seen.add(key.keyId);
|
|
238
|
+
keys.push(key);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return keys;
|
|
242
|
+
}
|
|
243
|
+
function buildCanonicalString(parts) {
|
|
244
|
+
return [parts.method.toUpperCase(), parts.path, parts.query, parts.timestamp, parts.nonce, parts.bodyHash].join("\n");
|
|
245
|
+
}
|
|
246
|
+
function hashBody(body) {
|
|
247
|
+
if (!body || body.length === 0) {
|
|
248
|
+
return "";
|
|
249
|
+
}
|
|
250
|
+
return typeof body === "string" ? createHash("sha256").update(body, "utf8").digest("hex") : createHash("sha256").update(body).digest("hex");
|
|
251
|
+
}
|
|
252
|
+
function computeSignature(secret, parts) {
|
|
253
|
+
return createHmac("sha256", secret).update(buildCanonicalString(parts)).digest("hex");
|
|
254
|
+
}
|
|
255
|
+
function safeEqualHex(a, b) {
|
|
256
|
+
const bufA = Buffer.from(a, "hex");
|
|
257
|
+
const bufB = Buffer.from(b, "hex");
|
|
258
|
+
if (bufA.length === 0 || bufA.length !== bufB.length) {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
return timingSafeEqual(bufA, bufB);
|
|
262
|
+
}
|
|
263
|
+
function verifyProxyRequest(input) {
|
|
264
|
+
const { signature, timestamp, nonce, keyId } = input;
|
|
265
|
+
if (!signature || !timestamp || !nonce || !keyId) {
|
|
266
|
+
return { valid: false, reason: "missing-headers" };
|
|
267
|
+
}
|
|
268
|
+
const ts = Number(timestamp);
|
|
269
|
+
if (!Number.isFinite(ts)) {
|
|
270
|
+
return { valid: false, reason: "bad-timestamp" };
|
|
271
|
+
}
|
|
272
|
+
const now = input.now ?? Date.now();
|
|
273
|
+
const windowMs = input.windowMs ?? 3e4;
|
|
274
|
+
if (Math.abs(now - ts) > windowMs) {
|
|
275
|
+
return { valid: false, reason: "stale-timestamp" };
|
|
276
|
+
}
|
|
277
|
+
const key = input.keys.find((k) => k.keyId === keyId);
|
|
278
|
+
if (!key) {
|
|
279
|
+
return { valid: false, reason: "unknown-key" };
|
|
280
|
+
}
|
|
281
|
+
const expected = computeSignature(key.secret, {
|
|
282
|
+
method: input.method,
|
|
283
|
+
path: input.path,
|
|
284
|
+
query: input.query ?? "",
|
|
285
|
+
timestamp,
|
|
286
|
+
nonce,
|
|
287
|
+
bodyHash: hashBody(input.body)
|
|
288
|
+
});
|
|
289
|
+
if (!safeEqualHex(signature, expected)) {
|
|
290
|
+
return { valid: false, reason: "signature-mismatch" };
|
|
291
|
+
}
|
|
292
|
+
return { valid: true, nonce, keyId };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// src/middleware/proxy-guard.ts
|
|
296
|
+
var guardLogger = logger.child("@spfn/core:proxy-guard");
|
|
297
|
+
var BODY_OVERSIZE = Symbol("proxy-guard:body-oversize");
|
|
298
|
+
var BODY_READ_ERROR = Symbol("proxy-guard:body-read-error");
|
|
299
|
+
async function readRawBody(c, maxBytes) {
|
|
300
|
+
const method = c.req.method;
|
|
301
|
+
if (method === "GET" || method === "HEAD") {
|
|
302
|
+
return void 0;
|
|
303
|
+
}
|
|
304
|
+
const contentType = c.req.header("content-type") || "";
|
|
305
|
+
if (contentType.includes("multipart/form-data")) {
|
|
306
|
+
return void 0;
|
|
307
|
+
}
|
|
308
|
+
const stream = c.req.raw.clone().body;
|
|
309
|
+
if (!stream) {
|
|
310
|
+
return void 0;
|
|
311
|
+
}
|
|
312
|
+
const reader = stream.getReader();
|
|
313
|
+
const chunks = [];
|
|
314
|
+
let total = 0;
|
|
315
|
+
try {
|
|
316
|
+
for (; ; ) {
|
|
317
|
+
const { done, value } = await reader.read();
|
|
318
|
+
if (done) {
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
total += value.byteLength;
|
|
322
|
+
if (maxBytes !== void 0 && total > maxBytes) {
|
|
323
|
+
void reader.cancel().catch(() => void 0);
|
|
324
|
+
return BODY_OVERSIZE;
|
|
325
|
+
}
|
|
326
|
+
chunks.push(value);
|
|
327
|
+
}
|
|
328
|
+
} catch {
|
|
329
|
+
guardLogger.debug("Request body read failed (client abort?)");
|
|
330
|
+
return BODY_READ_ERROR;
|
|
331
|
+
}
|
|
332
|
+
return Buffer.concat(chunks);
|
|
333
|
+
}
|
|
334
|
+
function isOriginAllowed(c, allowedOrigins) {
|
|
335
|
+
if (!allowedOrigins || allowedOrigins.length === 0) {
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
const origin = c.req.header("origin");
|
|
339
|
+
if (!origin) {
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
return allowedOrigins.includes(origin);
|
|
343
|
+
}
|
|
344
|
+
function createProxyGuard(config = {}) {
|
|
345
|
+
const mode = config.mode ?? "off";
|
|
346
|
+
const windowMs = config.windowMs ?? 3e4;
|
|
347
|
+
const allowedOrigins = config.allowedOrigins;
|
|
348
|
+
const nonceStore = config.nonceStore;
|
|
349
|
+
const skipPaths = new Set(config.skipPaths ?? []);
|
|
350
|
+
const maxBodyBytes = config.maxBodyBytes;
|
|
351
|
+
const activeRaw = config.secret ?? env.SPFN_PROXY_SECRET;
|
|
352
|
+
const previousRaw = config.previousSecrets ?? env.SPFN_PROXY_SECRET_PREVIOUS;
|
|
353
|
+
const keys = parseProxyKeySet([activeRaw, previousRaw]);
|
|
354
|
+
if (mode === "strict" && keys.length === 0) {
|
|
355
|
+
throw new Error(
|
|
356
|
+
'[proxy-guard] mode "strict" requires a proxy key but none is configured (SPFN_PROXY_SECRET is empty/unset). Refusing to start with the guard open \u2014 set the secret, or use mode "tag" / "off".'
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
if (activeRaw && previousRaw) {
|
|
360
|
+
const activeId = parseProxyKey(activeRaw).keyId;
|
|
361
|
+
const collides = previousRaw.split(",").some((p) => p.trim() && parseProxyKey(p.trim()).keyId === activeId);
|
|
362
|
+
if (collides) {
|
|
363
|
+
guardLogger.warn(
|
|
364
|
+
'Previous proxy key shares keyId with the active key (likely bare secrets without a "keyId:" prefix) \u2014 grace key ignored, rotation will drop in-flight requests. Use "<keyId>:<secret>" on both keys.',
|
|
365
|
+
{ keyId: activeId }
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return async (c, next) => {
|
|
370
|
+
if (mode === "off" || skipPaths.has(c.req.path)) {
|
|
371
|
+
return next();
|
|
372
|
+
}
|
|
373
|
+
if (c.req.method === "OPTIONS" && c.req.header("access-control-request-method")) {
|
|
374
|
+
c.set("clientType", "untrusted");
|
|
375
|
+
return next();
|
|
376
|
+
}
|
|
377
|
+
if (!isOriginAllowed(c, allowedOrigins)) {
|
|
378
|
+
return reject(c, mode, next, "origin-not-allowed");
|
|
379
|
+
}
|
|
380
|
+
if (keys.length === 0) {
|
|
381
|
+
c.set("clientType", "untrusted");
|
|
382
|
+
return next();
|
|
383
|
+
}
|
|
384
|
+
const signature = c.req.header(PROXY_SIGNATURE_HEADER);
|
|
385
|
+
const timestamp = c.req.header(PROXY_TIMESTAMP_HEADER);
|
|
386
|
+
const nonce = c.req.header(PROXY_NONCE_HEADER);
|
|
387
|
+
const keyId = c.req.header(PROXY_KEY_ID_HEADER);
|
|
388
|
+
if (!signature || !timestamp || !nonce || !keyId) {
|
|
389
|
+
return reject(c, mode, next, "missing-headers");
|
|
390
|
+
}
|
|
391
|
+
const body = await readRawBody(c, maxBodyBytes);
|
|
392
|
+
if (body === BODY_OVERSIZE) {
|
|
393
|
+
if (mode === "strict") {
|
|
394
|
+
return c.json({ error: "Payload Too Large" }, 413);
|
|
395
|
+
}
|
|
396
|
+
c.set("clientType", "untrusted");
|
|
397
|
+
return next();
|
|
398
|
+
}
|
|
399
|
+
if (body === BODY_READ_ERROR) {
|
|
400
|
+
return reject(c, mode, next, "body-read-error");
|
|
401
|
+
}
|
|
402
|
+
const url = new URL(c.req.url);
|
|
403
|
+
const result = verifyProxyRequest({
|
|
404
|
+
keys,
|
|
405
|
+
method: c.req.method,
|
|
406
|
+
path: url.pathname,
|
|
407
|
+
query: url.search,
|
|
408
|
+
body,
|
|
409
|
+
signature,
|
|
410
|
+
timestamp,
|
|
411
|
+
nonce,
|
|
412
|
+
keyId,
|
|
413
|
+
windowMs
|
|
414
|
+
});
|
|
415
|
+
if (!result.valid) {
|
|
416
|
+
return reject(c, mode, next, result.reason);
|
|
417
|
+
}
|
|
418
|
+
if (nonceStore && result.nonce) {
|
|
419
|
+
try {
|
|
420
|
+
const fresh = await nonceStore.checkAndSet(result.nonce, windowMs * 2);
|
|
421
|
+
if (!fresh) {
|
|
422
|
+
return reject(c, mode, next, "nonce-replay");
|
|
423
|
+
}
|
|
424
|
+
} catch (err) {
|
|
425
|
+
guardLogger.warn("Nonce store unavailable \u2014 falling back to timestamp window", {
|
|
426
|
+
error: err.message
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
c.set("clientType", "web");
|
|
431
|
+
return next();
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
function createCacheNonceStore(cache, prefix = "spfn:proxy-nonce:") {
|
|
435
|
+
return {
|
|
436
|
+
async checkAndSet(nonce, ttlMs) {
|
|
437
|
+
const res = await cache.set(`${prefix}${nonce}`, "1", "PX", Math.ceil(ttlMs), "NX");
|
|
438
|
+
return res === "OK";
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
function reject(c, mode, next, reason) {
|
|
443
|
+
if (mode === "strict") {
|
|
444
|
+
guardLogger.warn("Rejected unverified request", { reason, path: c.req.path, method: c.req.method });
|
|
445
|
+
return c.json({ error: "Forbidden", message: "Request origin could not be verified" }, 403);
|
|
446
|
+
}
|
|
447
|
+
guardLogger.debug("Unverified request (would reject in strict mode)", { reason, path: c.req.path });
|
|
448
|
+
c.set("clientType", "untrusted");
|
|
449
|
+
return next();
|
|
450
|
+
}
|
|
209
451
|
|
|
210
|
-
export { ErrorHandler, RequestLogger, maskSensitiveData };
|
|
452
|
+
export { ErrorHandler, RequestLogger, createCacheNonceStore, createProxyGuard, maskSensitiveData };
|
|
211
453
|
//# sourceMappingURL=index.js.map
|
|
212
454
|
//# sourceMappingURL=index.js.map
|