@luckystack/core 0.10.2 → 0.12.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.
Files changed (134) hide show
  1. package/AGENTS.md +11 -0
  2. package/CHANGELOG.md +18 -0
  3. package/README.md +2 -88
  4. package/dist/apiInterceptors.d.ts +36 -0
  5. package/dist/apiMethodMapRegistry.d.ts +5 -0
  6. package/dist/apiRequest.d.ts +71 -0
  7. package/dist/apiTypeStubs.d.ts +3 -5
  8. package/dist/avatarConfig.d.ts +22 -0
  9. package/dist/bindAddress.d.ts +15 -0
  10. package/dist/bootUuid.d.ts +24 -0
  11. package/dist/cancelRegistry.d.ts +7 -0
  12. package/dist/checkOrigin.d.ts +6 -0
  13. package/dist/{chunk-IJYNVM2W.js → chunk-P3SSLA3E.js} +2 -2
  14. package/dist/chunk-P3SSLA3E.js.map +1 -0
  15. package/dist/{chunk-5FXBTGYL.js → chunk-XDX5MV54.js} +2 -2
  16. package/dist/chunk-XDX5MV54.js.map +1 -0
  17. package/dist/client.d.ts +46 -335
  18. package/dist/client.js +5 -5
  19. package/dist/client.js.map +1 -1
  20. package/dist/clientHookBus.d.ts +62 -0
  21. package/dist/clients.d.ts +16 -0
  22. package/dist/config.d.ts +8 -2
  23. package/dist/configUtils.d.ts +16 -0
  24. package/dist/consoleLog.d.ts +1 -0
  25. package/dist/cookies.d.ts +28 -0
  26. package/dist/createRegistry.d.ts +39 -0
  27. package/dist/csrf.d.ts +30 -0
  28. package/dist/csrfConfig.d.ts +32 -0
  29. package/dist/db.d.ts +12 -0
  30. package/dist/dbHealthCheck.d.ts +6 -0
  31. package/dist/{config-CwW3KY9r.d.ts → deployConfigRegistry.d.ts} +8 -43
  32. package/dist/emailRegistry.d.ts +139 -0
  33. package/dist/env.d.ts +38 -0
  34. package/dist/errorFormatterRegistry.d.ts +54 -0
  35. package/dist/errorTrackerIdentity.d.ts +14 -0
  36. package/dist/errorTrackerRegistry.d.ts +85 -0
  37. package/dist/escapeHtml.d.ts +6 -0
  38. package/dist/eslint/index.d.ts +5 -8
  39. package/dist/eslint/index.js +125 -8
  40. package/dist/eslint/index.js.map +1 -1
  41. package/dist/eslint/internal/hasPackage.d.ts +1 -0
  42. package/dist/eslint/internal/ruleTypes.d.ts +4 -0
  43. package/dist/eslint/rules/no-arbitrary-tailwind-color.d.ts +3 -0
  44. package/dist/eslint/rules/no-direct-prisma-import-in-components.d.ts +3 -0
  45. package/dist/eslint/rules/no-local-socket-enumeration.d.ts +3 -0
  46. package/dist/eslint/rules/no-raw-fetch-in-src.d.ts +3 -0
  47. package/dist/eslint/rules/no-raw-try-catch.d.ts +3 -0
  48. package/dist/eslint/rules/no-unsafe-api-wrappers.d.ts +3 -0
  49. package/dist/eslint/rules/no-unsafe-sync-wrappers.d.ts +3 -0
  50. package/dist/eslint/rules/no-untranslated-jsx-text.d.ts +4 -0
  51. package/dist/eslint/rules/prefer-luckystack-confirm.d.ts +3 -0
  52. package/dist/eslint/rules/prefer-luckystack-dropdown.d.ts +3 -0
  53. package/dist/eslint/rules/prefer-luckystack-notify.d.ts +3 -0
  54. package/dist/extractToken.d.ts +12 -0
  55. package/dist/extractTokenFromRequest.d.ts +12 -0
  56. package/dist/getParams.d.ts +9 -0
  57. package/dist/hooks/registry.d.ts +12 -0
  58. package/dist/hooks/types.d.ts +446 -0
  59. package/dist/httpApiUtils.d.ts +54 -0
  60. package/dist/index.d.ts +110 -1892
  61. package/dist/index.js +2 -2
  62. package/dist/index.js.map +1 -1
  63. package/dist/lease.d.ts +18 -0
  64. package/dist/localSocketEnumerationGuard.d.ts +22 -0
  65. package/dist/localeReloader.d.ts +3 -0
  66. package/dist/localesRegistry.d.ts +8 -0
  67. package/dist/localizedNormalizer.d.ts +50 -0
  68. package/dist/loggerRegistry.d.ts +12 -0
  69. package/dist/middlewareRegistry.d.ts +45 -0
  70. package/dist/notifier.d.ts +17 -0
  71. package/dist/offlineQueue.d.ts +48 -0
  72. package/dist/oneTimeToken.d.ts +43 -0
  73. package/dist/pageRouteValidation.d.ts +37 -0
  74. package/dist/paths.d.ts +35 -0
  75. package/dist/peerDeps.d.ts +23 -0
  76. package/dist/portOverrideRegistry.d.ts +3 -0
  77. package/dist/processUpload.d.ts +44 -0
  78. package/dist/{sessionTypes-DanjuPaE.d.ts → projectConfig.d.ts} +32 -129
  79. package/dist/rateLimiter.d.ts +90 -0
  80. package/dist/react/AvatarProvider.d.ts +11 -0
  81. package/dist/react/Middleware.d.ts +10 -0
  82. package/dist/react/Router.d.ts +1 -0
  83. package/dist/react/TranslationProvider.d.ts +22 -0
  84. package/dist/react/notify.d.ts +12 -0
  85. package/dist/react/sessionContext.d.ts +31 -0
  86. package/dist/react/useTheme.d.ts +5 -0
  87. package/dist/redactedLogKeys.d.ts +22 -0
  88. package/dist/redis.d.ts +15 -0
  89. package/dist/redisKeyFormatter.d.ts +7 -0
  90. package/dist/resolveClientIp.d.ts +51 -0
  91. package/dist/responseNormalizer.d.ts +33 -0
  92. package/dist/roomNameFormatterRegistry.d.ts +36 -0
  93. package/dist/roomSockets.d.ts +23 -0
  94. package/dist/routedDataQuery.d.ts +7 -0
  95. package/dist/routedHttpInvocation.d.ts +20 -0
  96. package/dist/runtimeMapsRegistry.d.ts +18 -0
  97. package/dist/runtimeTypeValidation.d.ts +13 -0
  98. package/dist/secretsResolved.d.ts +3 -0
  99. package/dist/{sentrySetup-HNIURVUZ.js → sentrySetup-OLZ4BO7Q.js} +2 -2
  100. package/dist/sentrySetup.d.ts +21 -0
  101. package/dist/serveAvatars.d.ts +5 -0
  102. package/dist/serviceRoute.d.ts +27 -0
  103. package/dist/servicesConfigRegistry.d.ts +22 -0
  104. package/dist/sessionProviderRegistry.d.ts +46 -0
  105. package/dist/sessionTypes.d.ts +79 -0
  106. package/dist/sleep.d.ts +1 -0
  107. package/dist/socketEvents.d.ts +37 -0
  108. package/dist/socketMiddlewareRegistry.d.ts +20 -0
  109. package/dist/socketRedisAdapter.d.ts +21 -0
  110. package/dist/socketState.d.ts +5 -0
  111. package/dist/socketStatusTypes.d.ts +6 -0
  112. package/dist/socketTypes.d.ts +34 -0
  113. package/dist/synchronizedEnvHashes.d.ts +66 -0
  114. package/dist/testFileConvention.d.ts +11 -0
  115. package/dist/testResetTargets.d.ts +5 -0
  116. package/dist/tryCatch.d.ts +1 -0
  117. package/dist/tryCatchClient.d.ts +1 -0
  118. package/dist/tryCatchSync.d.ts +1 -0
  119. package/dist/validateRequest.d.ts +50 -0
  120. package/docs/app-bootstrap.md +4 -449
  121. package/docs/config-registry.md +3 -346
  122. package/docs/csrf-config.md +3 -86
  123. package/docs/error-tracker-registry.md +3 -192
  124. package/docs/hooks.md +2 -295
  125. package/docs/rate-limit-strategy.md +3 -168
  126. package/docs/redis-adapter.md +5 -110
  127. package/docs/session-types.md +3 -178
  128. package/docs/socket-bootstrap.md +5 -385
  129. package/package.json +4 -4
  130. package/CLAUDE.md +0 -256
  131. package/dist/chunk-5FXBTGYL.js.map +0 -1
  132. package/dist/chunk-IJYNVM2W.js.map +0 -1
  133. package/dist/pageRouteValidation-B4Cmtfjv.d.ts +0 -280
  134. /package/dist/{sentrySetup-HNIURVUZ.js.map → sentrySetup-OLZ4BO7Q.js.map} +0 -0
@@ -0,0 +1,36 @@
1
+ export interface RoomNameFormatterContext {
2
+ /**
3
+ * Which room FAMILY the name is being formatted for — a formatter may branch on
4
+ * this to isolate genuinely-separate families, but MUST NOT use it to give the
5
+ * same raw room code different physical names across a single family's
6
+ * operations. A content room is joined, left, membership-checked, broadcast to,
7
+ * and streamed to under the SINGLE canonical `'broadcast'` purpose (M5); the
8
+ * framework never varies a content room's physical name by operation, because a
9
+ * socket physically IN `f(R,'join')` could never receive a fanout targeting
10
+ * `f(R,'broadcast')`. `'presence'` is the one genuinely-separate family (its own
11
+ * rooms). `'join'`/`'leave'` remain in the union for backward-compat but the
12
+ * framework no longer emits them for room-NAME formatting — treat them, if ever
13
+ * seen, identically to `'broadcast'`.
14
+ */
15
+ purpose: 'join' | 'leave' | 'broadcast' | 'presence';
16
+ /**
17
+ * Session user id of whoever is performing THIS operation, when known, else
18
+ * null — CONTEXT ONLY. For a shared content room under `'broadcast'` the
19
+ * framework passes the joiner on join/rejoin, the SENDER on fan-out and the
20
+ * originator on `broadcastStream`, so the value differs per call for the SAME
21
+ * room. A formatter must therefore never fold `userId` into a content room's
22
+ * physical name: join and fan-out would resolve to different rooms and every
23
+ * broadcast would reach nobody, silently. Per-user isolation belongs in the
24
+ * raw room code the consumer chooses, or in the separate `'presence'` family.
25
+ */
26
+ userId?: string | null;
27
+ }
28
+ export type RoomNameFormatter = (rawRoomName: string, ctx: RoomNameFormatterContext) => string;
29
+ /** Override how every framework room name is namespaced (multi-tenant prefixing). */
30
+ export declare const registerRoomNameFormatter: (formatter: RoomNameFormatter) => void;
31
+ /** Read the active formatter (defaults to identity). */
32
+ export declare const getRoomNameFormatter: () => RoomNameFormatter;
33
+ /** Apply the active formatter — the single helper framework room-sites call. */
34
+ export declare const formatRoomName: (rawRoomName: string, ctx: RoomNameFormatterContext) => string;
35
+ /** The built-in identity formatter (exported for tests / explicit reset). */
36
+ export declare const defaultRoomNameFormatter: RoomNameFormatter;
@@ -0,0 +1,23 @@
1
+ import type { Server as SocketIOServer } from 'socket.io';
2
+ /** A recipient as returned by Socket.io's adapter-aware `fetchSockets()`. */
3
+ export type RoomSocket = Awaited<ReturnType<SocketIOServer['fetchSockets']>>[number];
4
+ export interface GetRoomSocketsOptions {
5
+ /**
6
+ * User id forwarded to the room-name formatter as CONTEXT. Leave it out (or
7
+ * pass `null`) for a shared room — the formatter contract forbids folding it
8
+ * into a content room's physical name, so it only matters to a formatter that
9
+ * logs or branches on it.
10
+ */
11
+ userId?: string | null;
12
+ }
13
+ /**
14
+ * Resolve every socket currently in `room`, on EVERY instance sharing the
15
+ * Redis adapter. `room` is the LOGICAL code (what the client joined); it is
16
+ * routed through the registered room-name formatter under the canonical
17
+ * `'broadcast'` purpose exactly like the sync fan-out. The `'all'` sentinel
18
+ * returns every connected socket everywhere.
19
+ *
20
+ * Throws when no Socket.io server is registered: a silent `[]` is precisely the
21
+ * failure class this helper exists to remove.
22
+ */
23
+ export declare const getRoomSockets: (room: string, options?: GetRoomSocketsOptions) => Promise<RoomSocket[]>;
@@ -0,0 +1,7 @@
1
+ export declare const ROUTED_DATA_QUERY_KEY = "__luckystack_data";
2
+ /**
3
+ * Append `data` to `path` as the reserved `__luckystack_data` query value
4
+ * (JSON-encoded), plus any `extraParams`. Tolerates a `path` that already
5
+ * carries a query string.
6
+ */
7
+ export declare const buildRoutedGetUrl: (path: string, data: unknown, extraParams?: Record<string, string>) => string;
@@ -0,0 +1,20 @@
1
+ import type { HttpMethodLiteral } from './apiMethodMapRegistry.js';
2
+ export type RoutedInvocationMethod = HttpMethodLiteral;
3
+ export interface RoutedInvocationResponse<TResponse> {
4
+ kind: 'response';
5
+ response: TResponse;
6
+ httpStatus: number;
7
+ }
8
+ export interface RoutedInvocationFailure {
9
+ kind: 'aborted' | 'timeout' | 'network-error' | 'invalid-response';
10
+ }
11
+ export type RoutedInvocationResult<TResponse> = RoutedInvocationResponse<TResponse> | RoutedInvocationFailure;
12
+ export interface RoutedHttpInvocationInput<TStream> {
13
+ path: string;
14
+ method: RoutedInvocationMethod;
15
+ data: Record<string, unknown>;
16
+ stream?: (payload: TStream) => void;
17
+ signals?: readonly (AbortSignal | null | undefined)[];
18
+ timeoutMs?: number | false;
19
+ }
20
+ export declare const invokeRoutedHttp: <TResponse, TStream = Record<string, unknown>>({ path, method, data, stream, signals, timeoutMs, }: RoutedHttpInvocationInput<TStream>) => Promise<RoutedInvocationResult<TResponse>>;
@@ -0,0 +1,18 @@
1
+ type RuntimeMapRecord = Record<string, unknown>;
2
+ export interface RuntimeApiMapsResult {
3
+ apisObject: RuntimeMapRecord;
4
+ functionsObject: RuntimeMapRecord;
5
+ }
6
+ export interface RuntimeSyncMapsResult {
7
+ syncObject: RuntimeMapRecord;
8
+ functionsObject: RuntimeMapRecord;
9
+ }
10
+ export interface RuntimeMapsProvider {
11
+ getRuntimeApiMaps: () => Promise<RuntimeApiMapsResult>;
12
+ getRuntimeSyncMaps: () => Promise<RuntimeSyncMapsResult>;
13
+ }
14
+ export declare const registerRuntimeMapsProvider: (provider: RuntimeMapsProvider) => void;
15
+ export declare const isRuntimeMapsProviderRegistered: () => boolean;
16
+ export declare const getRuntimeApiMaps: () => Promise<RuntimeApiMapsResult>;
17
+ export declare const getRuntimeSyncMaps: () => Promise<RuntimeSyncMapsResult>;
18
+ export {};
@@ -0,0 +1,13 @@
1
+ type ValidationResult = {
2
+ status: 'success';
3
+ } | {
4
+ status: 'error';
5
+ message: string;
6
+ };
7
+ export declare const validateInputByType: ({ typeText, value, rootKey, filePath, }: {
8
+ typeText?: string;
9
+ value: unknown;
10
+ rootKey: string;
11
+ filePath?: string;
12
+ }) => Promise<ValidationResult>;
13
+ export {};
@@ -0,0 +1,3 @@
1
+ export type SecretsResolvedListener = (changedKeys: readonly string[] | undefined) => void;
2
+ export declare const registerSecretsResolvedListener: (listener: SecretsResolvedListener) => (() => void);
3
+ export declare const notifySecretsResolved: (changedKeys?: readonly string[]) => void;
@@ -4,7 +4,7 @@ import {
4
4
  initSharedSentry,
5
5
  setSentryUser,
6
6
  startSpan
7
- } from "./chunk-IJYNVM2W.js";
7
+ } from "./chunk-P3SSLA3E.js";
8
8
  import "./chunk-OWMII7WC.js";
9
9
  export {
10
10
  captureException,
@@ -13,4 +13,4 @@ export {
13
13
  setSentryUser,
14
14
  startSpan
15
15
  };
16
- //# sourceMappingURL=sentrySetup-HNIURVUZ.js.map
16
+ //# sourceMappingURL=sentrySetup-OLZ4BO7Q.js.map
@@ -0,0 +1,21 @@
1
+ interface SentryInstance {
2
+ captureException: (exception: unknown, ...args: unknown[]) => string;
3
+ captureMessage: (message: string, ...args: unknown[]) => string;
4
+ setUser: (user: unknown) => void;
5
+ setContext: (key: string, context: unknown) => void;
6
+ startInactiveSpan?: (context: unknown) => unknown;
7
+ }
8
+ /**
9
+ * @deprecated Use `registerErrorTracker` from `@luckystack/error-tracking` instead.
10
+ * Will be removed in a future major version.
11
+ */
12
+ export declare const initSharedSentry: (instance: SentryInstance) => void;
13
+ export declare const captureException: (error: unknown, context?: Record<string, unknown>) => void;
14
+ export declare const captureMessage: (message: string, level?: 'info' | 'warning' | 'error' | 'fatal', context?: Record<string, unknown>) => void;
15
+ export declare const setSentryUser: (user: {
16
+ id?: string;
17
+ email?: string;
18
+ username?: string;
19
+ } | null) => void;
20
+ export declare const startSpan: (name: string, op: string) => unknown;
21
+ export {};
@@ -0,0 +1,5 @@
1
+ import { ServerResponse } from "node:http";
2
+ export declare const serveAvatar: ({ routePath, res, }: {
3
+ routePath: string;
4
+ res: ServerResponse;
5
+ }) => Promise<void>;
@@ -0,0 +1,27 @@
1
+ export interface ServiceRouteParseSuccess {
2
+ status: 'success';
3
+ normalizedRouteName: string;
4
+ service: string;
5
+ routeName: string;
6
+ }
7
+ export interface ServiceRouteParseError {
8
+ status: 'error';
9
+ reason: string;
10
+ }
11
+ export type ServiceRouteParseResult = ServiceRouteParseSuccess | ServiceRouteParseError;
12
+ export interface TransportRouteParseSuccess {
13
+ status: 'success';
14
+ normalizedFullName: string;
15
+ version: string;
16
+ serviceRoute: ServiceRouteParseSuccess;
17
+ }
18
+ export interface TransportRouteParseError {
19
+ status: 'error';
20
+ reason: string;
21
+ }
22
+ export type TransportRouteParseResult = TransportRouteParseSuccess | TransportRouteParseError;
23
+ export declare const parseServiceRouteName: (value: string) => ServiceRouteParseResult;
24
+ export declare const parseTransportRouteName: ({ value, prefix, }: {
25
+ value: string;
26
+ prefix: 'api' | 'sync';
27
+ }) => TransportRouteParseResult;
@@ -0,0 +1,22 @@
1
+ export interface ServiceDefinition {
2
+ /** 'root' -> reserved for the system service. Otherwise a folder name under srcDir. */
3
+ source: 'root' | string;
4
+ }
5
+ export interface PresetDefinition {
6
+ description?: string;
7
+ /** Services that are bundled together into one backend artifact. */
8
+ services: string[];
9
+ }
10
+ export interface ServicesConfigShape {
11
+ services: Record<string, ServiceDefinition>;
12
+ presets: Record<string, PresetDefinition>;
13
+ /**
14
+ * Pure-data ownership manifest for HTTP routes outside `/api/*` and `/sync/*`.
15
+ * Keys are absolute path prefixes; values are service keys. The router uses
16
+ * longest-prefix matching, so `/webhooks/admin` may override `/webhooks`.
17
+ */
18
+ customRoutes?: Record<string, string>;
19
+ }
20
+ export declare const registerServicesConfig: (config: ServicesConfigShape) => ServicesConfigShape;
21
+ export declare const getServicesConfig: () => ServicesConfigShape;
22
+ export declare const isServicesConfigRegistered: () => boolean;
@@ -0,0 +1,46 @@
1
+ import type { Socket } from 'socket.io';
2
+ import type { BaseSessionLayout } from './sessionTypes.js';
3
+ export interface SessionSaveResult {
4
+ ok: boolean;
5
+ errorCode?: string;
6
+ }
7
+ export interface SessionLogoutInput {
8
+ token: string | null;
9
+ socket?: Socket;
10
+ userId?: string | null;
11
+ skipSessionDelete?: boolean;
12
+ }
13
+ export interface SessionProvider {
14
+ getSession: (token: string | null) => Promise<BaseSessionLayout | null>;
15
+ saveSession: (token: string, data: BaseSessionLayout, newUser?: boolean, options?: {
16
+ supersedeToken?: string;
17
+ }) => Promise<SessionSaveResult>;
18
+ deleteSession: (token: string, options?: {
19
+ skipSocketLogout?: boolean;
20
+ }) => Promise<boolean>;
21
+ logout: (input: SessionLogoutInput) => Promise<void>;
22
+ }
23
+ export declare const registerSessionProvider: (next: SessionProvider) => void;
24
+ export declare const getRegisteredSessionProvider: () => SessionProvider | null;
25
+ export declare const isSessionProviderRegistered: () => boolean;
26
+ export declare const resetSessionProviderForTests: () => void;
27
+ export declare const readSession: (token: string | null) => Promise<BaseSessionLayout | null>;
28
+ /**
29
+ * Persist a session via the registered provider, or no-op if no login provider
30
+ * is registered (unauthenticated-app case).
31
+ *
32
+ * **WARNING**: when no session provider is registered, this returns `{ ok: true }`
33
+ * as a no-op — the session is NOT actually persisted anywhere. Callers that
34
+ * check `result.ok` before considering a user logged in will incorrectly
35
+ * proceed as if the session was saved. This is intentional for apps that do not
36
+ * use `@luckystack/login`, but on a partial install (login package misconfigured
37
+ * or not yet registered) the failure is silent rather than loud.
38
+ * Use `isSessionProviderRegistered()` at boot to verify the provider is present.
39
+ */
40
+ export declare const writeSession: (token: string, data: BaseSessionLayout, newUser?: boolean, options?: {
41
+ supersedeToken?: string;
42
+ }) => Promise<SessionSaveResult>;
43
+ export declare const removeSession: (token: string, options?: {
44
+ skipSocketLogout?: boolean;
45
+ }) => Promise<boolean>;
46
+ export declare const performLogout: (input: SessionLogoutInput) => Promise<void>;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * What `T` becomes after `JSON.stringify` → `JSON.parse`, at the type level.
3
+ *
4
+ * The mirror of what the codegen does to route OUTPUTS. Use it wherever you
5
+ * hand-write a type for something that CROSSES THE WIRE — the client never
6
+ * receives your server type, it receives JSON, and JSON has no `Date`:
7
+ *
8
+ * type ClientUser = Jsonify<User>; // createdAt: string, not Date
9
+ *
10
+ * Without it a type says `Date`, the client gets `"2026-07-15T15:20:15.553Z"`,
11
+ * and `user.createdAt.getTime()` compiles and then throws at runtime.
12
+ *
13
+ * Deliberately SHALLOW — one level of properties, descending only into arrays.
14
+ * A fully recursive object mapper does not survive contact with Prisma's
15
+ * self-referential `JsonValue`. Already-JSON-stable values are therefore an
16
+ * identity boundary: do not recursively rebuild what JSON already represents.
17
+ * The NON-DISTRIBUTIVE tuple guard is load-bearing — it recognizes the complete
18
+ * `JsonValue` union before the Date branch distributes `Date | null` into
19
+ * `string | null`.
20
+ */
21
+ type JsonStableValue = string | number | boolean | null | JsonStableValue[] | {
22
+ [key: string]: JsonStableValue | undefined;
23
+ };
24
+ type JsonifyValue<TValue> = [TValue] extends [JsonStableValue] ? TValue : TValue extends Date ? string : TValue extends readonly (infer TElement)[] ? JsonifyValue<TElement>[] : TValue;
25
+ export type Jsonify<T> = {
26
+ [TKey in keyof T]: JsonifyValue<T[TKey]>;
27
+ };
28
+ export interface SessionLocation {
29
+ pathName: string;
30
+ searchParams?: Record<string, string>;
31
+ }
32
+ export interface BaseSessionLayout {
33
+ id: string;
34
+ token?: string;
35
+ email?: string | null;
36
+ name?: string | null;
37
+ avatar?: string | null;
38
+ avatarFallback?: string | null;
39
+ admin?: boolean | null;
40
+ language?: string | null;
41
+ location?: SessionLocation;
42
+ roomCodes?: string[];
43
+ /**
44
+ * CSRF token bound to this session. Generated by `saveSession` on first
45
+ * write. Validated by the HTTP handler on state-changing requests when
46
+ * the session is in cookie mode. Token-mode sessions don't need this
47
+ * (cross-origin requests don't auto-attach a sessionStorage value).
48
+ */
49
+ csrfToken?: string;
50
+ /**
51
+ * Most recent successful login time (set to "now" on each login). Optional
52
+ * — populated only when the User schema has a `lastLogin` column.
53
+ */
54
+ lastLogin?: Date | string | null;
55
+ /**
56
+ * The previous successful login time, captured at this login's moment.
57
+ * Useful for "Last sign-in: X ago" UI on the post-login dashboard.
58
+ * Null on first-ever login. Runtime-only — not persisted to the DB.
59
+ */
60
+ previousLogin?: Date | string | null;
61
+ }
62
+ export interface AuthProps {
63
+ /** If true, user must have a valid session with an ID */
64
+ login: boolean;
65
+ /** Additional validation rules for session properties */
66
+ additional?: {
67
+ /** The session property to check (e.g., 'admin', 'email') */
68
+ key: keyof BaseSessionLayout;
69
+ /** Exact value the property must equal (strict comparison) */
70
+ value?: unknown;
71
+ /** Type the property must be */
72
+ type?: 'string' | 'number' | 'boolean';
73
+ /** If true, property must be null/undefined. If false, must NOT be null/undefined */
74
+ nullish?: boolean;
75
+ /** If true, property must be falsy. If false, must be truthy */
76
+ mustBeFalsy?: boolean;
77
+ }[];
78
+ }
79
+ export {};
@@ -0,0 +1 @@
1
+ export default function sleep(ms: number): Promise<void>;
@@ -0,0 +1,37 @@
1
+ export declare const socketEventNames: {
2
+ readonly apiRequest: 'apiRequest';
3
+ readonly apiCancel: 'apiCancel';
4
+ readonly sync: 'sync';
5
+ readonly syncCancel: 'syncCancel';
6
+ readonly joinRoom: 'joinRoom';
7
+ readonly leaveRoom: 'leaveRoom';
8
+ readonly getJoinedRooms: 'getJoinedRooms';
9
+ readonly updateLocation: 'updateLocation';
10
+ readonly updateSession: 'updateSession';
11
+ readonly sessionReplaced: 'sessionReplaced';
12
+ readonly logout: 'logout';
13
+ readonly intentionalDisconnect: 'intentionalDisconnect';
14
+ readonly intentionalReconnect: 'intentionalReconnect';
15
+ readonly activity: 'activity';
16
+ readonly userAfk: 'userAfk';
17
+ readonly userBack: 'userBack';
18
+ readonly userLeft: 'userLeft';
19
+ readonly connect: 'connect';
20
+ readonly disconnect: 'disconnect';
21
+ readonly reconnectAttempt: 'reconnect_attempt';
22
+ readonly connectError: 'connect_error';
23
+ readonly apiResponsePrefix: 'apiResponse-';
24
+ readonly apiStreamPrefix: 'apiStream-';
25
+ readonly syncResponsePrefix: 'sync-';
26
+ readonly syncProgressPrefix: 'sync-progress-';
27
+ readonly joinRoomResponsePrefix: 'joinRoom-';
28
+ readonly leaveRoomResponsePrefix: 'leaveRoom-';
29
+ readonly getJoinedRoomsResponsePrefix: 'getJoinedRooms-';
30
+ };
31
+ export declare const buildApiResponseEventName: (responseIndex: number | string) => string;
32
+ export declare const buildApiStreamEventName: (responseIndex: number | string) => string;
33
+ export declare const buildSyncResponseEventName: (responseIndex: number | string) => string;
34
+ export declare const buildSyncProgressEventName: (responseIndex: number | string) => string;
35
+ export declare const buildJoinRoomResponseEventName: (responseIndex: number | string) => string;
36
+ export declare const buildLeaveRoomResponseEventName: (responseIndex: number | string) => string;
37
+ export declare const buildGetJoinedRoomsResponseEventName: (responseIndex: number | string) => string;
@@ -0,0 +1,20 @@
1
+ import type { Socket, Server as SocketIOServer } from 'socket.io';
2
+ export type SocketMiddleware = (socket: Socket, next: (err?: Error) => void) => void;
3
+ /**
4
+ * Register a Socket.io middleware that will be wired via `io.use(...)` when
5
+ * the framework boots the socket server. Multiple middlewares can be
6
+ * registered — they run in registration order before any `connect`
7
+ * handler fires.
8
+ */
9
+ export declare const registerSocketMiddleware: (mw: SocketMiddleware) => void;
10
+ /** Read the registered middleware list (read-only view). */
11
+ export declare const getSocketMiddlewares: () => readonly SocketMiddleware[];
12
+ /** Test-only / hot-reload helper. Drops every registered middleware. */
13
+ export declare const clearSocketMiddlewares: () => void;
14
+ /**
15
+ * Wire every registered middleware into the running Socket.io server.
16
+ * Called by `@luckystack/server`'s `loadSocket` after the `SocketIOServer`
17
+ * is constructed and before any `connect` handler is attached, so custom
18
+ * middlewares run before framework handlers see the socket.
19
+ */
20
+ export declare const applySocketMiddlewares: (io: SocketIOServer) => void;
@@ -0,0 +1,21 @@
1
+ import type { Server as SocketIOServer } from 'socket.io';
2
+ import type { Redis as RedisClient } from 'ioredis';
3
+ import type { RedisAdapterOptions } from '@socket.io/redis-adapter';
4
+ export interface AttachSocketRedisAdapterOptions {
5
+ /**
6
+ * Options forwarded to `@socket.io/redis-adapter`'s `createAdapter`
7
+ * (e.g. `key` prefix, `requestsTimeout`, `publishOnSpecificResponseChannel`).
8
+ * `key` defaults to {@link resolveSocketAdapterKey} when omitted.
9
+ */
10
+ adapterOptions?: Partial<RedisAdapterOptions>;
11
+ /**
12
+ * Supply a pre-built pub client instead of `redis.duplicate()`. Use this for
13
+ * a dedicated adapter connection, custom auth, or a sentinel/cluster handle.
14
+ * When omitted the framework duplicates the default `redis` handle.
15
+ */
16
+ pubClient?: RedisClient;
17
+ /** Pre-built sub client (see `pubClient`). */
18
+ subClient?: RedisClient;
19
+ }
20
+ export declare const resolveSocketAdapterKey: () => string;
21
+ export declare const attachSocketRedisAdapter: (io: SocketIOServer, options?: AttachSocketRedisAdapterOptions) => void;
@@ -0,0 +1,5 @@
1
+ import type { Socket } from 'socket.io-client';
2
+ export declare let socket: Socket | null;
3
+ export declare const setSocket: (next: Socket | null) => void;
4
+ export declare const incrementResponseIndex: () => number;
5
+ export declare const waitForSocket: () => Promise<Socket | null>;
@@ -0,0 +1,6 @@
1
+ export type SOCKETSTATUS = "CONNECTED" | "DISCONNECTED" | "AFK" | "RECONNECTING" | "STARTUP";
2
+ export interface statusContent {
3
+ status: SOCKETSTATUS;
4
+ reconnectAttempt?: number;
5
+ endTime?: number;
6
+ }
@@ -0,0 +1,34 @@
1
+ import type { Server as SocketIOServer } from 'socket.io';
2
+ export interface apiMessage {
3
+ name: string;
4
+ data: object;
5
+ responseIndex: number;
6
+ }
7
+ export interface syncMessage {
8
+ name: string;
9
+ data: object;
10
+ cb: string;
11
+ receiver: string;
12
+ responseIndex?: number;
13
+ ignoreSelf?: boolean;
14
+ }
15
+ export declare const setIoInstance: (io: SocketIOServer | null) => void;
16
+ export interface GetIoInstanceOptions {
17
+ /**
18
+ * Return the raw Socket.io server even outside production. Reach for this
19
+ * ONLY when the per-instance view is what you want (backpressure sampling, a
20
+ * sweep cleaning up this process's own connections) — and say why in a
21
+ * comment, because the lint rule `luckystack/no-local-socket-enumeration`
22
+ * will still flag the access.
23
+ */
24
+ raw?: boolean;
25
+ }
26
+ /**
27
+ * Read the running Socket.io server. Outside production the returned value is
28
+ * a guarded view that THROWS on the per-instance surfaces
29
+ * (`sockets.adapter.rooms`, `sockets.adapter.sids`, enumerating
30
+ * `sockets.sockets`) — see `localSocketEnumerationGuard.ts` for why. Pass
31
+ * `{ raw: true }` for the unwrapped server. In production the raw server is
32
+ * always returned; the guard costs nothing there.
33
+ */
34
+ export declare const getIoInstance: (options?: GetIoInstanceOptions) => SocketIOServer | null;
@@ -0,0 +1,66 @@
1
+ import type { HealthHashConfig } from './projectConfig.js';
2
+ /**
3
+ * Hash a synchronized value under an EXPLICIT `{ mode, salt }` (SEC-13). The
4
+ * shared primitive the router calls with the same config the backend used, so
5
+ * the cross-env compare still holds:
6
+ * - `'plain'` → `sha256(value)` (today's behavior; `salt` ignored).
7
+ * - `'salted'` → `sha256(salt + value)`.
8
+ * - `'hmac'` → `HMAC-SHA256(key=salt, value)`.
9
+ * A `'salted'`/`'hmac'` mode with an empty resolved salt falls back to `'plain'`
10
+ * (and is the caller's responsibility to avoid) rather than silently hashing
11
+ * with an empty key.
12
+ */
13
+ export declare const hashSynchronizedValueWith: (config: Pick<HealthHashConfig, 'mode'> & {
14
+ salt: string;
15
+ }, value: string) => string;
16
+ /**
17
+ * Resolve the effective health-hash config from `http.healthHash`, expanding the
18
+ * `'@bootUuid'` salt sentinel to the supplied boot UUID. Honoured for BOTH
19
+ * `'salted'` and `'hmac'` (the 0.2.0 default is `hmac` + `'@bootUuid'`, so the
20
+ * per-boot UUID becomes the HMAC key — the synchronized-env fingerprint rotates
21
+ * every restart instead of being a stable, dictionary-attackable `sha256`). The
22
+ * server passes its current boot UUID; the router, comparing, passes the same
23
+ * value it read from the backend's handshake. When the sentinel is in play but
24
+ * no boot UUID is available the config collapses to `'plain'` so the cross-env
25
+ * compare never silently diverges.
26
+ */
27
+ export declare const resolveHealthHashConfig: (bootUuid?: string | null) => {
28
+ mode: HealthHashConfig['mode'];
29
+ salt: string;
30
+ };
31
+ /**
32
+ * Safe-to-expose descriptor of the resolved health-hash config. The router needs
33
+ * to hash with the SAME `{ mode, salt }` the backend used, but a static `salt`
34
+ * (mode `'salted'`/`'hmac'` with a non-sentinel salt) is a SECRET and must never
35
+ * leave the backend. So `/_health` exposes only the `mode` + whether the salt is
36
+ * the `'@bootUuid'` sentinel (the bootUuid itself is already in the response).
37
+ * From this the router can reproduce the hash for `'plain'` and `'@bootUuid'`
38
+ * configs, and KNOWS it cannot reproduce a static-salt config (so it skips the
39
+ * compare instead of reporting a false drift).
40
+ */
41
+ export interface HealthHashDescriptor {
42
+ mode: HealthHashConfig['mode'];
43
+ bootUuidSalt: boolean;
44
+ }
45
+ /** Describe the active health-hash config for the `/_health` payload (no secret salt). */
46
+ export declare const describeHealthHashConfig: () => HealthHashDescriptor;
47
+ /**
48
+ * Reproduce the backend's resolved `{ mode, salt }` from its `/_health`
49
+ * descriptor + the shared boot UUID. Returns `null` when the backend uses a
50
+ * STATIC salt the router cannot see — the caller must then skip the compare
51
+ * rather than hash with a different (default) config and report a false drift.
52
+ */
53
+ export declare const resolveHealthHashConfigFromDescriptor: (descriptor: HealthHashDescriptor, bootUuid: string) => {
54
+ mode: HealthHashConfig['mode'];
55
+ salt: string;
56
+ } | null;
57
+ export declare const collectSynchronizedEnvKeys: () => string[];
58
+ /**
59
+ * Compute the `/_health` hash map for every synchronized env var. Reads
60
+ * `http.healthHash` (call-time) so a consumer's salt/HMAC opt-in applies;
61
+ * `bootUuid` is consulted only when the salt is the `'@bootUuid'` sentinel.
62
+ * The 0.2.0 DEFAULT (`{ mode: 'hmac', salt: '@bootUuid' }`) HMAC-keys the output
63
+ * on `bootUuid`; pass `mode: 'plain'` to restore the historical unsalted output.
64
+ */
65
+ export declare const computeSynchronizedEnvHashes: (bootUuid?: string | null) => Record<string, string | null>;
66
+ export declare const hashSynchronizedValue: (value: string, bootUuid?: string | null) => string;
@@ -0,0 +1,11 @@
1
+ /** Matches `<name>.test|tests|spec.<ts|tsx|js|jsx|mts|cts|mjs|cjs>`. */
2
+ export declare const TEST_FILE_PATTERN: RegExp;
3
+ /** Directory names that conventionally hold tests and never runtime modules. */
4
+ export declare const TEST_DIRECTORY_NAMES: Set<string>;
5
+ /**
6
+ * True when the given file name (or path — only the last segment is examined)
7
+ * is a test file by the conventions above.
8
+ */
9
+ export declare const isTestFile: (fileNameOrPath: string) => boolean;
10
+ /** True when a directory name conventionally holds tests rather than runtime modules. */
11
+ export declare const isTestDirectory: (directoryName: string) => boolean;
@@ -0,0 +1,5 @@
1
+ export declare const TEST_RESET_TARGETS: readonly ['rateLimits', 'sessions', 'activeUsers', 'hooks'];
2
+ export type TestResetTarget = typeof TEST_RESET_TARGETS[number];
3
+ /** Query parameter that limits a reset to the listed targets (comma-separated). */
4
+ export declare const TEST_RESET_ONLY_PARAM = "only";
5
+ export declare const isTestResetTarget: (value: string) => value is TestResetTarget;
@@ -0,0 +1 @@
1
+ export default function tryCatch<T, P = void>(func: (values: P) => Promise<T> | T, params?: P, context?: Record<string, unknown>): Promise<[Error | null, T | null]>;
@@ -0,0 +1 @@
1
+ export default function tryCatch<T, P = void>(func: (values: P) => Promise<T> | T, params?: P, context?: Record<string, unknown>): Promise<[Error | null, T | null]>;
@@ -0,0 +1 @@
1
+ export default function tryCatchSync<T, P = void>(func: (values: P) => T, params?: P): [Error | null, T | null];
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Shared validation utilities for API and Sync request handlers.
3
+ * Extracted to avoid code duplication between handleApiRequest and handleSyncRequest.
4
+ */
5
+ import type { AuthProps, BaseSessionLayout } from './sessionTypes.js';
6
+ /**
7
+ * Check if a value is falsy (false, 0, 0n, '', null, undefined, or NaN)
8
+ */
9
+ export declare const isFalsy: (value: unknown) => boolean;
10
+ export interface ValidationResult {
11
+ status: 'success' | 'error';
12
+ errorCode?: string;
13
+ errorParams?: {
14
+ key: string;
15
+ value: string | number | boolean;
16
+ }[];
17
+ httpStatus?: number;
18
+ }
19
+ /**
20
+ * Validate a request against authentication requirements.
21
+ *
22
+ * Multiple constraints on a single `additional` entry are AND'd: every
23
+ * specified field (`nullish`, `type`, `value`, `mustBeFalsy`) must pass
24
+ * for that entry to validate. Each entry across the array is also AND'd:
25
+ * the request must pass every entry to succeed.
26
+ *
27
+ * Constraint semantics:
28
+ * - `nullish: true` — `val` must be `null` or `undefined`.
29
+ * - `nullish: false` — `val` must NOT be `null` or `undefined`.
30
+ * - `type: 'string'` — `typeof val === 'string'` (skipped when val is null/undefined).
31
+ * - `value: <x>` — strict equality `val === x`. Omit the field entirely if you
32
+ * don't want this check; setting `value: undefined`
33
+ * explicitly means "val must be undefined".
34
+ * - `mustBeFalsy: true` — `isFalsy(val)`.
35
+ * - `mustBeFalsy: false` — `!isFalsy(val)`.
36
+ *
37
+ * **SECURITY CONTRACT**: this function does NOT check `auth.login`. Login
38
+ * enforcement is the responsibility of the surrounding API/sync handler — it
39
+ * must verify the session exists before calling `validateRequest`. Callers
40
+ * that omit the login check will allow unauthenticated requests through the
41
+ * `additional[]` predicates even when `auth.login: true` is declared, because
42
+ * `validateRequest` short-circuits to success when `additional` is empty.
43
+ * The expected call pattern is:
44
+ * 1. Check session exists (enforce `auth.login`).
45
+ * 2. Call `validateRequest({ auth, user })` to enforce `auth.additional[]`.
46
+ */
47
+ export declare const validateRequest: ({ auth, user }: {
48
+ auth: AuthProps;
49
+ user?: BaseSessionLayout | null;
50
+ }) => ValidationResult;