@luckystack/api 0.8.0 → 0.8.1
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 +6 -0
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.8.1] - 2026-07-27
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Named deployment environments no longer enable development-only loopback rate-limit bypasses when `NODE_ENV=production`.
|
|
15
|
+
|
|
10
16
|
## [0.1.0]
|
|
11
17
|
|
|
12
18
|
### Added
|
package/dist/index.js
CHANGED
|
@@ -715,7 +715,7 @@ import {
|
|
|
715
715
|
applyErrorFormatter as applyErrorFormatter2,
|
|
716
716
|
isLoopbackIp,
|
|
717
717
|
resolveClientIp as resolveClientIp2,
|
|
718
|
-
|
|
718
|
+
isProductionRuntime
|
|
719
719
|
} from "@luckystack/core";
|
|
720
720
|
|
|
721
721
|
// src/_shared/httpValidationStage.ts
|
|
@@ -897,7 +897,7 @@ async function applyHttpApiRateLimits({
|
|
|
897
897
|
rateLimit
|
|
898
898
|
}, ctx) {
|
|
899
899
|
const effectiveIp = requesterIp ?? resolveClientIp2({ rawAddress: void 0, headers: {} });
|
|
900
|
-
const skipGlobalIpForLoopback = getProjectConfig5().rateLimiting.skipLoopbackInDev &&
|
|
900
|
+
const skipGlobalIpForLoopback = getProjectConfig5().rateLimiting.skipLoopbackInDev && !isProductionRuntime() && isLoopbackIp(requesterIp ?? "");
|
|
901
901
|
const rateLimitResult = await applyApiRateLimits({
|
|
902
902
|
resolvedIp: effectiveIp,
|
|
903
903
|
token,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/handleApiRequest.ts","../src/_shared/logFlags.ts","../src/_shared/responseEnvelope.ts","../src/_shared/requestLifecycle.ts","../src/_shared/backpressure.ts","../src/_shared/socketValidationStage.ts","../src/_shared/inputTypeWarning.ts","../src/_shared/applyApiRateLimits.ts","../src/handleHttpApiRequest.ts","../src/_shared/httpValidationStage.ts"],"sourcesContent":["import type { apiMessage, PostApiExecutePayload, BaseSessionLayout as SessionLayout, ErrorFormatter } from '@luckystack/core';\nimport {\n readSession,\n performLogout,\n runWithErrorTrackerIdentityScope,\n setCurrentErrorTrackerIdentity,\n getProjectConfig,\n getRuntimeApiMaps,\n validateRequest,\n checkRateLimit,\n tryCatch,\n parseTransportRouteName,\n buildApiResponseEventName,\n dispatchHook,\n getLogger,\n resolveClientIp,\n extractLanguageFromHeader,\n normalizeErrorResponse,\n applyErrorFormatter,\n} from '@luckystack/core';\n//? checkRateLimit and resolveClientIp are still used directly for the logout\n//? global-IP bucket (which doesn't go through the shared applyApiRateLimits helper\n//? because the logout shortcut bypasses the per-route bucket entirely).\nimport { Socket } from 'socket.io';\nimport type { ApiStreamPayload, ApiFlushPressure, RuntimeApiResponse, RuntimeApiEntry } from './_shared/apiTypes';\nimport { shouldLogDev } from './_shared/logFlags';\nimport { normalizeApiResponse } from './_shared/responseEnvelope';\nimport { createApiRequestLifecycle } from './_shared/requestLifecycle';\nimport { runSocketApiValidation } from './_shared/socketValidationStage';\nimport { applyApiRateLimits } from './_shared/applyApiRateLimits';\n\ninterface handleApiRequestType {\n msg: apiMessage,\n socket: Socket,\n token: string | null,\n}\n\ninterface ApiErrorResponse {\n status: 'error';\n httpStatus?: number;\n errorCode?: string;\n errorParams?: { key: string; value: string | number | boolean; }[];\n}\n\ntype EmitApiError = (args: { response: ApiErrorResponse; fallbackHttpStatus?: number; }) => Promise<void>;\n\nconst validateApiMessage = async ({ msg, emitApiError }: {\n msg: apiMessage;\n emitApiError: EmitApiError;\n}): Promise<{ name: string; data: Record<string, unknown> } | null> => {\n const { name, data, responseIndex } = msg;\n\n //? Drop a message with no usable response channel. Written as a single\n //? `typeof` test (equivalent to the former `!responseIndex && typeof !==\n //? 'number'` double-negative) so a `responseIndex: 0` is correctly KEPT — a\n //? future \"simplify\" to `!responseIndex` would silently drop every index-0\n //? request.\n if (typeof responseIndex !== 'number') {\n if (shouldLogDev()) {\n getLogger().warn('api: no response index given');\n }\n return null;\n }\n\n //? Positive plain-object guard (parity with the sync handler). `!data`\n //? excludes `null`; `Array.isArray` rejects an ARRAY payload that\n //? `typeof === 'object'` would otherwise admit to a handler expecting an\n //? object.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime defense: msg arrives from an untyped socket frame\n if (!name || !data || typeof name != 'string' || typeof data != 'object' || Array.isArray(data)) {\n await emitApiError({\n response: { status: 'error', errorCode: 'api.invalidRequest' },\n fallbackHttpStatus: 400,\n });\n return null;\n }\n\n return { name, data: data as Record<string, unknown> };\n};\n\nconst checkApiAuth = async ({ apiEntry, user, name, emitApiError }: {\n apiEntry: RuntimeApiEntry;\n user: SessionLayout | null;\n name: string;\n emitApiError: EmitApiError;\n}): Promise<boolean> => {\n if (apiEntry.auth.login && !user?.id) {\n if (shouldLogDev()) {\n getLogger().warn(`api: ${name} requires login`, { route: name });\n }\n void dispatchHook('apiAuthRejected', {\n routeName: name,\n reason: 'login-required',\n userId: null,\n transport: 'socket',\n });\n await emitApiError({\n response: { status: 'error', errorCode: 'auth.required' },\n fallbackHttpStatus: 401,\n });\n return false;\n }\n\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n const authResult = validateRequest({ auth: apiEntry.auth, user: user! });\n if (authResult.status === 'error') {\n if (shouldLogDev()) {\n getLogger().warn(`api: auth failed for ${name}`, { route: name, errorCode: authResult.errorCode });\n }\n void dispatchHook('apiAuthRejected', {\n routeName: name,\n reason: authResult.errorCode === 'auth.invalidCondition' ? 'invalid-condition' : 'additional-failed',\n userId: user?.id ?? null,\n transport: 'socket',\n failedKey: authResult.errorCode,\n });\n await emitApiError({\n response: {\n status: 'error',\n // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- empty string errorCode must still fall back\n errorCode: authResult.errorCode || 'auth.forbidden',\n errorParams: authResult.errorParams,\n httpStatus: authResult.httpStatus,\n },\n fallbackHttpStatus: authResult.httpStatus ?? 403,\n });\n return false;\n }\n\n return true;\n};\n\n//? API-O8 — rate-limit logic extracted to `_shared/applyApiRateLimits.ts` so\n//? both transports share one implementation. The socket transport resolves the\n//? effective IP here (once, from socket internals) before delegating to the helper.\nconst runSocketRateLimits = async ({ apiEntry, resolvedName, token, socket, user, emitApiError }: {\n apiEntry: RuntimeApiEntry;\n resolvedName: string;\n token: string | null;\n socket: Socket;\n user: SessionLayout | null;\n emitApiError: EmitApiError;\n}): Promise<boolean> => {\n //? Resolve the real client IP once. With the default `http.trustProxy: false`\n //? this returns `socket.handshake.address` verbatim (only IPv4-mapped IPv6 is\n //? canonicalized). When a trusted proxy is configured it honors XFF / X-Real-IP.\n const resolvedIp = resolveClientIp({\n rawAddress: socket.handshake.address,\n headers: socket.handshake.headers,\n trustProxy: getProjectConfig().http.trustProxy,\n trustedProxyHopCount: getProjectConfig().http.trustedProxyHopCount,\n });\n\n const result = await applyApiRateLimits({\n resolvedIp,\n token,\n user,\n resolvedName,\n rateLimit: apiEntry.rateLimit,\n transport: 'socket',\n });\n\n if (!result.allowed) {\n await emitApiError({\n response: {\n status: 'error',\n errorCode: result.errorCode ?? 'api.rateLimitExceeded',\n errorParams: [{ key: 'seconds', value: result.resetIn ?? 0 }],\n },\n fallbackHttpStatus: 429,\n });\n return false;\n }\n\n return true;\n};\n\nconst executeApiHandler = async ({ apiEntry, normalizedData, user, functionsObject, resolvedName, emitStream, abortSignal, abortController, flushPressure }: {\n apiEntry: RuntimeApiEntry;\n normalizedData: Record<string, unknown>;\n user: SessionLayout | null;\n functionsObject: Record<string, unknown>;\n resolvedName: string;\n emitStream: (payload?: ApiStreamPayload) => void;\n abortSignal: AbortSignal;\n abortController: AbortController;\n flushPressure: ApiFlushPressure;\n}): Promise<{ error: Error | null; result: RuntimeApiResponse | undefined; durationMs: number; timedOut?: boolean }> => {\n //? Span open/close + identity propagation moved to hook subscribers in\n //? `@luckystack/error-tracking` (preApiExecute / postApiExecute). This\n //? handler is now transport-agnostic instrumentation-wise.\n const executeStart = Date.now();\n\n //? API-O10 — race `main()` against a server-side timeout derived from\n //? `api.requestTimeoutMs`. When the timeout fires first we abort the\n //? AbortController (propagates to the handler's `abortSignal`) and resolve\n //? a sentinel so the orchestrator can emit a localized 504 envelope.\n const timeoutMs = getProjectConfig().api.requestTimeoutMs;\n let timeoutHandle: ReturnType<typeof setTimeout> | undefined;\n let timedOut = false;\n const timeoutPromise = timeoutMs === false\n ? null\n : new Promise<void>((resolve) => {\n timeoutHandle = setTimeout(() => {\n timedOut = true;\n abortController.abort();\n resolve();\n }, timeoutMs);\n });\n\n const handlerPromise = tryCatch(\n async () => await apiEntry.main({ data: normalizedData, user, functions: functionsObject, stream: emitStream, abortSignal, flushPressure }),\n undefined,\n {\n handler: 'handleApiRequest',\n api: resolvedName,\n userId: user?.id,\n transport: 'socket',\n },\n );\n\n if (timeoutPromise) {\n await Promise.race([handlerPromise, timeoutPromise]);\n }\n clearTimeout(timeoutHandle);\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- timedOut is mutated by the async timeout callback via Promise.race\n if (timedOut) {\n return { error: null, result: undefined, durationMs: Date.now() - executeStart, timedOut: true };\n }\n\n const [error, result] = await handlerPromise;\n return { error: error ?? null, result: result ?? undefined, durationMs: Date.now() - executeStart };\n};\n\n//? Execution stage: `preApiExecute` hook (stop-signal short-circuit) ->\n//? `executeApiHandler` (tryCatch-wrapped) -> `postApiExecute` hook. Extracted\n//? from the orchestrator per the `api` package audit. Returns either a stop\n//? signal (the orchestrator builds the localized error envelope from it) or the\n//? `(error, result)` pair to feed `emitApiResult`. The shared `executePayload`\n//? reference is created here and threaded through both hooks so the\n//? error-tracking auto-instrumentation can pin spans via WeakMap on it.\ntype SocketApiExecutionOutcome =\n | { stopped: true; errorCode: string; httpStatus?: number }\n | { stopped: false; error: Error | null; result: RuntimeApiResponse | undefined };\n\nconst runSocketApiExecution = async ({\n apiEntry,\n resolvedName,\n normalizedData,\n user,\n functionsObject,\n emitStream,\n abortSignal,\n abortController,\n flushPressure,\n}: {\n apiEntry: RuntimeApiEntry;\n resolvedName: string;\n normalizedData: Record<string, unknown>;\n user: SessionLayout | null;\n functionsObject: Record<string, unknown>;\n emitStream: (payload?: ApiStreamPayload) => void;\n abortSignal: AbortSignal;\n abortController: AbortController;\n flushPressure: ApiFlushPressure;\n}): Promise<SocketApiExecutionOutcome> => {\n //? Single payload reference reused by pre/post — auto-instrumentation\n //? subscribers in `@luckystack/error-tracking` pin spans via WeakMap on\n //? this object. Mutating `result/error/durationMs` is safe because\n //? handlers observe by-name (not by snapshot).\n const executePayload: PostApiExecutePayload = {\n routeName: resolvedName,\n data: normalizedData,\n user,\n transport: 'socket',\n result: undefined,\n error: null,\n durationMs: 0,\n };\n const preExecuteResult = await dispatchHook('preApiExecute', executePayload);\n if (preExecuteResult.stopped) {\n return { stopped: true, errorCode: preExecuteResult.signal.errorCode, httpStatus: preExecuteResult.signal.httpStatus };\n }\n\n const { error, result, durationMs, timedOut } = await executeApiHandler({\n apiEntry,\n normalizedData,\n user,\n functionsObject,\n resolvedName,\n emitStream,\n abortSignal,\n abortController,\n flushPressure,\n });\n\n //? API-O10 — a server-side timeout aborted the handler. Skip postApiExecute\n //? (no meaningful result to report) and surface a 504 to the client.\n if (timedOut) {\n return { stopped: true, errorCode: 'api.timeout', httpStatus: 504 };\n }\n\n executePayload.result = result;\n executePayload.error = error ?? null;\n executePayload.durationMs = durationMs;\n await dispatchHook('postApiExecute', executePayload);\n\n return { stopped: false, error, result };\n};\n\nconst emitApiResult = async ({\n socket,\n responseIndex,\n resolvedName,\n error,\n result,\n preferredLocale,\n user,\n perRouteFormatter,\n}: {\n socket: Socket;\n responseIndex: number;\n resolvedName: string;\n error: Error | null;\n result: RuntimeApiResponse | undefined;\n preferredLocale: string | undefined;\n user: SessionLayout | null;\n perRouteFormatter?: ErrorFormatter;\n}): Promise<void> => {\n const envelope = normalizeApiResponse({ resolvedName, error, result, preferredLocale, user });\n\n //? `preApiRespond` handlers may mutate `payload.response` to swap or rewrite\n //? the outgoing envelope (PII redaction, response signing, schema injection).\n //? A handler may also return a stop signal — when that happens we build a\n //? fresh error envelope from the signal's `errorCode` / `httpStatus` and\n //? emit that instead of the original.\n const preRespond = { routeName: resolvedName, user, response: envelope };\n const preRespondResult = await dispatchHook('preApiRespond', preRespond);\n\n const finalResponse = preRespondResult.stopped\n ? { ...normalizeErrorResponse({\n response: { status: 'error', errorCode: preRespondResult.signal.errorCode },\n preferredLocale,\n userLanguage: user?.language,\n fallbackHttpStatus: preRespondResult.signal.httpStatus ?? 403,\n }) }\n : preRespond.response;\n\n //? `transformApiResponse` fires AFTER preApiRespond and BEFORE socket emit.\n //? Designed for response mutation — header injection, body transformation,\n //? response signing — that's awkward inside preApiRespond. Handlers mutate\n //? `payload.response` in place (same shape as preApiRespond.response).\n const transformPayload = { routeName: resolvedName, user, response: finalResponse };\n await dispatchHook('transformApiResponse', transformPayload);\n\n //? Per-route → global → identity error formatter chain. No-op when status\n //? is success. Runs AFTER transformApiResponse so consumer hooks can still\n //? mutate the envelope before the final shape lands.\n const formattedResponse = applyErrorFormatter({\n response: transformPayload.response,\n routeName: resolvedName,\n transport: 'socket',\n userId: user?.id,\n perRouteFormatter,\n });\n\n socket.emit(buildApiResponseEventName(responseIndex), formattedResponse);\n\n //? `postApiRespond` is observation-only — the response is already on the\n //? wire. Use it for audit logging, metrics, dual-write replication.\n await dispatchHook('postApiRespond', { routeName: resolvedName, user, response: formattedResponse });\n};\n\n//? ET-02 — open a per-request error-tracker identity scope around the ENTIRE\n//? handler before any await that could interleave with another concurrent\n//? request. `readSession` (the first await below) is itself such a boundary, so\n//? the scope must wrap it. The session is written into the scope via\n//? `setCurrentErrorTrackerIdentity(...)` the moment it resolves; from then on\n//? every error-tracker capture during this request (handler throw, hook\n//? subscriber, fanout) reads THIS request's identity from the AsyncLocalStorage\n//? box, never another concurrent request's. Each request gets its own box.\nexport default async function handleApiRequest(args: handleApiRequestType): Promise<void> {\n //? handleApiRequestInner runs un-.catch()'d from loadSocket (a bare `void`\n //? call), so ANY throw from an unguarded dependency (readSession, the rate-limit\n //? Redis calls, getRuntimeApiMaps, the session store) would surface as a fatal\n //? unhandledRejection → worker crash on modern Node. Wrap the whole run (mirrors\n //? the sync handler's top-level tryCatch) and emit a generic error envelope on\n //? the response channel so the request fails cleanly instead of killing the worker.\n const [error] = await tryCatch(() => runWithErrorTrackerIdentityScope(() => handleApiRequestInner(args)));\n if (error) {\n getLogger().error('api: unhandled error in socket request handler', { error: error.message });\n const msg = args.msg as apiMessage | null;\n const responseIndex = msg?.responseIndex;\n if (typeof responseIndex === 'number') {\n args.socket.emit(buildApiResponseEventName(responseIndex), {\n status: 'error',\n errorCode: 'api.serverExecutionFailed',\n httpStatus: 500,\n });\n }\n }\n}\n\nasync function handleApiRequestInner({ msg, socket, token }: handleApiRequestType) {\n //? This event gets triggered when the client uses the apiRequest function.\n //? Validate the message, check auth, then execute the registered handler.\n\n //? `msg` is TYPED as apiMessage but the socket transport can deliver ANYTHING at\n //? runtime (null / array / primitive), so view it as `unknown` for the guard.\n //? `typeof null === 'object'` and arrays are objects, so the bare object check\n //? would let null/array through to `const { responseIndex } = msg` below —\n //? throwing before any handler. That throw, running un-.catch()'d from loadSocket,\n //? becomes a fatal unhandledRejection → worker crash; a connected socket could\n //? trigger it pre-auth via `socket.emit('api', null)` (remote DoS). Mirrors the\n //? sync handler's validateSyncMessage guard.\n const rawMsg: unknown = msg;\n if (typeof rawMsg !== 'object' || rawMsg === null || Array.isArray(rawMsg)) {\n if (shouldLogDev()) {\n getLogger().warn('api: socket message was not a json object');\n }\n return;\n }\n\n const { responseIndex } = msg;\n\n //? EXT-02 — per-message socket interception seam (counterpart to preHttpRequest\n //? in the HTTP pipeline and preSocketMessage in the sync handler). Fires before\n //? session lookup / route resolution / auth so a consumer can gate, throttle, or\n //? audit individual api messages. A stop signal rejects the message.\n const preMsgResult = await dispatchHook('preSocketMessage', {\n channel: 'api',\n socketId: socket.id,\n ip: socket.handshake.address,\n authenticated: Boolean(token),\n routeName: typeof msg.name === 'string' ? msg.name : undefined,\n });\n if (preMsgResult.stopped) {\n //? Guard the emit on a numeric responseIndex (mirrors the sync handler +\n //? validateApiMessage's own no-index branch below): this stop path runs\n //? BEFORE validateApiMessage, so a malformed frame lacking responseIndex\n //? would otherwise emit to the dead 'apiResponse-undefined' channel and\n //? leave the client hanging until its own ack timeout. Drop it silently.\n if (typeof responseIndex === 'number') {\n socket.emit(buildApiResponseEventName(responseIndex), {\n status: 'error',\n errorCode: preMsgResult.signal.errorCode,\n httpStatus: preMsgResult.signal.httpStatus ?? 403,\n });\n }\n return;\n }\n\n const user = await readSession(token);\n //? ET-02 — bind the resolved session into the active per-request ALS identity\n //? box so every subsequent capture attributes to this user. Identity also still\n //? flows to the legacy global via the `preApiValidate` hook subscriber in\n //? `@luckystack/error-tracking` (the ALS read takes precedence at capture time).\n setCurrentErrorTrackerIdentity(user?.id ? { id: user.id, email: user.email ?? undefined, username: user.name ?? undefined } : null);\n const preferredLocale =\n extractLanguageFromHeader(socket.handshake.headers['x-language'])\n ?? extractLanguageFromHeader(socket.handshake.headers['accept-language'])\n ?? undefined;\n\n //? Per-route formatter ref + resolved-name ref. Both start undefined and\n //? get set once the route is parsed + the apiEntry looked up — pre-lookup\n //? errors (invalid message shape, unknown route) emit with global formatter\n //? only because there's no apiEntry to read per-route formatter from. After\n //? lookup, every emit through `emitApiError` flows through the same chain.\n // eslint-disable-next-line prefer-const -- captured by emitApiError closure; assigned later after route resolution\n let currentRouteName: string | undefined;\n // eslint-disable-next-line prefer-const -- captured by emitApiError closure; assigned later after apiEntry lookup\n let currentPerRouteFormatter: ErrorFormatter | undefined;\n\n //? API-O6 parity — socket ERROR responses now run the SAME respond-hook chain\n //? as success responses (`emitApiResult`) and as the HTTP transport: preApiRespond\n //? (may rewrite/stop) → transformApiResponse (mutate) → error formatter → emit →\n //? postApiRespond (observe). Previously `emitApiError` emitted directly, so\n //? consumer respond hooks (PII redaction, response signing, audit logging) never\n //? fired on auth / rate-limit / validation / not-found rejections over WebSocket —\n //? a parity gap with HTTP, where every response (success AND error) runs them.\n const emitApiError: EmitApiError = async ({ response, fallbackHttpStatus }) => {\n const normalized = normalizeErrorResponse({\n response,\n preferredLocale,\n userLanguage: user?.language,\n fallbackHttpStatus,\n });\n const routeNameForHook = currentRouteName ?? 'api/unknown';\n\n //? preApiRespond may mutate the envelope or stop. A stop on an already-error\n //? response rebuilds a fresh error envelope from the signal (parity with\n //? `emitApiResult` + the HTTP path).\n // luckystack-allow no-as-any: hook payload boundary — normalizeErrorResponse returns a narrower type than the preApiRespond payload shape; fix requires @luckystack/core type alignment\n // eslint-disable-next-line no-restricted-syntax -- hook payload boundary, mirrors handleHttpApiRequest\n const preRespond = { routeName: routeNameForHook, user, response: normalized as unknown as { status: 'success' | 'error'; httpStatus?: number; [key: string]: unknown } };\n const preRespondResult = await dispatchHook('preApiRespond', preRespond);\n\n let finalResponse = preRespond.response;\n if (preRespondResult.stopped) {\n const stoppedEnvelope = normalizeErrorResponse({\n response: { status: 'error', errorCode: preRespondResult.signal.errorCode },\n preferredLocale,\n userLanguage: user?.language,\n fallbackHttpStatus: preRespondResult.signal.httpStatus ?? 403,\n });\n // luckystack-allow no-as-any: hook payload boundary — normalizeErrorResponse is narrower than the hook payload shape; fix requires @luckystack/core type alignment\n // eslint-disable-next-line no-restricted-syntax -- hook payload boundary, mirrors handleHttpApiRequest\n finalResponse = stoppedEnvelope as unknown as typeof preRespond.response;\n }\n\n const transformPayload = { routeName: routeNameForHook, user, response: finalResponse };\n await dispatchHook('transformApiResponse', transformPayload);\n\n const formatted = applyErrorFormatter({\n response: transformPayload.response,\n routeName: routeNameForHook,\n transport: 'socket',\n userId: user?.id,\n perRouteFormatter: currentPerRouteFormatter,\n });\n socket.emit(buildApiResponseEventName(responseIndex), formatted);\n\n //? postApiRespond is observation-only — the response is already on the wire.\n await dispatchHook('postApiRespond', { routeName: routeNameForHook, user, response: formatted });\n };\n\n const validated = await validateApiMessage({ msg, emitApiError });\n if (!validated) return;\n const { name, data: normalizedData } = validated;\n\n const parsedRoute = parseTransportRouteName({ value: name, prefix: 'api' });\n if (parsedRoute.status === 'error') {\n await emitApiError({\n response: {\n status: 'error',\n errorCode: 'routing.invalidServiceRouteName',\n errorParams: [{ key: 'name', value: name }],\n },\n fallbackHttpStatus: 400,\n });\n return;\n }\n\n const resolvedName = parsedRoute.normalizedFullName;\n currentRouteName = resolvedName;\n\n //? Built-in 'system/logout' needs special handling since it requires socket access.\n //? Match the full normalized route to avoid hijacking consumer routes whose final\n //? segment happens to be 'logout' (e.g. 'admin/logout/vN').\n if (parsedRoute.serviceRoute.normalizedRouteName === 'system/logout') {\n //? Apply the global per-IP bucket before logout so the shortcut can't be\n //? spammed uncapped — it bypasses applyApiRateLimits entirely.\n const logoutIpLimit = getProjectConfig().rateLimiting.defaultIpLimit;\n if (logoutIpLimit !== false && logoutIpLimit > 0) {\n const logoutIp = resolveClientIp({\n rawAddress: socket.handshake.address,\n headers: socket.handshake.headers,\n trustProxy: getProjectConfig().http.trustProxy,\n trustedProxyHopCount: getProjectConfig().http.trustedProxyHopCount,\n });\n const { allowed, resetIn } = await checkRateLimit({\n key: `ip:${logoutIp}:api:all`,\n limit: logoutIpLimit,\n windowMs: getProjectConfig().rateLimiting.windowMs,\n });\n if (!allowed) {\n void dispatchHook('rateLimitExceeded', {\n scope: 'ip',\n key: `ip:${logoutIp}:api:all`,\n limit: logoutIpLimit,\n windowMs: getProjectConfig().rateLimiting.windowMs,\n count: logoutIpLimit + 1,\n ip: logoutIp,\n });\n await emitApiError({\n response: {\n status: 'error',\n errorCode: 'api.rateLimitExceeded',\n errorParams: [{ key: 'seconds', value: resetIn }],\n },\n fallbackHttpStatus: 429,\n });\n return;\n }\n }\n //? API-O6 — route through `emitApiResult` so the respond-hook chain\n //? (preApiRespond, transformApiResponse, postApiRespond) fires for logout,\n //? matching all other routes. The return value of `performLogout` is void\n //? (success is implied by non-throw), so we pass a fixed success result.\n await performLogout({ token, socket, userId: user?.id ?? null });\n await emitApiResult({\n socket,\n responseIndex,\n resolvedName,\n error: null,\n result: { status: 'success', httpStatus: 200, result: true },\n preferredLocale,\n user,\n });\n return;\n }\n\n if (shouldLogDev()) {\n getLogger().debug(`api: ${resolvedName} called`);\n }\n\n const { apisObject, functionsObject } = await getRuntimeApiMaps();\n\n if (!apisObject[resolvedName]) {\n await emitApiError({\n response: {\n status: 'error',\n errorCode: 'api.notFound',\n errorParams: [{ key: 'name', value: name }],\n },\n fallbackHttpStatus: 404,\n });\n return;\n }\n\n const apiEntry = apisObject[resolvedName] as RuntimeApiEntry;\n currentPerRouteFormatter = apiEntry.errorFormatter;\n\n //? API-O4 — `httpMethod` (and the `inferHttpMethod` heuristic) is HTTP-ONLY.\n //? The socket transport does NOT enforce the declared HTTP method — all routes\n //? are callable over WebSocket regardless of method declaration. This is by\n //? design: socket.io has no HTTP-method concept and enforcing it would break\n //? existing consumers. If you need to restrict a route to HTTP only, do not\n //? expose it as a socket route, or gate it in `preSocketMessage`.\n\n //? Per-request lifecycle bundle (B1 abortController + cleanup, B2\n //? backpressure, abort-aware emitStream). Extracted to `_shared/\n //? requestLifecycle.ts`; the closures stay intact and the orchestrator\n //? threads the returned handles through the pipeline. `cleanupRequest`\n //? runs in every exit path below.\n const { abortController, abortSignal, emitStream, flushPressure, cleanupRequest } = createApiRequestLifecycle({\n socket,\n responseIndex,\n resolvedName,\n });\n\n //? Auth → rate-limit → validate → execute → respond.\n //? Auth runs before validate so unauthenticated probes can't enumerate\n //? routes or learn input shape from `inputValidation.message`.\n //? API-O7 — failed-auth requests do NOT consume a rate-limit bucket here.\n //? This is an explicit design choice: brute-force lockout is the\n //? responsibility of `@luckystack/login` (per-account lockout counter via\n //? `authLockout.ts`), and the `apiAuthRejected` hook lets consumers add\n //? additional throttles. Consuming the global-IP bucket on auth-fail would\n //? allow an attacker to trigger DoS for a victim account's IP by deliberately\n //? sending bad credentials.\n if (!(await checkApiAuth({ apiEntry, user, name, emitApiError }))) { cleanupRequest(); return; }\n\n const rateLimitOk = await runSocketRateLimits({ apiEntry, resolvedName, token, socket, user, emitApiError });\n if (!rateLimitOk) { cleanupRequest(); return; }\n\n //? Validation stage (mode resolve + preApiValidate -> validateInputByType ->\n //? postApiValidate). On failure it runs cleanup + emits the GENERIC\n //? `api.invalidInputType` (raw validator message never reaches the client)\n //? via the closures below, then returns false.\n const validationOk = await runSocketApiValidation({\n apiEntry,\n resolvedName,\n normalizedData,\n user,\n cleanupRequest,\n emitInvalidInputType: async () => { await emitApiError({\n response: { status: 'error', errorCode: 'api.invalidInputType' },\n fallbackHttpStatus: 400,\n }); },\n });\n if (!validationOk) return;\n\n //? Execution stage: preApiExecute (stop-signal short-circuit) -> handler ->\n //? postApiExecute. Returns either a stop signal or the (error, result) pair.\n const execution = await runSocketApiExecution({\n apiEntry,\n resolvedName,\n normalizedData,\n user,\n functionsObject,\n emitStream,\n abortSignal,\n abortController,\n flushPressure,\n });\n if (execution.stopped) {\n cleanupRequest();\n await emitApiError({\n response: { status: 'error', errorCode: execution.errorCode },\n fallbackHttpStatus: execution.httpStatus ?? 403,\n });\n return;\n }\n\n await emitApiResult({ socket, responseIndex, resolvedName, error: execution.error, result: execution.result, preferredLocale, user, perRouteFormatter: apiEntry.errorFormatter });\n cleanupRequest();\n}\n","import { getProjectConfig } from '@luckystack/core';\n\n//? CC-5 — config-getter convenience lambdas shared by both API transports.\n//? Read at call time (never at module load) so config registration order\n//? and hot-reload both stay correct.\n\n/** Gate for verbose per-request debug/warn/error logging. */\nexport const shouldLogDev = (): boolean => getProjectConfig().logging.devLogs;\n\n/** Gate for `emitStream` payload logging. */\nexport const shouldLogStream = (): boolean => getProjectConfig().logging.stream;\n","import type { BaseSessionLayout as SessionLayout } from '@luckystack/core';\nimport { defaultHttpStatusForResponse, normalizeErrorResponse, getLogger } from '@luckystack/core';\nimport { shouldLogDev } from './logFlags';\nimport type { ApiResponseEnvelope, RuntimeApiResponse } from './apiTypes';\n\n//? CC-6 — shared response-envelope assembly used by both API transports.\n//? Turns the `(error, result)` pair from the handler into the final wire\n//? envelope: localized error shapes (via `normalizeErrorResponse`) for\n//? failures/empty/invalid responses, or a success envelope with an inferred\n//? default HTTP status. Previously duplicated between `handleApiRequest`'s\n//? `buildApiResponseEnvelope` and `handleHttpApiRequest`'s inline assembly.\n\nexport const normalizeApiResponse = ({\n resolvedName,\n error,\n result,\n preferredLocale,\n user,\n}: {\n resolvedName: string;\n error: Error | null;\n result: RuntimeApiResponse | undefined | null;\n preferredLocale: string | null | undefined;\n user: SessionLayout | null;\n}): ApiResponseEnvelope => {\n if (error) {\n if (shouldLogDev()) {\n getLogger().error(`api: error in ${resolvedName}`, error, { route: resolvedName });\n }\n return {\n ...normalizeErrorResponse({\n response: { status: 'error', errorCode: 'api.internalServerError' },\n preferredLocale,\n userLanguage: user?.language,\n fallbackHttpStatus: 500,\n }),\n };\n }\n\n if (result === undefined || result === null) {\n if (shouldLogDev()) {\n getLogger().warn(`api: ${resolvedName} returned nothing`);\n }\n return {\n ...normalizeErrorResponse({\n response: { status: 'error', errorCode: 'api.emptyResponse' },\n preferredLocale,\n userLanguage: user?.language,\n fallbackHttpStatus: 500,\n }),\n };\n }\n\n if (shouldLogDev()) {\n getLogger().debug(`api: ${resolvedName} completed`);\n }\n\n //? Runtime guard: a handler is user code and can return a status outside the\n //? typed 'success' | 'error' union despite the RuntimeApiResponse type.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- defensive against malformed handler return at runtime\n if (result.status !== 'success' && result.status !== 'error') {\n return {\n ...normalizeErrorResponse({\n response: { status: 'error', errorCode: 'api.invalidResponseStatus' },\n preferredLocale,\n userLanguage: user?.language,\n fallbackHttpStatus: 500,\n }),\n };\n }\n\n if (result.status === 'error') {\n return {\n ...normalizeErrorResponse({\n response: result,\n preferredLocale,\n userLanguage: user?.language,\n fallbackHttpStatus: defaultHttpStatusForResponse({\n status: 'error',\n explicitHttpStatus: typeof result.httpStatus === 'number' ? result.httpStatus : undefined,\n }),\n }),\n };\n }\n\n //? API-O16 — the success envelope spreads ALL handler result fields directly\n //? onto the wire envelope. Handlers are responsible for not including sensitive\n //? data (e.g. `passwordHash`, raw session tokens) in their return value. There\n //? is no framework-level field allowlist; if your route aggregates a Prisma row\n //? with sensitive columns, pick explicit fields before returning.\n return {\n ...result,\n status: 'success',\n httpStatus: defaultHttpStatusForResponse({\n status: 'success',\n explicitHttpStatus: typeof result.httpStatus === 'number' ? result.httpStatus : undefined,\n }),\n };\n};\n","import type { Socket } from 'socket.io';\nimport {\n buildApiStreamEventName,\n getLogger,\n registerApiAbortController,\n unregisterApiAbortController,\n socketEventNames,\n} from '@luckystack/core';\nimport type { ApiStreamPayload, ApiFlushPressure } from './apiTypes';\nimport { shouldLogStream } from './logFlags';\nimport { createApiFlushPressure } from './backpressure';\n\n//? Per-request lifecycle bundle for the SOCKET API transport. Extracted from\n//? `handleApiRequest` per the `api` package audit (god-function decomposition).\n//? Keeps the abortController / cleanup / emitStream / flushPressure closures\n//? together and intact — the orchestrator wires them once and threads the\n//? returned handles through the pipeline. Behaviour is identical to the former\n//? inline closures:\n//? • B1 — per-request AbortController. Aborts on `apiCancel { responseIndex }`\n//? from the originator OR socket disconnect. `cleanupRequest()` runs in\n//? every exit path (errors, validation rejects, completion) to remove the\n//? disconnect listener and drop the cancel-registry entry. The signal is\n//? also handed to `emitStream` so chunks queued after an abort never hit\n//? the wire.\n//? • B2 — backpressure helper bound to the originator socket.\n\nexport interface ApiRequestLifecycle {\n abortController: AbortController;\n abortSignal: AbortSignal;\n emitStream: (payload?: ApiStreamPayload) => void;\n flushPressure: ApiFlushPressure;\n cleanupRequest: () => void;\n}\n\nexport const createApiRequestLifecycle = ({ socket, responseIndex, resolvedName }: {\n socket: Socket;\n responseIndex: number;\n resolvedName: string;\n}): ApiRequestLifecycle => {\n const abortController = new AbortController();\n const abortKey = registerApiAbortController(socket.id, responseIndex, abortController);\n const onSocketDisconnect = () => { abortController.abort(); };\n socket.once(socketEventNames.disconnect, onSocketDisconnect);\n\n let cleanupDone = false;\n const cleanupRequest = () => {\n if (cleanupDone) return;\n cleanupDone = true;\n socket.off(socketEventNames.disconnect, onSocketDisconnect);\n unregisterApiAbortController(abortKey);\n };\n\n const emitStream = (payload: ApiStreamPayload = {}) => {\n if (abortController.signal.aborted) {\n if (shouldLogStream()) {\n getLogger().debug(`api: ${resolvedName} stream skipped — request aborted`);\n }\n return;\n }\n if (shouldLogStream()) {\n getLogger().debug(`api: ${resolvedName} stream`, { payload });\n }\n socket.emit(buildApiStreamEventName(responseIndex), payload);\n };\n\n //? B2 — backpressure helper for API handlers. Same shape as the sync variant\n //? but always scoped to a single originator socket. Polls the engine.io\n //? writeBuffer length every 10ms until the buffer drains below threshold\n //? (default 1 MB ≈ 1024 packets at ~1KB each).\n const flushPressure = createApiFlushPressure(socket, abortController.signal);\n\n return { abortController, abortSignal: abortController.signal, emitStream, flushPressure, cleanupRequest };\n};\n","import type { Socket } from 'socket.io';\nimport { getLogger } from '@luckystack/core';\nimport type { ApiFlushPressure, ApiFlushPressureOptions } from './apiTypes';\nimport { shouldLogDev } from './logFlags';\n\n//? B2 — backpressure helper factory for the SOCKET API transport. Same shape\n//? as the sync variant; resolves once the originator socket's pending write\n//? buffer drops below the threshold. Default 1 MB (≈1024 packets at ~1KB each).\n//? Extracted from `handleApiRequest` per the `api` package audit (solid-isp).\n\ninterface EngineIoConnLike {\n writeBuffer?: { length: number };\n transport?: { writable?: boolean };\n}\n\nexport const createApiFlushPressure = (socket: Socket, abortSignal: AbortSignal): ApiFlushPressure => {\n return async ({ thresholdBytes }: ApiFlushPressureOptions = {}): Promise<void> => {\n const effectiveThresholdBytes = typeof thresholdBytes === 'number' && thresholdBytes > 0\n ? thresholdBytes\n : 1_048_576;\n const packetThreshold = Math.max(1, Math.ceil(effectiveThresholdBytes / 1024));\n //? Polls until the originator's socket write-buffer drains below the packet\n //? threshold, or the request aborts (loop condition), or the transport goes\n //? unwritable. Same exit conditions as the inline socket-handler original.\n while (!abortSignal.aborted) {\n //? Reading engine.io internals (writeBuffer / transport.writable) is the\n //? only way to measure socket backpressure; the cast is the documented\n //? boundary to those untyped internals. Moved verbatim from the socket\n //? handler — same access pattern, no behavioral change.\n // luckystack-allow no-as-unknown: engine.io internal — `conn` is not typed on Socket; this is the documented access pattern\n // eslint-disable-next-line no-restricted-syntax -- engine.io internals boundary\n const conn = (socket as unknown as { conn?: EngineIoConnLike }).conn;\n //? API-O15 — if engine.io renames `conn` this silently becomes a no-op\n //? (buffer always 0, writable always true). Warn in dev so a rename is\n //? caught before backpressure is broken in production.\n if (shouldLogDev() && !conn) {\n getLogger().warn('api: socket.conn absent — engine.io internal may have been renamed; backpressure is a no-op');\n }\n const packets = conn?.writeBuffer?.length ?? 0;\n const writable = conn?.transport?.writable ?? true;\n if (!writable) return;\n if (packets < packetThreshold) return;\n await new Promise<void>((resolve) => setTimeout(resolve, 10));\n }\n };\n};\n\n//? HTTP/SSE has no socket write-buffer to measure — `flushPressure` is a no-op\n//? on that transport. Shared at module scope so per-request allocation stays\n//? cheap and the handler param shape stays consistent across transports.\nexport const httpApiFlushPressureNoop: ApiFlushPressure = async () => {\n /* SSE backpressure is the caller's responsibility (res.write returns bool) */\n};\n","import type { BaseSessionLayout as SessionLayout } from '@luckystack/core';\nimport { dispatchHook, validateInputByType, getLogger } from '@luckystack/core';\nimport type { RuntimeApiEntry } from './apiTypes';\nimport { shouldLogDev } from './logFlags';\nimport { warnIfInputTypeMissing } from './inputTypeWarning';\n\n//? SOCKET-transport input-validation stage. Extracted from `handleApiRequest`\n//? per the `api` package audit (god-function decomposition). Behaviour is\n//? identical to the former inline block:\n//? • resolves the per-route validation mode (`'relaxed'` / `{ input: 'skip' }`\n//? => skip Zod validation; default `'strict'`);\n//? • dispatches `preApiValidate`, runs `validateInputByType` in strict mode,\n//? then `postApiValidate` (always, including the relaxed skip path);\n//? • on validation failure emits the GENERIC `api.invalidInputType` (the raw\n//? validator message is NEVER echoed to the client — it only travels to the\n//? postApiValidate hook + dev logs) and returns `false`.\n//? Error emission + per-request cleanup are delegated to the caller's closures\n//? so the orchestrator keeps ownership of the abort/cleanup/emitError refs.\n\ntype EmitInvalidInputType = () => void | Promise<void>;\n\nexport const resolveValidationMode = (validation: RuntimeApiEntry['validation']): 'strict' | 'relaxed' => {\n if (!validation) return 'strict';\n //? FAIL CLOSED on an unrecognized value (parity with the sync transport's\n //? `resolveSyncValidationMode`). Only the exact string `'relaxed'` (or the\n //? object form `{ input: 'skip' }`) skips validation; ANY other value — a\n //? typo like `'Strict'`/`'on'` — falls back to `'strict'` (validate) rather\n //? than being returned verbatim and silently DISABLING validation.\n if (typeof validation === 'string') {\n return validation === 'relaxed' ? 'relaxed' : 'strict';\n }\n return validation.input === 'skip' ? 'relaxed' : 'strict';\n};\n\nexport const runSocketApiValidation = async ({\n apiEntry,\n resolvedName,\n normalizedData,\n user,\n cleanupRequest,\n emitInvalidInputType,\n}: {\n apiEntry: RuntimeApiEntry;\n resolvedName: string;\n normalizedData: Record<string, unknown>;\n user: SessionLayout | null;\n cleanupRequest: () => void;\n emitInvalidInputType: EmitInvalidInputType;\n}): Promise<boolean> => {\n //? Per-route validation toggle. `'relaxed'` or `{ input: 'skip' }` skips\n //? runtime input validation entirely — useful for public webhooks (Stripe,\n //? Slack, GitHub) where the third party's payload shape isn't reasonable\n //? to model in TypeScript. Default `'strict'`.\n const validationMode = resolveValidationMode(apiEntry.validation);\n\n if (validationMode === 'strict') {\n warnIfInputTypeMissing(resolvedName, apiEntry.inputType);\n }\n await dispatchHook('preApiValidate', { routeName: resolvedName, data: normalizedData, user, transport: 'socket' });\n\n if (validationMode === 'strict') {\n const inputValidation = await validateInputByType({\n typeText: apiEntry.inputType,\n value: normalizedData,\n rootKey: 'data',\n filePath: apiEntry.inputTypeFilePath,\n });\n\n await dispatchHook('postApiValidate', {\n routeName: resolvedName,\n data: normalizedData,\n user,\n validation: inputValidation,\n transport: 'socket',\n });\n\n if (inputValidation.status === 'error') {\n //? SECURITY: do NOT echo the raw validator message (e.g.\n //? \"data.userId should be string\") back to the client — that lets an\n //? unauthenticated caller enumerate a route's input schema. The DETAILED\n //? message is routed to the `postApiValidate` hook above (and dev logs);\n //? the client only receives the generic `api.invalidInputType` code.\n if (shouldLogDev()) {\n getLogger().warn(`api: input validation failed for ${resolvedName}`, { route: resolvedName, message: inputValidation.message });\n }\n cleanupRequest();\n await emitInvalidInputType();\n return false;\n }\n } else {\n //? Relaxed: surface the skip via postApiValidate so audit handlers see it.\n await dispatchHook('postApiValidate', {\n routeName: resolvedName,\n data: normalizedData,\n user,\n validation: { status: 'success' },\n transport: 'socket',\n });\n }\n\n return true;\n};\n","import { getProjectConfig, getLogger } from '@luckystack/core';\n\n//? Dev-only one-shot warning when a route has no generated `inputType`\n//? (runtime Zod input validation is effectively disabled for it). Shared by\n//? both API transports so the \"warned once per route\" set is process-wide\n//? regardless of which transport hit the route first.\n\nconst warnedMissingInputType = new Set<string>();\n\nexport const warnIfInputTypeMissing = (resolvedName: string, inputType: string | undefined): void => {\n if (!getProjectConfig().dev.warnOnMissingInputType) return;\n if (inputType && inputType.trim().length > 0 && inputType.trim() !== 'any') return;\n if (warnedMissingInputType.has(resolvedName)) return;\n warnedMissingInputType.add(resolvedName);\n getLogger().warn(`api: route ${resolvedName} has no inputType — runtime input validation is disabled. Regenerate types or set the inputType on the handler.`);\n};\n","import type { BaseSessionLayout as SessionLayout } from '@luckystack/core';\nimport { checkRateLimit, getProjectConfig, dispatchHook, getLogger } from '@luckystack/core';\nimport { shouldLogDev } from './logFlags';\n\n//? API-O8 — shared rate-limit apply helper for BOTH API transports (socket +\n//? HTTP). Extracted so the two-bucket logic (per-route + global IP) stays in\n//? one place; each transport resolves the effective IP before calling here.\n\nexport interface ApplyApiRateLimitsArgs {\n /** Effective client IP (already resolved by the caller's transport adapter). */\n resolvedIp: string;\n token: string | null;\n user: SessionLayout | null;\n resolvedName: string;\n rateLimit: number | false | undefined;\n transport: 'socket' | 'http';\n /**\n * When true, skip the global-IP (`ip:…:api:all`) bucket check. Used by the\n * HTTP transport to honour `rateLimiting.skipLoopbackInDev` for loopback\n * callers without duplicating bucket logic here. The socket transport never\n * sets this (loopback skip is HTTP-only).\n */\n skipGlobalIpBucket?: boolean;\n}\n\nexport interface ApplyApiRateLimitsResult {\n /** True when all buckets allow the request; false when any bucket exceeded. */\n allowed: boolean;\n /** The error payload to emit when `allowed` is false. */\n errorCode?: string;\n resetIn?: number;\n}\n\n/**\n * Checks per-route + global-IP rate-limit buckets for an API request.\n * Returns `{ allowed: true }` when all buckets allow through, or\n * `{ allowed: false, errorCode, resetIn }` when any bucket is full.\n */\nexport const applyApiRateLimits = async ({\n resolvedIp,\n user,\n resolvedName,\n rateLimit,\n transport,\n skipGlobalIpBucket = false,\n}: ApplyApiRateLimitsArgs): Promise<ApplyApiRateLimitsResult> => {\n const config = getProjectConfig();\n const effectiveApiLimit = rateLimit ?? config.rateLimiting.defaultApiLimit;\n\n if (effectiveApiLimit !== false && effectiveApiLimit > 0) {\n const identityCb = config.rateLimiting.identity;\n const customIdentity = identityCb?.({ routeName: resolvedName, userId: user?.id ?? null, ip: resolvedIp, transport }) ?? null;\n //? Per-route bucket keyed on the VALIDATED user.id (never the token): a token\n //? is per-session, so token-keying let an authenticated abuser reset their\n //? bucket by re-logging-in (new token = fresh counter). user.id ties all of a\n //? user's sessions to one bucket; anonymous callers fall back to the IP.\n const requesterIdentity = customIdentity?.id ?? user?.id ?? resolvedIp;\n const keyPrefix = customIdentity?.scope ?? (user?.id ? 'user' : 'ip');\n const rateLimitKey = `${keyPrefix}:${requesterIdentity}:api:${resolvedName}`;\n\n const { allowed, resetIn } = await checkRateLimit({\n key: rateLimitKey,\n limit: effectiveApiLimit,\n windowMs: config.rateLimiting.windowMs,\n });\n\n if (!allowed) {\n //? The per-route bucket is keyed by the validated user when a token is\n //? present, else by the resolved IP (keyPrefix `ip`). Report the scope\n //? that matches the bucket's actual identity — an anonymous per-route\n //? bucket is IP-keyed, so it is `ip` (with `route` still set to mark it\n //? a per-route bucket vs the global `:api:all` IP bucket), never `route`.\n void dispatchHook('rateLimitExceeded', {\n scope: user?.id ? 'user' : 'ip',\n key: rateLimitKey,\n limit: effectiveApiLimit,\n windowMs: config.rateLimiting.windowMs,\n count: effectiveApiLimit + 1,\n route: resolvedName,\n userId: user?.id,\n //? IP only for an IP-keyed (anonymous) bucket; a user-keyed bucket omits it.\n ip: user?.id ? undefined : resolvedIp,\n });\n if (shouldLogDev()) {\n getLogger().warn(`api: rate limit exceeded for ${resolvedName}`, { route: resolvedName, key: rateLimitKey, transport });\n }\n return { allowed: false, errorCode: 'api.rateLimitExceeded', resetIn };\n }\n }\n\n const defaultIpLimit = config.rateLimiting.defaultIpLimit;\n if (!skipGlobalIpBucket && defaultIpLimit !== false && defaultIpLimit > 0) {\n const { allowed, resetIn } = await checkRateLimit({\n key: `ip:${resolvedIp}:api:all`,\n limit: defaultIpLimit,\n windowMs: config.rateLimiting.windowMs,\n });\n\n if (!allowed) {\n void dispatchHook('rateLimitExceeded', {\n scope: 'ip',\n key: `ip:${resolvedIp}:api:all`,\n limit: defaultIpLimit,\n windowMs: config.rateLimiting.windowMs,\n count: defaultIpLimit + 1,\n ip: resolvedIp,\n });\n if (shouldLogDev()) {\n getLogger().warn(`api: global IP rate limit exceeded`, { ip: resolvedIp, transport });\n }\n return { allowed: false, errorCode: 'api.rateLimitExceeded', resetIn };\n }\n }\n\n return { allowed: true };\n};\n","import type { BaseSessionLayout as SessionLayout, PostApiExecutePayload, ErrorFormatter } from '@luckystack/core';\nimport { getProjectConfig, readSession, getRuntimeApiMaps as getRuntimeApiMapsFromSource ,\n runWithErrorTrackerIdentityScope,\n setCurrentErrorTrackerIdentity,\n validateRequest,\n inferHttpMethod,\n HttpMethod,\n tryCatch,\n parseTransportRouteName,\n dispatchHook,\n getLogger, extractLanguageFromHeader, normalizeErrorResponse, applyErrorFormatter,\n isLoopbackIp, resolveClientIp, resolveEnvKey } from '@luckystack/core';\nimport type { ApiStreamPayload, RuntimeApiEntry } from './_shared/apiTypes';\nimport { shouldLogDev, shouldLogStream } from './_shared/logFlags';\nimport { normalizeApiResponse } from './_shared/responseEnvelope';\nimport { httpApiFlushPressureNoop } from './_shared/backpressure';\nimport { runHttpApiValidation } from './_shared/httpValidationStage';\nimport { applyApiRateLimits } from './_shared/applyApiRateLimits';\n\n\n\n\n\n\n/**\n * HTTP API Request Handler\n * \n * Handles API requests coming via HTTP (instead of WebSocket).\n * Reuses existing API handlers but returns results as HTTP response.\n * \n * Payload format:\n * ```json\n * {\n * \"name\": \"api/examples/publicApi\",\n * \"data\": { \"message\": \"hello\" }\n * }\n * ```\n * \n * @example\n * ```typescript\n * // In server.ts\n * if (pathname.startsWith('/api/')) {\n * const token = extractTokenFromRequest(req);\n * const body = await parseJsonBody(req);\n * const result = await handleHttpApiRequest({ name: body.name, data: body.data, token });\n * res.end(JSON.stringify(result));\n * }\n * ```\n */\n\ninterface HttpApiRequestParams {\n name: string;\n data: Record<string, unknown>;\n token: string | null;\n requesterIp?: string;\n xLanguageHeader?: string | string[];\n acceptLanguageHeader?: string | string[];\n /** HTTP method from the request */\n method?: HttpMethod;\n stream?: (payload: ApiHttpStreamEvent) => void;\n /**\n * Optional AbortSignal. The HTTP server (`@luckystack/server`) wires this\n * to `req.on('close', ...)` so a closed SSE/HTTP connection aborts in-flight\n * stream emits. Handlers receive it as `abortSignal` in params.\n */\n abortSignal?: AbortSignal;\n}\n\ntype ApiNetworkResponse<T = Record<string, unknown>> =\n | ({ status: 'success'; httpStatus: number } & T)\n | {\n status: 'error';\n httpStatus: number;\n message: string;\n errorCode: string;\n errorParams?: {\n key: string;\n value: string | number | boolean;\n }[];\n };\n\nexport type ApiHttpStreamEvent = ApiStreamPayload;\n\n//? ET-02 — wrap the whole HTTP request in a per-request error-tracker identity\n//? scope (opened before `readSession` in `runHandleHttpApiRequest`, the first\n//? interleaving await). The resolved session is written into the scope's ALS box\n//? the moment it resolves, so concurrent HTTP requests with different users can't\n//? cross-attribute captures. Each request gets its own isolated box.\nexport async function handleHttpApiRequest(params: HttpApiRequestParams): Promise<ApiNetworkResponse> {\n return runWithErrorTrackerIdentityScope(() => handleHttpApiRequestScoped(params));\n}\n\nasync function handleHttpApiRequestScoped(params: HttpApiRequestParams): Promise<ApiNetworkResponse> {\n const { response, user, preferredLocale, perRouteFormatter } = await runHandleHttpApiRequest(params);\n\n //? `name` may not parse to a valid route — pass the original raw name as\n //? routeName so hook handlers can still correlate the rejection.\n const routeNameForHook = params.name.startsWith('api/') ? params.name : `api/${params.name}`;\n\n const preRespond = { routeName: routeNameForHook, user, response: response as { status: 'success' | 'error'; httpStatus?: number; [key: string]: unknown } };\n const preRespondResult = await dispatchHook('preApiRespond', preRespond);\n\n //? Honor a stop signal from `preApiRespond`: rewrite the envelope into a\n //? localized error response per the signal's errorCode/httpStatus instead\n //? of letting the original through. Same contract as the socket variant.\n const finalResponse = preRespondResult.stopped\n ? normalizeErrorResponse({\n response: { status: 'error', errorCode: preRespondResult.signal.errorCode },\n preferredLocale,\n userLanguage: user?.language,\n fallbackHttpStatus: preRespondResult.signal.httpStatus ?? 403,\n }) as ApiNetworkResponse\n : preRespond.response as ApiNetworkResponse;\n\n //? `transformApiResponse` fires AFTER preApiRespond and BEFORE the wire\n //? response is finalized. Mirrors the socket handler (handleApiRequest)\n //? so BOTH transports run the same hook sequence per CLAUDE.md. Handlers\n //? mutate `payload.response` in place (header injection, body transform,\n //? response signing).\n const transformPayload = { routeName: routeNameForHook, user, response: finalResponse };\n await dispatchHook('transformApiResponse', transformPayload);\n\n //? Per-route → global → identity error formatter chain. Mirrors the socket\n //? handler so HTTP + WebSocket responses honor the same `errorFormatter`\n //? export on each route. No-op on success envelopes.\n //? `ApiNetworkResponse` is a discriminated union; `applyErrorFormatter`\n //? accepts the wider `Record<string, unknown> & { status?: string }` shape\n //? that union doesn't structurally satisfy. The double-cast is the\n //? documented framework boundary between the union and the formatter input.\n // luckystack-allow no-as-unknown: formatter boundary — ApiNetworkResponse union does not structurally satisfy applyErrorFormatter input; fix requires @luckystack/core type alignment\n // eslint-disable-next-line no-restricted-syntax -- formatter boundary cast\n const formatterInput = transformPayload.response as unknown as Record<string, unknown> & { status?: string };\n const formattedResponse = applyErrorFormatter({\n response: formatterInput,\n routeName: routeNameForHook,\n transport: 'http',\n userId: user?.id,\n perRouteFormatter,\n }) as ApiNetworkResponse;\n\n await dispatchHook('postApiRespond', { routeName: routeNameForHook, user, response: formattedResponse });\n\n return formattedResponse;\n}\n\ninterface RunHandleHttpApiRequestResult {\n response: ApiNetworkResponse;\n user: SessionLayout | null;\n preferredLocale: string | null | undefined;\n perRouteFormatter?: ErrorFormatter;\n}\n\nasync function runHandleHttpApiRequest(params: HttpApiRequestParams): Promise<RunHandleHttpApiRequestResult> {\n const preferredLocale =\n extractLanguageFromHeader(params.xLanguageHeader)\n ?? extractLanguageFromHeader(params.acceptLanguageHeader);\n const user = await readSession(params.token);\n //? ET-02 — bind the resolved session into the active per-request ALS identity\n //? box (opened by `handleHttpApiRequest`) so every capture in this request\n //? attributes to this user. The legacy global is still set via the\n //? `preApiValidate` hook subscriber; the ALS read wins at capture time.\n setCurrentErrorTrackerIdentity(user?.id ? { id: user.id, email: user.email ?? undefined, username: user.name ?? undefined } : null);\n const formatterHolder: { current?: ErrorFormatter } = {};\n const response = await runHandleHttpApiRequestInner(params, user, preferredLocale, formatterHolder);\n return { response, user, preferredLocale, perRouteFormatter: formatterHolder.current };\n}\n\n// ---------------------------------------------------------------------------\n// Private staged helpers for runHandleHttpApiRequestInner\n// ---------------------------------------------------------------------------\n\n/** Shared context threaded through each pipeline stage. */\ninterface HttpApiPipelineContext {\n buildNetworkError: (args: {\n response: { status: 'error'; httpStatus?: number; errorCode?: string; errorParams?: { key: string; value: string | number | boolean }[] };\n fallbackHttpStatus?: number;\n }) => ApiNetworkResponse;\n user: SessionLayout | null;\n preferredLocale: string | null | undefined;\n}\n\n/**\n * Validates the raw request shape (name string, data object, route-name\n * parsing) and resolves the normalised route name + runtime maps.\n * Returns an error response when any guard fails, otherwise the resolved\n * artefacts needed by later stages.\n */\nasync function validateHttpApiRequestShape(\n name: string,\n data: Record<string, unknown>,\n ctx: HttpApiPipelineContext,\n): Promise<\n | ApiNetworkResponse\n | {\n resolvedName: string;\n requestData: Record<string, unknown>;\n apisObject: Record<string, unknown>;\n functionsObject: Record<string, unknown>;\n }\n> {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime defense: caller may pass non-string despite types\n if (!name || typeof name !== 'string') {\n return ctx.buildNetworkError({\n response: { status: 'error', errorCode: 'api.invalidName' },\n fallbackHttpStatus: 400,\n });\n }\n\n //? API-O5 — mirror the socket guard: `typeof [] === 'object'`, so an array\n //? payload would pass the original check and hit a `{...}`-destructuring\n //? handler, causing a 500 instead of the correct 400.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime defense against malformed HTTP payloads despite types\n if (typeof data !== 'object' || data === null || Array.isArray(data)) {\n return ctx.buildNetworkError({\n response: { status: 'error', errorCode: 'api.invalidDataObject' },\n fallbackHttpStatus: 400,\n });\n }\n\n const normalizedName = name.startsWith('api/') ? name : `api/${name}`;\n const parsedRoute = parseTransportRouteName({ value: normalizedName, prefix: 'api' });\n if (parsedRoute.status === 'error') {\n return ctx.buildNetworkError({\n response: {\n status: 'error',\n errorCode: 'routing.invalidServiceRouteName',\n errorParams: [{ key: 'name', value: normalizedName }],\n },\n fallbackHttpStatus: 400,\n });\n }\n\n const resolvedName = parsedRoute.normalizedFullName;\n\n if (shouldLogDev()) {\n getLogger().debug(`http api: ${resolvedName} called`);\n }\n\n const { apisObject, functionsObject } = await getRuntimeApiMapsFromSource();\n\n if (!apisObject[resolvedName]) {\n return ctx.buildNetworkError({\n response: {\n status: 'error',\n errorCode: 'api.notFound',\n errorParams: [{ key: 'name', value: resolvedName }],\n },\n fallbackHttpStatus: 404,\n });\n }\n\n return { resolvedName, requestData: data, apisObject, functionsObject };\n}\n\n/**\n * Runs the login-required guard and the `validateRequest` additional-predicate\n * check. Returns an error response on failure, or `null` to signal success.\n *\n * Pipeline order: auth → rate-limit → method → validate → execute.\n * Auth runs first so unauthenticated probes can't enumerate routes or\n * learn input shape from `inputValidation.message` / method-mismatch params.\n * API-O7 — failed-auth requests do NOT consume a rate-limit bucket here.\n * Brute-force lockout is the responsibility of `@luckystack/login`\n * (per-account lockout counter via `authLockout.ts`); the `apiAuthRejected`\n * hook lets consumers add additional throttles. Consuming a per-IP bucket on\n * auth-fail would let an attacker DoS a victim's IP with bad credentials.\n */\nfunction runHttpApiAuth(\n {\n name,\n resolvedName,\n requesterIp,\n auth,\n }: {\n name: string;\n resolvedName: string;\n requesterIp: string | undefined;\n auth: RuntimeApiEntry['auth'];\n },\n ctx: HttpApiPipelineContext,\n): ApiNetworkResponse | null {\n if (auth.login && !ctx.user?.id) {\n if (shouldLogDev()) {\n getLogger().warn(`http-api: ${name} requires login`, { route: name, transport: 'http' });\n }\n void dispatchHook('apiAuthRejected', {\n routeName: resolvedName,\n reason: 'login-required',\n userId: null,\n transport: 'http',\n ip: requesterIp,\n });\n return ctx.buildNetworkError({\n response: { status: 'error', errorCode: 'auth.required' },\n fallbackHttpStatus: 401,\n });\n }\n\n //? NO bare `if (!user) -> auth.forbidden` here. Public routes\n //? (auth.login: false) must be callable without a session — exactly like the\n //? socket API handler (handleApiRequest) and both sync handlers.\n //? `validateRequest` is the single source of truth for `additional` auth\n //? predicates. (This handler previously forbade ALL anonymous HTTP calls,\n //? diverging from the other three transports and making public routes\n //? unreachable over HTTP — e.g. the test-runner contract/rate-limit sweeps.)\n //? `user` may be null on a public route; validateRequest tolerates it (and\n //? only the login/additional predicates read it). Same `user!` shape the\n //? socket API handler and both sync handlers use.\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n const authResult = validateRequest({ auth, user: ctx.user! });\n if (authResult.status === 'error') {\n if (shouldLogDev()) {\n getLogger().warn(`http-api: auth failed for ${name}`, { route: name, errorCode: authResult.errorCode, transport: 'http' });\n }\n void dispatchHook('apiAuthRejected', {\n routeName: resolvedName,\n reason: authResult.errorCode === 'auth.invalidCondition' ? 'invalid-condition' : 'additional-failed',\n userId: ctx.user?.id ?? null,\n transport: 'http',\n ip: requesterIp,\n failedKey: authResult.errorCode,\n });\n return ctx.buildNetworkError({\n response: {\n status: 'error',\n errorCode: authResult.errorCode ?? 'auth.forbidden',\n errorParams: authResult.errorParams,\n httpStatus: authResult.httpStatus,\n },\n fallbackHttpStatus: authResult.httpStatus ?? 403,\n });\n }\n\n return null;\n}\n\n/**\n * Resolves the effective client IP, computes the loopback-skip flag, and\n * delegates to the shared `applyApiRateLimits` helper.\n * Returns an error response when any bucket is exceeded, `null` otherwise.\n */\nasync function applyHttpApiRateLimits(\n {\n requesterIp,\n token,\n resolvedName,\n rateLimit,\n }: {\n requesterIp: string | undefined;\n token: string | null;\n resolvedName: string;\n rateLimit: number | false | undefined;\n },\n ctx: HttpApiPipelineContext,\n): Promise<ApiNetworkResponse | null> {\n //? API-O8 / API-O9 — resolve effective IP once for both rate-limit buckets.\n //? When the caller did not supply `requesterIp` we fall back to the shared\n //? `UNKNOWN_CLIENT_IP` sentinel so all unresolvable callers collapse into ONE\n //? deterministic bucket instead of mixing `'unknown'` and `'anonymous'`.\n const effectiveIp = requesterIp\n ?? resolveClientIp({ rawAddress: undefined, headers: {} });\n\n //? API-O2 — skip the global per-IP ABUSE limit for loopback traffic when the\n //? consumer explicitly opts in via `rateLimiting.skipLoopbackInDev` (default\n //? false). Only the cross-route `:api:all` bucket is skipped; per-route limits\n //? still apply. Pass the raw `requesterIp` (undefined → '') so only real\n //? loopback addresses qualify — avoids skipping for every unresolvable caller.\n const skipGlobalIpForLoopback = getProjectConfig().rateLimiting.skipLoopbackInDev\n && resolveEnvKey() !== 'production'\n && isLoopbackIp(requesterIp ?? '');\n\n const rateLimitResult = await applyApiRateLimits({\n resolvedIp: effectiveIp,\n token,\n user: ctx.user,\n resolvedName,\n rateLimit,\n transport: 'http',\n skipGlobalIpBucket: skipGlobalIpForLoopback,\n });\n\n if (!rateLimitResult.allowed) {\n return ctx.buildNetworkError({\n response: {\n status: 'error',\n errorCode: rateLimitResult.errorCode ?? 'api.rateLimitExceeded',\n errorParams: [{ key: 'seconds', value: rateLimitResult.resetIn ?? 0 }],\n },\n fallbackHttpStatus: 429,\n });\n }\n\n return null;\n}\n\n/**\n * Enforces the HTTP method constraint (declared or inferred).\n * Runs post-auth so unauthenticated probes don't learn that the route exists.\n * Returns an error response on mismatch, `null` otherwise.\n */\nfunction checkHttpApiMethod(\n {\n method,\n resolvedName,\n declaredMethod,\n }: {\n method: HttpMethod;\n resolvedName: string;\n declaredMethod: HttpMethod | undefined;\n },\n ctx: HttpApiPipelineContext,\n): ApiNetworkResponse | null {\n const expectedMethod = declaredMethod ?? inferHttpMethod(resolvedName);\n if (method !== expectedMethod) {\n if (shouldLogDev()) {\n getLogger().warn(`http api: method mismatch for ${resolvedName}`, { expected: expectedMethod, got: method });\n }\n return ctx.buildNetworkError({\n response: {\n status: 'error',\n errorCode: 'api.methodNotAllowed',\n errorParams: [{ key: 'method', value: expectedMethod }],\n },\n fallbackHttpStatus: 405,\n });\n }\n\n return null;\n}\n\n/**\n * Runs the handler execution stage: emitter setup, pre/post-execute hooks,\n * server-side timeout race, `main()` invocation, and response-envelope\n * assembly.\n */\nasync function runHttpApiExecution(\n {\n resolvedName,\n requestData,\n functionsObject,\n runtimeApiRoute,\n effectiveAbortSignal,\n stream,\n }: {\n resolvedName: string;\n requestData: Record<string, unknown>;\n functionsObject: Record<string, unknown>;\n runtimeApiRoute: RuntimeApiEntry;\n effectiveAbortSignal: AbortSignal;\n stream: HttpApiRequestParams['stream'];\n },\n ctx: HttpApiPipelineContext,\n): Promise<ApiNetworkResponse> {\n //? SSE has no socket write-buffer to measure — `flushPressure` is a no-op\n //? on HTTP transport. Kept for handler-shape parity with the socket path.\n const flushPressure = httpApiFlushPressureNoop;\n\n const emitApiStream = (payload: ApiStreamPayload = {}) => {\n if (!stream) {\n return;\n }\n if (effectiveAbortSignal.aborted) {\n if (shouldLogStream()) {\n getLogger().debug(`http api: ${resolvedName} stream skipped — request aborted`);\n }\n return;\n }\n\n if (shouldLogStream()) {\n getLogger().debug(`http api: ${resolvedName} stream`, { payload });\n }\n\n stream(payload);\n };\n\n //? Single payload reference reused by pre/post — auto-instrumentation\n //? subscribers in `@luckystack/error-tracking` pin spans via WeakMap on\n //? this object. Mutating `result/error/durationMs` is safe because\n //? handlers observe by-name (not by snapshot).\n const executePayload: PostApiExecutePayload = {\n routeName: resolvedName,\n data: requestData,\n user: ctx.user,\n transport: 'http',\n result: undefined,\n error: null,\n durationMs: 0,\n };\n const preExecuteResult = await dispatchHook('preApiExecute', executePayload);\n if (preExecuteResult.stopped) {\n return ctx.buildNetworkError({\n response: { status: 'error', errorCode: preExecuteResult.signal.errorCode },\n fallbackHttpStatus: preExecuteResult.signal.httpStatus ?? 403,\n });\n }\n\n //? API-O10 — race `main()` against a server-side timeout derived from\n //? `api.requestTimeoutMs`. When the timeout fires first we abort the\n //? effectiveAbortSignal chain and return a localized 504 envelope.\n //? A combined AbortController merges the caller's close signal and the\n //? timeout so the handler sees one unified abort.\n //? Streaming requests are EXEMPT from the wall-clock timeout even when one is\n //? configured: an SSE/stream endpoint is long-lived by design (live LLM tokens,\n //? progress) and a total-duration race would truncate it mid-stream. The\n //? abortSignal (wired to client disconnect) still tears it down when the caller\n //? leaves. Non-streaming requests honor `api.requestTimeoutMs` (default false).\n const timeoutMs = stream ? false : getProjectConfig().api.requestTimeoutMs;\n const combinedController = new AbortController();\n //? Forward the caller's abort (connection close) into the combined signal.\n if (effectiveAbortSignal.aborted) {\n combinedController.abort();\n } else {\n effectiveAbortSignal.addEventListener('abort', () => { combinedController.abort(); }, { once: true });\n }\n\n let timedOut = false;\n let timeoutHandle: ReturnType<typeof setTimeout> | undefined;\n const timeoutPromise = timeoutMs === false\n ? null\n : new Promise<void>((resolve) => {\n timeoutHandle = setTimeout(() => {\n timedOut = true;\n combinedController.abort();\n resolve();\n }, timeoutMs);\n });\n\n const executeStart = Date.now();\n const handlerPromise = tryCatch(\n async () => await runtimeApiRoute.main({\n data: requestData,\n user: ctx.user,\n functions: functionsObject,\n stream: emitApiStream,\n abortSignal: combinedController.signal,\n flushPressure,\n }),\n undefined,\n {\n handler: 'handleHttpApiRequest',\n api: resolvedName,\n userId: ctx.user?.id,\n transport: 'http',\n },\n );\n\n if (timeoutPromise) {\n await Promise.race([handlerPromise, timeoutPromise]);\n }\n clearTimeout(timeoutHandle);\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- timedOut is mutated by the async timeout callback via Promise.race\n if (timedOut) {\n return ctx.buildNetworkError({\n response: { status: 'error', errorCode: 'api.timeout' },\n fallbackHttpStatus: 504,\n });\n }\n\n const [error, result] = await handlerPromise;\n executePayload.result = result;\n executePayload.error = error ?? null;\n executePayload.durationMs = Date.now() - executeStart;\n await dispatchHook('postApiExecute', executePayload);\n\n //? Shared envelope assembly (CC-6) — identical to the socket handler's\n //? `buildApiResponseEnvelope`: localized error shapes for failure / empty /\n //? invalid-status, or a success envelope with inferred default HTTP status.\n //? `normalizeApiResponse` runtime-checks `result.status`, so a handler that\n //? returns a non-status object still maps to `api.invalidResponseStatus`.\n const envelope = normalizeApiResponse({\n resolvedName,\n error: error ?? null,\n result: result ?? undefined,\n preferredLocale: ctx.preferredLocale,\n user: ctx.user,\n });\n return envelope as ApiNetworkResponse;\n}\n\nasync function runHandleHttpApiRequestInner(\n {\n name,\n data,\n token,\n requesterIp,\n xLanguageHeader: _xLanguageHeader,\n acceptLanguageHeader: _acceptLanguageHeader,\n method = 'POST',\n stream,\n abortSignal,\n }: HttpApiRequestParams,\n user: SessionLayout | null,\n preferredLocale: string | null | undefined,\n formatterHolder: { current?: ErrorFormatter },\n): Promise<ApiNetworkResponse> {\n //? B1 — HTTP/SSE transport. The caller (typically `@luckystack/server`'s\n //? SSE bridge) wires `req.on('close', ...)` to a controller and passes its\n //? signal in. If no signal was provided we synthesize a never-aborting one\n //? so the handler param shape stays consistent.\n const effectiveAbortSignal = abortSignal ?? new AbortController().signal;\n\n const buildNetworkError = ({\n response,\n fallbackHttpStatus,\n }: {\n response: { status: 'error'; httpStatus?: number; errorCode?: string; errorParams?: { key: string; value: string | number | boolean; }[] };\n fallbackHttpStatus?: number;\n }): ApiNetworkResponse => {\n return normalizeErrorResponse({\n response,\n preferredLocale,\n userLanguage: user?.language,\n fallbackHttpStatus,\n }) as ApiNetworkResponse;\n };\n\n //? Identity propagation now flows via the `preApiExecute` hook subscriber\n //? registered by `@luckystack/error-tracking`'s `enableErrorTrackingAutoInstrumentation()`.\n //? Direct `setSentryUser` removed from this handler — see migration doc.\n const ctx: HttpApiPipelineContext = { buildNetworkError, user, preferredLocale };\n\n // Stage 1: validate request shape, parse route, resolve runtime maps\n const shapeResult = await validateHttpApiRequestShape(name, data, ctx);\n if ('status' in shapeResult) {\n return shapeResult;\n }\n const { resolvedName, requestData, apisObject, functionsObject } = shapeResult;\n\n const runtimeApiRoute = apisObject[resolvedName] as RuntimeApiEntry;\n formatterHolder.current = runtimeApiRoute.errorFormatter;\n\n // Stage 2: auth (login-required + additional predicates)\n //? Pipeline order: auth → rate-limit → method → validate → execute → respond.\n const authError = runHttpApiAuth({ name, resolvedName, requesterIp, auth: runtimeApiRoute.auth }, ctx);\n if (authError) {\n return authError;\n }\n\n // Stage 3: rate limiting (per-route + global-IP)\n const rateLimitError = await applyHttpApiRateLimits(\n { requesterIp, token, resolvedName, rateLimit: runtimeApiRoute.rateLimit },\n ctx,\n );\n if (rateLimitError) {\n return rateLimitError;\n }\n\n // Stage 4: HTTP method check (post-auth so unauthenticated probes don't learn the route exists)\n const methodError = checkHttpApiMethod(\n { method, resolvedName, declaredMethod: runtimeApiRoute.httpMethod },\n ctx,\n );\n if (methodError) {\n return methodError;\n }\n\n // Stage 5: input-type validation\n //? Input-type validation stage (preApiValidate -> validateInputByType ->\n //? postApiValidate). Extracted to `_shared/httpValidationStage.ts` for\n //? symmetry with the socket handler. On failure the caller builds the\n //? GENERIC `api.invalidInputType` (the raw validator message never reaches\n //? the client). Honors the per-route `validation: 'relaxed'` / `{ input:\n //? 'skip' }` escape hatch on BOTH transports — a public webhook route whose\n //? third-party payload can't be modeled in TS is reachable over HTTP, not\n //? just sockets.\n const httpValidation = await runHttpApiValidation({\n resolvedName,\n inputType: runtimeApiRoute.inputType,\n inputTypeFilePath: runtimeApiRoute.inputTypeFilePath,\n requestData,\n user,\n validation: runtimeApiRoute.validation,\n });\n if (!httpValidation.ok) {\n return buildNetworkError({\n response: {\n status: 'error',\n errorCode: 'api.invalidInputType',\n },\n fallbackHttpStatus: 400,\n });\n }\n\n // Stage 6: execute the API handler and assemble the response envelope\n return runHttpApiExecution(\n { resolvedName, requestData, functionsObject, runtimeApiRoute, effectiveAbortSignal, stream },\n ctx,\n );\n}\n","import type { BaseSessionLayout as SessionLayout } from '@luckystack/core';\nimport { dispatchHook, validateInputByType, getLogger } from '@luckystack/core';\nimport type { RuntimeApiEntry } from './apiTypes';\nimport { shouldLogDev } from './logFlags';\nimport { warnIfInputTypeMissing } from './inputTypeWarning';\nimport { resolveValidationMode } from './socketValidationStage';\n\n//? HTTP-transport input-validation stage. Mirrors the socket variant\n//? (`runSocketApiValidation`) for transport symmetry, extracted from\n//? `runHandleHttpApiRequestInner` per the `api` package audit. Behaviour:\n//? • resolves the per-route validation mode (`'relaxed'` / `{ input: 'skip' }`\n//? => skip Zod validation; default `'strict'`) — identical to the socket\n//? stage, so a public webhook route (Stripe/Slack/GitHub) whose third-party\n//? payload can't be modeled in TS is reachable over HTTP, not just sockets;\n//? • warns once if a strict-mode route has no generated input type;\n//? • dispatches `preApiValidate`, runs `validateInputByType` in strict mode,\n//? then `postApiValidate` (always, including the relaxed skip path);\n//? • on failure returns `{ ok: false }` (the caller builds the GENERIC\n//? `api.invalidInputType` network error — the raw validator message is\n//? NEVER echoed to the client; it only travels to the postApiValidate hook\n//? + dev logs).\n\nexport const runHttpApiValidation = async ({\n resolvedName,\n inputType,\n inputTypeFilePath,\n requestData,\n user,\n validation,\n}: {\n resolvedName: string;\n inputType?: string;\n inputTypeFilePath?: string;\n requestData: Record<string, unknown>;\n user: SessionLayout | null;\n validation: RuntimeApiEntry['validation'];\n}): Promise<{ ok: true } | { ok: false }> => {\n //? Per-route validation toggle, honored identically to the socket stage.\n const validationMode = resolveValidationMode(validation);\n\n // Input-type validation (post-auth so unauthenticated probes don't get input-shape leaks)\n if (validationMode === 'strict') {\n warnIfInputTypeMissing(resolvedName, inputType);\n }\n await dispatchHook('preApiValidate', { routeName: resolvedName, data: requestData, user, transport: 'http' });\n\n if (validationMode === 'relaxed') {\n //? Relaxed: surface the skip via postApiValidate so audit handlers see it.\n await dispatchHook('postApiValidate', {\n routeName: resolvedName,\n data: requestData,\n user,\n validation: { status: 'success' },\n transport: 'http',\n });\n return { ok: true };\n }\n\n const inputValidation = await validateInputByType({\n typeText: inputType,\n value: requestData,\n rootKey: 'data',\n filePath: inputTypeFilePath,\n });\n\n await dispatchHook('postApiValidate', {\n routeName: resolvedName,\n data: requestData,\n user,\n validation: inputValidation,\n transport: 'http',\n });\n\n if (inputValidation.status === 'error') {\n //? SECURITY: do NOT echo the raw validator message (e.g.\n //? \"data.userId should be string\") back to the client — that lets an\n //? unauthenticated caller enumerate a route's input schema. The DETAILED\n //? message is routed to the `postApiValidate` hook + dev logs above; the\n //? client only receives the generic `api.invalidInputType` code.\n if (shouldLogDev()) {\n getLogger().warn(`http-api: input validation failed for ${resolvedName}`, { route: resolvedName, message: inputValidation.message, transport: 'http' });\n }\n return { ok: false };\n }\n\n return { ok: true };\n};\n"],"mappings":"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","names":["getProjectConfig","checkRateLimit","dispatchHook","getLogger","normalizeErrorResponse","getLogger","getLogger","getLogger","getLogger","getLogger","getProjectConfig","getLogger","getLogger","getProjectConfig","dispatchHook","getLogger","getProjectConfig","dispatchHook","getLogger","getLogger","dispatchHook","getProjectConfig","normalizeErrorResponse","checkRateLimit","getProjectConfig","readSession","runWithErrorTrackerIdentityScope","setCurrentErrorTrackerIdentity","validateRequest","tryCatch","parseTransportRouteName","dispatchHook","getLogger","extractLanguageFromHeader","normalizeErrorResponse","applyErrorFormatter","resolveClientIp","dispatchHook","validateInputByType","getLogger","dispatchHook","validateInputByType","getLogger","runWithErrorTrackerIdentityScope","dispatchHook","normalizeErrorResponse","applyErrorFormatter","extractLanguageFromHeader","readSession","setCurrentErrorTrackerIdentity","parseTransportRouteName","getLogger","validateRequest","resolveClientIp","getProjectConfig","tryCatch"]}
|
|
1
|
+
{"version":3,"sources":["../src/handleApiRequest.ts","../src/_shared/logFlags.ts","../src/_shared/responseEnvelope.ts","../src/_shared/requestLifecycle.ts","../src/_shared/backpressure.ts","../src/_shared/socketValidationStage.ts","../src/_shared/inputTypeWarning.ts","../src/_shared/applyApiRateLimits.ts","../src/handleHttpApiRequest.ts","../src/_shared/httpValidationStage.ts"],"sourcesContent":["import type { apiMessage, PostApiExecutePayload, BaseSessionLayout as SessionLayout, ErrorFormatter } from '@luckystack/core';\nimport {\n readSession,\n performLogout,\n runWithErrorTrackerIdentityScope,\n setCurrentErrorTrackerIdentity,\n getProjectConfig,\n getRuntimeApiMaps,\n validateRequest,\n checkRateLimit,\n tryCatch,\n parseTransportRouteName,\n buildApiResponseEventName,\n dispatchHook,\n getLogger,\n resolveClientIp,\n extractLanguageFromHeader,\n normalizeErrorResponse,\n applyErrorFormatter,\n} from '@luckystack/core';\n//? checkRateLimit and resolveClientIp are still used directly for the logout\n//? global-IP bucket (which doesn't go through the shared applyApiRateLimits helper\n//? because the logout shortcut bypasses the per-route bucket entirely).\nimport { Socket } from 'socket.io';\nimport type { ApiStreamPayload, ApiFlushPressure, RuntimeApiResponse, RuntimeApiEntry } from './_shared/apiTypes';\nimport { shouldLogDev } from './_shared/logFlags';\nimport { normalizeApiResponse } from './_shared/responseEnvelope';\nimport { createApiRequestLifecycle } from './_shared/requestLifecycle';\nimport { runSocketApiValidation } from './_shared/socketValidationStage';\nimport { applyApiRateLimits } from './_shared/applyApiRateLimits';\n\ninterface handleApiRequestType {\n msg: apiMessage,\n socket: Socket,\n token: string | null,\n}\n\ninterface ApiErrorResponse {\n status: 'error';\n httpStatus?: number;\n errorCode?: string;\n errorParams?: { key: string; value: string | number | boolean; }[];\n}\n\ntype EmitApiError = (args: { response: ApiErrorResponse; fallbackHttpStatus?: number; }) => Promise<void>;\n\nconst validateApiMessage = async ({ msg, emitApiError }: {\n msg: apiMessage;\n emitApiError: EmitApiError;\n}): Promise<{ name: string; data: Record<string, unknown> } | null> => {\n const { name, data, responseIndex } = msg;\n\n //? Drop a message with no usable response channel. Written as a single\n //? `typeof` test (equivalent to the former `!responseIndex && typeof !==\n //? 'number'` double-negative) so a `responseIndex: 0` is correctly KEPT — a\n //? future \"simplify\" to `!responseIndex` would silently drop every index-0\n //? request.\n if (typeof responseIndex !== 'number') {\n if (shouldLogDev()) {\n getLogger().warn('api: no response index given');\n }\n return null;\n }\n\n //? Positive plain-object guard (parity with the sync handler). `!data`\n //? excludes `null`; `Array.isArray` rejects an ARRAY payload that\n //? `typeof === 'object'` would otherwise admit to a handler expecting an\n //? object.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime defense: msg arrives from an untyped socket frame\n if (!name || !data || typeof name != 'string' || typeof data != 'object' || Array.isArray(data)) {\n await emitApiError({\n response: { status: 'error', errorCode: 'api.invalidRequest' },\n fallbackHttpStatus: 400,\n });\n return null;\n }\n\n return { name, data: data as Record<string, unknown> };\n};\n\nconst checkApiAuth = async ({ apiEntry, user, name, emitApiError }: {\n apiEntry: RuntimeApiEntry;\n user: SessionLayout | null;\n name: string;\n emitApiError: EmitApiError;\n}): Promise<boolean> => {\n if (apiEntry.auth.login && !user?.id) {\n if (shouldLogDev()) {\n getLogger().warn(`api: ${name} requires login`, { route: name });\n }\n void dispatchHook('apiAuthRejected', {\n routeName: name,\n reason: 'login-required',\n userId: null,\n transport: 'socket',\n });\n await emitApiError({\n response: { status: 'error', errorCode: 'auth.required' },\n fallbackHttpStatus: 401,\n });\n return false;\n }\n\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n const authResult = validateRequest({ auth: apiEntry.auth, user: user! });\n if (authResult.status === 'error') {\n if (shouldLogDev()) {\n getLogger().warn(`api: auth failed for ${name}`, { route: name, errorCode: authResult.errorCode });\n }\n void dispatchHook('apiAuthRejected', {\n routeName: name,\n reason: authResult.errorCode === 'auth.invalidCondition' ? 'invalid-condition' : 'additional-failed',\n userId: user?.id ?? null,\n transport: 'socket',\n failedKey: authResult.errorCode,\n });\n await emitApiError({\n response: {\n status: 'error',\n // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- empty string errorCode must still fall back\n errorCode: authResult.errorCode || 'auth.forbidden',\n errorParams: authResult.errorParams,\n httpStatus: authResult.httpStatus,\n },\n fallbackHttpStatus: authResult.httpStatus ?? 403,\n });\n return false;\n }\n\n return true;\n};\n\n//? API-O8 — rate-limit logic extracted to `_shared/applyApiRateLimits.ts` so\n//? both transports share one implementation. The socket transport resolves the\n//? effective IP here (once, from socket internals) before delegating to the helper.\nconst runSocketRateLimits = async ({ apiEntry, resolvedName, token, socket, user, emitApiError }: {\n apiEntry: RuntimeApiEntry;\n resolvedName: string;\n token: string | null;\n socket: Socket;\n user: SessionLayout | null;\n emitApiError: EmitApiError;\n}): Promise<boolean> => {\n //? Resolve the real client IP once. With the default `http.trustProxy: false`\n //? this returns `socket.handshake.address` verbatim (only IPv4-mapped IPv6 is\n //? canonicalized). When a trusted proxy is configured it honors XFF / X-Real-IP.\n const resolvedIp = resolveClientIp({\n rawAddress: socket.handshake.address,\n headers: socket.handshake.headers,\n trustProxy: getProjectConfig().http.trustProxy,\n trustedProxyHopCount: getProjectConfig().http.trustedProxyHopCount,\n });\n\n const result = await applyApiRateLimits({\n resolvedIp,\n token,\n user,\n resolvedName,\n rateLimit: apiEntry.rateLimit,\n transport: 'socket',\n });\n\n if (!result.allowed) {\n await emitApiError({\n response: {\n status: 'error',\n errorCode: result.errorCode ?? 'api.rateLimitExceeded',\n errorParams: [{ key: 'seconds', value: result.resetIn ?? 0 }],\n },\n fallbackHttpStatus: 429,\n });\n return false;\n }\n\n return true;\n};\n\nconst executeApiHandler = async ({ apiEntry, normalizedData, user, functionsObject, resolvedName, emitStream, abortSignal, abortController, flushPressure }: {\n apiEntry: RuntimeApiEntry;\n normalizedData: Record<string, unknown>;\n user: SessionLayout | null;\n functionsObject: Record<string, unknown>;\n resolvedName: string;\n emitStream: (payload?: ApiStreamPayload) => void;\n abortSignal: AbortSignal;\n abortController: AbortController;\n flushPressure: ApiFlushPressure;\n}): Promise<{ error: Error | null; result: RuntimeApiResponse | undefined; durationMs: number; timedOut?: boolean }> => {\n //? Span open/close + identity propagation moved to hook subscribers in\n //? `@luckystack/error-tracking` (preApiExecute / postApiExecute). This\n //? handler is now transport-agnostic instrumentation-wise.\n const executeStart = Date.now();\n\n //? API-O10 — race `main()` against a server-side timeout derived from\n //? `api.requestTimeoutMs`. When the timeout fires first we abort the\n //? AbortController (propagates to the handler's `abortSignal`) and resolve\n //? a sentinel so the orchestrator can emit a localized 504 envelope.\n const timeoutMs = getProjectConfig().api.requestTimeoutMs;\n let timeoutHandle: ReturnType<typeof setTimeout> | undefined;\n let timedOut = false;\n const timeoutPromise = timeoutMs === false\n ? null\n : new Promise<void>((resolve) => {\n timeoutHandle = setTimeout(() => {\n timedOut = true;\n abortController.abort();\n resolve();\n }, timeoutMs);\n });\n\n const handlerPromise = tryCatch(\n async () => await apiEntry.main({ data: normalizedData, user, functions: functionsObject, stream: emitStream, abortSignal, flushPressure }),\n undefined,\n {\n handler: 'handleApiRequest',\n api: resolvedName,\n userId: user?.id,\n transport: 'socket',\n },\n );\n\n if (timeoutPromise) {\n await Promise.race([handlerPromise, timeoutPromise]);\n }\n clearTimeout(timeoutHandle);\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- timedOut is mutated by the async timeout callback via Promise.race\n if (timedOut) {\n return { error: null, result: undefined, durationMs: Date.now() - executeStart, timedOut: true };\n }\n\n const [error, result] = await handlerPromise;\n return { error: error ?? null, result: result ?? undefined, durationMs: Date.now() - executeStart };\n};\n\n//? Execution stage: `preApiExecute` hook (stop-signal short-circuit) ->\n//? `executeApiHandler` (tryCatch-wrapped) -> `postApiExecute` hook. Extracted\n//? from the orchestrator per the `api` package audit. Returns either a stop\n//? signal (the orchestrator builds the localized error envelope from it) or the\n//? `(error, result)` pair to feed `emitApiResult`. The shared `executePayload`\n//? reference is created here and threaded through both hooks so the\n//? error-tracking auto-instrumentation can pin spans via WeakMap on it.\ntype SocketApiExecutionOutcome =\n | { stopped: true; errorCode: string; httpStatus?: number }\n | { stopped: false; error: Error | null; result: RuntimeApiResponse | undefined };\n\nconst runSocketApiExecution = async ({\n apiEntry,\n resolvedName,\n normalizedData,\n user,\n functionsObject,\n emitStream,\n abortSignal,\n abortController,\n flushPressure,\n}: {\n apiEntry: RuntimeApiEntry;\n resolvedName: string;\n normalizedData: Record<string, unknown>;\n user: SessionLayout | null;\n functionsObject: Record<string, unknown>;\n emitStream: (payload?: ApiStreamPayload) => void;\n abortSignal: AbortSignal;\n abortController: AbortController;\n flushPressure: ApiFlushPressure;\n}): Promise<SocketApiExecutionOutcome> => {\n //? Single payload reference reused by pre/post — auto-instrumentation\n //? subscribers in `@luckystack/error-tracking` pin spans via WeakMap on\n //? this object. Mutating `result/error/durationMs` is safe because\n //? handlers observe by-name (not by snapshot).\n const executePayload: PostApiExecutePayload = {\n routeName: resolvedName,\n data: normalizedData,\n user,\n transport: 'socket',\n result: undefined,\n error: null,\n durationMs: 0,\n };\n const preExecuteResult = await dispatchHook('preApiExecute', executePayload);\n if (preExecuteResult.stopped) {\n return { stopped: true, errorCode: preExecuteResult.signal.errorCode, httpStatus: preExecuteResult.signal.httpStatus };\n }\n\n const { error, result, durationMs, timedOut } = await executeApiHandler({\n apiEntry,\n normalizedData,\n user,\n functionsObject,\n resolvedName,\n emitStream,\n abortSignal,\n abortController,\n flushPressure,\n });\n\n //? API-O10 — a server-side timeout aborted the handler. Skip postApiExecute\n //? (no meaningful result to report) and surface a 504 to the client.\n if (timedOut) {\n return { stopped: true, errorCode: 'api.timeout', httpStatus: 504 };\n }\n\n executePayload.result = result;\n executePayload.error = error ?? null;\n executePayload.durationMs = durationMs;\n await dispatchHook('postApiExecute', executePayload);\n\n return { stopped: false, error, result };\n};\n\nconst emitApiResult = async ({\n socket,\n responseIndex,\n resolvedName,\n error,\n result,\n preferredLocale,\n user,\n perRouteFormatter,\n}: {\n socket: Socket;\n responseIndex: number;\n resolvedName: string;\n error: Error | null;\n result: RuntimeApiResponse | undefined;\n preferredLocale: string | undefined;\n user: SessionLayout | null;\n perRouteFormatter?: ErrorFormatter;\n}): Promise<void> => {\n const envelope = normalizeApiResponse({ resolvedName, error, result, preferredLocale, user });\n\n //? `preApiRespond` handlers may mutate `payload.response` to swap or rewrite\n //? the outgoing envelope (PII redaction, response signing, schema injection).\n //? A handler may also return a stop signal — when that happens we build a\n //? fresh error envelope from the signal's `errorCode` / `httpStatus` and\n //? emit that instead of the original.\n const preRespond = { routeName: resolvedName, user, response: envelope };\n const preRespondResult = await dispatchHook('preApiRespond', preRespond);\n\n const finalResponse = preRespondResult.stopped\n ? { ...normalizeErrorResponse({\n response: { status: 'error', errorCode: preRespondResult.signal.errorCode },\n preferredLocale,\n userLanguage: user?.language,\n fallbackHttpStatus: preRespondResult.signal.httpStatus ?? 403,\n }) }\n : preRespond.response;\n\n //? `transformApiResponse` fires AFTER preApiRespond and BEFORE socket emit.\n //? Designed for response mutation — header injection, body transformation,\n //? response signing — that's awkward inside preApiRespond. Handlers mutate\n //? `payload.response` in place (same shape as preApiRespond.response).\n const transformPayload = { routeName: resolvedName, user, response: finalResponse };\n await dispatchHook('transformApiResponse', transformPayload);\n\n //? Per-route → global → identity error formatter chain. No-op when status\n //? is success. Runs AFTER transformApiResponse so consumer hooks can still\n //? mutate the envelope before the final shape lands.\n const formattedResponse = applyErrorFormatter({\n response: transformPayload.response,\n routeName: resolvedName,\n transport: 'socket',\n userId: user?.id,\n perRouteFormatter,\n });\n\n socket.emit(buildApiResponseEventName(responseIndex), formattedResponse);\n\n //? `postApiRespond` is observation-only — the response is already on the\n //? wire. Use it for audit logging, metrics, dual-write replication.\n await dispatchHook('postApiRespond', { routeName: resolvedName, user, response: formattedResponse });\n};\n\n//? ET-02 — open a per-request error-tracker identity scope around the ENTIRE\n//? handler before any await that could interleave with another concurrent\n//? request. `readSession` (the first await below) is itself such a boundary, so\n//? the scope must wrap it. The session is written into the scope via\n//? `setCurrentErrorTrackerIdentity(...)` the moment it resolves; from then on\n//? every error-tracker capture during this request (handler throw, hook\n//? subscriber, fanout) reads THIS request's identity from the AsyncLocalStorage\n//? box, never another concurrent request's. Each request gets its own box.\nexport default async function handleApiRequest(args: handleApiRequestType): Promise<void> {\n //? handleApiRequestInner runs un-.catch()'d from loadSocket (a bare `void`\n //? call), so ANY throw from an unguarded dependency (readSession, the rate-limit\n //? Redis calls, getRuntimeApiMaps, the session store) would surface as a fatal\n //? unhandledRejection → worker crash on modern Node. Wrap the whole run (mirrors\n //? the sync handler's top-level tryCatch) and emit a generic error envelope on\n //? the response channel so the request fails cleanly instead of killing the worker.\n const [error] = await tryCatch(() => runWithErrorTrackerIdentityScope(() => handleApiRequestInner(args)));\n if (error) {\n getLogger().error('api: unhandled error in socket request handler', { error: error.message });\n const msg = args.msg as apiMessage | null;\n const responseIndex = msg?.responseIndex;\n if (typeof responseIndex === 'number') {\n args.socket.emit(buildApiResponseEventName(responseIndex), {\n status: 'error',\n errorCode: 'api.serverExecutionFailed',\n httpStatus: 500,\n });\n }\n }\n}\n\nasync function handleApiRequestInner({ msg, socket, token }: handleApiRequestType) {\n //? This event gets triggered when the client uses the apiRequest function.\n //? Validate the message, check auth, then execute the registered handler.\n\n //? `msg` is TYPED as apiMessage but the socket transport can deliver ANYTHING at\n //? runtime (null / array / primitive), so view it as `unknown` for the guard.\n //? `typeof null === 'object'` and arrays are objects, so the bare object check\n //? would let null/array through to `const { responseIndex } = msg` below —\n //? throwing before any handler. That throw, running un-.catch()'d from loadSocket,\n //? becomes a fatal unhandledRejection → worker crash; a connected socket could\n //? trigger it pre-auth via `socket.emit('api', null)` (remote DoS). Mirrors the\n //? sync handler's validateSyncMessage guard.\n const rawMsg: unknown = msg;\n if (typeof rawMsg !== 'object' || rawMsg === null || Array.isArray(rawMsg)) {\n if (shouldLogDev()) {\n getLogger().warn('api: socket message was not a json object');\n }\n return;\n }\n\n const { responseIndex } = msg;\n\n //? EXT-02 — per-message socket interception seam (counterpart to preHttpRequest\n //? in the HTTP pipeline and preSocketMessage in the sync handler). Fires before\n //? session lookup / route resolution / auth so a consumer can gate, throttle, or\n //? audit individual api messages. A stop signal rejects the message.\n const preMsgResult = await dispatchHook('preSocketMessage', {\n channel: 'api',\n socketId: socket.id,\n ip: socket.handshake.address,\n authenticated: Boolean(token),\n routeName: typeof msg.name === 'string' ? msg.name : undefined,\n });\n if (preMsgResult.stopped) {\n //? Guard the emit on a numeric responseIndex (mirrors the sync handler +\n //? validateApiMessage's own no-index branch below): this stop path runs\n //? BEFORE validateApiMessage, so a malformed frame lacking responseIndex\n //? would otherwise emit to the dead 'apiResponse-undefined' channel and\n //? leave the client hanging until its own ack timeout. Drop it silently.\n if (typeof responseIndex === 'number') {\n socket.emit(buildApiResponseEventName(responseIndex), {\n status: 'error',\n errorCode: preMsgResult.signal.errorCode,\n httpStatus: preMsgResult.signal.httpStatus ?? 403,\n });\n }\n return;\n }\n\n const user = await readSession(token);\n //? ET-02 — bind the resolved session into the active per-request ALS identity\n //? box so every subsequent capture attributes to this user. Identity also still\n //? flows to the legacy global via the `preApiValidate` hook subscriber in\n //? `@luckystack/error-tracking` (the ALS read takes precedence at capture time).\n setCurrentErrorTrackerIdentity(user?.id ? { id: user.id, email: user.email ?? undefined, username: user.name ?? undefined } : null);\n const preferredLocale =\n extractLanguageFromHeader(socket.handshake.headers['x-language'])\n ?? extractLanguageFromHeader(socket.handshake.headers['accept-language'])\n ?? undefined;\n\n //? Per-route formatter ref + resolved-name ref. Both start undefined and\n //? get set once the route is parsed + the apiEntry looked up — pre-lookup\n //? errors (invalid message shape, unknown route) emit with global formatter\n //? only because there's no apiEntry to read per-route formatter from. After\n //? lookup, every emit through `emitApiError` flows through the same chain.\n // eslint-disable-next-line prefer-const -- captured by emitApiError closure; assigned later after route resolution\n let currentRouteName: string | undefined;\n // eslint-disable-next-line prefer-const -- captured by emitApiError closure; assigned later after apiEntry lookup\n let currentPerRouteFormatter: ErrorFormatter | undefined;\n\n //? API-O6 parity — socket ERROR responses now run the SAME respond-hook chain\n //? as success responses (`emitApiResult`) and as the HTTP transport: preApiRespond\n //? (may rewrite/stop) → transformApiResponse (mutate) → error formatter → emit →\n //? postApiRespond (observe). Previously `emitApiError` emitted directly, so\n //? consumer respond hooks (PII redaction, response signing, audit logging) never\n //? fired on auth / rate-limit / validation / not-found rejections over WebSocket —\n //? a parity gap with HTTP, where every response (success AND error) runs them.\n const emitApiError: EmitApiError = async ({ response, fallbackHttpStatus }) => {\n const normalized = normalizeErrorResponse({\n response,\n preferredLocale,\n userLanguage: user?.language,\n fallbackHttpStatus,\n });\n const routeNameForHook = currentRouteName ?? 'api/unknown';\n\n //? preApiRespond may mutate the envelope or stop. A stop on an already-error\n //? response rebuilds a fresh error envelope from the signal (parity with\n //? `emitApiResult` + the HTTP path).\n // luckystack-allow no-as-any: hook payload boundary — normalizeErrorResponse returns a narrower type than the preApiRespond payload shape; fix requires @luckystack/core type alignment\n // eslint-disable-next-line no-restricted-syntax -- hook payload boundary, mirrors handleHttpApiRequest\n const preRespond = { routeName: routeNameForHook, user, response: normalized as unknown as { status: 'success' | 'error'; httpStatus?: number; [key: string]: unknown } };\n const preRespondResult = await dispatchHook('preApiRespond', preRespond);\n\n let finalResponse = preRespond.response;\n if (preRespondResult.stopped) {\n const stoppedEnvelope = normalizeErrorResponse({\n response: { status: 'error', errorCode: preRespondResult.signal.errorCode },\n preferredLocale,\n userLanguage: user?.language,\n fallbackHttpStatus: preRespondResult.signal.httpStatus ?? 403,\n });\n // luckystack-allow no-as-any: hook payload boundary — normalizeErrorResponse is narrower than the hook payload shape; fix requires @luckystack/core type alignment\n // eslint-disable-next-line no-restricted-syntax -- hook payload boundary, mirrors handleHttpApiRequest\n finalResponse = stoppedEnvelope as unknown as typeof preRespond.response;\n }\n\n const transformPayload = { routeName: routeNameForHook, user, response: finalResponse };\n await dispatchHook('transformApiResponse', transformPayload);\n\n const formatted = applyErrorFormatter({\n response: transformPayload.response,\n routeName: routeNameForHook,\n transport: 'socket',\n userId: user?.id,\n perRouteFormatter: currentPerRouteFormatter,\n });\n socket.emit(buildApiResponseEventName(responseIndex), formatted);\n\n //? postApiRespond is observation-only — the response is already on the wire.\n await dispatchHook('postApiRespond', { routeName: routeNameForHook, user, response: formatted });\n };\n\n const validated = await validateApiMessage({ msg, emitApiError });\n if (!validated) return;\n const { name, data: normalizedData } = validated;\n\n const parsedRoute = parseTransportRouteName({ value: name, prefix: 'api' });\n if (parsedRoute.status === 'error') {\n await emitApiError({\n response: {\n status: 'error',\n errorCode: 'routing.invalidServiceRouteName',\n errorParams: [{ key: 'name', value: name }],\n },\n fallbackHttpStatus: 400,\n });\n return;\n }\n\n const resolvedName = parsedRoute.normalizedFullName;\n currentRouteName = resolvedName;\n\n //? Built-in 'system/logout' needs special handling since it requires socket access.\n //? Match the full normalized route to avoid hijacking consumer routes whose final\n //? segment happens to be 'logout' (e.g. 'admin/logout/vN').\n if (parsedRoute.serviceRoute.normalizedRouteName === 'system/logout') {\n //? Apply the global per-IP bucket before logout so the shortcut can't be\n //? spammed uncapped — it bypasses applyApiRateLimits entirely.\n const logoutIpLimit = getProjectConfig().rateLimiting.defaultIpLimit;\n if (logoutIpLimit !== false && logoutIpLimit > 0) {\n const logoutIp = resolveClientIp({\n rawAddress: socket.handshake.address,\n headers: socket.handshake.headers,\n trustProxy: getProjectConfig().http.trustProxy,\n trustedProxyHopCount: getProjectConfig().http.trustedProxyHopCount,\n });\n const { allowed, resetIn } = await checkRateLimit({\n key: `ip:${logoutIp}:api:all`,\n limit: logoutIpLimit,\n windowMs: getProjectConfig().rateLimiting.windowMs,\n });\n if (!allowed) {\n void dispatchHook('rateLimitExceeded', {\n scope: 'ip',\n key: `ip:${logoutIp}:api:all`,\n limit: logoutIpLimit,\n windowMs: getProjectConfig().rateLimiting.windowMs,\n count: logoutIpLimit + 1,\n ip: logoutIp,\n });\n await emitApiError({\n response: {\n status: 'error',\n errorCode: 'api.rateLimitExceeded',\n errorParams: [{ key: 'seconds', value: resetIn }],\n },\n fallbackHttpStatus: 429,\n });\n return;\n }\n }\n //? API-O6 — route through `emitApiResult` so the respond-hook chain\n //? (preApiRespond, transformApiResponse, postApiRespond) fires for logout,\n //? matching all other routes. The return value of `performLogout` is void\n //? (success is implied by non-throw), so we pass a fixed success result.\n await performLogout({ token, socket, userId: user?.id ?? null });\n await emitApiResult({\n socket,\n responseIndex,\n resolvedName,\n error: null,\n result: { status: 'success', httpStatus: 200, result: true },\n preferredLocale,\n user,\n });\n return;\n }\n\n if (shouldLogDev()) {\n getLogger().debug(`api: ${resolvedName} called`);\n }\n\n const { apisObject, functionsObject } = await getRuntimeApiMaps();\n\n if (!apisObject[resolvedName]) {\n await emitApiError({\n response: {\n status: 'error',\n errorCode: 'api.notFound',\n errorParams: [{ key: 'name', value: name }],\n },\n fallbackHttpStatus: 404,\n });\n return;\n }\n\n const apiEntry = apisObject[resolvedName] as RuntimeApiEntry;\n currentPerRouteFormatter = apiEntry.errorFormatter;\n\n //? API-O4 — `httpMethod` (and the `inferHttpMethod` heuristic) is HTTP-ONLY.\n //? The socket transport does NOT enforce the declared HTTP method — all routes\n //? are callable over WebSocket regardless of method declaration. This is by\n //? design: socket.io has no HTTP-method concept and enforcing it would break\n //? existing consumers. If you need to restrict a route to HTTP only, do not\n //? expose it as a socket route, or gate it in `preSocketMessage`.\n\n //? Per-request lifecycle bundle (B1 abortController + cleanup, B2\n //? backpressure, abort-aware emitStream). Extracted to `_shared/\n //? requestLifecycle.ts`; the closures stay intact and the orchestrator\n //? threads the returned handles through the pipeline. `cleanupRequest`\n //? runs in every exit path below.\n const { abortController, abortSignal, emitStream, flushPressure, cleanupRequest } = createApiRequestLifecycle({\n socket,\n responseIndex,\n resolvedName,\n });\n\n //? Auth → rate-limit → validate → execute → respond.\n //? Auth runs before validate so unauthenticated probes can't enumerate\n //? routes or learn input shape from `inputValidation.message`.\n //? API-O7 — failed-auth requests do NOT consume a rate-limit bucket here.\n //? This is an explicit design choice: brute-force lockout is the\n //? responsibility of `@luckystack/login` (per-account lockout counter via\n //? `authLockout.ts`), and the `apiAuthRejected` hook lets consumers add\n //? additional throttles. Consuming the global-IP bucket on auth-fail would\n //? allow an attacker to trigger DoS for a victim account's IP by deliberately\n //? sending bad credentials.\n if (!(await checkApiAuth({ apiEntry, user, name, emitApiError }))) { cleanupRequest(); return; }\n\n const rateLimitOk = await runSocketRateLimits({ apiEntry, resolvedName, token, socket, user, emitApiError });\n if (!rateLimitOk) { cleanupRequest(); return; }\n\n //? Validation stage (mode resolve + preApiValidate -> validateInputByType ->\n //? postApiValidate). On failure it runs cleanup + emits the GENERIC\n //? `api.invalidInputType` (raw validator message never reaches the client)\n //? via the closures below, then returns false.\n const validationOk = await runSocketApiValidation({\n apiEntry,\n resolvedName,\n normalizedData,\n user,\n cleanupRequest,\n emitInvalidInputType: async () => { await emitApiError({\n response: { status: 'error', errorCode: 'api.invalidInputType' },\n fallbackHttpStatus: 400,\n }); },\n });\n if (!validationOk) return;\n\n //? Execution stage: preApiExecute (stop-signal short-circuit) -> handler ->\n //? postApiExecute. Returns either a stop signal or the (error, result) pair.\n const execution = await runSocketApiExecution({\n apiEntry,\n resolvedName,\n normalizedData,\n user,\n functionsObject,\n emitStream,\n abortSignal,\n abortController,\n flushPressure,\n });\n if (execution.stopped) {\n cleanupRequest();\n await emitApiError({\n response: { status: 'error', errorCode: execution.errorCode },\n fallbackHttpStatus: execution.httpStatus ?? 403,\n });\n return;\n }\n\n await emitApiResult({ socket, responseIndex, resolvedName, error: execution.error, result: execution.result, preferredLocale, user, perRouteFormatter: apiEntry.errorFormatter });\n cleanupRequest();\n}\n","import { getProjectConfig } from '@luckystack/core';\n\n//? CC-5 — config-getter convenience lambdas shared by both API transports.\n//? Read at call time (never at module load) so config registration order\n//? and hot-reload both stay correct.\n\n/** Gate for verbose per-request debug/warn/error logging. */\nexport const shouldLogDev = (): boolean => getProjectConfig().logging.devLogs;\n\n/** Gate for `emitStream` payload logging. */\nexport const shouldLogStream = (): boolean => getProjectConfig().logging.stream;\n","import type { BaseSessionLayout as SessionLayout } from '@luckystack/core';\nimport { defaultHttpStatusForResponse, normalizeErrorResponse, getLogger } from '@luckystack/core';\nimport { shouldLogDev } from './logFlags';\nimport type { ApiResponseEnvelope, RuntimeApiResponse } from './apiTypes';\n\n//? CC-6 — shared response-envelope assembly used by both API transports.\n//? Turns the `(error, result)` pair from the handler into the final wire\n//? envelope: localized error shapes (via `normalizeErrorResponse`) for\n//? failures/empty/invalid responses, or a success envelope with an inferred\n//? default HTTP status. Previously duplicated between `handleApiRequest`'s\n//? `buildApiResponseEnvelope` and `handleHttpApiRequest`'s inline assembly.\n\nexport const normalizeApiResponse = ({\n resolvedName,\n error,\n result,\n preferredLocale,\n user,\n}: {\n resolvedName: string;\n error: Error | null;\n result: RuntimeApiResponse | undefined | null;\n preferredLocale: string | null | undefined;\n user: SessionLayout | null;\n}): ApiResponseEnvelope => {\n if (error) {\n if (shouldLogDev()) {\n getLogger().error(`api: error in ${resolvedName}`, error, { route: resolvedName });\n }\n return {\n ...normalizeErrorResponse({\n response: { status: 'error', errorCode: 'api.internalServerError' },\n preferredLocale,\n userLanguage: user?.language,\n fallbackHttpStatus: 500,\n }),\n };\n }\n\n if (result === undefined || result === null) {\n if (shouldLogDev()) {\n getLogger().warn(`api: ${resolvedName} returned nothing`);\n }\n return {\n ...normalizeErrorResponse({\n response: { status: 'error', errorCode: 'api.emptyResponse' },\n preferredLocale,\n userLanguage: user?.language,\n fallbackHttpStatus: 500,\n }),\n };\n }\n\n if (shouldLogDev()) {\n getLogger().debug(`api: ${resolvedName} completed`);\n }\n\n //? Runtime guard: a handler is user code and can return a status outside the\n //? typed 'success' | 'error' union despite the RuntimeApiResponse type.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- defensive against malformed handler return at runtime\n if (result.status !== 'success' && result.status !== 'error') {\n return {\n ...normalizeErrorResponse({\n response: { status: 'error', errorCode: 'api.invalidResponseStatus' },\n preferredLocale,\n userLanguage: user?.language,\n fallbackHttpStatus: 500,\n }),\n };\n }\n\n if (result.status === 'error') {\n return {\n ...normalizeErrorResponse({\n response: result,\n preferredLocale,\n userLanguage: user?.language,\n fallbackHttpStatus: defaultHttpStatusForResponse({\n status: 'error',\n explicitHttpStatus: typeof result.httpStatus === 'number' ? result.httpStatus : undefined,\n }),\n }),\n };\n }\n\n //? API-O16 — the success envelope spreads ALL handler result fields directly\n //? onto the wire envelope. Handlers are responsible for not including sensitive\n //? data (e.g. `passwordHash`, raw session tokens) in their return value. There\n //? is no framework-level field allowlist; if your route aggregates a Prisma row\n //? with sensitive columns, pick explicit fields before returning.\n return {\n ...result,\n status: 'success',\n httpStatus: defaultHttpStatusForResponse({\n status: 'success',\n explicitHttpStatus: typeof result.httpStatus === 'number' ? result.httpStatus : undefined,\n }),\n };\n};\n","import type { Socket } from 'socket.io';\nimport {\n buildApiStreamEventName,\n getLogger,\n registerApiAbortController,\n unregisterApiAbortController,\n socketEventNames,\n} from '@luckystack/core';\nimport type { ApiStreamPayload, ApiFlushPressure } from './apiTypes';\nimport { shouldLogStream } from './logFlags';\nimport { createApiFlushPressure } from './backpressure';\n\n//? Per-request lifecycle bundle for the SOCKET API transport. Extracted from\n//? `handleApiRequest` per the `api` package audit (god-function decomposition).\n//? Keeps the abortController / cleanup / emitStream / flushPressure closures\n//? together and intact — the orchestrator wires them once and threads the\n//? returned handles through the pipeline. Behaviour is identical to the former\n//? inline closures:\n//? • B1 — per-request AbortController. Aborts on `apiCancel { responseIndex }`\n//? from the originator OR socket disconnect. `cleanupRequest()` runs in\n//? every exit path (errors, validation rejects, completion) to remove the\n//? disconnect listener and drop the cancel-registry entry. The signal is\n//? also handed to `emitStream` so chunks queued after an abort never hit\n//? the wire.\n//? • B2 — backpressure helper bound to the originator socket.\n\nexport interface ApiRequestLifecycle {\n abortController: AbortController;\n abortSignal: AbortSignal;\n emitStream: (payload?: ApiStreamPayload) => void;\n flushPressure: ApiFlushPressure;\n cleanupRequest: () => void;\n}\n\nexport const createApiRequestLifecycle = ({ socket, responseIndex, resolvedName }: {\n socket: Socket;\n responseIndex: number;\n resolvedName: string;\n}): ApiRequestLifecycle => {\n const abortController = new AbortController();\n const abortKey = registerApiAbortController(socket.id, responseIndex, abortController);\n const onSocketDisconnect = () => { abortController.abort(); };\n socket.once(socketEventNames.disconnect, onSocketDisconnect);\n\n let cleanupDone = false;\n const cleanupRequest = () => {\n if (cleanupDone) return;\n cleanupDone = true;\n socket.off(socketEventNames.disconnect, onSocketDisconnect);\n unregisterApiAbortController(abortKey);\n };\n\n const emitStream = (payload: ApiStreamPayload = {}) => {\n if (abortController.signal.aborted) {\n if (shouldLogStream()) {\n getLogger().debug(`api: ${resolvedName} stream skipped — request aborted`);\n }\n return;\n }\n if (shouldLogStream()) {\n getLogger().debug(`api: ${resolvedName} stream`, { payload });\n }\n socket.emit(buildApiStreamEventName(responseIndex), payload);\n };\n\n //? B2 — backpressure helper for API handlers. Same shape as the sync variant\n //? but always scoped to a single originator socket. Polls the engine.io\n //? writeBuffer length every 10ms until the buffer drains below threshold\n //? (default 1 MB ≈ 1024 packets at ~1KB each).\n const flushPressure = createApiFlushPressure(socket, abortController.signal);\n\n return { abortController, abortSignal: abortController.signal, emitStream, flushPressure, cleanupRequest };\n};\n","import type { Socket } from 'socket.io';\nimport { getLogger } from '@luckystack/core';\nimport type { ApiFlushPressure, ApiFlushPressureOptions } from './apiTypes';\nimport { shouldLogDev } from './logFlags';\n\n//? B2 — backpressure helper factory for the SOCKET API transport. Same shape\n//? as the sync variant; resolves once the originator socket's pending write\n//? buffer drops below the threshold. Default 1 MB (≈1024 packets at ~1KB each).\n//? Extracted from `handleApiRequest` per the `api` package audit (solid-isp).\n\ninterface EngineIoConnLike {\n writeBuffer?: { length: number };\n transport?: { writable?: boolean };\n}\n\nexport const createApiFlushPressure = (socket: Socket, abortSignal: AbortSignal): ApiFlushPressure => {\n return async ({ thresholdBytes }: ApiFlushPressureOptions = {}): Promise<void> => {\n const effectiveThresholdBytes = typeof thresholdBytes === 'number' && thresholdBytes > 0\n ? thresholdBytes\n : 1_048_576;\n const packetThreshold = Math.max(1, Math.ceil(effectiveThresholdBytes / 1024));\n //? Polls until the originator's socket write-buffer drains below the packet\n //? threshold, or the request aborts (loop condition), or the transport goes\n //? unwritable. Same exit conditions as the inline socket-handler original.\n while (!abortSignal.aborted) {\n //? Reading engine.io internals (writeBuffer / transport.writable) is the\n //? only way to measure socket backpressure; the cast is the documented\n //? boundary to those untyped internals. Moved verbatim from the socket\n //? handler — same access pattern, no behavioral change.\n // luckystack-allow no-as-unknown: engine.io internal — `conn` is not typed on Socket; this is the documented access pattern\n // eslint-disable-next-line no-restricted-syntax -- engine.io internals boundary\n const conn = (socket as unknown as { conn?: EngineIoConnLike }).conn;\n //? API-O15 — if engine.io renames `conn` this silently becomes a no-op\n //? (buffer always 0, writable always true). Warn in dev so a rename is\n //? caught before backpressure is broken in production.\n if (shouldLogDev() && !conn) {\n getLogger().warn('api: socket.conn absent — engine.io internal may have been renamed; backpressure is a no-op');\n }\n const packets = conn?.writeBuffer?.length ?? 0;\n const writable = conn?.transport?.writable ?? true;\n if (!writable) return;\n if (packets < packetThreshold) return;\n await new Promise<void>((resolve) => setTimeout(resolve, 10));\n }\n };\n};\n\n//? HTTP/SSE has no socket write-buffer to measure — `flushPressure` is a no-op\n//? on that transport. Shared at module scope so per-request allocation stays\n//? cheap and the handler param shape stays consistent across transports.\nexport const httpApiFlushPressureNoop: ApiFlushPressure = async () => {\n /* SSE backpressure is the caller's responsibility (res.write returns bool) */\n};\n","import type { BaseSessionLayout as SessionLayout } from '@luckystack/core';\nimport { dispatchHook, validateInputByType, getLogger } from '@luckystack/core';\nimport type { RuntimeApiEntry } from './apiTypes';\nimport { shouldLogDev } from './logFlags';\nimport { warnIfInputTypeMissing } from './inputTypeWarning';\n\n//? SOCKET-transport input-validation stage. Extracted from `handleApiRequest`\n//? per the `api` package audit (god-function decomposition). Behaviour is\n//? identical to the former inline block:\n//? • resolves the per-route validation mode (`'relaxed'` / `{ input: 'skip' }`\n//? => skip Zod validation; default `'strict'`);\n//? • dispatches `preApiValidate`, runs `validateInputByType` in strict mode,\n//? then `postApiValidate` (always, including the relaxed skip path);\n//? • on validation failure emits the GENERIC `api.invalidInputType` (the raw\n//? validator message is NEVER echoed to the client — it only travels to the\n//? postApiValidate hook + dev logs) and returns `false`.\n//? Error emission + per-request cleanup are delegated to the caller's closures\n//? so the orchestrator keeps ownership of the abort/cleanup/emitError refs.\n\ntype EmitInvalidInputType = () => void | Promise<void>;\n\nexport const resolveValidationMode = (validation: RuntimeApiEntry['validation']): 'strict' | 'relaxed' => {\n if (!validation) return 'strict';\n //? FAIL CLOSED on an unrecognized value (parity with the sync transport's\n //? `resolveSyncValidationMode`). Only the exact string `'relaxed'` (or the\n //? object form `{ input: 'skip' }`) skips validation; ANY other value — a\n //? typo like `'Strict'`/`'on'` — falls back to `'strict'` (validate) rather\n //? than being returned verbatim and silently DISABLING validation.\n if (typeof validation === 'string') {\n return validation === 'relaxed' ? 'relaxed' : 'strict';\n }\n return validation.input === 'skip' ? 'relaxed' : 'strict';\n};\n\nexport const runSocketApiValidation = async ({\n apiEntry,\n resolvedName,\n normalizedData,\n user,\n cleanupRequest,\n emitInvalidInputType,\n}: {\n apiEntry: RuntimeApiEntry;\n resolvedName: string;\n normalizedData: Record<string, unknown>;\n user: SessionLayout | null;\n cleanupRequest: () => void;\n emitInvalidInputType: EmitInvalidInputType;\n}): Promise<boolean> => {\n //? Per-route validation toggle. `'relaxed'` or `{ input: 'skip' }` skips\n //? runtime input validation entirely — useful for public webhooks (Stripe,\n //? Slack, GitHub) where the third party's payload shape isn't reasonable\n //? to model in TypeScript. Default `'strict'`.\n const validationMode = resolveValidationMode(apiEntry.validation);\n\n if (validationMode === 'strict') {\n warnIfInputTypeMissing(resolvedName, apiEntry.inputType);\n }\n await dispatchHook('preApiValidate', { routeName: resolvedName, data: normalizedData, user, transport: 'socket' });\n\n if (validationMode === 'strict') {\n const inputValidation = await validateInputByType({\n typeText: apiEntry.inputType,\n value: normalizedData,\n rootKey: 'data',\n filePath: apiEntry.inputTypeFilePath,\n });\n\n await dispatchHook('postApiValidate', {\n routeName: resolvedName,\n data: normalizedData,\n user,\n validation: inputValidation,\n transport: 'socket',\n });\n\n if (inputValidation.status === 'error') {\n //? SECURITY: do NOT echo the raw validator message (e.g.\n //? \"data.userId should be string\") back to the client — that lets an\n //? unauthenticated caller enumerate a route's input schema. The DETAILED\n //? message is routed to the `postApiValidate` hook above (and dev logs);\n //? the client only receives the generic `api.invalidInputType` code.\n if (shouldLogDev()) {\n getLogger().warn(`api: input validation failed for ${resolvedName}`, { route: resolvedName, message: inputValidation.message });\n }\n cleanupRequest();\n await emitInvalidInputType();\n return false;\n }\n } else {\n //? Relaxed: surface the skip via postApiValidate so audit handlers see it.\n await dispatchHook('postApiValidate', {\n routeName: resolvedName,\n data: normalizedData,\n user,\n validation: { status: 'success' },\n transport: 'socket',\n });\n }\n\n return true;\n};\n","import { getProjectConfig, getLogger } from '@luckystack/core';\n\n//? Dev-only one-shot warning when a route has no generated `inputType`\n//? (runtime Zod input validation is effectively disabled for it). Shared by\n//? both API transports so the \"warned once per route\" set is process-wide\n//? regardless of which transport hit the route first.\n\nconst warnedMissingInputType = new Set<string>();\n\nexport const warnIfInputTypeMissing = (resolvedName: string, inputType: string | undefined): void => {\n if (!getProjectConfig().dev.warnOnMissingInputType) return;\n if (inputType && inputType.trim().length > 0 && inputType.trim() !== 'any') return;\n if (warnedMissingInputType.has(resolvedName)) return;\n warnedMissingInputType.add(resolvedName);\n getLogger().warn(`api: route ${resolvedName} has no inputType — runtime input validation is disabled. Regenerate types or set the inputType on the handler.`);\n};\n","import type { BaseSessionLayout as SessionLayout } from '@luckystack/core';\nimport { checkRateLimit, getProjectConfig, dispatchHook, getLogger } from '@luckystack/core';\nimport { shouldLogDev } from './logFlags';\n\n//? API-O8 — shared rate-limit apply helper for BOTH API transports (socket +\n//? HTTP). Extracted so the two-bucket logic (per-route + global IP) stays in\n//? one place; each transport resolves the effective IP before calling here.\n\nexport interface ApplyApiRateLimitsArgs {\n /** Effective client IP (already resolved by the caller's transport adapter). */\n resolvedIp: string;\n token: string | null;\n user: SessionLayout | null;\n resolvedName: string;\n rateLimit: number | false | undefined;\n transport: 'socket' | 'http';\n /**\n * When true, skip the global-IP (`ip:…:api:all`) bucket check. Used by the\n * HTTP transport to honour `rateLimiting.skipLoopbackInDev` for loopback\n * callers without duplicating bucket logic here. The socket transport never\n * sets this (loopback skip is HTTP-only).\n */\n skipGlobalIpBucket?: boolean;\n}\n\nexport interface ApplyApiRateLimitsResult {\n /** True when all buckets allow the request; false when any bucket exceeded. */\n allowed: boolean;\n /** The error payload to emit when `allowed` is false. */\n errorCode?: string;\n resetIn?: number;\n}\n\n/**\n * Checks per-route + global-IP rate-limit buckets for an API request.\n * Returns `{ allowed: true }` when all buckets allow through, or\n * `{ allowed: false, errorCode, resetIn }` when any bucket is full.\n */\nexport const applyApiRateLimits = async ({\n resolvedIp,\n user,\n resolvedName,\n rateLimit,\n transport,\n skipGlobalIpBucket = false,\n}: ApplyApiRateLimitsArgs): Promise<ApplyApiRateLimitsResult> => {\n const config = getProjectConfig();\n const effectiveApiLimit = rateLimit ?? config.rateLimiting.defaultApiLimit;\n\n if (effectiveApiLimit !== false && effectiveApiLimit > 0) {\n const identityCb = config.rateLimiting.identity;\n const customIdentity = identityCb?.({ routeName: resolvedName, userId: user?.id ?? null, ip: resolvedIp, transport }) ?? null;\n //? Per-route bucket keyed on the VALIDATED user.id (never the token): a token\n //? is per-session, so token-keying let an authenticated abuser reset their\n //? bucket by re-logging-in (new token = fresh counter). user.id ties all of a\n //? user's sessions to one bucket; anonymous callers fall back to the IP.\n const requesterIdentity = customIdentity?.id ?? user?.id ?? resolvedIp;\n const keyPrefix = customIdentity?.scope ?? (user?.id ? 'user' : 'ip');\n const rateLimitKey = `${keyPrefix}:${requesterIdentity}:api:${resolvedName}`;\n\n const { allowed, resetIn } = await checkRateLimit({\n key: rateLimitKey,\n limit: effectiveApiLimit,\n windowMs: config.rateLimiting.windowMs,\n });\n\n if (!allowed) {\n //? The per-route bucket is keyed by the validated user when a token is\n //? present, else by the resolved IP (keyPrefix `ip`). Report the scope\n //? that matches the bucket's actual identity — an anonymous per-route\n //? bucket is IP-keyed, so it is `ip` (with `route` still set to mark it\n //? a per-route bucket vs the global `:api:all` IP bucket), never `route`.\n void dispatchHook('rateLimitExceeded', {\n scope: user?.id ? 'user' : 'ip',\n key: rateLimitKey,\n limit: effectiveApiLimit,\n windowMs: config.rateLimiting.windowMs,\n count: effectiveApiLimit + 1,\n route: resolvedName,\n userId: user?.id,\n //? IP only for an IP-keyed (anonymous) bucket; a user-keyed bucket omits it.\n ip: user?.id ? undefined : resolvedIp,\n });\n if (shouldLogDev()) {\n getLogger().warn(`api: rate limit exceeded for ${resolvedName}`, { route: resolvedName, key: rateLimitKey, transport });\n }\n return { allowed: false, errorCode: 'api.rateLimitExceeded', resetIn };\n }\n }\n\n const defaultIpLimit = config.rateLimiting.defaultIpLimit;\n if (!skipGlobalIpBucket && defaultIpLimit !== false && defaultIpLimit > 0) {\n const { allowed, resetIn } = await checkRateLimit({\n key: `ip:${resolvedIp}:api:all`,\n limit: defaultIpLimit,\n windowMs: config.rateLimiting.windowMs,\n });\n\n if (!allowed) {\n void dispatchHook('rateLimitExceeded', {\n scope: 'ip',\n key: `ip:${resolvedIp}:api:all`,\n limit: defaultIpLimit,\n windowMs: config.rateLimiting.windowMs,\n count: defaultIpLimit + 1,\n ip: resolvedIp,\n });\n if (shouldLogDev()) {\n getLogger().warn(`api: global IP rate limit exceeded`, { ip: resolvedIp, transport });\n }\n return { allowed: false, errorCode: 'api.rateLimitExceeded', resetIn };\n }\n }\n\n return { allowed: true };\n};\n","import type { BaseSessionLayout as SessionLayout, PostApiExecutePayload, ErrorFormatter } from '@luckystack/core';\nimport { getProjectConfig, readSession, getRuntimeApiMaps as getRuntimeApiMapsFromSource ,\n runWithErrorTrackerIdentityScope,\n setCurrentErrorTrackerIdentity,\n validateRequest,\n inferHttpMethod,\n HttpMethod,\n tryCatch,\n parseTransportRouteName,\n dispatchHook,\n getLogger, extractLanguageFromHeader, normalizeErrorResponse, applyErrorFormatter,\n isLoopbackIp, resolveClientIp, isProductionRuntime } from '@luckystack/core';\nimport type { ApiStreamPayload, RuntimeApiEntry } from './_shared/apiTypes';\nimport { shouldLogDev, shouldLogStream } from './_shared/logFlags';\nimport { normalizeApiResponse } from './_shared/responseEnvelope';\nimport { httpApiFlushPressureNoop } from './_shared/backpressure';\nimport { runHttpApiValidation } from './_shared/httpValidationStage';\nimport { applyApiRateLimits } from './_shared/applyApiRateLimits';\n\n\n\n\n\n\n/**\n * HTTP API Request Handler\n * \n * Handles API requests coming via HTTP (instead of WebSocket).\n * Reuses existing API handlers but returns results as HTTP response.\n * \n * Payload format:\n * ```json\n * {\n * \"name\": \"api/examples/publicApi\",\n * \"data\": { \"message\": \"hello\" }\n * }\n * ```\n * \n * @example\n * ```typescript\n * // In server.ts\n * if (pathname.startsWith('/api/')) {\n * const token = extractTokenFromRequest(req);\n * const body = await parseJsonBody(req);\n * const result = await handleHttpApiRequest({ name: body.name, data: body.data, token });\n * res.end(JSON.stringify(result));\n * }\n * ```\n */\n\ninterface HttpApiRequestParams {\n name: string;\n data: Record<string, unknown>;\n token: string | null;\n requesterIp?: string;\n xLanguageHeader?: string | string[];\n acceptLanguageHeader?: string | string[];\n /** HTTP method from the request */\n method?: HttpMethod;\n stream?: (payload: ApiHttpStreamEvent) => void;\n /**\n * Optional AbortSignal. The HTTP server (`@luckystack/server`) wires this\n * to `req.on('close', ...)` so a closed SSE/HTTP connection aborts in-flight\n * stream emits. Handlers receive it as `abortSignal` in params.\n */\n abortSignal?: AbortSignal;\n}\n\ntype ApiNetworkResponse<T = Record<string, unknown>> =\n | ({ status: 'success'; httpStatus: number } & T)\n | {\n status: 'error';\n httpStatus: number;\n message: string;\n errorCode: string;\n errorParams?: {\n key: string;\n value: string | number | boolean;\n }[];\n };\n\nexport type ApiHttpStreamEvent = ApiStreamPayload;\n\n//? ET-02 — wrap the whole HTTP request in a per-request error-tracker identity\n//? scope (opened before `readSession` in `runHandleHttpApiRequest`, the first\n//? interleaving await). The resolved session is written into the scope's ALS box\n//? the moment it resolves, so concurrent HTTP requests with different users can't\n//? cross-attribute captures. Each request gets its own isolated box.\nexport async function handleHttpApiRequest(params: HttpApiRequestParams): Promise<ApiNetworkResponse> {\n return runWithErrorTrackerIdentityScope(() => handleHttpApiRequestScoped(params));\n}\n\nasync function handleHttpApiRequestScoped(params: HttpApiRequestParams): Promise<ApiNetworkResponse> {\n const { response, user, preferredLocale, perRouteFormatter } = await runHandleHttpApiRequest(params);\n\n //? `name` may not parse to a valid route — pass the original raw name as\n //? routeName so hook handlers can still correlate the rejection.\n const routeNameForHook = params.name.startsWith('api/') ? params.name : `api/${params.name}`;\n\n const preRespond = { routeName: routeNameForHook, user, response: response as { status: 'success' | 'error'; httpStatus?: number; [key: string]: unknown } };\n const preRespondResult = await dispatchHook('preApiRespond', preRespond);\n\n //? Honor a stop signal from `preApiRespond`: rewrite the envelope into a\n //? localized error response per the signal's errorCode/httpStatus instead\n //? of letting the original through. Same contract as the socket variant.\n const finalResponse = preRespondResult.stopped\n ? normalizeErrorResponse({\n response: { status: 'error', errorCode: preRespondResult.signal.errorCode },\n preferredLocale,\n userLanguage: user?.language,\n fallbackHttpStatus: preRespondResult.signal.httpStatus ?? 403,\n }) as ApiNetworkResponse\n : preRespond.response as ApiNetworkResponse;\n\n //? `transformApiResponse` fires AFTER preApiRespond and BEFORE the wire\n //? response is finalized. Mirrors the socket handler (handleApiRequest)\n //? so BOTH transports run the same hook sequence per CLAUDE.md. Handlers\n //? mutate `payload.response` in place (header injection, body transform,\n //? response signing).\n const transformPayload = { routeName: routeNameForHook, user, response: finalResponse };\n await dispatchHook('transformApiResponse', transformPayload);\n\n //? Per-route → global → identity error formatter chain. Mirrors the socket\n //? handler so HTTP + WebSocket responses honor the same `errorFormatter`\n //? export on each route. No-op on success envelopes.\n //? `ApiNetworkResponse` is a discriminated union; `applyErrorFormatter`\n //? accepts the wider `Record<string, unknown> & { status?: string }` shape\n //? that union doesn't structurally satisfy. The double-cast is the\n //? documented framework boundary between the union and the formatter input.\n // luckystack-allow no-as-unknown: formatter boundary — ApiNetworkResponse union does not structurally satisfy applyErrorFormatter input; fix requires @luckystack/core type alignment\n // eslint-disable-next-line no-restricted-syntax -- formatter boundary cast\n const formatterInput = transformPayload.response as unknown as Record<string, unknown> & { status?: string };\n const formattedResponse = applyErrorFormatter({\n response: formatterInput,\n routeName: routeNameForHook,\n transport: 'http',\n userId: user?.id,\n perRouteFormatter,\n }) as ApiNetworkResponse;\n\n await dispatchHook('postApiRespond', { routeName: routeNameForHook, user, response: formattedResponse });\n\n return formattedResponse;\n}\n\ninterface RunHandleHttpApiRequestResult {\n response: ApiNetworkResponse;\n user: SessionLayout | null;\n preferredLocale: string | null | undefined;\n perRouteFormatter?: ErrorFormatter;\n}\n\nasync function runHandleHttpApiRequest(params: HttpApiRequestParams): Promise<RunHandleHttpApiRequestResult> {\n const preferredLocale =\n extractLanguageFromHeader(params.xLanguageHeader)\n ?? extractLanguageFromHeader(params.acceptLanguageHeader);\n const user = await readSession(params.token);\n //? ET-02 — bind the resolved session into the active per-request ALS identity\n //? box (opened by `handleHttpApiRequest`) so every capture in this request\n //? attributes to this user. The legacy global is still set via the\n //? `preApiValidate` hook subscriber; the ALS read wins at capture time.\n setCurrentErrorTrackerIdentity(user?.id ? { id: user.id, email: user.email ?? undefined, username: user.name ?? undefined } : null);\n const formatterHolder: { current?: ErrorFormatter } = {};\n const response = await runHandleHttpApiRequestInner(params, user, preferredLocale, formatterHolder);\n return { response, user, preferredLocale, perRouteFormatter: formatterHolder.current };\n}\n\n// ---------------------------------------------------------------------------\n// Private staged helpers for runHandleHttpApiRequestInner\n// ---------------------------------------------------------------------------\n\n/** Shared context threaded through each pipeline stage. */\ninterface HttpApiPipelineContext {\n buildNetworkError: (args: {\n response: { status: 'error'; httpStatus?: number; errorCode?: string; errorParams?: { key: string; value: string | number | boolean }[] };\n fallbackHttpStatus?: number;\n }) => ApiNetworkResponse;\n user: SessionLayout | null;\n preferredLocale: string | null | undefined;\n}\n\n/**\n * Validates the raw request shape (name string, data object, route-name\n * parsing) and resolves the normalised route name + runtime maps.\n * Returns an error response when any guard fails, otherwise the resolved\n * artefacts needed by later stages.\n */\nasync function validateHttpApiRequestShape(\n name: string,\n data: Record<string, unknown>,\n ctx: HttpApiPipelineContext,\n): Promise<\n | ApiNetworkResponse\n | {\n resolvedName: string;\n requestData: Record<string, unknown>;\n apisObject: Record<string, unknown>;\n functionsObject: Record<string, unknown>;\n }\n> {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime defense: caller may pass non-string despite types\n if (!name || typeof name !== 'string') {\n return ctx.buildNetworkError({\n response: { status: 'error', errorCode: 'api.invalidName' },\n fallbackHttpStatus: 400,\n });\n }\n\n //? API-O5 — mirror the socket guard: `typeof [] === 'object'`, so an array\n //? payload would pass the original check and hit a `{...}`-destructuring\n //? handler, causing a 500 instead of the correct 400.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime defense against malformed HTTP payloads despite types\n if (typeof data !== 'object' || data === null || Array.isArray(data)) {\n return ctx.buildNetworkError({\n response: { status: 'error', errorCode: 'api.invalidDataObject' },\n fallbackHttpStatus: 400,\n });\n }\n\n const normalizedName = name.startsWith('api/') ? name : `api/${name}`;\n const parsedRoute = parseTransportRouteName({ value: normalizedName, prefix: 'api' });\n if (parsedRoute.status === 'error') {\n return ctx.buildNetworkError({\n response: {\n status: 'error',\n errorCode: 'routing.invalidServiceRouteName',\n errorParams: [{ key: 'name', value: normalizedName }],\n },\n fallbackHttpStatus: 400,\n });\n }\n\n const resolvedName = parsedRoute.normalizedFullName;\n\n if (shouldLogDev()) {\n getLogger().debug(`http api: ${resolvedName} called`);\n }\n\n const { apisObject, functionsObject } = await getRuntimeApiMapsFromSource();\n\n if (!apisObject[resolvedName]) {\n return ctx.buildNetworkError({\n response: {\n status: 'error',\n errorCode: 'api.notFound',\n errorParams: [{ key: 'name', value: resolvedName }],\n },\n fallbackHttpStatus: 404,\n });\n }\n\n return { resolvedName, requestData: data, apisObject, functionsObject };\n}\n\n/**\n * Runs the login-required guard and the `validateRequest` additional-predicate\n * check. Returns an error response on failure, or `null` to signal success.\n *\n * Pipeline order: auth → rate-limit → method → validate → execute.\n * Auth runs first so unauthenticated probes can't enumerate routes or\n * learn input shape from `inputValidation.message` / method-mismatch params.\n * API-O7 — failed-auth requests do NOT consume a rate-limit bucket here.\n * Brute-force lockout is the responsibility of `@luckystack/login`\n * (per-account lockout counter via `authLockout.ts`); the `apiAuthRejected`\n * hook lets consumers add additional throttles. Consuming a per-IP bucket on\n * auth-fail would let an attacker DoS a victim's IP with bad credentials.\n */\nfunction runHttpApiAuth(\n {\n name,\n resolvedName,\n requesterIp,\n auth,\n }: {\n name: string;\n resolvedName: string;\n requesterIp: string | undefined;\n auth: RuntimeApiEntry['auth'];\n },\n ctx: HttpApiPipelineContext,\n): ApiNetworkResponse | null {\n if (auth.login && !ctx.user?.id) {\n if (shouldLogDev()) {\n getLogger().warn(`http-api: ${name} requires login`, { route: name, transport: 'http' });\n }\n void dispatchHook('apiAuthRejected', {\n routeName: resolvedName,\n reason: 'login-required',\n userId: null,\n transport: 'http',\n ip: requesterIp,\n });\n return ctx.buildNetworkError({\n response: { status: 'error', errorCode: 'auth.required' },\n fallbackHttpStatus: 401,\n });\n }\n\n //? NO bare `if (!user) -> auth.forbidden` here. Public routes\n //? (auth.login: false) must be callable without a session — exactly like the\n //? socket API handler (handleApiRequest) and both sync handlers.\n //? `validateRequest` is the single source of truth for `additional` auth\n //? predicates. (This handler previously forbade ALL anonymous HTTP calls,\n //? diverging from the other three transports and making public routes\n //? unreachable over HTTP — e.g. the test-runner contract/rate-limit sweeps.)\n //? `user` may be null on a public route; validateRequest tolerates it (and\n //? only the login/additional predicates read it). Same `user!` shape the\n //? socket API handler and both sync handlers use.\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n const authResult = validateRequest({ auth, user: ctx.user! });\n if (authResult.status === 'error') {\n if (shouldLogDev()) {\n getLogger().warn(`http-api: auth failed for ${name}`, { route: name, errorCode: authResult.errorCode, transport: 'http' });\n }\n void dispatchHook('apiAuthRejected', {\n routeName: resolvedName,\n reason: authResult.errorCode === 'auth.invalidCondition' ? 'invalid-condition' : 'additional-failed',\n userId: ctx.user?.id ?? null,\n transport: 'http',\n ip: requesterIp,\n failedKey: authResult.errorCode,\n });\n return ctx.buildNetworkError({\n response: {\n status: 'error',\n errorCode: authResult.errorCode ?? 'auth.forbidden',\n errorParams: authResult.errorParams,\n httpStatus: authResult.httpStatus,\n },\n fallbackHttpStatus: authResult.httpStatus ?? 403,\n });\n }\n\n return null;\n}\n\n/**\n * Resolves the effective client IP, computes the loopback-skip flag, and\n * delegates to the shared `applyApiRateLimits` helper.\n * Returns an error response when any bucket is exceeded, `null` otherwise.\n */\nasync function applyHttpApiRateLimits(\n {\n requesterIp,\n token,\n resolvedName,\n rateLimit,\n }: {\n requesterIp: string | undefined;\n token: string | null;\n resolvedName: string;\n rateLimit: number | false | undefined;\n },\n ctx: HttpApiPipelineContext,\n): Promise<ApiNetworkResponse | null> {\n //? API-O8 / API-O9 — resolve effective IP once for both rate-limit buckets.\n //? When the caller did not supply `requesterIp` we fall back to the shared\n //? `UNKNOWN_CLIENT_IP` sentinel so all unresolvable callers collapse into ONE\n //? deterministic bucket instead of mixing `'unknown'` and `'anonymous'`.\n const effectiveIp = requesterIp\n ?? resolveClientIp({ rawAddress: undefined, headers: {} });\n\n //? API-O2 — skip the global per-IP ABUSE limit for loopback traffic when the\n //? consumer explicitly opts in via `rateLimiting.skipLoopbackInDev` (default\n //? false). Only the cross-route `:api:all` bucket is skipped; per-route limits\n //? still apply. Pass the raw `requesterIp` (undefined → '') so only real\n //? loopback addresses qualify — avoids skipping for every unresolvable caller.\n const skipGlobalIpForLoopback = getProjectConfig().rateLimiting.skipLoopbackInDev\n && !isProductionRuntime()\n && isLoopbackIp(requesterIp ?? '');\n\n const rateLimitResult = await applyApiRateLimits({\n resolvedIp: effectiveIp,\n token,\n user: ctx.user,\n resolvedName,\n rateLimit,\n transport: 'http',\n skipGlobalIpBucket: skipGlobalIpForLoopback,\n });\n\n if (!rateLimitResult.allowed) {\n return ctx.buildNetworkError({\n response: {\n status: 'error',\n errorCode: rateLimitResult.errorCode ?? 'api.rateLimitExceeded',\n errorParams: [{ key: 'seconds', value: rateLimitResult.resetIn ?? 0 }],\n },\n fallbackHttpStatus: 429,\n });\n }\n\n return null;\n}\n\n/**\n * Enforces the HTTP method constraint (declared or inferred).\n * Runs post-auth so unauthenticated probes don't learn that the route exists.\n * Returns an error response on mismatch, `null` otherwise.\n */\nfunction checkHttpApiMethod(\n {\n method,\n resolvedName,\n declaredMethod,\n }: {\n method: HttpMethod;\n resolvedName: string;\n declaredMethod: HttpMethod | undefined;\n },\n ctx: HttpApiPipelineContext,\n): ApiNetworkResponse | null {\n const expectedMethod = declaredMethod ?? inferHttpMethod(resolvedName);\n if (method !== expectedMethod) {\n if (shouldLogDev()) {\n getLogger().warn(`http api: method mismatch for ${resolvedName}`, { expected: expectedMethod, got: method });\n }\n return ctx.buildNetworkError({\n response: {\n status: 'error',\n errorCode: 'api.methodNotAllowed',\n errorParams: [{ key: 'method', value: expectedMethod }],\n },\n fallbackHttpStatus: 405,\n });\n }\n\n return null;\n}\n\n/**\n * Runs the handler execution stage: emitter setup, pre/post-execute hooks,\n * server-side timeout race, `main()` invocation, and response-envelope\n * assembly.\n */\nasync function runHttpApiExecution(\n {\n resolvedName,\n requestData,\n functionsObject,\n runtimeApiRoute,\n effectiveAbortSignal,\n stream,\n }: {\n resolvedName: string;\n requestData: Record<string, unknown>;\n functionsObject: Record<string, unknown>;\n runtimeApiRoute: RuntimeApiEntry;\n effectiveAbortSignal: AbortSignal;\n stream: HttpApiRequestParams['stream'];\n },\n ctx: HttpApiPipelineContext,\n): Promise<ApiNetworkResponse> {\n //? SSE has no socket write-buffer to measure — `flushPressure` is a no-op\n //? on HTTP transport. Kept for handler-shape parity with the socket path.\n const flushPressure = httpApiFlushPressureNoop;\n\n const emitApiStream = (payload: ApiStreamPayload = {}) => {\n if (!stream) {\n return;\n }\n if (effectiveAbortSignal.aborted) {\n if (shouldLogStream()) {\n getLogger().debug(`http api: ${resolvedName} stream skipped — request aborted`);\n }\n return;\n }\n\n if (shouldLogStream()) {\n getLogger().debug(`http api: ${resolvedName} stream`, { payload });\n }\n\n stream(payload);\n };\n\n //? Single payload reference reused by pre/post — auto-instrumentation\n //? subscribers in `@luckystack/error-tracking` pin spans via WeakMap on\n //? this object. Mutating `result/error/durationMs` is safe because\n //? handlers observe by-name (not by snapshot).\n const executePayload: PostApiExecutePayload = {\n routeName: resolvedName,\n data: requestData,\n user: ctx.user,\n transport: 'http',\n result: undefined,\n error: null,\n durationMs: 0,\n };\n const preExecuteResult = await dispatchHook('preApiExecute', executePayload);\n if (preExecuteResult.stopped) {\n return ctx.buildNetworkError({\n response: { status: 'error', errorCode: preExecuteResult.signal.errorCode },\n fallbackHttpStatus: preExecuteResult.signal.httpStatus ?? 403,\n });\n }\n\n //? API-O10 — race `main()` against a server-side timeout derived from\n //? `api.requestTimeoutMs`. When the timeout fires first we abort the\n //? effectiveAbortSignal chain and return a localized 504 envelope.\n //? A combined AbortController merges the caller's close signal and the\n //? timeout so the handler sees one unified abort.\n //? Streaming requests are EXEMPT from the wall-clock timeout even when one is\n //? configured: an SSE/stream endpoint is long-lived by design (live LLM tokens,\n //? progress) and a total-duration race would truncate it mid-stream. The\n //? abortSignal (wired to client disconnect) still tears it down when the caller\n //? leaves. Non-streaming requests honor `api.requestTimeoutMs` (default false).\n const timeoutMs = stream ? false : getProjectConfig().api.requestTimeoutMs;\n const combinedController = new AbortController();\n //? Forward the caller's abort (connection close) into the combined signal.\n if (effectiveAbortSignal.aborted) {\n combinedController.abort();\n } else {\n effectiveAbortSignal.addEventListener('abort', () => { combinedController.abort(); }, { once: true });\n }\n\n let timedOut = false;\n let timeoutHandle: ReturnType<typeof setTimeout> | undefined;\n const timeoutPromise = timeoutMs === false\n ? null\n : new Promise<void>((resolve) => {\n timeoutHandle = setTimeout(() => {\n timedOut = true;\n combinedController.abort();\n resolve();\n }, timeoutMs);\n });\n\n const executeStart = Date.now();\n const handlerPromise = tryCatch(\n async () => await runtimeApiRoute.main({\n data: requestData,\n user: ctx.user,\n functions: functionsObject,\n stream: emitApiStream,\n abortSignal: combinedController.signal,\n flushPressure,\n }),\n undefined,\n {\n handler: 'handleHttpApiRequest',\n api: resolvedName,\n userId: ctx.user?.id,\n transport: 'http',\n },\n );\n\n if (timeoutPromise) {\n await Promise.race([handlerPromise, timeoutPromise]);\n }\n clearTimeout(timeoutHandle);\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- timedOut is mutated by the async timeout callback via Promise.race\n if (timedOut) {\n return ctx.buildNetworkError({\n response: { status: 'error', errorCode: 'api.timeout' },\n fallbackHttpStatus: 504,\n });\n }\n\n const [error, result] = await handlerPromise;\n executePayload.result = result;\n executePayload.error = error ?? null;\n executePayload.durationMs = Date.now() - executeStart;\n await dispatchHook('postApiExecute', executePayload);\n\n //? Shared envelope assembly (CC-6) — identical to the socket handler's\n //? `buildApiResponseEnvelope`: localized error shapes for failure / empty /\n //? invalid-status, or a success envelope with inferred default HTTP status.\n //? `normalizeApiResponse` runtime-checks `result.status`, so a handler that\n //? returns a non-status object still maps to `api.invalidResponseStatus`.\n const envelope = normalizeApiResponse({\n resolvedName,\n error: error ?? null,\n result: result ?? undefined,\n preferredLocale: ctx.preferredLocale,\n user: ctx.user,\n });\n return envelope as ApiNetworkResponse;\n}\n\nasync function runHandleHttpApiRequestInner(\n {\n name,\n data,\n token,\n requesterIp,\n xLanguageHeader: _xLanguageHeader,\n acceptLanguageHeader: _acceptLanguageHeader,\n method = 'POST',\n stream,\n abortSignal,\n }: HttpApiRequestParams,\n user: SessionLayout | null,\n preferredLocale: string | null | undefined,\n formatterHolder: { current?: ErrorFormatter },\n): Promise<ApiNetworkResponse> {\n //? B1 — HTTP/SSE transport. The caller (typically `@luckystack/server`'s\n //? SSE bridge) wires `req.on('close', ...)` to a controller and passes its\n //? signal in. If no signal was provided we synthesize a never-aborting one\n //? so the handler param shape stays consistent.\n const effectiveAbortSignal = abortSignal ?? new AbortController().signal;\n\n const buildNetworkError = ({\n response,\n fallbackHttpStatus,\n }: {\n response: { status: 'error'; httpStatus?: number; errorCode?: string; errorParams?: { key: string; value: string | number | boolean; }[] };\n fallbackHttpStatus?: number;\n }): ApiNetworkResponse => {\n return normalizeErrorResponse({\n response,\n preferredLocale,\n userLanguage: user?.language,\n fallbackHttpStatus,\n }) as ApiNetworkResponse;\n };\n\n //? Identity propagation now flows via the `preApiExecute` hook subscriber\n //? registered by `@luckystack/error-tracking`'s `enableErrorTrackingAutoInstrumentation()`.\n //? Direct `setSentryUser` removed from this handler — see migration doc.\n const ctx: HttpApiPipelineContext = { buildNetworkError, user, preferredLocale };\n\n // Stage 1: validate request shape, parse route, resolve runtime maps\n const shapeResult = await validateHttpApiRequestShape(name, data, ctx);\n if ('status' in shapeResult) {\n return shapeResult;\n }\n const { resolvedName, requestData, apisObject, functionsObject } = shapeResult;\n\n const runtimeApiRoute = apisObject[resolvedName] as RuntimeApiEntry;\n formatterHolder.current = runtimeApiRoute.errorFormatter;\n\n // Stage 2: auth (login-required + additional predicates)\n //? Pipeline order: auth → rate-limit → method → validate → execute → respond.\n const authError = runHttpApiAuth({ name, resolvedName, requesterIp, auth: runtimeApiRoute.auth }, ctx);\n if (authError) {\n return authError;\n }\n\n // Stage 3: rate limiting (per-route + global-IP)\n const rateLimitError = await applyHttpApiRateLimits(\n { requesterIp, token, resolvedName, rateLimit: runtimeApiRoute.rateLimit },\n ctx,\n );\n if (rateLimitError) {\n return rateLimitError;\n }\n\n // Stage 4: HTTP method check (post-auth so unauthenticated probes don't learn the route exists)\n const methodError = checkHttpApiMethod(\n { method, resolvedName, declaredMethod: runtimeApiRoute.httpMethod },\n ctx,\n );\n if (methodError) {\n return methodError;\n }\n\n // Stage 5: input-type validation\n //? Input-type validation stage (preApiValidate -> validateInputByType ->\n //? postApiValidate). Extracted to `_shared/httpValidationStage.ts` for\n //? symmetry with the socket handler. On failure the caller builds the\n //? GENERIC `api.invalidInputType` (the raw validator message never reaches\n //? the client). Honors the per-route `validation: 'relaxed'` / `{ input:\n //? 'skip' }` escape hatch on BOTH transports — a public webhook route whose\n //? third-party payload can't be modeled in TS is reachable over HTTP, not\n //? just sockets.\n const httpValidation = await runHttpApiValidation({\n resolvedName,\n inputType: runtimeApiRoute.inputType,\n inputTypeFilePath: runtimeApiRoute.inputTypeFilePath,\n requestData,\n user,\n validation: runtimeApiRoute.validation,\n });\n if (!httpValidation.ok) {\n return buildNetworkError({\n response: {\n status: 'error',\n errorCode: 'api.invalidInputType',\n },\n fallbackHttpStatus: 400,\n });\n }\n\n // Stage 6: execute the API handler and assemble the response envelope\n return runHttpApiExecution(\n { resolvedName, requestData, functionsObject, runtimeApiRoute, effectiveAbortSignal, stream },\n ctx,\n );\n}\n","import type { BaseSessionLayout as SessionLayout } from '@luckystack/core';\nimport { dispatchHook, validateInputByType, getLogger } from '@luckystack/core';\nimport type { RuntimeApiEntry } from './apiTypes';\nimport { shouldLogDev } from './logFlags';\nimport { warnIfInputTypeMissing } from './inputTypeWarning';\nimport { resolveValidationMode } from './socketValidationStage';\n\n//? HTTP-transport input-validation stage. Mirrors the socket variant\n//? (`runSocketApiValidation`) for transport symmetry, extracted from\n//? `runHandleHttpApiRequestInner` per the `api` package audit. Behaviour:\n//? • resolves the per-route validation mode (`'relaxed'` / `{ input: 'skip' }`\n//? => skip Zod validation; default `'strict'`) — identical to the socket\n//? stage, so a public webhook route (Stripe/Slack/GitHub) whose third-party\n//? payload can't be modeled in TS is reachable over HTTP, not just sockets;\n//? • warns once if a strict-mode route has no generated input type;\n//? • dispatches `preApiValidate`, runs `validateInputByType` in strict mode,\n//? then `postApiValidate` (always, including the relaxed skip path);\n//? • on failure returns `{ ok: false }` (the caller builds the GENERIC\n//? `api.invalidInputType` network error — the raw validator message is\n//? NEVER echoed to the client; it only travels to the postApiValidate hook\n//? + dev logs).\n\nexport const runHttpApiValidation = async ({\n resolvedName,\n inputType,\n inputTypeFilePath,\n requestData,\n user,\n validation,\n}: {\n resolvedName: string;\n inputType?: string;\n inputTypeFilePath?: string;\n requestData: Record<string, unknown>;\n user: SessionLayout | null;\n validation: RuntimeApiEntry['validation'];\n}): Promise<{ ok: true } | { ok: false }> => {\n //? Per-route validation toggle, honored identically to the socket stage.\n const validationMode = resolveValidationMode(validation);\n\n // Input-type validation (post-auth so unauthenticated probes don't get input-shape leaks)\n if (validationMode === 'strict') {\n warnIfInputTypeMissing(resolvedName, inputType);\n }\n await dispatchHook('preApiValidate', { routeName: resolvedName, data: requestData, user, transport: 'http' });\n\n if (validationMode === 'relaxed') {\n //? Relaxed: surface the skip via postApiValidate so audit handlers see it.\n await dispatchHook('postApiValidate', {\n routeName: resolvedName,\n data: requestData,\n user,\n validation: { status: 'success' },\n transport: 'http',\n });\n return { ok: true };\n }\n\n const inputValidation = await validateInputByType({\n typeText: inputType,\n value: requestData,\n rootKey: 'data',\n filePath: inputTypeFilePath,\n });\n\n await dispatchHook('postApiValidate', {\n routeName: resolvedName,\n data: requestData,\n user,\n validation: inputValidation,\n transport: 'http',\n });\n\n if (inputValidation.status === 'error') {\n //? SECURITY: do NOT echo the raw validator message (e.g.\n //? \"data.userId should be string\") back to the client — that lets an\n //? unauthenticated caller enumerate a route's input schema. The DETAILED\n //? message is routed to the `postApiValidate` hook + dev logs above; the\n //? client only receives the generic `api.invalidInputType` code.\n if (shouldLogDev()) {\n getLogger().warn(`http-api: input validation failed for ${resolvedName}`, { route: resolvedName, message: inputValidation.message, transport: 'http' });\n }\n return { ok: false };\n }\n\n return { ok: true };\n};\n"],"mappings":"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","names":["getProjectConfig","checkRateLimit","dispatchHook","getLogger","normalizeErrorResponse","getLogger","getLogger","getLogger","getLogger","getLogger","getProjectConfig","getLogger","getLogger","getProjectConfig","dispatchHook","getLogger","getProjectConfig","dispatchHook","getLogger","getLogger","dispatchHook","getProjectConfig","normalizeErrorResponse","checkRateLimit","getProjectConfig","readSession","runWithErrorTrackerIdentityScope","setCurrentErrorTrackerIdentity","validateRequest","tryCatch","parseTransportRouteName","dispatchHook","getLogger","extractLanguageFromHeader","normalizeErrorResponse","applyErrorFormatter","resolveClientIp","dispatchHook","validateInputByType","getLogger","dispatchHook","validateInputByType","getLogger","runWithErrorTrackerIdentityScope","dispatchHook","normalizeErrorResponse","applyErrorFormatter","extractLanguageFromHeader","readSession","setCurrentErrorTrackerIdentity","parseTransportRouteName","getLogger","validateRequest","resolveClientIp","getProjectConfig","tryCatch"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@luckystack/api",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
@@ -56,8 +56,8 @@
|
|
|
56
56
|
"test": "vitest run"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@luckystack/core": "^0.8.
|
|
60
|
-
"@luckystack/error-tracking": "^0.8.
|
|
59
|
+
"@luckystack/core": "^0.8.1",
|
|
60
|
+
"@luckystack/error-tracking": "^0.8.1"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
63
63
|
"@prisma/client": "^6.19.0",
|