@luckystack/api 0.2.6 → 0.3.0

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/CLAUDE.md CHANGED
@@ -77,7 +77,7 @@ Read at request time via `getProjectConfig()` (from `@luckystack/core`). All liv
77
77
  - `rateLimiting.skipLoopbackInDev` — when `true` (default `false`), skips ONLY the global per-IP ABUSE bucket for loopback IPs in non-production (per-route bucket still applies). Lets a dev / the test runner hammer localhost without tripping the cross-route cap. Gated on this explicit flag (not inferred from `NODE_ENV` + a spoofable address) and on `isLoopbackIp(resolvedIp)`.
78
78
  - `rateLimiting.identity` — optional `(params) => { scope: 'user'|'ip'|'custom'; id } | null` callback that overrides the per-route bucket BASIS (api-key / tenant). Returning `null` falls back to the default (validated `user.id`, else resolved IP — NEVER the raw token).
79
79
 
80
- No environment variables are read directly by this package — secrets and toggles travel through `@luckystack/core`'s config layer.
80
+ No project env vars or secrets are read directly by this package — only the ambient `process.env.NODE_ENV` (for the dev-only loopback rate-limit skip in `handleHttpApiRequest`); all other toggles travel through `@luckystack/core`'s config layer.
81
81
 
82
82
  ## Peer dependencies
83
83
 
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  ## Install
6
6
 
7
7
  ```bash
8
- npm install @luckystack/api @luckystack/core @luckystack/login @luckystack/error-tracking socket.io
8
+ npm install @luckystack/api @luckystack/core @luckystack/error-tracking socket.io
9
9
  ```
10
10
 
11
11
  ## Quickstart
@@ -55,9 +55,9 @@ You typically don't call these yourself — `createLuckyStackServer` does. Use t
55
55
 
56
56
  ## How it integrates
57
57
 
58
- 1. **Validates** the inbound payload against the Zod schema generated from your `ApiParams['data']` interface (via `@luckystack/devkit`).
59
- 2. **Rate-limits** by IP + token using `checkRateLimit` from `@luckystack/core`.
60
- 3. **Authenticates** via `getSession` from `@luckystack/login` when `auth.login === true`.
58
+ 1. **Authenticates** via `readSession` from `@luckystack/core` (the session-provider registry; `@luckystack/login` is the optional default provider, not a hard dependency) when `auth.login === true`. Auth runs first so unauthenticated probes can't enumerate routes or learn input shape from the validation message.
59
+ 2. **Rate-limits** with two buckets via `checkRateLimit` from `@luckystack/core`: a per-route bucket keyed on the validated `user.id` (anonymous callers fall back to the resolved IP — never the raw session token, so re-login can't reset the limit; the basis is overridable via `rateLimiting.identity`) plus a global per-IP abuse bucket.
60
+ 3. **Validates** the inbound payload against the Zod schema generated from your `ApiParams['data']` interface (via `@luckystack/devkit`).
61
61
  4. **Dispatches** the `preApiExecute` hook (may abort with a stop signal).
62
62
  5. **Calls** your `main(...)` and captures errors via `tryCatch` (auto-forwarded to Sentry).
63
63
  6. **Dispatches** the `postApiExecute` hook with the result + duration.
@@ -96,7 +96,7 @@ Do not wrap `apiRequest` in `unknown` / `any` shims (see `.claude/CLAUDE.md` rul
96
96
 
97
97
  ## Dependencies
98
98
 
99
- - Runtime: `@luckystack/core`, `@luckystack/login`, `@luckystack/error-tracking`
99
+ - Runtime: `@luckystack/core`, `@luckystack/error-tracking` (`@luckystack/login` is NOT a runtime dependency — sessions resolve through core's session-provider registry; login is the optional default provider)
100
100
  - Peer (canonical ranges, standardized 2026-05-07):
101
101
  - `@prisma/client@^6.19.0` (transitively required via `@luckystack/core`)
102
102
  - `socket.io@^4.8.0`
package/dist/index.js CHANGED
@@ -211,7 +211,7 @@ var runSocketApiValidation = async ({
211
211
  getLogger5().warn(`api: input validation failed for ${resolvedName}`, { route: resolvedName, message: inputValidation.message });
212
212
  }
213
213
  cleanupRequest();
214
- emitInvalidInputType();
214
+ await emitInvalidInputType();
215
215
  return false;
216
216
  }
217
217
  } else {
@@ -228,14 +228,8 @@ var runSocketApiValidation = async ({
228
228
 
229
229
  // src/_shared/applyApiRateLimits.ts
230
230
  import { checkRateLimit, getProjectConfig as getProjectConfig3, dispatchHook as dispatchHook2, getLogger as getLogger6 } from "@luckystack/core";
231
-
232
- // src/_shared/rateLimitIdentity.ts
233
- import { deriveTokenBucketId } from "@luckystack/core";
234
-
235
- // src/_shared/applyApiRateLimits.ts
236
231
  var applyApiRateLimits = async ({
237
232
  resolvedIp,
238
- token,
239
233
  user,
240
234
  resolvedName,
241
235
  rateLimit,
@@ -247,8 +241,8 @@ var applyApiRateLimits = async ({
247
241
  if (effectiveApiLimit !== false && effectiveApiLimit > 0) {
248
242
  const identityCb = config.rateLimiting.identity;
249
243
  const customIdentity = identityCb?.({ routeName: resolvedName, userId: user?.id ?? null, ip: resolvedIp, transport }) ?? null;
250
- const requesterIdentity = customIdentity?.id ?? (token ? deriveTokenBucketId(token) : resolvedIp);
251
- const keyPrefix = customIdentity?.scope ?? (token ? "token" : "ip");
244
+ const requesterIdentity = customIdentity?.id ?? user?.id ?? resolvedIp;
245
+ const keyPrefix = customIdentity?.scope ?? (user?.id ? "user" : "ip");
252
246
  const rateLimitKey = `${keyPrefix}:${requesterIdentity}:api:${resolvedName}`;
253
247
  const { allowed, resetIn } = await checkRateLimit({
254
248
  key: rateLimitKey,
@@ -257,14 +251,15 @@ var applyApiRateLimits = async ({
257
251
  });
258
252
  if (!allowed) {
259
253
  void dispatchHook2("rateLimitExceeded", {
260
- scope: token ? "user" : "ip",
254
+ scope: user?.id ? "user" : "ip",
261
255
  key: rateLimitKey,
262
256
  limit: effectiveApiLimit,
263
257
  windowMs: config.rateLimiting.windowMs,
264
258
  count: effectiveApiLimit + 1,
265
259
  route: resolvedName,
266
260
  userId: user?.id,
267
- ip: token ? void 0 : resolvedIp
261
+ //? IP only for an IP-keyed (anonymous) bucket; a user-keyed bucket omits it.
262
+ ip: user?.id ? void 0 : resolvedIp
268
263
  });
269
264
  if (shouldLogDev()) {
270
265
  getLogger6().warn(`api: rate limit exceeded for ${resolvedName}`, { route: resolvedName, key: rateLimitKey, transport });
@@ -298,7 +293,7 @@ var applyApiRateLimits = async ({
298
293
  };
299
294
 
300
295
  // src/handleApiRequest.ts
301
- var validateApiMessage = ({ msg, emitApiError }) => {
296
+ var validateApiMessage = async ({ msg, emitApiError }) => {
302
297
  const { name, data, responseIndex } = msg;
303
298
  if (typeof responseIndex !== "number") {
304
299
  if (shouldLogDev()) {
@@ -307,7 +302,7 @@ var validateApiMessage = ({ msg, emitApiError }) => {
307
302
  return null;
308
303
  }
309
304
  if (!name || !data || typeof name != "string" || typeof data != "object" || Array.isArray(data)) {
310
- emitApiError({
305
+ await emitApiError({
311
306
  response: { status: "error", errorCode: "api.invalidRequest" },
312
307
  fallbackHttpStatus: 400
313
308
  });
@@ -315,7 +310,7 @@ var validateApiMessage = ({ msg, emitApiError }) => {
315
310
  }
316
311
  return { name, data };
317
312
  };
318
- var checkApiAuth = ({ apiEntry, user, name, emitApiError }) => {
313
+ var checkApiAuth = async ({ apiEntry, user, name, emitApiError }) => {
319
314
  if (apiEntry.auth.login && !user?.id) {
320
315
  if (shouldLogDev()) {
321
316
  getLogger7().warn(`api: ${name} requires login`, { route: name });
@@ -326,7 +321,7 @@ var checkApiAuth = ({ apiEntry, user, name, emitApiError }) => {
326
321
  userId: null,
327
322
  transport: "socket"
328
323
  });
329
- emitApiError({
324
+ await emitApiError({
330
325
  response: { status: "error", errorCode: "auth.required" },
331
326
  fallbackHttpStatus: 401
332
327
  });
@@ -339,12 +334,12 @@ var checkApiAuth = ({ apiEntry, user, name, emitApiError }) => {
339
334
  }
340
335
  void dispatchHook3("apiAuthRejected", {
341
336
  routeName: name,
342
- reason: authResult.errorCode === "auth.misconfiguredPredicate" ? "invalid-condition" : "additional-failed",
337
+ reason: authResult.errorCode === "auth.invalidCondition" ? "invalid-condition" : "additional-failed",
343
338
  userId: user?.id ?? null,
344
339
  transport: "socket",
345
340
  failedKey: authResult.errorCode
346
341
  });
347
- emitApiError({
342
+ await emitApiError({
348
343
  response: {
349
344
  status: "error",
350
345
  // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- empty string errorCode must still fall back
@@ -374,7 +369,7 @@ var runSocketRateLimits = async ({ apiEntry, resolvedName, token, socket, user,
374
369
  transport: "socket"
375
370
  });
376
371
  if (!result.allowed) {
377
- emitApiError({
372
+ await emitApiError({
378
373
  response: {
379
374
  status: "error",
380
375
  errorCode: result.errorCode ?? "api.rateLimitExceeded",
@@ -494,10 +489,23 @@ var emitApiResult = async ({
494
489
  await dispatchHook3("postApiRespond", { routeName: resolvedName, user, response: formattedResponse });
495
490
  };
496
491
  async function handleApiRequest(args) {
497
- await runWithErrorTrackerIdentityScope(() => handleApiRequestInner(args));
492
+ const [error] = await tryCatch(() => runWithErrorTrackerIdentityScope(() => handleApiRequestInner(args)));
493
+ if (error) {
494
+ getLogger7().error("api: unhandled error in socket request handler", { error: error.message });
495
+ const msg = args.msg;
496
+ const responseIndex = msg?.responseIndex;
497
+ if (typeof responseIndex === "number") {
498
+ args.socket.emit(buildApiResponseEventName(responseIndex), {
499
+ status: "error",
500
+ errorCode: "api.serverExecutionFailed",
501
+ httpStatus: 500
502
+ });
503
+ }
504
+ }
498
505
  }
499
506
  async function handleApiRequestInner({ msg, socket, token }) {
500
- if (typeof msg != "object") {
507
+ const rawMsg = msg;
508
+ if (typeof rawMsg !== "object" || rawMsg === null || Array.isArray(rawMsg)) {
501
509
  if (shouldLogDev()) {
502
510
  getLogger7().warn("api: socket message was not a json object");
503
511
  }
@@ -512,11 +520,13 @@ async function handleApiRequestInner({ msg, socket, token }) {
512
520
  routeName: typeof msg.name === "string" ? msg.name : void 0
513
521
  });
514
522
  if (preMsgResult.stopped) {
515
- socket.emit(buildApiResponseEventName(responseIndex), {
516
- status: "error",
517
- errorCode: preMsgResult.signal.errorCode,
518
- httpStatus: preMsgResult.signal.httpStatus ?? 403
519
- });
523
+ if (typeof responseIndex === "number") {
524
+ socket.emit(buildApiResponseEventName(responseIndex), {
525
+ status: "error",
526
+ errorCode: preMsgResult.signal.errorCode,
527
+ httpStatus: preMsgResult.signal.httpStatus ?? 403
528
+ });
529
+ }
520
530
  return;
521
531
  }
522
532
  const user = await readSession(token);
@@ -524,30 +534,44 @@ async function handleApiRequestInner({ msg, socket, token }) {
524
534
  const preferredLocale = extractLanguageFromHeader(socket.handshake.headers["x-language"]) ?? extractLanguageFromHeader(socket.handshake.headers["accept-language"]) ?? void 0;
525
535
  let currentRouteName;
526
536
  let currentPerRouteFormatter;
527
- const emitApiError = ({ response, fallbackHttpStatus }) => {
537
+ const emitApiError = async ({ response, fallbackHttpStatus }) => {
528
538
  const normalized = normalizeErrorResponse2({
529
539
  response,
530
540
  preferredLocale,
531
541
  userLanguage: user?.language,
532
542
  fallbackHttpStatus
533
543
  });
544
+ const routeNameForHook = currentRouteName ?? "api/unknown";
545
+ const preRespond = { routeName: routeNameForHook, user, response: normalized };
546
+ const preRespondResult = await dispatchHook3("preApiRespond", preRespond);
547
+ let finalResponse = preRespond.response;
548
+ if (preRespondResult.stopped) {
549
+ const stoppedEnvelope = normalizeErrorResponse2({
550
+ response: { status: "error", errorCode: preRespondResult.signal.errorCode },
551
+ preferredLocale,
552
+ userLanguage: user?.language,
553
+ fallbackHttpStatus: preRespondResult.signal.httpStatus ?? 403
554
+ });
555
+ finalResponse = stoppedEnvelope;
556
+ }
557
+ const transformPayload = { routeName: routeNameForHook, user, response: finalResponse };
558
+ await dispatchHook3("transformApiResponse", transformPayload);
534
559
  const formatted = applyErrorFormatter({
535
- // luckystack-allow no-as-unknown: formatter boundary — normalizeErrorResponse returns a narrower type than applyErrorFormatter accepts; fix requires @luckystack/core type alignment
536
- // eslint-disable-next-line no-restricted-syntax -- formatter boundary cast, mirrors handleHttpApiRequest
537
- response: normalized,
538
- routeName: currentRouteName ?? "api/unknown",
560
+ response: transformPayload.response,
561
+ routeName: routeNameForHook,
539
562
  transport: "socket",
540
563
  userId: user?.id,
541
564
  perRouteFormatter: currentPerRouteFormatter
542
565
  });
543
566
  socket.emit(buildApiResponseEventName(responseIndex), formatted);
567
+ await dispatchHook3("postApiRespond", { routeName: routeNameForHook, user, response: formatted });
544
568
  };
545
- const validated = validateApiMessage({ msg, emitApiError });
569
+ const validated = await validateApiMessage({ msg, emitApiError });
546
570
  if (!validated) return;
547
571
  const { name, data: normalizedData } = validated;
548
572
  const parsedRoute = parseTransportRouteName({ value: name, prefix: "api" });
549
573
  if (parsedRoute.status === "error") {
550
- emitApiError({
574
+ await emitApiError({
551
575
  response: {
552
576
  status: "error",
553
577
  errorCode: "routing.invalidServiceRouteName",
@@ -582,7 +606,7 @@ async function handleApiRequestInner({ msg, socket, token }) {
582
606
  count: logoutIpLimit + 1,
583
607
  ip: logoutIp
584
608
  });
585
- emitApiError({
609
+ await emitApiError({
586
610
  response: {
587
611
  status: "error",
588
612
  errorCode: "api.rateLimitExceeded",
@@ -610,7 +634,7 @@ async function handleApiRequestInner({ msg, socket, token }) {
610
634
  }
611
635
  const { apisObject, functionsObject } = await getRuntimeApiMaps();
612
636
  if (!apisObject[resolvedName]) {
613
- emitApiError({
637
+ await emitApiError({
614
638
  response: {
615
639
  status: "error",
616
640
  errorCode: "api.notFound",
@@ -627,7 +651,7 @@ async function handleApiRequestInner({ msg, socket, token }) {
627
651
  responseIndex,
628
652
  resolvedName
629
653
  });
630
- if (!checkApiAuth({ apiEntry, user, name, emitApiError })) {
654
+ if (!await checkApiAuth({ apiEntry, user, name, emitApiError })) {
631
655
  cleanupRequest();
632
656
  return;
633
657
  }
@@ -642,8 +666,8 @@ async function handleApiRequestInner({ msg, socket, token }) {
642
666
  normalizedData,
643
667
  user,
644
668
  cleanupRequest,
645
- emitInvalidInputType: () => {
646
- emitApiError({
669
+ emitInvalidInputType: async () => {
670
+ await emitApiError({
647
671
  response: { status: "error", errorCode: "api.invalidInputType" },
648
672
  fallbackHttpStatus: 400
649
673
  });
@@ -663,7 +687,7 @@ async function handleApiRequestInner({ msg, socket, token }) {
663
687
  });
664
688
  if (execution.stopped) {
665
689
  cleanupRequest();
666
- emitApiError({
690
+ await emitApiError({
667
691
  response: { status: "error", errorCode: execution.errorCode },
668
692
  fallbackHttpStatus: execution.httpStatus ?? 403
669
693
  });
@@ -690,7 +714,8 @@ import {
690
714
  normalizeErrorResponse as normalizeErrorResponse3,
691
715
  applyErrorFormatter as applyErrorFormatter2,
692
716
  isLoopbackIp,
693
- resolveClientIp as resolveClientIp2
717
+ resolveClientIp as resolveClientIp2,
718
+ resolveEnvKey
694
719
  } from "@luckystack/core";
695
720
 
696
721
  // src/_shared/httpValidationStage.ts
@@ -847,7 +872,7 @@ function runHttpApiAuth({
847
872
  }
848
873
  void dispatchHook5("apiAuthRejected", {
849
874
  routeName: resolvedName,
850
- reason: authResult.errorCode === "auth.misconfiguredPredicate" ? "invalid-condition" : "additional-failed",
875
+ reason: authResult.errorCode === "auth.invalidCondition" ? "invalid-condition" : "additional-failed",
851
876
  userId: ctx.user?.id ?? null,
852
877
  transport: "http",
853
878
  ip: requesterIp,
@@ -872,7 +897,7 @@ async function applyHttpApiRateLimits({
872
897
  rateLimit
873
898
  }, ctx) {
874
899
  const effectiveIp = requesterIp ?? resolveClientIp2({ rawAddress: void 0, headers: {} });
875
- const skipGlobalIpForLoopback = getProjectConfig5().rateLimiting.skipLoopbackInDev && process.env.NODE_ENV !== "production" && isLoopbackIp(requesterIp ?? "");
900
+ const skipGlobalIpForLoopback = getProjectConfig5().rateLimiting.skipLoopbackInDev && resolveEnvKey() !== "production" && isLoopbackIp(requesterIp ?? "");
876
901
  const rateLimitResult = await applyApiRateLimits({
877
902
  resolvedIp: effectiveIp,
878
903
  token,
@@ -955,7 +980,7 @@ async function runHttpApiExecution({
955
980
  fallbackHttpStatus: preExecuteResult.signal.httpStatus ?? 403
956
981
  });
957
982
  }
958
- const timeoutMs = getProjectConfig5().api.requestTimeoutMs;
983
+ const timeoutMs = stream ? false : getProjectConfig5().api.requestTimeoutMs;
959
984
  const combinedController = new AbortController();
960
985
  if (effectiveAbortSignal.aborted) {
961
986
  combinedController.abort();
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/handleApiRequest.ts","../src/_shared/logFlags.ts","../src/_shared/responseEnvelope.ts","../src/_shared/requestLifecycle.ts","../src/_shared/backpressure.ts","../src/_shared/socketValidationStage.ts","../src/_shared/inputTypeWarning.ts","../src/_shared/applyApiRateLimits.ts","../src/_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 // luckystack-allow no-as-unknown: formatter boundary — normalizeErrorResponse returns a narrower type than applyErrorFormatter accepts; fix requires @luckystack/core type alignment\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 // luckystack-allow no-as-unknown: engine.io internal — `conn` is not typed on Socket; this is the documented access pattern\n // eslint-disable-next-line no-restricted-syntax -- engine.io internals boundary\n const conn = (socket as unknown as { conn?: EngineIoConnLike }).conn;\n //? API-O15 — if engine.io renames `conn` this silently becomes a no-op\n //? (buffer always 0, writable always true). Warn in dev so a rename is\n //? caught before backpressure is broken in production.\n if (shouldLogDev() && !conn) {\n getLogger().warn('api: socket.conn absent — engine.io internal may have been renamed; backpressure is a no-op');\n }\n const packets = conn?.writeBuffer?.length ?? 0;\n const writable = conn?.transport?.writable ?? true;\n if (!writable) return;\n if (packets < packetThreshold) return;\n await new Promise<void>((resolve) => setTimeout(resolve, 10));\n }\n };\n};\n\n//? HTTP/SSE has no socket write-buffer to measure — `flushPressure` is a no-op\n//? on that transport. Shared at module scope so per-request allocation stays\n//? cheap and the handler param shape stays consistent across transports.\nexport const httpApiFlushPressureNoop: ApiFlushPressure = async () => {\n /* SSE backpressure is the caller's responsibility (res.write returns bool) */\n};\n","import type { BaseSessionLayout as SessionLayout } from '@luckystack/core';\nimport { dispatchHook, validateInputByType, getLogger } from '@luckystack/core';\nimport type { RuntimeApiEntry } from './apiTypes';\nimport { shouldLogDev } from './logFlags';\nimport { warnIfInputTypeMissing } from './inputTypeWarning';\n\n//? SOCKET-transport input-validation stage. Extracted from `handleApiRequest`\n//? per the `api` package audit (god-function decomposition). Behaviour is\n//? identical to the former inline block:\n//? • resolves the per-route validation mode (`'relaxed'` / `{ input: 'skip' }`\n//? => skip Zod validation; default `'strict'`);\n//? • dispatches `preApiValidate`, runs `validateInputByType` in strict mode,\n//? then `postApiValidate` (always, including the relaxed skip path);\n//? • on validation failure emits the GENERIC `api.invalidInputType` (the raw\n//? validator message is NEVER echoed to the client — it only travels to the\n//? postApiValidate hook + dev logs) and returns `false`.\n//? Error emission + per-request cleanup are delegated to the caller's closures\n//? so the orchestrator keeps ownership of the abort/cleanup/emitError refs.\n\ntype EmitInvalidInputType = () => void;\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 // luckystack-allow no-as-unknown: formatter boundary — ApiNetworkResponse union does not structurally satisfy applyErrorFormatter input; fix requires @luckystack/core type alignment\n // eslint-disable-next-line no-restricted-syntax -- formatter boundary cast\n const formatterInput = transformPayload.response as unknown as Record<string, unknown> & { status?: string };\n const formattedResponse = applyErrorFormatter({\n response: formatterInput,\n routeName: routeNameForHook,\n transport: 'http',\n userId: user?.id,\n perRouteFormatter,\n }) as ApiNetworkResponse;\n\n await dispatchHook('postApiRespond', { routeName: routeNameForHook, user, response: formattedResponse });\n\n return formattedResponse;\n}\n\ninterface RunHandleHttpApiRequestResult {\n response: ApiNetworkResponse;\n user: SessionLayout | null;\n preferredLocale: string | null | undefined;\n perRouteFormatter?: ErrorFormatter;\n}\n\nasync function runHandleHttpApiRequest(params: HttpApiRequestParams): Promise<RunHandleHttpApiRequestResult> {\n const preferredLocale =\n extractLanguageFromHeader(params.xLanguageHeader)\n ?? extractLanguageFromHeader(params.acceptLanguageHeader);\n const user = await readSession(params.token);\n //? ET-02 — bind the resolved session into the active per-request ALS identity\n //? box (opened by `handleHttpApiRequest`) so every capture in this request\n //? attributes to this user. The legacy global is still set via the\n //? `preApiValidate` hook subscriber; the ALS read wins at capture time.\n setCurrentErrorTrackerIdentity(user?.id ? { id: user.id, email: user.email ?? undefined, username: user.name ?? undefined } : null);\n const formatterHolder: { current?: ErrorFormatter } = {};\n const response = await runHandleHttpApiRequestInner(params, user, preferredLocale, formatterHolder);\n return { response, user, preferredLocale, perRouteFormatter: formatterHolder.current };\n}\n\n// ---------------------------------------------------------------------------\n// Private staged helpers for runHandleHttpApiRequestInner\n// ---------------------------------------------------------------------------\n\n/** Shared context threaded through each pipeline stage. */\ninterface HttpApiPipelineContext {\n buildNetworkError: (args: {\n response: { status: 'error'; httpStatus?: number; errorCode?: string; errorParams?: { key: string; value: string | number | boolean }[] };\n fallbackHttpStatus?: number;\n }) => ApiNetworkResponse;\n user: SessionLayout | null;\n preferredLocale: string | null | undefined;\n}\n\n/**\n * Validates the raw request shape (name string, data object, route-name\n * parsing) and resolves the normalised route name + runtime maps.\n * Returns an error response when any guard fails, otherwise the resolved\n * artefacts needed by later stages.\n */\nasync function validateHttpApiRequestShape(\n name: string,\n data: Record<string, unknown>,\n ctx: HttpApiPipelineContext,\n): Promise<\n | ApiNetworkResponse\n | {\n resolvedName: string;\n requestData: Record<string, unknown>;\n apisObject: Record<string, unknown>;\n functionsObject: Record<string, unknown>;\n }\n> {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime defense: caller may pass non-string despite types\n if (!name || typeof name !== 'string') {\n return ctx.buildNetworkError({\n response: { status: 'error', errorCode: 'api.invalidName' },\n fallbackHttpStatus: 400,\n });\n }\n\n //? API-O5 — mirror the socket guard: `typeof [] === 'object'`, so an array\n //? payload would pass the original check and hit a `{...}`-destructuring\n //? handler, causing a 500 instead of the correct 400.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime defense against malformed HTTP payloads despite types\n if (typeof data !== 'object' || data === null || Array.isArray(data)) {\n return ctx.buildNetworkError({\n response: { status: 'error', errorCode: 'api.invalidDataObject' },\n fallbackHttpStatus: 400,\n });\n }\n\n const normalizedName = name.startsWith('api/') ? name : `api/${name}`;\n const parsedRoute = parseTransportRouteName({ value: normalizedName, prefix: 'api' });\n if (parsedRoute.status === 'error') {\n return ctx.buildNetworkError({\n response: {\n status: 'error',\n errorCode: 'routing.invalidServiceRouteName',\n errorParams: [{ key: 'name', value: normalizedName }],\n },\n fallbackHttpStatus: 400,\n });\n }\n\n const resolvedName = parsedRoute.normalizedFullName;\n\n if (shouldLogDev()) {\n getLogger().debug(`http api: ${resolvedName} called`);\n }\n\n const { apisObject, functionsObject } = await getRuntimeApiMapsFromSource();\n\n if (!apisObject[resolvedName]) {\n return ctx.buildNetworkError({\n response: {\n status: 'error',\n errorCode: 'api.notFound',\n errorParams: [{ key: 'name', value: resolvedName }],\n },\n fallbackHttpStatus: 404,\n });\n }\n\n return { resolvedName, requestData: data, apisObject, functionsObject };\n}\n\n/**\n * Runs the login-required guard and the `validateRequest` additional-predicate\n * check. Returns an error response on failure, or `null` to signal success.\n *\n * Pipeline order: auth → rate-limit → method → validate → execute.\n * Auth runs first so unauthenticated probes can't enumerate routes or\n * learn input shape from `inputValidation.message` / method-mismatch params.\n * API-O7 — failed-auth requests do NOT consume a rate-limit bucket here.\n * Brute-force lockout is the responsibility of `@luckystack/login`\n * (per-account lockout counter via `authLockout.ts`); the `apiAuthRejected`\n * hook lets consumers add additional throttles. Consuming a per-IP bucket on\n * auth-fail would let an attacker DoS a victim's IP with bad credentials.\n */\nfunction runHttpApiAuth(\n {\n name,\n resolvedName,\n requesterIp,\n auth,\n }: {\n name: string;\n resolvedName: string;\n requesterIp: string | undefined;\n auth: RuntimeApiEntry['auth'];\n },\n ctx: HttpApiPipelineContext,\n): ApiNetworkResponse | null {\n if (auth.login && !ctx.user?.id) {\n if (shouldLogDev()) {\n getLogger().warn(`http-api: ${name} requires login`, { route: name, transport: 'http' });\n }\n void dispatchHook('apiAuthRejected', {\n routeName: resolvedName,\n reason: 'login-required',\n userId: null,\n transport: 'http',\n ip: requesterIp,\n });\n return ctx.buildNetworkError({\n response: { status: 'error', errorCode: 'auth.required' },\n fallbackHttpStatus: 401,\n });\n }\n\n //? NO bare `if (!user) -> auth.forbidden` here. Public routes\n //? (auth.login: false) must be callable without a session — exactly like the\n //? socket API handler (handleApiRequest) and both sync handlers.\n //? `validateRequest` is the single source of truth for `additional` auth\n //? predicates. (This handler previously forbade ALL anonymous HTTP calls,\n //? diverging from the other three transports and making public routes\n //? unreachable over HTTP — e.g. the test-runner contract/rate-limit sweeps.)\n //? `user` may be null on a public route; validateRequest tolerates it (and\n //? only the login/additional predicates read it). Same `user!` shape the\n //? socket API handler and both sync handlers use.\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n const authResult = validateRequest({ auth, user: ctx.user! });\n if (authResult.status === 'error') {\n if (shouldLogDev()) {\n getLogger().warn(`http-api: auth failed for ${name}`, { route: name, errorCode: authResult.errorCode, transport: 'http' });\n }\n void dispatchHook('apiAuthRejected', {\n routeName: resolvedName,\n reason: authResult.errorCode === 'auth.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":"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","names":["getProjectConfig","checkRateLimit","dispatchHook","getLogger","normalizeErrorResponse","getLogger","getLogger","getLogger","getLogger","getLogger","getProjectConfig","getLogger","getLogger","getProjectConfig","dispatchHook","getLogger","getProjectConfig","dispatchHook","getLogger","getLogger","dispatchHook","getProjectConfig","normalizeErrorResponse","checkRateLimit","getProjectConfig","readSession","runWithErrorTrackerIdentityScope","setCurrentErrorTrackerIdentity","validateRequest","tryCatch","parseTransportRouteName","dispatchHook","getLogger","extractLanguageFromHeader","normalizeErrorResponse","applyErrorFormatter","resolveClientIp","dispatchHook","validateInputByType","getLogger","dispatchHook","validateInputByType","getLogger","runWithErrorTrackerIdentityScope","dispatchHook","normalizeErrorResponse","applyErrorFormatter","extractLanguageFromHeader","readSession","setCurrentErrorTrackerIdentity","parseTransportRouteName","getLogger","validateRequest","resolveClientIp","getProjectConfig","tryCatch"]}
1
+ {"version":3,"sources":["../src/handleApiRequest.ts","../src/_shared/logFlags.ts","../src/_shared/responseEnvelope.ts","../src/_shared/requestLifecycle.ts","../src/_shared/backpressure.ts","../src/_shared/socketValidationStage.ts","../src/_shared/inputTypeWarning.ts","../src/_shared/applyApiRateLimits.ts","../src/handleHttpApiRequest.ts","../src/_shared/httpValidationStage.ts"],"sourcesContent":["import type { apiMessage, PostApiExecutePayload, BaseSessionLayout as SessionLayout, ErrorFormatter } from '@luckystack/core';\nimport {\n readSession,\n performLogout,\n runWithErrorTrackerIdentityScope,\n setCurrentErrorTrackerIdentity,\n getProjectConfig,\n getRuntimeApiMaps,\n validateRequest,\n checkRateLimit,\n tryCatch,\n parseTransportRouteName,\n buildApiResponseEventName,\n dispatchHook,\n getLogger,\n resolveClientIp,\n extractLanguageFromHeader,\n normalizeErrorResponse,\n applyErrorFormatter,\n} from '@luckystack/core';\n//? checkRateLimit and resolveClientIp are still used directly for the logout\n//? global-IP bucket (which doesn't go through the shared applyApiRateLimits helper\n//? because the logout shortcut bypasses the per-route bucket entirely).\nimport { Socket } from 'socket.io';\nimport type { ApiStreamPayload, ApiFlushPressure, RuntimeApiResponse, RuntimeApiEntry } from './_shared/apiTypes';\nimport { shouldLogDev } from './_shared/logFlags';\nimport { normalizeApiResponse } from './_shared/responseEnvelope';\nimport { createApiRequestLifecycle } from './_shared/requestLifecycle';\nimport { runSocketApiValidation } from './_shared/socketValidationStage';\nimport { applyApiRateLimits } from './_shared/applyApiRateLimits';\n\ninterface handleApiRequestType {\n msg: apiMessage,\n socket: Socket,\n token: string | null,\n}\n\ninterface ApiErrorResponse {\n status: 'error';\n httpStatus?: number;\n errorCode?: string;\n errorParams?: { key: string; value: string | number | boolean; }[];\n}\n\ntype EmitApiError = (args: { response: ApiErrorResponse; fallbackHttpStatus?: number; }) => Promise<void>;\n\nconst validateApiMessage = async ({ msg, emitApiError }: {\n msg: apiMessage;\n emitApiError: EmitApiError;\n}): Promise<{ name: string; data: Record<string, unknown> } | null> => {\n const { name, data, responseIndex } = msg;\n\n //? Drop a message with no usable response channel. Written as a single\n //? `typeof` test (equivalent to the former `!responseIndex && typeof !==\n //? 'number'` double-negative) so a `responseIndex: 0` is correctly KEPT — a\n //? future \"simplify\" to `!responseIndex` would silently drop every index-0\n //? request.\n if (typeof responseIndex !== 'number') {\n if (shouldLogDev()) {\n getLogger().warn('api: no response index given');\n }\n return null;\n }\n\n //? Positive plain-object guard (parity with the sync handler). `!data`\n //? excludes `null`; `Array.isArray` rejects an ARRAY payload that\n //? `typeof === 'object'` would otherwise admit to a handler expecting an\n //? object.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime defense: msg arrives from an untyped socket frame\n if (!name || !data || typeof name != 'string' || typeof data != 'object' || Array.isArray(data)) {\n await emitApiError({\n response: { status: 'error', errorCode: 'api.invalidRequest' },\n fallbackHttpStatus: 400,\n });\n return null;\n }\n\n return { name, data: data as Record<string, unknown> };\n};\n\nconst checkApiAuth = async ({ apiEntry, user, name, emitApiError }: {\n apiEntry: RuntimeApiEntry;\n user: SessionLayout | null;\n name: string;\n emitApiError: EmitApiError;\n}): Promise<boolean> => {\n if (apiEntry.auth.login && !user?.id) {\n if (shouldLogDev()) {\n getLogger().warn(`api: ${name} requires login`, { route: name });\n }\n void dispatchHook('apiAuthRejected', {\n routeName: name,\n reason: 'login-required',\n userId: null,\n transport: 'socket',\n });\n await emitApiError({\n response: { status: 'error', errorCode: 'auth.required' },\n fallbackHttpStatus: 401,\n });\n return false;\n }\n\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n const authResult = validateRequest({ auth: apiEntry.auth, user: user! });\n if (authResult.status === 'error') {\n if (shouldLogDev()) {\n getLogger().warn(`api: auth failed for ${name}`, { route: name, errorCode: authResult.errorCode });\n }\n void dispatchHook('apiAuthRejected', {\n routeName: name,\n reason: authResult.errorCode === 'auth.invalidCondition' ? 'invalid-condition' : 'additional-failed',\n userId: user?.id ?? null,\n transport: 'socket',\n failedKey: authResult.errorCode,\n });\n await emitApiError({\n response: {\n status: 'error',\n // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- empty string errorCode must still fall back\n errorCode: authResult.errorCode || 'auth.forbidden',\n errorParams: authResult.errorParams,\n httpStatus: authResult.httpStatus,\n },\n fallbackHttpStatus: authResult.httpStatus ?? 403,\n });\n return false;\n }\n\n return true;\n};\n\n//? API-O8 — rate-limit logic extracted to `_shared/applyApiRateLimits.ts` so\n//? both transports share one implementation. The socket transport resolves the\n//? effective IP here (once, from socket internals) before delegating to the helper.\nconst runSocketRateLimits = async ({ apiEntry, resolvedName, token, socket, user, emitApiError }: {\n apiEntry: RuntimeApiEntry;\n resolvedName: string;\n token: string | null;\n socket: Socket;\n user: SessionLayout | null;\n emitApiError: EmitApiError;\n}): Promise<boolean> => {\n //? Resolve the real client IP once. With the default `http.trustProxy: false`\n //? this returns `socket.handshake.address` verbatim (only IPv4-mapped IPv6 is\n //? canonicalized). When a trusted proxy is configured it honors XFF / X-Real-IP.\n const resolvedIp = resolveClientIp({\n rawAddress: socket.handshake.address,\n headers: socket.handshake.headers,\n trustProxy: getProjectConfig().http.trustProxy,\n trustedProxyHopCount: getProjectConfig().http.trustedProxyHopCount,\n });\n\n const result = await applyApiRateLimits({\n resolvedIp,\n token,\n user,\n resolvedName,\n rateLimit: apiEntry.rateLimit,\n transport: 'socket',\n });\n\n if (!result.allowed) {\n await emitApiError({\n response: {\n status: 'error',\n errorCode: result.errorCode ?? 'api.rateLimitExceeded',\n errorParams: [{ key: 'seconds', value: result.resetIn ?? 0 }],\n },\n fallbackHttpStatus: 429,\n });\n return false;\n }\n\n return true;\n};\n\nconst executeApiHandler = async ({ apiEntry, normalizedData, user, functionsObject, resolvedName, emitStream, abortSignal, abortController, flushPressure }: {\n apiEntry: RuntimeApiEntry;\n normalizedData: Record<string, unknown>;\n user: SessionLayout | null;\n functionsObject: Record<string, unknown>;\n resolvedName: string;\n emitStream: (payload?: ApiStreamPayload) => void;\n abortSignal: AbortSignal;\n abortController: AbortController;\n flushPressure: ApiFlushPressure;\n}): Promise<{ error: Error | null; result: RuntimeApiResponse | undefined; durationMs: number; timedOut?: boolean }> => {\n //? Span open/close + identity propagation moved to hook subscribers in\n //? `@luckystack/error-tracking` (preApiExecute / postApiExecute). This\n //? handler is now transport-agnostic instrumentation-wise.\n const executeStart = Date.now();\n\n //? API-O10 — race `main()` against a server-side timeout derived from\n //? `api.requestTimeoutMs`. When the timeout fires first we abort the\n //? AbortController (propagates to the handler's `abortSignal`) and resolve\n //? a sentinel so the orchestrator can emit a localized 504 envelope.\n const timeoutMs = getProjectConfig().api.requestTimeoutMs;\n let timeoutHandle: ReturnType<typeof setTimeout> | undefined;\n let timedOut = false;\n const timeoutPromise = timeoutMs === false\n ? null\n : new Promise<void>((resolve) => {\n timeoutHandle = setTimeout(() => {\n timedOut = true;\n abortController.abort();\n resolve();\n }, timeoutMs);\n });\n\n const handlerPromise = tryCatch(\n async () => await apiEntry.main({ data: normalizedData, user, functions: functionsObject, stream: emitStream, abortSignal, flushPressure }),\n undefined,\n {\n handler: 'handleApiRequest',\n api: resolvedName,\n userId: user?.id,\n transport: 'socket',\n },\n );\n\n if (timeoutPromise) {\n await Promise.race([handlerPromise, timeoutPromise]);\n }\n clearTimeout(timeoutHandle);\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- timedOut is mutated by the async timeout callback via Promise.race\n if (timedOut) {\n return { error: null, result: undefined, durationMs: Date.now() - executeStart, timedOut: true };\n }\n\n const [error, result] = await handlerPromise;\n return { error: error ?? null, result: result ?? undefined, durationMs: Date.now() - executeStart };\n};\n\n//? Execution stage: `preApiExecute` hook (stop-signal short-circuit) ->\n//? `executeApiHandler` (tryCatch-wrapped) -> `postApiExecute` hook. Extracted\n//? from the orchestrator per the `api` package audit. Returns either a stop\n//? signal (the orchestrator builds the localized error envelope from it) or the\n//? `(error, result)` pair to feed `emitApiResult`. The shared `executePayload`\n//? reference is created here and threaded through both hooks so the\n//? error-tracking auto-instrumentation can pin spans via WeakMap on it.\ntype SocketApiExecutionOutcome =\n | { stopped: true; errorCode: string; httpStatus?: number }\n | { stopped: false; error: Error | null; result: RuntimeApiResponse | undefined };\n\nconst runSocketApiExecution = async ({\n apiEntry,\n resolvedName,\n normalizedData,\n user,\n functionsObject,\n emitStream,\n abortSignal,\n abortController,\n flushPressure,\n}: {\n apiEntry: RuntimeApiEntry;\n resolvedName: string;\n normalizedData: Record<string, unknown>;\n user: SessionLayout | null;\n functionsObject: Record<string, unknown>;\n emitStream: (payload?: ApiStreamPayload) => void;\n abortSignal: AbortSignal;\n abortController: AbortController;\n flushPressure: ApiFlushPressure;\n}): Promise<SocketApiExecutionOutcome> => {\n //? Single payload reference reused by pre/post — auto-instrumentation\n //? subscribers in `@luckystack/error-tracking` pin spans via WeakMap on\n //? this object. Mutating `result/error/durationMs` is safe because\n //? handlers observe by-name (not by snapshot).\n const executePayload: PostApiExecutePayload = {\n routeName: resolvedName,\n data: normalizedData,\n user,\n transport: 'socket',\n result: undefined,\n error: null,\n durationMs: 0,\n };\n const preExecuteResult = await dispatchHook('preApiExecute', executePayload);\n if (preExecuteResult.stopped) {\n return { stopped: true, errorCode: preExecuteResult.signal.errorCode, httpStatus: preExecuteResult.signal.httpStatus };\n }\n\n const { error, result, durationMs, timedOut } = await executeApiHandler({\n apiEntry,\n normalizedData,\n user,\n functionsObject,\n resolvedName,\n emitStream,\n abortSignal,\n abortController,\n flushPressure,\n });\n\n //? API-O10 — a server-side timeout aborted the handler. Skip postApiExecute\n //? (no meaningful result to report) and surface a 504 to the client.\n if (timedOut) {\n return { stopped: true, errorCode: 'api.timeout', httpStatus: 504 };\n }\n\n executePayload.result = result;\n executePayload.error = error ?? null;\n executePayload.durationMs = durationMs;\n await dispatchHook('postApiExecute', executePayload);\n\n return { stopped: false, error, result };\n};\n\nconst emitApiResult = async ({\n socket,\n responseIndex,\n resolvedName,\n error,\n result,\n preferredLocale,\n user,\n perRouteFormatter,\n}: {\n socket: Socket;\n responseIndex: number;\n resolvedName: string;\n error: Error | null;\n result: RuntimeApiResponse | undefined;\n preferredLocale: string | undefined;\n user: SessionLayout | null;\n perRouteFormatter?: ErrorFormatter;\n}): Promise<void> => {\n const envelope = normalizeApiResponse({ resolvedName, error, result, preferredLocale, user });\n\n //? `preApiRespond` handlers may mutate `payload.response` to swap or rewrite\n //? the outgoing envelope (PII redaction, response signing, schema injection).\n //? A handler may also return a stop signal — when that happens we build a\n //? fresh error envelope from the signal's `errorCode` / `httpStatus` and\n //? emit that instead of the original.\n const preRespond = { routeName: resolvedName, user, response: envelope };\n const preRespondResult = await dispatchHook('preApiRespond', preRespond);\n\n const finalResponse = preRespondResult.stopped\n ? { ...normalizeErrorResponse({\n response: { status: 'error', errorCode: preRespondResult.signal.errorCode },\n preferredLocale,\n userLanguage: user?.language,\n fallbackHttpStatus: preRespondResult.signal.httpStatus ?? 403,\n }) }\n : preRespond.response;\n\n //? `transformApiResponse` fires AFTER preApiRespond and BEFORE socket emit.\n //? Designed for response mutation — header injection, body transformation,\n //? response signing — that's awkward inside preApiRespond. Handlers mutate\n //? `payload.response` in place (same shape as preApiRespond.response).\n const transformPayload = { routeName: resolvedName, user, response: finalResponse };\n await dispatchHook('transformApiResponse', transformPayload);\n\n //? Per-route → global → identity error formatter chain. No-op when status\n //? is success. Runs AFTER transformApiResponse so consumer hooks can still\n //? mutate the envelope before the final shape lands.\n const formattedResponse = applyErrorFormatter({\n response: transformPayload.response,\n routeName: resolvedName,\n transport: 'socket',\n userId: user?.id,\n perRouteFormatter,\n });\n\n socket.emit(buildApiResponseEventName(responseIndex), formattedResponse);\n\n //? `postApiRespond` is observation-only — the response is already on the\n //? wire. Use it for audit logging, metrics, dual-write replication.\n await dispatchHook('postApiRespond', { routeName: resolvedName, user, response: formattedResponse });\n};\n\n//? ET-02 — open a per-request error-tracker identity scope around the ENTIRE\n//? handler before any await that could interleave with another concurrent\n//? request. `readSession` (the first await below) is itself such a boundary, so\n//? the scope must wrap it. The session is written into the scope via\n//? `setCurrentErrorTrackerIdentity(...)` the moment it resolves; from then on\n//? every error-tracker capture during this request (handler throw, hook\n//? subscriber, fanout) reads THIS request's identity from the AsyncLocalStorage\n//? box, never another concurrent request's. Each request gets its own box.\nexport default async function handleApiRequest(args: handleApiRequestType): Promise<void> {\n //? handleApiRequestInner runs un-.catch()'d from loadSocket (a bare `void`\n //? call), so ANY throw from an unguarded dependency (readSession, the rate-limit\n //? Redis calls, getRuntimeApiMaps, the session store) would surface as a fatal\n //? unhandledRejection → worker crash on modern Node. Wrap the whole run (mirrors\n //? the sync handler's top-level tryCatch) and emit a generic error envelope on\n //? the response channel so the request fails cleanly instead of killing the worker.\n const [error] = await tryCatch(() => runWithErrorTrackerIdentityScope(() => handleApiRequestInner(args)));\n if (error) {\n getLogger().error('api: unhandled error in socket request handler', { error: error.message });\n const msg = args.msg as apiMessage | null;\n const responseIndex = msg?.responseIndex;\n if (typeof responseIndex === 'number') {\n args.socket.emit(buildApiResponseEventName(responseIndex), {\n status: 'error',\n errorCode: 'api.serverExecutionFailed',\n httpStatus: 500,\n });\n }\n }\n}\n\nasync function handleApiRequestInner({ msg, socket, token }: handleApiRequestType) {\n //? This event gets triggered when the client uses the apiRequest function.\n //? Validate the message, check auth, then execute the registered handler.\n\n //? `msg` is TYPED as apiMessage but the socket transport can deliver ANYTHING at\n //? runtime (null / array / primitive), so view it as `unknown` for the guard.\n //? `typeof null === 'object'` and arrays are objects, so the bare object check\n //? would let null/array through to `const { responseIndex } = msg` below —\n //? throwing before any handler. That throw, running un-.catch()'d from loadSocket,\n //? becomes a fatal unhandledRejection → worker crash; a connected socket could\n //? trigger it pre-auth via `socket.emit('api', null)` (remote DoS). Mirrors the\n //? sync handler's validateSyncMessage guard.\n const rawMsg: unknown = msg;\n if (typeof rawMsg !== 'object' || rawMsg === null || Array.isArray(rawMsg)) {\n if (shouldLogDev()) {\n getLogger().warn('api: socket message was not a json object');\n }\n return;\n }\n\n const { responseIndex } = msg;\n\n //? EXT-02 — per-message socket interception seam (counterpart to preHttpRequest\n //? in the HTTP pipeline and preSocketMessage in the sync handler). Fires before\n //? session lookup / route resolution / auth so a consumer can gate, throttle, or\n //? audit individual api messages. A stop signal rejects the message.\n const preMsgResult = await dispatchHook('preSocketMessage', {\n channel: 'api',\n socketId: socket.id,\n ip: socket.handshake.address,\n authenticated: Boolean(token),\n routeName: typeof msg.name === 'string' ? msg.name : undefined,\n });\n if (preMsgResult.stopped) {\n //? Guard the emit on a numeric responseIndex (mirrors the sync handler +\n //? validateApiMessage's own no-index branch below): this stop path runs\n //? BEFORE validateApiMessage, so a malformed frame lacking responseIndex\n //? would otherwise emit to the dead 'apiResponse-undefined' channel and\n //? leave the client hanging until its own ack timeout. Drop it silently.\n if (typeof responseIndex === 'number') {\n socket.emit(buildApiResponseEventName(responseIndex), {\n status: 'error',\n errorCode: preMsgResult.signal.errorCode,\n httpStatus: preMsgResult.signal.httpStatus ?? 403,\n });\n }\n return;\n }\n\n const user = await readSession(token);\n //? ET-02 — bind the resolved session into the active per-request ALS identity\n //? box so every subsequent capture attributes to this user. Identity also still\n //? flows to the legacy global via the `preApiValidate` hook subscriber in\n //? `@luckystack/error-tracking` (the ALS read takes precedence at capture time).\n setCurrentErrorTrackerIdentity(user?.id ? { id: user.id, email: user.email ?? undefined, username: user.name ?? undefined } : null);\n const preferredLocale =\n extractLanguageFromHeader(socket.handshake.headers['x-language'])\n ?? extractLanguageFromHeader(socket.handshake.headers['accept-language'])\n ?? undefined;\n\n //? Per-route formatter ref + resolved-name ref. Both start undefined and\n //? get set once the route is parsed + the apiEntry looked up — pre-lookup\n //? errors (invalid message shape, unknown route) emit with global formatter\n //? only because there's no apiEntry to read per-route formatter from. After\n //? lookup, every emit through `emitApiError` flows through the same chain.\n // eslint-disable-next-line prefer-const -- captured by emitApiError closure; assigned later after route resolution\n let currentRouteName: string | undefined;\n // eslint-disable-next-line prefer-const -- captured by emitApiError closure; assigned later after apiEntry lookup\n let currentPerRouteFormatter: ErrorFormatter | undefined;\n\n //? API-O6 parity — socket ERROR responses now run the SAME respond-hook chain\n //? as success responses (`emitApiResult`) and as the HTTP transport: preApiRespond\n //? (may rewrite/stop) → transformApiResponse (mutate) → error formatter → emit →\n //? postApiRespond (observe). Previously `emitApiError` emitted directly, so\n //? consumer respond hooks (PII redaction, response signing, audit logging) never\n //? fired on auth / rate-limit / validation / not-found rejections over WebSocket —\n //? a parity gap with HTTP, where every response (success AND error) runs them.\n const emitApiError: EmitApiError = async ({ response, fallbackHttpStatus }) => {\n const normalized = normalizeErrorResponse({\n response,\n preferredLocale,\n userLanguage: user?.language,\n fallbackHttpStatus,\n });\n const routeNameForHook = currentRouteName ?? 'api/unknown';\n\n //? preApiRespond may mutate the envelope or stop. A stop on an already-error\n //? response rebuilds a fresh error envelope from the signal (parity with\n //? `emitApiResult` + the HTTP path).\n // luckystack-allow no-as-any: hook payload boundary — normalizeErrorResponse returns a narrower type than the preApiRespond payload shape; fix requires @luckystack/core type alignment\n // eslint-disable-next-line no-restricted-syntax -- hook payload boundary, mirrors handleHttpApiRequest\n const preRespond = { routeName: routeNameForHook, user, response: normalized as unknown as { status: 'success' | 'error'; httpStatus?: number; [key: string]: unknown } };\n const preRespondResult = await dispatchHook('preApiRespond', preRespond);\n\n let finalResponse = preRespond.response;\n if (preRespondResult.stopped) {\n const stoppedEnvelope = normalizeErrorResponse({\n response: { status: 'error', errorCode: preRespondResult.signal.errorCode },\n preferredLocale,\n userLanguage: user?.language,\n fallbackHttpStatus: preRespondResult.signal.httpStatus ?? 403,\n });\n // luckystack-allow no-as-any: hook payload boundary — normalizeErrorResponse is narrower than the hook payload shape; fix requires @luckystack/core type alignment\n // eslint-disable-next-line no-restricted-syntax -- hook payload boundary, mirrors handleHttpApiRequest\n finalResponse = stoppedEnvelope as unknown as typeof preRespond.response;\n }\n\n const transformPayload = { routeName: routeNameForHook, user, response: finalResponse };\n await dispatchHook('transformApiResponse', transformPayload);\n\n const formatted = applyErrorFormatter({\n response: transformPayload.response,\n routeName: routeNameForHook,\n transport: 'socket',\n userId: user?.id,\n perRouteFormatter: currentPerRouteFormatter,\n });\n socket.emit(buildApiResponseEventName(responseIndex), formatted);\n\n //? postApiRespond is observation-only — the response is already on the wire.\n await dispatchHook('postApiRespond', { routeName: routeNameForHook, user, response: formatted });\n };\n\n const validated = await validateApiMessage({ msg, emitApiError });\n if (!validated) return;\n const { name, data: normalizedData } = validated;\n\n const parsedRoute = parseTransportRouteName({ value: name, prefix: 'api' });\n if (parsedRoute.status === 'error') {\n await emitApiError({\n response: {\n status: 'error',\n errorCode: 'routing.invalidServiceRouteName',\n errorParams: [{ key: 'name', value: name }],\n },\n fallbackHttpStatus: 400,\n });\n return;\n }\n\n const resolvedName = parsedRoute.normalizedFullName;\n currentRouteName = resolvedName;\n\n //? Built-in 'system/logout' needs special handling since it requires socket access.\n //? Match the full normalized route to avoid hijacking consumer routes whose final\n //? segment happens to be 'logout' (e.g. 'admin/logout/vN').\n if (parsedRoute.serviceRoute.normalizedRouteName === 'system/logout') {\n //? Apply the global per-IP bucket before logout so the shortcut can't be\n //? spammed uncapped — it bypasses applyApiRateLimits entirely.\n const logoutIpLimit = getProjectConfig().rateLimiting.defaultIpLimit;\n if (logoutIpLimit !== false && logoutIpLimit > 0) {\n const logoutIp = resolveClientIp({\n rawAddress: socket.handshake.address,\n headers: socket.handshake.headers,\n trustProxy: getProjectConfig().http.trustProxy,\n trustedProxyHopCount: getProjectConfig().http.trustedProxyHopCount,\n });\n const { allowed, resetIn } = await checkRateLimit({\n key: `ip:${logoutIp}:api:all`,\n limit: logoutIpLimit,\n windowMs: getProjectConfig().rateLimiting.windowMs,\n });\n if (!allowed) {\n void dispatchHook('rateLimitExceeded', {\n scope: 'ip',\n key: `ip:${logoutIp}:api:all`,\n limit: logoutIpLimit,\n windowMs: getProjectConfig().rateLimiting.windowMs,\n count: logoutIpLimit + 1,\n ip: logoutIp,\n });\n await emitApiError({\n response: {\n status: 'error',\n errorCode: 'api.rateLimitExceeded',\n errorParams: [{ key: 'seconds', value: resetIn }],\n },\n fallbackHttpStatus: 429,\n });\n return;\n }\n }\n //? API-O6 — route through `emitApiResult` so the respond-hook chain\n //? (preApiRespond, transformApiResponse, postApiRespond) fires for logout,\n //? matching all other routes. The return value of `performLogout` is void\n //? (success is implied by non-throw), so we pass a fixed success result.\n await performLogout({ token, socket, userId: user?.id ?? null });\n await emitApiResult({\n socket,\n responseIndex,\n resolvedName,\n error: null,\n result: { status: 'success', httpStatus: 200, result: true },\n preferredLocale,\n user,\n });\n return;\n }\n\n if (shouldLogDev()) {\n getLogger().debug(`api: ${resolvedName} called`);\n }\n\n const { apisObject, functionsObject } = await getRuntimeApiMaps();\n\n if (!apisObject[resolvedName]) {\n await emitApiError({\n response: {\n status: 'error',\n errorCode: 'api.notFound',\n errorParams: [{ key: 'name', value: name }],\n },\n fallbackHttpStatus: 404,\n });\n return;\n }\n\n const apiEntry = apisObject[resolvedName] as RuntimeApiEntry;\n currentPerRouteFormatter = apiEntry.errorFormatter;\n\n //? API-O4 — `httpMethod` (and the `inferHttpMethod` heuristic) is HTTP-ONLY.\n //? The socket transport does NOT enforce the declared HTTP method — all routes\n //? are callable over WebSocket regardless of method declaration. This is by\n //? design: socket.io has no HTTP-method concept and enforcing it would break\n //? existing consumers. If you need to restrict a route to HTTP only, do not\n //? expose it as a socket route, or gate it in `preSocketMessage`.\n\n //? Per-request lifecycle bundle (B1 abortController + cleanup, B2\n //? backpressure, abort-aware emitStream). Extracted to `_shared/\n //? requestLifecycle.ts`; the closures stay intact and the orchestrator\n //? threads the returned handles through the pipeline. `cleanupRequest`\n //? runs in every exit path below.\n const { abortController, abortSignal, emitStream, flushPressure, cleanupRequest } = createApiRequestLifecycle({\n socket,\n responseIndex,\n resolvedName,\n });\n\n //? Auth → rate-limit → validate → execute → respond.\n //? Auth runs before validate so unauthenticated probes can't enumerate\n //? routes or learn input shape from `inputValidation.message`.\n //? API-O7 — failed-auth requests do NOT consume a rate-limit bucket here.\n //? This is an explicit design choice: brute-force lockout is the\n //? responsibility of `@luckystack/login` (per-account lockout counter via\n //? `authLockout.ts`), and the `apiAuthRejected` hook lets consumers add\n //? additional throttles. Consuming the global-IP bucket on auth-fail would\n //? allow an attacker to trigger DoS for a victim account's IP by deliberately\n //? sending bad credentials.\n if (!(await checkApiAuth({ apiEntry, user, name, emitApiError }))) { cleanupRequest(); return; }\n\n const rateLimitOk = await runSocketRateLimits({ apiEntry, resolvedName, token, socket, user, emitApiError });\n if (!rateLimitOk) { cleanupRequest(); return; }\n\n //? Validation stage (mode resolve + preApiValidate -> validateInputByType ->\n //? postApiValidate). On failure it runs cleanup + emits the GENERIC\n //? `api.invalidInputType` (raw validator message never reaches the client)\n //? via the closures below, then returns false.\n const validationOk = await runSocketApiValidation({\n apiEntry,\n resolvedName,\n normalizedData,\n user,\n cleanupRequest,\n emitInvalidInputType: async () => { await emitApiError({\n response: { status: 'error', errorCode: 'api.invalidInputType' },\n fallbackHttpStatus: 400,\n }); },\n });\n if (!validationOk) return;\n\n //? Execution stage: preApiExecute (stop-signal short-circuit) -> handler ->\n //? postApiExecute. Returns either a stop signal or the (error, result) pair.\n const execution = await runSocketApiExecution({\n apiEntry,\n resolvedName,\n normalizedData,\n user,\n functionsObject,\n emitStream,\n abortSignal,\n abortController,\n flushPressure,\n });\n if (execution.stopped) {\n cleanupRequest();\n await emitApiError({\n response: { status: 'error', errorCode: execution.errorCode },\n fallbackHttpStatus: execution.httpStatus ?? 403,\n });\n return;\n }\n\n await emitApiResult({ socket, responseIndex, resolvedName, error: execution.error, result: execution.result, preferredLocale, user, perRouteFormatter: apiEntry.errorFormatter });\n cleanupRequest();\n}\n","import { getProjectConfig } from '@luckystack/core';\n\n//? CC-5 — config-getter convenience lambdas shared by both API transports.\n//? Read at call time (never at module load) so config registration order\n//? and hot-reload both stay correct.\n\n/** Gate for verbose per-request debug/warn/error logging. */\nexport const shouldLogDev = (): boolean => getProjectConfig().logging.devLogs;\n\n/** Gate for `emitStream` payload logging. */\nexport const shouldLogStream = (): boolean => getProjectConfig().logging.stream;\n","import type { BaseSessionLayout as SessionLayout } from '@luckystack/core';\nimport { defaultHttpStatusForResponse, normalizeErrorResponse, getLogger } from '@luckystack/core';\nimport { shouldLogDev } from './logFlags';\nimport type { ApiResponseEnvelope, RuntimeApiResponse } from './apiTypes';\n\n//? CC-6 — shared response-envelope assembly used by both API transports.\n//? Turns the `(error, result)` pair from the handler into the final wire\n//? envelope: localized error shapes (via `normalizeErrorResponse`) for\n//? failures/empty/invalid responses, or a success envelope with an inferred\n//? default HTTP status. Previously duplicated between `handleApiRequest`'s\n//? `buildApiResponseEnvelope` and `handleHttpApiRequest`'s inline assembly.\n\nexport const normalizeApiResponse = ({\n resolvedName,\n error,\n result,\n preferredLocale,\n user,\n}: {\n resolvedName: string;\n error: Error | null;\n result: RuntimeApiResponse | undefined | null;\n preferredLocale: string | null | undefined;\n user: SessionLayout | null;\n}): ApiResponseEnvelope => {\n if (error) {\n if (shouldLogDev()) {\n getLogger().error(`api: error in ${resolvedName}`, error, { route: resolvedName });\n }\n return {\n ...normalizeErrorResponse({\n response: { status: 'error', errorCode: 'api.internalServerError' },\n preferredLocale,\n userLanguage: user?.language,\n fallbackHttpStatus: 500,\n }),\n };\n }\n\n if (result === undefined || result === null) {\n if (shouldLogDev()) {\n getLogger().warn(`api: ${resolvedName} returned nothing`);\n }\n return {\n ...normalizeErrorResponse({\n response: { status: 'error', errorCode: 'api.emptyResponse' },\n preferredLocale,\n userLanguage: user?.language,\n fallbackHttpStatus: 500,\n }),\n };\n }\n\n if (shouldLogDev()) {\n getLogger().debug(`api: ${resolvedName} completed`);\n }\n\n //? Runtime guard: a handler is user code and can return a status outside the\n //? typed 'success' | 'error' union despite the RuntimeApiResponse type.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- defensive against malformed handler return at runtime\n if (result.status !== 'success' && result.status !== 'error') {\n return {\n ...normalizeErrorResponse({\n response: { status: 'error', errorCode: 'api.invalidResponseStatus' },\n preferredLocale,\n userLanguage: user?.language,\n fallbackHttpStatus: 500,\n }),\n };\n }\n\n if (result.status === 'error') {\n return {\n ...normalizeErrorResponse({\n response: result,\n preferredLocale,\n userLanguage: user?.language,\n fallbackHttpStatus: defaultHttpStatusForResponse({\n status: 'error',\n explicitHttpStatus: typeof result.httpStatus === 'number' ? result.httpStatus : undefined,\n }),\n }),\n };\n }\n\n //? API-O16 — the success envelope spreads ALL handler result fields directly\n //? onto the wire envelope. Handlers are responsible for not including sensitive\n //? data (e.g. `passwordHash`, raw session tokens) in their return value. There\n //? is no framework-level field allowlist; if your route aggregates a Prisma row\n //? with sensitive columns, pick explicit fields before returning.\n return {\n ...result,\n status: 'success',\n httpStatus: defaultHttpStatusForResponse({\n status: 'success',\n explicitHttpStatus: typeof result.httpStatus === 'number' ? result.httpStatus : undefined,\n }),\n };\n};\n","import type { Socket } from 'socket.io';\nimport {\n buildApiStreamEventName,\n getLogger,\n registerApiAbortController,\n unregisterApiAbortController,\n socketEventNames,\n} from '@luckystack/core';\nimport type { ApiStreamPayload, ApiFlushPressure } from './apiTypes';\nimport { shouldLogStream } from './logFlags';\nimport { createApiFlushPressure } from './backpressure';\n\n//? Per-request lifecycle bundle for the SOCKET API transport. Extracted from\n//? `handleApiRequest` per the `api` package audit (god-function decomposition).\n//? Keeps the abortController / cleanup / emitStream / flushPressure closures\n//? together and intact — the orchestrator wires them once and threads the\n//? returned handles through the pipeline. Behaviour is identical to the former\n//? inline closures:\n//? • B1 — per-request AbortController. Aborts on `apiCancel { responseIndex }`\n//? from the originator OR socket disconnect. `cleanupRequest()` runs in\n//? every exit path (errors, validation rejects, completion) to remove the\n//? disconnect listener and drop the cancel-registry entry. The signal is\n//? also handed to `emitStream` so chunks queued after an abort never hit\n//? the wire.\n//? • B2 — backpressure helper bound to the originator socket.\n\nexport interface ApiRequestLifecycle {\n abortController: AbortController;\n abortSignal: AbortSignal;\n emitStream: (payload?: ApiStreamPayload) => void;\n flushPressure: ApiFlushPressure;\n cleanupRequest: () => void;\n}\n\nexport const createApiRequestLifecycle = ({ socket, responseIndex, resolvedName }: {\n socket: Socket;\n responseIndex: number;\n resolvedName: string;\n}): ApiRequestLifecycle => {\n const abortController = new AbortController();\n const abortKey = registerApiAbortController(socket.id, responseIndex, abortController);\n const onSocketDisconnect = () => { abortController.abort(); };\n socket.once(socketEventNames.disconnect, onSocketDisconnect);\n\n let cleanupDone = false;\n const cleanupRequest = () => {\n if (cleanupDone) return;\n cleanupDone = true;\n socket.off(socketEventNames.disconnect, onSocketDisconnect);\n unregisterApiAbortController(abortKey);\n };\n\n const emitStream = (payload: ApiStreamPayload = {}) => {\n if (abortController.signal.aborted) {\n if (shouldLogStream()) {\n getLogger().debug(`api: ${resolvedName} stream skipped — request aborted`);\n }\n return;\n }\n if (shouldLogStream()) {\n getLogger().debug(`api: ${resolvedName} stream`, { payload });\n }\n socket.emit(buildApiStreamEventName(responseIndex), payload);\n };\n\n //? B2 — backpressure helper for API handlers. Same shape as the sync variant\n //? but always scoped to a single originator socket. Polls the engine.io\n //? writeBuffer length every 10ms until the buffer drains below threshold\n //? (default 1 MB ≈ 1024 packets at ~1KB each).\n const flushPressure = createApiFlushPressure(socket, abortController.signal);\n\n return { abortController, abortSignal: abortController.signal, emitStream, flushPressure, cleanupRequest };\n};\n","import type { Socket } from 'socket.io';\nimport { getLogger } from '@luckystack/core';\nimport type { ApiFlushPressure, ApiFlushPressureOptions } from './apiTypes';\nimport { shouldLogDev } from './logFlags';\n\n//? B2 — backpressure helper factory for the SOCKET API transport. Same shape\n//? as the sync variant; resolves once the originator socket's pending write\n//? buffer drops below the threshold. Default 1 MB (≈1024 packets at ~1KB each).\n//? Extracted from `handleApiRequest` per the `api` package audit (solid-isp).\n\ninterface EngineIoConnLike {\n writeBuffer?: { length: number };\n transport?: { writable?: boolean };\n}\n\nexport const createApiFlushPressure = (socket: Socket, abortSignal: AbortSignal): ApiFlushPressure => {\n return async ({ thresholdBytes }: ApiFlushPressureOptions = {}): Promise<void> => {\n const effectiveThresholdBytes = typeof thresholdBytes === 'number' && thresholdBytes > 0\n ? thresholdBytes\n : 1_048_576;\n const packetThreshold = Math.max(1, Math.ceil(effectiveThresholdBytes / 1024));\n //? Polls until the originator's socket write-buffer drains below the packet\n //? threshold, or the request aborts (loop condition), or the transport goes\n //? unwritable. Same exit conditions as the inline socket-handler original.\n while (!abortSignal.aborted) {\n //? Reading engine.io internals (writeBuffer / transport.writable) is the\n //? only way to measure socket backpressure; the cast is the documented\n //? boundary to those untyped internals. Moved verbatim from the socket\n //? handler — same access pattern, no behavioral change.\n // luckystack-allow no-as-unknown: engine.io internal — `conn` is not typed on Socket; this is the documented access pattern\n // eslint-disable-next-line no-restricted-syntax -- engine.io internals boundary\n const conn = (socket as unknown as { conn?: EngineIoConnLike }).conn;\n //? API-O15 — if engine.io renames `conn` this silently becomes a no-op\n //? (buffer always 0, writable always true). Warn in dev so a rename is\n //? caught before backpressure is broken in production.\n if (shouldLogDev() && !conn) {\n getLogger().warn('api: socket.conn absent — engine.io internal may have been renamed; backpressure is a no-op');\n }\n const packets = conn?.writeBuffer?.length ?? 0;\n const writable = conn?.transport?.writable ?? true;\n if (!writable) return;\n if (packets < packetThreshold) return;\n await new Promise<void>((resolve) => setTimeout(resolve, 10));\n }\n };\n};\n\n//? HTTP/SSE has no socket write-buffer to measure — `flushPressure` is a no-op\n//? on that transport. Shared at module scope so per-request allocation stays\n//? cheap and the handler param shape stays consistent across transports.\nexport const httpApiFlushPressureNoop: ApiFlushPressure = async () => {\n /* SSE backpressure is the caller's responsibility (res.write returns bool) */\n};\n","import type { BaseSessionLayout as SessionLayout } from '@luckystack/core';\nimport { dispatchHook, validateInputByType, getLogger } from '@luckystack/core';\nimport type { RuntimeApiEntry } from './apiTypes';\nimport { shouldLogDev } from './logFlags';\nimport { warnIfInputTypeMissing } from './inputTypeWarning';\n\n//? SOCKET-transport input-validation stage. Extracted from `handleApiRequest`\n//? per the `api` package audit (god-function decomposition). Behaviour is\n//? identical to the former inline block:\n//? • resolves the per-route validation mode (`'relaxed'` / `{ input: 'skip' }`\n//? => skip Zod validation; default `'strict'`);\n//? • dispatches `preApiValidate`, runs `validateInputByType` in strict mode,\n//? then `postApiValidate` (always, including the relaxed skip path);\n//? • on validation failure emits the GENERIC `api.invalidInputType` (the raw\n//? validator message is NEVER echoed to the client — it only travels to the\n//? postApiValidate hook + dev logs) and returns `false`.\n//? Error emission + per-request cleanup are delegated to the caller's closures\n//? so the orchestrator keeps ownership of the abort/cleanup/emitError refs.\n\ntype EmitInvalidInputType = () => void | Promise<void>;\n\nexport const resolveValidationMode = (validation: RuntimeApiEntry['validation']): 'strict' | 'relaxed' => {\n if (!validation) return 'strict';\n //? FAIL CLOSED on an unrecognized value (parity with the sync transport's\n //? `resolveSyncValidationMode`). Only the exact string `'relaxed'` (or the\n //? object form `{ input: 'skip' }`) skips validation; ANY other value — a\n //? typo like `'Strict'`/`'on'` — falls back to `'strict'` (validate) rather\n //? than being returned verbatim and silently DISABLING validation.\n if (typeof validation === 'string') {\n return validation === 'relaxed' ? 'relaxed' : 'strict';\n }\n return validation.input === 'skip' ? 'relaxed' : 'strict';\n};\n\nexport const runSocketApiValidation = async ({\n apiEntry,\n resolvedName,\n normalizedData,\n user,\n cleanupRequest,\n emitInvalidInputType,\n}: {\n apiEntry: RuntimeApiEntry;\n resolvedName: string;\n normalizedData: Record<string, unknown>;\n user: SessionLayout | null;\n cleanupRequest: () => void;\n emitInvalidInputType: EmitInvalidInputType;\n}): Promise<boolean> => {\n //? Per-route validation toggle. `'relaxed'` or `{ input: 'skip' }` skips\n //? runtime input validation entirely — useful for public webhooks (Stripe,\n //? Slack, GitHub) where the third party's payload shape isn't reasonable\n //? to model in TypeScript. Default `'strict'`.\n const validationMode = resolveValidationMode(apiEntry.validation);\n\n if (validationMode === 'strict') {\n warnIfInputTypeMissing(resolvedName, apiEntry.inputType);\n }\n await dispatchHook('preApiValidate', { routeName: resolvedName, data: normalizedData, user, transport: 'socket' });\n\n if (validationMode === 'strict') {\n const inputValidation = await validateInputByType({\n typeText: apiEntry.inputType,\n value: normalizedData,\n rootKey: 'data',\n filePath: apiEntry.inputTypeFilePath,\n });\n\n await dispatchHook('postApiValidate', {\n routeName: resolvedName,\n data: normalizedData,\n user,\n validation: inputValidation,\n transport: 'socket',\n });\n\n if (inputValidation.status === 'error') {\n //? SECURITY: do NOT echo the raw validator message (e.g.\n //? \"data.userId should be string\") back to the client — that lets an\n //? unauthenticated caller enumerate a route's input schema. The DETAILED\n //? message is routed to the `postApiValidate` hook above (and dev logs);\n //? the client only receives the generic `api.invalidInputType` code.\n if (shouldLogDev()) {\n getLogger().warn(`api: input validation failed for ${resolvedName}`, { route: resolvedName, message: inputValidation.message });\n }\n cleanupRequest();\n await emitInvalidInputType();\n return false;\n }\n } else {\n //? Relaxed: surface the skip via postApiValidate so audit handlers see it.\n await dispatchHook('postApiValidate', {\n routeName: resolvedName,\n data: normalizedData,\n user,\n validation: { status: 'success' },\n transport: 'socket',\n });\n }\n\n return true;\n};\n","import { getProjectConfig, getLogger } from '@luckystack/core';\n\n//? Dev-only one-shot warning when a route has no generated `inputType`\n//? (runtime Zod input validation is effectively disabled for it). Shared by\n//? both API transports so the \"warned once per route\" set is process-wide\n//? regardless of which transport hit the route first.\n\nconst warnedMissingInputType = new Set<string>();\n\nexport const warnIfInputTypeMissing = (resolvedName: string, inputType: string | undefined): void => {\n if (!getProjectConfig().dev.warnOnMissingInputType) return;\n if (inputType && inputType.trim().length > 0 && inputType.trim() !== 'any') return;\n if (warnedMissingInputType.has(resolvedName)) return;\n warnedMissingInputType.add(resolvedName);\n getLogger().warn(`api: route ${resolvedName} has no inputType — runtime input validation is disabled. Regenerate types or set the inputType on the handler.`);\n};\n","import type { BaseSessionLayout as SessionLayout } from '@luckystack/core';\nimport { checkRateLimit, getProjectConfig, dispatchHook, getLogger } from '@luckystack/core';\nimport { shouldLogDev } from './logFlags';\n\n//? API-O8 — shared rate-limit apply helper for BOTH API transports (socket +\n//? HTTP). Extracted so the two-bucket logic (per-route + global IP) stays in\n//? one place; each transport resolves the effective IP before calling here.\n\nexport interface ApplyApiRateLimitsArgs {\n /** Effective client IP (already resolved by the caller's transport adapter). */\n resolvedIp: string;\n token: string | null;\n user: SessionLayout | null;\n resolvedName: string;\n rateLimit: number | false | undefined;\n transport: 'socket' | 'http';\n /**\n * When true, skip the global-IP (`ip:…:api:all`) bucket check. Used by the\n * HTTP transport to honour `rateLimiting.skipLoopbackInDev` for loopback\n * callers without duplicating bucket logic here. The socket transport never\n * sets this (loopback skip is HTTP-only).\n */\n skipGlobalIpBucket?: boolean;\n}\n\nexport interface ApplyApiRateLimitsResult {\n /** True when all buckets allow the request; false when any bucket exceeded. */\n allowed: boolean;\n /** The error payload to emit when `allowed` is false. */\n errorCode?: string;\n resetIn?: number;\n}\n\n/**\n * Checks per-route + global-IP rate-limit buckets for an API request.\n * Returns `{ allowed: true }` when all buckets allow through, or\n * `{ allowed: false, errorCode, resetIn }` when any bucket is full.\n */\nexport const applyApiRateLimits = async ({\n resolvedIp,\n user,\n resolvedName,\n rateLimit,\n transport,\n skipGlobalIpBucket = false,\n}: ApplyApiRateLimitsArgs): Promise<ApplyApiRateLimitsResult> => {\n const config = getProjectConfig();\n const effectiveApiLimit = rateLimit ?? config.rateLimiting.defaultApiLimit;\n\n if (effectiveApiLimit !== false && effectiveApiLimit > 0) {\n const identityCb = config.rateLimiting.identity;\n const customIdentity = identityCb?.({ routeName: resolvedName, userId: user?.id ?? null, ip: resolvedIp, transport }) ?? null;\n //? Per-route bucket keyed on the VALIDATED user.id (never the token): a token\n //? is per-session, so token-keying let an authenticated abuser reset their\n //? bucket by re-logging-in (new token = fresh counter). user.id ties all of a\n //? user's sessions to one bucket; anonymous callers fall back to the IP.\n const requesterIdentity = customIdentity?.id ?? user?.id ?? resolvedIp;\n const keyPrefix = customIdentity?.scope ?? (user?.id ? 'user' : 'ip');\n const rateLimitKey = `${keyPrefix}:${requesterIdentity}:api:${resolvedName}`;\n\n const { allowed, resetIn } = await checkRateLimit({\n key: rateLimitKey,\n limit: effectiveApiLimit,\n windowMs: config.rateLimiting.windowMs,\n });\n\n if (!allowed) {\n //? The per-route bucket is keyed by the validated user when a token is\n //? present, else by the resolved IP (keyPrefix `ip`). Report the scope\n //? that matches the bucket's actual identity — an anonymous per-route\n //? bucket is IP-keyed, so it is `ip` (with `route` still set to mark it\n //? a per-route bucket vs the global `:api:all` IP bucket), never `route`.\n void dispatchHook('rateLimitExceeded', {\n scope: user?.id ? 'user' : 'ip',\n key: rateLimitKey,\n limit: effectiveApiLimit,\n windowMs: config.rateLimiting.windowMs,\n count: effectiveApiLimit + 1,\n route: resolvedName,\n userId: user?.id,\n //? IP only for an IP-keyed (anonymous) bucket; a user-keyed bucket omits it.\n ip: user?.id ? undefined : resolvedIp,\n });\n if (shouldLogDev()) {\n getLogger().warn(`api: rate limit exceeded for ${resolvedName}`, { route: resolvedName, key: rateLimitKey, transport });\n }\n return { allowed: false, errorCode: 'api.rateLimitExceeded', resetIn };\n }\n }\n\n const defaultIpLimit = config.rateLimiting.defaultIpLimit;\n if (!skipGlobalIpBucket && defaultIpLimit !== false && defaultIpLimit > 0) {\n const { allowed, resetIn } = await checkRateLimit({\n key: `ip:${resolvedIp}:api:all`,\n limit: defaultIpLimit,\n windowMs: config.rateLimiting.windowMs,\n });\n\n if (!allowed) {\n void dispatchHook('rateLimitExceeded', {\n scope: 'ip',\n key: `ip:${resolvedIp}:api:all`,\n limit: defaultIpLimit,\n windowMs: config.rateLimiting.windowMs,\n count: defaultIpLimit + 1,\n ip: resolvedIp,\n });\n if (shouldLogDev()) {\n getLogger().warn(`api: global IP rate limit exceeded`, { ip: resolvedIp, transport });\n }\n return { allowed: false, errorCode: 'api.rateLimitExceeded', resetIn };\n }\n }\n\n return { allowed: true };\n};\n","import type { BaseSessionLayout as SessionLayout, PostApiExecutePayload, ErrorFormatter } from '@luckystack/core';\nimport { getProjectConfig, readSession, getRuntimeApiMaps as getRuntimeApiMapsFromSource ,\n runWithErrorTrackerIdentityScope,\n setCurrentErrorTrackerIdentity,\n validateRequest,\n inferHttpMethod,\n HttpMethod,\n tryCatch,\n parseTransportRouteName,\n dispatchHook,\n getLogger, extractLanguageFromHeader, normalizeErrorResponse, applyErrorFormatter,\n isLoopbackIp, resolveClientIp, resolveEnvKey } from '@luckystack/core';\nimport type { ApiStreamPayload, RuntimeApiEntry } from './_shared/apiTypes';\nimport { shouldLogDev, shouldLogStream } from './_shared/logFlags';\nimport { normalizeApiResponse } from './_shared/responseEnvelope';\nimport { httpApiFlushPressureNoop } from './_shared/backpressure';\nimport { runHttpApiValidation } from './_shared/httpValidationStage';\nimport { applyApiRateLimits } from './_shared/applyApiRateLimits';\n\n\n\n\n\n\n/**\n * HTTP API Request Handler\n * \n * Handles API requests coming via HTTP (instead of WebSocket).\n * Reuses existing API handlers but returns results as HTTP response.\n * \n * Payload format:\n * ```json\n * {\n * \"name\": \"api/examples/publicApi\",\n * \"data\": { \"message\": \"hello\" }\n * }\n * ```\n * \n * @example\n * ```typescript\n * // In server.ts\n * if (pathname.startsWith('/api/')) {\n * const token = extractTokenFromRequest(req);\n * const body = await parseJsonBody(req);\n * const result = await handleHttpApiRequest({ name: body.name, data: body.data, token });\n * res.end(JSON.stringify(result));\n * }\n * ```\n */\n\ninterface HttpApiRequestParams {\n name: string;\n data: Record<string, unknown>;\n token: string | null;\n requesterIp?: string;\n xLanguageHeader?: string | string[];\n acceptLanguageHeader?: string | string[];\n /** HTTP method from the request */\n method?: HttpMethod;\n stream?: (payload: ApiHttpStreamEvent) => void;\n /**\n * Optional AbortSignal. The HTTP server (`@luckystack/server`) wires this\n * to `req.on('close', ...)` so a closed SSE/HTTP connection aborts in-flight\n * stream emits. Handlers receive it as `abortSignal` in params.\n */\n abortSignal?: AbortSignal;\n}\n\ntype ApiNetworkResponse<T = Record<string, unknown>> =\n | ({ status: 'success'; httpStatus: number } & T)\n | {\n status: 'error';\n httpStatus: number;\n message: string;\n errorCode: string;\n errorParams?: {\n key: string;\n value: string | number | boolean;\n }[];\n };\n\nexport type ApiHttpStreamEvent = ApiStreamPayload;\n\n//? ET-02 — wrap the whole HTTP request in a per-request error-tracker identity\n//? scope (opened before `readSession` in `runHandleHttpApiRequest`, the first\n//? interleaving await). The resolved session is written into the scope's ALS box\n//? the moment it resolves, so concurrent HTTP requests with different users can't\n//? cross-attribute captures. Each request gets its own isolated box.\nexport async function handleHttpApiRequest(params: HttpApiRequestParams): Promise<ApiNetworkResponse> {\n return runWithErrorTrackerIdentityScope(() => handleHttpApiRequestScoped(params));\n}\n\nasync function handleHttpApiRequestScoped(params: HttpApiRequestParams): Promise<ApiNetworkResponse> {\n const { response, user, preferredLocale, perRouteFormatter } = await runHandleHttpApiRequest(params);\n\n //? `name` may not parse to a valid route — pass the original raw name as\n //? routeName so hook handlers can still correlate the rejection.\n const routeNameForHook = params.name.startsWith('api/') ? params.name : `api/${params.name}`;\n\n const preRespond = { routeName: routeNameForHook, user, response: response as { status: 'success' | 'error'; httpStatus?: number; [key: string]: unknown } };\n const preRespondResult = await dispatchHook('preApiRespond', preRespond);\n\n //? Honor a stop signal from `preApiRespond`: rewrite the envelope into a\n //? localized error response per the signal's errorCode/httpStatus instead\n //? of letting the original through. Same contract as the socket variant.\n const finalResponse = preRespondResult.stopped\n ? normalizeErrorResponse({\n response: { status: 'error', errorCode: preRespondResult.signal.errorCode },\n preferredLocale,\n userLanguage: user?.language,\n fallbackHttpStatus: preRespondResult.signal.httpStatus ?? 403,\n }) as ApiNetworkResponse\n : preRespond.response as ApiNetworkResponse;\n\n //? `transformApiResponse` fires AFTER preApiRespond and BEFORE the wire\n //? response is finalized. Mirrors the socket handler (handleApiRequest)\n //? so BOTH transports run the same hook sequence per CLAUDE.md. Handlers\n //? mutate `payload.response` in place (header injection, body transform,\n //? response signing).\n const transformPayload = { routeName: routeNameForHook, user, response: finalResponse };\n await dispatchHook('transformApiResponse', transformPayload);\n\n //? Per-route → global → identity error formatter chain. Mirrors the socket\n //? handler so HTTP + WebSocket responses honor the same `errorFormatter`\n //? export on each route. No-op on success envelopes.\n //? `ApiNetworkResponse` is a discriminated union; `applyErrorFormatter`\n //? accepts the wider `Record<string, unknown> & { status?: string }` shape\n //? that union doesn't structurally satisfy. The double-cast is the\n //? documented framework boundary between the union and the formatter input.\n // luckystack-allow no-as-unknown: formatter boundary — ApiNetworkResponse union does not structurally satisfy applyErrorFormatter input; fix requires @luckystack/core type alignment\n // eslint-disable-next-line no-restricted-syntax -- formatter boundary cast\n const formatterInput = transformPayload.response as unknown as Record<string, unknown> & { status?: string };\n const formattedResponse = applyErrorFormatter({\n response: formatterInput,\n routeName: routeNameForHook,\n transport: 'http',\n userId: user?.id,\n perRouteFormatter,\n }) as ApiNetworkResponse;\n\n await dispatchHook('postApiRespond', { routeName: routeNameForHook, user, response: formattedResponse });\n\n return formattedResponse;\n}\n\ninterface RunHandleHttpApiRequestResult {\n response: ApiNetworkResponse;\n user: SessionLayout | null;\n preferredLocale: string | null | undefined;\n perRouteFormatter?: ErrorFormatter;\n}\n\nasync function runHandleHttpApiRequest(params: HttpApiRequestParams): Promise<RunHandleHttpApiRequestResult> {\n const preferredLocale =\n extractLanguageFromHeader(params.xLanguageHeader)\n ?? extractLanguageFromHeader(params.acceptLanguageHeader);\n const user = await readSession(params.token);\n //? ET-02 — bind the resolved session into the active per-request ALS identity\n //? box (opened by `handleHttpApiRequest`) so every capture in this request\n //? attributes to this user. The legacy global is still set via the\n //? `preApiValidate` hook subscriber; the ALS read wins at capture time.\n setCurrentErrorTrackerIdentity(user?.id ? { id: user.id, email: user.email ?? undefined, username: user.name ?? undefined } : null);\n const formatterHolder: { current?: ErrorFormatter } = {};\n const response = await runHandleHttpApiRequestInner(params, user, preferredLocale, formatterHolder);\n return { response, user, preferredLocale, perRouteFormatter: formatterHolder.current };\n}\n\n// ---------------------------------------------------------------------------\n// Private staged helpers for runHandleHttpApiRequestInner\n// ---------------------------------------------------------------------------\n\n/** Shared context threaded through each pipeline stage. */\ninterface HttpApiPipelineContext {\n buildNetworkError: (args: {\n response: { status: 'error'; httpStatus?: number; errorCode?: string; errorParams?: { key: string; value: string | number | boolean }[] };\n fallbackHttpStatus?: number;\n }) => ApiNetworkResponse;\n user: SessionLayout | null;\n preferredLocale: string | null | undefined;\n}\n\n/**\n * Validates the raw request shape (name string, data object, route-name\n * parsing) and resolves the normalised route name + runtime maps.\n * Returns an error response when any guard fails, otherwise the resolved\n * artefacts needed by later stages.\n */\nasync function validateHttpApiRequestShape(\n name: string,\n data: Record<string, unknown>,\n ctx: HttpApiPipelineContext,\n): Promise<\n | ApiNetworkResponse\n | {\n resolvedName: string;\n requestData: Record<string, unknown>;\n apisObject: Record<string, unknown>;\n functionsObject: Record<string, unknown>;\n }\n> {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime defense: caller may pass non-string despite types\n if (!name || typeof name !== 'string') {\n return ctx.buildNetworkError({\n response: { status: 'error', errorCode: 'api.invalidName' },\n fallbackHttpStatus: 400,\n });\n }\n\n //? API-O5 — mirror the socket guard: `typeof [] === 'object'`, so an array\n //? payload would pass the original check and hit a `{...}`-destructuring\n //? handler, causing a 500 instead of the correct 400.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime defense against malformed HTTP payloads despite types\n if (typeof data !== 'object' || data === null || Array.isArray(data)) {\n return ctx.buildNetworkError({\n response: { status: 'error', errorCode: 'api.invalidDataObject' },\n fallbackHttpStatus: 400,\n });\n }\n\n const normalizedName = name.startsWith('api/') ? name : `api/${name}`;\n const parsedRoute = parseTransportRouteName({ value: normalizedName, prefix: 'api' });\n if (parsedRoute.status === 'error') {\n return ctx.buildNetworkError({\n response: {\n status: 'error',\n errorCode: 'routing.invalidServiceRouteName',\n errorParams: [{ key: 'name', value: normalizedName }],\n },\n fallbackHttpStatus: 400,\n });\n }\n\n const resolvedName = parsedRoute.normalizedFullName;\n\n if (shouldLogDev()) {\n getLogger().debug(`http api: ${resolvedName} called`);\n }\n\n const { apisObject, functionsObject } = await getRuntimeApiMapsFromSource();\n\n if (!apisObject[resolvedName]) {\n return ctx.buildNetworkError({\n response: {\n status: 'error',\n errorCode: 'api.notFound',\n errorParams: [{ key: 'name', value: resolvedName }],\n },\n fallbackHttpStatus: 404,\n });\n }\n\n return { resolvedName, requestData: data, apisObject, functionsObject };\n}\n\n/**\n * Runs the login-required guard and the `validateRequest` additional-predicate\n * check. Returns an error response on failure, or `null` to signal success.\n *\n * Pipeline order: auth → rate-limit → method → validate → execute.\n * Auth runs first so unauthenticated probes can't enumerate routes or\n * learn input shape from `inputValidation.message` / method-mismatch params.\n * API-O7 — failed-auth requests do NOT consume a rate-limit bucket here.\n * Brute-force lockout is the responsibility of `@luckystack/login`\n * (per-account lockout counter via `authLockout.ts`); the `apiAuthRejected`\n * hook lets consumers add additional throttles. Consuming a per-IP bucket on\n * auth-fail would let an attacker DoS a victim's IP with bad credentials.\n */\nfunction runHttpApiAuth(\n {\n name,\n resolvedName,\n requesterIp,\n auth,\n }: {\n name: string;\n resolvedName: string;\n requesterIp: string | undefined;\n auth: RuntimeApiEntry['auth'];\n },\n ctx: HttpApiPipelineContext,\n): ApiNetworkResponse | null {\n if (auth.login && !ctx.user?.id) {\n if (shouldLogDev()) {\n getLogger().warn(`http-api: ${name} requires login`, { route: name, transport: 'http' });\n }\n void dispatchHook('apiAuthRejected', {\n routeName: resolvedName,\n reason: 'login-required',\n userId: null,\n transport: 'http',\n ip: requesterIp,\n });\n return ctx.buildNetworkError({\n response: { status: 'error', errorCode: 'auth.required' },\n fallbackHttpStatus: 401,\n });\n }\n\n //? NO bare `if (!user) -> auth.forbidden` here. Public routes\n //? (auth.login: false) must be callable without a session — exactly like the\n //? socket API handler (handleApiRequest) and both sync handlers.\n //? `validateRequest` is the single source of truth for `additional` auth\n //? predicates. (This handler previously forbade ALL anonymous HTTP calls,\n //? diverging from the other three transports and making public routes\n //? unreachable over HTTP — e.g. the test-runner contract/rate-limit sweeps.)\n //? `user` may be null on a public route; validateRequest tolerates it (and\n //? only the login/additional predicates read it). Same `user!` shape the\n //? socket API handler and both sync handlers use.\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n const authResult = validateRequest({ auth, user: ctx.user! });\n if (authResult.status === 'error') {\n if (shouldLogDev()) {\n getLogger().warn(`http-api: auth failed for ${name}`, { route: name, errorCode: authResult.errorCode, transport: 'http' });\n }\n void dispatchHook('apiAuthRejected', {\n routeName: resolvedName,\n reason: authResult.errorCode === 'auth.invalidCondition' ? 'invalid-condition' : 'additional-failed',\n userId: ctx.user?.id ?? null,\n transport: 'http',\n ip: requesterIp,\n failedKey: authResult.errorCode,\n });\n return ctx.buildNetworkError({\n response: {\n status: 'error',\n errorCode: authResult.errorCode ?? 'auth.forbidden',\n errorParams: authResult.errorParams,\n httpStatus: authResult.httpStatus,\n },\n fallbackHttpStatus: authResult.httpStatus ?? 403,\n });\n }\n\n return null;\n}\n\n/**\n * Resolves the effective client IP, computes the loopback-skip flag, and\n * delegates to the shared `applyApiRateLimits` helper.\n * Returns an error response when any bucket is exceeded, `null` otherwise.\n */\nasync function applyHttpApiRateLimits(\n {\n requesterIp,\n token,\n resolvedName,\n rateLimit,\n }: {\n requesterIp: string | undefined;\n token: string | null;\n resolvedName: string;\n rateLimit: number | false | undefined;\n },\n ctx: HttpApiPipelineContext,\n): Promise<ApiNetworkResponse | null> {\n //? API-O8 / API-O9 — resolve effective IP once for both rate-limit buckets.\n //? When the caller did not supply `requesterIp` we fall back to the shared\n //? `UNKNOWN_CLIENT_IP` sentinel so all unresolvable callers collapse into ONE\n //? deterministic bucket instead of mixing `'unknown'` and `'anonymous'`.\n const effectiveIp = requesterIp\n ?? resolveClientIp({ rawAddress: undefined, headers: {} });\n\n //? API-O2 — skip the global per-IP ABUSE limit for loopback traffic when the\n //? consumer explicitly opts in via `rateLimiting.skipLoopbackInDev` (default\n //? false). Only the cross-route `:api:all` bucket is skipped; per-route limits\n //? still apply. Pass the raw `requesterIp` (undefined → '') so only real\n //? loopback addresses qualify — avoids skipping for every unresolvable caller.\n const skipGlobalIpForLoopback = getProjectConfig().rateLimiting.skipLoopbackInDev\n && resolveEnvKey() !== 'production'\n && isLoopbackIp(requesterIp ?? '');\n\n const rateLimitResult = await applyApiRateLimits({\n resolvedIp: effectiveIp,\n token,\n user: ctx.user,\n resolvedName,\n rateLimit,\n transport: 'http',\n skipGlobalIpBucket: skipGlobalIpForLoopback,\n });\n\n if (!rateLimitResult.allowed) {\n return ctx.buildNetworkError({\n response: {\n status: 'error',\n errorCode: rateLimitResult.errorCode ?? 'api.rateLimitExceeded',\n errorParams: [{ key: 'seconds', value: rateLimitResult.resetIn ?? 0 }],\n },\n fallbackHttpStatus: 429,\n });\n }\n\n return null;\n}\n\n/**\n * Enforces the HTTP method constraint (declared or inferred).\n * Runs post-auth so unauthenticated probes don't learn that the route exists.\n * Returns an error response on mismatch, `null` otherwise.\n */\nfunction checkHttpApiMethod(\n {\n method,\n resolvedName,\n declaredMethod,\n }: {\n method: HttpMethod;\n resolvedName: string;\n declaredMethod: HttpMethod | undefined;\n },\n ctx: HttpApiPipelineContext,\n): ApiNetworkResponse | null {\n const expectedMethod = declaredMethod ?? inferHttpMethod(resolvedName);\n if (method !== expectedMethod) {\n if (shouldLogDev()) {\n getLogger().warn(`http api: method mismatch for ${resolvedName}`, { expected: expectedMethod, got: method });\n }\n return ctx.buildNetworkError({\n response: {\n status: 'error',\n errorCode: 'api.methodNotAllowed',\n errorParams: [{ key: 'method', value: expectedMethod }],\n },\n fallbackHttpStatus: 405,\n });\n }\n\n return null;\n}\n\n/**\n * Runs the handler execution stage: emitter setup, pre/post-execute hooks,\n * server-side timeout race, `main()` invocation, and response-envelope\n * assembly.\n */\nasync function runHttpApiExecution(\n {\n resolvedName,\n requestData,\n functionsObject,\n runtimeApiRoute,\n effectiveAbortSignal,\n stream,\n }: {\n resolvedName: string;\n requestData: Record<string, unknown>;\n functionsObject: Record<string, unknown>;\n runtimeApiRoute: RuntimeApiEntry;\n effectiveAbortSignal: AbortSignal;\n stream: HttpApiRequestParams['stream'];\n },\n ctx: HttpApiPipelineContext,\n): Promise<ApiNetworkResponse> {\n //? SSE has no socket write-buffer to measure — `flushPressure` is a no-op\n //? on HTTP transport. Kept for handler-shape parity with the socket path.\n const flushPressure = httpApiFlushPressureNoop;\n\n const emitApiStream = (payload: ApiStreamPayload = {}) => {\n if (!stream) {\n return;\n }\n if (effectiveAbortSignal.aborted) {\n if (shouldLogStream()) {\n getLogger().debug(`http api: ${resolvedName} stream skipped — request aborted`);\n }\n return;\n }\n\n if (shouldLogStream()) {\n getLogger().debug(`http api: ${resolvedName} stream`, { payload });\n }\n\n stream(payload);\n };\n\n //? Single payload reference reused by pre/post — auto-instrumentation\n //? subscribers in `@luckystack/error-tracking` pin spans via WeakMap on\n //? this object. Mutating `result/error/durationMs` is safe because\n //? handlers observe by-name (not by snapshot).\n const executePayload: PostApiExecutePayload = {\n routeName: resolvedName,\n data: requestData,\n user: ctx.user,\n transport: 'http',\n result: undefined,\n error: null,\n durationMs: 0,\n };\n const preExecuteResult = await dispatchHook('preApiExecute', executePayload);\n if (preExecuteResult.stopped) {\n return ctx.buildNetworkError({\n response: { status: 'error', errorCode: preExecuteResult.signal.errorCode },\n fallbackHttpStatus: preExecuteResult.signal.httpStatus ?? 403,\n });\n }\n\n //? API-O10 — race `main()` against a server-side timeout derived from\n //? `api.requestTimeoutMs`. When the timeout fires first we abort the\n //? effectiveAbortSignal chain and return a localized 504 envelope.\n //? A combined AbortController merges the caller's close signal and the\n //? timeout so the handler sees one unified abort.\n //? Streaming requests are EXEMPT from the wall-clock timeout even when one is\n //? configured: an SSE/stream endpoint is long-lived by design (live LLM tokens,\n //? progress) and a total-duration race would truncate it mid-stream. The\n //? abortSignal (wired to client disconnect) still tears it down when the caller\n //? leaves. Non-streaming requests honor `api.requestTimeoutMs` (default false).\n const timeoutMs = stream ? false : getProjectConfig().api.requestTimeoutMs;\n const combinedController = new AbortController();\n //? Forward the caller's abort (connection close) into the combined signal.\n if (effectiveAbortSignal.aborted) {\n combinedController.abort();\n } else {\n effectiveAbortSignal.addEventListener('abort', () => { combinedController.abort(); }, { once: true });\n }\n\n let timedOut = false;\n let timeoutHandle: ReturnType<typeof setTimeout> | undefined;\n const timeoutPromise = timeoutMs === false\n ? null\n : new Promise<void>((resolve) => {\n timeoutHandle = setTimeout(() => {\n timedOut = true;\n combinedController.abort();\n resolve();\n }, timeoutMs);\n });\n\n const executeStart = Date.now();\n const handlerPromise = tryCatch(\n async () => await runtimeApiRoute.main({\n data: requestData,\n user: ctx.user,\n functions: functionsObject,\n stream: emitApiStream,\n abortSignal: combinedController.signal,\n flushPressure,\n }),\n undefined,\n {\n handler: 'handleHttpApiRequest',\n api: resolvedName,\n userId: ctx.user?.id,\n transport: 'http',\n },\n );\n\n if (timeoutPromise) {\n await Promise.race([handlerPromise, timeoutPromise]);\n }\n clearTimeout(timeoutHandle);\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- timedOut is mutated by the async timeout callback via Promise.race\n if (timedOut) {\n return ctx.buildNetworkError({\n response: { status: 'error', errorCode: 'api.timeout' },\n fallbackHttpStatus: 504,\n });\n }\n\n const [error, result] = await handlerPromise;\n executePayload.result = result;\n executePayload.error = error ?? null;\n executePayload.durationMs = Date.now() - executeStart;\n await dispatchHook('postApiExecute', executePayload);\n\n //? Shared envelope assembly (CC-6) — identical to the socket handler's\n //? `buildApiResponseEnvelope`: localized error shapes for failure / empty /\n //? invalid-status, or a success envelope with inferred default HTTP status.\n //? `normalizeApiResponse` runtime-checks `result.status`, so a handler that\n //? returns a non-status object still maps to `api.invalidResponseStatus`.\n const envelope = normalizeApiResponse({\n resolvedName,\n error: error ?? null,\n result: result ?? undefined,\n preferredLocale: ctx.preferredLocale,\n user: ctx.user,\n });\n return envelope as ApiNetworkResponse;\n}\n\nasync function runHandleHttpApiRequestInner(\n {\n name,\n data,\n token,\n requesterIp,\n xLanguageHeader: _xLanguageHeader,\n acceptLanguageHeader: _acceptLanguageHeader,\n method = 'POST',\n stream,\n abortSignal,\n }: HttpApiRequestParams,\n user: SessionLayout | null,\n preferredLocale: string | null | undefined,\n formatterHolder: { current?: ErrorFormatter },\n): Promise<ApiNetworkResponse> {\n //? B1 — HTTP/SSE transport. The caller (typically `@luckystack/server`'s\n //? SSE bridge) wires `req.on('close', ...)` to a controller and passes its\n //? signal in. If no signal was provided we synthesize a never-aborting one\n //? so the handler param shape stays consistent.\n const effectiveAbortSignal = abortSignal ?? new AbortController().signal;\n\n const buildNetworkError = ({\n response,\n fallbackHttpStatus,\n }: {\n response: { status: 'error'; httpStatus?: number; errorCode?: string; errorParams?: { key: string; value: string | number | boolean; }[] };\n fallbackHttpStatus?: number;\n }): ApiNetworkResponse => {\n return normalizeErrorResponse({\n response,\n preferredLocale,\n userLanguage: user?.language,\n fallbackHttpStatus,\n }) as ApiNetworkResponse;\n };\n\n //? Identity propagation now flows via the `preApiExecute` hook subscriber\n //? registered by `@luckystack/error-tracking`'s `enableErrorTrackingAutoInstrumentation()`.\n //? Direct `setSentryUser` removed from this handler — see migration doc.\n const ctx: HttpApiPipelineContext = { buildNetworkError, user, preferredLocale };\n\n // Stage 1: validate request shape, parse route, resolve runtime maps\n const shapeResult = await validateHttpApiRequestShape(name, data, ctx);\n if ('status' in shapeResult) {\n return shapeResult;\n }\n const { resolvedName, requestData, apisObject, functionsObject } = shapeResult;\n\n const runtimeApiRoute = apisObject[resolvedName] as RuntimeApiEntry;\n formatterHolder.current = runtimeApiRoute.errorFormatter;\n\n // Stage 2: auth (login-required + additional predicates)\n //? Pipeline order: auth → rate-limit → method → validate → execute → respond.\n const authError = runHttpApiAuth({ name, resolvedName, requesterIp, auth: runtimeApiRoute.auth }, ctx);\n if (authError) {\n return authError;\n }\n\n // Stage 3: rate limiting (per-route + global-IP)\n const rateLimitError = await applyHttpApiRateLimits(\n { requesterIp, token, resolvedName, rateLimit: runtimeApiRoute.rateLimit },\n ctx,\n );\n if (rateLimitError) {\n return rateLimitError;\n }\n\n // Stage 4: HTTP method check (post-auth so unauthenticated probes don't learn the route exists)\n const methodError = checkHttpApiMethod(\n { method, resolvedName, declaredMethod: runtimeApiRoute.httpMethod },\n ctx,\n );\n if (methodError) {\n return methodError;\n }\n\n // Stage 5: input-type validation\n //? Input-type validation stage (preApiValidate -> validateInputByType ->\n //? postApiValidate). Extracted to `_shared/httpValidationStage.ts` for\n //? symmetry with the socket handler. On failure the caller builds the\n //? GENERIC `api.invalidInputType` (the raw validator message never reaches\n //? the client). Honors the per-route `validation: 'relaxed'` / `{ input:\n //? 'skip' }` escape hatch on BOTH transports — a public webhook route whose\n //? third-party payload can't be modeled in TS is reachable over HTTP, not\n //? just sockets.\n const httpValidation = await runHttpApiValidation({\n resolvedName,\n inputType: runtimeApiRoute.inputType,\n inputTypeFilePath: runtimeApiRoute.inputTypeFilePath,\n requestData,\n user,\n validation: runtimeApiRoute.validation,\n });\n if (!httpValidation.ok) {\n return buildNetworkError({\n response: {\n status: 'error',\n errorCode: 'api.invalidInputType',\n },\n fallbackHttpStatus: 400,\n });\n }\n\n // Stage 6: execute the API handler and assemble the response envelope\n return runHttpApiExecution(\n { resolvedName, requestData, functionsObject, runtimeApiRoute, effectiveAbortSignal, stream },\n ctx,\n );\n}\n","import type { BaseSessionLayout as SessionLayout } from '@luckystack/core';\nimport { dispatchHook, validateInputByType, getLogger } from '@luckystack/core';\nimport type { RuntimeApiEntry } from './apiTypes';\nimport { shouldLogDev } from './logFlags';\nimport { warnIfInputTypeMissing } from './inputTypeWarning';\nimport { resolveValidationMode } from './socketValidationStage';\n\n//? HTTP-transport input-validation stage. Mirrors the socket variant\n//? (`runSocketApiValidation`) for transport symmetry, extracted from\n//? `runHandleHttpApiRequestInner` per the `api` package audit. Behaviour:\n//? • resolves the per-route validation mode (`'relaxed'` / `{ input: 'skip' }`\n//? => skip Zod validation; default `'strict'`) — identical to the socket\n//? stage, so a public webhook route (Stripe/Slack/GitHub) whose third-party\n//? payload can't be modeled in TS is reachable over HTTP, not just sockets;\n//? • warns once if a strict-mode route has no generated input type;\n//? • dispatches `preApiValidate`, runs `validateInputByType` in strict mode,\n//? then `postApiValidate` (always, including the relaxed skip path);\n//? • on failure returns `{ ok: false }` (the caller builds the GENERIC\n//? `api.invalidInputType` network error — the raw validator message is\n//? NEVER echoed to the client; it only travels to the postApiValidate hook\n//? + dev logs).\n\nexport const runHttpApiValidation = async ({\n resolvedName,\n inputType,\n inputTypeFilePath,\n requestData,\n user,\n validation,\n}: {\n resolvedName: string;\n inputType?: string;\n inputTypeFilePath?: string;\n requestData: Record<string, unknown>;\n user: SessionLayout | null;\n validation: RuntimeApiEntry['validation'];\n}): Promise<{ ok: true } | { ok: false }> => {\n //? Per-route validation toggle, honored identically to the socket stage.\n const validationMode = resolveValidationMode(validation);\n\n // Input-type validation (post-auth so unauthenticated probes don't get input-shape leaks)\n if (validationMode === 'strict') {\n warnIfInputTypeMissing(resolvedName, inputType);\n }\n await dispatchHook('preApiValidate', { routeName: resolvedName, data: requestData, user, transport: 'http' });\n\n if (validationMode === 'relaxed') {\n //? Relaxed: surface the skip via postApiValidate so audit handlers see it.\n await dispatchHook('postApiValidate', {\n routeName: resolvedName,\n data: requestData,\n user,\n validation: { status: 'success' },\n transport: 'http',\n });\n return { ok: true };\n }\n\n const inputValidation = await validateInputByType({\n typeText: inputType,\n value: requestData,\n rootKey: 'data',\n filePath: inputTypeFilePath,\n });\n\n await dispatchHook('postApiValidate', {\n routeName: resolvedName,\n data: requestData,\n user,\n validation: inputValidation,\n transport: 'http',\n });\n\n if (inputValidation.status === 'error') {\n //? SECURITY: do NOT echo the raw validator message (e.g.\n //? \"data.userId should be string\") back to the client — that lets an\n //? unauthenticated caller enumerate a route's input schema. The DETAILED\n //? message is routed to the `postApiValidate` hook + dev logs above; the\n //? client only receives the generic `api.invalidInputType` code.\n if (shouldLogDev()) {\n getLogger().warn(`http-api: input validation failed for ${resolvedName}`, { route: resolvedName, message: inputValidation.message, transport: 'http' });\n }\n return { ok: false };\n }\n\n return { ok: true };\n};\n"],"mappings":"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","names":["getProjectConfig","checkRateLimit","dispatchHook","getLogger","normalizeErrorResponse","getLogger","getLogger","getLogger","getLogger","getLogger","getProjectConfig","getLogger","getLogger","getProjectConfig","dispatchHook","getLogger","getProjectConfig","dispatchHook","getLogger","getLogger","dispatchHook","getProjectConfig","normalizeErrorResponse","checkRateLimit","getProjectConfig","readSession","runWithErrorTrackerIdentityScope","setCurrentErrorTrackerIdentity","validateRequest","tryCatch","parseTransportRouteName","dispatchHook","getLogger","extractLanguageFromHeader","normalizeErrorResponse","applyErrorFormatter","resolveClientIp","dispatchHook","validateInputByType","getLogger","dispatchHook","validateInputByType","getLogger","runWithErrorTrackerIdentityScope","dispatchHook","normalizeErrorResponse","applyErrorFormatter","extractLanguageFromHeader","readSession","setCurrentErrorTrackerIdentity","parseTransportRouteName","getLogger","validateRequest","resolveClientIp","getProjectConfig","tryCatch"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luckystack/api",
3
- "version": "0.2.6",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -56,8 +56,8 @@
56
56
  "test": "vitest run"
57
57
  },
58
58
  "dependencies": {
59
- "@luckystack/core": "^0.2.6",
60
- "@luckystack/error-tracking": "^0.2.6"
59
+ "@luckystack/core": "^0.3.0",
60
+ "@luckystack/error-tracking": "^0.3.0"
61
61
  },
62
62
  "peerDependencies": {
63
63
  "@prisma/client": "^6.19.0",