@luckystack/core 0.1.8 → 0.2.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.
@@ -2,7 +2,22 @@ import { Socket } from 'socket.io-client';
2
2
 
3
3
  declare function sleep(ms: number): Promise<void>;
4
4
 
5
- declare function tryCatch<T, P>(func: (values: P) => Promise<T> | T, params?: P, context?: Record<string, unknown>): Promise<[Error | null, T | null]>;
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 link an Account row to the same User. Requires a
208
- * Prisma schema change documented in @luckystack/login's README.
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, 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 };
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":[]}