@spfn/core 0.2.0-beta.6 → 0.2.0-beta.64
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/LICENSE +1 -1
- package/README.md +181 -1281
- package/dist/authz/index.d.ts +34 -0
- package/dist/authz/index.js +415 -0
- package/dist/authz/index.js.map +1 -0
- package/dist/{boss-DI1r4kTS.d.ts → boss-gXhgctn6.d.ts} +40 -0
- package/dist/cache/index.js +42 -30
- package/dist/cache/index.js.map +1 -1
- package/dist/codegen/index.d.ts +55 -8
- package/dist/codegen/index.js +183 -7
- package/dist/codegen/index.js.map +1 -1
- package/dist/config/index.d.ts +585 -6
- package/dist/config/index.js +116 -5
- package/dist/config/index.js.map +1 -1
- package/dist/db/index.d.ts +270 -4
- package/dist/db/index.js +404 -60
- package/dist/db/index.js.map +1 -1
- package/dist/define-middleware-DuXD8Hvu.d.ts +167 -0
- package/dist/env/index.d.ts +26 -2
- package/dist/env/index.js +15 -5
- package/dist/env/index.js.map +1 -1
- package/dist/env/loader.d.ts +26 -19
- package/dist/env/loader.js +32 -25
- package/dist/env/loader.js.map +1 -1
- package/dist/errors/index.d.ts +10 -0
- package/dist/errors/index.js +20 -2
- package/dist/errors/index.js.map +1 -1
- package/dist/event/index.d.ts +33 -3
- package/dist/event/index.js +24 -3
- package/dist/event/index.js.map +1 -1
- package/dist/event/sse/client.d.ts +42 -3
- package/dist/event/sse/client.js +128 -45
- package/dist/event/sse/client.js.map +1 -1
- package/dist/event/sse/index.d.ts +12 -5
- package/dist/event/sse/index.js +271 -32
- package/dist/event/sse/index.js.map +1 -1
- package/dist/event/ws/client.d.ts +59 -0
- package/dist/event/ws/client.js +273 -0
- package/dist/event/ws/client.js.map +1 -0
- package/dist/event/ws/index.d.ts +94 -0
- package/dist/event/ws/index.js +272 -0
- package/dist/event/ws/index.js.map +1 -0
- package/dist/job/index.d.ts +2 -2
- package/dist/job/index.js +155 -42
- package/dist/job/index.js.map +1 -1
- package/dist/logger/index.d.ts +5 -0
- package/dist/logger/index.js +14 -0
- package/dist/logger/index.js.map +1 -1
- package/dist/middleware/index.d.ts +243 -2
- package/dist/middleware/index.js +1323 -13
- package/dist/middleware/index.js.map +1 -1
- package/dist/nextjs/index.d.ts +2 -2
- package/dist/nextjs/index.js +77 -31
- package/dist/nextjs/index.js.map +1 -1
- package/dist/nextjs/server.d.ts +53 -23
- package/dist/nextjs/server.js +197 -66
- package/dist/nextjs/server.js.map +1 -1
- package/dist/route/index.d.ts +138 -146
- package/dist/route/index.js +238 -22
- package/dist/route/index.js.map +1 -1
- package/dist/security/index.d.ts +83 -0
- package/dist/security/index.js +173 -0
- package/dist/security/index.js.map +1 -0
- package/dist/server/index.d.ts +450 -17
- package/dist/server/index.js +1756 -277
- package/dist/server/index.js.map +1 -1
- package/dist/{router-Di7ENoah.d.ts → token-manager-jKD_EsSE.d.ts} +121 -1
- package/dist/{types-D_N_U-Py.d.ts → types-7Mhoxnnt.d.ts} +21 -1
- package/dist/types-BFB72jbM.d.ts +282 -0
- package/dist/types-DVjf37yO.d.ts +205 -0
- package/docs/file-upload.md +717 -0
- package/package.json +235 -208
- 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"]}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,26 +1,43 @@
|
|
|
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 {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
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';
|
|
8
12
|
import '@sinclair/typebox';
|
|
9
13
|
import 'pg-boss';
|
|
10
14
|
|
|
11
15
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
*
|
|
17
|
-
* 3. .env.{NODE_ENV}.local
|
|
18
|
-
* 4. .env.local - Local overrides (gitignored)
|
|
19
|
-
* 5. .env.{NODE_ENV}
|
|
20
|
-
* 6. .env - Defaults
|
|
16
|
+
* @deprecated Use `loadEnv` from '@spfn/core/env/loader' instead.
|
|
17
|
+
* This module will be removed in the next major version.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* @deprecated Use `loadEnv()` from '@spfn/core/env/loader' instead.
|
|
21
21
|
*/
|
|
22
22
|
declare function loadEnvFiles(): void;
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Workflow router interface for @spfn/core integration
|
|
26
|
+
*
|
|
27
|
+
* This is a minimal interface that avoids circular dependency with @spfn/workflow.
|
|
28
|
+
* The actual WorkflowRouter from @spfn/workflow implements this interface.
|
|
29
|
+
*/
|
|
30
|
+
interface WorkflowRouterLike {
|
|
31
|
+
/**
|
|
32
|
+
* Initialize the workflow engine
|
|
33
|
+
* Called by server during infrastructure initialization
|
|
34
|
+
*
|
|
35
|
+
* @internal
|
|
36
|
+
*/
|
|
37
|
+
_init: (db: any, options?: {
|
|
38
|
+
largeOutputThreshold?: number;
|
|
39
|
+
}) => void;
|
|
40
|
+
}
|
|
24
41
|
/**
|
|
25
42
|
* CORS configuration options - inferred from hono/cors
|
|
26
43
|
*/
|
|
@@ -60,11 +77,86 @@ interface ServerConfig {
|
|
|
60
77
|
* Error handler (default: true)
|
|
61
78
|
*/
|
|
62
79
|
errorHandler?: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Callback invoked when an error occurs (passed to ErrorHandler)
|
|
82
|
+
*
|
|
83
|
+
* Called asynchronously without blocking the response.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```typescript
|
|
87
|
+
* import { createErrorSlackNotifier } from '@spfn/notification/server';
|
|
88
|
+
*
|
|
89
|
+
* middleware: {
|
|
90
|
+
* onError: createErrorSlackNotifier({ minStatusCode: 500 }),
|
|
91
|
+
* }
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
onError?: (err: Error, context: OnErrorContext) => Promise<void> | void;
|
|
63
95
|
};
|
|
64
96
|
/**
|
|
65
97
|
* Additional custom middleware
|
|
66
98
|
*/
|
|
67
99
|
use?: MiddlewareHandler[];
|
|
100
|
+
/**
|
|
101
|
+
* Proxy-guard: verify requests came through the trusted Next.js RPC proxy
|
|
102
|
+
* (HMAC signature) and/or an allowed browser origin, then tag `clientType`.
|
|
103
|
+
* Lets the backend reject direct-to-backend calls that bypass the proxy.
|
|
104
|
+
*
|
|
105
|
+
* Disabled by default (`mode: 'off'`). Requires the same `SPFN_PROXY_SECRET`
|
|
106
|
+
* on the proxy and the backend. See PROXY-BACKEND-AUTH-SPEC.md.
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```typescript
|
|
110
|
+
* .proxyGuard({ mode: 'strict', allowedOrigins: ['https://app.example.com'] })
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
proxyGuard?: Omit<ProxyGuardConfig, 'nonceStore'> & {
|
|
114
|
+
/**
|
|
115
|
+
* Enable hard replay rejection via a Redis nonce store. Evaluated in BOTH modes
|
|
116
|
+
* (tag observes replays, strict rejects). Requires a cache (CACHE_URL); without
|
|
117
|
+
* one, falls back to the timestamp window. Degrades to the window if the store
|
|
118
|
+
* is briefly unavailable. @default false
|
|
119
|
+
*/
|
|
120
|
+
nonce?: boolean;
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* Rate limiting: an optional global default limiter plus named policies.
|
|
124
|
+
*
|
|
125
|
+
* `mode: 'on'` applies `default` to every named-middleware route (opt out
|
|
126
|
+
* with `.skip(['rateLimit'])`); `policies` lets packages tag sensitive routes
|
|
127
|
+
* via `rateLimitPolicy(name, fallback)` while this app tunes the numbers in
|
|
128
|
+
* one place. Backed by the shared cache (CACHE_URL); without a cache it fails
|
|
129
|
+
* open unless `default.failClosed` is set. Disabled by default (`mode: 'off'`).
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```typescript
|
|
133
|
+
* .rateLimit({
|
|
134
|
+
* mode: 'on',
|
|
135
|
+
* default: { limit: 100, windowMs: 60_000 },
|
|
136
|
+
* policies: { 'auth-login': { limit: 5, windowMs: 60_000 } },
|
|
137
|
+
* })
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
rateLimit?: {
|
|
141
|
+
/** 'on' applies the default limiter to every route. @default 'off' */
|
|
142
|
+
mode?: 'off' | 'on';
|
|
143
|
+
/** Default policy applied to all routes when `mode` is 'on'. */
|
|
144
|
+
default?: RateLimitOptions;
|
|
145
|
+
/** Named policies referenced by `rateLimitPolicy(name, fallback)` tags. */
|
|
146
|
+
policies?: Record<string, RateLimitOptions>;
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* SSRF policy for outbound requests made via `safeFetch` (`@spfn/core/security`).
|
|
150
|
+
* Sets the process-wide default used by webhook/callback senders. Private and
|
|
151
|
+
* reserved IPs are blocked by default; set `allowHosts` to restrict to a known
|
|
152
|
+
* set of upstreams, or `blockPrivateIps: false` for trusted internal calls.
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* ```typescript
|
|
156
|
+
* .outboundFetch({ allowHosts: ['hooks.slack.com'] })
|
|
157
|
+
* ```
|
|
158
|
+
*/
|
|
159
|
+
outboundFetch?: SafeFetchPolicy;
|
|
68
160
|
/**
|
|
69
161
|
* Global middlewares with names for route-level skip control
|
|
70
162
|
* Use defineMiddleware() for type-safe middleware definitions
|
|
@@ -163,6 +255,30 @@ interface ServerConfig {
|
|
|
163
255
|
*/
|
|
164
256
|
path?: string;
|
|
165
257
|
};
|
|
258
|
+
/**
|
|
259
|
+
* WebSocket router for bidirectional real-time communication
|
|
260
|
+
*
|
|
261
|
+
* @example
|
|
262
|
+
* ```typescript
|
|
263
|
+
* import { defineWSRouter } from '@spfn/core/event/ws';
|
|
264
|
+
*
|
|
265
|
+
* export default defineServerConfig()
|
|
266
|
+
* .websockets(wsRouter) // → WS /ws
|
|
267
|
+
* .build();
|
|
268
|
+
* ```
|
|
269
|
+
*/
|
|
270
|
+
websockets?: WSRouterDef<any, any>;
|
|
271
|
+
/**
|
|
272
|
+
* WebSocket configuration options
|
|
273
|
+
* Only used if websockets router is provided
|
|
274
|
+
*/
|
|
275
|
+
websocketsConfig?: WSHandlerConfig & {
|
|
276
|
+
/**
|
|
277
|
+
* WebSocket endpoint path
|
|
278
|
+
* @default '/ws'
|
|
279
|
+
*/
|
|
280
|
+
path?: string;
|
|
281
|
+
};
|
|
166
282
|
/**
|
|
167
283
|
* Enable debug mode (default: NODE_ENV === 'development')
|
|
168
284
|
*/
|
|
@@ -282,6 +398,34 @@ interface ServerConfig {
|
|
|
282
398
|
*/
|
|
283
399
|
headers?: number;
|
|
284
400
|
};
|
|
401
|
+
/**
|
|
402
|
+
* Fetch (outbound HTTP) timeout configuration
|
|
403
|
+
* Controls Node.js undici global dispatcher timeouts for fetch() calls
|
|
404
|
+
* Applies to all outbound HTTP requests made via fetch() in this process
|
|
405
|
+
*/
|
|
406
|
+
fetchTimeout?: {
|
|
407
|
+
/**
|
|
408
|
+
* TCP connection timeout in milliseconds
|
|
409
|
+
* Time to establish socket connection to upstream server
|
|
410
|
+
* @default 10000 (10 seconds)
|
|
411
|
+
* @env FETCH_CONNECT_TIMEOUT
|
|
412
|
+
*/
|
|
413
|
+
connect?: number;
|
|
414
|
+
/**
|
|
415
|
+
* Response headers timeout in milliseconds
|
|
416
|
+
* Time to receive complete response headers after request sent
|
|
417
|
+
* @default 300000 (5 minutes)
|
|
418
|
+
* @env FETCH_HEADERS_TIMEOUT
|
|
419
|
+
*/
|
|
420
|
+
headers?: number;
|
|
421
|
+
/**
|
|
422
|
+
* Body data timeout in milliseconds
|
|
423
|
+
* Maximum time between body data chunks from upstream server
|
|
424
|
+
* @default 300000 (5 minutes)
|
|
425
|
+
* @env FETCH_BODY_TIMEOUT
|
|
426
|
+
*/
|
|
427
|
+
body?: number;
|
|
428
|
+
};
|
|
285
429
|
/**
|
|
286
430
|
* Graceful shutdown configuration
|
|
287
431
|
* Controls server shutdown behavior during SIGTERM/SIGINT signals
|
|
@@ -289,9 +433,13 @@ interface ServerConfig {
|
|
|
289
433
|
shutdown?: {
|
|
290
434
|
/**
|
|
291
435
|
* Graceful shutdown timeout in milliseconds
|
|
292
|
-
* Maximum time to wait for
|
|
293
|
-
* After timeout, forces process
|
|
294
|
-
*
|
|
436
|
+
* Maximum time to wait for in-flight operations to drain and resource cleanup
|
|
437
|
+
* After timeout, forces process.exit() before k8s SIGKILL
|
|
438
|
+
*
|
|
439
|
+
* Formula: terminationGracePeriodSeconds - preStopSleep - safetyMargin
|
|
440
|
+
* Default: 300s - 5s - 15s = 280s
|
|
441
|
+
*
|
|
442
|
+
* @default 280000 (280 seconds)
|
|
295
443
|
* @env SHUTDOWN_TIMEOUT
|
|
296
444
|
*/
|
|
297
445
|
timeout?: number;
|
|
@@ -338,6 +486,39 @@ interface ServerConfig {
|
|
|
338
486
|
*/
|
|
339
487
|
redis?: boolean;
|
|
340
488
|
};
|
|
489
|
+
/**
|
|
490
|
+
* Workflow router for workflow orchestration
|
|
491
|
+
*
|
|
492
|
+
* Automatically initializes the workflow engine after database is ready.
|
|
493
|
+
* Workflows are defined using @spfn/workflow package.
|
|
494
|
+
*
|
|
495
|
+
* @example
|
|
496
|
+
* ```typescript
|
|
497
|
+
* import { defineWorkflowRouter } from '@spfn/workflow';
|
|
498
|
+
*
|
|
499
|
+
* const workflowRouter = defineWorkflowRouter([
|
|
500
|
+
* provisionTenant,
|
|
501
|
+
* deprovisionTenant,
|
|
502
|
+
* ]);
|
|
503
|
+
*
|
|
504
|
+
* export default defineServerConfig()
|
|
505
|
+
* .workflows(workflowRouter)
|
|
506
|
+
* .build();
|
|
507
|
+
* ```
|
|
508
|
+
*/
|
|
509
|
+
workflows?: WorkflowRouterLike;
|
|
510
|
+
/**
|
|
511
|
+
* Workflow engine configuration
|
|
512
|
+
* Only used if workflows router is provided
|
|
513
|
+
*/
|
|
514
|
+
workflowsConfig?: {
|
|
515
|
+
/**
|
|
516
|
+
* Large output threshold in bytes
|
|
517
|
+
* Outputs larger than this will be stored in external storage
|
|
518
|
+
* @default 1024 * 1024 (1MB)
|
|
519
|
+
*/
|
|
520
|
+
largeOutputThreshold?: number;
|
|
521
|
+
};
|
|
341
522
|
/**
|
|
342
523
|
* Server lifecycle hooks for custom infrastructure setup and management
|
|
343
524
|
* Allows initialization of custom services and resources at different stages
|
|
@@ -517,6 +698,177 @@ declare function createServer(config?: ServerConfig): Promise<Hono>;
|
|
|
517
698
|
*/
|
|
518
699
|
declare function startServer(config?: ServerConfig): Promise<ServerInstance>;
|
|
519
700
|
|
|
701
|
+
/**
|
|
702
|
+
* Shutdown Manager
|
|
703
|
+
*
|
|
704
|
+
* Manages graceful shutdown with drain behavior.
|
|
705
|
+
* All tracked operations must complete before shutdown proceeds.
|
|
706
|
+
*
|
|
707
|
+
* Features:
|
|
708
|
+
* - Hook registry: Multiple modules can register independent cleanup handlers
|
|
709
|
+
* - Operation tracking: Long-running tasks are awaited during shutdown (drain)
|
|
710
|
+
* - State management: isShuttingDown() for rejecting new work
|
|
711
|
+
*/
|
|
712
|
+
interface ShutdownHookOptions {
|
|
713
|
+
/**
|
|
714
|
+
* Timeout for this hook in milliseconds
|
|
715
|
+
* If the hook exceeds this time, it is skipped and the next hook runs
|
|
716
|
+
* @default 10000 (10s)
|
|
717
|
+
*/
|
|
718
|
+
timeout?: number;
|
|
719
|
+
/**
|
|
720
|
+
* Execution order (lower runs first)
|
|
721
|
+
* @default 100
|
|
722
|
+
*/
|
|
723
|
+
order?: number;
|
|
724
|
+
}
|
|
725
|
+
declare class ShutdownManager {
|
|
726
|
+
private state;
|
|
727
|
+
private hooks;
|
|
728
|
+
private operations;
|
|
729
|
+
private operationCounter;
|
|
730
|
+
/**
|
|
731
|
+
* Register a shutdown hook
|
|
732
|
+
*
|
|
733
|
+
* Hooks run in order during shutdown, after all tracked operations drain.
|
|
734
|
+
* Each hook has its own timeout — failure does not block subsequent hooks.
|
|
735
|
+
*
|
|
736
|
+
* @example
|
|
737
|
+
* shutdown.onShutdown('ai-service', async () => {
|
|
738
|
+
* await aiService.cancelPending();
|
|
739
|
+
* }, { timeout: 30000, order: 10 });
|
|
740
|
+
*/
|
|
741
|
+
onShutdown(name: string, handler: () => Promise<void>, options?: ShutdownHookOptions): void;
|
|
742
|
+
/**
|
|
743
|
+
* Track a long-running operation
|
|
744
|
+
*
|
|
745
|
+
* During shutdown (drain phase), the process waits for ALL tracked
|
|
746
|
+
* operations to complete before proceeding with cleanup.
|
|
747
|
+
*
|
|
748
|
+
* If shutdown has already started, the operation is rejected immediately.
|
|
749
|
+
*
|
|
750
|
+
* @returns The operation result (pass-through)
|
|
751
|
+
*
|
|
752
|
+
* @example
|
|
753
|
+
* const result = await shutdown.trackOperation(
|
|
754
|
+
* 'ai-generate',
|
|
755
|
+
* aiService.generate(prompt)
|
|
756
|
+
* );
|
|
757
|
+
*/
|
|
758
|
+
trackOperation<T>(name: string, operation: Promise<T>): Promise<T>;
|
|
759
|
+
/**
|
|
760
|
+
* Whether the server is shutting down
|
|
761
|
+
*
|
|
762
|
+
* Use this to reject new work early (e.g., return 503 in route handlers).
|
|
763
|
+
*/
|
|
764
|
+
isShuttingDown(): boolean;
|
|
765
|
+
/**
|
|
766
|
+
* Number of currently active tracked operations
|
|
767
|
+
*/
|
|
768
|
+
getActiveOperationCount(): number;
|
|
769
|
+
/**
|
|
770
|
+
* Mark shutdown as started immediately
|
|
771
|
+
*
|
|
772
|
+
* Call this at the very beginning of the shutdown sequence so that:
|
|
773
|
+
* - Health check returns 503 right away
|
|
774
|
+
* - trackOperation() rejects new work
|
|
775
|
+
* - isShuttingDown() returns true
|
|
776
|
+
*/
|
|
777
|
+
beginShutdown(): void;
|
|
778
|
+
/**
|
|
779
|
+
* Execute the full shutdown sequence
|
|
780
|
+
*
|
|
781
|
+
* 1. State → draining (reject new operations)
|
|
782
|
+
* 2. Wait for all tracked operations to complete (drain)
|
|
783
|
+
* 3. Run shutdown hooks in order
|
|
784
|
+
* 4. State → closed
|
|
785
|
+
*
|
|
786
|
+
* @param drainTimeout - Max time to wait for operations to drain (ms)
|
|
787
|
+
*/
|
|
788
|
+
execute(drainTimeout: number): Promise<void>;
|
|
789
|
+
/**
|
|
790
|
+
* Wait for all tracked operations to complete, up to drainTimeout
|
|
791
|
+
*/
|
|
792
|
+
private drain;
|
|
793
|
+
/**
|
|
794
|
+
* Execute registered shutdown hooks in order
|
|
795
|
+
*/
|
|
796
|
+
private executeHooks;
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Get the global ShutdownManager instance
|
|
800
|
+
*
|
|
801
|
+
* Available after server starts. Use this to register shutdown hooks
|
|
802
|
+
* or track long-running operations.
|
|
803
|
+
*
|
|
804
|
+
* @example
|
|
805
|
+
* import { getShutdownManager } from '@spfn/core/server';
|
|
806
|
+
*
|
|
807
|
+
* const shutdown = getShutdownManager();
|
|
808
|
+
*
|
|
809
|
+
* // Register cleanup
|
|
810
|
+
* shutdown.onShutdown('my-service', async () => {
|
|
811
|
+
* await myService.close();
|
|
812
|
+
* });
|
|
813
|
+
*
|
|
814
|
+
* // Track long operation
|
|
815
|
+
* await shutdown.trackOperation('ai-task', longRunningPromise);
|
|
816
|
+
*/
|
|
817
|
+
declare function getShutdownManager(): ShutdownManager;
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Serverless target for SPFN.
|
|
821
|
+
*
|
|
822
|
+
* Produces a listen-free, initialized Hono app for serverless platforms (Vercel,
|
|
823
|
+
* AWS Lambda, Cloudflare) — wrap the result with a hono platform adapter, e.g.
|
|
824
|
+
* `handle(app)` from `hono/vercel`.
|
|
825
|
+
*
|
|
826
|
+
* Unlike {@link startServer} (which serve()s a long-lived process and is the
|
|
827
|
+
* always-on / container path), this:
|
|
828
|
+
* - initializes the database in-handler (startServer welds DB init to serve()),
|
|
829
|
+
* - runs at most once per warm container (memoized),
|
|
830
|
+
* - disables the periodic DB health-check (pointless — and a timer leak — on
|
|
831
|
+
* frozen invocations),
|
|
832
|
+
* - does NOT start the in-process pg-boss worker (it cannot run on a serverless
|
|
833
|
+
* platform); enqueue still works, but nothing drains the queue here,
|
|
834
|
+
* - does NOT run seed/RBAC provisioning per cold start — that moves to a
|
|
835
|
+
* deploy-time step, see {@link provisionInfrastructure}.
|
|
836
|
+
*
|
|
837
|
+
* `startServer()` / `spfn start` (the always-on path) is unchanged by this module.
|
|
838
|
+
*/
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Build — once per warm container — the initialized, listen-free Hono app for a
|
|
842
|
+
* serverless platform. Wrap the result with the platform adapter:
|
|
843
|
+
*
|
|
844
|
+
* ```ts
|
|
845
|
+
* import { handle } from 'hono/vercel';
|
|
846
|
+
* import { createServerlessApp } from '@spfn/core/server';
|
|
847
|
+
* import serverConfig from '@/server/server.config';
|
|
848
|
+
*
|
|
849
|
+
* const handler = async (req: Request) => handle(await createServerlessApp(serverConfig))(req);
|
|
850
|
+
* export const GET = handler;
|
|
851
|
+
* export const POST = handler;
|
|
852
|
+
* ```
|
|
853
|
+
*/
|
|
854
|
+
declare function createServerlessApp(config?: ServerConfig): Promise<Hono>;
|
|
855
|
+
/**
|
|
856
|
+
* Reset the memoized serverless app. Tests only.
|
|
857
|
+
*/
|
|
858
|
+
declare function resetServerlessApp(): void;
|
|
859
|
+
/**
|
|
860
|
+
* Deploy-time provisioning — run ONCE per deploy, not per request.
|
|
861
|
+
*
|
|
862
|
+
* Initializes the database and runs the config's provisioning lifecycle hooks
|
|
863
|
+
* (`beforeInfrastructure` / `afterInfrastructure`, e.g. admin seeding + RBAC init).
|
|
864
|
+
* Intended for a build/deploy step (`spfn provision`), keeping per-cold-start work
|
|
865
|
+
* out of the serverless handler. On always-on targets this is equally useful: it
|
|
866
|
+
* avoids re-seeding on every pod restart / replica.
|
|
867
|
+
*
|
|
868
|
+
* Does NOT start the HTTP server, jobs worker, or health-check.
|
|
869
|
+
*/
|
|
870
|
+
declare function provisionInfrastructure(config?: ServerConfig): Promise<void>;
|
|
871
|
+
|
|
520
872
|
/**
|
|
521
873
|
* Server Config Builder
|
|
522
874
|
*
|
|
@@ -550,6 +902,34 @@ declare class ServerConfigBuilder {
|
|
|
550
902
|
* Add named middlewares for route-level skip control
|
|
551
903
|
*/
|
|
552
904
|
middlewares(middlewares: ServerConfig['middlewares']): this;
|
|
905
|
+
/**
|
|
906
|
+
* Configure proxy-guard (verify trusted-proxy signature + origin → clientType)
|
|
907
|
+
*/
|
|
908
|
+
proxyGuard(proxyGuard: ServerConfig['proxyGuard']): this;
|
|
909
|
+
/**
|
|
910
|
+
* Configure rate limiting: an optional global default limiter plus the named
|
|
911
|
+
* policies that `rateLimitPolicy(name, fallback)` tags resolve against.
|
|
912
|
+
*
|
|
913
|
+
* @example
|
|
914
|
+
* ```typescript
|
|
915
|
+
* .rateLimit({
|
|
916
|
+
* mode: 'on',
|
|
917
|
+
* default: { limit: 100, windowMs: 60_000 },
|
|
918
|
+
* policies: { 'auth-login': { limit: 5, windowMs: 60_000 } },
|
|
919
|
+
* })
|
|
920
|
+
* ```
|
|
921
|
+
*/
|
|
922
|
+
rateLimit(rateLimit: ServerConfig['rateLimit']): this;
|
|
923
|
+
/**
|
|
924
|
+
* Configure the SSRF policy for outbound `safeFetch` calls (webhooks,
|
|
925
|
+
* callbacks). Private/reserved IPs are blocked by default.
|
|
926
|
+
*
|
|
927
|
+
* @example
|
|
928
|
+
* ```typescript
|
|
929
|
+
* .outboundFetch({ allowHosts: ['hooks.slack.com'] })
|
|
930
|
+
* ```
|
|
931
|
+
*/
|
|
932
|
+
outboundFetch(outboundFetch: ServerConfig['outboundFetch']): this;
|
|
553
933
|
/**
|
|
554
934
|
* Register define-route based router
|
|
555
935
|
*
|
|
@@ -619,8 +999,40 @@ declare class ServerConfigBuilder {
|
|
|
619
999
|
* .events(eventRouter, { path: '/sse' })
|
|
620
1000
|
* ```
|
|
621
1001
|
*/
|
|
622
|
-
events
|
|
1002
|
+
events<TRouter extends EventRouterDef<any>>(router: TRouter, config?: Omit<SSEHandlerConfig, 'auth'> & {
|
|
623
1003
|
path?: string;
|
|
1004
|
+
auth?: SSEAuthConfig<TRouter>;
|
|
1005
|
+
}): this;
|
|
1006
|
+
/**
|
|
1007
|
+
* Register WebSocket router for bidirectional real-time communication
|
|
1008
|
+
*
|
|
1009
|
+
* Enables type-safe WebSocket connections with:
|
|
1010
|
+
* - Server→client event push (via defineEvent + emit)
|
|
1011
|
+
* - Client→server message handling (via messages in defineWSRouter)
|
|
1012
|
+
*
|
|
1013
|
+
* @example
|
|
1014
|
+
* ```typescript
|
|
1015
|
+
* // src/server/ws.ts
|
|
1016
|
+
* export const wsRouter = defineWSRouter({
|
|
1017
|
+
* events: { userUpdated, notification },
|
|
1018
|
+
* messages: {
|
|
1019
|
+
* ping: ({ ws }) => ws.send('pong', {}),
|
|
1020
|
+
* },
|
|
1021
|
+
* });
|
|
1022
|
+
*
|
|
1023
|
+
* // server.config.ts
|
|
1024
|
+
* export default defineServerConfig()
|
|
1025
|
+
* .websockets(wsRouter) // → WS /ws
|
|
1026
|
+
* .websockets(wsRouter, {
|
|
1027
|
+
* path: '/realtime', // custom path
|
|
1028
|
+
* auth: { enabled: true }, // token authentication
|
|
1029
|
+
* })
|
|
1030
|
+
* .build();
|
|
1031
|
+
* ```
|
|
1032
|
+
*/
|
|
1033
|
+
websockets<TEvents extends Record<string, EventDef<any>>, TMessages extends WSMessageHandlers>(router: WSRouterDef<TEvents, TMessages>, config?: Omit<WSHandlerConfig, 'auth'> & {
|
|
1034
|
+
path?: string;
|
|
1035
|
+
auth?: WSAuthConfig<WSRouterDef<TEvents, TMessages>>;
|
|
624
1036
|
}): this;
|
|
625
1037
|
/**
|
|
626
1038
|
* Enable/disable debug mode
|
|
@@ -646,6 +1058,27 @@ declare class ServerConfigBuilder {
|
|
|
646
1058
|
* Configure infrastructure initialization
|
|
647
1059
|
*/
|
|
648
1060
|
infrastructure(infrastructure: ServerConfig['infrastructure']): this;
|
|
1061
|
+
/**
|
|
1062
|
+
* Register workflow router for workflow orchestration
|
|
1063
|
+
*
|
|
1064
|
+
* Automatically initializes the workflow engine after database is ready.
|
|
1065
|
+
*
|
|
1066
|
+
* @example
|
|
1067
|
+
* ```typescript
|
|
1068
|
+
* import { defineWorkflowRouter } from '@spfn/workflow';
|
|
1069
|
+
*
|
|
1070
|
+
* const workflowRouter = defineWorkflowRouter([
|
|
1071
|
+
* provisionTenant,
|
|
1072
|
+
* deprovisionTenant,
|
|
1073
|
+
* ]);
|
|
1074
|
+
*
|
|
1075
|
+
* export default defineServerConfig()
|
|
1076
|
+
* .routes(appRouter)
|
|
1077
|
+
* .workflows(workflowRouter)
|
|
1078
|
+
* .build();
|
|
1079
|
+
* ```
|
|
1080
|
+
*/
|
|
1081
|
+
workflows(router: ServerConfig['workflows'], config?: ServerConfig['workflowsConfig']): this;
|
|
649
1082
|
/**
|
|
650
1083
|
* Configure lifecycle hooks
|
|
651
1084
|
* Can be called multiple times - hooks will be executed in registration order
|
|
@@ -685,4 +1118,4 @@ declare class ServerConfigBuilder {
|
|
|
685
1118
|
*/
|
|
686
1119
|
declare function defineServerConfig(): ServerConfigBuilder;
|
|
687
1120
|
|
|
688
|
-
export { type AppFactory, type ServerConfig, type ServerInstance, createServer, defineServerConfig, loadEnvFiles, startServer };
|
|
1121
|
+
export { type AppFactory, type ServerConfig, type ServerInstance, type ShutdownHookOptions, createServer, createServerlessApp, defineServerConfig, getShutdownManager, loadEnvFiles, provisionInfrastructure, resetServerlessApp, startServer };
|