@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/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { a7 as ErrorParam, a8 as ErrorResponseInput, a9 as NormalizedErrorResponse, b as AuthProps, B as BaseSessionLayout } from './pageRouteValidation-CxZJVwZh.js';
2
- export { A as ApiMethodMap, a as AppConfig, aa as AuthConfig, ab as CorsConfig, D as DEFAULT_PAGE_ROUTE_RULES, ac as DEFAULT_PROJECT_CONFIG, ad as DevConfig, ae as HttpConfig, H as HttpMethodLiteral, af as HttpStreamConfig, ag as INVALID_ERROR_RESPONSE_CODE, L as Logger, c as LoggerContext, d as LoggingConfig, N as Notifier, e as NotifyInput, f as NotifyParam, ah as OfflineQueueConfig, P as PagePathValidationResult, g as PageRouteRules, ai as PasswordPolicyConfig, aj as PathsConfig, h as ProjectConfig, ak as ProjectConfigInput, R as RateLimitingConfig, S as SOCKETSTATUS, al as SecurityHeadersConfig, am as ServiceRouteParseError, an as ServiceRouteParseResult, ao as ServiceRouteParseSuccess, i as SessionConfig, j as SessionLocation, ap as SocketConfig, aq as SyncConfig, ar as SyncStreamThrottleConfig, as as TransportRouteParseError, at as TransportRouteParseResult, au as TransportRouteParseSuccess, k as buildApiResponseEventName, l as buildApiStreamEventName, m as buildGetJoinedRoomsResponseEventName, n as buildJoinRoomResponseEventName, o as buildLeaveRoomResponseEventName, p as buildSyncProgressEventName, q as buildSyncResponseEventName, r as clearCsrfToken, av as createDevLogger, aw as defaultHttpStatusForResponse, s as enqueueApiRequest, t as enqueueSyncRequest, u as flushApiQueue, v as flushSyncQueue, w as getApiQueueSize, x as getCsrfToken, y as getLogger, z as getNotifier, C as getProjectConfig, ax as getProjectName, E as getRegisteredApiMethod, F as getSyncQueueSize, G as httpFetch, I as incrementResponseIndex, J as isApiMethodMapRegistered, ay as isErrorParamArray, az as isLoggerRegistered, K as isOnline, M as isProjectConfigRegistered, O as normalizeErrorResponseCore, Q as notify, T as parseServiceRouteName, aA as parseTransportRouteName, U as registerApiMethodMap, V as registerLogger, W as registerNotifier, X as registerProjectConfig, Y as removeApiQueueItem, Z as removeApiQueueItemsByKey, _ as removeSyncQueueItem, aB as resetLoggerForTests, $ as setSocket, a0 as sleep, a1 as socket, a2 as socketEventNames, a3 as statusContent, a4 as tryCatch, a5 as validatePagePath, a6 as waitForSocket } from './pageRouteValidation-CxZJVwZh.js';
1
+ import { a6 as ErrorParam, a7 as ErrorResponseInput, a8 as NormalizedErrorResponse, a9 as HealthHashConfig, aa as DeepPartial, B as BaseSessionLayout, b as AuthProps } from './pageRouteValidation-BoZ-rQWi.js';
2
+ export { ab as ApiConfig, A as ApiMethodMap, a as AppConfig, ac as AuthConfig, ad as AuthRateLimitConfig, ae as CorsConfig, D as DEFAULT_PAGE_ROUTE_RULES, af as DEFAULT_PROJECT_CONFIG, ag as DevConfig, ah as HttpConfig, H as HttpMethodLiteral, ai as HttpStreamConfig, aj as INVALID_ERROR_RESPONSE_CODE, L as Logger, c as LoggerContext, d as LoggingConfig, N as Notifier, e as NotifyInput, f as NotifyParam, ak as OfflineQueueConfig, P as PagePathValidationResult, g as PageRouteRules, al as PasswordPolicyConfig, am as PathsConfig, h as ProjectConfig, an as ProjectConfigInput, ao as QueueDropReason, ap as RateLimitIdentity, aq as RateLimitIdentityParams, R as RateLimitingConfig, S as SOCKETSTATUS, ar as SecurityHeadersConfig, as as ServiceRouteParseError, at as ServiceRouteParseResult, au as ServiceRouteParseSuccess, i as SessionConfig, j as SessionLocation, av as SocketConfig, aw as SyncConfig, ax as SyncFlushPressureConfig, ay as SyncStreamThrottleConfig, az as TransportRouteParseError, aA as TransportRouteParseResult, aB as TransportRouteParseSuccess, aC as ValidationConfig, k as buildApiResponseEventName, l as buildApiStreamEventName, m as buildGetJoinedRoomsResponseEventName, n as buildJoinRoomResponseEventName, o as buildLeaveRoomResponseEventName, p as buildSyncProgressEventName, q as buildSyncResponseEventName, r as clearCsrfToken, aD as createDevLogger, aE as deepMerge, aF as defaultHttpStatusForResponse, s as enqueueApiRequest, t as enqueueSyncRequest, u as flushApiQueue, v as flushSyncQueue, w as getApiQueueSize, x as getCsrfToken, y as getLogger, z as getNotifier, C as getProjectConfig, aG as getProjectName, E as getRegisteredApiMethod, F as getSyncQueueSize, G as httpFetch, I as incrementResponseIndex, J as isApiMethodMapRegistered, aH as isErrorParamArray, aI as isLoggerRegistered, K as isOnline, aJ as isPlainObject, M as isProjectConfigRegistered, O as normalizeErrorResponseCore, Q as notify, T as parseServiceRouteName, aK as parseTransportRouteName, U as registerApiMethodMap, V as registerLogger, W as registerNotifier, X as registerProjectConfig, Y as removeApiQueueItem, Z as removeApiQueueItemsByKey, _ as removeSyncQueueItem, aL as resetLoggerForTests, $ as setSocket, a0 as sleep, a1 as socket, a2 as socketEventNames, a3 as statusContent, a4 as validatePagePath, a5 as waitForSocket } from './pageRouteValidation-BoZ-rQWi.js';
3
3
  export { ApiTypeMap, StreamPayload, SyncTypeMap } from './apiTypeStubs.js';
4
4
  import { Server, Socket } from 'socket.io';
5
5
  import { z } from 'zod';
@@ -7,9 +7,100 @@ import * as _prisma_client_runtime_library from '@prisma/client/runtime/library'
7
7
  import * as _prisma_client from '@prisma/client';
8
8
  import { PrismaClient } from '@prisma/client';
9
9
  import Redis, { Redis as Redis$1 } from 'ioredis';
10
- import { ServerResponse, IncomingMessage } from 'node:http';
10
+ import { RedisAdapterOptions } from '@socket.io/redis-adapter';
11
+ import { ServerResponse, IncomingMessage, IncomingHttpHeaders } from 'node:http';
11
12
  import 'socket.io-client';
12
13
 
14
+ declare function tryCatch<T, P = void>(func: (values: P) => Promise<T> | T, params?: P, context?: Record<string, unknown>): Promise<[Error | null, T | null]>;
15
+
16
+ declare function tryCatchSync<T, P>(func: (values: P) => T, params?: P): [Error | null, T | null];
17
+
18
+ type RequireTransform<TInput, TStored> = [TInput] extends [TStored] ? {
19
+ transform?: (input: TInput, current: TStored) => TStored;
20
+ } : {
21
+ transform: (input: TInput, current: TStored) => TStored;
22
+ };
23
+ interface RegistryOptionsBase<TStored> {
24
+ /**
25
+ * Called once per `register` with the freshly-stored value — used for the
26
+ * rate-limiter's `[RateLimiter] active strategy → …` debug line. Runs after
27
+ * the slot is updated so a logger registry can log through itself.
28
+ */
29
+ onRegister?: (stored: TStored) => void;
30
+ /**
31
+ * Resolve the value `get` returns when nothing has been registered yet.
32
+ * Defaults to returning `defaultValue`. `servicesConfig` throws here.
33
+ */
34
+ resolveDefault?: () => TStored;
35
+ }
36
+ type RegistryOptions<TInput, TStored> = RegistryOptionsBase<TStored> & RequireTransform<TInput, TStored>;
37
+ interface Registry<TInput, TStored> {
38
+ /** Store a value (after the optional `transform`) and mark the slot registered. */
39
+ register: (input: TInput) => TStored;
40
+ /** Read the active value at call time (registered value, else the default). */
41
+ get: () => TStored;
42
+ /** True once `register` has been called at least once. */
43
+ isRegistered: () => boolean;
44
+ /** Restore the unregistered state (test-only). */
45
+ reset: () => void;
46
+ }
47
+ /**
48
+ * Create a single-slot registry. `defaultValue` is returned by `get` until
49
+ * `register` is called (unless `resolveDefault` overrides that). When
50
+ * `TInput` differs from `TStored`, the `transform` option is mandatory (the
51
+ * `RequireTransform` constraint enforces it); when they match it is optional
52
+ * and defaults to identity. The second positional argument is likewise
53
+ * optional only in the identity case.
54
+ */
55
+ declare function createRegistry<TStored, TInput = TStored>(defaultValue: TStored, ...optionsArg: [TInput] extends [TStored] ? [options?: RegistryOptions<TInput, TStored>] : [options: RegistryOptions<TInput, TStored>]): Registry<TInput, TStored>;
56
+
57
+ /**
58
+ * Escape `&`, `<`, `>`, `"`, `'` for safe interpolation into HTML text nodes
59
+ * and quoted attribute values. Does NOT protect against CSS/JS/URL-context
60
+ * injection — use a dedicated sanitizer for those contexts.
61
+ */
62
+ declare const escapeHtml: (str: string) => string;
63
+
64
+ /** Minimal shape of a Node `require` with the `.resolve` member these guards use. */
65
+ interface PeerRequire {
66
+ (id: string): unknown;
67
+ resolve: (id: string) => string;
68
+ }
69
+ /**
70
+ * Assert that an optional peer package is installed, throwing a consistent,
71
+ * actionable error when it is not. Does NOT load the module — use `loadPeer`
72
+ * for that. Pass `requireFn` (a `createRequire(import.meta.url)` from the
73
+ * calling adapter) so resolution happens from the consumer's perspective.
74
+ *
75
+ * `hint` is appended to the standard "package X is not installed" sentence —
76
+ * e.g. `'Run `npm install resend`, or pick a different EmailSender adapter.'`.
77
+ */
78
+ declare const ensurePeerDepInstalled: (packageName: string, hint: string, requireFn?: PeerRequire) => void;
79
+ /**
80
+ * Synchronously load an optional peer package, asserting it is installed
81
+ * first (so a missing dependency fails with the actionable `hint` rather than
82
+ * a raw `MODULE_NOT_FOUND`). Returns the module typed as `T`. Pass `requireFn`
83
+ * (built with `createRequire(import.meta.url)` at the call site) so resolution
84
+ * + load happen relative to the calling module.
85
+ */
86
+ declare const loadPeer: <T>(packageName: string, hint: string, requireFn?: PeerRequire) => T;
87
+
88
+ interface RoomNameFormatterContext {
89
+ /** Why the room name is being formatted — so a formatter can branch if needed. */
90
+ purpose: 'join' | 'leave' | 'broadcast' | 'presence';
91
+ /** Session user id when known (join/leave by an authenticated socket), else null. */
92
+ userId?: string | null;
93
+ }
94
+ type RoomNameFormatter = (rawRoomName: string, ctx: RoomNameFormatterContext) => string;
95
+ /** Override how every framework room name is namespaced (multi-tenant prefixing). */
96
+ declare const registerRoomNameFormatter: (formatter: RoomNameFormatter) => void;
97
+ /** Read the active formatter (defaults to identity). */
98
+ declare const getRoomNameFormatter: () => RoomNameFormatter;
99
+ /** Apply the active formatter — the single helper framework room-sites call. */
100
+ declare const formatRoomName: (rawRoomName: string, ctx: RoomNameFormatterContext) => string;
101
+ /** The built-in identity formatter (exported for tests / explicit reset). */
102
+ declare const defaultRoomNameFormatter: RoomNameFormatter;
103
+
13
104
  type LanguageCode = string;
14
105
  interface TranslateInput {
15
106
  language: LanguageCode;
@@ -68,13 +159,26 @@ interface ErrorFormatterContext {
68
159
  /** Session id when available. */
69
160
  userId?: string | null;
70
161
  }
71
- type ErrorFormatter = (error: {
162
+ /** The normalized error envelope handed to every formatter. */
163
+ interface FormatterErrorEnvelope {
72
164
  status: 'error';
73
165
  errorCode?: string;
74
166
  message?: string;
75
167
  httpStatus?: number;
76
168
  [key: string]: unknown;
77
- }, ctx: ErrorFormatterContext) => Record<string, unknown>;
169
+ }
170
+ type ErrorFormatter = (error: FormatterErrorEnvelope, ctx: ErrorFormatterContext) => Record<string, unknown>;
171
+ /**
172
+ * Minimal structural contract every formatter-eligible envelope satisfies:
173
+ * a discriminant `status` plus an open string-index so formatters can read /
174
+ * add arbitrary fields. Generic helpers below constrain to this so transport
175
+ * handlers no longer need `as unknown as` double-casts at the formatter
176
+ * boundary — see `applyErrorFormatter` / `buildFormattedError`.
177
+ */
178
+ interface FormatterEnvelope {
179
+ status?: string;
180
+ [key: string]: unknown;
181
+ }
78
182
  /**
79
183
  * Register a global error-response formatter. Receives the normalized
80
184
  * error envelope + context; return a (possibly extended) object that the
@@ -94,15 +198,13 @@ declare const getErrorFormatter: () => ErrorFormatter | null;
94
198
  * are caught and logged; the un-formatted envelope is returned in that
95
199
  * case to keep the error path crash-resistant.
96
200
  */
97
- declare const applyErrorFormatter: (input: {
98
- response: Record<string, unknown> & {
99
- status?: string;
100
- };
201
+ declare const applyErrorFormatter: <T extends FormatterEnvelope>(input: {
202
+ response: T;
101
203
  routeName: string;
102
204
  transport: "socket" | "http";
103
205
  userId?: string | null;
104
206
  perRouteFormatter?: ErrorFormatter | undefined | null;
105
- }) => Record<string, unknown>;
207
+ }) => T;
106
208
 
107
209
  interface apiMessage {
108
210
  name: string;
@@ -138,6 +240,28 @@ declare const isRuntimeMapsProviderRegistered: () => boolean;
138
240
  declare const getRuntimeApiMaps: () => Promise<RuntimeApiMapsResult>;
139
241
  declare const getRuntimeSyncMaps: () => Promise<RuntimeSyncMapsResult>;
140
242
 
243
+ /**
244
+ * A single email attachment (email F2). Adapters (Resend, SMTP/Nodemailer, SES)
245
+ * thread these into their provider payload. `content` is the raw bytes
246
+ * (`Buffer`/`Uint8Array`) or a base64 string; provide a `path`/`href` instead
247
+ * for adapters that fetch externally. Shapes are intentionally permissive so a
248
+ * single attachment object works across providers — each adapter maps the
249
+ * fields it supports.
250
+ */
251
+ interface EmailAttachment {
252
+ /** File name shown to the recipient (e.g. `invoice.pdf`). */
253
+ filename: string;
254
+ /** Inline bytes or base64 string. Mutually exclusive with `path`/`href`. */
255
+ content?: Buffer | Uint8Array | string;
256
+ /** Local file path the adapter reads (when it supports path-based sends). */
257
+ path?: string;
258
+ /** Remote URL the adapter fetches (when it supports href-based sends). */
259
+ href?: string;
260
+ /** MIME type (e.g. `application/pdf`). Adapter may infer from `filename` if omitted. */
261
+ contentType?: string;
262
+ /** `Content-ID` for inline/embedded images referenced by `cid:` in the HTML. */
263
+ cid?: string;
264
+ }
141
265
  interface EmailMessage {
142
266
  to: string | string[];
143
267
  subject: string;
@@ -147,6 +271,20 @@ interface EmailMessage {
147
271
  replyTo?: string;
148
272
  cc?: string | string[];
149
273
  bcc?: string | string[];
274
+ /**
275
+ * File attachments (email F2). Owned by core (not `@luckystack/email`) so the
276
+ * type is the single source of truth across the framework and every adapter;
277
+ * adapters thread these into their provider's attachment payload. Optional —
278
+ * a message without attachments behaves exactly as before.
279
+ */
280
+ attachments?: EmailAttachment[];
281
+ /**
282
+ * Custom message headers (email F2) — e.g. `X-Entity-Ref-ID`,
283
+ * `List-Unsubscribe`, idempotency keys. Adapters merge these over the headers
284
+ * they set themselves (adapter-reserved headers win to avoid breaking
285
+ * delivery). Optional.
286
+ */
287
+ headers?: Record<string, string>;
150
288
  }
151
289
  type EmailResult = {
152
290
  ok: true;
@@ -228,6 +366,31 @@ interface DeployRoutingShape {
228
366
  healthProbeTimeoutMs?: number;
229
367
  /** Seconds the boot-UUID Redis key lives for (default 3600 = 1h). */
230
368
  bootKeyTtlSeconds?: number;
369
+ /**
370
+ * ms before the router gives up proxying to an upstream backend (SEC-30 /
371
+ * router-DeployRoutingShape). Guards against slow-loris upstreams holding a
372
+ * router worker open. The router reads this when building its proxy request.
373
+ * DEFAULT undefined → router falls back to its own built-in default.
374
+ */
375
+ upstreamTimeoutMs?: number;
376
+ /**
377
+ * Name of the service that terminates websocket (Socket.io) traffic. The
378
+ * router pins WS upgrade requests to this service (they cannot be load-balanced
379
+ * round-robin without sticky sessions). DEFAULT undefined → router uses its
380
+ * `system` convention.
381
+ */
382
+ websocketService?: string;
383
+ /**
384
+ * Path the router exposes for its OWN health/liveness (distinct from the
385
+ * backend `/_health` boot-handshake endpoint). DEFAULT undefined → router
386
+ * uses its built-in default.
387
+ */
388
+ routerHealthPath?: string;
389
+ /**
390
+ * Max request body size (bytes) the router will buffer/forward before
391
+ * rejecting with 413. DEFAULT undefined → router uses its built-in default.
392
+ */
393
+ maxRequestBodyBytes?: number;
231
394
  }
232
395
  interface DeployDevelopmentShape {
233
396
  enableFallbackRouting?: boolean;
@@ -283,6 +446,11 @@ declare const setSentryUser: (user: {
283
446
  declare const startSpan: (name: string, op: string) => unknown;
284
447
 
285
448
  type ErrorTrackerContext = Record<string, unknown>;
449
+ declare const sanitizeErrorString: (value: string) => string;
450
+ declare const sanitizeErrorStrings: (error: unknown) => {
451
+ message: string;
452
+ stack: string | undefined;
453
+ } | null;
286
454
  interface ErrorTrackerUser {
287
455
  id?: string;
288
456
  email?: string;
@@ -306,15 +474,75 @@ interface ErrorTracker {
306
474
  startSpan?: <T>(name: string, op: string, fn: () => T) => T;
307
475
  recordMetric?: (name: string, value: number, tags?: Record<string, string>) => void;
308
476
  beforeSend?: (event: ErrorTrackerEvent) => ErrorTrackerEvent | null;
477
+ /**
478
+ * Optional flush lifecycle hook (error-tracking ET batch). Called by
479
+ * {@link flushErrorTrackers} on graceful shutdown so a buffered adapter
480
+ * (PostHog batch, Sentry transport) can drain in-flight events before exit.
481
+ * Returns a promise that resolves when the adapter has flushed.
482
+ */
483
+ flush?: () => Promise<void>;
309
484
  }
485
+ /**
486
+ * Open a per-request error-tracker identity scope and run `fn` inside it. Call at
487
+ * request ENTRY (before any await that could interleave with another request); the
488
+ * identity starts null and is filled in later via {@link setCurrentErrorTrackerIdentity}
489
+ * once the session is known. Each invocation gets an isolated box (ET-02).
490
+ */
491
+ declare const runWithErrorTrackerIdentityScope: <T>(fn: () => T) => T;
492
+ /**
493
+ * Run `fn` with `user` bound as the ambient error-tracker identity (ET-02). Convenience
494
+ * wrapper that opens a scope and immediately sets the identity — used where the user is
495
+ * already known up front (and by the adapter regression tests).
496
+ */
497
+ declare const runWithErrorTrackerIdentity: <T>(user: ErrorTrackerUser | null, fn: () => T) => T;
498
+ /**
499
+ * Write `user` into the active identity box opened by {@link runWithErrorTrackerIdentityScope}
500
+ * / {@link runWithErrorTrackerIdentity}. No-op when called outside any scope (a background /
501
+ * non-request capture has no per-request box; adapters fall back to their own global).
502
+ */
503
+ declare const setCurrentErrorTrackerIdentity: (user: ErrorTrackerUser | null) => void;
504
+ /** Read the ambient per-event identity for the active request scope, if any. */
505
+ declare const getCurrentErrorTrackerIdentity: () => ErrorTrackerUser | null;
506
+ type PreCaptureFilter = (event: ErrorTrackerEvent) => boolean;
507
+ declare const registerPreCaptureFilter: (filter: PreCaptureFilter | null) => void;
310
508
  declare const registerErrorTracker: (tracker: ErrorTracker) => void;
311
509
  declare const registerErrorTrackers: (trackers: ErrorTracker[]) => void;
510
+ /**
511
+ * APPEND primitive (ET-05): add a tracker WITHOUT clobbering already-registered
512
+ * ones. Fixes the async-PostHog-vs-consumer-overlay race where the loser of
513
+ * `registerErrorTracker`'s replace silently vanished. De-duplicates by `name`
514
+ * (re-appending the same adapter name replaces that one entry in place) so a
515
+ * lazy adapter that registers a proxy then the real client doesn't double-fire.
516
+ */
517
+ declare const appendErrorTracker: (tracker: ErrorTracker) => void;
312
518
  declare const getActiveErrorTrackers: () => ErrorTracker[];
519
+ /**
520
+ * Flush lifecycle (error-tracking ET batch): drain every adapter that exposes a
521
+ * `flush()` before shutdown. Per-adapter failures are swallowed + logged so one
522
+ * stuck adapter can't block graceful shutdown. Safe to call when no adapter has
523
+ * `flush`.
524
+ */
525
+ declare const flushErrorTrackers: () => Promise<void>;
313
526
  declare const captureExceptionAcrossTrackers: (error: unknown, context?: ErrorTrackerContext) => void;
314
527
  declare const captureMessageAcrossTrackers: (message: string, level: "info" | "warning" | "error" | "fatal", context?: ErrorTrackerContext) => void;
315
528
  declare const setErrorTrackerUser: (user: ErrorTrackerUser | null) => void;
316
529
  declare const recordMetricAcrossTrackers: (name: string, value: number, tags?: Record<string, string>) => void;
317
530
  declare const startSpanAcrossTrackers: <T>(name: string, op: string, fn: () => T) => T;
531
+ /**
532
+ * Handle-style span (error-tracking ET batch): for callers that can't wrap the
533
+ * measured work in a single synchronous `fn` (streaming, cross-await spans).
534
+ * Returns a handle whose `finish()` resolves the span's duration. The default
535
+ * implementation is a lightweight wall-clock timer (works with any backend);
536
+ * an adapter can supply a richer span via the existing `startSpan` callback
537
+ * form when it needs native instrumentation. `finish()` is idempotent.
538
+ */
539
+ interface SpanHandle {
540
+ /** Stop the span; safe to call more than once (subsequent calls are no-ops). */
541
+ finish: () => void;
542
+ /** Milliseconds elapsed at the moment `finish()` first ran (0 before). */
543
+ readonly durationMs: number;
544
+ }
545
+ declare const startSpanHandle: (_name: string, _op: string) => SpanHandle;
318
546
 
319
547
  declare const EnvSchema: z.ZodObject<{
320
548
  NODE_ENV: z.ZodDefault<z.ZodEnum<{
@@ -348,6 +576,7 @@ declare const env: {
348
576
  PROJECT_NAME: string;
349
577
  };
350
578
  declare const isProduction: boolean;
579
+ declare const getEnv: () => RuntimeEnv;
351
580
 
352
581
  declare global {
353
582
  var __luckystackPrisma: PrismaClient | undefined;
@@ -361,6 +590,7 @@ interface RedisConnectionOptions {
361
590
  password?: string;
362
591
  }
363
592
  declare const getRedisConnectionOptions: () => RedisConnectionOptions;
593
+ declare const registerStrayPrefixCommand: (...commands: string[]) => void;
364
594
  declare const redis: Redis;
365
595
 
366
596
  type RedisKeyFormatter = (namespace: string, suffix: string) => string;
@@ -403,26 +633,147 @@ declare const getPrismaClient: () => PrismaClient;
403
633
  declare const getRedisClient: () => Redis$1;
404
634
  declare const resetClientsForTests: () => void;
405
635
 
406
- declare const attachSocketRedisAdapter: (io: Server) => void;
636
+ interface AttachSocketRedisAdapterOptions {
637
+ /**
638
+ * Options forwarded to `@socket.io/redis-adapter`'s `createAdapter`
639
+ * (e.g. `key` prefix, `requestsTimeout`, `publishOnSpecificResponseChannel`).
640
+ */
641
+ adapterOptions?: Partial<RedisAdapterOptions>;
642
+ /**
643
+ * Supply a pre-built pub client instead of `redis.duplicate()`. Use this for
644
+ * a dedicated adapter connection, custom auth, or a sentinel/cluster handle.
645
+ * When omitted the framework duplicates the default `redis` handle.
646
+ */
647
+ pubClient?: Redis$1;
648
+ /** Pre-built sub client (see `pubClient`). */
649
+ subClient?: Redis$1;
650
+ }
651
+ declare const attachSocketRedisAdapter: (io: Server, options?: AttachSocketRedisAdapterOptions) => void;
407
652
 
408
653
  declare const BOOT_KEY_PREFIX = "luckystack:boot:";
409
654
  declare const resolveEnvKey: () => string;
410
655
  declare const writeBootUuid: (envKey?: string) => Promise<string>;
411
656
  declare const readBootUuid: (envKey?: string) => Promise<string | null>;
412
657
 
658
+ /**
659
+ * Hash a synchronized value under an EXPLICIT `{ mode, salt }` (SEC-13). The
660
+ * shared primitive the router calls with the same config the backend used, so
661
+ * the cross-env compare still holds:
662
+ * - `'plain'` → `sha256(value)` (today's behavior; `salt` ignored).
663
+ * - `'salted'` → `sha256(salt + value)`.
664
+ * - `'hmac'` → `HMAC-SHA256(key=salt, value)`.
665
+ * A `'salted'`/`'hmac'` mode with an empty resolved salt falls back to `'plain'`
666
+ * (and is the caller's responsibility to avoid) rather than silently hashing
667
+ * with an empty key.
668
+ */
669
+ declare const hashSynchronizedValueWith: (config: Pick<HealthHashConfig, "mode"> & {
670
+ salt: string;
671
+ }, value: string) => string;
672
+ /**
673
+ * Resolve the effective health-hash config from `http.healthHash`, expanding the
674
+ * `'@bootUuid'` salt sentinel to the supplied boot UUID. Honoured for BOTH
675
+ * `'salted'` and `'hmac'` (the 0.2.0 default is `hmac` + `'@bootUuid'`, so the
676
+ * per-boot UUID becomes the HMAC key — the synchronized-env fingerprint rotates
677
+ * every restart instead of being a stable, dictionary-attackable `sha256`). The
678
+ * server passes its current boot UUID; the router, comparing, passes the same
679
+ * value it read from the backend's handshake. When the sentinel is in play but
680
+ * no boot UUID is available the config collapses to `'plain'` so the cross-env
681
+ * compare never silently diverges.
682
+ */
683
+ declare const resolveHealthHashConfig: (bootUuid?: string | null) => {
684
+ mode: HealthHashConfig["mode"];
685
+ salt: string;
686
+ };
687
+ /**
688
+ * Safe-to-expose descriptor of the resolved health-hash config. The router needs
689
+ * to hash with the SAME `{ mode, salt }` the backend used, but a static `salt`
690
+ * (mode `'salted'`/`'hmac'` with a non-sentinel salt) is a SECRET and must never
691
+ * leave the backend. So `/_health` exposes only the `mode` + whether the salt is
692
+ * the `'@bootUuid'` sentinel (the bootUuid itself is already in the response).
693
+ * From this the router can reproduce the hash for `'plain'` and `'@bootUuid'`
694
+ * configs, and KNOWS it cannot reproduce a static-salt config (so it skips the
695
+ * compare instead of reporting a false drift).
696
+ */
697
+ interface HealthHashDescriptor {
698
+ mode: HealthHashConfig['mode'];
699
+ bootUuidSalt: boolean;
700
+ }
701
+ /** Describe the active health-hash config for the `/_health` payload (no secret salt). */
702
+ declare const describeHealthHashConfig: () => HealthHashDescriptor;
703
+ /**
704
+ * Reproduce the backend's resolved `{ mode, salt }` from its `/_health`
705
+ * descriptor + the shared boot UUID. Returns `null` when the backend uses a
706
+ * STATIC salt the router cannot see — the caller must then skip the compare
707
+ * rather than hash with a different (default) config and report a false drift.
708
+ */
709
+ declare const resolveHealthHashConfigFromDescriptor: (descriptor: HealthHashDescriptor, bootUuid: string) => {
710
+ mode: HealthHashConfig["mode"];
711
+ salt: string;
712
+ } | null;
413
713
  declare const collectSynchronizedEnvKeys: () => string[];
414
- declare const computeSynchronizedEnvHashes: () => Record<string, string | null>;
415
- declare const hashSynchronizedValue: (value: string) => string;
714
+ /**
715
+ * Compute the `/_health` hash map for every synchronized env var. Reads
716
+ * `http.healthHash` (call-time) so a consumer's salt/HMAC opt-in applies;
717
+ * `bootUuid` is consulted only when the salt is the `'@bootUuid'` sentinel.
718
+ * The 0.2.0 DEFAULT (`{ mode: 'hmac', salt: '@bootUuid' }`) HMAC-keys the output
719
+ * on `bootUuid`; pass `mode: 'plain'` to restore the historical unsalted output.
720
+ */
721
+ declare const computeSynchronizedEnvHashes: (bootUuid?: string | null) => Record<string, string | null>;
722
+ declare const hashSynchronizedValue: (value: string, bootUuid?: string | null) => string;
416
723
 
417
724
  declare const initConsolelog: () => void;
418
725
 
726
+ declare const DEFAULT_REDACTED_LOG_KEYS: readonly string[];
419
727
  declare const registerRedactedLogKeys: (keys: readonly string[]) => void;
420
728
  declare const getRedactedLogKeys: () => readonly string[];
421
729
  declare const isRedactedLogKey: (key: string) => boolean;
422
730
  declare const resetRedactedLogKeysForTests: () => void;
731
+ /** Placeholder substituted for a redacted value. */
732
+ declare const REDACTED_PLACEHOLDER = "[redacted]";
733
+ declare const DEPTH_TRUNCATED_PLACEHOLDER = "[truncated: max depth]";
734
+ /**
735
+ * Recursively deep-copy `value`, replacing any property whose key matches the
736
+ * registered redacted-keys set (case-insensitive) with {@link REDACTED_PLACEHOLDER}
737
+ * (SYNC-17 defense-in-depth). Use it at raw log / `captureException` context
738
+ * sites so a token/password nested anywhere in a payload never lands verbatim in
739
+ * a logger sink or error-tracker breadcrumb.
740
+ *
741
+ * - Primitives pass through unchanged.
742
+ * - Arrays are mapped element-wise (array indices are never "keys", so an
743
+ * element is only redacted by its own nested object keys).
744
+ * - Cycles / over-deep graphs are cut at {@link MAX_SANITIZE_DEPTH} with
745
+ * {@link DEPTH_TRUNCATED_PLACEHOLDER}; the input is never mutated.
746
+ */
747
+ declare const sanitizeForLog: (value: unknown, depth?: number) => unknown;
423
748
 
424
749
  declare const getCookieValue: (cookieHeader: string | undefined, cookieName: string) => string | null;
425
750
  declare const hasCookie: (cookieHeader: string | undefined, cookieName: string) => boolean;
751
+ /** Attributes a cookie-builder must honour, after applying any name prefix. */
752
+ interface CookiePrefixConstraints {
753
+ /** Effective cookie name (prefix prepended when applicable). */
754
+ name: string;
755
+ /** Whether `Secure` must be set (forced true by either prefix). */
756
+ secure: boolean;
757
+ /** Whether `Path=/` is forced (true under `__Host-`). */
758
+ forcePathRoot: boolean;
759
+ /** Whether a `Domain` attribute is FORBIDDEN (true under `__Host-`). */
760
+ forbidDomain: boolean;
761
+ }
762
+ /**
763
+ * Compute the attribute constraints a `__Host-` / `__Secure-` cookie-name prefix
764
+ * imposes (CORE-10/39), so the server's session + CSRF cookie builders apply the
765
+ * browser-enforced rules consistently:
766
+ *
767
+ * - `'__Host-'` → name prefixed, `Secure` forced, `Path=/` forced, `Domain` forbidden.
768
+ * - `'__Secure-'` → name prefixed, `Secure` forced.
769
+ * - `undefined` → name unchanged, no forced attributes (today's behavior).
770
+ *
771
+ * Pure + deterministic — the builder still owns serialization; this only
772
+ * resolves the constraints so server and any other cookie-site can't drift.
773
+ * `secureOverride` lets the caller force `Secure` independent of a prefix
774
+ * (mirrors `http.sessionCookieSecure`).
775
+ */
776
+ declare const applyCookiePrefixConstraints: (baseName: string, prefix: "__Host-" | "__Secure-" | undefined, secureOverride?: boolean) => CookiePrefixConstraints;
426
777
 
427
778
  /**
428
779
  * HTTP API Utilities
@@ -432,6 +783,13 @@ declare const hasCookie: (cookieHeader: string | undefined, cookieName: string)
432
783
  * - Method validation
433
784
  */
434
785
  type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
786
+ /**
787
+ * Single source of truth for the GET-prefix naming heuristic
788
+ * (`get` / `fetch` / `list`). Used by both `inferHttpMethod` here and the
789
+ * client transport's `isGetMethod` fallback in `apiRequest.ts`, so the two
790
+ * prefix heuristics can't drift apart. Expects an already-lowercased leaf name.
791
+ */
792
+ declare const isGetMethodName: (methodNameLower: string) => boolean;
435
793
  /**
436
794
  * Infer the HTTP method from an API name based on common naming conventions.
437
795
  *
@@ -462,7 +820,13 @@ declare const getEffectiveHttpMethod: (apiConfig: {
462
820
  *
463
821
  * @param requestMethod - The method from the HTTP request
464
822
  * @param allowedMethod - The method the API accepts
465
- * @returns true if the method matches or if it's a preflight OPTIONS request
823
+ * @returns true only when the method matches the route's allowed method.
824
+ *
825
+ * NOTE: `OPTIONS` is intentionally NOT treated as allowed here. A custom-route
826
+ * author wiring this into their dispatch must answer CORS preflights BEFORE the
827
+ * route check — letting `OPTIONS` through would execute the handler on a request
828
+ * the CSRF middleware treats as non-state-changing (CSRF-exempt), opening a
829
+ * bypass on method-locked routes.
466
830
  */
467
831
  declare const isMethodAllowed: (requestMethod: string, allowedMethod: HttpMethod) => boolean;
468
832
 
@@ -634,9 +998,6 @@ interface AvatarConfig {
634
998
  cacheControl: string;
635
999
  }
636
1000
  declare const DEFAULT_AVATAR_CONFIG: AvatarConfig;
637
- type DeepPartial<T> = {
638
- [K in keyof T]?: T[K] extends object | undefined ? DeepPartial<NonNullable<T[K]>> : T[K];
639
- };
640
1001
  type AvatarConfigInput = DeepPartial<AvatarConfig>;
641
1002
  declare const registerAvatarConfig: (config: AvatarConfigInput) => void;
642
1003
  declare const getAvatarConfig: () => AvatarConfig;
@@ -673,6 +1034,90 @@ declare const extractTokenFromSocket: (socket: Pick<Socket, "handshake">) => str
673
1034
  */
674
1035
  declare const extractTokenFromRequest: (req: IncomingMessage) => string | null;
675
1036
 
1037
+ /**
1038
+ * Stable sentinel returned when no usable client address could be resolved.
1039
+ * Kept as a single exported constant so every rate-limit key-site funnels
1040
+ * missing addresses into one deterministic bucket instead of ad-hoc literals.
1041
+ */
1042
+ declare const UNKNOWN_CLIENT_IP = "unknown";
1043
+ interface ResolveClientIpParams {
1044
+ /**
1045
+ * The raw transport peer address — `socket.handshake.address` (Socket.io)
1046
+ * or `req.socket.remoteAddress` (raw HTTP). May be `undefined`/`null` when
1047
+ * the connection has already been torn down.
1048
+ */
1049
+ rawAddress: string | null | undefined;
1050
+ /** Request / handshake headers. Only consulted when `trustProxy` is true. */
1051
+ headers: IncomingHttpHeaders;
1052
+ /**
1053
+ * Whether a known reverse proxy sits in front of this server. DEFAULT false.
1054
+ * When false the raw peer address is returned verbatim (only IPv4-mapped
1055
+ * IPv6 canonicalization is applied), preserving historical behaviour. Only
1056
+ * enable when a trusted proxy populates `X-Forwarded-For` / `X-Real-IP`,
1057
+ * otherwise a client can spoof its own IP via those headers.
1058
+ */
1059
+ trustProxy?: boolean;
1060
+ /**
1061
+ * Number of proxy hops to skip from the RIGHT of `X-Forwarded-For` when
1062
+ * `trustProxy` is on (CORE-O3). The rightmost entries are appended by your own
1063
+ * trusted proxies; the resolved client IP is the entry that many hops in from
1064
+ * the end. The leftmost hop is client-controlled and is never trusted. DEFAULT
1065
+ * 1 (the immediate upstream proxy). Clamped to the list length so an over-large
1066
+ * count falls back to the leftmost trusted hop rather than the spoofable entry.
1067
+ */
1068
+ trustedProxyHopCount?: number;
1069
+ }
1070
+ /**
1071
+ * Whether a resolved client IP is a loopback / unresolved address — used by the
1072
+ * api/sync transports to honour `rateLimiting.skipLoopbackInDev` (api F5/F11).
1073
+ * Matches IPv4 loopback (`127.0.0.0/8`), IPv6 loopback (`::1`), the IPv4-mapped
1074
+ * form (`::ffff:127.0.0.1`, already canonicalized away by {@link resolveClientIp}),
1075
+ * and the {@link UNKNOWN_CLIENT_IP} sentinel. Pass an already-resolved IP.
1076
+ */
1077
+ declare const isLoopbackIp: (ip: string) => boolean;
1078
+ /**
1079
+ * Derive the deterministic, non-reversible bucket component for a session
1080
+ * token. The same token always yields the same value (bucket identity is
1081
+ * preserved) while the raw token never appears in the returned string.
1082
+ * Used by api AND sync rate-limit key builders — lives in core so both
1083
+ * packages can share one implementation without a cross-package dependency.
1084
+ */
1085
+ declare const deriveTokenBucketId: (token: string) => string;
1086
+ declare const resolveClientIp: ({ rawAddress, headers, trustProxy, trustedProxyHopCount }: ResolveClientIpParams) => string;
1087
+
1088
+ interface SessionSaveResult {
1089
+ ok: boolean;
1090
+ errorCode?: string;
1091
+ }
1092
+ interface SessionLogoutInput {
1093
+ token: string | null;
1094
+ socket?: Socket;
1095
+ userId?: string | null;
1096
+ skipSessionDelete?: boolean;
1097
+ }
1098
+ interface SessionProvider {
1099
+ getSession: (token: string | null) => Promise<BaseSessionLayout | null>;
1100
+ saveSession: (token: string, data: BaseSessionLayout, newUser?: boolean, options?: {
1101
+ supersedeToken?: string;
1102
+ }) => Promise<SessionSaveResult>;
1103
+ deleteSession: (token: string, options?: {
1104
+ skipSocketLogout?: boolean;
1105
+ }) => Promise<boolean>;
1106
+ logout: (input: SessionLogoutInput) => Promise<void>;
1107
+ }
1108
+ declare const registerSessionProvider: (next: SessionProvider) => void;
1109
+ declare const getRegisteredSessionProvider: () => SessionProvider | null;
1110
+ declare const isSessionProviderRegistered: () => boolean;
1111
+ declare const resetSessionProviderForTests: () => void;
1112
+ declare const readSession: (token: string | null) => Promise<BaseSessionLayout | null>;
1113
+ declare const writeSession: (token: string, data: BaseSessionLayout, newUser?: boolean, options?: {
1114
+ supersedeToken?: string;
1115
+ }) => Promise<SessionSaveResult>;
1116
+ declare const removeSession: (token: string, options?: {
1117
+ skipSocketLogout?: boolean;
1118
+ }) => Promise<boolean>;
1119
+ declare const performLogout: (input: SessionLogoutInput) => Promise<void>;
1120
+
676
1121
  /**
677
1122
  * Shared validation utilities for API and Sync request handlers.
678
1123
  * Extracted to avoid code duplication between handleApiRequest and handleSyncRequest.
@@ -711,7 +1156,7 @@ interface ValidationResult$1 {
711
1156
  */
712
1157
  declare const validateRequest: ({ auth, user }: {
713
1158
  auth: AuthProps;
714
- user: BaseSessionLayout;
1159
+ user?: BaseSessionLayout | null;
715
1160
  }) => ValidationResult$1;
716
1161
 
717
1162
  /**
@@ -798,7 +1243,10 @@ declare const getRateLimitStatus: (key: string, limit: number) => Promise<RateLi
798
1243
  declare const clearRateLimit: (key: string) => Promise<void>;
799
1244
  /**
800
1245
  * Clear all rate limits.
801
- * Useful for testing or server restart.
1246
+ * Intended for testing or dev-mode server reset only.
1247
+ * CORE-O8: throws in production to prevent accidental global counter wipe across
1248
+ * all tenants. If you need a production-safe reset, pass a scoped key to
1249
+ * `clearRateLimit` or implement a tenant-namespaced strategy.
802
1250
  */
803
1251
  declare const clearAllRateLimits: () => Promise<void>;
804
1252
 
@@ -815,6 +1263,10 @@ declare const validateInputByType: ({ typeText, value, rootKey, filePath, }: {
815
1263
  filePath?: string;
816
1264
  }) => Promise<ValidationResult>;
817
1265
 
1266
+ declare const normalizeOrigin: ({ value, secure }: {
1267
+ value: string;
1268
+ secure: boolean;
1269
+ }) => string;
818
1270
  declare const allowedOrigin: (origin: string) => boolean;
819
1271
 
820
1272
  declare const registerSyncAbortController: (socketId: string, cb: string, controller: AbortController) => string;
@@ -825,6 +1277,47 @@ declare const unregisterApiAbortController: (key: string) => void;
825
1277
  declare const abortApiByResponseIndex: (socketId: string, responseIndex: number | string) => boolean;
826
1278
  declare const abortAllForSocket: (socketId: string) => void;
827
1279
 
1280
+ /**
1281
+ * Handle returned by {@link issueOneTimeToken}. `token` is the RAW token to hand
1282
+ * to the caller (embed in the emailed URL); call `store()` to persist its hash
1283
+ * to Redis with the configured TTL. Persisting is a separate step so the caller
1284
+ * can run veto logic (hooks, validation) between minting and committing without
1285
+ * leaving an orphan Redis key.
1286
+ */
1287
+ interface OneTimeTokenHandle {
1288
+ /** The raw, single-use token. Returned exactly once; never stored verbatim. */
1289
+ token: string;
1290
+ /** Persist `sha256(token)` -> `payload` in Redis with the issue TTL. */
1291
+ store(): Promise<void>;
1292
+ }
1293
+ /**
1294
+ * Mint a one-time token in `namespace` carrying `payload`, redeemable for `ttl`
1295
+ * seconds. Returns the raw token plus a `store()` committer. The payload is
1296
+ * serialised to a string (objects via JSON); on redemption it is returned as a
1297
+ * string and the caller parses it (or use {@link consumeOneTimeTokenJson}).
1298
+ *
1299
+ * @param namespace Key namespace (e.g. `'-pwreset'`, `'-email-change'`) — routed
1300
+ * through `formatKey` so the active key formatter / project prefix applies.
1301
+ * @param ttlSeconds Time-to-live in seconds for the stored hash.
1302
+ * @param payload Value bound to the token. A string is stored verbatim; any
1303
+ * other value is `JSON.stringify`-d.
1304
+ */
1305
+ declare const issueOneTimeToken: (namespace: string, ttlSeconds: number, payload: string | Record<string, unknown>) => OneTimeTokenHandle;
1306
+ /**
1307
+ * Atomically validate + consume a one-time token from `namespace`. Returns the
1308
+ * stored payload STRING on success (and deletes the entry, so it can never be
1309
+ * reused), or `null` when the token is missing / expired / malformed / already
1310
+ * consumed. A single `MULTI` `GET`+`DEL` guarantees at-most-once redemption.
1311
+ */
1312
+ declare const consumeOneTimeToken: (namespace: string, token: string) => Promise<string | null>;
1313
+ /**
1314
+ * Convenience wrapper over {@link consumeOneTimeToken} that JSON-parses the
1315
+ * stored payload into `T`. Returns `null` on miss OR on a malformed JSON
1316
+ * payload. Use when the token was issued with an object payload.
1317
+ */
1318
+ declare const consumeOneTimeTokenJson: <T>(namespace: string, token: string) => Promise<T | null>;
1319
+ declare const oneTimeTokenKey: (namespace: string, token: string) => string;
1320
+
828
1321
  /**
829
1322
  * Core hook contract: framework-generic payload shapes + augmentable registry.
830
1323
  *
@@ -868,6 +1361,14 @@ interface HookStopSignal {
868
1361
  stop: true;
869
1362
  errorCode: string;
870
1363
  httpStatus?: number;
1364
+ /**
1365
+ * Optional output override for per-recipient hooks (`preSyncRecipient`).
1366
+ * When present on a stop signal the framework DOES NOT skip the recipient —
1367
+ * instead it sends this value in place of the full `serverOutput`. Use to
1368
+ * redact PII before delivery without excluding the socket from the fanout.
1369
+ * Ignored by all other hook dispatch sites.
1370
+ */
1371
+ overrideOutput?: unknown;
871
1372
  }
872
1373
  /** Handlers return undefined / void to continue, or a stop signal to abort the main flow. */
873
1374
  type HookResult = void | undefined | HookStopSignal;
@@ -948,6 +1449,41 @@ interface PostSyncAuthorizePayload {
948
1449
  /** Optional transport tag — see {@link PreApiValidatePayload.transport}. */
949
1450
  transport?: 'socket' | 'http';
950
1451
  }
1452
+ interface PreSyncValidatePayload {
1453
+ routeName: string;
1454
+ data: Record<string, unknown>;
1455
+ user: HookSessionShape | null;
1456
+ receiver: string;
1457
+ /** Optional transport tag — see {@link PreApiValidatePayload.transport}. */
1458
+ transport?: 'socket' | 'http';
1459
+ }
1460
+ interface PostSyncValidatePayload extends PreSyncValidatePayload {
1461
+ validation: {
1462
+ status: 'success';
1463
+ } | {
1464
+ status: 'error';
1465
+ message: string;
1466
+ };
1467
+ }
1468
+ interface PreSyncExecutePayload {
1469
+ routeName: string;
1470
+ data: Record<string, unknown>;
1471
+ user: HookSessionShape | null;
1472
+ receiver: string;
1473
+ /** Optional transport tag — see {@link PreApiValidatePayload.transport}. */
1474
+ transport?: 'socket' | 'http';
1475
+ }
1476
+ interface PostSyncExecutePayload {
1477
+ routeName: string;
1478
+ data: Record<string, unknown>;
1479
+ user: HookSessionShape | null;
1480
+ receiver: string;
1481
+ result: unknown;
1482
+ error: Error | null;
1483
+ durationMs: number;
1484
+ /** Optional transport tag — see {@link PreApiValidatePayload.transport}. */
1485
+ transport?: 'socket' | 'http';
1486
+ }
951
1487
  interface PreSyncFanoutPayload {
952
1488
  routeName: string;
953
1489
  data: Record<string, unknown>;
@@ -1012,6 +1548,14 @@ interface CorsRejectedPayload {
1012
1548
  allowLocalhost: boolean;
1013
1549
  /** Optional route the rejected request was for. */
1014
1550
  route?: string;
1551
+ /**
1552
+ * Why the request was rejected (server HOK-27). Distinguishes a present-but-
1553
+ * disallowed Origin (`'origin-not-allowed'`) from a MISSING Origin header
1554
+ * (`'origin-missing'`, the 403 dispatched by the server's origin gate) and the
1555
+ * normalization-failure case. Optional so existing `allowedOrigin()` callers
1556
+ * that don't set it stay source-compatible.
1557
+ */
1558
+ reason?: 'origin-not-allowed' | 'origin-missing' | 'origin-malformed';
1015
1559
  }
1016
1560
  interface PreSessionRefreshPayload {
1017
1561
  token: string;
@@ -1027,6 +1571,18 @@ interface PostSessionRefreshPayload {
1027
1571
  /** True if Redis EXPIRE succeeded; false on failure or non-existent key. */
1028
1572
  applied: boolean;
1029
1573
  }
1574
+ interface SessionCreatedPayload {
1575
+ token: string;
1576
+ userId: string;
1577
+ /** How the session came to exist (credentials login, OAuth, refresh-mint, ...). */
1578
+ via?: string;
1579
+ }
1580
+ interface SessionRevokedPayload {
1581
+ token: string;
1582
+ userId: string | null;
1583
+ /** Why the session was revoked (logout, kicked-by-new-device, admin, expiry, ...). */
1584
+ reason?: string;
1585
+ }
1030
1586
  interface OnUploadStartPayload {
1031
1587
  userId: string;
1032
1588
  contentType: string;
@@ -1039,6 +1595,20 @@ interface OnUploadCompletePayload {
1039
1595
  sizeBytes: number;
1040
1596
  uploadKind: 'avatar' | string;
1041
1597
  }
1598
+ interface PreAvatarServePayload {
1599
+ /** The route path requested (`/avatars/:fileId`). */
1600
+ routePath: string;
1601
+ /** The allowlisted file id resolved from the route path. */
1602
+ fileId: string;
1603
+ }
1604
+ interface PostAvatarServePayload {
1605
+ routePath: string;
1606
+ fileId: string;
1607
+ /** Disk extension of the format that was served (e.g. `'webp'`). */
1608
+ extension: string;
1609
+ /** Content-Type header sent for the served file. */
1610
+ contentType: string;
1611
+ }
1042
1612
  interface CsrfMismatchPayload {
1043
1613
  /** Path of the rejected request. */
1044
1614
  route: string;
@@ -1051,6 +1621,18 @@ interface CsrfMismatchPayload {
1051
1621
  /** Whether a token was provided in `x-csrf-token` (presence only — never the value). */
1052
1622
  providedToken: boolean;
1053
1623
  }
1624
+ interface PreSocketMessagePayload {
1625
+ /** Which socket pipeline received the message. */
1626
+ channel: 'api' | 'sync';
1627
+ /** Socket.io connection id of the originator. */
1628
+ socketId: string;
1629
+ /** Handshake address of the originator (best-effort; honours no proxy trust). */
1630
+ ip: string;
1631
+ /** Whether the originating socket carried a session token (presence only — never the value). */
1632
+ authenticated: boolean;
1633
+ /** Raw route name from the message envelope, when present (un-parsed, un-trusted). */
1634
+ routeName?: string;
1635
+ }
1054
1636
  interface PreHttpRequestPayload {
1055
1637
  /** HTTP method (always uppercase: GET/POST/...). */
1056
1638
  method: string;
@@ -1067,8 +1649,64 @@ interface PreHttpRequestPayload {
1067
1649
  */
1068
1650
  headers: Record<string, string>;
1069
1651
  }
1652
+ interface PostHttpRequestPayload {
1653
+ /** HTTP method (always uppercase). */
1654
+ method: string;
1655
+ /** Request URL (path + query). */
1656
+ url: string;
1657
+ /** Caller-supplied or framework-generated `X-Request-Id`. */
1658
+ requestId: string;
1659
+ /** Final HTTP status code written to the response. */
1660
+ statusCode: number;
1661
+ /** Wall-clock duration from request receipt to response, in ms. */
1662
+ durationMs: number;
1663
+ }
1664
+ interface ApiAuthRejectedPayload {
1665
+ /** Resolved route name (`api/billing/getInvoice/v1`). */
1666
+ routeName: string;
1667
+ /** Why auth failed. `'login-required'` = no/invalid session on a login route;
1668
+ * `'additional-failed'` = a session existed but an `additional[]` predicate
1669
+ * rejected; `'invalid-condition'` = a misconfigured predicate (setup error). */
1670
+ reason: 'login-required' | 'additional-failed' | 'invalid-condition';
1671
+ /** Session user id when a (insufficient) session was present, else null. */
1672
+ userId: string | null;
1673
+ /** Resolved client IP (honours `http.trustProxy`). */
1674
+ ip?: string;
1675
+ /** Transport the request arrived on. */
1676
+ transport?: 'socket' | 'http';
1677
+ /** The specific `additional[]` key that failed, when `reason === 'additional-failed'`. */
1678
+ failedKey?: string;
1679
+ }
1680
+ interface PreSyncRecipientPayload {
1681
+ /** Resolved route name (`sync/board/moveCard/v1`). */
1682
+ routeName: string;
1683
+ /** Room code / receiver this fanout is for. */
1684
+ receiver: string;
1685
+ /** Socket id of THIS recipient. */
1686
+ recipientSocketId: string;
1687
+ /**
1688
+ * Recipient's session user id. `null` by default (avoids a Redis read per
1689
+ * recipient on the hot path). Populated when the consumer registers a
1690
+ * `resolveRecipientUser` function via `registerHookHandler` options.
1691
+ */
1692
+ recipientUserId: string | null;
1693
+ /** The server-validated output about to be sent. */
1694
+ serverOutput: unknown;
1695
+ }
1696
+ interface PreServerStopPayload {
1697
+ /** Why the server is stopping — the signal name, or `'manual'` for a programmatic stop. */
1698
+ reason: 'SIGTERM' | 'SIGINT' | 'SIGHUP' | 'manual';
1699
+ /**
1700
+ * Soft budget (ms) the server intends to wait for shutdown handlers + in-flight
1701
+ * requests to drain before forcing exit. Handlers should not block longer than
1702
+ * this. `undefined` when the server applies no deadline.
1703
+ */
1704
+ timeoutMs?: number;
1705
+ }
1070
1706
  interface HookPayloads {
1071
1707
  preHttpRequest: PreHttpRequestPayload;
1708
+ postHttpRequest: PostHttpRequestPayload;
1709
+ preSocketMessage: PreSocketMessagePayload;
1072
1710
  preApiValidate: PreApiValidatePayload;
1073
1711
  postApiValidate: PostApiValidatePayload;
1074
1712
  preApiExecute: PreApiExecutePayload;
@@ -1078,19 +1716,30 @@ interface HookPayloads {
1078
1716
  postApiRespond: PostApiRespondPayload;
1079
1717
  preSyncAuthorize: PreSyncAuthorizePayload;
1080
1718
  postSyncAuthorize: PostSyncAuthorizePayload;
1719
+ preSyncValidate: PreSyncValidatePayload;
1720
+ postSyncValidate: PostSyncValidatePayload;
1721
+ preSyncExecute: PreSyncExecutePayload;
1722
+ postSyncExecute: PostSyncExecutePayload;
1081
1723
  preSyncFanout: PreSyncFanoutPayload;
1082
1724
  postSyncFanout: PostSyncFanoutPayload;
1725
+ preSyncRecipient: PreSyncRecipientPayload;
1083
1726
  preSyncStream: PreSyncStreamPayload;
1084
1727
  postSyncStream: PostSyncStreamPayload;
1085
1728
  apiError: ApiErrorPayload;
1086
1729
  syncError: SyncErrorPayload;
1730
+ apiAuthRejected: ApiAuthRejectedPayload;
1087
1731
  rateLimitExceeded: RateLimitExceededPayload;
1088
1732
  corsRejected: CorsRejectedPayload;
1089
1733
  csrfMismatch: CsrfMismatchPayload;
1090
1734
  preSessionRefresh: PreSessionRefreshPayload;
1091
1735
  postSessionRefresh: PostSessionRefreshPayload;
1736
+ sessionCreated: SessionCreatedPayload;
1737
+ sessionRevoked: SessionRevokedPayload;
1092
1738
  onUploadStart: OnUploadStartPayload;
1093
1739
  onUploadComplete: OnUploadCompletePayload;
1740
+ preAvatarServe: PreAvatarServePayload;
1741
+ postAvatarServe: PostAvatarServePayload;
1742
+ preServerStop: PreServerStopPayload;
1094
1743
  }
1095
1744
  interface PreErrorNormalizePayload {
1096
1745
  response: ErrorResponseInput;
@@ -1117,10 +1766,10 @@ type DispatchResult = {
1117
1766
  stopped: true;
1118
1767
  signal: HookStopSignal;
1119
1768
  };
1120
- declare const registerHook: <TName extends HookName>(name: TName, handler: HookHandler<HookPayloads[TName]>) => void;
1769
+ declare const registerHook: <TName extends HookName>(name: TName, handler: HookHandler<HookPayloads[TName]>) => (() => void);
1121
1770
  declare const dispatchHook: <TName extends HookName>(name: TName, payload: HookPayloads[TName]) => Promise<DispatchResult>;
1122
1771
  declare const clearAllHooks: () => void;
1123
- declare const registerSyncHook: <TName extends SyncHookName>(name: TName, handler: SyncHookHandler<SyncHookPayloads[TName]>) => void;
1772
+ declare const registerSyncHook: <TName extends SyncHookName>(name: TName, handler: SyncHookHandler<SyncHookPayloads[TName]>) => (() => void);
1124
1773
  declare const dispatchSyncHook: <TName extends SyncHookName>(name: TName, payload: SyncHookPayloads[TName]) => void;
1125
1774
 
1126
- export { type ApiErrorPayload, type ApiResponseEnvelope, AuthProps, type AvatarConfig, type AvatarConfigInput, BOOT_KEY_PREFIX, BaseSessionLayout, type CheckRateLimitParams, type CorsRejectedPayload, type CreateLocalizedNormalizerInput, type CsrfConfig, type CsrfCookieOptions, type CsrfMismatchPayload, DEFAULT_AVATAR_CONFIG, DEFAULT_CLIENT_KEY, DEFAULT_CSRF_CONFIG, DEFAULT_ENV_FILES, type DeployConfigShape, type DeployDevelopmentShape, type DeployEnvironmentShape, type DeployResourceShape, type DeployRoutingShape, type DispatchResult, type EmailMessage, type EmailResult, type EmailSender, type EmailSenderRegistry, type ErrorFormatter, type ErrorFormatterContext, ErrorParam, ErrorResponseInput, type ErrorTracker, type ErrorTrackerContext, type ErrorTrackerEvent, type ErrorTrackerUser, GENERATED_API_DOCS_PATH, GENERATED_API_SCHEMAS_PATH, GENERATED_SOCKET_TYPES_PATH, type HookHandler, type HookName, type HookPayloads, type HookResult, type HookSessionShape, type HookStopSignal, type HttpMethod, type LanguageCode, type LocaleReloader, type LocalizedNormalizer, type NormalizeErrorResponseInput, NormalizedErrorResponse, type OnUploadCompletePayload, type OnUploadStartPayload, PUBLIC_DIR, type PostApiExecutePayload, type PostApiRespondPayload, type PostApiValidatePayload, type PostErrorNormalizePayload, type PostSessionRefreshPayload, type PostSyncFanoutPayload, type PreApiExecutePayload, type PreApiRespondPayload, type PreApiValidatePayload, type PreErrorNormalizePayload, type PreSessionRefreshPayload, type PreSyncAuthorizePayload, type PreSyncFanoutPayload, type PresetDefinition, type ProcessUploadInput, type ProcessUploadResult, ROOT_DIR, type RateLimitExceededPayload, type RateLimitResult, type RateLimitStrategy, type RedisConnectionOptions, type RedisKeyFormatter, type RuntimeApiMapsResult, type RuntimeEnv, type RuntimeMapsProvider, type RuntimeSyncMapsResult, SERVER_DIR, SERVER_FUNCTIONS_DIR, SHARED_DIR, SRC_DIR, type ServiceDefinition, type ServicesConfigShape, type SocketMiddleware, type SpanResult, type SyncErrorPayload, type SyncHookHandler, type SyncHookName, type SyncHookPayloads, TSCONFIG_ALIAS_FILES, type TranslateFunction, type TranslateInput, UPLOADS_DIR, type ValidationResult$1 as ValidationResult, abortAllForSocket, abortApiByResponseIndex, abortSyncByCb, acquireLease, allowedOrigin, type apiMessage, applyErrorFormatter, applySocketMiddlewares, applyStrayKeyPrefix, attachSocketRedisAdapter, bootstrapEnv, captureException, captureExceptionAcrossTrackers, captureMessage, captureMessageAcrossTrackers, checkRateLimit, clearAllHooks, clearAllRateLimits, clearRateLimit, clearSocketMiddlewares, collectSynchronizedEnvKeys, computeSynchronizedEnvHashes, createLocalizedNormalizer, defaultRateLimitStrategy, defaultRedisKeyFormatter, dispatchHook, dispatchSyncHook, env, extractLanguageFromHeader, extractTokenFromRequest, extractTokenFromSocket, formatKey, getActiveErrorTrackers, getAvatarConfig, getBindAddress, getCookieValue, getCsrfConfig, getDeployConfig, getEffectiveHttpMethod, getEmailSender, getEmailSenderByName, getEnvFiles, getErrorFormatter, getGeneratedApiDocsPath, getGeneratedApiSchemasPath, getGeneratedSocketTypesPath, getIoInstance, getLocaleReloader, getLocalizedNormalizer, getParams, getPrismaClient, getPrismaClientFor, getPrismaClientKeys, getPublicDir, getRateLimitStatus, getRateLimitStrategy, getRedactedLogKeys, getRedisClient, getRedisClientFor, getRedisClientKeys, getRedisConnectionOptions, getRedisKeyFormatter, getRuntimeApiMaps, getRuntimeSyncMaps, getServerDir, getServerFunctionDirs, getServerFunctionsDir, getServicesConfig, getSharedDir, getSocketMiddlewares, getSrcDir, getUploadsDir, hasCookie, hashSynchronizedValue, inferHttpMethod, initConsolelog, initSharedSentry, isDeployConfigRegistered, isEmailSenderRegistered, isFalsy, isLocalizedNormalizerRegistered, isMethodAllowed, isPrismaClientRegistered, isProduction, isRedactedLogKey, isRedisClientRegistered, isRuntimeMapsProviderRegistered, isServicesConfigRegistered, listEmailSenderNames, loadEnvFiles, normalizeErrorResponse, prisma, processUpload, readBootUuid, recordMetricAcrossTrackers, redis, registerApiAbortController, registerAvatarConfig, registerBindAddress, registerCsrfConfig, registerDeployConfig, registerEmailSender, registerEmailSenders, registerErrorFormatter, registerErrorTracker, registerErrorTrackers, registerHook, registerLocaleReloader, registerLocalizedNormalizer, registerPrismaClient, registerRateLimitStrategy, registerRedactedLogKeys, registerRedisClient, registerRedisKeyFormatter, registerRuntimeMapsProvider, registerServicesConfig, registerSocketMiddleware, registerSyncAbortController, registerSyncHook, releaseLease, renewLease, resetClientsForTests, resetCsrfConfigForTests, resetRedactedLogKeysForTests, resetRedisKeyFormatterForTests, resolveEnvKey, resolveErrorMessage, resolveFromRoot, serveAvatar, setErrorTrackerUser, setIoInstance, setSentryUser, startSpan, startSpanAcrossTrackers, type syncMessage, unregisterApiAbortController, unregisterSyncAbortController, validateInputByType, validateRequest, writeBootUuid };
1775
+ export { type ApiAuthRejectedPayload, type ApiErrorPayload, type ApiResponseEnvelope, AuthProps, type AvatarConfig, type AvatarConfigInput, BOOT_KEY_PREFIX, BaseSessionLayout, type CheckRateLimitParams, type CookiePrefixConstraints, type CorsRejectedPayload, type CreateLocalizedNormalizerInput, type CsrfConfig, type CsrfCookieOptions, type CsrfMismatchPayload, DEFAULT_AVATAR_CONFIG, DEFAULT_CLIENT_KEY, DEFAULT_CSRF_CONFIG, DEFAULT_ENV_FILES, DEFAULT_REDACTED_LOG_KEYS, DEPTH_TRUNCATED_PLACEHOLDER, DeepPartial, type DeployConfigShape, type DeployDevelopmentShape, type DeployEnvironmentShape, type DeployResourceShape, type DeployRoutingShape, type DispatchResult, type EmailAttachment, type EmailMessage, type EmailResult, type EmailSender, type EmailSenderRegistry, type ErrorFormatter, type ErrorFormatterContext, ErrorParam, ErrorResponseInput, type ErrorTracker, type ErrorTrackerContext, type ErrorTrackerEvent, type ErrorTrackerUser, type FormatterEnvelope, GENERATED_API_DOCS_PATH, GENERATED_API_SCHEMAS_PATH, GENERATED_SOCKET_TYPES_PATH, HealthHashConfig, type HealthHashDescriptor, type HookHandler, type HookName, type HookPayloads, type HookResult, type HookSessionShape, type HookStopSignal, type HttpMethod, type LanguageCode, type LocaleReloader, type LocalizedNormalizer, type NormalizeErrorResponseInput, NormalizedErrorResponse, type OnUploadCompletePayload, type OnUploadStartPayload, type OneTimeTokenHandle, PUBLIC_DIR, type PeerRequire, type PostApiExecutePayload, type PostApiRespondPayload, type PostApiValidatePayload, type PostAvatarServePayload, type PostErrorNormalizePayload, type PostHttpRequestPayload, type PostSessionRefreshPayload, type PostSyncAuthorizePayload, type PostSyncExecutePayload, type PostSyncFanoutPayload, type PostSyncValidatePayload, type PreApiExecutePayload, type PreApiRespondPayload, type PreApiValidatePayload, type PreAvatarServePayload, type PreCaptureFilter, type PreErrorNormalizePayload, type PreHttpRequestPayload, type PreServerStopPayload, type PreSessionRefreshPayload, type PreSocketMessagePayload, type PreSyncAuthorizePayload, type PreSyncExecutePayload, type PreSyncFanoutPayload, type PreSyncRecipientPayload, type PreSyncValidatePayload, type PresetDefinition, type ProcessUploadInput, type ProcessUploadResult, REDACTED_PLACEHOLDER, ROOT_DIR, type RateLimitExceededPayload, type RateLimitResult, type RateLimitStrategy, type RedisConnectionOptions, type RedisKeyFormatter, type Registry, type RegistryOptions, type ResolveClientIpParams, type RoomNameFormatter, type RoomNameFormatterContext, type RuntimeApiMapsResult, type RuntimeEnv, type RuntimeMapsProvider, type RuntimeSyncMapsResult, SERVER_DIR, SERVER_FUNCTIONS_DIR, SHARED_DIR, SRC_DIR, type ServiceDefinition, type ServicesConfigShape, type SessionCreatedPayload, type SessionLogoutInput, type SessionProvider, type SessionRevokedPayload, type SessionSaveResult, type SocketMiddleware, type SpanHandle, type SpanResult, type SyncErrorPayload, type SyncHookHandler, type SyncHookName, type SyncHookPayloads, TSCONFIG_ALIAS_FILES, type TranslateFunction, type TranslateInput, UNKNOWN_CLIENT_IP, UPLOADS_DIR, type ValidationResult$1 as ValidationResult, abortAllForSocket, abortApiByResponseIndex, abortSyncByCb, acquireLease, allowedOrigin, type apiMessage, appendErrorTracker, applyCookiePrefixConstraints, applyErrorFormatter, applySocketMiddlewares, applyStrayKeyPrefix, attachSocketRedisAdapter, bootstrapEnv, captureException, captureExceptionAcrossTrackers, captureMessage, captureMessageAcrossTrackers, checkRateLimit, clearAllHooks, clearAllRateLimits, clearRateLimit, clearSocketMiddlewares, collectSynchronizedEnvKeys, computeSynchronizedEnvHashes, consumeOneTimeToken, consumeOneTimeTokenJson, createLocalizedNormalizer, createRegistry, defaultRateLimitStrategy, defaultRedisKeyFormatter, defaultRoomNameFormatter, deriveTokenBucketId, describeHealthHashConfig, dispatchHook, dispatchSyncHook, ensurePeerDepInstalled, env, escapeHtml, extractLanguageFromHeader, extractTokenFromRequest, extractTokenFromSocket, flushErrorTrackers, formatKey, formatRoomName, getActiveErrorTrackers, getAvatarConfig, getBindAddress, getCookieValue, getCsrfConfig, getCurrentErrorTrackerIdentity, getDeployConfig, getEffectiveHttpMethod, getEmailSender, getEmailSenderByName, getEnv, getEnvFiles, getErrorFormatter, getGeneratedApiDocsPath, getGeneratedApiSchemasPath, getGeneratedSocketTypesPath, getIoInstance, getLocaleReloader, getLocalizedNormalizer, getParams, getPrismaClient, getPrismaClientFor, getPrismaClientKeys, getPublicDir, getRateLimitStatus, getRateLimitStrategy, getRedactedLogKeys, getRedisClient, getRedisClientFor, getRedisClientKeys, getRedisConnectionOptions, getRedisKeyFormatter, getRegisteredSessionProvider, getRoomNameFormatter, getRuntimeApiMaps, getRuntimeSyncMaps, getServerDir, getServerFunctionDirs, getServerFunctionsDir, getServicesConfig, getSharedDir, getSocketMiddlewares, getSrcDir, getUploadsDir, hasCookie, hashSynchronizedValue, hashSynchronizedValueWith, inferHttpMethod, initConsolelog, initSharedSentry, isDeployConfigRegistered, isEmailSenderRegistered, isFalsy, isGetMethodName, isLocalizedNormalizerRegistered, isLoopbackIp, isMethodAllowed, isPrismaClientRegistered, isProduction, isRedactedLogKey, isRedisClientRegistered, isRuntimeMapsProviderRegistered, isServicesConfigRegistered, isSessionProviderRegistered, issueOneTimeToken, listEmailSenderNames, loadEnvFiles, loadPeer, normalizeErrorResponse, normalizeOrigin, oneTimeTokenKey, performLogout, prisma, processUpload, readBootUuid, readSession, recordMetricAcrossTrackers, redis, registerApiAbortController, registerAvatarConfig, registerBindAddress, registerCsrfConfig, registerDeployConfig, registerEmailSender, registerEmailSenders, registerErrorFormatter, registerErrorTracker, registerErrorTrackers, registerHook, registerLocaleReloader, registerLocalizedNormalizer, registerPreCaptureFilter, registerPrismaClient, registerRateLimitStrategy, registerRedactedLogKeys, registerRedisClient, registerRedisKeyFormatter, registerRoomNameFormatter, registerRuntimeMapsProvider, registerServicesConfig, registerSessionProvider, registerSocketMiddleware, registerStrayPrefixCommand, registerSyncAbortController, registerSyncHook, releaseLease, removeSession, renewLease, resetClientsForTests, resetCsrfConfigForTests, resetRedactedLogKeysForTests, resetRedisKeyFormatterForTests, resetSessionProviderForTests, resolveClientIp, resolveEnvKey, resolveErrorMessage, resolveFromRoot, resolveHealthHashConfig, resolveHealthHashConfigFromDescriptor, runWithErrorTrackerIdentity, runWithErrorTrackerIdentityScope, sanitizeErrorString, sanitizeErrorStrings, sanitizeForLog, serveAvatar, setCurrentErrorTrackerIdentity, setErrorTrackerUser, setIoInstance, setSentryUser, startSpan, startSpanAcrossTrackers, startSpanHandle, type syncMessage, tryCatch, tryCatchSync, unregisterApiAbortController, unregisterSyncAbortController, validateInputByType, validateRequest, writeBootUuid, writeSession };