@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/CHANGELOG.md +14 -0
- package/CLAUDE.md +210 -0
- package/LICENSE +21 -0
- package/README.md +93 -0
- package/dist/apiTypeStubs.d.ts +7 -0
- package/dist/apiTypeStubs.js +1 -0
- package/dist/apiTypeStubs.js.map +1 -0
- package/dist/client.d.ts +236 -0
- package/dist/client.js +1344 -0
- package/dist/client.js.map +1 -0
- package/dist/eslint/index.d.ts +10 -0
- package/dist/eslint/index.js +436 -0
- package/dist/eslint/index.js.map +1 -0
- package/dist/index.d.ts +1128 -0
- package/dist/index.js +2619 -0
- package/dist/index.js.map +1 -0
- package/dist/pageRouteValidation-CxZJVwZh.d.ts +665 -0
- package/docs/app-bootstrap.md +419 -0
- package/docs/config-registry.md +350 -0
- package/docs/csrf-config.md +88 -0
- package/docs/error-tracker-registry.md +174 -0
- package/docs/hooks.md +298 -0
- package/docs/rate-limit-strategy.md +170 -0
- package/docs/redis-adapter.md +111 -0
- package/docs/session-types.md +180 -0
- package/docs/socket-bootstrap.md +366 -0
- package/package.json +91 -0
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
import { Socket } from 'socket.io-client';
|
|
2
|
+
|
|
3
|
+
declare function sleep(ms: number): Promise<void>;
|
|
4
|
+
|
|
5
|
+
declare function tryCatch<T, P>(func: (values: P) => Promise<T> | T, params?: P, context?: Record<string, unknown>): Promise<[Error | null, T | null]>;
|
|
6
|
+
|
|
7
|
+
interface LoggingConfig {
|
|
8
|
+
devLogs: boolean;
|
|
9
|
+
devNotifications: boolean;
|
|
10
|
+
socketStatus: boolean;
|
|
11
|
+
socketStartup: boolean;
|
|
12
|
+
stream: boolean;
|
|
13
|
+
}
|
|
14
|
+
interface RateLimitingConfig {
|
|
15
|
+
/**
|
|
16
|
+
* Global kill-switch for all rate limiting. When `false`, every
|
|
17
|
+
* `checkRateLimit` call short-circuits to "allowed" and counters are not
|
|
18
|
+
* touched. Useful for local dev, load tests, or trusted internal tooling.
|
|
19
|
+
* Defaults to `true`.
|
|
20
|
+
*/
|
|
21
|
+
enabled: boolean;
|
|
22
|
+
store: 'memory' | 'redis';
|
|
23
|
+
redisKeyPrefix: string;
|
|
24
|
+
defaultApiLimit: number | false;
|
|
25
|
+
defaultIpLimit: number | false;
|
|
26
|
+
windowMs: number;
|
|
27
|
+
/** How often the in-memory store evicts expired entries. */
|
|
28
|
+
cleanupIntervalMs: number;
|
|
29
|
+
}
|
|
30
|
+
interface SessionConfig {
|
|
31
|
+
basedToken: boolean;
|
|
32
|
+
expiryDays: number;
|
|
33
|
+
/**
|
|
34
|
+
* @deprecated Use `perUser: 'multiple'` instead. Kept for backwards
|
|
35
|
+
* compatibility; when set, maps to `perUser: 'multiple'` with no concurrent
|
|
36
|
+
* cap. The framework emits a one-shot deprecation warning at boot when
|
|
37
|
+
* `allowMultiple` is explicitly set.
|
|
38
|
+
*/
|
|
39
|
+
allowMultiple: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Whether multiple parallel logins per browser are permitted.
|
|
42
|
+
* `'single'` (default): one active login per browser; opening a second tab
|
|
43
|
+
* and logging in elsewhere replaces the first. `'multiple'`: each tab/window
|
|
44
|
+
* can hold its own session independently.
|
|
45
|
+
*/
|
|
46
|
+
perBrowser: 'single' | 'multiple';
|
|
47
|
+
/**
|
|
48
|
+
* Whether a single user can hold multiple active sessions across devices.
|
|
49
|
+
* `'single'` (default): logging in on a new device kicks the previous one.
|
|
50
|
+
* `'multiple'`: device A and device B can both be logged in concurrently.
|
|
51
|
+
*/
|
|
52
|
+
perUser: 'single' | 'multiple';
|
|
53
|
+
/**
|
|
54
|
+
* Optional cap on concurrent sessions when `perUser === 'multiple'`. Null
|
|
55
|
+
* = unlimited. When the cap is reached, the policy from `onConflict`
|
|
56
|
+
* decides whether the new login is accepted (kicking oldest) or rejected.
|
|
57
|
+
*/
|
|
58
|
+
maxConcurrentPerUser: number | null;
|
|
59
|
+
/**
|
|
60
|
+
* What to do when a session limit is exceeded.
|
|
61
|
+
* `'revokeOld'` (default): kick the oldest session, accept the new login.
|
|
62
|
+
* `'rejectNew'`: refuse the new login, leave existing sessions intact.
|
|
63
|
+
*/
|
|
64
|
+
onConflict: 'revokeOld' | 'rejectNew';
|
|
65
|
+
/**
|
|
66
|
+
* When `onConflict === 'revokeOld'`, whether to broadcast a short UI
|
|
67
|
+
* notification ("logged in elsewhere") to the kicked device before
|
|
68
|
+
* disconnecting it. Default true.
|
|
69
|
+
*/
|
|
70
|
+
notifyOldDeviceOnRevoke: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Prefix used for Redis session/activeUsers keys (e.g. `${projectName}-session:<token>`).
|
|
73
|
+
* Falls back to `process.env.PROJECT_NAME` then this default at config build time.
|
|
74
|
+
*/
|
|
75
|
+
projectName: string;
|
|
76
|
+
}
|
|
77
|
+
interface AppConfig {
|
|
78
|
+
/**
|
|
79
|
+
* Public URL of the app — used by OAuth callback redirects, transactional
|
|
80
|
+
* email links (when `@luckystack/email` is installed), and any other
|
|
81
|
+
* framework code that needs to render an absolute link to your app.
|
|
82
|
+
* Empty default — consumers should set this to the public origin of their
|
|
83
|
+
* deployment (e.g. `https://app.example.com`).
|
|
84
|
+
*/
|
|
85
|
+
publicUrl: string;
|
|
86
|
+
}
|
|
87
|
+
interface HttpStreamConfig {
|
|
88
|
+
/** Query-string flag clients pass to opt into SSE streaming. */
|
|
89
|
+
queryParam: string;
|
|
90
|
+
/** Value the query-string flag must have. */
|
|
91
|
+
enabledValue: string;
|
|
92
|
+
/** Initial comment line written when an SSE stream opens (keeps proxies happy). */
|
|
93
|
+
connectedComment: string;
|
|
94
|
+
}
|
|
95
|
+
interface SecurityHeadersConfig {
|
|
96
|
+
frameOptions: string;
|
|
97
|
+
referrerPolicy: string;
|
|
98
|
+
xssProtection: string;
|
|
99
|
+
contentTypeOptions: string;
|
|
100
|
+
}
|
|
101
|
+
interface CorsConfig {
|
|
102
|
+
allowedMethods: string;
|
|
103
|
+
allowedHeaders: string;
|
|
104
|
+
exposedHeaders: string;
|
|
105
|
+
/** Send `Access-Control-Allow-Credentials: true`. */
|
|
106
|
+
credentials: boolean;
|
|
107
|
+
/**
|
|
108
|
+
* Origins permitted to make cross-origin requests. The framework always
|
|
109
|
+
* permits the `SERVER_IP:SERVER_PORT` it binds to. Set `allowLocalhost`
|
|
110
|
+
* to true to also accept any `localhost` origin (useful for dev).
|
|
111
|
+
* Origins are normalized (scheme + host + port) before comparison.
|
|
112
|
+
*
|
|
113
|
+
* Static list mode: pass a `string[]`. Origins are matched exactly.
|
|
114
|
+
* Dynamic mode: pass a synchronous function `(origin) => boolean` for
|
|
115
|
+
* per-tenant / per-time-window allow-listing. Async resolvers are NOT
|
|
116
|
+
* supported here because Socket.io's CORS check is synchronous; use a
|
|
117
|
+
* pre-computed in-memory cache that the function can read from.
|
|
118
|
+
*/
|
|
119
|
+
allowedOrigins: string[] | ((origin: string) => boolean);
|
|
120
|
+
/**
|
|
121
|
+
* Accept any origin matching `localhost` (any port). Convenient for local
|
|
122
|
+
* development; should be `false` in production. Defaults to `false` so
|
|
123
|
+
* production deployments fail closed.
|
|
124
|
+
*/
|
|
125
|
+
allowLocalhost: boolean;
|
|
126
|
+
}
|
|
127
|
+
interface HttpConfig {
|
|
128
|
+
sessionCookieName: string;
|
|
129
|
+
sessionCookieSameSite: 'Strict' | 'Lax' | 'None';
|
|
130
|
+
sessionCookiePath: string;
|
|
131
|
+
/** Maximum body size accepted on `/api/*` and `/sync/*` POSTs. */
|
|
132
|
+
requestBodyMaxBytes: number;
|
|
133
|
+
/** Path of the router boot-handshake endpoint. */
|
|
134
|
+
healthEndpoint: string;
|
|
135
|
+
/**
|
|
136
|
+
* Liveness endpoint. Always returns 200 if the process is up and
|
|
137
|
+
* responsive — does NOT check dependencies. Suitable for K8s `livenessProbe`.
|
|
138
|
+
*/
|
|
139
|
+
liveEndpoint: string;
|
|
140
|
+
/**
|
|
141
|
+
* Readiness endpoint. Returns 200 only when Redis + Prisma are reachable
|
|
142
|
+
* AND boot UUID is set. Suitable for K8s `readinessProbe`.
|
|
143
|
+
*/
|
|
144
|
+
readyEndpoint: string;
|
|
145
|
+
/** Path of the dev-only state-reset endpoint (gated by NODE_ENV + token). */
|
|
146
|
+
testResetEndpoint: string;
|
|
147
|
+
stream: HttpStreamConfig;
|
|
148
|
+
securityHeaders: SecurityHeadersConfig;
|
|
149
|
+
cors: CorsConfig;
|
|
150
|
+
}
|
|
151
|
+
interface PasswordPolicyConfig {
|
|
152
|
+
/** Minimum total length. Default 8. */
|
|
153
|
+
minLength: number;
|
|
154
|
+
/** Maximum total length. Default 191 (DB column-friendly). */
|
|
155
|
+
maxLength: number;
|
|
156
|
+
/** Require at least one uppercase letter (A-Z). Default false. */
|
|
157
|
+
requireUppercase: boolean;
|
|
158
|
+
/** Require at least one lowercase letter (a-z). Default false. */
|
|
159
|
+
requireLowercase: boolean;
|
|
160
|
+
/** Require at least one digit (0-9). Default false. */
|
|
161
|
+
requireNumber: boolean;
|
|
162
|
+
/** Require at least one non-alphanumeric character. Default false. */
|
|
163
|
+
requireSpecial: boolean;
|
|
164
|
+
/**
|
|
165
|
+
* Reject the 10k most common passwords (built-in list shipped with
|
|
166
|
+
* `@luckystack/login`). Default true. Set to false in tests or when
|
|
167
|
+
* onboarding from a legacy system that allowed weak passwords.
|
|
168
|
+
*/
|
|
169
|
+
forbidCommon: boolean;
|
|
170
|
+
/**
|
|
171
|
+
* Optional consumer-supplied predicate. Return null when the password is
|
|
172
|
+
* acceptable, or a string describing why it isn't (the string is used as
|
|
173
|
+
* the i18n errorCode-style key surfaced to the client).
|
|
174
|
+
*/
|
|
175
|
+
customValidator?: (password: string) => string | null;
|
|
176
|
+
}
|
|
177
|
+
interface AuthConfig {
|
|
178
|
+
/** TTL for OAuth state tokens stored in Redis. */
|
|
179
|
+
oauthStateTtlSeconds: number;
|
|
180
|
+
/**
|
|
181
|
+
* @deprecated Read `passwordPolicy.minLength`. Kept as a top-level field
|
|
182
|
+
* for older consumer code; new code should use the structured policy.
|
|
183
|
+
*/
|
|
184
|
+
passwordMinLength: number;
|
|
185
|
+
/**
|
|
186
|
+
* @deprecated Read `passwordPolicy.maxLength`. Kept as a top-level field
|
|
187
|
+
* for older consumer code; new code should use the structured policy.
|
|
188
|
+
*/
|
|
189
|
+
passwordMaxLength: number;
|
|
190
|
+
/** Full credentials password policy. Replaces the deprecated min/max-length pair. */
|
|
191
|
+
passwordPolicy: PasswordPolicyConfig;
|
|
192
|
+
/** Maximum email length for credentials auth. */
|
|
193
|
+
emailMaxLength: number;
|
|
194
|
+
/** Maximum display-name length for credentials auth. */
|
|
195
|
+
nameMaxLength: number;
|
|
196
|
+
/**
|
|
197
|
+
* bcrypt cost factor used when hashing passwords (registration, password
|
|
198
|
+
* change, password reset). Default 10 matches industry guidance for 2026
|
|
199
|
+
* hardware. Bump to 12 for higher-value accounts; 4 is acceptable for tests
|
|
200
|
+
* to keep them fast.
|
|
201
|
+
*/
|
|
202
|
+
bcryptRounds: number;
|
|
203
|
+
/**
|
|
204
|
+
* `'per-provider'` (default) — same email via Google and GitHub creates
|
|
205
|
+
* two separate User rows (current behavior, no schema change).
|
|
206
|
+
* `'unified'` — same email maps to a single User; subsequent sign-ins via
|
|
207
|
+
* different providers link an Account row to the same User. Requires a
|
|
208
|
+
* Prisma schema change documented in @luckystack/login's README.
|
|
209
|
+
*/
|
|
210
|
+
providerAccountStrategy: 'per-provider' | 'unified';
|
|
211
|
+
/**
|
|
212
|
+
* `'framework'` — login package ships the /reset-password pages + APIs and
|
|
213
|
+
* sends the reset email. Requires a registered email sender.
|
|
214
|
+
* `'custom'` — login exposes only the primitives
|
|
215
|
+
* (`createPasswordResetToken`, `consumePasswordResetToken`,
|
|
216
|
+
* `updatePasswordHash`); the consumer wires their own UI and emails.
|
|
217
|
+
* `'disabled'` (default) — no forgot-password feature; the link in
|
|
218
|
+
* `LoginForm` does not render.
|
|
219
|
+
*/
|
|
220
|
+
forgotPassword: 'framework' | 'custom' | 'disabled';
|
|
221
|
+
/** Reset-token TTL in seconds when `forgotPassword === 'framework'`. */
|
|
222
|
+
passwordResetTtlSeconds: number;
|
|
223
|
+
/**
|
|
224
|
+
* Brand/display name used in framework-mode password-reset emails (subject +
|
|
225
|
+
* greeting + footer). Falls back to `'LuckyStack'` if unset; consumers should
|
|
226
|
+
* override this to their own brand.
|
|
227
|
+
*/
|
|
228
|
+
passwordResetBrand?: string;
|
|
229
|
+
/** Email-change confirmation-token TTL in seconds. Default 3600 (1 hour). */
|
|
230
|
+
emailChangeTtlSeconds: number;
|
|
231
|
+
}
|
|
232
|
+
interface OfflineQueueConfig {
|
|
233
|
+
/** Hard cap on items per queue (api + sync are tracked separately). Default 200. */
|
|
234
|
+
maxSize: number;
|
|
235
|
+
/** Drop items older than this many ms when flushing/enqueuing. Default 1 hour. */
|
|
236
|
+
maxAgeMs: number;
|
|
237
|
+
/**
|
|
238
|
+
* What to do when the queue is full:
|
|
239
|
+
* - 'drop-oldest' (default): evict the oldest item, append the new one.
|
|
240
|
+
* - 'drop-newest': reject the new item, keep the existing queue.
|
|
241
|
+
* - 'reject': do not enqueue; caller must handle the failure.
|
|
242
|
+
*/
|
|
243
|
+
dropPolicy: 'drop-oldest' | 'drop-newest' | 'reject';
|
|
244
|
+
}
|
|
245
|
+
interface SyncStreamThrottleConfig {
|
|
246
|
+
/** Default `flushAtChars` for `createStreamThrottle({...})`. */
|
|
247
|
+
flushAtChars: number;
|
|
248
|
+
/** Default `flushEveryMs` for `createStreamThrottle({...})`. */
|
|
249
|
+
flushEveryMs: number | false;
|
|
250
|
+
/** Default `field` (payload key) for emitted chunks. */
|
|
251
|
+
field: string;
|
|
252
|
+
}
|
|
253
|
+
interface SyncConfig {
|
|
254
|
+
streamThrottle: SyncStreamThrottleConfig;
|
|
255
|
+
/**
|
|
256
|
+
* Yield to the event loop every N recipients during a broadcast fanout
|
|
257
|
+
* (`receiver: 'all'` or large rooms). Lower = more responsive, higher
|
|
258
|
+
* overhead. Default 100.
|
|
259
|
+
*/
|
|
260
|
+
fanoutYieldEvery: number;
|
|
261
|
+
/** Milliseconds to sleep when yielding. Default 1ms. */
|
|
262
|
+
fanoutYieldMs: number;
|
|
263
|
+
}
|
|
264
|
+
interface SocketConfig {
|
|
265
|
+
/** Maximum payload size for any single Socket.io message (bytes). */
|
|
266
|
+
maxHttpBufferSize: number;
|
|
267
|
+
/** ms with no pong response before the server considers the client gone. */
|
|
268
|
+
pingTimeout: number;
|
|
269
|
+
/** ms between pings. */
|
|
270
|
+
pingInterval: number;
|
|
271
|
+
}
|
|
272
|
+
interface DevConfig {
|
|
273
|
+
/** Debounce window for hot-reload triggers. */
|
|
274
|
+
hotReloadDebounceMs: number;
|
|
275
|
+
/** Chokidar `awaitWriteFinish.stabilityThreshold`. */
|
|
276
|
+
watcherStabilityThresholdMs: number;
|
|
277
|
+
/** Chokidar `awaitWriteFinish.pollInterval`. */
|
|
278
|
+
watcherPollIntervalMs: number;
|
|
279
|
+
/**
|
|
280
|
+
* When true, log a warning the first time an api/sync route is invoked
|
|
281
|
+
* without an `inputType` (typically because generated types haven't been
|
|
282
|
+
* regenerated since the route was added). Helps catch routes that ship
|
|
283
|
+
* with no runtime input validation. Default false (silent), set to true
|
|
284
|
+
* in dev to surface missing types.
|
|
285
|
+
*/
|
|
286
|
+
warnOnMissingInputType?: boolean;
|
|
287
|
+
}
|
|
288
|
+
interface PathsConfig {
|
|
289
|
+
/** Frontend source root. */
|
|
290
|
+
srcDir: string;
|
|
291
|
+
/** Server-side source root. */
|
|
292
|
+
serverDir: string;
|
|
293
|
+
/** Shared (universal) source root. */
|
|
294
|
+
sharedDir: string;
|
|
295
|
+
/** Where uploaded user assets (avatars, etc.) live. */
|
|
296
|
+
uploadsDir: string;
|
|
297
|
+
/** Public static assets served as-is. */
|
|
298
|
+
publicDir: string;
|
|
299
|
+
/**
|
|
300
|
+
* @deprecated Use `serverFunctionDirs` instead. Kept for backwards
|
|
301
|
+
* compatibility with projects that set the singular form before the
|
|
302
|
+
* function-injection system grew multi-directory support. When both are
|
|
303
|
+
* present, `serverFunctionDirs` wins.
|
|
304
|
+
*/
|
|
305
|
+
serverFunctionsDir: string;
|
|
306
|
+
/**
|
|
307
|
+
* Server-side function modules consumed by API/sync handlers. The
|
|
308
|
+
* codegen and runtime walk every listed directory in order and merge the
|
|
309
|
+
* results into one `Functions` interface / object. Duplicate keys
|
|
310
|
+
* (same `<dirname>.<filename>.<export>` path produced by two roots)
|
|
311
|
+
* fail the build — `shared/` is canonical, so the conflict tells the
|
|
312
|
+
* consumer to delete the override.
|
|
313
|
+
*
|
|
314
|
+
* Default: `['functions', 'shared']`.
|
|
315
|
+
*
|
|
316
|
+
* Nested subdirectories work transparently — `functions/test/helper.ts`
|
|
317
|
+
* with `export const foo = …` shows up as
|
|
318
|
+
* `functions.test.helper.foo` on the injected `functions` parameter.
|
|
319
|
+
*/
|
|
320
|
+
serverFunctionDirs: string[];
|
|
321
|
+
/** Generated socket types output path (relative to project root). */
|
|
322
|
+
generatedSocketTypes: string;
|
|
323
|
+
/** Generated API input schemas output path. */
|
|
324
|
+
generatedApiSchemas: string;
|
|
325
|
+
/** Generated API docs JSON output path. */
|
|
326
|
+
generatedApiDocs: string;
|
|
327
|
+
}
|
|
328
|
+
interface ProjectConfig {
|
|
329
|
+
app: AppConfig;
|
|
330
|
+
logging: LoggingConfig;
|
|
331
|
+
rateLimiting: RateLimitingConfig;
|
|
332
|
+
session: SessionConfig;
|
|
333
|
+
http: HttpConfig;
|
|
334
|
+
auth: AuthConfig;
|
|
335
|
+
socket: SocketConfig;
|
|
336
|
+
sync: SyncConfig;
|
|
337
|
+
offlineQueue: OfflineQueueConfig;
|
|
338
|
+
dev: DevConfig;
|
|
339
|
+
paths: PathsConfig;
|
|
340
|
+
defaultLanguage: string;
|
|
341
|
+
/** Default theme for new users / fallback when no session preference exists. */
|
|
342
|
+
defaultTheme?: 'light' | 'dark';
|
|
343
|
+
/** Enable per-room activity broadcasting (presence). */
|
|
344
|
+
socketActivityBroadcaster?: boolean;
|
|
345
|
+
/** Show the floating socket-status indicator badge from `@luckystack/presence/client`. */
|
|
346
|
+
socketStatusIndicator?: boolean;
|
|
347
|
+
/** Enable client → server `updateLocation` syncing. */
|
|
348
|
+
locationProviderEnabled?: boolean;
|
|
349
|
+
/** Where to redirect the user after a successful OAuth callback. */
|
|
350
|
+
loginRedirectUrl?: string;
|
|
351
|
+
}
|
|
352
|
+
type DeepPartial<T> = {
|
|
353
|
+
[K in keyof T]?: T[K] extends object | undefined ? DeepPartial<NonNullable<T[K]>> : T[K];
|
|
354
|
+
};
|
|
355
|
+
type ProjectConfigInput = DeepPartial<ProjectConfig>;
|
|
356
|
+
declare const DEFAULT_PROJECT_CONFIG: ProjectConfig;
|
|
357
|
+
declare const registerProjectConfig: (config: ProjectConfigInput) => void;
|
|
358
|
+
declare const getProjectConfig: () => ProjectConfig;
|
|
359
|
+
declare const getProjectName: () => string;
|
|
360
|
+
declare const isProjectConfigRegistered: () => boolean;
|
|
361
|
+
|
|
362
|
+
interface NotifyParam {
|
|
363
|
+
key: string;
|
|
364
|
+
value: string | number | boolean;
|
|
365
|
+
}
|
|
366
|
+
interface NotifyInput {
|
|
367
|
+
key: string;
|
|
368
|
+
params?: NotifyParam[];
|
|
369
|
+
}
|
|
370
|
+
interface Notifier {
|
|
371
|
+
success: (input: NotifyInput) => void;
|
|
372
|
+
error: (input: NotifyInput) => void;
|
|
373
|
+
info: (input: NotifyInput) => void;
|
|
374
|
+
warning: (input: NotifyInput) => void;
|
|
375
|
+
}
|
|
376
|
+
declare const registerNotifier: (notifier: Notifier) => void;
|
|
377
|
+
declare const getNotifier: () => Notifier;
|
|
378
|
+
declare const notify: Notifier;
|
|
379
|
+
|
|
380
|
+
declare let socket: Socket | null;
|
|
381
|
+
declare const setSocket: (next: Socket | null) => void;
|
|
382
|
+
declare const incrementResponseIndex: () => number;
|
|
383
|
+
declare const waitForSocket: () => Promise<Socket | null>;
|
|
384
|
+
|
|
385
|
+
interface ApiQueueItem {
|
|
386
|
+
id: string;
|
|
387
|
+
key: string;
|
|
388
|
+
run: (socketInstance: Socket) => void;
|
|
389
|
+
createdAt: number;
|
|
390
|
+
/**
|
|
391
|
+
* Per-request override of `projectConfig.offlineQueue.dropPolicy`. See
|
|
392
|
+
* `SyncQueueItem.dropPolicy` for details.
|
|
393
|
+
*/
|
|
394
|
+
dropPolicy?: 'drop-oldest' | 'drop-newest' | 'reject';
|
|
395
|
+
}
|
|
396
|
+
interface SyncQueueItem {
|
|
397
|
+
id: string;
|
|
398
|
+
key: string;
|
|
399
|
+
run: (socketInstance: Socket) => void;
|
|
400
|
+
createdAt: number;
|
|
401
|
+
/**
|
|
402
|
+
* Per-request override of `projectConfig.offlineQueue.dropPolicy`. Lets a
|
|
403
|
+
* specific sync ("editor cursor move") pick `'drop-oldest'` while the app
|
|
404
|
+
* default stays `'reject'` for safer sends. When omitted, falls back to
|
|
405
|
+
* the global config.
|
|
406
|
+
*/
|
|
407
|
+
dropPolicy?: 'drop-oldest' | 'drop-newest' | 'reject';
|
|
408
|
+
}
|
|
409
|
+
declare const isOnline: () => boolean;
|
|
410
|
+
declare const enqueueApiRequest: (item: ApiQueueItem) => boolean;
|
|
411
|
+
declare const enqueueSyncRequest: (item: SyncQueueItem) => boolean;
|
|
412
|
+
declare const removeApiQueueItem: (id: string) => void;
|
|
413
|
+
declare const removeSyncQueueItem: (id: string) => void;
|
|
414
|
+
declare const removeApiQueueItemsByKey: (key: string) => void;
|
|
415
|
+
declare const flushApiQueue: (canRun: () => boolean, socketInstance: Socket) => void;
|
|
416
|
+
declare const flushSyncQueue: (canRun: () => boolean, socketInstance: Socket) => void;
|
|
417
|
+
declare const getApiQueueSize: () => number;
|
|
418
|
+
declare const getSyncQueueSize: () => number;
|
|
419
|
+
|
|
420
|
+
interface ErrorParam {
|
|
421
|
+
key: string;
|
|
422
|
+
value: string | number | boolean;
|
|
423
|
+
}
|
|
424
|
+
interface ErrorResponseInput {
|
|
425
|
+
status?: unknown;
|
|
426
|
+
errorCode?: unknown;
|
|
427
|
+
errorParams?: unknown;
|
|
428
|
+
httpStatus?: unknown;
|
|
429
|
+
}
|
|
430
|
+
interface NormalizedErrorResponse {
|
|
431
|
+
status: 'error';
|
|
432
|
+
message: string;
|
|
433
|
+
errorCode: string;
|
|
434
|
+
errorParams?: ErrorParam[];
|
|
435
|
+
httpStatus?: number;
|
|
436
|
+
}
|
|
437
|
+
declare const INVALID_ERROR_RESPONSE_CODE = "error.invalidResponse";
|
|
438
|
+
declare const isErrorParamArray: (value: unknown) => value is ErrorParam[];
|
|
439
|
+
declare const normalizeErrorResponseCore: ({ response, fallbackHttpStatus, fallbackErrorCode, resolveMessage, }: {
|
|
440
|
+
response: ErrorResponseInput;
|
|
441
|
+
fallbackHttpStatus?: number;
|
|
442
|
+
fallbackErrorCode?: string;
|
|
443
|
+
resolveMessage?: (args: {
|
|
444
|
+
errorCode: string;
|
|
445
|
+
errorParams?: ErrorParam[];
|
|
446
|
+
}) => string;
|
|
447
|
+
}) => NormalizedErrorResponse;
|
|
448
|
+
declare const defaultHttpStatusForResponse: ({ status, explicitHttpStatus, fallbackErrorStatus, }: {
|
|
449
|
+
status: "success" | "error";
|
|
450
|
+
explicitHttpStatus?: number;
|
|
451
|
+
fallbackErrorStatus?: number;
|
|
452
|
+
}) => number;
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Fetch (or return the cached) CSRF token for the active session. Returns
|
|
456
|
+
* null in token mode or when no session exists. Callers should attach the
|
|
457
|
+
* returned value as the `X-CSRF-Token` request header.
|
|
458
|
+
*/
|
|
459
|
+
declare const getCsrfToken: () => Promise<string | null>;
|
|
460
|
+
/** Drop the cached token. Call this on logout or when a 403 csrfMismatch is seen. */
|
|
461
|
+
declare const clearCsrfToken: () => void;
|
|
462
|
+
/**
|
|
463
|
+
* Drop-in replacement for `fetch` that automatically attaches the framework's
|
|
464
|
+
* `X-CSRF-Token` and `X-Request-Id` headers to state-changing requests in
|
|
465
|
+
* cookie mode. Use this for any project-side HTTP call that hits a
|
|
466
|
+
* LuckyStack `/api/*`, `/sync/*`, or `/auth/api/*` endpoint.
|
|
467
|
+
*
|
|
468
|
+
* Behavior:
|
|
469
|
+
* - GET / OPTIONS / HEAD: passes through without CSRF (read-only).
|
|
470
|
+
* - Token mode: passes through without CSRF (immune by design).
|
|
471
|
+
* - Cookie mode + state-changing method: lazily fetches `/auth/csrf` once,
|
|
472
|
+
* caches, attaches as `x-csrf-token`. On a 403 `auth.csrfMismatch`
|
|
473
|
+
* response, clears the cache and retries once.
|
|
474
|
+
* - `credentials: 'include'` is forced unless the caller overrode it.
|
|
475
|
+
*
|
|
476
|
+
* @example
|
|
477
|
+
* import { httpFetch } from '@luckystack/core/client';
|
|
478
|
+
* const res = await httpFetch('/api/system/myRoute/v1', {
|
|
479
|
+
* method: 'POST',
|
|
480
|
+
* body: JSON.stringify({ ... }),
|
|
481
|
+
* });
|
|
482
|
+
*/
|
|
483
|
+
declare const httpFetch: typeof fetch;
|
|
484
|
+
|
|
485
|
+
interface ServiceRouteParseSuccess {
|
|
486
|
+
status: 'success';
|
|
487
|
+
normalizedRouteName: string;
|
|
488
|
+
service: string;
|
|
489
|
+
routeName: string;
|
|
490
|
+
}
|
|
491
|
+
interface ServiceRouteParseError {
|
|
492
|
+
status: 'error';
|
|
493
|
+
reason: string;
|
|
494
|
+
}
|
|
495
|
+
type ServiceRouteParseResult = ServiceRouteParseSuccess | ServiceRouteParseError;
|
|
496
|
+
interface TransportRouteParseSuccess {
|
|
497
|
+
status: 'success';
|
|
498
|
+
normalizedFullName: string;
|
|
499
|
+
version: string;
|
|
500
|
+
serviceRoute: ServiceRouteParseSuccess;
|
|
501
|
+
}
|
|
502
|
+
interface TransportRouteParseError {
|
|
503
|
+
status: 'error';
|
|
504
|
+
reason: string;
|
|
505
|
+
}
|
|
506
|
+
type TransportRouteParseResult = TransportRouteParseSuccess | TransportRouteParseError;
|
|
507
|
+
declare const parseServiceRouteName: (value: string) => ServiceRouteParseResult;
|
|
508
|
+
declare const parseTransportRouteName: ({ value, prefix, }: {
|
|
509
|
+
value: string;
|
|
510
|
+
prefix: "api" | "sync";
|
|
511
|
+
}) => TransportRouteParseResult;
|
|
512
|
+
|
|
513
|
+
declare const socketEventNames: {
|
|
514
|
+
readonly apiRequest: "apiRequest";
|
|
515
|
+
readonly apiCancel: "apiCancel";
|
|
516
|
+
readonly sync: "sync";
|
|
517
|
+
readonly syncCancel: "syncCancel";
|
|
518
|
+
readonly joinRoom: "joinRoom";
|
|
519
|
+
readonly leaveRoom: "leaveRoom";
|
|
520
|
+
readonly getJoinedRooms: "getJoinedRooms";
|
|
521
|
+
readonly updateLocation: "updateLocation";
|
|
522
|
+
readonly updateSession: "updateSession";
|
|
523
|
+
readonly sessionReplaced: "sessionReplaced";
|
|
524
|
+
readonly logout: "logout";
|
|
525
|
+
readonly intentionalDisconnect: "intentionalDisconnect";
|
|
526
|
+
readonly intentionalReconnect: "intentionalReconnect";
|
|
527
|
+
readonly userAfk: "userAfk";
|
|
528
|
+
readonly userBack: "userBack";
|
|
529
|
+
readonly connect: "connect";
|
|
530
|
+
readonly disconnect: "disconnect";
|
|
531
|
+
readonly reconnectAttempt: "reconnect_attempt";
|
|
532
|
+
readonly connectError: "connect_error";
|
|
533
|
+
readonly apiResponsePrefix: "apiResponse-";
|
|
534
|
+
readonly apiStreamPrefix: "apiStream-";
|
|
535
|
+
readonly syncResponsePrefix: "sync-";
|
|
536
|
+
readonly syncProgressPrefix: "sync-progress-";
|
|
537
|
+
readonly joinRoomResponsePrefix: "joinRoom-";
|
|
538
|
+
readonly leaveRoomResponsePrefix: "leaveRoom-";
|
|
539
|
+
readonly getJoinedRoomsResponsePrefix: "getJoinedRooms-";
|
|
540
|
+
};
|
|
541
|
+
declare const buildApiResponseEventName: (responseIndex: number | string) => string;
|
|
542
|
+
declare const buildApiStreamEventName: (responseIndex: number | string) => string;
|
|
543
|
+
declare const buildSyncResponseEventName: (responseIndex: number | string) => string;
|
|
544
|
+
declare const buildSyncProgressEventName: (responseIndex: number | string) => string;
|
|
545
|
+
declare const buildJoinRoomResponseEventName: (responseIndex: number | string) => string;
|
|
546
|
+
declare const buildLeaveRoomResponseEventName: (responseIndex: number | string) => string;
|
|
547
|
+
declare const buildGetJoinedRoomsResponseEventName: (responseIndex: number | string) => string;
|
|
548
|
+
|
|
549
|
+
type SOCKETSTATUS = "CONNECTED" | "DISCONNECTED" | "AFK" | "RECONNECTING" | "STARTUP";
|
|
550
|
+
interface statusContent {
|
|
551
|
+
status: SOCKETSTATUS;
|
|
552
|
+
reconnectAttempt?: number;
|
|
553
|
+
endTime?: number;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
interface SessionLocation {
|
|
557
|
+
pathName: string;
|
|
558
|
+
searchParams?: Record<string, string>;
|
|
559
|
+
}
|
|
560
|
+
interface BaseSessionLayout {
|
|
561
|
+
id: string;
|
|
562
|
+
token: string;
|
|
563
|
+
email?: string | null;
|
|
564
|
+
name?: string | null;
|
|
565
|
+
avatar?: string | null;
|
|
566
|
+
avatarFallback?: string | null;
|
|
567
|
+
admin?: boolean | null;
|
|
568
|
+
language?: string | null;
|
|
569
|
+
location?: SessionLocation;
|
|
570
|
+
roomCodes?: string[];
|
|
571
|
+
/**
|
|
572
|
+
* CSRF token bound to this session. Generated by `saveSession` on first
|
|
573
|
+
* write. Validated by the HTTP handler on state-changing requests when
|
|
574
|
+
* the session is in cookie mode. Token-mode sessions don't need this
|
|
575
|
+
* (cross-origin requests don't auto-attach a sessionStorage value).
|
|
576
|
+
*/
|
|
577
|
+
csrfToken?: string;
|
|
578
|
+
/**
|
|
579
|
+
* Most recent successful login time (set to "now" on each login). Optional
|
|
580
|
+
* — populated only when the User schema has a `lastLogin` column.
|
|
581
|
+
*/
|
|
582
|
+
lastLogin?: Date | string | null;
|
|
583
|
+
/**
|
|
584
|
+
* The previous successful login time, captured at this login's moment.
|
|
585
|
+
* Useful for "Last sign-in: X ago" UI on the post-login dashboard.
|
|
586
|
+
* Null on first-ever login. Runtime-only — not persisted to the DB.
|
|
587
|
+
*/
|
|
588
|
+
previousLogin?: Date | string | null;
|
|
589
|
+
}
|
|
590
|
+
interface AuthProps {
|
|
591
|
+
/** If true, user must have a valid session with an ID */
|
|
592
|
+
login: boolean;
|
|
593
|
+
/** Additional validation rules for session properties */
|
|
594
|
+
additional?: {
|
|
595
|
+
/** The session property to check (e.g., 'admin', 'email') */
|
|
596
|
+
key: keyof BaseSessionLayout;
|
|
597
|
+
/** Exact value the property must equal (strict comparison) */
|
|
598
|
+
value?: unknown;
|
|
599
|
+
/** Type the property must be */
|
|
600
|
+
type?: 'string' | 'number' | 'boolean';
|
|
601
|
+
/** If true, property must be null/undefined. If false, must NOT be null/undefined */
|
|
602
|
+
nullish?: boolean;
|
|
603
|
+
/** If true, property must be falsy. If false, must be truthy */
|
|
604
|
+
mustBeFalsy?: boolean;
|
|
605
|
+
}[];
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
type LoggerContext = Record<string, unknown>;
|
|
609
|
+
interface Logger {
|
|
610
|
+
debug: (message: string, context?: LoggerContext) => void;
|
|
611
|
+
info: (message: string, context?: LoggerContext) => void;
|
|
612
|
+
warn: (message: string, context?: LoggerContext) => void;
|
|
613
|
+
error: (message: string, error?: unknown, context?: LoggerContext) => void;
|
|
614
|
+
}
|
|
615
|
+
declare const createDevLogger: () => Logger;
|
|
616
|
+
declare const registerLogger: (logger: Logger) => void;
|
|
617
|
+
declare const getLogger: () => Logger;
|
|
618
|
+
declare const isLoggerRegistered: () => boolean;
|
|
619
|
+
declare const resetLoggerForTests: () => void;
|
|
620
|
+
|
|
621
|
+
type HttpMethodLiteral = 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
622
|
+
type ApiMethodMap = Record<string, Record<string, Record<string, HttpMethodLiteral>>>;
|
|
623
|
+
declare const registerApiMethodMap: (map: ApiMethodMap) => void;
|
|
624
|
+
declare const getRegisteredApiMethod: (pagePath: string, apiName: string, version: string) => HttpMethodLiteral | undefined;
|
|
625
|
+
declare const isApiMethodMapRegistered: () => boolean;
|
|
626
|
+
|
|
627
|
+
interface PageRouteRules {
|
|
628
|
+
/**
|
|
629
|
+
* Single-character prefix that marks a folder as **invisible-parent** for
|
|
630
|
+
* page routing. Default: `'_'`.
|
|
631
|
+
*/
|
|
632
|
+
privateFolderPrefix: string;
|
|
633
|
+
/**
|
|
634
|
+
* Folder names that are reserved for framework-internal use and may NEVER
|
|
635
|
+
* host a `page.tsx`, nested or otherwise. Scaffold + page-discovery emit
|
|
636
|
+
* a hard error when a page is placed inside one.
|
|
637
|
+
*/
|
|
638
|
+
scaffoldIgnoredFolders: readonly string[];
|
|
639
|
+
}
|
|
640
|
+
declare const DEFAULT_PAGE_ROUTE_RULES: PageRouteRules;
|
|
641
|
+
interface PagePathValidationResult {
|
|
642
|
+
/** `true` when the page file produces a routeable URL. */
|
|
643
|
+
valid: boolean;
|
|
644
|
+
/** Computed URL when `valid`. Always begins with `/`. `/` for root page. */
|
|
645
|
+
route?: string;
|
|
646
|
+
/** Human-readable explanation when `!valid`. */
|
|
647
|
+
reason?: string;
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Validate a `page.tsx` file path relative to `src/`. Accepts both
|
|
651
|
+
* forward and back slashes, with or without a leading `./` or `/`.
|
|
652
|
+
* The filename **must** be `page.tsx` (or `page.jsx`); other filenames
|
|
653
|
+
* return `valid: false`.
|
|
654
|
+
*
|
|
655
|
+
* Examples (defaults):
|
|
656
|
+
* admin/page.tsx -> { valid: true, route: '/admin' }
|
|
657
|
+
* _housing/renting/page.tsx -> { valid: true, route: '/renting' }
|
|
658
|
+
* _housing/page.tsx -> { valid: false, reason: 'no URL segment left after stripping underscore folders' }
|
|
659
|
+
* _api/something/page.tsx -> { valid: false, reason: 'page.tsx cannot live inside reserved folder _api' }
|
|
660
|
+
* page.tsx -> { valid: true, route: '/' }
|
|
661
|
+
* some/route.tsx -> { valid: false, reason: 'not a page file (expected page.tsx)' }
|
|
662
|
+
*/
|
|
663
|
+
declare const validatePagePath: (srcRelativePath: string, rules?: PageRouteRules) => PagePathValidationResult;
|
|
664
|
+
|
|
665
|
+
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, tryCatch as a4, validatePagePath as a5, waitForSocket as a6, type ErrorParam as a7, type ErrorResponseInput as a8, type NormalizedErrorResponse as a9, parseTransportRouteName as aA, resetLoggerForTests as aB, type AuthConfig as aa, type CorsConfig as ab, DEFAULT_PROJECT_CONFIG as ac, type DevConfig as ad, type HttpConfig as ae, type HttpStreamConfig as af, INVALID_ERROR_RESPONSE_CODE as ag, type OfflineQueueConfig as ah, type PasswordPolicyConfig as ai, type PathsConfig as aj, type ProjectConfigInput as ak, type SecurityHeadersConfig as al, type ServiceRouteParseError as am, type ServiceRouteParseResult as an, type ServiceRouteParseSuccess as ao, type SocketConfig as ap, type SyncConfig as aq, type SyncStreamThrottleConfig as ar, type TransportRouteParseError as as, type TransportRouteParseResult as at, type TransportRouteParseSuccess as au, createDevLogger as av, defaultHttpStatusForResponse as aw, getProjectName as ax, isErrorParamArray as ay, isLoggerRegistered 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 };
|