@luckystack/api 0.14.0 → 0.15.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/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,16 @@ All notable changes to `@luckystack/api` are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.15.0] - 2026-10-19
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Breaking:** a `preApiValidate` hook that returns a stop signal now blocks the request on both the socket and HTTP transports. The client gets the signal's `errorCode` with its `httpStatus` (default 403), and the route handler does not run. Before, the stop was silently ignored. Upgrade: review every `preApiValidate` handler that returns `{ stop: true, ... }`; those requests are now rejected, as the hook contract always promised.
|
|
13
|
+
- **HTTP API respond hooks** (`preApiRespond`, `transformApiResponse`, `postApiRespond`) **and the error formatter get the canonical route name.** Before, they got the request's raw spelling, so a trailing slash, backslash or surrounding whitespace could get past redaction or audit filters keyed by route.
|
|
14
|
+
- **Socket API `apiAuthRejected` gets the canonical route name** instead of the raw message name, so spelling variants of one route share one throttle key, as they already did over HTTP.
|
|
15
|
+
- **`api.requestTimeoutMs: 0`, or any negative value, disables the server-side API timeout** on both transports, as the client already read it. Before, every request that awaited I/O returned `api.timeout` (504).
|
|
16
|
+
- **`preApiRespond`, `transformApiResponse` and `postApiRespond` payloads carry the documented `transport` tag** (`'socket'` or `'http'`) on both transports, for success and error responses.
|
|
17
|
+
|
|
8
18
|
## [0.12.0] - 2026-10-15
|
|
9
19
|
|
|
10
20
|
### Changed
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import type { BaseSessionLayout as SessionLayout } from '@luckystack/core';
|
|
2
2
|
import type { RuntimeApiEntry } from './apiTypes.js';
|
|
3
3
|
type EmitInvalidInputType = () => void | Promise<void>;
|
|
4
|
+
type EmitValidationStopped = (signal: {
|
|
5
|
+
errorCode: string;
|
|
6
|
+
httpStatus?: number;
|
|
7
|
+
}) => void | Promise<void>;
|
|
4
8
|
export declare const resolveValidationMode: (validation: RuntimeApiEntry['validation']) => 'strict' | 'relaxed';
|
|
5
|
-
export declare const runSocketApiValidation: ({ apiEntry, resolvedName, normalizedData, user, cleanupRequest, emitInvalidInputType, }: {
|
|
9
|
+
export declare const runSocketApiValidation: ({ apiEntry, resolvedName, normalizedData, user, cleanupRequest, emitInvalidInputType, emitValidationStopped, }: {
|
|
6
10
|
apiEntry: RuntimeApiEntry;
|
|
7
11
|
resolvedName: string;
|
|
8
12
|
normalizedData: Record<string, unknown>;
|
|
9
13
|
user: SessionLayout | null;
|
|
10
14
|
cleanupRequest: () => void;
|
|
11
15
|
emitInvalidInputType: EmitInvalidInputType;
|
|
16
|
+
emitValidationStopped: EmitValidationStopped;
|
|
12
17
|
}) => Promise<boolean>;
|
|
13
18
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -185,13 +185,19 @@ var runSocketApiValidation = async ({
|
|
|
185
185
|
normalizedData,
|
|
186
186
|
user,
|
|
187
187
|
cleanupRequest,
|
|
188
|
-
emitInvalidInputType
|
|
188
|
+
emitInvalidInputType,
|
|
189
|
+
emitValidationStopped
|
|
189
190
|
}) => {
|
|
190
191
|
const validationMode = resolveValidationMode(apiEntry.validation);
|
|
191
192
|
if (validationMode === "strict") {
|
|
192
193
|
warnIfInputTypeMissing(resolvedName, apiEntry.inputType);
|
|
193
194
|
}
|
|
194
|
-
await dispatchHook("preApiValidate", { routeName: resolvedName, data: normalizedData, user, transport: "socket" });
|
|
195
|
+
const preValidateResult = await dispatchHook("preApiValidate", { routeName: resolvedName, data: normalizedData, user, transport: "socket" });
|
|
196
|
+
if (preValidateResult.stopped) {
|
|
197
|
+
cleanupRequest();
|
|
198
|
+
await emitValidationStopped({ errorCode: preValidateResult.signal.errorCode, httpStatus: preValidateResult.signal.httpStatus });
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
195
201
|
if (validationMode === "strict") {
|
|
196
202
|
const inputValidation = await validateInputByType({
|
|
197
203
|
typeText: apiEntry.inputType,
|
|
@@ -310,13 +316,13 @@ var validateApiMessage = async ({ msg, emitApiError }) => {
|
|
|
310
316
|
}
|
|
311
317
|
return { name, data };
|
|
312
318
|
};
|
|
313
|
-
var checkApiAuth = async ({ apiEntry, user,
|
|
319
|
+
var checkApiAuth = async ({ apiEntry, user, resolvedName, emitApiError }) => {
|
|
314
320
|
if (apiEntry.auth.login && !user?.id) {
|
|
315
321
|
if (shouldLogDev()) {
|
|
316
|
-
getLogger7().warn(`api: ${
|
|
322
|
+
getLogger7().warn(`api: ${resolvedName} requires login`, { route: resolvedName });
|
|
317
323
|
}
|
|
318
324
|
void dispatchHook3("apiAuthRejected", {
|
|
319
|
-
routeName:
|
|
325
|
+
routeName: resolvedName,
|
|
320
326
|
reason: "login-required",
|
|
321
327
|
userId: null,
|
|
322
328
|
transport: "socket"
|
|
@@ -330,10 +336,10 @@ var checkApiAuth = async ({ apiEntry, user, name, emitApiError }) => {
|
|
|
330
336
|
const authResult = validateRequest({ auth: apiEntry.auth, user });
|
|
331
337
|
if (authResult.status === "error") {
|
|
332
338
|
if (shouldLogDev()) {
|
|
333
|
-
getLogger7().warn(`api: auth failed for ${
|
|
339
|
+
getLogger7().warn(`api: auth failed for ${resolvedName}`, { route: resolvedName, errorCode: authResult.errorCode });
|
|
334
340
|
}
|
|
335
341
|
void dispatchHook3("apiAuthRejected", {
|
|
336
|
-
routeName:
|
|
342
|
+
routeName: resolvedName,
|
|
337
343
|
reason: authResult.errorCode === "auth.invalidCondition" ? "invalid-condition" : "additional-failed",
|
|
338
344
|
userId: user?.id ?? null,
|
|
339
345
|
transport: "socket",
|
|
@@ -386,13 +392,13 @@ var executeApiHandler = async ({ apiEntry, normalizedData, user, functionsObject
|
|
|
386
392
|
const timeoutMs = getProjectConfig4().api.requestTimeoutMs;
|
|
387
393
|
let timeoutHandle;
|
|
388
394
|
let timedOut = false;
|
|
389
|
-
const timeoutPromise = timeoutMs ===
|
|
395
|
+
const timeoutPromise = typeof timeoutMs === "number" && timeoutMs > 0 ? new Promise((resolve) => {
|
|
390
396
|
timeoutHandle = setTimeout(() => {
|
|
391
397
|
timedOut = true;
|
|
392
398
|
abortController.abort();
|
|
393
399
|
resolve();
|
|
394
400
|
}, timeoutMs);
|
|
395
|
-
});
|
|
401
|
+
}) : null;
|
|
396
402
|
const handlerPromise = tryCatch(
|
|
397
403
|
async () => await apiEntry.main({ data: normalizedData, user, functions: functionsObject, stream: emitStream, abortSignal, flushPressure }),
|
|
398
404
|
void 0,
|
|
@@ -468,7 +474,7 @@ var emitApiResult = async ({
|
|
|
468
474
|
perRouteFormatter
|
|
469
475
|
}) => {
|
|
470
476
|
const envelope = normalizeApiResponse({ resolvedName, error, result, preferredLocale, user });
|
|
471
|
-
const preRespond = { routeName: resolvedName, user, response: envelope };
|
|
477
|
+
const preRespond = { routeName: resolvedName, user, transport: "socket", response: envelope };
|
|
472
478
|
const preRespondResult = await dispatchHook3("preApiRespond", preRespond);
|
|
473
479
|
const finalResponse = preRespondResult.stopped ? { ...normalizeErrorResponse2({
|
|
474
480
|
response: { status: "error", errorCode: preRespondResult.signal.errorCode },
|
|
@@ -476,7 +482,7 @@ var emitApiResult = async ({
|
|
|
476
482
|
userLanguage: user?.language,
|
|
477
483
|
fallbackHttpStatus: preRespondResult.signal.httpStatus ?? 403
|
|
478
484
|
}) } : preRespond.response;
|
|
479
|
-
const transformPayload = { routeName: resolvedName, user, response: finalResponse };
|
|
485
|
+
const transformPayload = { routeName: resolvedName, user, transport: "socket", response: finalResponse };
|
|
480
486
|
await dispatchHook3("transformApiResponse", transformPayload);
|
|
481
487
|
const formattedResponse = applyErrorFormatter({
|
|
482
488
|
response: transformPayload.response,
|
|
@@ -486,7 +492,7 @@ var emitApiResult = async ({
|
|
|
486
492
|
perRouteFormatter
|
|
487
493
|
});
|
|
488
494
|
socket.emit(buildApiResponseEventName(responseIndex), formattedResponse);
|
|
489
|
-
await dispatchHook3("postApiRespond", { routeName: resolvedName, user, response: formattedResponse });
|
|
495
|
+
await dispatchHook3("postApiRespond", { routeName: resolvedName, user, transport: "socket", response: formattedResponse });
|
|
490
496
|
};
|
|
491
497
|
async function handleApiRequest(args) {
|
|
492
498
|
const [error] = await tryCatch(() => runWithErrorTrackerIdentityScope(() => handleApiRequestInner(args)));
|
|
@@ -542,7 +548,7 @@ async function handleApiRequestInner({ msg, socket, token }) {
|
|
|
542
548
|
fallbackHttpStatus
|
|
543
549
|
});
|
|
544
550
|
const routeNameForHook = currentRouteName ?? "api/unknown";
|
|
545
|
-
const preRespond = { routeName: routeNameForHook, user, response: normalized };
|
|
551
|
+
const preRespond = { routeName: routeNameForHook, user, transport: "socket", response: normalized };
|
|
546
552
|
const preRespondResult = await dispatchHook3("preApiRespond", preRespond);
|
|
547
553
|
let finalResponse = preRespond.response;
|
|
548
554
|
if (preRespondResult.stopped) {
|
|
@@ -554,7 +560,7 @@ async function handleApiRequestInner({ msg, socket, token }) {
|
|
|
554
560
|
});
|
|
555
561
|
finalResponse = stoppedEnvelope;
|
|
556
562
|
}
|
|
557
|
-
const transformPayload = { routeName: routeNameForHook, user, response: finalResponse };
|
|
563
|
+
const transformPayload = { routeName: routeNameForHook, user, transport: "socket", response: finalResponse };
|
|
558
564
|
await dispatchHook3("transformApiResponse", transformPayload);
|
|
559
565
|
const formatted = applyErrorFormatter({
|
|
560
566
|
response: transformPayload.response,
|
|
@@ -564,7 +570,7 @@ async function handleApiRequestInner({ msg, socket, token }) {
|
|
|
564
570
|
perRouteFormatter: currentPerRouteFormatter
|
|
565
571
|
});
|
|
566
572
|
socket.emit(buildApiResponseEventName(responseIndex), formatted);
|
|
567
|
-
await dispatchHook3("postApiRespond", { routeName: routeNameForHook, user, response: formatted });
|
|
573
|
+
await dispatchHook3("postApiRespond", { routeName: routeNameForHook, user, transport: "socket", response: formatted });
|
|
568
574
|
};
|
|
569
575
|
const validated = await validateApiMessage({ msg, emitApiError });
|
|
570
576
|
if (!validated) return;
|
|
@@ -651,7 +657,7 @@ async function handleApiRequestInner({ msg, socket, token }) {
|
|
|
651
657
|
responseIndex,
|
|
652
658
|
resolvedName
|
|
653
659
|
});
|
|
654
|
-
if (!await checkApiAuth({ apiEntry, user,
|
|
660
|
+
if (!await checkApiAuth({ apiEntry, user, resolvedName, emitApiError })) {
|
|
655
661
|
cleanupRequest();
|
|
656
662
|
return;
|
|
657
663
|
}
|
|
@@ -671,6 +677,13 @@ async function handleApiRequestInner({ msg, socket, token }) {
|
|
|
671
677
|
response: { status: "error", errorCode: "api.invalidInputType" },
|
|
672
678
|
fallbackHttpStatus: 400
|
|
673
679
|
});
|
|
680
|
+
},
|
|
681
|
+
//? preApiValidate stop signal — same envelope + 403 default as preApiExecute.
|
|
682
|
+
emitValidationStopped: async ({ errorCode, httpStatus }) => {
|
|
683
|
+
await emitApiError({
|
|
684
|
+
response: { status: "error", errorCode },
|
|
685
|
+
fallbackHttpStatus: httpStatus ?? 403
|
|
686
|
+
});
|
|
674
687
|
}
|
|
675
688
|
});
|
|
676
689
|
if (!validationOk) return;
|
|
@@ -732,7 +745,10 @@ var runHttpApiValidation = async ({
|
|
|
732
745
|
if (validationMode === "strict") {
|
|
733
746
|
warnIfInputTypeMissing(resolvedName, inputType);
|
|
734
747
|
}
|
|
735
|
-
await dispatchHook4("preApiValidate", { routeName: resolvedName, data: requestData, user, transport: "http" });
|
|
748
|
+
const preValidateResult = await dispatchHook4("preApiValidate", { routeName: resolvedName, data: requestData, user, transport: "http" });
|
|
749
|
+
if (preValidateResult.stopped) {
|
|
750
|
+
return { ok: false, stop: { errorCode: preValidateResult.signal.errorCode, httpStatus: preValidateResult.signal.httpStatus } };
|
|
751
|
+
}
|
|
736
752
|
if (validationMode === "relaxed") {
|
|
737
753
|
await dispatchHook4("postApiValidate", {
|
|
738
754
|
routeName: resolvedName,
|
|
@@ -770,9 +786,9 @@ async function handleHttpApiRequest(params) {
|
|
|
770
786
|
return runWithErrorTrackerIdentityScope2(() => handleHttpApiRequestScoped(params));
|
|
771
787
|
}
|
|
772
788
|
async function handleHttpApiRequestScoped(params) {
|
|
773
|
-
const { response, user, preferredLocale, perRouteFormatter } = await runHandleHttpApiRequest(params);
|
|
774
|
-
const routeNameForHook = params.name.startsWith("api/") ? params.name : `api/${params.name}
|
|
775
|
-
const preRespond = { routeName: routeNameForHook, user, response };
|
|
789
|
+
const { response, user, preferredLocale, perRouteFormatter, resolvedName } = await runHandleHttpApiRequest(params);
|
|
790
|
+
const routeNameForHook = resolvedName ?? (params.name.startsWith("api/") ? params.name : `api/${params.name}`);
|
|
791
|
+
const preRespond = { routeName: routeNameForHook, user, transport: "http", response };
|
|
776
792
|
const preRespondResult = await dispatchHook5("preApiRespond", preRespond);
|
|
777
793
|
const finalResponse = preRespondResult.stopped ? normalizeErrorResponse3({
|
|
778
794
|
response: { status: "error", errorCode: preRespondResult.signal.errorCode },
|
|
@@ -780,7 +796,7 @@ async function handleHttpApiRequestScoped(params) {
|
|
|
780
796
|
userLanguage: user?.language,
|
|
781
797
|
fallbackHttpStatus: preRespondResult.signal.httpStatus ?? 403
|
|
782
798
|
}) : preRespond.response;
|
|
783
|
-
const transformPayload = { routeName: routeNameForHook, user, response: finalResponse };
|
|
799
|
+
const transformPayload = { routeName: routeNameForHook, user, transport: "http", response: finalResponse };
|
|
784
800
|
await dispatchHook5("transformApiResponse", transformPayload);
|
|
785
801
|
const formatterInput = transformPayload.response;
|
|
786
802
|
const formattedResponse = applyErrorFormatter2({
|
|
@@ -790,18 +806,18 @@ async function handleHttpApiRequestScoped(params) {
|
|
|
790
806
|
userId: user?.id,
|
|
791
807
|
perRouteFormatter
|
|
792
808
|
});
|
|
793
|
-
await dispatchHook5("postApiRespond", { routeName: routeNameForHook, user, response: formattedResponse });
|
|
809
|
+
await dispatchHook5("postApiRespond", { routeName: routeNameForHook, user, transport: "http", response: formattedResponse });
|
|
794
810
|
return formattedResponse;
|
|
795
811
|
}
|
|
796
812
|
async function runHandleHttpApiRequest(params) {
|
|
797
813
|
const preferredLocale = extractLanguageFromHeader2(params.xLanguageHeader) ?? extractLanguageFromHeader2(params.acceptLanguageHeader);
|
|
798
814
|
const user = await readSession2(params.token);
|
|
799
815
|
setCurrentErrorTrackerIdentity2(user?.id ? { id: user.id, email: user.email ?? void 0, username: user.name ?? void 0 } : null);
|
|
800
|
-
const
|
|
801
|
-
const response = await runHandleHttpApiRequestInner(params, user, preferredLocale,
|
|
802
|
-
return { response, user, preferredLocale, perRouteFormatter:
|
|
816
|
+
const routeHolder = {};
|
|
817
|
+
const response = await runHandleHttpApiRequestInner(params, user, preferredLocale, routeHolder);
|
|
818
|
+
return { response, user, preferredLocale, perRouteFormatter: routeHolder.perRouteFormatter, resolvedName: routeHolder.resolvedName };
|
|
803
819
|
}
|
|
804
|
-
async function validateHttpApiRequestShape(name, data, ctx) {
|
|
820
|
+
async function validateHttpApiRequestShape(name, data, ctx, routeHolder) {
|
|
805
821
|
if (!name || typeof name !== "string") {
|
|
806
822
|
return ctx.buildNetworkError({
|
|
807
823
|
response: { status: "error", errorCode: "api.invalidName" },
|
|
@@ -827,6 +843,7 @@ async function validateHttpApiRequestShape(name, data, ctx) {
|
|
|
827
843
|
});
|
|
828
844
|
}
|
|
829
845
|
const resolvedName = parsedRoute.normalizedFullName;
|
|
846
|
+
routeHolder.resolvedName = resolvedName;
|
|
830
847
|
if (shouldLogDev()) {
|
|
831
848
|
getLogger9().debug(`http api: ${resolvedName} called`);
|
|
832
849
|
}
|
|
@@ -991,13 +1008,13 @@ async function runHttpApiExecution({
|
|
|
991
1008
|
}
|
|
992
1009
|
let timedOut = false;
|
|
993
1010
|
let timeoutHandle;
|
|
994
|
-
const timeoutPromise = timeoutMs ===
|
|
1011
|
+
const timeoutPromise = typeof timeoutMs === "number" && timeoutMs > 0 ? new Promise((resolve) => {
|
|
995
1012
|
timeoutHandle = setTimeout(() => {
|
|
996
1013
|
timedOut = true;
|
|
997
1014
|
combinedController.abort();
|
|
998
1015
|
resolve();
|
|
999
1016
|
}, timeoutMs);
|
|
1000
|
-
});
|
|
1017
|
+
}) : null;
|
|
1001
1018
|
const executeStart = Date.now();
|
|
1002
1019
|
const handlerPromise = tryCatch2(
|
|
1003
1020
|
async () => await runtimeApiRoute.main({
|
|
@@ -1050,7 +1067,7 @@ async function runHandleHttpApiRequestInner({
|
|
|
1050
1067
|
method = "POST",
|
|
1051
1068
|
stream,
|
|
1052
1069
|
abortSignal
|
|
1053
|
-
}, user, preferredLocale,
|
|
1070
|
+
}, user, preferredLocale, routeHolder) {
|
|
1054
1071
|
const effectiveAbortSignal = abortSignal ?? new AbortController().signal;
|
|
1055
1072
|
const buildNetworkError = ({
|
|
1056
1073
|
response,
|
|
@@ -1064,13 +1081,13 @@ async function runHandleHttpApiRequestInner({
|
|
|
1064
1081
|
});
|
|
1065
1082
|
};
|
|
1066
1083
|
const ctx = { buildNetworkError, user, preferredLocale };
|
|
1067
|
-
const shapeResult = await validateHttpApiRequestShape(name, data, ctx);
|
|
1084
|
+
const shapeResult = await validateHttpApiRequestShape(name, data, ctx, routeHolder);
|
|
1068
1085
|
if ("status" in shapeResult) {
|
|
1069
1086
|
return shapeResult;
|
|
1070
1087
|
}
|
|
1071
1088
|
const { resolvedName, requestData, apisObject, functionsObject } = shapeResult;
|
|
1072
1089
|
const runtimeApiRoute = apisObject[resolvedName];
|
|
1073
|
-
|
|
1090
|
+
routeHolder.perRouteFormatter = runtimeApiRoute.errorFormatter;
|
|
1074
1091
|
const authError = runHttpApiAuth({ name, resolvedName, requesterIp, auth: runtimeApiRoute.auth }, ctx);
|
|
1075
1092
|
if (authError) {
|
|
1076
1093
|
return authError;
|
|
@@ -1098,6 +1115,12 @@ async function runHandleHttpApiRequestInner({
|
|
|
1098
1115
|
validation: runtimeApiRoute.validation
|
|
1099
1116
|
});
|
|
1100
1117
|
if (!httpValidation.ok) {
|
|
1118
|
+
if (httpValidation.stop) {
|
|
1119
|
+
return buildNetworkError({
|
|
1120
|
+
response: { status: "error", errorCode: httpValidation.stop.errorCode },
|
|
1121
|
+
fallbackHttpStatus: httpValidation.stop.httpStatus ?? 403
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1101
1124
|
return buildNetworkError({
|
|
1102
1125
|
response: {
|
|
1103
1126
|
status: "error",
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/handleApiRequest.ts","../src/_shared/logFlags.ts","../src/_shared/responseEnvelope.ts","../src/_shared/requestLifecycle.ts","../src/_shared/backpressure.ts","../src/_shared/socketValidationStage.ts","../src/_shared/inputTypeWarning.ts","../src/_shared/applyApiRateLimits.ts","../src/handleHttpApiRequest.ts","../src/_shared/httpValidationStage.ts"],"sourcesContent":["import type { apiMessage, PostApiExecutePayload, BaseSessionLayout as SessionLayout, ErrorFormatter } from '@luckystack/core';\nimport {\n readSession,\n performLogout,\n runWithErrorTrackerIdentityScope,\n setCurrentErrorTrackerIdentity,\n getProjectConfig,\n getRuntimeApiMaps,\n validateRequest,\n checkRateLimit,\n tryCatch,\n parseTransportRouteName,\n buildApiResponseEventName,\n dispatchHook,\n getLogger,\n resolveClientIp,\n extractLanguageFromHeader,\n normalizeErrorResponse,\n applyErrorFormatter,\n} from '@luckystack/core';\n//? checkRateLimit and resolveClientIp are still used directly for the logout\n//? global-IP bucket (which doesn't go through the shared applyApiRateLimits helper\n//? because the logout shortcut bypasses the per-route bucket entirely).\nimport { Socket } from 'socket.io';\nimport type { ApiStreamPayload, ApiFlushPressure, RuntimeApiResponse, RuntimeApiEntry } from './_shared/apiTypes';\nimport { shouldLogDev } from './_shared/logFlags';\nimport { normalizeApiResponse } from './_shared/responseEnvelope';\nimport { createApiRequestLifecycle } from './_shared/requestLifecycle';\nimport { runSocketApiValidation } from './_shared/socketValidationStage';\nimport { applyApiRateLimits } from './_shared/applyApiRateLimits';\n\ninterface handleApiRequestType {\n msg: apiMessage,\n socket: Socket,\n token: string | null,\n}\n\ninterface ApiErrorResponse {\n status: 'error';\n httpStatus?: number;\n errorCode?: string;\n errorParams?: { key: string; value: string | number | boolean; }[];\n}\n\ntype EmitApiError = (args: { response: ApiErrorResponse; fallbackHttpStatus?: number; }) => Promise<void>;\n\nconst validateApiMessage = async ({ msg, emitApiError }: {\n msg: apiMessage;\n emitApiError: EmitApiError;\n}): Promise<{ name: string; data: Record<string, unknown> } | null> => {\n const { name, data, responseIndex } = msg;\n\n //? Drop a message with no usable response channel. Written as a single\n //? `typeof` test (equivalent to the former `!responseIndex && typeof !==\n //? 'number'` double-negative) so a `responseIndex: 0` is correctly KEPT — a\n //? future \"simplify\" to `!responseIndex` would silently drop every index-0\n //? request.\n if (typeof responseIndex !== 'number') {\n if (shouldLogDev()) {\n getLogger().warn('api: no response index given');\n }\n return null;\n }\n\n //? Positive plain-object guard (parity with the sync handler). `!data`\n //? excludes `null`; `Array.isArray` rejects an ARRAY payload that\n //? `typeof === 'object'` would otherwise admit to a handler expecting an\n //? object.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime defense: msg arrives from an untyped socket frame\n if (!name || !data || typeof name != 'string' || typeof data != 'object' || Array.isArray(data)) {\n await emitApiError({\n response: { status: 'error', errorCode: 'api.invalidRequest' },\n fallbackHttpStatus: 400,\n });\n return null;\n }\n\n return { name, data: data as Record<string, unknown> };\n};\n\nconst checkApiAuth = async ({ apiEntry, user, name, emitApiError }: {\n apiEntry: RuntimeApiEntry;\n user: SessionLayout | null;\n name: string;\n emitApiError: EmitApiError;\n}): Promise<boolean> => {\n if (apiEntry.auth.login && !user?.id) {\n if (shouldLogDev()) {\n getLogger().warn(`api: ${name} requires login`, { route: name });\n }\n void dispatchHook('apiAuthRejected', {\n routeName: name,\n reason: 'login-required',\n userId: null,\n transport: 'socket',\n });\n await emitApiError({\n response: { status: 'error', errorCode: 'auth.required' },\n fallbackHttpStatus: 401,\n });\n return false;\n }\n\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n const authResult = validateRequest({ auth: apiEntry.auth, user: user! });\n if (authResult.status === 'error') {\n if (shouldLogDev()) {\n getLogger().warn(`api: auth failed for ${name}`, { route: name, errorCode: authResult.errorCode });\n }\n void dispatchHook('apiAuthRejected', {\n routeName: name,\n reason: authResult.errorCode === 'auth.invalidCondition' ? 'invalid-condition' : 'additional-failed',\n userId: user?.id ?? null,\n transport: 'socket',\n failedKey: authResult.errorCode,\n });\n await emitApiError({\n response: {\n status: 'error',\n // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- empty string errorCode must still fall back\n errorCode: authResult.errorCode || 'auth.forbidden',\n errorParams: authResult.errorParams,\n httpStatus: authResult.httpStatus,\n },\n fallbackHttpStatus: authResult.httpStatus ?? 403,\n });\n return false;\n }\n\n return true;\n};\n\n//? API-O8 — rate-limit logic extracted to `_shared/applyApiRateLimits.ts` so\n//? both transports share one implementation. The socket transport resolves the\n//? effective IP here (once, from socket internals) before delegating to the helper.\nconst runSocketRateLimits = async ({ apiEntry, resolvedName, token, socket, user, emitApiError }: {\n apiEntry: RuntimeApiEntry;\n resolvedName: string;\n token: string | null;\n socket: Socket;\n user: SessionLayout | null;\n emitApiError: EmitApiError;\n}): Promise<boolean> => {\n //? Resolve the real client IP once. With the default `http.trustProxy: false`\n //? this returns `socket.handshake.address` verbatim (only IPv4-mapped IPv6 is\n //? canonicalized). When a trusted proxy is configured it honors XFF / X-Real-IP.\n const resolvedIp = resolveClientIp({\n rawAddress: socket.handshake.address,\n headers: socket.handshake.headers,\n trustProxy: getProjectConfig().http.trustProxy,\n trustedProxyHopCount: getProjectConfig().http.trustedProxyHopCount,\n });\n\n const result = await applyApiRateLimits({\n resolvedIp,\n token,\n user,\n resolvedName,\n rateLimit: apiEntry.rateLimit,\n transport: 'socket',\n });\n\n if (!result.allowed) {\n await emitApiError({\n response: {\n status: 'error',\n errorCode: result.errorCode ?? 'api.rateLimitExceeded',\n errorParams: [{ key: 'seconds', value: result.resetIn ?? 0 }],\n },\n fallbackHttpStatus: 429,\n });\n return false;\n }\n\n return true;\n};\n\nconst executeApiHandler = async ({ apiEntry, normalizedData, user, functionsObject, resolvedName, emitStream, abortSignal, abortController, flushPressure }: {\n apiEntry: RuntimeApiEntry;\n normalizedData: Record<string, unknown>;\n user: SessionLayout | null;\n functionsObject: Record<string, unknown>;\n resolvedName: string;\n emitStream: (payload?: ApiStreamPayload) => void;\n abortSignal: AbortSignal;\n abortController: AbortController;\n flushPressure: ApiFlushPressure;\n}): Promise<{ error: Error | null; result: RuntimeApiResponse | undefined; durationMs: number; timedOut?: boolean }> => {\n //? Span open/close + identity propagation moved to hook subscribers in\n //? `@luckystack/error-tracking` (preApiExecute / postApiExecute). This\n //? handler is now transport-agnostic instrumentation-wise.\n const executeStart = Date.now();\n\n //? API-O10 — race `main()` against a server-side timeout derived from\n //? `api.requestTimeoutMs`. When the timeout fires first we abort the\n //? AbortController (propagates to the handler's `abortSignal`) and resolve\n //? a sentinel so the orchestrator can emit a localized 504 envelope.\n const timeoutMs = getProjectConfig().api.requestTimeoutMs;\n let timeoutHandle: ReturnType<typeof setTimeout> | undefined;\n let timedOut = false;\n const timeoutPromise = timeoutMs === false\n ? null\n : new Promise<void>((resolve) => {\n timeoutHandle = setTimeout(() => {\n timedOut = true;\n abortController.abort();\n resolve();\n }, timeoutMs);\n });\n\n const handlerPromise = tryCatch(\n async () => await apiEntry.main({ data: normalizedData, user, functions: functionsObject, stream: emitStream, abortSignal, flushPressure }),\n undefined,\n {\n handler: 'handleApiRequest',\n api: resolvedName,\n userId: user?.id,\n transport: 'socket',\n },\n );\n\n if (timeoutPromise) {\n await Promise.race([handlerPromise, timeoutPromise]);\n }\n clearTimeout(timeoutHandle);\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- timedOut is mutated by the async timeout callback via Promise.race\n if (timedOut) {\n return { error: null, result: undefined, durationMs: Date.now() - executeStart, timedOut: true };\n }\n\n const [error, result] = await handlerPromise;\n return { error: error ?? null, result: result ?? undefined, durationMs: Date.now() - executeStart };\n};\n\n//? Execution stage: `preApiExecute` hook (stop-signal short-circuit) ->\n//? `executeApiHandler` (tryCatch-wrapped) -> `postApiExecute` hook. Extracted\n//? from the orchestrator per the `api` package audit. Returns either a stop\n//? signal (the orchestrator builds the localized error envelope from it) or the\n//? `(error, result)` pair to feed `emitApiResult`. The shared `executePayload`\n//? reference is created here and threaded through both hooks so the\n//? error-tracking auto-instrumentation can pin spans via WeakMap on it.\ntype SocketApiExecutionOutcome =\n | { stopped: true; errorCode: string; httpStatus?: number }\n | { stopped: false; error: Error | null; result: RuntimeApiResponse | undefined };\n\nconst runSocketApiExecution = async ({\n apiEntry,\n resolvedName,\n normalizedData,\n user,\n functionsObject,\n emitStream,\n abortSignal,\n abortController,\n flushPressure,\n}: {\n apiEntry: RuntimeApiEntry;\n resolvedName: string;\n normalizedData: Record<string, unknown>;\n user: SessionLayout | null;\n functionsObject: Record<string, unknown>;\n emitStream: (payload?: ApiStreamPayload) => void;\n abortSignal: AbortSignal;\n abortController: AbortController;\n flushPressure: ApiFlushPressure;\n}): Promise<SocketApiExecutionOutcome> => {\n //? Single payload reference reused by pre/post — auto-instrumentation\n //? subscribers in `@luckystack/error-tracking` pin spans via WeakMap on\n //? this object. Mutating `result/error/durationMs` is safe because\n //? handlers observe by-name (not by snapshot).\n const executePayload: PostApiExecutePayload = {\n routeName: resolvedName,\n data: normalizedData,\n user,\n transport: 'socket',\n result: undefined,\n error: null,\n durationMs: 0,\n };\n const preExecuteResult = await dispatchHook('preApiExecute', executePayload);\n if (preExecuteResult.stopped) {\n return { stopped: true, errorCode: preExecuteResult.signal.errorCode, httpStatus: preExecuteResult.signal.httpStatus };\n }\n\n const { error, result, durationMs, timedOut } = await executeApiHandler({\n apiEntry,\n normalizedData,\n user,\n functionsObject,\n resolvedName,\n emitStream,\n abortSignal,\n abortController,\n flushPressure,\n });\n\n //? API-O10 — a server-side timeout aborted the handler. Skip postApiExecute\n //? (no meaningful result to report) and surface a 504 to the client.\n if (timedOut) {\n return { stopped: true, errorCode: 'api.timeout', httpStatus: 504 };\n }\n\n executePayload.result = result;\n executePayload.error = error ?? null;\n executePayload.durationMs = durationMs;\n await dispatchHook('postApiExecute', executePayload);\n\n return { stopped: false, error, result };\n};\n\nconst emitApiResult = async ({\n socket,\n responseIndex,\n resolvedName,\n error,\n result,\n preferredLocale,\n user,\n perRouteFormatter,\n}: {\n socket: Socket;\n responseIndex: number;\n resolvedName: string;\n error: Error | null;\n result: RuntimeApiResponse | undefined;\n preferredLocale: string | undefined;\n user: SessionLayout | null;\n perRouteFormatter?: ErrorFormatter;\n}): Promise<void> => {\n const envelope = normalizeApiResponse({ resolvedName, error, result, preferredLocale, user });\n\n //? `preApiRespond` handlers may mutate `payload.response` to swap or rewrite\n //? the outgoing envelope (PII redaction, response signing, schema injection).\n //? A handler may also return a stop signal — when that happens we build a\n //? fresh error envelope from the signal's `errorCode` / `httpStatus` and\n //? emit that instead of the original.\n const preRespond = { routeName: resolvedName, user, response: envelope };\n const preRespondResult = await dispatchHook('preApiRespond', preRespond);\n\n const finalResponse = preRespondResult.stopped\n ? { ...normalizeErrorResponse({\n response: { status: 'error', errorCode: preRespondResult.signal.errorCode },\n preferredLocale,\n userLanguage: user?.language,\n fallbackHttpStatus: preRespondResult.signal.httpStatus ?? 403,\n }) }\n : preRespond.response;\n\n //? `transformApiResponse` fires AFTER preApiRespond and BEFORE socket emit.\n //? Designed for response mutation — header injection, body transformation,\n //? response signing — that's awkward inside preApiRespond. Handlers mutate\n //? `payload.response` in place (same shape as preApiRespond.response).\n const transformPayload = { routeName: resolvedName, user, response: finalResponse };\n await dispatchHook('transformApiResponse', transformPayload);\n\n //? Per-route → global → identity error formatter chain. No-op when status\n //? is success. Runs AFTER transformApiResponse so consumer hooks can still\n //? mutate the envelope before the final shape lands.\n const formattedResponse = applyErrorFormatter({\n response: transformPayload.response,\n routeName: resolvedName,\n transport: 'socket',\n userId: user?.id,\n perRouteFormatter,\n });\n\n socket.emit(buildApiResponseEventName(responseIndex), formattedResponse);\n\n //? `postApiRespond` is observation-only — the response is already on the\n //? wire. Use it for audit logging, metrics, dual-write replication.\n await dispatchHook('postApiRespond', { routeName: resolvedName, user, response: formattedResponse });\n};\n\n//? ET-02 — open a per-request error-tracker identity scope around the ENTIRE\n//? handler before any await that could interleave with another concurrent\n//? request. `readSession` (the first await below) is itself such a boundary, so\n//? the scope must wrap it. The session is written into the scope via\n//? `setCurrentErrorTrackerIdentity(...)` the moment it resolves; from then on\n//? every error-tracker capture during this request (handler throw, hook\n//? subscriber, fanout) reads THIS request's identity from the AsyncLocalStorage\n//? box, never another concurrent request's. Each request gets its own box.\nexport default async function handleApiRequest(args: handleApiRequestType): Promise<void> {\n //? handleApiRequestInner runs un-.catch()'d from loadSocket (a bare `void`\n //? call), so ANY throw from an unguarded dependency (readSession, the rate-limit\n //? Redis calls, getRuntimeApiMaps, the session store) would surface as a fatal\n //? unhandledRejection → worker crash on modern Node. Wrap the whole run (mirrors\n //? the sync handler's top-level tryCatch) and emit a generic error envelope on\n //? the response channel so the request fails cleanly instead of killing the worker.\n const [error] = await tryCatch(() => runWithErrorTrackerIdentityScope(() => handleApiRequestInner(args)));\n if (error) {\n getLogger().error('api: unhandled error in socket request handler', { error: error.message });\n const msg = args.msg as apiMessage | null;\n const responseIndex = msg?.responseIndex;\n if (typeof responseIndex === 'number') {\n args.socket.emit(buildApiResponseEventName(responseIndex), {\n status: 'error',\n errorCode: 'api.serverExecutionFailed',\n httpStatus: 500,\n });\n }\n }\n}\n\nasync function handleApiRequestInner({ msg, socket, token }: handleApiRequestType) {\n //? This event gets triggered when the client uses the apiRequest function.\n //? Validate the message, check auth, then execute the registered handler.\n\n //? `msg` is TYPED as apiMessage but the socket transport can deliver ANYTHING at\n //? runtime (null / array / primitive), so view it as `unknown` for the guard.\n //? `typeof null === 'object'` and arrays are objects, so the bare object check\n //? would let null/array through to `const { responseIndex } = msg` below —\n //? throwing before any handler. That throw, running un-.catch()'d from loadSocket,\n //? becomes a fatal unhandledRejection → worker crash; a connected socket could\n //? trigger it pre-auth via `socket.emit('api', null)` (remote DoS). Mirrors the\n //? sync handler's validateSyncMessage guard.\n const rawMsg: unknown = msg;\n if (typeof rawMsg !== 'object' || rawMsg === null || Array.isArray(rawMsg)) {\n if (shouldLogDev()) {\n getLogger().warn('api: socket message was not a json object');\n }\n return;\n }\n\n const { responseIndex } = msg;\n\n //? EXT-02 — per-message socket interception seam (counterpart to preHttpRequest\n //? in the HTTP pipeline and preSocketMessage in the sync handler). Fires before\n //? session lookup / route resolution / auth so a consumer can gate, throttle, or\n //? audit individual api messages. A stop signal rejects the message.\n const preMsgResult = await dispatchHook('preSocketMessage', {\n channel: 'api',\n socketId: socket.id,\n ip: socket.handshake.address,\n authenticated: Boolean(token),\n routeName: typeof msg.name === 'string' ? msg.name : undefined,\n });\n if (preMsgResult.stopped) {\n //? Guard the emit on a numeric responseIndex (mirrors the sync handler +\n //? validateApiMessage's own no-index branch below): this stop path runs\n //? BEFORE validateApiMessage, so a malformed frame lacking responseIndex\n //? would otherwise emit to the dead 'apiResponse-undefined' channel and\n //? leave the client hanging until its own ack timeout. Drop it silently.\n if (typeof responseIndex === 'number') {\n socket.emit(buildApiResponseEventName(responseIndex), {\n status: 'error',\n errorCode: preMsgResult.signal.errorCode,\n httpStatus: preMsgResult.signal.httpStatus ?? 403,\n });\n }\n return;\n }\n\n const user = await readSession(token);\n //? ET-02 — bind the resolved session into the active per-request ALS identity\n //? box so every subsequent capture attributes to this user. Identity also still\n //? flows to the legacy global via the `preApiValidate` hook subscriber in\n //? `@luckystack/error-tracking` (the ALS read takes precedence at capture time).\n setCurrentErrorTrackerIdentity(user?.id ? { id: user.id, email: user.email ?? undefined, username: user.name ?? undefined } : null);\n const preferredLocale =\n extractLanguageFromHeader(socket.handshake.headers['x-language'])\n ?? extractLanguageFromHeader(socket.handshake.headers['accept-language'])\n ?? undefined;\n\n //? Per-route formatter ref + resolved-name ref. Both start undefined and\n //? get set once the route is parsed + the apiEntry looked up — pre-lookup\n //? errors (invalid message shape, unknown route) emit with global formatter\n //? only because there's no apiEntry to read per-route formatter from. After\n //? lookup, every emit through `emitApiError` flows through the same chain.\n // eslint-disable-next-line prefer-const -- captured by emitApiError closure; assigned later after route resolution\n let currentRouteName: string | undefined;\n // eslint-disable-next-line prefer-const -- captured by emitApiError closure; assigned later after apiEntry lookup\n let currentPerRouteFormatter: ErrorFormatter | undefined;\n\n //? API-O6 parity — socket ERROR responses now run the SAME respond-hook chain\n //? as success responses (`emitApiResult`) and as the HTTP transport: preApiRespond\n //? (may rewrite/stop) → transformApiResponse (mutate) → error formatter → emit →\n //? postApiRespond (observe). Previously `emitApiError` emitted directly, so\n //? consumer respond hooks (PII redaction, response signing, audit logging) never\n //? fired on auth / rate-limit / validation / not-found rejections over WebSocket —\n //? a parity gap with HTTP, where every response (success AND error) runs them.\n const emitApiError: EmitApiError = async ({ response, fallbackHttpStatus }) => {\n const normalized = normalizeErrorResponse({\n response,\n preferredLocale,\n userLanguage: user?.language,\n fallbackHttpStatus,\n });\n const routeNameForHook = currentRouteName ?? 'api/unknown';\n\n //? preApiRespond may mutate the envelope or stop. A stop on an already-error\n //? response rebuilds a fresh error envelope from the signal (parity with\n //? `emitApiResult` + the HTTP path).\n // luckystack-allow no-as-any: hook payload boundary — normalizeErrorResponse returns a narrower type than the preApiRespond payload shape; fix requires @luckystack/core type alignment\n // eslint-disable-next-line no-restricted-syntax -- hook payload boundary, mirrors handleHttpApiRequest\n const preRespond = { routeName: routeNameForHook, user, response: normalized as unknown as { status: 'success' | 'error'; httpStatus?: number; [key: string]: unknown } };\n const preRespondResult = await dispatchHook('preApiRespond', preRespond);\n\n let finalResponse = preRespond.response;\n if (preRespondResult.stopped) {\n const stoppedEnvelope = normalizeErrorResponse({\n response: { status: 'error', errorCode: preRespondResult.signal.errorCode },\n preferredLocale,\n userLanguage: user?.language,\n fallbackHttpStatus: preRespondResult.signal.httpStatus ?? 403,\n });\n // luckystack-allow no-as-any: hook payload boundary — normalizeErrorResponse is narrower than the hook payload shape; fix requires @luckystack/core type alignment\n // eslint-disable-next-line no-restricted-syntax -- hook payload boundary, mirrors handleHttpApiRequest\n finalResponse = stoppedEnvelope as unknown as typeof preRespond.response;\n }\n\n const transformPayload = { routeName: routeNameForHook, user, response: finalResponse };\n await dispatchHook('transformApiResponse', transformPayload);\n\n const formatted = applyErrorFormatter({\n response: transformPayload.response,\n routeName: routeNameForHook,\n transport: 'socket',\n userId: user?.id,\n perRouteFormatter: currentPerRouteFormatter,\n });\n socket.emit(buildApiResponseEventName(responseIndex), formatted);\n\n //? postApiRespond is observation-only — the response is already on the wire.\n await dispatchHook('postApiRespond', { routeName: routeNameForHook, user, response: formatted });\n };\n\n const validated = await validateApiMessage({ msg, emitApiError });\n if (!validated) return;\n const { name, data: normalizedData } = validated;\n\n const parsedRoute = parseTransportRouteName({ value: name, prefix: 'api' });\n if (parsedRoute.status === 'error') {\n await emitApiError({\n response: {\n status: 'error',\n errorCode: 'routing.invalidServiceRouteName',\n errorParams: [{ key: 'name', value: name }],\n },\n fallbackHttpStatus: 400,\n });\n return;\n }\n\n const resolvedName = parsedRoute.normalizedFullName;\n currentRouteName = resolvedName;\n\n //? Built-in 'system/logout' needs special handling since it requires socket access.\n //? Match the full normalized route to avoid hijacking consumer routes whose final\n //? segment happens to be 'logout' (e.g. 'admin/logout/vN').\n if (parsedRoute.serviceRoute.normalizedRouteName === 'system/logout') {\n //? Apply the global per-IP bucket before logout so the shortcut can't be\n //? spammed uncapped — it bypasses applyApiRateLimits entirely.\n const logoutIpLimit = getProjectConfig().rateLimiting.defaultIpLimit;\n if (logoutIpLimit !== false && logoutIpLimit > 0) {\n const logoutIp = resolveClientIp({\n rawAddress: socket.handshake.address,\n headers: socket.handshake.headers,\n trustProxy: getProjectConfig().http.trustProxy,\n trustedProxyHopCount: getProjectConfig().http.trustedProxyHopCount,\n });\n const { allowed, resetIn } = await checkRateLimit({\n key: `ip:${logoutIp}:api:all`,\n limit: logoutIpLimit,\n windowMs: getProjectConfig().rateLimiting.windowMs,\n });\n if (!allowed) {\n void dispatchHook('rateLimitExceeded', {\n scope: 'ip',\n key: `ip:${logoutIp}:api:all`,\n limit: logoutIpLimit,\n windowMs: getProjectConfig().rateLimiting.windowMs,\n count: logoutIpLimit + 1,\n ip: logoutIp,\n });\n await emitApiError({\n response: {\n status: 'error',\n errorCode: 'api.rateLimitExceeded',\n errorParams: [{ key: 'seconds', value: resetIn }],\n },\n fallbackHttpStatus: 429,\n });\n return;\n }\n }\n //? API-O6 — route through `emitApiResult` so the respond-hook chain\n //? (preApiRespond, transformApiResponse, postApiRespond) fires for logout,\n //? matching all other routes. The return value of `performLogout` is void\n //? (success is implied by non-throw), so we pass a fixed success result.\n await performLogout({ token, socket, userId: user?.id ?? null });\n await emitApiResult({\n socket,\n responseIndex,\n resolvedName,\n error: null,\n result: { status: 'success', httpStatus: 200, result: true },\n preferredLocale,\n user,\n });\n return;\n }\n\n if (shouldLogDev()) {\n getLogger().debug(`api: ${resolvedName} called`);\n }\n\n const { apisObject, functionsObject } = await getRuntimeApiMaps();\n\n if (!apisObject[resolvedName]) {\n await emitApiError({\n response: {\n status: 'error',\n errorCode: 'api.notFound',\n errorParams: [{ key: 'name', value: name }],\n },\n fallbackHttpStatus: 404,\n });\n return;\n }\n\n const apiEntry = apisObject[resolvedName] as RuntimeApiEntry;\n currentPerRouteFormatter = apiEntry.errorFormatter;\n\n //? API-O4 — `httpMethod` (and the `inferHttpMethod` heuristic) is HTTP-ONLY.\n //? The socket transport does NOT enforce the declared HTTP method — all routes\n //? are callable over WebSocket regardless of method declaration. This is by\n //? design: socket.io has no HTTP-method concept and enforcing it would break\n //? existing consumers. If you need to restrict a route to HTTP only, do not\n //? expose it as a socket route, or gate it in `preSocketMessage`.\n\n //? Per-request lifecycle bundle (B1 abortController + cleanup, B2\n //? backpressure, abort-aware emitStream). Extracted to `_shared/\n //? requestLifecycle.ts`; the closures stay intact and the orchestrator\n //? threads the returned handles through the pipeline. `cleanupRequest`\n //? runs in every exit path below.\n const { abortController, abortSignal, emitStream, flushPressure, cleanupRequest } = createApiRequestLifecycle({\n socket,\n responseIndex,\n resolvedName,\n });\n\n //? Auth → rate-limit → validate → execute → respond.\n //? Auth runs before validate so unauthenticated probes can't enumerate\n //? routes or learn input shape from `inputValidation.message`.\n //? API-O7 — failed-auth requests do NOT consume a rate-limit bucket here.\n //? This is an explicit design choice: brute-force lockout is the\n //? responsibility of `@luckystack/login` (per-account lockout counter via\n //? `authLockout.ts`), and the `apiAuthRejected` hook lets consumers add\n //? additional throttles. Consuming the global-IP bucket on auth-fail would\n //? allow an attacker to trigger DoS for a victim account's IP by deliberately\n //? sending bad credentials.\n if (!(await checkApiAuth({ apiEntry, user, name, emitApiError }))) { cleanupRequest(); return; }\n\n const rateLimitOk = await runSocketRateLimits({ apiEntry, resolvedName, token, socket, user, emitApiError });\n if (!rateLimitOk) { cleanupRequest(); return; }\n\n //? Validation stage (mode resolve + preApiValidate -> validateInputByType ->\n //? postApiValidate). On failure it runs cleanup + emits the GENERIC\n //? `api.invalidInputType` (raw validator message never reaches the client)\n //? via the closures below, then returns false.\n const validationOk = await runSocketApiValidation({\n apiEntry,\n resolvedName,\n normalizedData,\n user,\n cleanupRequest,\n emitInvalidInputType: async () => { await emitApiError({\n response: { status: 'error', errorCode: 'api.invalidInputType' },\n fallbackHttpStatus: 400,\n }); },\n });\n if (!validationOk) return;\n\n //? Execution stage: preApiExecute (stop-signal short-circuit) -> handler ->\n //? postApiExecute. Returns either a stop signal or the (error, result) pair.\n const execution = await runSocketApiExecution({\n apiEntry,\n resolvedName,\n normalizedData,\n user,\n functionsObject,\n emitStream,\n abortSignal,\n abortController,\n flushPressure,\n });\n if (execution.stopped) {\n cleanupRequest();\n await emitApiError({\n response: { status: 'error', errorCode: execution.errorCode },\n fallbackHttpStatus: execution.httpStatus ?? 403,\n });\n return;\n }\n\n await emitApiResult({ socket, responseIndex, resolvedName, error: execution.error, result: execution.result, preferredLocale, user, perRouteFormatter: apiEntry.errorFormatter });\n cleanupRequest();\n}\n","import { getProjectConfig } from '@luckystack/core';\n\n//? CC-5 — config-getter convenience lambdas shared by both API transports.\n//? Read at call time (never at module load) so config registration order\n//? and hot-reload both stay correct.\n\n/** Gate for verbose per-request debug/warn/error logging. */\nexport const shouldLogDev = (): boolean => getProjectConfig().logging.devLogs;\n\n/** Gate for `emitStream` payload logging. */\nexport const shouldLogStream = (): boolean => getProjectConfig().logging.stream;\n","import type { BaseSessionLayout as SessionLayout } from '@luckystack/core';\nimport { defaultHttpStatusForResponse, normalizeErrorResponse, getLogger } from '@luckystack/core';\nimport { shouldLogDev } from './logFlags';\nimport type { ApiResponseEnvelope, RuntimeApiResponse } from './apiTypes';\n\n//? CC-6 — shared response-envelope assembly used by both API transports.\n//? Turns the `(error, result)` pair from the handler into the final wire\n//? envelope: localized error shapes (via `normalizeErrorResponse`) for\n//? failures/empty/invalid responses, or a success envelope with an inferred\n//? default HTTP status. Previously duplicated between `handleApiRequest`'s\n//? `buildApiResponseEnvelope` and `handleHttpApiRequest`'s inline assembly.\n\nexport const normalizeApiResponse = ({\n resolvedName,\n error,\n result,\n preferredLocale,\n user,\n}: {\n resolvedName: string;\n error: Error | null;\n result: RuntimeApiResponse | undefined | null;\n preferredLocale: string | null | undefined;\n user: SessionLayout | null;\n}): ApiResponseEnvelope => {\n if (error) {\n if (shouldLogDev()) {\n getLogger().error(`api: error in ${resolvedName}`, error, { route: resolvedName });\n }\n return {\n ...normalizeErrorResponse({\n response: { status: 'error', errorCode: 'api.internalServerError' },\n preferredLocale,\n userLanguage: user?.language,\n fallbackHttpStatus: 500,\n }),\n };\n }\n\n if (result === undefined || result === null) {\n if (shouldLogDev()) {\n getLogger().warn(`api: ${resolvedName} returned nothing`);\n }\n return {\n ...normalizeErrorResponse({\n response: { status: 'error', errorCode: 'api.emptyResponse' },\n preferredLocale,\n userLanguage: user?.language,\n fallbackHttpStatus: 500,\n }),\n };\n }\n\n if (shouldLogDev()) {\n getLogger().debug(`api: ${resolvedName} completed`);\n }\n\n //? Runtime guard: a handler is user code and can return a status outside the\n //? typed 'success' | 'error' union despite the RuntimeApiResponse type.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- defensive against malformed handler return at runtime\n if (result.status !== 'success' && result.status !== 'error') {\n return {\n ...normalizeErrorResponse({\n response: { status: 'error', errorCode: 'api.invalidResponseStatus' },\n preferredLocale,\n userLanguage: user?.language,\n fallbackHttpStatus: 500,\n }),\n };\n }\n\n if (result.status === 'error') {\n return {\n ...normalizeErrorResponse({\n response: result,\n preferredLocale,\n userLanguage: user?.language,\n fallbackHttpStatus: defaultHttpStatusForResponse({\n status: 'error',\n explicitHttpStatus: typeof result.httpStatus === 'number' ? result.httpStatus : undefined,\n }),\n }),\n };\n }\n\n //? API-O16 — the success envelope spreads ALL handler result fields directly\n //? onto the wire envelope. Handlers are responsible for not including sensitive\n //? data (e.g. `passwordHash`, raw session tokens) in their return value. There\n //? is no framework-level field allowlist; if your route aggregates a Prisma row\n //? with sensitive columns, pick explicit fields before returning.\n return {\n ...result,\n status: 'success',\n httpStatus: defaultHttpStatusForResponse({\n status: 'success',\n explicitHttpStatus: typeof result.httpStatus === 'number' ? result.httpStatus : undefined,\n }),\n };\n};\n","import type { Socket } from 'socket.io';\nimport {\n buildApiStreamEventName,\n getLogger,\n registerApiAbortController,\n unregisterApiAbortController,\n socketEventNames,\n} from '@luckystack/core';\nimport type { ApiStreamPayload, ApiFlushPressure } from './apiTypes';\nimport { shouldLogStream } from './logFlags';\nimport { createApiFlushPressure } from './backpressure';\n\n//? Per-request lifecycle bundle for the SOCKET API transport. Extracted from\n//? `handleApiRequest` per the `api` package audit (god-function decomposition).\n//? Keeps the abortController / cleanup / emitStream / flushPressure closures\n//? together and intact — the orchestrator wires them once and threads the\n//? returned handles through the pipeline. Behaviour is identical to the former\n//? inline closures:\n//? • B1 — per-request AbortController. Aborts on `apiCancel { responseIndex }`\n//? from the originator OR socket disconnect. `cleanupRequest()` runs in\n//? every exit path (errors, validation rejects, completion) to remove the\n//? disconnect listener and drop the cancel-registry entry. The signal is\n//? also handed to `emitStream` so chunks queued after an abort never hit\n//? the wire.\n//? • B2 — backpressure helper bound to the originator socket.\n\nexport interface ApiRequestLifecycle {\n abortController: AbortController;\n abortSignal: AbortSignal;\n emitStream: (payload?: ApiStreamPayload) => void;\n flushPressure: ApiFlushPressure;\n cleanupRequest: () => void;\n}\n\nexport const createApiRequestLifecycle = ({ socket, responseIndex, resolvedName }: {\n socket: Socket;\n responseIndex: number;\n resolvedName: string;\n}): ApiRequestLifecycle => {\n const abortController = new AbortController();\n const abortKey = registerApiAbortController(socket.id, responseIndex, abortController);\n const onSocketDisconnect = () => { abortController.abort(); };\n socket.once(socketEventNames.disconnect, onSocketDisconnect);\n\n let cleanupDone = false;\n const cleanupRequest = () => {\n if (cleanupDone) return;\n cleanupDone = true;\n socket.off(socketEventNames.disconnect, onSocketDisconnect);\n unregisterApiAbortController(abortKey);\n };\n\n const emitStream = (payload: ApiStreamPayload = {}) => {\n if (abortController.signal.aborted) {\n if (shouldLogStream()) {\n getLogger().debug(`api: ${resolvedName} stream skipped — request aborted`);\n }\n return;\n }\n if (shouldLogStream()) {\n getLogger().debug(`api: ${resolvedName} stream`, { payload });\n }\n socket.emit(buildApiStreamEventName(responseIndex), payload);\n };\n\n //? B2 — backpressure helper for API handlers. Same shape as the sync variant\n //? but always scoped to a single originator socket. Polls the engine.io\n //? writeBuffer length every 10ms until the buffer drains below threshold\n //? (default 1 MB ≈ 1024 packets at ~1KB each).\n const flushPressure = createApiFlushPressure(socket, abortController.signal);\n\n return { abortController, abortSignal: abortController.signal, emitStream, flushPressure, cleanupRequest };\n};\n","import type { Socket } from 'socket.io';\nimport { getLogger } from '@luckystack/core';\nimport type { ApiFlushPressure, ApiFlushPressureOptions } from './apiTypes';\nimport { shouldLogDev } from './logFlags';\n\n//? B2 — backpressure helper factory for the SOCKET API transport. Same shape\n//? as the sync variant; resolves once the originator socket's pending write\n//? buffer drops below the threshold. Default 1 MB (≈1024 packets at ~1KB each).\n//? Extracted from `handleApiRequest` per the `api` package audit (solid-isp).\n\ninterface EngineIoConnLike {\n writeBuffer?: { length: number };\n transport?: { writable?: boolean };\n}\n\nexport const createApiFlushPressure = (socket: Socket, abortSignal: AbortSignal): ApiFlushPressure => {\n return async ({ thresholdBytes }: ApiFlushPressureOptions = {}): Promise<void> => {\n const effectiveThresholdBytes = typeof thresholdBytes === 'number' && thresholdBytes > 0\n ? thresholdBytes\n : 1_048_576;\n const packetThreshold = Math.max(1, Math.ceil(effectiveThresholdBytes / 1024));\n //? Polls until the originator's socket write-buffer drains below the packet\n //? threshold, or the request aborts (loop condition), or the transport goes\n //? unwritable. Same exit conditions as the inline socket-handler original.\n while (!abortSignal.aborted) {\n //? Reading engine.io internals (writeBuffer / transport.writable) is the\n //? only way to measure socket backpressure; the cast is the documented\n //? boundary to those untyped internals. Moved verbatim from the socket\n //? handler — same access pattern, no behavioral change.\n // luckystack-allow no-as-unknown: engine.io internal — `conn` is not typed on Socket; this is the documented access pattern\n // eslint-disable-next-line no-restricted-syntax -- engine.io internals boundary\n const conn = (socket as unknown as { conn?: EngineIoConnLike }).conn;\n //? API-O15 — if engine.io renames `conn` this silently becomes a no-op\n //? (buffer always 0, writable always true). Warn in dev so a rename is\n //? caught before backpressure is broken in production.\n if (shouldLogDev() && !conn) {\n getLogger().warn('api: socket.conn absent — engine.io internal may have been renamed; backpressure is a no-op');\n }\n const packets = conn?.writeBuffer?.length ?? 0;\n const writable = conn?.transport?.writable ?? true;\n if (!writable) return;\n if (packets < packetThreshold) return;\n await new Promise<void>((resolve) => setTimeout(resolve, 10));\n }\n };\n};\n\n//? HTTP/SSE has no socket write-buffer to measure — `flushPressure` is a no-op\n//? on that transport. Shared at module scope so per-request allocation stays\n//? cheap and the handler param shape stays consistent across transports.\nexport const httpApiFlushPressureNoop: ApiFlushPressure = async () => {\n /* SSE backpressure is the caller's responsibility (res.write returns bool) */\n};\n","import type { BaseSessionLayout as SessionLayout } from '@luckystack/core';\nimport { dispatchHook, validateInputByType, getLogger } from '@luckystack/core';\nimport type { RuntimeApiEntry } from './apiTypes';\nimport { shouldLogDev } from './logFlags';\nimport { warnIfInputTypeMissing } from './inputTypeWarning';\n\n//? SOCKET-transport input-validation stage. Extracted from `handleApiRequest`\n//? per the `api` package audit (god-function decomposition). Behaviour is\n//? identical to the former inline block:\n//? • resolves the per-route validation mode (`'relaxed'` / `{ input: 'skip' }`\n//? => skip Zod validation; default `'strict'`);\n//? • dispatches `preApiValidate`, runs `validateInputByType` in strict mode,\n//? then `postApiValidate` (always, including the relaxed skip path);\n//? • on validation failure emits the GENERIC `api.invalidInputType` (the raw\n//? validator message is NEVER echoed to the client — it only travels to the\n//? postApiValidate hook + dev logs) and returns `false`.\n//? Error emission + per-request cleanup are delegated to the caller's closures\n//? so the orchestrator keeps ownership of the abort/cleanup/emitError refs.\n\ntype EmitInvalidInputType = () => void | Promise<void>;\n\nexport const resolveValidationMode = (validation: RuntimeApiEntry['validation']): 'strict' | 'relaxed' => {\n if (!validation) return 'strict';\n //? FAIL CLOSED on an unrecognized value (parity with the sync transport's\n //? `resolveSyncValidationMode`). Only the exact string `'relaxed'` (or the\n //? object form `{ input: 'skip' }`) skips validation; ANY other value — a\n //? typo like `'Strict'`/`'on'` — falls back to `'strict'` (validate) rather\n //? than being returned verbatim and silently DISABLING validation.\n if (typeof validation === 'string') {\n return validation === 'relaxed' ? 'relaxed' : 'strict';\n }\n return validation.input === 'skip' ? 'relaxed' : 'strict';\n};\n\nexport const runSocketApiValidation = async ({\n apiEntry,\n resolvedName,\n normalizedData,\n user,\n cleanupRequest,\n emitInvalidInputType,\n}: {\n apiEntry: RuntimeApiEntry;\n resolvedName: string;\n normalizedData: Record<string, unknown>;\n user: SessionLayout | null;\n cleanupRequest: () => void;\n emitInvalidInputType: EmitInvalidInputType;\n}): Promise<boolean> => {\n //? Per-route validation toggle. `'relaxed'` or `{ input: 'skip' }` skips\n //? runtime input validation entirely — useful for public webhooks (Stripe,\n //? Slack, GitHub) where the third party's payload shape isn't reasonable\n //? to model in TypeScript. Default `'strict'`.\n const validationMode = resolveValidationMode(apiEntry.validation);\n\n if (validationMode === 'strict') {\n warnIfInputTypeMissing(resolvedName, apiEntry.inputType);\n }\n await dispatchHook('preApiValidate', { routeName: resolvedName, data: normalizedData, user, transport: 'socket' });\n\n if (validationMode === 'strict') {\n const inputValidation = await validateInputByType({\n typeText: apiEntry.inputType,\n value: normalizedData,\n rootKey: 'data',\n filePath: apiEntry.inputTypeFilePath,\n });\n\n await dispatchHook('postApiValidate', {\n routeName: resolvedName,\n data: normalizedData,\n user,\n validation: inputValidation,\n transport: 'socket',\n });\n\n if (inputValidation.status === 'error') {\n //? SECURITY: do NOT echo the raw validator message (e.g.\n //? \"data.userId should be string\") back to the client — that lets an\n //? unauthenticated caller enumerate a route's input schema. The DETAILED\n //? message is routed to the `postApiValidate` hook above (and dev logs);\n //? the client only receives the generic `api.invalidInputType` code.\n if (shouldLogDev()) {\n getLogger().warn(`api: input validation failed for ${resolvedName}`, { route: resolvedName, message: inputValidation.message });\n }\n cleanupRequest();\n await emitInvalidInputType();\n return false;\n }\n } else {\n //? Relaxed: surface the skip via postApiValidate so audit handlers see it.\n await dispatchHook('postApiValidate', {\n routeName: resolvedName,\n data: normalizedData,\n user,\n validation: { status: 'success' },\n transport: 'socket',\n });\n }\n\n return true;\n};\n","import { getProjectConfig, getLogger } from '@luckystack/core';\n\n//? Dev-only one-shot warning when a route has no generated `inputType`\n//? (runtime Zod input validation is effectively disabled for it). Shared by\n//? both API transports so the \"warned once per route\" set is process-wide\n//? regardless of which transport hit the route first.\n\nconst warnedMissingInputType = new Set<string>();\n\nexport const warnIfInputTypeMissing = (resolvedName: string, inputType: string | undefined): void => {\n if (!getProjectConfig().dev.warnOnMissingInputType) return;\n if (inputType && inputType.trim().length > 0 && inputType.trim() !== 'any') return;\n if (warnedMissingInputType.has(resolvedName)) return;\n warnedMissingInputType.add(resolvedName);\n getLogger().warn(`api: route ${resolvedName} has no inputType — runtime input validation is disabled. Regenerate types or set the inputType on the handler.`);\n};\n","import type { BaseSessionLayout as SessionLayout } from '@luckystack/core';\nimport { checkRateLimit, getProjectConfig, dispatchHook, getLogger } from '@luckystack/core';\nimport { shouldLogDev } from './logFlags';\n\n//? API-O8 — shared rate-limit apply helper for BOTH API transports (socket +\n//? HTTP). Extracted so the two-bucket logic (per-route + global IP) stays in\n//? one place; each transport resolves the effective IP before calling here.\n\nexport interface ApplyApiRateLimitsArgs {\n /** Effective client IP (already resolved by the caller's transport adapter). */\n resolvedIp: string;\n token: string | null;\n user: SessionLayout | null;\n resolvedName: string;\n rateLimit: number | false | undefined;\n transport: 'socket' | 'http';\n /**\n * When true, skip the global-IP (`ip:…:api:all`) bucket check. Used by the\n * HTTP transport to honour `rateLimiting.skipLoopbackInDev` for loopback\n * callers without duplicating bucket logic here. The socket transport never\n * sets this (loopback skip is HTTP-only).\n */\n skipGlobalIpBucket?: boolean;\n}\n\nexport interface ApplyApiRateLimitsResult {\n /** True when all buckets allow the request; false when any bucket exceeded. */\n allowed: boolean;\n /** The error payload to emit when `allowed` is false. */\n errorCode?: string;\n resetIn?: number;\n}\n\n/**\n * Checks per-route + global-IP rate-limit buckets for an API request.\n * Returns `{ allowed: true }` when all buckets allow through, or\n * `{ allowed: false, errorCode, resetIn }` when any bucket is full.\n */\nexport const applyApiRateLimits = async ({\n resolvedIp,\n user,\n resolvedName,\n rateLimit,\n transport,\n skipGlobalIpBucket = false,\n}: ApplyApiRateLimitsArgs): Promise<ApplyApiRateLimitsResult> => {\n const config = getProjectConfig();\n const effectiveApiLimit = rateLimit ?? config.rateLimiting.defaultApiLimit;\n\n if (effectiveApiLimit !== false && effectiveApiLimit > 0) {\n const identityCb = config.rateLimiting.identity;\n const customIdentity = identityCb?.({ routeName: resolvedName, userId: user?.id ?? null, ip: resolvedIp, transport }) ?? null;\n //? Per-route bucket keyed on the VALIDATED user.id (never the token): a token\n //? is per-session, so token-keying let an authenticated abuser reset their\n //? bucket by re-logging-in (new token = fresh counter). user.id ties all of a\n //? user's sessions to one bucket; anonymous callers fall back to the IP.\n const requesterIdentity = customIdentity?.id ?? user?.id ?? resolvedIp;\n const keyPrefix = customIdentity?.scope ?? (user?.id ? 'user' : 'ip');\n const rateLimitKey = `${keyPrefix}:${requesterIdentity}:api:${resolvedName}`;\n\n const { allowed, resetIn } = await checkRateLimit({\n key: rateLimitKey,\n limit: effectiveApiLimit,\n windowMs: config.rateLimiting.windowMs,\n });\n\n if (!allowed) {\n //? The per-route bucket is keyed by the validated user when a token is\n //? present, else by the resolved IP (keyPrefix `ip`). Report the scope\n //? that matches the bucket's actual identity — an anonymous per-route\n //? bucket is IP-keyed, so it is `ip` (with `route` still set to mark it\n //? a per-route bucket vs the global `:api:all` IP bucket), never `route`.\n void dispatchHook('rateLimitExceeded', {\n scope: user?.id ? 'user' : 'ip',\n key: rateLimitKey,\n limit: effectiveApiLimit,\n windowMs: config.rateLimiting.windowMs,\n count: effectiveApiLimit + 1,\n route: resolvedName,\n userId: user?.id,\n //? IP only for an IP-keyed (anonymous) bucket; a user-keyed bucket omits it.\n ip: user?.id ? undefined : resolvedIp,\n });\n if (shouldLogDev()) {\n getLogger().warn(`api: rate limit exceeded for ${resolvedName}`, { route: resolvedName, key: rateLimitKey, transport });\n }\n return { allowed: false, errorCode: 'api.rateLimitExceeded', resetIn };\n }\n }\n\n const defaultIpLimit = config.rateLimiting.defaultIpLimit;\n if (!skipGlobalIpBucket && defaultIpLimit !== false && defaultIpLimit > 0) {\n const { allowed, resetIn } = await checkRateLimit({\n key: `ip:${resolvedIp}:api:all`,\n limit: defaultIpLimit,\n windowMs: config.rateLimiting.windowMs,\n });\n\n if (!allowed) {\n void dispatchHook('rateLimitExceeded', {\n scope: 'ip',\n key: `ip:${resolvedIp}:api:all`,\n limit: defaultIpLimit,\n windowMs: config.rateLimiting.windowMs,\n count: defaultIpLimit + 1,\n ip: resolvedIp,\n });\n if (shouldLogDev()) {\n getLogger().warn(`api: global IP rate limit exceeded`, { ip: resolvedIp, transport });\n }\n return { allowed: false, errorCode: 'api.rateLimitExceeded', resetIn };\n }\n }\n\n return { allowed: true };\n};\n","import type { BaseSessionLayout as SessionLayout, PostApiExecutePayload, ErrorFormatter } from '@luckystack/core';\nimport { getProjectConfig, readSession, getRuntimeApiMaps as getRuntimeApiMapsFromSource ,\n runWithErrorTrackerIdentityScope,\n setCurrentErrorTrackerIdentity,\n validateRequest,\n inferHttpMethod,\n HttpMethod,\n tryCatch,\n parseTransportRouteName,\n dispatchHook,\n getLogger, extractLanguageFromHeader, normalizeErrorResponse, applyErrorFormatter,\n isLoopbackIp, resolveClientIp, isProductionRuntime } from '@luckystack/core';\nimport type { ApiStreamPayload, RuntimeApiEntry } from './_shared/apiTypes';\nimport { shouldLogDev, shouldLogStream } from './_shared/logFlags';\nimport { normalizeApiResponse } from './_shared/responseEnvelope';\nimport { httpApiFlushPressureNoop } from './_shared/backpressure';\nimport { runHttpApiValidation } from './_shared/httpValidationStage';\nimport { applyApiRateLimits } from './_shared/applyApiRateLimits';\n\n\n\n\n\n\n/**\n * HTTP API Request Handler\n * \n * Handles API requests coming via HTTP (instead of WebSocket).\n * Reuses existing API handlers but returns results as HTTP response.\n * \n * Payload format:\n * ```json\n * {\n * \"name\": \"api/examples/publicApi\",\n * \"data\": { \"message\": \"hello\" }\n * }\n * ```\n * \n * @example\n * ```typescript\n * // In server.ts\n * if (pathname.startsWith('/api/')) {\n * const token = extractTokenFromRequest(req);\n * const body = await parseJsonBody(req);\n * const result = await handleHttpApiRequest({ name: body.name, data: body.data, token });\n * res.end(JSON.stringify(result));\n * }\n * ```\n */\n\ninterface HttpApiRequestParams {\n name: string;\n data: Record<string, unknown>;\n token: string | null;\n requesterIp?: string;\n xLanguageHeader?: string | string[];\n acceptLanguageHeader?: string | string[];\n /** HTTP method from the request */\n method?: HttpMethod;\n stream?: (payload: ApiHttpStreamEvent) => void;\n /**\n * Optional AbortSignal. The HTTP server (`@luckystack/server`) wires this\n * to `req.on('close', ...)` so a closed SSE/HTTP connection aborts in-flight\n * stream emits. Handlers receive it as `abortSignal` in params.\n */\n abortSignal?: AbortSignal;\n}\n\ntype ApiNetworkResponse<T = Record<string, unknown>> =\n | ({ status: 'success'; httpStatus: number } & T)\n | {\n status: 'error';\n httpStatus: number;\n message: string;\n errorCode: string;\n errorParams?: {\n key: string;\n value: string | number | boolean;\n }[];\n };\n\nexport type ApiHttpStreamEvent = ApiStreamPayload;\n\n//? ET-02 — wrap the whole HTTP request in a per-request error-tracker identity\n//? scope (opened before `readSession` in `runHandleHttpApiRequest`, the first\n//? interleaving await). The resolved session is written into the scope's ALS box\n//? the moment it resolves, so concurrent HTTP requests with different users can't\n//? cross-attribute captures. Each request gets its own isolated box.\nexport async function handleHttpApiRequest(params: HttpApiRequestParams): Promise<ApiNetworkResponse> {\n return runWithErrorTrackerIdentityScope(() => handleHttpApiRequestScoped(params));\n}\n\nasync function handleHttpApiRequestScoped(params: HttpApiRequestParams): Promise<ApiNetworkResponse> {\n const { response, user, preferredLocale, perRouteFormatter } = await runHandleHttpApiRequest(params);\n\n //? `name` may not parse to a valid route — pass the original raw name as\n //? routeName so hook handlers can still correlate the rejection.\n const routeNameForHook = params.name.startsWith('api/') ? params.name : `api/${params.name}`;\n\n const preRespond = { routeName: routeNameForHook, user, response: response as { status: 'success' | 'error'; httpStatus?: number; [key: string]: unknown } };\n const preRespondResult = await dispatchHook('preApiRespond', preRespond);\n\n //? Honor a stop signal from `preApiRespond`: rewrite the envelope into a\n //? localized error response per the signal's errorCode/httpStatus instead\n //? of letting the original through. Same contract as the socket variant.\n const finalResponse = preRespondResult.stopped\n ? normalizeErrorResponse({\n response: { status: 'error', errorCode: preRespondResult.signal.errorCode },\n preferredLocale,\n userLanguage: user?.language,\n fallbackHttpStatus: preRespondResult.signal.httpStatus ?? 403,\n }) as ApiNetworkResponse\n : preRespond.response as ApiNetworkResponse;\n\n //? `transformApiResponse` fires AFTER preApiRespond and BEFORE the wire\n //? response is finalized. Mirrors the socket handler (handleApiRequest)\n //? so BOTH transports preserve the same policy/hook ordering. 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 //? Runtime defense: a caller may pass a non-string despite the types.\n if (!name || typeof name !== 'string') {\n return ctx.buildNetworkError({\n response: { status: 'error', errorCode: 'api.invalidName' },\n fallbackHttpStatus: 400,\n });\n }\n\n //? API-O5 — mirror the socket guard: `typeof [] === 'object'`, so an array\n //? payload would pass the original check and hit a `{...}`-destructuring\n //? handler, causing a 500 instead of the correct 400.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime defense against malformed HTTP payloads despite types\n if (typeof data !== 'object' || data === null || Array.isArray(data)) {\n return ctx.buildNetworkError({\n response: { status: 'error', errorCode: 'api.invalidDataObject' },\n fallbackHttpStatus: 400,\n });\n }\n\n const normalizedName = name.startsWith('api/') ? name : `api/${name}`;\n const parsedRoute = parseTransportRouteName({ value: normalizedName, prefix: 'api' });\n if (parsedRoute.status === 'error') {\n return ctx.buildNetworkError({\n response: {\n status: 'error',\n errorCode: 'routing.invalidServiceRouteName',\n errorParams: [{ key: 'name', value: normalizedName }],\n },\n fallbackHttpStatus: 400,\n });\n }\n\n const resolvedName = parsedRoute.normalizedFullName;\n\n if (shouldLogDev()) {\n getLogger().debug(`http api: ${resolvedName} called`);\n }\n\n const { apisObject, functionsObject } = await getRuntimeApiMapsFromSource();\n\n if (!apisObject[resolvedName]) {\n return ctx.buildNetworkError({\n response: {\n status: 'error',\n errorCode: 'api.notFound',\n errorParams: [{ key: 'name', value: resolvedName }],\n },\n fallbackHttpStatus: 404,\n });\n }\n\n return { resolvedName, requestData: data, apisObject, functionsObject };\n}\n\n/**\n * Runs the login-required guard and the `validateRequest` additional-predicate\n * check. Returns an error response on failure, or `null` to signal success.\n *\n * Pipeline order: auth → rate-limit → method → validate → execute.\n * Auth runs first so unauthenticated probes can't enumerate routes or\n * learn input shape from `inputValidation.message` / method-mismatch params.\n * API-O7 — failed-auth requests do NOT consume a rate-limit bucket here.\n * Brute-force lockout is the responsibility of `@luckystack/login`\n * (per-account lockout counter via `authLockout.ts`); the `apiAuthRejected`\n * hook lets consumers add additional throttles. Consuming a per-IP bucket on\n * auth-fail would let an attacker DoS a victim's IP with bad credentials.\n */\nfunction runHttpApiAuth(\n {\n name,\n resolvedName,\n requesterIp,\n auth,\n }: {\n name: string;\n resolvedName: string;\n requesterIp: string | undefined;\n auth: RuntimeApiEntry['auth'];\n },\n ctx: HttpApiPipelineContext,\n): ApiNetworkResponse | null {\n if (auth.login && !ctx.user?.id) {\n if (shouldLogDev()) {\n getLogger().warn(`http-api: ${name} requires login`, { route: name, transport: 'http' });\n }\n void dispatchHook('apiAuthRejected', {\n routeName: resolvedName,\n reason: 'login-required',\n userId: null,\n transport: 'http',\n ip: requesterIp,\n });\n return ctx.buildNetworkError({\n response: { status: 'error', errorCode: 'auth.required' },\n fallbackHttpStatus: 401,\n });\n }\n\n //? NO bare `if (!user) -> auth.forbidden` here. Public routes\n //? (auth.login: false) must be callable without a session — exactly like the\n //? socket API handler (handleApiRequest) and both sync handlers.\n //? `validateRequest` is the single source of truth for `additional` auth\n //? predicates. (This handler previously forbade ALL anonymous HTTP calls,\n //? diverging from the other three transports and making public routes\n //? unreachable over HTTP — e.g. the test-runner contract/rate-limit sweeps.)\n //? `user` may be null on a public route; validateRequest tolerates it (and\n //? only the login/additional predicates read it). Same `user!` shape the\n //? socket API handler and both sync handlers use.\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n const authResult = validateRequest({ auth, user: ctx.user! });\n if (authResult.status === 'error') {\n if (shouldLogDev()) {\n getLogger().warn(`http-api: auth failed for ${name}`, { route: name, errorCode: authResult.errorCode, transport: 'http' });\n }\n void dispatchHook('apiAuthRejected', {\n routeName: resolvedName,\n reason: authResult.errorCode === 'auth.invalidCondition' ? 'invalid-condition' : 'additional-failed',\n userId: ctx.user?.id ?? null,\n transport: 'http',\n ip: requesterIp,\n failedKey: authResult.errorCode,\n });\n return ctx.buildNetworkError({\n response: {\n status: 'error',\n errorCode: authResult.errorCode ?? 'auth.forbidden',\n errorParams: authResult.errorParams,\n httpStatus: authResult.httpStatus,\n },\n fallbackHttpStatus: authResult.httpStatus ?? 403,\n });\n }\n\n return null;\n}\n\n/**\n * Resolves the effective client IP, computes the loopback-skip flag, and\n * delegates to the shared `applyApiRateLimits` helper.\n * Returns an error response when any bucket is exceeded, `null` otherwise.\n */\nasync function applyHttpApiRateLimits(\n {\n requesterIp,\n token,\n resolvedName,\n rateLimit,\n }: {\n requesterIp: string | undefined;\n token: string | null;\n resolvedName: string;\n rateLimit: number | false | undefined;\n },\n ctx: HttpApiPipelineContext,\n): Promise<ApiNetworkResponse | null> {\n //? API-O8 / API-O9 — resolve effective IP once for both rate-limit buckets.\n //? When the caller did not supply `requesterIp` we fall back to the shared\n //? `UNKNOWN_CLIENT_IP` sentinel so all unresolvable callers collapse into ONE\n //? deterministic bucket instead of mixing `'unknown'` and `'anonymous'`.\n const effectiveIp = requesterIp\n ?? resolveClientIp({ rawAddress: undefined, headers: {} });\n\n //? API-O2 — skip the global per-IP ABUSE limit for loopback traffic when the\n //? consumer explicitly opts in via `rateLimiting.skipLoopbackInDev` (default\n //? false). Only the cross-route `:api:all` bucket is skipped; per-route limits\n //? still apply. Pass the raw `requesterIp` (undefined → '') so only real\n //? loopback addresses qualify — avoids skipping for every unresolvable caller.\n const skipGlobalIpForLoopback = getProjectConfig().rateLimiting.skipLoopbackInDev\n && !isProductionRuntime()\n && isLoopbackIp(requesterIp ?? '');\n\n const rateLimitResult = await applyApiRateLimits({\n resolvedIp: effectiveIp,\n token,\n user: ctx.user,\n resolvedName,\n rateLimit,\n transport: 'http',\n skipGlobalIpBucket: skipGlobalIpForLoopback,\n });\n\n if (!rateLimitResult.allowed) {\n return ctx.buildNetworkError({\n response: {\n status: 'error',\n errorCode: rateLimitResult.errorCode ?? 'api.rateLimitExceeded',\n errorParams: [{ key: 'seconds', value: rateLimitResult.resetIn ?? 0 }],\n },\n fallbackHttpStatus: 429,\n });\n }\n\n return null;\n}\n\n/**\n * Enforces the HTTP method constraint (declared or inferred).\n * Runs post-auth so unauthenticated probes don't learn that the route exists.\n * Returns an error response on mismatch, `null` otherwise.\n */\nfunction checkHttpApiMethod(\n {\n method,\n resolvedName,\n declaredMethod,\n }: {\n method: HttpMethod;\n resolvedName: string;\n declaredMethod: HttpMethod | undefined;\n },\n ctx: HttpApiPipelineContext,\n): ApiNetworkResponse | null {\n const expectedMethod = declaredMethod ?? inferHttpMethod(resolvedName);\n if (method !== expectedMethod) {\n if (shouldLogDev()) {\n getLogger().warn(`http api: method mismatch for ${resolvedName}`, { expected: expectedMethod, got: method });\n }\n return ctx.buildNetworkError({\n response: {\n status: 'error',\n errorCode: 'api.methodNotAllowed',\n errorParams: [{ key: 'method', value: expectedMethod }],\n },\n fallbackHttpStatus: 405,\n });\n }\n\n return null;\n}\n\n/**\n * Runs the handler execution stage: emitter setup, pre/post-execute hooks,\n * server-side timeout race, `main()` invocation, and response-envelope\n * assembly.\n */\nasync function runHttpApiExecution(\n {\n resolvedName,\n requestData,\n functionsObject,\n runtimeApiRoute,\n effectiveAbortSignal,\n stream,\n }: {\n resolvedName: string;\n requestData: Record<string, unknown>;\n functionsObject: Record<string, unknown>;\n runtimeApiRoute: RuntimeApiEntry;\n effectiveAbortSignal: AbortSignal;\n stream: HttpApiRequestParams['stream'];\n },\n ctx: HttpApiPipelineContext,\n): Promise<ApiNetworkResponse> {\n //? SSE has no socket write-buffer to measure — `flushPressure` is a no-op\n //? on HTTP transport. Kept for handler-shape parity with the socket path.\n const flushPressure = httpApiFlushPressureNoop;\n\n const emitApiStream = (payload: ApiStreamPayload = {}) => {\n if (!stream) {\n return;\n }\n if (effectiveAbortSignal.aborted) {\n if (shouldLogStream()) {\n getLogger().debug(`http api: ${resolvedName} stream skipped — request aborted`);\n }\n return;\n }\n\n if (shouldLogStream()) {\n getLogger().debug(`http api: ${resolvedName} stream`, { payload });\n }\n\n stream(payload);\n };\n\n //? Single payload reference reused by pre/post — auto-instrumentation\n //? subscribers in `@luckystack/error-tracking` pin spans via WeakMap on\n //? this object. Mutating `result/error/durationMs` is safe because\n //? handlers observe by-name (not by snapshot).\n const executePayload: PostApiExecutePayload = {\n routeName: resolvedName,\n data: requestData,\n user: ctx.user,\n transport: 'http',\n result: undefined,\n error: null,\n durationMs: 0,\n };\n const preExecuteResult = await dispatchHook('preApiExecute', executePayload);\n if (preExecuteResult.stopped) {\n return ctx.buildNetworkError({\n response: { status: 'error', errorCode: preExecuteResult.signal.errorCode },\n fallbackHttpStatus: preExecuteResult.signal.httpStatus ?? 403,\n });\n }\n\n //? API-O10 — race `main()` against a server-side timeout derived from\n //? `api.requestTimeoutMs`. When the timeout fires first we abort the\n //? effectiveAbortSignal chain and return a localized 504 envelope.\n //? A combined AbortController merges the caller's close signal and the\n //? timeout so the handler sees one unified abort.\n //? Streaming requests are EXEMPT from the wall-clock timeout even when one is\n //? configured: an SSE/stream endpoint is long-lived by design (live LLM tokens,\n //? progress) and a total-duration race would truncate it mid-stream. The\n //? abortSignal (wired to client disconnect) still tears it down when the caller\n //? leaves. Non-streaming requests honor `api.requestTimeoutMs` (default false).\n const timeoutMs = stream ? false : getProjectConfig().api.requestTimeoutMs;\n const combinedController = new AbortController();\n //? Forward the caller's abort (connection close) into the combined signal.\n if (effectiveAbortSignal.aborted) {\n combinedController.abort();\n } else {\n effectiveAbortSignal.addEventListener('abort', () => { combinedController.abort(); }, { once: true });\n }\n\n let timedOut = false;\n let timeoutHandle: ReturnType<typeof setTimeout> | undefined;\n const timeoutPromise = timeoutMs === false\n ? null\n : new Promise<void>((resolve) => {\n timeoutHandle = setTimeout(() => {\n timedOut = true;\n combinedController.abort();\n resolve();\n }, timeoutMs);\n });\n\n const executeStart = Date.now();\n const handlerPromise = tryCatch(\n async () => await runtimeApiRoute.main({\n data: requestData,\n user: ctx.user,\n functions: functionsObject,\n stream: emitApiStream,\n abortSignal: combinedController.signal,\n flushPressure,\n }),\n undefined,\n {\n handler: 'handleHttpApiRequest',\n api: resolvedName,\n userId: ctx.user?.id,\n transport: 'http',\n },\n );\n\n if (timeoutPromise) {\n await Promise.race([handlerPromise, timeoutPromise]);\n }\n clearTimeout(timeoutHandle);\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- timedOut is mutated by the async timeout callback via Promise.race\n if (timedOut) {\n return ctx.buildNetworkError({\n response: { status: 'error', errorCode: 'api.timeout' },\n fallbackHttpStatus: 504,\n });\n }\n\n const [error, result] = await handlerPromise;\n executePayload.result = result;\n executePayload.error = error ?? null;\n executePayload.durationMs = Date.now() - executeStart;\n await dispatchHook('postApiExecute', executePayload);\n\n //? Shared envelope assembly (CC-6) — identical to the socket handler's\n //? `buildApiResponseEnvelope`: localized error shapes for failure / empty /\n //? invalid-status, or a success envelope with inferred default HTTP status.\n //? `normalizeApiResponse` runtime-checks `result.status`, so a handler that\n //? returns a non-status object still maps to `api.invalidResponseStatus`.\n const envelope = normalizeApiResponse({\n resolvedName,\n error: error ?? null,\n result: result ?? undefined,\n preferredLocale: ctx.preferredLocale,\n user: ctx.user,\n });\n return envelope as ApiNetworkResponse;\n}\n\nasync function runHandleHttpApiRequestInner(\n {\n name,\n data,\n token,\n requesterIp,\n xLanguageHeader: _xLanguageHeader,\n acceptLanguageHeader: _acceptLanguageHeader,\n method = 'POST',\n stream,\n abortSignal,\n }: HttpApiRequestParams,\n user: SessionLayout | null,\n preferredLocale: string | null | undefined,\n formatterHolder: { current?: ErrorFormatter },\n): Promise<ApiNetworkResponse> {\n //? B1 — HTTP/SSE transport. The caller (typically `@luckystack/server`'s\n //? SSE bridge) wires `req.on('close', ...)` to a controller and passes its\n //? signal in. If no signal was provided we synthesize a never-aborting one\n //? so the handler param shape stays consistent.\n const effectiveAbortSignal = abortSignal ?? new AbortController().signal;\n\n const buildNetworkError = ({\n response,\n fallbackHttpStatus,\n }: {\n response: { status: 'error'; httpStatus?: number; errorCode?: string; errorParams?: { key: string; value: string | number | boolean; }[] };\n fallbackHttpStatus?: number;\n }): ApiNetworkResponse => {\n return normalizeErrorResponse({\n response,\n preferredLocale,\n userLanguage: user?.language,\n fallbackHttpStatus,\n }) as ApiNetworkResponse;\n };\n\n //? Identity propagation now flows via the `preApiExecute` hook subscriber\n //? registered by `@luckystack/error-tracking`'s `enableErrorTrackingAutoInstrumentation()`.\n //? Direct `setSentryUser` removed from this handler — see migration doc.\n const ctx: HttpApiPipelineContext = { buildNetworkError, user, preferredLocale };\n\n // Stage 1: validate request shape, parse route, resolve runtime maps\n const shapeResult = await validateHttpApiRequestShape(name, data, ctx);\n if ('status' in shapeResult) {\n return shapeResult;\n }\n const { resolvedName, requestData, apisObject, functionsObject } = shapeResult;\n\n const runtimeApiRoute = apisObject[resolvedName] as RuntimeApiEntry;\n formatterHolder.current = runtimeApiRoute.errorFormatter;\n\n // Stage 2: auth (login-required + additional predicates)\n //? Pipeline order: auth → rate-limit → method → validate → execute → respond.\n const authError = runHttpApiAuth({ name, resolvedName, requesterIp, auth: runtimeApiRoute.auth }, ctx);\n if (authError) {\n return authError;\n }\n\n // Stage 3: rate limiting (per-route + global-IP)\n const rateLimitError = await applyHttpApiRateLimits(\n { requesterIp, token, resolvedName, rateLimit: runtimeApiRoute.rateLimit },\n ctx,\n );\n if (rateLimitError) {\n return rateLimitError;\n }\n\n // Stage 4: HTTP method check (post-auth so unauthenticated probes don't learn the route exists)\n const methodError = checkHttpApiMethod(\n { method, resolvedName, declaredMethod: runtimeApiRoute.httpMethod },\n ctx,\n );\n if (methodError) {\n return methodError;\n }\n\n // Stage 5: input-type validation\n //? Input-type validation stage (preApiValidate -> validateInputByType ->\n //? postApiValidate). Extracted to `_shared/httpValidationStage.ts` for\n //? symmetry with the socket handler. On failure the caller builds the\n //? GENERIC `api.invalidInputType` (the raw validator message never reaches\n //? the client). Honors the per-route `validation: 'relaxed'` / `{ input:\n //? 'skip' }` escape hatch on BOTH transports — a public webhook route whose\n //? third-party payload can't be modeled in TS is reachable over HTTP, not\n //? just sockets.\n const httpValidation = await runHttpApiValidation({\n resolvedName,\n inputType: runtimeApiRoute.inputType,\n inputTypeFilePath: runtimeApiRoute.inputTypeFilePath,\n requestData,\n user,\n validation: runtimeApiRoute.validation,\n });\n if (!httpValidation.ok) {\n return buildNetworkError({\n response: {\n status: 'error',\n errorCode: 'api.invalidInputType',\n },\n fallbackHttpStatus: 400,\n });\n }\n\n // Stage 6: execute the API handler and assemble the response envelope\n return runHttpApiExecution(\n { resolvedName, requestData, functionsObject, runtimeApiRoute, effectiveAbortSignal, stream },\n ctx,\n );\n}\n","import type { BaseSessionLayout as SessionLayout } from '@luckystack/core';\nimport { dispatchHook, validateInputByType, getLogger } from '@luckystack/core';\nimport type { RuntimeApiEntry } from './apiTypes';\nimport { shouldLogDev } from './logFlags';\nimport { warnIfInputTypeMissing } from './inputTypeWarning';\nimport { resolveValidationMode } from './socketValidationStage';\n\n//? HTTP-transport input-validation stage. Mirrors the socket variant\n//? (`runSocketApiValidation`) for transport symmetry, extracted from\n//? `runHandleHttpApiRequestInner` per the `api` package audit. Behaviour:\n//? • resolves the per-route validation mode (`'relaxed'` / `{ input: 'skip' }`\n//? => skip Zod validation; default `'strict'`) — identical to the socket\n//? stage, so a public webhook route (Stripe/Slack/GitHub) whose third-party\n//? payload can't be modeled in TS is reachable over HTTP, not just sockets;\n//? • warns once if a strict-mode route has no generated input type;\n//? • dispatches `preApiValidate`, runs `validateInputByType` in strict mode,\n//? then `postApiValidate` (always, including the relaxed skip path);\n//? • on failure returns `{ ok: false }` (the caller builds the GENERIC\n//? `api.invalidInputType` network error — the raw validator message is\n//? NEVER echoed to the client; it only travels to the postApiValidate hook\n//? + dev logs).\n\nexport const runHttpApiValidation = async ({\n resolvedName,\n inputType,\n inputTypeFilePath,\n requestData,\n user,\n validation,\n}: {\n resolvedName: string;\n inputType?: string;\n inputTypeFilePath?: string;\n requestData: Record<string, unknown>;\n user: SessionLayout | null;\n validation: RuntimeApiEntry['validation'];\n}): Promise<{ ok: true } | { ok: false }> => {\n //? Per-route validation toggle, honored identically to the socket stage.\n const validationMode = resolveValidationMode(validation);\n\n // Input-type validation (post-auth so unauthenticated probes don't get input-shape leaks)\n if (validationMode === 'strict') {\n warnIfInputTypeMissing(resolvedName, inputType);\n }\n await dispatchHook('preApiValidate', { routeName: resolvedName, data: requestData, user, transport: 'http' });\n\n if (validationMode === 'relaxed') {\n //? Relaxed: surface the skip via postApiValidate so audit handlers see it.\n await dispatchHook('postApiValidate', {\n routeName: resolvedName,\n data: requestData,\n user,\n validation: { status: 'success' },\n transport: 'http',\n });\n return { ok: true };\n }\n\n const inputValidation = await validateInputByType({\n typeText: inputType,\n value: requestData,\n rootKey: 'data',\n filePath: inputTypeFilePath,\n });\n\n await dispatchHook('postApiValidate', {\n routeName: resolvedName,\n data: requestData,\n user,\n validation: inputValidation,\n transport: 'http',\n });\n\n if (inputValidation.status === 'error') {\n //? SECURITY: do NOT echo the raw validator message (e.g.\n //? \"data.userId should be string\") back to the client — that lets an\n //? unauthenticated caller enumerate a route's input schema. The DETAILED\n //? message is routed to the `postApiValidate` hook + dev logs above; the\n //? client only receives the generic `api.invalidInputType` code.\n if (shouldLogDev()) {\n getLogger().warn(`http-api: input validation failed for ${resolvedName}`, { route: resolvedName, message: inputValidation.message, transport: 'http' });\n }\n return { ok: false };\n }\n\n return { ok: true };\n};\n"],"mappings":";AACA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,oBAAAA;AAAA,EACA;AAAA,EACA;AAAA,EACA,kBAAAC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,gBAAAC;AAAA,EACA,aAAAC;AAAA,EACA;AAAA,EACA;AAAA,EACA,0BAAAC;AAAA,EACA;AAAA,OACK;;;ACnBP,SAAS,wBAAwB;AAO1B,IAAM,eAAe,MAAe,iBAAiB,EAAE,QAAQ;AAG/D,IAAM,kBAAkB,MAAe,iBAAiB,EAAE,QAAQ;;;ACTzE,SAAS,8BAA8B,wBAAwB,iBAAiB;AAWzE,IAAM,uBAAuB,CAAC;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAM2B;AACzB,MAAI,OAAO;AACT,QAAI,aAAa,GAAG;AAClB,gBAAU,EAAE,MAAM,iBAAiB,YAAY,IAAI,OAAO,EAAE,OAAO,aAAa,CAAC;AAAA,IACnF;AACA,WAAO;AAAA,MACL,GAAG,uBAAuB;AAAA,QACxB,UAAU,EAAE,QAAQ,SAAS,WAAW,0BAA0B;AAAA,QAClE;AAAA,QACA,cAAc,MAAM;AAAA,QACpB,oBAAoB;AAAA,MACtB,CAAC;AAAA,IACH;AAAA,EACF;AAEA,MAAI,WAAW,UAAa,WAAW,MAAM;AAC3C,QAAI,aAAa,GAAG;AAClB,gBAAU,EAAE,KAAK,QAAQ,YAAY,mBAAmB;AAAA,IAC1D;AACA,WAAO;AAAA,MACL,GAAG,uBAAuB;AAAA,QACxB,UAAU,EAAE,QAAQ,SAAS,WAAW,oBAAoB;AAAA,QAC5D;AAAA,QACA,cAAc,MAAM;AAAA,QACpB,oBAAoB;AAAA,MACtB,CAAC;AAAA,IACH;AAAA,EACF;AAEA,MAAI,aAAa,GAAG;AAClB,cAAU,EAAE,MAAM,QAAQ,YAAY,YAAY;AAAA,EACpD;AAKA,MAAI,OAAO,WAAW,aAAa,OAAO,WAAW,SAAS;AAC5D,WAAO;AAAA,MACL,GAAG,uBAAuB;AAAA,QACxB,UAAU,EAAE,QAAQ,SAAS,WAAW,4BAA4B;AAAA,QACpE;AAAA,QACA,cAAc,MAAM;AAAA,QACpB,oBAAoB;AAAA,MACtB,CAAC;AAAA,IACH;AAAA,EACF;AAEA,MAAI,OAAO,WAAW,SAAS;AAC7B,WAAO;AAAA,MACL,GAAG,uBAAuB;AAAA,QACxB,UAAU;AAAA,QACV;AAAA,QACA,cAAc,MAAM;AAAA,QACpB,oBAAoB,6BAA6B;AAAA,UAC/C,QAAQ;AAAA,UACR,oBAAoB,OAAO,OAAO,eAAe,WAAW,OAAO,aAAa;AAAA,QAClF,CAAC;AAAA,MACH,CAAC;AAAA,IACH;AAAA,EACF;AAOA,SAAO;AAAA,IACL,GAAG;AAAA,IACH,QAAQ;AAAA,IACR,YAAY,6BAA6B;AAAA,MACvC,QAAQ;AAAA,MACR,oBAAoB,OAAO,OAAO,eAAe,WAAW,OAAO,aAAa;AAAA,IAClF,CAAC;AAAA,EACH;AACF;;;ACjGA;AAAA,EACE;AAAA,EACA,aAAAC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;;;ACNP,SAAS,aAAAC,kBAAiB;AAcnB,IAAM,yBAAyB,CAAC,QAAgB,gBAA+C;AACpG,SAAO,OAAO,EAAE,eAAe,IAA6B,CAAC,MAAqB;AAChF,UAAM,0BAA0B,OAAO,mBAAmB,YAAY,iBAAiB,IACnF,iBACA;AACJ,UAAM,kBAAkB,KAAK,IAAI,GAAG,KAAK,KAAK,0BAA0B,IAAI,CAAC;AAI7E,WAAO,CAAC,YAAY,SAAS;AAO3B,YAAM,OAAQ,OAAkD;AAIhE,UAAI,aAAa,KAAK,CAAC,MAAM;AAC3B,QAAAC,WAAU,EAAE,KAAK,kGAA6F;AAAA,MAChH;AACA,YAAM,UAAU,MAAM,aAAa,UAAU;AAC7C,YAAM,WAAW,MAAM,WAAW,YAAY;AAC9C,UAAI,CAAC,SAAU;AACf,UAAI,UAAU,gBAAiB;AAC/B,YAAM,IAAI,QAAc,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AAAA,IAC9D;AAAA,EACF;AACF;AAKO,IAAM,2BAA6C,YAAY;AAEtE;;;ADlBO,IAAM,4BAA4B,CAAC,EAAE,QAAQ,eAAe,aAAa,MAIrD;AACzB,QAAM,kBAAkB,IAAI,gBAAgB;AAC5C,QAAM,WAAW,2BAA2B,OAAO,IAAI,eAAe,eAAe;AACrF,QAAM,qBAAqB,MAAM;AAAE,oBAAgB,MAAM;AAAA,EAAG;AAC5D,SAAO,KAAK,iBAAiB,YAAY,kBAAkB;AAE3D,MAAI,cAAc;AAClB,QAAM,iBAAiB,MAAM;AAC3B,QAAI,YAAa;AACjB,kBAAc;AACd,WAAO,IAAI,iBAAiB,YAAY,kBAAkB;AAC1D,iCAA6B,QAAQ;AAAA,EACvC;AAEA,QAAM,aAAa,CAAC,UAA4B,CAAC,MAAM;AACrD,QAAI,gBAAgB,OAAO,SAAS;AAClC,UAAI,gBAAgB,GAAG;AACrB,QAAAC,WAAU,EAAE,MAAM,QAAQ,YAAY,wCAAmC;AAAA,MAC3E;AACA;AAAA,IACF;AACA,QAAI,gBAAgB,GAAG;AACrB,MAAAA,WAAU,EAAE,MAAM,QAAQ,YAAY,WAAW,EAAE,QAAQ,CAAC;AAAA,IAC9D;AACA,WAAO,KAAK,wBAAwB,aAAa,GAAG,OAAO;AAAA,EAC7D;AAMA,QAAM,gBAAgB,uBAAuB,QAAQ,gBAAgB,MAAM;AAE3E,SAAO,EAAE,iBAAiB,aAAa,gBAAgB,QAAQ,YAAY,eAAe,eAAe;AAC3G;;;AEvEA,SAAS,cAAc,qBAAqB,aAAAC,kBAAiB;;;ACD7D,SAAS,oBAAAC,mBAAkB,aAAAC,kBAAiB;AAO5C,IAAM,yBAAyB,oBAAI,IAAY;AAExC,IAAM,yBAAyB,CAAC,cAAsB,cAAwC;AACnG,MAAI,CAACD,kBAAiB,EAAE,IAAI,uBAAwB;AACpD,MAAI,aAAa,UAAU,KAAK,EAAE,SAAS,KAAK,UAAU,KAAK,MAAM,MAAO;AAC5E,MAAI,uBAAuB,IAAI,YAAY,EAAG;AAC9C,yBAAuB,IAAI,YAAY;AACvC,EAAAC,WAAU,EAAE,KAAK,cAAc,YAAY,sHAAiH;AAC9J;;;ADMO,IAAM,wBAAwB,CAAC,eAAoE;AACxG,MAAI,CAAC,WAAY,QAAO;AAMxB,MAAI,OAAO,eAAe,UAAU;AAClC,WAAO,eAAe,YAAY,YAAY;AAAA,EAChD;AACA,SAAO,WAAW,UAAU,SAAS,YAAY;AACnD;AAEO,IAAM,yBAAyB,OAAO;AAAA,EAC3C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAOwB;AAKtB,QAAM,iBAAiB,sBAAsB,SAAS,UAAU;AAEhE,MAAI,mBAAmB,UAAU;AAC/B,2BAAuB,cAAc,SAAS,SAAS;AAAA,EACzD;AACA,QAAM,aAAa,kBAAkB,EAAE,WAAW,cAAc,MAAM,gBAAgB,MAAM,WAAW,SAAS,CAAC;AAEjH,MAAI,mBAAmB,UAAU;AAC/B,UAAM,kBAAkB,MAAM,oBAAoB;AAAA,MAChD,UAAU,SAAS;AAAA,MACnB,OAAO;AAAA,MACP,SAAS;AAAA,MACT,UAAU,SAAS;AAAA,IACrB,CAAC;AAED,UAAM,aAAa,mBAAmB;AAAA,MACpC,WAAW;AAAA,MACX,MAAM;AAAA,MACN;AAAA,MACA,YAAY;AAAA,MACZ,WAAW;AAAA,IACb,CAAC;AAED,QAAI,gBAAgB,WAAW,SAAS;AAMtC,UAAI,aAAa,GAAG;AAClB,QAAAC,WAAU,EAAE,KAAK,oCAAoC,YAAY,IAAI,EAAE,OAAO,cAAc,SAAS,gBAAgB,QAAQ,CAAC;AAAA,MAChI;AACA,qBAAe;AACf,YAAM,qBAAqB;AAC3B,aAAO;AAAA,IACT;AAAA,EACF,OAAO;AAEL,UAAM,aAAa,mBAAmB;AAAA,MACpC,WAAW;AAAA,MACX,MAAM;AAAA,MACN;AAAA,MACA,YAAY,EAAE,QAAQ,UAAU;AAAA,MAChC,WAAW;AAAA,IACb,CAAC;AAAA,EACH;AAEA,SAAO;AACT;;;AEpGA,SAAS,gBAAgB,oBAAAC,mBAAkB,gBAAAC,eAAc,aAAAC,kBAAiB;AAqCnE,IAAM,qBAAqB,OAAO;AAAA,EACvC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,qBAAqB;AACvB,MAAiE;AAC/D,QAAM,SAASC,kBAAiB;AAChC,QAAM,oBAAoB,aAAa,OAAO,aAAa;AAE3D,MAAI,sBAAsB,SAAS,oBAAoB,GAAG;AACxD,UAAM,aAAa,OAAO,aAAa;AACvC,UAAM,iBAAiB,aAAa,EAAE,WAAW,cAAc,QAAQ,MAAM,MAAM,MAAM,IAAI,YAAY,UAAU,CAAC,KAAK;AAKzH,UAAM,oBAAoB,gBAAgB,MAAM,MAAM,MAAM;AAC5D,UAAM,YAAY,gBAAgB,UAAU,MAAM,KAAK,SAAS;AAChE,UAAM,eAAe,GAAG,SAAS,IAAI,iBAAiB,QAAQ,YAAY;AAE1E,UAAM,EAAE,SAAS,QAAQ,IAAI,MAAM,eAAe;AAAA,MAChD,KAAK;AAAA,MACL,OAAO;AAAA,MACP,UAAU,OAAO,aAAa;AAAA,IAChC,CAAC;AAED,QAAI,CAAC,SAAS;AAMZ,WAAKC,cAAa,qBAAqB;AAAA,QACrC,OAAO,MAAM,KAAK,SAAS;AAAA,QAC3B,KAAK;AAAA,QACL,OAAO;AAAA,QACP,UAAU,OAAO,aAAa;AAAA,QAC9B,OAAO,oBAAoB;AAAA,QAC3B,OAAO;AAAA,QACP,QAAQ,MAAM;AAAA;AAAA,QAEd,IAAI,MAAM,KAAK,SAAY;AAAA,MAC7B,CAAC;AACD,UAAI,aAAa,GAAG;AAClB,QAAAC,WAAU,EAAE,KAAK,gCAAgC,YAAY,IAAI,EAAE,OAAO,cAAc,KAAK,cAAc,UAAU,CAAC;AAAA,MACxH;AACA,aAAO,EAAE,SAAS,OAAO,WAAW,yBAAyB,QAAQ;AAAA,IACvE;AAAA,EACF;AAEA,QAAM,iBAAiB,OAAO,aAAa;AAC3C,MAAI,CAAC,sBAAsB,mBAAmB,SAAS,iBAAiB,GAAG;AACzE,UAAM,EAAE,SAAS,QAAQ,IAAI,MAAM,eAAe;AAAA,MAChD,KAAK,MAAM,UAAU;AAAA,MACrB,OAAO;AAAA,MACP,UAAU,OAAO,aAAa;AAAA,IAChC,CAAC;AAED,QAAI,CAAC,SAAS;AACZ,WAAKD,cAAa,qBAAqB;AAAA,QACrC,OAAO;AAAA,QACP,KAAK,MAAM,UAAU;AAAA,QACrB,OAAO;AAAA,QACP,UAAU,OAAO,aAAa;AAAA,QAC9B,OAAO,iBAAiB;AAAA,QACxB,IAAI;AAAA,MACN,CAAC;AACD,UAAI,aAAa,GAAG;AAClB,QAAAC,WAAU,EAAE,KAAK,sCAAsC,EAAE,IAAI,YAAY,UAAU,CAAC;AAAA,MACtF;AACA,aAAO,EAAE,SAAS,OAAO,WAAW,yBAAyB,QAAQ;AAAA,IACvE;AAAA,EACF;AAEA,SAAO,EAAE,SAAS,KAAK;AACzB;;;APrEA,IAAM,qBAAqB,OAAO,EAAE,KAAK,aAAa,MAGiB;AACrE,QAAM,EAAE,MAAM,MAAM,cAAc,IAAI;AAOtC,MAAI,OAAO,kBAAkB,UAAU;AACrC,QAAI,aAAa,GAAG;AAClB,MAAAC,WAAU,EAAE,KAAK,8BAA8B;AAAA,IACjD;AACA,WAAO;AAAA,EACT;AAOA,MAAI,CAAC,QAAQ,CAAC,QAAQ,OAAO,QAAQ,YAAY,OAAO,QAAQ,YAAY,MAAM,QAAQ,IAAI,GAAG;AAC/F,UAAM,aAAa;AAAA,MACjB,UAAU,EAAE,QAAQ,SAAS,WAAW,qBAAqB;AAAA,MAC7D,oBAAoB;AAAA,IACtB,CAAC;AACD,WAAO;AAAA,EACT;AAEA,SAAO,EAAE,MAAM,KAAsC;AACvD;AAEA,IAAM,eAAe,OAAO,EAAE,UAAU,MAAM,MAAM,aAAa,MAKzC;AACtB,MAAI,SAAS,KAAK,SAAS,CAAC,MAAM,IAAI;AACpC,QAAI,aAAa,GAAG;AAClB,MAAAA,WAAU,EAAE,KAAK,QAAQ,IAAI,mBAAmB,EAAE,OAAO,KAAK,CAAC;AAAA,IACjE;AACA,SAAKC,cAAa,mBAAmB;AAAA,MACnC,WAAW;AAAA,MACX,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,WAAW;AAAA,IACb,CAAC;AACD,UAAM,aAAa;AAAA,MACjB,UAAU,EAAE,QAAQ,SAAS,WAAW,gBAAgB;AAAA,MACxD,oBAAoB;AAAA,IACtB,CAAC;AACD,WAAO;AAAA,EACT;AAGA,QAAM,aAAa,gBAAgB,EAAE,MAAM,SAAS,MAAM,KAAY,CAAC;AACvE,MAAI,WAAW,WAAW,SAAS;AACjC,QAAI,aAAa,GAAG;AAClB,MAAAD,WAAU,EAAE,KAAK,wBAAwB,IAAI,IAAI,EAAE,OAAO,MAAM,WAAW,WAAW,UAAU,CAAC;AAAA,IACnG;AACA,SAAKC,cAAa,mBAAmB;AAAA,MACnC,WAAW;AAAA,MACX,QAAQ,WAAW,cAAc,0BAA0B,sBAAsB;AAAA,MACjF,QAAQ,MAAM,MAAM;AAAA,MACpB,WAAW;AAAA,MACX,WAAW,WAAW;AAAA,IACxB,CAAC;AACD,UAAM,aAAa;AAAA,MACjB,UAAU;AAAA,QACR,QAAQ;AAAA;AAAA,QAER,WAAW,WAAW,aAAa;AAAA,QACnC,aAAa,WAAW;AAAA,QACxB,YAAY,WAAW;AAAA,MACzB;AAAA,MACA,oBAAoB,WAAW,cAAc;AAAA,IAC/C,CAAC;AACD,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAKA,IAAM,sBAAsB,OAAO,EAAE,UAAU,cAAc,OAAO,QAAQ,MAAM,aAAa,MAOvE;AAItB,QAAM,aAAa,gBAAgB;AAAA,IACjC,YAAY,OAAO,UAAU;AAAA,IAC7B,SAAS,OAAO,UAAU;AAAA,IAC1B,YAAYC,kBAAiB,EAAE,KAAK;AAAA,IACpC,sBAAsBA,kBAAiB,EAAE,KAAK;AAAA,EAChD,CAAC;AAED,QAAM,SAAS,MAAM,mBAAmB;AAAA,IACtC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW,SAAS;AAAA,IACpB,WAAW;AAAA,EACb,CAAC;AAED,MAAI,CAAC,OAAO,SAAS;AACnB,UAAM,aAAa;AAAA,MACjB,UAAU;AAAA,QACR,QAAQ;AAAA,QACR,WAAW,OAAO,aAAa;AAAA,QAC/B,aAAa,CAAC,EAAE,KAAK,WAAW,OAAO,OAAO,WAAW,EAAE,CAAC;AAAA,MAC9D;AAAA,MACA,oBAAoB;AAAA,IACtB,CAAC;AACD,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAEA,IAAM,oBAAoB,OAAO,EAAE,UAAU,gBAAgB,MAAM,iBAAiB,cAAc,YAAY,aAAa,iBAAiB,cAAc,MAUlC;AAItH,QAAM,eAAe,KAAK,IAAI;AAM9B,QAAM,YAAYA,kBAAiB,EAAE,IAAI;AACzC,MAAI;AACJ,MAAI,WAAW;AACf,QAAM,iBAAiB,cAAc,QACjC,OACA,IAAI,QAAc,CAAC,YAAY;AAC7B,oBAAgB,WAAW,MAAM;AAC/B,iBAAW;AACX,sBAAgB,MAAM;AACtB,cAAQ;AAAA,IACV,GAAG,SAAS;AAAA,EACd,CAAC;AAEL,QAAM,iBAAiB;AAAA,IACrB,YAAY,MAAM,SAAS,KAAK,EAAE,MAAM,gBAAgB,MAAM,WAAW,iBAAiB,QAAQ,YAAY,aAAa,cAAc,CAAC;AAAA,IAC1I;AAAA,IACA;AAAA,MACE,SAAS;AAAA,MACT,KAAK;AAAA,MACL,QAAQ,MAAM;AAAA,MACd,WAAW;AAAA,IACb;AAAA,EACF;AAEA,MAAI,gBAAgB;AAClB,UAAM,QAAQ,KAAK,CAAC,gBAAgB,cAAc,CAAC;AAAA,EACrD;AACA,eAAa,aAAa;AAG1B,MAAI,UAAU;AACZ,WAAO,EAAE,OAAO,MAAM,QAAQ,QAAW,YAAY,KAAK,IAAI,IAAI,cAAc,UAAU,KAAK;AAAA,EACjG;AAEA,QAAM,CAAC,OAAO,MAAM,IAAI,MAAM;AAC9B,SAAO,EAAE,OAAO,SAAS,MAAM,QAAQ,UAAU,QAAW,YAAY,KAAK,IAAI,IAAI,aAAa;AACpG;AAaA,IAAM,wBAAwB,OAAO;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAU0C;AAKxC,QAAM,iBAAwC;AAAA,IAC5C,WAAW;AAAA,IACX,MAAM;AAAA,IACN;AAAA,IACA,WAAW;AAAA,IACX,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,YAAY;AAAA,EACd;AACA,QAAM,mBAAmB,MAAMD,cAAa,iBAAiB,cAAc;AAC3E,MAAI,iBAAiB,SAAS;AAC5B,WAAO,EAAE,SAAS,MAAM,WAAW,iBAAiB,OAAO,WAAW,YAAY,iBAAiB,OAAO,WAAW;AAAA,EACvH;AAEA,QAAM,EAAE,OAAO,QAAQ,YAAY,SAAS,IAAI,MAAM,kBAAkB;AAAA,IACtE;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAID,MAAI,UAAU;AACZ,WAAO,EAAE,SAAS,MAAM,WAAW,eAAe,YAAY,IAAI;AAAA,EACpE;AAEA,iBAAe,SAAS;AACxB,iBAAe,QAAQ,SAAS;AAChC,iBAAe,aAAa;AAC5B,QAAMA,cAAa,kBAAkB,cAAc;AAEnD,SAAO,EAAE,SAAS,OAAO,OAAO,OAAO;AACzC;AAEA,IAAM,gBAAgB,OAAO;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MASqB;AACnB,QAAM,WAAW,qBAAqB,EAAE,cAAc,OAAO,QAAQ,iBAAiB,KAAK,CAAC;AAO5F,QAAM,aAAa,EAAE,WAAW,cAAc,MAAM,UAAU,SAAS;AACvE,QAAM,mBAAmB,MAAMA,cAAa,iBAAiB,UAAU;AAEvE,QAAM,gBAAgB,iBAAiB,UACnC,EAAE,GAAGE,wBAAuB;AAAA,IAC1B,UAAU,EAAE,QAAQ,SAAS,WAAW,iBAAiB,OAAO,UAAU;AAAA,IAC1E;AAAA,IACA,cAAc,MAAM;AAAA,IACpB,oBAAoB,iBAAiB,OAAO,cAAc;AAAA,EAC5D,CAAC,EAAE,IACH,WAAW;AAMf,QAAM,mBAAmB,EAAE,WAAW,cAAc,MAAM,UAAU,cAAc;AAClF,QAAMF,cAAa,wBAAwB,gBAAgB;AAK3D,QAAM,oBAAoB,oBAAoB;AAAA,IAC5C,UAAU,iBAAiB;AAAA,IAC3B,WAAW;AAAA,IACX,WAAW;AAAA,IACX,QAAQ,MAAM;AAAA,IACd;AAAA,EACF,CAAC;AAED,SAAO,KAAK,0BAA0B,aAAa,GAAG,iBAAiB;AAIvE,QAAMA,cAAa,kBAAkB,EAAE,WAAW,cAAc,MAAM,UAAU,kBAAkB,CAAC;AACrG;AAUA,eAAO,iBAAwC,MAA2C;AAOxF,QAAM,CAAC,KAAK,IAAI,MAAM,SAAS,MAAM,iCAAiC,MAAM,sBAAsB,IAAI,CAAC,CAAC;AACxG,MAAI,OAAO;AACT,IAAAD,WAAU,EAAE,MAAM,kDAAkD,EAAE,OAAO,MAAM,QAAQ,CAAC;AAC5F,UAAM,MAAM,KAAK;AACjB,UAAM,gBAAgB,KAAK;AAC3B,QAAI,OAAO,kBAAkB,UAAU;AACrC,WAAK,OAAO,KAAK,0BAA0B,aAAa,GAAG;AAAA,QACzD,QAAQ;AAAA,QACR,WAAW;AAAA,QACX,YAAY;AAAA,MACd,CAAC;AAAA,IACH;AAAA,EACF;AACF;AAEA,eAAe,sBAAsB,EAAE,KAAK,QAAQ,MAAM,GAAyB;AAYjF,QAAM,SAAkB;AACxB,MAAI,OAAO,WAAW,YAAY,WAAW,QAAQ,MAAM,QAAQ,MAAM,GAAG;AAC1E,QAAI,aAAa,GAAG;AAClB,MAAAA,WAAU,EAAE,KAAK,2CAA2C;AAAA,IAC9D;AACA;AAAA,EACF;AAEA,QAAM,EAAE,cAAc,IAAI;AAM1B,QAAM,eAAe,MAAMC,cAAa,oBAAoB;AAAA,IAC1D,SAAS;AAAA,IACT,UAAU,OAAO;AAAA,IACjB,IAAI,OAAO,UAAU;AAAA,IACrB,eAAe,QAAQ,KAAK;AAAA,IAC5B,WAAW,OAAO,IAAI,SAAS,WAAW,IAAI,OAAO;AAAA,EACvD,CAAC;AACD,MAAI,aAAa,SAAS;AAMxB,QAAI,OAAO,kBAAkB,UAAU;AACrC,aAAO,KAAK,0BAA0B,aAAa,GAAG;AAAA,QACpD,QAAQ;AAAA,QACR,WAAW,aAAa,OAAO;AAAA,QAC/B,YAAY,aAAa,OAAO,cAAc;AAAA,MAChD,CAAC;AAAA,IACH;AACA;AAAA,EACF;AAEA,QAAM,OAAO,MAAM,YAAY,KAAK;AAKpC,iCAA+B,MAAM,KAAK,EAAE,IAAI,KAAK,IAAI,OAAO,KAAK,SAAS,QAAW,UAAU,KAAK,QAAQ,OAAU,IAAI,IAAI;AAClI,QAAM,kBACJ,0BAA0B,OAAO,UAAU,QAAQ,YAAY,CAAC,KAC7D,0BAA0B,OAAO,UAAU,QAAQ,iBAAiB,CAAC,KACrE;AAQL,MAAI;AAEJ,MAAI;AASJ,QAAM,eAA6B,OAAO,EAAE,UAAU,mBAAmB,MAAM;AAC7E,UAAM,aAAaE,wBAAuB;AAAA,MACxC;AAAA,MACA;AAAA,MACA,cAAc,MAAM;AAAA,MACpB;AAAA,IACF,CAAC;AACD,UAAM,mBAAmB,oBAAoB;AAO7C,UAAM,aAAa,EAAE,WAAW,kBAAkB,MAAM,UAAU,WAAsG;AACxK,UAAM,mBAAmB,MAAMF,cAAa,iBAAiB,UAAU;AAEvE,QAAI,gBAAgB,WAAW;AAC/B,QAAI,iBAAiB,SAAS;AAC5B,YAAM,kBAAkBE,wBAAuB;AAAA,QAC7C,UAAU,EAAE,QAAQ,SAAS,WAAW,iBAAiB,OAAO,UAAU;AAAA,QAC1E;AAAA,QACA,cAAc,MAAM;AAAA,QACpB,oBAAoB,iBAAiB,OAAO,cAAc;AAAA,MAC5D,CAAC;AAGD,sBAAgB;AAAA,IAClB;AAEA,UAAM,mBAAmB,EAAE,WAAW,kBAAkB,MAAM,UAAU,cAAc;AACtF,UAAMF,cAAa,wBAAwB,gBAAgB;AAE3D,UAAM,YAAY,oBAAoB;AAAA,MACpC,UAAU,iBAAiB;AAAA,MAC3B,WAAW;AAAA,MACX,WAAW;AAAA,MACX,QAAQ,MAAM;AAAA,MACd,mBAAmB;AAAA,IACrB,CAAC;AACD,WAAO,KAAK,0BAA0B,aAAa,GAAG,SAAS;AAG/D,UAAMA,cAAa,kBAAkB,EAAE,WAAW,kBAAkB,MAAM,UAAU,UAAU,CAAC;AAAA,EACjG;AAEA,QAAM,YAAY,MAAM,mBAAmB,EAAE,KAAK,aAAa,CAAC;AAChE,MAAI,CAAC,UAAW;AAChB,QAAM,EAAE,MAAM,MAAM,eAAe,IAAI;AAEvC,QAAM,cAAc,wBAAwB,EAAE,OAAO,MAAM,QAAQ,MAAM,CAAC;AAC1E,MAAI,YAAY,WAAW,SAAS;AAClC,UAAM,aAAa;AAAA,MACjB,UAAU;AAAA,QACR,QAAQ;AAAA,QACR,WAAW;AAAA,QACX,aAAa,CAAC,EAAE,KAAK,QAAQ,OAAO,KAAK,CAAC;AAAA,MAC5C;AAAA,MACA,oBAAoB;AAAA,IACtB,CAAC;AACD;AAAA,EACF;AAEA,QAAM,eAAe,YAAY;AACjC,qBAAmB;AAKnB,MAAI,YAAY,aAAa,wBAAwB,iBAAiB;AAGpE,UAAM,gBAAgBC,kBAAiB,EAAE,aAAa;AACtD,QAAI,kBAAkB,SAAS,gBAAgB,GAAG;AAChD,YAAM,WAAW,gBAAgB;AAAA,QAC/B,YAAY,OAAO,UAAU;AAAA,QAC7B,SAAS,OAAO,UAAU;AAAA,QAC1B,YAAYA,kBAAiB,EAAE,KAAK;AAAA,QACpC,sBAAsBA,kBAAiB,EAAE,KAAK;AAAA,MAChD,CAAC;AACD,YAAM,EAAE,SAAS,QAAQ,IAAI,MAAME,gBAAe;AAAA,QAChD,KAAK,MAAM,QAAQ;AAAA,QACnB,OAAO;AAAA,QACP,UAAUF,kBAAiB,EAAE,aAAa;AAAA,MAC5C,CAAC;AACD,UAAI,CAAC,SAAS;AACZ,aAAKD,cAAa,qBAAqB;AAAA,UACrC,OAAO;AAAA,UACP,KAAK,MAAM,QAAQ;AAAA,UACnB,OAAO;AAAA,UACP,UAAUC,kBAAiB,EAAE,aAAa;AAAA,UAC1C,OAAO,gBAAgB;AAAA,UACvB,IAAI;AAAA,QACN,CAAC;AACD,cAAM,aAAa;AAAA,UACjB,UAAU;AAAA,YACR,QAAQ;AAAA,YACR,WAAW;AAAA,YACX,aAAa,CAAC,EAAE,KAAK,WAAW,OAAO,QAAQ,CAAC;AAAA,UAClD;AAAA,UACA,oBAAoB;AAAA,QACtB,CAAC;AACD;AAAA,MACF;AAAA,IACF;AAKA,UAAM,cAAc,EAAE,OAAO,QAAQ,QAAQ,MAAM,MAAM,KAAK,CAAC;AAC/D,UAAM,cAAc;AAAA,MAClB;AAAA,MACA;AAAA,MACA;AAAA,MACA,OAAO;AAAA,MACP,QAAQ,EAAE,QAAQ,WAAW,YAAY,KAAK,QAAQ,KAAK;AAAA,MAC3D;AAAA,MACA;AAAA,IACF,CAAC;AACD;AAAA,EACF;AAEA,MAAI,aAAa,GAAG;AAClB,IAAAF,WAAU,EAAE,MAAM,QAAQ,YAAY,SAAS;AAAA,EACjD;AAEA,QAAM,EAAE,YAAY,gBAAgB,IAAI,MAAM,kBAAkB;AAEhE,MAAI,CAAC,WAAW,YAAY,GAAG;AAC7B,UAAM,aAAa;AAAA,MACjB,UAAU;AAAA,QACR,QAAQ;AAAA,QACR,WAAW;AAAA,QACX,aAAa,CAAC,EAAE,KAAK,QAAQ,OAAO,KAAK,CAAC;AAAA,MAC5C;AAAA,MACA,oBAAoB;AAAA,IACtB,CAAC;AACD;AAAA,EACF;AAEA,QAAM,WAAW,WAAW,YAAY;AACxC,6BAA2B,SAAS;AAcpC,QAAM,EAAE,iBAAiB,aAAa,YAAY,eAAe,eAAe,IAAI,0BAA0B;AAAA,IAC5G;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAYD,MAAI,CAAE,MAAM,aAAa,EAAE,UAAU,MAAM,MAAM,aAAa,CAAC,GAAI;AAAE,mBAAe;AAAG;AAAA,EAAQ;AAE/F,QAAM,cAAc,MAAM,oBAAoB,EAAE,UAAU,cAAc,OAAO,QAAQ,MAAM,aAAa,CAAC;AAC3G,MAAI,CAAC,aAAa;AAAE,mBAAe;AAAG;AAAA,EAAQ;AAM9C,QAAM,eAAe,MAAM,uBAAuB;AAAA,IAChD;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,sBAAsB,YAAY;AAAE,YAAM,aAAa;AAAA,QACrD,UAAU,EAAE,QAAQ,SAAS,WAAW,uBAAuB;AAAA,QAC/D,oBAAoB;AAAA,MACtB,CAAC;AAAA,IAAG;AAAA,EACN,CAAC;AACD,MAAI,CAAC,aAAc;AAInB,QAAM,YAAY,MAAM,sBAAsB;AAAA,IAC5C;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AACD,MAAI,UAAU,SAAS;AACrB,mBAAe;AACf,UAAM,aAAa;AAAA,MACjB,UAAU,EAAE,QAAQ,SAAS,WAAW,UAAU,UAAU;AAAA,MAC5D,oBAAoB,UAAU,cAAc;AAAA,IAC9C,CAAC;AACD;AAAA,EACF;AAEA,QAAM,cAAc,EAAE,QAAQ,eAAe,cAAc,OAAO,UAAU,OAAO,QAAQ,UAAU,QAAQ,iBAAiB,MAAM,mBAAmB,SAAS,eAAe,CAAC;AAChL,iBAAe;AACjB;;;AQzrBA;AAAA,EAAS,oBAAAK;AAAA,EAAkB,eAAAC;AAAA,EAAa,qBAAqB;AAAA,EAC3D,oCAAAC;AAAA,EACA,kCAAAC;AAAA,EACA,mBAAAC;AAAA,EACA;AAAA,EAEA,YAAAC;AAAA,EACA,2BAAAC;AAAA,EACA,gBAAAC;AAAA,EACA,aAAAC;AAAA,EAAW,6BAAAC;AAAA,EAA2B,0BAAAC;AAAA,EAAwB,uBAAAC;AAAA,EAC9D;AAAA,EAAc,mBAAAC;AAAA,EAAiB;AAAA,OAA2B;;;ACV5D,SAAS,gBAAAC,eAAc,uBAAAC,sBAAqB,aAAAC,kBAAiB;AAqBtD,IAAM,uBAAuB,OAAO;AAAA,EACzC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAO6C;AAE3C,QAAM,iBAAiB,sBAAsB,UAAU;AAGvD,MAAI,mBAAmB,UAAU;AAC/B,2BAAuB,cAAc,SAAS;AAAA,EAChD;AACA,QAAMC,cAAa,kBAAkB,EAAE,WAAW,cAAc,MAAM,aAAa,MAAM,WAAW,OAAO,CAAC;AAE5G,MAAI,mBAAmB,WAAW;AAEhC,UAAMA,cAAa,mBAAmB;AAAA,MACpC,WAAW;AAAA,MACX,MAAM;AAAA,MACN;AAAA,MACA,YAAY,EAAE,QAAQ,UAAU;AAAA,MAChC,WAAW;AAAA,IACb,CAAC;AACD,WAAO,EAAE,IAAI,KAAK;AAAA,EACpB;AAEA,QAAM,kBAAkB,MAAMC,qBAAoB;AAAA,IAChD,UAAU;AAAA,IACV,OAAO;AAAA,IACP,SAAS;AAAA,IACT,UAAU;AAAA,EACZ,CAAC;AAED,QAAMD,cAAa,mBAAmB;AAAA,IACpC,WAAW;AAAA,IACX,MAAM;AAAA,IACN;AAAA,IACA,YAAY;AAAA,IACZ,WAAW;AAAA,EACb,CAAC;AAED,MAAI,gBAAgB,WAAW,SAAS;AAMtC,QAAI,aAAa,GAAG;AAClB,MAAAE,WAAU,EAAE,KAAK,yCAAyC,YAAY,IAAI,EAAE,OAAO,cAAc,SAAS,gBAAgB,SAAS,WAAW,OAAO,CAAC;AAAA,IACxJ;AACA,WAAO,EAAE,IAAI,MAAM;AAAA,EACrB;AAEA,SAAO,EAAE,IAAI,KAAK;AACpB;;;ADEA,eAAsB,qBAAqB,QAA2D;AACpG,SAAOC,kCAAiC,MAAM,2BAA2B,MAAM,CAAC;AAClF;AAEA,eAAe,2BAA2B,QAA2D;AACnG,QAAM,EAAE,UAAU,MAAM,iBAAiB,kBAAkB,IAAI,MAAM,wBAAwB,MAAM;AAInG,QAAM,mBAAmB,OAAO,KAAK,WAAW,MAAM,IAAI,OAAO,OAAO,OAAO,OAAO,IAAI;AAE1F,QAAM,aAAa,EAAE,WAAW,kBAAkB,MAAM,SAAmG;AAC3J,QAAM,mBAAmB,MAAMC,cAAa,iBAAiB,UAAU;AAKvE,QAAM,gBAAgB,iBAAiB,UACnCC,wBAAuB;AAAA,IACrB,UAAU,EAAE,QAAQ,SAAS,WAAW,iBAAiB,OAAO,UAAU;AAAA,IAC1E;AAAA,IACA,cAAc,MAAM;AAAA,IACpB,oBAAoB,iBAAiB,OAAO,cAAc;AAAA,EAC5D,CAAC,IACD,WAAW;AAOf,QAAM,mBAAmB,EAAE,WAAW,kBAAkB,MAAM,UAAU,cAAc;AACtF,QAAMD,cAAa,wBAAwB,gBAAgB;AAW3D,QAAM,iBAAiB,iBAAiB;AACxC,QAAM,oBAAoBE,qBAAoB;AAAA,IAC5C,UAAU;AAAA,IACV,WAAW;AAAA,IACX,WAAW;AAAA,IACX,QAAQ,MAAM;AAAA,IACd;AAAA,EACF,CAAC;AAED,QAAMF,cAAa,kBAAkB,EAAE,WAAW,kBAAkB,MAAM,UAAU,kBAAkB,CAAC;AAEvG,SAAO;AACT;AASA,eAAe,wBAAwB,QAAsE;AAC3G,QAAM,kBACJG,2BAA0B,OAAO,eAAe,KAC7CA,2BAA0B,OAAO,oBAAoB;AAC1D,QAAM,OAAO,MAAMC,aAAY,OAAO,KAAK;AAK3C,EAAAC,gCAA+B,MAAM,KAAK,EAAE,IAAI,KAAK,IAAI,OAAO,KAAK,SAAS,QAAW,UAAU,KAAK,QAAQ,OAAU,IAAI,IAAI;AAClI,QAAM,kBAAgD,CAAC;AACvD,QAAM,WAAW,MAAM,6BAA6B,QAAQ,MAAM,iBAAiB,eAAe;AAClG,SAAO,EAAE,UAAU,MAAM,iBAAiB,mBAAmB,gBAAgB,QAAQ;AACvF;AAsBA,eAAe,4BACb,MACA,MACA,KASA;AAEA,MAAI,CAAC,QAAQ,OAAO,SAAS,UAAU;AACrC,WAAO,IAAI,kBAAkB;AAAA,MAC3B,UAAU,EAAE,QAAQ,SAAS,WAAW,kBAAkB;AAAA,MAC1D,oBAAoB;AAAA,IACtB,CAAC;AAAA,EACH;AAMA,MAAI,OAAO,SAAS,YAAY,SAAS,QAAQ,MAAM,QAAQ,IAAI,GAAG;AACpE,WAAO,IAAI,kBAAkB;AAAA,MAC3B,UAAU,EAAE,QAAQ,SAAS,WAAW,wBAAwB;AAAA,MAChE,oBAAoB;AAAA,IACtB,CAAC;AAAA,EACH;AAEA,QAAM,iBAAiB,KAAK,WAAW,MAAM,IAAI,OAAO,OAAO,IAAI;AACnE,QAAM,cAAcC,yBAAwB,EAAE,OAAO,gBAAgB,QAAQ,MAAM,CAAC;AACpF,MAAI,YAAY,WAAW,SAAS;AAClC,WAAO,IAAI,kBAAkB;AAAA,MAC3B,UAAU;AAAA,QACR,QAAQ;AAAA,QACR,WAAW;AAAA,QACX,aAAa,CAAC,EAAE,KAAK,QAAQ,OAAO,eAAe,CAAC;AAAA,MACtD;AAAA,MACA,oBAAoB;AAAA,IACtB,CAAC;AAAA,EACH;AAEA,QAAM,eAAe,YAAY;AAEjC,MAAI,aAAa,GAAG;AAClB,IAAAC,WAAU,EAAE,MAAM,aAAa,YAAY,SAAS;AAAA,EACtD;AAEA,QAAM,EAAE,YAAY,gBAAgB,IAAI,MAAM,4BAA4B;AAE1E,MAAI,CAAC,WAAW,YAAY,GAAG;AAC7B,WAAO,IAAI,kBAAkB;AAAA,MAC3B,UAAU;AAAA,QACR,QAAQ;AAAA,QACR,WAAW;AAAA,QACX,aAAa,CAAC,EAAE,KAAK,QAAQ,OAAO,aAAa,CAAC;AAAA,MACpD;AAAA,MACA,oBAAoB;AAAA,IACtB,CAAC;AAAA,EACH;AAEA,SAAO,EAAE,cAAc,aAAa,MAAM,YAAY,gBAAgB;AACxE;AAeA,SAAS,eACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMA,KAC2B;AAC3B,MAAI,KAAK,SAAS,CAAC,IAAI,MAAM,IAAI;AAC/B,QAAI,aAAa,GAAG;AAClB,MAAAA,WAAU,EAAE,KAAK,aAAa,IAAI,mBAAmB,EAAE,OAAO,MAAM,WAAW,OAAO,CAAC;AAAA,IACzF;AACA,SAAKP,cAAa,mBAAmB;AAAA,MACnC,WAAW;AAAA,MACX,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,WAAW;AAAA,MACX,IAAI;AAAA,IACN,CAAC;AACD,WAAO,IAAI,kBAAkB;AAAA,MAC3B,UAAU,EAAE,QAAQ,SAAS,WAAW,gBAAgB;AAAA,MACxD,oBAAoB;AAAA,IACtB,CAAC;AAAA,EACH;AAaA,QAAM,aAAaQ,iBAAgB,EAAE,MAAM,MAAM,IAAI,KAAM,CAAC;AAC5D,MAAI,WAAW,WAAW,SAAS;AACjC,QAAI,aAAa,GAAG;AAClB,MAAAD,WAAU,EAAE,KAAK,6BAA6B,IAAI,IAAI,EAAE,OAAO,MAAM,WAAW,WAAW,WAAW,WAAW,OAAO,CAAC;AAAA,IAC3H;AACA,SAAKP,cAAa,mBAAmB;AAAA,MACnC,WAAW;AAAA,MACX,QAAQ,WAAW,cAAc,0BAA0B,sBAAsB;AAAA,MACjF,QAAQ,IAAI,MAAM,MAAM;AAAA,MACxB,WAAW;AAAA,MACX,IAAI;AAAA,MACJ,WAAW,WAAW;AAAA,IACxB,CAAC;AACD,WAAO,IAAI,kBAAkB;AAAA,MAC3B,UAAU;AAAA,QACR,QAAQ;AAAA,QACR,WAAW,WAAW,aAAa;AAAA,QACnC,aAAa,WAAW;AAAA,QACxB,YAAY,WAAW;AAAA,MACzB;AAAA,MACA,oBAAoB,WAAW,cAAc;AAAA,IAC/C,CAAC;AAAA,EACH;AAEA,SAAO;AACT;AAOA,eAAe,uBACb;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMA,KACoC;AAKpC,QAAM,cAAc,eACfS,iBAAgB,EAAE,YAAY,QAAW,SAAS,CAAC,EAAE,CAAC;AAO3D,QAAM,0BAA0BC,kBAAiB,EAAE,aAAa,qBAC3D,CAAC,oBAAoB,KACrB,aAAa,eAAe,EAAE;AAEnC,QAAM,kBAAkB,MAAM,mBAAmB;AAAA,IAC/C,YAAY;AAAA,IACZ;AAAA,IACA,MAAM,IAAI;AAAA,IACV;AAAA,IACA;AAAA,IACA,WAAW;AAAA,IACX,oBAAoB;AAAA,EACtB,CAAC;AAED,MAAI,CAAC,gBAAgB,SAAS;AAC5B,WAAO,IAAI,kBAAkB;AAAA,MAC3B,UAAU;AAAA,QACR,QAAQ;AAAA,QACR,WAAW,gBAAgB,aAAa;AAAA,QACxC,aAAa,CAAC,EAAE,KAAK,WAAW,OAAO,gBAAgB,WAAW,EAAE,CAAC;AAAA,MACvE;AAAA,MACA,oBAAoB;AAAA,IACtB,CAAC;AAAA,EACH;AAEA,SAAO;AACT;AAOA,SAAS,mBACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AACF,GAKA,KAC2B;AAC3B,QAAM,iBAAiB,kBAAkB,gBAAgB,YAAY;AACrE,MAAI,WAAW,gBAAgB;AAC7B,QAAI,aAAa,GAAG;AAClB,MAAAH,WAAU,EAAE,KAAK,iCAAiC,YAAY,IAAI,EAAE,UAAU,gBAAgB,KAAK,OAAO,CAAC;AAAA,IAC7G;AACA,WAAO,IAAI,kBAAkB;AAAA,MAC3B,UAAU;AAAA,QACR,QAAQ;AAAA,QACR,WAAW;AAAA,QACX,aAAa,CAAC,EAAE,KAAK,UAAU,OAAO,eAAe,CAAC;AAAA,MACxD;AAAA,MACA,oBAAoB;AAAA,IACtB,CAAC;AAAA,EACH;AAEA,SAAO;AACT;AAOA,eAAe,oBACb;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAQA,KAC6B;AAG7B,QAAM,gBAAgB;AAEtB,QAAM,gBAAgB,CAAC,UAA4B,CAAC,MAAM;AACxD,QAAI,CAAC,QAAQ;AACX;AAAA,IACF;AACA,QAAI,qBAAqB,SAAS;AAChC,UAAI,gBAAgB,GAAG;AACrB,QAAAA,WAAU,EAAE,MAAM,aAAa,YAAY,wCAAmC;AAAA,MAChF;AACA;AAAA,IACF;AAEA,QAAI,gBAAgB,GAAG;AACrB,MAAAA,WAAU,EAAE,MAAM,aAAa,YAAY,WAAW,EAAE,QAAQ,CAAC;AAAA,IACnE;AAEA,WAAO,OAAO;AAAA,EAChB;AAMA,QAAM,iBAAwC;AAAA,IAC5C,WAAW;AAAA,IACX,MAAM;AAAA,IACN,MAAM,IAAI;AAAA,IACV,WAAW;AAAA,IACX,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,YAAY;AAAA,EACd;AACA,QAAM,mBAAmB,MAAMP,cAAa,iBAAiB,cAAc;AAC3E,MAAI,iBAAiB,SAAS;AAC5B,WAAO,IAAI,kBAAkB;AAAA,MAC3B,UAAU,EAAE,QAAQ,SAAS,WAAW,iBAAiB,OAAO,UAAU;AAAA,MAC1E,oBAAoB,iBAAiB,OAAO,cAAc;AAAA,IAC5D,CAAC;AAAA,EACH;AAYA,QAAM,YAAY,SAAS,QAAQU,kBAAiB,EAAE,IAAI;AAC1D,QAAM,qBAAqB,IAAI,gBAAgB;AAE/C,MAAI,qBAAqB,SAAS;AAChC,uBAAmB,MAAM;AAAA,EAC3B,OAAO;AACL,yBAAqB,iBAAiB,SAAS,MAAM;AAAE,yBAAmB,MAAM;AAAA,IAAG,GAAG,EAAE,MAAM,KAAK,CAAC;AAAA,EACtG;AAEA,MAAI,WAAW;AACf,MAAI;AACJ,QAAM,iBAAiB,cAAc,QACjC,OACA,IAAI,QAAc,CAAC,YAAY;AAC7B,oBAAgB,WAAW,MAAM;AAC/B,iBAAW;AACX,yBAAmB,MAAM;AACzB,cAAQ;AAAA,IACV,GAAG,SAAS;AAAA,EACd,CAAC;AAEL,QAAM,eAAe,KAAK,IAAI;AAC9B,QAAM,iBAAiBC;AAAA,IACrB,YAAY,MAAM,gBAAgB,KAAK;AAAA,MACrC,MAAM;AAAA,MACN,MAAM,IAAI;AAAA,MACV,WAAW;AAAA,MACX,QAAQ;AAAA,MACR,aAAa,mBAAmB;AAAA,MAChC;AAAA,IACF,CAAC;AAAA,IACD;AAAA,IACA;AAAA,MACE,SAAS;AAAA,MACT,KAAK;AAAA,MACL,QAAQ,IAAI,MAAM;AAAA,MAClB,WAAW;AAAA,IACb;AAAA,EACF;AAEA,MAAI,gBAAgB;AAClB,UAAM,QAAQ,KAAK,CAAC,gBAAgB,cAAc,CAAC;AAAA,EACrD;AACA,eAAa,aAAa;AAG1B,MAAI,UAAU;AACZ,WAAO,IAAI,kBAAkB;AAAA,MAC3B,UAAU,EAAE,QAAQ,SAAS,WAAW,cAAc;AAAA,MACtD,oBAAoB;AAAA,IACtB,CAAC;AAAA,EACH;AAEA,QAAM,CAAC,OAAO,MAAM,IAAI,MAAM;AAC9B,iBAAe,SAAS;AACxB,iBAAe,QAAQ,SAAS;AAChC,iBAAe,aAAa,KAAK,IAAI,IAAI;AACzC,QAAMX,cAAa,kBAAkB,cAAc;AAOnD,QAAM,WAAW,qBAAqB;AAAA,IACpC;AAAA,IACA,OAAO,SAAS;AAAA,IAChB,QAAQ,UAAU;AAAA,IAClB,iBAAiB,IAAI;AAAA,IACrB,MAAM,IAAI;AAAA,EACZ,CAAC;AACD,SAAO;AACT;AAEA,eAAe,6BACb;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,iBAAiB;AAAA,EACjB,sBAAsB;AAAA,EACtB,SAAS;AAAA,EACT;AAAA,EACA;AACF,GACA,MACA,iBACA,iBAC6B;AAK7B,QAAM,uBAAuB,eAAe,IAAI,gBAAgB,EAAE;AAElE,QAAM,oBAAoB,CAAC;AAAA,IACzB;AAAA,IACA;AAAA,EACF,MAG0B;AACxB,WAAOC,wBAAuB;AAAA,MAC5B;AAAA,MACA;AAAA,MACA,cAAc,MAAM;AAAA,MACpB;AAAA,IACF,CAAC;AAAA,EACH;AAKA,QAAM,MAA8B,EAAE,mBAAmB,MAAM,gBAAgB;AAG/E,QAAM,cAAc,MAAM,4BAA4B,MAAM,MAAM,GAAG;AACrE,MAAI,YAAY,aAAa;AAC3B,WAAO;AAAA,EACT;AACA,QAAM,EAAE,cAAc,aAAa,YAAY,gBAAgB,IAAI;AAEnE,QAAM,kBAAkB,WAAW,YAAY;AAC/C,kBAAgB,UAAU,gBAAgB;AAI1C,QAAM,YAAY,eAAe,EAAE,MAAM,cAAc,aAAa,MAAM,gBAAgB,KAAK,GAAG,GAAG;AACrG,MAAI,WAAW;AACb,WAAO;AAAA,EACT;AAGA,QAAM,iBAAiB,MAAM;AAAA,IAC3B,EAAE,aAAa,OAAO,cAAc,WAAW,gBAAgB,UAAU;AAAA,IACzE;AAAA,EACF;AACA,MAAI,gBAAgB;AAClB,WAAO;AAAA,EACT;AAGA,QAAM,cAAc;AAAA,IAClB,EAAE,QAAQ,cAAc,gBAAgB,gBAAgB,WAAW;AAAA,IACnE;AAAA,EACF;AACA,MAAI,aAAa;AACf,WAAO;AAAA,EACT;AAWA,QAAM,iBAAiB,MAAM,qBAAqB;AAAA,IAChD;AAAA,IACA,WAAW,gBAAgB;AAAA,IAC3B,mBAAmB,gBAAgB;AAAA,IACnC;AAAA,IACA;AAAA,IACA,YAAY,gBAAgB;AAAA,EAC9B,CAAC;AACD,MAAI,CAAC,eAAe,IAAI;AACtB,WAAO,kBAAkB;AAAA,MACvB,UAAU;AAAA,QACR,QAAQ;AAAA,QACR,WAAW;AAAA,MACb;AAAA,MACA,oBAAoB;AAAA,IACtB,CAAC;AAAA,EACH;AAGA,SAAO;AAAA,IACL,EAAE,cAAc,aAAa,iBAAiB,iBAAiB,sBAAsB,OAAO;AAAA,IAC5F;AAAA,EACF;AACF;","names":["getProjectConfig","checkRateLimit","dispatchHook","getLogger","normalizeErrorResponse","getLogger","getLogger","getLogger","getLogger","getLogger","getProjectConfig","getLogger","getLogger","getProjectConfig","dispatchHook","getLogger","getProjectConfig","dispatchHook","getLogger","getLogger","dispatchHook","getProjectConfig","normalizeErrorResponse","checkRateLimit","getProjectConfig","readSession","runWithErrorTrackerIdentityScope","setCurrentErrorTrackerIdentity","validateRequest","tryCatch","parseTransportRouteName","dispatchHook","getLogger","extractLanguageFromHeader","normalizeErrorResponse","applyErrorFormatter","resolveClientIp","dispatchHook","validateInputByType","getLogger","dispatchHook","validateInputByType","getLogger","runWithErrorTrackerIdentityScope","dispatchHook","normalizeErrorResponse","applyErrorFormatter","extractLanguageFromHeader","readSession","setCurrentErrorTrackerIdentity","parseTransportRouteName","getLogger","validateRequest","resolveClientIp","getProjectConfig","tryCatch"]}
|
|
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\n//? `resolvedName` (not the raw message name) keys the apiAuthRejected hook, so\n//? spelling variants of one route ('admin/x/v1', ' api/admin/x/v1/') land in the\n//? same consumer throttle key — parity with the HTTP transport.\nconst checkApiAuth = async ({ apiEntry, user, resolvedName, emitApiError }: {\n apiEntry: RuntimeApiEntry;\n user: SessionLayout | null;\n resolvedName: string;\n emitApiError: EmitApiError;\n}): Promise<boolean> => {\n if (apiEntry.auth.login && !user?.id) {\n if (shouldLogDev()) {\n getLogger().warn(`api: ${resolvedName} requires login`, { route: resolvedName });\n }\n void dispatchHook('apiAuthRejected', {\n routeName: resolvedName,\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 ${resolvedName}`, { route: resolvedName, errorCode: authResult.errorCode });\n }\n void dispatchHook('apiAuthRejected', {\n routeName: resolvedName,\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 //? Only a positive number arms the timer: `0` / negative mean \"disabled\",\n //? exactly as the client's `apiRequest` timeout reads the same config key.\n const timeoutPromise = typeof timeoutMs === 'number' && timeoutMs > 0\n ? new Promise<void>((resolve) => {\n timeoutHandle = setTimeout(() => {\n timedOut = true;\n abortController.abort();\n resolve();\n }, timeoutMs);\n })\n : null;\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, transport: 'socket' as const, 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, transport: 'socket' as const, 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, transport: 'socket', 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, transport: 'socket' as const, 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, transport: 'socket' as const, 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, transport: 'socket', 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, resolvedName, 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 //? preApiValidate stop signal — same envelope + 403 default as preApiExecute.\n emitValidationStopped: async ({ errorCode, httpStatus }) => { await emitApiError({\n response: { status: 'error', errorCode },\n fallbackHttpStatus: httpStatus ?? 403,\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` (a stop signal vetoes the request before\n//? validation, like `preSyncValidate`), runs `validateInputByType` in strict\n//? mode, 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>;\ntype EmitValidationStopped = (signal: { errorCode: string; httpStatus?: number }) => 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 emitValidationStopped,\n}: {\n apiEntry: RuntimeApiEntry;\n resolvedName: string;\n normalizedData: Record<string, unknown>;\n user: SessionLayout | null;\n cleanupRequest: () => void;\n emitInvalidInputType: EmitInvalidInputType;\n emitValidationStopped: EmitValidationStopped;\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 const preValidateResult = await dispatchHook('preApiValidate', { routeName: resolvedName, data: normalizedData, user, transport: 'socket' });\n //? A stop signal vetoes the request before validation and execution (the\n //? HookResult contract, mirrored by `preSyncValidate`). postApiValidate does\n //? not fire: no validation ran.\n if (preValidateResult.stopped) {\n cleanupRequest();\n await emitValidationStopped({ errorCode: preValidateResult.signal.errorCode, httpStatus: preValidateResult.signal.httpStatus });\n return false;\n }\n\n if (validationMode === 'strict') {\n const inputValidation = await validateInputByType({\n typeText: apiEntry.inputType,\n value: normalizedData,\n rootKey: 'data',\n filePath: apiEntry.inputTypeFilePath,\n });\n\n await dispatchHook('postApiValidate', {\n routeName: resolvedName,\n data: normalizedData,\n user,\n validation: inputValidation,\n transport: 'socket',\n });\n\n if (inputValidation.status === 'error') {\n //? SECURITY: do NOT echo the raw validator message (e.g.\n //? \"data.userId should be string\") back to the client — that lets an\n //? unauthenticated caller enumerate a route's input schema. The DETAILED\n //? message is routed to the `postApiValidate` hook above (and dev logs);\n //? the client only receives the generic `api.invalidInputType` code.\n if (shouldLogDev()) {\n getLogger().warn(`api: input validation failed for ${resolvedName}`, { route: resolvedName, message: inputValidation.message });\n }\n cleanupRequest();\n await emitInvalidInputType();\n return false;\n }\n } else {\n //? Relaxed: surface the skip via postApiValidate so audit handlers see it.\n await dispatchHook('postApiValidate', {\n routeName: resolvedName,\n data: normalizedData,\n user,\n validation: { status: 'success' },\n transport: 'socket',\n });\n }\n\n return true;\n};\n","import { getProjectConfig, getLogger } from '@luckystack/core';\n\n//? Dev-only one-shot warning when a route has no generated `inputType`\n//? (runtime Zod input validation is effectively disabled for it). Shared by\n//? both API transports so the \"warned once per route\" set is process-wide\n//? regardless of which transport hit the route first.\n\nconst warnedMissingInputType = new Set<string>();\n\nexport const warnIfInputTypeMissing = (resolvedName: string, inputType: string | undefined): void => {\n if (!getProjectConfig().dev.warnOnMissingInputType) return;\n if (inputType && inputType.trim().length > 0 && inputType.trim() !== 'any') return;\n if (warnedMissingInputType.has(resolvedName)) return;\n warnedMissingInputType.add(resolvedName);\n getLogger().warn(`api: route ${resolvedName} has no inputType — runtime input validation is disabled. Regenerate types or set the inputType on the handler.`);\n};\n","import type { BaseSessionLayout as SessionLayout } from '@luckystack/core';\nimport { checkRateLimit, getProjectConfig, dispatchHook, getLogger } from '@luckystack/core';\nimport { shouldLogDev } from './logFlags';\n\n//? API-O8 — shared rate-limit apply helper for BOTH API transports (socket +\n//? HTTP). Extracted so the two-bucket logic (per-route + global IP) stays in\n//? one place; each transport resolves the effective IP before calling here.\n\nexport interface ApplyApiRateLimitsArgs {\n /** Effective client IP (already resolved by the caller's transport adapter). */\n resolvedIp: string;\n token: string | null;\n user: SessionLayout | null;\n resolvedName: string;\n rateLimit: number | false | undefined;\n transport: 'socket' | 'http';\n /**\n * When true, skip the global-IP (`ip:…:api:all`) bucket check. Used by the\n * HTTP transport to honour `rateLimiting.skipLoopbackInDev` for loopback\n * callers without duplicating bucket logic here. The socket transport never\n * sets this (loopback skip is HTTP-only).\n */\n skipGlobalIpBucket?: boolean;\n}\n\nexport interface ApplyApiRateLimitsResult {\n /** True when all buckets allow the request; false when any bucket exceeded. */\n allowed: boolean;\n /** The error payload to emit when `allowed` is false. */\n errorCode?: string;\n resetIn?: number;\n}\n\n/**\n * Checks per-route + global-IP rate-limit buckets for an API request.\n * Returns `{ allowed: true }` when all buckets allow through, or\n * `{ allowed: false, errorCode, resetIn }` when any bucket is full.\n */\nexport const applyApiRateLimits = async ({\n resolvedIp,\n user,\n resolvedName,\n rateLimit,\n transport,\n skipGlobalIpBucket = false,\n}: ApplyApiRateLimitsArgs): Promise<ApplyApiRateLimitsResult> => {\n const config = getProjectConfig();\n const effectiveApiLimit = rateLimit ?? config.rateLimiting.defaultApiLimit;\n\n if (effectiveApiLimit !== false && effectiveApiLimit > 0) {\n const identityCb = config.rateLimiting.identity;\n const customIdentity = identityCb?.({ routeName: resolvedName, userId: user?.id ?? null, ip: resolvedIp, transport }) ?? null;\n //? Per-route bucket keyed on the VALIDATED user.id (never the token): a token\n //? is per-session, so token-keying let an authenticated abuser reset their\n //? bucket by re-logging-in (new token = fresh counter). user.id ties all of a\n //? user's sessions to one bucket; anonymous callers fall back to the IP.\n const requesterIdentity = customIdentity?.id ?? user?.id ?? resolvedIp;\n const keyPrefix = customIdentity?.scope ?? (user?.id ? 'user' : 'ip');\n const rateLimitKey = `${keyPrefix}:${requesterIdentity}:api:${resolvedName}`;\n\n const { allowed, resetIn } = await checkRateLimit({\n key: rateLimitKey,\n limit: effectiveApiLimit,\n windowMs: config.rateLimiting.windowMs,\n });\n\n if (!allowed) {\n //? The per-route bucket is keyed by the validated user when a token is\n //? present, else by the resolved IP (keyPrefix `ip`). Report the scope\n //? that matches the bucket's actual identity — an anonymous per-route\n //? bucket is IP-keyed, so it is `ip` (with `route` still set to mark it\n //? a per-route bucket vs the global `:api:all` IP bucket), never `route`.\n void dispatchHook('rateLimitExceeded', {\n scope: user?.id ? 'user' : 'ip',\n key: rateLimitKey,\n limit: effectiveApiLimit,\n windowMs: config.rateLimiting.windowMs,\n count: effectiveApiLimit + 1,\n route: resolvedName,\n userId: user?.id,\n //? IP only for an IP-keyed (anonymous) bucket; a user-keyed bucket omits it.\n ip: user?.id ? undefined : resolvedIp,\n });\n if (shouldLogDev()) {\n getLogger().warn(`api: rate limit exceeded for ${resolvedName}`, { route: resolvedName, key: rateLimitKey, transport });\n }\n return { allowed: false, errorCode: 'api.rateLimitExceeded', resetIn };\n }\n }\n\n const defaultIpLimit = config.rateLimiting.defaultIpLimit;\n if (!skipGlobalIpBucket && defaultIpLimit !== false && defaultIpLimit > 0) {\n const { allowed, resetIn } = await checkRateLimit({\n key: `ip:${resolvedIp}:api:all`,\n limit: defaultIpLimit,\n windowMs: config.rateLimiting.windowMs,\n });\n\n if (!allowed) {\n void dispatchHook('rateLimitExceeded', {\n scope: 'ip',\n key: `ip:${resolvedIp}:api:all`,\n limit: defaultIpLimit,\n windowMs: config.rateLimiting.windowMs,\n count: defaultIpLimit + 1,\n ip: resolvedIp,\n });\n if (shouldLogDev()) {\n getLogger().warn(`api: global IP rate limit exceeded`, { ip: resolvedIp, transport });\n }\n return { allowed: false, errorCode: 'api.rateLimitExceeded', resetIn };\n }\n }\n\n return { allowed: true };\n};\n","import type { BaseSessionLayout as SessionLayout, PostApiExecutePayload, ErrorFormatter } from '@luckystack/core';\nimport { getProjectConfig, readSession, getRuntimeApiMaps as getRuntimeApiMapsFromSource ,\n runWithErrorTrackerIdentityScope,\n setCurrentErrorTrackerIdentity,\n validateRequest,\n inferHttpMethod,\n HttpMethod,\n tryCatch,\n parseTransportRouteName,\n dispatchHook,\n getLogger, extractLanguageFromHeader, normalizeErrorResponse, applyErrorFormatter,\n isLoopbackIp, resolveClientIp, isProductionRuntime } from '@luckystack/core';\nimport type { ApiStreamPayload, RuntimeApiEntry } from './_shared/apiTypes';\nimport { shouldLogDev, shouldLogStream } from './_shared/logFlags';\nimport { normalizeApiResponse } from './_shared/responseEnvelope';\nimport { httpApiFlushPressureNoop } from './_shared/backpressure';\nimport { runHttpApiValidation } from './_shared/httpValidationStage';\nimport { applyApiRateLimits } from './_shared/applyApiRateLimits';\n\n\n\n\n\n\n/**\n * HTTP API Request Handler\n * \n * Handles API requests coming via HTTP (instead of WebSocket).\n * Reuses existing API handlers but returns results as HTTP response.\n * \n * Payload format:\n * ```json\n * {\n * \"name\": \"api/examples/publicApi\",\n * \"data\": { \"message\": \"hello\" }\n * }\n * ```\n * \n * @example\n * ```typescript\n * // In server.ts\n * if (pathname.startsWith('/api/')) {\n * const token = extractTokenFromRequest(req);\n * const body = await parseJsonBody(req);\n * const result = await handleHttpApiRequest({ name: body.name, data: body.data, token });\n * res.end(JSON.stringify(result));\n * }\n * ```\n */\n\ninterface HttpApiRequestParams {\n name: string;\n data: Record<string, unknown>;\n token: string | null;\n requesterIp?: string;\n xLanguageHeader?: string | string[];\n acceptLanguageHeader?: string | string[];\n /** HTTP method from the request */\n method?: HttpMethod;\n stream?: (payload: ApiHttpStreamEvent) => void;\n /**\n * Optional AbortSignal. The HTTP server (`@luckystack/server`) wires this\n * to `req.on('close', ...)` so a closed SSE/HTTP connection aborts in-flight\n * stream emits. Handlers receive it as `abortSignal` in params.\n */\n abortSignal?: AbortSignal;\n}\n\ntype ApiNetworkResponse<T = Record<string, unknown>> =\n | ({ status: 'success'; httpStatus: number } & T)\n | {\n status: 'error';\n httpStatus: number;\n message: string;\n errorCode: string;\n errorParams?: {\n key: string;\n value: string | number | boolean;\n }[];\n };\n\nexport type ApiHttpStreamEvent = ApiStreamPayload;\n\n//? ET-02 — wrap the whole HTTP request in a per-request error-tracker identity\n//? scope (opened before `readSession` in `runHandleHttpApiRequest`, the first\n//? interleaving await). The resolved session is written into the scope's ALS box\n//? the moment it resolves, so concurrent HTTP requests with different users can't\n//? cross-attribute captures. Each request gets its own isolated box.\nexport async function handleHttpApiRequest(params: HttpApiRequestParams): Promise<ApiNetworkResponse> {\n return runWithErrorTrackerIdentityScope(() => handleHttpApiRequestScoped(params));\n}\n\nasync function handleHttpApiRequestScoped(params: HttpApiRequestParams): Promise<ApiNetworkResponse> {\n const { response, user, preferredLocale, perRouteFormatter, resolvedName } = await runHandleHttpApiRequest(params);\n\n //? Respond hooks and the error formatter key on the canonical route name the\n //? request actually resolved to (same as the socket transport), so a variant\n //? spelling (trailing slash, backslash, whitespace) cannot dodge route-keyed\n //? redaction. Only a name that failed to parse falls back to the raw name, so\n //? hook handlers can still correlate the rejection.\n const routeNameForHook = resolvedName ?? (params.name.startsWith('api/') ? params.name : `api/${params.name}`);\n\n const preRespond = { routeName: routeNameForHook, user, transport: 'http' as const, 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 preserve the same policy/hook ordering. Handlers\n //? mutate `payload.response` in place (header injection, body transform,\n //? response signing).\n const transformPayload = { routeName: routeNameForHook, user, transport: 'http' as const, 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, transport: 'http', 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 /** Canonical route name; undefined when the request failed before the name parsed. */\n resolvedName?: string;\n}\n\n/** Route facts the pipeline resolves on the way, read back by the respond stage. */\ninterface ResolvedRouteHolder {\n perRouteFormatter?: ErrorFormatter;\n resolvedName?: string;\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 routeHolder: ResolvedRouteHolder = {};\n const response = await runHandleHttpApiRequestInner(params, user, preferredLocale, routeHolder);\n return { response, user, preferredLocale, perRouteFormatter: routeHolder.perRouteFormatter, resolvedName: routeHolder.resolvedName };\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 routeHolder: ResolvedRouteHolder,\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 //? Runtime defense: a caller may pass a non-string despite the 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 //? Recorded before the lookup so a not-found response also reports the\n //? canonical name to the respond hooks, as the socket transport does.\n routeHolder.resolvedName = resolvedName;\n\n if (shouldLogDev()) {\n getLogger().debug(`http api: ${resolvedName} called`);\n }\n\n const { apisObject, functionsObject } = await getRuntimeApiMapsFromSource();\n\n if (!apisObject[resolvedName]) {\n return ctx.buildNetworkError({\n response: {\n status: 'error',\n errorCode: 'api.notFound',\n errorParams: [{ key: 'name', value: resolvedName }],\n },\n fallbackHttpStatus: 404,\n });\n }\n\n return { resolvedName, requestData: data, apisObject, functionsObject };\n}\n\n/**\n * Runs the login-required guard and the `validateRequest` additional-predicate\n * check. Returns an error response on failure, or `null` to signal success.\n *\n * Pipeline order: auth → rate-limit → method → validate → execute.\n * Auth runs first so unauthenticated probes can't enumerate routes or\n * learn input shape from `inputValidation.message` / method-mismatch params.\n * API-O7 — failed-auth requests do NOT consume a rate-limit bucket here.\n * Brute-force lockout is the responsibility of `@luckystack/login`\n * (per-account lockout counter via `authLockout.ts`); the `apiAuthRejected`\n * hook lets consumers add additional throttles. Consuming a per-IP bucket on\n * auth-fail would let an attacker DoS a victim's IP with bad credentials.\n */\nfunction runHttpApiAuth(\n {\n name,\n resolvedName,\n requesterIp,\n auth,\n }: {\n name: string;\n resolvedName: string;\n requesterIp: string | undefined;\n auth: RuntimeApiEntry['auth'];\n },\n ctx: HttpApiPipelineContext,\n): ApiNetworkResponse | null {\n if (auth.login && !ctx.user?.id) {\n if (shouldLogDev()) {\n getLogger().warn(`http-api: ${name} requires login`, { route: name, transport: 'http' });\n }\n void dispatchHook('apiAuthRejected', {\n routeName: resolvedName,\n reason: 'login-required',\n userId: null,\n transport: 'http',\n ip: requesterIp,\n });\n return ctx.buildNetworkError({\n response: { status: 'error', errorCode: 'auth.required' },\n fallbackHttpStatus: 401,\n });\n }\n\n //? NO bare `if (!user) -> auth.forbidden` here. Public routes\n //? (auth.login: false) must be callable without a session — exactly like the\n //? socket API handler (handleApiRequest) and both sync handlers.\n //? `validateRequest` is the single source of truth for `additional` auth\n //? predicates. (This handler previously forbade ALL anonymous HTTP calls,\n //? diverging from the other three transports and making public routes\n //? unreachable over HTTP — e.g. the test-runner contract/rate-limit sweeps.)\n //? `user` may be null on a public route; validateRequest tolerates it (and\n //? only the login/additional predicates read it). Same `user!` shape the\n //? socket API handler and both sync handlers use.\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n const authResult = validateRequest({ auth, user: ctx.user! });\n if (authResult.status === 'error') {\n if (shouldLogDev()) {\n getLogger().warn(`http-api: auth failed for ${name}`, { route: name, errorCode: authResult.errorCode, transport: 'http' });\n }\n void dispatchHook('apiAuthRejected', {\n routeName: resolvedName,\n reason: authResult.errorCode === 'auth.invalidCondition' ? 'invalid-condition' : 'additional-failed',\n userId: ctx.user?.id ?? null,\n transport: 'http',\n ip: requesterIp,\n failedKey: authResult.errorCode,\n });\n return ctx.buildNetworkError({\n response: {\n status: 'error',\n errorCode: authResult.errorCode ?? 'auth.forbidden',\n errorParams: authResult.errorParams,\n httpStatus: authResult.httpStatus,\n },\n fallbackHttpStatus: authResult.httpStatus ?? 403,\n });\n }\n\n return null;\n}\n\n/**\n * Resolves the effective client IP, computes the loopback-skip flag, and\n * delegates to the shared `applyApiRateLimits` helper.\n * Returns an error response when any bucket is exceeded, `null` otherwise.\n */\nasync function applyHttpApiRateLimits(\n {\n requesterIp,\n token,\n resolvedName,\n rateLimit,\n }: {\n requesterIp: string | undefined;\n token: string | null;\n resolvedName: string;\n rateLimit: number | false | undefined;\n },\n ctx: HttpApiPipelineContext,\n): Promise<ApiNetworkResponse | null> {\n //? API-O8 / API-O9 — resolve effective IP once for both rate-limit buckets.\n //? When the caller did not supply `requesterIp` we fall back to the shared\n //? `UNKNOWN_CLIENT_IP` sentinel so all unresolvable callers collapse into ONE\n //? deterministic bucket instead of mixing `'unknown'` and `'anonymous'`.\n const effectiveIp = requesterIp\n ?? resolveClientIp({ rawAddress: undefined, headers: {} });\n\n //? API-O2 — skip the global per-IP ABUSE limit for loopback traffic when the\n //? consumer explicitly opts in via `rateLimiting.skipLoopbackInDev` (default\n //? false). Only the cross-route `:api:all` bucket is skipped; per-route limits\n //? still apply. Pass the raw `requesterIp` (undefined → '') so only real\n //? loopback addresses qualify — avoids skipping for every unresolvable caller.\n const skipGlobalIpForLoopback = getProjectConfig().rateLimiting.skipLoopbackInDev\n && !isProductionRuntime()\n && isLoopbackIp(requesterIp ?? '');\n\n const rateLimitResult = await applyApiRateLimits({\n resolvedIp: effectiveIp,\n token,\n user: ctx.user,\n resolvedName,\n rateLimit,\n transport: 'http',\n skipGlobalIpBucket: skipGlobalIpForLoopback,\n });\n\n if (!rateLimitResult.allowed) {\n return ctx.buildNetworkError({\n response: {\n status: 'error',\n errorCode: rateLimitResult.errorCode ?? 'api.rateLimitExceeded',\n errorParams: [{ key: 'seconds', value: rateLimitResult.resetIn ?? 0 }],\n },\n fallbackHttpStatus: 429,\n });\n }\n\n return null;\n}\n\n/**\n * Enforces the HTTP method constraint (declared or inferred).\n * Runs post-auth so unauthenticated probes don't learn that the route exists.\n * Returns an error response on mismatch, `null` otherwise.\n */\nfunction checkHttpApiMethod(\n {\n method,\n resolvedName,\n declaredMethod,\n }: {\n method: HttpMethod;\n resolvedName: string;\n declaredMethod: HttpMethod | undefined;\n },\n ctx: HttpApiPipelineContext,\n): ApiNetworkResponse | null {\n const expectedMethod = declaredMethod ?? inferHttpMethod(resolvedName);\n if (method !== expectedMethod) {\n if (shouldLogDev()) {\n getLogger().warn(`http api: method mismatch for ${resolvedName}`, { expected: expectedMethod, got: method });\n }\n return ctx.buildNetworkError({\n response: {\n status: 'error',\n errorCode: 'api.methodNotAllowed',\n errorParams: [{ key: 'method', value: expectedMethod }],\n },\n fallbackHttpStatus: 405,\n });\n }\n\n return null;\n}\n\n/**\n * Runs the handler execution stage: emitter setup, pre/post-execute hooks,\n * server-side timeout race, `main()` invocation, and response-envelope\n * assembly.\n */\nasync function runHttpApiExecution(\n {\n resolvedName,\n requestData,\n functionsObject,\n runtimeApiRoute,\n effectiveAbortSignal,\n stream,\n }: {\n resolvedName: string;\n requestData: Record<string, unknown>;\n functionsObject: Record<string, unknown>;\n runtimeApiRoute: RuntimeApiEntry;\n effectiveAbortSignal: AbortSignal;\n stream: HttpApiRequestParams['stream'];\n },\n ctx: HttpApiPipelineContext,\n): Promise<ApiNetworkResponse> {\n //? SSE has no socket write-buffer to measure — `flushPressure` is a no-op\n //? on HTTP transport. Kept for handler-shape parity with the socket path.\n const flushPressure = httpApiFlushPressureNoop;\n\n const emitApiStream = (payload: ApiStreamPayload = {}) => {\n if (!stream) {\n return;\n }\n if (effectiveAbortSignal.aborted) {\n if (shouldLogStream()) {\n getLogger().debug(`http api: ${resolvedName} stream skipped — request aborted`);\n }\n return;\n }\n\n if (shouldLogStream()) {\n getLogger().debug(`http api: ${resolvedName} stream`, { payload });\n }\n\n stream(payload);\n };\n\n //? Single payload reference reused by pre/post — auto-instrumentation\n //? subscribers in `@luckystack/error-tracking` pin spans via WeakMap on\n //? this object. Mutating `result/error/durationMs` is safe because\n //? handlers observe by-name (not by snapshot).\n const executePayload: PostApiExecutePayload = {\n routeName: resolvedName,\n data: requestData,\n user: ctx.user,\n transport: 'http',\n result: undefined,\n error: null,\n durationMs: 0,\n };\n const preExecuteResult = await dispatchHook('preApiExecute', executePayload);\n if (preExecuteResult.stopped) {\n return ctx.buildNetworkError({\n response: { status: 'error', errorCode: preExecuteResult.signal.errorCode },\n fallbackHttpStatus: preExecuteResult.signal.httpStatus ?? 403,\n });\n }\n\n //? API-O10 — race `main()` against a server-side timeout derived from\n //? `api.requestTimeoutMs`. When the timeout fires first we abort the\n //? effectiveAbortSignal chain and return a localized 504 envelope.\n //? A combined AbortController merges the caller's close signal and the\n //? timeout so the handler sees one unified abort.\n //? Streaming requests are EXEMPT from the wall-clock timeout even when one is\n //? configured: an SSE/stream endpoint is long-lived by design (live LLM tokens,\n //? progress) and a total-duration race would truncate it mid-stream. The\n //? abortSignal (wired to client disconnect) still tears it down when the caller\n //? leaves. Non-streaming requests honor `api.requestTimeoutMs` (default false).\n const timeoutMs = stream ? false : getProjectConfig().api.requestTimeoutMs;\n const combinedController = new AbortController();\n //? Forward the caller's abort (connection close) into the combined signal.\n if (effectiveAbortSignal.aborted) {\n combinedController.abort();\n } else {\n effectiveAbortSignal.addEventListener('abort', () => { combinedController.abort(); }, { once: true });\n }\n\n let timedOut = false;\n let timeoutHandle: ReturnType<typeof setTimeout> | undefined;\n //? Only a positive number arms the timer: `0` / negative mean \"disabled\",\n //? exactly as the client's `apiRequest` timeout reads the same config key.\n const timeoutPromise = typeof timeoutMs === 'number' && timeoutMs > 0\n ? new Promise<void>((resolve) => {\n timeoutHandle = setTimeout(() => {\n timedOut = true;\n combinedController.abort();\n resolve();\n }, timeoutMs);\n })\n : null;\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 routeHolder: ResolvedRouteHolder,\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, routeHolder);\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 routeHolder.perRouteFormatter = 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 if (httpValidation.stop) {\n return buildNetworkError({\n response: { status: 'error', errorCode: httpValidation.stop.errorCode },\n fallbackHttpStatus: httpValidation.stop.httpStatus ?? 403,\n });\n }\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` (a stop signal vetoes the request before\n//? validation and is returned as `stop`), runs `validateInputByType` in\n//? strict mode, then `postApiValidate` (always, including the relaxed skip\n//? 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; stop?: { errorCode: string; httpStatus?: number } }> => {\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 const preValidateResult = await dispatchHook('preApiValidate', { routeName: resolvedName, data: requestData, user, transport: 'http' });\n //? Stop signal vetoes the request, identical to the socket stage.\n if (preValidateResult.stopped) {\n return { ok: false, stop: { errorCode: preValidateResult.signal.errorCode, httpStatus: preValidateResult.signal.httpStatus } };\n }\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.
|
|
3
|
+
"version": "0.15.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.
|
|
60
|
-
"@luckystack/error-tracking": "^0.
|
|
59
|
+
"@luckystack/core": "^0.15.0",
|
|
60
|
+
"@luckystack/error-tracking": "^0.15.0"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
63
63
|
"@prisma/client": "^6.19.0",
|