@luckystack/core 0.1.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/dist/index.js ADDED
@@ -0,0 +1,2619 @@
1
+ // src/sleep.ts
2
+ function sleep(ms) {
3
+ return new Promise((resolve) => setTimeout(resolve, ms));
4
+ }
5
+
6
+ // src/errorTrackerRegistry.ts
7
+ var activeTrackers = [];
8
+ var registerErrorTracker = (tracker) => {
9
+ activeTrackers = [tracker];
10
+ };
11
+ var registerErrorTrackers = (trackers) => {
12
+ activeTrackers = [...trackers];
13
+ };
14
+ var getActiveErrorTrackers = () => activeTrackers;
15
+ var captureExceptionAcrossTrackers = (error, context) => {
16
+ for (const tracker of activeTrackers) {
17
+ try {
18
+ tracker.captureException(error, context);
19
+ } catch {
20
+ }
21
+ }
22
+ };
23
+ var captureMessageAcrossTrackers = (message, level, context) => {
24
+ for (const tracker of activeTrackers) {
25
+ try {
26
+ tracker.captureMessage(message, level, context);
27
+ } catch {
28
+ }
29
+ }
30
+ };
31
+ var setErrorTrackerUser = (user) => {
32
+ for (const tracker of activeTrackers) {
33
+ try {
34
+ tracker.setUser(user);
35
+ } catch {
36
+ }
37
+ }
38
+ };
39
+ var recordMetricAcrossTrackers = (name, value, tags) => {
40
+ for (const tracker of activeTrackers) {
41
+ if (tracker.recordMetric) {
42
+ try {
43
+ tracker.recordMetric(name, value, tags);
44
+ } catch {
45
+ }
46
+ }
47
+ }
48
+ };
49
+ var startSpanAcrossTrackers = (name, op, fn) => {
50
+ const first = activeTrackers.find((t) => t.startSpan);
51
+ if (!first?.startSpan) return fn();
52
+ return first.startSpan(name, op, fn);
53
+ };
54
+
55
+ // src/sentrySetup.ts
56
+ var sentry;
57
+ var initSharedSentry = (instance) => {
58
+ sentry = instance;
59
+ };
60
+ var captureException = (error, context) => {
61
+ if (sentry) {
62
+ if (context) {
63
+ sentry.captureException(error, { extra: context });
64
+ } else {
65
+ sentry.captureException(error);
66
+ }
67
+ }
68
+ captureExceptionAcrossTrackers(error, context);
69
+ };
70
+ var captureMessage = (message, level = "info", context) => {
71
+ if (sentry) {
72
+ if (context) {
73
+ sentry.captureMessage(message, { level, extra: context });
74
+ } else {
75
+ sentry.captureMessage(message, level);
76
+ }
77
+ }
78
+ captureMessageAcrossTrackers(message, level, context);
79
+ };
80
+ var setSentryUser = (user) => {
81
+ sentry?.setUser(user);
82
+ setErrorTrackerUser(user);
83
+ };
84
+ var startSpan = (name, op) => {
85
+ if (sentry?.startInactiveSpan) {
86
+ return sentry.startInactiveSpan({ name, op });
87
+ }
88
+ startSpanAcrossTrackers(name, op, () => {
89
+ });
90
+ };
91
+
92
+ // src/tryCatch.ts
93
+ async function tryCatch(func, params, context) {
94
+ try {
95
+ const response = await func(params);
96
+ return [null, response];
97
+ } catch (error) {
98
+ captureException(error, context);
99
+ return [error, null];
100
+ }
101
+ }
102
+
103
+ // src/serviceRoute.ts
104
+ var VERSION_SEGMENT_REGEX = /^v\d+$/;
105
+ var normalizeRawRoute = (value) => {
106
+ return value.trim().replaceAll("\\", "/").replaceAll(/^\/+/g, "").replaceAll(/\/+$/g, "");
107
+ };
108
+ var hasEmptySegments = (value) => {
109
+ return value.split("/").some((segment) => segment.length === 0);
110
+ };
111
+ var parseServiceRouteName = (value) => {
112
+ const normalized = normalizeRawRoute(value);
113
+ if (!normalized) {
114
+ return { status: "error", reason: "Route name cannot be empty." };
115
+ }
116
+ if (hasEmptySegments(normalized)) {
117
+ return { status: "error", reason: "Route name cannot contain empty segments." };
118
+ }
119
+ const segments = normalized.split("/");
120
+ if (segments.length < 2) {
121
+ return { status: "error", reason: "Route name must include service and route segments." };
122
+ }
123
+ const [service, ...routeSegments] = segments;
124
+ const routeName = routeSegments.join("/");
125
+ if (!service || !routeName) {
126
+ return { status: "error", reason: "Route name must include service and route segments." };
127
+ }
128
+ return {
129
+ status: "success",
130
+ normalizedRouteName: `${service}/${routeName}`,
131
+ service,
132
+ routeName
133
+ };
134
+ };
135
+ var parseTransportRouteName = ({
136
+ value,
137
+ prefix
138
+ }) => {
139
+ const normalized = normalizeRawRoute(value);
140
+ if (!normalized) {
141
+ return { status: "error", reason: "Transport route cannot be empty." };
142
+ }
143
+ if (hasEmptySegments(normalized)) {
144
+ return { status: "error", reason: "Transport route cannot contain empty segments." };
145
+ }
146
+ const segments = normalized.split("/");
147
+ const routeSegments = segments[0] === prefix ? segments.slice(1) : segments;
148
+ if (routeSegments.length < 3) {
149
+ return {
150
+ status: "error",
151
+ reason: `Transport route must use ${prefix}/{service}/{name}/{version} format.`
152
+ };
153
+ }
154
+ const version = routeSegments.at(-1);
155
+ if (!version || !VERSION_SEGMENT_REGEX.test(version)) {
156
+ return { status: "error", reason: "Route version segment must match v{number}." };
157
+ }
158
+ const serviceRouteToken = routeSegments.slice(0, -1).join("/");
159
+ const serviceRoute = parseServiceRouteName(serviceRouteToken);
160
+ if (serviceRoute.status === "error") {
161
+ return {
162
+ status: "error",
163
+ reason: serviceRoute.reason
164
+ };
165
+ }
166
+ return {
167
+ status: "success",
168
+ normalizedFullName: `${prefix}/${serviceRoute.normalizedRouteName}/${version}`,
169
+ version,
170
+ serviceRoute
171
+ };
172
+ };
173
+
174
+ // src/socketEvents.ts
175
+ var withIndex = (prefix, responseIndex2) => {
176
+ return `${prefix}${String(responseIndex2)}`;
177
+ };
178
+ var socketEventNames = {
179
+ apiRequest: "apiRequest",
180
+ apiCancel: "apiCancel",
181
+ sync: "sync",
182
+ syncCancel: "syncCancel",
183
+ joinRoom: "joinRoom",
184
+ leaveRoom: "leaveRoom",
185
+ getJoinedRooms: "getJoinedRooms",
186
+ updateLocation: "updateLocation",
187
+ updateSession: "updateSession",
188
+ sessionReplaced: "sessionReplaced",
189
+ logout: "logout",
190
+ intentionalDisconnect: "intentionalDisconnect",
191
+ intentionalReconnect: "intentionalReconnect",
192
+ userAfk: "userAfk",
193
+ userBack: "userBack",
194
+ connect: "connect",
195
+ disconnect: "disconnect",
196
+ reconnectAttempt: "reconnect_attempt",
197
+ connectError: "connect_error",
198
+ apiResponsePrefix: "apiResponse-",
199
+ apiStreamPrefix: "apiStream-",
200
+ syncResponsePrefix: "sync-",
201
+ syncProgressPrefix: "sync-progress-",
202
+ joinRoomResponsePrefix: "joinRoom-",
203
+ leaveRoomResponsePrefix: "leaveRoom-",
204
+ getJoinedRoomsResponsePrefix: "getJoinedRooms-"
205
+ };
206
+ var buildApiResponseEventName = (responseIndex2) => {
207
+ return withIndex(socketEventNames.apiResponsePrefix, responseIndex2);
208
+ };
209
+ var buildApiStreamEventName = (responseIndex2) => {
210
+ return withIndex(socketEventNames.apiStreamPrefix, responseIndex2);
211
+ };
212
+ var buildSyncResponseEventName = (responseIndex2) => {
213
+ return withIndex(socketEventNames.syncResponsePrefix, responseIndex2);
214
+ };
215
+ var buildSyncProgressEventName = (responseIndex2) => {
216
+ return withIndex(socketEventNames.syncProgressPrefix, responseIndex2);
217
+ };
218
+ var buildJoinRoomResponseEventName = (responseIndex2) => {
219
+ return withIndex(socketEventNames.joinRoomResponsePrefix, responseIndex2);
220
+ };
221
+ var buildLeaveRoomResponseEventName = (responseIndex2) => {
222
+ return withIndex(socketEventNames.leaveRoomResponsePrefix, responseIndex2);
223
+ };
224
+ var buildGetJoinedRoomsResponseEventName = (responseIndex2) => {
225
+ return withIndex(socketEventNames.getJoinedRoomsResponsePrefix, responseIndex2);
226
+ };
227
+
228
+ // src/responseNormalizer.ts
229
+ var INVALID_ERROR_RESPONSE_CODE = "error.invalidResponse";
230
+ var isErrorParamArray = (value) => {
231
+ return Array.isArray(value) && value.every((item) => {
232
+ return !!item && typeof item === "object" && typeof item.key === "string" && ["string", "number", "boolean"].includes(typeof item.value);
233
+ });
234
+ };
235
+ var normalizeErrorResponseCore = ({
236
+ response,
237
+ fallbackHttpStatus,
238
+ fallbackErrorCode,
239
+ resolveMessage
240
+ }) => {
241
+ const normalizedParams = isErrorParamArray(response.errorParams) ? response.errorParams : void 0;
242
+ const errorCodeValue = typeof response.errorCode === "string" ? response.errorCode.trim() : "";
243
+ const hasErrorCode = errorCodeValue.length > 0;
244
+ const finalErrorCode = hasErrorCode ? errorCodeValue : fallbackErrorCode ?? INVALID_ERROR_RESPONSE_CODE;
245
+ const providedHttpStatus = typeof response.httpStatus === "number" ? response.httpStatus : void 0;
246
+ const message = resolveMessage ? resolveMessage({ errorCode: finalErrorCode, errorParams: normalizedParams }) : finalErrorCode;
247
+ return {
248
+ status: "error",
249
+ message,
250
+ errorCode: finalErrorCode,
251
+ errorParams: normalizedParams,
252
+ httpStatus: providedHttpStatus ?? fallbackHttpStatus
253
+ };
254
+ };
255
+ var defaultHttpStatusForResponse = ({
256
+ status,
257
+ explicitHttpStatus,
258
+ fallbackErrorStatus = 400
259
+ }) => {
260
+ if (typeof explicitHttpStatus === "number") return explicitHttpStatus;
261
+ return status === "success" ? 200 : fallbackErrorStatus;
262
+ };
263
+
264
+ // src/loggerRegistry.ts
265
+ var defaultLogger = {
266
+ debug: (message, context) => {
267
+ if (context === void 0) {
268
+ console.debug(message);
269
+ } else {
270
+ console.debug(message, context);
271
+ }
272
+ },
273
+ info: (message, context) => {
274
+ if (context === void 0) {
275
+ console.info(message);
276
+ } else {
277
+ console.info(message, context);
278
+ }
279
+ },
280
+ warn: (message, context) => {
281
+ if (context === void 0) {
282
+ console.warn(message);
283
+ } else {
284
+ console.warn(message, context);
285
+ }
286
+ },
287
+ error: (message, error, context) => {
288
+ if (error !== void 0 && context !== void 0) console.error(message, error, context);
289
+ else if (error !== void 0) console.error(message, error);
290
+ else if (context === void 0) {
291
+ console.error(message);
292
+ } else {
293
+ console.error(message, context);
294
+ }
295
+ }
296
+ };
297
+ var createDevLogger = () => ({
298
+ debug: (message, context) => {
299
+ if (context === void 0) {
300
+ console.log(message, "cyan");
301
+ } else {
302
+ console.log(message, context, "cyan");
303
+ }
304
+ },
305
+ info: (message, context) => {
306
+ if (context === void 0) {
307
+ console.log(message);
308
+ } else {
309
+ console.log(message, context);
310
+ }
311
+ },
312
+ warn: (message, context) => {
313
+ if (context === void 0) {
314
+ console.log(message, "yellow");
315
+ } else {
316
+ console.log(message, context, "yellow");
317
+ }
318
+ },
319
+ error: (message, error, context) => {
320
+ if (error !== void 0 && context !== void 0) console.log(message, error, context, "red");
321
+ else if (error !== void 0) console.log(message, error, "red");
322
+ else if (context === void 0) {
323
+ console.log(message, "red");
324
+ } else {
325
+ console.log(message, context, "red");
326
+ }
327
+ }
328
+ });
329
+ var activeLogger = defaultLogger;
330
+ var isRegistered = false;
331
+ var registerLogger = (logger) => {
332
+ activeLogger = logger;
333
+ isRegistered = true;
334
+ };
335
+ var getLogger = () => activeLogger;
336
+ var isLoggerRegistered = () => isRegistered;
337
+ var resetLoggerForTests = () => {
338
+ activeLogger = defaultLogger;
339
+ isRegistered = false;
340
+ };
341
+
342
+ // src/hooks/registry.ts
343
+ var hookHandlers = /* @__PURE__ */ new Map();
344
+ var registerHook = (name, handler) => {
345
+ const existing = hookHandlers.get(name) ?? [];
346
+ existing.push(handler);
347
+ hookHandlers.set(name, existing);
348
+ };
349
+ var dispatchHook = async (name, payload) => {
350
+ const handlers = hookHandlers.get(name) ?? [];
351
+ for (const handler of handlers) {
352
+ let result;
353
+ try {
354
+ result = await handler(payload);
355
+ } catch (error) {
356
+ getLogger().error(`hook: handler for "${name}" threw`, error, { hook: name });
357
+ captureException(error, { hook: name });
358
+ continue;
359
+ }
360
+ if (result !== void 0) {
361
+ return { stopped: true, signal: result };
362
+ }
363
+ }
364
+ return { stopped: false };
365
+ };
366
+ var clearAllHooks = () => {
367
+ hookHandlers.clear();
368
+ syncHookHandlers.clear();
369
+ };
370
+ var syncHookHandlers = /* @__PURE__ */ new Map();
371
+ var registerSyncHook = (name, handler) => {
372
+ const existing = syncHookHandlers.get(name) ?? [];
373
+ existing.push(handler);
374
+ syncHookHandlers.set(name, existing);
375
+ };
376
+ var dispatchSyncHook = (name, payload) => {
377
+ const handlers = syncHookHandlers.get(name) ?? [];
378
+ for (const handler of handlers) {
379
+ try {
380
+ handler(payload);
381
+ } catch (error) {
382
+ getLogger().error(`hook: sync handler for "${name}" threw`, error, { hook: name });
383
+ captureException(error, { hook: name });
384
+ }
385
+ }
386
+ };
387
+
388
+ // src/localizedNormalizer.ts
389
+ var normalizeLanguageCode = (language, isSupported) => {
390
+ if (!language) return null;
391
+ const short = language.toLowerCase().split("-")[0];
392
+ if (!short) return null;
393
+ return isSupported(short) ? short : null;
394
+ };
395
+ var createLocalizedNormalizer = (input) => {
396
+ const defaultLanguage = input.defaultLanguage ?? "en";
397
+ const isSupported = input.isSupportedLanguage ?? (() => true);
398
+ const extractLanguageFromHeader2 = (header) => {
399
+ if (!header) return null;
400
+ const normalized = Array.isArray(header) ? header.join(",") : header;
401
+ const candidates = normalized.split(",").map((part) => part.trim().split(";")[0]).filter(Boolean);
402
+ for (const candidate of candidates) {
403
+ const language = normalizeLanguageCode(candidate, isSupported);
404
+ if (language) return language;
405
+ }
406
+ return null;
407
+ };
408
+ const resolveLanguage = ({
409
+ preferredLocale,
410
+ userLanguage
411
+ }) => {
412
+ return normalizeLanguageCode(userLanguage, isSupported) ?? normalizeLanguageCode(preferredLocale, isSupported) ?? defaultLanguage;
413
+ };
414
+ const resolveErrorMessage2 = ({
415
+ errorCode,
416
+ errorParams,
417
+ preferredLocale,
418
+ userLanguage
419
+ }) => {
420
+ const language = resolveLanguage({ preferredLocale, userLanguage });
421
+ return input.translate({ language, key: errorCode, params: errorParams });
422
+ };
423
+ const normalizeErrorResponse2 = ({
424
+ response,
425
+ preferredLocale,
426
+ userLanguage,
427
+ fallbackHttpStatus
428
+ }) => {
429
+ const preNormalize = { response: { ...response }, preferredLocale, userLanguage, fallbackHttpStatus };
430
+ dispatchSyncHook("preErrorNormalize", preNormalize);
431
+ const normalized = normalizeErrorResponseCore({
432
+ response: preNormalize.response,
433
+ fallbackHttpStatus: preNormalize.fallbackHttpStatus,
434
+ resolveMessage: ({ errorCode, errorParams }) => resolveErrorMessage2({
435
+ errorCode,
436
+ errorParams,
437
+ preferredLocale: preNormalize.preferredLocale,
438
+ userLanguage: preNormalize.userLanguage
439
+ })
440
+ });
441
+ const finalized = {
442
+ ...normalized,
443
+ httpStatus: defaultHttpStatusForResponse({
444
+ status: "error",
445
+ explicitHttpStatus: normalized.httpStatus
446
+ })
447
+ };
448
+ const postNormalize = { normalized: finalized, preferredLocale: preNormalize.preferredLocale, userLanguage: preNormalize.userLanguage };
449
+ dispatchSyncHook("postErrorNormalize", postNormalize);
450
+ return postNormalize.normalized;
451
+ };
452
+ return { normalizeErrorResponse: normalizeErrorResponse2, resolveErrorMessage: resolveErrorMessage2, extractLanguageFromHeader: extractLanguageFromHeader2 };
453
+ };
454
+ var identityNormalizer = createLocalizedNormalizer({
455
+ translate: ({ key }) => key
456
+ });
457
+ var activeNormalizer = identityNormalizer;
458
+ var normalizerRegistered = false;
459
+ var registerLocalizedNormalizer = (normalizer) => {
460
+ activeNormalizer = normalizer;
461
+ normalizerRegistered = true;
462
+ };
463
+ var getLocalizedNormalizer = () => activeNormalizer;
464
+ var isLocalizedNormalizerRegistered = () => normalizerRegistered;
465
+ var normalizeErrorResponse = (input) => activeNormalizer.normalizeErrorResponse(input);
466
+ var resolveErrorMessage = (input) => activeNormalizer.resolveErrorMessage(input);
467
+ var extractLanguageFromHeader = (header) => activeNormalizer.extractLanguageFromHeader(header);
468
+
469
+ // src/errorFormatterRegistry.ts
470
+ var activeFormatter = null;
471
+ var registerErrorFormatter = (formatter2) => {
472
+ activeFormatter = formatter2;
473
+ };
474
+ var getErrorFormatter = () => activeFormatter;
475
+ var applyErrorFormatter = (input) => {
476
+ const { response, routeName, transport, userId, perRouteFormatter } = input;
477
+ if (response.status !== "error") return response;
478
+ const ctx = { routeName, transport, userId };
479
+ const errorEnvelope = response;
480
+ if (perRouteFormatter) {
481
+ try {
482
+ return perRouteFormatter(errorEnvelope, ctx);
483
+ } catch (error) {
484
+ console.error(`[errorFormatter] per-route formatter for ${routeName} threw:`, error);
485
+ }
486
+ }
487
+ const globalFormatter = activeFormatter;
488
+ if (globalFormatter) {
489
+ try {
490
+ return globalFormatter(errorEnvelope, ctx);
491
+ } catch (error) {
492
+ console.error(`[errorFormatter] global formatter threw on ${routeName}:`, error);
493
+ return response;
494
+ }
495
+ }
496
+ return response;
497
+ };
498
+
499
+ // src/pageRouteValidation.ts
500
+ var DEFAULT_PAGE_ROUTE_RULES = {
501
+ privateFolderPrefix: "_",
502
+ scaffoldIgnoredFolders: [
503
+ "_api",
504
+ "_sync",
505
+ "_function",
506
+ "_functions",
507
+ "_component",
508
+ "_components",
509
+ "_provider",
510
+ "_providers",
511
+ "_locale",
512
+ "_locales",
513
+ "_socket",
514
+ "_sockets",
515
+ "_shared",
516
+ "_server"
517
+ ]
518
+ };
519
+ var validatePagePath = (srcRelativePath, rules = DEFAULT_PAGE_ROUTE_RULES) => {
520
+ const normalized = srcRelativePath.replaceAll("\\", "/").replace(/^\.\//, "").replace(/^\/+/, "");
521
+ const segments = normalized.split("/").filter((segment) => segment.length > 0);
522
+ if (segments.length === 0) {
523
+ return { valid: false, reason: "empty path" };
524
+ }
525
+ const filename = segments.at(-1);
526
+ if (filename !== "page.tsx" && filename !== "page.jsx") {
527
+ return { valid: false, reason: "not a page file (expected page.tsx)" };
528
+ }
529
+ const folderSegments = segments.slice(0, -1);
530
+ const prefix = rules.privateFolderPrefix;
531
+ const reserved = rules.scaffoldIgnoredFolders;
532
+ for (const reservedName of reserved) {
533
+ if (folderSegments.includes(reservedName)) {
534
+ return { valid: false, reason: `page.tsx cannot live inside reserved folder ${reservedName}` };
535
+ }
536
+ }
537
+ const visibleSegments = folderSegments.filter((segment) => !segment.startsWith(prefix));
538
+ if (folderSegments.length > 0 && visibleSegments.length === 0) {
539
+ return { valid: false, reason: "no URL segment left after stripping underscore folders" };
540
+ }
541
+ const route = visibleSegments.length === 0 ? "/" : `/${visibleSegments.join("/")}`;
542
+ return { valid: true, route };
543
+ };
544
+
545
+ // src/socketTypes.ts
546
+ var _ioInstance = null;
547
+ var setIoInstance = (io) => {
548
+ _ioInstance = io;
549
+ };
550
+ var getIoInstance = () => _ioInstance;
551
+
552
+ // src/projectConfig.ts
553
+ var DEFAULT_PROJECT_CONFIG = {
554
+ app: {
555
+ publicUrl: ""
556
+ },
557
+ logging: {
558
+ devLogs: false,
559
+ devNotifications: false,
560
+ socketStatus: false,
561
+ socketStartup: false,
562
+ stream: false
563
+ },
564
+ rateLimiting: {
565
+ enabled: true,
566
+ store: "memory",
567
+ redisKeyPrefix: "rate-limit",
568
+ defaultApiLimit: 60,
569
+ defaultIpLimit: 100,
570
+ windowMs: 6e4,
571
+ cleanupIntervalMs: 6e4
572
+ },
573
+ session: {
574
+ basedToken: false,
575
+ expiryDays: 7,
576
+ allowMultiple: false,
577
+ perBrowser: "single",
578
+ perUser: "single",
579
+ maxConcurrentPerUser: null,
580
+ onConflict: "revokeOld",
581
+ notifyOldDeviceOnRevoke: true,
582
+ //? Empty default — `getProjectName()` reads `process.env.PROJECT_NAME` at
583
+ //? call time and falls back to `'luckystack'`. Avoids capturing env at
584
+ //? module-load before dotenv runs.
585
+ projectName: ""
586
+ },
587
+ http: {
588
+ sessionCookieName: "token",
589
+ sessionCookieSameSite: "Strict",
590
+ sessionCookiePath: "/",
591
+ requestBodyMaxBytes: 1024 * 1024,
592
+ healthEndpoint: "/_health",
593
+ liveEndpoint: "/livez",
594
+ readyEndpoint: "/readyz",
595
+ testResetEndpoint: "/_test/reset",
596
+ stream: {
597
+ queryParam: "stream",
598
+ enabledValue: "true",
599
+ connectedComment: ": connected"
600
+ },
601
+ securityHeaders: {
602
+ frameOptions: "SAMEORIGIN",
603
+ referrerPolicy: "no-referrer",
604
+ xssProtection: "1; mode=block",
605
+ contentTypeOptions: "nosniff"
606
+ },
607
+ cors: {
608
+ allowedMethods: "GET, POST, PUT, DELETE, OPTIONS",
609
+ allowedHeaders: "Content-Type, Authorization, X-Session-Based-Token, X-CSRF-Token, X-Request-Id",
610
+ exposedHeaders: "X-Session-Token, X-Request-Id",
611
+ credentials: true,
612
+ allowedOrigins: [],
613
+ allowLocalhost: false
614
+ }
615
+ },
616
+ auth: {
617
+ oauthStateTtlSeconds: 60 * 10,
618
+ passwordMinLength: 8,
619
+ passwordMaxLength: 191,
620
+ passwordPolicy: {
621
+ minLength: 8,
622
+ maxLength: 191,
623
+ requireUppercase: false,
624
+ requireLowercase: false,
625
+ requireNumber: false,
626
+ requireSpecial: false,
627
+ forbidCommon: true
628
+ },
629
+ emailMaxLength: 191,
630
+ nameMaxLength: 191,
631
+ bcryptRounds: 10,
632
+ providerAccountStrategy: "per-provider",
633
+ forgotPassword: "disabled",
634
+ passwordResetTtlSeconds: 60 * 60,
635
+ emailChangeTtlSeconds: 60 * 60
636
+ },
637
+ socket: {
638
+ maxHttpBufferSize: 5 * 1024 * 1024,
639
+ pingTimeout: 2e4,
640
+ pingInterval: 25e3
641
+ },
642
+ sync: {
643
+ streamThrottle: {
644
+ flushAtChars: 32,
645
+ flushEveryMs: 50,
646
+ field: "chunk"
647
+ },
648
+ fanoutYieldEvery: 100,
649
+ fanoutYieldMs: 1
650
+ },
651
+ offlineQueue: {
652
+ maxSize: 200,
653
+ maxAgeMs: 60 * 60 * 1e3,
654
+ dropPolicy: "drop-oldest"
655
+ },
656
+ dev: {
657
+ hotReloadDebounceMs: 120,
658
+ watcherStabilityThresholdMs: 120,
659
+ watcherPollIntervalMs: 20
660
+ },
661
+ paths: {
662
+ srcDir: "src",
663
+ serverDir: "server",
664
+ sharedDir: "shared",
665
+ uploadsDir: "uploads",
666
+ publicDir: "public",
667
+ serverFunctionsDir: "server/functions",
668
+ serverFunctionDirs: ["functions", "shared"],
669
+ generatedSocketTypes: "src/_sockets/apiTypes.generated.ts",
670
+ generatedApiSchemas: "src/_sockets/apiInputSchemas.generated.ts",
671
+ generatedApiDocs: "src/docs/apiDocs.generated.json"
672
+ },
673
+ defaultLanguage: "en",
674
+ defaultTheme: "light",
675
+ socketActivityBroadcaster: false,
676
+ socketStatusIndicator: false,
677
+ locationProviderEnabled: false,
678
+ loginRedirectUrl: "/"
679
+ };
680
+ var isPlainObject = (value) => {
681
+ if (value === null || typeof value !== "object") return false;
682
+ const proto = Object.getPrototypeOf(value);
683
+ return proto === Object.prototype || proto === null;
684
+ };
685
+ var deepMerge = (base, override) => {
686
+ if (override === void 0) return base;
687
+ if (!isPlainObject(base) || !isPlainObject(override)) {
688
+ return override ?? base;
689
+ }
690
+ const result = { ...base };
691
+ for (const [key, overrideValue] of Object.entries(override)) {
692
+ if (overrideValue === void 0) continue;
693
+ const baseValue = base[key];
694
+ result[key] = isPlainObject(baseValue) && isPlainObject(overrideValue) ? deepMerge(baseValue, overrideValue) : overrideValue;
695
+ }
696
+ return result;
697
+ };
698
+ var activeConfig = DEFAULT_PROJECT_CONFIG;
699
+ var isRegistered2 = false;
700
+ var registerProjectConfig = (config) => {
701
+ activeConfig = deepMerge(DEFAULT_PROJECT_CONFIG, config);
702
+ isRegistered2 = true;
703
+ };
704
+ var getProjectConfig = () => activeConfig;
705
+ var getProjectName = () => {
706
+ const fromConfig = activeConfig.session.projectName;
707
+ if (fromConfig && fromConfig.length > 0) return fromConfig;
708
+ const fromEnv = process.env.PROJECT_NAME;
709
+ if (fromEnv && fromEnv.length > 0) return fromEnv;
710
+ return "luckystack";
711
+ };
712
+ var isProjectConfigRegistered = () => isRegistered2;
713
+
714
+ // src/runtimeMapsRegistry.ts
715
+ var emptyApi = { apisObject: {}, functionsObject: {} };
716
+ var emptySync = { syncObject: {}, functionsObject: {} };
717
+ var activeProvider = {
718
+ getRuntimeApiMaps: () => Promise.resolve(emptyApi),
719
+ getRuntimeSyncMaps: () => Promise.resolve(emptySync)
720
+ };
721
+ var providerRegistered = false;
722
+ var registerRuntimeMapsProvider = (provider) => {
723
+ activeProvider = provider;
724
+ providerRegistered = true;
725
+ };
726
+ var isRuntimeMapsProviderRegistered = () => providerRegistered;
727
+ var getRuntimeApiMaps = async () => activeProvider.getRuntimeApiMaps();
728
+ var getRuntimeSyncMaps = async () => activeProvider.getRuntimeSyncMaps();
729
+
730
+ // src/apiMethodMapRegistry.ts
731
+ var activeMap = null;
732
+ var registerApiMethodMap = (map) => {
733
+ activeMap = map;
734
+ };
735
+ var getRegisteredApiMethod = (pagePath, apiName, version) => {
736
+ if (!activeMap) return void 0;
737
+ return activeMap[pagePath]?.[apiName]?.[version];
738
+ };
739
+ var isApiMethodMapRegistered = () => activeMap !== null;
740
+
741
+ // src/notifier.ts
742
+ var noopNotifier = {
743
+ success: () => {
744
+ },
745
+ error: () => {
746
+ },
747
+ info: () => {
748
+ },
749
+ warning: () => {
750
+ }
751
+ };
752
+ var activeNotifier = noopNotifier;
753
+ var registerNotifier = (notifier) => {
754
+ activeNotifier = notifier;
755
+ };
756
+ var getNotifier = () => activeNotifier;
757
+ var notify = {
758
+ success: (input) => {
759
+ activeNotifier.success(input);
760
+ },
761
+ error: (input) => {
762
+ activeNotifier.error(input);
763
+ },
764
+ info: (input) => {
765
+ activeNotifier.info(input);
766
+ },
767
+ warning: (input) => {
768
+ activeNotifier.warning(input);
769
+ }
770
+ };
771
+
772
+ // src/emailRegistry.ts
773
+ var activeSender = null;
774
+ var registry = {};
775
+ var registerEmailSender = (sender) => {
776
+ activeSender = sender;
777
+ registry = { ...registry, default: sender };
778
+ };
779
+ var registerEmailSenders = (senders) => {
780
+ registry = { ...senders };
781
+ if (senders.default) {
782
+ activeSender = senders.default;
783
+ }
784
+ };
785
+ var getEmailSender = () => activeSender ?? registry.default ?? null;
786
+ var getEmailSenderByName = (name) => {
787
+ if (registry[name]) return registry[name];
788
+ if (name === "default" && activeSender) return activeSender;
789
+ return null;
790
+ };
791
+ var listEmailSenderNames = () => Object.keys(registry);
792
+ var isEmailSenderRegistered = () => activeSender !== null || Object.keys(registry).length > 0;
793
+
794
+ // src/deployConfigRegistry.ts
795
+ var DEFAULT_DEPLOY_CONFIG = {
796
+ resources: {}
797
+ };
798
+ var activeConfig2 = DEFAULT_DEPLOY_CONFIG;
799
+ var registered = false;
800
+ var registerDeployConfig = (config) => {
801
+ activeConfig2 = config;
802
+ registered = true;
803
+ return activeConfig2;
804
+ };
805
+ var getDeployConfig = () => activeConfig2;
806
+ var isDeployConfigRegistered = () => registered;
807
+
808
+ // src/servicesConfigRegistry.ts
809
+ var activeServicesConfig = null;
810
+ var registerServicesConfig = (config) => {
811
+ activeServicesConfig = config;
812
+ return activeServicesConfig;
813
+ };
814
+ var getServicesConfig = () => {
815
+ if (!activeServicesConfig) {
816
+ throw new Error(
817
+ "@luckystack/core: services config has not been registered. Call `registerServicesConfig({...})` from your `services.config.ts` (which should be imported as a side-effect during boot)."
818
+ );
819
+ }
820
+ return activeServicesConfig;
821
+ };
822
+ var isServicesConfigRegistered = () => activeServicesConfig !== null;
823
+
824
+ // src/localeReloader.ts
825
+ var activeReloader = null;
826
+ var registerLocaleReloader = (reloader) => {
827
+ activeReloader = reloader;
828
+ return reloader;
829
+ };
830
+ var getLocaleReloader = () => activeReloader;
831
+
832
+ // src/env.ts
833
+ import { config as loadDotenv } from "dotenv";
834
+ import { z } from "zod";
835
+ var EnvSchema = z.object({
836
+ NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
837
+ SERVER_IP: z.string().min(1).default("127.0.0.1"),
838
+ SERVER_PORT: z.string().regex(/^\d+$/).default("80"),
839
+ SECURE: z.enum(["true", "false"]).default("false"),
840
+ DNS: z.string().default(""),
841
+ REDIS_HOST: z.string().min(1).default("127.0.0.1"),
842
+ REDIS_PORT: z.string().regex(/^\d+$/).default("6379"),
843
+ PROJECT_NAME: z.string().min(1).default("luckystack")
844
+ }).loose();
845
+ var cachedEnv = null;
846
+ var DEFAULT_ENV_FILES = [".env", ".env.local"];
847
+ var getEnvFiles = () => {
848
+ const override = process.env.LUCKYSTACK_ENV_FILES;
849
+ if (override) {
850
+ const list = override.split(",").map((entry) => entry.trim()).filter(Boolean);
851
+ if (list.length > 0) return list;
852
+ }
853
+ return DEFAULT_ENV_FILES;
854
+ };
855
+ var loadEnvFiles = () => {
856
+ for (const [index, file] of getEnvFiles().entries()) {
857
+ loadDotenv({ path: file, override: index > 0 });
858
+ }
859
+ };
860
+ var applyResolvedDefaultsToProcessEnv = (resolvedEnv) => {
861
+ process.env.NODE_ENV = process.env.NODE_ENV ?? resolvedEnv.NODE_ENV;
862
+ process.env.SERVER_IP = process.env.SERVER_IP ?? resolvedEnv.SERVER_IP;
863
+ process.env.SERVER_PORT = process.env.SERVER_PORT ?? resolvedEnv.SERVER_PORT;
864
+ process.env.SECURE = process.env.SECURE ?? resolvedEnv.SECURE;
865
+ process.env.DNS = process.env.DNS ?? resolvedEnv.DNS;
866
+ process.env.REDIS_HOST = process.env.REDIS_HOST ?? resolvedEnv.REDIS_HOST;
867
+ process.env.REDIS_PORT = process.env.REDIS_PORT ?? resolvedEnv.REDIS_PORT;
868
+ process.env.PROJECT_NAME = process.env.PROJECT_NAME ?? resolvedEnv.PROJECT_NAME;
869
+ };
870
+ var bootstrapEnv = () => {
871
+ if (cachedEnv) {
872
+ return cachedEnv;
873
+ }
874
+ loadEnvFiles();
875
+ const parsed = EnvSchema.safeParse(process.env);
876
+ if (!parsed.success) {
877
+ const details = parsed.error.issues.map((issue) => `${issue.path.join(".") || "env"}: ${issue.message}`).join("; ");
878
+ throw new Error(`Invalid environment configuration: ${details}`);
879
+ }
880
+ cachedEnv = parsed.data;
881
+ applyResolvedDefaultsToProcessEnv(cachedEnv);
882
+ return cachedEnv;
883
+ };
884
+ var env = bootstrapEnv();
885
+ var isProduction = env.NODE_ENV === "production";
886
+
887
+ // src/db.ts
888
+ import { PrismaClient } from "@prisma/client";
889
+
890
+ // src/clients.ts
891
+ var DEFAULT_CLIENT_KEY = "default";
892
+ var prismaClients = /* @__PURE__ */ new Map();
893
+ var redisClients = /* @__PURE__ */ new Map();
894
+ var registerPrismaClient = (client, key = DEFAULT_CLIENT_KEY) => {
895
+ prismaClients.set(key, client);
896
+ return client;
897
+ };
898
+ var registerRedisClient = (client, key = DEFAULT_CLIENT_KEY) => {
899
+ redisClients.set(key, client);
900
+ return client;
901
+ };
902
+ var isPrismaClientRegistered = () => prismaClients.has(DEFAULT_CLIENT_KEY);
903
+ var isRedisClientRegistered = () => redisClients.has(DEFAULT_CLIENT_KEY);
904
+ var getPrismaClientKeys = () => [...prismaClients.keys()];
905
+ var getRedisClientKeys = () => [...redisClients.keys()];
906
+ var defaultPrismaResolver = null;
907
+ var defaultRedisResolver = null;
908
+ var setDefaultPrismaResolver = (resolver) => {
909
+ defaultPrismaResolver = resolver;
910
+ };
911
+ var setDefaultRedisResolver = (resolver) => {
912
+ defaultRedisResolver = resolver;
913
+ };
914
+ var getPrismaClientFor = (key = DEFAULT_CLIENT_KEY) => {
915
+ const client = prismaClients.get(key);
916
+ if (client) return client;
917
+ if (key === DEFAULT_CLIENT_KEY && defaultPrismaResolver) return defaultPrismaResolver();
918
+ throw new Error(
919
+ key === DEFAULT_CLIENT_KEY ? 'No Prisma client available. Either register one via registerPrismaClient(), or import "@luckystack/core" from a context where the default resolver was set.' : `No Prisma client registered for slot "${key}". Register one via registerPrismaClient(client, "${key}").`
920
+ );
921
+ };
922
+ var getRedisClientFor = (key = DEFAULT_CLIENT_KEY) => {
923
+ const client = redisClients.get(key);
924
+ if (client) return client;
925
+ if (key === DEFAULT_CLIENT_KEY && defaultRedisResolver) return defaultRedisResolver();
926
+ throw new Error(
927
+ key === DEFAULT_CLIENT_KEY ? 'No Redis client available. Either register one via registerRedisClient(), or import "@luckystack/core" from a context where the default resolver was set.' : `No Redis client registered for slot "${key}". Register one via registerRedisClient(client, "${key}").`
928
+ );
929
+ };
930
+ var getPrismaClient = () => getPrismaClientFor(DEFAULT_CLIENT_KEY);
931
+ var getRedisClient = () => getRedisClientFor(DEFAULT_CLIENT_KEY);
932
+ var resetClientsForTests = () => {
933
+ prismaClients.clear();
934
+ redisClients.clear();
935
+ };
936
+
937
+ // src/db.ts
938
+ var buildDefaultPrismaClient = () => {
939
+ globalThis.__luckystackPrisma ??= new PrismaClient();
940
+ return globalThis.__luckystackPrisma;
941
+ };
942
+ setDefaultPrismaResolver(buildDefaultPrismaClient);
943
+ if (process.env.NODE_ENV !== "production") {
944
+ buildDefaultPrismaClient();
945
+ }
946
+ var prisma = new Proxy({}, {
947
+ //? Bind methods to the real client so a CAPTURED method keeps its `this`.
948
+ //? Without binding, a detached call (`const q = prisma.$queryRaw; q(...)`)
949
+ //? runs with `this === undefined` and Prisma's internal `this`-access throws
950
+ //? — exactly how the `/readyz` prisma ping silently failed (it captures
951
+ //? `$runCommandRaw` / `$queryRaw` into a local before calling, so the ping
952
+ //? always threw → `prisma: false` → 503, even with the DB reachable).
953
+ //? Mirrors the `redis` proxy. Non-function reads (model delegates like
954
+ //? `prisma.user`) pass through untouched, so their methods bind to the
955
+ //? delegate as before.
956
+ get: (_target, prop, receiver) => {
957
+ const real = getPrismaClient();
958
+ const value = Reflect.get(real, prop, receiver);
959
+ if (typeof value !== "function") return value;
960
+ const fn = value;
961
+ return fn.bind(real);
962
+ },
963
+ has: (_target, prop) => Reflect.has(getPrismaClient(), prop),
964
+ ownKeys: () => Reflect.ownKeys(getPrismaClient()),
965
+ getOwnPropertyDescriptor: (_target, prop) => Reflect.getOwnPropertyDescriptor(getPrismaClient(), prop)
966
+ });
967
+
968
+ // src/redis.ts
969
+ import Redis from "ioredis";
970
+
971
+ // src/redisKeyFormatter.ts
972
+ var defaultRedisKeyFormatter = (namespace, suffix) => {
973
+ const root = /^[-:]/.test(namespace) ? `${getProjectName()}${namespace}` : `${getProjectName()}:${namespace}`;
974
+ return suffix === "" ? root : `${root}:${suffix}`;
975
+ };
976
+ var formatter = null;
977
+ var registerRedisKeyFormatter = (fn) => {
978
+ formatter = fn;
979
+ };
980
+ var getRedisKeyFormatter = () => formatter ?? defaultRedisKeyFormatter;
981
+ var resetRedisKeyFormatterForTests = () => {
982
+ formatter = null;
983
+ };
984
+ var formatKey = (namespace, suffix = "") => getRedisKeyFormatter()(namespace, suffix);
985
+ var applyStrayKeyPrefix = (key) => key.includes(":") ? key : `${getProjectName()}:${key}`;
986
+
987
+ // src/redis.ts
988
+ var cachedDefault = null;
989
+ var MAX_REDIS_RECONNECT_ATTEMPTS = 50;
990
+ var buildDefaultRedisClient = () => {
991
+ if (cachedDefault) return cachedDefault;
992
+ cachedDefault = new Redis({
993
+ host: env.REDIS_HOST,
994
+ port: Number.parseInt(env.REDIS_PORT, 10),
995
+ ...process.env.REDIS_USERNAME && { username: process.env.REDIS_USERNAME },
996
+ ...process.env.REDIS_PASSWORD && { password: process.env.REDIS_PASSWORD },
997
+ retryStrategy(times) {
998
+ if (times > MAX_REDIS_RECONNECT_ATTEMPTS) {
999
+ getLogger().error(
1000
+ `Redis unreachable after ${String(MAX_REDIS_RECONNECT_ATTEMPTS)} reconnect attempts; giving up. Check REDIS_HOST / REDIS_PORT / credentials and that Redis is reachable.`
1001
+ );
1002
+ return null;
1003
+ }
1004
+ return Math.min(times * 50, 2e3);
1005
+ }
1006
+ });
1007
+ cachedDefault.on("ready", async () => {
1008
+ getLogger().info("Connected to Redis");
1009
+ });
1010
+ cachedDefault.on("error", (err) => {
1011
+ getLogger().error("Error connecting to Redis", err);
1012
+ });
1013
+ return cachedDefault;
1014
+ };
1015
+ setDefaultRedisResolver(buildDefaultRedisClient);
1016
+ var getRedisConnectionOptions = () => ({
1017
+ host: env.REDIS_HOST,
1018
+ port: Number.parseInt(env.REDIS_PORT, 10),
1019
+ ...process.env.REDIS_USERNAME ? { username: process.env.REDIS_USERNAME } : {},
1020
+ ...process.env.REDIS_PASSWORD ? { password: process.env.REDIS_PASSWORD } : {}
1021
+ });
1022
+ var STRAY_PREFIX_COMMANDS = /* @__PURE__ */ new Set([
1023
+ "get",
1024
+ "set",
1025
+ "setex",
1026
+ "psetex",
1027
+ "setnx",
1028
+ "getset",
1029
+ "getdel",
1030
+ "append",
1031
+ "strlen",
1032
+ "expire",
1033
+ "pexpire",
1034
+ "expireat",
1035
+ "pexpireat",
1036
+ "persist",
1037
+ "ttl",
1038
+ "pttl",
1039
+ "type",
1040
+ "incr",
1041
+ "incrby",
1042
+ "incrbyfloat",
1043
+ "decr",
1044
+ "decrby",
1045
+ "sadd",
1046
+ "srem",
1047
+ "smembers",
1048
+ "scard",
1049
+ "sismember",
1050
+ "spop",
1051
+ "srandmember",
1052
+ "hget",
1053
+ "hset",
1054
+ "hdel",
1055
+ "hgetall",
1056
+ "hkeys",
1057
+ "hvals",
1058
+ "hexists",
1059
+ "hincrby",
1060
+ "hmget",
1061
+ "hmset",
1062
+ "lpush",
1063
+ "rpush",
1064
+ "lpop",
1065
+ "rpop",
1066
+ "llen",
1067
+ "lrange",
1068
+ "lrem",
1069
+ "zadd",
1070
+ "zrem",
1071
+ "zscore",
1072
+ "zrange",
1073
+ "zrank",
1074
+ "zcard",
1075
+ "getbit",
1076
+ "setbit",
1077
+ "bitcount"
1078
+ ]);
1079
+ var redisProxy = new Proxy({}, {
1080
+ get: (_target, prop, receiver) => {
1081
+ const real = getRedisClient();
1082
+ const value = Reflect.get(real, prop, receiver);
1083
+ if (typeof value !== "function") return value;
1084
+ const fn = value;
1085
+ if (typeof prop === "string" && STRAY_PREFIX_COMMANDS.has(prop.toLowerCase())) {
1086
+ return (...args) => {
1087
+ if (args.length > 0 && typeof args[0] === "string") {
1088
+ args[0] = applyStrayKeyPrefix(args[0]);
1089
+ }
1090
+ return fn.apply(real, args);
1091
+ };
1092
+ }
1093
+ return fn.bind(real);
1094
+ },
1095
+ has: (_target, prop) => Reflect.has(getRedisClient(), prop)
1096
+ });
1097
+ var redis = redisProxy;
1098
+
1099
+ // src/lease.ts
1100
+ import { randomBytes } from "crypto";
1101
+ var leaseKey = (name) => formatKey("lease", name);
1102
+ var RENEW_SCRIPT = `
1103
+ if redis.call('get', KEYS[1]) == ARGV[1] then
1104
+ return redis.call('pexpire', KEYS[1], ARGV[2])
1105
+ else
1106
+ return 0
1107
+ end
1108
+ `;
1109
+ var RELEASE_SCRIPT = `
1110
+ if redis.call('get', KEYS[1]) == ARGV[1] then
1111
+ return redis.call('del', KEYS[1])
1112
+ else
1113
+ return 0
1114
+ end
1115
+ `;
1116
+ var acquireLease = async (name, ttlMs) => {
1117
+ const token = randomBytes(16).toString("hex");
1118
+ const [error, result] = await tryCatch(
1119
+ async () => redis.set(leaseKey(name), token, "PX", ttlMs, "NX")
1120
+ );
1121
+ if (error) return null;
1122
+ return result === "OK" ? token : null;
1123
+ };
1124
+ var renewLease = async (name, token, ttlMs) => {
1125
+ const [error, result] = await tryCatch(
1126
+ async () => redis.eval(RENEW_SCRIPT, 1, leaseKey(name), token, String(ttlMs))
1127
+ );
1128
+ if (error) return false;
1129
+ return result === 1;
1130
+ };
1131
+ var releaseLease = async (name, token) => {
1132
+ const [error, result] = await tryCatch(
1133
+ async () => redis.eval(RELEASE_SCRIPT, 1, leaseKey(name), token)
1134
+ );
1135
+ if (error) return false;
1136
+ return result === 1;
1137
+ };
1138
+
1139
+ // src/socketRedisAdapter.ts
1140
+ import { createAdapter } from "@socket.io/redis-adapter";
1141
+ var attachSocketRedisAdapter = (io) => {
1142
+ const pubClient = redis.duplicate();
1143
+ const subClient = redis.duplicate();
1144
+ pubClient.on("error", (err) => {
1145
+ getLogger().error("[socket-redis-adapter] pub client error", err);
1146
+ });
1147
+ subClient.on("error", (err) => {
1148
+ getLogger().error("[socket-redis-adapter] sub client error", err);
1149
+ });
1150
+ io.adapter(createAdapter(pubClient, subClient));
1151
+ };
1152
+
1153
+ // src/bootUuid.ts
1154
+ import { randomUUID } from "crypto";
1155
+ var BOOT_KEY_PREFIX = "luckystack:boot:";
1156
+ var DEFAULT_BOOT_KEY_TTL_SECONDS = 3600;
1157
+ var getBootKeyTtlSeconds = () => getDeployConfig().routing?.bootKeyTtlSeconds ?? DEFAULT_BOOT_KEY_TTL_SECONDS;
1158
+ var resolveEnvKey = () => {
1159
+ return process.env.LUCKYSTACK_ENV ?? process.env.NODE_ENV ?? "development";
1160
+ };
1161
+ var writeBootUuid = async (envKey) => {
1162
+ const key = envKey ?? resolveEnvKey();
1163
+ const uuid = randomUUID();
1164
+ await redis.set(`${BOOT_KEY_PREFIX}${key}`, uuid, "EX", getBootKeyTtlSeconds());
1165
+ return uuid;
1166
+ };
1167
+ var readBootUuid = async (envKey) => {
1168
+ const key = envKey ?? resolveEnvKey();
1169
+ return redis.get(`${BOOT_KEY_PREFIX}${key}`);
1170
+ };
1171
+
1172
+ // src/synchronizedEnvHashes.ts
1173
+ import { createHash } from "crypto";
1174
+ var hashValue = (value) => {
1175
+ return createHash("sha256").update(value).digest("hex");
1176
+ };
1177
+ var collectSynchronizedEnvKeys = () => {
1178
+ const keys = /* @__PURE__ */ new Set();
1179
+ for (const resource of Object.values(getDeployConfig().resources)) {
1180
+ for (const key of resource.synchronizedEnvKeys ?? []) {
1181
+ keys.add(key);
1182
+ }
1183
+ }
1184
+ return [...keys].toSorted();
1185
+ };
1186
+ var computeSynchronizedEnvHashes = () => {
1187
+ const keys = collectSynchronizedEnvKeys();
1188
+ const out = {};
1189
+ for (const key of keys) {
1190
+ const value = process.env[key];
1191
+ out[key] = value === void 0 ? null : hashValue(value);
1192
+ }
1193
+ return out;
1194
+ };
1195
+ var hashSynchronizedValue = (value) => hashValue(value);
1196
+
1197
+ // src/consoleLog.ts
1198
+ var COLORS = {
1199
+ black: "\x1B[30m",
1200
+ red: "\x1B[31m",
1201
+ green: "\x1B[32m",
1202
+ yellow: "\x1B[33m",
1203
+ blue: "\x1B[34m",
1204
+ magenta: "\x1B[35m",
1205
+ cyan: "\x1B[36m",
1206
+ white: "\x1B[37m",
1207
+ reset: "\x1B[0m"
1208
+ };
1209
+ var FILE_NAME_REGEX = /[\\/]([^\\/]+)$/;
1210
+ var extractFrameLabel = (frame) => {
1211
+ let lineInfo = frame.slice(frame.indexOf("(") + 1, frame.lastIndexOf(")"));
1212
+ if (lineInfo === "") lineInfo = frame;
1213
+ const match = FILE_NAME_REGEX.exec(lineInfo);
1214
+ const fileName = match?.[1] ?? lineInfo;
1215
+ return fileName.replace(/:\d+(?::\d+)?$/, "");
1216
+ };
1217
+ var initConsolelog = () => {
1218
+ const originalLog = console.log;
1219
+ console.log = (...args) => {
1220
+ const stackLines = new Error("console.log trace").stack?.split("\n") ?? [];
1221
+ const frame = stackLines.slice(1).find((line) => {
1222
+ const trimmed = line.trim();
1223
+ return trimmed.length > 0 && !trimmed.includes("consoleLog.ts") && !trimmed.includes("loggerRegistry.ts");
1224
+ })?.trim();
1225
+ if (!frame) {
1226
+ originalLog(...args);
1227
+ return;
1228
+ }
1229
+ const extractedInfo = extractFrameLabel(frame);
1230
+ let colorCode = COLORS.white;
1231
+ for (const key of Object.keys(COLORS)) {
1232
+ const index = args.indexOf(key);
1233
+ if (index !== -1) {
1234
+ colorCode = COLORS[key];
1235
+ args.splice(index, 1);
1236
+ break;
1237
+ }
1238
+ }
1239
+ if (typeof args[0] === "object") {
1240
+ originalLog(`${colorCode}${extractedInfo}${COLORS.reset}`);
1241
+ originalLog(...args);
1242
+ } else {
1243
+ originalLog(`${colorCode}${extractedInfo} -- ${args.join(" ")}${COLORS.reset}`);
1244
+ }
1245
+ };
1246
+ };
1247
+
1248
+ // src/redactedLogKeys.ts
1249
+ var DEFAULT_REDACTED_LOG_KEYS = [
1250
+ "password",
1251
+ "confirmpassword",
1252
+ "token",
1253
+ "newtoken",
1254
+ "authorization",
1255
+ "cookie",
1256
+ "set-cookie"
1257
+ ];
1258
+ var redactedKeys = new Set(DEFAULT_REDACTED_LOG_KEYS.map((key) => key.toLowerCase()));
1259
+ var registerRedactedLogKeys = (keys) => {
1260
+ for (const key of keys) {
1261
+ redactedKeys.add(key.toLowerCase());
1262
+ }
1263
+ };
1264
+ var getRedactedLogKeys = () => [...redactedKeys];
1265
+ var isRedactedLogKey = (key) => {
1266
+ return redactedKeys.has(key.toLowerCase());
1267
+ };
1268
+ var resetRedactedLogKeysForTests = () => {
1269
+ redactedKeys.clear();
1270
+ for (const key of DEFAULT_REDACTED_LOG_KEYS) {
1271
+ redactedKeys.add(key.toLowerCase());
1272
+ }
1273
+ };
1274
+
1275
+ // src/cookies.ts
1276
+ var escapeRegExp = (value) => {
1277
+ return value.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
1278
+ };
1279
+ var getCookieValue = (cookieHeader, cookieName) => {
1280
+ if (!cookieHeader || !cookieName) {
1281
+ return null;
1282
+ }
1283
+ const safeCookieName = escapeRegExp(cookieName);
1284
+ const cookieRegex = new RegExp(String.raw`(?:^|;\s*)${safeCookieName}=([^;]*)`);
1285
+ const match = cookieHeader.match(cookieRegex);
1286
+ const rawValue = match?.[1];
1287
+ if (!rawValue) {
1288
+ return null;
1289
+ }
1290
+ try {
1291
+ return decodeURIComponent(rawValue);
1292
+ } catch {
1293
+ return rawValue;
1294
+ }
1295
+ };
1296
+ var hasCookie = (cookieHeader, cookieName) => {
1297
+ return getCookieValue(cookieHeader, cookieName) !== null;
1298
+ };
1299
+
1300
+ // src/httpApiUtils.ts
1301
+ var inferHttpMethod = (apiName) => {
1302
+ const nameParts = apiName.split("/");
1303
+ const lastPart = nameParts.at(-1)?.toLowerCase() ?? "";
1304
+ const methodName = /^v\d+$/.test(lastPart) ? (nameParts.at(-2) ?? "").toLowerCase() : lastPart;
1305
+ if (methodName.startsWith("get") || methodName.startsWith("fetch") || methodName.startsWith("list")) {
1306
+ return "GET";
1307
+ }
1308
+ if (methodName.startsWith("delete") || methodName.startsWith("remove")) {
1309
+ return "DELETE";
1310
+ }
1311
+ if (methodName.startsWith("update") || methodName.startsWith("edit") || methodName.startsWith("patch")) {
1312
+ return "PUT";
1313
+ }
1314
+ return "POST";
1315
+ };
1316
+ var getEffectiveHttpMethod = (apiConfig, apiName) => {
1317
+ return apiConfig.httpMethod ?? inferHttpMethod(apiName);
1318
+ };
1319
+ var isMethodAllowed = (requestMethod, allowedMethod) => {
1320
+ return requestMethod === allowedMethod || requestMethod === "OPTIONS";
1321
+ };
1322
+
1323
+ // src/paths.ts
1324
+ import path from "path";
1325
+ import fs from "fs";
1326
+ import { fileURLToPath } from "url";
1327
+ var __filename = fileURLToPath(import.meta.url);
1328
+ var __dirname = path.dirname(__filename);
1329
+ var isWorkspaceRoot = (candidate) => {
1330
+ return fs.existsSync(path.join(candidate, "package.json")) && (fs.existsSync(path.join(candidate, "tsconfig.json")) || fs.existsSync(path.join(candidate, "tsconfig.server.json")));
1331
+ };
1332
+ var findWorkspaceRoot = (startDir) => {
1333
+ let current = path.resolve(startDir);
1334
+ while (true) {
1335
+ if (isWorkspaceRoot(current)) {
1336
+ return current;
1337
+ }
1338
+ const parent = path.dirname(current);
1339
+ if (parent === current) return null;
1340
+ current = parent;
1341
+ }
1342
+ };
1343
+ var ROOT_DIR = findWorkspaceRoot(process.cwd()) ?? findWorkspaceRoot(__dirname) ?? process.cwd();
1344
+ var resolveAgainstRoot = (relative) => path.isAbsolute(relative) ? relative : path.join(ROOT_DIR, relative);
1345
+ var getSrcDir = () => resolveAgainstRoot(getProjectConfig().paths.srcDir);
1346
+ var getServerDir = () => resolveAgainstRoot(getProjectConfig().paths.serverDir);
1347
+ var getSharedDir = () => resolveAgainstRoot(getProjectConfig().paths.sharedDir);
1348
+ var getUploadsDir = () => resolveAgainstRoot(getProjectConfig().paths.uploadsDir);
1349
+ var getPublicDir = () => resolveAgainstRoot(getProjectConfig().paths.publicDir);
1350
+ var getServerFunctionDirs = () => {
1351
+ const paths = getProjectConfig().paths;
1352
+ const arrayConfig = paths.serverFunctionDirs;
1353
+ if (Array.isArray(arrayConfig) && arrayConfig.length > 0) {
1354
+ return arrayConfig.map((p) => resolveAgainstRoot(p));
1355
+ }
1356
+ const legacySingular = paths.serverFunctionsDir;
1357
+ if (typeof legacySingular === "string" && legacySingular.length > 0) {
1358
+ return [resolveAgainstRoot(legacySingular)];
1359
+ }
1360
+ return [];
1361
+ };
1362
+ var getServerFunctionsDir = () => {
1363
+ const dirs = getServerFunctionDirs();
1364
+ return dirs[0] ?? resolveAgainstRoot("functions");
1365
+ };
1366
+ var getGeneratedSocketTypesPath = () => resolveAgainstRoot(getProjectConfig().paths.generatedSocketTypes);
1367
+ var getGeneratedApiSchemasPath = () => resolveAgainstRoot(getProjectConfig().paths.generatedApiSchemas);
1368
+ var getGeneratedApiDocsPath = () => resolveAgainstRoot(getProjectConfig().paths.generatedApiDocs);
1369
+ var SRC_DIR = path.join(ROOT_DIR, "src");
1370
+ var SERVER_DIR = path.join(ROOT_DIR, "server");
1371
+ var SHARED_DIR = path.join(ROOT_DIR, "shared");
1372
+ var UPLOADS_DIR = path.join(ROOT_DIR, "uploads");
1373
+ var PUBLIC_DIR = path.join(ROOT_DIR, "public");
1374
+ var SERVER_FUNCTIONS_DIR = path.join(SERVER_DIR, "functions");
1375
+ var GENERATED_SOCKET_TYPES_PATH = path.join(SRC_DIR, "_sockets", "apiTypes.generated.ts");
1376
+ var GENERATED_API_SCHEMAS_PATH = path.join(SRC_DIR, "_sockets", "apiInputSchemas.generated.ts");
1377
+ var GENERATED_API_DOCS_PATH = path.join(SRC_DIR, "docs", "apiDocs.generated.json");
1378
+ var TSCONFIG_ALIAS_FILES = ["tsconfig.server.json", "tsconfig.client.json"];
1379
+ var resolveFromRoot = (...segments) => path.join(ROOT_DIR, ...segments);
1380
+
1381
+ // src/socketMiddlewareRegistry.ts
1382
+ var middlewares = [];
1383
+ var registerSocketMiddleware = (mw) => {
1384
+ middlewares.push(mw);
1385
+ };
1386
+ var getSocketMiddlewares = () => middlewares;
1387
+ var clearSocketMiddlewares = () => {
1388
+ middlewares.length = 0;
1389
+ };
1390
+ var applySocketMiddlewares = (io) => {
1391
+ for (const mw of middlewares) {
1392
+ io.use(mw);
1393
+ }
1394
+ };
1395
+
1396
+ // src/csrfConfig.ts
1397
+ var DEFAULT_CSRF_CONFIG = {
1398
+ cookieName: "csrf-token",
1399
+ headerName: "x-csrf-token",
1400
+ tokenLength: 32,
1401
+ cookieOptions: {
1402
+ sameSite: "lax",
1403
+ secure: true,
1404
+ httpOnly: false,
1405
+ path: "/",
1406
+ maxAgeMs: 24 * 60 * 60 * 1e3
1407
+ }
1408
+ };
1409
+ var active = DEFAULT_CSRF_CONFIG;
1410
+ var registerCsrfConfig = (input) => {
1411
+ active = {
1412
+ ...active,
1413
+ ...input,
1414
+ cookieOptions: { ...active.cookieOptions, ...input.cookieOptions }
1415
+ };
1416
+ };
1417
+ var getCsrfConfig = () => active;
1418
+ var resetCsrfConfigForTests = () => {
1419
+ active = DEFAULT_CSRF_CONFIG;
1420
+ };
1421
+
1422
+ // src/serveAvatars.ts
1423
+ import path2 from "path";
1424
+ import { access } from "fs/promises";
1425
+ import fs2 from "fs";
1426
+
1427
+ // src/avatarConfig.ts
1428
+ var DEFAULT_AVATAR_CONFIG = {
1429
+ formats: [{ extension: "webp", contentType: "image/webp" }],
1430
+ cacheControl: "public, max-age=86400"
1431
+ };
1432
+ var activeConfig3 = DEFAULT_AVATAR_CONFIG;
1433
+ var registerAvatarConfig = (config) => {
1434
+ activeConfig3 = {
1435
+ formats: config.formats?.length ? config.formats : DEFAULT_AVATAR_CONFIG.formats,
1436
+ cacheControl: config.cacheControl ?? DEFAULT_AVATAR_CONFIG.cacheControl
1437
+ };
1438
+ };
1439
+ var getAvatarConfig = () => activeConfig3;
1440
+
1441
+ // src/serveAvatars.ts
1442
+ var FILE_ID_REGEX = /^[A-Za-z0-9_-]{1,128}$/;
1443
+ var serveAvatar = async ({
1444
+ routePath,
1445
+ res
1446
+ }) => {
1447
+ const uploadsFolder = getUploadsDir();
1448
+ const fileId = path2.basename(routePath, path2.extname(routePath));
1449
+ if (!fileId || !FILE_ID_REGEX.test(fileId)) {
1450
+ res.writeHead(404, { "Content-Type": "text/plain" });
1451
+ res.end("File not found");
1452
+ return;
1453
+ }
1454
+ const { formats, cacheControl } = getAvatarConfig();
1455
+ for (const { extension, contentType } of formats) {
1456
+ const filePath = path2.join(uploadsFolder, `${fileId}.${extension}`);
1457
+ const [accessError] = await tryCatch(() => access(filePath));
1458
+ if (accessError) continue;
1459
+ res.writeHead(200, {
1460
+ "Content-Type": contentType,
1461
+ "Cache-Control": cacheControl
1462
+ });
1463
+ fs2.createReadStream(filePath).pipe(res);
1464
+ return;
1465
+ }
1466
+ getLogger().debug("avatar: file not found", { routePath, fileId });
1467
+ res.writeHead(404, { "Content-Type": "text/plain" });
1468
+ res.end("File not found");
1469
+ };
1470
+
1471
+ // src/processUpload.ts
1472
+ var processUpload = async (input) => {
1473
+ const uploadKind = input.uploadKind ?? "avatar";
1474
+ const startSignal = await dispatchHook("onUploadStart", {
1475
+ userId: input.userId,
1476
+ contentType: input.contentType,
1477
+ sizeBytes: input.buffer.byteLength,
1478
+ uploadKind
1479
+ });
1480
+ if (startSignal.stopped) {
1481
+ return { status: "rejected", errorCode: startSignal.signal.errorCode };
1482
+ }
1483
+ const [error, finalSize] = await tryCatch(() => input.encodeAndSave(input.buffer));
1484
+ if (error) {
1485
+ return { status: "error", reason: error.message || "encode-failed", cause: error };
1486
+ }
1487
+ const sizeBytes = finalSize ?? input.buffer.byteLength;
1488
+ await dispatchHook("onUploadComplete", {
1489
+ userId: input.userId,
1490
+ fileName: input.fileName,
1491
+ sizeBytes,
1492
+ uploadKind
1493
+ });
1494
+ return { status: "success", sizeBytes };
1495
+ };
1496
+
1497
+ // src/bindAddress.ts
1498
+ var registered2 = null;
1499
+ var registerBindAddress = (address) => {
1500
+ registered2 = address;
1501
+ };
1502
+ var getBindAddress = () => {
1503
+ if (registered2) {
1504
+ return { ip: registered2.ip, port: String(registered2.port) };
1505
+ }
1506
+ return {
1507
+ ip: process.env.SERVER_IP ?? "127.0.0.1",
1508
+ port: process.env.SERVER_PORT ?? ""
1509
+ };
1510
+ };
1511
+
1512
+ // src/getParams.ts
1513
+ async function getParams({ method, req, res: _res, queryString }) {
1514
+ if (method == "GET") {
1515
+ return Object.fromEntries(new URLSearchParams(queryString || ""));
1516
+ }
1517
+ return new Promise((resolve, reject) => {
1518
+ const contentType = req.headers["content-type"] || "";
1519
+ const contentLengthHeader = req.headers["content-length"];
1520
+ const declaredLength = typeof contentLengthHeader === "string" ? Number(contentLengthHeader) : Number.NaN;
1521
+ const maxBodyBytes = getProjectConfig().http.requestBodyMaxBytes;
1522
+ if (Number.isFinite(declaredLength) && declaredLength > maxBodyBytes) {
1523
+ _res.setHeader("Content-Type", "application/json");
1524
+ _res.writeHead(413);
1525
+ _res.end(JSON.stringify({
1526
+ status: "error",
1527
+ httpStatus: 413,
1528
+ message: "api.payloadTooLarge",
1529
+ errorCode: "api.payloadTooLarge"
1530
+ }));
1531
+ resolve(null);
1532
+ return;
1533
+ }
1534
+ let body = "";
1535
+ let bodySize = 0;
1536
+ req.on("data", (chunk) => {
1537
+ bodySize += chunk.length;
1538
+ if (bodySize > maxBodyBytes) {
1539
+ _res.setHeader("Content-Type", "application/json");
1540
+ _res.writeHead(413);
1541
+ _res.end(JSON.stringify({
1542
+ status: "error",
1543
+ httpStatus: 413,
1544
+ message: "api.payloadTooLarge",
1545
+ errorCode: "api.payloadTooLarge"
1546
+ }));
1547
+ req.destroy();
1548
+ resolve(null);
1549
+ return;
1550
+ }
1551
+ body += chunk.toString();
1552
+ });
1553
+ req.on("end", async () => {
1554
+ if (contentType.startsWith("application/x-www-form-urlencoded")) {
1555
+ const parseData = () => {
1556
+ const data = new URLSearchParams(body);
1557
+ return Object.fromEntries(data);
1558
+ };
1559
+ const [, response] = await tryCatch(parseData);
1560
+ if (response) {
1561
+ resolve(response);
1562
+ return;
1563
+ }
1564
+ _res.setHeader("Content-Type", "application/json");
1565
+ _res.writeHead(400);
1566
+ _res.end(JSON.stringify({
1567
+ status: "error",
1568
+ httpStatus: 400,
1569
+ message: "api.invalidRequestFormat",
1570
+ errorCode: "api.invalidRequestFormat"
1571
+ }));
1572
+ resolve(null);
1573
+ return;
1574
+ }
1575
+ if (contentType.startsWith("application/json")) {
1576
+ const parseData = () => {
1577
+ return JSON.parse(body || "{}");
1578
+ };
1579
+ const [, response] = await tryCatch(parseData);
1580
+ if (response && typeof response === "object" && !Array.isArray(response)) {
1581
+ resolve(response);
1582
+ return;
1583
+ }
1584
+ _res.setHeader("Content-Type", "application/json");
1585
+ _res.writeHead(400);
1586
+ _res.end(JSON.stringify({
1587
+ status: "error",
1588
+ httpStatus: 400,
1589
+ message: "api.invalidRequestFormat",
1590
+ errorCode: "api.invalidRequestFormat"
1591
+ }));
1592
+ resolve(null);
1593
+ return;
1594
+ }
1595
+ _res.setHeader("Content-Type", "application/json");
1596
+ _res.writeHead(415);
1597
+ _res.end(JSON.stringify({
1598
+ status: "error",
1599
+ httpStatus: 415,
1600
+ message: "api.unsupportedMediaType",
1601
+ errorCode: "api.unsupportedMediaType"
1602
+ }));
1603
+ resolve(null);
1604
+ });
1605
+ req.on("error", (error) => {
1606
+ reject(error);
1607
+ });
1608
+ });
1609
+ }
1610
+
1611
+ // src/extractToken.ts
1612
+ var extractTokenFromSocket = (socket2) => {
1613
+ const cookie = socket2.handshake.headers.cookie;
1614
+ const sessionToken = typeof socket2.handshake.auth?.token === "string" ? socket2.handshake.auth.token : null;
1615
+ const cookieToken = getCookieValue(cookie, getProjectConfig().http.sessionCookieName);
1616
+ if (getProjectConfig().session.basedToken) {
1617
+ return sessionToken ?? cookieToken;
1618
+ }
1619
+ return cookieToken ?? sessionToken;
1620
+ };
1621
+
1622
+ // src/extractTokenFromRequest.ts
1623
+ var extractTokenFromRequest = (req) => {
1624
+ const authHeader = req.headers.authorization;
1625
+ const bearerToken = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
1626
+ const cookieToken = getCookieValue(req.headers.cookie, getProjectConfig().http.sessionCookieName);
1627
+ if (getProjectConfig().session.basedToken) {
1628
+ return bearerToken ?? cookieToken;
1629
+ }
1630
+ return cookieToken ?? bearerToken;
1631
+ };
1632
+
1633
+ // src/validateRequest.ts
1634
+ var isFalsy = (value) => {
1635
+ return value === false || value === 0 || value === 0n || value === "" || value === null || value === void 0 || typeof value === "number" && Number.isNaN(value);
1636
+ };
1637
+ var validateRequest = ({
1638
+ auth,
1639
+ user
1640
+ }) => {
1641
+ if (!auth.additional) {
1642
+ return { status: "success" };
1643
+ }
1644
+ const forbid = (key) => ({
1645
+ status: "error",
1646
+ errorCode: "auth.forbidden",
1647
+ errorParams: [{ key: "key", value: key }],
1648
+ httpStatus: 403
1649
+ });
1650
+ for (const condition of auth.additional) {
1651
+ if (!(condition.key in user)) {
1652
+ return {
1653
+ status: "error",
1654
+ errorCode: "auth.invalidCondition",
1655
+ errorParams: [{ key: "key", value: condition.key }],
1656
+ httpStatus: 500
1657
+ };
1658
+ }
1659
+ const val = user[condition.key];
1660
+ const isNullish = val === null || val === void 0;
1661
+ if (typeof condition.nullish === "boolean" && condition.nullish !== isNullish) {
1662
+ return forbid(condition.key);
1663
+ }
1664
+ if (condition.type && !isNullish && typeof val !== condition.type) {
1665
+ return forbid(condition.key);
1666
+ }
1667
+ if ("value" in condition && val !== condition.value) {
1668
+ return forbid(condition.key);
1669
+ }
1670
+ if (typeof condition.mustBeFalsy === "boolean" && condition.mustBeFalsy !== isFalsy(val)) {
1671
+ return forbid(condition.key);
1672
+ }
1673
+ }
1674
+ return { status: "success" };
1675
+ };
1676
+
1677
+ // src/rateLimiter.ts
1678
+ var rateLimitStore = /* @__PURE__ */ new Map();
1679
+ var getRedisPrefix = () => formatKey(`:${getProjectConfig().rateLimiting.redisKeyPrefix}`, "");
1680
+ var isRedisMode = () => getProjectConfig().rateLimiting.store === "redis";
1681
+ var isRateLimitingEnabled = () => getProjectConfig().rateLimiting.enabled;
1682
+ var buildAllowedResult = (limit) => ({
1683
+ allowed: true,
1684
+ remaining: Math.max(0, limit),
1685
+ resetIn: 0
1686
+ });
1687
+ var redisFallbackLogged = false;
1688
+ var RATE_LIMIT_INCREMENT_SCRIPT = `
1689
+ local current = redis.call('INCR', KEYS[1])
1690
+ if current == 1 then
1691
+ redis.call('PEXPIRE', KEYS[1], ARGV[1])
1692
+ end
1693
+ local ttl = redis.call('PTTL', KEYS[1])
1694
+ return { current, ttl }
1695
+ `;
1696
+ var normalizeWindowMs = (windowMs) => {
1697
+ if (!Number.isFinite(windowMs) || windowMs <= 0) {
1698
+ return getProjectConfig().rateLimiting.windowMs;
1699
+ }
1700
+ return windowMs;
1701
+ };
1702
+ var getRedisRateLimitKey = (key) => `${getRedisPrefix()}:${key}`;
1703
+ var checkRateLimitInMemory = ({
1704
+ key,
1705
+ limit,
1706
+ windowMs = 6e4
1707
+ }) => {
1708
+ const safeWindowMs = normalizeWindowMs(windowMs);
1709
+ const now = Date.now();
1710
+ const entry = rateLimitStore.get(key);
1711
+ if (!entry || entry.resetAt < now) {
1712
+ rateLimitStore.set(key, { count: 1, resetAt: now + safeWindowMs });
1713
+ return { allowed: true, remaining: limit - 1, resetIn: Math.ceil(safeWindowMs / 1e3) };
1714
+ }
1715
+ entry.count += 1;
1716
+ return {
1717
+ allowed: entry.count <= limit,
1718
+ remaining: Math.max(0, limit - entry.count),
1719
+ resetIn: Math.ceil((entry.resetAt - now) / 1e3)
1720
+ };
1721
+ };
1722
+ var checkRateLimitInRedis = async ({
1723
+ key,
1724
+ limit,
1725
+ windowMs = 6e4
1726
+ }) => {
1727
+ const safeWindowMs = normalizeWindowMs(windowMs);
1728
+ const redisKey = getRedisRateLimitKey(key);
1729
+ const [evalError, evalResponse] = await tryCatch(
1730
+ async () => await redis.eval(RATE_LIMIT_INCREMENT_SCRIPT, 1, redisKey, String(safeWindowMs))
1731
+ );
1732
+ if (evalError || !Array.isArray(evalResponse) || evalResponse.length < 2) {
1733
+ return null;
1734
+ }
1735
+ const requestCount = Number(evalResponse[0]);
1736
+ const ttlMs = Number(evalResponse[1]);
1737
+ const safeCount = Number.isFinite(requestCount) ? requestCount : 1;
1738
+ const safeTtlMs = Number.isFinite(ttlMs) && ttlMs > 0 ? ttlMs : safeWindowMs;
1739
+ return {
1740
+ allowed: safeCount <= limit,
1741
+ remaining: Math.max(0, limit - safeCount),
1742
+ resetIn: Math.ceil(safeTtlMs / 1e3)
1743
+ };
1744
+ };
1745
+ var getRateLimitStatusInMemory = (key, limit) => {
1746
+ const now = Date.now();
1747
+ const entry = rateLimitStore.get(key);
1748
+ if (!entry || entry.resetAt < now) {
1749
+ return { allowed: true, remaining: limit, resetIn: 0 };
1750
+ }
1751
+ return {
1752
+ allowed: entry.count < limit,
1753
+ remaining: Math.max(0, limit - entry.count),
1754
+ resetIn: Math.ceil((entry.resetAt - now) / 1e3)
1755
+ };
1756
+ };
1757
+ var getRateLimitStatusInRedis = async (key, limit) => {
1758
+ const redisKey = getRedisRateLimitKey(key);
1759
+ const [txError, txResponse] = await tryCatch(
1760
+ async () => await redis.multi().get(redisKey).pttl(redisKey).exec()
1761
+ );
1762
+ if (txError || !txResponse || txResponse.length < 2) return null;
1763
+ const getResult = txResponse[0];
1764
+ const ttlResult = txResponse[1];
1765
+ if (!getResult || !ttlResult || getResult[0] || ttlResult[0]) return null;
1766
+ const currentCount = Number(getResult[1] ?? 0);
1767
+ const ttlMs = Number(ttlResult[1] ?? 0);
1768
+ if (currentCount <= 0 || ttlMs <= 0) {
1769
+ return { allowed: true, remaining: limit, resetIn: 0 };
1770
+ }
1771
+ return {
1772
+ allowed: currentCount < limit,
1773
+ remaining: Math.max(0, limit - currentCount),
1774
+ resetIn: Math.ceil(ttlMs / 1e3)
1775
+ };
1776
+ };
1777
+ var logRedisFallback = () => {
1778
+ if (redisFallbackLogged) return;
1779
+ redisFallbackLogged = true;
1780
+ getLogger().warn("[RateLimiter] Redis mode unavailable, falling back to in-memory mode");
1781
+ };
1782
+ var defaultRateLimitStrategy = {
1783
+ name: "default-memory-redis",
1784
+ async check(params) {
1785
+ if (!isRateLimitingEnabled()) return buildAllowedResult(params.limit);
1786
+ if (isRedisMode()) {
1787
+ const redisResult = await checkRateLimitInRedis(params);
1788
+ if (redisResult) return redisResult;
1789
+ logRedisFallback();
1790
+ }
1791
+ return checkRateLimitInMemory(params);
1792
+ },
1793
+ async getStatus(key, limit) {
1794
+ if (!isRateLimitingEnabled()) return buildAllowedResult(limit);
1795
+ if (isRedisMode()) {
1796
+ const redisResult = await getRateLimitStatusInRedis(key, limit);
1797
+ if (redisResult) return redisResult;
1798
+ logRedisFallback();
1799
+ }
1800
+ return getRateLimitStatusInMemory(key, limit);
1801
+ },
1802
+ async clear(key) {
1803
+ if (isRedisMode()) {
1804
+ await tryCatch(async () => await redis.del(getRedisRateLimitKey(key)));
1805
+ }
1806
+ rateLimitStore.delete(key);
1807
+ },
1808
+ async clearAll() {
1809
+ if (isRedisMode()) {
1810
+ let cursor = "0";
1811
+ do {
1812
+ const [scanError, scanResponse] = await tryCatch(
1813
+ async () => await redis.scan(cursor, "MATCH", `${getRedisPrefix()}:*`, "COUNT", 100)
1814
+ );
1815
+ if (scanError || !scanResponse) break;
1816
+ cursor = scanResponse[0];
1817
+ const keys = scanResponse[1];
1818
+ if (keys.length > 0) {
1819
+ await tryCatch(async () => await redis.del(...keys));
1820
+ }
1821
+ } while (cursor !== "0");
1822
+ }
1823
+ rateLimitStore.clear();
1824
+ }
1825
+ };
1826
+ var activeStrategy = defaultRateLimitStrategy;
1827
+ var registerRateLimitStrategy = (strategy) => {
1828
+ activeStrategy = strategy;
1829
+ getLogger().debug(`[RateLimiter] active strategy \u2192 ${strategy.name}`);
1830
+ };
1831
+ var getRateLimitStrategy = () => activeStrategy;
1832
+ var checkRateLimit = async (params) => activeStrategy.check(params);
1833
+ var getRateLimitStatus = async (key, limit) => activeStrategy.getStatus(key, limit);
1834
+ var clearRateLimit = async (key) => activeStrategy.clear(key);
1835
+ var clearAllRateLimits = async () => activeStrategy.clearAll();
1836
+ var scheduleCleanup = () => {
1837
+ const intervalMs = getProjectConfig().rateLimiting.cleanupIntervalMs;
1838
+ const timer = setTimeout(() => {
1839
+ const now = Date.now();
1840
+ let cleaned = 0;
1841
+ for (const [key, entry] of rateLimitStore) {
1842
+ if (entry.resetAt < now) {
1843
+ rateLimitStore.delete(key);
1844
+ cleaned++;
1845
+ }
1846
+ }
1847
+ if (cleaned > 0) {
1848
+ getLogger().debug(`[RateLimiter] Cleaned ${String(cleaned)} expired entries`);
1849
+ }
1850
+ scheduleCleanup();
1851
+ }, intervalMs);
1852
+ timer.unref();
1853
+ };
1854
+ scheduleCleanup();
1855
+
1856
+ // src/runtimeTypeValidation.ts
1857
+ var splitTopLevel = (value, splitter) => {
1858
+ const items = [];
1859
+ let depthParen = 0;
1860
+ let depthBrace = 0;
1861
+ let depthBracket = 0;
1862
+ let token = "";
1863
+ for (const char of value) {
1864
+ if (char === "(") depthParen += 1;
1865
+ if (char === ")") depthParen -= 1;
1866
+ if (char === "{") depthBrace += 1;
1867
+ if (char === "}") depthBrace -= 1;
1868
+ if (char === "[") depthBracket += 1;
1869
+ if (char === "]") depthBracket -= 1;
1870
+ if (char === splitter && depthParen === 0 && depthBrace === 0 && depthBracket === 0) {
1871
+ items.push(token.trim());
1872
+ token = "";
1873
+ continue;
1874
+ }
1875
+ token += char;
1876
+ }
1877
+ if (token.trim()) items.push(token.trim());
1878
+ return items;
1879
+ };
1880
+ var parseObjectFields = (typeText) => {
1881
+ const clean = typeText.trim();
1882
+ if (!clean.startsWith("{") || !clean.endsWith("}")) {
1883
+ return { fields: [], indexSignatures: [] };
1884
+ }
1885
+ const inner = clean.slice(1, -1);
1886
+ const fields = [];
1887
+ const indexSignatures = [];
1888
+ let part = "";
1889
+ let depth = 0;
1890
+ for (const char of inner) {
1891
+ if (char === "{" || char === "[" || char === "(" || char === "<") depth += 1;
1892
+ if (char === "}" || char === "]" || char === ")" || char === ">") depth -= 1;
1893
+ if (char === ";" && depth === 0) {
1894
+ const trimmed = part.trim();
1895
+ if (trimmed) {
1896
+ const fieldMatch = /^("|')?[A-Za-z_][A-Za-z0-9_]*("|')?(\?)?\s*:\s*([\s\S]+)$/.exec(trimmed);
1897
+ if (fieldMatch) {
1898
+ const keyMatch = /^("|')?[A-Za-z_][A-Za-z0-9_]*("|')?/.exec(trimmed);
1899
+ const rawKey = keyMatch?.[0] ?? "";
1900
+ fields.push({
1901
+ key: rawKey.replaceAll(/^['"]|['"]$/g, ""),
1902
+ optional: Boolean(fieldMatch[3]),
1903
+ type: (fieldMatch[4] ?? "").trim()
1904
+ });
1905
+ } else {
1906
+ const indexMatch = /^\[\s*([A-Za-z_][A-Za-z0-9_]*)\s*:\s*([^\]]+)\]\s*:\s*([\s\S]+)$/.exec(trimmed);
1907
+ if (indexMatch) {
1908
+ indexSignatures.push({
1909
+ keyName: (indexMatch[1] ?? "").trim(),
1910
+ keyType: (indexMatch[2] ?? "").trim(),
1911
+ type: (indexMatch[3] ?? "").trim()
1912
+ });
1913
+ }
1914
+ }
1915
+ }
1916
+ part = "";
1917
+ continue;
1918
+ }
1919
+ part += char;
1920
+ }
1921
+ const final = part.trim();
1922
+ if (final) {
1923
+ const fieldMatch = /^("|')?[A-Za-z_][A-Za-z0-9_]*("|')?(\?)?\s*:\s*([\s\S]+)$/.exec(final);
1924
+ if (fieldMatch) {
1925
+ const keyMatch = /^("|')?[A-Za-z_][A-Za-z0-9_]*("|')?/.exec(final);
1926
+ const rawKey = keyMatch?.[0] ?? "";
1927
+ fields.push({
1928
+ key: rawKey.replaceAll(/^['"]|['"]$/g, ""),
1929
+ optional: Boolean(fieldMatch[3]),
1930
+ type: (fieldMatch[4] ?? "").trim()
1931
+ });
1932
+ } else {
1933
+ const indexMatch = /^\[\s*([A-Za-z_][A-Za-z0-9_]*)\s*:\s*([^\]]+)\]\s*:\s*([\s\S]+)$/.exec(final);
1934
+ if (indexMatch) {
1935
+ indexSignatures.push({
1936
+ keyName: (indexMatch[1] ?? "").trim(),
1937
+ keyType: (indexMatch[2] ?? "").trim(),
1938
+ type: (indexMatch[3] ?? "").trim()
1939
+ });
1940
+ }
1941
+ }
1942
+ }
1943
+ return { fields, indexSignatures };
1944
+ };
1945
+ var matchesIndexKeyType = ({ keyType, key }) => {
1946
+ const normalized = keyType.trim();
1947
+ if (normalized === "string") return true;
1948
+ if (normalized === "number") return /^-?\d+(\.\d+)?$/.test(key);
1949
+ if (normalized.includes("|")) {
1950
+ const parts = splitTopLevel(normalized, "|").map((part) => part.trim());
1951
+ return parts.some((part) => matchesIndexKeyType({ keyType: part, key }));
1952
+ }
1953
+ if (normalized.startsWith("'") && normalized.endsWith("'") || normalized.startsWith('"') && normalized.endsWith('"')) {
1954
+ return key === normalized.slice(1, -1);
1955
+ }
1956
+ return false;
1957
+ };
1958
+ var isPrimitiveMatch = (type, value) => {
1959
+ if (type === "string") return typeof value === "string";
1960
+ if (type === "number") return typeof value === "number" && Number.isFinite(value);
1961
+ if (type === "boolean") return typeof value === "boolean";
1962
+ if (type === "true") return value === true;
1963
+ if (type === "false") return value === false;
1964
+ if (type === "null") return value === null;
1965
+ if (type === "undefined") return value === void 0;
1966
+ if (type === "Date") return typeof value === "string" || value instanceof Date;
1967
+ return false;
1968
+ };
1969
+ var isPrimitiveType = (type) => {
1970
+ return ["string", "number", "boolean", "true", "false", "null", "undefined", "Date"].includes(type);
1971
+ };
1972
+ var validateType = (typeText, value, path3) => {
1973
+ const type = typeText.trim();
1974
+ if (type.startsWith("__RUNTIME_UNRESOLVED__::")) {
1975
+ const unresolvedMessage = type.replace("__RUNTIME_UNRESOLVED__::", "").trim();
1976
+ return { status: "error", message: `${path3}: ${unresolvedMessage}` };
1977
+ }
1978
+ if (type.startsWith("(") && type.endsWith(")")) {
1979
+ return validateType(type.slice(1, -1), value, path3);
1980
+ }
1981
+ if (type.includes("|")) {
1982
+ const unionParts = splitTopLevel(type, "|").filter(Boolean);
1983
+ if (unionParts.length > 1) {
1984
+ for (const unionType of unionParts) {
1985
+ const result = validateType(unionType, value, path3);
1986
+ if (result.status === "success") return result;
1987
+ }
1988
+ return { status: "error", message: `${path3} does not match union type ${type}` };
1989
+ }
1990
+ }
1991
+ if (type.includes("&")) {
1992
+ const intersectionParts = splitTopLevel(type, "&").filter(Boolean);
1993
+ if (intersectionParts.length > 1) {
1994
+ for (const intersectionType of intersectionParts) {
1995
+ const result = validateType(intersectionType, value, path3);
1996
+ if (result.status === "error") return result;
1997
+ }
1998
+ return { status: "success" };
1999
+ }
2000
+ }
2001
+ if (type.endsWith("[]")) {
2002
+ if (!Array.isArray(value)) {
2003
+ return { status: "error", message: `${path3} should be an array` };
2004
+ }
2005
+ const itemType = type.slice(0, -2).trim();
2006
+ if (itemType === type) {
2007
+ return { status: "success" };
2008
+ }
2009
+ for (const [index, element] of value.entries()) {
2010
+ const result = validateType(itemType, element, `${path3}[${String(index)}]`);
2011
+ if (result.status === "error") return result;
2012
+ }
2013
+ return { status: "success" };
2014
+ }
2015
+ if (type.startsWith("'") && type.endsWith("'") || type.startsWith('"') && type.endsWith('"')) {
2016
+ const literal = type.slice(1, -1);
2017
+ return value === literal ? { status: "success" } : { status: "error", message: `${path3} should equal ${literal}` };
2018
+ }
2019
+ if (isPrimitiveMatch(type, value)) {
2020
+ return { status: "success" };
2021
+ }
2022
+ if (isPrimitiveType(type)) {
2023
+ const expectedType = type === "Date" ? "Date (ISO string or Date)" : type;
2024
+ return { status: "error", message: `${path3} should be ${expectedType}` };
2025
+ }
2026
+ if (type === "any" || type === "unknown") {
2027
+ return { status: "success" };
2028
+ }
2029
+ if (/^Record<.+>$/.test(type)) {
2030
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
2031
+ return { status: "success" };
2032
+ }
2033
+ return { status: "error", message: `${path3} should be an object` };
2034
+ }
2035
+ if (type.startsWith("{") && type.endsWith("}")) {
2036
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
2037
+ return { status: "error", message: `${path3} should be an object` };
2038
+ }
2039
+ const { fields, indexSignatures } = parseObjectFields(type);
2040
+ const input = value;
2041
+ for (const field of fields) {
2042
+ const fieldValue = input[field.key];
2043
+ if (fieldValue === void 0) {
2044
+ if (field.optional) continue;
2045
+ return { status: "error", message: `${path3}.${field.key} is required` };
2046
+ }
2047
+ const result = validateType(field.type, fieldValue, `${path3}.${field.key}`);
2048
+ if (result.status === "error") return result;
2049
+ }
2050
+ const allowedKeys = new Set(fields.map((field) => field.key));
2051
+ for (const key of Object.keys(input)) {
2052
+ if (allowedKeys.has(key)) {
2053
+ continue;
2054
+ }
2055
+ if (indexSignatures.length === 0) {
2056
+ return { status: "error", message: `${path3}.${key} is not allowed` };
2057
+ }
2058
+ const indexValue = input[key];
2059
+ let matched = false;
2060
+ for (const indexSignature of indexSignatures) {
2061
+ if (!matchesIndexKeyType({ keyType: indexSignature.keyType, key })) {
2062
+ continue;
2063
+ }
2064
+ const indexResult = validateType(indexSignature.type, indexValue, `${path3}.${key}`);
2065
+ if (indexResult.status === "success") {
2066
+ matched = true;
2067
+ break;
2068
+ }
2069
+ }
2070
+ if (!matched) {
2071
+ return { status: "error", message: `${path3}.${key} is not allowed` };
2072
+ }
2073
+ }
2074
+ return { status: "success" };
2075
+ }
2076
+ if (/^[A-Za-z_][A-Za-z0-9_]*(?:<.+>)?$/.test(type)) {
2077
+ if (/^[A-Za-z_][A-Za-z0-9_]*<.+>$/.test(type)) {
2078
+ return { status: "error", message: `${path3}: unresolved utility ${type}` };
2079
+ }
2080
+ return { status: "error", message: `${path3}: unresolved type ${type}` };
2081
+ }
2082
+ return { status: "success" };
2083
+ };
2084
+ var validateInputByType = async ({
2085
+ typeText,
2086
+ value,
2087
+ rootKey,
2088
+ filePath
2089
+ }) => {
2090
+ if (!typeText || typeText.trim() === "" || typeText.trim() === "any") {
2091
+ return { status: "success" };
2092
+ }
2093
+ if (process.env.NODE_ENV === "production") {
2094
+ return { status: "success" };
2095
+ }
2096
+ const devkitModuleId = "@luckystack/devkit";
2097
+ const devkit = await import(devkitModuleId);
2098
+ const resolvedType = devkit.resolveRuntimeTypeText({ typeText, filePath });
2099
+ if (resolvedType.status === "error") {
2100
+ return { status: "error", message: `${rootKey}: ${resolvedType.message}` };
2101
+ }
2102
+ return validateType(resolvedType.typeText, value, rootKey);
2103
+ };
2104
+
2105
+ // src/checkOrigin.ts
2106
+ var normalizeOrigin = ({ value, secure }) => {
2107
+ const trimmedValue = value.trim().toLowerCase();
2108
+ if (!trimmedValue) {
2109
+ return "";
2110
+ }
2111
+ const withProtocol = trimmedValue.startsWith("http://") || trimmedValue.startsWith("https://") ? trimmedValue : `http${secure ? "s" : ""}://${trimmedValue}`;
2112
+ const extractedOrigin = /^(https?:\/\/[^/?#]+)/.exec(withProtocol)?.[1] || "";
2113
+ if (!extractedOrigin) {
2114
+ return "";
2115
+ }
2116
+ return extractedOrigin.replace(/^http:\/\/(.+):80$/i, "http://$1").replace(/^https:\/\/(.+):443$/i, "https://$1");
2117
+ };
2118
+ var isLocalhostOrigin = (normalized) => {
2119
+ return /^https?:\/\/localhost(:\d+)?$/i.test(normalized);
2120
+ };
2121
+ var allowedOrigin = (origin) => {
2122
+ const secure = process.env.SECURE === "true";
2123
+ const cors = getProjectConfig().http.cors;
2124
+ const configured = cors.allowedOrigins;
2125
+ const { ip: bindIp, port: bindPort } = getBindAddress();
2126
+ const location = bindPort ? `http${secure ? "s" : ""}://${bindIp}:${bindPort}` : `http${secure ? "s" : ""}://${bindIp}`;
2127
+ const normalizedOrigin = normalizeOrigin({ value: origin, secure });
2128
+ if (cors.allowLocalhost && normalizedOrigin && isLocalhostOrigin(normalizedOrigin)) {
2129
+ return true;
2130
+ }
2131
+ if (typeof configured === "function") {
2132
+ if (normalizedOrigin === location) return true;
2133
+ if (configured(origin)) return true;
2134
+ } else {
2135
+ const normalizedAllowedOrigins = new Set(
2136
+ [location, ...configured].map((value) => normalizeOrigin({ value, secure })).filter(Boolean)
2137
+ );
2138
+ if (normalizedOrigin && normalizedAllowedOrigins.has(normalizedOrigin)) {
2139
+ return true;
2140
+ }
2141
+ }
2142
+ const debugAllowedOrigins = typeof configured === "function" ? ["<dynamic-resolver>", location] : [location, ...configured].map((value) => normalizeOrigin({ value, secure })).filter(Boolean);
2143
+ if (getProjectConfig().logging.devLogs) {
2144
+ getLogger().warn("cors: origin not allowed", {
2145
+ origin,
2146
+ normalizedOrigin,
2147
+ allowedOrigins: debugAllowedOrigins,
2148
+ allowLocalhost: cors.allowLocalhost
2149
+ });
2150
+ }
2151
+ void dispatchHook("corsRejected", {
2152
+ origin,
2153
+ normalizedOrigin,
2154
+ allowedOrigins: debugAllowedOrigins,
2155
+ allowLocalhost: cors.allowLocalhost
2156
+ });
2157
+ return false;
2158
+ };
2159
+ var checkOrigin_default = allowedOrigin;
2160
+
2161
+ // src/offlineQueue.ts
2162
+ var apiQueue = [];
2163
+ var syncQueue = [];
2164
+ var isFlushingApi = false;
2165
+ var isFlushingSync = false;
2166
+ var isOnline = () => {
2167
+ if (typeof navigator === "undefined") return true;
2168
+ return navigator.onLine;
2169
+ };
2170
+ var evictExpired = (queue) => {
2171
+ const maxAgeMs = getProjectConfig().offlineQueue.maxAgeMs;
2172
+ if (maxAgeMs <= 0) return;
2173
+ const cutoff = Date.now() - maxAgeMs;
2174
+ for (let i = queue.length - 1; i >= 0; i -= 1) {
2175
+ const item = queue[i];
2176
+ if (item && item.createdAt < cutoff) {
2177
+ queue.splice(i, 1);
2178
+ }
2179
+ }
2180
+ };
2181
+ var enqueueWithPolicy = (queue, item, label) => {
2182
+ evictExpired(queue);
2183
+ const { maxSize, dropPolicy: globalPolicy } = getProjectConfig().offlineQueue;
2184
+ const effectivePolicy = item.dropPolicy ?? globalPolicy;
2185
+ if (queue.length < maxSize) {
2186
+ queue.push(item);
2187
+ return true;
2188
+ }
2189
+ if (effectivePolicy === "drop-oldest") {
2190
+ queue.shift();
2191
+ queue.push(item);
2192
+ getLogger().debug(`offlineQueue:${label} full \u2014 dropped oldest item (policy=${effectivePolicy})`);
2193
+ return true;
2194
+ }
2195
+ if (effectivePolicy === "drop-newest") {
2196
+ getLogger().debug(`offlineQueue:${label} full \u2014 dropped newest item (policy=${effectivePolicy})`);
2197
+ return false;
2198
+ }
2199
+ getLogger().warn(`offlineQueue:${label} full \u2014 rejecting enqueue (policy=${effectivePolicy})`);
2200
+ return false;
2201
+ };
2202
+ var enqueueApiRequest = (item) => enqueueWithPolicy(apiQueue, item, "api");
2203
+ var enqueueSyncRequest = (item) => enqueueWithPolicy(syncQueue, item, "sync");
2204
+ var removeApiQueueItem = (id) => {
2205
+ const index = apiQueue.findIndex((item) => item.id === id);
2206
+ if (index !== -1) {
2207
+ apiQueue.splice(index, 1);
2208
+ }
2209
+ };
2210
+ var removeSyncQueueItem = (id) => {
2211
+ const index = syncQueue.findIndex((item) => item.id === id);
2212
+ if (index !== -1) {
2213
+ syncQueue.splice(index, 1);
2214
+ }
2215
+ };
2216
+ var removeApiQueueItemsByKey = (key) => {
2217
+ for (let i = apiQueue.length - 1; i >= 0; i -= 1) {
2218
+ if (apiQueue[i]?.key === key) {
2219
+ apiQueue.splice(i, 1);
2220
+ }
2221
+ }
2222
+ };
2223
+ var flushApiQueue = (canRun, socketInstance) => {
2224
+ if (isFlushingApi) return;
2225
+ evictExpired(apiQueue);
2226
+ if (apiQueue.length === 0) return;
2227
+ if (!canRun()) return;
2228
+ isFlushingApi = true;
2229
+ while (apiQueue.length > 0) {
2230
+ if (!canRun()) break;
2231
+ const item = apiQueue.shift();
2232
+ item?.run(socketInstance);
2233
+ }
2234
+ isFlushingApi = false;
2235
+ };
2236
+ var flushSyncQueue = (canRun, socketInstance) => {
2237
+ if (isFlushingSync) return;
2238
+ evictExpired(syncQueue);
2239
+ if (syncQueue.length === 0) return;
2240
+ if (!canRun()) return;
2241
+ isFlushingSync = true;
2242
+ while (syncQueue.length > 0) {
2243
+ if (!canRun()) break;
2244
+ const item = syncQueue.shift();
2245
+ item?.run(socketInstance);
2246
+ }
2247
+ isFlushingSync = false;
2248
+ };
2249
+ var getApiQueueSize = () => apiQueue.length;
2250
+ var getSyncQueueSize = () => syncQueue.length;
2251
+
2252
+ // src/socketState.ts
2253
+ var socket = null;
2254
+ var setSocket = (next) => {
2255
+ socket = next;
2256
+ };
2257
+ var responseIndex = 0;
2258
+ var WAIT_FOR_SOCKET_INTERVAL_MS = 10;
2259
+ var WAIT_FOR_SOCKET_MAX_ITERATIONS = 500;
2260
+ var incrementResponseIndex = () => {
2261
+ responseIndex = responseIndex + 1;
2262
+ return responseIndex;
2263
+ };
2264
+ var waitForSocket = async () => {
2265
+ let i = 0;
2266
+ while (!socket) {
2267
+ await new Promise((resolve) => setTimeout(resolve, WAIT_FOR_SOCKET_INTERVAL_MS));
2268
+ i++;
2269
+ if (i > WAIT_FOR_SOCKET_MAX_ITERATIONS) {
2270
+ return null;
2271
+ }
2272
+ }
2273
+ return socket;
2274
+ };
2275
+
2276
+ // src/cancelRegistry.ts
2277
+ var syncControllers = /* @__PURE__ */ new Map();
2278
+ var apiControllers = /* @__PURE__ */ new Map();
2279
+ var buildKey = (socketId, identifier) => `${socketId}:${String(identifier)}`;
2280
+ var registerSyncAbortController = (socketId, cb, controller) => {
2281
+ const key = buildKey(socketId, cb);
2282
+ syncControllers.set(key, controller);
2283
+ return key;
2284
+ };
2285
+ var unregisterSyncAbortController = (key) => {
2286
+ syncControllers.delete(key);
2287
+ };
2288
+ var abortSyncByCb = (socketId, cb) => {
2289
+ const key = buildKey(socketId, cb);
2290
+ const controller = syncControllers.get(key);
2291
+ if (!controller) return false;
2292
+ controller.abort();
2293
+ syncControllers.delete(key);
2294
+ return true;
2295
+ };
2296
+ var registerApiAbortController = (socketId, responseIndex2, controller) => {
2297
+ const key = buildKey(socketId, responseIndex2);
2298
+ apiControllers.set(key, controller);
2299
+ return key;
2300
+ };
2301
+ var unregisterApiAbortController = (key) => {
2302
+ apiControllers.delete(key);
2303
+ };
2304
+ var abortApiByResponseIndex = (socketId, responseIndex2) => {
2305
+ const key = buildKey(socketId, responseIndex2);
2306
+ const controller = apiControllers.get(key);
2307
+ if (!controller) return false;
2308
+ controller.abort();
2309
+ apiControllers.delete(key);
2310
+ return true;
2311
+ };
2312
+ var abortAllForSocket = (socketId) => {
2313
+ const prefix = `${socketId}:`;
2314
+ for (const [key, controller] of syncControllers) {
2315
+ if (key.startsWith(prefix)) {
2316
+ controller.abort();
2317
+ syncControllers.delete(key);
2318
+ }
2319
+ }
2320
+ for (const [key, controller] of apiControllers) {
2321
+ if (key.startsWith(prefix)) {
2322
+ controller.abort();
2323
+ apiControllers.delete(key);
2324
+ }
2325
+ }
2326
+ };
2327
+
2328
+ // src/csrf.ts
2329
+ var cachedToken = null;
2330
+ var inflightFetch = null;
2331
+ var resolveBackendUrl = () => {
2332
+ if (globalThis.window !== void 0 && globalThis.location?.origin) {
2333
+ return globalThis.location.origin;
2334
+ }
2335
+ return "";
2336
+ };
2337
+ var fetchCsrfToken = async () => {
2338
+ const base = resolveBackendUrl();
2339
+ const [error, token] = await tryCatch(async () => {
2340
+ const response = await fetch(`${base}/auth/csrf`, {
2341
+ method: "GET",
2342
+ credentials: "include"
2343
+ });
2344
+ if (!response.ok) return null;
2345
+ const body = await response.json();
2346
+ return body.csrfToken ?? null;
2347
+ });
2348
+ if (error) return null;
2349
+ return token ?? null;
2350
+ };
2351
+ var getCsrfToken = async () => {
2352
+ if (getProjectConfig().session.basedToken) {
2353
+ return null;
2354
+ }
2355
+ if (cachedToken) return cachedToken;
2356
+ if (inflightFetch) return inflightFetch;
2357
+ inflightFetch = fetchCsrfToken().then((token) => {
2358
+ cachedToken = token;
2359
+ inflightFetch = null;
2360
+ return token;
2361
+ });
2362
+ return inflightFetch;
2363
+ };
2364
+ var clearCsrfToken = () => {
2365
+ cachedToken = null;
2366
+ inflightFetch = null;
2367
+ };
2368
+ var STATE_CHANGING = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
2369
+ var isCsrfMismatchResponse = async (response) => {
2370
+ if (response.status !== 403) return false;
2371
+ const [error, isMismatch] = await tryCatch(async () => {
2372
+ const cloned = response.clone();
2373
+ const body = await cloned.json();
2374
+ return body.errorCode === "auth.csrfMismatch";
2375
+ });
2376
+ if (error) return false;
2377
+ return isMismatch ?? false;
2378
+ };
2379
+ var httpFetch = async (input, init = {}) => {
2380
+ const method = (init.method ?? "GET").toUpperCase();
2381
+ const headers = new Headers(init.headers);
2382
+ if (!headers.has("Content-Type") && init.body && typeof init.body === "string") {
2383
+ headers.set("Content-Type", "application/json");
2384
+ }
2385
+ const csrfHeaderName = getCsrfConfig().headerName;
2386
+ const send = async (csrfToken) => {
2387
+ const finalHeaders = new Headers(headers);
2388
+ if (csrfToken && STATE_CHANGING.has(method) && !finalHeaders.has(csrfHeaderName)) {
2389
+ finalHeaders.set(csrfHeaderName, csrfToken);
2390
+ }
2391
+ return fetch(input, {
2392
+ ...init,
2393
+ method,
2394
+ headers: finalHeaders,
2395
+ credentials: init.credentials ?? "include"
2396
+ });
2397
+ };
2398
+ if (!STATE_CHANGING.has(method) || getProjectConfig().session.basedToken) {
2399
+ return send(null);
2400
+ }
2401
+ const token = await getCsrfToken();
2402
+ let response = await send(token);
2403
+ if (await isCsrfMismatchResponse(response)) {
2404
+ clearCsrfToken();
2405
+ const refreshed = await getCsrfToken();
2406
+ if (refreshed) {
2407
+ response = await send(refreshed);
2408
+ }
2409
+ }
2410
+ return response;
2411
+ };
2412
+ export {
2413
+ BOOT_KEY_PREFIX,
2414
+ DEFAULT_AVATAR_CONFIG,
2415
+ DEFAULT_CLIENT_KEY,
2416
+ DEFAULT_CSRF_CONFIG,
2417
+ DEFAULT_ENV_FILES,
2418
+ DEFAULT_PAGE_ROUTE_RULES,
2419
+ DEFAULT_PROJECT_CONFIG,
2420
+ GENERATED_API_DOCS_PATH,
2421
+ GENERATED_API_SCHEMAS_PATH,
2422
+ GENERATED_SOCKET_TYPES_PATH,
2423
+ INVALID_ERROR_RESPONSE_CODE,
2424
+ PUBLIC_DIR,
2425
+ ROOT_DIR,
2426
+ SERVER_DIR,
2427
+ SERVER_FUNCTIONS_DIR,
2428
+ SHARED_DIR,
2429
+ SRC_DIR,
2430
+ TSCONFIG_ALIAS_FILES,
2431
+ UPLOADS_DIR,
2432
+ abortAllForSocket,
2433
+ abortApiByResponseIndex,
2434
+ abortSyncByCb,
2435
+ acquireLease,
2436
+ checkOrigin_default as allowedOrigin,
2437
+ applyErrorFormatter,
2438
+ applySocketMiddlewares,
2439
+ applyStrayKeyPrefix,
2440
+ attachSocketRedisAdapter,
2441
+ bootstrapEnv,
2442
+ buildApiResponseEventName,
2443
+ buildApiStreamEventName,
2444
+ buildGetJoinedRoomsResponseEventName,
2445
+ buildJoinRoomResponseEventName,
2446
+ buildLeaveRoomResponseEventName,
2447
+ buildSyncProgressEventName,
2448
+ buildSyncResponseEventName,
2449
+ captureException,
2450
+ captureExceptionAcrossTrackers,
2451
+ captureMessage,
2452
+ captureMessageAcrossTrackers,
2453
+ checkRateLimit,
2454
+ clearAllHooks,
2455
+ clearAllRateLimits,
2456
+ clearCsrfToken,
2457
+ clearRateLimit,
2458
+ clearSocketMiddlewares,
2459
+ collectSynchronizedEnvKeys,
2460
+ computeSynchronizedEnvHashes,
2461
+ createDevLogger,
2462
+ createLocalizedNormalizer,
2463
+ defaultHttpStatusForResponse,
2464
+ defaultRateLimitStrategy,
2465
+ defaultRedisKeyFormatter,
2466
+ dispatchHook,
2467
+ dispatchSyncHook,
2468
+ enqueueApiRequest,
2469
+ enqueueSyncRequest,
2470
+ env,
2471
+ extractLanguageFromHeader,
2472
+ extractTokenFromRequest,
2473
+ extractTokenFromSocket,
2474
+ flushApiQueue,
2475
+ flushSyncQueue,
2476
+ formatKey,
2477
+ getActiveErrorTrackers,
2478
+ getApiQueueSize,
2479
+ getAvatarConfig,
2480
+ getBindAddress,
2481
+ getCookieValue,
2482
+ getCsrfConfig,
2483
+ getCsrfToken,
2484
+ getDeployConfig,
2485
+ getEffectiveHttpMethod,
2486
+ getEmailSender,
2487
+ getEmailSenderByName,
2488
+ getEnvFiles,
2489
+ getErrorFormatter,
2490
+ getGeneratedApiDocsPath,
2491
+ getGeneratedApiSchemasPath,
2492
+ getGeneratedSocketTypesPath,
2493
+ getIoInstance,
2494
+ getLocaleReloader,
2495
+ getLocalizedNormalizer,
2496
+ getLogger,
2497
+ getNotifier,
2498
+ getParams,
2499
+ getPrismaClient,
2500
+ getPrismaClientFor,
2501
+ getPrismaClientKeys,
2502
+ getProjectConfig,
2503
+ getProjectName,
2504
+ getPublicDir,
2505
+ getRateLimitStatus,
2506
+ getRateLimitStrategy,
2507
+ getRedactedLogKeys,
2508
+ getRedisClient,
2509
+ getRedisClientFor,
2510
+ getRedisClientKeys,
2511
+ getRedisConnectionOptions,
2512
+ getRedisKeyFormatter,
2513
+ getRegisteredApiMethod,
2514
+ getRuntimeApiMaps,
2515
+ getRuntimeSyncMaps,
2516
+ getServerDir,
2517
+ getServerFunctionDirs,
2518
+ getServerFunctionsDir,
2519
+ getServicesConfig,
2520
+ getSharedDir,
2521
+ getSocketMiddlewares,
2522
+ getSrcDir,
2523
+ getSyncQueueSize,
2524
+ getUploadsDir,
2525
+ hasCookie,
2526
+ hashSynchronizedValue,
2527
+ httpFetch,
2528
+ incrementResponseIndex,
2529
+ inferHttpMethod,
2530
+ initConsolelog,
2531
+ initSharedSentry,
2532
+ isApiMethodMapRegistered,
2533
+ isDeployConfigRegistered,
2534
+ isEmailSenderRegistered,
2535
+ isErrorParamArray,
2536
+ isFalsy,
2537
+ isLocalizedNormalizerRegistered,
2538
+ isLoggerRegistered,
2539
+ isMethodAllowed,
2540
+ isOnline,
2541
+ isPrismaClientRegistered,
2542
+ isProduction,
2543
+ isProjectConfigRegistered,
2544
+ isRedactedLogKey,
2545
+ isRedisClientRegistered,
2546
+ isRuntimeMapsProviderRegistered,
2547
+ isServicesConfigRegistered,
2548
+ listEmailSenderNames,
2549
+ loadEnvFiles,
2550
+ normalizeErrorResponse,
2551
+ normalizeErrorResponseCore,
2552
+ notify,
2553
+ parseServiceRouteName,
2554
+ parseTransportRouteName,
2555
+ prisma,
2556
+ processUpload,
2557
+ readBootUuid,
2558
+ recordMetricAcrossTrackers,
2559
+ redis,
2560
+ registerApiAbortController,
2561
+ registerApiMethodMap,
2562
+ registerAvatarConfig,
2563
+ registerBindAddress,
2564
+ registerCsrfConfig,
2565
+ registerDeployConfig,
2566
+ registerEmailSender,
2567
+ registerEmailSenders,
2568
+ registerErrorFormatter,
2569
+ registerErrorTracker,
2570
+ registerErrorTrackers,
2571
+ registerHook,
2572
+ registerLocaleReloader,
2573
+ registerLocalizedNormalizer,
2574
+ registerLogger,
2575
+ registerNotifier,
2576
+ registerPrismaClient,
2577
+ registerProjectConfig,
2578
+ registerRateLimitStrategy,
2579
+ registerRedactedLogKeys,
2580
+ registerRedisClient,
2581
+ registerRedisKeyFormatter,
2582
+ registerRuntimeMapsProvider,
2583
+ registerServicesConfig,
2584
+ registerSocketMiddleware,
2585
+ registerSyncAbortController,
2586
+ registerSyncHook,
2587
+ releaseLease,
2588
+ removeApiQueueItem,
2589
+ removeApiQueueItemsByKey,
2590
+ removeSyncQueueItem,
2591
+ renewLease,
2592
+ resetClientsForTests,
2593
+ resetCsrfConfigForTests,
2594
+ resetLoggerForTests,
2595
+ resetRedactedLogKeysForTests,
2596
+ resetRedisKeyFormatterForTests,
2597
+ resolveEnvKey,
2598
+ resolveErrorMessage,
2599
+ resolveFromRoot,
2600
+ serveAvatar,
2601
+ setErrorTrackerUser,
2602
+ setIoInstance,
2603
+ setSentryUser,
2604
+ setSocket,
2605
+ sleep,
2606
+ socket,
2607
+ socketEventNames,
2608
+ startSpan,
2609
+ startSpanAcrossTrackers,
2610
+ tryCatch,
2611
+ unregisterApiAbortController,
2612
+ unregisterSyncAbortController,
2613
+ validateInputByType,
2614
+ validatePagePath,
2615
+ validateRequest,
2616
+ waitForSocket,
2617
+ writeBootUuid
2618
+ };
2619
+ //# sourceMappingURL=index.js.map