@luckystack/core 0.6.7 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +148 -107
  2. package/CLAUDE.md +245 -245
  3. package/LICENSE +21 -21
  4. package/README.md +93 -93
  5. package/dist/{chunk-WFABOO6J.js → chunk-AFG7KKMS.js} +3 -3
  6. package/dist/chunk-AFG7KKMS.js.map +1 -0
  7. package/dist/{chunk-3AWP2YEA.js → chunk-J3UOWYT4.js} +2 -28
  8. package/dist/chunk-J3UOWYT4.js.map +1 -0
  9. package/dist/{chunk-LU3IPFTR.js → chunk-LTCDQU5Z.js} +1 -1
  10. package/dist/chunk-LTCDQU5Z.js.map +1 -0
  11. package/dist/chunk-R7IRJFVC.js +67 -0
  12. package/dist/chunk-R7IRJFVC.js.map +1 -0
  13. package/dist/client.d.ts +3 -2
  14. package/dist/client.js +4 -4
  15. package/dist/client.js.map +1 -1
  16. package/dist/config-DZCzBNwP.d.ts +122 -0
  17. package/dist/config.d.ts +2 -0
  18. package/dist/config.js +31 -0
  19. package/dist/eslint/index.js.map +1 -1
  20. package/dist/index.d.ts +21 -149
  21. package/dist/index.js +66 -70
  22. package/dist/index.js.map +1 -1
  23. package/dist/pageRouteValidation-sJJCX-YV.d.ts +270 -0
  24. package/dist/{projectConfig-XL5VB76A.js → projectConfig-DBLSQ3NY.js} +2 -2
  25. package/dist/{sentrySetup-4NTWLN2A.js → sentrySetup-MF6JSVWA.js} +3 -3
  26. package/dist/sentrySetup-MF6JSVWA.js.map +1 -0
  27. package/dist/{pageRouteValidation-gqktrJci.d.ts → sessionTypes-Dzen_rPK.d.ts} +23 -265
  28. package/docs/app-bootstrap.md +418 -418
  29. package/docs/config-registry.md +348 -348
  30. package/docs/csrf-config.md +88 -88
  31. package/docs/error-tracker-registry.md +194 -174
  32. package/docs/hooks.md +298 -298
  33. package/docs/rate-limit-strategy.md +170 -170
  34. package/docs/redis-adapter.md +111 -111
  35. package/docs/session-types.md +180 -180
  36. package/docs/socket-bootstrap.md +366 -366
  37. package/package.json +6 -2
  38. package/dist/chunk-3AWP2YEA.js.map +0 -1
  39. package/dist/chunk-LU3IPFTR.js.map +0 -1
  40. package/dist/chunk-WFABOO6J.js.map +0 -1
  41. /package/dist/{projectConfig-XL5VB76A.js.map → config.js.map} +0 -0
  42. /package/dist/{sentrySetup-4NTWLN2A.js.map → projectConfig-DBLSQ3NY.js.map} +0 -0
@@ -0,0 +1,270 @@
1
+ import { Socket } from 'socket.io-client';
2
+
3
+ declare function sleep(ms: number): Promise<void>;
4
+
5
+ interface NotifyParam {
6
+ key: string;
7
+ value: string | number | boolean;
8
+ }
9
+ interface NotifyInput {
10
+ key: string;
11
+ params?: NotifyParam[];
12
+ }
13
+ interface Notifier {
14
+ success: (input: NotifyInput) => void;
15
+ error: (input: NotifyInput) => void;
16
+ info: (input: NotifyInput) => void;
17
+ warning: (input: NotifyInput) => void;
18
+ }
19
+ declare const registerNotifier: (notifier: Notifier) => void;
20
+ declare const getNotifier: () => Notifier;
21
+ declare const notify: Notifier;
22
+
23
+ declare let socket: Socket | null;
24
+ declare const setSocket: (next: Socket | null) => void;
25
+ declare const incrementResponseIndex: () => number;
26
+ declare const waitForSocket: () => Promise<Socket | null>;
27
+
28
+ /** Why a queued item was dropped (handed to its `onDrop` callback). */
29
+ type QueueDropReason = 'expired' | 'queue-full';
30
+ interface ApiQueueItem {
31
+ id: string;
32
+ key: string;
33
+ run: (socketInstance: Socket) => void;
34
+ createdAt: number;
35
+ /**
36
+ * Per-request override of `projectConfig.offlineQueue.dropPolicy`. See
37
+ * `SyncQueueItem.dropPolicy` for details.
38
+ */
39
+ dropPolicy?: 'drop-oldest' | 'drop-newest' | 'reject';
40
+ /**
41
+ * Optional callback invoked when THIS item is dropped (evicted for age or
42
+ * because the queue was full / rejected) instead of ever running (SYNC-09).
43
+ * The transport wires this to settle the pending `syncRequest`/`apiRequest`
44
+ * promise with an error envelope, so an evicted offline request resolves
45
+ * rather than hanging forever. Invoked at most once; failures are swallowed.
46
+ */
47
+ onDrop?: (reason: QueueDropReason) => void;
48
+ }
49
+ interface SyncQueueItem {
50
+ id: string;
51
+ key: string;
52
+ run: (socketInstance: Socket) => void;
53
+ createdAt: number;
54
+ /**
55
+ * Per-request override of `projectConfig.offlineQueue.dropPolicy`. Lets a
56
+ * specific sync ("editor cursor move") pick `'drop-oldest'` while the app
57
+ * default stays `'reject'` for safer sends. When omitted, falls back to
58
+ * the global config.
59
+ */
60
+ dropPolicy?: 'drop-oldest' | 'drop-newest' | 'reject';
61
+ /** See {@link ApiQueueItem.onDrop} (SYNC-09). */
62
+ onDrop?: (reason: QueueDropReason) => void;
63
+ }
64
+ declare const isOnline: () => boolean;
65
+ declare const enqueueApiRequest: (item: ApiQueueItem) => boolean;
66
+ declare const enqueueSyncRequest: (item: SyncQueueItem) => boolean;
67
+ declare const removeApiQueueItem: (id: string) => void;
68
+ declare const removeSyncQueueItem: (id: string) => void;
69
+ declare const removeApiQueueItemsByKey: (key: string) => void;
70
+ declare const flushApiQueue: (canRun: () => boolean, socketInstance: Socket) => void;
71
+ declare const flushSyncQueue: (canRun: () => boolean, socketInstance: Socket) => void;
72
+ declare const getApiQueueSize: () => number;
73
+ declare const getSyncQueueSize: () => number;
74
+
75
+ interface ErrorParam {
76
+ key: string;
77
+ value: string | number | boolean;
78
+ }
79
+ interface ErrorResponseInput {
80
+ status?: unknown;
81
+ errorCode?: unknown;
82
+ errorParams?: unknown;
83
+ httpStatus?: unknown;
84
+ }
85
+ interface NormalizedErrorResponse {
86
+ status: 'error';
87
+ message: string;
88
+ errorCode: string;
89
+ errorParams?: ErrorParam[];
90
+ httpStatus?: number;
91
+ }
92
+ declare const INVALID_ERROR_RESPONSE_CODE = "error.invalidResponse";
93
+ declare const isErrorParamArray: (value: unknown) => value is ErrorParam[];
94
+ declare const normalizeErrorResponseCore: ({ response, fallbackHttpStatus, fallbackErrorCode, resolveMessage, }: {
95
+ response: ErrorResponseInput;
96
+ fallbackHttpStatus?: number;
97
+ fallbackErrorCode?: string;
98
+ resolveMessage?: (args: {
99
+ errorCode: string;
100
+ errorParams?: ErrorParam[];
101
+ }) => string;
102
+ }) => NormalizedErrorResponse;
103
+ declare const defaultHttpStatusForResponse: ({ status, explicitHttpStatus, fallbackErrorStatus, }: {
104
+ status: "success" | "error";
105
+ explicitHttpStatus?: number;
106
+ fallbackErrorStatus?: number;
107
+ }) => number;
108
+
109
+ /**
110
+ * Fetch (or return the cached) CSRF token for the active session. Returns
111
+ * null in token mode or when no session exists. Callers should attach the
112
+ * returned value as the `X-CSRF-Token` request header.
113
+ */
114
+ declare const getCsrfToken: () => Promise<string | null>;
115
+ /** Drop the cached token. Call this on logout or when a 403 csrfMismatch is seen. */
116
+ declare const clearCsrfToken: () => void;
117
+ /**
118
+ * Drop-in replacement for `fetch` that automatically attaches the framework's
119
+ * `X-CSRF-Token` and `X-Request-Id` headers to state-changing requests in
120
+ * cookie mode. Use this for any project-side HTTP call that hits a
121
+ * LuckyStack `/api/*`, `/sync/*`, or `/auth/api/*` endpoint.
122
+ *
123
+ * Behavior:
124
+ * - GET / OPTIONS / HEAD: passes through without CSRF (read-only).
125
+ * - Token mode: passes through without CSRF (immune by design).
126
+ * - Cookie mode + state-changing method: lazily fetches `/auth/csrf` once,
127
+ * caches, attaches as `x-csrf-token`. On a 403 `auth.csrfMismatch`
128
+ * response, clears the cache and retries once.
129
+ * - `credentials: 'include'` is forced unless the caller overrode it.
130
+ *
131
+ * @example
132
+ * import { httpFetch } from '@luckystack/core/client';
133
+ * const res = await httpFetch('/api/system/myRoute/v1', {
134
+ * method: 'POST',
135
+ * body: JSON.stringify({ ... }),
136
+ * });
137
+ */
138
+ declare const httpFetch: typeof fetch;
139
+
140
+ interface ServiceRouteParseSuccess {
141
+ status: 'success';
142
+ normalizedRouteName: string;
143
+ service: string;
144
+ routeName: string;
145
+ }
146
+ interface ServiceRouteParseError {
147
+ status: 'error';
148
+ reason: string;
149
+ }
150
+ type ServiceRouteParseResult = ServiceRouteParseSuccess | ServiceRouteParseError;
151
+ interface TransportRouteParseSuccess {
152
+ status: 'success';
153
+ normalizedFullName: string;
154
+ version: string;
155
+ serviceRoute: ServiceRouteParseSuccess;
156
+ }
157
+ interface TransportRouteParseError {
158
+ status: 'error';
159
+ reason: string;
160
+ }
161
+ type TransportRouteParseResult = TransportRouteParseSuccess | TransportRouteParseError;
162
+ declare const parseServiceRouteName: (value: string) => ServiceRouteParseResult;
163
+ declare const parseTransportRouteName: ({ value, prefix, }: {
164
+ value: string;
165
+ prefix: "api" | "sync";
166
+ }) => TransportRouteParseResult;
167
+
168
+ declare const socketEventNames: {
169
+ readonly apiRequest: "apiRequest";
170
+ readonly apiCancel: "apiCancel";
171
+ readonly sync: "sync";
172
+ readonly syncCancel: "syncCancel";
173
+ readonly joinRoom: "joinRoom";
174
+ readonly leaveRoom: "leaveRoom";
175
+ readonly getJoinedRooms: "getJoinedRooms";
176
+ readonly updateLocation: "updateLocation";
177
+ readonly updateSession: "updateSession";
178
+ readonly sessionReplaced: "sessionReplaced";
179
+ readonly logout: "logout";
180
+ readonly intentionalDisconnect: "intentionalDisconnect";
181
+ readonly intentionalReconnect: "intentionalReconnect";
182
+ readonly activity: "activity";
183
+ readonly userAfk: "userAfk";
184
+ readonly userBack: "userBack";
185
+ readonly userLeft: "userLeft";
186
+ readonly connect: "connect";
187
+ readonly disconnect: "disconnect";
188
+ readonly reconnectAttempt: "reconnect_attempt";
189
+ readonly connectError: "connect_error";
190
+ readonly apiResponsePrefix: "apiResponse-";
191
+ readonly apiStreamPrefix: "apiStream-";
192
+ readonly syncResponsePrefix: "sync-";
193
+ readonly syncProgressPrefix: "sync-progress-";
194
+ readonly joinRoomResponsePrefix: "joinRoom-";
195
+ readonly leaveRoomResponsePrefix: "leaveRoom-";
196
+ readonly getJoinedRoomsResponsePrefix: "getJoinedRooms-";
197
+ };
198
+ declare const buildApiResponseEventName: (responseIndex: number | string) => string;
199
+ declare const buildApiStreamEventName: (responseIndex: number | string) => string;
200
+ declare const buildSyncResponseEventName: (responseIndex: number | string) => string;
201
+ declare const buildSyncProgressEventName: (responseIndex: number | string) => string;
202
+ declare const buildJoinRoomResponseEventName: (responseIndex: number | string) => string;
203
+ declare const buildLeaveRoomResponseEventName: (responseIndex: number | string) => string;
204
+ declare const buildGetJoinedRoomsResponseEventName: (responseIndex: number | string) => string;
205
+
206
+ type SOCKETSTATUS = "CONNECTED" | "DISCONNECTED" | "AFK" | "RECONNECTING" | "STARTUP";
207
+ interface statusContent {
208
+ status: SOCKETSTATUS;
209
+ reconnectAttempt?: number;
210
+ endTime?: number;
211
+ }
212
+
213
+ type LoggerContext = Record<string, unknown>;
214
+ interface Logger {
215
+ debug: (message: string, context?: LoggerContext) => void;
216
+ info: (message: string, context?: LoggerContext) => void;
217
+ warn: (message: string, context?: LoggerContext) => void;
218
+ error: (message: string, error?: unknown, context?: LoggerContext) => void;
219
+ }
220
+ declare const createDevLogger: () => Logger;
221
+ declare const registerLogger: (logger: Logger) => void;
222
+ declare const getLogger: () => Logger;
223
+ declare const isLoggerRegistered: () => boolean;
224
+ declare const resetLoggerForTests: () => void;
225
+
226
+ type HttpMethodLiteral = 'GET' | 'POST' | 'PUT' | 'DELETE';
227
+ type ApiMethodMap = Record<string, Record<string, Record<string, HttpMethodLiteral>>>;
228
+ declare const registerApiMethodMap: (map: ApiMethodMap) => void;
229
+ declare const getRegisteredApiMethod: (pagePath: string, apiName: string, version: string) => HttpMethodLiteral | undefined;
230
+ declare const isApiMethodMapRegistered: () => boolean;
231
+
232
+ interface PageRouteRules {
233
+ /**
234
+ * Single-character prefix that marks a folder as **invisible-parent** for
235
+ * page routing. Default: `'_'`.
236
+ */
237
+ privateFolderPrefix: string;
238
+ /**
239
+ * Folder names that are reserved for framework-internal use and may NEVER
240
+ * host a `page.tsx`, nested or otherwise. Scaffold + page-discovery emit
241
+ * a hard error when a page is placed inside one.
242
+ */
243
+ scaffoldIgnoredFolders: readonly string[];
244
+ }
245
+ declare const DEFAULT_PAGE_ROUTE_RULES: PageRouteRules;
246
+ interface PagePathValidationResult {
247
+ /** `true` when the page file produces a routeable URL. */
248
+ valid: boolean;
249
+ /** Computed URL when `valid`. Always begins with `/`. `/` for root page. */
250
+ route?: string;
251
+ /** Human-readable explanation when `!valid`. */
252
+ reason?: string;
253
+ }
254
+ /**
255
+ * Validate a `page.tsx` file path relative to `src/`. Accepts both
256
+ * forward and back slashes, with or without a leading `./` or `/`.
257
+ * The filename **must** be `page.tsx` (or `page.jsx`); other filenames
258
+ * return `valid: false`.
259
+ *
260
+ * Examples (defaults):
261
+ * admin/page.tsx -> { valid: true, route: '/admin' }
262
+ * _housing/renting/page.tsx -> { valid: true, route: '/renting' }
263
+ * _housing/page.tsx -> { valid: false, reason: 'no URL segment left after stripping underscore folders' }
264
+ * _api/something/page.tsx -> { valid: false, reason: 'page.tsx cannot live inside reserved folder _api' }
265
+ * page.tsx -> { valid: true, route: '/' }
266
+ * some/route.tsx -> { valid: false, reason: 'not a page file (expected page.tsx)' }
267
+ */
268
+ declare const validatePagePath: (srcRelativePath: string, rules?: PageRouteRules) => PagePathValidationResult;
269
+
270
+ export { type QueueDropReason as $, type ApiMethodMap as A, normalizeErrorResponseCore as B, notify as C, DEFAULT_PAGE_ROUTE_RULES as D, parseServiceRouteName as E, registerApiMethodMap as F, registerLogger as G, type HttpMethodLiteral as H, registerNotifier as I, removeApiQueueItem as J, removeApiQueueItemsByKey as K, type Logger as L, removeSyncQueueItem as M, type Notifier as N, setSocket as O, type PagePathValidationResult as P, sleep as Q, socket as R, type SOCKETSTATUS as S, socketEventNames as T, type statusContent as U, validatePagePath as V, waitForSocket as W, type ErrorParam as X, type ErrorResponseInput as Y, type NormalizedErrorResponse as Z, INVALID_ERROR_RESPONSE_CODE as _, type LoggerContext as a, type ServiceRouteParseError as a0, type ServiceRouteParseResult as a1, type ServiceRouteParseSuccess as a2, type TransportRouteParseError as a3, type TransportRouteParseResult as a4, type TransportRouteParseSuccess as a5, createDevLogger as a6, defaultHttpStatusForResponse as a7, isErrorParamArray as a8, isLoggerRegistered as a9, parseTransportRouteName as aa, resetLoggerForTests as ab, type NotifyInput as b, type NotifyParam as c, type PageRouteRules as d, buildApiResponseEventName as e, buildApiStreamEventName as f, buildGetJoinedRoomsResponseEventName as g, buildJoinRoomResponseEventName as h, buildLeaveRoomResponseEventName as i, buildSyncProgressEventName as j, buildSyncResponseEventName as k, clearCsrfToken as l, enqueueApiRequest as m, enqueueSyncRequest as n, flushApiQueue as o, flushSyncQueue as p, getApiQueueSize as q, getCsrfToken as r, getLogger as s, getNotifier as t, getRegisteredApiMethod as u, getSyncQueueSize as v, httpFetch as w, incrementResponseIndex as x, isApiMethodMapRegistered as y, isOnline as z };
@@ -4,7 +4,7 @@ import {
4
4
  getProjectName,
5
5
  isProjectConfigRegistered,
6
6
  registerProjectConfig
7
- } from "./chunk-LU3IPFTR.js";
7
+ } from "./chunk-LTCDQU5Z.js";
8
8
  export {
9
9
  DEFAULT_PROJECT_CONFIG,
10
10
  getProjectConfig,
@@ -12,4 +12,4 @@ export {
12
12
  isProjectConfigRegistered,
13
13
  registerProjectConfig
14
14
  };
15
- //# sourceMappingURL=projectConfig-XL5VB76A.js.map
15
+ //# sourceMappingURL=projectConfig-DBLSQ3NY.js.map
@@ -4,8 +4,8 @@ import {
4
4
  initSharedSentry,
5
5
  setSentryUser,
6
6
  startSpan
7
- } from "./chunk-3AWP2YEA.js";
8
- import "./chunk-LU3IPFTR.js";
7
+ } from "./chunk-J3UOWYT4.js";
8
+ import "./chunk-LTCDQU5Z.js";
9
9
  export {
10
10
  captureException,
11
11
  captureMessage,
@@ -13,4 +13,4 @@ export {
13
13
  setSentryUser,
14
14
  startSpan
15
15
  };
16
- //# sourceMappingURL=sentrySetup-4NTWLN2A.js.map
16
+ //# sourceMappingURL=sentrySetup-MF6JSVWA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -1,7 +1,3 @@
1
- import { Socket } from 'socket.io-client';
2
-
3
- declare function sleep(ms: number): Promise<void>;
4
-
5
1
  type DeepPartial<T> = {
6
2
  [K in keyof T]?: T[K] extends object | undefined ? DeepPartial<NonNullable<T[K]>> : T[K];
7
3
  };
@@ -734,214 +730,33 @@ declare const getProjectConfig: () => ProjectConfig;
734
730
  declare const getProjectName: () => string;
735
731
  declare const isProjectConfigRegistered: () => boolean;
736
732
 
737
- interface NotifyParam {
738
- key: string;
739
- value: string | number | boolean;
740
- }
741
- interface NotifyInput {
742
- key: string;
743
- params?: NotifyParam[];
744
- }
745
- interface Notifier {
746
- success: (input: NotifyInput) => void;
747
- error: (input: NotifyInput) => void;
748
- info: (input: NotifyInput) => void;
749
- warning: (input: NotifyInput) => void;
750
- }
751
- declare const registerNotifier: (notifier: Notifier) => void;
752
- declare const getNotifier: () => Notifier;
753
- declare const notify: Notifier;
754
-
755
- declare let socket: Socket | null;
756
- declare const setSocket: (next: Socket | null) => void;
757
- declare const incrementResponseIndex: () => number;
758
- declare const waitForSocket: () => Promise<Socket | null>;
759
-
760
- /** Why a queued item was dropped (handed to its `onDrop` callback). */
761
- type QueueDropReason = 'expired' | 'queue-full';
762
- interface ApiQueueItem {
763
- id: string;
764
- key: string;
765
- run: (socketInstance: Socket) => void;
766
- createdAt: number;
767
- /**
768
- * Per-request override of `projectConfig.offlineQueue.dropPolicy`. See
769
- * `SyncQueueItem.dropPolicy` for details.
770
- */
771
- dropPolicy?: 'drop-oldest' | 'drop-newest' | 'reject';
772
- /**
773
- * Optional callback invoked when THIS item is dropped (evicted for age or
774
- * because the queue was full / rejected) instead of ever running (SYNC-09).
775
- * The transport wires this to settle the pending `syncRequest`/`apiRequest`
776
- * promise with an error envelope, so an evicted offline request resolves
777
- * rather than hanging forever. Invoked at most once; failures are swallowed.
778
- */
779
- onDrop?: (reason: QueueDropReason) => void;
780
- }
781
- interface SyncQueueItem {
782
- id: string;
783
- key: string;
784
- run: (socketInstance: Socket) => void;
785
- createdAt: number;
786
- /**
787
- * Per-request override of `projectConfig.offlineQueue.dropPolicy`. Lets a
788
- * specific sync ("editor cursor move") pick `'drop-oldest'` while the app
789
- * default stays `'reject'` for safer sends. When omitted, falls back to
790
- * the global config.
791
- */
792
- dropPolicy?: 'drop-oldest' | 'drop-newest' | 'reject';
793
- /** See {@link ApiQueueItem.onDrop} (SYNC-09). */
794
- onDrop?: (reason: QueueDropReason) => void;
795
- }
796
- declare const isOnline: () => boolean;
797
- declare const enqueueApiRequest: (item: ApiQueueItem) => boolean;
798
- declare const enqueueSyncRequest: (item: SyncQueueItem) => boolean;
799
- declare const removeApiQueueItem: (id: string) => void;
800
- declare const removeSyncQueueItem: (id: string) => void;
801
- declare const removeApiQueueItemsByKey: (key: string) => void;
802
- declare const flushApiQueue: (canRun: () => boolean, socketInstance: Socket) => void;
803
- declare const flushSyncQueue: (canRun: () => boolean, socketInstance: Socket) => void;
804
- declare const getApiQueueSize: () => number;
805
- declare const getSyncQueueSize: () => number;
806
-
807
- interface ErrorParam {
808
- key: string;
809
- value: string | number | boolean;
810
- }
811
- interface ErrorResponseInput {
812
- status?: unknown;
813
- errorCode?: unknown;
814
- errorParams?: unknown;
815
- httpStatus?: unknown;
816
- }
817
- interface NormalizedErrorResponse {
818
- status: 'error';
819
- message: string;
820
- errorCode: string;
821
- errorParams?: ErrorParam[];
822
- httpStatus?: number;
823
- }
824
- declare const INVALID_ERROR_RESPONSE_CODE = "error.invalidResponse";
825
- declare const isErrorParamArray: (value: unknown) => value is ErrorParam[];
826
- declare const normalizeErrorResponseCore: ({ response, fallbackHttpStatus, fallbackErrorCode, resolveMessage, }: {
827
- response: ErrorResponseInput;
828
- fallbackHttpStatus?: number;
829
- fallbackErrorCode?: string;
830
- resolveMessage?: (args: {
831
- errorCode: string;
832
- errorParams?: ErrorParam[];
833
- }) => string;
834
- }) => NormalizedErrorResponse;
835
- declare const defaultHttpStatusForResponse: ({ status, explicitHttpStatus, fallbackErrorStatus, }: {
836
- status: "success" | "error";
837
- explicitHttpStatus?: number;
838
- fallbackErrorStatus?: number;
839
- }) => number;
840
-
841
733
  /**
842
- * Fetch (or return the cached) CSRF token for the active session. Returns
843
- * null in token mode or when no session exists. Callers should attach the
844
- * returned value as the `X-CSRF-Token` request header.
845
- */
846
- declare const getCsrfToken: () => Promise<string | null>;
847
- /** Drop the cached token. Call this on logout or when a 403 csrfMismatch is seen. */
848
- declare const clearCsrfToken: () => void;
849
- /**
850
- * Drop-in replacement for `fetch` that automatically attaches the framework's
851
- * `X-CSRF-Token` and `X-Request-Id` headers to state-changing requests in
852
- * cookie mode. Use this for any project-side HTTP call that hits a
853
- * LuckyStack `/api/*`, `/sync/*`, or `/auth/api/*` endpoint.
734
+ * What `T` becomes after `JSON.stringify` → `JSON.parse`, at the type level.
735
+ *
736
+ * The mirror of what the codegen does to route OUTPUTS. Use it wherever you
737
+ * hand-write a type for something that CROSSES THE WIRE — the client never
738
+ * receives your server type, it receives JSON, and JSON has no `Date`:
854
739
  *
855
- * Behavior:
856
- * - GET / OPTIONS / HEAD: passes through without CSRF (read-only).
857
- * - Token mode: passes through without CSRF (immune by design).
858
- * - Cookie mode + state-changing method: lazily fetches `/auth/csrf` once,
859
- * caches, attaches as `x-csrf-token`. On a 403 `auth.csrfMismatch`
860
- * response, clears the cache and retries once.
861
- * - `credentials: 'include'` is forced unless the caller overrode it.
740
+ * type ClientUser = Jsonify<User>; // createdAt: string, not Date
862
741
  *
863
- * @example
864
- * import { httpFetch } from '@luckystack/core/client';
865
- * const res = await httpFetch('/api/system/myRoute/v1', {
866
- * method: 'POST',
867
- * body: JSON.stringify({ ... }),
868
- * });
742
+ * Without it a type says `Date`, the client gets `"2026-07-15T15:20:15.553Z"`,
743
+ * and `user.createdAt.getTime()` compiles and then throws at runtime.
744
+ *
745
+ * Deliberately SHALLOW — one level of properties, descending only into arrays.
746
+ * A fully recursive object mapper does not survive contact with Prisma's
747
+ * self-referential `JsonValue`. Already-JSON-stable values are therefore an
748
+ * identity boundary: do not recursively rebuild what JSON already represents.
749
+ * The NON-DISTRIBUTIVE tuple guard is load-bearing — it recognizes the complete
750
+ * `JsonValue` union before the Date branch distributes `Date | null` into
751
+ * `string | null`.
869
752
  */
870
- declare const httpFetch: typeof fetch;
871
-
872
- interface ServiceRouteParseSuccess {
873
- status: 'success';
874
- normalizedRouteName: string;
875
- service: string;
876
- routeName: string;
877
- }
878
- interface ServiceRouteParseError {
879
- status: 'error';
880
- reason: string;
881
- }
882
- type ServiceRouteParseResult = ServiceRouteParseSuccess | ServiceRouteParseError;
883
- interface TransportRouteParseSuccess {
884
- status: 'success';
885
- normalizedFullName: string;
886
- version: string;
887
- serviceRoute: ServiceRouteParseSuccess;
888
- }
889
- interface TransportRouteParseError {
890
- status: 'error';
891
- reason: string;
892
- }
893
- type TransportRouteParseResult = TransportRouteParseSuccess | TransportRouteParseError;
894
- declare const parseServiceRouteName: (value: string) => ServiceRouteParseResult;
895
- declare const parseTransportRouteName: ({ value, prefix, }: {
896
- value: string;
897
- prefix: "api" | "sync";
898
- }) => TransportRouteParseResult;
899
-
900
- declare const socketEventNames: {
901
- readonly apiRequest: "apiRequest";
902
- readonly apiCancel: "apiCancel";
903
- readonly sync: "sync";
904
- readonly syncCancel: "syncCancel";
905
- readonly joinRoom: "joinRoom";
906
- readonly leaveRoom: "leaveRoom";
907
- readonly getJoinedRooms: "getJoinedRooms";
908
- readonly updateLocation: "updateLocation";
909
- readonly updateSession: "updateSession";
910
- readonly sessionReplaced: "sessionReplaced";
911
- readonly logout: "logout";
912
- readonly intentionalDisconnect: "intentionalDisconnect";
913
- readonly intentionalReconnect: "intentionalReconnect";
914
- readonly activity: "activity";
915
- readonly userAfk: "userAfk";
916
- readonly userBack: "userBack";
917
- readonly userLeft: "userLeft";
918
- readonly connect: "connect";
919
- readonly disconnect: "disconnect";
920
- readonly reconnectAttempt: "reconnect_attempt";
921
- readonly connectError: "connect_error";
922
- readonly apiResponsePrefix: "apiResponse-";
923
- readonly apiStreamPrefix: "apiStream-";
924
- readonly syncResponsePrefix: "sync-";
925
- readonly syncProgressPrefix: "sync-progress-";
926
- readonly joinRoomResponsePrefix: "joinRoom-";
927
- readonly leaveRoomResponsePrefix: "leaveRoom-";
928
- readonly getJoinedRoomsResponsePrefix: "getJoinedRooms-";
753
+ type JsonStableValue = string | number | boolean | null | JsonStableValue[] | {
754
+ [key: string]: JsonStableValue | undefined;
755
+ };
756
+ type JsonifyValue<TValue> = [TValue] extends [JsonStableValue] ? TValue : TValue extends Date ? string : TValue extends readonly (infer TElement)[] ? JsonifyValue<TElement>[] : TValue;
757
+ type Jsonify<T> = {
758
+ [TKey in keyof T]: JsonifyValue<T[TKey]>;
929
759
  };
930
- declare const buildApiResponseEventName: (responseIndex: number | string) => string;
931
- declare const buildApiStreamEventName: (responseIndex: number | string) => string;
932
- declare const buildSyncResponseEventName: (responseIndex: number | string) => string;
933
- declare const buildSyncProgressEventName: (responseIndex: number | string) => string;
934
- declare const buildJoinRoomResponseEventName: (responseIndex: number | string) => string;
935
- declare const buildLeaveRoomResponseEventName: (responseIndex: number | string) => string;
936
- declare const buildGetJoinedRoomsResponseEventName: (responseIndex: number | string) => string;
937
-
938
- type SOCKETSTATUS = "CONNECTED" | "DISCONNECTED" | "AFK" | "RECONNECTING" | "STARTUP";
939
- interface statusContent {
940
- status: SOCKETSTATUS;
941
- reconnectAttempt?: number;
942
- endTime?: number;
943
- }
944
-
945
760
  interface SessionLocation {
946
761
  pathName: string;
947
762
  searchParams?: Record<string, string>;
@@ -994,61 +809,4 @@ interface AuthProps {
994
809
  }[];
995
810
  }
996
811
 
997
- type LoggerContext = Record<string, unknown>;
998
- interface Logger {
999
- debug: (message: string, context?: LoggerContext) => void;
1000
- info: (message: string, context?: LoggerContext) => void;
1001
- warn: (message: string, context?: LoggerContext) => void;
1002
- error: (message: string, error?: unknown, context?: LoggerContext) => void;
1003
- }
1004
- declare const createDevLogger: () => Logger;
1005
- declare const registerLogger: (logger: Logger) => void;
1006
- declare const getLogger: () => Logger;
1007
- declare const isLoggerRegistered: () => boolean;
1008
- declare const resetLoggerForTests: () => void;
1009
-
1010
- type HttpMethodLiteral = 'GET' | 'POST' | 'PUT' | 'DELETE';
1011
- type ApiMethodMap = Record<string, Record<string, Record<string, HttpMethodLiteral>>>;
1012
- declare const registerApiMethodMap: (map: ApiMethodMap) => void;
1013
- declare const getRegisteredApiMethod: (pagePath: string, apiName: string, version: string) => HttpMethodLiteral | undefined;
1014
- declare const isApiMethodMapRegistered: () => boolean;
1015
-
1016
- interface PageRouteRules {
1017
- /**
1018
- * Single-character prefix that marks a folder as **invisible-parent** for
1019
- * page routing. Default: `'_'`.
1020
- */
1021
- privateFolderPrefix: string;
1022
- /**
1023
- * Folder names that are reserved for framework-internal use and may NEVER
1024
- * host a `page.tsx`, nested or otherwise. Scaffold + page-discovery emit
1025
- * a hard error when a page is placed inside one.
1026
- */
1027
- scaffoldIgnoredFolders: readonly string[];
1028
- }
1029
- declare const DEFAULT_PAGE_ROUTE_RULES: PageRouteRules;
1030
- interface PagePathValidationResult {
1031
- /** `true` when the page file produces a routeable URL. */
1032
- valid: boolean;
1033
- /** Computed URL when `valid`. Always begins with `/`. `/` for root page. */
1034
- route?: string;
1035
- /** Human-readable explanation when `!valid`. */
1036
- reason?: string;
1037
- }
1038
- /**
1039
- * Validate a `page.tsx` file path relative to `src/`. Accepts both
1040
- * forward and back slashes, with or without a leading `./` or `/`.
1041
- * The filename **must** be `page.tsx` (or `page.jsx`); other filenames
1042
- * return `valid: false`.
1043
- *
1044
- * Examples (defaults):
1045
- * admin/page.tsx -> { valid: true, route: '/admin' }
1046
- * _housing/renting/page.tsx -> { valid: true, route: '/renting' }
1047
- * _housing/page.tsx -> { valid: false, reason: 'no URL segment left after stripping underscore folders' }
1048
- * _api/something/page.tsx -> { valid: false, reason: 'page.tsx cannot live inside reserved folder _api' }
1049
- * page.tsx -> { valid: true, route: '/' }
1050
- * some/route.tsx -> { valid: false, reason: 'not a page file (expected page.tsx)' }
1051
- */
1052
- declare const validatePagePath: (srcRelativePath: string, rules?: PageRouteRules) => PagePathValidationResult;
1053
-
1054
- export { setSocket as $, type ApiMethodMap as A, type BaseSessionLayout as B, getProjectConfig as C, DEFAULT_PAGE_ROUTE_RULES as D, getRegisteredApiMethod as E, getSyncQueueSize as F, httpFetch as G, type HttpMethodLiteral as H, incrementResponseIndex as I, isApiMethodMapRegistered as J, isOnline as K, type Logger as L, isProjectConfigRegistered as M, type Notifier as N, normalizeErrorResponseCore as O, type PagePathValidationResult as P, notify as Q, type RateLimitingConfig as R, type SOCKETSTATUS as S, parseServiceRouteName as T, registerApiMethodMap as U, registerLogger as V, registerNotifier as W, registerProjectConfig as X, removeApiQueueItem as Y, removeApiQueueItemsByKey as Z, removeSyncQueueItem as _, type AppConfig as a, sleep as a0, socket as a1, socketEventNames as a2, type statusContent as a3, validatePagePath as a4, waitForSocket as a5, type ErrorParam as a6, type ErrorResponseInput as a7, type NormalizedErrorResponse as a8, type HealthHashConfig as a9, type TransportRouteParseResult as aA, type TransportRouteParseSuccess as aB, type ValidationConfig as aC, createDevLogger as aD, deepMerge as aE, defaultHttpStatusForResponse as aF, getProjectName as aG, isErrorParamArray as aH, isLoggerRegistered as aI, isPlainObject as aJ, parseTransportRouteName as aK, resetLoggerForTests as aL, type DeepPartial as aa, type ApiConfig as ab, type AuthConfig as ac, type AuthRateLimitConfig as ad, type CorsConfig as ae, DEFAULT_PROJECT_CONFIG as af, type DevConfig as ag, type HttpConfig as ah, type HttpStreamConfig as ai, INVALID_ERROR_RESPONSE_CODE as aj, type OfflineQueueConfig as ak, type PasswordPolicyConfig as al, type PathsConfig as am, type ProjectConfigInput as an, type QueueDropReason as ao, type RateLimitIdentity as ap, type RateLimitIdentityParams as aq, type SecurityHeadersConfig as ar, type ServiceRouteParseError as as, type ServiceRouteParseResult as at, type ServiceRouteParseSuccess as au, type SocketConfig as av, type SyncConfig as aw, type SyncFlushPressureConfig as ax, type SyncStreamThrottleConfig as ay, type TransportRouteParseError as az, type AuthProps as b, type LoggerContext as c, type LoggingConfig as d, type NotifyInput as e, type NotifyParam as f, type PageRouteRules as g, type ProjectConfig as h, type SessionConfig as i, type SessionLocation as j, buildApiResponseEventName as k, buildApiStreamEventName as l, buildGetJoinedRoomsResponseEventName as m, buildJoinRoomResponseEventName as n, buildLeaveRoomResponseEventName as o, buildSyncProgressEventName as p, buildSyncResponseEventName as q, clearCsrfToken as r, enqueueApiRequest as s, enqueueSyncRequest as t, flushApiQueue as u, flushSyncQueue as v, getApiQueueSize as w, getCsrfToken as x, getLogger as y, getNotifier as z };
812
+ export { type AppConfig as A, type BaseSessionLayout as B, type CorsConfig as C, type DeepPartial as D, type HealthHashConfig as H, type Jsonify as J, type LoggingConfig as L, type OfflineQueueConfig as O, type ProjectConfig as P, type RateLimitingConfig as R, type SessionConfig as S, type ValidationConfig as V, type AuthProps as a, type SessionLocation as b, type ApiConfig as c, type AuthConfig as d, type AuthRateLimitConfig as e, DEFAULT_PROJECT_CONFIG as f, getProjectConfig as g, type DevConfig as h, isProjectConfigRegistered as i, type HttpConfig as j, type HttpStreamConfig as k, type PasswordPolicyConfig as l, type PathsConfig as m, type ProjectConfigInput as n, type RateLimitIdentity as o, type RateLimitIdentityParams as p, type SecurityHeadersConfig as q, registerProjectConfig as r, type SocketConfig as s, type SyncConfig as t, type SyncFlushPressureConfig as u, type SyncStreamThrottleConfig as v, deepMerge as w, getProjectName as x, isPlainObject as y };