@luckystack/server 0.6.2 → 0.6.3
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/CHANGELOG.md +11 -0
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/createServer.ts","../src/httpHandler.ts","../src/logSanitize.ts","../src/securityHeadersRegistry.ts","../src/httpRoutes/csrfMiddleware.ts","../src/capabilities.ts","../src/originExemptRegistry.ts","../src/httpRoutes/timingSafeEqual.ts","../src/httpRoutes/csrfRoute.ts","../src/httpRoutes/faviconRoute.ts","../src/httpRoutes/healthRoutes.ts","../src/httpRoutes/testResetRoute.ts","../src/httpRoutes/uploadsRoute.ts","../src/httpRoutes/authApiRoute.ts","../src/httpRoutes/sessionCookie.ts","../src/httpRoutes/authSecondFactorRoutes.ts","../src/httpRoutes/authLogoutRoute.ts","../src/httpRoutes/authProvidersRoute.ts","../src/httpRoutes/authCallbackRoute.ts","../src/httpRoutes/apiRoute.ts","../src/sse.ts","../src/httpRoutes/resolveRequesterIp.ts","../src/httpRoutes/syncRoute.ts","../src/httpRoutes/customRoutes.ts","../src/customRoutesRegistry.ts","../src/httpRoutes/staticRoutes.ts","../src/loadSocket.ts","../src/verifyBootstrap.ts","../src/runtimeMapsLoader.ts","../src/stopServer.ts","../src/devServerInfo.ts","../src/bootstrap.ts","../src/errorFormatterRegistry.ts"],"sourcesContent":["import http, { type Server as HttpServer } from 'node:http';\r\nimport { registerBindAddress, writeBootUuid, getLogger, getProjectConfig, tryCatch, isProduction, resolveEnvKey, dispatchHook } from '@luckystack/core';\r\nimport { handleHttpRequest } from './httpHandler';\r\nimport { loadSocket } from './loadSocket';\r\nimport { verifyBootstrap } from './verifyBootstrap';\r\nimport { registerProdRuntimeMapsProvider } from './runtimeMapsLoader';\r\nimport { getParsedPort } from './argv';\r\nimport { canResolve } from './capabilities';\r\nimport { runGracefulShutdown } from './stopServer';\r\nimport { writeDevServerInfo, clearDevServerInfo } from './devServerInfo';\r\nimport type {\r\n CreateLuckyStackServerOptions,\r\n RunningLuckyStackServer,\r\n StopLuckyStackServerOptions,\r\n} from './types';\r\n\r\n/**\r\n * One-call server bootstrap for a LuckyStack project.\r\n *\r\n * Wires together:\r\n * - HTTP server with framework routes (`/api/*`, `/sync/*`, `/_health`,\r\n * `/_test/reset`, `/uploads/*`, `/auth/api`, `/auth/callback`)\r\n * - Socket.io server with the Redis adapter, room handlers, presence\r\n * integration, location sync\r\n * - Boot-UUID write so the router's handshake can verify topology\r\n * - Optional dev-mode tooling (devkit hot reload + REPL)\r\n *\r\n * Project responsibilities (passed in as options):\r\n * - `serveFile` / `serveFavicon` — your project's static file handlers\r\n * - `customRoutes` — any additional HTTP routes your app needs\r\n *\r\n * Pre-conditions:\r\n * - `registerProjectConfig(...)` must have run (side-effect import of your\r\n * `config.ts` does this).\r\n * - `registerDeployConfig(...)` must have run (side-effect import of your\r\n * `deploy.config.ts`).\r\n * - `registerRuntimeMapsProvider(...)` must have run (side-effect import of\r\n * your `server/prod/runtimeMaps.ts`).\r\n * - `registerLocalizedNormalizer(...)` must have run (side-effect import of\r\n * your `server/utils/responseNormalizer.ts`).\r\n *\r\n * @example\r\n * ```ts\r\n * import './config';\r\n * import './deploy.config';\r\n * import './prod/runtimeMaps';\r\n * import './utils/responseNormalizer';\r\n * import { createLuckyStackServer } from '@luckystack/server';\r\n * import { serveFile, serveFavicon } from './prod/serveFile';\r\n *\r\n * const server = await createLuckyStackServer({ serveFile, serveFavicon });\r\n * await server.listen();\r\n * ```\r\n */\r\n//? Extracted verbatim from the previous inline `if (enableDevTools) { ... }`\r\n//? block. Same dynamic imports, same ordering, same SIGINT/SIGTERM handlers —\r\n//? hoisted so the bootstrap function reads as a sequence of named steps.\r\nconst initDevTools = async (): Promise<void> => {\r\n //? Dev-only: console-log color tagger + devkit hot reload + REPL.\r\n //? Kept dynamic so tier-A consumers in production never load the\r\n //? typescript compiler API or chokidar's filesystem watchers.\r\n const { initConsolelog } = await import('@luckystack/core');\r\n initConsolelog();\r\n //? Belt-and-braces: explicit SIGINT/SIGTERM handler so Ctrl+C is honored even\r\n //? if a sync CPU burst (TS Program build, large require chain) is still in\r\n //? flight when the signal arrives. Installed BEFORE the guarded devkit import\r\n //? so Ctrl+C still works when devkit is absent and we early-return below.\r\n //? Give `preServerStop` subscribers (cron lease release, tracker flush) a\r\n //? BOUNDED window before the hard exit — without it a dev Ctrl+C leaves the\r\n //? cron leader lease dangling until its TTL, so no jobs fire for up to 30s\r\n //? after every restart. Dev iteration speed still wins: 2s cap, then exit.\r\n const devSignalExit = (reason: 'SIGINT' | 'SIGTERM'): void => {\r\n void Promise.race([\r\n dispatchHook('preServerStop', { reason, timeoutMs: 2000 }),\r\n new Promise((resolve) => {\r\n setTimeout(resolve, 2000);\r\n }),\r\n // eslint-disable-next-line unicorn/no-process-exit -- deliberate dev hard-exit once the bounded hook window closes (mirrors the original inline handler)\r\n ]).finally(() => process.exit(0));\r\n };\r\n process.once('SIGINT', () => {\r\n devSignalExit('SIGINT');\r\n });\r\n process.once('SIGTERM', () => {\r\n devSignalExit('SIGTERM');\r\n });\r\n //? @luckystack/devkit is an OPTIONAL peer, normally ABSENT in production. If a\r\n //? deploy forgets `NODE_ENV=production`, enableDevTools stays true and an\r\n //? unguarded `import('@luckystack/devkit')` would crash boot with\r\n //? ERR_MODULE_NOT_FOUND. Resolve-guard + tryCatch (mirroring bootstrap.ts's\r\n //? optional-package imports) so a missing/broken devkit logs an actionable\r\n //? warning and boot continues instead of taking the whole server down.\r\n const devkitModuleId = '@luckystack/devkit';\r\n if (!canResolve(devkitModuleId)) {\r\n getLogger().warn(\r\n 'dev tooling unavailable — @luckystack/devkit is not installed, so hot reload + type-map generation are off. Install it as a devDependency for dev, or set NODE_ENV=production to run in production mode.',\r\n );\r\n return;\r\n }\r\n const [devkitError] = await tryCatch(async () => {\r\n const devkit = (await import(devkitModuleId)) as {\r\n initializeAll: () => Promise<void>;\r\n setupWatchers: () => void;\r\n };\r\n await devkit.initializeAll();\r\n devkit.setupWatchers();\r\n });\r\n if (devkitError) {\r\n getLogger().warn('dev tooling failed to initialize — continuing without hot reload.', { error: devkitError.message });\r\n }\r\n};\r\n\r\n//? Extracted verbatim from the previous inline `listen` closure. Same\r\n//? auto-increment opt-in, same EADDRINUSE retry, same truthful-failure\r\n//? logging, same success log + callback + resolve. `httpServer`, `ip`, and\r\n//? `port` are threaded in as parameters instead of closed over.\r\nexport const listenLuckyStackServer = (\r\n httpServer: HttpServer,\r\n ip: string,\r\n port: string | number,\r\n callback?: () => void,\r\n): Promise<HttpServer> =>\r\n new Promise<HttpServer>((resolve, reject) => {\r\n const startPort = typeof port === 'string' ? Number.parseInt(port, 10) : port;\r\n //? Auto-pick the next free port resolution:\r\n //? - When `SERVER_PORT_AUTO_INCREMENT` is set EXPLICITLY it always wins\r\n //? (`1`/`true` -> on, `0`/`false` -> off), so a consumer can force either\r\n //? behaviour in any environment.\r\n //? - When it is NOT set we default to ON in dev and OFF in production. The\r\n //? dev-vs-prod signal is `isProduction` from `@luckystack/core` (NODE_ENV)\r\n //? — the same canonical flag this file already uses to gate dev tooling.\r\n //? Prod stays OFF by default because `SERVER_PORT` also drives `config.ts`'s\r\n //? `backendOrigin` / OAuth callback base; silently moving the listen port\r\n //? there would leave clients talking to the old one. In dev a port clash is\r\n //? almost always a leftover `npm run server`, so quietly hopping to the next\r\n //? free port is the friendlier default.\r\n const autoIncrementEnv = (process.env.SERVER_PORT_AUTO_INCREMENT ?? '').toLowerCase();\r\n let autoIncrement: boolean;\r\n if (['0', 'false'].includes(autoIncrementEnv)) autoIncrement = false;\r\n else if (['1', 'true'].includes(autoIncrementEnv)) autoIncrement = true;\r\n else autoIncrement = !isProduction;\r\n\r\n const tryListen = (attemptPort: number): void => {\r\n const onError = (err: NodeJS.ErrnoException): void => {\r\n if (err.code !== 'EADDRINUSE') {\r\n reject(err);\r\n return;\r\n }\r\n if (autoIncrement) {\r\n getLogger().warn(\r\n `Port ${String(attemptPort)} is in use — trying ${String(attemptPort + 1)} (auto-increment; set SERVER_PORT_AUTO_INCREMENT=0 to disable)`,\r\n );\r\n tryListen(attemptPort + 1);\r\n return;\r\n }\r\n //? Truthful failure. The old code unconditionally logged \"running on\r\n //? :<port>\" inside the listen callback even when the bind never\r\n //? succeeded, so an in-use port looked like a healthy boot. Surface\r\n //? the real problem and the two ways out instead.\r\n getLogger().error(\r\n `Port ${String(attemptPort)} is already in use — the server did NOT start. ` +\r\n `Another \\`npm run server\\` is probably still running (stop it), or set ` +\r\n `SERVER_PORT to a free port, or set SERVER_PORT_AUTO_INCREMENT=1 to auto-pick the next free port.`,\r\n );\r\n reject(err);\r\n };\r\n\r\n httpServer.once('error', onError);\r\n httpServer.listen(attemptPort, ip, () => {\r\n httpServer.off('error', onError);\r\n //? Dev only: advertise the ACTUALLY-bound port so the Vite proxy follows\r\n //? us when auto-increment moved the listen off `SERVER_PORT`. Skipped in\r\n //? production (no proxy) and under the test runner (avoid stray files +\r\n //? exit handlers in unit tests). Best-effort — never blocks the boot.\r\n if (!isProduction && process.env.NODE_ENV !== 'test') {\r\n writeDevServerInfo(ip, attemptPort);\r\n process.once('exit', clearDevServerInfo);\r\n }\r\n const config = getProjectConfig();\r\n if (config.logging.socketStartup || config.logging.devLogs) {\r\n getLogger().info(`Server is running on http://${ip}:${String(attemptPort)}/`);\r\n }\r\n callback?.();\r\n resolve(httpServer);\r\n });\r\n };\r\n\r\n tryListen(startPort);\r\n });\r\n\r\nexport const createLuckyStackServer = async (\r\n options: CreateLuckyStackServerOptions = {}\r\n): Promise<RunningLuckyStackServer> => {\r\n //? Auto-register the framework-shipped runtime maps provider when the\r\n //? consumer supplied a `loadGeneratedMaps` callback. Runs before\r\n //? `verifyBootstrap` so the registration counts toward the boot check.\r\n //? Consumers who hand-rolled their own provider via\r\n //? `registerRuntimeMapsProvider` can simply omit `loadGeneratedMaps` and\r\n //? the framework leaves their registration alone.\r\n if (options.loadGeneratedMaps) {\r\n registerProdRuntimeMapsProvider({\r\n loadGenerated: options.loadGeneratedMaps,\r\n preset: options.runtimeMapsPreset,\r\n });\r\n }\r\n\r\n //? Fail fast if a project's overlay forgot to register a critical piece.\r\n //? Surface a single readable error instead of a stack trace deep inside\r\n //? a request handler.\r\n await verifyBootstrap({\r\n requireDeployConfig: options.requireDeployConfig,\r\n requireServicesConfig: options.requireServicesConfig,\r\n requireOAuthProviders: options.requireOAuthProviders,\r\n });\r\n\r\n const port = options.port ?? getParsedPort() ?? options.defaultPort ?? process.env.SERVER_PORT ?? 80;\r\n const ip = options.ip ?? process.env.SERVER_IP ?? '127.0.0.1';\r\n const enableDevTools = options.enableDevTools ?? resolveEnvKey() !== 'production';\r\n\r\n //? Register the resolved bind address so framework code that needs it\r\n //? (e.g. `checkOrigin` building the same-origin entry) doesn't drift when\r\n //? the consumer passed `options.ip`/`options.port` without also setting\r\n //? the legacy `SERVER_IP`/`SERVER_PORT` env vars.\r\n registerBindAddress({\r\n ip,\r\n port: typeof port === 'string' ? Number.parseInt(port, 10) : port,\r\n });\r\n\r\n if (enableDevTools) {\r\n await initDevTools();\r\n }\r\n\r\n //? Boot UUID must be written before /_health can answer truthfully. Router\r\n //? boot handshake consumes /_health to verify shared-Redis topology. A\r\n //? failure here is almost always Redis being unreachable or misconfigured\r\n //? (bad credentials, wrong host); throw a clear, actionable error so boot\r\n //? halts on THIS message instead of the raw ioredis `ReplyError` dump that\r\n //? confused operators before. Library code must not `process.exit()`; the\r\n //? throw propagates to the boot entry and the dev supervisor respawns.\r\n //? `tryCatch` already captured the underlying error to the error tracker.\r\n const [bootUuidError] = await tryCatch(() => writeBootUuid());\r\n if (bootUuidError) {\r\n throw new Error(\r\n 'Failed to write the boot UUID to Redis. Check REDIS_HOST / REDIS_PORT / REDIS_USER / REDIS_PASSWORD and that Redis is reachable.',\r\n { cause: bootUuidError },\r\n );\r\n }\r\n\r\n const httpServer: HttpServer = http.createServer((req, res) => {\r\n void handleHttpRequest(req, res, options);\r\n });\r\n\r\n //? Persistent error listener. `listenLuckyStackServer` attaches a ONE-OFF\r\n //? `error` handler only around the bind attempt; once listening, an async\r\n //? socket error (e.g. EMFILE under load, an abrupt peer reset surfacing at the\r\n //? server level) would otherwise be an unhandled `'error'` event and crash the\r\n //? process. Log it instead so the server stays up and the cause is visible.\r\n httpServer.on('error', (err: NodeJS.ErrnoException) => {\r\n //? EADDRINUSE during bind is owned by the one-off handler in\r\n //? `listenLuckyStackServer` (which rejects/retries); don't double-log it.\r\n if (err.code === 'EADDRINUSE') return;\r\n getLogger().error('[http-server] runtime error', err);\r\n });\r\n\r\n const { io: ioServer, adapterClients } = loadSocket(httpServer, {\r\n maxHttpBufferSize: options.maxHttpBufferSize,\r\n });\r\n\r\n const listen = (callback?: () => void): Promise<HttpServer> =>\r\n listenLuckyStackServer(httpServer, ip, port, callback);\r\n\r\n //? Idempotent graceful shutdown. A second call (e.g. SIGINT then SIGTERM, or a\r\n //? programmatic `stop()` racing a signal) returns the in-flight promise rather\r\n //? than running the teardown twice.\r\n let shutdownPromise: Promise<void> | null = null;\r\n const stop = (stopOptions: StopLuckyStackServerOptions = {}): Promise<void> => {\r\n shutdownPromise ??= runGracefulShutdown({ httpServer, ioServer, adapterClients }, stopOptions);\r\n return shutdownPromise;\r\n };\r\n\r\n //? Production signal wiring (MIS-016). In dev, `initDevTools` already installs\r\n //? fast `process.exit(0)` handlers (hot-reload supervisor restarts). In prod\r\n //? we run the FULL graceful shutdown and exit only after it settles — an\r\n //? orchestrator's SIGTERM should drain connections + flush trackers, not hard-\r\n //? kill. `process.once` so a repeated signal doesn't stack handlers; the\r\n //? `stop()` idempotency covers a SIGINT-then-SIGTERM sequence.\r\n if (!enableDevTools) {\r\n const handleSignal = (reason: 'SIGTERM' | 'SIGINT'): void => {\r\n void (async () => {\r\n await stop({ reason });\r\n //? Terminate AFTER the graceful drain completes. This is the process\r\n //? entry's signal handler (not deep library code) — an orchestrator's\r\n //? SIGTERM expects the process to exit once it has drained, so exiting\r\n //? here is correct. Mirrors the dev handlers in `initDevTools`.\r\n // eslint-disable-next-line unicorn/no-process-exit -- top-level signal handler, exits after graceful drain\r\n process.exit(0);\r\n })();\r\n };\r\n process.once('SIGTERM', () => { handleSignal('SIGTERM'); });\r\n process.once('SIGINT', () => { handleSignal('SIGINT'); });\r\n }\r\n\r\n return { httpServer, ioServer, listen, stop, close: stop };\r\n};\r\n","import type { IncomingMessage, ServerResponse } from 'node:http';\r\nimport { randomUUID } from 'node:crypto';\r\nimport {\r\n allowedOrigin,\r\n dispatchHook,\r\n extractTokenFromRequest,\r\n getLogger,\r\n getParams,\r\n getProjectConfig,\r\n hasCookie,\r\n normalizeOrigin,\r\n readSession,\r\n tryCatch,\r\n tryCatchSync,\r\n} from '@luckystack/core';\r\nimport { sanitizeForLog } from './logSanitize';\r\nimport { getSecurityHeadersBuilder } from './securityHeadersRegistry';\r\nimport type { CreateLuckyStackServerOptions } from './types';\r\nimport { enforceCsrfOnStateChangingRequest } from './httpRoutes/csrfMiddleware';\r\nimport { handleCsrfRoute } from './httpRoutes/csrfRoute';\r\nimport { handleFaviconRoute } from './httpRoutes/faviconRoute';\r\nimport { handleHealthRoute, handleLivezRoute, handleReadyzRoute } from './httpRoutes/healthRoutes';\r\nimport { handleTestResetRoute } from './httpRoutes/testResetRoute';\r\nimport { handleUploadsRoute } from './httpRoutes/uploadsRoute';\r\nimport { handleAuthApiRoute } from './httpRoutes/authApiRoute';\r\nimport { handleAuthEmailCodeRoute, handleAuthTwoFactorRoute } from './httpRoutes/authSecondFactorRoutes';\r\nimport { handleAuthLogoutRoute } from './httpRoutes/authLogoutRoute';\r\nimport { handleAuthProvidersRoute } from './httpRoutes/authProvidersRoute';\r\nimport { handleAuthCallbackRoute } from './httpRoutes/authCallbackRoute';\r\nimport { handleApiRoute } from './httpRoutes/apiRoute';\r\nimport { handleSyncRoute } from './httpRoutes/syncRoute';\r\nimport { handleCustomRoutes, handlePreParamsCustomRoutes } from './httpRoutes/customRoutes';\r\nimport { handleStaticAndSpaFallback } from './httpRoutes/staticRoutes';\r\nimport { isOriginExemptPath } from './originExemptRegistry';\r\nimport { resolveCookieSecure } from './httpRoutes/sessionCookie';\r\nimport type { HttpRouteContext, HttpRouteHandler } from './httpRoutes/types';\r\n\r\nconst buildSessionCookieOptions = (\r\n sessionExpiryDays: number,\r\n secure: boolean,\r\n http: ReturnType<typeof getProjectConfig>['http'],\r\n): string =>\r\n `HttpOnly; SameSite=${http.sessionCookieSameSite}; Path=${http.sessionCookiePath}; Max-Age=${60 * 60 * 24 * sessionExpiryDays}; ${secure ? 'Secure;' : ''}`;\r\n\r\nconst setSecurityHeaders = (req: IncomingMessage, res: ServerResponse, origin: string) => {\r\n const { cors, securityHeaders } = getProjectConfig().http;\r\n res.setHeader('Access-Control-Allow-Origin', origin);\r\n res.setHeader('Access-Control-Allow-Methods', cors.allowedMethods);\r\n res.setHeader('Access-Control-Allow-Headers', cors.allowedHeaders);\r\n res.setHeader('Access-Control-Expose-Headers', cors.exposedHeaders);\r\n if (cors.credentials) {\r\n res.setHeader('Access-Control-Allow-Credentials', 'true');\r\n }\r\n res.setHeader('Referrer-Policy', securityHeaders.referrerPolicy);\r\n res.setHeader('X-Frame-Options', securityHeaders.frameOptions);\r\n res.setHeader('X-XSS-Protection', securityHeaders.xssProtection);\r\n res.setHeader('X-Content-Type-Options', securityHeaders.contentTypeOptions);\r\n\r\n //? Consumer-registered builder runs AFTER framework defaults so it can\r\n //? override (CSP, HSTS, Permissions-Policy) or extend. Errors fall\r\n //? through to defaults so a buggy builder can't kill response delivery.\r\n const builder = getSecurityHeadersBuilder();\r\n if (builder) {\r\n //? Wrap both the builder call AND the header writes so a buggy builder (or\r\n //? an invalid header name/value it returns) can't kill response delivery —\r\n //? same guarded scope as the original raw try/catch, just via tryCatchSync.\r\n const [error] = tryCatchSync(() => {\r\n const custom = builder(req);\r\n if (custom) {\r\n for (const [name, value] of Object.entries(custom)) {\r\n res.setHeader(name, value);\r\n }\r\n }\r\n });\r\n if (error) {\r\n getLogger().warn('securityHeadersBuilder threw — falling back to defaults', { err: error });\r\n }\r\n }\r\n};\r\n\r\n//? Routes that run BEFORE params parsing — they don't need (and shouldn't\r\n//? consume) the request body. The framework fast-paths run first; consumer\r\n//? `'pre-params'` custom routes (webhooks / streaming uploads) run last, after\r\n//? the probes but before `getParams` drains the body.\r\nconst PRE_PARAMS_ROUTES: HttpRouteHandler[] = [\r\n handleCsrfRoute,\r\n handleFaviconRoute,\r\n handleLivezRoute,\r\n handleReadyzRoute,\r\n handleHealthRoute,\r\n handleTestResetRoute,\r\n handleAuthProvidersRoute,\r\n handleAuthLogoutRoute,\r\n handlePreParamsCustomRoutes,\r\n];\r\n\r\n//? Routes that run AFTER params parsing.\r\n//? ORDER: the email-code + 2FA routes MUST run before handleAuthApiRoute —\r\n//? that handler catch-alls every `/auth/api/*` path into `providerNotFound`.\r\nconst POST_PARAMS_ROUTES: HttpRouteHandler[] = [\r\n handleUploadsRoute,\r\n handleAuthEmailCodeRoute,\r\n handleAuthTwoFactorRoute,\r\n handleAuthApiRoute,\r\n handleAuthCallbackRoute,\r\n handleApiRoute,\r\n handleSyncRoute,\r\n handleCustomRoutes,\r\n handleStaticAndSpaFallback,\r\n];\r\n\r\nconst dispatchRoutes = async (handlers: HttpRouteHandler[], ctx: HttpRouteContext): Promise<boolean> => {\r\n for (const handler of handlers) {\r\n const handled = await handler(ctx);\r\n if (handled || ctx.res.writableEnded) return true;\r\n }\r\n return false;\r\n};\r\n\r\nconst enforceOriginPolicy = (\r\n req: IncomingMessage,\r\n res: ServerResponse,\r\n routePath: string,\r\n): { origin: string; rejected: boolean } => {\r\n //? Do NOT fall back to `req.headers.host` — host always equals the bound\r\n //? origin so non-browser callers (curl, native apps) would silently bypass\r\n //? `allowedOrigin()`. Only browsers attach Origin/Referer.\r\n //? Normalize at the source: a Referer fallback is a FULL URL (with path/query),\r\n //? so reduce it to scheme+host before it's both allowlist-checked AND reflected\r\n //? into `Access-Control-Allow-Origin`. A raw referer would otherwise produce an\r\n //? invalid ACAO (containing a path) that the browser rejects — silently breaking\r\n //? credentialed cross-origin clients that send Referer but no Origin header.\r\n const origin = normalizeOrigin({\r\n value: req.headers.origin ?? req.headers.referer ?? '',\r\n secure: process.env.SECURE === 'true',\r\n });\r\n const isStateChangingMethod = req.method !== 'GET' && req.method !== 'HEAD' && req.method !== 'OPTIONS';\r\n\r\n //? Registered webhook / server-to-server prefixes opt out of the browser\r\n //? origin gate — they authenticate via signature/HMAC in the handler, not via\r\n //? Origin (which they never send). Empty by default; opt-in only. The handler\r\n //? is still responsible for verifying the caller. See originExemptRegistry.\r\n if (isOriginExemptPath(routePath)) {\r\n //? L1 (CORS hardening): exempt paths skip the origin GATE — server-to-server\r\n //? callers send no Origin and authenticate via signature/HMAC in the handler.\r\n //? But the raw, unvalidated Origin must NOT be reflected into\r\n //? `Access-Control-Allow-Origin` alongside `Access-Control-Allow-Credentials`\r\n //? — that is a CORS misconfig the moment an exempt prefix serves any\r\n //? browser-readable, cookie-authenticated data. Return an empty origin so no\r\n //? cross-origin ACAO is emitted for exempt routes (they don't need CORS).\r\n return { origin: '', rejected: false };\r\n }\r\n\r\n if (!origin) {\r\n //? No browser-attributable origin: fail-close on state-changing methods,\r\n //? allow read-only (GET/HEAD/OPTIONS) so health probes and asset fetches\r\n //? from non-browser tooling continue to work.\r\n if (isStateChangingMethod) {\r\n res.statusCode = 403;\r\n res.setHeader('Content-Type', 'text/plain');\r\n res.end('Forbidden');\r\n return { origin, rejected: true };\r\n }\r\n } else if (!allowedOrigin(origin)) {\r\n res.statusCode = 403;\r\n res.setHeader('Content-Type', 'text/plain');\r\n res.end('Forbidden');\r\n return { origin, rejected: true };\r\n }\r\n return { origin, rejected: false };\r\n};\r\n\r\nconst refreshSessionCookieIfPresent = async ({\r\n req,\r\n res,\r\n token,\r\n sessionCookieName,\r\n sessionCookieOptions,\r\n}: {\r\n req: IncomingMessage;\r\n res: ServerResponse;\r\n token: string | null;\r\n sessionCookieName: string;\r\n sessionCookieOptions: string;\r\n}) => {\r\n const hasTokenCookie = hasCookie(req.headers.cookie, sessionCookieName);\r\n if (!hasTokenCookie || !token) return;\r\n const currentSession = await readSession(token);\r\n if (currentSession?.id) {\r\n //? Sliding expiration in cookie mode: keep browser token lifetime\r\n //? aligned with Redis TTL.\r\n res.setHeader('Set-Cookie', `${sessionCookieName}=${token}; ${sessionCookieOptions}`);\r\n }\r\n};\r\n\r\nconst parseRequestParams = async ({\r\n req,\r\n res,\r\n method,\r\n routePath,\r\n queryString,\r\n requestId,\r\n shouldLogDev,\r\n}: {\r\n req: IncomingMessage;\r\n res: ServerResponse;\r\n method: 'GET' | 'POST' | 'PUT' | 'DELETE';\r\n routePath: string;\r\n queryString: string | undefined;\r\n requestId: string;\r\n shouldLogDev: boolean;\r\n}): Promise<object | null> => {\r\n let params: object | null = await getParams({ method, req, res, queryString });\r\n if (res.writableEnded) return null;\r\n\r\n if (params && typeof params === 'object' && Object.keys(params).length > 0) {\r\n if (shouldLogDev) {\r\n getLogger().debug(`[${requestId}] ${method} ${routePath}`, {\r\n params: sanitizeForLog(params) as Record<string, unknown>,\r\n });\r\n }\r\n } else {\r\n if (shouldLogDev) {\r\n getLogger().debug(`[${requestId}] ${method} ${routePath}`);\r\n }\r\n params = {};\r\n }\r\n return params;\r\n};\r\n\r\nconst handleHttpRequestInner = async (\r\n req: IncomingMessage,\r\n res: ServerResponse,\r\n options: CreateLuckyStackServerOptions\r\n): Promise<void> => {\r\n const config = getProjectConfig();\r\n const shouldLogDev = config.logging.devLogs;\r\n const sessionCookieName = config.http.sessionCookieName;\r\n const sessionCookieOptions = buildSessionCookieOptions(\r\n config.session.expiryDays,\r\n //? Honor the explicit `http.sessionCookieSecure` override (CORE-39), else the\r\n //? `SECURE` env flag — shared with the OAuth state cookie via\r\n //? `resolveCookieSecure` so the two can never drift (WAVE4).\r\n resolveCookieSecure(config.http.sessionCookieSecure, process.env.SECURE),\r\n config.http,\r\n );\r\n\r\n //? Parse the path up-front so the origin gate can consult the exempt-path\r\n //? registry (registered webhooks) before it would otherwise 403 a\r\n //? header-less, state-changing request.\r\n //? SEC: decode percent-encoded characters so that e.g. `/auth%2Flogout` cannot\r\n //? bypass route guards that compare against plain `/auth/logout`. Malformed\r\n //? encoding returns 400 so the request does not silently fall through.\r\n const url = req.url ?? '/';\r\n const [routePathRaw, queryStringRaw] = url.split('?');\r\n const [decodeError, decodedPath] = tryCatchSync(() => decodeURIComponent(routePathRaw ?? '/'));\r\n if (decodeError || decodedPath === null) {\r\n res.statusCode = 400;\r\n res.setHeader('Content-Type', 'text/plain');\r\n res.end('Bad Request');\r\n return;\r\n }\r\n const routePath = decodedPath;\r\n const queryString = queryStringRaw ?? '';\r\n\r\n const { origin, rejected } = enforceOriginPolicy(req, res, routePath);\r\n if (rejected) return;\r\n\r\n setSecurityHeaders(req, res, origin);\r\n\r\n //? Honor an incoming x-request-id (idempotent for proxies/retries) or\r\n //? generate a fresh UUID. Echo back as a response header so client-side\r\n //? logs and Sentry can correlate.\r\n //? SEC: validate before reflecting — only alphanumeric + hyphens, max 128 chars,\r\n //? to prevent header-injection via a crafted x-request-id value.\r\n const incomingRequestId = req.headers['x-request-id'];\r\n const rawRequestId = Array.isArray(incomingRequestId) ? incomingRequestId[0] : incomingRequestId;\r\n const requestId = (rawRequestId && /^[a-zA-Z0-9-]{1,128}$/.test(rawRequestId)) ? rawRequestId : randomUUID();\r\n res.setHeader('X-Request-Id', requestId);\r\n\r\n //? `preHttpRequest` fires before any route dispatch. Use to instrument\r\n //? requests (latency timer, audit log), enforce IP allow-lists, or stop\r\n //? specific paths with a custom error. Header subset excludes auth/cookie.\r\n const safeHeaders: Record<string, string> = {};\r\n for (const [k, v] of Object.entries(req.headers)) {\r\n if (k === 'authorization' || k === 'cookie' || k === 'set-cookie' || k === 'x-csrf-token' || k === 'x-test-reset-token' || k === 'x-session-based-token') continue;\r\n safeHeaders[k] = Array.isArray(v) ? v.join(', ') : (v ?? '');\r\n }\r\n const preHttpResult = await dispatchHook('preHttpRequest', {\r\n method: req.method?.toUpperCase() ?? 'GET',\r\n url: req.url ?? '/',\r\n requestId,\r\n origin,\r\n headers: safeHeaders,\r\n });\r\n if (preHttpResult.stopped) {\r\n res.statusCode = preHttpResult.signal.httpStatus ?? 403;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ status: 'error', errorCode: preHttpResult.signal.errorCode }));\r\n return;\r\n }\r\n\r\n if (req.method === 'OPTIONS') {\r\n res.writeHead(204);\r\n res.end();\r\n return;\r\n }\r\n\r\n const method = req.method;\r\n\r\n if (method !== 'GET' && method !== 'POST' && method !== 'PUT' && method !== 'DELETE') {\r\n res.statusCode = 404;\r\n res.setHeader('Content-Type', 'text/plain');\r\n res.end(`method: ${String(method)} not supported, use one of: GET, POST, PUT, DELETE`);\r\n return;\r\n }\r\n\r\n const token = extractTokenFromRequest(req);\r\n await refreshSessionCookieIfPresent({ req, res, token, sessionCookieName, sessionCookieOptions });\r\n\r\n const csrfRejected = await enforceCsrfOnStateChangingRequest({ req, res, routePath, token, requestId });\r\n if (csrfRejected) return;\r\n\r\n const baseCtx: Omit<HttpRouteContext, 'params'> = {\r\n req,\r\n res,\r\n options,\r\n routePath,\r\n queryString,\r\n method,\r\n token,\r\n requestId,\r\n sessionCookieOptions,\r\n };\r\n\r\n //? Route /auth/csrf, health probes, _test/reset, favicon — all fast paths\r\n //? that should not consume the request body.\r\n if (await dispatchRoutes(PRE_PARAMS_ROUTES, { ...baseCtx, params: {} })) return;\r\n\r\n const params = await parseRequestParams({\r\n req, res, method, routePath, queryString, requestId, shouldLogDev,\r\n });\r\n if (params === null) return;\r\n\r\n await dispatchRoutes(POST_PARAMS_ROUTES, { ...baseCtx, params });\r\n};\r\n\r\nexport const handleHttpRequest = async (\r\n req: IncomingMessage,\r\n res: ServerResponse,\r\n options: CreateLuckyStackServerOptions\r\n): Promise<void> => {\r\n //? Top-level error boundary: catches any unhandled throw that escapes route\r\n //? handlers and prevents it from propagating into the Node.js\r\n //? unhandled-rejection handler (which would crash the process in Node ≥15).\r\n //? Returns 500 so the client gets a defined response instead of a hang.\r\n const [error] = await tryCatch(() => handleHttpRequestInner(req, res, options));\r\n if (error && !res.writableEnded) {\r\n getLogger().error('handleHttpRequest: unhandled error', { err: error });\r\n res.statusCode = 500;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ status: 'error', errorCode: 'server.internalError' }));\r\n }\r\n};\r\n","//? Recursive log sanitizer: never log known-sensitive keys. Reads the\r\n//? extensible redacted-key registry from core so feature packages can register\r\n//? their own domain-specific keys (password, apiKey, mrn, etc.) at boot.\r\n\r\nimport { getProjectConfig, isRedactedLogKey } from '@luckystack/core';\r\n\r\nconst REDACTED_PLACEHOLDER = '[REDACTED]';\r\nconst TRUNCATED_PLACEHOLDER = '[TRUNCATED]';\r\n\r\n//? Bound recursion so an attacker-supplied deeply-nested JSON body (within the\r\n//? request body cap) cannot stack-overflow the dev-log sanitizer. Past this\r\n//? depth we emit a sentinel instead of recursing further.\r\nconst MAX_SANITIZE_DEPTH = 40;\r\n\r\nconst isRedactedKey = (key: string): boolean => {\r\n if (isRedactedLogKey(key)) return true;\r\n return key.toLowerCase() === getProjectConfig().http.sessionCookieName.toLowerCase();\r\n};\r\n\r\nconst sanitizeWithGuards = (value: unknown, depth: number, seen: WeakSet<object>): unknown => {\r\n if (value === null || typeof value !== 'object') return value;\r\n //? Cycle guard: a self-referential object would otherwise recurse forever.\r\n if (seen.has(value)) return TRUNCATED_PLACEHOLDER;\r\n if (depth >= MAX_SANITIZE_DEPTH) return TRUNCATED_PLACEHOLDER;\r\n\r\n seen.add(value);\r\n try {\r\n if (Array.isArray(value)) {\r\n return value.map((entry) => sanitizeWithGuards(entry, depth + 1, seen));\r\n }\r\n const out: Record<string, unknown> = {};\r\n for (const [key, val] of Object.entries(value as Record<string, unknown>)) {\r\n out[key] = isRedactedKey(key) ? REDACTED_PLACEHOLDER : sanitizeWithGuards(val, depth + 1, seen);\r\n }\r\n return out;\r\n } finally {\r\n //? Allow the same object to appear in sibling branches (shared, not cyclic).\r\n seen.delete(value);\r\n }\r\n};\r\n\r\nexport const sanitizeForLog = (value: unknown): unknown => sanitizeWithGuards(value, 0, new WeakSet());\r\n","//? Security-headers builder registry. The framework ships sensible defaults\r\n//? (Referrer-Policy, X-Frame-Options, X-XSS-Protection, X-Content-Type-Options)\r\n//? read from `projectConfig.http.securityHeaders`. Consumers can register\r\n//? a custom builder to add Content-Security-Policy, Strict-Transport-Security,\r\n//? Permissions-Policy, or to override defaults per request.\r\n//?\r\n//? Resolution order:\r\n//? 1. Built-in defaults from `projectConfig.http.securityHeaders`.\r\n//? 2. Headers from the registered builder (override OR augment).\r\n//?\r\n//? A nullish return from the builder means \"use defaults only\". An object\r\n//? merges on top of defaults (later wins per key).\r\n\r\nimport type { IncomingMessage } from 'node:http';\r\nimport { createRegistry } from '@luckystack/core';\r\n\r\nexport type SecurityHeadersBuilder = (req: IncomingMessage) => Record<string, string> | null | undefined;\r\n\r\n//? Single-slot registry: one builder at a time, last-write-wins, `null` is the\r\n//? unregistered baseline. Backed by core's `createRegistry` so the register /\r\n//? read / reset triad isn't hand-rolled. The public `registerSecurityHeaders` /\r\n//? `getSecurityHeadersBuilder` signatures below are preserved verbatim.\r\nconst builderRegistry = createRegistry<SecurityHeadersBuilder | null>(null);\r\n\r\n/**\r\n * Register a custom security-headers builder. Called for every HTTP\r\n * request; return a plain object that gets merged on top of the framework\r\n * defaults. Use for Content-Security-Policy, HSTS, Permissions-Policy.\r\n *\r\n * Last-write-wins: subsequent calls replace the previous builder. Pass\r\n * `null` to unregister.\r\n */\r\nexport const registerSecurityHeaders = (builder: SecurityHeadersBuilder | null): void => {\r\n builderRegistry.register(builder);\r\n};\r\n\r\n/** Read the active builder (or null). */\r\nexport const getSecurityHeadersBuilder = (): SecurityHeadersBuilder | null => builderRegistry.get();\r\n","import type { IncomingMessage, ServerResponse } from 'node:http';\r\nimport { dispatchHook, getCookieValue, getCsrfConfig, getProjectConfig, readSession } from '@luckystack/core';\r\nimport { capabilities } from '../capabilities';\r\nimport { isOriginExemptPath } from '../originExemptRegistry';\r\nimport { timingSafeStringEqual } from './timingSafeEqual';\r\n\r\n//? Returns true when the request was rejected (CSRF mismatch) and the response\r\n//? has been ended. Caller should bail out of the request loop.\r\nexport const enforceCsrfOnStateChangingRequest = async ({\r\n req,\r\n res,\r\n routePath,\r\n token,\r\n requestId,\r\n}: {\r\n req: IncomingMessage;\r\n res: ServerResponse;\r\n routePath: string;\r\n token: string | null;\r\n requestId?: string;\r\n}): Promise<boolean> => {\r\n const config = getProjectConfig();\r\n const isCookieMode = !config.session.basedToken;\r\n //? HEAD is read-only and excluded from `enforceOriginPolicy`; mirror that\r\n //? here so the two state-changing predicates agree (HEAD is 404'd at the\r\n //? method gate before this runs, so this is parity, not a behavior change).\r\n const isStateChanging = req.method !== 'GET' && req.method !== 'HEAD' && req.method !== 'OPTIONS';\r\n const isCallbackPath = routePath.startsWith('/auth/callback');\r\n //? The credentials login/register endpoint is the session BOOTSTRAP. Requiring a\r\n //? pre-existing session's CSRF token to authenticate is circular, and it blocks\r\n //? legitimate same-site re-login / register while a (stale) session cookie is\r\n //? present. Cross-site abuse is already prevented by the SameSite=Strict session\r\n //? cookie — a cross-site POST never carries it, so `token` would be absent here\r\n //? and this guard wouldn't fire anyway. Exempting it removes no real protection.\r\n //? ADR 0024: the email-code + 2FA LOGIN routes share the credentials-route\r\n //? bootstrap semantics — they authenticate via their own factor (code /\r\n //? challenge token), may run while a stale session cookie is present, and a\r\n //? cross-site POST never carries the SameSite=Strict cookie anyway. The\r\n //? authenticated 2FA ENROLLMENT routes (/auth/api/2fa/setup|enable|disable|\r\n //? recovery-codes) are deliberately NOT exempt — they are state-changing\r\n //? actions on a live session.\r\n const AUTH_BOOTSTRAP_PATHS = new Set([\r\n '/auth/api/credentials',\r\n '/auth/api/email-code/request',\r\n '/auth/api/email-code/verify',\r\n '/auth/api/2fa',\r\n '/auth/api/2fa/email-code',\r\n ]);\r\n const isAuthBootstrap = AUTH_BOOTSTRAP_PATHS.has(routePath);\r\n //? CSRF covers all framework routes (/api/, /sync/, /auth/api/) AND\r\n //? state-changing custom routes registered via `registerCustomRoute` that\r\n //? are not marked origin-exempt (those authenticate via HMAC/signature, not\r\n //? the session cookie, so the double-submit check is irrelevant there).\r\n //?\r\n //? IMPORTANT: `registerOriginExemptPath({ pathPrefix })` exempts ALL routes\r\n //? whose path starts with the given prefix from BOTH the origin gate AND CSRF.\r\n //? Register only the narrowest prefix needed (prefer ending with `/` — e.g.\r\n //? `/webhooks/` — to avoid accidentally exempting `/webhooksAdmin`).\r\n const isExemptFromCsrf =\r\n isAuthBootstrap\r\n || isCallbackPath\r\n || isOriginExemptPath(routePath);\r\n const isCsrfCandidate =\r\n routePath.startsWith('/api/')\r\n || routePath.startsWith('/sync/')\r\n || routePath.startsWith('/auth/api/')\r\n || (!routePath.startsWith('/auth/') && !routePath.startsWith('/assets/'));\r\n\r\n //? CSRF only applies to cookie-mode, state-changing routes not already\r\n //? exempted by another auth mechanism (origin-exempt webhooks, bootstrap).\r\n if (!(isCookieMode && isStateChanging && isCsrfCandidate && !isExemptFromCsrf)) {\r\n return false;\r\n }\r\n\r\n //? Read the active CSRF header name from the registry so consumers\r\n //? can rename it (e.g. legacy `x-xsrf-token`, custom `x-app-csrf`).\r\n const csrfConfig = getCsrfConfig();\r\n const headerKey = csrfConfig.headerName.toLowerCase();\r\n const headerValue = req.headers[headerKey];\r\n const provided = Array.isArray(headerValue) ? headerValue[0] : headerValue;\r\n\r\n //? Login-ABSENT path: stateless DOUBLE-SUBMIT. The session-bound token store\r\n //? lives in @luckystack/login; without it we compare the csrf COOKIE value\r\n //? against the x-csrf-token HEADER (both seeded by GET /auth/csrf). No session\r\n //? read. A cross-site POST can't read the cookie value to forge the header.\r\n if (!capabilities.login) {\r\n const cookieValue = getCookieValue(req.headers.cookie, csrfConfig.cookieName);\r\n if (cookieValue && provided && timingSafeStringEqual(provided, cookieValue)) return false;\r\n\r\n void dispatchHook('csrfMismatch', {\r\n route: routePath,\r\n method: req.method,\r\n requestId,\r\n userId: undefined,\r\n providedToken: Boolean(provided),\r\n });\r\n\r\n res.statusCode = 403;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({\r\n status: 'error',\r\n errorCode: 'auth.csrfMismatch',\r\n message: 'CSRF token missing or invalid. Fetch /auth/csrf first.',\r\n }));\r\n return true;\r\n }\r\n\r\n //? Login-PRESENT path: session-bound CSRF (unchanged). Requires a session\r\n //? token — without one there is no session to protect, so do not enforce.\r\n if (!token) return false;\r\n\r\n const csrfSession = await readSession(token);\r\n if (!csrfSession?.id) return false;\r\n\r\n if (provided && csrfSession.csrfToken && timingSafeStringEqual(provided, csrfSession.csrfToken)) return false;\r\n\r\n void dispatchHook('csrfMismatch', {\r\n route: routePath,\r\n method: req.method,\r\n requestId,\r\n userId: csrfSession.id,\r\n providedToken: Boolean(provided),\r\n });\r\n\r\n res.statusCode = 403;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({\r\n status: 'error',\r\n errorCode: 'auth.csrfMismatch',\r\n message: 'CSRF token missing or invalid. Fetch /auth/csrf first.',\r\n }));\r\n return true;\r\n};\r\n","//? Optional-package capability layer (0.2.0). `@luckystack/login`, `presence`,\r\n//? and `sync` are OPTIONAL peers of `@luckystack/server`. We detect each once at\r\n//? boot via `require.resolve` (cheap, cached) and lazy-import it only when\r\n//? present, so a consumer who omits one gets graceful degradation instead of an\r\n//? `ERR_MODULE_NOT_FOUND` crash. Mirrors `@luckystack/error-tracking`'s resolve\r\n//? guard. Session reads/writes do NOT go through here — they use core's\r\n//? `readSession`/`writeSession` accessors, which login populates via the session\r\n//? provider registry. This layer is for the ROUTE/SOCKET wiring that must be\r\n//? conditionally registered (auth routes, the sync listener, presence lifecycle).\r\n\r\nimport { createRequire } from 'node:module';\r\n\r\nconst localRequire = createRequire(import.meta.url);\r\n\r\n//? Detect whether an optional package (or one of its subpaths) is installed.\r\n//?\r\n//? IMPORTANT: every `@luckystack/*` package ships an **import-only** `exports`\r\n//? map (`{ \"import\": ..., \"types\": ... }` — no `\"require\"`/`\"default\"`). A\r\n//? CJS `require.resolve()` resolves with CJS conditions and therefore THROWS\r\n//? `ERR_PACKAGE_PATH_NOT_EXPORTED` on those maps — i.e. it reports every\r\n//? installed `@luckystack/*` package as ABSENT. We must resolve with the ESM\r\n//? resolver (`import.meta.resolve`, which honors the `\"import\"` condition).\r\n//? `createRequire().resolve` is kept only as a fallback for Node < 20.6 where\r\n//? synchronous `import.meta.resolve` is unavailable.\r\nconst esmResolve = (import.meta as { resolve?: (specifier: string) => string }).resolve;\r\n\r\n//? Warn once when `import.meta.resolve` is absent (Node < 20.6). The CJS\r\n//? fallback misreports all import-only packages as absent, so optional\r\n//? features (login, presence, sync) silently degrade. Surface this so the\r\n//? operator can upgrade Node rather than debugging mysterious capability gaps.\r\nlet _warnedResolverMissing = false;\r\nconst warnResolverMissingOnce = (): void => {\r\n if (_warnedResolverMissing) return;\r\n _warnedResolverMissing = true;\r\n // eslint-disable-next-line no-console -- intentional boot diagnostic; logger not yet available\r\n console.warn(\r\n '[luckystack:capabilities] import.meta.resolve is unavailable (Node < 20.6). ' +\r\n 'Optional package detection falls back to require.resolve, which cannot resolve ' +\r\n 'import-only exports maps and may misreport @luckystack/* packages as absent. ' +\r\n 'Upgrade to Node >= 20.6 to ensure correct capability detection.',\r\n );\r\n};\r\n\r\nconst has = (pkg: string): boolean => {\r\n if (typeof esmResolve === 'function') {\r\n try {\r\n esmResolve(pkg);\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n }\r\n //? CJS fallback: `ERR_PACKAGE_PATH_NOT_EXPORTED` means the package IS\r\n //? installed but its exports map has no CJS condition — treat as PRESENT.\r\n //? Any other error (MODULE_NOT_FOUND, ERR_MODULE_NOT_FOUND) means absent.\r\n warnResolverMissingOnce();\r\n try {\r\n localRequire.resolve(pkg);\r\n return true;\r\n } catch (error: unknown) {\r\n if (error instanceof Error && (error as NodeJS.ErrnoException).code === 'ERR_PACKAGE_PATH_NOT_EXPORTED') {\r\n return true;\r\n }\r\n return false;\r\n }\r\n};\r\n\r\n//? Resolved once at module load. `as const` so callers branch on literal booleans.\r\nexport const capabilities = {\r\n login: has('@luckystack/login'),\r\n presence: has('@luckystack/presence'),\r\n sync: has('@luckystack/sync'),\r\n} as const;\r\n\r\n//? Optional packages that ship a side-effect `@luckystack/<pkg>/register`\r\n//? subpath. `bootstrapLuckyStack` resolve-guards + imports each one BEFORE the\r\n//? consumer overlay folder so a hand-written overlay (last writer) still wins.\r\n//? Each `./register` is an env-driven, idempotent no-op when its env is unset.\r\n//? Order is topological (mirrors `OVERLAY_ORDER`). NOTES:\r\n//? - `sync` is excluded: it has no server-side register; its add-later wiring\r\n//? is the client receive bridge (`@luckystack/sync/client` attachSyncReceiver).\r\n//? - `secret-manager` is excluded: it is resolved explicitly in the consumer\r\n//? entry (fails-OPEN), not via a register subpath.\r\nexport const OPTIONAL_PACKAGES = [\r\n 'login',\r\n 'email',\r\n 'error-tracking',\r\n 'presence',\r\n 'cron',\r\n 'docs-ui',\r\n] as const;\r\n\r\n//? Cheap resolve guard reused by the bootstrap auto-detect loop: true when the\r\n//? given specifier (e.g. `@luckystack/login/register`) is installed + resolvable.\r\nexport const canResolve = (specifier: string): boolean => has(specifier);\r\n\r\nlet loginMod: typeof import('@luckystack/login') | null | undefined;\r\nexport const getLogin = async (): Promise<typeof import('@luckystack/login') | null> => {\r\n if (loginMod !== undefined) return loginMod;\r\n loginMod = capabilities.login ? await import('@luckystack/login') : null;\r\n return loginMod;\r\n};\r\n\r\nlet presenceMod: typeof import('@luckystack/presence') | null | undefined;\r\nexport const getPresence = async (): Promise<typeof import('@luckystack/presence') | null> => {\r\n if (presenceMod !== undefined) return presenceMod;\r\n presenceMod = capabilities.presence ? await import('@luckystack/presence') : null;\r\n return presenceMod;\r\n};\r\n\r\nlet syncMod: typeof import('@luckystack/sync') | null | undefined;\r\nexport const getSync = async (): Promise<typeof import('@luckystack/sync') | null> => {\r\n if (syncMod !== undefined) return syncMod;\r\n syncMod = capabilities.sync ? await import('@luckystack/sync') : null;\r\n return syncMod;\r\n};\r\n","//? Origin-exempt path registry. `enforceOriginPolicy` fail-closes every\r\n//? state-changing POST that carries no browser-attributable Origin/Referer —\r\n//? which is exactly a legitimate server-to-server webhook (GitLab, Stripe, ...).\r\n//? Registering a path prefix here lets such an endpoint through the browser\r\n//? CSRF/origin gate.\r\n//?\r\n//? SECURITY: origin exemption is NOT authentication. It only removes the\r\n//? browser-origin check; the exempted handler MUST authenticate the caller\r\n//? itself (HMAC over the raw body, a shared-secret header, mTLS, ...). Pair\r\n//? with a `'pre-params'` custom route so the handler can read the raw body for\r\n//? signature verification. Empty by default — opt-in only. Do NOT register a\r\n//? prefix that overlaps framework routes (`/api`, `/auth`, `/sync`); keep\r\n//? webhooks on a dedicated prefix like `/webhooks/`. See\r\n//? docs/ARCHITECTURE_HTTP.md.\r\n\r\nexport interface OriginExemptMatcher {\r\n /**\r\n * A route is exempt when its path equals this prefix OR continues past it on a\r\n * path-SEGMENT boundary (i.e. `<prefix>/...`). Matching is boundary-aware so a\r\n * prefix can't bleed into a sibling route — `/webhooks` exempts `/webhooks` and\r\n * `/webhooks/stripe` but NOT `/webhooksadmin`.\r\n */\r\n pathPrefix: string;\r\n}\r\n\r\nconst exemptPaths: OriginExemptMatcher[] = [];\r\n\r\nexport const registerOriginExemptPath = (matcher: OriginExemptMatcher): void => {\r\n exemptPaths.push(matcher);\r\n};\r\n\r\nexport const getOriginExemptPaths = (): readonly OriginExemptMatcher[] => exemptPaths;\r\n\r\nexport const clearOriginExemptPaths = (): void => {\r\n exemptPaths.length = 0;\r\n};\r\n\r\n//? True when `routePath` matches a registered exempt prefix. Consulted by\r\n//? `enforceOriginPolicy` before it can 403 a header-less request AND by the CSRF\r\n//? middleware — so a single mis-matching prefix would drop BOTH protections.\r\n//? Match on a path-SEGMENT boundary (exact, or `<prefix>/...`) so a prefix like\r\n//? `/webhooks` can't silently exempt a sibling like `/webhooksadmin` (L5).\r\nexport const isOriginExemptPath = (routePath: string): boolean =>\r\n exemptPaths.some(({ pathPrefix }) => {\r\n if (!pathPrefix) return false;\r\n if (routePath === pathPrefix) return true;\r\n const boundary = pathPrefix.endsWith('/') ? pathPrefix : `${pathPrefix}/`;\r\n return routePath.startsWith(boundary);\r\n });\r\n","import { timingSafeEqual as cryptoTimingSafeEqual } from 'node:crypto';\n\n//? Constant-time string comparison for security tokens (CSRF tokens, the\n//? test-reset token). Plain `===` short-circuits on the first differing byte,\n//? leaking length + prefix-match information through response timing. Compare\n//? the UTF-8 byte buffers with `crypto.timingSafeEqual`, which requires equal\n//? lengths — so we length-check first (itself not secret-dependent) and only\n//? run the constant-time compare on equal-length inputs.\nexport const timingSafeStringEqual = (a: string, b: string): boolean => {\n const aBuf = Buffer.from(a, 'utf8');\n const bBuf = Buffer.from(b, 'utf8');\n if (aBuf.length !== bBuf.length) return false;\n return cryptoTimingSafeEqual(aBuf, bBuf);\n};\n","import { randomBytes } from 'node:crypto';\r\nimport { getCsrfConfig, readSession, type CsrfCookieOptions } from '@luckystack/core';\r\nimport { capabilities } from '../capabilities';\r\nimport type { HttpRouteHandler } from './types';\r\n\r\n//? Serialize the configured CSRF cookie options into a Set-Cookie string. Only\r\n//? used on the login-ABSENT double-submit path; the login-present path delivers\r\n//? the token in the JSON body (session-bound, no cookie write here).\r\nconst serializeCsrfCookie = (name: string, value: string, opts: CsrfCookieOptions): string => {\r\n const parts = [`${name}=${value}`];\r\n if (opts.httpOnly) parts.push('HttpOnly');\r\n if (opts.sameSite) parts.push(`SameSite=${opts.sameSite.charAt(0).toUpperCase()}${opts.sameSite.slice(1)}`);\r\n if (opts.secure) parts.push('Secure');\r\n parts.push(`Path=${opts.path ?? '/'}`);\r\n if (typeof opts.maxAgeMs === 'number') parts.push(`Max-Age=${String(Math.floor(opts.maxAgeMs / 1000))}`);\r\n return parts.join('; ');\r\n};\r\n\r\nexport const handleCsrfRoute: HttpRouteHandler = async ({ res, routePath, token }) => {\r\n if (routePath !== '/auth/csrf') return false;\r\n\r\n const csrfConfig = getCsrfConfig();\r\n\r\n //? Login-ABSENT (unauthenticated app): there is no per-session CSRF token, so\r\n //? issue a stateless DOUBLE-SUBMIT token — set it as the csrf cookie AND return\r\n //? it in the body. `enforceCsrfOnStateChangingRequest` later compares the cookie\r\n //? value against the `x-csrf-token` header (no session read). A cross-site\r\n //? attacker can neither read the body (CORS) nor forge the header to match the\r\n //? victim's cookie, so this blocks cross-site state changes without login.\r\n if (!capabilities.login) {\r\n //? Stateless double-submit: the SAME random value is set as the CSRF cookie\r\n //? and echoed in the JSON body. `enforceCsrfOnStateChangingRequest` later\r\n //? compares the cookie against the `x-csrf-token` request header. The cookie\r\n //? is deliberately NOT HttpOnly (the client JS must read it to echo it as the\r\n //? header); cross-site protection rests on SameSite + same-origin/CORS — an\r\n //? attacker on another origin can neither read the cookie nor the JSON body,\r\n //? so they cannot forge a matching header.\r\n //?\r\n //? KNOWN LIMITATION: without HMAC binding to a server secret this token\r\n //? cannot survive a subdomain compromise (an attacker on sub.example.com\r\n //? can set a cookie on .example.com). This is accepted in the login-absent\r\n //? posture; add `registerCsrfConfig({ sign: true })` to enable HMAC signing\r\n //? when that threat model applies.\r\n const doubleSubmit = randomBytes(csrfConfig.tokenLength).toString('hex');\r\n res.statusCode = 200;\r\n //? Resolve `Secure` per-environment (env SECURE) when the config leaves it\r\n //? unset — mirrors the session cookie so the double-submit cookie isn't\r\n //? dropped over plain HTTP in dev (which would 403 every POST). An explicit\r\n //? config `secure` (true/false) always wins.\r\n const cookieOptions: CsrfCookieOptions = {\r\n ...csrfConfig.cookieOptions,\r\n secure: csrfConfig.cookieOptions.secure ?? (process.env.SECURE === 'true'),\r\n };\r\n res.setHeader('Set-Cookie', serializeCsrfCookie(csrfConfig.cookieName, doubleSubmit, cookieOptions));\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ status: 'success', csrfToken: doubleSubmit }));\r\n return true;\r\n }\r\n\r\n //? Login-PRESENT: session-bound CSRF token (unchanged behaviour).\r\n if (!token) {\r\n res.statusCode = 401;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ status: 'error', errorCode: 'auth.unauthenticated' }));\r\n return true;\r\n }\r\n const csrfSession = await readSession(token);\r\n if (!csrfSession?.id) {\r\n res.statusCode = 401;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ status: 'error', errorCode: 'auth.unauthenticated' }));\r\n return true;\r\n }\r\n res.statusCode = 200;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({\r\n status: 'success',\r\n csrfToken: csrfSession.csrfToken ?? null,\r\n }));\r\n return true;\r\n};\r\n","import type { HttpRouteHandler } from './types';\n\nexport const handleFaviconRoute: HttpRouteHandler = async ({ res, routePath, options }) => {\n if (routePath !== '/favicon.ico') return false;\n if (options.serveFavicon) {\n await options.serveFavicon(res);\n return true;\n }\n res.writeHead(404);\n res.end();\n return true;\n};\n","import {\r\n computeSynchronizedEnvHashes,\r\n describeHealthHashConfig,\r\n getDbHealthCheck,\r\n getProjectConfig,\r\n isPrismaClientRegistered,\r\n isPrismaClientResolvable,\r\n prisma,\r\n readBootUuid,\r\n redis,\r\n resolveEnvKey,\r\n tryCatch,\r\n} from '@luckystack/core';\r\nimport type { HttpRouteHandler } from './types';\r\n\r\n//? Cross-provider connectivity ping. Prisma's generated TypeScript surface\r\n//? differs per provider: SQL providers expose `$queryRaw`, MongoDB exposes\r\n//? `$runCommandRaw`. The framework can't know which one the consumer's schema\r\n//? uses, so this single seam asserts both as optional and probes at runtime.\r\n//? Detecting the provider via `_engineConfig.activeProvider` is private API\r\n//? and has drifted between Prisma major versions — runtime probing is more\r\n//? robust.\r\ninterface PrismaPingShape {\r\n $queryRaw?: (template: TemplateStringsArray, ...values: unknown[]) => Promise<unknown>;\r\n $runCommandRaw?: (command: Record<string, unknown>) => Promise<unknown>;\r\n}\r\n\r\nconst pingPrisma = async (): Promise<boolean> => {\r\n //? Prisma's generated client typings don't expose `$queryRaw` /\r\n //? `$runCommandRaw` uniformly (depends on the active datasource). The\r\n //? `PrismaPingShape` is our minimal local probe interface; the boundary\r\n //? cast is the structural exception the strict-typing policy allows.\r\n // eslint-disable-next-line no-restricted-syntax -- Prisma datasource-conditional shape\r\n const client = prisma as unknown as PrismaPingShape;\r\n //? Capture each function into a local before calling so the typeof\r\n //? narrow holds without `!`. The shape `PrismaPingShape` declares both\r\n //? methods optional because Prisma's generated types include one or the\r\n //? other depending on active provider — capturing into a local also\r\n //? removes the assertion-style cast that the strict-typing policy disallows.\r\n const queryRaw = client.$queryRaw;\r\n if (typeof queryRaw === 'function') {\r\n const [sqlError] = await tryCatch(() => queryRaw`SELECT 1`);\r\n if (!sqlError) return true;\r\n }\r\n const runCommandRaw = client.$runCommandRaw;\r\n if (typeof runCommandRaw === 'function') {\r\n const [mongoError] = await tryCatch(() => runCommandRaw({ ping: 1 }));\r\n return !mongoError;\r\n }\r\n return false;\r\n};\r\n\r\n//? Database readiness (ADR 0020): a registered custom probe wins; otherwise\r\n//? the built-in Prisma ping runs only when Prisma is actually part of this\r\n//? install (registered client or resolvable '@prisma/client'). A deliberately\r\n//? DB-less project (orm: 'none') reports 'skipped' and can still go ready —\r\n//? previously the hard-wired Prisma ping kept it 503 forever.\r\nconst checkDatabaseReady = async (): Promise<boolean | 'skipped'> => {\r\n const registered = getDbHealthCheck();\r\n if (registered) {\r\n const [error, result] = await tryCatch(async () => registered());\r\n if (error || result === null) return false;\r\n return result;\r\n }\r\n if (isPrismaClientRegistered() || isPrismaClientResolvable()) return pingPrisma();\r\n return 'skipped';\r\n};\r\n\r\nexport const handleLivezRoute: HttpRouteHandler = ({ res, routePath }) => {\r\n if (routePath !== getProjectConfig().http.liveEndpoint) return Promise.resolve(false);\r\n res.statusCode = 200;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ status: 'live' }));\r\n return Promise.resolve(true);\r\n};\r\n\r\nexport const handleReadyzRoute: HttpRouteHandler = async ({ res, routePath }) => {\r\n if (routePath !== getProjectConfig().http.readyEndpoint) return false;\r\n\r\n //? SEC: this endpoint is intentionally unauthenticated (orchestrators and load\r\n //? balancers probe it without credentials). Each call pings Redis + Prisma, so\r\n //? callers can trigger non-trivial backend load. Mitigate at the infra layer\r\n //? (network policy, rate-limiting ingress) rather than here, to keep the probe\r\n //? surface simple and avoid circular-dependency on session/auth bootstrap.\r\n const bootUuid = await readBootUuid();\r\n\r\n const [redisError, pong] = await tryCatch(() => redis.ping());\r\n const redisOk = !redisError && (pong === 'PONG' || Boolean(pong));\r\n\r\n const databaseResult = await checkDatabaseReady();\r\n\r\n const ready = Boolean(bootUuid) && redisOk && databaseResult !== false;\r\n res.statusCode = ready ? 200 : 503;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({\r\n status: ready ? 'ready' : 'not-ready',\r\n //? `prisma` kept for backward compatibility with existing probes/dashboards\r\n //? (true when the database check passed OR was deliberately skipped);\r\n //? `database` carries the richer tri-state.\r\n checks: {\r\n bootUuid: Boolean(bootUuid),\r\n redis: redisOk,\r\n database: databaseResult,\r\n prisma: databaseResult !== false,\r\n },\r\n }));\r\n return true;\r\n};\r\n\r\nexport const handleHealthRoute: HttpRouteHandler = async ({ res, routePath }) => {\r\n if (routePath !== getProjectConfig().http.healthEndpoint) return false;\r\n\r\n //? SEC: unauthenticated by design (router and monitoring systems probe without\r\n //? session tokens). Consider binding this to an internal/loopback interface only\r\n //? in production, or protecting it with `registerCustomRoute` + a probe token,\r\n //? to prevent external amplification of the Prisma + Redis ping path.\r\n const bootUuid = await readBootUuid();\r\n //? SEC-13: pass the boot UUID so the `'@bootUuid'` salt sentinel (the 0.2.0\r\n //? default `http.healthHash` = `{ mode: 'hmac', salt: '@bootUuid' }`) resolves\r\n //? to a per-boot HMAC key. Previously the arg was omitted, so the sentinel\r\n //? always collapsed to `'plain'` and `/_health` leaked a stable, unsalted\r\n //? `sha256(secret)` fingerprint of every synchronized env value.\r\n const synchronizedHashes = computeSynchronizedEnvHashes(bootUuid);\r\n res.statusCode = bootUuid ? 200 : 503;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({\r\n status: bootUuid ? 'ok' : 'degraded',\r\n bootUuid,\r\n envKey: resolveEnvKey(),\r\n synchronizedHashes,\r\n //? Tell the router HOW these hashes were produced (mode + whether the salt is\r\n //? the `@bootUuid` sentinel) so it can hash its local values with the SAME\r\n //? config instead of its own default. Never exposes a static salt (a secret).\r\n healthHash: describeHealthHashConfig(),\r\n }));\r\n return true;\r\n};\r\n","import {\r\n clearAllHooks,\r\n clearAllRateLimits,\r\n getProjectConfig,\r\n formatKey,\r\n redis,\r\n tryCatch,\r\n} from '@luckystack/core';\r\nimport { timingSafeStringEqual } from './timingSafeEqual';\r\nimport type { HttpRouteHandler } from './types';\r\n\r\nexport const handleTestResetRoute: HttpRouteHandler = async ({ req, res, routePath }) => {\r\n if (routePath !== getProjectConfig().http.testResetEndpoint) return false;\r\n\r\n //? Fail-closed: require explicit dev/test NODE_ENV (not just \"anything but\r\n //? production\") so a missing or misconfigured NODE_ENV cannot expose this\r\n //? destructive endpoint. Also require TEST_RESET_TOKEN unconditionally —\r\n //? an unset token must NOT mean \"no auth required\".\r\n const nodeEnv = process.env.NODE_ENV;\r\n if (nodeEnv !== 'development' && nodeEnv !== 'test') {\r\n res.statusCode = 404;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ status: 'error', errorCode: 'notFound' }));\r\n return true;\r\n }\r\n //? Only POST performs the destructive reset (the documented contract). A GET/HEAD\r\n //? would also sidestep the origin gate's state-changing fail-closed branch, so\r\n //? reject any non-POST method. Runs AFTER the env check so prod still 404s rather\r\n //? than revealing the endpoint via a 405.\r\n if (req.method !== 'POST') {\r\n res.statusCode = 405;\r\n res.setHeader('Allow', 'POST');\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ status: 'error', errorCode: 'api.methodNotAllowed' }));\r\n return true;\r\n }\r\n const requiredToken = process.env.TEST_RESET_TOKEN;\r\n const providedToken = req.headers['x-test-reset-token'];\r\n const tokenValue = Array.isArray(providedToken) ? providedToken[0] : providedToken;\r\n if (!requiredToken || !tokenValue || !timingSafeStringEqual(tokenValue, requiredToken)) {\r\n res.statusCode = 403;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ status: 'error', errorCode: 'auth.forbidden' }));\r\n return true;\r\n }\r\n\r\n const cleared: string[] = [];\r\n await clearAllRateLimits();\r\n cleared.push('rateLimits');\r\n\r\n //? Flush sessions + activeUsers Redis keys so integration tests start from\r\n //? a clean slate. Patterns are derived through the shared `formatKey(...)`\r\n //? authority so they track any registered key formatter (see also\r\n //? session.ts, sessionAdapter.ts, rateLimiter.ts).\r\n const sessionPattern = `${formatKey('-session', '')}:*`;\r\n const activeUsersPattern = `${formatKey('-activeUsers', '')}:*`;\r\n\r\n const scanAndDelete = async (pattern: string, label: string): Promise<number> => {\r\n const [error, deleted] = await tryCatch(async () => {\r\n let cursor = '0';\r\n let total = 0;\r\n do {\r\n const [next, keys] = await redis.scan(cursor, 'MATCH', pattern, 'COUNT', 200);\r\n cursor = next;\r\n if (Array.isArray(keys) && keys.length > 0) {\r\n await redis.del(...keys);\r\n total += keys.length;\r\n }\r\n } while (cursor !== '0');\r\n return total;\r\n });\r\n if (error || !deleted) return 0;\r\n if (deleted > 0) cleared.push(label);\r\n return deleted;\r\n };\r\n\r\n await scanAndDelete(sessionPattern, 'sessions');\r\n await scanAndDelete(activeUsersPattern, 'activeUsers');\r\n\r\n //? Opt-in hook clear via `?include=hooks` because clearing all hooks would\r\n //? also drop framework-internal handlers (e.g. presence postLogout). URL\r\n //? parsing failure is the expected branch for malformed `req.url`, so use\r\n //? `URL.canParse` instead of try/catch.\r\n const rawUrl = req.url ?? '/';\r\n //? Use a fixed loopback base — `req.headers.host` is client-controlled and\r\n //? must not influence URL resolution; only the path and query matter here.\r\n const includeFlag = URL.canParse(rawUrl, 'http://localhost')\r\n ? new URL(rawUrl, 'http://localhost').searchParams.get('include') ?? ''\r\n : '';\r\n if (includeFlag.split(',').map((s) => s.trim()).includes('hooks')) {\r\n clearAllHooks();\r\n cleared.push('hooks');\r\n }\r\n\r\n res.statusCode = 200;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ status: 'success', cleared }));\r\n return true;\r\n};\r\n","import { serveAvatar } from '@luckystack/core';\nimport type { HttpRouteHandler } from './types';\n\nexport const handleUploadsRoute: HttpRouteHandler = async ({ res, routePath }) => {\n if (!routePath.startsWith('/uploads/')) return false;\n await serveAvatar({ routePath, res });\n return true;\n};\n","import {\r\n checkRateLimit,\r\n dispatchHook,\r\n getLogger,\r\n getProjectConfig,\r\n resolveClientIp,\r\n} from '@luckystack/core';\r\nimport { getLogin } from '../capabilities';\r\nimport { resolveCookieSecure } from './sessionCookie';\r\nimport type { HttpRouteHandler } from './types';\r\n\r\nconst parseSessionBasedTokenHeader = (headerValue: string | string[] | undefined): boolean | null => {\r\n if (headerValue === undefined) return null;\r\n const value = Array.isArray(headerValue) ? headerValue[0] : headerValue;\r\n if (value === '1' || value === 'true') return true;\r\n if (value === '0' || value === 'false') return false;\r\n return null;\r\n};\r\n\r\n//? Reserved OAuth authorize-query params owned by the framework — a provider's\r\n//? `extraAuthorizationParams` (CFG-21) can override any OTHER key (e.g. `prompt`,\r\n//? `access_type`, `login_hint`) but never these, so a consumer config can't break\r\n//? the state / PKCE / redirect-URI binding the callback relies on.\r\nconst RESERVED_OAUTH_PARAMS = new Set([\r\n 'client_id',\r\n 'redirect_uri',\r\n 'scope',\r\n 'response_type',\r\n 'state',\r\n 'code_challenge',\r\n 'code_challenge_method',\r\n]);\r\n\r\nexport const handleAuthApiRoute: HttpRouteHandler = async ({\r\n req,\r\n res,\r\n routePath,\r\n token,\r\n params,\r\n sessionCookieOptions,\r\n}) => {\r\n if (!routePath.startsWith('/auth/api')) return false;\r\n\r\n //? @luckystack/login is optional. Without it there is no auth surface, so every\r\n //? /auth/api/* route reports the disabled contract instead of crashing.\r\n const login = await getLogin();\r\n if (!login) {\r\n res.setHeader('content-type', 'application/json; charset=utf-8');\r\n res.end(JSON.stringify({ status: false, reason: 'auth.disabled' }));\r\n return true;\r\n }\r\n\r\n const config = getProjectConfig();\r\n const sessionCookieName = config.http.sessionCookieName;\r\n const shouldLogDev = config.logging.devLogs;\r\n\r\n const providerName = routePath.split('/')[3];\r\n const provider = login.getOAuthProviders().find((p) => p.name === providerName);\r\n if (!provider?.name) {\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ status: false, reason: 'login.providerNotFound' }));\r\n return true;\r\n }\r\n\r\n if (login.isFullOAuthProvider(provider)) {\r\n //? Throttle OAuth-init BEFORE the Redis state write (M4). This branch runs on\r\n //? a plain unauthenticated GET navigation and calls `createOAuthState` (a Redis\r\n //? write) on every hit; the origin gate does not stop a header-less GET, so\r\n //? without a per-IP cap an anonymous caller can loop it for Redis write-\r\n //? amplification. Same limit derivation as the credentials branch below (own\r\n //? `oauth-init` bucket) so it degrades to a no-op unless a limit is configured.\r\n const oauthRateLimiting = config.rateLimiting;\r\n const oauthRequesterIp = resolveClientIp({\r\n rawAddress: req.socket.remoteAddress,\r\n headers: req.headers,\r\n trustProxy: config.http.trustProxy,\r\n trustedProxyHopCount: config.http.trustedProxyHopCount,\r\n });\r\n const oauthInitLimit =\r\n oauthRateLimiting.defaultApiLimit !== false && oauthRateLimiting.defaultApiLimit > 0\r\n ? oauthRateLimiting.defaultApiLimit\r\n : (oauthRateLimiting.auth.enabled && oauthRateLimiting.auth.maxAttempts > 0\r\n ? oauthRateLimiting.auth.maxAttempts\r\n : null);\r\n if (oauthInitLimit !== null) {\r\n //? L4: derive the window from the SAME \"is the general limit active?\" test\r\n //? as the count above (`!== false && > 0`), so `defaultApiLimit: 0` doesn't\r\n //? pair the auth COUNT with the general WINDOW (an inconsistent bucket).\r\n const oauthInitWindowMs =\r\n oauthRateLimiting.defaultApiLimit !== false && oauthRateLimiting.defaultApiLimit > 0\r\n ? oauthRateLimiting.windowMs\r\n : oauthRateLimiting.auth.windowMs;\r\n const { allowed, resetIn } = await checkRateLimit({\r\n key: `ip:${oauthRequesterIp}:auth:oauth-init`,\r\n limit: oauthInitLimit,\r\n windowMs: oauthInitWindowMs,\r\n });\r\n if (!allowed) {\r\n void dispatchHook('rateLimitExceeded', {\r\n scope: 'auth',\r\n key: `ip:${oauthRequesterIp}:auth:oauth-init`,\r\n limit: oauthInitLimit,\r\n windowMs: oauthInitWindowMs,\r\n count: oauthInitLimit + 1,\r\n route: routePath,\r\n ip: oauthRequesterIp,\r\n });\r\n res.writeHead(429, { 'content-type': 'application/json; charset=utf-8' });\r\n res.end(JSON.stringify({\r\n status: false,\r\n reason: 'api.rateLimitExceeded',\r\n errorParams: [{ key: 'seconds', value: resetIn }],\r\n }));\r\n return true;\r\n }\r\n }\r\n\r\n //? Read the optional `return_url` query param set by the frontend when it\r\n //? initiates the OAuth flow. The value is the full URL the browser should\r\n //? land on AFTER the callback (e.g. http://localhost:5174/playground).\r\n //? Stored server-side in Redis alongside the state — NOT echoed from the\r\n //? client at callback time — so it cannot be tampered with mid-flight.\r\n //? Validation against allowedOrigins + allowLocalhost happens in loginCallback\r\n //? before the redirect is issued (isAllowedRedirectUrl gate in login.ts).\r\n const reqUrl = new URL(req.url ?? '/', 'http://placeholder');\r\n const returnUrl = reqUrl.searchParams.get('return_url') ?? undefined;\r\n\r\n const oauthState = await login.createOAuthState(provider.name, { usePkce: provider.usePkce, returnUrl });\r\n if (!oauthState) {\r\n res.writeHead(500, { 'Content-Type': 'application/json' });\r\n res.end(JSON.stringify({ status: false, reason: 'login.oauthStateInitFailed' }));\r\n return true;\r\n }\r\n\r\n //? Bind the OAuth flow to THIS browser: the callback only accepts the flow\r\n //? when the same browser presents back the nonce we stash in the\r\n //? short-lived state cookie. Same Secure derivation as the session-token\r\n //? cookie (`resolveCookieSecure`, shared seam) so the binding cookie isn't\r\n //? sent over plaintext when the deployment is HTTPS.\r\n const stateTtl = config.auth.oauthStateTtlSeconds;\r\n const secureFlag = resolveCookieSecure(config.http.sessionCookieSecure, process.env.SECURE) ? ' Secure;' : '';\r\n res.setHeader(\r\n 'Set-Cookie',\r\n `${login.OAUTH_STATE_COOKIE_NAME}=${oauthState.stateCookie}; Path=/; HttpOnly;${secureFlag} SameSite=Lax; Max-Age=${stateTtl}`,\r\n );\r\n\r\n //? Build the authorize-redirect query via URLSearchParams so a provider's\r\n //? `extraAuthorizationParams` (CFG-21) merges OVER the framework defaults —\r\n //? e.g. `access_type=offline` for Google refresh tokens, `login_hint`, or\r\n //? overriding the default `prompt=select_account`. Reserved OAuth params are\r\n //? framework-owned (skipped in the merge); `state` + the PKCE S256 challenge\r\n //? are set LAST so a consumer key can never clobber the browser binding.\r\n const authParams = new URLSearchParams({\r\n client_id: provider.clientID,\r\n redirect_uri: provider.callbackURL,\r\n scope: provider.scope.join(' '),\r\n response_type: 'code',\r\n prompt: 'select_account',\r\n });\r\n for (const [key, value] of Object.entries(provider.extraAuthorizationParams ?? {})) {\r\n if (RESERVED_OAUTH_PARAMS.has(key)) continue;\r\n authParams.set(key, value);\r\n }\r\n authParams.set('state', oauthState.state);\r\n if (oauthState.codeChallenge) {\r\n authParams.set('code_challenge', oauthState.codeChallenge);\r\n authParams.set('code_challenge_method', 'S256');\r\n }\r\n\r\n res.writeHead(302, {\r\n Location: `${provider.authorizationURL}?${authParams.toString()}`,\r\n });\r\n res.end();\r\n return true;\r\n }\r\n\r\n const rateLimiting = config.rateLimiting;\r\n //? DD-LOGIN-F5: resolve the client IP once, unconditionally, so it can be\r\n //? threaded into `loginWithCredentials` for the IP+account composite lockout\r\n //? key even when the per-IP rate-limit gate is disabled. When trustProxy is\r\n //? false this returns the raw socket address (sentinel `'unknown'` when absent).\r\n const requesterIp = resolveClientIp({\r\n rawAddress: req.socket.remoteAddress,\r\n headers: req.headers,\r\n trustProxy: config.http.trustProxy,\r\n trustedProxyHopCount: config.http.trustedProxyHopCount,\r\n });\r\n\r\n //? Derive the per-IP limit for this credentials endpoint. When the global\r\n //? `defaultApiLimit` is disabled (set to `false`), fall back to the auth-\r\n //? specific `rateLimiting.auth` slot so an IP spraying across accounts is\r\n //? still throttled — even when consumers explicitly disable the general limit\r\n //? for performance reasons. The auth slot defaults to `{ enabled: false }`,\r\n //? so the fallback is also a no-op unless the consumer opts in.\r\n const ipLimitCount =\r\n rateLimiting.defaultApiLimit !== false && rateLimiting.defaultApiLimit > 0\r\n ? rateLimiting.defaultApiLimit\r\n : (rateLimiting.auth.enabled && rateLimiting.auth.maxAttempts > 0\r\n ? rateLimiting.auth.maxAttempts\r\n : null);\r\n if (ipLimitCount !== null) {\r\n //? L4: window derived from the SAME predicate as `ipLimitCount` so\r\n //? `defaultApiLimit: 0` doesn't mix the auth count with the general window.\r\n const ipWindowMs =\r\n rateLimiting.defaultApiLimit !== false && rateLimiting.defaultApiLimit > 0\r\n ? rateLimiting.windowMs\r\n : rateLimiting.auth.windowMs;\r\n const { allowed, resetIn } = await checkRateLimit({\r\n key: `ip:${requesterIp}:auth:credentials`,\r\n limit: ipLimitCount,\r\n windowMs: ipWindowMs,\r\n });\r\n\r\n if (!allowed) {\r\n void dispatchHook('rateLimitExceeded', {\r\n scope: 'auth',\r\n key: `ip:${requesterIp}:auth:credentials`,\r\n limit: ipLimitCount,\r\n windowMs: ipWindowMs,\r\n count: ipLimitCount + 1,\r\n route: routePath,\r\n ip: requesterIp,\r\n });\r\n res.setHeader('content-type', 'application/json; charset=utf-8');\r\n res.end(JSON.stringify({\r\n status: false,\r\n reason: 'api.rateLimitExceeded',\r\n errorParams: [{ key: 'seconds', value: resetIn }],\r\n }));\r\n return true;\r\n }\r\n }\r\n\r\n //? Pass the requester's current session token as `supersedeToken` and the\r\n //? resolved `requesterIp` (DD-LOGIN-F5 composite lockout key) so that on\r\n //? a re-login while already signed in, single-session enforcement does NOT\r\n //? kick this same browser's old session.\r\n const result = (await login.loginWithCredentials(params, { supersedeToken: token ?? undefined, requesterIp })) as {\r\n status: boolean;\r\n reason: string;\r\n newToken: string | null;\r\n session: unknown;\r\n //? ADR 0024: 2FA half-way state — first factor OK, no session minted yet.\r\n requiresTwoFactor?: true;\r\n challengeToken?: string;\r\n twoFactorMethods?: string[];\r\n } | undefined;\r\n\r\n if (!result?.status) {\r\n const reasonKey =\r\n typeof result?.reason === 'string' && result.reason.length > 0\r\n ? result.reason\r\n : 'api.internalServerError';\r\n res.setHeader('content-type', 'application/json; charset=utf-8');\r\n res.end(JSON.stringify({ status: false, reason: reasonKey }));\r\n return true;\r\n }\r\n\r\n //? 2FA challenge (ADR 0024): relay the parked-login envelope. NO session\r\n //? transport is set — `/auth/api/2fa` completes the login and mints it.\r\n if (result.requiresTwoFactor) {\r\n res.setHeader('content-type', 'application/json; charset=utf-8');\r\n res.end(JSON.stringify({\r\n status: true,\r\n reason: result.reason,\r\n requiresTwoFactor: true,\r\n challengeToken: result.challengeToken,\r\n twoFactorMethods: result.twoFactorMethods,\r\n authenticated: false,\r\n }));\r\n return true;\r\n }\r\n\r\n if (result.newToken) {\r\n //? Old session was excluded from enforcement above (supersedeToken). Clean it\r\n //? up silently — `skipSocketLogout` prevents a `logout` emit to this same\r\n //? browser's live socket, which would bounce it to /login and null the new\r\n //? session before the success redirect runs.\r\n if (token) await login.deleteSession(token, { skipSocketLogout: true });\r\n\r\n const requestedSessionMode = parseSessionBasedTokenHeader(req.headers['x-session-based-token']);\r\n const useSessionBasedToken = requestedSessionMode ?? config.session.basedToken;\r\n\r\n if (shouldLogDev) getLogger().debug('http: setting cookie with new token');\r\n\r\n if (useSessionBasedToken) {\r\n res.setHeader('X-Session-Token', result.newToken);\r\n } else {\r\n res.setHeader('Set-Cookie', `${sessionCookieName}=${result.newToken}; ${sessionCookieOptions}`);\r\n }\r\n }\r\n\r\n res.setHeader('content-type', 'application/json; charset=utf-8');\r\n res.end(JSON.stringify({\r\n status: result.status,\r\n reason: result.reason,\r\n session: result.session,\r\n authenticated: Boolean(result.newToken),\r\n }));\r\n return true;\r\n};\r\n","import { resolveEnvKey } from '@luckystack/core';\n\n//? Single source of truth for the session / OAuth-state cookie `Secure` flag:\n//? honor the explicit `http.sessionCookieSecure` override (CORE-39), else the\n//? `SECURE` env flag, else default ON in production (L5). Used by BOTH the\n//? session-token cookie (`httpHandler`) and the OAuth state cookie\n//? (`authApiRoute`) so the two can never drift again (WAVE4 — the M2 fix had\n//? updated only the OAuth cookie, leaving the session cookie ignoring the override).\n//?\n//? L5: previously an HTTPS production deploy that set NEITHER\n//? `http.sessionCookieSecure` NOR `SECURE=true` shipped the session + OAuth-state\n//? cookies WITHOUT `Secure` — correct only if the operator remembered a flag.\n//? Now production defaults `Secure` ON; a genuine plain-HTTP prod deploy (rare —\n//? e.g. no TLS anywhere) must opt out with `http.sessionCookieSecure: false`.\nexport const resolveCookieSecure = (\n sessionCookieSecure: boolean | undefined,\n secureEnv: string | undefined,\n): boolean => {\n if (sessionCookieSecure !== undefined) return sessionCookieSecure;\n if (secureEnv === 'true') return true;\n return resolveEnvKey() === 'production';\n};\n","//? Email-code login + 2FA HTTP routes (ADR 0024). These live in the framework\r\n//? layer — NOT file-based `_api` routes — because completing a login must set\r\n//? the HttpOnly session cookie, and only this layer can write `Set-Cookie`\r\n//? (same seam as authApiRoute.ts). Registered BEFORE handleAuthApiRoute in\r\n//? POST_PARAMS_ROUTES: that handler catch-alls `/auth/api/*` into\r\n//? `login.providerNotFound`.\r\n//?\r\n//? Surface (all POST, JSON):\r\n//? /auth/api/email-code/request { email } → { status: true } (anti-enumeration)\r\n//? /auth/api/email-code/verify { email, code } → login envelope (may be a 2FA challenge)\r\n//? /auth/api/2fa { challengeToken, code, method? } → login envelope (completes the login)\r\n//? /auth/api/2fa/email-code { challengeToken } → send the fallback code for a challenge\r\n//? — authenticated (require a live session): —\r\n//? /auth/api/2fa/setup {} → { secret, otpauthUri }\r\n//? /auth/api/2fa/enable { code } → { recoveryCodes[] } (raw, exactly once)\r\n//? /auth/api/2fa/disable { code } → { status }\r\n//? /auth/api/2fa/recovery-codes { code } → { recoveryCodes[] } (fresh set)\r\n\r\nimport { checkRateLimit, getProjectConfig, resolveClientIp } from '@luckystack/core';\r\nimport { getLogin } from '../capabilities';\r\nimport type { HttpRouteContext, HttpRouteHandler } from './types';\r\n\r\ntype LoginModule = NonNullable<Awaited<ReturnType<typeof getLogin>>>;\r\n\r\nconst json = (ctx: HttpRouteContext, statusCode: number, body: unknown): true => {\r\n ctx.res.statusCode = statusCode;\r\n ctx.res.setHeader('content-type', 'application/json; charset=utf-8');\r\n ctx.res.end(JSON.stringify(body));\r\n return true;\r\n};\r\n\r\nconst methodNotAllowed = (ctx: HttpRouteContext): true => {\r\n ctx.res.statusCode = 405;\r\n ctx.res.setHeader('Allow', 'POST');\r\n ctx.res.end();\r\n return true;\r\n};\r\n\r\nconst requesterIpOf = (ctx: HttpRouteContext): string => {\r\n const config = getProjectConfig();\r\n return resolveClientIp({\r\n rawAddress: ctx.req.socket.remoteAddress,\r\n headers: ctx.req.headers,\r\n trustProxy: config.http.trustProxy,\r\n trustedProxyHopCount: config.http.trustedProxyHopCount,\r\n });\r\n};\r\n\r\n//? Per-IP shield in front of the login-package logic (which adds its own\r\n//? per-email / per-challenge budgets). Fixed windows keep the bucket\r\n//? meaningful even when the general API limiter is off.\r\nconst ipThrottled = async (ctx: HttpRouteContext, bucket: string, limit: number): Promise<boolean> => {\r\n const { allowed } = await checkRateLimit({\r\n key: `ip:${requesterIpOf(ctx)}:auth:${bucket}`,\r\n limit,\r\n windowMs: 15 * 60 * 1000,\r\n });\r\n return !allowed;\r\n};\r\n\r\nconst str = (value: unknown): string => (typeof value === 'string' ? value : '');\r\n\r\n//? THE COOKIE SEAM (mirror of authApiRoute.ts): relay a CredentialsLoginResult\r\n//? to the wire. Full success → session transport (header or cookie) + session\r\n//? envelope; 2FA challenge → challenge envelope, NO transport; failure → reason.\r\nconst sendLoginResult = async (\r\n ctx: HttpRouteContext,\r\n login: LoginModule,\r\n result: { status: boolean; reason: string; newToken?: string; session?: unknown; requiresTwoFactor?: true; challengeToken?: string; twoFactorMethods?: string[] },\r\n): Promise<true> => {\r\n if (!result.status) return json(ctx, 200, { status: false, reason: result.reason });\r\n\r\n if (result.requiresTwoFactor) {\r\n return json(ctx, 200, {\r\n status: true,\r\n reason: result.reason,\r\n requiresTwoFactor: true,\r\n challengeToken: result.challengeToken,\r\n twoFactorMethods: result.twoFactorMethods,\r\n authenticated: false,\r\n });\r\n }\r\n\r\n if (result.newToken) {\r\n //? Same supersede semantics as the credentials route: the old session was\r\n //? excluded from enforcement; clean it up without bouncing this browser.\r\n if (ctx.token) await login.deleteSession(ctx.token, { skipSocketLogout: true });\r\n const config = getProjectConfig();\r\n const headerValue = ctx.req.headers['x-session-based-token'];\r\n const raw = Array.isArray(headerValue) ? headerValue[0] : headerValue;\r\n const requestedSessionMode = raw === 'true' ? true : (raw === 'false' ? false : null);\r\n const useSessionBasedToken = requestedSessionMode ?? config.session.basedToken;\r\n if (useSessionBasedToken) {\r\n ctx.res.setHeader('X-Session-Token', result.newToken);\r\n } else {\r\n ctx.res.setHeader('Set-Cookie', `${config.http.sessionCookieName}=${result.newToken}; ${ctx.sessionCookieOptions}`);\r\n }\r\n }\r\n return json(ctx, 200, {\r\n status: true,\r\n reason: result.reason,\r\n session: result.session,\r\n authenticated: Boolean(result.newToken),\r\n });\r\n};\r\n\r\n//? Resolve the FRESH user record behind an authenticated request. The session\r\n//? copy is sanitized (no totpSecret/recoveryCodes by design), so enrollment\r\n//? routes re-read through the adapter.\r\nconst requireUser = async (ctx: HttpRouteContext, login: LoginModule) => {\r\n if (!ctx.token) return null;\r\n const session = await login.getSession(ctx.token);\r\n const userId = (session as { id?: string } | null)?.id;\r\n if (!userId) return null;\r\n return login.getUserAdapter().findById(userId);\r\n};\r\n\r\nexport const handleAuthEmailCodeRoute: HttpRouteHandler = async (ctx) => {\r\n if (ctx.routePath !== '/auth/api/email-code/request' && ctx.routePath !== '/auth/api/email-code/verify') return false;\r\n if (ctx.method !== 'POST') return methodNotAllowed(ctx);\r\n\r\n const login = await getLogin();\r\n if (!login) return json(ctx, 200, { status: false, reason: 'auth.disabled' });\r\n\r\n const params = ctx.params as { email?: unknown; code?: unknown };\r\n const requesterIp = requesterIpOf(ctx);\r\n\r\n if (ctx.routePath === '/auth/api/email-code/request') {\r\n if (await ipThrottled(ctx, 'email-code-request', 10)) return json(ctx, 429, { status: false, reason: 'api.rateLimitExceeded' });\r\n const result = await login.requestEmailLoginCode({ email: str(params.email), requesterIp });\r\n //? Anti-enumeration is inside the login package; disabled-feature and\r\n //? send-failure reasons pass through (they are not account signals).\r\n return json(ctx, 200, result.ok ? { status: true } : { status: false, reason: result.reason });\r\n }\r\n\r\n if (await ipThrottled(ctx, 'email-code-verify', 20)) return json(ctx, 429, { status: false, reason: 'api.rateLimitExceeded' });\r\n const result = await login.verifyEmailLoginCode({\r\n email: str(params.email),\r\n code: str(params.code),\r\n supersedeToken: ctx.token ?? undefined,\r\n requesterIp,\r\n });\r\n return sendLoginResult(ctx, login, result);\r\n};\r\n\r\nexport const handleAuthTwoFactorRoute: HttpRouteHandler = async (ctx) => {\r\n if (ctx.routePath !== '/auth/api/2fa' && !ctx.routePath.startsWith('/auth/api/2fa/')) return false;\r\n if (ctx.method !== 'POST') return methodNotAllowed(ctx);\r\n\r\n const login = await getLogin();\r\n if (!login) return json(ctx, 200, { status: false, reason: 'auth.disabled' });\r\n\r\n const params = ctx.params as { challengeToken?: unknown; code?: unknown; method?: unknown };\r\n const requesterIp = requesterIpOf(ctx);\r\n\r\n //? ── pre-login: complete a parked challenge ──\r\n if (ctx.routePath === '/auth/api/2fa') {\r\n if (await ipThrottled(ctx, '2fa-verify', 20)) return json(ctx, 429, { status: false, reason: 'api.rateLimitExceeded' });\r\n const method = str(params.method);\r\n const result = await login.verifyTwoFactorChallenge({\r\n challengeToken: str(params.challengeToken),\r\n code: str(params.code),\r\n method: method === 'email-code' || method === 'recovery-code' ? method : 'totp',\r\n supersedeToken: ctx.token ?? undefined,\r\n requesterIp,\r\n });\r\n return sendLoginResult(ctx, login, result);\r\n }\r\n\r\n if (ctx.routePath === '/auth/api/2fa/email-code') {\r\n if (await ipThrottled(ctx, '2fa-email-code', 5)) return json(ctx, 429, { status: false, reason: 'api.rateLimitExceeded' });\r\n const result = await login.requestTwoFactorEmailCode(str(params.challengeToken));\r\n return json(ctx, 200, result.ok ? { status: true } : { status: false, reason: result.reason });\r\n }\r\n\r\n //? ── authenticated: enrollment management ──\r\n //? Per-IP throttle on the code-checking management actions too — disable /\r\n //? recovery-codes verify a TOTP/recovery code and (unlike the login path)\r\n //? are not behind the per-challenge budget; without this a hijacked session\r\n //? could brute-force the current code unbounded.\r\n if (await ipThrottled(ctx, '2fa-manage', 15)) return json(ctx, 429, { status: false, reason: 'api.rateLimitExceeded' });\r\n const user = await requireUser(ctx, login);\r\n if (!user) return json(ctx, 401, { status: false, reason: 'api.unauthorized' });\r\n\r\n switch (ctx.routePath) {\r\n case '/auth/api/2fa/setup': {\r\n const start = await login.beginTotpEnrollment(user);\r\n return start.ok\r\n ? json(ctx, 200, { status: true, secret: start.secret, otpauthUri: start.otpauthUri })\r\n : json(ctx, 200, { status: false, reason: start.reason });\r\n }\r\n case '/auth/api/2fa/enable': {\r\n const confirmed = await login.confirmTotpEnrollment(user, str(params.code));\r\n return confirmed.ok\r\n ? json(ctx, 200, { status: true, recoveryCodes: confirmed.recoveryCodes })\r\n : json(ctx, 200, { status: false, reason: confirmed.reason });\r\n }\r\n case '/auth/api/2fa/disable': {\r\n const disabled = await login.disableTwoFactor(user, str(params.code));\r\n return json(ctx, 200, disabled.ok ? { status: true } : { status: false, reason: disabled.reason ?? 'login.twoFactorInvalidCode' });\r\n }\r\n case '/auth/api/2fa/recovery-codes': {\r\n const regenerated = await login.regenerateRecoveryCodes(user, str(params.code));\r\n return regenerated.ok\r\n ? json(ctx, 200, { status: true, recoveryCodes: regenerated.recoveryCodes })\r\n : json(ctx, 200, { status: false, reason: regenerated.reason });\r\n }\r\n default: {\r\n return json(ctx, 404, { status: false, reason: 'common.404' });\r\n }\r\n }\r\n};\r\n","import { getLogger, getProjectConfig, tryCatch } from '@luckystack/core';\r\nimport { getLogin } from '../capabilities';\r\nimport { resolveCookieSecure } from './sessionCookie';\r\nimport type { HttpRouteHandler } from './types';\r\n\r\n//? Warn-once guard. The SameSite weakening is a boot-time config property, not a\r\n//? per-request condition, so we surface it a single time per process — in ALL\r\n//? environments (production is exactly where the missing CSRF mitigation matters\r\n//? most), without flooding the log on every logout request.\r\nlet warnedNonStrictSameSite = false;\r\n\r\n//? HTTP logout endpoint — POST /auth/logout.\r\n//?\r\n//? Logout must terminate over HTTP in cookie mode because only an HTTP\r\n//? response can clear the HttpOnly session cookie — the socket transport the\r\n//? rest of the logout flow uses cannot touch cookies. The route deletes the\r\n//? session when the request still carries a live token (tolerating an\r\n//? already-deleted one — the socket logout usually ran first) and ALWAYS\r\n//? answers with an expiring Set-Cookie so the browser drops the stale\r\n//? credential.\r\n//?\r\n//? CSRF: deliberately outside the `/auth/api` prefix the CSRF middleware\r\n//? guards. The SameSite=Strict session cookie never rides on a cross-site\r\n//? POST, so a forged request arrives token-less and clears nothing — same\r\n//? reasoning as the credentials-bootstrap exemption in csrfMiddleware.ts.\r\nexport const handleAuthLogoutRoute: HttpRouteHandler = async ({ res, routePath, method, token }) => {\r\n if (routePath !== '/auth/logout') return false;\r\n\r\n if (method !== 'POST') {\r\n res.statusCode = 405;\r\n res.setHeader('Allow', 'POST');\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ status: 'error', errorCode: 'api.methodNotAllowed' }));\r\n return true;\r\n }\r\n\r\n if (token) {\r\n const login = await getLogin();\r\n if (login) {\r\n //? deleteSession dispatches the session-delete hooks and reuses the\r\n //? socket logout for any still-connected sockets. A dead/unknown token\r\n //? no-ops; an adapter blip must not block the cookie clear below.\r\n const [deleteError] = await tryCatch(() => login.deleteSession(token));\r\n if (deleteError) {\r\n getLogger().warn('http logout: deleteSession failed — clearing cookie anyway', { err: deleteError });\r\n }\r\n }\r\n }\r\n\r\n //? Clearing cookie: identity = name + path (+ domain). Mirror the attributes\r\n //? of buildSessionCookieOptions in httpHandler.ts, with Max-Age=0 so the\r\n //? browser expires it immediately. setHeader (not append) intentionally\r\n //? overrides the sliding-expiration refresh that ran earlier this request.\r\n const http = getProjectConfig().http;\r\n //? SEC: /auth/logout relies on SameSite=Strict as its CSRF mitigation (the\r\n //? route is deliberately outside the CSRF middleware's candidate check — see\r\n //? csrfMiddleware.ts). Warn (once per process, all envs) when the config\r\n //? weakens this assumption so operators know they must add explicit CSRF\r\n //? protection.\r\n if (http.sessionCookieSameSite !== 'Strict' && !warnedNonStrictSameSite) {\r\n warnedNonStrictSameSite = true;\r\n getLogger().warn(\r\n `/auth/logout is exempt from CSRF middleware and relies on SameSite=Strict. ` +\r\n `Current sessionCookieSameSite=\"${http.sessionCookieSameSite}\" weakens this protection.`,\r\n );\r\n }\r\n const secure = resolveCookieSecure(http.sessionCookieSecure, process.env.SECURE);\r\n res.setHeader(\r\n 'Set-Cookie',\r\n `${http.sessionCookieName}=; HttpOnly; SameSite=${http.sessionCookieSameSite}; Path=${http.sessionCookiePath}; Max-Age=0; ${secure ? 'Secure;' : ''}`,\r\n );\r\n res.statusCode = 200;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ status: 'success', result: true }));\r\n return true;\r\n};\r\n","import { getProjectConfig } from '@luckystack/core';\r\nimport { getLogin } from '../capabilities';\r\nimport type { HttpRouteHandler } from './types';\r\n\r\n//? GET /auth/providers — public list of enabled auth-provider names, derived\r\n//? from the env-driven OAuth registry (see the consumer's\r\n//? `luckystack/login/oauthProviders.ts`, which registers a provider only when\r\n//? its credentials env vars are present). The login form fetches this to decide\r\n//? which OAuth buttons to render WITHOUT ever shipping client secrets to the\r\n//? browser. Read-only + bodyless, so it runs in the pre-params phase.\r\nexport const handleAuthProvidersRoute: HttpRouteHandler = async ({ req, res, routePath }) => {\r\n if (routePath !== '/auth/providers' || req.method !== 'GET') return false;\r\n\r\n //? @luckystack/login optional: when absent there are no providers to advertise.\r\n const login = await getLogin();\r\n const providers = login ? login.getOAuthProviders().map((provider) => provider.name) : [];\r\n\r\n //? ADR 0024: advertise whether passwordless email-code login is enabled so\r\n //? the login form can render the \"email me a code\" entry point. A boolean\r\n //? config flag only — no secrets, same trust level as the provider names.\r\n const emailCodeLogin = Boolean(login) && getProjectConfig().auth.emailCodeLogin;\r\n\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ providers, emailCodeLogin }));\r\n return true;\r\n};\r\n","import { getLogger, getProjectConfig } from '@luckystack/core';\r\nimport { getLogin } from '../capabilities';\r\nimport type { HttpRouteHandler } from './types';\r\n\r\nexport const handleAuthCallbackRoute: HttpRouteHandler = async ({\r\n req,\r\n res,\r\n routePath,\r\n token,\r\n sessionCookieOptions,\r\n}) => {\r\n if (!routePath.startsWith('/auth/callback')) return false;\r\n\r\n //? @luckystack/login optional — no auth package means no OAuth callback.\r\n const login = await getLogin();\r\n if (!login) {\r\n res.writeHead(404, { 'Content-Type': 'text/plain' });\r\n res.end('Auth is not enabled');\r\n return true;\r\n }\r\n\r\n const config = getProjectConfig();\r\n const sessionCookieName = config.http.sessionCookieName;\r\n const shouldLogDev = config.logging.devLogs;\r\n\r\n //? Fallback redirect target when no `postLoginRedirect` resolver returns a URL.\r\n //? `projectConfig.app.publicUrl` is the public origin where users browse (also\r\n //? used for transactional email links) — dev: the frontend dev server, prod:\r\n //? your domain. After an OAuth callback (handled on the BACKEND origin) we must\r\n //? send the browser back to this public origin, not the backend.\r\n //?\r\n //? `loginRedirectUrl` is the configured post-login PATH (e.g. '/dashboard') and\r\n //? lives on the public origin, so we join it onto `publicUrl` to land the user\r\n //? where credentials login also lands them — not the bare public root. An\r\n //? already-absolute `loginRedirectUrl` is used as-is; an empty result falls\r\n //? through to '/dashboard' or whichever path the consumer configured.\r\n const publicOrigin = (config.app.publicUrl || '').replace(/\\/+$/, '');\r\n // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- empty string must fall through\r\n const loginRedirect = config.loginRedirectUrl || '/';\r\n const baseLocation = /^https?:\\/\\//i.test(loginRedirect)\r\n ? loginRedirect\r\n : `${publicOrigin}${loginRedirect.startsWith('/') ? loginRedirect : `/${loginRedirect}`}`;\r\n const callbackResult = await login.loginCallback(routePath, req, res, {\r\n defaultRedirectUrl: baseLocation,\r\n supersedeToken: token ?? undefined,\r\n });\r\n\r\n if (!callbackResult) {\r\n //? Clear the one-time OAuth state-binding cookie on failure too — the Redis\r\n //? state is single-use, so don't leave a spent credential-shaped cookie around.\r\n res.setHeader('Set-Cookie', `${login.OAUTH_STATE_COOKIE_NAME}=; Path=/; HttpOnly; Max-Age=0; SameSite=Lax`);\r\n res.writeHead(401, { 'Content-Type': 'text/plain' });\r\n res.end('Login failed');\r\n return true;\r\n }\r\n\r\n //? Re-login while already signed in: the new session was just created with\r\n //? `supersedeToken` so single-session enforcement did NOT kick this browser's\r\n //? old token. Clean the old session up silently — `skipSocketLogout` avoids\r\n //? emitting a `logout` to this same browser's live socket, which would\r\n //? otherwise bounce the user back to the login page and null their session.\r\n if (token) await login.deleteSession(token, { skipSocketLogout: true });\r\n\r\n if (shouldLogDev) getLogger().debug('http: setting cookie or redirect with new token');\r\n\r\n const { token: newToken, redirectUrl } = callbackResult;\r\n //? The OAuth state-binding cookie is single-use — clear it now that the callback\r\n //? has consumed the state, so a spent credential-shaped cookie doesn't linger in\r\n //? the browser until its Max-Age expires.\r\n const clearStateCookie = `${login.OAUTH_STATE_COOKIE_NAME}=; Path=/; HttpOnly; Max-Age=0; SameSite=Lax`;\r\n if (config.session.basedToken) {\r\n //? Deliver the based-token in the URL fragment, never the query string: a\r\n //? `#token=` fragment is not sent to the server, not logged, and not leaked\r\n //? via the `Referer` header on the next navigation. Drop any pre-existing\r\n //? fragment on the redirect target so we never emit a double-`#`.\r\n const hashIndex = redirectUrl.indexOf('#');\r\n const baseUrl = hashIndex === -1 ? redirectUrl : redirectUrl.slice(0, hashIndex);\r\n res.setHeader('Set-Cookie', clearStateCookie);\r\n res.writeHead(302, { Location: `${baseUrl}#token=${newToken}` });\r\n } else {\r\n res.setHeader('Set-Cookie', [clearStateCookie, `${sessionCookieName}=${newToken}; ${sessionCookieOptions}`]);\r\n res.writeHead(302, { Location: redirectUrl });\r\n }\r\n res.end();\r\n return true;\r\n};\r\n","import {\r\n captureException,\r\n dispatchHook,\r\n extractTokenFromRequest,\r\n getLogger,\r\n tryCatch,\r\n} from '@luckystack/core';\r\nimport { handleHttpApiRequest } from '@luckystack/api';\r\nimport { initSseResponse, sendSseEvent, shouldUseHttpStream } from '../sse';\r\nimport { resolveRequesterIp } from './resolveRequesterIp';\r\nimport type { HttpRouteHandler } from './types';\r\n\r\nexport const handleApiRoute: HttpRouteHandler = async ({\r\n req,\r\n res,\r\n routePath,\r\n queryString,\r\n method,\r\n params,\r\n requestId,\r\n}) => {\r\n if (!routePath.startsWith('/api/')) return false;\r\n\r\n const useHttpStream = shouldUseHttpStream({ acceptHeader: req.headers.accept, queryString });\r\n let streamClosed = false;\r\n let sseInitialized = false;\r\n //? SRV-O5 — open the SSE response LAZILY (first stream chunk, or a SUCCESSFUL\r\n //? final), NEVER eagerly. The eager `initSseResponse` committed HTTP 200 +\r\n //? event-stream headers BEFORE `handleHttpApiRequest` ran its auth / rate-limit /\r\n //? route / method gates, so an unauthenticated or rate-limited caller hitting a\r\n //? streaming endpoint received 200 with the real error buried in the SSE body\r\n //? instead of a 401 / 403 / 404 / 429 status. Deferring the open lets a\r\n //? pre-execution gate failure (which never emits a stream chunk) fall through to\r\n //? a proper-status JSON response below; the stream is only opened once the\r\n //? request has actually passed the gates and is streaming or succeeding.\r\n const ensureSseOpen = () => {\r\n if (sseInitialized || res.writableEnded) return;\r\n initSseResponse(res);\r\n sseInitialized = true;\r\n };\r\n //? SRV-O6 — wire the handler's abortSignal to client disconnect. The api\r\n //? pipeline accepts an `abortSignal` and races `main()` against it, but the HTTP\r\n //? route never supplied one, so a slow handler kept running after the caller went\r\n //? away (wasted DB/CPU work, no cancellation). Abort on any terminal connection\r\n //? event, for BOTH streaming and non-streaming requests.\r\n const abortController = new AbortController();\r\n //? Mirror the four-listener pattern: req 'error' + 'aborted' + 'close' and res\r\n //? 'error' all (1) flip streamClosed so subsequent SSE writes become no-ops and\r\n //? a broken socket can't crash the worker with an unhandled 'error', and (2)\r\n //? abort the in-flight handler. Harmless once the request has already completed.\r\n const markClosed = () => {\r\n streamClosed = true;\r\n if (!abortController.signal.aborted) abortController.abort();\r\n };\r\n req.on('close', markClosed);\r\n req.on('error', markClosed);\r\n req.on('aborted', markClosed);\r\n res.on('error', markClosed);\r\n\r\n const [error, handled] = await tryCatch(async () => {\r\n const httpToken = extractTokenFromRequest(req);\r\n const apiName = routePath.slice(5); // strip \"/api/\"\r\n\r\n if (!apiName) {\r\n const response = {\r\n status: 'error' as const,\r\n httpStatus: 400,\r\n message: 'api.invalidName',\r\n errorCode: 'api.invalidName',\r\n };\r\n //? Pre-gate failure: SSE is never opened, so emit a real 400 status.\r\n res.setHeader('Content-Type', 'application/json');\r\n res.writeHead(400);\r\n res.end(JSON.stringify(response));\r\n return true;\r\n }\r\n\r\n const apiData = typeof params === 'object'\r\n ? { ...(params as Record<string, unknown>) }\r\n : {};\r\n delete (apiData as Record<string, unknown>).stream;\r\n\r\n //? Resolve the real client IP for per-IP rate limiting (honors\r\n //? `http.trustProxy`; preserves the historical `undefined` fallback when\r\n //? there is genuinely no address). See `resolveRequesterIp`.\r\n const requesterIp = resolveRequesterIp(req);\r\n\r\n const result = await handleHttpApiRequest({\r\n name: apiName,\r\n data: apiData,\r\n token: httpToken,\r\n requesterIp,\r\n xLanguageHeader: req.headers['x-language'],\r\n acceptLanguageHeader: req.headers['accept-language'],\r\n method,\r\n abortSignal: abortController.signal,\r\n stream: useHttpStream\r\n ? (payload) => {\r\n if (streamClosed || res.writableEnded) return;\r\n ensureSseOpen();\r\n sendSseEvent({ res, event: 'stream', data: payload });\r\n }\r\n : undefined,\r\n });\r\n\r\n //? Stream the final envelope only when the request actually qualifies: SSE was\r\n //? already opened mid-stream, OR the result is a success (open it now, even if\r\n //? the handler emitted zero chunks). A gate-rejection result (auth / rate-limit\r\n //? / not-found / method) that never opened the stream falls through to the\r\n //? proper-status JSON write below instead of a 200 + SSE-wrapped error.\r\n if (useHttpStream && (sseInitialized || result.status === 'success')) {\r\n ensureSseOpen();\r\n if (!streamClosed) sendSseEvent({ res, event: 'final', data: result });\r\n res.end();\r\n return true;\r\n }\r\n\r\n res.setHeader('Content-Type', 'application/json');\r\n res.writeHead(result.httpStatus);\r\n res.end(JSON.stringify(result));\r\n return true;\r\n }, undefined, { routePath, method, requestId, source: 'httpHandler.api' });\r\n\r\n if (!error) {\r\n return handled ?? true;\r\n }\r\n\r\n getLogger().error('http-api: top-level handler threw', error, { routePath, method, requestId });\r\n captureException(error, { routePath, method, requestId, source: 'httpHandler.api' });\r\n void dispatchHook('apiError', {\r\n route: routePath,\r\n method,\r\n requestId,\r\n error,\r\n });\r\n\r\n const errResponse = {\r\n status: 'error' as const,\r\n httpStatus: 500,\r\n message: 'api.invalidRequestFormat',\r\n errorCode: 'api.invalidRequestFormat',\r\n };\r\n\r\n //? Only route the 500 through SSE when the response was actually committed (the\r\n //? stream opened). `res.headersSent` is the authoritative signal here — in the\r\n //? streaming path only `initSseResponse` writes headers before this catch — and\r\n //? it sidesteps the closure-narrowing of `sseInitialized` at this scope. When\r\n //? headers are still unwritten (gate/parse failure before any chunk), emit a real\r\n //? 500 status rather than an SSE error event on a non-SSE response.\r\n if (useHttpStream && res.headersSent) {\r\n if (!res.writableEnded) sendSseEvent({ res, event: 'error', data: errResponse });\r\n res.end();\r\n return true;\r\n }\r\n\r\n res.setHeader('Content-Type', 'application/json');\r\n res.writeHead(500);\r\n res.end(JSON.stringify(errResponse));\r\n return true;\r\n};\r\n","import type { ServerResponse } from 'node:http';\r\nimport { getProjectConfig } from '@luckystack/core';\r\n\r\n//? Server-Sent Events (SSE) helpers shared by /api/* and /sync/* HTTP\r\n//? streaming. Only used when the client opted in via Accept: text/event-stream\r\n//? or the configured ?<queryParam>=<enabledValue>.\r\n\r\nconst isExpectingEventStream = (acceptHeader: string | string[] | undefined): boolean => {\r\n if (!acceptHeader) return false;\r\n const value = Array.isArray(acceptHeader) ? acceptHeader.join(',') : acceptHeader;\r\n return value.toLowerCase().includes('text/event-stream');\r\n};\r\n\r\nconst queryRequestsStream = (queryString: string | undefined): boolean => {\r\n if (!queryString) return false;\r\n const { queryParam, enabledValue } = getProjectConfig().http.stream;\r\n const params = new URLSearchParams(queryString);\r\n const value = params.get(queryParam);\r\n return value === enabledValue || value === '1';\r\n};\r\n\r\nexport const shouldUseHttpStream = ({\r\n acceptHeader,\r\n queryString,\r\n}: {\r\n acceptHeader: string | string[] | undefined;\r\n queryString: string | undefined;\r\n}): boolean => isExpectingEventStream(acceptHeader) || queryRequestsStream(queryString);\r\n\r\nexport const initSseResponse = (res: ServerResponse): void => {\r\n res.writeHead(200, {\r\n 'Content-Type': 'text/event-stream',\r\n 'Cache-Control': 'no-cache, no-transform',\r\n Connection: 'keep-alive',\r\n 'X-Accel-Buffering': 'no',\r\n });\r\n const connectedComment = getProjectConfig().http.stream.connectedComment;\r\n if (connectedComment) {\r\n res.write(`${connectedComment}\\n\\n`);\r\n }\r\n};\r\n\r\nexport const sendSseEvent = ({\r\n res,\r\n event,\r\n data,\r\n}: {\r\n res: ServerResponse;\r\n event: string;\r\n data: unknown;\r\n}): void => {\r\n if (res.writableEnded) return;\r\n //? Defense-in-depth: only write SSE frames to a response that was actually\r\n //? opened as an event-stream via `initSseResponse`. Every framework caller\r\n //? does init first, so this changes no real path — it just prevents a future\r\n //? caller from emitting `event:`/`data:` lines onto a non-SSE response (which\r\n //? would corrupt the body the client expected).\r\n const contentType = res.getHeader('Content-Type');\r\n if (typeof contentType !== 'string' || !contentType.includes('text/event-stream')) return;\r\n res.write(`event: ${event}\\n`);\r\n res.write(`data: ${JSON.stringify(data)}\\n\\n`);\r\n};\r\n","import type { IncomingMessage } from 'node:http';\r\nimport { getProjectConfig, resolveClientIp } from '@luckystack/core';\r\n\r\n/**\r\n * Resolve the real client IP for per-IP rate limiting on the HTTP api/sync\r\n * transports.\r\n *\r\n * Extracted verbatim from the previously-duplicated inline block in\r\n * `apiRoute.ts` and `syncRoute.ts` (both were byte-identical). Behaviour is\r\n * preserved exactly:\r\n *\r\n * - Reads `http.trustProxy` from the active project config. Default\r\n * `trustProxy: false` returns the raw `req.socket.remoteAddress` (with only\r\n * IPv4-mapped IPv6 canonicalization via `resolveClientIp`); a trusted proxy\r\n * honours `X-Forwarded-For` / `X-Real-IP`.\r\n * - Preserves the historical `undefined` fallback when there is genuinely no\r\n * address to resolve (no raw socket address AND — when proxied — no trusted\r\n * forwarded header), so downstream `?? 'anonymous'` / `?? 'unknown'`\r\n * bucketing stays byte-identical. This `undefined` short-circuit is the\r\n * reason this helper exists rather than calling `resolveClientIp` directly:\r\n * `resolveClientIp` would return the `'unknown'` sentinel here, which the\r\n * api/sync transports historically did NOT key on.\r\n *\r\n * NOTE: the `/auth/api` route intentionally does NOT use this helper — it\r\n * keys on `resolveClientIp(...)` unconditionally (returning the `'unknown'`\r\n * sentinel when nothing resolves), which is a different, deliberate\r\n * contract. Unifying the two would change behaviour, so it is left as-is.\r\n */\r\nexport const resolveRequesterIp = (req: IncomingMessage): string | undefined => {\r\n const trustProxy = getProjectConfig().http.trustProxy;\r\n const trustedProxyHopCount = getProjectConfig().http.trustedProxyHopCount;\r\n const rawRemoteAddress = req.socket.remoteAddress;\r\n return (rawRemoteAddress || (trustProxy && (req.headers['x-forwarded-for'] || req.headers['x-real-ip'])))\r\n ? resolveClientIp({ rawAddress: rawRemoteAddress, headers: req.headers, trustProxy, trustedProxyHopCount })\r\n : undefined;\r\n};\r\n","import {\r\n captureException,\r\n dispatchHook,\r\n extractTokenFromRequest,\r\n getLogger,\r\n tryCatch,\r\n} from '@luckystack/core';\r\nimport type { HttpSyncStreamEvent } from '@luckystack/sync';\r\nimport { capabilities, getSync } from '../capabilities';\r\nimport { initSseResponse, sendSseEvent, shouldUseHttpStream } from '../sse';\r\nimport { resolveRequesterIp } from './resolveRequesterIp';\r\nimport type { HttpRouteHandler } from './types';\r\n\r\ninterface NormalizedHttpSyncParams {\r\n data: Record<string, unknown>;\r\n receiver: string;\r\n ignoreSelf?: boolean;\r\n cb?: string;\r\n}\r\n\r\nconst normalizeHttpSyncParams = (params: object | null): NormalizedHttpSyncParams => {\r\n const source = (params ?? {}) as Record<string, unknown>;\r\n const data = (source.data && typeof source.data === 'object'\r\n ? (source.data as Record<string, unknown>)\r\n : {});\r\n return {\r\n data,\r\n receiver: typeof source.receiver === 'string' ? source.receiver : '',\r\n ignoreSelf: typeof source.ignoreSelf === 'boolean' ? source.ignoreSelf : undefined,\r\n cb: typeof source.cb === 'string' ? source.cb : undefined,\r\n };\r\n};\r\n\r\nexport const handleSyncRoute: HttpRouteHandler = async ({\r\n req,\r\n res,\r\n routePath,\r\n queryString,\r\n method,\r\n params,\r\n requestId,\r\n}) => {\r\n if (!routePath.startsWith('/sync/')) return false;\r\n\r\n //? @luckystack/sync is optional. Absent => no real-time fanout; report the\r\n //? disabled contract so a developer hitting /sync/* gets a clear signal.\r\n const sync = capabilities.sync ? await getSync() : null;\r\n if (!sync) {\r\n res.setHeader('Content-Type', 'application/json');\r\n res.writeHead(404);\r\n res.end(JSON.stringify({ status: 'error', errorCode: 'sync.disabled', message: 'sync.disabled' }));\r\n return true;\r\n }\r\n\r\n const useHttpStream = shouldUseHttpStream({ acceptHeader: req.headers.accept, queryString });\r\n let streamClosed = false;\r\n let sseInitialized = false;\r\n //? SRV-O5 (parity with apiRoute) — open the SSE response LAZILY (first stream\r\n //? chunk, or a SUCCESSFUL final), NEVER eagerly. The eager `initSseResponse`\r\n //? committed HTTP 200 + event-stream headers BEFORE handleHttpSyncRequest ran its\r\n //? method / auth / rate-limit / route / membership gates, so a rejected caller\r\n //? hitting a streaming sync endpoint got 200 with the real error buried in the\r\n //? SSE body instead of a 405 / 401 / 403 / 404 status.\r\n const ensureSseOpen = () => {\r\n if (sseInitialized || res.writableEnded) return;\r\n initSseResponse(res);\r\n sseInitialized = true;\r\n };\r\n //? SRV-O6 — wire the handler's abortSignal to client disconnect (the sync\r\n //? pipeline accepts an `abortSignal` and races the `_server` run against it, but\r\n //? the route never supplied one). Mark the SSE stream closed on EVERY terminal\r\n //? connection event (so subsequent `sendSseEvent` calls become no-ops and a\r\n //? broken socket can't crash the worker with an unhandled 'error') AND abort the\r\n //? in-flight handler. Attached for both streaming and non-streaming requests.\r\n const abortController = new AbortController();\r\n const markClosed = () => {\r\n streamClosed = true;\r\n if (!abortController.signal.aborted) abortController.abort();\r\n };\r\n req.on('close', markClosed);\r\n req.on('error', markClosed);\r\n req.on('aborted', markClosed);\r\n res.on('error', markClosed);\r\n\r\n const [error, handled] = await tryCatch(async () => {\r\n if (method !== 'POST') {\r\n const response = {\r\n status: 'error' as const,\r\n message: 'sync.methodNotAllowed',\r\n errorCode: 'sync.methodNotAllowed',\r\n };\r\n //? Pre-gate failure: SSE is never opened, so emit a real 405 status.\r\n res.setHeader('Content-Type', 'application/json');\r\n res.writeHead(405);\r\n res.end(JSON.stringify(response));\r\n return true;\r\n }\r\n\r\n const httpToken = extractTokenFromRequest(req);\r\n const syncName = routePath.slice(6); // strip \"/sync/\"\r\n\r\n if (!syncName) {\r\n const response = {\r\n status: 'error' as const,\r\n message: 'sync.invalidName',\r\n errorCode: 'sync.invalidName',\r\n };\r\n //? Pre-gate failure: SSE is never opened, so emit a real 400 status.\r\n res.setHeader('Content-Type', 'application/json');\r\n res.writeHead(400);\r\n res.end(JSON.stringify(response));\r\n return true;\r\n }\r\n\r\n const syncParams = normalizeHttpSyncParams(params);\r\n\r\n //? Resolve the real client IP for per-IP rate limiting (honors\r\n //? `http.trustProxy`; preserves the historical `undefined` fallback when\r\n //? there is genuinely no address). See `resolveRequesterIp`.\r\n const requesterIp = resolveRequesterIp(req);\r\n\r\n const result = await sync.handleHttpSyncRequest({\r\n name: `sync/${syncName}`,\r\n cb: syncParams.cb,\r\n data: syncParams.data,\r\n receiver: syncParams.receiver,\r\n ignoreSelf: syncParams.ignoreSelf,\r\n token: httpToken,\r\n requesterIp,\r\n xLanguageHeader: req.headers['x-language'],\r\n acceptLanguageHeader: req.headers['accept-language'],\r\n abortSignal: abortController.signal,\r\n stream: useHttpStream\r\n ? (payload: HttpSyncStreamEvent) => {\r\n if (streamClosed || res.writableEnded) return;\r\n ensureSseOpen();\r\n sendSseEvent({ res, event: 'stream', data: payload });\r\n }\r\n : undefined,\r\n });\r\n\r\n //? Stream the final envelope only when the request actually qualifies: SSE was\r\n //? already opened mid-stream, OR the result is a success (open it now). A\r\n //? gate-rejection result that never opened the stream falls through to the\r\n //? proper-status JSON write below instead of a 200 + SSE-wrapped error.\r\n if (useHttpStream && (sseInitialized || result.status === 'success')) {\r\n ensureSseOpen();\r\n if (!streamClosed) sendSseEvent({ res, event: 'final', data: result });\r\n res.end();\r\n return true;\r\n }\r\n\r\n res.setHeader('Content-Type', 'application/json');\r\n res.writeHead(result.httpStatus ?? (result.status === 'success' ? 200 : 400));\r\n res.end(JSON.stringify(result));\r\n return true;\r\n }, undefined, { routePath, method, requestId, source: 'httpHandler.sync' });\r\n\r\n if (!error) {\r\n return handled ?? true;\r\n }\r\n\r\n getLogger().error('http-sync: top-level handler threw', error, { routePath, method, requestId });\r\n captureException(error, { routePath, method, requestId, source: 'httpHandler.sync' });\r\n void dispatchHook('syncError', {\r\n route: routePath,\r\n method,\r\n requestId,\r\n error,\r\n });\r\n\r\n const errResponse = {\r\n status: 'error' as const,\r\n message: 'sync.invalidRequestFormat',\r\n errorCode: 'sync.invalidRequestFormat',\r\n };\r\n\r\n //? Only route the 500 through SSE when the response was actually committed (the\r\n //? stream opened). `res.headersSent` is authoritative — in the streaming path\r\n //? only `initSseResponse` writes headers before this catch. When headers are\r\n //? still unwritten (gate/parse failure before any chunk), emit a real 500 status.\r\n if (useHttpStream && res.headersSent) {\r\n if (!res.writableEnded) sendSseEvent({ res, event: 'error', data: errResponse });\r\n res.end();\r\n return true;\r\n }\r\n\r\n res.setHeader('Content-Type', 'application/json');\r\n res.writeHead(500);\r\n res.end(JSON.stringify(errResponse));\r\n return true;\r\n};\r\n","import { captureException, getLogger, tryCatch } from '@luckystack/core';\r\nimport type { IncomingMessage, ServerResponse } from 'node:http';\r\nimport type { CustomRouteHandler, RouteContext } from '../types';\r\nimport { getCustomRoutes, getPreParamsCustomRoutes } from '../customRoutesRegistry';\r\nimport type { HttpRouteHandler } from './types';\r\n\r\nconst runHandler = async (\r\n handler: CustomRouteHandler,\r\n req: IncomingMessage,\r\n res: ServerResponse,\r\n ctx: RouteContext,\r\n source: string,\r\n): Promise<boolean> => {\r\n const [error, handled] = await tryCatch(() => handler(req, res, ctx));\r\n if (error) {\r\n //? Custom route handlers come from third-party overlay packages — one\r\n //? misbehaving handler must not crash the request loop or leak the\r\n //? error to the client. Surface to logger + Sentry, then return as\r\n //? handled so the caller short-circuits.\r\n getLogger().error(`${source} threw`, error, { routePath: ctx.routePath, method: ctx.method });\r\n captureException(error, { routePath: ctx.routePath, method: ctx.method, source });\r\n if (!res.writableEnded) {\r\n res.writeHead(500, { 'Content-Type': 'application/json' });\r\n res.end(JSON.stringify({ status: 'error', errorCode: 'server.customRouteFailed' }));\r\n }\r\n return true;\r\n }\r\n return Boolean(handled) || res.writableEnded;\r\n};\r\n\r\n//? PRE-PARAMS phase: consumer routes registered with `{ phase: 'pre-params' }`,\r\n//? dispatched BEFORE the body is parsed so the handler can read the raw `req`\r\n//? stream (webhook HMAC verification, streaming/multipart uploads). A handler\r\n//? that returns `false` without consuming the body falls through to the normal\r\n//? pipeline. Runs after the framework's own pre-params fast-paths.\r\nexport const handlePreParamsCustomRoutes: HttpRouteHandler = async ({\r\n req,\r\n res,\r\n routePath,\r\n queryString,\r\n method,\r\n token,\r\n}) => {\r\n const ctx: RouteContext = { routePath, method, queryString, token };\r\n for (const handler of getPreParamsCustomRoutes()) {\r\n const handled = await runHandler(handler, req, res, ctx, 'preParamsCustomRoute');\r\n if (handled) return true;\r\n }\r\n return false;\r\n};\r\n\r\nexport const handleCustomRoutes: HttpRouteHandler = async ({\r\n req,\r\n res,\r\n options,\r\n routePath,\r\n queryString,\r\n method,\r\n token,\r\n}) => {\r\n //? Two sources, evaluated in order: (1) handlers registered via\r\n //? `registerCustomRoute(...)` from overlay packages (`@luckystack/docs-ui`,\r\n //? etc.); (2) the legacy `customRoutes` option on\r\n //? `CreateLuckyStackServerOptions`. First one to return `true` (or end\r\n //? the response) wins.\r\n const ctx: RouteContext = { routePath, method, queryString, token };\r\n\r\n for (const handler of getCustomRoutes()) {\r\n const handled = await runHandler(handler, req, res, ctx, 'customRoutesRegistry');\r\n if (handled) return true;\r\n }\r\n if (options.customRoutes) {\r\n const handled = await runHandler(options.customRoutes, req, res, ctx, 'createServer.customRoutes');\r\n if (handled) return true;\r\n }\r\n return false;\r\n};\r\n","//? Custom-route registry. Lets overlay files (luckystack/<package>/index.ts)\r\n//? register HTTP route handlers without each one touching server/server.ts.\r\n//?\r\n//? Why a registry instead of a single function on CreateLuckyStackServerOptions:\r\n//? The overlay-folder pattern means multiple packages may want to add\r\n//? routes (docs-ui, an admin panel, a webhook receiver, ...). Each\r\n//? registers independently; the framework iterates them in the order they\r\n//? were registered, returning the first one that handles the request.\r\n//?\r\n//? The original `customRoutes` option on `CreateLuckyStackServerOptions`\r\n//? still works — it's appended to the registry at boot and runs last.\r\n//?\r\n//? Two phases (see `CustomRoutePhase`): `'post-params'` (default — runs after\r\n//? the body is parsed) and `'pre-params'` (runs before the body is read, so\r\n//? the handler gets the raw `req` stream — webhooks + streaming uploads).\r\n\r\nimport type { CustomRouteHandler, CustomRoutePhase } from './types';\r\n\r\nexport interface RegisterCustomRouteOptions {\r\n /** Pipeline phase the handler runs in. Defaults to `'post-params'`. */\r\n phase?: CustomRoutePhase;\r\n}\r\n\r\n//? `handlers` backs the default `'post-params'` phase. `getCustomRoutes()`\r\n//? returns this array by reference (callers + tests rely on the clear-in-place\r\n//? semantics), so keep it as the post-params store.\r\nconst handlers: CustomRouteHandler[] = [];\r\nconst preParamsHandlers: CustomRouteHandler[] = [];\r\n\r\nexport const registerCustomRoute = (\r\n handler: CustomRouteHandler,\r\n options: RegisterCustomRouteOptions = {},\r\n): void => {\r\n if (options.phase === 'pre-params') {\r\n preParamsHandlers.push(handler);\r\n return;\r\n }\r\n handlers.push(handler);\r\n};\r\n\r\nexport const getCustomRoutes = (): readonly CustomRouteHandler[] => handlers;\r\n\r\nexport const getPreParamsCustomRoutes = (): readonly CustomRouteHandler[] => preParamsHandlers;\r\n\r\nexport const clearCustomRoutes = (): void => {\r\n handlers.length = 0;\r\n preParamsHandlers.length = 0;\r\n};\r\n","import path from 'node:path';\r\nimport type { IncomingMessage, ServerResponse } from 'node:http';\r\nimport type { StaticFileHandler } from '../types';\r\nimport type { HttpRouteHandler } from './types';\r\n\r\nconst KNOWN_STATIC_FILE_REGEX = /^\\/(assets\\/[a-zA-Z0-9_/-]+|[a-zA-Z0-9_-]+)\\.(png|jpg|jpeg|gif|svg|html|css|js)$/;\r\n\r\n//? Source-disclosure denylist. The server bundle (`dist/server.js`) and ANY\r\n//? source map (`*.map`) must never be served by a framework static path —\r\n//? leaking either hands an attacker the readable server source. This guards\r\n//? every branch below (assets, known-extension, SPA catch-all) regardless of\r\n//? which `serveFile` the consumer wired, so the protection is structural and\r\n//? does not rely on the default noop handler.\r\nconst SERVE_DENYLIST_REGEX = /(^\\/server\\.js$)|(\\.map$)/;\r\n\r\n//? `serveFile` consumers (Vite middleware, custom static handlers) read\r\n//? `req.url`, so we need to swap it for the rewritten asset path. We swap\r\n//? around the call and restore on return so anything downstream that\r\n//? observes `req.url` (request loggers, metrics middleware) still sees the\r\n//? original incoming URL after this handler.\r\nconst serveWithRewrittenUrl = async (\r\n serveFile: StaticFileHandler,\r\n req: IncomingMessage,\r\n res: ServerResponse,\r\n rewrittenUrl: string,\r\n): Promise<void> => {\r\n const originalUrl = req.url;\r\n req.url = rewrittenUrl;\r\n try {\r\n await serveFile(req, res);\r\n } finally {\r\n req.url = originalUrl;\r\n }\r\n};\r\n\r\nexport const handleStaticAndSpaFallback: HttpRouteHandler = async ({\r\n req,\r\n res,\r\n routePath,\r\n options,\r\n}) => {\r\n // ── denylist — never serve the server bundle or source maps ───────────\r\n if (SERVE_DENYLIST_REGEX.test(routePath)) {\r\n res.writeHead(404, { 'Content-Type': 'text/plain' });\r\n res.end('Not Found');\r\n return true;\r\n }\r\n\r\n // ── /assets/* — static assets ──────────────────────────────────────────\r\n //? `startsWith` not `includes` — avoids matching `/other/assets/foo` and\r\n //? prevents the indexOf slice from drifting to an interior segment.\r\n if (routePath.startsWith('/assets/')) {\r\n //? Reject path-traversal at the framework boundary BEFORE handing the path to\r\n //? the consumer-supplied serveFile. routePath is already percent-DECODED, so\r\n //? `/assets/%2e%2e/server.js` arrives as `/assets/../server.js` and would\r\n //? otherwise reach a naive `path.join(dir, req.url)` static handler (source\r\n //? disclosure). The leading denylist only blocks the literal `/server.js`, not\r\n //? a traversal to it — so this `..` reject makes the protection structural.\r\n if (routePath.includes('..')) {\r\n res.writeHead(404, { 'Content-Type': 'text/plain' });\r\n res.end('Not Found');\r\n return true;\r\n }\r\n if (!options.serveFile) {\r\n res.writeHead(404);\r\n res.end('Not Found');\r\n return true;\r\n }\r\n await serveWithRewrittenUrl(options.serveFile, req, res, routePath);\r\n return true;\r\n }\r\n\r\n // ── *.{png,jpg,...} — known static file extensions ────────────────────\r\n if (KNOWN_STATIC_FILE_REGEX.test(routePath)) {\r\n if (!options.serveFile) {\r\n res.writeHead(404);\r\n res.end('Not Found');\r\n return true;\r\n }\r\n await options.serveFile(req, res);\r\n return true;\r\n }\r\n\r\n // ── path with extension we don't recognize — 404 ──────────────────────\r\n if (path.extname(routePath)) {\r\n res.writeHead(404, { 'Content-Type': 'text/plain' });\r\n res.end('Not Found');\r\n return true;\r\n }\r\n\r\n // ── catch-all (index.html for SPA routing) ────────────────────────────\r\n if (!options.serveFile) {\r\n res.writeHead(404, { 'Content-Type': 'text/plain' });\r\n res.end('Not Found');\r\n return true;\r\n }\r\n\r\n await serveWithRewrittenUrl(options.serveFile, req, res, '/');\r\n return true;\r\n};\r\n","import type { Server as HttpServer } from 'node:http';\r\nimport type { Socket } from 'socket.io';\r\nimport type { Redis as RedisClient } from 'ioredis';\r\nimport { Server as SocketIOServer } from 'socket.io';\r\nimport {\r\n abortAllForSocket,\r\n abortApiByResponseIndex,\r\n abortSyncByCb,\r\n allowedOrigin,\r\n applySocketMiddlewares,\r\n attachSocketRedisAdapter,\r\n formatRoomName,\r\n redis,\r\n setIoInstance,\r\n socketEventNames,\r\n buildJoinRoomResponseEventName,\r\n buildLeaveRoomResponseEventName,\r\n buildGetJoinedRoomsResponseEventName,\r\n extractLanguageFromHeader,\r\n extractTokenFromSocket,\r\n getLogger,\r\n getProjectConfig,\r\n dispatchHook,\r\n normalizeErrorResponse,\r\n readSession,\r\n writeSession,\r\n tryCatch,\r\n type apiMessage,\r\n type syncMessage,\r\n type BaseSessionLayout,\r\n} from '@luckystack/core';\r\nimport { handleApiRequest } from '@luckystack/api';\r\n//? login / presence / sync are OPTIONAL peers — resolved + lazy-loaded through\r\n//? the capability layer so the server boots + runs without them. Session reads\r\n//? use core's `readSession`/`writeSession` (login populates the provider).\r\nimport { capabilities, getPresence, getSync } from './capabilities';\r\n\r\n//? Per-token lock to serialize session mutations (prevents read-modify-write races\r\n//? when multiple room joins/leaves arrive in quick succession from the same socket).\r\nconst sessionLocks = new Map<string, Promise<void>>();\r\nconst withSessionLock = async (token: string, fn: () => Promise<void>) => {\r\n const prev = sessionLocks.get(token) ?? Promise.resolve();\r\n //? Both `then` handlers point to `fn` so a previous lock failure (rejected\r\n //? promise) doesn't block the next caller — we want each `withSessionLock`\r\n //? invocation to run regardless of whether the prior one threw.\r\n const next = prev.then(fn, fn);\r\n sessionLocks.set(token, next);\r\n await tryCatch(() => next);\r\n //? Cleanup runs unconditionally — same semantics the prior `try/finally`\r\n //? provided. Only delete if we still own the slot (a later caller may\r\n //? have already overwritten it with their own `next`).\r\n if (sessionLocks.get(token) === next) sessionLocks.delete(token);\r\n};\r\n\r\nconst getVisibleSocketRooms = (\r\n socket: { rooms: Set<string>; id: string },\r\n token: string | null\r\n): string[] => {\r\n return [...socket.rooms]\r\n .filter((room): room is string => typeof room === 'string')\r\n .filter((room) => room !== socket.id)\r\n .filter((room) => !token || room !== token);\r\n};\r\n\r\nconst getSessionRoomCodes = (session: BaseSessionLayout): string[] => {\r\n const roomCodes = Array.isArray(session.roomCodes)\r\n ? session.roomCodes.filter(\r\n (roomCode): roomCode is string => typeof roomCode === 'string' && roomCode.length > 0\r\n )\r\n : [];\r\n return [...new Set(roomCodes)];\r\n};\r\n\r\nconst sanitizeSessionRoomKeys = (session: BaseSessionLayout): BaseSessionLayout => {\r\n const { code: _legacyCode, codes: _legacyCodes, ...sanitizedSession } = session as BaseSessionLayout & {\r\n code?: string;\r\n codes?: string[];\r\n };\r\n return sanitizedSession;\r\n};\r\n\r\nexport interface LoadSocketOptions {\r\n maxHttpBufferSize?: number;\r\n}\r\n\r\nexport interface LoadSocketResult {\r\n io: SocketIOServer;\r\n /**\r\n * The duplicated Redis pub/sub clients backing the Socket.io adapter. The\r\n * server bootstrap owns them so graceful shutdown can `quit()` them — they are\r\n * NOT closed by `io.close()` (which only tears down the engine + connections).\r\n * Created here (instead of letting `attachSocketRedisAdapter` duplicate\r\n * internally) precisely so the server holds a handle to disconnect them.\r\n */\r\n adapterClients: { pubClient: RedisClient; subClient: RedisClient };\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Per-socket context passed to each per-event registrar\r\n// ---------------------------------------------------------------------------\r\n\r\ninterface SocketContext {\r\n socket: Socket;\r\n token: string | null;\r\n preferredLocale: string | undefined;\r\n activityBroadcasterEnabled: boolean;\r\n locationProviderEnabled: boolean;\r\n shouldLogDev: boolean;\r\n io: SocketIOServer;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Shared guard: validates the common room-mutation preconditions before the\r\n// async locked body runs. Returns false and emits the error when invalid.\r\n// ---------------------------------------------------------------------------\r\n\r\nconst validateRoomRequest = (\r\n socket: Socket,\r\n responseIndex: number | undefined,\r\n token: string | null,\r\n group: string,\r\n preferredLocale: string | undefined,\r\n buildResponseEventName: (idx: number) => string\r\n): responseIndex is number => {\r\n if (typeof responseIndex !== 'number') return false;\r\n\r\n if (!token) {\r\n socket.emit(buildResponseEventName(responseIndex), normalizeErrorResponse({\r\n response: { status: 'error', errorCode: 'auth.required' },\r\n preferredLocale,\r\n }));\r\n return false;\r\n }\r\n if (!group || group.length > 256) {\r\n socket.emit(buildResponseEventName(responseIndex), normalizeErrorResponse({\r\n response: { status: 'error', errorCode: 'room.invalid' },\r\n preferredLocale,\r\n }));\r\n return false;\r\n }\r\n return true;\r\n};\r\n\r\n// ---------------------------------------------------------------------------\r\n// Shared async body for room mutations (join / leave). The `mutate` callback\r\n// performs the transport-specific operation (join or leave) and computes the\r\n// next roomCodes array; the hook names and blocked error code differ per direction.\r\n// ---------------------------------------------------------------------------\r\n\r\ninterface RoomMutationOptions {\r\n socket: Socket;\r\n token: string;\r\n group: string;\r\n responseIndex: number;\r\n preferredLocale: string | undefined;\r\n shouldLogDev: boolean;\r\n buildResponseEventName: (idx: number) => string;\r\n preHook: 'preRoomJoin' | 'preRoomLeave';\r\n postHook: 'postRoomJoin' | 'postRoomLeave';\r\n blockedErrorCode: string;\r\n logVerb: string;\r\n mutate: (socket: Socket, physicalRoom: string, rawGroup: string, existingRoomCodes: string[], userId: string | undefined) => Promise<string[]>;\r\n}\r\n\r\nconst executeRoomMutation = async (opts: RoomMutationOptions): Promise<void> => {\r\n const {\r\n socket, token, group, responseIndex, preferredLocale, shouldLogDev,\r\n buildResponseEventName, preHook, postHook, blockedErrorCode, logVerb, mutate,\r\n } = opts;\r\n\r\n const session = await readSession(token);\r\n if (!session) {\r\n socket.emit(buildResponseEventName(responseIndex), normalizeErrorResponse({\r\n response: { status: 'error', errorCode: 'session.notFound' },\r\n preferredLocale,\r\n }));\r\n return;\r\n }\r\n\r\n const preResult = await dispatchHook(preHook, { token, room: group });\r\n if (preResult.stopped) {\r\n socket.emit(buildResponseEventName(responseIndex), normalizeErrorResponse({\r\n response: {\r\n status: 'error',\r\n errorCode: preResult.signal.errorCode || blockedErrorCode,\r\n },\r\n preferredLocale,\r\n userLanguage: session.language,\r\n }));\r\n return;\r\n }\r\n\r\n const existingRoomCodes = getSessionRoomCodes(session);\r\n //? Route the raw room code through the core room-name formatter so a\r\n //? non-identity `registerRoomNameFormatter` (e.g. per-tenant prefixing) applies\r\n //? to the socket.io room name the socket physically joins/leaves. The session\r\n //? stores the RAW code; only the Socket.io room name uses the physical form.\r\n //? M5: a content room's physical name MUST be identical across join, leave,\r\n //? membership-check, and broadcast/fanout — otherwise a `purpose`-branching\r\n //? formatter (multi-tenant prefixing) would place the socket in one physical\r\n //? room (`join:R`) but the sync membership check + fanout would target another\r\n //? (`broadcast:R`), so legit members get rejected and fanout reaches nobody. All\r\n //? content-room operations therefore use the single canonical `'broadcast'`\r\n //? purpose; only a genuinely-separate room family (presence) uses a different\r\n //? one. `preHook` still distinguishes the join vs leave OPERATION for the hooks\r\n //? and `mutate` below — it just no longer changes the physical room NAME.\r\n const physicalRoom = formatRoomName(group, { purpose: 'broadcast', userId: session.id });\r\n const nextRoomCodes = await mutate(socket, physicalRoom, group, existingRoomCodes, session.id);\r\n\r\n const sanitizedSession = sanitizeSessionRoomKeys(session);\r\n await writeSession(token, { ...sanitizedSession, roomCodes: nextRoomCodes });\r\n\r\n const visibleRooms = getVisibleSocketRooms(socket, token);\r\n socket.emit(buildResponseEventName(responseIndex), { rooms: visibleRooms });\r\n if (shouldLogDev) {\r\n getLogger().debug(`Socket ${socket.id} ${logVerb} group ${group}`);\r\n }\r\n\r\n void dispatchHook(postHook, { token, room: group, allRooms: visibleRooms });\r\n};\r\n\r\n// ---------------------------------------------------------------------------\r\n// Per-event registrar helpers\r\n// ---------------------------------------------------------------------------\r\n\r\nconst registerApiAndSyncEvents = (ctx: SocketContext): void => {\r\n const { socket, token } = ctx;\r\n\r\n socket.on(socketEventNames.apiRequest, (msg: apiMessage) => {\r\n void handleApiRequest({ msg, socket, token });\r\n });\r\n\r\n //? Only wire the sync listener when @luckystack/sync is installed. Absent =>\r\n //? clients that emit `sync` get no handler (their request times out / the\r\n //? HTTP fallback returns `sync.disabled`). Lazy-loaded once on first event.\r\n if (capabilities.sync) {\r\n socket.on(socketEventNames.sync, (msg: syncMessage) => {\r\n void (async () => {\r\n const sync = await getSync();\r\n if (sync) await sync.handleSyncRequest({ msg, socket, token });\r\n })();\r\n });\r\n }\r\n};\r\n\r\nconst registerCancellationEvents = (ctx: SocketContext): void => {\r\n const { socket } = ctx;\r\n\r\n //? B1 — cancellation events. Client emits `{ cb }` (sync) or\r\n //? `{ responseIndex }` (api) on the matching cancel channel; we look up\r\n //? the in-flight AbortController by `${socket.id}:<key>` and abort it.\r\n //? Server-side handler chains gate further chunk emits on the signal\r\n //? and exit early via the cleanup paths registered in each handler.\r\n socket.on(socketEventNames.syncCancel, (data: { cb?: string }) => {\r\n const cb = typeof data.cb === 'string' ? data.cb : null;\r\n if (!cb) return;\r\n abortSyncByCb(socket.id, cb);\r\n });\r\n socket.on(socketEventNames.apiCancel, (data: { responseIndex?: number | string }) => {\r\n const responseIndex = data.responseIndex;\r\n if (typeof responseIndex !== 'number' && typeof responseIndex !== 'string') return;\r\n abortApiByResponseIndex(socket.id, responseIndex);\r\n });\r\n};\r\n\r\nconst registerRoomEvents = (ctx: SocketContext): void => {\r\n const { socket, token, preferredLocale, shouldLogDev } = ctx;\r\n\r\n socket.on(socketEventNames.joinRoom, (data: { group?: string; responseIndex?: number }) => {\r\n const group = typeof data.group === 'string' ? data.group.trim() : '';\r\n const responseIndex = data.responseIndex;\r\n\r\n if (!validateRoomRequest(socket, responseIndex, token, group, preferredLocale, buildJoinRoomResponseEventName)) return;\r\n // validateRoomRequest already rejects null tokens, so this guard is redundant but narrows the type\r\n if (!token) return;\r\n\r\n void withSessionLock(token, async () => {\r\n await executeRoomMutation({\r\n socket,\r\n token,\r\n group,\r\n responseIndex,\r\n preferredLocale,\r\n shouldLogDev,\r\n buildResponseEventName: buildJoinRoomResponseEventName,\r\n preHook: 'preRoomJoin',\r\n postHook: 'postRoomJoin',\r\n blockedErrorCode: 'room.joinBlocked',\r\n logVerb: 'joined',\r\n mutate: async (sock, physicalRoom, rawGroup, existingCodes, userId) => {\r\n //? Enforce the per-session room cap (socket.maxRoomsPerSession, default\r\n //? 50) with FIFO eviction: joining a NEW room beyond the cap leaves the\r\n //? OLDEST joined room first, so `roomCodes` can't grow unbounded in Redis\r\n //? (session-bloat DoS). Re-joining an already-joined room never grows the\r\n //? set, so it never evicts.\r\n const maxRooms = getProjectConfig().socket.maxRoomsPerSession;\r\n let kept = existingCodes;\r\n if (maxRooms !== false && maxRooms > 0 && !existingCodes.includes(rawGroup)) {\r\n while (kept.length >= maxRooms) {\r\n const oldest = kept[0];\r\n if (oldest === undefined) break; // unreachable (length >= maxRooms >= 1) but narrows for noUncheckedIndexedAccess\r\n kept = kept.slice(1);\r\n //? M5: canonical content-room purpose (see the join site above) so the\r\n //? FIFO-evict leaves the SAME physical room the socket actually joined.\r\n await sock.leave(formatRoomName(oldest, { purpose: 'broadcast', userId }));\r\n }\r\n }\r\n await sock.join(physicalRoom);\r\n return [...new Set([...kept, rawGroup])];\r\n },\r\n });\r\n });\r\n });\r\n\r\n socket.on(socketEventNames.leaveRoom, (data: { group?: string; responseIndex?: number }) => {\r\n const group = typeof data.group === 'string' ? data.group.trim() : '';\r\n const responseIndex = data.responseIndex;\r\n\r\n if (!validateRoomRequest(socket, responseIndex, token, group, preferredLocale, buildLeaveRoomResponseEventName)) return;\r\n // validateRoomRequest already rejects null tokens, so this guard is redundant but narrows the type\r\n if (!token) return;\r\n\r\n void withSessionLock(token, async () => {\r\n await executeRoomMutation({\r\n socket,\r\n token,\r\n group,\r\n responseIndex,\r\n preferredLocale,\r\n shouldLogDev,\r\n buildResponseEventName: buildLeaveRoomResponseEventName,\r\n preHook: 'preRoomLeave',\r\n postHook: 'postRoomLeave',\r\n blockedErrorCode: 'room.leaveBlocked',\r\n logVerb: 'left',\r\n mutate: async (sock, physicalRoom, rawGroup, existingCodes, _userId) => {\r\n const nextCodes = existingCodes.filter((c) => c !== rawGroup);\r\n await sock.leave(physicalRoom);\r\n return nextCodes;\r\n },\r\n });\r\n });\r\n });\r\n\r\n socket.on(socketEventNames.getJoinedRooms, (data: { responseIndex?: number }) => {\r\n const responseIndex = data.responseIndex;\r\n if (typeof responseIndex !== 'number') return;\r\n\r\n if (!token) {\r\n socket.emit(buildGetJoinedRoomsResponseEventName(responseIndex), {\r\n ...normalizeErrorResponse({\r\n response: { status: 'error', errorCode: 'auth.required' },\r\n preferredLocale,\r\n }),\r\n rooms: [],\r\n });\r\n return;\r\n }\r\n\r\n socket.emit(buildGetJoinedRoomsResponseEventName(responseIndex), {\r\n rooms: getVisibleSocketRooms(socket, token),\r\n });\r\n });\r\n};\r\n\r\nconst registerDisconnectEvent = (ctx: SocketContext): void => {\r\n const { socket, token, activityBroadcasterEnabled, shouldLogDev } = ctx;\r\n\r\n socket.on(socketEventNames.disconnect, (reason: string) => {\r\n //? B1 — safety-net sweep. Per-request handlers also register their\r\n //? own `socket.once(disconnect, ...)` listeners that abort + clean up,\r\n //? but `abortAllForSocket` covers anything that slipped through (e.g.\r\n //? handler crashed before registering its disconnect listener).\r\n abortAllForSocket(socket.id);\r\n void dispatchHook('onSocketDisconnect', { socketId: socket.id, token, reason });\r\n\r\n if (activityBroadcasterEnabled) {\r\n //? Pass the token so presence can clear the token-level AFK refractory entry\r\n //? once the last socket for that token leaves — without it, lastAfkFireByToken\r\n //? grew unbounded (one stale entry per token that ever went AFK).\r\n void getPresence().then((presence) => { presence?.clearActivity(socket.id, token ?? undefined); });\r\n }\r\n\r\n if (activityBroadcasterEnabled && token) {\r\n void (async () => {\r\n const presence = await getPresence();\r\n if (presence) presence.socketDisconnecting({ token, socket, reason });\r\n })();\r\n } else {\r\n if (!token) return;\r\n if (shouldLogDev) {\r\n getLogger().debug(`user disconnected`, { reason });\r\n }\r\n }\r\n });\r\n};\r\n\r\nconst registerUpdateLocationEvent = (ctx: SocketContext): void => {\r\n const { socket, token, activityBroadcasterEnabled, locationProviderEnabled, shouldLogDev } = ctx;\r\n\r\n socket.on(\r\n socketEventNames.updateLocation,\r\n (newLocation: { pathName: string; searchParams?: Record<string, string> }) => {\r\n if (!token) return;\r\n if (!locationProviderEnabled) return;\r\n //? SEC: validate client-supplied location fields before persisting in session.\r\n //? pathName must start with '/', be at most 2048 chars, and contain no\r\n //? null bytes. searchParams keys + values are capped to prevent session bloat.\r\n if (\r\n typeof newLocation.pathName !== 'string'\r\n || !newLocation.pathName.startsWith('/')\r\n || newLocation.pathName.length > 2048\r\n || newLocation.pathName.includes('\\0')\r\n ) return;\r\n if (newLocation.searchParams !== undefined) {\r\n if (typeof newLocation.searchParams !== 'object' || Array.isArray(newLocation.searchParams)) return;\r\n const entries = Object.entries(newLocation.searchParams);\r\n if (entries.length > 50) return;\r\n for (const [k, v] of entries) {\r\n if (typeof k !== 'string' || k.length > 256 || typeof v !== 'string' || v.length > 1024) return;\r\n }\r\n }\r\n if (shouldLogDev) {\r\n getLogger().debug('updating location', { pathName: newLocation.pathName });\r\n }\r\n\r\n void withSessionLock(token, async () => {\r\n let returnedUser: BaseSessionLayout | null = null;\r\n if (activityBroadcasterEnabled) {\r\n const presence = await getPresence();\r\n if (presence) {\r\n returnedUser = await presence.socketLeaveRoom({ token, socket, newPath: newLocation.pathName });\r\n }\r\n }\r\n\r\n const user = returnedUser ?? (await readSession(token));\r\n if (!user) return;\r\n\r\n const extendedUser = user as BaseSessionLayout & { location?: typeof newLocation };\r\n const oldLocation = extendedUser.location;\r\n //? Spread into a fresh object (no in-place mutation) and sanitize\r\n //? legacy `code`/`codes` keys — same hygiene as the join/leave paths.\r\n const sanitizedUser = sanitizeSessionRoomKeys({ ...extendedUser, location: newLocation });\r\n await writeSession(token, sanitizedUser);\r\n\r\n void dispatchHook('onLocationUpdate', { token, oldLocation, newLocation });\r\n });\r\n }\r\n );\r\n};\r\n\r\nconst registerActivityEvents = (ctx: SocketContext): void => {\r\n const { socket, token, activityBroadcasterEnabled, io } = ctx;\r\n\r\n if (activityBroadcasterEnabled && token) {\r\n void (async () => {\r\n const presence = await getPresence();\r\n if (presence) presence.initActivityBroadcaster({ socket, token });\r\n })();\r\n }\r\n\r\n //? Activity tracking (production AFK + custom activity events). Seed the\r\n //? socket's last-activity on connect, start the single sampler interval\r\n //? (idempotent), and refresh last-activity on every client heartbeat /\r\n //? tab-return. The sampler walks all sockets and fires registered events\r\n //? (built-in AFK + any consumer pause/kick events).\r\n if (activityBroadcasterEnabled) {\r\n void (async () => {\r\n const presence = await getPresence();\r\n if (!presence) return;\r\n presence.startActivitySampler({ io });\r\n presence.recordActivity(socket.id);\r\n })();\r\n socket.on(socketEventNames.activity, () => {\r\n void getPresence().then((presence) => { presence?.recordActivity(socket.id); });\r\n });\r\n socket.on(socketEventNames.intentionalReconnect, () => {\r\n void getPresence().then((presence) => { presence?.recordActivity(socket.id); });\r\n });\r\n }\r\n};\r\n\r\nconst rejoinPersistedRooms = (ctx: SocketContext): void => {\r\n const { socket, token, shouldLogDev } = ctx;\r\n\r\n if (!token) return;\r\n\r\n //? Rebuild this session's room membership on (re)connect. Socket.io rooms\r\n //? are per-connection + in-memory, so a page refresh (brand-new socket) or\r\n //? a server restart drops them while `session.roomCodes` (persisted in\r\n //? Redis) still lists them. Without replaying them here the reconnected\r\n //? socket only sits in its private token room, so a `syncRequest` fan-out\r\n //? (`io.in(room).fetchSockets()`) finds zero members and returns\r\n //? `sync.noReceiversFound`. Sequenced (await) so membership is restored\r\n //? ASAP, and logged so a failed/empty rejoin is visible. Idempotent —\r\n //? re-joining an already-joined room is a no-op.\r\n void (async () => {\r\n const [rejoinError, codes] = await tryCatch(async () => {\r\n //? The token room is a private identity room (not a user-visible room code),\r\n //? so it is NOT routed through the room-name formatter.\r\n await socket.join(token);\r\n const session = await readSession(token);\r\n const roomCodes = session ? getSessionRoomCodes(session) : [];\r\n const userId = session?.id ?? null;\r\n for (const roomCode of roomCodes) {\r\n //? Route through the formatter so a multi-tenant prefix applies on\r\n //? reconnect exactly as it did on the original join (PRESENCE-1). Canonical\r\n //? content-room purpose (M5) so the rejoined physical room matches the sync\r\n //? membership check + fanout target.\r\n await socket.join(formatRoomName(roomCode, { purpose: 'broadcast', userId }));\r\n }\r\n return roomCodes;\r\n });\r\n if (rejoinError) {\r\n getLogger().warn(`socket room rejoin failed for ${socket.id}`, { error: rejoinError.message });\r\n return;\r\n }\r\n if (shouldLogDev) {\r\n getLogger().debug(`socket ${socket.id} (re)joined rooms: ${(codes ?? []).join(', ') || '(none)'}`);\r\n }\r\n })();\r\n};\r\n\r\nexport const loadSocket = (httpServer: HttpServer, options: LoadSocketOptions = {}): LoadSocketResult => {\r\n const config = getProjectConfig();\r\n const shouldLogDev = config.logging.devLogs;\r\n const shouldLogSocketStartup = config.logging.socketStartup;\r\n\r\n const io = new SocketIOServer(httpServer, {\r\n cors: {\r\n methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],\r\n origin: (origin, callback) => {\r\n //? CORS does not apply to same-origin requests, and browsers omit the\r\n //? `Origin` header entirely on a same-origin GET — which is exactly\r\n //? what the initial Socket.io polling handshake is in BOTH supported\r\n //? topologies: dev (Vite dev server on :5173 proxying to the backend)\r\n //? and prod-with-router (the @luckystack/router serves the frontend and\r\n //? backend from one origin). Socket.io *must* complete that origin-less\r\n //? HTTP handshake before it can upgrade to WebSocket, so rejecting an\r\n //? absent Origin here — as this layer did before — returned\r\n //? `400 {\"code\":3,\"message\":\"Bad request\"}` (engine.io's\r\n //? `MIDDLEWARE_FAILURE`) and broke every fresh connection.\r\n //?\r\n //? The security rationale that used to gate this (\"the CORS layer is\r\n //? the last browser-origin gate on the WS path\") was misplaced: this\r\n //? callback also fires on the plain-HTTP polling handshake, and an\r\n //? absent Origin there is the *same-origin browser* signal, not a\r\n //? CSRF vector. The real auth gate is the session token extracted from\r\n //? the handshake (`extractTokenFromSocket`) + the auth hooks, which run\r\n //? regardless of the Origin header. `allowOriginless` is kept for\r\n //? symmetry/documented opt-in but no longer gates the handshake.\r\n //?\r\n //? THREAT MODEL: non-browser callers (CLI tools, native apps, server-to-\r\n //? server) can connect without an Origin header and bypass the CORS check.\r\n //? Mitigation: all socket events require a valid session token; use\r\n //? `applySocketMiddlewares` (via `@luckystack/core`) to add an `io.use(...)`\r\n //? middleware that rejects connections lacking a token if authentication is\r\n //? required for ALL socket connections in the application.\r\n if (!origin) {\r\n callback(null, true);\r\n return;\r\n }\r\n if (allowedOrigin(origin)) {\r\n callback(null, true);\r\n } else {\r\n callback(new Error('Not allowed by CORS'));\r\n }\r\n },\r\n credentials: true,\r\n },\r\n maxHttpBufferSize: options.maxHttpBufferSize ?? config.socket.maxHttpBufferSize,\r\n pingTimeout: config.socket.pingTimeout,\r\n pingInterval: config.socket.pingInterval,\r\n });\r\n\r\n setIoInstance(io);\r\n\r\n //? Apply consumer-registered Socket.io middlewares BEFORE the connect\r\n //? handler is attached so they run on the handshake of every incoming\r\n //? socket — same contract as a direct `io.use(...)` call.\r\n applySocketMiddlewares(io);\r\n\r\n //? Redis adapter so room broadcasts fan out across instances. Required for\r\n //? split/fallback deployments; safe overhead in single-instance deploys.\r\n //? We duplicate the pub/sub clients HERE and pass them in (rather than letting\r\n //? `attachSocketRedisAdapter` duplicate internally) so the server bootstrap\r\n //? holds the handles and can `quit()` them on graceful shutdown — `io.close()`\r\n //? does not close these adapter connections.\r\n const pubClient = redis.duplicate();\r\n const subClient = redis.duplicate();\r\n attachSocketRedisAdapter(io, { pubClient, subClient });\r\n\r\n if (shouldLogSocketStartup) {\r\n getLogger().info('SocketIO server initialized (redis adapter attached)');\r\n }\r\n\r\n io.on(socketEventNames.connect, (socket) => {\r\n const token = extractTokenFromSocket(socket);\r\n //? Cache per-connection feature flags so we don't refetch the project\r\n //? config on every event.\r\n const activityBroadcasterEnabled = config.socketActivityBroadcaster ?? false;\r\n const locationProviderEnabled = config.locationProviderEnabled ?? false;\r\n //? `extractLanguageFromHeader` can return an empty string for unparseable\r\n //? headers — those should fall through to the next source, so keep `||`.\r\n /* eslint-disable @typescript-eslint/prefer-nullish-coalescing -- see comment above */\r\n const preferredLocale =\r\n extractLanguageFromHeader(socket.handshake.headers['x-language'])\r\n || extractLanguageFromHeader(socket.handshake.headers['accept-language'])\r\n || undefined;\r\n /* eslint-enable @typescript-eslint/prefer-nullish-coalescing */\r\n\r\n if (token && capabilities.presence) {\r\n void (async () => {\r\n const presence = await getPresence();\r\n if (presence) await presence.socketConnected({ token, io });\r\n })();\r\n }\r\n\r\n void dispatchHook('onSocketConnect', {\r\n socketId: socket.id,\r\n token,\r\n ip: socket.handshake.address,\r\n });\r\n\r\n const ctx: SocketContext = {\r\n socket,\r\n token,\r\n preferredLocale,\r\n activityBroadcasterEnabled,\r\n locationProviderEnabled,\r\n shouldLogDev,\r\n io,\r\n };\r\n\r\n registerApiAndSyncEvents(ctx);\r\n registerCancellationEvents(ctx);\r\n registerRoomEvents(ctx);\r\n registerDisconnectEvent(ctx);\r\n registerUpdateLocationEvent(ctx);\r\n registerActivityEvents(ctx);\r\n rejoinPersistedRooms(ctx);\r\n });\r\n\r\n return { io, adapterClients: { pubClient, subClient } };\r\n};\r\n","//? Boot-time validation that every registry the framework relies on has been\r\n//? populated by the project's overlay files. Replaces silent runtime crashes\r\n//? deep inside a request handler with a single, descriptive error thrown\r\n//? before `httpServer.listen()` is called.\r\n//?\r\n//? The check is intentionally lightweight: it only confirms that the required\r\n//? registrations happened. It does not validate the contents of those\r\n//? registrations — that's done by the registries' own type / runtime\r\n//? validators (e.g. `getProjectConfig()` always returns a deeply-merged value).\r\n\r\nimport {\r\n collectSynchronizedEnvKeys,\r\n getLogger,\r\n getProjectConfig,\r\n isDeployConfigRegistered,\r\n isLocalizedNormalizerRegistered,\r\n isProjectConfigRegistered,\r\n isRuntimeMapsProviderRegistered,\r\n resolveEnvKey,\r\n} from '@luckystack/core';\r\nimport { getLogin } from './capabilities';\r\n\r\nexport interface BootstrapRequirements {\r\n /**\r\n * If true, require a deploy config to have been registered. Defaults to\r\n * false because single-instance deployments don't need one.\r\n */\r\n requireDeployConfig?: boolean;\r\n /**\r\n * If true, require a services config to have been registered. Defaults to\r\n * false; only needed when running the router.\r\n */\r\n requireServicesConfig?: boolean;\r\n /**\r\n * If true, require an OAuth provider list to have been registered.\r\n * Defaults to false (an app may use credentials only).\r\n */\r\n requireOAuthProviders?: boolean;\r\n}\r\n\r\nexport const verifyBootstrap = async (requirements: BootstrapRequirements = {}): Promise<void> => {\r\n const missing: string[] = [];\r\n\r\n if (!isProjectConfigRegistered()) {\r\n missing.push(\r\n 'ProjectConfig — call `registerProjectConfig({...})` from `luckystack/core/bootstrap.ts` (or your config.ts).'\r\n );\r\n }\r\n\r\n if (requirements.requireDeployConfig && !isDeployConfigRegistered()) {\r\n missing.push(\r\n 'DeployConfig — call `registerDeployConfig({...})` from `luckystack/deploy/deploy.config.ts`.'\r\n );\r\n }\r\n\r\n if (requirements.requireServicesConfig) {\r\n const { isServicesConfigRegistered } = await import('@luckystack/core');\r\n if (!isServicesConfigRegistered()) {\r\n missing.push(\r\n 'ServicesConfig — call `registerServicesConfig({...})` from `services.config.ts`.'\r\n );\r\n }\r\n }\r\n\r\n if (requirements.requireOAuthProviders) {\r\n const login = await getLogin();\r\n if (login) {\r\n //? Fail only when the registry is empty OR holds ONLY the default\r\n //? `{ name: 'credentials' }` entry. A plain count check (`<= 1`) wrongly\r\n //? rejected a valid single-OAuth-provider, no-credentials app\r\n //? (e.g. `registerOAuthProviders([googleProvider({...})])`) — also length 1.\r\n const providers = login.getOAuthProviders();\r\n const onlyDefaultCredentials =\r\n providers.length === 0 || (providers.length === 1 && providers[0]?.name === 'credentials');\r\n if (onlyDefaultCredentials) {\r\n missing.push(\r\n 'OAuth providers — call `registerOAuthProviders([...])` from `luckystack/login/oauthProviders.ts` (or skip this check if your app uses credentials only).'\r\n );\r\n }\r\n } else {\r\n missing.push(\r\n 'OAuth providers required (`requireOAuthProviders`) but `@luckystack/login` is not installed. Install it, or drop the requirement if this app has no auth.'\r\n );\r\n }\r\n }\r\n\r\n //? Runtime maps provider — without it, every API/sync request silently\r\n //? returns `notFound`. Hard-fail in production; loud-warn in dev because\r\n //? tests and the bare-server dev mode legitimately boot without one.\r\n if (!isRuntimeMapsProviderRegistered()) {\r\n if (resolveEnvKey() === 'production') {\r\n missing.push(\r\n 'RuntimeMapsProvider — call `registerRuntimeMapsProvider({...})` from `server/prod/runtimeMaps.ts`. Without it, every api/sync request returns notFound.'\r\n );\r\n } else {\r\n getLogger().warn(\r\n '[LuckyStack] No RuntimeMapsProvider registered — api/sync requests will resolve to empty maps. Devkit hot-reload usually registers one automatically.',\r\n );\r\n }\r\n }\r\n\r\n //? Localized normalizer — without it, error responses degrade to\r\n //? errorCode-as-message (no i18n). Hard-fail in production; warn in dev.\r\n if (!isLocalizedNormalizerRegistered()) {\r\n if (resolveEnvKey() === 'production') {\r\n missing.push(\r\n 'LocalizedNormalizer — call `registerLocalizedNormalizer({...})` from your bootstrap. Without it, error response messages will be the raw errorCode (no i18n).'\r\n );\r\n } else {\r\n getLogger().warn(\r\n '[LuckyStack] No LocalizedNormalizer registered — error messages will pass through as the raw errorCode.',\r\n );\r\n }\r\n }\r\n\r\n //? SEC-13: the unauthenticated `/_health` endpoint emits per-`synchronizedEnvKeys`\r\n //? fingerprints so the router can detect cross-env drift. The 0.2.0 default is\r\n //? `healthHash.mode: 'hmac'` (salt `'@bootUuid'`), NOT `'plain'` — so this warning\r\n //? fires ONLY when a consumer EXPLICITLY downgrades to `mode: 'plain'`, which emits\r\n //? UNSALTED `sha256(<secret>)`: a public, offline-bruteforceable fingerprint.\r\n //? CAVEAT: the hmac default keys on the bootUuid, which /_health ALSO publishes,\r\n //? so it resists cross-boot rainbow tables but not a same-boot dictionary attack\r\n //? on low-entropy synchronized values — keep synchronized keys high-entropy or\r\n //? gate /_health. Warn, not hard-fail, to preserve boot behavior.\r\n if (isProjectConfigRegistered()) {\r\n const synchronizedKeyCount = collectSynchronizedEnvKeys().length;\r\n const healthHashMode = getProjectConfig().http.healthHash.mode;\r\n if (synchronizedKeyCount > 0 && healthHashMode === 'plain') {\r\n getLogger().warn(\r\n '[LuckyStack] SECURITY: /_health exposes UNSALTED sha256 fingerprints of '\r\n + `${String(synchronizedKeyCount)} synchronized env secret(s) by default. `\r\n + \"Set `http.healthHash.mode` to 'hmac' (or 'salted' with salt '@bootUuid') \"\r\n + 'to stop publishing brute-forceable secret fingerprints to unauthenticated callers.',\r\n );\r\n }\r\n }\r\n\r\n //? L2: the credentials login/register + OAuth-callback endpoints are CSRF-EXEMPT\r\n //? (a same-site session bootstrap can't present a pre-existing CSRF token). That\r\n //? exemption's safety rests on the session cookie being `SameSite=Strict` — a\r\n //? cross-site POST then never carries it, so login-CSRF is impossible. If a\r\n //? consumer relaxes `http.sessionCookieSameSite` to 'Lax'/'None' in cookie mode,\r\n //? a cross-site POST DOES carry the cookie and login-CSRF becomes possible. Warn\r\n //? so the coupling isn't a silent dependency on a mutable config value.\r\n if (isProjectConfigRegistered()) {\r\n const cfg = getProjectConfig();\r\n if (!cfg.session.basedToken && cfg.http.sessionCookieSameSite !== 'Strict') {\r\n getLogger().warn(\r\n `[LuckyStack] SECURITY: http.sessionCookieSameSite is '${cfg.http.sessionCookieSameSite}', not 'Strict', `\r\n + 'but the CSRF-exempt auth-bootstrap endpoints (/auth/api/credentials, /auth/callback/*) rely on '\r\n + \"SameSite=Strict to block cross-site login-CSRF. Use 'Strict', or add your own CSRF/origin check on those routes.\",\r\n );\r\n }\r\n }\r\n\r\n if (missing.length === 0) return;\r\n\r\n const detail = missing.map((line, idx) => ` ${idx + 1}. ${line}`).join('\\n');\r\n throw new Error(\r\n [\r\n '[LuckyStack] Bootstrap incomplete — the following registrations are missing:',\r\n detail,\r\n '',\r\n 'See docs/ARCHITECTURE_PACKAGING.md (overlay layout) for the recommended bootstrap order.',\r\n ].join('\\n')\r\n );\r\n};\r\n","//? Production runtime-maps loader shipped by the framework so consumers\r\n//? don't have to maintain their own `server/prod/runtimeMaps.ts`. The\r\n//? consumer only supplies a `loadGenerated` callback because dynamic-import\r\n//? path resolution is module-scoped — the framework cannot resolve\r\n//? `./generatedApis.<preset>` on the consumer's behalf.\r\n//?\r\n//? Multiple presets can be loaded into a single process by passing a\r\n//? comma-separated list as the first positional argv (parsed by\r\n//? `@luckystack/server/parseArgv`). All resolved maps are shallow-merged\r\n//? into one runtime view; collisions throw at boot (services own one\r\n//? preset by design, see docs/ARCHITECTURE_PACKAGING.md §10).\r\n//?\r\n//? Dev branch lazy-imports `@luckystack/devkit` to pull the auto-discovered\r\n//? api/sync/function maps. Devkit is excluded from the production bundle, so\r\n//? this dynamic import is never reached when NODE_ENV === 'production'.\r\n\r\nimport {\r\n getLogger,\r\n registerRuntimeMapsProvider,\r\n resolveEnvKey,\r\n type RuntimeApiMapsResult,\r\n type RuntimeMapsProvider,\r\n type RuntimeSyncMapsResult,\r\n} from '@luckystack/core';\r\nimport { getParsedBundles } from './argv';\r\n\r\ntype RuntimeMapRecord = Record<string, unknown>;\r\n\r\n/**\r\n * The shape that the generated maps module must export. `scripts/generateServerRequests.ts`\r\n * emits exactly this shape: `{ apis, syncs, functions }` where each value is a\r\n * `Record<string, Handler>`. Consumers can import this type to validate their\r\n * generated file in CI or type-check the `loadGenerated` callback return.\r\n */\r\nexport interface GeneratedRuntimeMapsModule {\r\n apis: RuntimeMapRecord;\r\n syncs: RuntimeMapRecord;\r\n functions: RuntimeMapRecord;\r\n}\r\n\r\ninterface LoadedRuntimeMaps {\r\n apisObject: RuntimeMapRecord;\r\n syncObject: RuntimeMapRecord;\r\n functionsObject: RuntimeMapRecord;\r\n}\r\n\r\ninterface DevkitRuntimeMaps {\r\n devApis: RuntimeMapRecord;\r\n devSyncs: RuntimeMapRecord;\r\n devFunctions: RuntimeMapRecord;\r\n}\r\n\r\nconst emptyRuntimeMaps: LoadedRuntimeMaps = {\r\n apisObject: {},\r\n syncObject: {},\r\n functionsObject: {},\r\n};\r\n\r\nconst isRuntimeMapRecord = (value: unknown): value is RuntimeMapRecord =>\r\n Boolean(value) && typeof value === 'object';\r\n\r\nconst normalizeGeneratedModule = (moduleValue: unknown, preset: string): LoadedRuntimeMaps => {\r\n const moduleRecord = moduleValue && typeof moduleValue === 'object'\r\n ? (moduleValue as Record<string, unknown>)\r\n : {};\r\n\r\n const apiCandidate = moduleRecord.apis;\r\n const syncCandidate = moduleRecord.syncs;\r\n const functionCandidate = moduleRecord.functions;\r\n\r\n //? Warn when none of the expected keys are present — this is almost certainly\r\n //? a mis-shaped module (wrong export, stale generated file, default-export\r\n //? wrapper) rather than a legitimately empty app. Silent coercion to `{}`\r\n //? causes every route to silently 404, which is hard to diagnose.\r\n if (\r\n !isRuntimeMapRecord(apiCandidate)\r\n && !isRuntimeMapRecord(syncCandidate)\r\n && !isRuntimeMapRecord(functionCandidate)\r\n ) {\r\n getLogger().warn(\r\n `[luckystack:runtimeMaps] preset \"${preset}\" loaded but has no recognised shape ` +\r\n `(expected { apis, syncs, functions }). ` +\r\n `Verify that generateServerRequests has been run and the correct file is imported. ` +\r\n `All routes for this preset will return notFound.`,\r\n );\r\n }\r\n\r\n return {\r\n apisObject: isRuntimeMapRecord(apiCandidate) ? apiCandidate : {},\r\n syncObject: isRuntimeMapRecord(syncCandidate) ? syncCandidate : {},\r\n functionsObject: isRuntimeMapRecord(functionCandidate) ? functionCandidate : {},\r\n };\r\n};\r\n\r\nexport interface ProdRuntimeMapsLoaderOptions {\r\n /**\r\n * Dynamic-import callback for the generated maps module of a given preset.\r\n * Called once per preset per process lifetime; the result is cached.\r\n *\r\n * The resolved module must have shape `{ apis, syncs, functions }` (the\r\n * shape `scripts/generateServerRequests.ts` emits — see\r\n * {@link GeneratedRuntimeMapsModule}). A mis-shaped module logs a warning\r\n * and results in every route for that preset returning `notFound`.\r\n *\r\n * Pass a function that calls `import()` with a path relative to YOUR\r\n * server-side module — the framework cannot resolve a relative path on\r\n * your behalf because dynamic-import resolution is module-scoped.\r\n *\r\n * @example\r\n * loadGenerated: (preset) => import(`./prod/generatedApis.${preset}`)\r\n */\r\n //? Returns `unknown` because the consumer hands us an `import()` of a generated\r\n //? module whose shape we cannot statically guarantee; `normalizeGeneratedModule`\r\n //? validates it at runtime against `GeneratedRuntimeMapsModule` and warns on drift.\r\n loadGenerated: (preset: string) => Promise<unknown>;\r\n /**\r\n * Override the preset(s) to load. Skips the argv lookup. Accepts a single\r\n * preset name or an array. Useful in tests or when the preset list comes\r\n * from a non-argv source.\r\n */\r\n preset?: string | string[];\r\n}\r\n\r\nconst resolvePresets = (options: ProdRuntimeMapsLoaderOptions): string[] => {\r\n const fromOptions = options.preset;\r\n if (typeof fromOptions === 'string' && fromOptions.length > 0) {\r\n return [fromOptions];\r\n }\r\n if (Array.isArray(fromOptions) && fromOptions.length > 0) {\r\n return [...new Set(fromOptions)];\r\n }\r\n const fromArgv = getParsedBundles();\r\n if (fromArgv.length > 0) {\r\n return fromArgv;\r\n }\r\n return ['default'];\r\n};\r\n\r\nconst mergeInto = (\r\n target: RuntimeMapRecord,\r\n source: RuntimeMapRecord,\r\n kind: 'api' | 'sync' | 'function',\r\n fromPreset: string,\r\n keyOrigin: Map<string, string>,\r\n): void => {\r\n for (const key of Object.keys(source)) {\r\n const previousPreset = keyOrigin.get(key);\r\n if (previousPreset !== undefined && previousPreset !== fromPreset) {\r\n throw new Error(\r\n `[luckystack:runtimeMaps] ${kind} key collision: \"${key}\" present in both ` +\r\n `preset \"${previousPreset}\" and preset \"${fromPreset}\". ` +\r\n `Services must belong to exactly one preset (see docs/ARCHITECTURE_PACKAGING.md §10).`,\r\n );\r\n }\r\n keyOrigin.set(key, fromPreset);\r\n target[key] = source[key];\r\n }\r\n};\r\n\r\n/**\r\n * Build a `RuntimeMapsProvider` that loads generated maps in production and\r\n * delegates to `@luckystack/devkit`'s discovery in dev. Same shape consumers\r\n * used to hand-roll in `server/prod/runtimeMaps.ts`.\r\n *\r\n * Call `registerRuntimeMapsProvider(...)` with the result, or use\r\n * `registerProdRuntimeMapsProvider(...)` (this module) to do both in one\r\n * step.\r\n */\r\nconst isProduction = (): boolean => resolveEnvKey() === 'production';\r\n\r\nexport const createProdRuntimeMapsProvider = (\r\n options: ProdRuntimeMapsLoaderOptions,\r\n): RuntimeMapsProvider => {\r\n let prodMapsPromise: Promise<LoadedRuntimeMaps> | null = null;\r\n\r\n let devkitModulePromise: Promise<DevkitRuntimeMaps> | null = null;\r\n const getDevkit = async (): Promise<DevkitRuntimeMaps> => {\r\n devkitModulePromise ??= import('@luckystack/devkit') as Promise<DevkitRuntimeMaps>;\r\n return await devkitModulePromise;\r\n };\r\n\r\n const loadProdRuntimeMaps = async (): Promise<LoadedRuntimeMaps> => {\r\n if (prodMapsPromise) return await prodMapsPromise;\r\n\r\n prodMapsPromise = (async () => {\r\n const presets = resolvePresets(options);\r\n const merged: LoadedRuntimeMaps = {\r\n apisObject: {},\r\n syncObject: {},\r\n functionsObject: {},\r\n };\r\n const apiOrigin = new Map<string, string>();\r\n const syncOrigin = new Map<string, string>();\r\n const functionOrigin = new Map<string, string>();\r\n\r\n const loadedModules = await Promise.all(\r\n presets.map(async (preset) => ({\r\n preset,\r\n mod: await options.loadGenerated(preset).catch(() => null),\r\n })),\r\n );\r\n\r\n let loadedAny = false;\r\n for (const { preset, mod } of loadedModules) {\r\n if (!mod) {\r\n getLogger().warn(\r\n `[luckystack:runtimeMaps] preset \"${preset}\" failed to load — skipping. ` +\r\n `Calls owned by that preset will return notFound until the generated module resolves.`,\r\n );\r\n continue;\r\n }\r\n loadedAny = true;\r\n const normalized = normalizeGeneratedModule(mod, preset);\r\n mergeInto(merged.apisObject, normalized.apisObject, 'api', preset, apiOrigin);\r\n mergeInto(merged.syncObject, normalized.syncObject, 'sync', preset, syncOrigin);\r\n mergeInto(merged.functionsObject, normalized.functionsObject, 'function', preset, functionOrigin);\r\n }\r\n\r\n if (!loadedAny) {\r\n getLogger().warn(\r\n `[luckystack:runtimeMaps] no presets resolved (tried: ${presets.join(', ')}). ` +\r\n `Every api/sync request will return notFound until at least one generated module loads.`,\r\n );\r\n return emptyRuntimeMaps;\r\n }\r\n\r\n return merged;\r\n })();\r\n\r\n return await prodMapsPromise;\r\n };\r\n\r\n const getRuntimeApiMaps = async (): Promise<RuntimeApiMapsResult> => {\r\n if (!isProduction()) {\r\n const { devApis, devFunctions } = await getDevkit();\r\n return {\r\n apisObject: isRuntimeMapRecord(devApis) ? devApis : {},\r\n functionsObject: isRuntimeMapRecord(devFunctions) ? devFunctions : {},\r\n };\r\n }\r\n const { apisObject, functionsObject } = await loadProdRuntimeMaps();\r\n return { apisObject, functionsObject };\r\n };\r\n\r\n const getRuntimeSyncMaps = async (): Promise<RuntimeSyncMapsResult> => {\r\n if (!isProduction()) {\r\n const { devSyncs, devFunctions } = await getDevkit();\r\n return {\r\n syncObject: isRuntimeMapRecord(devSyncs) ? devSyncs : {},\r\n functionsObject: isRuntimeMapRecord(devFunctions) ? devFunctions : {},\r\n };\r\n }\r\n const { syncObject, functionsObject } = await loadProdRuntimeMaps();\r\n return { syncObject, functionsObject };\r\n };\r\n\r\n return { getRuntimeApiMaps, getRuntimeSyncMaps };\r\n};\r\n\r\n/**\r\n * Convenience wrapper around `createProdRuntimeMapsProvider` +\r\n * `registerRuntimeMapsProvider`. Most consumers want this — pass the\r\n * loader callback once and the runtime is wired.\r\n */\r\nexport const registerProdRuntimeMapsProvider = (\r\n options: ProdRuntimeMapsLoaderOptions,\r\n): RuntimeMapsProvider => {\r\n const provider = createProdRuntimeMapsProvider(options);\r\n registerRuntimeMapsProvider(provider);\r\n return provider;\r\n};\r\n","import type { Server as HttpServer } from 'node:http';\nimport type { Server as SocketIOServer } from 'socket.io';\nimport type { Redis as RedisClient } from 'ioredis';\nimport {\n dispatchHook,\n flushErrorTrackers,\n getLogger,\n tryCatch,\n} from '@luckystack/core';\nimport type { StopLuckyStackServerOptions } from './types';\n\nconst DEFAULT_SHUTDOWN_TIMEOUT_MS = 10_000;\n\nexport interface GracefulShutdownDeps {\n httpServer: HttpServer;\n ioServer: SocketIOServer;\n adapterClients: { pubClient: RedisClient; subClient: RedisClient };\n}\n\n//? Race a promise against a timeout so one hanging shutdown step (a stuck\n//? `flush`, a socket that never closes, a half-dead Redis connection) can never\n//? stall the whole sequence past the deadline. Resolves `true` if `fn`\n//? completed, `false` if the timeout won. Never rejects — shutdown is\n//? best-effort and a thrown step must not abort the remaining steps.\nconst withTimeout = async (\n label: string,\n timeoutMs: number,\n fn: () => Promise<void>,\n): Promise<boolean> => {\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<false>((resolve) => {\n timer = setTimeout(() => {\n getLogger().warn(`[shutdown] step \"${label}\" exceeded ${String(timeoutMs)}ms — moving on`);\n resolve(false);\n }, timeoutMs);\n //? Don't let the timer keep the event loop alive once everything else is done.\n timer.unref();\n });\n const run = (async (): Promise<true> => {\n const [error] = await tryCatch(fn);\n if (error) getLogger().warn(`[shutdown] step \"${label}\" failed: ${error.message}`);\n return true;\n })();\n const completed = await Promise.race([run, timeout]);\n if (timer) clearTimeout(timer);\n return completed;\n};\n\nconst closeHttpServer = (httpServer: HttpServer): Promise<void> =>\n new Promise<void>((resolve, reject) => {\n //? `close()` stops accepting NEW connections and fires the callback once all\n //? existing connections end. The timeout race in the caller bounds the wait\n //? for slow keep-alive clients. Surface the callback error (e.g.\n //? `ERR_SERVER_NOT_RUNNING` when close() is called before listen()) so the\n //? caller's `withTimeout` tryCatch logs it like every other shutdown step,\n //? instead of resolving silently.\n httpServer.close((error) => { if (error) reject(error); else resolve(); });\n });\n\nconst closeIoServer = (ioServer: SocketIOServer): Promise<void> =>\n new Promise<void>((resolve, reject) => {\n //? `io.close(cb)` also returns a promise in socket.io 4.x; we drive\n //? completion off the callback, so explicitly ignore the returned promise.\n //? Surface the callback error (forwarded from the underlying http close) so\n //? the caller's `withTimeout` tryCatch logs it instead of swallowing it.\n void ioServer.close((error) => { if (error) reject(error); else resolve(); });\n });\n\nconst quitRedisClient = async (client: RedisClient): Promise<void> => {\n await client.quit();\n};\n\n/**\n * Run the graceful-shutdown sequence (MIS-016). Ordered + isolated:\n * 1. Stop accepting NEW HTTP connections (`httpServer.close()` is kicked off\n * immediately; we await it last so in-flight requests can drain).\n * 2. Dispatch the core `preServerStop` hook so consumers can flush queues /\n * release leases / close pools.\n * 3. `flushErrorTrackers()` so buffered events aren't lost — bounded by the\n * timeout race.\n * 4. Close the Socket.io server, then the HTTP server, then `quit()` the\n * Redis-adapter pub/sub clients.\n *\n * Every step is wrapped in {@link withTimeout} so a single failing/hanging step\n * cannot hang the whole shutdown — the sequence always settles within\n * `timeoutMs` per step. Never rejects.\n */\nexport const runGracefulShutdown = async (\n deps: GracefulShutdownDeps,\n options: StopLuckyStackServerOptions = {},\n): Promise<void> => {\n const { httpServer, ioServer, adapterClients } = deps;\n const reason = options.reason ?? 'manual';\n const timeoutMs = options.timeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;\n\n getLogger().info(`[shutdown] graceful shutdown started (reason=${reason})`);\n\n //? Kick off \"stop accepting new connections\" first. `httpServer.close()` only\n //? finishes once existing connections drain, so START it now and AWAIT it near\n //? the end — that way the hook + flush run while connections wind down.\n const httpClosed = withTimeout('http-close', timeoutMs, () => closeHttpServer(httpServer));\n\n //? Best-effort consumer shutdown hook. A returned stop signal is ignored — the\n //? process is going down — and the dispatcher already swallows per-handler\n //? throws; the timeout guards against a handler that simply never resolves.\n await withTimeout('preServerStop-hook', timeoutMs, async () => {\n await dispatchHook('preServerStop', { reason, timeoutMs });\n });\n\n //? Flush buffered error-tracker events before tearing down. Bounded so a\n //? wedged transport (e.g. Sentry unreachable) can't hang shutdown.\n await withTimeout('flush-error-trackers', timeoutMs, () => flushErrorTrackers());\n\n //? Close the socket layer before the HTTP server fully settles so no new\n //? socket upgrades sneak in.\n await withTimeout('io-close', timeoutMs, () => closeIoServer(ioServer));\n\n await httpClosed;\n\n //? Disconnect the Redis-adapter pub/sub clients — `io.close()` leaves them\n //? open, so without this the process keeps two live Redis sockets.\n await withTimeout('redis-pub-quit', timeoutMs, () => quitRedisClient(adapterClients.pubClient));\n await withTimeout('redis-sub-quit', timeoutMs, () => quitRedisClient(adapterClients.subClient));\n\n getLogger().info('[shutdown] graceful shutdown complete');\n};\n","import fs from 'node:fs';\r\nimport path from 'node:path';\r\nimport { getLogger } from '@luckystack/core';\r\n\r\n//? Dev-only handshake between the two local processes (`npm run server` and\r\n//? `npm run client`). When the backend auto-increments off a busy `SERVER_PORT`\r\n//? (see `listenLuckyStackServer`), the Vite proxy — which only knows the `.env`\r\n//? `SERVER_PORT` — would keep targeting the OLD port and every proxied request\r\n//? (api / sync / the socket.io websocket) would miss. So the backend advertises\r\n//? its ACTUAL bound port here and the template `vite.config.ts` reads it.\r\n//?\r\n//? Location: `node_modules/.luckystack/` — always gitignored (no `.gitignore`\r\n//? edit, no repo clutter) and present at the shared project cwd for both\r\n//? processes. Purely ephemeral: rewritten on every dev boot, removed on exit;\r\n//? a stale file is harmless (the proxy falls back to `SERVER_PORT`).\r\nconst DEV_SERVER_FILE = ['node_modules', '.luckystack', 'dev-server.json'] as const;\r\n\r\nconst devServerInfoPath = (): string => path.join(process.cwd(), ...DEV_SERVER_FILE);\r\n\r\nexport interface DevServerInfo {\r\n ip: string;\r\n port: number;\r\n pid: number;\r\n}\r\n\r\n//? Best-effort: a failed write must NEVER take the server down. Worst case the\r\n//? proxy falls back to `.env` `SERVER_PORT` (the pre-existing behaviour).\r\nexport const writeDevServerInfo = (ip: string, port: number): void => {\r\n try {\r\n const file = devServerInfoPath();\r\n fs.mkdirSync(path.dirname(file), { recursive: true });\r\n const info: DevServerInfo = { ip, port, pid: process.pid };\r\n fs.writeFileSync(file, `${JSON.stringify(info, null, 2)}\\n`);\r\n } catch (error) {\r\n getLogger().debug(\r\n `[dev-server-info] could not write port file (proxy will fall back to SERVER_PORT): ${\r\n error instanceof Error ? error.message : String(error)\r\n }`,\r\n );\r\n }\r\n};\r\n\r\n//? Remove the file on a clean exit so a later `npm run client` started without a\r\n//? backend doesn't proxy to a dead port. Silently ignores a missing file.\r\nexport const clearDevServerInfo = (): void => {\r\n try {\r\n fs.rmSync(devServerInfoPath(), { force: true });\r\n } catch {\r\n //? Ignore — a leftover file is rewritten on the next dev boot, and the proxy\r\n //? falls back to SERVER_PORT when it points at a port nothing answers on.\r\n }\r\n};\r\n","//? One-call bootstrap helper. Auto-imports the consumer's `luckystack/`\r\n//? overlay folder in the canonical order, then starts the server.\r\n//?\r\n//? The convention is documented in `docs/ARCHITECTURE_PACKAGING.md`. Each\r\n//? overlay file calls one of the framework's `register*` functions at module\r\n//? load (side-effect import), so by the time `createLuckyStackServer` runs\r\n//? the registries are populated.\r\n//?\r\n//? Order matters because some registries depend on others (e.g. login's\r\n//? userAdapter needs the Prisma client registered first). The framework\r\n//? imports them in topological order.\r\n\r\nimport path from 'node:path';\r\nimport fs from 'node:fs';\r\nimport { pathToFileURL } from 'node:url';\r\nimport { ROOT_DIR, tryCatch } from '@luckystack/core';\r\nimport { createLuckyStackServer } from './createServer';\r\nimport { OPTIONAL_PACKAGES, canResolve, getLogin } from './capabilities';\r\nimport type {\r\n CreateLuckyStackServerOptions,\r\n RunningLuckyStackServer,\r\n} from './types';\r\n\r\nexport interface BootstrapLuckyStackOptions extends CreateLuckyStackServerOptions {\r\n /**\r\n * Folder name (relative to project root) that contains the overlay files.\r\n * Default: `luckystack`. Each subfolder mirrors a framework package and\r\n * holds the project's overrides for that package.\r\n */\r\n overlayRoot?: string;\r\n /**\r\n * Skip auto-loading the overlay folder. Useful for tests that build the\r\n * registries by hand.\r\n */\r\n skipOverlayLoad?: boolean;\r\n}\r\n\r\n//? Exported (0.4.2): the consumer's `scripts/bundleServer.mjs` imports this at\r\n//? build time so the prod-bundle overlay walk can never drift from the dev\r\n//? walk — a hardcoded copy once silently dropped a slot (cron) from prod.\r\nexport const OVERLAY_ORDER = [\r\n // Core registries first — clients, paths, routing rules. Anything below\r\n // depends on these being in place.\r\n 'core',\r\n // Deploy + services topology — needed by the router but harmless for\r\n // single-instance deploys.\r\n 'deploy',\r\n // Auth — OAuth providers + user adapter sit on top of core.\r\n 'login',\r\n // Transactional email sender registration (optional @luckystack/email).\r\n 'email',\r\n // Sentry / docs-ui / presence sit on top of core but don't block boot.\r\n 'sentry',\r\n 'presence',\r\n // Cron jobs — `registerCronJob(...)` calls in `luckystack/cron/*.ts` lazily\r\n // start the leader-elected scheduler (optional @luckystack/cron).\r\n 'cron',\r\n 'docs-ui',\r\n // Server overlay last — typically empty, but a place to wire framework\r\n // hooks (`registerHook('onSocketConnect', ...)` etc.) before listen().\r\n 'server',\r\n];\r\n\r\nconst importIfExists = async (filePath: string): Promise<void> => {\r\n if (!fs.existsSync(filePath)) return;\r\n const [error] = await tryCatch(async () => {\r\n await import(pathToFileURL(filePath).href);\r\n });\r\n if (error) {\r\n //? Fail fast, but ACTIONABLY: a raw ERR_MODULE_NOT_FOUND from an overlay\r\n //? file (classic cause: `luckystack remove <pkg>` dropped a dependency\r\n //? that `luckystack/<pkg>/*.ts` still imports) tells the consumer nothing\r\n //? about WHICH of their files broke the boot.\r\n throw new Error(\r\n `[luckystack] failed to load overlay file ${filePath}: ${error.message}. ` +\r\n 'If you removed an optional package (e.g. via `luckystack remove <feature>` or `luckystack manage`), ' +\r\n 'delete or update the overlay files that still import it (the `luckystack/<feature>/` folder).',\r\n );\r\n }\r\n};\r\n\r\n//? Production-bundle seam. `loadOverlayFolder` imports raw `luckystack/**/*.ts`\r\n//? files at runtime — fine under tsx in dev, but a bundled server runs under\r\n//? plain `node`, where importing a `.ts` file is a hard crash\r\n//? (ERR_UNKNOWN_FILE_EXTENSION). The server bundler (`scripts/bundleServer.mjs`)\r\n//? therefore generates an entry that statically imports the overlay files\r\n//? (so esbuild compiles them into the bundle) and registers a loader here.\r\n//? When a loader is registered, `bootstrapLuckyStack` runs it INSTEAD of the\r\n//? filesystem walk — same files, same order, but resolved at build time.\r\ntype OverlayLoader = () => Promise<void>;\r\nlet registeredOverlayLoader: OverlayLoader | null = null;\r\n\r\nexport const registerOverlayLoader = (loader: OverlayLoader): void => {\r\n registeredOverlayLoader = loader;\r\n};\r\n\r\nconst loadOverlayFolder = async (overlayRoot: string): Promise<void> => {\r\n const overlayAbs = path.isAbsolute(overlayRoot)\r\n ? overlayRoot\r\n : path.join(ROOT_DIR, overlayRoot);\r\n\r\n if (!fs.existsSync(overlayAbs)) {\r\n //? No overlay folder is fine — projects can register everything from a\r\n //? single `config.ts` if they prefer the legacy layout.\r\n return;\r\n }\r\n\r\n for (const packageName of OVERLAY_ORDER) {\r\n const packageDir = path.join(overlayAbs, packageName);\r\n if (!fs.existsSync(packageDir)) continue;\r\n\r\n //? Load `index.ts` first if present, then any other `*.ts` files in\r\n //? alphabetical order. Each file is responsible for its own\r\n //? side-effect registration.\r\n const indexCandidates = ['index.ts', 'index.js'];\r\n for (const candidate of indexCandidates) {\r\n await importIfExists(path.join(packageDir, candidate));\r\n }\r\n\r\n const entries = fs.readdirSync(packageDir).toSorted();\r\n for (const entry of entries) {\r\n if (indexCandidates.includes(entry)) continue;\r\n if (!entry.endsWith('.ts') && !entry.endsWith('.js')) continue;\r\n await importIfExists(path.join(packageDir, entry));\r\n }\r\n }\r\n};\r\n\r\n//? Auto-detect phase (0.2.0 \"install-anything-anytime\"). For each optional\r\n//? package that ships a `./register` side-effect subpath, import it so the\r\n//? package self-wires from env WITHOUT any consumer code edit — `npm i\r\n//? @luckystack/<pkg>` + env + restart is enough. Resolve-guarded so an absent\r\n//? package is a silent no-op; a single register's internal failure must never\r\n//? crash boot (register modules log their own errors). Runs BEFORE\r\n//? `loadOverlayFolder` so a consumer overlay file (last writer) can override the\r\n//? auto-wired defaults.\r\nconst importOptionalPackageRegisters = async (): Promise<void> => {\r\n for (const pkg of OPTIONAL_PACKAGES) {\r\n const specifier = `@luckystack/${pkg}/register`;\r\n if (!canResolve(specifier)) continue;\r\n await importIfExistsSpecifier(specifier);\r\n }\r\n};\r\n\r\nconst importIfExistsSpecifier = async (specifier: string): Promise<void> => {\r\n try {\r\n await import(specifier);\r\n } catch {\r\n //? A register module that throws at load (e.g. an internal peer-dep error)\r\n //? should not take the whole server down — the module is responsible for\r\n //? logging its own failure. Boot continues with that feature degraded.\r\n }\r\n};\r\n\r\nexport const bootstrapLuckyStack = async (\r\n options: BootstrapLuckyStackOptions = {}\r\n): Promise<RunningLuckyStackServer> => {\r\n const overlayRoot = options.overlayRoot ?? 'luckystack';\r\n\r\n if (!options.skipOverlayLoad) {\r\n //? Package self-wiring first, consumer overlay second (overlay overrides).\r\n await importOptionalPackageRegisters();\r\n //? Production bundle: overlay files were compiled into the bundle and the\r\n //? generated entry registered a loader — never touch raw .ts on disk then.\r\n await (registeredOverlayLoader ? registeredOverlayLoader() : loadOverlayFolder(overlayRoot));\r\n }\r\n\r\n //? Force login to load when installed so its session provider registers into\r\n //? core (`registerSessionProvider`, side-effect of the package index) even in\r\n //? an app that never imports any `@luckystack/login` export directly. No-op\r\n //? when login is absent — the app runs unauthenticated.\r\n await getLogin();\r\n\r\n const server = await createLuckyStackServer(options);\r\n return server;\r\n};\r\n","//? Backward-compatibility re-export. The error-formatter registry moved\r\n//? to @luckystack/core so transport handlers in @luckystack/api and\r\n//? @luckystack/sync can dispatch per-route + global formatters without\r\n//? depending on this server package (which would cycle). Consumers'\r\n//? existing `import { registerErrorFormatter } from '@luckystack/server'`\r\n//? continues to resolve through this shim.\r\n\r\nexport {\r\n registerErrorFormatter,\r\n getErrorFormatter,\r\n applyErrorFormatter,\r\n} from '@luckystack/core';\r\nexport type { ErrorFormatter, ErrorFormatterContext } from '@luckystack/core';\r\n"],"mappings":"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","names":["getLogger","getProjectConfig","tryCatch","isProduction","resolveEnvKey","dispatchHook","dispatchHook","extractTokenFromRequest","getLogger","getProjectConfig","readSession","tryCatch","getProjectConfig","getProjectConfig","getCsrfConfig","readSession","getCsrfConfig","readSession","getProjectConfig","getProjectConfig","redis","tryCatch","getProjectConfig","tryCatch","redis","dispatchHook","getProjectConfig","resolveEnvKey","getProjectConfig","dispatchHook","checkRateLimit","getProjectConfig","resolveClientIp","getProjectConfig","resolveClientIp","checkRateLimit","result","getLogger","getProjectConfig","tryCatch","tryCatch","getLogger","http","getProjectConfig","getProjectConfig","getProjectConfig","getLogger","getProjectConfig","getProjectConfig","getLogger","dispatchHook","getLogger","tryCatch","getProjectConfig","getProjectConfig","resolveClientIp","tryCatch","getLogger","dispatchHook","captureException","dispatchHook","extractTokenFromRequest","getLogger","tryCatch","tryCatch","extractTokenFromRequest","getLogger","captureException","dispatchHook","captureException","getLogger","tryCatch","tryCatch","getLogger","captureException","http","getProjectConfig","getLogger","handlers","readSession","dispatchHook","extractTokenFromRequest","tryCatch","allowedOrigin","redis","getLogger","getProjectConfig","dispatchHook","readSession","tryCatch","tryCatch","readSession","dispatchHook","getLogger","getProjectConfig","allowedOrigin","redis","getLogger","getProjectConfig","resolveEnvKey","resolveEnvKey","getLogger","getProjectConfig","getLogger","resolveEnvKey","getLogger","resolveEnvKey","dispatchHook","getLogger","tryCatch","path","getLogger","dispatchHook","getLogger","tryCatch","isProduction","getProjectConfig","resolveEnvKey","path","fs","tryCatch","fs","tryCatch","path"]}
|
|
1
|
+
{"version":3,"sources":["../src/createServer.ts","../src/httpHandler.ts","../src/logSanitize.ts","../src/securityHeadersRegistry.ts","../src/httpRoutes/csrfMiddleware.ts","../src/capabilities.ts","../src/originExemptRegistry.ts","../src/httpRoutes/timingSafeEqual.ts","../src/httpRoutes/csrfRoute.ts","../src/httpRoutes/faviconRoute.ts","../src/httpRoutes/healthRoutes.ts","../src/httpRoutes/testResetRoute.ts","../src/httpRoutes/uploadsRoute.ts","../src/httpRoutes/authApiRoute.ts","../src/httpRoutes/sessionCookie.ts","../src/httpRoutes/authSecondFactorRoutes.ts","../src/httpRoutes/authLogoutRoute.ts","../src/httpRoutes/authProvidersRoute.ts","../src/httpRoutes/authCallbackRoute.ts","../src/httpRoutes/apiRoute.ts","../src/sse.ts","../src/httpRoutes/resolveRequesterIp.ts","../src/httpRoutes/syncRoute.ts","../src/httpRoutes/customRoutes.ts","../src/customRoutesRegistry.ts","../src/httpRoutes/staticRoutes.ts","../src/loadSocket.ts","../src/verifyBootstrap.ts","../src/runtimeMapsLoader.ts","../src/stopServer.ts","../src/devServerInfo.ts","../src/bootstrap.ts","../src/errorFormatterRegistry.ts"],"sourcesContent":["import http, { type Server as HttpServer } from 'node:http';\r\nimport { registerBindAddress, writeBootUuid, getLogger, getProjectConfig, tryCatch, isProduction, resolveEnvKey, dispatchHook, resetDefaultRedisClient } from '@luckystack/core';\r\nimport { handleHttpRequest } from './httpHandler';\r\nimport { loadSocket } from './loadSocket';\r\nimport { verifyBootstrap } from './verifyBootstrap';\r\nimport { registerProdRuntimeMapsProvider } from './runtimeMapsLoader';\r\nimport { getParsedPort } from './argv';\r\nimport { canResolve } from './capabilities';\r\nimport { runGracefulShutdown } from './stopServer';\r\nimport { writeDevServerInfo, clearDevServerInfo } from './devServerInfo';\r\nimport type {\r\n CreateLuckyStackServerOptions,\r\n RunningLuckyStackServer,\r\n StopLuckyStackServerOptions,\r\n} from './types';\r\n\r\n/**\r\n * One-call server bootstrap for a LuckyStack project.\r\n *\r\n * Wires together:\r\n * - HTTP server with framework routes (`/api/*`, `/sync/*`, `/_health`,\r\n * `/_test/reset`, `/uploads/*`, `/auth/api`, `/auth/callback`)\r\n * - Socket.io server with the Redis adapter, room handlers, presence\r\n * integration, location sync\r\n * - Boot-UUID write so the router's handshake can verify topology\r\n * - Optional dev-mode tooling (devkit hot reload + REPL)\r\n *\r\n * Project responsibilities (passed in as options):\r\n * - `serveFile` / `serveFavicon` — your project's static file handlers\r\n * - `customRoutes` — any additional HTTP routes your app needs\r\n *\r\n * Pre-conditions:\r\n * - `registerProjectConfig(...)` must have run (side-effect import of your\r\n * `config.ts` does this).\r\n * - `registerDeployConfig(...)` must have run (side-effect import of your\r\n * `deploy.config.ts`).\r\n * - `registerRuntimeMapsProvider(...)` must have run (side-effect import of\r\n * your `server/prod/runtimeMaps.ts`).\r\n * - `registerLocalizedNormalizer(...)` must have run (side-effect import of\r\n * your `server/utils/responseNormalizer.ts`).\r\n *\r\n * @example\r\n * ```ts\r\n * import './config';\r\n * import './deploy.config';\r\n * import './prod/runtimeMaps';\r\n * import './utils/responseNormalizer';\r\n * import { createLuckyStackServer } from '@luckystack/server';\r\n * import { serveFile, serveFavicon } from './prod/serveFile';\r\n *\r\n * const server = await createLuckyStackServer({ serveFile, serveFavicon });\r\n * await server.listen();\r\n * ```\r\n */\r\n//? Extracted verbatim from the previous inline `if (enableDevTools) { ... }`\r\n//? block. Same dynamic imports, same ordering, same SIGINT/SIGTERM handlers —\r\n//? hoisted so the bootstrap function reads as a sequence of named steps.\r\nconst initDevTools = async (): Promise<void> => {\r\n //? Dev-only: console-log color tagger + devkit hot reload + REPL.\r\n //? Kept dynamic so tier-A consumers in production never load the\r\n //? typescript compiler API or chokidar's filesystem watchers.\r\n const { initConsolelog } = await import('@luckystack/core');\r\n initConsolelog();\r\n //? Belt-and-braces: explicit SIGINT/SIGTERM handler so Ctrl+C is honored even\r\n //? if a sync CPU burst (TS Program build, large require chain) is still in\r\n //? flight when the signal arrives. Installed BEFORE the guarded devkit import\r\n //? so Ctrl+C still works when devkit is absent and we early-return below.\r\n //? Give `preServerStop` subscribers (cron lease release, tracker flush) a\r\n //? BOUNDED window before the hard exit — without it a dev Ctrl+C leaves the\r\n //? cron leader lease dangling until its TTL, so no jobs fire for up to 30s\r\n //? after every restart. Dev iteration speed still wins: 2s cap, then exit.\r\n const devSignalExit = (reason: 'SIGINT' | 'SIGTERM'): void => {\r\n void Promise.race([\r\n dispatchHook('preServerStop', { reason, timeoutMs: 2000 }),\r\n new Promise((resolve) => {\r\n setTimeout(resolve, 2000);\r\n }),\r\n // eslint-disable-next-line unicorn/no-process-exit -- deliberate dev hard-exit once the bounded hook window closes (mirrors the original inline handler)\r\n ]).finally(() => process.exit(0));\r\n };\r\n process.once('SIGINT', () => {\r\n devSignalExit('SIGINT');\r\n });\r\n process.once('SIGTERM', () => {\r\n devSignalExit('SIGTERM');\r\n });\r\n //? @luckystack/devkit is an OPTIONAL peer, normally ABSENT in production. If a\r\n //? deploy forgets `NODE_ENV=production`, enableDevTools stays true and an\r\n //? unguarded `import('@luckystack/devkit')` would crash boot with\r\n //? ERR_MODULE_NOT_FOUND. Resolve-guard + tryCatch (mirroring bootstrap.ts's\r\n //? optional-package imports) so a missing/broken devkit logs an actionable\r\n //? warning and boot continues instead of taking the whole server down.\r\n const devkitModuleId = '@luckystack/devkit';\r\n if (!canResolve(devkitModuleId)) {\r\n getLogger().warn(\r\n 'dev tooling unavailable — @luckystack/devkit is not installed, so hot reload + type-map generation are off. Install it as a devDependency for dev, or set NODE_ENV=production to run in production mode.',\r\n );\r\n return;\r\n }\r\n const [devkitError] = await tryCatch(async () => {\r\n const devkit = (await import(devkitModuleId)) as {\r\n initializeAll: () => Promise<void>;\r\n setupWatchers: () => void;\r\n };\r\n await devkit.initializeAll();\r\n devkit.setupWatchers();\r\n });\r\n if (devkitError) {\r\n getLogger().warn('dev tooling failed to initialize — continuing without hot reload.', { error: devkitError.message });\r\n }\r\n};\r\n\r\n//? Extracted verbatim from the previous inline `listen` closure. Same\r\n//? auto-increment opt-in, same EADDRINUSE retry, same truthful-failure\r\n//? logging, same success log + callback + resolve. `httpServer`, `ip`, and\r\n//? `port` are threaded in as parameters instead of closed over.\r\nexport const listenLuckyStackServer = (\r\n httpServer: HttpServer,\r\n ip: string,\r\n port: string | number,\r\n callback?: () => void,\r\n): Promise<HttpServer> =>\r\n new Promise<HttpServer>((resolve, reject) => {\r\n const startPort = typeof port === 'string' ? Number.parseInt(port, 10) : port;\r\n //? Auto-pick the next free port resolution:\r\n //? - When `SERVER_PORT_AUTO_INCREMENT` is set EXPLICITLY it always wins\r\n //? (`1`/`true` -> on, `0`/`false` -> off), so a consumer can force either\r\n //? behaviour in any environment.\r\n //? - When it is NOT set we default to ON in dev and OFF in production. The\r\n //? dev-vs-prod signal is `isProduction` from `@luckystack/core` (NODE_ENV)\r\n //? — the same canonical flag this file already uses to gate dev tooling.\r\n //? Prod stays OFF by default because `SERVER_PORT` also drives `config.ts`'s\r\n //? `backendOrigin` / OAuth callback base; silently moving the listen port\r\n //? there would leave clients talking to the old one. In dev a port clash is\r\n //? almost always a leftover `npm run server`, so quietly hopping to the next\r\n //? free port is the friendlier default.\r\n const autoIncrementEnv = (process.env.SERVER_PORT_AUTO_INCREMENT ?? '').toLowerCase();\r\n let autoIncrement: boolean;\r\n if (['0', 'false'].includes(autoIncrementEnv)) autoIncrement = false;\r\n else if (['1', 'true'].includes(autoIncrementEnv)) autoIncrement = true;\r\n else autoIncrement = !isProduction;\r\n\r\n const tryListen = (attemptPort: number): void => {\r\n const onError = (err: NodeJS.ErrnoException): void => {\r\n if (err.code !== 'EADDRINUSE') {\r\n reject(err);\r\n return;\r\n }\r\n if (autoIncrement) {\r\n getLogger().warn(\r\n `Port ${String(attemptPort)} is in use — trying ${String(attemptPort + 1)} (auto-increment; set SERVER_PORT_AUTO_INCREMENT=0 to disable)`,\r\n );\r\n tryListen(attemptPort + 1);\r\n return;\r\n }\r\n //? Truthful failure. The old code unconditionally logged \"running on\r\n //? :<port>\" inside the listen callback even when the bind never\r\n //? succeeded, so an in-use port looked like a healthy boot. Surface\r\n //? the real problem and the two ways out instead.\r\n getLogger().error(\r\n `Port ${String(attemptPort)} is already in use — the server did NOT start. ` +\r\n `Another \\`npm run server\\` is probably still running (stop it), or set ` +\r\n `SERVER_PORT to a free port, or set SERVER_PORT_AUTO_INCREMENT=1 to auto-pick the next free port.`,\r\n );\r\n reject(err);\r\n };\r\n\r\n httpServer.once('error', onError);\r\n httpServer.listen(attemptPort, ip, () => {\r\n httpServer.off('error', onError);\r\n //? Dev only: advertise the ACTUALLY-bound port so the Vite proxy follows\r\n //? us when auto-increment moved the listen off `SERVER_PORT`. Skipped in\r\n //? production (no proxy) and under the test runner (avoid stray files +\r\n //? exit handlers in unit tests). Best-effort — never blocks the boot.\r\n if (!isProduction && process.env.NODE_ENV !== 'test') {\r\n writeDevServerInfo(ip, attemptPort);\r\n process.once('exit', clearDevServerInfo);\r\n }\r\n const config = getProjectConfig();\r\n if (config.logging.socketStartup || config.logging.devLogs) {\r\n getLogger().info(`Server is running on http://${ip}:${String(attemptPort)}/`);\r\n }\r\n callback?.();\r\n resolve(httpServer);\r\n });\r\n };\r\n\r\n tryListen(startPort);\r\n });\r\n\r\nexport const createLuckyStackServer = async (\r\n options: CreateLuckyStackServerOptions = {}\r\n): Promise<RunningLuckyStackServer> => {\r\n //? Auto-register the framework-shipped runtime maps provider when the\r\n //? consumer supplied a `loadGeneratedMaps` callback. Runs before\r\n //? `verifyBootstrap` so the registration counts toward the boot check.\r\n //? Consumers who hand-rolled their own provider via\r\n //? `registerRuntimeMapsProvider` can simply omit `loadGeneratedMaps` and\r\n //? the framework leaves their registration alone.\r\n if (options.loadGeneratedMaps) {\r\n registerProdRuntimeMapsProvider({\r\n loadGenerated: options.loadGeneratedMaps,\r\n preset: options.runtimeMapsPreset,\r\n });\r\n }\r\n\r\n //? Fail fast if a project's overlay forgot to register a critical piece.\r\n //? Surface a single readable error instead of a stack trace deep inside\r\n //? a request handler.\r\n await verifyBootstrap({\r\n requireDeployConfig: options.requireDeployConfig,\r\n requireServicesConfig: options.requireServicesConfig,\r\n requireOAuthProviders: options.requireOAuthProviders,\r\n });\r\n\r\n const port = options.port ?? getParsedPort() ?? options.defaultPort ?? process.env.SERVER_PORT ?? 80;\r\n const ip = options.ip ?? process.env.SERVER_IP ?? '127.0.0.1';\r\n const enableDevTools = options.enableDevTools ?? resolveEnvKey() !== 'production';\r\n\r\n //? Register the resolved bind address so framework code that needs it\r\n //? (e.g. `checkOrigin` building the same-origin entry) doesn't drift when\r\n //? the consumer passed `options.ip`/`options.port` without also setting\r\n //? the legacy `SERVER_IP`/`SERVER_PORT` env vars.\r\n registerBindAddress({\r\n ip,\r\n port: typeof port === 'string' ? Number.parseInt(port, 10) : port,\r\n });\r\n\r\n if (enableDevTools) {\r\n await initDevTools();\r\n }\r\n\r\n //? FIX-1 (secret-manager Redis pointer): if a secret resolver is configured,\r\n //? drop any DEFAULT Redis client that an early import may have built while\r\n //? `REDIS_PASSWORD`/`REDIS_USER` was still an UNRESOLVED secret-manager pointer.\r\n //? By this point the consumer's `initSecretManager(...)` (called before\r\n //? `bootstrapLuckyStack`) has overwritten `process.env` with the real values,\r\n //? so the first real Redis use below (the boot-UUID write) rebuilds from the\r\n //? resolved env. Belt-and-suspenders alongside core's `secretsResolved` hook —\r\n //? this also covers projects that resolved secrets by other means. No-op when\r\n //? nothing was cached or no resolver is configured.\r\n if (getProjectConfig().secretManager?.url) {\r\n resetDefaultRedisClient();\r\n }\r\n\r\n //? Boot UUID must be written before /_health can answer truthfully. Router\r\n //? boot handshake consumes /_health to verify shared-Redis topology. A\r\n //? failure here is almost always Redis being unreachable or misconfigured\r\n //? (bad credentials, wrong host); throw a clear, actionable error so boot\r\n //? halts on THIS message instead of the raw ioredis `ReplyError` dump that\r\n //? confused operators before. Library code must not `process.exit()`; the\r\n //? throw propagates to the boot entry and the dev supervisor respawns.\r\n //? `tryCatch` already captured the underlying error to the error tracker.\r\n const [bootUuidError] = await tryCatch(() => writeBootUuid());\r\n if (bootUuidError) {\r\n throw new Error(\r\n 'Failed to write the boot UUID to Redis. Check REDIS_HOST / REDIS_PORT / REDIS_USER / REDIS_PASSWORD and that Redis is reachable.',\r\n { cause: bootUuidError },\r\n );\r\n }\r\n\r\n const httpServer: HttpServer = http.createServer((req, res) => {\r\n void handleHttpRequest(req, res, options);\r\n });\r\n\r\n //? Persistent error listener. `listenLuckyStackServer` attaches a ONE-OFF\r\n //? `error` handler only around the bind attempt; once listening, an async\r\n //? socket error (e.g. EMFILE under load, an abrupt peer reset surfacing at the\r\n //? server level) would otherwise be an unhandled `'error'` event and crash the\r\n //? process. Log it instead so the server stays up and the cause is visible.\r\n httpServer.on('error', (err: NodeJS.ErrnoException) => {\r\n //? EADDRINUSE during bind is owned by the one-off handler in\r\n //? `listenLuckyStackServer` (which rejects/retries); don't double-log it.\r\n if (err.code === 'EADDRINUSE') return;\r\n getLogger().error('[http-server] runtime error', err);\r\n });\r\n\r\n const { io: ioServer, adapterClients } = loadSocket(httpServer, {\r\n maxHttpBufferSize: options.maxHttpBufferSize,\r\n });\r\n\r\n const listen = (callback?: () => void): Promise<HttpServer> =>\r\n listenLuckyStackServer(httpServer, ip, port, callback);\r\n\r\n //? Idempotent graceful shutdown. A second call (e.g. SIGINT then SIGTERM, or a\r\n //? programmatic `stop()` racing a signal) returns the in-flight promise rather\r\n //? than running the teardown twice.\r\n let shutdownPromise: Promise<void> | null = null;\r\n const stop = (stopOptions: StopLuckyStackServerOptions = {}): Promise<void> => {\r\n shutdownPromise ??= runGracefulShutdown({ httpServer, ioServer, adapterClients }, stopOptions);\r\n return shutdownPromise;\r\n };\r\n\r\n //? Production signal wiring (MIS-016). In dev, `initDevTools` already installs\r\n //? fast `process.exit(0)` handlers (hot-reload supervisor restarts). In prod\r\n //? we run the FULL graceful shutdown and exit only after it settles — an\r\n //? orchestrator's SIGTERM should drain connections + flush trackers, not hard-\r\n //? kill. `process.once` so a repeated signal doesn't stack handlers; the\r\n //? `stop()` idempotency covers a SIGINT-then-SIGTERM sequence.\r\n if (!enableDevTools) {\r\n const handleSignal = (reason: 'SIGTERM' | 'SIGINT'): void => {\r\n void (async () => {\r\n await stop({ reason });\r\n //? Terminate AFTER the graceful drain completes. This is the process\r\n //? entry's signal handler (not deep library code) — an orchestrator's\r\n //? SIGTERM expects the process to exit once it has drained, so exiting\r\n //? here is correct. Mirrors the dev handlers in `initDevTools`.\r\n // eslint-disable-next-line unicorn/no-process-exit -- top-level signal handler, exits after graceful drain\r\n process.exit(0);\r\n })();\r\n };\r\n process.once('SIGTERM', () => { handleSignal('SIGTERM'); });\r\n process.once('SIGINT', () => { handleSignal('SIGINT'); });\r\n }\r\n\r\n return { httpServer, ioServer, listen, stop, close: stop };\r\n};\r\n","import type { IncomingMessage, ServerResponse } from 'node:http';\r\nimport { randomUUID } from 'node:crypto';\r\nimport {\r\n allowedOrigin,\r\n dispatchHook,\r\n extractTokenFromRequest,\r\n getLogger,\r\n getParams,\r\n getProjectConfig,\r\n hasCookie,\r\n normalizeOrigin,\r\n readSession,\r\n tryCatch,\r\n tryCatchSync,\r\n} from '@luckystack/core';\r\nimport { sanitizeForLog } from './logSanitize';\r\nimport { getSecurityHeadersBuilder } from './securityHeadersRegistry';\r\nimport type { CreateLuckyStackServerOptions } from './types';\r\nimport { enforceCsrfOnStateChangingRequest } from './httpRoutes/csrfMiddleware';\r\nimport { handleCsrfRoute } from './httpRoutes/csrfRoute';\r\nimport { handleFaviconRoute } from './httpRoutes/faviconRoute';\r\nimport { handleHealthRoute, handleLivezRoute, handleReadyzRoute } from './httpRoutes/healthRoutes';\r\nimport { handleTestResetRoute } from './httpRoutes/testResetRoute';\r\nimport { handleUploadsRoute } from './httpRoutes/uploadsRoute';\r\nimport { handleAuthApiRoute } from './httpRoutes/authApiRoute';\r\nimport { handleAuthEmailCodeRoute, handleAuthTwoFactorRoute } from './httpRoutes/authSecondFactorRoutes';\r\nimport { handleAuthLogoutRoute } from './httpRoutes/authLogoutRoute';\r\nimport { handleAuthProvidersRoute } from './httpRoutes/authProvidersRoute';\r\nimport { handleAuthCallbackRoute } from './httpRoutes/authCallbackRoute';\r\nimport { handleApiRoute } from './httpRoutes/apiRoute';\r\nimport { handleSyncRoute } from './httpRoutes/syncRoute';\r\nimport { handleCustomRoutes, handlePreParamsCustomRoutes } from './httpRoutes/customRoutes';\r\nimport { handleStaticAndSpaFallback } from './httpRoutes/staticRoutes';\r\nimport { isOriginExemptPath } from './originExemptRegistry';\r\nimport { resolveCookieSecure } from './httpRoutes/sessionCookie';\r\nimport type { HttpRouteContext, HttpRouteHandler } from './httpRoutes/types';\r\n\r\nconst buildSessionCookieOptions = (\r\n sessionExpiryDays: number,\r\n secure: boolean,\r\n http: ReturnType<typeof getProjectConfig>['http'],\r\n): string =>\r\n `HttpOnly; SameSite=${http.sessionCookieSameSite}; Path=${http.sessionCookiePath}; Max-Age=${60 * 60 * 24 * sessionExpiryDays}; ${secure ? 'Secure;' : ''}`;\r\n\r\nconst setSecurityHeaders = (req: IncomingMessage, res: ServerResponse, origin: string) => {\r\n const { cors, securityHeaders } = getProjectConfig().http;\r\n res.setHeader('Access-Control-Allow-Origin', origin);\r\n res.setHeader('Access-Control-Allow-Methods', cors.allowedMethods);\r\n res.setHeader('Access-Control-Allow-Headers', cors.allowedHeaders);\r\n res.setHeader('Access-Control-Expose-Headers', cors.exposedHeaders);\r\n if (cors.credentials) {\r\n res.setHeader('Access-Control-Allow-Credentials', 'true');\r\n }\r\n res.setHeader('Referrer-Policy', securityHeaders.referrerPolicy);\r\n res.setHeader('X-Frame-Options', securityHeaders.frameOptions);\r\n res.setHeader('X-XSS-Protection', securityHeaders.xssProtection);\r\n res.setHeader('X-Content-Type-Options', securityHeaders.contentTypeOptions);\r\n\r\n //? Consumer-registered builder runs AFTER framework defaults so it can\r\n //? override (CSP, HSTS, Permissions-Policy) or extend. Errors fall\r\n //? through to defaults so a buggy builder can't kill response delivery.\r\n const builder = getSecurityHeadersBuilder();\r\n if (builder) {\r\n //? Wrap both the builder call AND the header writes so a buggy builder (or\r\n //? an invalid header name/value it returns) can't kill response delivery —\r\n //? same guarded scope as the original raw try/catch, just via tryCatchSync.\r\n const [error] = tryCatchSync(() => {\r\n const custom = builder(req);\r\n if (custom) {\r\n for (const [name, value] of Object.entries(custom)) {\r\n res.setHeader(name, value);\r\n }\r\n }\r\n });\r\n if (error) {\r\n getLogger().warn('securityHeadersBuilder threw — falling back to defaults', { err: error });\r\n }\r\n }\r\n};\r\n\r\n//? Routes that run BEFORE params parsing — they don't need (and shouldn't\r\n//? consume) the request body. The framework fast-paths run first; consumer\r\n//? `'pre-params'` custom routes (webhooks / streaming uploads) run last, after\r\n//? the probes but before `getParams` drains the body.\r\nconst PRE_PARAMS_ROUTES: HttpRouteHandler[] = [\r\n handleCsrfRoute,\r\n handleFaviconRoute,\r\n handleLivezRoute,\r\n handleReadyzRoute,\r\n handleHealthRoute,\r\n handleTestResetRoute,\r\n handleAuthProvidersRoute,\r\n handleAuthLogoutRoute,\r\n handlePreParamsCustomRoutes,\r\n];\r\n\r\n//? Routes that run AFTER params parsing.\r\n//? ORDER: the email-code + 2FA routes MUST run before handleAuthApiRoute —\r\n//? that handler catch-alls every `/auth/api/*` path into `providerNotFound`.\r\nconst POST_PARAMS_ROUTES: HttpRouteHandler[] = [\r\n handleUploadsRoute,\r\n handleAuthEmailCodeRoute,\r\n handleAuthTwoFactorRoute,\r\n handleAuthApiRoute,\r\n handleAuthCallbackRoute,\r\n handleApiRoute,\r\n handleSyncRoute,\r\n handleCustomRoutes,\r\n handleStaticAndSpaFallback,\r\n];\r\n\r\nconst dispatchRoutes = async (handlers: HttpRouteHandler[], ctx: HttpRouteContext): Promise<boolean> => {\r\n for (const handler of handlers) {\r\n const handled = await handler(ctx);\r\n if (handled || ctx.res.writableEnded) return true;\r\n }\r\n return false;\r\n};\r\n\r\nconst enforceOriginPolicy = (\r\n req: IncomingMessage,\r\n res: ServerResponse,\r\n routePath: string,\r\n): { origin: string; rejected: boolean } => {\r\n //? Do NOT fall back to `req.headers.host` — host always equals the bound\r\n //? origin so non-browser callers (curl, native apps) would silently bypass\r\n //? `allowedOrigin()`. Only browsers attach Origin/Referer.\r\n //? Normalize at the source: a Referer fallback is a FULL URL (with path/query),\r\n //? so reduce it to scheme+host before it's both allowlist-checked AND reflected\r\n //? into `Access-Control-Allow-Origin`. A raw referer would otherwise produce an\r\n //? invalid ACAO (containing a path) that the browser rejects — silently breaking\r\n //? credentialed cross-origin clients that send Referer but no Origin header.\r\n const origin = normalizeOrigin({\r\n value: req.headers.origin ?? req.headers.referer ?? '',\r\n secure: process.env.SECURE === 'true',\r\n });\r\n const isStateChangingMethod = req.method !== 'GET' && req.method !== 'HEAD' && req.method !== 'OPTIONS';\r\n\r\n //? Registered webhook / server-to-server prefixes opt out of the browser\r\n //? origin gate — they authenticate via signature/HMAC in the handler, not via\r\n //? Origin (which they never send). Empty by default; opt-in only. The handler\r\n //? is still responsible for verifying the caller. See originExemptRegistry.\r\n if (isOriginExemptPath(routePath)) {\r\n //? L1 (CORS hardening): exempt paths skip the origin GATE — server-to-server\r\n //? callers send no Origin and authenticate via signature/HMAC in the handler.\r\n //? But the raw, unvalidated Origin must NOT be reflected into\r\n //? `Access-Control-Allow-Origin` alongside `Access-Control-Allow-Credentials`\r\n //? — that is a CORS misconfig the moment an exempt prefix serves any\r\n //? browser-readable, cookie-authenticated data. Return an empty origin so no\r\n //? cross-origin ACAO is emitted for exempt routes (they don't need CORS).\r\n return { origin: '', rejected: false };\r\n }\r\n\r\n if (!origin) {\r\n //? No browser-attributable origin: fail-close on state-changing methods,\r\n //? allow read-only (GET/HEAD/OPTIONS) so health probes and asset fetches\r\n //? from non-browser tooling continue to work.\r\n if (isStateChangingMethod) {\r\n res.statusCode = 403;\r\n res.setHeader('Content-Type', 'text/plain');\r\n res.end('Forbidden');\r\n return { origin, rejected: true };\r\n }\r\n } else if (!allowedOrigin(origin)) {\r\n res.statusCode = 403;\r\n res.setHeader('Content-Type', 'text/plain');\r\n res.end('Forbidden');\r\n return { origin, rejected: true };\r\n }\r\n return { origin, rejected: false };\r\n};\r\n\r\nconst refreshSessionCookieIfPresent = async ({\r\n req,\r\n res,\r\n token,\r\n sessionCookieName,\r\n sessionCookieOptions,\r\n}: {\r\n req: IncomingMessage;\r\n res: ServerResponse;\r\n token: string | null;\r\n sessionCookieName: string;\r\n sessionCookieOptions: string;\r\n}) => {\r\n const hasTokenCookie = hasCookie(req.headers.cookie, sessionCookieName);\r\n if (!hasTokenCookie || !token) return;\r\n const currentSession = await readSession(token);\r\n if (currentSession?.id) {\r\n //? Sliding expiration in cookie mode: keep browser token lifetime\r\n //? aligned with Redis TTL.\r\n res.setHeader('Set-Cookie', `${sessionCookieName}=${token}; ${sessionCookieOptions}`);\r\n }\r\n};\r\n\r\nconst parseRequestParams = async ({\r\n req,\r\n res,\r\n method,\r\n routePath,\r\n queryString,\r\n requestId,\r\n shouldLogDev,\r\n}: {\r\n req: IncomingMessage;\r\n res: ServerResponse;\r\n method: 'GET' | 'POST' | 'PUT' | 'DELETE';\r\n routePath: string;\r\n queryString: string | undefined;\r\n requestId: string;\r\n shouldLogDev: boolean;\r\n}): Promise<object | null> => {\r\n let params: object | null = await getParams({ method, req, res, queryString });\r\n if (res.writableEnded) return null;\r\n\r\n if (params && typeof params === 'object' && Object.keys(params).length > 0) {\r\n if (shouldLogDev) {\r\n getLogger().debug(`[${requestId}] ${method} ${routePath}`, {\r\n params: sanitizeForLog(params) as Record<string, unknown>,\r\n });\r\n }\r\n } else {\r\n if (shouldLogDev) {\r\n getLogger().debug(`[${requestId}] ${method} ${routePath}`);\r\n }\r\n params = {};\r\n }\r\n return params;\r\n};\r\n\r\nconst handleHttpRequestInner = async (\r\n req: IncomingMessage,\r\n res: ServerResponse,\r\n options: CreateLuckyStackServerOptions\r\n): Promise<void> => {\r\n const config = getProjectConfig();\r\n const shouldLogDev = config.logging.devLogs;\r\n const sessionCookieName = config.http.sessionCookieName;\r\n const sessionCookieOptions = buildSessionCookieOptions(\r\n config.session.expiryDays,\r\n //? Honor the explicit `http.sessionCookieSecure` override (CORE-39), else the\r\n //? `SECURE` env flag — shared with the OAuth state cookie via\r\n //? `resolveCookieSecure` so the two can never drift (WAVE4).\r\n resolveCookieSecure(config.http.sessionCookieSecure, process.env.SECURE),\r\n config.http,\r\n );\r\n\r\n //? Parse the path up-front so the origin gate can consult the exempt-path\r\n //? registry (registered webhooks) before it would otherwise 403 a\r\n //? header-less, state-changing request.\r\n //? SEC: decode percent-encoded characters so that e.g. `/auth%2Flogout` cannot\r\n //? bypass route guards that compare against plain `/auth/logout`. Malformed\r\n //? encoding returns 400 so the request does not silently fall through.\r\n const url = req.url ?? '/';\r\n const [routePathRaw, queryStringRaw] = url.split('?');\r\n const [decodeError, decodedPath] = tryCatchSync(() => decodeURIComponent(routePathRaw ?? '/'));\r\n if (decodeError || decodedPath === null) {\r\n res.statusCode = 400;\r\n res.setHeader('Content-Type', 'text/plain');\r\n res.end('Bad Request');\r\n return;\r\n }\r\n const routePath = decodedPath;\r\n const queryString = queryStringRaw ?? '';\r\n\r\n const { origin, rejected } = enforceOriginPolicy(req, res, routePath);\r\n if (rejected) return;\r\n\r\n setSecurityHeaders(req, res, origin);\r\n\r\n //? Honor an incoming x-request-id (idempotent for proxies/retries) or\r\n //? generate a fresh UUID. Echo back as a response header so client-side\r\n //? logs and Sentry can correlate.\r\n //? SEC: validate before reflecting — only alphanumeric + hyphens, max 128 chars,\r\n //? to prevent header-injection via a crafted x-request-id value.\r\n const incomingRequestId = req.headers['x-request-id'];\r\n const rawRequestId = Array.isArray(incomingRequestId) ? incomingRequestId[0] : incomingRequestId;\r\n const requestId = (rawRequestId && /^[a-zA-Z0-9-]{1,128}$/.test(rawRequestId)) ? rawRequestId : randomUUID();\r\n res.setHeader('X-Request-Id', requestId);\r\n\r\n //? `preHttpRequest` fires before any route dispatch. Use to instrument\r\n //? requests (latency timer, audit log), enforce IP allow-lists, or stop\r\n //? specific paths with a custom error. Header subset excludes auth/cookie.\r\n const safeHeaders: Record<string, string> = {};\r\n for (const [k, v] of Object.entries(req.headers)) {\r\n if (k === 'authorization' || k === 'cookie' || k === 'set-cookie' || k === 'x-csrf-token' || k === 'x-test-reset-token' || k === 'x-session-based-token') continue;\r\n safeHeaders[k] = Array.isArray(v) ? v.join(', ') : (v ?? '');\r\n }\r\n const preHttpResult = await dispatchHook('preHttpRequest', {\r\n method: req.method?.toUpperCase() ?? 'GET',\r\n url: req.url ?? '/',\r\n requestId,\r\n origin,\r\n headers: safeHeaders,\r\n });\r\n if (preHttpResult.stopped) {\r\n res.statusCode = preHttpResult.signal.httpStatus ?? 403;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ status: 'error', errorCode: preHttpResult.signal.errorCode }));\r\n return;\r\n }\r\n\r\n if (req.method === 'OPTIONS') {\r\n res.writeHead(204);\r\n res.end();\r\n return;\r\n }\r\n\r\n const method = req.method;\r\n\r\n if (method !== 'GET' && method !== 'POST' && method !== 'PUT' && method !== 'DELETE') {\r\n res.statusCode = 404;\r\n res.setHeader('Content-Type', 'text/plain');\r\n res.end(`method: ${String(method)} not supported, use one of: GET, POST, PUT, DELETE`);\r\n return;\r\n }\r\n\r\n const token = extractTokenFromRequest(req);\r\n await refreshSessionCookieIfPresent({ req, res, token, sessionCookieName, sessionCookieOptions });\r\n\r\n const csrfRejected = await enforceCsrfOnStateChangingRequest({ req, res, routePath, token, requestId });\r\n if (csrfRejected) return;\r\n\r\n const baseCtx: Omit<HttpRouteContext, 'params'> = {\r\n req,\r\n res,\r\n options,\r\n routePath,\r\n queryString,\r\n method,\r\n token,\r\n requestId,\r\n sessionCookieOptions,\r\n };\r\n\r\n //? Route /auth/csrf, health probes, _test/reset, favicon — all fast paths\r\n //? that should not consume the request body.\r\n if (await dispatchRoutes(PRE_PARAMS_ROUTES, { ...baseCtx, params: {} })) return;\r\n\r\n const params = await parseRequestParams({\r\n req, res, method, routePath, queryString, requestId, shouldLogDev,\r\n });\r\n if (params === null) return;\r\n\r\n await dispatchRoutes(POST_PARAMS_ROUTES, { ...baseCtx, params });\r\n};\r\n\r\nexport const handleHttpRequest = async (\r\n req: IncomingMessage,\r\n res: ServerResponse,\r\n options: CreateLuckyStackServerOptions\r\n): Promise<void> => {\r\n //? Top-level error boundary: catches any unhandled throw that escapes route\r\n //? handlers and prevents it from propagating into the Node.js\r\n //? unhandled-rejection handler (which would crash the process in Node ≥15).\r\n //? Returns 500 so the client gets a defined response instead of a hang.\r\n const [error] = await tryCatch(() => handleHttpRequestInner(req, res, options));\r\n if (error && !res.writableEnded) {\r\n getLogger().error('handleHttpRequest: unhandled error', { err: error });\r\n res.statusCode = 500;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ status: 'error', errorCode: 'server.internalError' }));\r\n }\r\n};\r\n","//? Recursive log sanitizer: never log known-sensitive keys. Reads the\r\n//? extensible redacted-key registry from core so feature packages can register\r\n//? their own domain-specific keys (password, apiKey, mrn, etc.) at boot.\r\n\r\nimport { getProjectConfig, isRedactedLogKey } from '@luckystack/core';\r\n\r\nconst REDACTED_PLACEHOLDER = '[REDACTED]';\r\nconst TRUNCATED_PLACEHOLDER = '[TRUNCATED]';\r\n\r\n//? Bound recursion so an attacker-supplied deeply-nested JSON body (within the\r\n//? request body cap) cannot stack-overflow the dev-log sanitizer. Past this\r\n//? depth we emit a sentinel instead of recursing further.\r\nconst MAX_SANITIZE_DEPTH = 40;\r\n\r\nconst isRedactedKey = (key: string): boolean => {\r\n if (isRedactedLogKey(key)) return true;\r\n return key.toLowerCase() === getProjectConfig().http.sessionCookieName.toLowerCase();\r\n};\r\n\r\nconst sanitizeWithGuards = (value: unknown, depth: number, seen: WeakSet<object>): unknown => {\r\n if (value === null || typeof value !== 'object') return value;\r\n //? Cycle guard: a self-referential object would otherwise recurse forever.\r\n if (seen.has(value)) return TRUNCATED_PLACEHOLDER;\r\n if (depth >= MAX_SANITIZE_DEPTH) return TRUNCATED_PLACEHOLDER;\r\n\r\n seen.add(value);\r\n try {\r\n if (Array.isArray(value)) {\r\n return value.map((entry) => sanitizeWithGuards(entry, depth + 1, seen));\r\n }\r\n const out: Record<string, unknown> = {};\r\n for (const [key, val] of Object.entries(value as Record<string, unknown>)) {\r\n out[key] = isRedactedKey(key) ? REDACTED_PLACEHOLDER : sanitizeWithGuards(val, depth + 1, seen);\r\n }\r\n return out;\r\n } finally {\r\n //? Allow the same object to appear in sibling branches (shared, not cyclic).\r\n seen.delete(value);\r\n }\r\n};\r\n\r\nexport const sanitizeForLog = (value: unknown): unknown => sanitizeWithGuards(value, 0, new WeakSet());\r\n","//? Security-headers builder registry. The framework ships sensible defaults\r\n//? (Referrer-Policy, X-Frame-Options, X-XSS-Protection, X-Content-Type-Options)\r\n//? read from `projectConfig.http.securityHeaders`. Consumers can register\r\n//? a custom builder to add Content-Security-Policy, Strict-Transport-Security,\r\n//? Permissions-Policy, or to override defaults per request.\r\n//?\r\n//? Resolution order:\r\n//? 1. Built-in defaults from `projectConfig.http.securityHeaders`.\r\n//? 2. Headers from the registered builder (override OR augment).\r\n//?\r\n//? A nullish return from the builder means \"use defaults only\". An object\r\n//? merges on top of defaults (later wins per key).\r\n\r\nimport type { IncomingMessage } from 'node:http';\r\nimport { createRegistry } from '@luckystack/core';\r\n\r\nexport type SecurityHeadersBuilder = (req: IncomingMessage) => Record<string, string> | null | undefined;\r\n\r\n//? Single-slot registry: one builder at a time, last-write-wins, `null` is the\r\n//? unregistered baseline. Backed by core's `createRegistry` so the register /\r\n//? read / reset triad isn't hand-rolled. The public `registerSecurityHeaders` /\r\n//? `getSecurityHeadersBuilder` signatures below are preserved verbatim.\r\nconst builderRegistry = createRegistry<SecurityHeadersBuilder | null>(null);\r\n\r\n/**\r\n * Register a custom security-headers builder. Called for every HTTP\r\n * request; return a plain object that gets merged on top of the framework\r\n * defaults. Use for Content-Security-Policy, HSTS, Permissions-Policy.\r\n *\r\n * Last-write-wins: subsequent calls replace the previous builder. Pass\r\n * `null` to unregister.\r\n */\r\nexport const registerSecurityHeaders = (builder: SecurityHeadersBuilder | null): void => {\r\n builderRegistry.register(builder);\r\n};\r\n\r\n/** Read the active builder (or null). */\r\nexport const getSecurityHeadersBuilder = (): SecurityHeadersBuilder | null => builderRegistry.get();\r\n","import type { IncomingMessage, ServerResponse } from 'node:http';\r\nimport { dispatchHook, getCookieValue, getCsrfConfig, getProjectConfig, readSession } from '@luckystack/core';\r\nimport { capabilities } from '../capabilities';\r\nimport { isOriginExemptPath } from '../originExemptRegistry';\r\nimport { timingSafeStringEqual } from './timingSafeEqual';\r\n\r\n//? Returns true when the request was rejected (CSRF mismatch) and the response\r\n//? has been ended. Caller should bail out of the request loop.\r\nexport const enforceCsrfOnStateChangingRequest = async ({\r\n req,\r\n res,\r\n routePath,\r\n token,\r\n requestId,\r\n}: {\r\n req: IncomingMessage;\r\n res: ServerResponse;\r\n routePath: string;\r\n token: string | null;\r\n requestId?: string;\r\n}): Promise<boolean> => {\r\n const config = getProjectConfig();\r\n const isCookieMode = !config.session.basedToken;\r\n //? HEAD is read-only and excluded from `enforceOriginPolicy`; mirror that\r\n //? here so the two state-changing predicates agree (HEAD is 404'd at the\r\n //? method gate before this runs, so this is parity, not a behavior change).\r\n const isStateChanging = req.method !== 'GET' && req.method !== 'HEAD' && req.method !== 'OPTIONS';\r\n const isCallbackPath = routePath.startsWith('/auth/callback');\r\n //? The credentials login/register endpoint is the session BOOTSTRAP. Requiring a\r\n //? pre-existing session's CSRF token to authenticate is circular, and it blocks\r\n //? legitimate same-site re-login / register while a (stale) session cookie is\r\n //? present. Cross-site abuse is already prevented by the SameSite=Strict session\r\n //? cookie — a cross-site POST never carries it, so `token` would be absent here\r\n //? and this guard wouldn't fire anyway. Exempting it removes no real protection.\r\n //? ADR 0024: the email-code + 2FA LOGIN routes share the credentials-route\r\n //? bootstrap semantics — they authenticate via their own factor (code /\r\n //? challenge token), may run while a stale session cookie is present, and a\r\n //? cross-site POST never carries the SameSite=Strict cookie anyway. The\r\n //? authenticated 2FA ENROLLMENT routes (/auth/api/2fa/setup|enable|disable|\r\n //? recovery-codes) are deliberately NOT exempt — they are state-changing\r\n //? actions on a live session.\r\n const AUTH_BOOTSTRAP_PATHS = new Set([\r\n '/auth/api/credentials',\r\n '/auth/api/email-code/request',\r\n '/auth/api/email-code/verify',\r\n '/auth/api/2fa',\r\n '/auth/api/2fa/email-code',\r\n ]);\r\n const isAuthBootstrap = AUTH_BOOTSTRAP_PATHS.has(routePath);\r\n //? CSRF covers all framework routes (/api/, /sync/, /auth/api/) AND\r\n //? state-changing custom routes registered via `registerCustomRoute` that\r\n //? are not marked origin-exempt (those authenticate via HMAC/signature, not\r\n //? the session cookie, so the double-submit check is irrelevant there).\r\n //?\r\n //? IMPORTANT: `registerOriginExemptPath({ pathPrefix })` exempts ALL routes\r\n //? whose path starts with the given prefix from BOTH the origin gate AND CSRF.\r\n //? Register only the narrowest prefix needed (prefer ending with `/` — e.g.\r\n //? `/webhooks/` — to avoid accidentally exempting `/webhooksAdmin`).\r\n const isExemptFromCsrf =\r\n isAuthBootstrap\r\n || isCallbackPath\r\n || isOriginExemptPath(routePath);\r\n const isCsrfCandidate =\r\n routePath.startsWith('/api/')\r\n || routePath.startsWith('/sync/')\r\n || routePath.startsWith('/auth/api/')\r\n || (!routePath.startsWith('/auth/') && !routePath.startsWith('/assets/'));\r\n\r\n //? CSRF only applies to cookie-mode, state-changing routes not already\r\n //? exempted by another auth mechanism (origin-exempt webhooks, bootstrap).\r\n if (!(isCookieMode && isStateChanging && isCsrfCandidate && !isExemptFromCsrf)) {\r\n return false;\r\n }\r\n\r\n //? Read the active CSRF header name from the registry so consumers\r\n //? can rename it (e.g. legacy `x-xsrf-token`, custom `x-app-csrf`).\r\n const csrfConfig = getCsrfConfig();\r\n const headerKey = csrfConfig.headerName.toLowerCase();\r\n const headerValue = req.headers[headerKey];\r\n const provided = Array.isArray(headerValue) ? headerValue[0] : headerValue;\r\n\r\n //? Login-ABSENT path: stateless DOUBLE-SUBMIT. The session-bound token store\r\n //? lives in @luckystack/login; without it we compare the csrf COOKIE value\r\n //? against the x-csrf-token HEADER (both seeded by GET /auth/csrf). No session\r\n //? read. A cross-site POST can't read the cookie value to forge the header.\r\n if (!capabilities.login) {\r\n const cookieValue = getCookieValue(req.headers.cookie, csrfConfig.cookieName);\r\n if (cookieValue && provided && timingSafeStringEqual(provided, cookieValue)) return false;\r\n\r\n void dispatchHook('csrfMismatch', {\r\n route: routePath,\r\n method: req.method,\r\n requestId,\r\n userId: undefined,\r\n providedToken: Boolean(provided),\r\n });\r\n\r\n res.statusCode = 403;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({\r\n status: 'error',\r\n errorCode: 'auth.csrfMismatch',\r\n message: 'CSRF token missing or invalid. Fetch /auth/csrf first.',\r\n }));\r\n return true;\r\n }\r\n\r\n //? Login-PRESENT path: session-bound CSRF (unchanged). Requires a session\r\n //? token — without one there is no session to protect, so do not enforce.\r\n if (!token) return false;\r\n\r\n const csrfSession = await readSession(token);\r\n if (!csrfSession?.id) return false;\r\n\r\n if (provided && csrfSession.csrfToken && timingSafeStringEqual(provided, csrfSession.csrfToken)) return false;\r\n\r\n void dispatchHook('csrfMismatch', {\r\n route: routePath,\r\n method: req.method,\r\n requestId,\r\n userId: csrfSession.id,\r\n providedToken: Boolean(provided),\r\n });\r\n\r\n res.statusCode = 403;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({\r\n status: 'error',\r\n errorCode: 'auth.csrfMismatch',\r\n message: 'CSRF token missing or invalid. Fetch /auth/csrf first.',\r\n }));\r\n return true;\r\n};\r\n","//? Optional-package capability layer (0.2.0). `@luckystack/login`, `presence`,\r\n//? and `sync` are OPTIONAL peers of `@luckystack/server`. We detect each once at\r\n//? boot via `require.resolve` (cheap, cached) and lazy-import it only when\r\n//? present, so a consumer who omits one gets graceful degradation instead of an\r\n//? `ERR_MODULE_NOT_FOUND` crash. Mirrors `@luckystack/error-tracking`'s resolve\r\n//? guard. Session reads/writes do NOT go through here — they use core's\r\n//? `readSession`/`writeSession` accessors, which login populates via the session\r\n//? provider registry. This layer is for the ROUTE/SOCKET wiring that must be\r\n//? conditionally registered (auth routes, the sync listener, presence lifecycle).\r\n\r\nimport { createRequire } from 'node:module';\r\n\r\nconst localRequire = createRequire(import.meta.url);\r\n\r\n//? Detect whether an optional package (or one of its subpaths) is installed.\r\n//?\r\n//? IMPORTANT: every `@luckystack/*` package ships an **import-only** `exports`\r\n//? map (`{ \"import\": ..., \"types\": ... }` — no `\"require\"`/`\"default\"`). A\r\n//? CJS `require.resolve()` resolves with CJS conditions and therefore THROWS\r\n//? `ERR_PACKAGE_PATH_NOT_EXPORTED` on those maps — i.e. it reports every\r\n//? installed `@luckystack/*` package as ABSENT. We must resolve with the ESM\r\n//? resolver (`import.meta.resolve`, which honors the `\"import\"` condition).\r\n//? `createRequire().resolve` is kept only as a fallback for Node < 20.6 where\r\n//? synchronous `import.meta.resolve` is unavailable.\r\nconst esmResolve = (import.meta as { resolve?: (specifier: string) => string }).resolve;\r\n\r\n//? Warn once when `import.meta.resolve` is absent (Node < 20.6). The CJS\r\n//? fallback misreports all import-only packages as absent, so optional\r\n//? features (login, presence, sync) silently degrade. Surface this so the\r\n//? operator can upgrade Node rather than debugging mysterious capability gaps.\r\nlet _warnedResolverMissing = false;\r\nconst warnResolverMissingOnce = (): void => {\r\n if (_warnedResolverMissing) return;\r\n _warnedResolverMissing = true;\r\n // eslint-disable-next-line no-console -- intentional boot diagnostic; logger not yet available\r\n console.warn(\r\n '[luckystack:capabilities] import.meta.resolve is unavailable (Node < 20.6). ' +\r\n 'Optional package detection falls back to require.resolve, which cannot resolve ' +\r\n 'import-only exports maps and may misreport @luckystack/* packages as absent. ' +\r\n 'Upgrade to Node >= 20.6 to ensure correct capability detection.',\r\n );\r\n};\r\n\r\nconst has = (pkg: string): boolean => {\r\n if (typeof esmResolve === 'function') {\r\n try {\r\n esmResolve(pkg);\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n }\r\n //? CJS fallback: `ERR_PACKAGE_PATH_NOT_EXPORTED` means the package IS\r\n //? installed but its exports map has no CJS condition — treat as PRESENT.\r\n //? Any other error (MODULE_NOT_FOUND, ERR_MODULE_NOT_FOUND) means absent.\r\n warnResolverMissingOnce();\r\n try {\r\n localRequire.resolve(pkg);\r\n return true;\r\n } catch (error: unknown) {\r\n if (error instanceof Error && (error as NodeJS.ErrnoException).code === 'ERR_PACKAGE_PATH_NOT_EXPORTED') {\r\n return true;\r\n }\r\n return false;\r\n }\r\n};\r\n\r\n//? Resolved once at module load. `as const` so callers branch on literal booleans.\r\nexport const capabilities = {\r\n login: has('@luckystack/login'),\r\n presence: has('@luckystack/presence'),\r\n sync: has('@luckystack/sync'),\r\n} as const;\r\n\r\n//? Optional packages that ship a side-effect `@luckystack/<pkg>/register`\r\n//? subpath. `bootstrapLuckyStack` resolve-guards + imports each one BEFORE the\r\n//? consumer overlay folder so a hand-written overlay (last writer) still wins.\r\n//? Each `./register` is an env-driven, idempotent no-op when its env is unset.\r\n//? Order is topological (mirrors `OVERLAY_ORDER`). NOTES:\r\n//? - `sync` is excluded: it has no server-side register; its add-later wiring\r\n//? is the client receive bridge (`@luckystack/sync/client` attachSyncReceiver).\r\n//? - `secret-manager` is excluded: it is resolved explicitly in the consumer\r\n//? entry (fails-OPEN), not via a register subpath.\r\nexport const OPTIONAL_PACKAGES = [\r\n 'login',\r\n 'email',\r\n 'error-tracking',\r\n 'presence',\r\n 'cron',\r\n 'docs-ui',\r\n] as const;\r\n\r\n//? Cheap resolve guard reused by the bootstrap auto-detect loop: true when the\r\n//? given specifier (e.g. `@luckystack/login/register`) is installed + resolvable.\r\nexport const canResolve = (specifier: string): boolean => has(specifier);\r\n\r\nlet loginMod: typeof import('@luckystack/login') | null | undefined;\r\nexport const getLogin = async (): Promise<typeof import('@luckystack/login') | null> => {\r\n if (loginMod !== undefined) return loginMod;\r\n loginMod = capabilities.login ? await import('@luckystack/login') : null;\r\n return loginMod;\r\n};\r\n\r\nlet presenceMod: typeof import('@luckystack/presence') | null | undefined;\r\nexport const getPresence = async (): Promise<typeof import('@luckystack/presence') | null> => {\r\n if (presenceMod !== undefined) return presenceMod;\r\n presenceMod = capabilities.presence ? await import('@luckystack/presence') : null;\r\n return presenceMod;\r\n};\r\n\r\nlet syncMod: typeof import('@luckystack/sync') | null | undefined;\r\nexport const getSync = async (): Promise<typeof import('@luckystack/sync') | null> => {\r\n if (syncMod !== undefined) return syncMod;\r\n syncMod = capabilities.sync ? await import('@luckystack/sync') : null;\r\n return syncMod;\r\n};\r\n","//? Origin-exempt path registry. `enforceOriginPolicy` fail-closes every\r\n//? state-changing POST that carries no browser-attributable Origin/Referer —\r\n//? which is exactly a legitimate server-to-server webhook (GitLab, Stripe, ...).\r\n//? Registering a path prefix here lets such an endpoint through the browser\r\n//? CSRF/origin gate.\r\n//?\r\n//? SECURITY: origin exemption is NOT authentication. It only removes the\r\n//? browser-origin check; the exempted handler MUST authenticate the caller\r\n//? itself (HMAC over the raw body, a shared-secret header, mTLS, ...). Pair\r\n//? with a `'pre-params'` custom route so the handler can read the raw body for\r\n//? signature verification. Empty by default — opt-in only. Do NOT register a\r\n//? prefix that overlaps framework routes (`/api`, `/auth`, `/sync`); keep\r\n//? webhooks on a dedicated prefix like `/webhooks/`. See\r\n//? docs/ARCHITECTURE_HTTP.md.\r\n\r\nexport interface OriginExemptMatcher {\r\n /**\r\n * A route is exempt when its path equals this prefix OR continues past it on a\r\n * path-SEGMENT boundary (i.e. `<prefix>/...`). Matching is boundary-aware so a\r\n * prefix can't bleed into a sibling route — `/webhooks` exempts `/webhooks` and\r\n * `/webhooks/stripe` but NOT `/webhooksadmin`.\r\n */\r\n pathPrefix: string;\r\n}\r\n\r\nconst exemptPaths: OriginExemptMatcher[] = [];\r\n\r\nexport const registerOriginExemptPath = (matcher: OriginExemptMatcher): void => {\r\n exemptPaths.push(matcher);\r\n};\r\n\r\nexport const getOriginExemptPaths = (): readonly OriginExemptMatcher[] => exemptPaths;\r\n\r\nexport const clearOriginExemptPaths = (): void => {\r\n exemptPaths.length = 0;\r\n};\r\n\r\n//? True when `routePath` matches a registered exempt prefix. Consulted by\r\n//? `enforceOriginPolicy` before it can 403 a header-less request AND by the CSRF\r\n//? middleware — so a single mis-matching prefix would drop BOTH protections.\r\n//? Match on a path-SEGMENT boundary (exact, or `<prefix>/...`) so a prefix like\r\n//? `/webhooks` can't silently exempt a sibling like `/webhooksadmin` (L5).\r\nexport const isOriginExemptPath = (routePath: string): boolean =>\r\n exemptPaths.some(({ pathPrefix }) => {\r\n if (!pathPrefix) return false;\r\n if (routePath === pathPrefix) return true;\r\n const boundary = pathPrefix.endsWith('/') ? pathPrefix : `${pathPrefix}/`;\r\n return routePath.startsWith(boundary);\r\n });\r\n","import { timingSafeEqual as cryptoTimingSafeEqual } from 'node:crypto';\n\n//? Constant-time string comparison for security tokens (CSRF tokens, the\n//? test-reset token). Plain `===` short-circuits on the first differing byte,\n//? leaking length + prefix-match information through response timing. Compare\n//? the UTF-8 byte buffers with `crypto.timingSafeEqual`, which requires equal\n//? lengths — so we length-check first (itself not secret-dependent) and only\n//? run the constant-time compare on equal-length inputs.\nexport const timingSafeStringEqual = (a: string, b: string): boolean => {\n const aBuf = Buffer.from(a, 'utf8');\n const bBuf = Buffer.from(b, 'utf8');\n if (aBuf.length !== bBuf.length) return false;\n return cryptoTimingSafeEqual(aBuf, bBuf);\n};\n","import { randomBytes } from 'node:crypto';\r\nimport { getCsrfConfig, readSession, type CsrfCookieOptions } from '@luckystack/core';\r\nimport { capabilities } from '../capabilities';\r\nimport type { HttpRouteHandler } from './types';\r\n\r\n//? Serialize the configured CSRF cookie options into a Set-Cookie string. Only\r\n//? used on the login-ABSENT double-submit path; the login-present path delivers\r\n//? the token in the JSON body (session-bound, no cookie write here).\r\nconst serializeCsrfCookie = (name: string, value: string, opts: CsrfCookieOptions): string => {\r\n const parts = [`${name}=${value}`];\r\n if (opts.httpOnly) parts.push('HttpOnly');\r\n if (opts.sameSite) parts.push(`SameSite=${opts.sameSite.charAt(0).toUpperCase()}${opts.sameSite.slice(1)}`);\r\n if (opts.secure) parts.push('Secure');\r\n parts.push(`Path=${opts.path ?? '/'}`);\r\n if (typeof opts.maxAgeMs === 'number') parts.push(`Max-Age=${String(Math.floor(opts.maxAgeMs / 1000))}`);\r\n return parts.join('; ');\r\n};\r\n\r\nexport const handleCsrfRoute: HttpRouteHandler = async ({ res, routePath, token }) => {\r\n if (routePath !== '/auth/csrf') return false;\r\n\r\n const csrfConfig = getCsrfConfig();\r\n\r\n //? Login-ABSENT (unauthenticated app): there is no per-session CSRF token, so\r\n //? issue a stateless DOUBLE-SUBMIT token — set it as the csrf cookie AND return\r\n //? it in the body. `enforceCsrfOnStateChangingRequest` later compares the cookie\r\n //? value against the `x-csrf-token` header (no session read). A cross-site\r\n //? attacker can neither read the body (CORS) nor forge the header to match the\r\n //? victim's cookie, so this blocks cross-site state changes without login.\r\n if (!capabilities.login) {\r\n //? Stateless double-submit: the SAME random value is set as the CSRF cookie\r\n //? and echoed in the JSON body. `enforceCsrfOnStateChangingRequest` later\r\n //? compares the cookie against the `x-csrf-token` request header. The cookie\r\n //? is deliberately NOT HttpOnly (the client JS must read it to echo it as the\r\n //? header); cross-site protection rests on SameSite + same-origin/CORS — an\r\n //? attacker on another origin can neither read the cookie nor the JSON body,\r\n //? so they cannot forge a matching header.\r\n //?\r\n //? KNOWN LIMITATION: without HMAC binding to a server secret this token\r\n //? cannot survive a subdomain compromise (an attacker on sub.example.com\r\n //? can set a cookie on .example.com). This is accepted in the login-absent\r\n //? posture; add `registerCsrfConfig({ sign: true })` to enable HMAC signing\r\n //? when that threat model applies.\r\n const doubleSubmit = randomBytes(csrfConfig.tokenLength).toString('hex');\r\n res.statusCode = 200;\r\n //? Resolve `Secure` per-environment (env SECURE) when the config leaves it\r\n //? unset — mirrors the session cookie so the double-submit cookie isn't\r\n //? dropped over plain HTTP in dev (which would 403 every POST). An explicit\r\n //? config `secure` (true/false) always wins.\r\n const cookieOptions: CsrfCookieOptions = {\r\n ...csrfConfig.cookieOptions,\r\n secure: csrfConfig.cookieOptions.secure ?? (process.env.SECURE === 'true'),\r\n };\r\n res.setHeader('Set-Cookie', serializeCsrfCookie(csrfConfig.cookieName, doubleSubmit, cookieOptions));\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ status: 'success', csrfToken: doubleSubmit }));\r\n return true;\r\n }\r\n\r\n //? Login-PRESENT: session-bound CSRF token (unchanged behaviour).\r\n if (!token) {\r\n res.statusCode = 401;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ status: 'error', errorCode: 'auth.unauthenticated' }));\r\n return true;\r\n }\r\n const csrfSession = await readSession(token);\r\n if (!csrfSession?.id) {\r\n res.statusCode = 401;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ status: 'error', errorCode: 'auth.unauthenticated' }));\r\n return true;\r\n }\r\n res.statusCode = 200;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({\r\n status: 'success',\r\n csrfToken: csrfSession.csrfToken ?? null,\r\n }));\r\n return true;\r\n};\r\n","import type { HttpRouteHandler } from './types';\n\nexport const handleFaviconRoute: HttpRouteHandler = async ({ res, routePath, options }) => {\n if (routePath !== '/favicon.ico') return false;\n if (options.serveFavicon) {\n await options.serveFavicon(res);\n return true;\n }\n res.writeHead(404);\n res.end();\n return true;\n};\n","import {\r\n computeSynchronizedEnvHashes,\r\n describeHealthHashConfig,\r\n getDbHealthCheck,\r\n getProjectConfig,\r\n isPrismaClientRegistered,\r\n isPrismaClientResolvable,\r\n prisma,\r\n readBootUuid,\r\n redis,\r\n resolveEnvKey,\r\n tryCatch,\r\n} from '@luckystack/core';\r\nimport type { HttpRouteHandler } from './types';\r\n\r\n//? Cross-provider connectivity ping. Prisma's generated TypeScript surface\r\n//? differs per provider: SQL providers expose `$queryRaw`, MongoDB exposes\r\n//? `$runCommandRaw`. The framework can't know which one the consumer's schema\r\n//? uses, so this single seam asserts both as optional and probes at runtime.\r\n//? Detecting the provider via `_engineConfig.activeProvider` is private API\r\n//? and has drifted between Prisma major versions — runtime probing is more\r\n//? robust.\r\ninterface PrismaPingShape {\r\n $queryRaw?: (template: TemplateStringsArray, ...values: unknown[]) => Promise<unknown>;\r\n $runCommandRaw?: (command: Record<string, unknown>) => Promise<unknown>;\r\n}\r\n\r\nconst pingPrisma = async (): Promise<boolean> => {\r\n //? Prisma's generated client typings don't expose `$queryRaw` /\r\n //? `$runCommandRaw` uniformly (depends on the active datasource). The\r\n //? `PrismaPingShape` is our minimal local probe interface; the boundary\r\n //? cast is the structural exception the strict-typing policy allows.\r\n // eslint-disable-next-line no-restricted-syntax -- Prisma datasource-conditional shape\r\n const client = prisma as unknown as PrismaPingShape;\r\n //? Capture each function into a local before calling so the typeof\r\n //? narrow holds without `!`. The shape `PrismaPingShape` declares both\r\n //? methods optional because Prisma's generated types include one or the\r\n //? other depending on active provider — capturing into a local also\r\n //? removes the assertion-style cast that the strict-typing policy disallows.\r\n const queryRaw = client.$queryRaw;\r\n if (typeof queryRaw === 'function') {\r\n const [sqlError] = await tryCatch(() => queryRaw`SELECT 1`);\r\n if (!sqlError) return true;\r\n }\r\n const runCommandRaw = client.$runCommandRaw;\r\n if (typeof runCommandRaw === 'function') {\r\n const [mongoError] = await tryCatch(() => runCommandRaw({ ping: 1 }));\r\n return !mongoError;\r\n }\r\n return false;\r\n};\r\n\r\n//? Database readiness (ADR 0020): a registered custom probe wins; otherwise\r\n//? the built-in Prisma ping runs only when Prisma is actually part of this\r\n//? install (registered client or resolvable '@prisma/client'). A deliberately\r\n//? DB-less project (orm: 'none') reports 'skipped' and can still go ready —\r\n//? previously the hard-wired Prisma ping kept it 503 forever.\r\nconst checkDatabaseReady = async (): Promise<boolean | 'skipped'> => {\r\n const registered = getDbHealthCheck();\r\n if (registered) {\r\n const [error, result] = await tryCatch(async () => registered());\r\n if (error || result === null) return false;\r\n return result;\r\n }\r\n if (isPrismaClientRegistered() || isPrismaClientResolvable()) return pingPrisma();\r\n return 'skipped';\r\n};\r\n\r\nexport const handleLivezRoute: HttpRouteHandler = ({ res, routePath }) => {\r\n if (routePath !== getProjectConfig().http.liveEndpoint) return Promise.resolve(false);\r\n res.statusCode = 200;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ status: 'live' }));\r\n return Promise.resolve(true);\r\n};\r\n\r\nexport const handleReadyzRoute: HttpRouteHandler = async ({ res, routePath }) => {\r\n if (routePath !== getProjectConfig().http.readyEndpoint) return false;\r\n\r\n //? SEC: this endpoint is intentionally unauthenticated (orchestrators and load\r\n //? balancers probe it without credentials). Each call pings Redis + Prisma, so\r\n //? callers can trigger non-trivial backend load. Mitigate at the infra layer\r\n //? (network policy, rate-limiting ingress) rather than here, to keep the probe\r\n //? surface simple and avoid circular-dependency on session/auth bootstrap.\r\n const bootUuid = await readBootUuid();\r\n\r\n const [redisError, pong] = await tryCatch(() => redis.ping());\r\n const redisOk = !redisError && (pong === 'PONG' || Boolean(pong));\r\n\r\n const databaseResult = await checkDatabaseReady();\r\n\r\n const ready = Boolean(bootUuid) && redisOk && databaseResult !== false;\r\n res.statusCode = ready ? 200 : 503;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({\r\n status: ready ? 'ready' : 'not-ready',\r\n //? `prisma` kept for backward compatibility with existing probes/dashboards\r\n //? (true when the database check passed OR was deliberately skipped);\r\n //? `database` carries the richer tri-state.\r\n checks: {\r\n bootUuid: Boolean(bootUuid),\r\n redis: redisOk,\r\n database: databaseResult,\r\n prisma: databaseResult !== false,\r\n },\r\n }));\r\n return true;\r\n};\r\n\r\nexport const handleHealthRoute: HttpRouteHandler = async ({ res, routePath }) => {\r\n if (routePath !== getProjectConfig().http.healthEndpoint) return false;\r\n\r\n //? SEC: unauthenticated by design (router and monitoring systems probe without\r\n //? session tokens). Consider binding this to an internal/loopback interface only\r\n //? in production, or protecting it with `registerCustomRoute` + a probe token,\r\n //? to prevent external amplification of the Prisma + Redis ping path.\r\n const bootUuid = await readBootUuid();\r\n //? SEC-13: pass the boot UUID so the `'@bootUuid'` salt sentinel (the 0.2.0\r\n //? default `http.healthHash` = `{ mode: 'hmac', salt: '@bootUuid' }`) resolves\r\n //? to a per-boot HMAC key. Previously the arg was omitted, so the sentinel\r\n //? always collapsed to `'plain'` and `/_health` leaked a stable, unsalted\r\n //? `sha256(secret)` fingerprint of every synchronized env value.\r\n const synchronizedHashes = computeSynchronizedEnvHashes(bootUuid);\r\n res.statusCode = bootUuid ? 200 : 503;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({\r\n status: bootUuid ? 'ok' : 'degraded',\r\n bootUuid,\r\n envKey: resolveEnvKey(),\r\n synchronizedHashes,\r\n //? Tell the router HOW these hashes were produced (mode + whether the salt is\r\n //? the `@bootUuid` sentinel) so it can hash its local values with the SAME\r\n //? config instead of its own default. Never exposes a static salt (a secret).\r\n healthHash: describeHealthHashConfig(),\r\n }));\r\n return true;\r\n};\r\n","import {\r\n clearAllHooks,\r\n clearAllRateLimits,\r\n getProjectConfig,\r\n formatKey,\r\n redis,\r\n tryCatch,\r\n} from '@luckystack/core';\r\nimport { timingSafeStringEqual } from './timingSafeEqual';\r\nimport type { HttpRouteHandler } from './types';\r\n\r\nexport const handleTestResetRoute: HttpRouteHandler = async ({ req, res, routePath }) => {\r\n if (routePath !== getProjectConfig().http.testResetEndpoint) return false;\r\n\r\n //? Fail-closed: require explicit dev/test NODE_ENV (not just \"anything but\r\n //? production\") so a missing or misconfigured NODE_ENV cannot expose this\r\n //? destructive endpoint. Also require TEST_RESET_TOKEN unconditionally —\r\n //? an unset token must NOT mean \"no auth required\".\r\n const nodeEnv = process.env.NODE_ENV;\r\n if (nodeEnv !== 'development' && nodeEnv !== 'test') {\r\n res.statusCode = 404;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ status: 'error', errorCode: 'notFound' }));\r\n return true;\r\n }\r\n //? Only POST performs the destructive reset (the documented contract). A GET/HEAD\r\n //? would also sidestep the origin gate's state-changing fail-closed branch, so\r\n //? reject any non-POST method. Runs AFTER the env check so prod still 404s rather\r\n //? than revealing the endpoint via a 405.\r\n if (req.method !== 'POST') {\r\n res.statusCode = 405;\r\n res.setHeader('Allow', 'POST');\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ status: 'error', errorCode: 'api.methodNotAllowed' }));\r\n return true;\r\n }\r\n const requiredToken = process.env.TEST_RESET_TOKEN;\r\n const providedToken = req.headers['x-test-reset-token'];\r\n const tokenValue = Array.isArray(providedToken) ? providedToken[0] : providedToken;\r\n if (!requiredToken || !tokenValue || !timingSafeStringEqual(tokenValue, requiredToken)) {\r\n res.statusCode = 403;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ status: 'error', errorCode: 'auth.forbidden' }));\r\n return true;\r\n }\r\n\r\n const cleared: string[] = [];\r\n await clearAllRateLimits();\r\n cleared.push('rateLimits');\r\n\r\n //? Flush sessions + activeUsers Redis keys so integration tests start from\r\n //? a clean slate. Patterns are derived through the shared `formatKey(...)`\r\n //? authority so they track any registered key formatter (see also\r\n //? session.ts, sessionAdapter.ts, rateLimiter.ts).\r\n const sessionPattern = `${formatKey('-session', '')}:*`;\r\n const activeUsersPattern = `${formatKey('-activeUsers', '')}:*`;\r\n\r\n const scanAndDelete = async (pattern: string, label: string): Promise<number> => {\r\n const [error, deleted] = await tryCatch(async () => {\r\n let cursor = '0';\r\n let total = 0;\r\n do {\r\n const [next, keys] = await redis.scan(cursor, 'MATCH', pattern, 'COUNT', 200);\r\n cursor = next;\r\n if (Array.isArray(keys) && keys.length > 0) {\r\n await redis.del(...keys);\r\n total += keys.length;\r\n }\r\n } while (cursor !== '0');\r\n return total;\r\n });\r\n if (error || !deleted) return 0;\r\n if (deleted > 0) cleared.push(label);\r\n return deleted;\r\n };\r\n\r\n await scanAndDelete(sessionPattern, 'sessions');\r\n await scanAndDelete(activeUsersPattern, 'activeUsers');\r\n\r\n //? Opt-in hook clear via `?include=hooks` because clearing all hooks would\r\n //? also drop framework-internal handlers (e.g. presence postLogout). URL\r\n //? parsing failure is the expected branch for malformed `req.url`, so use\r\n //? `URL.canParse` instead of try/catch.\r\n const rawUrl = req.url ?? '/';\r\n //? Use a fixed loopback base — `req.headers.host` is client-controlled and\r\n //? must not influence URL resolution; only the path and query matter here.\r\n const includeFlag = URL.canParse(rawUrl, 'http://localhost')\r\n ? new URL(rawUrl, 'http://localhost').searchParams.get('include') ?? ''\r\n : '';\r\n if (includeFlag.split(',').map((s) => s.trim()).includes('hooks')) {\r\n clearAllHooks();\r\n cleared.push('hooks');\r\n }\r\n\r\n res.statusCode = 200;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ status: 'success', cleared }));\r\n return true;\r\n};\r\n","import { serveAvatar } from '@luckystack/core';\nimport type { HttpRouteHandler } from './types';\n\nexport const handleUploadsRoute: HttpRouteHandler = async ({ res, routePath }) => {\n if (!routePath.startsWith('/uploads/')) return false;\n await serveAvatar({ routePath, res });\n return true;\n};\n","import {\r\n checkRateLimit,\r\n dispatchHook,\r\n getLogger,\r\n getProjectConfig,\r\n resolveClientIp,\r\n} from '@luckystack/core';\r\nimport { getLogin } from '../capabilities';\r\nimport { resolveCookieSecure } from './sessionCookie';\r\nimport type { HttpRouteHandler } from './types';\r\n\r\nconst parseSessionBasedTokenHeader = (headerValue: string | string[] | undefined): boolean | null => {\r\n if (headerValue === undefined) return null;\r\n const value = Array.isArray(headerValue) ? headerValue[0] : headerValue;\r\n if (value === '1' || value === 'true') return true;\r\n if (value === '0' || value === 'false') return false;\r\n return null;\r\n};\r\n\r\n//? Reserved OAuth authorize-query params owned by the framework — a provider's\r\n//? `extraAuthorizationParams` (CFG-21) can override any OTHER key (e.g. `prompt`,\r\n//? `access_type`, `login_hint`) but never these, so a consumer config can't break\r\n//? the state / PKCE / redirect-URI binding the callback relies on.\r\nconst RESERVED_OAUTH_PARAMS = new Set([\r\n 'client_id',\r\n 'redirect_uri',\r\n 'scope',\r\n 'response_type',\r\n 'state',\r\n 'code_challenge',\r\n 'code_challenge_method',\r\n]);\r\n\r\nexport const handleAuthApiRoute: HttpRouteHandler = async ({\r\n req,\r\n res,\r\n routePath,\r\n token,\r\n params,\r\n sessionCookieOptions,\r\n}) => {\r\n if (!routePath.startsWith('/auth/api')) return false;\r\n\r\n //? @luckystack/login is optional. Without it there is no auth surface, so every\r\n //? /auth/api/* route reports the disabled contract instead of crashing.\r\n const login = await getLogin();\r\n if (!login) {\r\n res.setHeader('content-type', 'application/json; charset=utf-8');\r\n res.end(JSON.stringify({ status: false, reason: 'auth.disabled' }));\r\n return true;\r\n }\r\n\r\n const config = getProjectConfig();\r\n const sessionCookieName = config.http.sessionCookieName;\r\n const shouldLogDev = config.logging.devLogs;\r\n\r\n const providerName = routePath.split('/')[3];\r\n const provider = login.getOAuthProviders().find((p) => p.name === providerName);\r\n if (!provider?.name) {\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ status: false, reason: 'login.providerNotFound' }));\r\n return true;\r\n }\r\n\r\n if (login.isFullOAuthProvider(provider)) {\r\n //? Throttle OAuth-init BEFORE the Redis state write (M4). This branch runs on\r\n //? a plain unauthenticated GET navigation and calls `createOAuthState` (a Redis\r\n //? write) on every hit; the origin gate does not stop a header-less GET, so\r\n //? without a per-IP cap an anonymous caller can loop it for Redis write-\r\n //? amplification. Same limit derivation as the credentials branch below (own\r\n //? `oauth-init` bucket) so it degrades to a no-op unless a limit is configured.\r\n const oauthRateLimiting = config.rateLimiting;\r\n const oauthRequesterIp = resolveClientIp({\r\n rawAddress: req.socket.remoteAddress,\r\n headers: req.headers,\r\n trustProxy: config.http.trustProxy,\r\n trustedProxyHopCount: config.http.trustedProxyHopCount,\r\n });\r\n const oauthInitLimit =\r\n oauthRateLimiting.defaultApiLimit !== false && oauthRateLimiting.defaultApiLimit > 0\r\n ? oauthRateLimiting.defaultApiLimit\r\n : (oauthRateLimiting.auth.enabled && oauthRateLimiting.auth.maxAttempts > 0\r\n ? oauthRateLimiting.auth.maxAttempts\r\n : null);\r\n if (oauthInitLimit !== null) {\r\n //? L4: derive the window from the SAME \"is the general limit active?\" test\r\n //? as the count above (`!== false && > 0`), so `defaultApiLimit: 0` doesn't\r\n //? pair the auth COUNT with the general WINDOW (an inconsistent bucket).\r\n const oauthInitWindowMs =\r\n oauthRateLimiting.defaultApiLimit !== false && oauthRateLimiting.defaultApiLimit > 0\r\n ? oauthRateLimiting.windowMs\r\n : oauthRateLimiting.auth.windowMs;\r\n const { allowed, resetIn } = await checkRateLimit({\r\n key: `ip:${oauthRequesterIp}:auth:oauth-init`,\r\n limit: oauthInitLimit,\r\n windowMs: oauthInitWindowMs,\r\n });\r\n if (!allowed) {\r\n void dispatchHook('rateLimitExceeded', {\r\n scope: 'auth',\r\n key: `ip:${oauthRequesterIp}:auth:oauth-init`,\r\n limit: oauthInitLimit,\r\n windowMs: oauthInitWindowMs,\r\n count: oauthInitLimit + 1,\r\n route: routePath,\r\n ip: oauthRequesterIp,\r\n });\r\n res.writeHead(429, { 'content-type': 'application/json; charset=utf-8' });\r\n res.end(JSON.stringify({\r\n status: false,\r\n reason: 'api.rateLimitExceeded',\r\n errorParams: [{ key: 'seconds', value: resetIn }],\r\n }));\r\n return true;\r\n }\r\n }\r\n\r\n //? Read the optional `return_url` query param set by the frontend when it\r\n //? initiates the OAuth flow. The value is the full URL the browser should\r\n //? land on AFTER the callback (e.g. http://localhost:5174/playground).\r\n //? Stored server-side in Redis alongside the state — NOT echoed from the\r\n //? client at callback time — so it cannot be tampered with mid-flight.\r\n //? Validation against allowedOrigins + allowLocalhost happens in loginCallback\r\n //? before the redirect is issued (isAllowedRedirectUrl gate in login.ts).\r\n const reqUrl = new URL(req.url ?? '/', 'http://placeholder');\r\n const returnUrl = reqUrl.searchParams.get('return_url') ?? undefined;\r\n\r\n const oauthState = await login.createOAuthState(provider.name, { usePkce: provider.usePkce, returnUrl });\r\n if (!oauthState) {\r\n res.writeHead(500, { 'Content-Type': 'application/json' });\r\n res.end(JSON.stringify({ status: false, reason: 'login.oauthStateInitFailed' }));\r\n return true;\r\n }\r\n\r\n //? Bind the OAuth flow to THIS browser: the callback only accepts the flow\r\n //? when the same browser presents back the nonce we stash in the\r\n //? short-lived state cookie. Same Secure derivation as the session-token\r\n //? cookie (`resolveCookieSecure`, shared seam) so the binding cookie isn't\r\n //? sent over plaintext when the deployment is HTTPS.\r\n const stateTtl = config.auth.oauthStateTtlSeconds;\r\n const secureFlag = resolveCookieSecure(config.http.sessionCookieSecure, process.env.SECURE) ? ' Secure;' : '';\r\n res.setHeader(\r\n 'Set-Cookie',\r\n `${login.OAUTH_STATE_COOKIE_NAME}=${oauthState.stateCookie}; Path=/; HttpOnly;${secureFlag} SameSite=Lax; Max-Age=${stateTtl}`,\r\n );\r\n\r\n //? Build the authorize-redirect query via URLSearchParams so a provider's\r\n //? `extraAuthorizationParams` (CFG-21) merges OVER the framework defaults —\r\n //? e.g. `access_type=offline` for Google refresh tokens, `login_hint`, or\r\n //? overriding the default `prompt=select_account`. Reserved OAuth params are\r\n //? framework-owned (skipped in the merge); `state` + the PKCE S256 challenge\r\n //? are set LAST so a consumer key can never clobber the browser binding.\r\n const authParams = new URLSearchParams({\r\n client_id: provider.clientID,\r\n redirect_uri: provider.callbackURL,\r\n scope: provider.scope.join(' '),\r\n response_type: 'code',\r\n prompt: 'select_account',\r\n });\r\n for (const [key, value] of Object.entries(provider.extraAuthorizationParams ?? {})) {\r\n if (RESERVED_OAUTH_PARAMS.has(key)) continue;\r\n authParams.set(key, value);\r\n }\r\n authParams.set('state', oauthState.state);\r\n if (oauthState.codeChallenge) {\r\n authParams.set('code_challenge', oauthState.codeChallenge);\r\n authParams.set('code_challenge_method', 'S256');\r\n }\r\n\r\n res.writeHead(302, {\r\n Location: `${provider.authorizationURL}?${authParams.toString()}`,\r\n });\r\n res.end();\r\n return true;\r\n }\r\n\r\n const rateLimiting = config.rateLimiting;\r\n //? DD-LOGIN-F5: resolve the client IP once, unconditionally, so it can be\r\n //? threaded into `loginWithCredentials` for the IP+account composite lockout\r\n //? key even when the per-IP rate-limit gate is disabled. When trustProxy is\r\n //? false this returns the raw socket address (sentinel `'unknown'` when absent).\r\n const requesterIp = resolveClientIp({\r\n rawAddress: req.socket.remoteAddress,\r\n headers: req.headers,\r\n trustProxy: config.http.trustProxy,\r\n trustedProxyHopCount: config.http.trustedProxyHopCount,\r\n });\r\n\r\n //? Derive the per-IP limit for this credentials endpoint. When the global\r\n //? `defaultApiLimit` is disabled (set to `false`), fall back to the auth-\r\n //? specific `rateLimiting.auth` slot so an IP spraying across accounts is\r\n //? still throttled — even when consumers explicitly disable the general limit\r\n //? for performance reasons. The auth slot defaults to `{ enabled: false }`,\r\n //? so the fallback is also a no-op unless the consumer opts in.\r\n const ipLimitCount =\r\n rateLimiting.defaultApiLimit !== false && rateLimiting.defaultApiLimit > 0\r\n ? rateLimiting.defaultApiLimit\r\n : (rateLimiting.auth.enabled && rateLimiting.auth.maxAttempts > 0\r\n ? rateLimiting.auth.maxAttempts\r\n : null);\r\n if (ipLimitCount !== null) {\r\n //? L4: window derived from the SAME predicate as `ipLimitCount` so\r\n //? `defaultApiLimit: 0` doesn't mix the auth count with the general window.\r\n const ipWindowMs =\r\n rateLimiting.defaultApiLimit !== false && rateLimiting.defaultApiLimit > 0\r\n ? rateLimiting.windowMs\r\n : rateLimiting.auth.windowMs;\r\n const { allowed, resetIn } = await checkRateLimit({\r\n key: `ip:${requesterIp}:auth:credentials`,\r\n limit: ipLimitCount,\r\n windowMs: ipWindowMs,\r\n });\r\n\r\n if (!allowed) {\r\n void dispatchHook('rateLimitExceeded', {\r\n scope: 'auth',\r\n key: `ip:${requesterIp}:auth:credentials`,\r\n limit: ipLimitCount,\r\n windowMs: ipWindowMs,\r\n count: ipLimitCount + 1,\r\n route: routePath,\r\n ip: requesterIp,\r\n });\r\n res.setHeader('content-type', 'application/json; charset=utf-8');\r\n res.end(JSON.stringify({\r\n status: false,\r\n reason: 'api.rateLimitExceeded',\r\n errorParams: [{ key: 'seconds', value: resetIn }],\r\n }));\r\n return true;\r\n }\r\n }\r\n\r\n //? Pass the requester's current session token as `supersedeToken` and the\r\n //? resolved `requesterIp` (DD-LOGIN-F5 composite lockout key) so that on\r\n //? a re-login while already signed in, single-session enforcement does NOT\r\n //? kick this same browser's old session.\r\n const result = (await login.loginWithCredentials(params, { supersedeToken: token ?? undefined, requesterIp })) as {\r\n status: boolean;\r\n reason: string;\r\n newToken: string | null;\r\n session: unknown;\r\n //? ADR 0024: 2FA half-way state — first factor OK, no session minted yet.\r\n requiresTwoFactor?: true;\r\n challengeToken?: string;\r\n twoFactorMethods?: string[];\r\n } | undefined;\r\n\r\n if (!result?.status) {\r\n const reasonKey =\r\n typeof result?.reason === 'string' && result.reason.length > 0\r\n ? result.reason\r\n : 'api.internalServerError';\r\n res.setHeader('content-type', 'application/json; charset=utf-8');\r\n res.end(JSON.stringify({ status: false, reason: reasonKey }));\r\n return true;\r\n }\r\n\r\n //? 2FA challenge (ADR 0024): relay the parked-login envelope. NO session\r\n //? transport is set — `/auth/api/2fa` completes the login and mints it.\r\n if (result.requiresTwoFactor) {\r\n res.setHeader('content-type', 'application/json; charset=utf-8');\r\n res.end(JSON.stringify({\r\n status: true,\r\n reason: result.reason,\r\n requiresTwoFactor: true,\r\n challengeToken: result.challengeToken,\r\n twoFactorMethods: result.twoFactorMethods,\r\n authenticated: false,\r\n }));\r\n return true;\r\n }\r\n\r\n if (result.newToken) {\r\n //? Old session was excluded from enforcement above (supersedeToken). Clean it\r\n //? up silently — `skipSocketLogout` prevents a `logout` emit to this same\r\n //? browser's live socket, which would bounce it to /login and null the new\r\n //? session before the success redirect runs.\r\n if (token) await login.deleteSession(token, { skipSocketLogout: true });\r\n\r\n const requestedSessionMode = parseSessionBasedTokenHeader(req.headers['x-session-based-token']);\r\n const useSessionBasedToken = requestedSessionMode ?? config.session.basedToken;\r\n\r\n if (shouldLogDev) getLogger().debug('http: setting cookie with new token');\r\n\r\n if (useSessionBasedToken) {\r\n res.setHeader('X-Session-Token', result.newToken);\r\n } else {\r\n res.setHeader('Set-Cookie', `${sessionCookieName}=${result.newToken}; ${sessionCookieOptions}`);\r\n }\r\n }\r\n\r\n res.setHeader('content-type', 'application/json; charset=utf-8');\r\n res.end(JSON.stringify({\r\n status: result.status,\r\n reason: result.reason,\r\n session: result.session,\r\n authenticated: Boolean(result.newToken),\r\n }));\r\n return true;\r\n};\r\n","import { resolveEnvKey } from '@luckystack/core';\n\n//? Single source of truth for the session / OAuth-state cookie `Secure` flag:\n//? honor the explicit `http.sessionCookieSecure` override (CORE-39), else the\n//? `SECURE` env flag, else default ON in production (L5). Used by BOTH the\n//? session-token cookie (`httpHandler`) and the OAuth state cookie\n//? (`authApiRoute`) so the two can never drift again (WAVE4 — the M2 fix had\n//? updated only the OAuth cookie, leaving the session cookie ignoring the override).\n//?\n//? L5: previously an HTTPS production deploy that set NEITHER\n//? `http.sessionCookieSecure` NOR `SECURE=true` shipped the session + OAuth-state\n//? cookies WITHOUT `Secure` — correct only if the operator remembered a flag.\n//? Now production defaults `Secure` ON; a genuine plain-HTTP prod deploy (rare —\n//? e.g. no TLS anywhere) must opt out with `http.sessionCookieSecure: false`.\nexport const resolveCookieSecure = (\n sessionCookieSecure: boolean | undefined,\n secureEnv: string | undefined,\n): boolean => {\n if (sessionCookieSecure !== undefined) return sessionCookieSecure;\n if (secureEnv === 'true') return true;\n return resolveEnvKey() === 'production';\n};\n","//? Email-code login + 2FA HTTP routes (ADR 0024). These live in the framework\r\n//? layer — NOT file-based `_api` routes — because completing a login must set\r\n//? the HttpOnly session cookie, and only this layer can write `Set-Cookie`\r\n//? (same seam as authApiRoute.ts). Registered BEFORE handleAuthApiRoute in\r\n//? POST_PARAMS_ROUTES: that handler catch-alls `/auth/api/*` into\r\n//? `login.providerNotFound`.\r\n//?\r\n//? Surface (all POST, JSON):\r\n//? /auth/api/email-code/request { email } → { status: true } (anti-enumeration)\r\n//? /auth/api/email-code/verify { email, code } → login envelope (may be a 2FA challenge)\r\n//? /auth/api/2fa { challengeToken, code, method? } → login envelope (completes the login)\r\n//? /auth/api/2fa/email-code { challengeToken } → send the fallback code for a challenge\r\n//? — authenticated (require a live session): —\r\n//? /auth/api/2fa/setup {} → { secret, otpauthUri }\r\n//? /auth/api/2fa/enable { code } → { recoveryCodes[] } (raw, exactly once)\r\n//? /auth/api/2fa/disable { code } → { status }\r\n//? /auth/api/2fa/recovery-codes { code } → { recoveryCodes[] } (fresh set)\r\n\r\nimport { checkRateLimit, getProjectConfig, resolveClientIp } from '@luckystack/core';\r\nimport { getLogin } from '../capabilities';\r\nimport type { HttpRouteContext, HttpRouteHandler } from './types';\r\n\r\ntype LoginModule = NonNullable<Awaited<ReturnType<typeof getLogin>>>;\r\n\r\nconst json = (ctx: HttpRouteContext, statusCode: number, body: unknown): true => {\r\n ctx.res.statusCode = statusCode;\r\n ctx.res.setHeader('content-type', 'application/json; charset=utf-8');\r\n ctx.res.end(JSON.stringify(body));\r\n return true;\r\n};\r\n\r\nconst methodNotAllowed = (ctx: HttpRouteContext): true => {\r\n ctx.res.statusCode = 405;\r\n ctx.res.setHeader('Allow', 'POST');\r\n ctx.res.end();\r\n return true;\r\n};\r\n\r\nconst requesterIpOf = (ctx: HttpRouteContext): string => {\r\n const config = getProjectConfig();\r\n return resolveClientIp({\r\n rawAddress: ctx.req.socket.remoteAddress,\r\n headers: ctx.req.headers,\r\n trustProxy: config.http.trustProxy,\r\n trustedProxyHopCount: config.http.trustedProxyHopCount,\r\n });\r\n};\r\n\r\n//? Per-IP shield in front of the login-package logic (which adds its own\r\n//? per-email / per-challenge budgets). Fixed windows keep the bucket\r\n//? meaningful even when the general API limiter is off.\r\nconst ipThrottled = async (ctx: HttpRouteContext, bucket: string, limit: number): Promise<boolean> => {\r\n const { allowed } = await checkRateLimit({\r\n key: `ip:${requesterIpOf(ctx)}:auth:${bucket}`,\r\n limit,\r\n windowMs: 15 * 60 * 1000,\r\n });\r\n return !allowed;\r\n};\r\n\r\nconst str = (value: unknown): string => (typeof value === 'string' ? value : '');\r\n\r\n//? THE COOKIE SEAM (mirror of authApiRoute.ts): relay a CredentialsLoginResult\r\n//? to the wire. Full success → session transport (header or cookie) + session\r\n//? envelope; 2FA challenge → challenge envelope, NO transport; failure → reason.\r\nconst sendLoginResult = async (\r\n ctx: HttpRouteContext,\r\n login: LoginModule,\r\n result: { status: boolean; reason: string; newToken?: string; session?: unknown; requiresTwoFactor?: true; challengeToken?: string; twoFactorMethods?: string[] },\r\n): Promise<true> => {\r\n if (!result.status) return json(ctx, 200, { status: false, reason: result.reason });\r\n\r\n if (result.requiresTwoFactor) {\r\n return json(ctx, 200, {\r\n status: true,\r\n reason: result.reason,\r\n requiresTwoFactor: true,\r\n challengeToken: result.challengeToken,\r\n twoFactorMethods: result.twoFactorMethods,\r\n authenticated: false,\r\n });\r\n }\r\n\r\n if (result.newToken) {\r\n //? Same supersede semantics as the credentials route: the old session was\r\n //? excluded from enforcement; clean it up without bouncing this browser.\r\n if (ctx.token) await login.deleteSession(ctx.token, { skipSocketLogout: true });\r\n const config = getProjectConfig();\r\n const headerValue = ctx.req.headers['x-session-based-token'];\r\n const raw = Array.isArray(headerValue) ? headerValue[0] : headerValue;\r\n const requestedSessionMode = raw === 'true' ? true : (raw === 'false' ? false : null);\r\n const useSessionBasedToken = requestedSessionMode ?? config.session.basedToken;\r\n if (useSessionBasedToken) {\r\n ctx.res.setHeader('X-Session-Token', result.newToken);\r\n } else {\r\n ctx.res.setHeader('Set-Cookie', `${config.http.sessionCookieName}=${result.newToken}; ${ctx.sessionCookieOptions}`);\r\n }\r\n }\r\n return json(ctx, 200, {\r\n status: true,\r\n reason: result.reason,\r\n session: result.session,\r\n authenticated: Boolean(result.newToken),\r\n });\r\n};\r\n\r\n//? Resolve the FRESH user record behind an authenticated request. The session\r\n//? copy is sanitized (no totpSecret/recoveryCodes by design), so enrollment\r\n//? routes re-read through the adapter.\r\nconst requireUser = async (ctx: HttpRouteContext, login: LoginModule) => {\r\n if (!ctx.token) return null;\r\n const session = await login.getSession(ctx.token);\r\n const userId = (session as { id?: string } | null)?.id;\r\n if (!userId) return null;\r\n return login.getUserAdapter().findById(userId);\r\n};\r\n\r\nexport const handleAuthEmailCodeRoute: HttpRouteHandler = async (ctx) => {\r\n if (ctx.routePath !== '/auth/api/email-code/request' && ctx.routePath !== '/auth/api/email-code/verify') return false;\r\n if (ctx.method !== 'POST') return methodNotAllowed(ctx);\r\n\r\n const login = await getLogin();\r\n if (!login) return json(ctx, 200, { status: false, reason: 'auth.disabled' });\r\n\r\n const params = ctx.params as { email?: unknown; code?: unknown };\r\n const requesterIp = requesterIpOf(ctx);\r\n\r\n if (ctx.routePath === '/auth/api/email-code/request') {\r\n if (await ipThrottled(ctx, 'email-code-request', 10)) return json(ctx, 429, { status: false, reason: 'api.rateLimitExceeded' });\r\n const result = await login.requestEmailLoginCode({ email: str(params.email), requesterIp });\r\n //? Anti-enumeration is inside the login package; disabled-feature and\r\n //? send-failure reasons pass through (they are not account signals).\r\n return json(ctx, 200, result.ok ? { status: true } : { status: false, reason: result.reason });\r\n }\r\n\r\n if (await ipThrottled(ctx, 'email-code-verify', 20)) return json(ctx, 429, { status: false, reason: 'api.rateLimitExceeded' });\r\n const result = await login.verifyEmailLoginCode({\r\n email: str(params.email),\r\n code: str(params.code),\r\n supersedeToken: ctx.token ?? undefined,\r\n requesterIp,\r\n });\r\n return sendLoginResult(ctx, login, result);\r\n};\r\n\r\nexport const handleAuthTwoFactorRoute: HttpRouteHandler = async (ctx) => {\r\n if (ctx.routePath !== '/auth/api/2fa' && !ctx.routePath.startsWith('/auth/api/2fa/')) return false;\r\n if (ctx.method !== 'POST') return methodNotAllowed(ctx);\r\n\r\n const login = await getLogin();\r\n if (!login) return json(ctx, 200, { status: false, reason: 'auth.disabled' });\r\n\r\n const params = ctx.params as { challengeToken?: unknown; code?: unknown; method?: unknown };\r\n const requesterIp = requesterIpOf(ctx);\r\n\r\n //? ── pre-login: complete a parked challenge ──\r\n if (ctx.routePath === '/auth/api/2fa') {\r\n if (await ipThrottled(ctx, '2fa-verify', 20)) return json(ctx, 429, { status: false, reason: 'api.rateLimitExceeded' });\r\n const method = str(params.method);\r\n const result = await login.verifyTwoFactorChallenge({\r\n challengeToken: str(params.challengeToken),\r\n code: str(params.code),\r\n method: method === 'email-code' || method === 'recovery-code' ? method : 'totp',\r\n supersedeToken: ctx.token ?? undefined,\r\n requesterIp,\r\n });\r\n return sendLoginResult(ctx, login, result);\r\n }\r\n\r\n if (ctx.routePath === '/auth/api/2fa/email-code') {\r\n if (await ipThrottled(ctx, '2fa-email-code', 5)) return json(ctx, 429, { status: false, reason: 'api.rateLimitExceeded' });\r\n const result = await login.requestTwoFactorEmailCode(str(params.challengeToken));\r\n return json(ctx, 200, result.ok ? { status: true } : { status: false, reason: result.reason });\r\n }\r\n\r\n //? ── authenticated: enrollment management ──\r\n //? Per-IP throttle on the code-checking management actions too — disable /\r\n //? recovery-codes verify a TOTP/recovery code and (unlike the login path)\r\n //? are not behind the per-challenge budget; without this a hijacked session\r\n //? could brute-force the current code unbounded.\r\n if (await ipThrottled(ctx, '2fa-manage', 15)) return json(ctx, 429, { status: false, reason: 'api.rateLimitExceeded' });\r\n const user = await requireUser(ctx, login);\r\n if (!user) return json(ctx, 401, { status: false, reason: 'api.unauthorized' });\r\n\r\n switch (ctx.routePath) {\r\n case '/auth/api/2fa/setup': {\r\n const start = await login.beginTotpEnrollment(user);\r\n return start.ok\r\n ? json(ctx, 200, { status: true, secret: start.secret, otpauthUri: start.otpauthUri })\r\n : json(ctx, 200, { status: false, reason: start.reason });\r\n }\r\n case '/auth/api/2fa/enable': {\r\n const confirmed = await login.confirmTotpEnrollment(user, str(params.code));\r\n return confirmed.ok\r\n ? json(ctx, 200, { status: true, recoveryCodes: confirmed.recoveryCodes })\r\n : json(ctx, 200, { status: false, reason: confirmed.reason });\r\n }\r\n case '/auth/api/2fa/disable': {\r\n const disabled = await login.disableTwoFactor(user, str(params.code));\r\n return json(ctx, 200, disabled.ok ? { status: true } : { status: false, reason: disabled.reason ?? 'login.twoFactorInvalidCode' });\r\n }\r\n case '/auth/api/2fa/recovery-codes': {\r\n const regenerated = await login.regenerateRecoveryCodes(user, str(params.code));\r\n return regenerated.ok\r\n ? json(ctx, 200, { status: true, recoveryCodes: regenerated.recoveryCodes })\r\n : json(ctx, 200, { status: false, reason: regenerated.reason });\r\n }\r\n default: {\r\n return json(ctx, 404, { status: false, reason: 'common.404' });\r\n }\r\n }\r\n};\r\n","import { getLogger, getProjectConfig, tryCatch } from '@luckystack/core';\r\nimport { getLogin } from '../capabilities';\r\nimport { resolveCookieSecure } from './sessionCookie';\r\nimport type { HttpRouteHandler } from './types';\r\n\r\n//? Warn-once guard. The SameSite weakening is a boot-time config property, not a\r\n//? per-request condition, so we surface it a single time per process — in ALL\r\n//? environments (production is exactly where the missing CSRF mitigation matters\r\n//? most), without flooding the log on every logout request.\r\nlet warnedNonStrictSameSite = false;\r\n\r\n//? HTTP logout endpoint — POST /auth/logout.\r\n//?\r\n//? Logout must terminate over HTTP in cookie mode because only an HTTP\r\n//? response can clear the HttpOnly session cookie — the socket transport the\r\n//? rest of the logout flow uses cannot touch cookies. The route deletes the\r\n//? session when the request still carries a live token (tolerating an\r\n//? already-deleted one — the socket logout usually ran first) and ALWAYS\r\n//? answers with an expiring Set-Cookie so the browser drops the stale\r\n//? credential.\r\n//?\r\n//? CSRF: deliberately outside the `/auth/api` prefix the CSRF middleware\r\n//? guards. The SameSite=Strict session cookie never rides on a cross-site\r\n//? POST, so a forged request arrives token-less and clears nothing — same\r\n//? reasoning as the credentials-bootstrap exemption in csrfMiddleware.ts.\r\nexport const handleAuthLogoutRoute: HttpRouteHandler = async ({ res, routePath, method, token }) => {\r\n if (routePath !== '/auth/logout') return false;\r\n\r\n if (method !== 'POST') {\r\n res.statusCode = 405;\r\n res.setHeader('Allow', 'POST');\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ status: 'error', errorCode: 'api.methodNotAllowed' }));\r\n return true;\r\n }\r\n\r\n if (token) {\r\n const login = await getLogin();\r\n if (login) {\r\n //? deleteSession dispatches the session-delete hooks and reuses the\r\n //? socket logout for any still-connected sockets. A dead/unknown token\r\n //? no-ops; an adapter blip must not block the cookie clear below.\r\n const [deleteError] = await tryCatch(() => login.deleteSession(token));\r\n if (deleteError) {\r\n getLogger().warn('http logout: deleteSession failed — clearing cookie anyway', { err: deleteError });\r\n }\r\n }\r\n }\r\n\r\n //? Clearing cookie: identity = name + path (+ domain). Mirror the attributes\r\n //? of buildSessionCookieOptions in httpHandler.ts, with Max-Age=0 so the\r\n //? browser expires it immediately. setHeader (not append) intentionally\r\n //? overrides the sliding-expiration refresh that ran earlier this request.\r\n const http = getProjectConfig().http;\r\n //? SEC: /auth/logout relies on SameSite=Strict as its CSRF mitigation (the\r\n //? route is deliberately outside the CSRF middleware's candidate check — see\r\n //? csrfMiddleware.ts). Warn (once per process, all envs) when the config\r\n //? weakens this assumption so operators know they must add explicit CSRF\r\n //? protection.\r\n if (http.sessionCookieSameSite !== 'Strict' && !warnedNonStrictSameSite) {\r\n warnedNonStrictSameSite = true;\r\n getLogger().warn(\r\n `/auth/logout is exempt from CSRF middleware and relies on SameSite=Strict. ` +\r\n `Current sessionCookieSameSite=\"${http.sessionCookieSameSite}\" weakens this protection.`,\r\n );\r\n }\r\n const secure = resolveCookieSecure(http.sessionCookieSecure, process.env.SECURE);\r\n res.setHeader(\r\n 'Set-Cookie',\r\n `${http.sessionCookieName}=; HttpOnly; SameSite=${http.sessionCookieSameSite}; Path=${http.sessionCookiePath}; Max-Age=0; ${secure ? 'Secure;' : ''}`,\r\n );\r\n res.statusCode = 200;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ status: 'success', result: true }));\r\n return true;\r\n};\r\n","import { getProjectConfig } from '@luckystack/core';\r\nimport { getLogin } from '../capabilities';\r\nimport type { HttpRouteHandler } from './types';\r\n\r\n//? GET /auth/providers — public list of enabled auth-provider names, derived\r\n//? from the env-driven OAuth registry (see the consumer's\r\n//? `luckystack/login/oauthProviders.ts`, which registers a provider only when\r\n//? its credentials env vars are present). The login form fetches this to decide\r\n//? which OAuth buttons to render WITHOUT ever shipping client secrets to the\r\n//? browser. Read-only + bodyless, so it runs in the pre-params phase.\r\nexport const handleAuthProvidersRoute: HttpRouteHandler = async ({ req, res, routePath }) => {\r\n if (routePath !== '/auth/providers' || req.method !== 'GET') return false;\r\n\r\n //? @luckystack/login optional: when absent there are no providers to advertise.\r\n const login = await getLogin();\r\n const providers = login ? login.getOAuthProviders().map((provider) => provider.name) : [];\r\n\r\n //? ADR 0024: advertise whether passwordless email-code login is enabled so\r\n //? the login form can render the \"email me a code\" entry point. A boolean\r\n //? config flag only — no secrets, same trust level as the provider names.\r\n const emailCodeLogin = Boolean(login) && getProjectConfig().auth.emailCodeLogin;\r\n\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ providers, emailCodeLogin }));\r\n return true;\r\n};\r\n","import { getLogger, getProjectConfig } from '@luckystack/core';\r\nimport { getLogin } from '../capabilities';\r\nimport type { HttpRouteHandler } from './types';\r\n\r\nexport const handleAuthCallbackRoute: HttpRouteHandler = async ({\r\n req,\r\n res,\r\n routePath,\r\n token,\r\n sessionCookieOptions,\r\n}) => {\r\n if (!routePath.startsWith('/auth/callback')) return false;\r\n\r\n //? @luckystack/login optional — no auth package means no OAuth callback.\r\n const login = await getLogin();\r\n if (!login) {\r\n res.writeHead(404, { 'Content-Type': 'text/plain' });\r\n res.end('Auth is not enabled');\r\n return true;\r\n }\r\n\r\n const config = getProjectConfig();\r\n const sessionCookieName = config.http.sessionCookieName;\r\n const shouldLogDev = config.logging.devLogs;\r\n\r\n //? Fallback redirect target when no `postLoginRedirect` resolver returns a URL.\r\n //? `projectConfig.app.publicUrl` is the public origin where users browse (also\r\n //? used for transactional email links) — dev: the frontend dev server, prod:\r\n //? your domain. After an OAuth callback (handled on the BACKEND origin) we must\r\n //? send the browser back to this public origin, not the backend.\r\n //?\r\n //? `loginRedirectUrl` is the configured post-login PATH (e.g. '/dashboard') and\r\n //? lives on the public origin, so we join it onto `publicUrl` to land the user\r\n //? where credentials login also lands them — not the bare public root. An\r\n //? already-absolute `loginRedirectUrl` is used as-is; an empty result falls\r\n //? through to '/dashboard' or whichever path the consumer configured.\r\n const publicOrigin = (config.app.publicUrl || '').replace(/\\/+$/, '');\r\n // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- empty string must fall through\r\n const loginRedirect = config.loginRedirectUrl || '/';\r\n const baseLocation = /^https?:\\/\\//i.test(loginRedirect)\r\n ? loginRedirect\r\n : `${publicOrigin}${loginRedirect.startsWith('/') ? loginRedirect : `/${loginRedirect}`}`;\r\n const callbackResult = await login.loginCallback(routePath, req, res, {\r\n defaultRedirectUrl: baseLocation,\r\n supersedeToken: token ?? undefined,\r\n });\r\n\r\n if (!callbackResult) {\r\n //? Clear the one-time OAuth state-binding cookie on failure too — the Redis\r\n //? state is single-use, so don't leave a spent credential-shaped cookie around.\r\n res.setHeader('Set-Cookie', `${login.OAUTH_STATE_COOKIE_NAME}=; Path=/; HttpOnly; Max-Age=0; SameSite=Lax`);\r\n res.writeHead(401, { 'Content-Type': 'text/plain' });\r\n res.end('Login failed');\r\n return true;\r\n }\r\n\r\n //? Re-login while already signed in: the new session was just created with\r\n //? `supersedeToken` so single-session enforcement did NOT kick this browser's\r\n //? old token. Clean the old session up silently — `skipSocketLogout` avoids\r\n //? emitting a `logout` to this same browser's live socket, which would\r\n //? otherwise bounce the user back to the login page and null their session.\r\n if (token) await login.deleteSession(token, { skipSocketLogout: true });\r\n\r\n if (shouldLogDev) getLogger().debug('http: setting cookie or redirect with new token');\r\n\r\n const { token: newToken, redirectUrl } = callbackResult;\r\n //? The OAuth state-binding cookie is single-use — clear it now that the callback\r\n //? has consumed the state, so a spent credential-shaped cookie doesn't linger in\r\n //? the browser until its Max-Age expires.\r\n const clearStateCookie = `${login.OAUTH_STATE_COOKIE_NAME}=; Path=/; HttpOnly; Max-Age=0; SameSite=Lax`;\r\n if (config.session.basedToken) {\r\n //? Deliver the based-token in the URL fragment, never the query string: a\r\n //? `#token=` fragment is not sent to the server, not logged, and not leaked\r\n //? via the `Referer` header on the next navigation. Drop any pre-existing\r\n //? fragment on the redirect target so we never emit a double-`#`.\r\n const hashIndex = redirectUrl.indexOf('#');\r\n const baseUrl = hashIndex === -1 ? redirectUrl : redirectUrl.slice(0, hashIndex);\r\n res.setHeader('Set-Cookie', clearStateCookie);\r\n res.writeHead(302, { Location: `${baseUrl}#token=${newToken}` });\r\n } else {\r\n res.setHeader('Set-Cookie', [clearStateCookie, `${sessionCookieName}=${newToken}; ${sessionCookieOptions}`]);\r\n res.writeHead(302, { Location: redirectUrl });\r\n }\r\n res.end();\r\n return true;\r\n};\r\n","import {\r\n captureException,\r\n dispatchHook,\r\n extractTokenFromRequest,\r\n getLogger,\r\n tryCatch,\r\n} from '@luckystack/core';\r\nimport { handleHttpApiRequest } from '@luckystack/api';\r\nimport { initSseResponse, sendSseEvent, shouldUseHttpStream } from '../sse';\r\nimport { resolveRequesterIp } from './resolveRequesterIp';\r\nimport type { HttpRouteHandler } from './types';\r\n\r\nexport const handleApiRoute: HttpRouteHandler = async ({\r\n req,\r\n res,\r\n routePath,\r\n queryString,\r\n method,\r\n params,\r\n requestId,\r\n}) => {\r\n if (!routePath.startsWith('/api/')) return false;\r\n\r\n const useHttpStream = shouldUseHttpStream({ acceptHeader: req.headers.accept, queryString });\r\n let streamClosed = false;\r\n let sseInitialized = false;\r\n //? SRV-O5 — open the SSE response LAZILY (first stream chunk, or a SUCCESSFUL\r\n //? final), NEVER eagerly. The eager `initSseResponse` committed HTTP 200 +\r\n //? event-stream headers BEFORE `handleHttpApiRequest` ran its auth / rate-limit /\r\n //? route / method gates, so an unauthenticated or rate-limited caller hitting a\r\n //? streaming endpoint received 200 with the real error buried in the SSE body\r\n //? instead of a 401 / 403 / 404 / 429 status. Deferring the open lets a\r\n //? pre-execution gate failure (which never emits a stream chunk) fall through to\r\n //? a proper-status JSON response below; the stream is only opened once the\r\n //? request has actually passed the gates and is streaming or succeeding.\r\n const ensureSseOpen = () => {\r\n if (sseInitialized || res.writableEnded) return;\r\n initSseResponse(res);\r\n sseInitialized = true;\r\n };\r\n //? SRV-O6 — wire the handler's abortSignal to client disconnect. The api\r\n //? pipeline accepts an `abortSignal` and races `main()` against it, but the HTTP\r\n //? route never supplied one, so a slow handler kept running after the caller went\r\n //? away (wasted DB/CPU work, no cancellation). Abort on any terminal connection\r\n //? event, for BOTH streaming and non-streaming requests.\r\n const abortController = new AbortController();\r\n //? Mirror the four-listener pattern: req 'error' + 'aborted' + 'close' and res\r\n //? 'error' all (1) flip streamClosed so subsequent SSE writes become no-ops and\r\n //? a broken socket can't crash the worker with an unhandled 'error', and (2)\r\n //? abort the in-flight handler. Harmless once the request has already completed.\r\n const markClosed = () => {\r\n streamClosed = true;\r\n if (!abortController.signal.aborted) abortController.abort();\r\n };\r\n req.on('close', markClosed);\r\n req.on('error', markClosed);\r\n req.on('aborted', markClosed);\r\n res.on('error', markClosed);\r\n\r\n const [error, handled] = await tryCatch(async () => {\r\n const httpToken = extractTokenFromRequest(req);\r\n const apiName = routePath.slice(5); // strip \"/api/\"\r\n\r\n if (!apiName) {\r\n const response = {\r\n status: 'error' as const,\r\n httpStatus: 400,\r\n message: 'api.invalidName',\r\n errorCode: 'api.invalidName',\r\n };\r\n //? Pre-gate failure: SSE is never opened, so emit a real 400 status.\r\n res.setHeader('Content-Type', 'application/json');\r\n res.writeHead(400);\r\n res.end(JSON.stringify(response));\r\n return true;\r\n }\r\n\r\n const apiData = typeof params === 'object'\r\n ? { ...(params as Record<string, unknown>) }\r\n : {};\r\n delete (apiData as Record<string, unknown>).stream;\r\n\r\n //? Resolve the real client IP for per-IP rate limiting (honors\r\n //? `http.trustProxy`; preserves the historical `undefined` fallback when\r\n //? there is genuinely no address). See `resolveRequesterIp`.\r\n const requesterIp = resolveRequesterIp(req);\r\n\r\n const result = await handleHttpApiRequest({\r\n name: apiName,\r\n data: apiData,\r\n token: httpToken,\r\n requesterIp,\r\n xLanguageHeader: req.headers['x-language'],\r\n acceptLanguageHeader: req.headers['accept-language'],\r\n method,\r\n abortSignal: abortController.signal,\r\n stream: useHttpStream\r\n ? (payload) => {\r\n if (streamClosed || res.writableEnded) return;\r\n ensureSseOpen();\r\n sendSseEvent({ res, event: 'stream', data: payload });\r\n }\r\n : undefined,\r\n });\r\n\r\n //? Stream the final envelope only when the request actually qualifies: SSE was\r\n //? already opened mid-stream, OR the result is a success (open it now, even if\r\n //? the handler emitted zero chunks). A gate-rejection result (auth / rate-limit\r\n //? / not-found / method) that never opened the stream falls through to the\r\n //? proper-status JSON write below instead of a 200 + SSE-wrapped error.\r\n if (useHttpStream && (sseInitialized || result.status === 'success')) {\r\n ensureSseOpen();\r\n if (!streamClosed) sendSseEvent({ res, event: 'final', data: result });\r\n res.end();\r\n return true;\r\n }\r\n\r\n res.setHeader('Content-Type', 'application/json');\r\n res.writeHead(result.httpStatus);\r\n res.end(JSON.stringify(result));\r\n return true;\r\n }, undefined, { routePath, method, requestId, source: 'httpHandler.api' });\r\n\r\n if (!error) {\r\n return handled ?? true;\r\n }\r\n\r\n getLogger().error('http-api: top-level handler threw', error, { routePath, method, requestId });\r\n captureException(error, { routePath, method, requestId, source: 'httpHandler.api' });\r\n void dispatchHook('apiError', {\r\n route: routePath,\r\n method,\r\n requestId,\r\n error,\r\n });\r\n\r\n const errResponse = {\r\n status: 'error' as const,\r\n httpStatus: 500,\r\n message: 'api.invalidRequestFormat',\r\n errorCode: 'api.invalidRequestFormat',\r\n };\r\n\r\n //? Only route the 500 through SSE when the response was actually committed (the\r\n //? stream opened). `res.headersSent` is the authoritative signal here — in the\r\n //? streaming path only `initSseResponse` writes headers before this catch — and\r\n //? it sidesteps the closure-narrowing of `sseInitialized` at this scope. When\r\n //? headers are still unwritten (gate/parse failure before any chunk), emit a real\r\n //? 500 status rather than an SSE error event on a non-SSE response.\r\n if (useHttpStream && res.headersSent) {\r\n if (!res.writableEnded) sendSseEvent({ res, event: 'error', data: errResponse });\r\n res.end();\r\n return true;\r\n }\r\n\r\n res.setHeader('Content-Type', 'application/json');\r\n res.writeHead(500);\r\n res.end(JSON.stringify(errResponse));\r\n return true;\r\n};\r\n","import type { ServerResponse } from 'node:http';\r\nimport { getProjectConfig } from '@luckystack/core';\r\n\r\n//? Server-Sent Events (SSE) helpers shared by /api/* and /sync/* HTTP\r\n//? streaming. Only used when the client opted in via Accept: text/event-stream\r\n//? or the configured ?<queryParam>=<enabledValue>.\r\n\r\nconst isExpectingEventStream = (acceptHeader: string | string[] | undefined): boolean => {\r\n if (!acceptHeader) return false;\r\n const value = Array.isArray(acceptHeader) ? acceptHeader.join(',') : acceptHeader;\r\n return value.toLowerCase().includes('text/event-stream');\r\n};\r\n\r\nconst queryRequestsStream = (queryString: string | undefined): boolean => {\r\n if (!queryString) return false;\r\n const { queryParam, enabledValue } = getProjectConfig().http.stream;\r\n const params = new URLSearchParams(queryString);\r\n const value = params.get(queryParam);\r\n return value === enabledValue || value === '1';\r\n};\r\n\r\nexport const shouldUseHttpStream = ({\r\n acceptHeader,\r\n queryString,\r\n}: {\r\n acceptHeader: string | string[] | undefined;\r\n queryString: string | undefined;\r\n}): boolean => isExpectingEventStream(acceptHeader) || queryRequestsStream(queryString);\r\n\r\nexport const initSseResponse = (res: ServerResponse): void => {\r\n res.writeHead(200, {\r\n 'Content-Type': 'text/event-stream',\r\n 'Cache-Control': 'no-cache, no-transform',\r\n Connection: 'keep-alive',\r\n 'X-Accel-Buffering': 'no',\r\n });\r\n const connectedComment = getProjectConfig().http.stream.connectedComment;\r\n if (connectedComment) {\r\n res.write(`${connectedComment}\\n\\n`);\r\n }\r\n};\r\n\r\nexport const sendSseEvent = ({\r\n res,\r\n event,\r\n data,\r\n}: {\r\n res: ServerResponse;\r\n event: string;\r\n data: unknown;\r\n}): void => {\r\n if (res.writableEnded) return;\r\n //? Defense-in-depth: only write SSE frames to a response that was actually\r\n //? opened as an event-stream via `initSseResponse`. Every framework caller\r\n //? does init first, so this changes no real path — it just prevents a future\r\n //? caller from emitting `event:`/`data:` lines onto a non-SSE response (which\r\n //? would corrupt the body the client expected).\r\n const contentType = res.getHeader('Content-Type');\r\n if (typeof contentType !== 'string' || !contentType.includes('text/event-stream')) return;\r\n res.write(`event: ${event}\\n`);\r\n res.write(`data: ${JSON.stringify(data)}\\n\\n`);\r\n};\r\n","import type { IncomingMessage } from 'node:http';\r\nimport { getProjectConfig, resolveClientIp } from '@luckystack/core';\r\n\r\n/**\r\n * Resolve the real client IP for per-IP rate limiting on the HTTP api/sync\r\n * transports.\r\n *\r\n * Extracted verbatim from the previously-duplicated inline block in\r\n * `apiRoute.ts` and `syncRoute.ts` (both were byte-identical). Behaviour is\r\n * preserved exactly:\r\n *\r\n * - Reads `http.trustProxy` from the active project config. Default\r\n * `trustProxy: false` returns the raw `req.socket.remoteAddress` (with only\r\n * IPv4-mapped IPv6 canonicalization via `resolveClientIp`); a trusted proxy\r\n * honours `X-Forwarded-For` / `X-Real-IP`.\r\n * - Preserves the historical `undefined` fallback when there is genuinely no\r\n * address to resolve (no raw socket address AND — when proxied — no trusted\r\n * forwarded header), so downstream `?? 'anonymous'` / `?? 'unknown'`\r\n * bucketing stays byte-identical. This `undefined` short-circuit is the\r\n * reason this helper exists rather than calling `resolveClientIp` directly:\r\n * `resolveClientIp` would return the `'unknown'` sentinel here, which the\r\n * api/sync transports historically did NOT key on.\r\n *\r\n * NOTE: the `/auth/api` route intentionally does NOT use this helper — it\r\n * keys on `resolveClientIp(...)` unconditionally (returning the `'unknown'`\r\n * sentinel when nothing resolves), which is a different, deliberate\r\n * contract. Unifying the two would change behaviour, so it is left as-is.\r\n */\r\nexport const resolveRequesterIp = (req: IncomingMessage): string | undefined => {\r\n const trustProxy = getProjectConfig().http.trustProxy;\r\n const trustedProxyHopCount = getProjectConfig().http.trustedProxyHopCount;\r\n const rawRemoteAddress = req.socket.remoteAddress;\r\n return (rawRemoteAddress || (trustProxy && (req.headers['x-forwarded-for'] || req.headers['x-real-ip'])))\r\n ? resolveClientIp({ rawAddress: rawRemoteAddress, headers: req.headers, trustProxy, trustedProxyHopCount })\r\n : undefined;\r\n};\r\n","import {\r\n captureException,\r\n dispatchHook,\r\n extractTokenFromRequest,\r\n getLogger,\r\n tryCatch,\r\n} from '@luckystack/core';\r\nimport type { HttpSyncStreamEvent } from '@luckystack/sync';\r\nimport { capabilities, getSync } from '../capabilities';\r\nimport { initSseResponse, sendSseEvent, shouldUseHttpStream } from '../sse';\r\nimport { resolveRequesterIp } from './resolveRequesterIp';\r\nimport type { HttpRouteHandler } from './types';\r\n\r\ninterface NormalizedHttpSyncParams {\r\n data: Record<string, unknown>;\r\n receiver: string;\r\n ignoreSelf?: boolean;\r\n cb?: string;\r\n}\r\n\r\nconst normalizeHttpSyncParams = (params: object | null): NormalizedHttpSyncParams => {\r\n const source = (params ?? {}) as Record<string, unknown>;\r\n const data = (source.data && typeof source.data === 'object'\r\n ? (source.data as Record<string, unknown>)\r\n : {});\r\n return {\r\n data,\r\n receiver: typeof source.receiver === 'string' ? source.receiver : '',\r\n ignoreSelf: typeof source.ignoreSelf === 'boolean' ? source.ignoreSelf : undefined,\r\n cb: typeof source.cb === 'string' ? source.cb : undefined,\r\n };\r\n};\r\n\r\nexport const handleSyncRoute: HttpRouteHandler = async ({\r\n req,\r\n res,\r\n routePath,\r\n queryString,\r\n method,\r\n params,\r\n requestId,\r\n}) => {\r\n if (!routePath.startsWith('/sync/')) return false;\r\n\r\n //? @luckystack/sync is optional. Absent => no real-time fanout; report the\r\n //? disabled contract so a developer hitting /sync/* gets a clear signal.\r\n const sync = capabilities.sync ? await getSync() : null;\r\n if (!sync) {\r\n res.setHeader('Content-Type', 'application/json');\r\n res.writeHead(404);\r\n res.end(JSON.stringify({ status: 'error', errorCode: 'sync.disabled', message: 'sync.disabled' }));\r\n return true;\r\n }\r\n\r\n const useHttpStream = shouldUseHttpStream({ acceptHeader: req.headers.accept, queryString });\r\n let streamClosed = false;\r\n let sseInitialized = false;\r\n //? SRV-O5 (parity with apiRoute) — open the SSE response LAZILY (first stream\r\n //? chunk, or a SUCCESSFUL final), NEVER eagerly. The eager `initSseResponse`\r\n //? committed HTTP 200 + event-stream headers BEFORE handleHttpSyncRequest ran its\r\n //? method / auth / rate-limit / route / membership gates, so a rejected caller\r\n //? hitting a streaming sync endpoint got 200 with the real error buried in the\r\n //? SSE body instead of a 405 / 401 / 403 / 404 status.\r\n const ensureSseOpen = () => {\r\n if (sseInitialized || res.writableEnded) return;\r\n initSseResponse(res);\r\n sseInitialized = true;\r\n };\r\n //? SRV-O6 — wire the handler's abortSignal to client disconnect (the sync\r\n //? pipeline accepts an `abortSignal` and races the `_server` run against it, but\r\n //? the route never supplied one). Mark the SSE stream closed on EVERY terminal\r\n //? connection event (so subsequent `sendSseEvent` calls become no-ops and a\r\n //? broken socket can't crash the worker with an unhandled 'error') AND abort the\r\n //? in-flight handler. Attached for both streaming and non-streaming requests.\r\n const abortController = new AbortController();\r\n const markClosed = () => {\r\n streamClosed = true;\r\n if (!abortController.signal.aborted) abortController.abort();\r\n };\r\n req.on('close', markClosed);\r\n req.on('error', markClosed);\r\n req.on('aborted', markClosed);\r\n res.on('error', markClosed);\r\n\r\n const [error, handled] = await tryCatch(async () => {\r\n if (method !== 'POST') {\r\n const response = {\r\n status: 'error' as const,\r\n message: 'sync.methodNotAllowed',\r\n errorCode: 'sync.methodNotAllowed',\r\n };\r\n //? Pre-gate failure: SSE is never opened, so emit a real 405 status.\r\n res.setHeader('Content-Type', 'application/json');\r\n res.writeHead(405);\r\n res.end(JSON.stringify(response));\r\n return true;\r\n }\r\n\r\n const httpToken = extractTokenFromRequest(req);\r\n const syncName = routePath.slice(6); // strip \"/sync/\"\r\n\r\n if (!syncName) {\r\n const response = {\r\n status: 'error' as const,\r\n message: 'sync.invalidName',\r\n errorCode: 'sync.invalidName',\r\n };\r\n //? Pre-gate failure: SSE is never opened, so emit a real 400 status.\r\n res.setHeader('Content-Type', 'application/json');\r\n res.writeHead(400);\r\n res.end(JSON.stringify(response));\r\n return true;\r\n }\r\n\r\n const syncParams = normalizeHttpSyncParams(params);\r\n\r\n //? Resolve the real client IP for per-IP rate limiting (honors\r\n //? `http.trustProxy`; preserves the historical `undefined` fallback when\r\n //? there is genuinely no address). See `resolveRequesterIp`.\r\n const requesterIp = resolveRequesterIp(req);\r\n\r\n const result = await sync.handleHttpSyncRequest({\r\n name: `sync/${syncName}`,\r\n cb: syncParams.cb,\r\n data: syncParams.data,\r\n receiver: syncParams.receiver,\r\n ignoreSelf: syncParams.ignoreSelf,\r\n token: httpToken,\r\n requesterIp,\r\n xLanguageHeader: req.headers['x-language'],\r\n acceptLanguageHeader: req.headers['accept-language'],\r\n abortSignal: abortController.signal,\r\n stream: useHttpStream\r\n ? (payload: HttpSyncStreamEvent) => {\r\n if (streamClosed || res.writableEnded) return;\r\n ensureSseOpen();\r\n sendSseEvent({ res, event: 'stream', data: payload });\r\n }\r\n : undefined,\r\n });\r\n\r\n //? Stream the final envelope only when the request actually qualifies: SSE was\r\n //? already opened mid-stream, OR the result is a success (open it now). A\r\n //? gate-rejection result that never opened the stream falls through to the\r\n //? proper-status JSON write below instead of a 200 + SSE-wrapped error.\r\n if (useHttpStream && (sseInitialized || result.status === 'success')) {\r\n ensureSseOpen();\r\n if (!streamClosed) sendSseEvent({ res, event: 'final', data: result });\r\n res.end();\r\n return true;\r\n }\r\n\r\n res.setHeader('Content-Type', 'application/json');\r\n res.writeHead(result.httpStatus ?? (result.status === 'success' ? 200 : 400));\r\n res.end(JSON.stringify(result));\r\n return true;\r\n }, undefined, { routePath, method, requestId, source: 'httpHandler.sync' });\r\n\r\n if (!error) {\r\n return handled ?? true;\r\n }\r\n\r\n getLogger().error('http-sync: top-level handler threw', error, { routePath, method, requestId });\r\n captureException(error, { routePath, method, requestId, source: 'httpHandler.sync' });\r\n void dispatchHook('syncError', {\r\n route: routePath,\r\n method,\r\n requestId,\r\n error,\r\n });\r\n\r\n const errResponse = {\r\n status: 'error' as const,\r\n message: 'sync.invalidRequestFormat',\r\n errorCode: 'sync.invalidRequestFormat',\r\n };\r\n\r\n //? Only route the 500 through SSE when the response was actually committed (the\r\n //? stream opened). `res.headersSent` is authoritative — in the streaming path\r\n //? only `initSseResponse` writes headers before this catch. When headers are\r\n //? still unwritten (gate/parse failure before any chunk), emit a real 500 status.\r\n if (useHttpStream && res.headersSent) {\r\n if (!res.writableEnded) sendSseEvent({ res, event: 'error', data: errResponse });\r\n res.end();\r\n return true;\r\n }\r\n\r\n res.setHeader('Content-Type', 'application/json');\r\n res.writeHead(500);\r\n res.end(JSON.stringify(errResponse));\r\n return true;\r\n};\r\n","import { captureException, getLogger, tryCatch } from '@luckystack/core';\r\nimport type { IncomingMessage, ServerResponse } from 'node:http';\r\nimport type { CustomRouteHandler, RouteContext } from '../types';\r\nimport { getCustomRoutes, getPreParamsCustomRoutes } from '../customRoutesRegistry';\r\nimport type { HttpRouteHandler } from './types';\r\n\r\nconst runHandler = async (\r\n handler: CustomRouteHandler,\r\n req: IncomingMessage,\r\n res: ServerResponse,\r\n ctx: RouteContext,\r\n source: string,\r\n): Promise<boolean> => {\r\n const [error, handled] = await tryCatch(() => handler(req, res, ctx));\r\n if (error) {\r\n //? Custom route handlers come from third-party overlay packages — one\r\n //? misbehaving handler must not crash the request loop or leak the\r\n //? error to the client. Surface to logger + Sentry, then return as\r\n //? handled so the caller short-circuits.\r\n getLogger().error(`${source} threw`, error, { routePath: ctx.routePath, method: ctx.method });\r\n captureException(error, { routePath: ctx.routePath, method: ctx.method, source });\r\n if (!res.writableEnded) {\r\n res.writeHead(500, { 'Content-Type': 'application/json' });\r\n res.end(JSON.stringify({ status: 'error', errorCode: 'server.customRouteFailed' }));\r\n }\r\n return true;\r\n }\r\n return Boolean(handled) || res.writableEnded;\r\n};\r\n\r\n//? PRE-PARAMS phase: consumer routes registered with `{ phase: 'pre-params' }`,\r\n//? dispatched BEFORE the body is parsed so the handler can read the raw `req`\r\n//? stream (webhook HMAC verification, streaming/multipart uploads). A handler\r\n//? that returns `false` without consuming the body falls through to the normal\r\n//? pipeline. Runs after the framework's own pre-params fast-paths.\r\nexport const handlePreParamsCustomRoutes: HttpRouteHandler = async ({\r\n req,\r\n res,\r\n routePath,\r\n queryString,\r\n method,\r\n token,\r\n}) => {\r\n const ctx: RouteContext = { routePath, method, queryString, token };\r\n for (const handler of getPreParamsCustomRoutes()) {\r\n const handled = await runHandler(handler, req, res, ctx, 'preParamsCustomRoute');\r\n if (handled) return true;\r\n }\r\n return false;\r\n};\r\n\r\nexport const handleCustomRoutes: HttpRouteHandler = async ({\r\n req,\r\n res,\r\n options,\r\n routePath,\r\n queryString,\r\n method,\r\n token,\r\n}) => {\r\n //? Two sources, evaluated in order: (1) handlers registered via\r\n //? `registerCustomRoute(...)` from overlay packages (`@luckystack/docs-ui`,\r\n //? etc.); (2) the legacy `customRoutes` option on\r\n //? `CreateLuckyStackServerOptions`. First one to return `true` (or end\r\n //? the response) wins.\r\n const ctx: RouteContext = { routePath, method, queryString, token };\r\n\r\n for (const handler of getCustomRoutes()) {\r\n const handled = await runHandler(handler, req, res, ctx, 'customRoutesRegistry');\r\n if (handled) return true;\r\n }\r\n if (options.customRoutes) {\r\n const handled = await runHandler(options.customRoutes, req, res, ctx, 'createServer.customRoutes');\r\n if (handled) return true;\r\n }\r\n return false;\r\n};\r\n","//? Custom-route registry. Lets overlay files (luckystack/<package>/index.ts)\r\n//? register HTTP route handlers without each one touching server/server.ts.\r\n//?\r\n//? Why a registry instead of a single function on CreateLuckyStackServerOptions:\r\n//? The overlay-folder pattern means multiple packages may want to add\r\n//? routes (docs-ui, an admin panel, a webhook receiver, ...). Each\r\n//? registers independently; the framework iterates them in the order they\r\n//? were registered, returning the first one that handles the request.\r\n//?\r\n//? The original `customRoutes` option on `CreateLuckyStackServerOptions`\r\n//? still works — it's appended to the registry at boot and runs last.\r\n//?\r\n//? Two phases (see `CustomRoutePhase`): `'post-params'` (default — runs after\r\n//? the body is parsed) and `'pre-params'` (runs before the body is read, so\r\n//? the handler gets the raw `req` stream — webhooks + streaming uploads).\r\n\r\nimport type { CustomRouteHandler, CustomRoutePhase } from './types';\r\n\r\nexport interface RegisterCustomRouteOptions {\r\n /** Pipeline phase the handler runs in. Defaults to `'post-params'`. */\r\n phase?: CustomRoutePhase;\r\n}\r\n\r\n//? `handlers` backs the default `'post-params'` phase. `getCustomRoutes()`\r\n//? returns this array by reference (callers + tests rely on the clear-in-place\r\n//? semantics), so keep it as the post-params store.\r\nconst handlers: CustomRouteHandler[] = [];\r\nconst preParamsHandlers: CustomRouteHandler[] = [];\r\n\r\nexport const registerCustomRoute = (\r\n handler: CustomRouteHandler,\r\n options: RegisterCustomRouteOptions = {},\r\n): void => {\r\n if (options.phase === 'pre-params') {\r\n preParamsHandlers.push(handler);\r\n return;\r\n }\r\n handlers.push(handler);\r\n};\r\n\r\nexport const getCustomRoutes = (): readonly CustomRouteHandler[] => handlers;\r\n\r\nexport const getPreParamsCustomRoutes = (): readonly CustomRouteHandler[] => preParamsHandlers;\r\n\r\nexport const clearCustomRoutes = (): void => {\r\n handlers.length = 0;\r\n preParamsHandlers.length = 0;\r\n};\r\n","import path from 'node:path';\r\nimport type { IncomingMessage, ServerResponse } from 'node:http';\r\nimport type { StaticFileHandler } from '../types';\r\nimport type { HttpRouteHandler } from './types';\r\n\r\nconst KNOWN_STATIC_FILE_REGEX = /^\\/(assets\\/[a-zA-Z0-9_/-]+|[a-zA-Z0-9_-]+)\\.(png|jpg|jpeg|gif|svg|html|css|js)$/;\r\n\r\n//? Source-disclosure denylist. The server bundle (`dist/server.js`) and ANY\r\n//? source map (`*.map`) must never be served by a framework static path —\r\n//? leaking either hands an attacker the readable server source. This guards\r\n//? every branch below (assets, known-extension, SPA catch-all) regardless of\r\n//? which `serveFile` the consumer wired, so the protection is structural and\r\n//? does not rely on the default noop handler.\r\nconst SERVE_DENYLIST_REGEX = /(^\\/server\\.js$)|(\\.map$)/;\r\n\r\n//? `serveFile` consumers (Vite middleware, custom static handlers) read\r\n//? `req.url`, so we need to swap it for the rewritten asset path. We swap\r\n//? around the call and restore on return so anything downstream that\r\n//? observes `req.url` (request loggers, metrics middleware) still sees the\r\n//? original incoming URL after this handler.\r\nconst serveWithRewrittenUrl = async (\r\n serveFile: StaticFileHandler,\r\n req: IncomingMessage,\r\n res: ServerResponse,\r\n rewrittenUrl: string,\r\n): Promise<void> => {\r\n const originalUrl = req.url;\r\n req.url = rewrittenUrl;\r\n try {\r\n await serveFile(req, res);\r\n } finally {\r\n req.url = originalUrl;\r\n }\r\n};\r\n\r\nexport const handleStaticAndSpaFallback: HttpRouteHandler = async ({\r\n req,\r\n res,\r\n routePath,\r\n options,\r\n}) => {\r\n // ── denylist — never serve the server bundle or source maps ───────────\r\n if (SERVE_DENYLIST_REGEX.test(routePath)) {\r\n res.writeHead(404, { 'Content-Type': 'text/plain' });\r\n res.end('Not Found');\r\n return true;\r\n }\r\n\r\n // ── /assets/* — static assets ──────────────────────────────────────────\r\n //? `startsWith` not `includes` — avoids matching `/other/assets/foo` and\r\n //? prevents the indexOf slice from drifting to an interior segment.\r\n if (routePath.startsWith('/assets/')) {\r\n //? Reject path-traversal at the framework boundary BEFORE handing the path to\r\n //? the consumer-supplied serveFile. routePath is already percent-DECODED, so\r\n //? `/assets/%2e%2e/server.js` arrives as `/assets/../server.js` and would\r\n //? otherwise reach a naive `path.join(dir, req.url)` static handler (source\r\n //? disclosure). The leading denylist only blocks the literal `/server.js`, not\r\n //? a traversal to it — so this `..` reject makes the protection structural.\r\n if (routePath.includes('..')) {\r\n res.writeHead(404, { 'Content-Type': 'text/plain' });\r\n res.end('Not Found');\r\n return true;\r\n }\r\n if (!options.serveFile) {\r\n res.writeHead(404);\r\n res.end('Not Found');\r\n return true;\r\n }\r\n await serveWithRewrittenUrl(options.serveFile, req, res, routePath);\r\n return true;\r\n }\r\n\r\n // ── *.{png,jpg,...} — known static file extensions ────────────────────\r\n if (KNOWN_STATIC_FILE_REGEX.test(routePath)) {\r\n if (!options.serveFile) {\r\n res.writeHead(404);\r\n res.end('Not Found');\r\n return true;\r\n }\r\n await options.serveFile(req, res);\r\n return true;\r\n }\r\n\r\n // ── path with extension we don't recognize — 404 ──────────────────────\r\n if (path.extname(routePath)) {\r\n res.writeHead(404, { 'Content-Type': 'text/plain' });\r\n res.end('Not Found');\r\n return true;\r\n }\r\n\r\n // ── catch-all (index.html for SPA routing) ────────────────────────────\r\n if (!options.serveFile) {\r\n res.writeHead(404, { 'Content-Type': 'text/plain' });\r\n res.end('Not Found');\r\n return true;\r\n }\r\n\r\n await serveWithRewrittenUrl(options.serveFile, req, res, '/');\r\n return true;\r\n};\r\n","import type { Server as HttpServer } from 'node:http';\r\nimport type { Socket } from 'socket.io';\r\nimport type { Redis as RedisClient } from 'ioredis';\r\nimport { Server as SocketIOServer } from 'socket.io';\r\nimport {\r\n abortAllForSocket,\r\n abortApiByResponseIndex,\r\n abortSyncByCb,\r\n allowedOrigin,\r\n applySocketMiddlewares,\r\n attachSocketRedisAdapter,\r\n formatRoomName,\r\n redis,\r\n setIoInstance,\r\n socketEventNames,\r\n buildJoinRoomResponseEventName,\r\n buildLeaveRoomResponseEventName,\r\n buildGetJoinedRoomsResponseEventName,\r\n extractLanguageFromHeader,\r\n extractTokenFromSocket,\r\n getLogger,\r\n getProjectConfig,\r\n dispatchHook,\r\n normalizeErrorResponse,\r\n readSession,\r\n writeSession,\r\n tryCatch,\r\n type apiMessage,\r\n type syncMessage,\r\n type BaseSessionLayout,\r\n} from '@luckystack/core';\r\nimport { handleApiRequest } from '@luckystack/api';\r\n//? login / presence / sync are OPTIONAL peers — resolved + lazy-loaded through\r\n//? the capability layer so the server boots + runs without them. Session reads\r\n//? use core's `readSession`/`writeSession` (login populates the provider).\r\nimport { capabilities, getPresence, getSync } from './capabilities';\r\n\r\n//? Per-token lock to serialize session mutations (prevents read-modify-write races\r\n//? when multiple room joins/leaves arrive in quick succession from the same socket).\r\nconst sessionLocks = new Map<string, Promise<void>>();\r\nconst withSessionLock = async (token: string, fn: () => Promise<void>) => {\r\n const prev = sessionLocks.get(token) ?? Promise.resolve();\r\n //? Both `then` handlers point to `fn` so a previous lock failure (rejected\r\n //? promise) doesn't block the next caller — we want each `withSessionLock`\r\n //? invocation to run regardless of whether the prior one threw.\r\n const next = prev.then(fn, fn);\r\n sessionLocks.set(token, next);\r\n await tryCatch(() => next);\r\n //? Cleanup runs unconditionally — same semantics the prior `try/finally`\r\n //? provided. Only delete if we still own the slot (a later caller may\r\n //? have already overwritten it with their own `next`).\r\n if (sessionLocks.get(token) === next) sessionLocks.delete(token);\r\n};\r\n\r\nconst getVisibleSocketRooms = (\r\n socket: { rooms: Set<string>; id: string },\r\n token: string | null\r\n): string[] => {\r\n return [...socket.rooms]\r\n .filter((room): room is string => typeof room === 'string')\r\n .filter((room) => room !== socket.id)\r\n .filter((room) => !token || room !== token);\r\n};\r\n\r\nconst getSessionRoomCodes = (session: BaseSessionLayout): string[] => {\r\n const roomCodes = Array.isArray(session.roomCodes)\r\n ? session.roomCodes.filter(\r\n (roomCode): roomCode is string => typeof roomCode === 'string' && roomCode.length > 0\r\n )\r\n : [];\r\n return [...new Set(roomCodes)];\r\n};\r\n\r\nconst sanitizeSessionRoomKeys = (session: BaseSessionLayout): BaseSessionLayout => {\r\n const { code: _legacyCode, codes: _legacyCodes, ...sanitizedSession } = session as BaseSessionLayout & {\r\n code?: string;\r\n codes?: string[];\r\n };\r\n return sanitizedSession;\r\n};\r\n\r\nexport interface LoadSocketOptions {\r\n maxHttpBufferSize?: number;\r\n}\r\n\r\nexport interface LoadSocketResult {\r\n io: SocketIOServer;\r\n /**\r\n * The duplicated Redis pub/sub clients backing the Socket.io adapter. The\r\n * server bootstrap owns them so graceful shutdown can `quit()` them — they are\r\n * NOT closed by `io.close()` (which only tears down the engine + connections).\r\n * Created here (instead of letting `attachSocketRedisAdapter` duplicate\r\n * internally) precisely so the server holds a handle to disconnect them.\r\n */\r\n adapterClients: { pubClient: RedisClient; subClient: RedisClient };\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Per-socket context passed to each per-event registrar\r\n// ---------------------------------------------------------------------------\r\n\r\ninterface SocketContext {\r\n socket: Socket;\r\n token: string | null;\r\n preferredLocale: string | undefined;\r\n activityBroadcasterEnabled: boolean;\r\n locationProviderEnabled: boolean;\r\n shouldLogDev: boolean;\r\n io: SocketIOServer;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Shared guard: validates the common room-mutation preconditions before the\r\n// async locked body runs. Returns false and emits the error when invalid.\r\n// ---------------------------------------------------------------------------\r\n\r\nconst validateRoomRequest = (\r\n socket: Socket,\r\n responseIndex: number | undefined,\r\n token: string | null,\r\n group: string,\r\n preferredLocale: string | undefined,\r\n buildResponseEventName: (idx: number) => string\r\n): responseIndex is number => {\r\n if (typeof responseIndex !== 'number') return false;\r\n\r\n if (!token) {\r\n socket.emit(buildResponseEventName(responseIndex), normalizeErrorResponse({\r\n response: { status: 'error', errorCode: 'auth.required' },\r\n preferredLocale,\r\n }));\r\n return false;\r\n }\r\n if (!group || group.length > 256) {\r\n socket.emit(buildResponseEventName(responseIndex), normalizeErrorResponse({\r\n response: { status: 'error', errorCode: 'room.invalid' },\r\n preferredLocale,\r\n }));\r\n return false;\r\n }\r\n return true;\r\n};\r\n\r\n// ---------------------------------------------------------------------------\r\n// Shared async body for room mutations (join / leave). The `mutate` callback\r\n// performs the transport-specific operation (join or leave) and computes the\r\n// next roomCodes array; the hook names and blocked error code differ per direction.\r\n// ---------------------------------------------------------------------------\r\n\r\ninterface RoomMutationOptions {\r\n socket: Socket;\r\n token: string;\r\n group: string;\r\n responseIndex: number;\r\n preferredLocale: string | undefined;\r\n shouldLogDev: boolean;\r\n buildResponseEventName: (idx: number) => string;\r\n preHook: 'preRoomJoin' | 'preRoomLeave';\r\n postHook: 'postRoomJoin' | 'postRoomLeave';\r\n blockedErrorCode: string;\r\n logVerb: string;\r\n mutate: (socket: Socket, physicalRoom: string, rawGroup: string, existingRoomCodes: string[], userId: string | undefined) => Promise<string[]>;\r\n}\r\n\r\nconst executeRoomMutation = async (opts: RoomMutationOptions): Promise<void> => {\r\n const {\r\n socket, token, group, responseIndex, preferredLocale, shouldLogDev,\r\n buildResponseEventName, preHook, postHook, blockedErrorCode, logVerb, mutate,\r\n } = opts;\r\n\r\n const session = await readSession(token);\r\n if (!session) {\r\n socket.emit(buildResponseEventName(responseIndex), normalizeErrorResponse({\r\n response: { status: 'error', errorCode: 'session.notFound' },\r\n preferredLocale,\r\n }));\r\n return;\r\n }\r\n\r\n const preResult = await dispatchHook(preHook, { token, room: group });\r\n if (preResult.stopped) {\r\n socket.emit(buildResponseEventName(responseIndex), normalizeErrorResponse({\r\n response: {\r\n status: 'error',\r\n errorCode: preResult.signal.errorCode || blockedErrorCode,\r\n },\r\n preferredLocale,\r\n userLanguage: session.language,\r\n }));\r\n return;\r\n }\r\n\r\n const existingRoomCodes = getSessionRoomCodes(session);\r\n //? Route the raw room code through the core room-name formatter so a\r\n //? non-identity `registerRoomNameFormatter` (e.g. per-tenant prefixing) applies\r\n //? to the socket.io room name the socket physically joins/leaves. The session\r\n //? stores the RAW code; only the Socket.io room name uses the physical form.\r\n //? M5: a content room's physical name MUST be identical across join, leave,\r\n //? membership-check, and broadcast/fanout — otherwise a `purpose`-branching\r\n //? formatter (multi-tenant prefixing) would place the socket in one physical\r\n //? room (`join:R`) but the sync membership check + fanout would target another\r\n //? (`broadcast:R`), so legit members get rejected and fanout reaches nobody. All\r\n //? content-room operations therefore use the single canonical `'broadcast'`\r\n //? purpose; only a genuinely-separate room family (presence) uses a different\r\n //? one. `preHook` still distinguishes the join vs leave OPERATION for the hooks\r\n //? and `mutate` below — it just no longer changes the physical room NAME.\r\n const physicalRoom = formatRoomName(group, { purpose: 'broadcast', userId: session.id });\r\n const nextRoomCodes = await mutate(socket, physicalRoom, group, existingRoomCodes, session.id);\r\n\r\n const sanitizedSession = sanitizeSessionRoomKeys(session);\r\n await writeSession(token, { ...sanitizedSession, roomCodes: nextRoomCodes });\r\n\r\n const visibleRooms = getVisibleSocketRooms(socket, token);\r\n socket.emit(buildResponseEventName(responseIndex), { rooms: visibleRooms });\r\n if (shouldLogDev) {\r\n getLogger().debug(`Socket ${socket.id} ${logVerb} group ${group}`);\r\n }\r\n\r\n void dispatchHook(postHook, { token, room: group, allRooms: visibleRooms });\r\n};\r\n\r\n// ---------------------------------------------------------------------------\r\n// Per-event registrar helpers\r\n// ---------------------------------------------------------------------------\r\n\r\nconst registerApiAndSyncEvents = (ctx: SocketContext): void => {\r\n const { socket, token } = ctx;\r\n\r\n socket.on(socketEventNames.apiRequest, (msg: apiMessage) => {\r\n void handleApiRequest({ msg, socket, token });\r\n });\r\n\r\n //? Only wire the sync listener when @luckystack/sync is installed. Absent =>\r\n //? clients that emit `sync` get no handler (their request times out / the\r\n //? HTTP fallback returns `sync.disabled`). Lazy-loaded once on first event.\r\n if (capabilities.sync) {\r\n socket.on(socketEventNames.sync, (msg: syncMessage) => {\r\n void (async () => {\r\n const sync = await getSync();\r\n if (sync) await sync.handleSyncRequest({ msg, socket, token });\r\n })();\r\n });\r\n }\r\n};\r\n\r\nconst registerCancellationEvents = (ctx: SocketContext): void => {\r\n const { socket } = ctx;\r\n\r\n //? B1 — cancellation events. Client emits `{ cb }` (sync) or\r\n //? `{ responseIndex }` (api) on the matching cancel channel; we look up\r\n //? the in-flight AbortController by `${socket.id}:<key>` and abort it.\r\n //? Server-side handler chains gate further chunk emits on the signal\r\n //? and exit early via the cleanup paths registered in each handler.\r\n socket.on(socketEventNames.syncCancel, (data: { cb?: string }) => {\r\n const cb = typeof data.cb === 'string' ? data.cb : null;\r\n if (!cb) return;\r\n abortSyncByCb(socket.id, cb);\r\n });\r\n socket.on(socketEventNames.apiCancel, (data: { responseIndex?: number | string }) => {\r\n const responseIndex = data.responseIndex;\r\n if (typeof responseIndex !== 'number' && typeof responseIndex !== 'string') return;\r\n abortApiByResponseIndex(socket.id, responseIndex);\r\n });\r\n};\r\n\r\nconst registerRoomEvents = (ctx: SocketContext): void => {\r\n const { socket, token, preferredLocale, shouldLogDev } = ctx;\r\n\r\n socket.on(socketEventNames.joinRoom, (data: { group?: string; responseIndex?: number }) => {\r\n const group = typeof data.group === 'string' ? data.group.trim() : '';\r\n const responseIndex = data.responseIndex;\r\n\r\n if (!validateRoomRequest(socket, responseIndex, token, group, preferredLocale, buildJoinRoomResponseEventName)) return;\r\n // validateRoomRequest already rejects null tokens, so this guard is redundant but narrows the type\r\n if (!token) return;\r\n\r\n void withSessionLock(token, async () => {\r\n await executeRoomMutation({\r\n socket,\r\n token,\r\n group,\r\n responseIndex,\r\n preferredLocale,\r\n shouldLogDev,\r\n buildResponseEventName: buildJoinRoomResponseEventName,\r\n preHook: 'preRoomJoin',\r\n postHook: 'postRoomJoin',\r\n blockedErrorCode: 'room.joinBlocked',\r\n logVerb: 'joined',\r\n mutate: async (sock, physicalRoom, rawGroup, existingCodes, userId) => {\r\n //? Enforce the per-session room cap (socket.maxRoomsPerSession, default\r\n //? 50) with FIFO eviction: joining a NEW room beyond the cap leaves the\r\n //? OLDEST joined room first, so `roomCodes` can't grow unbounded in Redis\r\n //? (session-bloat DoS). Re-joining an already-joined room never grows the\r\n //? set, so it never evicts.\r\n const maxRooms = getProjectConfig().socket.maxRoomsPerSession;\r\n let kept = existingCodes;\r\n if (maxRooms !== false && maxRooms > 0 && !existingCodes.includes(rawGroup)) {\r\n while (kept.length >= maxRooms) {\r\n const oldest = kept[0];\r\n if (oldest === undefined) break; // unreachable (length >= maxRooms >= 1) but narrows for noUncheckedIndexedAccess\r\n kept = kept.slice(1);\r\n //? M5: canonical content-room purpose (see the join site above) so the\r\n //? FIFO-evict leaves the SAME physical room the socket actually joined.\r\n await sock.leave(formatRoomName(oldest, { purpose: 'broadcast', userId }));\r\n }\r\n }\r\n await sock.join(physicalRoom);\r\n return [...new Set([...kept, rawGroup])];\r\n },\r\n });\r\n });\r\n });\r\n\r\n socket.on(socketEventNames.leaveRoom, (data: { group?: string; responseIndex?: number }) => {\r\n const group = typeof data.group === 'string' ? data.group.trim() : '';\r\n const responseIndex = data.responseIndex;\r\n\r\n if (!validateRoomRequest(socket, responseIndex, token, group, preferredLocale, buildLeaveRoomResponseEventName)) return;\r\n // validateRoomRequest already rejects null tokens, so this guard is redundant but narrows the type\r\n if (!token) return;\r\n\r\n void withSessionLock(token, async () => {\r\n await executeRoomMutation({\r\n socket,\r\n token,\r\n group,\r\n responseIndex,\r\n preferredLocale,\r\n shouldLogDev,\r\n buildResponseEventName: buildLeaveRoomResponseEventName,\r\n preHook: 'preRoomLeave',\r\n postHook: 'postRoomLeave',\r\n blockedErrorCode: 'room.leaveBlocked',\r\n logVerb: 'left',\r\n mutate: async (sock, physicalRoom, rawGroup, existingCodes, _userId) => {\r\n const nextCodes = existingCodes.filter((c) => c !== rawGroup);\r\n await sock.leave(physicalRoom);\r\n return nextCodes;\r\n },\r\n });\r\n });\r\n });\r\n\r\n socket.on(socketEventNames.getJoinedRooms, (data: { responseIndex?: number }) => {\r\n const responseIndex = data.responseIndex;\r\n if (typeof responseIndex !== 'number') return;\r\n\r\n if (!token) {\r\n socket.emit(buildGetJoinedRoomsResponseEventName(responseIndex), {\r\n ...normalizeErrorResponse({\r\n response: { status: 'error', errorCode: 'auth.required' },\r\n preferredLocale,\r\n }),\r\n rooms: [],\r\n });\r\n return;\r\n }\r\n\r\n socket.emit(buildGetJoinedRoomsResponseEventName(responseIndex), {\r\n rooms: getVisibleSocketRooms(socket, token),\r\n });\r\n });\r\n};\r\n\r\nconst registerDisconnectEvent = (ctx: SocketContext): void => {\r\n const { socket, token, activityBroadcasterEnabled, shouldLogDev } = ctx;\r\n\r\n socket.on(socketEventNames.disconnect, (reason: string) => {\r\n //? B1 — safety-net sweep. Per-request handlers also register their\r\n //? own `socket.once(disconnect, ...)` listeners that abort + clean up,\r\n //? but `abortAllForSocket` covers anything that slipped through (e.g.\r\n //? handler crashed before registering its disconnect listener).\r\n abortAllForSocket(socket.id);\r\n void dispatchHook('onSocketDisconnect', { socketId: socket.id, token, reason });\r\n\r\n if (activityBroadcasterEnabled) {\r\n //? Pass the token so presence can clear the token-level AFK refractory entry\r\n //? once the last socket for that token leaves — without it, lastAfkFireByToken\r\n //? grew unbounded (one stale entry per token that ever went AFK).\r\n void getPresence().then((presence) => { presence?.clearActivity(socket.id, token ?? undefined); });\r\n }\r\n\r\n if (activityBroadcasterEnabled && token) {\r\n void (async () => {\r\n const presence = await getPresence();\r\n if (presence) presence.socketDisconnecting({ token, socket, reason });\r\n })();\r\n } else {\r\n if (!token) return;\r\n if (shouldLogDev) {\r\n getLogger().debug(`user disconnected`, { reason });\r\n }\r\n }\r\n });\r\n};\r\n\r\nconst registerUpdateLocationEvent = (ctx: SocketContext): void => {\r\n const { socket, token, activityBroadcasterEnabled, locationProviderEnabled, shouldLogDev } = ctx;\r\n\r\n socket.on(\r\n socketEventNames.updateLocation,\r\n (newLocation: { pathName: string; searchParams?: Record<string, string> }) => {\r\n if (!token) return;\r\n if (!locationProviderEnabled) return;\r\n //? SEC: validate client-supplied location fields before persisting in session.\r\n //? pathName must start with '/', be at most 2048 chars, and contain no\r\n //? null bytes. searchParams keys + values are capped to prevent session bloat.\r\n if (\r\n typeof newLocation.pathName !== 'string'\r\n || !newLocation.pathName.startsWith('/')\r\n || newLocation.pathName.length > 2048\r\n || newLocation.pathName.includes('\\0')\r\n ) return;\r\n if (newLocation.searchParams !== undefined) {\r\n if (typeof newLocation.searchParams !== 'object' || Array.isArray(newLocation.searchParams)) return;\r\n const entries = Object.entries(newLocation.searchParams);\r\n if (entries.length > 50) return;\r\n for (const [k, v] of entries) {\r\n if (typeof k !== 'string' || k.length > 256 || typeof v !== 'string' || v.length > 1024) return;\r\n }\r\n }\r\n if (shouldLogDev) {\r\n getLogger().debug('updating location', { pathName: newLocation.pathName });\r\n }\r\n\r\n void withSessionLock(token, async () => {\r\n let returnedUser: BaseSessionLayout | null = null;\r\n if (activityBroadcasterEnabled) {\r\n const presence = await getPresence();\r\n if (presence) {\r\n returnedUser = await presence.socketLeaveRoom({ token, socket, newPath: newLocation.pathName });\r\n }\r\n }\r\n\r\n const user = returnedUser ?? (await readSession(token));\r\n if (!user) return;\r\n\r\n const extendedUser = user as BaseSessionLayout & { location?: typeof newLocation };\r\n const oldLocation = extendedUser.location;\r\n //? Spread into a fresh object (no in-place mutation) and sanitize\r\n //? legacy `code`/`codes` keys — same hygiene as the join/leave paths.\r\n const sanitizedUser = sanitizeSessionRoomKeys({ ...extendedUser, location: newLocation });\r\n await writeSession(token, sanitizedUser);\r\n\r\n void dispatchHook('onLocationUpdate', { token, oldLocation, newLocation });\r\n });\r\n }\r\n );\r\n};\r\n\r\nconst registerActivityEvents = (ctx: SocketContext): void => {\r\n const { socket, token, activityBroadcasterEnabled, io } = ctx;\r\n\r\n if (activityBroadcasterEnabled && token) {\r\n void (async () => {\r\n const presence = await getPresence();\r\n if (presence) presence.initActivityBroadcaster({ socket, token });\r\n })();\r\n }\r\n\r\n //? Activity tracking (production AFK + custom activity events). Seed the\r\n //? socket's last-activity on connect, start the single sampler interval\r\n //? (idempotent), and refresh last-activity on every client heartbeat /\r\n //? tab-return. The sampler walks all sockets and fires registered events\r\n //? (built-in AFK + any consumer pause/kick events).\r\n if (activityBroadcasterEnabled) {\r\n void (async () => {\r\n const presence = await getPresence();\r\n if (!presence) return;\r\n presence.startActivitySampler({ io });\r\n presence.recordActivity(socket.id);\r\n })();\r\n socket.on(socketEventNames.activity, () => {\r\n void getPresence().then((presence) => { presence?.recordActivity(socket.id); });\r\n });\r\n socket.on(socketEventNames.intentionalReconnect, () => {\r\n void getPresence().then((presence) => { presence?.recordActivity(socket.id); });\r\n });\r\n }\r\n};\r\n\r\nconst rejoinPersistedRooms = (ctx: SocketContext): void => {\r\n const { socket, token, shouldLogDev } = ctx;\r\n\r\n if (!token) return;\r\n\r\n //? Rebuild this session's room membership on (re)connect. Socket.io rooms\r\n //? are per-connection + in-memory, so a page refresh (brand-new socket) or\r\n //? a server restart drops them while `session.roomCodes` (persisted in\r\n //? Redis) still lists them. Without replaying them here the reconnected\r\n //? socket only sits in its private token room, so a `syncRequest` fan-out\r\n //? (`io.in(room).fetchSockets()`) finds zero members and returns\r\n //? `sync.noReceiversFound`. Sequenced (await) so membership is restored\r\n //? ASAP, and logged so a failed/empty rejoin is visible. Idempotent —\r\n //? re-joining an already-joined room is a no-op.\r\n void (async () => {\r\n const [rejoinError, codes] = await tryCatch(async () => {\r\n //? The token room is a private identity room (not a user-visible room code),\r\n //? so it is NOT routed through the room-name formatter.\r\n await socket.join(token);\r\n const session = await readSession(token);\r\n const roomCodes = session ? getSessionRoomCodes(session) : [];\r\n const userId = session?.id ?? null;\r\n for (const roomCode of roomCodes) {\r\n //? Route through the formatter so a multi-tenant prefix applies on\r\n //? reconnect exactly as it did on the original join (PRESENCE-1). Canonical\r\n //? content-room purpose (M5) so the rejoined physical room matches the sync\r\n //? membership check + fanout target.\r\n await socket.join(formatRoomName(roomCode, { purpose: 'broadcast', userId }));\r\n }\r\n return roomCodes;\r\n });\r\n if (rejoinError) {\r\n getLogger().warn(`socket room rejoin failed for ${socket.id}`, { error: rejoinError.message });\r\n return;\r\n }\r\n if (shouldLogDev) {\r\n getLogger().debug(`socket ${socket.id} (re)joined rooms: ${(codes ?? []).join(', ') || '(none)'}`);\r\n }\r\n })();\r\n};\r\n\r\nexport const loadSocket = (httpServer: HttpServer, options: LoadSocketOptions = {}): LoadSocketResult => {\r\n const config = getProjectConfig();\r\n const shouldLogDev = config.logging.devLogs;\r\n const shouldLogSocketStartup = config.logging.socketStartup;\r\n\r\n const io = new SocketIOServer(httpServer, {\r\n cors: {\r\n methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],\r\n origin: (origin, callback) => {\r\n //? CORS does not apply to same-origin requests, and browsers omit the\r\n //? `Origin` header entirely on a same-origin GET — which is exactly\r\n //? what the initial Socket.io polling handshake is in BOTH supported\r\n //? topologies: dev (Vite dev server on :5173 proxying to the backend)\r\n //? and prod-with-router (the @luckystack/router serves the frontend and\r\n //? backend from one origin). Socket.io *must* complete that origin-less\r\n //? HTTP handshake before it can upgrade to WebSocket, so rejecting an\r\n //? absent Origin here — as this layer did before — returned\r\n //? `400 {\"code\":3,\"message\":\"Bad request\"}` (engine.io's\r\n //? `MIDDLEWARE_FAILURE`) and broke every fresh connection.\r\n //?\r\n //? The security rationale that used to gate this (\"the CORS layer is\r\n //? the last browser-origin gate on the WS path\") was misplaced: this\r\n //? callback also fires on the plain-HTTP polling handshake, and an\r\n //? absent Origin there is the *same-origin browser* signal, not a\r\n //? CSRF vector. The real auth gate is the session token extracted from\r\n //? the handshake (`extractTokenFromSocket`) + the auth hooks, which run\r\n //? regardless of the Origin header. `allowOriginless` is kept for\r\n //? symmetry/documented opt-in but no longer gates the handshake.\r\n //?\r\n //? THREAT MODEL: non-browser callers (CLI tools, native apps, server-to-\r\n //? server) can connect without an Origin header and bypass the CORS check.\r\n //? Mitigation: all socket events require a valid session token; use\r\n //? `applySocketMiddlewares` (via `@luckystack/core`) to add an `io.use(...)`\r\n //? middleware that rejects connections lacking a token if authentication is\r\n //? required for ALL socket connections in the application.\r\n if (!origin) {\r\n callback(null, true);\r\n return;\r\n }\r\n if (allowedOrigin(origin)) {\r\n callback(null, true);\r\n } else {\r\n callback(new Error('Not allowed by CORS'));\r\n }\r\n },\r\n credentials: true,\r\n },\r\n maxHttpBufferSize: options.maxHttpBufferSize ?? config.socket.maxHttpBufferSize,\r\n pingTimeout: config.socket.pingTimeout,\r\n pingInterval: config.socket.pingInterval,\r\n });\r\n\r\n setIoInstance(io);\r\n\r\n //? Apply consumer-registered Socket.io middlewares BEFORE the connect\r\n //? handler is attached so they run on the handshake of every incoming\r\n //? socket — same contract as a direct `io.use(...)` call.\r\n applySocketMiddlewares(io);\r\n\r\n //? Redis adapter so room broadcasts fan out across instances. Required for\r\n //? split/fallback deployments; safe overhead in single-instance deploys.\r\n //? We duplicate the pub/sub clients HERE and pass them in (rather than letting\r\n //? `attachSocketRedisAdapter` duplicate internally) so the server bootstrap\r\n //? holds the handles and can `quit()` them on graceful shutdown — `io.close()`\r\n //? does not close these adapter connections.\r\n const pubClient = redis.duplicate();\r\n const subClient = redis.duplicate();\r\n attachSocketRedisAdapter(io, { pubClient, subClient });\r\n\r\n if (shouldLogSocketStartup) {\r\n getLogger().info('SocketIO server initialized (redis adapter attached)');\r\n }\r\n\r\n io.on(socketEventNames.connect, (socket) => {\r\n const token = extractTokenFromSocket(socket);\r\n //? Cache per-connection feature flags so we don't refetch the project\r\n //? config on every event.\r\n const activityBroadcasterEnabled = config.socketActivityBroadcaster ?? false;\r\n const locationProviderEnabled = config.locationProviderEnabled ?? false;\r\n //? `extractLanguageFromHeader` can return an empty string for unparseable\r\n //? headers — those should fall through to the next source, so keep `||`.\r\n /* eslint-disable @typescript-eslint/prefer-nullish-coalescing -- see comment above */\r\n const preferredLocale =\r\n extractLanguageFromHeader(socket.handshake.headers['x-language'])\r\n || extractLanguageFromHeader(socket.handshake.headers['accept-language'])\r\n || undefined;\r\n /* eslint-enable @typescript-eslint/prefer-nullish-coalescing */\r\n\r\n if (token && capabilities.presence) {\r\n void (async () => {\r\n const presence = await getPresence();\r\n if (presence) await presence.socketConnected({ token, io });\r\n })();\r\n }\r\n\r\n void dispatchHook('onSocketConnect', {\r\n socketId: socket.id,\r\n token,\r\n ip: socket.handshake.address,\r\n });\r\n\r\n const ctx: SocketContext = {\r\n socket,\r\n token,\r\n preferredLocale,\r\n activityBroadcasterEnabled,\r\n locationProviderEnabled,\r\n shouldLogDev,\r\n io,\r\n };\r\n\r\n registerApiAndSyncEvents(ctx);\r\n registerCancellationEvents(ctx);\r\n registerRoomEvents(ctx);\r\n registerDisconnectEvent(ctx);\r\n registerUpdateLocationEvent(ctx);\r\n registerActivityEvents(ctx);\r\n rejoinPersistedRooms(ctx);\r\n });\r\n\r\n return { io, adapterClients: { pubClient, subClient } };\r\n};\r\n","//? Boot-time validation that every registry the framework relies on has been\r\n//? populated by the project's overlay files. Replaces silent runtime crashes\r\n//? deep inside a request handler with a single, descriptive error thrown\r\n//? before `httpServer.listen()` is called.\r\n//?\r\n//? The check is intentionally lightweight: it only confirms that the required\r\n//? registrations happened. It does not validate the contents of those\r\n//? registrations — that's done by the registries' own type / runtime\r\n//? validators (e.g. `getProjectConfig()` always returns a deeply-merged value).\r\n\r\nimport {\r\n collectSynchronizedEnvKeys,\r\n getLogger,\r\n getProjectConfig,\r\n isDeployConfigRegistered,\r\n isLocalizedNormalizerRegistered,\r\n isProjectConfigRegistered,\r\n isRuntimeMapsProviderRegistered,\r\n resolveEnvKey,\r\n} from '@luckystack/core';\r\nimport { getLogin } from './capabilities';\r\n\r\nexport interface BootstrapRequirements {\r\n /**\r\n * If true, require a deploy config to have been registered. Defaults to\r\n * false because single-instance deployments don't need one.\r\n */\r\n requireDeployConfig?: boolean;\r\n /**\r\n * If true, require a services config to have been registered. Defaults to\r\n * false; only needed when running the router.\r\n */\r\n requireServicesConfig?: boolean;\r\n /**\r\n * If true, require an OAuth provider list to have been registered.\r\n * Defaults to false (an app may use credentials only).\r\n */\r\n requireOAuthProviders?: boolean;\r\n}\r\n\r\nexport const verifyBootstrap = async (requirements: BootstrapRequirements = {}): Promise<void> => {\r\n const missing: string[] = [];\r\n\r\n if (!isProjectConfigRegistered()) {\r\n missing.push(\r\n 'ProjectConfig — call `registerProjectConfig({...})` from `luckystack/core/bootstrap.ts` (or your config.ts).'\r\n );\r\n }\r\n\r\n if (requirements.requireDeployConfig && !isDeployConfigRegistered()) {\r\n missing.push(\r\n 'DeployConfig — call `registerDeployConfig({...})` from `luckystack/deploy/deploy.config.ts`.'\r\n );\r\n }\r\n\r\n if (requirements.requireServicesConfig) {\r\n const { isServicesConfigRegistered } = await import('@luckystack/core');\r\n if (!isServicesConfigRegistered()) {\r\n missing.push(\r\n 'ServicesConfig — call `registerServicesConfig({...})` from `services.config.ts`.'\r\n );\r\n }\r\n }\r\n\r\n if (requirements.requireOAuthProviders) {\r\n const login = await getLogin();\r\n if (login) {\r\n //? Fail only when the registry is empty OR holds ONLY the default\r\n //? `{ name: 'credentials' }` entry. A plain count check (`<= 1`) wrongly\r\n //? rejected a valid single-OAuth-provider, no-credentials app\r\n //? (e.g. `registerOAuthProviders([googleProvider({...})])`) — also length 1.\r\n const providers = login.getOAuthProviders();\r\n const onlyDefaultCredentials =\r\n providers.length === 0 || (providers.length === 1 && providers[0]?.name === 'credentials');\r\n if (onlyDefaultCredentials) {\r\n missing.push(\r\n 'OAuth providers — call `registerOAuthProviders([...])` from `luckystack/login/oauthProviders.ts` (or skip this check if your app uses credentials only).'\r\n );\r\n }\r\n } else {\r\n missing.push(\r\n 'OAuth providers required (`requireOAuthProviders`) but `@luckystack/login` is not installed. Install it, or drop the requirement if this app has no auth.'\r\n );\r\n }\r\n }\r\n\r\n //? Runtime maps provider — without it, every API/sync request silently\r\n //? returns `notFound`. Hard-fail in production; loud-warn in dev because\r\n //? tests and the bare-server dev mode legitimately boot without one.\r\n if (!isRuntimeMapsProviderRegistered()) {\r\n if (resolveEnvKey() === 'production') {\r\n missing.push(\r\n 'RuntimeMapsProvider — call `registerRuntimeMapsProvider({...})` from `server/prod/runtimeMaps.ts`. Without it, every api/sync request returns notFound.'\r\n );\r\n } else {\r\n getLogger().warn(\r\n '[LuckyStack] No RuntimeMapsProvider registered — api/sync requests will resolve to empty maps. Devkit hot-reload usually registers one automatically.',\r\n );\r\n }\r\n }\r\n\r\n //? Localized normalizer — without it, error responses degrade to\r\n //? errorCode-as-message (no i18n). Hard-fail in production; warn in dev.\r\n if (!isLocalizedNormalizerRegistered()) {\r\n if (resolveEnvKey() === 'production') {\r\n missing.push(\r\n 'LocalizedNormalizer — call `registerLocalizedNormalizer({...})` from your bootstrap. Without it, error response messages will be the raw errorCode (no i18n).'\r\n );\r\n } else {\r\n getLogger().warn(\r\n '[LuckyStack] No LocalizedNormalizer registered — error messages will pass through as the raw errorCode.',\r\n );\r\n }\r\n }\r\n\r\n //? SEC-13: the unauthenticated `/_health` endpoint emits per-`synchronizedEnvKeys`\r\n //? fingerprints so the router can detect cross-env drift. The 0.2.0 default is\r\n //? `healthHash.mode: 'hmac'` (salt `'@bootUuid'`), NOT `'plain'` — so this warning\r\n //? fires ONLY when a consumer EXPLICITLY downgrades to `mode: 'plain'`, which emits\r\n //? UNSALTED `sha256(<secret>)`: a public, offline-bruteforceable fingerprint.\r\n //? CAVEAT: the hmac default keys on the bootUuid, which /_health ALSO publishes,\r\n //? so it resists cross-boot rainbow tables but not a same-boot dictionary attack\r\n //? on low-entropy synchronized values — keep synchronized keys high-entropy or\r\n //? gate /_health. Warn, not hard-fail, to preserve boot behavior.\r\n if (isProjectConfigRegistered()) {\r\n const synchronizedKeyCount = collectSynchronizedEnvKeys().length;\r\n const healthHashMode = getProjectConfig().http.healthHash.mode;\r\n if (synchronizedKeyCount > 0 && healthHashMode === 'plain') {\r\n getLogger().warn(\r\n '[LuckyStack] SECURITY: /_health exposes UNSALTED sha256 fingerprints of '\r\n + `${String(synchronizedKeyCount)} synchronized env secret(s) by default. `\r\n + \"Set `http.healthHash.mode` to 'hmac' (or 'salted' with salt '@bootUuid') \"\r\n + 'to stop publishing brute-forceable secret fingerprints to unauthenticated callers.',\r\n );\r\n }\r\n }\r\n\r\n //? L2: the credentials login/register + OAuth-callback endpoints are CSRF-EXEMPT\r\n //? (a same-site session bootstrap can't present a pre-existing CSRF token). That\r\n //? exemption's safety rests on the session cookie being `SameSite=Strict` — a\r\n //? cross-site POST then never carries it, so login-CSRF is impossible. If a\r\n //? consumer relaxes `http.sessionCookieSameSite` to 'Lax'/'None' in cookie mode,\r\n //? a cross-site POST DOES carry the cookie and login-CSRF becomes possible. Warn\r\n //? so the coupling isn't a silent dependency on a mutable config value.\r\n if (isProjectConfigRegistered()) {\r\n const cfg = getProjectConfig();\r\n if (!cfg.session.basedToken && cfg.http.sessionCookieSameSite !== 'Strict') {\r\n getLogger().warn(\r\n `[LuckyStack] SECURITY: http.sessionCookieSameSite is '${cfg.http.sessionCookieSameSite}', not 'Strict', `\r\n + 'but the CSRF-exempt auth-bootstrap endpoints (/auth/api/credentials, /auth/callback/*) rely on '\r\n + \"SameSite=Strict to block cross-site login-CSRF. Use 'Strict', or add your own CSRF/origin check on those routes.\",\r\n );\r\n }\r\n }\r\n\r\n if (missing.length === 0) return;\r\n\r\n const detail = missing.map((line, idx) => ` ${idx + 1}. ${line}`).join('\\n');\r\n throw new Error(\r\n [\r\n '[LuckyStack] Bootstrap incomplete — the following registrations are missing:',\r\n detail,\r\n '',\r\n 'See docs/ARCHITECTURE_PACKAGING.md (overlay layout) for the recommended bootstrap order.',\r\n ].join('\\n')\r\n );\r\n};\r\n","//? Production runtime-maps loader shipped by the framework so consumers\r\n//? don't have to maintain their own `server/prod/runtimeMaps.ts`. The\r\n//? consumer only supplies a `loadGenerated` callback because dynamic-import\r\n//? path resolution is module-scoped — the framework cannot resolve\r\n//? `./generatedApis.<preset>` on the consumer's behalf.\r\n//?\r\n//? Multiple presets can be loaded into a single process by passing a\r\n//? comma-separated list as the first positional argv (parsed by\r\n//? `@luckystack/server/parseArgv`). All resolved maps are shallow-merged\r\n//? into one runtime view; collisions throw at boot (services own one\r\n//? preset by design, see docs/ARCHITECTURE_PACKAGING.md §10).\r\n//?\r\n//? Dev branch lazy-imports `@luckystack/devkit` to pull the auto-discovered\r\n//? api/sync/function maps. Devkit is excluded from the production bundle, so\r\n//? this dynamic import is never reached when NODE_ENV === 'production'.\r\n\r\nimport {\r\n getLogger,\r\n registerRuntimeMapsProvider,\r\n resolveEnvKey,\r\n type RuntimeApiMapsResult,\r\n type RuntimeMapsProvider,\r\n type RuntimeSyncMapsResult,\r\n} from '@luckystack/core';\r\nimport { getParsedBundles } from './argv';\r\n\r\ntype RuntimeMapRecord = Record<string, unknown>;\r\n\r\n/**\r\n * The shape that the generated maps module must export. `scripts/generateServerRequests.ts`\r\n * emits exactly this shape: `{ apis, syncs, functions }` where each value is a\r\n * `Record<string, Handler>`. Consumers can import this type to validate their\r\n * generated file in CI or type-check the `loadGenerated` callback return.\r\n */\r\nexport interface GeneratedRuntimeMapsModule {\r\n apis: RuntimeMapRecord;\r\n syncs: RuntimeMapRecord;\r\n functions: RuntimeMapRecord;\r\n}\r\n\r\ninterface LoadedRuntimeMaps {\r\n apisObject: RuntimeMapRecord;\r\n syncObject: RuntimeMapRecord;\r\n functionsObject: RuntimeMapRecord;\r\n}\r\n\r\ninterface DevkitRuntimeMaps {\r\n devApis: RuntimeMapRecord;\r\n devSyncs: RuntimeMapRecord;\r\n devFunctions: RuntimeMapRecord;\r\n}\r\n\r\nconst emptyRuntimeMaps: LoadedRuntimeMaps = {\r\n apisObject: {},\r\n syncObject: {},\r\n functionsObject: {},\r\n};\r\n\r\nconst isRuntimeMapRecord = (value: unknown): value is RuntimeMapRecord =>\r\n Boolean(value) && typeof value === 'object';\r\n\r\nconst normalizeGeneratedModule = (moduleValue: unknown, preset: string): LoadedRuntimeMaps => {\r\n const moduleRecord = moduleValue && typeof moduleValue === 'object'\r\n ? (moduleValue as Record<string, unknown>)\r\n : {};\r\n\r\n const apiCandidate = moduleRecord.apis;\r\n const syncCandidate = moduleRecord.syncs;\r\n const functionCandidate = moduleRecord.functions;\r\n\r\n //? Warn when none of the expected keys are present — this is almost certainly\r\n //? a mis-shaped module (wrong export, stale generated file, default-export\r\n //? wrapper) rather than a legitimately empty app. Silent coercion to `{}`\r\n //? causes every route to silently 404, which is hard to diagnose.\r\n if (\r\n !isRuntimeMapRecord(apiCandidate)\r\n && !isRuntimeMapRecord(syncCandidate)\r\n && !isRuntimeMapRecord(functionCandidate)\r\n ) {\r\n getLogger().warn(\r\n `[luckystack:runtimeMaps] preset \"${preset}\" loaded but has no recognised shape ` +\r\n `(expected { apis, syncs, functions }). ` +\r\n `Verify that generateServerRequests has been run and the correct file is imported. ` +\r\n `All routes for this preset will return notFound.`,\r\n );\r\n }\r\n\r\n return {\r\n apisObject: isRuntimeMapRecord(apiCandidate) ? apiCandidate : {},\r\n syncObject: isRuntimeMapRecord(syncCandidate) ? syncCandidate : {},\r\n functionsObject: isRuntimeMapRecord(functionCandidate) ? functionCandidate : {},\r\n };\r\n};\r\n\r\nexport interface ProdRuntimeMapsLoaderOptions {\r\n /**\r\n * Dynamic-import callback for the generated maps module of a given preset.\r\n * Called once per preset per process lifetime; the result is cached.\r\n *\r\n * The resolved module must have shape `{ apis, syncs, functions }` (the\r\n * shape `scripts/generateServerRequests.ts` emits — see\r\n * {@link GeneratedRuntimeMapsModule}). A mis-shaped module logs a warning\r\n * and results in every route for that preset returning `notFound`.\r\n *\r\n * Pass a function that calls `import()` with a path relative to YOUR\r\n * server-side module — the framework cannot resolve a relative path on\r\n * your behalf because dynamic-import resolution is module-scoped.\r\n *\r\n * @example\r\n * loadGenerated: (preset) => import(`./prod/generatedApis.${preset}`)\r\n */\r\n //? Returns `unknown` because the consumer hands us an `import()` of a generated\r\n //? module whose shape we cannot statically guarantee; `normalizeGeneratedModule`\r\n //? validates it at runtime against `GeneratedRuntimeMapsModule` and warns on drift.\r\n loadGenerated: (preset: string) => Promise<unknown>;\r\n /**\r\n * Override the preset(s) to load. Skips the argv lookup. Accepts a single\r\n * preset name or an array. Useful in tests or when the preset list comes\r\n * from a non-argv source.\r\n */\r\n preset?: string | string[];\r\n}\r\n\r\nconst resolvePresets = (options: ProdRuntimeMapsLoaderOptions): string[] => {\r\n const fromOptions = options.preset;\r\n if (typeof fromOptions === 'string' && fromOptions.length > 0) {\r\n return [fromOptions];\r\n }\r\n if (Array.isArray(fromOptions) && fromOptions.length > 0) {\r\n return [...new Set(fromOptions)];\r\n }\r\n const fromArgv = getParsedBundles();\r\n if (fromArgv.length > 0) {\r\n return fromArgv;\r\n }\r\n return ['default'];\r\n};\r\n\r\nconst mergeInto = (\r\n target: RuntimeMapRecord,\r\n source: RuntimeMapRecord,\r\n kind: 'api' | 'sync' | 'function',\r\n fromPreset: string,\r\n keyOrigin: Map<string, string>,\r\n): void => {\r\n for (const key of Object.keys(source)) {\r\n const previousPreset = keyOrigin.get(key);\r\n if (previousPreset !== undefined && previousPreset !== fromPreset) {\r\n throw new Error(\r\n `[luckystack:runtimeMaps] ${kind} key collision: \"${key}\" present in both ` +\r\n `preset \"${previousPreset}\" and preset \"${fromPreset}\". ` +\r\n `Services must belong to exactly one preset (see docs/ARCHITECTURE_PACKAGING.md §10).`,\r\n );\r\n }\r\n keyOrigin.set(key, fromPreset);\r\n target[key] = source[key];\r\n }\r\n};\r\n\r\n/**\r\n * Build a `RuntimeMapsProvider` that loads generated maps in production and\r\n * delegates to `@luckystack/devkit`'s discovery in dev. Same shape consumers\r\n * used to hand-roll in `server/prod/runtimeMaps.ts`.\r\n *\r\n * Call `registerRuntimeMapsProvider(...)` with the result, or use\r\n * `registerProdRuntimeMapsProvider(...)` (this module) to do both in one\r\n * step.\r\n */\r\nconst isProduction = (): boolean => resolveEnvKey() === 'production';\r\n\r\nexport const createProdRuntimeMapsProvider = (\r\n options: ProdRuntimeMapsLoaderOptions,\r\n): RuntimeMapsProvider => {\r\n let prodMapsPromise: Promise<LoadedRuntimeMaps> | null = null;\r\n\r\n let devkitModulePromise: Promise<DevkitRuntimeMaps> | null = null;\r\n const getDevkit = async (): Promise<DevkitRuntimeMaps> => {\r\n devkitModulePromise ??= import('@luckystack/devkit') as Promise<DevkitRuntimeMaps>;\r\n return await devkitModulePromise;\r\n };\r\n\r\n const loadProdRuntimeMaps = async (): Promise<LoadedRuntimeMaps> => {\r\n if (prodMapsPromise) return await prodMapsPromise;\r\n\r\n prodMapsPromise = (async () => {\r\n const presets = resolvePresets(options);\r\n const merged: LoadedRuntimeMaps = {\r\n apisObject: {},\r\n syncObject: {},\r\n functionsObject: {},\r\n };\r\n const apiOrigin = new Map<string, string>();\r\n const syncOrigin = new Map<string, string>();\r\n const functionOrigin = new Map<string, string>();\r\n\r\n const loadedModules = await Promise.all(\r\n presets.map(async (preset) => ({\r\n preset,\r\n mod: await options.loadGenerated(preset).catch(() => null),\r\n })),\r\n );\r\n\r\n let loadedAny = false;\r\n for (const { preset, mod } of loadedModules) {\r\n if (!mod) {\r\n getLogger().warn(\r\n `[luckystack:runtimeMaps] preset \"${preset}\" failed to load — skipping. ` +\r\n `Calls owned by that preset will return notFound until the generated module resolves.`,\r\n );\r\n continue;\r\n }\r\n loadedAny = true;\r\n const normalized = normalizeGeneratedModule(mod, preset);\r\n mergeInto(merged.apisObject, normalized.apisObject, 'api', preset, apiOrigin);\r\n mergeInto(merged.syncObject, normalized.syncObject, 'sync', preset, syncOrigin);\r\n mergeInto(merged.functionsObject, normalized.functionsObject, 'function', preset, functionOrigin);\r\n }\r\n\r\n if (!loadedAny) {\r\n getLogger().warn(\r\n `[luckystack:runtimeMaps] no presets resolved (tried: ${presets.join(', ')}). ` +\r\n `Every api/sync request will return notFound until at least one generated module loads.`,\r\n );\r\n return emptyRuntimeMaps;\r\n }\r\n\r\n return merged;\r\n })();\r\n\r\n return await prodMapsPromise;\r\n };\r\n\r\n const getRuntimeApiMaps = async (): Promise<RuntimeApiMapsResult> => {\r\n if (!isProduction()) {\r\n const { devApis, devFunctions } = await getDevkit();\r\n return {\r\n apisObject: isRuntimeMapRecord(devApis) ? devApis : {},\r\n functionsObject: isRuntimeMapRecord(devFunctions) ? devFunctions : {},\r\n };\r\n }\r\n const { apisObject, functionsObject } = await loadProdRuntimeMaps();\r\n return { apisObject, functionsObject };\r\n };\r\n\r\n const getRuntimeSyncMaps = async (): Promise<RuntimeSyncMapsResult> => {\r\n if (!isProduction()) {\r\n const { devSyncs, devFunctions } = await getDevkit();\r\n return {\r\n syncObject: isRuntimeMapRecord(devSyncs) ? devSyncs : {},\r\n functionsObject: isRuntimeMapRecord(devFunctions) ? devFunctions : {},\r\n };\r\n }\r\n const { syncObject, functionsObject } = await loadProdRuntimeMaps();\r\n return { syncObject, functionsObject };\r\n };\r\n\r\n return { getRuntimeApiMaps, getRuntimeSyncMaps };\r\n};\r\n\r\n/**\r\n * Convenience wrapper around `createProdRuntimeMapsProvider` +\r\n * `registerRuntimeMapsProvider`. Most consumers want this — pass the\r\n * loader callback once and the runtime is wired.\r\n */\r\nexport const registerProdRuntimeMapsProvider = (\r\n options: ProdRuntimeMapsLoaderOptions,\r\n): RuntimeMapsProvider => {\r\n const provider = createProdRuntimeMapsProvider(options);\r\n registerRuntimeMapsProvider(provider);\r\n return provider;\r\n};\r\n","import type { Server as HttpServer } from 'node:http';\nimport type { Server as SocketIOServer } from 'socket.io';\nimport type { Redis as RedisClient } from 'ioredis';\nimport {\n dispatchHook,\n flushErrorTrackers,\n getLogger,\n tryCatch,\n} from '@luckystack/core';\nimport type { StopLuckyStackServerOptions } from './types';\n\nconst DEFAULT_SHUTDOWN_TIMEOUT_MS = 10_000;\n\nexport interface GracefulShutdownDeps {\n httpServer: HttpServer;\n ioServer: SocketIOServer;\n adapterClients: { pubClient: RedisClient; subClient: RedisClient };\n}\n\n//? Race a promise against a timeout so one hanging shutdown step (a stuck\n//? `flush`, a socket that never closes, a half-dead Redis connection) can never\n//? stall the whole sequence past the deadline. Resolves `true` if `fn`\n//? completed, `false` if the timeout won. Never rejects — shutdown is\n//? best-effort and a thrown step must not abort the remaining steps.\nconst withTimeout = async (\n label: string,\n timeoutMs: number,\n fn: () => Promise<void>,\n): Promise<boolean> => {\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<false>((resolve) => {\n timer = setTimeout(() => {\n getLogger().warn(`[shutdown] step \"${label}\" exceeded ${String(timeoutMs)}ms — moving on`);\n resolve(false);\n }, timeoutMs);\n //? Don't let the timer keep the event loop alive once everything else is done.\n timer.unref();\n });\n const run = (async (): Promise<true> => {\n const [error] = await tryCatch(fn);\n if (error) getLogger().warn(`[shutdown] step \"${label}\" failed: ${error.message}`);\n return true;\n })();\n const completed = await Promise.race([run, timeout]);\n if (timer) clearTimeout(timer);\n return completed;\n};\n\nconst closeHttpServer = (httpServer: HttpServer): Promise<void> =>\n new Promise<void>((resolve, reject) => {\n //? `close()` stops accepting NEW connections and fires the callback once all\n //? existing connections end. The timeout race in the caller bounds the wait\n //? for slow keep-alive clients. Surface the callback error (e.g.\n //? `ERR_SERVER_NOT_RUNNING` when close() is called before listen()) so the\n //? caller's `withTimeout` tryCatch logs it like every other shutdown step,\n //? instead of resolving silently.\n httpServer.close((error) => { if (error) reject(error); else resolve(); });\n });\n\nconst closeIoServer = (ioServer: SocketIOServer): Promise<void> =>\n new Promise<void>((resolve, reject) => {\n //? `io.close(cb)` also returns a promise in socket.io 4.x; we drive\n //? completion off the callback, so explicitly ignore the returned promise.\n //? Surface the callback error (forwarded from the underlying http close) so\n //? the caller's `withTimeout` tryCatch logs it instead of swallowing it.\n void ioServer.close((error) => { if (error) reject(error); else resolve(); });\n });\n\nconst quitRedisClient = async (client: RedisClient): Promise<void> => {\n await client.quit();\n};\n\n/**\n * Run the graceful-shutdown sequence (MIS-016). Ordered + isolated:\n * 1. Stop accepting NEW HTTP connections (`httpServer.close()` is kicked off\n * immediately; we await it last so in-flight requests can drain).\n * 2. Dispatch the core `preServerStop` hook so consumers can flush queues /\n * release leases / close pools.\n * 3. `flushErrorTrackers()` so buffered events aren't lost — bounded by the\n * timeout race.\n * 4. Close the Socket.io server, then the HTTP server, then `quit()` the\n * Redis-adapter pub/sub clients.\n *\n * Every step is wrapped in {@link withTimeout} so a single failing/hanging step\n * cannot hang the whole shutdown — the sequence always settles within\n * `timeoutMs` per step. Never rejects.\n */\nexport const runGracefulShutdown = async (\n deps: GracefulShutdownDeps,\n options: StopLuckyStackServerOptions = {},\n): Promise<void> => {\n const { httpServer, ioServer, adapterClients } = deps;\n const reason = options.reason ?? 'manual';\n const timeoutMs = options.timeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;\n\n getLogger().info(`[shutdown] graceful shutdown started (reason=${reason})`);\n\n //? Kick off \"stop accepting new connections\" first. `httpServer.close()` only\n //? finishes once existing connections drain, so START it now and AWAIT it near\n //? the end — that way the hook + flush run while connections wind down.\n const httpClosed = withTimeout('http-close', timeoutMs, () => closeHttpServer(httpServer));\n\n //? Best-effort consumer shutdown hook. A returned stop signal is ignored — the\n //? process is going down — and the dispatcher already swallows per-handler\n //? throws; the timeout guards against a handler that simply never resolves.\n await withTimeout('preServerStop-hook', timeoutMs, async () => {\n await dispatchHook('preServerStop', { reason, timeoutMs });\n });\n\n //? Flush buffered error-tracker events before tearing down. Bounded so a\n //? wedged transport (e.g. Sentry unreachable) can't hang shutdown.\n await withTimeout('flush-error-trackers', timeoutMs, () => flushErrorTrackers());\n\n //? Close the socket layer before the HTTP server fully settles so no new\n //? socket upgrades sneak in.\n await withTimeout('io-close', timeoutMs, () => closeIoServer(ioServer));\n\n await httpClosed;\n\n //? Disconnect the Redis-adapter pub/sub clients — `io.close()` leaves them\n //? open, so without this the process keeps two live Redis sockets.\n await withTimeout('redis-pub-quit', timeoutMs, () => quitRedisClient(adapterClients.pubClient));\n await withTimeout('redis-sub-quit', timeoutMs, () => quitRedisClient(adapterClients.subClient));\n\n getLogger().info('[shutdown] graceful shutdown complete');\n};\n","import fs from 'node:fs';\r\nimport path from 'node:path';\r\nimport { getLogger } from '@luckystack/core';\r\n\r\n//? Dev-only handshake between the two local processes (`npm run server` and\r\n//? `npm run client`). When the backend auto-increments off a busy `SERVER_PORT`\r\n//? (see `listenLuckyStackServer`), the Vite proxy — which only knows the `.env`\r\n//? `SERVER_PORT` — would keep targeting the OLD port and every proxied request\r\n//? (api / sync / the socket.io websocket) would miss. So the backend advertises\r\n//? its ACTUAL bound port here and the template `vite.config.ts` reads it.\r\n//?\r\n//? Location: `node_modules/.luckystack/` — always gitignored (no `.gitignore`\r\n//? edit, no repo clutter) and present at the shared project cwd for both\r\n//? processes. Purely ephemeral: rewritten on every dev boot, removed on exit;\r\n//? a stale file is harmless (the proxy falls back to `SERVER_PORT`).\r\nconst DEV_SERVER_FILE = ['node_modules', '.luckystack', 'dev-server.json'] as const;\r\n\r\nconst devServerInfoPath = (): string => path.join(process.cwd(), ...DEV_SERVER_FILE);\r\n\r\nexport interface DevServerInfo {\r\n ip: string;\r\n port: number;\r\n pid: number;\r\n}\r\n\r\n//? Best-effort: a failed write must NEVER take the server down. Worst case the\r\n//? proxy falls back to `.env` `SERVER_PORT` (the pre-existing behaviour).\r\nexport const writeDevServerInfo = (ip: string, port: number): void => {\r\n try {\r\n const file = devServerInfoPath();\r\n fs.mkdirSync(path.dirname(file), { recursive: true });\r\n const info: DevServerInfo = { ip, port, pid: process.pid };\r\n fs.writeFileSync(file, `${JSON.stringify(info, null, 2)}\\n`);\r\n } catch (error) {\r\n getLogger().debug(\r\n `[dev-server-info] could not write port file (proxy will fall back to SERVER_PORT): ${\r\n error instanceof Error ? error.message : String(error)\r\n }`,\r\n );\r\n }\r\n};\r\n\r\n//? Remove the file on a clean exit so a later `npm run client` started without a\r\n//? backend doesn't proxy to a dead port. Silently ignores a missing file.\r\nexport const clearDevServerInfo = (): void => {\r\n try {\r\n fs.rmSync(devServerInfoPath(), { force: true });\r\n } catch {\r\n //? Ignore — a leftover file is rewritten on the next dev boot, and the proxy\r\n //? falls back to SERVER_PORT when it points at a port nothing answers on.\r\n }\r\n};\r\n","//? One-call bootstrap helper. Auto-imports the consumer's `luckystack/`\r\n//? overlay folder in the canonical order, then starts the server.\r\n//?\r\n//? The convention is documented in `docs/ARCHITECTURE_PACKAGING.md`. Each\r\n//? overlay file calls one of the framework's `register*` functions at module\r\n//? load (side-effect import), so by the time `createLuckyStackServer` runs\r\n//? the registries are populated.\r\n//?\r\n//? Order matters because some registries depend on others (e.g. login's\r\n//? userAdapter needs the Prisma client registered first). The framework\r\n//? imports them in topological order.\r\n\r\nimport path from 'node:path';\r\nimport fs from 'node:fs';\r\nimport { pathToFileURL } from 'node:url';\r\nimport { ROOT_DIR, tryCatch } from '@luckystack/core';\r\nimport { createLuckyStackServer } from './createServer';\r\nimport { OPTIONAL_PACKAGES, canResolve, getLogin } from './capabilities';\r\nimport type {\r\n CreateLuckyStackServerOptions,\r\n RunningLuckyStackServer,\r\n} from './types';\r\n\r\nexport interface BootstrapLuckyStackOptions extends CreateLuckyStackServerOptions {\r\n /**\r\n * Folder name (relative to project root) that contains the overlay files.\r\n * Default: `luckystack`. Each subfolder mirrors a framework package and\r\n * holds the project's overrides for that package.\r\n */\r\n overlayRoot?: string;\r\n /**\r\n * Skip auto-loading the overlay folder. Useful for tests that build the\r\n * registries by hand.\r\n */\r\n skipOverlayLoad?: boolean;\r\n}\r\n\r\n//? Exported (0.4.2): the consumer's `scripts/bundleServer.mjs` imports this at\r\n//? build time so the prod-bundle overlay walk can never drift from the dev\r\n//? walk — a hardcoded copy once silently dropped a slot (cron) from prod.\r\nexport const OVERLAY_ORDER = [\r\n // Core registries first — clients, paths, routing rules. Anything below\r\n // depends on these being in place.\r\n 'core',\r\n // Deploy + services topology — needed by the router but harmless for\r\n // single-instance deploys.\r\n 'deploy',\r\n // Auth — OAuth providers + user adapter sit on top of core.\r\n 'login',\r\n // Transactional email sender registration (optional @luckystack/email).\r\n 'email',\r\n // Sentry / docs-ui / presence sit on top of core but don't block boot.\r\n 'sentry',\r\n 'presence',\r\n // Cron jobs — `registerCronJob(...)` calls in `luckystack/cron/*.ts` lazily\r\n // start the leader-elected scheduler (optional @luckystack/cron).\r\n 'cron',\r\n 'docs-ui',\r\n // Server overlay last — typically empty, but a place to wire framework\r\n // hooks (`registerHook('onSocketConnect', ...)` etc.) before listen().\r\n 'server',\r\n];\r\n\r\nconst importIfExists = async (filePath: string): Promise<void> => {\r\n if (!fs.existsSync(filePath)) return;\r\n const [error] = await tryCatch(async () => {\r\n await import(pathToFileURL(filePath).href);\r\n });\r\n if (error) {\r\n //? Fail fast, but ACTIONABLY: a raw ERR_MODULE_NOT_FOUND from an overlay\r\n //? file (classic cause: `luckystack remove <pkg>` dropped a dependency\r\n //? that `luckystack/<pkg>/*.ts` still imports) tells the consumer nothing\r\n //? about WHICH of their files broke the boot.\r\n throw new Error(\r\n `[luckystack] failed to load overlay file ${filePath}: ${error.message}. ` +\r\n 'If you removed an optional package (e.g. via `luckystack remove <feature>` or `luckystack manage`), ' +\r\n 'delete or update the overlay files that still import it (the `luckystack/<feature>/` folder).',\r\n );\r\n }\r\n};\r\n\r\n//? Production-bundle seam. `loadOverlayFolder` imports raw `luckystack/**/*.ts`\r\n//? files at runtime — fine under tsx in dev, but a bundled server runs under\r\n//? plain `node`, where importing a `.ts` file is a hard crash\r\n//? (ERR_UNKNOWN_FILE_EXTENSION). The server bundler (`scripts/bundleServer.mjs`)\r\n//? therefore generates an entry that statically imports the overlay files\r\n//? (so esbuild compiles them into the bundle) and registers a loader here.\r\n//? When a loader is registered, `bootstrapLuckyStack` runs it INSTEAD of the\r\n//? filesystem walk — same files, same order, but resolved at build time.\r\ntype OverlayLoader = () => Promise<void>;\r\nlet registeredOverlayLoader: OverlayLoader | null = null;\r\n\r\nexport const registerOverlayLoader = (loader: OverlayLoader): void => {\r\n registeredOverlayLoader = loader;\r\n};\r\n\r\nconst loadOverlayFolder = async (overlayRoot: string): Promise<void> => {\r\n const overlayAbs = path.isAbsolute(overlayRoot)\r\n ? overlayRoot\r\n : path.join(ROOT_DIR, overlayRoot);\r\n\r\n if (!fs.existsSync(overlayAbs)) {\r\n //? No overlay folder is fine — projects can register everything from a\r\n //? single `config.ts` if they prefer the legacy layout.\r\n return;\r\n }\r\n\r\n for (const packageName of OVERLAY_ORDER) {\r\n const packageDir = path.join(overlayAbs, packageName);\r\n if (!fs.existsSync(packageDir)) continue;\r\n\r\n //? Load `index.ts` first if present, then any other `*.ts` files in\r\n //? alphabetical order. Each file is responsible for its own\r\n //? side-effect registration.\r\n const indexCandidates = ['index.ts', 'index.js'];\r\n for (const candidate of indexCandidates) {\r\n await importIfExists(path.join(packageDir, candidate));\r\n }\r\n\r\n const entries = fs.readdirSync(packageDir).toSorted();\r\n for (const entry of entries) {\r\n if (indexCandidates.includes(entry)) continue;\r\n if (!entry.endsWith('.ts') && !entry.endsWith('.js')) continue;\r\n await importIfExists(path.join(packageDir, entry));\r\n }\r\n }\r\n};\r\n\r\n//? Auto-detect phase (0.2.0 \"install-anything-anytime\"). For each optional\r\n//? package that ships a `./register` side-effect subpath, import it so the\r\n//? package self-wires from env WITHOUT any consumer code edit — `npm i\r\n//? @luckystack/<pkg>` + env + restart is enough. Resolve-guarded so an absent\r\n//? package is a silent no-op; a single register's internal failure must never\r\n//? crash boot (register modules log their own errors). Runs BEFORE\r\n//? `loadOverlayFolder` so a consumer overlay file (last writer) can override the\r\n//? auto-wired defaults.\r\nconst importOptionalPackageRegisters = async (): Promise<void> => {\r\n for (const pkg of OPTIONAL_PACKAGES) {\r\n const specifier = `@luckystack/${pkg}/register`;\r\n if (!canResolve(specifier)) continue;\r\n await importIfExistsSpecifier(specifier);\r\n }\r\n};\r\n\r\nconst importIfExistsSpecifier = async (specifier: string): Promise<void> => {\r\n try {\r\n await import(specifier);\r\n } catch {\r\n //? A register module that throws at load (e.g. an internal peer-dep error)\r\n //? should not take the whole server down — the module is responsible for\r\n //? logging its own failure. Boot continues with that feature degraded.\r\n }\r\n};\r\n\r\nexport const bootstrapLuckyStack = async (\r\n options: BootstrapLuckyStackOptions = {}\r\n): Promise<RunningLuckyStackServer> => {\r\n const overlayRoot = options.overlayRoot ?? 'luckystack';\r\n\r\n if (!options.skipOverlayLoad) {\r\n //? Package self-wiring first, consumer overlay second (overlay overrides).\r\n await importOptionalPackageRegisters();\r\n //? Production bundle: overlay files were compiled into the bundle and the\r\n //? generated entry registered a loader — never touch raw .ts on disk then.\r\n await (registeredOverlayLoader ? registeredOverlayLoader() : loadOverlayFolder(overlayRoot));\r\n }\r\n\r\n //? Force login to load when installed so its session provider registers into\r\n //? core (`registerSessionProvider`, side-effect of the package index) even in\r\n //? an app that never imports any `@luckystack/login` export directly. No-op\r\n //? when login is absent — the app runs unauthenticated.\r\n await getLogin();\r\n\r\n const server = await createLuckyStackServer(options);\r\n return server;\r\n};\r\n","//? Backward-compatibility re-export. The error-formatter registry moved\r\n//? to @luckystack/core so transport handlers in @luckystack/api and\r\n//? @luckystack/sync can dispatch per-route + global formatters without\r\n//? depending on this server package (which would cycle). Consumers'\r\n//? existing `import { registerErrorFormatter } from '@luckystack/server'`\r\n//? continues to resolve through this shim.\r\n\r\nexport {\r\n registerErrorFormatter,\r\n getErrorFormatter,\r\n applyErrorFormatter,\r\n} from '@luckystack/core';\r\nexport type { ErrorFormatter, ErrorFormatterContext } from '@luckystack/core';\r\n"],"mappings":"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","names":["getLogger","getProjectConfig","tryCatch","isProduction","resolveEnvKey","dispatchHook","dispatchHook","extractTokenFromRequest","getLogger","getProjectConfig","readSession","tryCatch","getProjectConfig","getProjectConfig","getCsrfConfig","readSession","getCsrfConfig","readSession","getProjectConfig","getProjectConfig","redis","tryCatch","getProjectConfig","tryCatch","redis","dispatchHook","getProjectConfig","resolveEnvKey","getProjectConfig","dispatchHook","checkRateLimit","getProjectConfig","resolveClientIp","getProjectConfig","resolveClientIp","checkRateLimit","result","getLogger","getProjectConfig","tryCatch","tryCatch","getLogger","http","getProjectConfig","getProjectConfig","getProjectConfig","getLogger","getProjectConfig","getProjectConfig","getLogger","dispatchHook","getLogger","tryCatch","getProjectConfig","getProjectConfig","resolveClientIp","tryCatch","getLogger","dispatchHook","captureException","dispatchHook","extractTokenFromRequest","getLogger","tryCatch","tryCatch","extractTokenFromRequest","getLogger","captureException","dispatchHook","captureException","getLogger","tryCatch","tryCatch","getLogger","captureException","http","getProjectConfig","getLogger","handlers","readSession","dispatchHook","extractTokenFromRequest","tryCatch","allowedOrigin","redis","getLogger","getProjectConfig","dispatchHook","readSession","tryCatch","tryCatch","readSession","dispatchHook","getLogger","getProjectConfig","allowedOrigin","redis","getLogger","getProjectConfig","resolveEnvKey","resolveEnvKey","getLogger","getProjectConfig","getLogger","resolveEnvKey","getLogger","resolveEnvKey","dispatchHook","getLogger","tryCatch","path","getLogger","dispatchHook","getLogger","tryCatch","isProduction","getProjectConfig","resolveEnvKey","path","fs","tryCatch","fs","tryCatch","path"]}
|