@luckystack/core 0.1.9 → 0.2.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.
- package/CHANGELOG.md +25 -25
- package/CLAUDE.md +243 -209
- package/LICENSE +21 -21
- package/README.md +93 -93
- package/dist/chunk-OZTB2RL2.js +699 -0
- package/dist/chunk-OZTB2RL2.js.map +1 -0
- package/dist/chunk-USQA6QTX.js +28 -0
- package/dist/chunk-USQA6QTX.js.map +1 -0
- package/dist/chunk-V2WKGTZH.js +357 -0
- package/dist/chunk-V2WKGTZH.js.map +1 -0
- package/dist/chunk-W62GGT2P.js +241 -0
- package/dist/chunk-W62GGT2P.js.map +1 -0
- package/dist/client.d.ts +57 -4
- package/dist/client.js +251 -705
- package/dist/client.js.map +1 -1
- package/dist/eslint/index.js +4 -3
- package/dist/eslint/index.js.map +1 -1
- package/dist/index.d.ts +671 -22
- package/dist/index.js +808 -1156
- package/dist/index.js.map +1 -1
- package/dist/{pageRouteValidation-CxZJVwZh.d.ts → pageRouteValidation-BoZ-rQWi.d.ts} +336 -7
- package/dist/projectConfig-JX563K43.js +16 -0
- package/dist/projectConfig-JX563K43.js.map +1 -0
- package/dist/sentrySetup-Q4BKUAQ3.js +16 -0
- package/dist/sentrySetup-Q4BKUAQ3.js.map +1 -0
- package/docs/app-bootstrap.md +418 -418
- package/docs/config-registry.md +350 -350
- package/docs/csrf-config.md +88 -88
- package/docs/error-tracker-registry.md +174 -174
- package/docs/hooks.md +298 -298
- package/docs/rate-limit-strategy.md +170 -170
- package/docs/redis-adapter.md +111 -111
- package/docs/session-types.md +180 -180
- package/docs/socket-bootstrap.md +366 -366
- package/package.json +3 -2
|
@@ -2,7 +2,22 @@ import { Socket } from 'socket.io-client';
|
|
|
2
2
|
|
|
3
3
|
declare function sleep(ms: number): Promise<void>;
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
type DeepPartial<T> = {
|
|
6
|
+
[K in keyof T]?: T[K] extends object | undefined ? DeepPartial<NonNullable<T[K]>> : T[K];
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* True when `value` is a plain object literal (prototype is `Object.prototype`
|
|
10
|
+
* or `null`). Arrays, class instances, Maps, Dates, and `null` are rejected so
|
|
11
|
+
* they are treated as opaque replacement values during a merge.
|
|
12
|
+
*/
|
|
13
|
+
declare const isPlainObject: (value: unknown) => value is Record<string, unknown>;
|
|
14
|
+
/**
|
|
15
|
+
* Deep-merge a deep-partial `override` over a fully-populated `base`,
|
|
16
|
+
* returning a new object. See the module header for the exact behaviour
|
|
17
|
+
* contract. Arrays and non-plain-object values are replaced wholesale; nested
|
|
18
|
+
* plain objects merge recursively; `undefined` override values are ignored.
|
|
19
|
+
*/
|
|
20
|
+
declare const deepMerge: <T>(base: T, override: DeepPartial<T> | undefined) => T;
|
|
6
21
|
|
|
7
22
|
interface LoggingConfig {
|
|
8
23
|
devLogs: boolean;
|
|
@@ -26,6 +41,77 @@ interface RateLimitingConfig {
|
|
|
26
41
|
windowMs: number;
|
|
27
42
|
/** How often the in-memory store evicts expired entries. */
|
|
28
43
|
cleanupIntervalMs: number;
|
|
44
|
+
/**
|
|
45
|
+
* What the default strategy does when `store: 'redis'` is active but a Redis
|
|
46
|
+
* operation fails (disconnect, eval error). DEFAULT `'memory'` — degrade to
|
|
47
|
+
* the process-local in-memory store (fail-open, backwards-compatible) and log
|
|
48
|
+
* a one-shot warning. In a multi-instance deployment this silently relaxes the
|
|
49
|
+
* global limit to per-instance (N×). Set `'deny'` to instead reject the
|
|
50
|
+
* request (fail-closed) so a Redis outage can't bypass rate limiting.
|
|
51
|
+
*/
|
|
52
|
+
onStoreError: 'memory' | 'deny';
|
|
53
|
+
/**
|
|
54
|
+
* Skip rate limiting for requests that resolve to a loopback client IP
|
|
55
|
+
* (`127.0.0.1`, `::1`, the `UNKNOWN_CLIENT_IP` sentinel) when running in
|
|
56
|
+
* development (`NODE_ENV !== 'production'`). Lets a dev machine hammer its own
|
|
57
|
+
* server (HMR reload storms, local load tests) without tripping limits, while
|
|
58
|
+
* staying fully enforced in production. DEFAULT `false` (enforce everywhere) —
|
|
59
|
+
* a missing key keeps today's behavior. The api/sync transport handlers read
|
|
60
|
+
* this when building the per-IP key; core only owns the flag + the
|
|
61
|
+
* {@link isLoopbackIp} helper.
|
|
62
|
+
*/
|
|
63
|
+
skipLoopbackInDev: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Identity callback that decides the BASIS of a rate-limit key for a given
|
|
66
|
+
* request — i.e. whether to key per-user, per-IP, or some custom dimension.
|
|
67
|
+
* The api/sync handlers call this (when set) to derive the `{ scope, id }`
|
|
68
|
+
* used to build the bucket key, instead of the built-in user-then-IP default.
|
|
69
|
+
* Returning `null` falls back to the framework default. Synchronous by design
|
|
70
|
+
* (it runs on the hot request path). DEFAULT unset.
|
|
71
|
+
*/
|
|
72
|
+
identity?: (params: RateLimitIdentityParams) => RateLimitIdentity | null;
|
|
73
|
+
/**
|
|
74
|
+
* Per-account brute-force lockout slot, keyed independently from the general
|
|
75
|
+
* api/ip limits so credential endpoints (login, password-reset) can apply a
|
|
76
|
+
* stricter window without affecting normal traffic.
|
|
77
|
+
*
|
|
78
|
+
* NOTE — DD-CORE-D3: **core does NOT consume this slot**. The core package
|
|
79
|
+
* only declares and stores the config; the `@luckystack/login` package reads
|
|
80
|
+
* `rateLimiting.auth` when keying a per-account attempt counter
|
|
81
|
+
* (`auth:<accountKey>`). If you search the core source for usages of this
|
|
82
|
+
* field and find none, that is expected — it is not dead config; it is login-
|
|
83
|
+
* owned config that lives here for a single source of truth.
|
|
84
|
+
*
|
|
85
|
+
* DEFAULT `{ enabled: false }` — a missing/disabled slot keeps today's
|
|
86
|
+
* behavior (no per-account lockout; only the per-IP throttle applies).
|
|
87
|
+
*/
|
|
88
|
+
auth: AuthRateLimitConfig;
|
|
89
|
+
}
|
|
90
|
+
/** Inputs handed to the {@link RateLimitingConfig.identity} callback. */
|
|
91
|
+
interface RateLimitIdentityParams {
|
|
92
|
+
/** Resolved route name (`api/billing/getInvoice/v1`, `sync/...`). */
|
|
93
|
+
routeName: string;
|
|
94
|
+
/** Session user id when the request is authenticated, else null. */
|
|
95
|
+
userId: string | null;
|
|
96
|
+
/** Resolved client IP (already honours `http.trustProxy`). */
|
|
97
|
+
ip: string;
|
|
98
|
+
/** Transport the request arrived on. */
|
|
99
|
+
transport: 'socket' | 'http';
|
|
100
|
+
}
|
|
101
|
+
/** Result of a {@link RateLimitingConfig.identity} callback. */
|
|
102
|
+
interface RateLimitIdentity {
|
|
103
|
+
/** Which dimension the key is built from. */
|
|
104
|
+
scope: 'user' | 'ip' | 'custom';
|
|
105
|
+
/** The identity value (user id, ip, or a custom string). Never a raw token. */
|
|
106
|
+
id: string;
|
|
107
|
+
}
|
|
108
|
+
interface AuthRateLimitConfig {
|
|
109
|
+
/** When false (default), no per-account lockout is applied. */
|
|
110
|
+
enabled: boolean;
|
|
111
|
+
/** Max failed attempts per account within `windowMs` before lockout. Default 5. */
|
|
112
|
+
maxAttempts: number;
|
|
113
|
+
/** Rolling window in ms over which `maxAttempts` is counted. Default 900000 (15 min). */
|
|
114
|
+
windowMs: number;
|
|
29
115
|
}
|
|
30
116
|
interface SessionConfig {
|
|
31
117
|
basedToken: boolean;
|
|
@@ -123,11 +209,49 @@ interface CorsConfig {
|
|
|
123
209
|
* production deployments fail closed.
|
|
124
210
|
*/
|
|
125
211
|
allowLocalhost: boolean;
|
|
212
|
+
/**
|
|
213
|
+
* Allow WebSocket upgrades that carry NO `Origin` header (server-to-server
|
|
214
|
+
* native WS clients, proxies). Defaults to `false` (fail-closed): an
|
|
215
|
+
* origin-less upgrade is rejected because the Socket.io CORS layer is the
|
|
216
|
+
* last browser-origin gate on the WS path. Set to `true` only when your
|
|
217
|
+
* architecture includes a non-browser WS consumer (e.g. a backend service
|
|
218
|
+
* connecting to push events).
|
|
219
|
+
*/
|
|
220
|
+
allowOriginless?: boolean;
|
|
126
221
|
}
|
|
127
222
|
interface HttpConfig {
|
|
128
223
|
sessionCookieName: string;
|
|
129
224
|
sessionCookieSameSite: 'Strict' | 'Lax' | 'None';
|
|
130
225
|
sessionCookiePath: string;
|
|
226
|
+
/**
|
|
227
|
+
* Optional `Domain` attribute for the session cookie (CORE-39). Leave unset
|
|
228
|
+
* (DEFAULT) for a host-only cookie (most secure). Set to a parent domain
|
|
229
|
+
* (`.example.com`) only when the cookie must be shared across subdomains.
|
|
230
|
+
* IGNORED when `sessionCookiePrefix === '__Host-'` (that prefix forbids
|
|
231
|
+
* `Domain`). A missing key keeps today's host-only behavior.
|
|
232
|
+
*/
|
|
233
|
+
sessionCookieDomain?: string;
|
|
234
|
+
/**
|
|
235
|
+
* Optional cookie name prefix enforcing browser-level guarantees (CORE-10/39):
|
|
236
|
+
*
|
|
237
|
+
* - `'__Host-'` — forces `Secure`, `Path=/`, and NO `Domain` (blocks
|
|
238
|
+
* subdomain cookie-tossing). The strongest option for a host-only session.
|
|
239
|
+
* - `'__Secure-'` — forces `Secure` (cookie only sent over HTTPS).
|
|
240
|
+
* - unset (DEFAULT) — no prefix; today's behavior. A missing key changes
|
|
241
|
+
* nothing.
|
|
242
|
+
*
|
|
243
|
+
* The server's cookie builder (`buildSessionCookie`) reads this and applies
|
|
244
|
+
* the prefix to `sessionCookieName` plus the forced attributes. Use
|
|
245
|
+
* {@link applyCookiePrefixConstraints} to compute the effective attributes.
|
|
246
|
+
*/
|
|
247
|
+
sessionCookiePrefix?: '__Host-' | '__Secure-';
|
|
248
|
+
/**
|
|
249
|
+
* Per-cookie `Secure` override (CORE-39). When unset (DEFAULT) the server
|
|
250
|
+
* derives `Secure` from `process.env.SECURE` as today. Set explicitly to
|
|
251
|
+
* force on/off independently of the env flag. A `__Host-`/`__Secure-` prefix
|
|
252
|
+
* forces `Secure: true` regardless.
|
|
253
|
+
*/
|
|
254
|
+
sessionCookieSecure?: boolean;
|
|
131
255
|
/** Maximum body size accepted on `/api/*` and `/sync/*` POSTs. */
|
|
132
256
|
requestBodyMaxBytes: number;
|
|
133
257
|
/** Path of the router boot-handshake endpoint. */
|
|
@@ -144,10 +268,81 @@ interface HttpConfig {
|
|
|
144
268
|
readyEndpoint: string;
|
|
145
269
|
/** Path of the dev-only state-reset endpoint (gated by NODE_ENV + token). */
|
|
146
270
|
testResetEndpoint: string;
|
|
271
|
+
/**
|
|
272
|
+
* Whether a known reverse proxy sits in front of this server. DEFAULT false.
|
|
273
|
+
* When false, per-IP rate-limit keys derive from the raw transport peer
|
|
274
|
+
* address (historical behaviour). When true, the framework resolves the real
|
|
275
|
+
* client IP from `X-Forwarded-For` / `X-Real-IP` before keying — so per-IP
|
|
276
|
+
* limits stay meaningful behind nginx/HAProxy. Only enable when a trusted
|
|
277
|
+
* proxy populates those headers, otherwise clients can spoof their IP.
|
|
278
|
+
*/
|
|
279
|
+
trustProxy?: boolean;
|
|
280
|
+
/**
|
|
281
|
+
* How many proxy hops to skip from the RIGHT of `X-Forwarded-For` when
|
|
282
|
+
* `trustProxy` is on (CORE-O3). The rightmost entries are the ones appended by
|
|
283
|
+
* YOUR trusted proxies, so the resolved client IP is the entry that many hops
|
|
284
|
+
* in from the end. The LEFTMOST hop is client-controlled and must never be
|
|
285
|
+
* trusted (it enables per-IP rate-limit evasion + audit-IP spoofing).
|
|
286
|
+
*
|
|
287
|
+
* DEFAULT 1 — with the standard single-trusted-proxy topology this selects the
|
|
288
|
+
* immediate upstream peer (the rightmost real hop), i.e. the IP the proxy saw
|
|
289
|
+
* the connection arrive from. Raise it to match the number of trusted proxies
|
|
290
|
+
* in the chain. Clamped to the list length so an over-large count falls back
|
|
291
|
+
* to the leftmost available trusted hop rather than the spoofable client entry.
|
|
292
|
+
* IGNORED when `trustProxy` is false (raw peer address is used verbatim).
|
|
293
|
+
*/
|
|
294
|
+
trustedProxyHopCount?: number;
|
|
295
|
+
/**
|
|
296
|
+
* Whether cookie-mode sessions (`session.basedToken === false`) also accept a
|
|
297
|
+
* token supplied via `Authorization: Bearer` / `handshake.auth.token` as a
|
|
298
|
+
* fallback (CORE-O10). DEFAULT false — in cookie-mode the framework reads ONLY
|
|
299
|
+
* the session cookie and ignores any bearer/handshake-auth token, so a stolen
|
|
300
|
+
* token replayed through a header can no longer defeat the cookie/CSRF model.
|
|
301
|
+
* Set true to restore the legacy behaviour (cookie-then-bearer fallback).
|
|
302
|
+
* IGNORED in token-mode (`session.basedToken === true`), which is unaffected:
|
|
303
|
+
* it always reads the bearer/handshake token first, cookie as fallback.
|
|
304
|
+
*/
|
|
305
|
+
acceptBearerInCookieMode?: boolean;
|
|
306
|
+
/**
|
|
307
|
+
* Controls how `/_health` exposes synchronized-env-var hashes (SEC-13).
|
|
308
|
+
* Previously `/_health` returned an UNSALTED `sha256(value)` of each
|
|
309
|
+
* synchronized secret, unauthenticated — enabling offline dictionary attacks
|
|
310
|
+
* on low-entropy secrets + key-name disclosure. This config + the shared
|
|
311
|
+
* {@link hashSynchronizedValue} helper let both the server (`/_health`) and the
|
|
312
|
+
* router (boot handshake) salt/HMAC consistently so the compare still works.
|
|
313
|
+
* DEFAULTS keep today's wire behavior (`mode: 'plain'`) so a missing key does
|
|
314
|
+
* not break an existing router handshake — opt into `'salted'`/`'hmac'`
|
|
315
|
+
* explicitly (and bump server + router together).
|
|
316
|
+
*/
|
|
317
|
+
healthHash: HealthHashConfig;
|
|
147
318
|
stream: HttpStreamConfig;
|
|
148
319
|
securityHeaders: SecurityHeadersConfig;
|
|
149
320
|
cors: CorsConfig;
|
|
150
321
|
}
|
|
322
|
+
interface HealthHashConfig {
|
|
323
|
+
/**
|
|
324
|
+
* - `'plain'` — unsalted `sha256(value)`; the pre-0.2.0 behavior, kept so an
|
|
325
|
+
* existing router boot handshake keeps comparing successfully. Opt in with
|
|
326
|
+
* `mode:'plain'` to restore the legacy wire output.
|
|
327
|
+
* - `'salted'` — `sha256(salt + value)` using `salt` below (or the boot UUID
|
|
328
|
+
* when `salt` is the literal `'@bootUuid'`). Stable across a boot, rotates on
|
|
329
|
+
* restart when bound to the boot UUID.
|
|
330
|
+
* - `'hmac'` (DEFAULT in 0.2.0) — `HMAC-SHA256(key=salt, value)`. The default
|
|
331
|
+
* `salt:'@bootUuid'` keys it on the per-boot UUID server + router already
|
|
332
|
+
* share, so `/_health` no longer exposes a stable, offline-attackable
|
|
333
|
+
* `sha256(secret)`. Collapses to `'plain'` when no boot UUID is available so
|
|
334
|
+
* the boot handshake never silently diverges.
|
|
335
|
+
*/
|
|
336
|
+
mode: 'plain' | 'salted' | 'hmac';
|
|
337
|
+
/**
|
|
338
|
+
* Shared salt / HMAC key. Both the backend `/_health` and the router's
|
|
339
|
+
* compare MUST use the same value. The literal `'@bootUuid'` is a sentinel
|
|
340
|
+
* meaning "use the current boot UUID as the salt" (valid with `'salted'` or
|
|
341
|
+
* `'hmac'`). DEFAULTS to `'@bootUuid'` in 0.2.0 (only consulted when
|
|
342
|
+
* `mode !== 'plain'`); set a non-empty static value to pin a stable key.
|
|
343
|
+
*/
|
|
344
|
+
salt: string;
|
|
345
|
+
}
|
|
151
346
|
interface PasswordPolicyConfig {
|
|
152
347
|
/** Minimum total length. Default 8. */
|
|
153
348
|
minLength: number;
|
|
@@ -175,6 +370,14 @@ interface PasswordPolicyConfig {
|
|
|
175
370
|
customValidator?: (password: string) => string | null;
|
|
176
371
|
}
|
|
177
372
|
interface AuthConfig {
|
|
373
|
+
/**
|
|
374
|
+
* Whether email+password (credentials) auth is enabled. When `true` (default)
|
|
375
|
+
* `@luckystack/login/register` registers the credentials provider, so it shows
|
|
376
|
+
* up in `GET /auth/providers` and the login form renders the email/password
|
|
377
|
+
* fields. Set `false` for an OAuth-only app — the form hides the fields AND the
|
|
378
|
+
* `/auth/api/credentials` route rejects (`auth.credentialsDisabled`).
|
|
379
|
+
*/
|
|
380
|
+
credentials: boolean;
|
|
178
381
|
/** TTL for OAuth state tokens stored in Redis. */
|
|
179
382
|
oauthStateTtlSeconds: number;
|
|
180
383
|
/**
|
|
@@ -204,8 +407,9 @@ interface AuthConfig {
|
|
|
204
407
|
* `'per-provider'` (default) — same email via Google and GitHub creates
|
|
205
408
|
* two separate User rows (current behavior, no schema change).
|
|
206
409
|
* `'unified'` — same email maps to a single User; subsequent sign-ins via
|
|
207
|
-
* different providers
|
|
208
|
-
*
|
|
410
|
+
* different providers resolve to (link to) that same User row instead of
|
|
411
|
+
* creating a duplicate. Requires `email` to be `@unique` — see the
|
|
412
|
+
* "Account strategy" migration steps in @luckystack/login's README.
|
|
209
413
|
*/
|
|
210
414
|
providerAccountStrategy: 'per-provider' | 'unified';
|
|
211
415
|
/**
|
|
@@ -228,6 +432,27 @@ interface AuthConfig {
|
|
|
228
432
|
passwordResetBrand?: string;
|
|
229
433
|
/** Email-change confirmation-token TTL in seconds. Default 3600 (1 hour). */
|
|
230
434
|
emailChangeTtlSeconds: number;
|
|
435
|
+
/**
|
|
436
|
+
* Whether public self-service registration is permitted (login F18). When
|
|
437
|
+
* `false`, the credentials `/register` route rejects with
|
|
438
|
+
* `auth.registrationDisabled` and the login UI hides the "create account"
|
|
439
|
+
* affordance — useful for invite-only / admin-provisioned apps. OAuth-driven
|
|
440
|
+
* first-login account creation is governed separately by the provider flow.
|
|
441
|
+
* DEFAULT `true` (today's behavior — open registration).
|
|
442
|
+
*/
|
|
443
|
+
allowRegistration: boolean;
|
|
444
|
+
/**
|
|
445
|
+
* Frontend path the framework-mode password-reset email links to (login F22).
|
|
446
|
+
* The reset token is appended as a query param. DEFAULT `/reset-password`.
|
|
447
|
+
* Override when your reset page lives at a different route.
|
|
448
|
+
*/
|
|
449
|
+
passwordResetPath: string;
|
|
450
|
+
/**
|
|
451
|
+
* Frontend path the email-change confirmation email links to (login F22).
|
|
452
|
+
* The confirmation token is appended as a query param.
|
|
453
|
+
* DEFAULT `/confirm-email-change`.
|
|
454
|
+
*/
|
|
455
|
+
emailChangeConfirmPath: string;
|
|
231
456
|
}
|
|
232
457
|
interface OfflineQueueConfig {
|
|
233
458
|
/** Hard cap on items per queue (api + sync are tracked separately). Default 200. */
|
|
@@ -250,8 +475,45 @@ interface SyncStreamThrottleConfig {
|
|
|
250
475
|
/** Default `field` (payload key) for emitted chunks. */
|
|
251
476
|
field: string;
|
|
252
477
|
}
|
|
478
|
+
interface ApiConfig {
|
|
479
|
+
/**
|
|
480
|
+
* Default response timeout (ms) for `apiRequest`. After this elapses with no
|
|
481
|
+
* response (e.g. server restart/crash between emit and reply) the request
|
|
482
|
+
* settles with `{ status:'error', errorCode:'api.timeout', httpStatus:504 }`
|
|
483
|
+
* instead of hanging forever. Set `false` to disable the timeout. A per-call
|
|
484
|
+
* `timeoutMs` overrides this. Default 30000.
|
|
485
|
+
*/
|
|
486
|
+
requestTimeoutMs: number | false;
|
|
487
|
+
}
|
|
488
|
+
interface ValidationConfig {
|
|
489
|
+
/**
|
|
490
|
+
* Whether runtime input validation runs in PRODUCTION (CORE-01).
|
|
491
|
+
*
|
|
492
|
+
* - `'enforce'` (DEFAULT) — the structural validator (`validateType`) runs
|
|
493
|
+
* against the route's resolved input type in production too, so a malformed
|
|
494
|
+
* payload is rejected with `api.invalidInputType` / `sync.invalidInputType`
|
|
495
|
+
* instead of reaching the handler. Only the dev-only devkit DEEP type
|
|
496
|
+
* resolver (TypeScript compiler API) is skipped in prod — the already-resolved
|
|
497
|
+
* generated type text is validated directly.
|
|
498
|
+
* - `'off'` — restore the legacy behavior where prod skips input validation
|
|
499
|
+
* entirely (input shape is the handler's responsibility). This is the loud,
|
|
500
|
+
* documented opt-out; set it only if a route's generated type text can't be
|
|
501
|
+
* validated structurally in your deployment.
|
|
502
|
+
*
|
|
503
|
+
* DEFAULT `'enforce'`. Note: this CHANGES prior behavior (prod was a no-op).
|
|
504
|
+
* Set `'off'` to keep the old no-op.
|
|
505
|
+
*/
|
|
506
|
+
runtimeMode: 'enforce' | 'off';
|
|
507
|
+
}
|
|
253
508
|
interface SyncConfig {
|
|
254
509
|
streamThrottle: SyncStreamThrottleConfig;
|
|
510
|
+
/**
|
|
511
|
+
* Default response timeout (ms) for `syncRequest`'s acknowledgement. Same
|
|
512
|
+
* semantics as `api.requestTimeoutMs` but for sync. Settles with
|
|
513
|
+
* `sync.requestTimeout` / httpStatus 504 on expiry. `false` to disable.
|
|
514
|
+
* Default 30000.
|
|
515
|
+
*/
|
|
516
|
+
requestTimeoutMs: number | false;
|
|
255
517
|
/**
|
|
256
518
|
* Yield to the event loop every N recipients during a broadcast fanout
|
|
257
519
|
* (`receiver: 'all'` or large rooms). Lower = more responsive, higher
|
|
@@ -260,6 +522,43 @@ interface SyncConfig {
|
|
|
260
522
|
fanoutYieldEvery: number;
|
|
261
523
|
/** Milliseconds to sleep when yielding. Default 1ms. */
|
|
262
524
|
fanoutYieldMs: number;
|
|
525
|
+
/**
|
|
526
|
+
* Receiver-authorization policy (SYNC-07). These flags add framework-level
|
|
527
|
+
* defaults read by `@luckystack/sync`'s `authorizeSyncReceiver`:
|
|
528
|
+
*
|
|
529
|
+
* - `allowClientReceiverAll` — when false, a client requesting the broadcast
|
|
530
|
+
* receiver `'all'` is rejected unless a `preSyncAuthorize` handler approves
|
|
531
|
+
* it. DEFAULT `false` (0.2.0 secure-default flip — a client can no longer
|
|
532
|
+
* broadcast cluster-wide by default; opt back in with `true` for the legacy
|
|
533
|
+
* permissive behavior).
|
|
534
|
+
* - `requireRoomMembership` — when true, a client may only target a room it
|
|
535
|
+
* has actually joined (its `roomCodes`); targeting an unjoined room is
|
|
536
|
+
* rejected. DEFAULT `true` (0.2.0 secure-default flip — a client can no
|
|
537
|
+
* longer fan out to a room it never joined; set `false` for the legacy
|
|
538
|
+
* any-room behavior).
|
|
539
|
+
*
|
|
540
|
+
* BREAKING (0.2.0): both defaults now fail CLOSED. Apps that relied on
|
|
541
|
+
* implicit cluster-wide / arbitrary-room broadcasts must either join the room
|
|
542
|
+
* before targeting it, approve the receiver via a `preSyncAuthorize` handler,
|
|
543
|
+
* or explicitly opt back into the permissive values.
|
|
544
|
+
*/
|
|
545
|
+
allowClientReceiverAll: boolean;
|
|
546
|
+
requireRoomMembership: boolean;
|
|
547
|
+
/**
|
|
548
|
+
* Stream backpressure tuning (SYNC-15) for `createStreamThrottle` /
|
|
549
|
+
* server-initiated stream emits. Constants were previously hardcoded in the
|
|
550
|
+
* sync package; surfacing them here lets a consumer tune flush cadence under
|
|
551
|
+
* load without forking. DEFAULTS reproduce the historical hardcoded values.
|
|
552
|
+
*/
|
|
553
|
+
flushPressure: SyncFlushPressureConfig;
|
|
554
|
+
}
|
|
555
|
+
interface SyncFlushPressureConfig {
|
|
556
|
+
/** Max queued chunks before the stream applies backpressure (pauses). Default 1000. */
|
|
557
|
+
highWaterMarkChunks: number;
|
|
558
|
+
/** Resume emitting once the queue drains below this. Default 250. */
|
|
559
|
+
lowWaterMarkChunks: number;
|
|
560
|
+
/** Hard cap on bytes buffered for a single stream before dropping/erroring. Default 5_242_880 (5 MiB). */
|
|
561
|
+
maxBufferedBytes: number;
|
|
263
562
|
}
|
|
264
563
|
interface SocketConfig {
|
|
265
564
|
/** Maximum payload size for any single Socket.io message (bytes). */
|
|
@@ -268,6 +567,14 @@ interface SocketConfig {
|
|
|
268
567
|
pingTimeout: number;
|
|
269
568
|
/** ms between pings. */
|
|
270
569
|
pingInterval: number;
|
|
570
|
+
/**
|
|
571
|
+
* Minimum ms between client → server `activity` heartbeats (repo-src C-CFG /
|
|
572
|
+
* presence C1). The consumer template's activity tracker + `@luckystack/presence`
|
|
573
|
+
* read this to throttle how often mouse/keyboard/touch activity is reported
|
|
574
|
+
* (previously hardcoded to 10s). DEFAULT 10000 (10s) — a missing key keeps the
|
|
575
|
+
* historical cadence.
|
|
576
|
+
*/
|
|
577
|
+
activityHeartbeatThrottleMs: number;
|
|
271
578
|
}
|
|
272
579
|
interface DevConfig {
|
|
273
580
|
/** Debounce window for hot-reload triggers. */
|
|
@@ -333,7 +640,9 @@ interface ProjectConfig {
|
|
|
333
640
|
http: HttpConfig;
|
|
334
641
|
auth: AuthConfig;
|
|
335
642
|
socket: SocketConfig;
|
|
643
|
+
api: ApiConfig;
|
|
336
644
|
sync: SyncConfig;
|
|
645
|
+
validation: ValidationConfig;
|
|
337
646
|
offlineQueue: OfflineQueueConfig;
|
|
338
647
|
dev: DevConfig;
|
|
339
648
|
paths: PathsConfig;
|
|
@@ -348,10 +657,16 @@ interface ProjectConfig {
|
|
|
348
657
|
locationProviderEnabled?: boolean;
|
|
349
658
|
/** Where to redirect the user after a successful OAuth callback. */
|
|
350
659
|
loginRedirectUrl?: string;
|
|
660
|
+
/**
|
|
661
|
+
* Base origin used to build OAuth callback redirect URIs
|
|
662
|
+
* (`<oauthCallbackBase>/auth/callback/<provider>`). This is the BACKEND
|
|
663
|
+
* origin the provider redirects to — in dev typically `http://localhost:80`,
|
|
664
|
+
* in prod the public domain. Read by `@luckystack/login/register`'s env-driven
|
|
665
|
+
* provider scan so OAuth wiring needs no consumer code. Empty default — the
|
|
666
|
+
* consumer's `config.ts` sets it.
|
|
667
|
+
*/
|
|
668
|
+
oauthCallbackBase?: string;
|
|
351
669
|
}
|
|
352
|
-
type DeepPartial<T> = {
|
|
353
|
-
[K in keyof T]?: T[K] extends object | undefined ? DeepPartial<NonNullable<T[K]>> : T[K];
|
|
354
|
-
};
|
|
355
670
|
type ProjectConfigInput = DeepPartial<ProjectConfig>;
|
|
356
671
|
declare const DEFAULT_PROJECT_CONFIG: ProjectConfig;
|
|
357
672
|
declare const registerProjectConfig: (config: ProjectConfigInput) => void;
|
|
@@ -382,6 +697,8 @@ declare const setSocket: (next: Socket | null) => void;
|
|
|
382
697
|
declare const incrementResponseIndex: () => number;
|
|
383
698
|
declare const waitForSocket: () => Promise<Socket | null>;
|
|
384
699
|
|
|
700
|
+
/** Why a queued item was dropped (handed to its `onDrop` callback). */
|
|
701
|
+
type QueueDropReason = 'expired' | 'queue-full';
|
|
385
702
|
interface ApiQueueItem {
|
|
386
703
|
id: string;
|
|
387
704
|
key: string;
|
|
@@ -392,6 +709,14 @@ interface ApiQueueItem {
|
|
|
392
709
|
* `SyncQueueItem.dropPolicy` for details.
|
|
393
710
|
*/
|
|
394
711
|
dropPolicy?: 'drop-oldest' | 'drop-newest' | 'reject';
|
|
712
|
+
/**
|
|
713
|
+
* Optional callback invoked when THIS item is dropped (evicted for age or
|
|
714
|
+
* because the queue was full / rejected) instead of ever running (SYNC-09).
|
|
715
|
+
* The transport wires this to settle the pending `syncRequest`/`apiRequest`
|
|
716
|
+
* promise with an error envelope, so an evicted offline request resolves
|
|
717
|
+
* rather than hanging forever. Invoked at most once; failures are swallowed.
|
|
718
|
+
*/
|
|
719
|
+
onDrop?: (reason: QueueDropReason) => void;
|
|
395
720
|
}
|
|
396
721
|
interface SyncQueueItem {
|
|
397
722
|
id: string;
|
|
@@ -405,6 +730,8 @@ interface SyncQueueItem {
|
|
|
405
730
|
* the global config.
|
|
406
731
|
*/
|
|
407
732
|
dropPolicy?: 'drop-oldest' | 'drop-newest' | 'reject';
|
|
733
|
+
/** See {@link ApiQueueItem.onDrop} (SYNC-09). */
|
|
734
|
+
onDrop?: (reason: QueueDropReason) => void;
|
|
408
735
|
}
|
|
409
736
|
declare const isOnline: () => boolean;
|
|
410
737
|
declare const enqueueApiRequest: (item: ApiQueueItem) => boolean;
|
|
@@ -524,8 +851,10 @@ declare const socketEventNames: {
|
|
|
524
851
|
readonly logout: "logout";
|
|
525
852
|
readonly intentionalDisconnect: "intentionalDisconnect";
|
|
526
853
|
readonly intentionalReconnect: "intentionalReconnect";
|
|
854
|
+
readonly activity: "activity";
|
|
527
855
|
readonly userAfk: "userAfk";
|
|
528
856
|
readonly userBack: "userBack";
|
|
857
|
+
readonly userLeft: "userLeft";
|
|
529
858
|
readonly connect: "connect";
|
|
530
859
|
readonly disconnect: "disconnect";
|
|
531
860
|
readonly reconnectAttempt: "reconnect_attempt";
|
|
@@ -662,4 +991,4 @@ interface PagePathValidationResult {
|
|
|
662
991
|
*/
|
|
663
992
|
declare const validatePagePath: (srcRelativePath: string, rules?: PageRouteRules) => PagePathValidationResult;
|
|
664
993
|
|
|
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,
|
|
994
|
+
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 };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_PROJECT_CONFIG,
|
|
3
|
+
getProjectConfig,
|
|
4
|
+
getProjectName,
|
|
5
|
+
isProjectConfigRegistered,
|
|
6
|
+
registerProjectConfig
|
|
7
|
+
} from "./chunk-W62GGT2P.js";
|
|
8
|
+
import "./chunk-USQA6QTX.js";
|
|
9
|
+
export {
|
|
10
|
+
DEFAULT_PROJECT_CONFIG,
|
|
11
|
+
getProjectConfig,
|
|
12
|
+
getProjectName,
|
|
13
|
+
isProjectConfigRegistered,
|
|
14
|
+
registerProjectConfig
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=projectConfig-JX563K43.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {
|
|
2
|
+
captureException,
|
|
3
|
+
captureMessage,
|
|
4
|
+
initSharedSentry,
|
|
5
|
+
setSentryUser,
|
|
6
|
+
startSpan
|
|
7
|
+
} from "./chunk-V2WKGTZH.js";
|
|
8
|
+
import "./chunk-USQA6QTX.js";
|
|
9
|
+
export {
|
|
10
|
+
captureException,
|
|
11
|
+
captureMessage,
|
|
12
|
+
initSharedSentry,
|
|
13
|
+
setSentryUser,
|
|
14
|
+
startSpan
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=sentrySetup-Q4BKUAQ3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|