@luckystack/api 0.1.9 → 0.2.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 +14 -14
- package/CLAUDE.md +113 -106
- package/LICENSE +21 -21
- package/README.md +106 -106
- package/dist/index.d.ts +4 -2
- package/dist/index.js +779 -503
- package/dist/index.js.map +1 -1
- package/docs/api-request-lifecycle.md +2 -2
- package/docs/auth-flow.md +232 -232
- package/docs/error-handling.md +3 -3
- package/docs/generated-types.md +1 -1
- package/docs/rate-limiting.md +1 -1
- package/package.json +5 -5
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/handleApiRequest.ts","../src/handleHttpApiRequest.ts"],"sourcesContent":["/* eslint-disable unicorn/no-abusive-eslint-disable */\r\n/* eslint-disable */\r\nimport type { apiMessage, PostApiExecutePayload } from '@luckystack/core';\r\nimport { getSession, logout } from '@luckystack/login';\r\nimport type { BaseSessionLayout as SessionLayout } from '@luckystack/login';\r\nimport { getProjectConfig } from '@luckystack/core';\r\nimport type { AuthProps } from '@luckystack/login';\r\nimport { Socket } from 'socket.io';\r\nimport { getRuntimeApiMaps } from '@luckystack/core';\r\nimport {\r\n validateRequest,\r\n checkRateLimit,\r\n tryCatch,\r\n parseTransportRouteName,\r\n buildApiResponseEventName,\r\n buildApiStreamEventName,\r\n dispatchHook,\r\n validateInputByType,\r\n getLogger,\r\n} from '@luckystack/core';\r\nimport {\r\n defaultHttpStatusForResponse,\r\n extractLanguageFromHeader,\r\n normalizeErrorResponse,\r\n applyErrorFormatter,\r\n registerApiAbortController,\r\n unregisterApiAbortController,\r\n socketEventNames,\r\n} from '@luckystack/core';\r\nimport type { ErrorFormatter } from '@luckystack/core';\r\n\r\ninterface handleApiRequestType {\r\n msg: apiMessage,\r\n socket: Socket,\r\n token: string | null,\r\n}\r\n\r\ntype ApiStreamPayload = Record<string, unknown>;\r\n\r\ninterface RuntimeErrorResponse {\r\n status: 'error';\r\n errorCode?: string;\r\n errorParams?: { key: string; value: string | number | boolean; }[];\r\n httpStatus?: number;\r\n message?: string;\r\n [key: string]: unknown;\r\n}\r\n\r\ninterface RuntimeSuccessResponse {\r\n status: 'success';\r\n message?: string;\r\n httpStatus?: number;\r\n [key: string]: unknown;\r\n}\r\n\r\ntype RuntimeApiResponse = RuntimeSuccessResponse | RuntimeErrorResponse;\r\n\r\n//? B2 — backpressure helper for API handlers. Same opt-in pattern as\r\n//? sync; resolves once the originator socket's pending write buffer drops\r\n//? below the threshold. Default 1 MB.\r\nexport interface ApiFlushPressureOptions {\r\n thresholdBytes?: number;\r\n}\r\nexport type ApiFlushPressure = (options?: ApiFlushPressureOptions) => Promise<void>;\r\n\r\ninterface RuntimeApiEntry {\r\n auth: AuthProps;\r\n main: (params: {\r\n data: Record<string, unknown>;\r\n user: SessionLayout | null;\r\n functions: Record<string, unknown>;\r\n stream: (payload?: ApiStreamPayload) => void;\r\n //? B1 — aborts on `apiCancel` or socket disconnect.\r\n abortSignal: AbortSignal;\r\n //? B2 — backpressure helper bound to the originator socket.\r\n flushPressure: ApiFlushPressure;\r\n }) => Promise<RuntimeApiResponse>;\r\n inputType?: string;\r\n inputTypeFilePath?: string;\r\n rateLimit?: number | false;\r\n /**\r\n * Per-route validation strictness.\r\n * `'strict'` (default): runtime Zod validation runs, mismatched payloads\r\n * are rejected with `api.invalidInputType`.\r\n * `'relaxed'` / `{ input: 'skip' }`: skip the validate step. Use for public\r\n * webhooks that receive third-party-shaped payloads you can't model in TS,\r\n * or for migration windows when input shapes are in flux.\r\n */\r\n validation?: 'strict' | 'relaxed' | { input: 'skip' | 'strict' };\r\n /**\r\n * Per-route error response formatter. Receives the normalized error\r\n * envelope + context and returns the shape to emit. Falls back to the\r\n * global formatter from `registerErrorFormatter(...)`, then to the\r\n * framework default `normalizeErrorResponse`. See\r\n * docs/ARCHITECTURE_EXTENSION_POINTS.md for the resolution order.\r\n */\r\n errorFormatter?: ErrorFormatter;\r\n}\r\n\r\ninterface ApiErrorResponse {\r\n status: 'error';\r\n httpStatus?: number;\r\n errorCode?: string;\r\n errorParams?: { key: string; value: string | number | boolean; }[];\r\n}\r\n\r\ntype EmitApiError = (args: { response: ApiErrorResponse; fallbackHttpStatus?: number; }) => void;\r\n\r\nconst shouldLogDev = () => getProjectConfig().logging.devLogs;\r\nconst shouldLogStream = () => getProjectConfig().logging.stream;\r\n\r\n//? Track routes we've already warned about so we don't spam the log every\r\n//? request. Set is module-level — fine for the dev-only warning.\r\nconst warnedMissingInputType = new Set<string>();\r\nconst warnIfInputTypeMissing = (resolvedName: string, inputType: string | undefined): void => {\r\n if (!getProjectConfig().dev.warnOnMissingInputType) return;\r\n if (inputType && inputType.trim().length > 0 && inputType.trim() !== 'any') return;\r\n if (warnedMissingInputType.has(resolvedName)) return;\r\n warnedMissingInputType.add(resolvedName);\r\n getLogger().warn(`api: route ${resolvedName} has no inputType — runtime input validation is disabled. Regenerate types or set the inputType on the handler.`);\r\n};\r\n\r\nconst validateApiMessage = ({ msg, emitApiError }: {\r\n msg: apiMessage;\r\n emitApiError: EmitApiError;\r\n}): { name: string; data: Record<string, unknown> } | null => {\r\n const { name, data, responseIndex } = msg;\r\n\r\n if (!responseIndex && typeof responseIndex !== 'number') {\r\n if (shouldLogDev()) {\r\n getLogger().warn('api: no response index given');\r\n }\r\n return null;\r\n }\r\n\r\n if (!name || !data || typeof name != 'string' || typeof data != 'object') {\r\n emitApiError({\r\n response: { status: 'error', errorCode: 'api.invalidRequest' },\r\n fallbackHttpStatus: 400,\r\n });\r\n return null;\r\n }\r\n\r\n return { name, data: data as Record<string, unknown> };\r\n};\r\n\r\nconst checkApiAuth = ({ apiEntry, user, name, emitApiError }: {\r\n apiEntry: RuntimeApiEntry;\r\n user: SessionLayout | null;\r\n name: string;\r\n emitApiError: EmitApiError;\r\n}): boolean => {\r\n if (apiEntry.auth.login && !user?.id) {\r\n if (shouldLogDev()) {\r\n getLogger().warn(`api: ${name} requires login`, { route: name });\r\n }\r\n emitApiError({\r\n response: { status: 'error', errorCode: 'auth.required' },\r\n fallbackHttpStatus: 401,\r\n });\r\n return false;\r\n }\r\n\r\n const authResult = validateRequest({ auth: apiEntry.auth, user: user! });\r\n if (authResult.status === 'error') {\r\n if (shouldLogDev()) {\r\n getLogger().warn(`api: auth failed for ${name}`, { route: name, errorCode: authResult.errorCode });\r\n }\r\n emitApiError({\r\n response: {\r\n status: 'error',\r\n errorCode: authResult.errorCode || 'auth.forbidden',\r\n errorParams: authResult.errorParams,\r\n httpStatus: authResult.httpStatus,\r\n },\r\n fallbackHttpStatus: authResult.httpStatus ?? 403,\r\n });\r\n return false;\r\n }\r\n\r\n return true;\r\n};\r\n\r\nconst applyApiRateLimits = async ({ apiEntry, resolvedName, token, socket, user, emitApiError }: {\r\n apiEntry: RuntimeApiEntry;\r\n resolvedName: string;\r\n token: string | null;\r\n socket: Socket;\r\n user: SessionLayout | null;\r\n emitApiError: EmitApiError;\r\n}): Promise<boolean> => {\r\n const apiRateLimit = apiEntry.rateLimit;\r\n const effectiveApiLimit = apiRateLimit === undefined\r\n ? getProjectConfig().rateLimiting.defaultApiLimit\r\n : apiRateLimit;\r\n\r\n if (effectiveApiLimit !== false && effectiveApiLimit > 0) {\r\n const requesterIdentity = token ?? socket.handshake.address ?? 'unknown';\r\n const keyPrefix = token ? 'token' : 'ip';\r\n const rateLimitKey = `${keyPrefix}:${requesterIdentity}:api:${resolvedName}`;\r\n\r\n const { allowed, resetIn } = await checkRateLimit({\r\n key: rateLimitKey,\r\n limit: effectiveApiLimit,\r\n windowMs: getProjectConfig().rateLimiting.windowMs,\r\n });\r\n\r\n if (!allowed) {\r\n void dispatchHook('rateLimitExceeded', {\r\n scope: token ? 'user' : 'route',\r\n key: rateLimitKey,\r\n limit: effectiveApiLimit,\r\n windowMs: getProjectConfig().rateLimiting.windowMs,\r\n count: effectiveApiLimit + 1,\r\n route: resolvedName,\r\n userId: user?.id,\r\n });\r\n if (shouldLogDev()) {\r\n getLogger().warn(`api: rate limit exceeded for ${resolvedName}`, { route: resolvedName, key: rateLimitKey });\r\n }\r\n emitApiError({\r\n response: {\r\n status: 'error',\r\n errorCode: 'api.rateLimitExceeded',\r\n errorParams: [{ key: 'seconds', value: resetIn }],\r\n },\r\n fallbackHttpStatus: 429,\r\n });\r\n return false;\r\n }\r\n }\r\n\r\n const defaultIpLimit = getProjectConfig().rateLimiting.defaultIpLimit;\r\n if (defaultIpLimit !== false && defaultIpLimit > 0) {\r\n const requesterIp = socket.handshake.address ?? 'unknown';\r\n\r\n const { allowed, resetIn } = await checkRateLimit({\r\n key: `ip:${requesterIp}:api:all`,\r\n limit: defaultIpLimit,\r\n windowMs: getProjectConfig().rateLimiting.windowMs,\r\n });\r\n\r\n if (!allowed) {\r\n void dispatchHook('rateLimitExceeded', {\r\n scope: 'ip',\r\n key: `ip:${requesterIp}:api:all`,\r\n limit: defaultIpLimit,\r\n windowMs: getProjectConfig().rateLimiting.windowMs,\r\n count: defaultIpLimit + 1,\r\n ip: requesterIp,\r\n });\r\n if (shouldLogDev()) {\r\n getLogger().warn(`api: global IP rate limit exceeded`, { ip: requesterIp });\r\n }\r\n emitApiError({\r\n response: {\r\n status: 'error',\r\n errorCode: 'api.rateLimitExceeded',\r\n errorParams: [{ key: 'seconds', value: resetIn }],\r\n },\r\n fallbackHttpStatus: 429,\r\n });\r\n return false;\r\n }\r\n }\r\n\r\n return true;\r\n};\r\n\r\nconst executeApiHandler = async ({ apiEntry, normalizedData, user, functionsObject, resolvedName, emitStream, abortSignal, flushPressure }: {\r\n apiEntry: RuntimeApiEntry;\r\n normalizedData: Record<string, unknown>;\r\n user: SessionLayout | null;\r\n functionsObject: Record<string, unknown>;\r\n resolvedName: string;\r\n emitStream: (payload?: ApiStreamPayload) => void;\r\n abortSignal: AbortSignal;\r\n flushPressure: ApiFlushPressure;\r\n}): Promise<{ error: Error | null; result: RuntimeApiResponse | undefined; durationMs: number }> => {\r\n //? Span open/close + identity propagation moved to hook subscribers in\r\n //? `@luckystack/error-tracking` (preApiExecute / postApiExecute). This\r\n //? handler is now transport-agnostic instrumentation-wise.\r\n const executeStart = Date.now();\r\n const [error, result] = await tryCatch(\r\n async () => await apiEntry.main({ data: normalizedData, user, functions: functionsObject, stream: emitStream, abortSignal, flushPressure }),\r\n undefined,\r\n {\r\n handler: 'handleApiRequest',\r\n api: resolvedName,\r\n userId: user?.id,\r\n transport: 'socket',\r\n },\r\n );\r\n return { error: error ?? null, result: result ?? undefined, durationMs: Date.now() - executeStart };\r\n};\r\n\r\nconst buildApiResponseEnvelope = ({\r\n resolvedName,\r\n error,\r\n result,\r\n preferredLocale,\r\n user,\r\n}: {\r\n resolvedName: string;\r\n error: Error | null;\r\n result: RuntimeApiResponse | undefined;\r\n preferredLocale: string | undefined;\r\n user: SessionLayout | null;\r\n}): { status: 'success' | 'error'; httpStatus?: number; [key: string]: unknown } => {\r\n if (error) {\r\n if (shouldLogDev()) {\r\n getLogger().error(`api: error in ${resolvedName}`, error, { route: resolvedName });\r\n }\r\n return { ...normalizeErrorResponse({\r\n response: { status: 'error', errorCode: 'api.internalServerError' },\r\n preferredLocale,\r\n userLanguage: user?.language,\r\n fallbackHttpStatus: 500,\r\n }) };\r\n }\r\n\r\n if (result === undefined || result === null) {\r\n if (shouldLogDev()) {\r\n getLogger().warn(`api: ${resolvedName} returned nothing`);\r\n }\r\n return { ...normalizeErrorResponse({\r\n response: { status: 'error', errorCode: 'api.emptyResponse' },\r\n preferredLocale,\r\n userLanguage: user?.language,\r\n fallbackHttpStatus: 500,\r\n }) };\r\n }\r\n\r\n if (shouldLogDev()) {\r\n getLogger().debug(`api: ${resolvedName} completed`);\r\n }\r\n\r\n if (result.status !== 'success' && result.status !== 'error') {\r\n return { ...normalizeErrorResponse({\r\n response: { status: 'error', errorCode: 'api.invalidResponseStatus' },\r\n preferredLocale,\r\n userLanguage: user?.language,\r\n fallbackHttpStatus: 500,\r\n }) };\r\n }\r\n\r\n if (result.status === 'error') {\r\n return { ...normalizeErrorResponse({\r\n response: result,\r\n preferredLocale,\r\n userLanguage: user?.language,\r\n fallbackHttpStatus: defaultHttpStatusForResponse({\r\n status: 'error',\r\n explicitHttpStatus: typeof result.httpStatus === 'number' ? result.httpStatus : undefined,\r\n }),\r\n }) };\r\n }\r\n\r\n return {\r\n ...result,\r\n status: 'success',\r\n httpStatus: defaultHttpStatusForResponse({\r\n status: 'success',\r\n explicitHttpStatus: typeof result.httpStatus === 'number' ? result.httpStatus : undefined,\r\n }),\r\n };\r\n};\r\n\r\nconst emitApiResult = async ({\r\n socket,\r\n responseIndex,\r\n resolvedName,\r\n error,\r\n result,\r\n preferredLocale,\r\n user,\r\n perRouteFormatter,\r\n}: {\r\n socket: Socket;\r\n responseIndex: number;\r\n resolvedName: string;\r\n error: Error | null;\r\n result: RuntimeApiResponse | undefined;\r\n preferredLocale: string | undefined;\r\n user: SessionLayout | null;\r\n perRouteFormatter?: ErrorFormatter;\r\n}): Promise<void> => {\r\n const envelope = buildApiResponseEnvelope({ resolvedName, error, result, preferredLocale, user });\r\n\r\n //? `preApiRespond` handlers may mutate `payload.response` to swap or rewrite\r\n //? the outgoing envelope (PII redaction, response signing, schema injection).\r\n //? A handler may also return a stop signal — when that happens we build a\r\n //? fresh error envelope from the signal's `errorCode` / `httpStatus` and\r\n //? emit that instead of the original.\r\n const preRespond = { routeName: resolvedName, user, response: envelope as { status: 'success' | 'error'; httpStatus?: number; [key: string]: unknown } };\r\n const preRespondResult = await dispatchHook('preApiRespond', preRespond);\r\n\r\n const finalResponse = preRespondResult.stopped\r\n ? { ...normalizeErrorResponse({\r\n response: { status: 'error', errorCode: preRespondResult.signal.errorCode },\r\n preferredLocale,\r\n userLanguage: user?.language,\r\n fallbackHttpStatus: preRespondResult.signal.httpStatus ?? 403,\r\n }) }\r\n : preRespond.response;\r\n\r\n //? `transformApiResponse` fires AFTER preApiRespond and BEFORE socket emit.\r\n //? Designed for response mutation — header injection, body transformation,\r\n //? response signing — that's awkward inside preApiRespond. Handlers mutate\r\n //? `payload.response` in place (same shape as preApiRespond.response).\r\n const transformPayload = { routeName: resolvedName, user, response: finalResponse };\r\n await dispatchHook('transformApiResponse', transformPayload);\r\n\r\n //? Per-route → global → identity error formatter chain. No-op when status\r\n //? is success. Runs AFTER transformApiResponse so consumer hooks can still\r\n //? mutate the envelope before the final shape lands.\r\n const formattedResponse = applyErrorFormatter({\r\n response: transformPayload.response,\r\n routeName: resolvedName,\r\n transport: 'socket',\r\n userId: user?.id,\r\n perRouteFormatter,\r\n }) as { status: 'success' | 'error'; httpStatus?: number; [key: string]: unknown };\r\n\r\n socket.emit(buildApiResponseEventName(responseIndex), formattedResponse);\r\n\r\n //? `postApiRespond` is observation-only — the response is already on the\r\n //? wire. Use it for audit logging, metrics, dual-write replication.\r\n await dispatchHook('postApiRespond', { routeName: resolvedName, user, response: formattedResponse });\r\n};\r\n\r\nexport default async function handleApiRequest({ msg, socket, token }: handleApiRequestType) {\r\n //? This event gets triggered when the client uses the apiRequest function.\r\n //? Validate the message, check auth, then execute the registered handler.\r\n\r\n if (typeof msg != 'object') {\r\n if (shouldLogDev()) {\r\n getLogger().warn('api: socket message was not a json object');\r\n }\r\n return;\r\n }\r\n\r\n const { responseIndex } = msg;\r\n const user = await getSession(token);\r\n //? Identity propagation now flows via the `preApiExecute` hook subscriber\r\n //? registered by `@luckystack/error-tracking`'s `enableErrorTrackingAutoInstrumentation()`.\r\n //? Direct `setSentryUser` removed from this handler — see migration doc.\r\n const preferredLocale =\r\n extractLanguageFromHeader(socket.handshake.headers['x-language'])\r\n || extractLanguageFromHeader(socket.handshake.headers['accept-language'])\r\n || undefined;\r\n\r\n //? Per-route formatter ref + resolved-name ref. Both start undefined and\r\n //? get set once the route is parsed + the apiEntry looked up — pre-lookup\r\n //? errors (invalid message shape, unknown route) emit with global formatter\r\n //? only because there's no apiEntry to read per-route formatter from. After\r\n //? lookup, every emit through `emitApiError` flows through the same chain.\r\n let currentRouteName: string | undefined;\r\n let currentPerRouteFormatter: ErrorFormatter | undefined;\r\n\r\n const emitApiError: EmitApiError = ({ response, fallbackHttpStatus }) => {\r\n const normalized = normalizeErrorResponse({\r\n response,\r\n preferredLocale,\r\n userLanguage: user?.language,\r\n fallbackHttpStatus,\r\n });\r\n const formatted = applyErrorFormatter({\r\n response: normalized as unknown as Record<string, unknown> & { status?: string },\r\n routeName: currentRouteName ?? 'api/unknown',\r\n transport: 'socket',\r\n userId: user?.id,\r\n perRouteFormatter: currentPerRouteFormatter,\r\n });\r\n socket.emit(buildApiResponseEventName(responseIndex), formatted);\r\n };\r\n\r\n const validated = validateApiMessage({ msg, emitApiError });\r\n if (!validated) return;\r\n const { name, data: normalizedData } = validated;\r\n\r\n const parsedRoute = parseTransportRouteName({ value: name, prefix: 'api' });\r\n if (parsedRoute.status === 'error') {\r\n return emitApiError({\r\n response: {\r\n status: 'error',\r\n errorCode: 'routing.invalidServiceRouteName',\r\n errorParams: [{ key: 'name', value: name }],\r\n },\r\n fallbackHttpStatus: 400,\r\n });\r\n }\r\n\r\n const resolvedName = parsedRoute.normalizedFullName;\r\n currentRouteName = resolvedName;\r\n\r\n //? Built-in 'system/logout' needs special handling since it requires socket access.\r\n //? Match the full normalized route to avoid hijacking consumer routes whose final\r\n //? segment happens to be 'logout' (e.g. 'admin/logout/vN').\r\n if (parsedRoute.serviceRoute.normalizedRouteName === 'system/logout') {\r\n await logout({ token, socket, userId: user?.id || null });\r\n return socket.emit(buildApiResponseEventName(responseIndex), {\r\n status: 'success',\r\n httpStatus: 200,\r\n result: true,\r\n });\r\n }\r\n\r\n if (shouldLogDev()) {\r\n getLogger().debug(`api: ${resolvedName} called`);\r\n }\r\n\r\n const { apisObject, functionsObject } = await getRuntimeApiMaps();\r\n\r\n if (!apisObject[resolvedName]) {\r\n return emitApiError({\r\n response: {\r\n status: 'error',\r\n errorCode: 'api.notFound',\r\n errorParams: [{ key: 'name', value: name }],\r\n },\r\n fallbackHttpStatus: 404,\r\n });\r\n }\r\n\r\n const apiEntry = apisObject[resolvedName] as RuntimeApiEntry;\r\n currentPerRouteFormatter = apiEntry.errorFormatter;\r\n\r\n //? B1 — per-request AbortController. Aborts on `apiCancel { responseIndex }`\r\n //? from the originator OR socket disconnect. Cleanup runs in every exit\r\n //? path (errors, validation rejects, completion) to remove the disconnect\r\n //? listener and drop the cancel-registry entry. The signal is also handed\r\n //? to `emitStream` so chunks queued after an abort never hit the wire.\r\n const abortController = new AbortController();\r\n const abortKey = registerApiAbortController(socket.id, responseIndex, abortController);\r\n const onSocketDisconnect = () => { abortController.abort(); };\r\n socket.once(socketEventNames.disconnect, onSocketDisconnect);\r\n let cleanupDone = false;\r\n const cleanupRequest = () => {\r\n if (cleanupDone) return;\r\n cleanupDone = true;\r\n socket.off(socketEventNames.disconnect, onSocketDisconnect);\r\n unregisterApiAbortController(abortKey);\r\n };\r\n\r\n const emitStream = (payload: ApiStreamPayload = {}) => {\r\n if (abortController.signal.aborted) {\r\n if (shouldLogStream()) {\r\n getLogger().debug(`api: ${resolvedName} stream skipped — request aborted`);\r\n }\r\n return;\r\n }\r\n if (shouldLogStream()) {\r\n getLogger().debug(`api: ${resolvedName} stream`, { payload });\r\n }\r\n socket.emit(buildApiStreamEventName(responseIndex), payload);\r\n };\r\n\r\n //? B2 — backpressure helper for API handlers. Same shape as the sync\r\n //? variant but always scoped to a single originator socket. Polls the\r\n //? engine.io writeBuffer length every 10ms until the buffer drains below\r\n //? threshold (default 1 MB ≈ 1024 packets at ~1KB each).\r\n const apiFlushPressure: ApiFlushPressure = async ({ thresholdBytes } = {}) => {\r\n if (abortController.signal.aborted) return;\r\n const effectiveThresholdBytes = typeof thresholdBytes === 'number' && thresholdBytes > 0\r\n ? thresholdBytes\r\n : 1_048_576;\r\n const packetThreshold = Math.max(1, Math.ceil(effectiveThresholdBytes / 1024));\r\n interface EngineIoConnLike {\r\n writeBuffer?: { length: number };\r\n transport?: { writable?: boolean };\r\n }\r\n // eslint-disable-next-line no-constant-condition\r\n while (true) {\r\n if (abortController.signal.aborted) return;\r\n const conn = (socket as unknown as { conn?: EngineIoConnLike }).conn;\r\n const packets = conn?.writeBuffer?.length ?? 0;\r\n const writable = conn?.transport?.writable ?? true;\r\n if (!writable) return;\r\n if (packets < packetThreshold) return;\r\n await new Promise<void>((resolve) => setTimeout(resolve, 10));\r\n }\r\n };\r\n\r\n //? Auth → rate-limit → validate → execute → respond.\r\n //? Auth runs before validate so unauthenticated probes can't enumerate\r\n //? routes or learn input shape from `inputValidation.message`.\r\n if (!checkApiAuth({ apiEntry, user, name, emitApiError })) { cleanupRequest(); return; }\r\n\r\n const rateLimitOk = await applyApiRateLimits({ apiEntry, resolvedName, token, socket, user, emitApiError });\r\n if (!rateLimitOk) { cleanupRequest(); return; }\r\n\r\n //? Per-route validation toggle. `'relaxed'` or `{ input: 'skip' }` skips\r\n //? runtime input validation entirely — useful for public webhooks (Stripe,\r\n //? Slack, GitHub) where the third party's payload shape isn't reasonable\r\n //? to model in TypeScript. Default `'strict'`.\r\n const validationMode = (() => {\r\n const v = apiEntry.validation;\r\n if (!v) return 'strict';\r\n if (typeof v === 'string') return v;\r\n return v.input === 'skip' ? 'relaxed' : 'strict';\r\n })();\r\n\r\n if (validationMode === 'strict') {\r\n warnIfInputTypeMissing(resolvedName, apiEntry.inputType);\r\n }\r\n await dispatchHook('preApiValidate', { routeName: resolvedName, data: normalizedData, user, transport: 'socket' });\r\n\r\n if (validationMode === 'strict') {\r\n const inputValidation = await validateInputByType({\r\n typeText: apiEntry.inputType,\r\n value: normalizedData,\r\n rootKey: 'data',\r\n filePath: apiEntry.inputTypeFilePath,\r\n });\r\n\r\n await dispatchHook('postApiValidate', {\r\n routeName: resolvedName,\r\n data: normalizedData,\r\n user,\r\n validation: inputValidation,\r\n transport: 'socket',\r\n });\r\n\r\n if (inputValidation.status === 'error') {\r\n cleanupRequest();\r\n return emitApiError({\r\n response: {\r\n status: 'error',\r\n errorCode: 'api.invalidInputType',\r\n errorParams: [{ key: 'message', value: inputValidation.message }],\r\n },\r\n fallbackHttpStatus: 400,\r\n });\r\n }\r\n } else {\r\n //? Relaxed: surface the skip via postApiValidate so audit handlers see it.\r\n await dispatchHook('postApiValidate', {\r\n routeName: resolvedName,\r\n data: normalizedData,\r\n user,\r\n validation: { status: 'success' },\r\n transport: 'socket',\r\n });\r\n }\r\n\r\n //? Single payload reference reused by pre/post — auto-instrumentation\r\n //? subscribers in `@luckystack/error-tracking` pin spans via WeakMap on\r\n //? this object. Mutating `result/error/durationMs` is safe because\r\n //? handlers observe by-name (not by snapshot).\r\n const executePayload: PostApiExecutePayload = {\r\n routeName: resolvedName,\r\n data: normalizedData,\r\n user,\r\n transport: 'socket',\r\n result: undefined,\r\n error: null,\r\n durationMs: 0,\r\n };\r\n const preExecuteResult = await dispatchHook('preApiExecute', executePayload);\r\n if (preExecuteResult.stopped) {\r\n cleanupRequest();\r\n return emitApiError({\r\n response: { status: 'error', errorCode: preExecuteResult.signal.errorCode },\r\n fallbackHttpStatus: preExecuteResult.signal.httpStatus ?? 403,\r\n });\r\n }\r\n\r\n const { error, result, durationMs } = await executeApiHandler({\r\n apiEntry,\r\n normalizedData,\r\n user,\r\n functionsObject,\r\n resolvedName,\r\n emitStream,\r\n abortSignal: abortController.signal,\r\n flushPressure: apiFlushPressure,\r\n });\r\n\r\n executePayload.result = result;\r\n executePayload.error = error ?? null;\r\n executePayload.durationMs = durationMs;\r\n await dispatchHook('postApiExecute', executePayload);\r\n\r\n await emitApiResult({ socket, responseIndex, resolvedName, error, result, preferredLocale, user, perRouteFormatter: apiEntry.errorFormatter });\r\n cleanupRequest();\r\n}\r\n","/* eslint-disable @typescript-eslint/no-unnecessary-condition, @typescript-eslint/restrict-template-expressions, @typescript-eslint/prefer-nullish-coalescing */\r\n\r\nimport { getSession } from '@luckystack/login';\r\nimport type { BaseSessionLayout as SessionLayout, AuthProps } from '@luckystack/login';\r\nimport type { PostApiExecutePayload, ErrorFormatter } from '@luckystack/core';\r\nimport { getProjectConfig, getRuntimeApiMaps as getRuntimeApiMapsFromSource ,\r\n validateRequest,\r\n checkRateLimit,\r\n inferHttpMethod,\r\n HttpMethod,\r\n tryCatch,\r\n parseTransportRouteName,\r\n validateInputByType,\r\n dispatchHook,\r\n getLogger, defaultHttpStatusForResponse, extractLanguageFromHeader, normalizeErrorResponse, applyErrorFormatter } from '@luckystack/core';\r\n\r\n\r\n\r\n\r\n\r\n\r\n/**\r\n * HTTP API Request Handler\r\n * \r\n * Handles API requests coming via HTTP (instead of WebSocket).\r\n * Reuses existing API handlers but returns results as HTTP response.\r\n * \r\n * Payload format:\r\n * ```json\r\n * {\r\n * \"name\": \"api/examples/publicApi\",\r\n * \"data\": { \"message\": \"hello\" }\r\n * }\r\n * ```\r\n * \r\n * @example\r\n * ```typescript\r\n * // In server.ts\r\n * if (pathname.startsWith('/api/')) {\r\n * const token = extractTokenFromRequest(req);\r\n * const body = await parseJsonBody(req);\r\n * const result = await handleHttpApiRequest({ name: body.name, data: body.data, token });\r\n * res.end(JSON.stringify(result));\r\n * }\r\n * ```\r\n */\r\n\r\ninterface HttpApiRequestParams {\r\n name: string;\r\n data: Record<string, unknown>;\r\n token: string | null;\r\n requesterIp?: string;\r\n xLanguageHeader?: string | string[];\r\n acceptLanguageHeader?: string | string[];\r\n /** HTTP method from the request */\r\n method?: HttpMethod;\r\n stream?: (payload: ApiHttpStreamEvent) => void;\r\n /**\r\n * Optional AbortSignal. The HTTP server (`@luckystack/server`) wires this\r\n * to `req.on('close', ...)` so a closed SSE/HTTP connection aborts in-flight\r\n * stream emits. Handlers receive it as `abortSignal` in params.\r\n */\r\n abortSignal?: AbortSignal;\r\n}\r\n\r\ntype ApiNetworkResponse<T = Record<string, unknown>> =\r\n | ({ status: 'success'; httpStatus: number } & T)\r\n | {\r\n status: 'error';\r\n httpStatus: number;\r\n message: string;\r\n errorCode: string;\r\n errorParams?: {\r\n key: string;\r\n value: string | number | boolean;\r\n }[];\r\n };\r\n\r\ntype ApiStreamPayload = Record<string, unknown>;\r\n\r\n//? B2 — backpressure helper for HTTP API handlers. SSE has no socket\r\n//? write-buffer, so this is a no-op for HTTP transport — but the shape\r\n//? matches the socket variant so handlers don't branch by transport.\r\nexport interface HttpApiFlushPressureOptions {\r\n thresholdBytes?: number;\r\n}\r\n\r\n//? No-op shared at module scope so per-request allocation stays cheap and\r\n//? the consistent-function-scoping lint rule doesn't trip.\r\nconst httpApiFlushPressureNoop: HttpApiFlushPressure = async () => {\r\n /* SSE backpressure is the caller's responsibility (res.write returns bool) */\r\n};\r\nexport type HttpApiFlushPressure = (options?: HttpApiFlushPressureOptions) => Promise<void>;\r\n\r\ninterface RuntimeApiRoute {\r\n auth: AuthProps;\r\n main: (params: {\r\n data: Record<string, unknown>;\r\n user: SessionLayout | null;\r\n functions: Record<string, unknown>;\r\n stream: (payload?: ApiStreamPayload) => void;\r\n abortSignal: AbortSignal;\r\n flushPressure: HttpApiFlushPressure;\r\n }) => Promise<RuntimeApiResult> | RuntimeApiResult;\r\n inputType?: string;\r\n inputTypeFilePath?: string;\r\n rateLimit?: number | false;\r\n httpMethod?: HttpMethod;\r\n validation?: 'strict' | 'relaxed' | { input: 'skip' | 'strict' };\r\n errorFormatter?: ErrorFormatter;\r\n}\r\n\r\ntype RuntimeApiResult =\r\n | {\r\n status: 'success';\r\n httpStatus?: number;\r\n message?: string;\r\n [key: string]: unknown;\r\n }\r\n | {\r\n status: 'error';\r\n httpStatus?: number;\r\n errorCode?: string;\r\n errorParams?: { key: string; value: string | number | boolean }[];\r\n message?: string;\r\n [key: string]: unknown;\r\n };\r\n\r\nconst isRuntimeApiResult = (value: unknown): value is RuntimeApiResult => {\r\n if (!value || typeof value !== 'object') {\r\n return false;\r\n }\r\n\r\n const status = (value as { status?: unknown }).status;\r\n return status === 'success' || status === 'error';\r\n};\r\n\r\nconst shouldLogDev = () => getProjectConfig().logging.devLogs;\r\nconst shouldLogStream = () => getProjectConfig().logging.stream;\r\n\r\nconst warnedMissingInputType = new Set<string>();\r\nconst warnIfInputTypeMissing = (resolvedName: string, inputType: string | undefined): void => {\r\n if (!getProjectConfig().dev.warnOnMissingInputType) return;\r\n if (inputType && inputType.trim().length > 0 && inputType.trim() !== 'any') return;\r\n if (warnedMissingInputType.has(resolvedName)) return;\r\n warnedMissingInputType.add(resolvedName);\r\n getLogger().warn(`http-api: route ${resolvedName} has no inputType — runtime input validation is disabled.`);\r\n};\r\n\r\nexport type ApiHttpStreamEvent = ApiStreamPayload;\r\n\r\nexport async function handleHttpApiRequest(params: HttpApiRequestParams): Promise<ApiNetworkResponse> {\r\n const { response, user, preferredLocale, perRouteFormatter } = await runHandleHttpApiRequest(params);\r\n\r\n //? `name` may not parse to a valid route — pass the original raw name as\r\n //? routeName so hook handlers can still correlate the rejection.\r\n const routeNameForHook = params.name.startsWith('api/') ? params.name : `api/${params.name}`;\r\n\r\n const preRespond = { routeName: routeNameForHook, user, response: response as { status: 'success' | 'error'; httpStatus?: number; [key: string]: unknown } };\r\n const preRespondResult = await dispatchHook('preApiRespond', preRespond);\r\n\r\n //? Honor a stop signal from `preApiRespond`: rewrite the envelope into a\r\n //? localized error response per the signal's errorCode/httpStatus instead\r\n //? of letting the original through. Same contract as the socket variant.\r\n const finalResponse = preRespondResult.stopped\r\n ? normalizeErrorResponse({\r\n response: { status: 'error', errorCode: preRespondResult.signal.errorCode },\r\n preferredLocale,\r\n userLanguage: user?.language,\r\n fallbackHttpStatus: preRespondResult.signal.httpStatus ?? 403,\r\n }) as ApiNetworkResponse\r\n : preRespond.response as ApiNetworkResponse;\r\n\r\n //? Per-route → global → identity error formatter chain. Mirrors the socket\r\n //? handler so HTTP + WebSocket responses honor the same `errorFormatter`\r\n //? export on each route. No-op on success envelopes.\r\n //? `ApiNetworkResponse` is a discriminated union; `applyErrorFormatter`\r\n //? accepts the wider `Record<string, unknown> & { status?: string }` shape\r\n //? that union doesn't structurally satisfy. The double-cast is the\r\n //? documented framework boundary between the union and the formatter input.\r\n // eslint-disable-next-line no-restricted-syntax -- formatter boundary cast\r\n const formatterInput = finalResponse as unknown as Record<string, unknown> & { status?: string };\r\n const formattedResponse = applyErrorFormatter({\r\n response: formatterInput,\r\n routeName: routeNameForHook,\r\n transport: 'http',\r\n userId: user?.id,\r\n perRouteFormatter,\r\n }) as ApiNetworkResponse;\r\n\r\n await dispatchHook('postApiRespond', { routeName: routeNameForHook, user, response: formattedResponse });\r\n\r\n return formattedResponse;\r\n}\r\n\r\ninterface RunHandleHttpApiRequestResult {\r\n response: ApiNetworkResponse;\r\n user: SessionLayout | null;\r\n preferredLocale: string | null | undefined;\r\n perRouteFormatter?: ErrorFormatter;\r\n}\r\n\r\nasync function runHandleHttpApiRequest(params: HttpApiRequestParams): Promise<RunHandleHttpApiRequestResult> {\r\n const preferredLocale =\r\n extractLanguageFromHeader(params.xLanguageHeader)\r\n ?? extractLanguageFromHeader(params.acceptLanguageHeader);\r\n const user = await getSession(params.token);\r\n const formatterHolder: { current?: ErrorFormatter } = {};\r\n const response = await runHandleHttpApiRequestInner(params, user, preferredLocale, formatterHolder);\r\n return { response, user, preferredLocale, perRouteFormatter: formatterHolder.current };\r\n}\r\n\r\nasync function runHandleHttpApiRequestInner(\r\n {\r\n name,\r\n data,\r\n token,\r\n requesterIp,\r\n xLanguageHeader: _xLanguageHeader,\r\n acceptLanguageHeader: _acceptLanguageHeader,\r\n method = 'POST',\r\n stream,\r\n abortSignal,\r\n }: HttpApiRequestParams,\r\n user: SessionLayout | null,\r\n preferredLocale: string | null | undefined,\r\n formatterHolder: { current?: ErrorFormatter },\r\n): Promise<ApiNetworkResponse> {\r\n //? B1 — HTTP/SSE transport. The caller (typically `@luckystack/server`'s\r\n //? SSE bridge) wires `req.on('close', ...)` to a controller and passes its\r\n //? signal in. If no signal was provided we synthesize a never-aborting one\r\n //? so the handler param shape stays consistent.\r\n const effectiveAbortSignal = abortSignal ?? new AbortController().signal;\r\n //? SSE has no socket write-buffer to measure — `flushPressure` is a no-op\r\n //? on HTTP transport. Kept for handler-shape parity with the socket path.\r\n const flushPressure = httpApiFlushPressureNoop;\r\n\r\n const normalizedName = name.startsWith('api/') ? name : `api/${name}`;\r\n //? Identity propagation now flows via the `preApiExecute` hook subscriber\r\n //? registered by `@luckystack/error-tracking`'s `enableErrorTrackingAutoInstrumentation()`.\r\n //? Direct `setSentryUser` removed from this handler — see migration doc.\r\n\r\n const buildNetworkError = ({\r\n response,\r\n fallbackHttpStatus,\r\n }: {\r\n response: { status: 'error'; httpStatus?: number; errorCode?: string; errorParams?: { key: string; value: string | number | boolean; }[] };\r\n fallbackHttpStatus?: number;\r\n }): ApiNetworkResponse => {\r\n return normalizeErrorResponse({\r\n response,\r\n preferredLocale,\r\n userLanguage: user?.language,\r\n fallbackHttpStatus,\r\n }) as ApiNetworkResponse;\r\n };\r\n\r\n // Validate request format\r\n if (!name || typeof name !== 'string') {\r\n return buildNetworkError({\r\n response: { status: 'error', errorCode: 'api.invalidName' },\r\n fallbackHttpStatus: 400,\r\n });\r\n }\r\n\r\n if (typeof data !== 'object' || data === null) {\r\n return buildNetworkError({\r\n response: { status: 'error', errorCode: 'api.invalidDataObject' },\r\n fallbackHttpStatus: 400,\r\n });\r\n }\r\n\r\n const requestData = data ?? {};\r\n\r\n const parsedRoute = parseTransportRouteName({ value: normalizedName, prefix: 'api' });\r\n if (parsedRoute.status === 'error') {\r\n return buildNetworkError({\r\n response: {\r\n status: 'error',\r\n errorCode: 'routing.invalidServiceRouteName',\r\n errorParams: [{ key: 'name', value: normalizedName }],\r\n },\r\n fallbackHttpStatus: 400,\r\n });\r\n }\r\n\r\n const resolvedName = parsedRoute.normalizedFullName;\r\n\r\n if (shouldLogDev()) {\r\n getLogger().debug(`http api: ${resolvedName} called`);\r\n }\r\n\r\n const { apisObject, functionsObject } = await getRuntimeApiMapsFromSource();\r\n\r\n // Check if API exists\r\n if (!apisObject[resolvedName]) {\r\n return buildNetworkError({\r\n response: {\r\n status: 'error',\r\n errorCode: 'api.notFound',\r\n errorParams: [{ key: 'name', value: resolvedName }],\r\n },\r\n fallbackHttpStatus: 404,\r\n });\r\n }\r\n\r\n const runtimeApiRoute = apisObject[resolvedName] as RuntimeApiRoute;\r\n formatterHolder.current = runtimeApiRoute.errorFormatter;\r\n const { auth, main, httpMethod: declaredMethod } = runtimeApiRoute;\r\n const inputType = runtimeApiRoute.inputType;\r\n const inputTypeFilePath = runtimeApiRoute.inputTypeFilePath;\r\n\r\n //? Pipeline order: auth → rate-limit → method → validate → execute → respond.\r\n //? Auth runs first so unauthenticated probes can't enumerate routes or\r\n //? learn input shape from `inputValidation.message` / method-mismatch params.\r\n\r\n // Auth validation: check login requirement\r\n if (auth.login && !user?.id) {\r\n if (shouldLogDev()) {\r\n getLogger().warn(`http-api: ${name} requires login`, { route: name, transport: 'http' });\r\n }\r\n return buildNetworkError({\r\n response: { status: 'error', errorCode: 'auth.required' },\r\n fallbackHttpStatus: 401,\r\n });\r\n }\r\n\r\n //? NO bare `if (!user) -> auth.forbidden` here. Public routes\r\n //? (auth.login: false) must be callable without a session — exactly like the\r\n //? socket API handler (handleApiRequest) and both sync handlers.\r\n //? `validateRequest` is the single source of truth for `additional` auth\r\n //? predicates. (This handler previously forbade ALL anonymous HTTP calls,\r\n //? diverging from the other three transports and making public routes\r\n //? unreachable over HTTP — e.g. the test-runner contract/rate-limit sweeps.)\r\n //? `user` may be null on a public route; validateRequest tolerates it (and\r\n //? only the login/additional predicates read it). Same `user!` shape the\r\n //? socket API handler and both sync handlers use.\r\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\r\n const authResult = validateRequest({ auth, user: user! });\r\n if (authResult.status === 'error') {\r\n if (shouldLogDev()) {\r\n getLogger().warn(`http-api: auth failed for ${name}`, { route: name, errorCode: authResult.errorCode, transport: 'http' });\r\n }\r\n return buildNetworkError({\r\n response: {\r\n status: 'error',\r\n errorCode: authResult.errorCode ?? 'auth.forbidden',\r\n errorParams: authResult.errorParams,\r\n httpStatus: authResult.httpStatus,\r\n },\r\n fallbackHttpStatus: authResult.httpStatus ?? 403,\r\n });\r\n }\r\n\r\n // Rate limiting check: per-API bucket (custom rateLimit or defaultApiLimit fallback)\r\n const apiRateLimit = runtimeApiRoute.rateLimit;\r\n const effectiveApiLimit = apiRateLimit === undefined\r\n ? getProjectConfig().rateLimiting.defaultApiLimit\r\n : apiRateLimit;\r\n\r\n if (effectiveApiLimit !== false && effectiveApiLimit > 0) {\r\n const requesterIdentity = token ?? requesterIp ?? 'anonymous';\r\n const keyPrefix = token ? 'token' : 'ip';\r\n const rateLimitKey = `${keyPrefix}:${requesterIdentity}:api:${resolvedName}`;\r\n\r\n const { allowed, resetIn } = await checkRateLimit({\r\n key: rateLimitKey,\r\n limit: effectiveApiLimit,\r\n windowMs: getProjectConfig().rateLimiting.windowMs\r\n });\r\n\r\n if (!allowed) {\r\n void dispatchHook('rateLimitExceeded', {\r\n scope: token ? 'user' : 'route',\r\n key: rateLimitKey,\r\n limit: effectiveApiLimit,\r\n windowMs: getProjectConfig().rateLimiting.windowMs,\r\n count: effectiveApiLimit + 1,\r\n route: resolvedName,\r\n userId: user?.id,\r\n });\r\n if (shouldLogDev()) {\r\n getLogger().warn(`http api: rate limit exceeded for ${resolvedName}`);\r\n }\r\n return buildNetworkError({\r\n response: {\r\n status: 'error',\r\n errorCode: 'api.rateLimitExceeded',\r\n errorParams: [{ key: 'seconds', value: resetIn }],\r\n },\r\n fallbackHttpStatus: 429,\r\n });\r\n }\r\n }\r\n\r\n // Global per-IP bucket across all APIs\r\n const defaultIpLimit = getProjectConfig().rateLimiting.defaultIpLimit;\r\n //? Skip the global per-IP ABUSE limit for loopback traffic in non-production.\r\n //? A developer (and the test suite) hammering localhost should not rate-limit\r\n //? itself — this keeps the test runner scalable to any number of cases. Note\r\n //? this only skips the cross-route `:api:all` bucket; PER-ROUTE limits still\r\n //? apply, and production (NODE_ENV=production) is unaffected.\r\n const requesterIsLoopback = process.env.NODE_ENV !== 'production'\r\n && (requesterIp === '127.0.0.1' || requesterIp === '::1' || requesterIp === '::ffff:127.0.0.1'\r\n || (typeof requesterIp === 'string' && requesterIp.startsWith('127.')));\r\n if (!requesterIsLoopback && defaultIpLimit !== false && defaultIpLimit > 0) {\r\n const ipBucket = requesterIp ?? 'unknown';\r\n const { allowed, resetIn } = await checkRateLimit({\r\n key: `ip:${ipBucket}:api:all`,\r\n limit: defaultIpLimit,\r\n windowMs: getProjectConfig().rateLimiting.windowMs\r\n });\r\n\r\n if (!allowed) {\r\n void dispatchHook('rateLimitExceeded', {\r\n scope: 'ip',\r\n key: `ip:${ipBucket}:api:all`,\r\n limit: defaultIpLimit,\r\n windowMs: getProjectConfig().rateLimiting.windowMs,\r\n count: defaultIpLimit + 1,\r\n ip: ipBucket,\r\n });\r\n if (shouldLogDev()) {\r\n getLogger().warn(`http api: global IP rate limit exceeded`, { ip: ipBucket });\r\n }\r\n return buildNetworkError({\r\n response: {\r\n status: 'error',\r\n errorCode: 'api.rateLimitExceeded',\r\n errorParams: [{ key: 'seconds', value: resetIn }],\r\n },\r\n fallbackHttpStatus: 429,\r\n });\r\n }\r\n }\r\n\r\n // HTTP method validation (post-auth so unauthenticated probes don't learn the route exists)\r\n const expectedMethod = declaredMethod ?? inferHttpMethod(resolvedName);\r\n if (method !== expectedMethod) {\r\n if (shouldLogDev()) {\r\n getLogger().warn(`http api: method mismatch for ${resolvedName}`, { expected: expectedMethod, got: method });\r\n }\r\n return buildNetworkError({\r\n response: {\r\n status: 'error',\r\n errorCode: 'api.methodNotAllowed',\r\n errorParams: [{ key: 'method', value: expectedMethod }],\r\n },\r\n fallbackHttpStatus: 405,\r\n });\r\n }\r\n\r\n // Input-type validation (post-auth so unauthenticated probes don't get input-shape leaks)\r\n warnIfInputTypeMissing(resolvedName, inputType);\r\n await dispatchHook('preApiValidate', { routeName: resolvedName, data: requestData, user, transport: 'http' });\r\n\r\n const inputValidation = await validateInputByType({\r\n typeText: inputType,\r\n value: requestData,\r\n rootKey: 'data',\r\n filePath: inputTypeFilePath,\r\n });\r\n\r\n await dispatchHook('postApiValidate', {\r\n routeName: resolvedName,\r\n data: requestData,\r\n user,\r\n validation: inputValidation,\r\n transport: 'http',\r\n });\r\n\r\n if (inputValidation.status === 'error') {\r\n return buildNetworkError({\r\n response: {\r\n status: 'error',\r\n errorCode: 'api.invalidInputType',\r\n errorParams: [{ key: 'message', value: inputValidation.message }],\r\n },\r\n fallbackHttpStatus: 400,\r\n });\r\n }\r\n\r\n // Execute the API handler\r\n const emitApiStream = (payload: ApiStreamPayload = {}) => {\r\n if (!stream) {\r\n return;\r\n }\r\n if (effectiveAbortSignal.aborted) {\r\n if (shouldLogStream()) {\r\n getLogger().debug(`http api: ${resolvedName} stream skipped — request aborted`);\r\n }\r\n return;\r\n }\r\n\r\n if (shouldLogStream()) {\r\n getLogger().debug(`http api: ${resolvedName} stream`, { payload });\r\n }\r\n\r\n stream(payload);\r\n };\r\n\r\n //? Single payload reference reused by pre/post — auto-instrumentation\r\n //? subscribers in `@luckystack/error-tracking` pin spans via WeakMap on\r\n //? this object. Mutating `result/error/durationMs` is safe because\r\n //? handlers observe by-name (not by snapshot).\r\n const executePayload: PostApiExecutePayload = {\r\n routeName: resolvedName,\r\n data: requestData,\r\n user,\r\n transport: 'http',\r\n result: undefined,\r\n error: null,\r\n durationMs: 0,\r\n };\r\n const preExecuteResult = await dispatchHook('preApiExecute', executePayload);\r\n if (preExecuteResult.stopped) {\r\n return buildNetworkError({\r\n response: { status: 'error', errorCode: preExecuteResult.signal.errorCode },\r\n fallbackHttpStatus: preExecuteResult.signal.httpStatus ?? 403,\r\n });\r\n }\r\n\r\n const executeStart = Date.now();\r\n const [error, result] = await tryCatch(\r\n async () => await main({\r\n data: requestData,\r\n user,\r\n functions: functionsObject,\r\n stream: emitApiStream,\r\n abortSignal: effectiveAbortSignal,\r\n flushPressure,\r\n }),\r\n undefined,\r\n {\r\n handler: 'handleHttpApiRequest',\r\n api: resolvedName,\r\n userId: user?.id,\r\n transport: 'http',\r\n },\r\n );\r\n\r\n executePayload.result = result;\r\n executePayload.error = error ?? null;\r\n executePayload.durationMs = Date.now() - executeStart;\r\n await dispatchHook('postApiExecute', executePayload);\r\n\r\n if (error) {\r\n if (shouldLogDev()) {\r\n getLogger().error(`http-api: error in ${resolvedName}`, error, { route: resolvedName, transport: 'http' });\r\n }\r\n return buildNetworkError({\r\n response: { status: 'error', errorCode: 'api.internalServerError' },\r\n fallbackHttpStatus: 500,\r\n });\r\n }\r\n\r\n if (result !== undefined && result !== null) {\r\n if (shouldLogDev()) {\r\n getLogger().debug(`http api: ${resolvedName} completed`);\r\n }\r\n\r\n // Check if result is already formatted as ApiResponse\r\n if (isRuntimeApiResult(result)) {\r\n if (result.status === 'error') {\r\n return buildNetworkError({\r\n response: result,\r\n fallbackHttpStatus: defaultHttpStatusForResponse({\r\n status: 'error',\r\n explicitHttpStatus: typeof result.httpStatus === 'number' ? result.httpStatus : undefined,\r\n }),\r\n });\r\n }\r\n\r\n const successEnvelope: ApiNetworkResponse = {\r\n ...result,\r\n status: 'success',\r\n httpStatus: defaultHttpStatusForResponse({\r\n status: 'success',\r\n explicitHttpStatus: typeof result.httpStatus === 'number' ? result.httpStatus : undefined,\r\n }),\r\n };\r\n return successEnvelope;\r\n }\r\n\r\n return buildNetworkError({\r\n response: { status: 'error', errorCode: 'api.invalidResponseStatus' },\r\n fallbackHttpStatus: 500,\r\n });\r\n }\r\n\r\n if (shouldLogDev()) {\r\n getLogger().warn(`http api: ${resolvedName} returned nothing`);\r\n }\r\n return buildNetworkError({\r\n response: { status: 'error', errorCode: 'api.emptyResponse' },\r\n fallbackHttpStatus: 500,\r\n });\r\n}\r\n"],"mappings":";AAGA,SAAS,YAAY,cAAc;AAEnC,SAAS,wBAAwB;AAGjC,SAAS,yBAAyB;AAClC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAgFP,IAAM,eAAe,MAAM,iBAAiB,EAAE,QAAQ;AACtD,IAAM,kBAAkB,MAAM,iBAAiB,EAAE,QAAQ;AAIzD,IAAM,yBAAyB,oBAAI,IAAY;AAC/C,IAAM,yBAAyB,CAAC,cAAsB,cAAwC;AAC5F,MAAI,CAAC,iBAAiB,EAAE,IAAI,uBAAwB;AACpD,MAAI,aAAa,UAAU,KAAK,EAAE,SAAS,KAAK,UAAU,KAAK,MAAM,MAAO;AAC5E,MAAI,uBAAuB,IAAI,YAAY,EAAG;AAC9C,yBAAuB,IAAI,YAAY;AACvC,YAAU,EAAE,KAAK,cAAc,YAAY,sHAAiH;AAC9J;AAEA,IAAM,qBAAqB,CAAC,EAAE,KAAK,aAAa,MAGc;AAC5D,QAAM,EAAE,MAAM,MAAM,cAAc,IAAI;AAEtC,MAAI,CAAC,iBAAiB,OAAO,kBAAkB,UAAU;AACvD,QAAI,aAAa,GAAG;AAClB,gBAAU,EAAE,KAAK,8BAA8B;AAAA,IACjD;AACA,WAAO;AAAA,EACT;AAEA,MAAI,CAAC,QAAQ,CAAC,QAAQ,OAAO,QAAQ,YAAY,OAAO,QAAQ,UAAU;AACxE,iBAAa;AAAA,MACX,UAAU,EAAE,QAAQ,SAAS,WAAW,qBAAqB;AAAA,MAC7D,oBAAoB;AAAA,IACtB,CAAC;AACD,WAAO;AAAA,EACT;AAEA,SAAO,EAAE,MAAM,KAAsC;AACvD;AAEA,IAAM,eAAe,CAAC,EAAE,UAAU,MAAM,MAAM,aAAa,MAK5C;AACb,MAAI,SAAS,KAAK,SAAS,CAAC,MAAM,IAAI;AACpC,QAAI,aAAa,GAAG;AAClB,gBAAU,EAAE,KAAK,QAAQ,IAAI,mBAAmB,EAAE,OAAO,KAAK,CAAC;AAAA,IACjE;AACA,iBAAa;AAAA,MACX,UAAU,EAAE,QAAQ,SAAS,WAAW,gBAAgB;AAAA,MACxD,oBAAoB;AAAA,IACtB,CAAC;AACD,WAAO;AAAA,EACT;AAEA,QAAM,aAAa,gBAAgB,EAAE,MAAM,SAAS,MAAM,KAAY,CAAC;AACvE,MAAI,WAAW,WAAW,SAAS;AACjC,QAAI,aAAa,GAAG;AAClB,gBAAU,EAAE,KAAK,wBAAwB,IAAI,IAAI,EAAE,OAAO,MAAM,WAAW,WAAW,UAAU,CAAC;AAAA,IACnG;AACA,iBAAa;AAAA,MACX,UAAU;AAAA,QACR,QAAQ;AAAA,QACR,WAAW,WAAW,aAAa;AAAA,QACnC,aAAa,WAAW;AAAA,QACxB,YAAY,WAAW;AAAA,MACzB;AAAA,MACA,oBAAoB,WAAW,cAAc;AAAA,IAC/C,CAAC;AACD,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAEA,IAAM,qBAAqB,OAAO,EAAE,UAAU,cAAc,OAAO,QAAQ,MAAM,aAAa,MAOtE;AACtB,QAAM,eAAe,SAAS;AAC9B,QAAM,oBAAoB,iBAAiB,SACvC,iBAAiB,EAAE,aAAa,kBAChC;AAEJ,MAAI,sBAAsB,SAAS,oBAAoB,GAAG;AACxD,UAAM,oBAAoB,SAAS,OAAO,UAAU,WAAW;AAC/D,UAAM,YAAY,QAAQ,UAAU;AACpC,UAAM,eAAe,GAAG,SAAS,IAAI,iBAAiB,QAAQ,YAAY;AAE1E,UAAM,EAAE,SAAS,QAAQ,IAAI,MAAM,eAAe;AAAA,MAChD,KAAK;AAAA,MACL,OAAO;AAAA,MACP,UAAU,iBAAiB,EAAE,aAAa;AAAA,IAC5C,CAAC;AAED,QAAI,CAAC,SAAS;AACZ,WAAK,aAAa,qBAAqB;AAAA,QACrC,OAAO,QAAQ,SAAS;AAAA,QACxB,KAAK;AAAA,QACL,OAAO;AAAA,QACP,UAAU,iBAAiB,EAAE,aAAa;AAAA,QAC1C,OAAO,oBAAoB;AAAA,QAC3B,OAAO;AAAA,QACP,QAAQ,MAAM;AAAA,MAChB,CAAC;AACD,UAAI,aAAa,GAAG;AAClB,kBAAU,EAAE,KAAK,gCAAgC,YAAY,IAAI,EAAE,OAAO,cAAc,KAAK,aAAa,CAAC;AAAA,MAC7G;AACA,mBAAa;AAAA,QACX,UAAU;AAAA,UACR,QAAQ;AAAA,UACR,WAAW;AAAA,UACX,aAAa,CAAC,EAAE,KAAK,WAAW,OAAO,QAAQ,CAAC;AAAA,QAClD;AAAA,QACA,oBAAoB;AAAA,MACtB,CAAC;AACD,aAAO;AAAA,IACT;AAAA,EACF;AAEA,QAAM,iBAAiB,iBAAiB,EAAE,aAAa;AACvD,MAAI,mBAAmB,SAAS,iBAAiB,GAAG;AAClD,UAAM,cAAc,OAAO,UAAU,WAAW;AAEhD,UAAM,EAAE,SAAS,QAAQ,IAAI,MAAM,eAAe;AAAA,MAChD,KAAK,MAAM,WAAW;AAAA,MACtB,OAAO;AAAA,MACP,UAAU,iBAAiB,EAAE,aAAa;AAAA,IAC5C,CAAC;AAED,QAAI,CAAC,SAAS;AACZ,WAAK,aAAa,qBAAqB;AAAA,QACrC,OAAO;AAAA,QACP,KAAK,MAAM,WAAW;AAAA,QACtB,OAAO;AAAA,QACP,UAAU,iBAAiB,EAAE,aAAa;AAAA,QAC1C,OAAO,iBAAiB;AAAA,QACxB,IAAI;AAAA,MACN,CAAC;AACD,UAAI,aAAa,GAAG;AAClB,kBAAU,EAAE,KAAK,sCAAsC,EAAE,IAAI,YAAY,CAAC;AAAA,MAC5E;AACA,mBAAa;AAAA,QACX,UAAU;AAAA,UACR,QAAQ;AAAA,UACR,WAAW;AAAA,UACX,aAAa,CAAC,EAAE,KAAK,WAAW,OAAO,QAAQ,CAAC;AAAA,QAClD;AAAA,QACA,oBAAoB;AAAA,MACtB,CAAC;AACD,aAAO;AAAA,IACT;AAAA,EACF;AAEA,SAAO;AACT;AAEA,IAAM,oBAAoB,OAAO,EAAE,UAAU,gBAAgB,MAAM,iBAAiB,cAAc,YAAY,aAAa,cAAc,MASrC;AAIlG,QAAM,eAAe,KAAK,IAAI;AAC9B,QAAM,CAAC,OAAO,MAAM,IAAI,MAAM;AAAA,IAC5B,YAAY,MAAM,SAAS,KAAK,EAAE,MAAM,gBAAgB,MAAM,WAAW,iBAAiB,QAAQ,YAAY,aAAa,cAAc,CAAC;AAAA,IAC1I;AAAA,IACA;AAAA,MACE,SAAS;AAAA,MACT,KAAK;AAAA,MACL,QAAQ,MAAM;AAAA,MACd,WAAW;AAAA,IACb;AAAA,EACF;AACA,SAAO,EAAE,OAAO,SAAS,MAAM,QAAQ,UAAU,QAAW,YAAY,KAAK,IAAI,IAAI,aAAa;AACpG;AAEA,IAAM,2BAA2B,CAAC;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAMoF;AAClF,MAAI,OAAO;AACT,QAAI,aAAa,GAAG;AAClB,gBAAU,EAAE,MAAM,iBAAiB,YAAY,IAAI,OAAO,EAAE,OAAO,aAAa,CAAC;AAAA,IACnF;AACA,WAAO,EAAE,GAAG,uBAAuB;AAAA,MACjC,UAAU,EAAE,QAAQ,SAAS,WAAW,0BAA0B;AAAA,MAClE;AAAA,MACA,cAAc,MAAM;AAAA,MACpB,oBAAoB;AAAA,IACtB,CAAC,EAAE;AAAA,EACL;AAEA,MAAI,WAAW,UAAa,WAAW,MAAM;AAC3C,QAAI,aAAa,GAAG;AAClB,gBAAU,EAAE,KAAK,QAAQ,YAAY,mBAAmB;AAAA,IAC1D;AACA,WAAO,EAAE,GAAG,uBAAuB;AAAA,MACjC,UAAU,EAAE,QAAQ,SAAS,WAAW,oBAAoB;AAAA,MAC5D;AAAA,MACA,cAAc,MAAM;AAAA,MACpB,oBAAoB;AAAA,IACtB,CAAC,EAAE;AAAA,EACL;AAEA,MAAI,aAAa,GAAG;AAClB,cAAU,EAAE,MAAM,QAAQ,YAAY,YAAY;AAAA,EACpD;AAEA,MAAI,OAAO,WAAW,aAAa,OAAO,WAAW,SAAS;AAC5D,WAAO,EAAE,GAAG,uBAAuB;AAAA,MACjC,UAAU,EAAE,QAAQ,SAAS,WAAW,4BAA4B;AAAA,MACpE;AAAA,MACA,cAAc,MAAM;AAAA,MACpB,oBAAoB;AAAA,IACtB,CAAC,EAAE;AAAA,EACL;AAEA,MAAI,OAAO,WAAW,SAAS;AAC7B,WAAO,EAAE,GAAG,uBAAuB;AAAA,MACjC,UAAU;AAAA,MACV;AAAA,MACA,cAAc,MAAM;AAAA,MACpB,oBAAoB,6BAA6B;AAAA,QAC/C,QAAQ;AAAA,QACR,oBAAoB,OAAO,OAAO,eAAe,WAAW,OAAO,aAAa;AAAA,MAClF,CAAC;AAAA,IACH,CAAC,EAAE;AAAA,EACL;AAEA,SAAO;AAAA,IACL,GAAG;AAAA,IACH,QAAQ;AAAA,IACR,YAAY,6BAA6B;AAAA,MACvC,QAAQ;AAAA,MACR,oBAAoB,OAAO,OAAO,eAAe,WAAW,OAAO,aAAa;AAAA,IAClF,CAAC;AAAA,EACH;AACF;AAEA,IAAM,gBAAgB,OAAO;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MASqB;AACnB,QAAM,WAAW,yBAAyB,EAAE,cAAc,OAAO,QAAQ,iBAAiB,KAAK,CAAC;AAOhG,QAAM,aAAa,EAAE,WAAW,cAAc,MAAM,UAAU,SAAyF;AACvJ,QAAM,mBAAmB,MAAM,aAAa,iBAAiB,UAAU;AAEvE,QAAM,gBAAgB,iBAAiB,UACnC,EAAE,GAAG,uBAAuB;AAAA,IAC1B,UAAU,EAAE,QAAQ,SAAS,WAAW,iBAAiB,OAAO,UAAU;AAAA,IAC1E;AAAA,IACA,cAAc,MAAM;AAAA,IACpB,oBAAoB,iBAAiB,OAAO,cAAc;AAAA,EAC5D,CAAC,EAAE,IACH,WAAW;AAMf,QAAM,mBAAmB,EAAE,WAAW,cAAc,MAAM,UAAU,cAAc;AAClF,QAAM,aAAa,wBAAwB,gBAAgB;AAK3D,QAAM,oBAAoB,oBAAoB;AAAA,IAC5C,UAAU,iBAAiB;AAAA,IAC3B,WAAW;AAAA,IACX,WAAW;AAAA,IACX,QAAQ,MAAM;AAAA,IACd;AAAA,EACF,CAAC;AAED,SAAO,KAAK,0BAA0B,aAAa,GAAG,iBAAiB;AAIvE,QAAM,aAAa,kBAAkB,EAAE,WAAW,cAAc,MAAM,UAAU,kBAAkB,CAAC;AACrG;AAEA,eAAO,iBAAwC,EAAE,KAAK,QAAQ,MAAM,GAAyB;AAI3F,MAAI,OAAO,OAAO,UAAU;AAC1B,QAAI,aAAa,GAAG;AAClB,gBAAU,EAAE,KAAK,2CAA2C;AAAA,IAC9D;AACA;AAAA,EACF;AAEA,QAAM,EAAE,cAAc,IAAI;AAC1B,QAAM,OAAO,MAAM,WAAW,KAAK;AAInC,QAAM,kBACJ,0BAA0B,OAAO,UAAU,QAAQ,YAAY,CAAC,KAC7D,0BAA0B,OAAO,UAAU,QAAQ,iBAAiB,CAAC,KACrE;AAOL,MAAI;AACJ,MAAI;AAEJ,QAAM,eAA6B,CAAC,EAAE,UAAU,mBAAmB,MAAM;AACvE,UAAM,aAAa,uBAAuB;AAAA,MACxC;AAAA,MACA;AAAA,MACA,cAAc,MAAM;AAAA,MACpB;AAAA,IACF,CAAC;AACD,UAAM,YAAY,oBAAoB;AAAA,MACpC,UAAU;AAAA,MACV,WAAW,oBAAoB;AAAA,MAC/B,WAAW;AAAA,MACX,QAAQ,MAAM;AAAA,MACd,mBAAmB;AAAA,IACrB,CAAC;AACD,WAAO,KAAK,0BAA0B,aAAa,GAAG,SAAS;AAAA,EACjE;AAEA,QAAM,YAAY,mBAAmB,EAAE,KAAK,aAAa,CAAC;AAC1D,MAAI,CAAC,UAAW;AAChB,QAAM,EAAE,MAAM,MAAM,eAAe,IAAI;AAEvC,QAAM,cAAc,wBAAwB,EAAE,OAAO,MAAM,QAAQ,MAAM,CAAC;AAC1E,MAAI,YAAY,WAAW,SAAS;AAClC,WAAO,aAAa;AAAA,MAClB,UAAU;AAAA,QACR,QAAQ;AAAA,QACR,WAAW;AAAA,QACX,aAAa,CAAC,EAAE,KAAK,QAAQ,OAAO,KAAK,CAAC;AAAA,MAC5C;AAAA,MACA,oBAAoB;AAAA,IACtB,CAAC;AAAA,EACH;AAEA,QAAM,eAAe,YAAY;AACjC,qBAAmB;AAKnB,MAAI,YAAY,aAAa,wBAAwB,iBAAiB;AACpE,UAAM,OAAO,EAAE,OAAO,QAAQ,QAAQ,MAAM,MAAM,KAAK,CAAC;AACxD,WAAO,OAAO,KAAK,0BAA0B,aAAa,GAAG;AAAA,MAC3D,QAAQ;AAAA,MACR,YAAY;AAAA,MACZ,QAAQ;AAAA,IACV,CAAC;AAAA,EACH;AAEA,MAAI,aAAa,GAAG;AAClB,cAAU,EAAE,MAAM,QAAQ,YAAY,SAAS;AAAA,EACjD;AAEA,QAAM,EAAE,YAAY,gBAAgB,IAAI,MAAM,kBAAkB;AAEhE,MAAI,CAAC,WAAW,YAAY,GAAG;AAC7B,WAAO,aAAa;AAAA,MAClB,UAAU;AAAA,QACR,QAAQ;AAAA,QACR,WAAW;AAAA,QACX,aAAa,CAAC,EAAE,KAAK,QAAQ,OAAO,KAAK,CAAC;AAAA,MAC5C;AAAA,MACA,oBAAoB;AAAA,IACtB,CAAC;AAAA,EACH;AAEA,QAAM,WAAW,WAAW,YAAY;AACxC,6BAA2B,SAAS;AAOpC,QAAM,kBAAkB,IAAI,gBAAgB;AAC5C,QAAM,WAAW,2BAA2B,OAAO,IAAI,eAAe,eAAe;AACrF,QAAM,qBAAqB,MAAM;AAAE,oBAAgB,MAAM;AAAA,EAAG;AAC5D,SAAO,KAAK,iBAAiB,YAAY,kBAAkB;AAC3D,MAAI,cAAc;AAClB,QAAM,iBAAiB,MAAM;AAC3B,QAAI,YAAa;AACjB,kBAAc;AACd,WAAO,IAAI,iBAAiB,YAAY,kBAAkB;AAC1D,iCAA6B,QAAQ;AAAA,EACvC;AAEA,QAAM,aAAa,CAAC,UAA4B,CAAC,MAAM;AACrD,QAAI,gBAAgB,OAAO,SAAS;AAClC,UAAI,gBAAgB,GAAG;AACrB,kBAAU,EAAE,MAAM,QAAQ,YAAY,wCAAmC;AAAA,MAC3E;AACA;AAAA,IACF;AACA,QAAI,gBAAgB,GAAG;AACrB,gBAAU,EAAE,MAAM,QAAQ,YAAY,WAAW,EAAE,QAAQ,CAAC;AAAA,IAC9D;AACA,WAAO,KAAK,wBAAwB,aAAa,GAAG,OAAO;AAAA,EAC7D;AAMA,QAAM,mBAAqC,OAAO,EAAE,eAAe,IAAI,CAAC,MAAM;AAC5E,QAAI,gBAAgB,OAAO,QAAS;AACpC,UAAM,0BAA0B,OAAO,mBAAmB,YAAY,iBAAiB,IACnF,iBACA;AACJ,UAAM,kBAAkB,KAAK,IAAI,GAAG,KAAK,KAAK,0BAA0B,IAAI,CAAC;AAM7E,WAAO,MAAM;AACX,UAAI,gBAAgB,OAAO,QAAS;AACpC,YAAM,OAAQ,OAAkD;AAChE,YAAM,UAAU,MAAM,aAAa,UAAU;AAC7C,YAAM,WAAW,MAAM,WAAW,YAAY;AAC9C,UAAI,CAAC,SAAU;AACf,UAAI,UAAU,gBAAiB;AAC/B,YAAM,IAAI,QAAc,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AAAA,IAC9D;AAAA,EACF;AAKA,MAAI,CAAC,aAAa,EAAE,UAAU,MAAM,MAAM,aAAa,CAAC,GAAG;AAAE,mBAAe;AAAG;AAAA,EAAQ;AAEvF,QAAM,cAAc,MAAM,mBAAmB,EAAE,UAAU,cAAc,OAAO,QAAQ,MAAM,aAAa,CAAC;AAC1G,MAAI,CAAC,aAAa;AAAE,mBAAe;AAAG;AAAA,EAAQ;AAM9C,QAAM,kBAAkB,MAAM;AAC5B,UAAM,IAAI,SAAS;AACnB,QAAI,CAAC,EAAG,QAAO;AACf,QAAI,OAAO,MAAM,SAAU,QAAO;AAClC,WAAO,EAAE,UAAU,SAAS,YAAY;AAAA,EAC1C,GAAG;AAEH,MAAI,mBAAmB,UAAU;AAC/B,2BAAuB,cAAc,SAAS,SAAS;AAAA,EACzD;AACA,QAAM,aAAa,kBAAkB,EAAE,WAAW,cAAc,MAAM,gBAAgB,MAAM,WAAW,SAAS,CAAC;AAEjH,MAAI,mBAAmB,UAAU;AAC/B,UAAM,kBAAkB,MAAM,oBAAoB;AAAA,MAChD,UAAU,SAAS;AAAA,MACnB,OAAO;AAAA,MACP,SAAS;AAAA,MACT,UAAU,SAAS;AAAA,IACrB,CAAC;AAED,UAAM,aAAa,mBAAmB;AAAA,MACpC,WAAW;AAAA,MACX,MAAM;AAAA,MACN;AAAA,MACA,YAAY;AAAA,MACZ,WAAW;AAAA,IACb,CAAC;AAED,QAAI,gBAAgB,WAAW,SAAS;AACtC,qBAAe;AACf,aAAO,aAAa;AAAA,QAClB,UAAU;AAAA,UACR,QAAQ;AAAA,UACR,WAAW;AAAA,UACX,aAAa,CAAC,EAAE,KAAK,WAAW,OAAO,gBAAgB,QAAQ,CAAC;AAAA,QAClE;AAAA,QACA,oBAAoB;AAAA,MACtB,CAAC;AAAA,IACH;AAAA,EACF,OAAO;AAEL,UAAM,aAAa,mBAAmB;AAAA,MACpC,WAAW;AAAA,MACX,MAAM;AAAA,MACN;AAAA,MACA,YAAY,EAAE,QAAQ,UAAU;AAAA,MAChC,WAAW;AAAA,IACb,CAAC;AAAA,EACH;AAMA,QAAM,iBAAwC;AAAA,IAC5C,WAAW;AAAA,IACX,MAAM;AAAA,IACN;AAAA,IACA,WAAW;AAAA,IACX,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,YAAY;AAAA,EACd;AACA,QAAM,mBAAmB,MAAM,aAAa,iBAAiB,cAAc;AAC3E,MAAI,iBAAiB,SAAS;AAC5B,mBAAe;AACf,WAAO,aAAa;AAAA,MAClB,UAAU,EAAE,QAAQ,SAAS,WAAW,iBAAiB,OAAO,UAAU;AAAA,MAC1E,oBAAoB,iBAAiB,OAAO,cAAc;AAAA,IAC5D,CAAC;AAAA,EACH;AAEA,QAAM,EAAE,OAAO,QAAQ,WAAW,IAAI,MAAM,kBAAkB;AAAA,IAC5D;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,aAAa,gBAAgB;AAAA,IAC7B,eAAe;AAAA,EACjB,CAAC;AAED,iBAAe,SAAS;AACxB,iBAAe,QAAQ,SAAS;AAChC,iBAAe,aAAa;AAC5B,QAAM,aAAa,kBAAkB,cAAc;AAEnD,QAAM,cAAc,EAAE,QAAQ,eAAe,cAAc,OAAO,QAAQ,iBAAiB,MAAM,mBAAmB,SAAS,eAAe,CAAC;AAC7I,iBAAe;AACjB;;;AC5qBA,SAAS,cAAAA,mBAAkB;AAG3B;AAAA,EAAS,oBAAAC;AAAA,EAAkB,qBAAqB;AAAA,EAC9C,mBAAAC;AAAA,EACA,kBAAAC;AAAA,EACA;AAAA,EAEA,YAAAC;AAAA,EACA,2BAAAC;AAAA,EACA,uBAAAC;AAAA,EACA,gBAAAC;AAAA,EACA,aAAAC;AAAA,EAAW,gCAAAC;AAAA,EAA8B,6BAAAC;AAAA,EAA2B,0BAAAC;AAAA,EAAwB,uBAAAC;AAAA,OAA4B;AA2E1H,IAAM,2BAAiD,YAAY;AAEnE;AAqCA,IAAM,qBAAqB,CAAC,UAA8C;AACxE,MAAI,CAAC,SAAS,OAAO,UAAU,UAAU;AACvC,WAAO;AAAA,EACT;AAEA,QAAM,SAAU,MAA+B;AAC/C,SAAO,WAAW,aAAa,WAAW;AAC5C;AAEA,IAAMC,gBAAe,MAAMZ,kBAAiB,EAAE,QAAQ;AACtD,IAAMa,mBAAkB,MAAMb,kBAAiB,EAAE,QAAQ;AAEzD,IAAMc,0BAAyB,oBAAI,IAAY;AAC/C,IAAMC,0BAAyB,CAAC,cAAsB,cAAwC;AAC5F,MAAI,CAACf,kBAAiB,EAAE,IAAI,uBAAwB;AACpD,MAAI,aAAa,UAAU,KAAK,EAAE,SAAS,KAAK,UAAU,KAAK,MAAM,MAAO;AAC5E,MAAIc,wBAAuB,IAAI,YAAY,EAAG;AAC9C,EAAAA,wBAAuB,IAAI,YAAY;AACvC,EAAAP,WAAU,EAAE,KAAK,mBAAmB,YAAY,gEAA2D;AAC7G;AAIA,eAAsB,qBAAqB,QAA2D;AACpG,QAAM,EAAE,UAAU,MAAM,iBAAiB,kBAAkB,IAAI,MAAM,wBAAwB,MAAM;AAInG,QAAM,mBAAmB,OAAO,KAAK,WAAW,MAAM,IAAI,OAAO,OAAO,OAAO,OAAO,IAAI;AAE1F,QAAM,aAAa,EAAE,WAAW,kBAAkB,MAAM,SAAmG;AAC3J,QAAM,mBAAmB,MAAMD,cAAa,iBAAiB,UAAU;AAKvE,QAAM,gBAAgB,iBAAiB,UACnCI,wBAAuB;AAAA,IACrB,UAAU,EAAE,QAAQ,SAAS,WAAW,iBAAiB,OAAO,UAAU;AAAA,IAC1E;AAAA,IACA,cAAc,MAAM;AAAA,IACpB,oBAAoB,iBAAiB,OAAO,cAAc;AAAA,EAC5D,CAAC,IACD,WAAW;AAUf,QAAM,iBAAiB;AACvB,QAAM,oBAAoBC,qBAAoB;AAAA,IAC5C,UAAU;AAAA,IACV,WAAW;AAAA,IACX,WAAW;AAAA,IACX,QAAQ,MAAM;AAAA,IACd;AAAA,EACF,CAAC;AAED,QAAML,cAAa,kBAAkB,EAAE,WAAW,kBAAkB,MAAM,UAAU,kBAAkB,CAAC;AAEvG,SAAO;AACT;AASA,eAAe,wBAAwB,QAAsE;AAC3G,QAAM,kBACJG,2BAA0B,OAAO,eAAe,KAC7CA,2BAA0B,OAAO,oBAAoB;AAC1D,QAAM,OAAO,MAAMV,YAAW,OAAO,KAAK;AAC1C,QAAM,kBAAgD,CAAC;AACvD,QAAM,WAAW,MAAM,6BAA6B,QAAQ,MAAM,iBAAiB,eAAe;AAClG,SAAO,EAAE,UAAU,MAAM,iBAAiB,mBAAmB,gBAAgB,QAAQ;AACvF;AAEA,eAAe,6BACb;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,iBAAiB;AAAA,EACjB,sBAAsB;AAAA,EACtB,SAAS;AAAA,EACT;AAAA,EACA;AACF,GACA,MACA,iBACA,iBAC6B;AAK7B,QAAM,uBAAuB,eAAe,IAAI,gBAAgB,EAAE;AAGlE,QAAM,gBAAgB;AAEtB,QAAM,iBAAiB,KAAK,WAAW,MAAM,IAAI,OAAO,OAAO,IAAI;AAKnE,QAAM,oBAAoB,CAAC;AAAA,IACzB;AAAA,IACA;AAAA,EACF,MAG0B;AACxB,WAAOW,wBAAuB;AAAA,MAC5B;AAAA,MACA;AAAA,MACA,cAAc,MAAM;AAAA,MACpB;AAAA,IACF,CAAC;AAAA,EACH;AAGA,MAAI,CAAC,QAAQ,OAAO,SAAS,UAAU;AACrC,WAAO,kBAAkB;AAAA,MACvB,UAAU,EAAE,QAAQ,SAAS,WAAW,kBAAkB;AAAA,MAC1D,oBAAoB;AAAA,IACtB,CAAC;AAAA,EACH;AAEA,MAAI,OAAO,SAAS,YAAY,SAAS,MAAM;AAC7C,WAAO,kBAAkB;AAAA,MACvB,UAAU,EAAE,QAAQ,SAAS,WAAW,wBAAwB;AAAA,MAChE,oBAAoB;AAAA,IACtB,CAAC;AAAA,EACH;AAEA,QAAM,cAAc,QAAQ,CAAC;AAE7B,QAAM,cAAcN,yBAAwB,EAAE,OAAO,gBAAgB,QAAQ,MAAM,CAAC;AACpF,MAAI,YAAY,WAAW,SAAS;AAClC,WAAO,kBAAkB;AAAA,MACvB,UAAU;AAAA,QACR,QAAQ;AAAA,QACR,WAAW;AAAA,QACX,aAAa,CAAC,EAAE,KAAK,QAAQ,OAAO,eAAe,CAAC;AAAA,MACtD;AAAA,MACA,oBAAoB;AAAA,IACtB,CAAC;AAAA,EACH;AAEA,QAAM,eAAe,YAAY;AAEjC,MAAIQ,cAAa,GAAG;AAClB,IAAAL,WAAU,EAAE,MAAM,aAAa,YAAY,SAAS;AAAA,EACtD;AAEA,QAAM,EAAE,YAAY,gBAAgB,IAAI,MAAM,4BAA4B;AAG1E,MAAI,CAAC,WAAW,YAAY,GAAG;AAC7B,WAAO,kBAAkB;AAAA,MACvB,UAAU;AAAA,QACR,QAAQ;AAAA,QACR,WAAW;AAAA,QACX,aAAa,CAAC,EAAE,KAAK,QAAQ,OAAO,aAAa,CAAC;AAAA,MACpD;AAAA,MACA,oBAAoB;AAAA,IACtB,CAAC;AAAA,EACH;AAEA,QAAM,kBAAkB,WAAW,YAAY;AAC/C,kBAAgB,UAAU,gBAAgB;AAC1C,QAAM,EAAE,MAAM,MAAM,YAAY,eAAe,IAAI;AACnD,QAAM,YAAY,gBAAgB;AAClC,QAAM,oBAAoB,gBAAgB;AAO1C,MAAI,KAAK,SAAS,CAAC,MAAM,IAAI;AACzB,QAAIK,cAAa,GAAG;AAClB,MAAAL,WAAU,EAAE,KAAK,aAAa,IAAI,mBAAmB,EAAE,OAAO,MAAM,WAAW,OAAO,CAAC;AAAA,IACzF;AACA,WAAO,kBAAkB;AAAA,MACvB,UAAU,EAAE,QAAQ,SAAS,WAAW,gBAAgB;AAAA,MACxD,oBAAoB;AAAA,IACtB,CAAC;AAAA,EACH;AAaF,QAAM,aAAaN,iBAAgB,EAAE,MAAM,KAAY,CAAC;AACxD,MAAI,WAAW,WAAW,SAAS;AACjC,QAAIW,cAAa,GAAG;AAClB,MAAAL,WAAU,EAAE,KAAK,6BAA6B,IAAI,IAAI,EAAE,OAAO,MAAM,WAAW,WAAW,WAAW,WAAW,OAAO,CAAC;AAAA,IAC3H;AACA,WAAO,kBAAkB;AAAA,MACvB,UAAU;AAAA,QACR,QAAQ;AAAA,QACR,WAAW,WAAW,aAAa;AAAA,QACnC,aAAa,WAAW;AAAA,QACxB,YAAY,WAAW;AAAA,MACzB;AAAA,MACA,oBAAoB,WAAW,cAAc;AAAA,IAC/C,CAAC;AAAA,EACH;AAGA,QAAM,eAAe,gBAAgB;AACrC,QAAM,oBAAoB,iBAAiB,SACvCP,kBAAiB,EAAE,aAAa,kBAChC;AAEJ,MAAI,sBAAsB,SAAS,oBAAoB,GAAG;AACxD,UAAM,oBAAoB,SAAS,eAAe;AAClD,UAAM,YAAY,QAAQ,UAAU;AACpC,UAAM,eAAe,GAAG,SAAS,IAAI,iBAAiB,QAAQ,YAAY;AAE1E,UAAM,EAAE,SAAS,QAAQ,IAAI,MAAME,gBAAe;AAAA,MAChD,KAAK;AAAA,MACL,OAAO;AAAA,MACP,UAAUF,kBAAiB,EAAE,aAAa;AAAA,IAC5C,CAAC;AAED,QAAI,CAAC,SAAS;AACZ,WAAKM,cAAa,qBAAqB;AAAA,QACrC,OAAO,QAAQ,SAAS;AAAA,QACxB,KAAK;AAAA,QACL,OAAO;AAAA,QACP,UAAUN,kBAAiB,EAAE,aAAa;AAAA,QAC1C,OAAO,oBAAoB;AAAA,QAC3B,OAAO;AAAA,QACP,QAAQ,MAAM;AAAA,MAChB,CAAC;AACD,UAAIY,cAAa,GAAG;AAClB,QAAAL,WAAU,EAAE,KAAK,qCAAqC,YAAY,EAAE;AAAA,MACtE;AACA,aAAO,kBAAkB;AAAA,QACvB,UAAU;AAAA,UACR,QAAQ;AAAA,UACR,WAAW;AAAA,UACX,aAAa,CAAC,EAAE,KAAK,WAAW,OAAO,QAAQ,CAAC;AAAA,QAClD;AAAA,QACA,oBAAoB;AAAA,MACtB,CAAC;AAAA,IACH;AAAA,EACF;AAGA,QAAM,iBAAiBP,kBAAiB,EAAE,aAAa;AAMvD,QAAM,sBAAsB,QAAQ,IAAI,aAAa,iBAC/C,gBAAgB,eAAe,gBAAgB,SAAS,gBAAgB,sBACtE,OAAO,gBAAgB,YAAY,YAAY,WAAW,MAAM;AACxE,MAAI,CAAC,uBAAuB,mBAAmB,SAAS,iBAAiB,GAAG;AAC1E,UAAM,WAAW,eAAe;AAChC,UAAM,EAAE,SAAS,QAAQ,IAAI,MAAME,gBAAe;AAAA,MAChD,KAAK,MAAM,QAAQ;AAAA,MACnB,OAAO;AAAA,MACP,UAAUF,kBAAiB,EAAE,aAAa;AAAA,IAC5C,CAAC;AAED,QAAI,CAAC,SAAS;AACZ,WAAKM,cAAa,qBAAqB;AAAA,QACrC,OAAO;AAAA,QACP,KAAK,MAAM,QAAQ;AAAA,QACnB,OAAO;AAAA,QACP,UAAUN,kBAAiB,EAAE,aAAa;AAAA,QAC1C,OAAO,iBAAiB;AAAA,QACxB,IAAI;AAAA,MACN,CAAC;AACD,UAAIY,cAAa,GAAG;AAClB,QAAAL,WAAU,EAAE,KAAK,2CAA2C,EAAE,IAAI,SAAS,CAAC;AAAA,MAC9E;AACA,aAAO,kBAAkB;AAAA,QACvB,UAAU;AAAA,UACR,QAAQ;AAAA,UACR,WAAW;AAAA,UACX,aAAa,CAAC,EAAE,KAAK,WAAW,OAAO,QAAQ,CAAC;AAAA,QAClD;AAAA,QACA,oBAAoB;AAAA,MACtB,CAAC;AAAA,IACH;AAAA,EACF;AAGA,QAAM,iBAAiB,kBAAkB,gBAAgB,YAAY;AACrE,MAAI,WAAW,gBAAgB;AAC7B,QAAIK,cAAa,GAAG;AAClB,MAAAL,WAAU,EAAE,KAAK,iCAAiC,YAAY,IAAI,EAAE,UAAU,gBAAgB,KAAK,OAAO,CAAC;AAAA,IAC7G;AACA,WAAO,kBAAkB;AAAA,MACvB,UAAU;AAAA,QACR,QAAQ;AAAA,QACR,WAAW;AAAA,QACX,aAAa,CAAC,EAAE,KAAK,UAAU,OAAO,eAAe,CAAC;AAAA,MACxD;AAAA,MACA,oBAAoB;AAAA,IACtB,CAAC;AAAA,EACH;AAGA,EAAAQ,wBAAuB,cAAc,SAAS;AAC9C,QAAMT,cAAa,kBAAkB,EAAE,WAAW,cAAc,MAAM,aAAa,MAAM,WAAW,OAAO,CAAC;AAE5G,QAAM,kBAAkB,MAAMD,qBAAoB;AAAA,IAChD,UAAU;AAAA,IACV,OAAO;AAAA,IACP,SAAS;AAAA,IACT,UAAU;AAAA,EACZ,CAAC;AAED,QAAMC,cAAa,mBAAmB;AAAA,IACpC,WAAW;AAAA,IACX,MAAM;AAAA,IACN;AAAA,IACA,YAAY;AAAA,IACZ,WAAW;AAAA,EACb,CAAC;AAED,MAAI,gBAAgB,WAAW,SAAS;AACtC,WAAO,kBAAkB;AAAA,MACvB,UAAU;AAAA,QACR,QAAQ;AAAA,QACR,WAAW;AAAA,QACX,aAAa,CAAC,EAAE,KAAK,WAAW,OAAO,gBAAgB,QAAQ,CAAC;AAAA,MAClE;AAAA,MACA,oBAAoB;AAAA,IACtB,CAAC;AAAA,EACH;AAGA,QAAM,gBAAgB,CAAC,UAA4B,CAAC,MAAM;AACxD,QAAI,CAAC,QAAQ;AACX;AAAA,IACF;AACA,QAAI,qBAAqB,SAAS;AAChC,UAAIO,iBAAgB,GAAG;AACrB,QAAAN,WAAU,EAAE,MAAM,aAAa,YAAY,wCAAmC;AAAA,MAChF;AACA;AAAA,IACF;AAEA,QAAIM,iBAAgB,GAAG;AACrB,MAAAN,WAAU,EAAE,MAAM,aAAa,YAAY,WAAW,EAAE,QAAQ,CAAC;AAAA,IACnE;AAEA,WAAO,OAAO;AAAA,EAChB;AAMA,QAAM,iBAAwC;AAAA,IAC5C,WAAW;AAAA,IACX,MAAM;AAAA,IACN;AAAA,IACA,WAAW;AAAA,IACX,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,YAAY;AAAA,EACd;AACA,QAAM,mBAAmB,MAAMD,cAAa,iBAAiB,cAAc;AAC3E,MAAI,iBAAiB,SAAS;AAC5B,WAAO,kBAAkB;AAAA,MACvB,UAAU,EAAE,QAAQ,SAAS,WAAW,iBAAiB,OAAO,UAAU;AAAA,MAC1E,oBAAoB,iBAAiB,OAAO,cAAc;AAAA,IAC5D,CAAC;AAAA,EACH;AAEA,QAAM,eAAe,KAAK,IAAI;AAC9B,QAAM,CAAC,OAAO,MAAM,IAAI,MAAMH;AAAA,IAC5B,YAAY,MAAM,KAAK;AAAA,MACrB,MAAM;AAAA,MACN;AAAA,MACA,WAAW;AAAA,MACX,QAAQ;AAAA,MACR,aAAa;AAAA,MACb;AAAA,IACF,CAAC;AAAA,IACD;AAAA,IACA;AAAA,MACE,SAAS;AAAA,MACT,KAAK;AAAA,MACL,QAAQ,MAAM;AAAA,MACd,WAAW;AAAA,IACb;AAAA,EACF;AAEA,iBAAe,SAAS;AACxB,iBAAe,QAAQ,SAAS;AAChC,iBAAe,aAAa,KAAK,IAAI,IAAI;AACzC,QAAMG,cAAa,kBAAkB,cAAc;AAEnD,MAAI,OAAO;AACT,QAAIM,cAAa,GAAG;AAClB,MAAAL,WAAU,EAAE,MAAM,sBAAsB,YAAY,IAAI,OAAO,EAAE,OAAO,cAAc,WAAW,OAAO,CAAC;AAAA,IAC3G;AACA,WAAO,kBAAkB;AAAA,MACvB,UAAU,EAAE,QAAQ,SAAS,WAAW,0BAA0B;AAAA,MAClE,oBAAoB;AAAA,IACtB,CAAC;AAAA,EACH;AAEA,MAAI,WAAW,UAAa,WAAW,MAAM;AAC3C,QAAIK,cAAa,GAAG;AAClB,MAAAL,WAAU,EAAE,MAAM,aAAa,YAAY,YAAY;AAAA,IACzD;AAGA,QAAI,mBAAmB,MAAM,GAAG;AAC9B,UAAI,OAAO,WAAW,SAAS;AAC7B,eAAO,kBAAkB;AAAA,UACvB,UAAU;AAAA,UACV,oBAAoBC,8BAA6B;AAAA,YAC/C,QAAQ;AAAA,YACR,oBAAoB,OAAO,OAAO,eAAe,WAAW,OAAO,aAAa;AAAA,UAClF,CAAC;AAAA,QACH,CAAC;AAAA,MACH;AAEA,YAAM,kBAAsC;AAAA,QAC1C,GAAG;AAAA,QACH,QAAQ;AAAA,QACR,YAAYA,8BAA6B;AAAA,UACvC,QAAQ;AAAA,UACR,oBAAoB,OAAO,OAAO,eAAe,WAAW,OAAO,aAAa;AAAA,QAClF,CAAC;AAAA,MACH;AACA,aAAO;AAAA,IACT;AAEA,WAAO,kBAAkB;AAAA,MACvB,UAAU,EAAE,QAAQ,SAAS,WAAW,4BAA4B;AAAA,MACpE,oBAAoB;AAAA,IACtB,CAAC;AAAA,EACH;AAEA,MAAII,cAAa,GAAG;AAClB,IAAAL,WAAU,EAAE,KAAK,aAAa,YAAY,mBAAmB;AAAA,EAC/D;AACA,SAAO,kBAAkB;AAAA,IACvB,UAAU,EAAE,QAAQ,SAAS,WAAW,oBAAoB;AAAA,IAC5D,oBAAoB;AAAA,EACtB,CAAC;AACH;","names":["getSession","getProjectConfig","validateRequest","checkRateLimit","tryCatch","parseTransportRouteName","validateInputByType","dispatchHook","getLogger","defaultHttpStatusForResponse","extractLanguageFromHeader","normalizeErrorResponse","applyErrorFormatter","shouldLogDev","shouldLogStream","warnedMissingInputType","warnIfInputTypeMissing"]}
|
|
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/_shared/rateLimitIdentity.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; }) => void;\n\nconst validateApiMessage = ({ msg, emitApiError }: {\n msg: apiMessage;\n emitApiError: EmitApiError;\n}): { 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 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 = ({ apiEntry, user, name, emitApiError }: {\n apiEntry: RuntimeApiEntry;\n user: SessionLayout | null;\n name: string;\n emitApiError: EmitApiError;\n}): 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 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.misconfiguredPredicate' ? 'invalid-condition' : 'additional-failed',\n userId: user?.id ?? null,\n transport: 'socket',\n failedKey: authResult.errorCode,\n });\n 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 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 await runWithErrorTrackerIdentityScope(() => handleApiRequestInner(args));\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 if (typeof msg != 'object') {\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 socket.emit(buildApiResponseEventName(responseIndex), {\n status: 'error',\n errorCode: preMsgResult.signal.errorCode,\n httpStatus: preMsgResult.signal.httpStatus ?? 403,\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 const emitApiError: EmitApiError = ({ response, fallbackHttpStatus }) => {\n const normalized = normalizeErrorResponse({\n response,\n preferredLocale,\n userLanguage: user?.language,\n fallbackHttpStatus,\n });\n const formatted = applyErrorFormatter({\n // eslint-disable-next-line no-restricted-syntax -- formatter boundary cast, mirrors handleHttpApiRequest\n response: normalized as unknown as Record<string, unknown> & { status?: string },\n routeName: currentRouteName ?? 'api/unknown',\n transport: 'socket',\n userId: user?.id,\n perRouteFormatter: currentPerRouteFormatter,\n });\n socket.emit(buildApiResponseEventName(responseIndex), formatted);\n };\n\n const validated = 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 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 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 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 (!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: () => { 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 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 // 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;\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 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 { deriveTokenBucketId } from './rateLimitIdentity';\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 token,\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 const requesterIdentity = customIdentity?.id ?? (token ? deriveTokenBucketId(token) : resolvedIp);\n const keyPrefix = customIdentity?.scope ?? (token ? 'token' : '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: token ? '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: token ? 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","//? Re-export from core so both @luckystack/api and @luckystack/sync share one\n//? implementation without a cross-package dependency. The canonical source\n//? is packages/core/src/resolveClientIp.ts.\nexport { deriveTokenBucketId } from '@luckystack/core';\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 } 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 // 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.misconfiguredPredicate' ? '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 && process.env.NODE_ENV !== '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 const timeoutMs = 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":";AACA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,oBAAAA;AAAA,EACA;AAAA,EACA;AAAA,EACA,kBAAAC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,gBAAAC;AAAA,EACA,aAAAC;AAAA,EACA;AAAA,EACA;AAAA,EACA,0BAAAC;AAAA,EACA;AAAA,OACK;;;ACnBP,SAAS,wBAAwB;AAO1B,IAAM,eAAe,MAAe,iBAAiB,EAAE,QAAQ;AAG/D,IAAM,kBAAkB,MAAe,iBAAiB,EAAE,QAAQ;;;ACTzE,SAAS,8BAA8B,wBAAwB,iBAAiB;AAWzE,IAAM,uBAAuB,CAAC;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAM2B;AACzB,MAAI,OAAO;AACT,QAAI,aAAa,GAAG;AAClB,gBAAU,EAAE,MAAM,iBAAiB,YAAY,IAAI,OAAO,EAAE,OAAO,aAAa,CAAC;AAAA,IACnF;AACA,WAAO;AAAA,MACL,GAAG,uBAAuB;AAAA,QACxB,UAAU,EAAE,QAAQ,SAAS,WAAW,0BAA0B;AAAA,QAClE;AAAA,QACA,cAAc,MAAM;AAAA,QACpB,oBAAoB;AAAA,MACtB,CAAC;AAAA,IACH;AAAA,EACF;AAEA,MAAI,WAAW,UAAa,WAAW,MAAM;AAC3C,QAAI,aAAa,GAAG;AAClB,gBAAU,EAAE,KAAK,QAAQ,YAAY,mBAAmB;AAAA,IAC1D;AACA,WAAO;AAAA,MACL,GAAG,uBAAuB;AAAA,QACxB,UAAU,EAAE,QAAQ,SAAS,WAAW,oBAAoB;AAAA,QAC5D;AAAA,QACA,cAAc,MAAM;AAAA,QACpB,oBAAoB;AAAA,MACtB,CAAC;AAAA,IACH;AAAA,EACF;AAEA,MAAI,aAAa,GAAG;AAClB,cAAU,EAAE,MAAM,QAAQ,YAAY,YAAY;AAAA,EACpD;AAKA,MAAI,OAAO,WAAW,aAAa,OAAO,WAAW,SAAS;AAC5D,WAAO;AAAA,MACL,GAAG,uBAAuB;AAAA,QACxB,UAAU,EAAE,QAAQ,SAAS,WAAW,4BAA4B;AAAA,QACpE;AAAA,QACA,cAAc,MAAM;AAAA,QACpB,oBAAoB;AAAA,MACtB,CAAC;AAAA,IACH;AAAA,EACF;AAEA,MAAI,OAAO,WAAW,SAAS;AAC7B,WAAO;AAAA,MACL,GAAG,uBAAuB;AAAA,QACxB,UAAU;AAAA,QACV;AAAA,QACA,cAAc,MAAM;AAAA,QACpB,oBAAoB,6BAA6B;AAAA,UAC/C,QAAQ;AAAA,UACR,oBAAoB,OAAO,OAAO,eAAe,WAAW,OAAO,aAAa;AAAA,QAClF,CAAC;AAAA,MACH,CAAC;AAAA,IACH;AAAA,EACF;AAOA,SAAO;AAAA,IACL,GAAG;AAAA,IACH,QAAQ;AAAA,IACR,YAAY,6BAA6B;AAAA,MACvC,QAAQ;AAAA,MACR,oBAAoB,OAAO,OAAO,eAAe,WAAW,OAAO,aAAa;AAAA,IAClF,CAAC;AAAA,EACH;AACF;;;ACjGA;AAAA,EACE;AAAA,EACA,aAAAC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;;;ACNP,SAAS,aAAAC,kBAAiB;AAcnB,IAAM,yBAAyB,CAAC,QAAgB,gBAA+C;AACpG,SAAO,OAAO,EAAE,eAAe,IAA6B,CAAC,MAAqB;AAChF,UAAM,0BAA0B,OAAO,mBAAmB,YAAY,iBAAiB,IACnF,iBACA;AACJ,UAAM,kBAAkB,KAAK,IAAI,GAAG,KAAK,KAAK,0BAA0B,IAAI,CAAC;AAI7E,WAAO,CAAC,YAAY,SAAS;AAM3B,YAAM,OAAQ,OAAkD;AAIhE,UAAI,aAAa,KAAK,CAAC,MAAM;AAC3B,QAAAC,WAAU,EAAE,KAAK,kGAA6F;AAAA,MAChH;AACA,YAAM,UAAU,MAAM,aAAa,UAAU;AAC7C,YAAM,WAAW,MAAM,WAAW,YAAY;AAC9C,UAAI,CAAC,SAAU;AACf,UAAI,UAAU,gBAAiB;AAC/B,YAAM,IAAI,QAAc,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AAAA,IAC9D;AAAA,EACF;AACF;AAKO,IAAM,2BAA6C,YAAY;AAEtE;;;ADjBO,IAAM,4BAA4B,CAAC,EAAE,QAAQ,eAAe,aAAa,MAIrD;AACzB,QAAM,kBAAkB,IAAI,gBAAgB;AAC5C,QAAM,WAAW,2BAA2B,OAAO,IAAI,eAAe,eAAe;AACrF,QAAM,qBAAqB,MAAM;AAAE,oBAAgB,MAAM;AAAA,EAAG;AAC5D,SAAO,KAAK,iBAAiB,YAAY,kBAAkB;AAE3D,MAAI,cAAc;AAClB,QAAM,iBAAiB,MAAM;AAC3B,QAAI,YAAa;AACjB,kBAAc;AACd,WAAO,IAAI,iBAAiB,YAAY,kBAAkB;AAC1D,iCAA6B,QAAQ;AAAA,EACvC;AAEA,QAAM,aAAa,CAAC,UAA4B,CAAC,MAAM;AACrD,QAAI,gBAAgB,OAAO,SAAS;AAClC,UAAI,gBAAgB,GAAG;AACrB,QAAAC,WAAU,EAAE,MAAM,QAAQ,YAAY,wCAAmC;AAAA,MAC3E;AACA;AAAA,IACF;AACA,QAAI,gBAAgB,GAAG;AACrB,MAAAA,WAAU,EAAE,MAAM,QAAQ,YAAY,WAAW,EAAE,QAAQ,CAAC;AAAA,IAC9D;AACA,WAAO,KAAK,wBAAwB,aAAa,GAAG,OAAO;AAAA,EAC7D;AAMA,QAAM,gBAAgB,uBAAuB,QAAQ,gBAAgB,MAAM;AAE3E,SAAO,EAAE,iBAAiB,aAAa,gBAAgB,QAAQ,YAAY,eAAe,eAAe;AAC3G;;;AEvEA,SAAS,cAAc,qBAAqB,aAAAC,kBAAiB;;;ACD7D,SAAS,oBAAAC,mBAAkB,aAAAC,kBAAiB;AAO5C,IAAM,yBAAyB,oBAAI,IAAY;AAExC,IAAM,yBAAyB,CAAC,cAAsB,cAAwC;AACnG,MAAI,CAACD,kBAAiB,EAAE,IAAI,uBAAwB;AACpD,MAAI,aAAa,UAAU,KAAK,EAAE,SAAS,KAAK,UAAU,KAAK,MAAM,MAAO;AAC5E,MAAI,uBAAuB,IAAI,YAAY,EAAG;AAC9C,yBAAuB,IAAI,YAAY;AACvC,EAAAC,WAAU,EAAE,KAAK,cAAc,YAAY,sHAAiH;AAC9J;;;ADMO,IAAM,wBAAwB,CAAC,eAAoE;AACxG,MAAI,CAAC,WAAY,QAAO;AAMxB,MAAI,OAAO,eAAe,UAAU;AAClC,WAAO,eAAe,YAAY,YAAY;AAAA,EAChD;AACA,SAAO,WAAW,UAAU,SAAS,YAAY;AACnD;AAEO,IAAM,yBAAyB,OAAO;AAAA,EAC3C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAOwB;AAKtB,QAAM,iBAAiB,sBAAsB,SAAS,UAAU;AAEhE,MAAI,mBAAmB,UAAU;AAC/B,2BAAuB,cAAc,SAAS,SAAS;AAAA,EACzD;AACA,QAAM,aAAa,kBAAkB,EAAE,WAAW,cAAc,MAAM,gBAAgB,MAAM,WAAW,SAAS,CAAC;AAEjH,MAAI,mBAAmB,UAAU;AAC/B,UAAM,kBAAkB,MAAM,oBAAoB;AAAA,MAChD,UAAU,SAAS;AAAA,MACnB,OAAO;AAAA,MACP,SAAS;AAAA,MACT,UAAU,SAAS;AAAA,IACrB,CAAC;AAED,UAAM,aAAa,mBAAmB;AAAA,MACpC,WAAW;AAAA,MACX,MAAM;AAAA,MACN;AAAA,MACA,YAAY;AAAA,MACZ,WAAW;AAAA,IACb,CAAC;AAED,QAAI,gBAAgB,WAAW,SAAS;AAMtC,UAAI,aAAa,GAAG;AAClB,QAAAC,WAAU,EAAE,KAAK,oCAAoC,YAAY,IAAI,EAAE,OAAO,cAAc,SAAS,gBAAgB,QAAQ,CAAC;AAAA,MAChI;AACA,qBAAe;AACf,2BAAqB;AACrB,aAAO;AAAA,IACT;AAAA,EACF,OAAO;AAEL,UAAM,aAAa,mBAAmB;AAAA,MACpC,WAAW;AAAA,MACX,MAAM;AAAA,MACN;AAAA,MACA,YAAY,EAAE,QAAQ,UAAU;AAAA,MAChC,WAAW;AAAA,IACb,CAAC;AAAA,EACH;AAEA,SAAO;AACT;;;AEpGA,SAAS,gBAAgB,oBAAAC,mBAAkB,gBAAAC,eAAc,aAAAC,kBAAiB;;;ACE1E,SAAS,2BAA2B;;;ADoC7B,IAAM,qBAAqB,OAAO;AAAA,EACvC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,qBAAqB;AACvB,MAAiE;AAC/D,QAAM,SAASC,kBAAiB;AAChC,QAAM,oBAAoB,aAAa,OAAO,aAAa;AAE3D,MAAI,sBAAsB,SAAS,oBAAoB,GAAG;AACxD,UAAM,aAAa,OAAO,aAAa;AACvC,UAAM,iBAAiB,aAAa,EAAE,WAAW,cAAc,QAAQ,MAAM,MAAM,MAAM,IAAI,YAAY,UAAU,CAAC,KAAK;AACzH,UAAM,oBAAoB,gBAAgB,OAAO,QAAQ,oBAAoB,KAAK,IAAI;AACtF,UAAM,YAAY,gBAAgB,UAAU,QAAQ,UAAU;AAC9D,UAAM,eAAe,GAAG,SAAS,IAAI,iBAAiB,QAAQ,YAAY;AAE1E,UAAM,EAAE,SAAS,QAAQ,IAAI,MAAM,eAAe;AAAA,MAChD,KAAK;AAAA,MACL,OAAO;AAAA,MACP,UAAU,OAAO,aAAa;AAAA,IAChC,CAAC;AAED,QAAI,CAAC,SAAS;AAMZ,WAAKC,cAAa,qBAAqB;AAAA,QACrC,OAAO,QAAQ,SAAS;AAAA,QACxB,KAAK;AAAA,QACL,OAAO;AAAA,QACP,UAAU,OAAO,aAAa;AAAA,QAC9B,OAAO,oBAAoB;AAAA,QAC3B,OAAO;AAAA,QACP,QAAQ,MAAM;AAAA,QACd,IAAI,QAAQ,SAAY;AAAA,MAC1B,CAAC;AACD,UAAI,aAAa,GAAG;AAClB,QAAAC,WAAU,EAAE,KAAK,gCAAgC,YAAY,IAAI,EAAE,OAAO,cAAc,KAAK,cAAc,UAAU,CAAC;AAAA,MACxH;AACA,aAAO,EAAE,SAAS,OAAO,WAAW,yBAAyB,QAAQ;AAAA,IACvE;AAAA,EACF;AAEA,QAAM,iBAAiB,OAAO,aAAa;AAC3C,MAAI,CAAC,sBAAsB,mBAAmB,SAAS,iBAAiB,GAAG;AACzE,UAAM,EAAE,SAAS,QAAQ,IAAI,MAAM,eAAe;AAAA,MAChD,KAAK,MAAM,UAAU;AAAA,MACrB,OAAO;AAAA,MACP,UAAU,OAAO,aAAa;AAAA,IAChC,CAAC;AAED,QAAI,CAAC,SAAS;AACZ,WAAKD,cAAa,qBAAqB;AAAA,QACrC,OAAO;AAAA,QACP,KAAK,MAAM,UAAU;AAAA,QACrB,OAAO;AAAA,QACP,UAAU,OAAO,aAAa;AAAA,QAC9B,OAAO,iBAAiB;AAAA,QACxB,IAAI;AAAA,MACN,CAAC;AACD,UAAI,aAAa,GAAG;AAClB,QAAAC,WAAU,EAAE,KAAK,sCAAsC,EAAE,IAAI,YAAY,UAAU,CAAC;AAAA,MACtF;AACA,aAAO,EAAE,SAAS,OAAO,WAAW,yBAAyB,QAAQ;AAAA,IACvE;AAAA,EACF;AAEA,SAAO,EAAE,SAAS,KAAK;AACzB;;;APlEA,IAAM,qBAAqB,CAAC,EAAE,KAAK,aAAa,MAGc;AAC5D,QAAM,EAAE,MAAM,MAAM,cAAc,IAAI;AAOtC,MAAI,OAAO,kBAAkB,UAAU;AACrC,QAAI,aAAa,GAAG;AAClB,MAAAC,WAAU,EAAE,KAAK,8BAA8B;AAAA,IACjD;AACA,WAAO;AAAA,EACT;AAOA,MAAI,CAAC,QAAQ,CAAC,QAAQ,OAAO,QAAQ,YAAY,OAAO,QAAQ,YAAY,MAAM,QAAQ,IAAI,GAAG;AAC/F,iBAAa;AAAA,MACX,UAAU,EAAE,QAAQ,SAAS,WAAW,qBAAqB;AAAA,MAC7D,oBAAoB;AAAA,IACtB,CAAC;AACD,WAAO;AAAA,EACT;AAEA,SAAO,EAAE,MAAM,KAAsC;AACvD;AAEA,IAAM,eAAe,CAAC,EAAE,UAAU,MAAM,MAAM,aAAa,MAK5C;AACb,MAAI,SAAS,KAAK,SAAS,CAAC,MAAM,IAAI;AACpC,QAAI,aAAa,GAAG;AAClB,MAAAA,WAAU,EAAE,KAAK,QAAQ,IAAI,mBAAmB,EAAE,OAAO,KAAK,CAAC;AAAA,IACjE;AACA,SAAKC,cAAa,mBAAmB;AAAA,MACnC,WAAW;AAAA,MACX,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,WAAW;AAAA,IACb,CAAC;AACD,iBAAa;AAAA,MACX,UAAU,EAAE,QAAQ,SAAS,WAAW,gBAAgB;AAAA,MACxD,oBAAoB;AAAA,IACtB,CAAC;AACD,WAAO;AAAA,EACT;AAGA,QAAM,aAAa,gBAAgB,EAAE,MAAM,SAAS,MAAM,KAAY,CAAC;AACvE,MAAI,WAAW,WAAW,SAAS;AACjC,QAAI,aAAa,GAAG;AAClB,MAAAD,WAAU,EAAE,KAAK,wBAAwB,IAAI,IAAI,EAAE,OAAO,MAAM,WAAW,WAAW,UAAU,CAAC;AAAA,IACnG;AACA,SAAKC,cAAa,mBAAmB;AAAA,MACnC,WAAW;AAAA,MACX,QAAQ,WAAW,cAAc,gCAAgC,sBAAsB;AAAA,MACvF,QAAQ,MAAM,MAAM;AAAA,MACpB,WAAW;AAAA,MACX,WAAW,WAAW;AAAA,IACxB,CAAC;AACD,iBAAa;AAAA,MACX,UAAU;AAAA,QACR,QAAQ;AAAA;AAAA,QAER,WAAW,WAAW,aAAa;AAAA,QACnC,aAAa,WAAW;AAAA,QACxB,YAAY,WAAW;AAAA,MACzB;AAAA,MACA,oBAAoB,WAAW,cAAc;AAAA,IAC/C,CAAC;AACD,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAKA,IAAM,sBAAsB,OAAO,EAAE,UAAU,cAAc,OAAO,QAAQ,MAAM,aAAa,MAOvE;AAItB,QAAM,aAAa,gBAAgB;AAAA,IACjC,YAAY,OAAO,UAAU;AAAA,IAC7B,SAAS,OAAO,UAAU;AAAA,IAC1B,YAAYC,kBAAiB,EAAE,KAAK;AAAA,IACpC,sBAAsBA,kBAAiB,EAAE,KAAK;AAAA,EAChD,CAAC;AAED,QAAM,SAAS,MAAM,mBAAmB;AAAA,IACtC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW,SAAS;AAAA,IACpB,WAAW;AAAA,EACb,CAAC;AAED,MAAI,CAAC,OAAO,SAAS;AACnB,iBAAa;AAAA,MACX,UAAU;AAAA,QACR,QAAQ;AAAA,QACR,WAAW,OAAO,aAAa;AAAA,QAC/B,aAAa,CAAC,EAAE,KAAK,WAAW,OAAO,OAAO,WAAW,EAAE,CAAC;AAAA,MAC9D;AAAA,MACA,oBAAoB;AAAA,IACtB,CAAC;AACD,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAEA,IAAM,oBAAoB,OAAO,EAAE,UAAU,gBAAgB,MAAM,iBAAiB,cAAc,YAAY,aAAa,iBAAiB,cAAc,MAUlC;AAItH,QAAM,eAAe,KAAK,IAAI;AAM9B,QAAM,YAAYA,kBAAiB,EAAE,IAAI;AACzC,MAAI;AACJ,MAAI,WAAW;AACf,QAAM,iBAAiB,cAAc,QACjC,OACA,IAAI,QAAc,CAAC,YAAY;AAC7B,oBAAgB,WAAW,MAAM;AAC/B,iBAAW;AACX,sBAAgB,MAAM;AACtB,cAAQ;AAAA,IACV,GAAG,SAAS;AAAA,EACd,CAAC;AAEL,QAAM,iBAAiB;AAAA,IACrB,YAAY,MAAM,SAAS,KAAK,EAAE,MAAM,gBAAgB,MAAM,WAAW,iBAAiB,QAAQ,YAAY,aAAa,cAAc,CAAC;AAAA,IAC1I;AAAA,IACA;AAAA,MACE,SAAS;AAAA,MACT,KAAK;AAAA,MACL,QAAQ,MAAM;AAAA,MACd,WAAW;AAAA,IACb;AAAA,EACF;AAEA,MAAI,gBAAgB;AAClB,UAAM,QAAQ,KAAK,CAAC,gBAAgB,cAAc,CAAC;AAAA,EACrD;AACA,eAAa,aAAa;AAG1B,MAAI,UAAU;AACZ,WAAO,EAAE,OAAO,MAAM,QAAQ,QAAW,YAAY,KAAK,IAAI,IAAI,cAAc,UAAU,KAAK;AAAA,EACjG;AAEA,QAAM,CAAC,OAAO,MAAM,IAAI,MAAM;AAC9B,SAAO,EAAE,OAAO,SAAS,MAAM,QAAQ,UAAU,QAAW,YAAY,KAAK,IAAI,IAAI,aAAa;AACpG;AAaA,IAAM,wBAAwB,OAAO;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAU0C;AAKxC,QAAM,iBAAwC;AAAA,IAC5C,WAAW;AAAA,IACX,MAAM;AAAA,IACN;AAAA,IACA,WAAW;AAAA,IACX,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,YAAY;AAAA,EACd;AACA,QAAM,mBAAmB,MAAMD,cAAa,iBAAiB,cAAc;AAC3E,MAAI,iBAAiB,SAAS;AAC5B,WAAO,EAAE,SAAS,MAAM,WAAW,iBAAiB,OAAO,WAAW,YAAY,iBAAiB,OAAO,WAAW;AAAA,EACvH;AAEA,QAAM,EAAE,OAAO,QAAQ,YAAY,SAAS,IAAI,MAAM,kBAAkB;AAAA,IACtE;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAID,MAAI,UAAU;AACZ,WAAO,EAAE,SAAS,MAAM,WAAW,eAAe,YAAY,IAAI;AAAA,EACpE;AAEA,iBAAe,SAAS;AACxB,iBAAe,QAAQ,SAAS;AAChC,iBAAe,aAAa;AAC5B,QAAMA,cAAa,kBAAkB,cAAc;AAEnD,SAAO,EAAE,SAAS,OAAO,OAAO,OAAO;AACzC;AAEA,IAAM,gBAAgB,OAAO;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MASqB;AACnB,QAAM,WAAW,qBAAqB,EAAE,cAAc,OAAO,QAAQ,iBAAiB,KAAK,CAAC;AAO5F,QAAM,aAAa,EAAE,WAAW,cAAc,MAAM,UAAU,SAAS;AACvE,QAAM,mBAAmB,MAAMA,cAAa,iBAAiB,UAAU;AAEvE,QAAM,gBAAgB,iBAAiB,UACnC,EAAE,GAAGE,wBAAuB;AAAA,IAC1B,UAAU,EAAE,QAAQ,SAAS,WAAW,iBAAiB,OAAO,UAAU;AAAA,IAC1E;AAAA,IACA,cAAc,MAAM;AAAA,IACpB,oBAAoB,iBAAiB,OAAO,cAAc;AAAA,EAC5D,CAAC,EAAE,IACH,WAAW;AAMf,QAAM,mBAAmB,EAAE,WAAW,cAAc,MAAM,UAAU,cAAc;AAClF,QAAMF,cAAa,wBAAwB,gBAAgB;AAK3D,QAAM,oBAAoB,oBAAoB;AAAA,IAC5C,UAAU,iBAAiB;AAAA,IAC3B,WAAW;AAAA,IACX,WAAW;AAAA,IACX,QAAQ,MAAM;AAAA,IACd;AAAA,EACF,CAAC;AAED,SAAO,KAAK,0BAA0B,aAAa,GAAG,iBAAiB;AAIvE,QAAMA,cAAa,kBAAkB,EAAE,WAAW,cAAc,MAAM,UAAU,kBAAkB,CAAC;AACrG;AAUA,eAAO,iBAAwC,MAA2C;AACxF,QAAM,iCAAiC,MAAM,sBAAsB,IAAI,CAAC;AAC1E;AAEA,eAAe,sBAAsB,EAAE,KAAK,QAAQ,MAAM,GAAyB;AAIjF,MAAI,OAAO,OAAO,UAAU;AAC1B,QAAI,aAAa,GAAG;AAClB,MAAAD,WAAU,EAAE,KAAK,2CAA2C;AAAA,IAC9D;AACA;AAAA,EACF;AAEA,QAAM,EAAE,cAAc,IAAI;AAM1B,QAAM,eAAe,MAAMC,cAAa,oBAAoB;AAAA,IAC1D,SAAS;AAAA,IACT,UAAU,OAAO;AAAA,IACjB,IAAI,OAAO,UAAU;AAAA,IACrB,eAAe,QAAQ,KAAK;AAAA,IAC5B,WAAW,OAAO,IAAI,SAAS,WAAW,IAAI,OAAO;AAAA,EACvD,CAAC;AACD,MAAI,aAAa,SAAS;AACxB,WAAO,KAAK,0BAA0B,aAAa,GAAG;AAAA,MACpD,QAAQ;AAAA,MACR,WAAW,aAAa,OAAO;AAAA,MAC/B,YAAY,aAAa,OAAO,cAAc;AAAA,IAChD,CAAC;AACD;AAAA,EACF;AAEA,QAAM,OAAO,MAAM,YAAY,KAAK;AAKpC,iCAA+B,MAAM,KAAK,EAAE,IAAI,KAAK,IAAI,OAAO,KAAK,SAAS,QAAW,UAAU,KAAK,QAAQ,OAAU,IAAI,IAAI;AAClI,QAAM,kBACJ,0BAA0B,OAAO,UAAU,QAAQ,YAAY,CAAC,KAC7D,0BAA0B,OAAO,UAAU,QAAQ,iBAAiB,CAAC,KACrE;AAQL,MAAI;AAEJ,MAAI;AAEJ,QAAM,eAA6B,CAAC,EAAE,UAAU,mBAAmB,MAAM;AACvE,UAAM,aAAaE,wBAAuB;AAAA,MACxC;AAAA,MACA;AAAA,MACA,cAAc,MAAM;AAAA,MACpB;AAAA,IACF,CAAC;AACD,UAAM,YAAY,oBAAoB;AAAA;AAAA,MAEpC,UAAU;AAAA,MACV,WAAW,oBAAoB;AAAA,MAC/B,WAAW;AAAA,MACX,QAAQ,MAAM;AAAA,MACd,mBAAmB;AAAA,IACrB,CAAC;AACD,WAAO,KAAK,0BAA0B,aAAa,GAAG,SAAS;AAAA,EACjE;AAEA,QAAM,YAAY,mBAAmB,EAAE,KAAK,aAAa,CAAC;AAC1D,MAAI,CAAC,UAAW;AAChB,QAAM,EAAE,MAAM,MAAM,eAAe,IAAI;AAEvC,QAAM,cAAc,wBAAwB,EAAE,OAAO,MAAM,QAAQ,MAAM,CAAC;AAC1E,MAAI,YAAY,WAAW,SAAS;AAClC,iBAAa;AAAA,MACX,UAAU;AAAA,QACR,QAAQ;AAAA,QACR,WAAW;AAAA,QACX,aAAa,CAAC,EAAE,KAAK,QAAQ,OAAO,KAAK,CAAC;AAAA,MAC5C;AAAA,MACA,oBAAoB;AAAA,IACtB,CAAC;AACD;AAAA,EACF;AAEA,QAAM,eAAe,YAAY;AACjC,qBAAmB;AAKnB,MAAI,YAAY,aAAa,wBAAwB,iBAAiB;AAGpE,UAAM,gBAAgBD,kBAAiB,EAAE,aAAa;AACtD,QAAI,kBAAkB,SAAS,gBAAgB,GAAG;AAChD,YAAM,WAAW,gBAAgB;AAAA,QAC/B,YAAY,OAAO,UAAU;AAAA,QAC7B,SAAS,OAAO,UAAU;AAAA,QAC1B,YAAYA,kBAAiB,EAAE,KAAK;AAAA,QACpC,sBAAsBA,kBAAiB,EAAE,KAAK;AAAA,MAChD,CAAC;AACD,YAAM,EAAE,SAAS,QAAQ,IAAI,MAAME,gBAAe;AAAA,QAChD,KAAK,MAAM,QAAQ;AAAA,QACnB,OAAO;AAAA,QACP,UAAUF,kBAAiB,EAAE,aAAa;AAAA,MAC5C,CAAC;AACD,UAAI,CAAC,SAAS;AACZ,aAAKD,cAAa,qBAAqB;AAAA,UACrC,OAAO;AAAA,UACP,KAAK,MAAM,QAAQ;AAAA,UACnB,OAAO;AAAA,UACP,UAAUC,kBAAiB,EAAE,aAAa;AAAA,UAC1C,OAAO,gBAAgB;AAAA,UACvB,IAAI;AAAA,QACN,CAAC;AACD,qBAAa;AAAA,UACX,UAAU;AAAA,YACR,QAAQ;AAAA,YACR,WAAW;AAAA,YACX,aAAa,CAAC,EAAE,KAAK,WAAW,OAAO,QAAQ,CAAC;AAAA,UAClD;AAAA,UACA,oBAAoB;AAAA,QACtB,CAAC;AACD;AAAA,MACF;AAAA,IACF;AAKA,UAAM,cAAc,EAAE,OAAO,QAAQ,QAAQ,MAAM,MAAM,KAAK,CAAC;AAC/D,UAAM,cAAc;AAAA,MAClB;AAAA,MACA;AAAA,MACA;AAAA,MACA,OAAO;AAAA,MACP,QAAQ,EAAE,QAAQ,WAAW,YAAY,KAAK,QAAQ,KAAK;AAAA,MAC3D;AAAA,MACA;AAAA,IACF,CAAC;AACD;AAAA,EACF;AAEA,MAAI,aAAa,GAAG;AAClB,IAAAF,WAAU,EAAE,MAAM,QAAQ,YAAY,SAAS;AAAA,EACjD;AAEA,QAAM,EAAE,YAAY,gBAAgB,IAAI,MAAM,kBAAkB;AAEhE,MAAI,CAAC,WAAW,YAAY,GAAG;AAC7B,iBAAa;AAAA,MACX,UAAU;AAAA,QACR,QAAQ;AAAA,QACR,WAAW;AAAA,QACX,aAAa,CAAC,EAAE,KAAK,QAAQ,OAAO,KAAK,CAAC;AAAA,MAC5C;AAAA,MACA,oBAAoB;AAAA,IACtB,CAAC;AACD;AAAA,EACF;AAEA,QAAM,WAAW,WAAW,YAAY;AACxC,6BAA2B,SAAS;AAcpC,QAAM,EAAE,iBAAiB,aAAa,YAAY,eAAe,eAAe,IAAI,0BAA0B;AAAA,IAC5G;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAYD,MAAI,CAAC,aAAa,EAAE,UAAU,MAAM,MAAM,aAAa,CAAC,GAAG;AAAE,mBAAe;AAAG;AAAA,EAAQ;AAEvF,QAAM,cAAc,MAAM,oBAAoB,EAAE,UAAU,cAAc,OAAO,QAAQ,MAAM,aAAa,CAAC;AAC3G,MAAI,CAAC,aAAa;AAAE,mBAAe;AAAG;AAAA,EAAQ;AAM9C,QAAM,eAAe,MAAM,uBAAuB;AAAA,IAChD;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,sBAAsB,MAAM;AAAE,mBAAa;AAAA,QACzC,UAAU,EAAE,QAAQ,SAAS,WAAW,uBAAuB;AAAA,QAC/D,oBAAoB;AAAA,MACtB,CAAC;AAAA,IAAG;AAAA,EACN,CAAC;AACD,MAAI,CAAC,aAAc;AAInB,QAAM,YAAY,MAAM,sBAAsB;AAAA,IAC5C;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AACD,MAAI,UAAU,SAAS;AACrB,mBAAe;AACf,iBAAa;AAAA,MACX,UAAU,EAAE,QAAQ,SAAS,WAAW,UAAU,UAAU;AAAA,MAC5D,oBAAoB,UAAU,cAAc;AAAA,IAC9C,CAAC;AACD;AAAA,EACF;AAEA,QAAM,cAAc,EAAE,QAAQ,eAAe,cAAc,OAAO,UAAU,OAAO,QAAQ,UAAU,QAAQ,iBAAiB,MAAM,mBAAmB,SAAS,eAAe,CAAC;AAChL,iBAAe;AACjB;;;ASpnBA;AAAA,EAAS,oBAAAK;AAAA,EAAkB,eAAAC;AAAA,EAAa,qBAAqB;AAAA,EAC3D,oCAAAC;AAAA,EACA,kCAAAC;AAAA,EACA,mBAAAC;AAAA,EACA;AAAA,EAEA,YAAAC;AAAA,EACA,2BAAAC;AAAA,EACA,gBAAAC;AAAA,EACA,aAAAC;AAAA,EAAW,6BAAAC;AAAA,EAA2B,0BAAAC;AAAA,EAAwB,uBAAAC;AAAA,EAC9D;AAAA,EAAc,mBAAAC;AAAA,OAAuB;;;ACVvC,SAAS,gBAAAC,eAAc,uBAAAC,sBAAqB,aAAAC,kBAAiB;AAqBtD,IAAM,uBAAuB,OAAO;AAAA,EACzC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAO6C;AAE3C,QAAM,iBAAiB,sBAAsB,UAAU;AAGvD,MAAI,mBAAmB,UAAU;AAC/B,2BAAuB,cAAc,SAAS;AAAA,EAChD;AACA,QAAMC,cAAa,kBAAkB,EAAE,WAAW,cAAc,MAAM,aAAa,MAAM,WAAW,OAAO,CAAC;AAE5G,MAAI,mBAAmB,WAAW;AAEhC,UAAMA,cAAa,mBAAmB;AAAA,MACpC,WAAW;AAAA,MACX,MAAM;AAAA,MACN;AAAA,MACA,YAAY,EAAE,QAAQ,UAAU;AAAA,MAChC,WAAW;AAAA,IACb,CAAC;AACD,WAAO,EAAE,IAAI,KAAK;AAAA,EACpB;AAEA,QAAM,kBAAkB,MAAMC,qBAAoB;AAAA,IAChD,UAAU;AAAA,IACV,OAAO;AAAA,IACP,SAAS;AAAA,IACT,UAAU;AAAA,EACZ,CAAC;AAED,QAAMD,cAAa,mBAAmB;AAAA,IACpC,WAAW;AAAA,IACX,MAAM;AAAA,IACN;AAAA,IACA,YAAY;AAAA,IACZ,WAAW;AAAA,EACb,CAAC;AAED,MAAI,gBAAgB,WAAW,SAAS;AAMtC,QAAI,aAAa,GAAG;AAClB,MAAAE,WAAU,EAAE,KAAK,yCAAyC,YAAY,IAAI,EAAE,OAAO,cAAc,SAAS,gBAAgB,SAAS,WAAW,OAAO,CAAC;AAAA,IACxJ;AACA,WAAO,EAAE,IAAI,MAAM;AAAA,EACrB;AAEA,SAAO,EAAE,IAAI,KAAK;AACpB;;;ADEA,eAAsB,qBAAqB,QAA2D;AACpG,SAAOC,kCAAiC,MAAM,2BAA2B,MAAM,CAAC;AAClF;AAEA,eAAe,2BAA2B,QAA2D;AACnG,QAAM,EAAE,UAAU,MAAM,iBAAiB,kBAAkB,IAAI,MAAM,wBAAwB,MAAM;AAInG,QAAM,mBAAmB,OAAO,KAAK,WAAW,MAAM,IAAI,OAAO,OAAO,OAAO,OAAO,IAAI;AAE1F,QAAM,aAAa,EAAE,WAAW,kBAAkB,MAAM,SAAmG;AAC3J,QAAM,mBAAmB,MAAMC,cAAa,iBAAiB,UAAU;AAKvE,QAAM,gBAAgB,iBAAiB,UACnCC,wBAAuB;AAAA,IACrB,UAAU,EAAE,QAAQ,SAAS,WAAW,iBAAiB,OAAO,UAAU;AAAA,IAC1E;AAAA,IACA,cAAc,MAAM;AAAA,IACpB,oBAAoB,iBAAiB,OAAO,cAAc;AAAA,EAC5D,CAAC,IACD,WAAW;AAOf,QAAM,mBAAmB,EAAE,WAAW,kBAAkB,MAAM,UAAU,cAAc;AACtF,QAAMD,cAAa,wBAAwB,gBAAgB;AAU3D,QAAM,iBAAiB,iBAAiB;AACxC,QAAM,oBAAoBE,qBAAoB;AAAA,IAC5C,UAAU;AAAA,IACV,WAAW;AAAA,IACX,WAAW;AAAA,IACX,QAAQ,MAAM;AAAA,IACd;AAAA,EACF,CAAC;AAED,QAAMF,cAAa,kBAAkB,EAAE,WAAW,kBAAkB,MAAM,UAAU,kBAAkB,CAAC;AAEvG,SAAO;AACT;AASA,eAAe,wBAAwB,QAAsE;AAC3G,QAAM,kBACJG,2BAA0B,OAAO,eAAe,KAC7CA,2BAA0B,OAAO,oBAAoB;AAC1D,QAAM,OAAO,MAAMC,aAAY,OAAO,KAAK;AAK3C,EAAAC,gCAA+B,MAAM,KAAK,EAAE,IAAI,KAAK,IAAI,OAAO,KAAK,SAAS,QAAW,UAAU,KAAK,QAAQ,OAAU,IAAI,IAAI;AAClI,QAAM,kBAAgD,CAAC;AACvD,QAAM,WAAW,MAAM,6BAA6B,QAAQ,MAAM,iBAAiB,eAAe;AAClG,SAAO,EAAE,UAAU,MAAM,iBAAiB,mBAAmB,gBAAgB,QAAQ;AACvF;AAsBA,eAAe,4BACb,MACA,MACA,KASA;AAEA,MAAI,CAAC,QAAQ,OAAO,SAAS,UAAU;AACrC,WAAO,IAAI,kBAAkB;AAAA,MAC3B,UAAU,EAAE,QAAQ,SAAS,WAAW,kBAAkB;AAAA,MAC1D,oBAAoB;AAAA,IACtB,CAAC;AAAA,EACH;AAMA,MAAI,OAAO,SAAS,YAAY,SAAS,QAAQ,MAAM,QAAQ,IAAI,GAAG;AACpE,WAAO,IAAI,kBAAkB;AAAA,MAC3B,UAAU,EAAE,QAAQ,SAAS,WAAW,wBAAwB;AAAA,MAChE,oBAAoB;AAAA,IACtB,CAAC;AAAA,EACH;AAEA,QAAM,iBAAiB,KAAK,WAAW,MAAM,IAAI,OAAO,OAAO,IAAI;AACnE,QAAM,cAAcC,yBAAwB,EAAE,OAAO,gBAAgB,QAAQ,MAAM,CAAC;AACpF,MAAI,YAAY,WAAW,SAAS;AAClC,WAAO,IAAI,kBAAkB;AAAA,MAC3B,UAAU;AAAA,QACR,QAAQ;AAAA,QACR,WAAW;AAAA,QACX,aAAa,CAAC,EAAE,KAAK,QAAQ,OAAO,eAAe,CAAC;AAAA,MACtD;AAAA,MACA,oBAAoB;AAAA,IACtB,CAAC;AAAA,EACH;AAEA,QAAM,eAAe,YAAY;AAEjC,MAAI,aAAa,GAAG;AAClB,IAAAC,WAAU,EAAE,MAAM,aAAa,YAAY,SAAS;AAAA,EACtD;AAEA,QAAM,EAAE,YAAY,gBAAgB,IAAI,MAAM,4BAA4B;AAE1E,MAAI,CAAC,WAAW,YAAY,GAAG;AAC7B,WAAO,IAAI,kBAAkB;AAAA,MAC3B,UAAU;AAAA,QACR,QAAQ;AAAA,QACR,WAAW;AAAA,QACX,aAAa,CAAC,EAAE,KAAK,QAAQ,OAAO,aAAa,CAAC;AAAA,MACpD;AAAA,MACA,oBAAoB;AAAA,IACtB,CAAC;AAAA,EACH;AAEA,SAAO,EAAE,cAAc,aAAa,MAAM,YAAY,gBAAgB;AACxE;AAeA,SAAS,eACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMA,KAC2B;AAC3B,MAAI,KAAK,SAAS,CAAC,IAAI,MAAM,IAAI;AAC/B,QAAI,aAAa,GAAG;AAClB,MAAAA,WAAU,EAAE,KAAK,aAAa,IAAI,mBAAmB,EAAE,OAAO,MAAM,WAAW,OAAO,CAAC;AAAA,IACzF;AACA,SAAKP,cAAa,mBAAmB;AAAA,MACnC,WAAW;AAAA,MACX,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,WAAW;AAAA,MACX,IAAI;AAAA,IACN,CAAC;AACD,WAAO,IAAI,kBAAkB;AAAA,MAC3B,UAAU,EAAE,QAAQ,SAAS,WAAW,gBAAgB;AAAA,MACxD,oBAAoB;AAAA,IACtB,CAAC;AAAA,EACH;AAaA,QAAM,aAAaQ,iBAAgB,EAAE,MAAM,MAAM,IAAI,KAAM,CAAC;AAC5D,MAAI,WAAW,WAAW,SAAS;AACjC,QAAI,aAAa,GAAG;AAClB,MAAAD,WAAU,EAAE,KAAK,6BAA6B,IAAI,IAAI,EAAE,OAAO,MAAM,WAAW,WAAW,WAAW,WAAW,OAAO,CAAC;AAAA,IAC3H;AACA,SAAKP,cAAa,mBAAmB;AAAA,MACnC,WAAW;AAAA,MACX,QAAQ,WAAW,cAAc,gCAAgC,sBAAsB;AAAA,MACvF,QAAQ,IAAI,MAAM,MAAM;AAAA,MACxB,WAAW;AAAA,MACX,IAAI;AAAA,MACJ,WAAW,WAAW;AAAA,IACxB,CAAC;AACD,WAAO,IAAI,kBAAkB;AAAA,MAC3B,UAAU;AAAA,QACR,QAAQ;AAAA,QACR,WAAW,WAAW,aAAa;AAAA,QACnC,aAAa,WAAW;AAAA,QACxB,YAAY,WAAW;AAAA,MACzB;AAAA,MACA,oBAAoB,WAAW,cAAc;AAAA,IAC/C,CAAC;AAAA,EACH;AAEA,SAAO;AACT;AAOA,eAAe,uBACb;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMA,KACoC;AAKpC,QAAM,cAAc,eACfS,iBAAgB,EAAE,YAAY,QAAW,SAAS,CAAC,EAAE,CAAC;AAO3D,QAAM,0BAA0BC,kBAAiB,EAAE,aAAa,qBAC3D,QAAQ,IAAI,aAAa,gBACzB,aAAa,eAAe,EAAE;AAEnC,QAAM,kBAAkB,MAAM,mBAAmB;AAAA,IAC/C,YAAY;AAAA,IACZ;AAAA,IACA,MAAM,IAAI;AAAA,IACV;AAAA,IACA;AAAA,IACA,WAAW;AAAA,IACX,oBAAoB;AAAA,EACtB,CAAC;AAED,MAAI,CAAC,gBAAgB,SAAS;AAC5B,WAAO,IAAI,kBAAkB;AAAA,MAC3B,UAAU;AAAA,QACR,QAAQ;AAAA,QACR,WAAW,gBAAgB,aAAa;AAAA,QACxC,aAAa,CAAC,EAAE,KAAK,WAAW,OAAO,gBAAgB,WAAW,EAAE,CAAC;AAAA,MACvE;AAAA,MACA,oBAAoB;AAAA,IACtB,CAAC;AAAA,EACH;AAEA,SAAO;AACT;AAOA,SAAS,mBACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AACF,GAKA,KAC2B;AAC3B,QAAM,iBAAiB,kBAAkB,gBAAgB,YAAY;AACrE,MAAI,WAAW,gBAAgB;AAC7B,QAAI,aAAa,GAAG;AAClB,MAAAH,WAAU,EAAE,KAAK,iCAAiC,YAAY,IAAI,EAAE,UAAU,gBAAgB,KAAK,OAAO,CAAC;AAAA,IAC7G;AACA,WAAO,IAAI,kBAAkB;AAAA,MAC3B,UAAU;AAAA,QACR,QAAQ;AAAA,QACR,WAAW;AAAA,QACX,aAAa,CAAC,EAAE,KAAK,UAAU,OAAO,eAAe,CAAC;AAAA,MACxD;AAAA,MACA,oBAAoB;AAAA,IACtB,CAAC;AAAA,EACH;AAEA,SAAO;AACT;AAOA,eAAe,oBACb;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAQA,KAC6B;AAG7B,QAAM,gBAAgB;AAEtB,QAAM,gBAAgB,CAAC,UAA4B,CAAC,MAAM;AACxD,QAAI,CAAC,QAAQ;AACX;AAAA,IACF;AACA,QAAI,qBAAqB,SAAS;AAChC,UAAI,gBAAgB,GAAG;AACrB,QAAAA,WAAU,EAAE,MAAM,aAAa,YAAY,wCAAmC;AAAA,MAChF;AACA;AAAA,IACF;AAEA,QAAI,gBAAgB,GAAG;AACrB,MAAAA,WAAU,EAAE,MAAM,aAAa,YAAY,WAAW,EAAE,QAAQ,CAAC;AAAA,IACnE;AAEA,WAAO,OAAO;AAAA,EAChB;AAMA,QAAM,iBAAwC;AAAA,IAC5C,WAAW;AAAA,IACX,MAAM;AAAA,IACN,MAAM,IAAI;AAAA,IACV,WAAW;AAAA,IACX,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,YAAY;AAAA,EACd;AACA,QAAM,mBAAmB,MAAMP,cAAa,iBAAiB,cAAc;AAC3E,MAAI,iBAAiB,SAAS;AAC5B,WAAO,IAAI,kBAAkB;AAAA,MAC3B,UAAU,EAAE,QAAQ,SAAS,WAAW,iBAAiB,OAAO,UAAU;AAAA,MAC1E,oBAAoB,iBAAiB,OAAO,cAAc;AAAA,IAC5D,CAAC;AAAA,EACH;AAOA,QAAM,YAAYU,kBAAiB,EAAE,IAAI;AACzC,QAAM,qBAAqB,IAAI,gBAAgB;AAE/C,MAAI,qBAAqB,SAAS;AAChC,uBAAmB,MAAM;AAAA,EAC3B,OAAO;AACL,yBAAqB,iBAAiB,SAAS,MAAM;AAAE,yBAAmB,MAAM;AAAA,IAAG,GAAG,EAAE,MAAM,KAAK,CAAC;AAAA,EACtG;AAEA,MAAI,WAAW;AACf,MAAI;AACJ,QAAM,iBAAiB,cAAc,QACjC,OACA,IAAI,QAAc,CAAC,YAAY;AAC7B,oBAAgB,WAAW,MAAM;AAC/B,iBAAW;AACX,yBAAmB,MAAM;AACzB,cAAQ;AAAA,IACV,GAAG,SAAS;AAAA,EACd,CAAC;AAEL,QAAM,eAAe,KAAK,IAAI;AAC9B,QAAM,iBAAiBC;AAAA,IACrB,YAAY,MAAM,gBAAgB,KAAK;AAAA,MACrC,MAAM;AAAA,MACN,MAAM,IAAI;AAAA,MACV,WAAW;AAAA,MACX,QAAQ;AAAA,MACR,aAAa,mBAAmB;AAAA,MAChC;AAAA,IACF,CAAC;AAAA,IACD;AAAA,IACA;AAAA,MACE,SAAS;AAAA,MACT,KAAK;AAAA,MACL,QAAQ,IAAI,MAAM;AAAA,MAClB,WAAW;AAAA,IACb;AAAA,EACF;AAEA,MAAI,gBAAgB;AAClB,UAAM,QAAQ,KAAK,CAAC,gBAAgB,cAAc,CAAC;AAAA,EACrD;AACA,eAAa,aAAa;AAG1B,MAAI,UAAU;AACZ,WAAO,IAAI,kBAAkB;AAAA,MAC3B,UAAU,EAAE,QAAQ,SAAS,WAAW,cAAc;AAAA,MACtD,oBAAoB;AAAA,IACtB,CAAC;AAAA,EACH;AAEA,QAAM,CAAC,OAAO,MAAM,IAAI,MAAM;AAC9B,iBAAe,SAAS;AACxB,iBAAe,QAAQ,SAAS;AAChC,iBAAe,aAAa,KAAK,IAAI,IAAI;AACzC,QAAMX,cAAa,kBAAkB,cAAc;AAOnD,QAAM,WAAW,qBAAqB;AAAA,IACpC;AAAA,IACA,OAAO,SAAS;AAAA,IAChB,QAAQ,UAAU;AAAA,IAClB,iBAAiB,IAAI;AAAA,IACrB,MAAM,IAAI;AAAA,EACZ,CAAC;AACD,SAAO;AACT;AAEA,eAAe,6BACb;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,iBAAiB;AAAA,EACjB,sBAAsB;AAAA,EACtB,SAAS;AAAA,EACT;AAAA,EACA;AACF,GACA,MACA,iBACA,iBAC6B;AAK7B,QAAM,uBAAuB,eAAe,IAAI,gBAAgB,EAAE;AAElE,QAAM,oBAAoB,CAAC;AAAA,IACzB;AAAA,IACA;AAAA,EACF,MAG0B;AACxB,WAAOC,wBAAuB;AAAA,MAC5B;AAAA,MACA;AAAA,MACA,cAAc,MAAM;AAAA,MACpB;AAAA,IACF,CAAC;AAAA,EACH;AAKA,QAAM,MAA8B,EAAE,mBAAmB,MAAM,gBAAgB;AAG/E,QAAM,cAAc,MAAM,4BAA4B,MAAM,MAAM,GAAG;AACrE,MAAI,YAAY,aAAa;AAC3B,WAAO;AAAA,EACT;AACA,QAAM,EAAE,cAAc,aAAa,YAAY,gBAAgB,IAAI;AAEnE,QAAM,kBAAkB,WAAW,YAAY;AAC/C,kBAAgB,UAAU,gBAAgB;AAI1C,QAAM,YAAY,eAAe,EAAE,MAAM,cAAc,aAAa,MAAM,gBAAgB,KAAK,GAAG,GAAG;AACrG,MAAI,WAAW;AACb,WAAO;AAAA,EACT;AAGA,QAAM,iBAAiB,MAAM;AAAA,IAC3B,EAAE,aAAa,OAAO,cAAc,WAAW,gBAAgB,UAAU;AAAA,IACzE;AAAA,EACF;AACA,MAAI,gBAAgB;AAClB,WAAO;AAAA,EACT;AAGA,QAAM,cAAc;AAAA,IAClB,EAAE,QAAQ,cAAc,gBAAgB,gBAAgB,WAAW;AAAA,IACnE;AAAA,EACF;AACA,MAAI,aAAa;AACf,WAAO;AAAA,EACT;AAWA,QAAM,iBAAiB,MAAM,qBAAqB;AAAA,IAChD;AAAA,IACA,WAAW,gBAAgB;AAAA,IAC3B,mBAAmB,gBAAgB;AAAA,IACnC;AAAA,IACA;AAAA,IACA,YAAY,gBAAgB;AAAA,EAC9B,CAAC;AACD,MAAI,CAAC,eAAe,IAAI;AACtB,WAAO,kBAAkB;AAAA,MACvB,UAAU;AAAA,QACR,QAAQ;AAAA,QACR,WAAW;AAAA,MACb;AAAA,MACA,oBAAoB;AAAA,IACtB,CAAC;AAAA,EACH;AAGA,SAAO;AAAA,IACL,EAAE,cAAc,aAAa,iBAAiB,iBAAiB,sBAAsB,OAAO;AAAA,IAC5F;AAAA,EACF;AACF;","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"]}
|
|
@@ -22,7 +22,7 @@ Both transports execute the same sequence. The HTTP transport adds one extra ste
|
|
|
22
22
|
6. **`checkApiAuth({ apiEntry, user, name })`.** Two-stage auth gate: first checks `auth.login` (returns `auth.required` + `401` if missing), then runs `validateRequest({ auth, user })` which evaluates `auth.additional[]` predicates. Failures return `auth.forbidden` (or the predicate's overridden `errorCode`) with the predicate's `httpStatus` or `403` fallback. See [`./auth-flow.md`](./auth-flow.md).
|
|
23
23
|
7. **`applyApiRateLimits(...)`.** Two-bucket rate limit: per-route (uses the route's `rateLimit` export, or `rateLimiting.defaultApiLimit` fallback) keyed by `<token|ip>:<id>:api:<route>`, then a global per-IP bucket keyed by `ip:<ip>:api:all` (uses `rateLimiting.defaultIpLimit`). Either rejection emits `rateLimitExceeded` and returns `api.rateLimitExceeded` + `429`. See [`./rate-limiting.md`](./rate-limiting.md).
|
|
24
24
|
8. **HTTP-only: method check.** `expectedMethod = apiEntry.httpMethod ?? inferHttpMethod(resolvedName)`. Mismatched methods return `api.methodNotAllowed` + `405` with `errorParams: [{ key: 'method', value: expectedMethod }]`. Method check runs after auth + rate limit, on purpose — unauthenticated callers should not learn whether a method-incorrect route exists.
|
|
25
|
-
9. **`preApiValidate` hook → `validateInputByType(...)` → `postApiValidate` hook.** Hook fires before validation runs (handlers can mutate `data`, can NOT stop). `validateInputByType` checks the payload against the Zod-equivalent schema derived from the route's `inputType` text. On the socket transport only, the per-route `validation: 'relaxed'` or `validation: { input: 'skip' }` flag skips validation entirely (useful for third-party webhooks). Validation failures return `api.invalidInputType`
|
|
25
|
+
9. **`preApiValidate` hook → `validateInputByType(...)` → `postApiValidate` hook.** Hook fires before validation runs (handlers can mutate `data`, can NOT stop). `validateInputByType` checks the payload against the Zod-equivalent schema derived from the route's `inputType` text. On the socket transport only, the per-route `validation: 'relaxed'` or `validation: { input: 'skip' }` flag skips validation entirely (useful for third-party webhooks). Validation failures return the generic `api.invalidInputType` code — the validator's message is NOT echoed to the client (that would let unauthenticated callers enumerate the input schema); it travels to the `postApiValidate` hook (`validation.message`) and the dev logs instead. `postApiValidate` always fires with the resulting validation result, even on skip.
|
|
26
26
|
10. **`preApiExecute` hook.** Last gate before execution. A handler may return a `HookStopSignal` (`{ stop: true, errorCode, httpStatus? }`) to abort. Stop signals are normalized into a localized error envelope using the signal's `errorCode` (or `403` if no `httpStatus`).
|
|
27
27
|
11. **`executeApiHandler(...)`.** Wraps `apiEntry.main({ data, user, functions, stream })` in `tryCatch` (which captures any thrown error to Sentry via the active `@luckystack/error-tracking` adapter) and in a `startSpan(name, 'api.request')` (HTTP variant uses `'api.request.http'`). Records `durationMs`.
|
|
28
28
|
12. **`postApiExecute` hook.** Observation point. Receives `result`, `error`, and `durationMs`. Use for audit logging, metrics, or post-run side effects. Cannot mutate the response.
|
|
@@ -79,7 +79,7 @@ export default async function handleApiRequest(args: {
|
|
|
79
79
|
| `auth.forbidden` | `auth.additional[]` predicate rejects. | 403 |
|
|
80
80
|
| `auth.invalidCondition` | `auth.additional[]` references a session key that is not on the session shape. Setup error. | 500 |
|
|
81
81
|
| `api.rateLimitExceeded` | Per-route or per-IP bucket rejects. `errorParams[0].value` is `resetIn` (seconds). | 429 |
|
|
82
|
-
| `api.invalidInputType` | `validateInputByType` rejects the payload.
|
|
82
|
+
| `api.invalidInputType` | `validateInputByType` rejects the payload. Generic code only — the validator's message is routed to the `postApiValidate` hook + dev logs, never echoed to the client (schema-enumeration guard). | 400 |
|
|
83
83
|
| `api.internalServerError` | `main(...)` threw. | 500 |
|
|
84
84
|
| `api.emptyResponse` | `main(...)` returned `null` or `undefined`. | 500 |
|
|
85
85
|
| `api.invalidResponseStatus` | Handler returned an object whose `status` is neither `'success'` nor `'error'`. | 500 |
|