@spfn/core 0.2.0-beta.6 → 0.2.0-beta.66

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.
Files changed (73) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +183 -1281
  3. package/dist/authz/index.d.ts +34 -0
  4. package/dist/authz/index.js +415 -0
  5. package/dist/authz/index.js.map +1 -0
  6. package/dist/{boss-DI1r4kTS.d.ts → boss-gXhgctn6.d.ts} +40 -0
  7. package/dist/cache/index.js +42 -30
  8. package/dist/cache/index.js.map +1 -1
  9. package/dist/codegen/index.d.ts +55 -8
  10. package/dist/codegen/index.js +183 -7
  11. package/dist/codegen/index.js.map +1 -1
  12. package/dist/config/index.d.ts +585 -6
  13. package/dist/config/index.js +116 -5
  14. package/dist/config/index.js.map +1 -1
  15. package/dist/db/index.d.ts +379 -75
  16. package/dist/db/index.js +625 -113
  17. package/dist/db/index.js.map +1 -1
  18. package/dist/define-middleware-DuXD8Hvu.d.ts +167 -0
  19. package/dist/env/index.d.ts +26 -2
  20. package/dist/env/index.js +15 -5
  21. package/dist/env/index.js.map +1 -1
  22. package/dist/env/loader.d.ts +26 -19
  23. package/dist/env/loader.js +32 -25
  24. package/dist/env/loader.js.map +1 -1
  25. package/dist/errors/index.d.ts +10 -0
  26. package/dist/errors/index.js +20 -2
  27. package/dist/errors/index.js.map +1 -1
  28. package/dist/event/index.d.ts +33 -3
  29. package/dist/event/index.js +24 -3
  30. package/dist/event/index.js.map +1 -1
  31. package/dist/event/sse/client.d.ts +42 -3
  32. package/dist/event/sse/client.js +128 -45
  33. package/dist/event/sse/client.js.map +1 -1
  34. package/dist/event/sse/index.d.ts +12 -5
  35. package/dist/event/sse/index.js +271 -32
  36. package/dist/event/sse/index.js.map +1 -1
  37. package/dist/event/ws/client.d.ts +59 -0
  38. package/dist/event/ws/client.js +273 -0
  39. package/dist/event/ws/client.js.map +1 -0
  40. package/dist/event/ws/index.d.ts +94 -0
  41. package/dist/event/ws/index.js +272 -0
  42. package/dist/event/ws/index.js.map +1 -0
  43. package/dist/job/index.d.ts +2 -2
  44. package/dist/job/index.js +155 -42
  45. package/dist/job/index.js.map +1 -1
  46. package/dist/logger/index.d.ts +5 -0
  47. package/dist/logger/index.js +14 -0
  48. package/dist/logger/index.js.map +1 -1
  49. package/dist/middleware/index.d.ts +243 -2
  50. package/dist/middleware/index.js +1323 -13
  51. package/dist/middleware/index.js.map +1 -1
  52. package/dist/nextjs/index.d.ts +2 -2
  53. package/dist/nextjs/index.js +77 -31
  54. package/dist/nextjs/index.js.map +1 -1
  55. package/dist/nextjs/server.d.ts +53 -23
  56. package/dist/nextjs/server.js +197 -66
  57. package/dist/nextjs/server.js.map +1 -1
  58. package/dist/route/index.d.ts +138 -146
  59. package/dist/route/index.js +238 -22
  60. package/dist/route/index.js.map +1 -1
  61. package/dist/security/index.d.ts +83 -0
  62. package/dist/security/index.js +173 -0
  63. package/dist/security/index.js.map +1 -0
  64. package/dist/server/index.d.ts +458 -17
  65. package/dist/server/index.js +1756 -277
  66. package/dist/server/index.js.map +1 -1
  67. package/dist/{router-Di7ENoah.d.ts → token-manager-jKD_EsSE.d.ts} +121 -1
  68. package/dist/{types-D_N_U-Py.d.ts → types-7Mhoxnnt.d.ts} +21 -1
  69. package/dist/types-BFB72jbM.d.ts +282 -0
  70. package/dist/types-DVjf37yO.d.ts +205 -0
  71. package/docs/file-upload.md +717 -0
  72. package/package.json +237 -208
  73. package/dist/types-B-e_f2dQ.d.ts +0 -121
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/security/safe-fetch.ts"],"names":["dnsPromises","undiciFetch"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @spfn/core - SSRF-safe outbound fetch\n *\n * A drop-in `fetch` for calling URLs that may be influenced by user input\n * (webhooks, image fetchers, callback URLs). It blocks requests to private and\n * reserved IP ranges — including the cloud metadata address (169.254.169.254) —\n * and defeats DNS rebinding by resolving the hostname, validating every returned\n * address, and pinning the connection to a validated IP via a custom undici\n * `lookup`. Redirects go through the same dispatcher, so each hop is re-validated.\n *\n * A plain string allowlist cannot stop SSRF on its own: an attacker-controlled\n * hostname can resolve to a private IP (DNS rebinding). The pinning is the point.\n */\n\nimport { promises as dnsPromises } from 'node:dns';\nimport { isIP, BlockList, type LookupFunction } from 'node:net';\nimport { fetch as undiciFetch, Agent } from 'undici';\n\n/** Thrown when a request target is blocked by the SSRF policy. */\nexport class SsrfBlockedError extends Error\n{\n constructor(message: string)\n {\n super(message);\n this.name = 'SsrfBlockedError';\n }\n}\n\nexport interface SafeFetchPolicy\n{\n /** URL schemes allowed. @default ['http:', 'https:'] */\n allowedProtocols?: string[];\n\n /**\n * Reject targets that resolve to a private or reserved IP range (loopback,\n * link-local/metadata, RFC1918, ULA, multicast, …). @default true\n */\n blockPrivateIps?: boolean;\n\n /**\n * Exact hostname allowlist (case-insensitive). When set, only these hosts\n * are reachable — the strongest control for a known set of upstreams.\n * Enforced on every redirect hop, not just the first URL.\n */\n allowHosts?: string[];\n\n /** Max redirects to follow, each re-validated. @default 5 */\n maxRedirects?: number;\n}\n\nconst DEFAULT_MAX_REDIRECTS = 5;\n\nconst DEFAULTS: Required<Pick<SafeFetchPolicy, 'allowedProtocols' | 'blockPrivateIps'>> = {\n allowedProtocols: ['http:', 'https:'],\n blockPrivateIps: true,\n};\n\n/**\n * Private/reserved IP ranges. Built once. `BlockList` handles CIDR membership\n * for both families; IPv4-mapped IPv6 is unwrapped and checked as IPv4 so a\n * mapped public address still resolves.\n */\nconst blockedRanges = (() =>\n{\n const list = new BlockList();\n\n // IPv4 — RFC 1918 + special-purpose / reserved\n list.addSubnet('0.0.0.0', 8, 'ipv4');\n list.addSubnet('10.0.0.0', 8, 'ipv4');\n list.addSubnet('100.64.0.0', 10, 'ipv4'); // CGNAT\n list.addSubnet('127.0.0.0', 8, 'ipv4'); // loopback\n list.addSubnet('169.254.0.0', 16, 'ipv4'); // link-local incl. cloud metadata\n list.addSubnet('172.16.0.0', 12, 'ipv4');\n list.addSubnet('192.0.0.0', 24, 'ipv4');\n list.addSubnet('192.0.2.0', 24, 'ipv4'); // TEST-NET-1\n list.addSubnet('192.168.0.0', 16, 'ipv4');\n list.addSubnet('198.18.0.0', 15, 'ipv4'); // benchmarking\n list.addSubnet('198.51.100.0', 24, 'ipv4'); // TEST-NET-2\n list.addSubnet('203.0.113.0', 24, 'ipv4'); // TEST-NET-3\n list.addSubnet('224.0.0.0', 4, 'ipv4'); // multicast\n list.addSubnet('240.0.0.0', 4, 'ipv4'); // reserved + broadcast\n\n // IPv6\n list.addAddress('::1', 'ipv6'); // loopback\n list.addAddress('::', 'ipv6'); // unspecified\n list.addSubnet('fc00::', 7, 'ipv6'); // unique local\n list.addSubnet('fe80::', 10, 'ipv6'); // link-local\n list.addSubnet('ff00::', 8, 'ipv6'); // multicast\n list.addSubnet('2001:db8::', 32, 'ipv6'); // documentation\n list.addSubnet('64:ff9b::', 96, 'ipv6'); // NAT64 well-known prefix (can wrap private v4)\n list.addSubnet('2002::', 16, 'ipv6'); // 6to4 (can wrap private v4)\n list.addSubnet('192.88.99.0', 24, 'ipv4'); // 6to4 anycast relay\n\n return list;\n})();\n\n/**\n * Whether an IP literal falls in a private or reserved range. A non-IP input is\n * treated as unsafe (`true`) — callers pass resolved addresses, never hostnames.\n */\nexport function isPrivateOrReservedIp(ip: string): boolean\n{\n const family = isIP(ip);\n\n if (family === 4)\n {\n return blockedRanges.check(ip, 'ipv4');\n }\n\n if (family === 6)\n {\n // IPv4-mapped (::ffff:a.b.c.d): judge by the embedded IPv4.\n const mapped = /^::ffff:(\\d{1,3}(?:\\.\\d{1,3}){3})$/i.exec(ip);\n if (mapped)\n {\n return blockedRanges.check(mapped[1], 'ipv4');\n }\n\n return blockedRanges.check(ip, 'ipv6');\n }\n\n return true;\n}\n\nfunction stripBrackets(hostname: string): string\n{\n return hostname.replace(/^\\[/, '').replace(/\\]$/, '');\n}\n\n/**\n * Synchronous, no-DNS checks: protocol, host allowlist, and — when the host is\n * an IP literal — the private-range check. Throws SsrfBlockedError on violation.\n */\nfunction assertUrlAllowed(rawUrl: string, policy: SafeFetchPolicy): URL\n{\n let url: URL;\n try\n {\n url = new URL(rawUrl);\n }\n catch\n {\n throw new SsrfBlockedError(`Invalid URL: ${rawUrl}`);\n }\n\n const protocols = policy.allowedProtocols ?? DEFAULTS.allowedProtocols;\n if (!protocols.includes(url.protocol))\n {\n throw new SsrfBlockedError(`Protocol not allowed: ${url.protocol}`);\n }\n\n const host = stripBrackets(url.hostname);\n\n if (policy.allowHosts)\n {\n const allowed = policy.allowHosts.some(h => h.toLowerCase() === host.toLowerCase());\n if (!allowed)\n {\n throw new SsrfBlockedError(`Host not in allowlist: ${host}`);\n }\n }\n\n if (policy.blockPrivateIps !== false && isIP(host) && isPrivateOrReservedIp(host))\n {\n throw new SsrfBlockedError(`Blocked address: ${host}`);\n }\n\n return url;\n}\n\n/**\n * Validate a URL for SSRF without making the request: runs the sync checks and,\n * for hostnames, resolves DNS and rejects if any address is private/reserved.\n *\n * Use this to guard a URL handed to code you do not control (so you cannot pin\n * the connection). It cannot prevent rebinding between this check and that\n * code's own connection — for requests you make yourself, use {@link safeFetch}.\n */\nexport async function assertSafeUrl(rawUrl: string, policy?: SafeFetchPolicy): Promise<void>\n{\n const merged = { ...getDefaultSafeFetchPolicy(), ...policy };\n const url = assertUrlAllowed(rawUrl, merged);\n const host = stripBrackets(url.hostname);\n\n // IP literals are fully judged by assertUrlAllowed; only hostnames need DNS.\n if (isIP(host) || merged.blockPrivateIps === false)\n {\n return;\n }\n\n const addresses = await dnsPromises.lookup(host, { all: true });\n for (const { address } of addresses)\n {\n if (isPrivateOrReservedIp(address))\n {\n throw new SsrfBlockedError(`Host resolves to a blocked address: ${host} → ${address}`);\n }\n }\n}\n\n/**\n * Custom DNS lookup for undici: resolve, drop private/reserved addresses, and\n * return only validated ones — so the connection is pinned to an address we\n * already checked (no rebinding window between check and connect).\n */\nfunction pinnedLookup(policy: SafeFetchPolicy): LookupFunction\n{\n return (hostname, options, callback) =>\n {\n const family = typeof options === 'object' && typeof options.family === 'number' ? options.family : 0;\n const wantsAll = typeof options === 'object' && options.all === true;\n\n dnsPromises.lookup(hostname, { all: true, verbatim: true, family }).then(\n (addresses) =>\n {\n const safe = policy.blockPrivateIps === false\n ? addresses\n : addresses.filter(a => !isPrivateOrReservedIp(a.address));\n\n if (safe.length === 0)\n {\n callback(new SsrfBlockedError(`Host resolves only to blocked addresses: ${hostname}`), '', 0);\n\n return;\n }\n\n if (wantsAll)\n {\n (callback as unknown as (err: null, addresses: typeof safe) => void)(null, safe);\n\n return;\n }\n\n callback(null, safe[0].address, safe[0].family);\n },\n (err: NodeJS.ErrnoException) => callback(err, '', 0),\n );\n };\n}\n\ntype FetchInput = Parameters<typeof undiciFetch>[0];\n\ntype FetchInit = Parameters<typeof undiciFetch>[1];\n\ntype FetchReturn = ReturnType<typeof undiciFetch>;\n\nfunction urlOf(input: FetchInput): string\n{\n if (typeof input === 'string')\n {\n return input;\n }\n if (input instanceof URL)\n {\n return input.href;\n }\n\n return (input as { url: string }).url;\n}\n\ntype SafeFetchFn = ((input: FetchInput, init?: FetchInit) => FetchReturn) & { _dispatcher: Agent };\n\n/**\n * Build an SSRF-safe fetch bound to a policy. Reuse the returned function (it\n * owns a pooled dispatcher) rather than calling this per request.\n *\n * Redirects are followed manually so EVERY hop is validated — including a hop\n * whose target is a bare IP literal, which undici would otherwise connect to\n * directly without invoking the pinning lookup. The original method/body and\n * headers are NOT replayed across a redirect: the next hop may be attacker-\n * chosen, so forwarding the payload or auth headers would leak them.\n */\nexport function createSafeFetch(policy: SafeFetchPolicy = {}): SafeFetchFn\n{\n const merged = { ...DEFAULTS, ...policy };\n const maxRedirects = merged.maxRedirects ?? DEFAULT_MAX_REDIRECTS;\n const dispatcher = new Agent({ connect: { lookup: pinnedLookup(merged) } });\n\n const run = async (input: FetchInput, init?: FetchInit): Promise<Awaited<FetchReturn>> =>\n {\n let url = urlOf(input);\n let hopInit: FetchInit = init;\n\n for (let hop = 0; ; hop++)\n {\n assertUrlAllowed(url, merged);\n\n const res = await undiciFetch(url, { ...hopInit, dispatcher, redirect: 'manual' });\n\n const location = res.status >= 300 && res.status < 400 ? res.headers.get('location') : null;\n if (!location)\n {\n return res;\n }\n\n await res.body?.cancel().catch(() => \n {});\n\n if (hop >= maxRedirects)\n {\n throw new SsrfBlockedError(`Too many redirects (> ${maxRedirects})`);\n }\n\n url = new URL(location, url).href;\n hopInit = { method: 'GET' };\n }\n };\n\n const fn = ((input: FetchInput, init?: FetchInit) => run(input, init)) as SafeFetchFn;\n fn._dispatcher = dispatcher;\n\n return fn;\n}\n\n// ---------------------------------------------------------------------------\n// Default policy registry — set once at server boot, read by safeFetch().\n// ---------------------------------------------------------------------------\n\nlet configuredPolicy: SafeFetchPolicy = {};\nlet cachedDefaultFetch: SafeFetchFn | undefined;\n\n/**\n * Replace the default policy used by {@link safeFetch}. Called by the server at\n * boot from `defineServerConfig().outboundFetch(...)`. Passing undefined resets\n * to the secure defaults (private IPs blocked, http/https only).\n */\nexport function setDefaultSafeFetchPolicy(policy?: SafeFetchPolicy): void\n{\n // Close the previous dispatcher's connection pool so repeated boots\n // (tests, multi-app processes) don't leak Agents.\n cachedDefaultFetch?._dispatcher.close().catch(() => \n {});\n configuredPolicy = policy ?? {};\n cachedDefaultFetch = undefined;\n}\n\n/** The effective default policy (secure defaults merged with any configured overrides). */\nexport function getDefaultSafeFetchPolicy(): SafeFetchPolicy\n{\n return { ...DEFAULTS, ...configuredPolicy };\n}\n\n/**\n * SSRF-safe `fetch` using the configured default policy. Drop-in replacement for\n * `fetch` when the URL may be influenced by user input.\n */\nexport function safeFetch(input: FetchInput, init?: FetchInit): FetchReturn\n{\n cachedDefaultFetch ??= createSafeFetch(getDefaultSafeFetchPolicy());\n\n return cachedDefaultFetch(input, init);\n}\n"]}
@@ -1,26 +1,44 @@
1
+ export { loadEnv } from '../env/loader.js';
1
2
  import { MiddlewareHandler, Hono } from 'hono';
2
3
  import { cors } from 'hono/cors';
3
4
  import { serve } from '@hono/node-server';
4
5
  import { NamedMiddleware, Router } from '@spfn/core/route';
5
- import { J as JobRouter, B as BossOptions } from '../boss-DI1r4kTS.js';
6
- import { E as EventRouterDef } from '../router-Di7ENoah.js';
7
- import { S as SSEHandlerConfig } from '../types-B-e_f2dQ.js';
6
+ import { OnErrorContext, ProxyGuardConfig, RateLimitOptions } from '@spfn/core/middleware';
7
+ import { SafeFetchPolicy } from '@spfn/core/security';
8
+ import { J as JobRouter, B as BossOptions } from '../boss-gXhgctn6.js';
9
+ import { E as EventRouterDef, a as EventDef } from '../token-manager-jKD_EsSE.js';
10
+ import { S as SSEHandlerConfig, a as SSEAuthConfig } from '../types-BFB72jbM.js';
11
+ import { W as WSRouterDef, a as WSHandlerConfig, b as WSMessageHandlers, c as WSAuthConfig } from '../types-DVjf37yO.js';
12
+ import { DatabaseProvider } from '@spfn/core/db';
8
13
  import '@sinclair/typebox';
9
14
  import 'pg-boss';
10
15
 
11
16
  /**
12
- * Load environment files for SPFN server
13
- *
14
- * Priority (high → low, later files don't override):
15
- * 1. .env.server.local - Server-only secrets (gitignored)
16
- * 2. .env.server - Server-only defaults
17
- * 3. .env.{NODE_ENV}.local
18
- * 4. .env.local - Local overrides (gitignored)
19
- * 5. .env.{NODE_ENV}
20
- * 6. .env - Defaults
17
+ * @deprecated Use `loadEnv` from '@spfn/core/env/loader' instead.
18
+ * This module will be removed in the next major version.
19
+ */
20
+ /**
21
+ * @deprecated Use `loadEnv()` from '@spfn/core/env/loader' instead.
21
22
  */
22
23
  declare function loadEnvFiles(): void;
23
24
 
25
+ /**
26
+ * Workflow router interface for @spfn/core integration
27
+ *
28
+ * This is a minimal interface that avoids circular dependency with @spfn/workflow.
29
+ * The actual WorkflowRouter from @spfn/workflow implements this interface.
30
+ */
31
+ interface WorkflowRouterLike {
32
+ /**
33
+ * Initialize the workflow engine
34
+ * Called by server during infrastructure initialization
35
+ *
36
+ * @internal
37
+ */
38
+ _init: (db: any, options?: {
39
+ largeOutputThreshold?: number;
40
+ }) => void;
41
+ }
24
42
  /**
25
43
  * CORS configuration options - inferred from hono/cors
26
44
  */
@@ -60,11 +78,86 @@ interface ServerConfig {
60
78
  * Error handler (default: true)
61
79
  */
62
80
  errorHandler?: boolean;
81
+ /**
82
+ * Callback invoked when an error occurs (passed to ErrorHandler)
83
+ *
84
+ * Called asynchronously without blocking the response.
85
+ *
86
+ * @example
87
+ * ```typescript
88
+ * import { createErrorSlackNotifier } from '@spfn/notification/server';
89
+ *
90
+ * middleware: {
91
+ * onError: createErrorSlackNotifier({ minStatusCode: 500 }),
92
+ * }
93
+ * ```
94
+ */
95
+ onError?: (err: Error, context: OnErrorContext) => Promise<void> | void;
63
96
  };
64
97
  /**
65
98
  * Additional custom middleware
66
99
  */
67
100
  use?: MiddlewareHandler[];
101
+ /**
102
+ * Proxy-guard: verify requests came through the trusted Next.js RPC proxy
103
+ * (HMAC signature) and/or an allowed browser origin, then tag `clientType`.
104
+ * Lets the backend reject direct-to-backend calls that bypass the proxy.
105
+ *
106
+ * Disabled by default (`mode: 'off'`). Requires the same `SPFN_PROXY_SECRET`
107
+ * on the proxy and the backend. See PROXY-BACKEND-AUTH-SPEC.md.
108
+ *
109
+ * @example
110
+ * ```typescript
111
+ * .proxyGuard({ mode: 'strict', allowedOrigins: ['https://app.example.com'] })
112
+ * ```
113
+ */
114
+ proxyGuard?: Omit<ProxyGuardConfig, 'nonceStore'> & {
115
+ /**
116
+ * Enable hard replay rejection via a Redis nonce store. Evaluated in BOTH modes
117
+ * (tag observes replays, strict rejects). Requires a cache (CACHE_URL); without
118
+ * one, falls back to the timestamp window. Degrades to the window if the store
119
+ * is briefly unavailable. @default false
120
+ */
121
+ nonce?: boolean;
122
+ };
123
+ /**
124
+ * Rate limiting: an optional global default limiter plus named policies.
125
+ *
126
+ * `mode: 'on'` applies `default` to every named-middleware route (opt out
127
+ * with `.skip(['rateLimit'])`); `policies` lets packages tag sensitive routes
128
+ * via `rateLimitPolicy(name, fallback)` while this app tunes the numbers in
129
+ * one place. Backed by the shared cache (CACHE_URL); without a cache it fails
130
+ * open unless `default.failClosed` is set. Disabled by default (`mode: 'off'`).
131
+ *
132
+ * @example
133
+ * ```typescript
134
+ * .rateLimit({
135
+ * mode: 'on',
136
+ * default: { limit: 100, windowMs: 60_000 },
137
+ * policies: { 'auth-login': { limit: 5, windowMs: 60_000 } },
138
+ * })
139
+ * ```
140
+ */
141
+ rateLimit?: {
142
+ /** 'on' applies the default limiter to every route. @default 'off' */
143
+ mode?: 'off' | 'on';
144
+ /** Default policy applied to all routes when `mode` is 'on'. */
145
+ default?: RateLimitOptions;
146
+ /** Named policies referenced by `rateLimitPolicy(name, fallback)` tags. */
147
+ policies?: Record<string, RateLimitOptions>;
148
+ };
149
+ /**
150
+ * SSRF policy for outbound requests made via `safeFetch` (`@spfn/core/security`).
151
+ * Sets the process-wide default used by webhook/callback senders. Private and
152
+ * reserved IPs are blocked by default; set `allowHosts` to restrict to a known
153
+ * set of upstreams, or `blockPrivateIps: false` for trusted internal calls.
154
+ *
155
+ * @example
156
+ * ```typescript
157
+ * .outboundFetch({ allowHosts: ['hooks.slack.com'] })
158
+ * ```
159
+ */
160
+ outboundFetch?: SafeFetchPolicy;
68
161
  /**
69
162
  * Global middlewares with names for route-level skip control
70
163
  * Use defineMiddleware() for type-safe middleware definitions
@@ -163,6 +256,30 @@ interface ServerConfig {
163
256
  */
164
257
  path?: string;
165
258
  };
259
+ /**
260
+ * WebSocket router for bidirectional real-time communication
261
+ *
262
+ * @example
263
+ * ```typescript
264
+ * import { defineWSRouter } from '@spfn/core/event/ws';
265
+ *
266
+ * export default defineServerConfig()
267
+ * .websockets(wsRouter) // → WS /ws
268
+ * .build();
269
+ * ```
270
+ */
271
+ websockets?: WSRouterDef<any, any>;
272
+ /**
273
+ * WebSocket configuration options
274
+ * Only used if websockets router is provided
275
+ */
276
+ websocketsConfig?: WSHandlerConfig & {
277
+ /**
278
+ * WebSocket endpoint path
279
+ * @default '/ws'
280
+ */
281
+ path?: string;
282
+ };
166
283
  /**
167
284
  * Enable debug mode (default: NODE_ENV === 'development')
168
285
  */
@@ -171,6 +288,13 @@ interface ServerConfig {
171
288
  * Database configuration
172
289
  */
173
290
  database?: {
291
+ /**
292
+ * Externally owned PostgreSQL Drizzle provider.
293
+ *
294
+ * When supplied, SPFN skips DATABASE_URL/postgres.js initialization
295
+ * and closes the provider during graceful shutdown.
296
+ */
297
+ provider?: DatabaseProvider;
174
298
  /**
175
299
  * Connection pool configuration
176
300
  * Overrides environment variables and defaults
@@ -282,6 +406,34 @@ interface ServerConfig {
282
406
  */
283
407
  headers?: number;
284
408
  };
409
+ /**
410
+ * Fetch (outbound HTTP) timeout configuration
411
+ * Controls Node.js undici global dispatcher timeouts for fetch() calls
412
+ * Applies to all outbound HTTP requests made via fetch() in this process
413
+ */
414
+ fetchTimeout?: {
415
+ /**
416
+ * TCP connection timeout in milliseconds
417
+ * Time to establish socket connection to upstream server
418
+ * @default 10000 (10 seconds)
419
+ * @env FETCH_CONNECT_TIMEOUT
420
+ */
421
+ connect?: number;
422
+ /**
423
+ * Response headers timeout in milliseconds
424
+ * Time to receive complete response headers after request sent
425
+ * @default 300000 (5 minutes)
426
+ * @env FETCH_HEADERS_TIMEOUT
427
+ */
428
+ headers?: number;
429
+ /**
430
+ * Body data timeout in milliseconds
431
+ * Maximum time between body data chunks from upstream server
432
+ * @default 300000 (5 minutes)
433
+ * @env FETCH_BODY_TIMEOUT
434
+ */
435
+ body?: number;
436
+ };
285
437
  /**
286
438
  * Graceful shutdown configuration
287
439
  * Controls server shutdown behavior during SIGTERM/SIGINT signals
@@ -289,9 +441,13 @@ interface ServerConfig {
289
441
  shutdown?: {
290
442
  /**
291
443
  * Graceful shutdown timeout in milliseconds
292
- * Maximum time to wait for ongoing requests and resource cleanup
293
- * After timeout, forces process termination
294
- * @default 30000 (30 seconds)
444
+ * Maximum time to wait for in-flight operations to drain and resource cleanup
445
+ * After timeout, forces process.exit() before k8s SIGKILL
446
+ *
447
+ * Formula: terminationGracePeriodSeconds - preStopSleep - safetyMargin
448
+ * Default: 300s - 5s - 15s = 280s
449
+ *
450
+ * @default 280000 (280 seconds)
295
451
  * @env SHUTDOWN_TIMEOUT
296
452
  */
297
453
  timeout?: number;
@@ -338,6 +494,39 @@ interface ServerConfig {
338
494
  */
339
495
  redis?: boolean;
340
496
  };
497
+ /**
498
+ * Workflow router for workflow orchestration
499
+ *
500
+ * Automatically initializes the workflow engine after database is ready.
501
+ * Workflows are defined using @spfn/workflow package.
502
+ *
503
+ * @example
504
+ * ```typescript
505
+ * import { defineWorkflowRouter } from '@spfn/workflow';
506
+ *
507
+ * const workflowRouter = defineWorkflowRouter([
508
+ * provisionTenant,
509
+ * deprovisionTenant,
510
+ * ]);
511
+ *
512
+ * export default defineServerConfig()
513
+ * .workflows(workflowRouter)
514
+ * .build();
515
+ * ```
516
+ */
517
+ workflows?: WorkflowRouterLike;
518
+ /**
519
+ * Workflow engine configuration
520
+ * Only used if workflows router is provided
521
+ */
522
+ workflowsConfig?: {
523
+ /**
524
+ * Large output threshold in bytes
525
+ * Outputs larger than this will be stored in external storage
526
+ * @default 1024 * 1024 (1MB)
527
+ */
528
+ largeOutputThreshold?: number;
529
+ };
341
530
  /**
342
531
  * Server lifecycle hooks for custom infrastructure setup and management
343
532
  * Allows initialization of custom services and resources at different stages
@@ -517,6 +706,177 @@ declare function createServer(config?: ServerConfig): Promise<Hono>;
517
706
  */
518
707
  declare function startServer(config?: ServerConfig): Promise<ServerInstance>;
519
708
 
709
+ /**
710
+ * Shutdown Manager
711
+ *
712
+ * Manages graceful shutdown with drain behavior.
713
+ * All tracked operations must complete before shutdown proceeds.
714
+ *
715
+ * Features:
716
+ * - Hook registry: Multiple modules can register independent cleanup handlers
717
+ * - Operation tracking: Long-running tasks are awaited during shutdown (drain)
718
+ * - State management: isShuttingDown() for rejecting new work
719
+ */
720
+ interface ShutdownHookOptions {
721
+ /**
722
+ * Timeout for this hook in milliseconds
723
+ * If the hook exceeds this time, it is skipped and the next hook runs
724
+ * @default 10000 (10s)
725
+ */
726
+ timeout?: number;
727
+ /**
728
+ * Execution order (lower runs first)
729
+ * @default 100
730
+ */
731
+ order?: number;
732
+ }
733
+ declare class ShutdownManager {
734
+ private state;
735
+ private hooks;
736
+ private operations;
737
+ private operationCounter;
738
+ /**
739
+ * Register a shutdown hook
740
+ *
741
+ * Hooks run in order during shutdown, after all tracked operations drain.
742
+ * Each hook has its own timeout — failure does not block subsequent hooks.
743
+ *
744
+ * @example
745
+ * shutdown.onShutdown('ai-service', async () => {
746
+ * await aiService.cancelPending();
747
+ * }, { timeout: 30000, order: 10 });
748
+ */
749
+ onShutdown(name: string, handler: () => Promise<void>, options?: ShutdownHookOptions): void;
750
+ /**
751
+ * Track a long-running operation
752
+ *
753
+ * During shutdown (drain phase), the process waits for ALL tracked
754
+ * operations to complete before proceeding with cleanup.
755
+ *
756
+ * If shutdown has already started, the operation is rejected immediately.
757
+ *
758
+ * @returns The operation result (pass-through)
759
+ *
760
+ * @example
761
+ * const result = await shutdown.trackOperation(
762
+ * 'ai-generate',
763
+ * aiService.generate(prompt)
764
+ * );
765
+ */
766
+ trackOperation<T>(name: string, operation: Promise<T>): Promise<T>;
767
+ /**
768
+ * Whether the server is shutting down
769
+ *
770
+ * Use this to reject new work early (e.g., return 503 in route handlers).
771
+ */
772
+ isShuttingDown(): boolean;
773
+ /**
774
+ * Number of currently active tracked operations
775
+ */
776
+ getActiveOperationCount(): number;
777
+ /**
778
+ * Mark shutdown as started immediately
779
+ *
780
+ * Call this at the very beginning of the shutdown sequence so that:
781
+ * - Health check returns 503 right away
782
+ * - trackOperation() rejects new work
783
+ * - isShuttingDown() returns true
784
+ */
785
+ beginShutdown(): void;
786
+ /**
787
+ * Execute the full shutdown sequence
788
+ *
789
+ * 1. State → draining (reject new operations)
790
+ * 2. Wait for all tracked operations to complete (drain)
791
+ * 3. Run shutdown hooks in order
792
+ * 4. State → closed
793
+ *
794
+ * @param drainTimeout - Max time to wait for operations to drain (ms)
795
+ */
796
+ execute(drainTimeout: number): Promise<void>;
797
+ /**
798
+ * Wait for all tracked operations to complete, up to drainTimeout
799
+ */
800
+ private drain;
801
+ /**
802
+ * Execute registered shutdown hooks in order
803
+ */
804
+ private executeHooks;
805
+ }
806
+ /**
807
+ * Get the global ShutdownManager instance
808
+ *
809
+ * Available after server starts. Use this to register shutdown hooks
810
+ * or track long-running operations.
811
+ *
812
+ * @example
813
+ * import { getShutdownManager } from '@spfn/core/server';
814
+ *
815
+ * const shutdown = getShutdownManager();
816
+ *
817
+ * // Register cleanup
818
+ * shutdown.onShutdown('my-service', async () => {
819
+ * await myService.close();
820
+ * });
821
+ *
822
+ * // Track long operation
823
+ * await shutdown.trackOperation('ai-task', longRunningPromise);
824
+ */
825
+ declare function getShutdownManager(): ShutdownManager;
826
+
827
+ /**
828
+ * Serverless target for SPFN.
829
+ *
830
+ * Produces a listen-free, initialized Hono app for serverless platforms (Vercel,
831
+ * AWS Lambda, Cloudflare) — wrap the result with a hono platform adapter, e.g.
832
+ * `handle(app)` from `hono/vercel`.
833
+ *
834
+ * Unlike {@link startServer} (which serve()s a long-lived process and is the
835
+ * always-on / container path), this:
836
+ * - initializes the database in-handler (startServer welds DB init to serve()),
837
+ * - runs at most once per warm container (memoized),
838
+ * - disables the periodic DB health-check (pointless — and a timer leak — on
839
+ * frozen invocations),
840
+ * - does NOT start the in-process pg-boss worker (it cannot run on a serverless
841
+ * platform); enqueue still works, but nothing drains the queue here,
842
+ * - does NOT run seed/RBAC provisioning per cold start — that moves to a
843
+ * deploy-time step, see {@link provisionInfrastructure}.
844
+ *
845
+ * `startServer()` / `spfn start` (the always-on path) is unchanged by this module.
846
+ */
847
+
848
+ /**
849
+ * Build — once per warm container — the initialized, listen-free Hono app for a
850
+ * serverless platform. Wrap the result with the platform adapter:
851
+ *
852
+ * ```ts
853
+ * import { handle } from 'hono/vercel';
854
+ * import { createServerlessApp } from '@spfn/core/server';
855
+ * import serverConfig from '@/server/server.config';
856
+ *
857
+ * const handler = async (req: Request) => handle(await createServerlessApp(serverConfig))(req);
858
+ * export const GET = handler;
859
+ * export const POST = handler;
860
+ * ```
861
+ */
862
+ declare function createServerlessApp(config?: ServerConfig): Promise<Hono>;
863
+ /**
864
+ * Reset the memoized serverless app. Tests only.
865
+ */
866
+ declare function resetServerlessApp(): void;
867
+ /**
868
+ * Deploy-time provisioning — run ONCE per deploy, not per request.
869
+ *
870
+ * Initializes the database and runs the config's provisioning lifecycle hooks
871
+ * (`beforeInfrastructure` / `afterInfrastructure`, e.g. admin seeding + RBAC init).
872
+ * Intended for a build/deploy step (`spfn provision`), keeping per-cold-start work
873
+ * out of the serverless handler. On always-on targets this is equally useful: it
874
+ * avoids re-seeding on every pod restart / replica.
875
+ *
876
+ * Does NOT start the HTTP server, jobs worker, or health-check.
877
+ */
878
+ declare function provisionInfrastructure(config?: ServerConfig): Promise<void>;
879
+
520
880
  /**
521
881
  * Server Config Builder
522
882
  *
@@ -550,6 +910,34 @@ declare class ServerConfigBuilder {
550
910
  * Add named middlewares for route-level skip control
551
911
  */
552
912
  middlewares(middlewares: ServerConfig['middlewares']): this;
913
+ /**
914
+ * Configure proxy-guard (verify trusted-proxy signature + origin → clientType)
915
+ */
916
+ proxyGuard(proxyGuard: ServerConfig['proxyGuard']): this;
917
+ /**
918
+ * Configure rate limiting: an optional global default limiter plus the named
919
+ * policies that `rateLimitPolicy(name, fallback)` tags resolve against.
920
+ *
921
+ * @example
922
+ * ```typescript
923
+ * .rateLimit({
924
+ * mode: 'on',
925
+ * default: { limit: 100, windowMs: 60_000 },
926
+ * policies: { 'auth-login': { limit: 5, windowMs: 60_000 } },
927
+ * })
928
+ * ```
929
+ */
930
+ rateLimit(rateLimit: ServerConfig['rateLimit']): this;
931
+ /**
932
+ * Configure the SSRF policy for outbound `safeFetch` calls (webhooks,
933
+ * callbacks). Private/reserved IPs are blocked by default.
934
+ *
935
+ * @example
936
+ * ```typescript
937
+ * .outboundFetch({ allowHosts: ['hooks.slack.com'] })
938
+ * ```
939
+ */
940
+ outboundFetch(outboundFetch: ServerConfig['outboundFetch']): this;
553
941
  /**
554
942
  * Register define-route based router
555
943
  *
@@ -619,8 +1007,40 @@ declare class ServerConfigBuilder {
619
1007
  * .events(eventRouter, { path: '/sse' })
620
1008
  * ```
621
1009
  */
622
- events(router: EventRouterDef<any>, config?: SSEHandlerConfig & {
1010
+ events<TRouter extends EventRouterDef<any>>(router: TRouter, config?: Omit<SSEHandlerConfig, 'auth'> & {
1011
+ path?: string;
1012
+ auth?: SSEAuthConfig<TRouter>;
1013
+ }): this;
1014
+ /**
1015
+ * Register WebSocket router for bidirectional real-time communication
1016
+ *
1017
+ * Enables type-safe WebSocket connections with:
1018
+ * - Server→client event push (via defineEvent + emit)
1019
+ * - Client→server message handling (via messages in defineWSRouter)
1020
+ *
1021
+ * @example
1022
+ * ```typescript
1023
+ * // src/server/ws.ts
1024
+ * export const wsRouter = defineWSRouter({
1025
+ * events: { userUpdated, notification },
1026
+ * messages: {
1027
+ * ping: ({ ws }) => ws.send('pong', {}),
1028
+ * },
1029
+ * });
1030
+ *
1031
+ * // server.config.ts
1032
+ * export default defineServerConfig()
1033
+ * .websockets(wsRouter) // → WS /ws
1034
+ * .websockets(wsRouter, {
1035
+ * path: '/realtime', // custom path
1036
+ * auth: { enabled: true }, // token authentication
1037
+ * })
1038
+ * .build();
1039
+ * ```
1040
+ */
1041
+ websockets<TEvents extends Record<string, EventDef<any>>, TMessages extends WSMessageHandlers>(router: WSRouterDef<TEvents, TMessages>, config?: Omit<WSHandlerConfig, 'auth'> & {
623
1042
  path?: string;
1043
+ auth?: WSAuthConfig<WSRouterDef<TEvents, TMessages>>;
624
1044
  }): this;
625
1045
  /**
626
1046
  * Enable/disable debug mode
@@ -646,6 +1066,27 @@ declare class ServerConfigBuilder {
646
1066
  * Configure infrastructure initialization
647
1067
  */
648
1068
  infrastructure(infrastructure: ServerConfig['infrastructure']): this;
1069
+ /**
1070
+ * Register workflow router for workflow orchestration
1071
+ *
1072
+ * Automatically initializes the workflow engine after database is ready.
1073
+ *
1074
+ * @example
1075
+ * ```typescript
1076
+ * import { defineWorkflowRouter } from '@spfn/workflow';
1077
+ *
1078
+ * const workflowRouter = defineWorkflowRouter([
1079
+ * provisionTenant,
1080
+ * deprovisionTenant,
1081
+ * ]);
1082
+ *
1083
+ * export default defineServerConfig()
1084
+ * .routes(appRouter)
1085
+ * .workflows(workflowRouter)
1086
+ * .build();
1087
+ * ```
1088
+ */
1089
+ workflows(router: ServerConfig['workflows'], config?: ServerConfig['workflowsConfig']): this;
649
1090
  /**
650
1091
  * Configure lifecycle hooks
651
1092
  * Can be called multiple times - hooks will be executed in registration order
@@ -685,4 +1126,4 @@ declare class ServerConfigBuilder {
685
1126
  */
686
1127
  declare function defineServerConfig(): ServerConfigBuilder;
687
1128
 
688
- export { type AppFactory, type ServerConfig, type ServerInstance, createServer, defineServerConfig, loadEnvFiles, startServer };
1129
+ export { type AppFactory, type ServerConfig, type ServerInstance, type ShutdownHookOptions, createServer, createServerlessApp, defineServerConfig, getShutdownManager, loadEnvFiles, provisionInfrastructure, resetServerlessApp, startServer };