@luckystack/core 0.10.2 → 0.11.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.
Files changed (122) hide show
  1. package/AGENTS.md +11 -0
  2. package/CHANGELOG.md +7 -0
  3. package/README.md +2 -88
  4. package/dist/apiInterceptors.d.ts +36 -0
  5. package/dist/apiMethodMapRegistry.d.ts +5 -0
  6. package/dist/apiRequest.d.ts +71 -0
  7. package/dist/apiTypeStubs.d.ts +3 -5
  8. package/dist/avatarConfig.d.ts +22 -0
  9. package/dist/bindAddress.d.ts +15 -0
  10. package/dist/bootUuid.d.ts +24 -0
  11. package/dist/cancelRegistry.d.ts +7 -0
  12. package/dist/checkOrigin.d.ts +6 -0
  13. package/dist/client.d.ts +46 -335
  14. package/dist/client.js +2 -2
  15. package/dist/client.js.map +1 -1
  16. package/dist/clientHookBus.d.ts +62 -0
  17. package/dist/clients.d.ts +16 -0
  18. package/dist/config.d.ts +8 -2
  19. package/dist/configUtils.d.ts +16 -0
  20. package/dist/consoleLog.d.ts +1 -0
  21. package/dist/cookies.d.ts +28 -0
  22. package/dist/createRegistry.d.ts +39 -0
  23. package/dist/csrf.d.ts +30 -0
  24. package/dist/csrfConfig.d.ts +32 -0
  25. package/dist/db.d.ts +12 -0
  26. package/dist/dbHealthCheck.d.ts +6 -0
  27. package/dist/{config-CwW3KY9r.d.ts → deployConfigRegistry.d.ts} +8 -43
  28. package/dist/emailRegistry.d.ts +139 -0
  29. package/dist/env.d.ts +38 -0
  30. package/dist/errorFormatterRegistry.d.ts +54 -0
  31. package/dist/errorTrackerIdentity.d.ts +14 -0
  32. package/dist/errorTrackerRegistry.d.ts +85 -0
  33. package/dist/escapeHtml.d.ts +6 -0
  34. package/dist/eslint/index.d.ts +5 -8
  35. package/dist/eslint/internal/hasPackage.d.ts +1 -0
  36. package/dist/eslint/internal/ruleTypes.d.ts +4 -0
  37. package/dist/eslint/rules/no-arbitrary-tailwind-color.d.ts +3 -0
  38. package/dist/eslint/rules/no-direct-prisma-import-in-components.d.ts +3 -0
  39. package/dist/eslint/rules/no-local-socket-enumeration.d.ts +3 -0
  40. package/dist/eslint/rules/no-raw-fetch-in-src.d.ts +3 -0
  41. package/dist/eslint/rules/no-raw-try-catch.d.ts +3 -0
  42. package/dist/eslint/rules/no-unsafe-api-wrappers.d.ts +3 -0
  43. package/dist/eslint/rules/no-unsafe-sync-wrappers.d.ts +3 -0
  44. package/dist/eslint/rules/prefer-luckystack-confirm.d.ts +3 -0
  45. package/dist/eslint/rules/prefer-luckystack-dropdown.d.ts +3 -0
  46. package/dist/eslint/rules/prefer-luckystack-notify.d.ts +3 -0
  47. package/dist/extractToken.d.ts +12 -0
  48. package/dist/extractTokenFromRequest.d.ts +12 -0
  49. package/dist/getParams.d.ts +9 -0
  50. package/dist/hooks/registry.d.ts +12 -0
  51. package/dist/hooks/types.d.ts +446 -0
  52. package/dist/httpApiUtils.d.ts +54 -0
  53. package/dist/index.d.ts +110 -1892
  54. package/dist/index.js.map +1 -1
  55. package/dist/lease.d.ts +18 -0
  56. package/dist/localSocketEnumerationGuard.d.ts +22 -0
  57. package/dist/localeReloader.d.ts +3 -0
  58. package/dist/localesRegistry.d.ts +8 -0
  59. package/dist/localizedNormalizer.d.ts +50 -0
  60. package/dist/loggerRegistry.d.ts +12 -0
  61. package/dist/middlewareRegistry.d.ts +45 -0
  62. package/dist/notifier.d.ts +17 -0
  63. package/dist/offlineQueue.d.ts +48 -0
  64. package/dist/oneTimeToken.d.ts +43 -0
  65. package/dist/pageRouteValidation.d.ts +37 -0
  66. package/dist/paths.d.ts +35 -0
  67. package/dist/peerDeps.d.ts +23 -0
  68. package/dist/portOverrideRegistry.d.ts +3 -0
  69. package/dist/processUpload.d.ts +44 -0
  70. package/dist/{sessionTypes-DanjuPaE.d.ts → projectConfig.d.ts} +32 -129
  71. package/dist/rateLimiter.d.ts +90 -0
  72. package/dist/react/AvatarProvider.d.ts +11 -0
  73. package/dist/react/Middleware.d.ts +10 -0
  74. package/dist/react/Router.d.ts +1 -0
  75. package/dist/react/TranslationProvider.d.ts +22 -0
  76. package/dist/react/notify.d.ts +12 -0
  77. package/dist/react/sessionContext.d.ts +31 -0
  78. package/dist/react/useTheme.d.ts +5 -0
  79. package/dist/redactedLogKeys.d.ts +22 -0
  80. package/dist/redis.d.ts +15 -0
  81. package/dist/redisKeyFormatter.d.ts +7 -0
  82. package/dist/resolveClientIp.d.ts +51 -0
  83. package/dist/responseNormalizer.d.ts +33 -0
  84. package/dist/roomNameFormatterRegistry.d.ts +36 -0
  85. package/dist/roomSockets.d.ts +23 -0
  86. package/dist/routedDataQuery.d.ts +7 -0
  87. package/dist/routedHttpInvocation.d.ts +20 -0
  88. package/dist/runtimeMapsRegistry.d.ts +18 -0
  89. package/dist/runtimeTypeValidation.d.ts +13 -0
  90. package/dist/secretsResolved.d.ts +3 -0
  91. package/dist/sentrySetup.d.ts +21 -0
  92. package/dist/serveAvatars.d.ts +5 -0
  93. package/dist/serviceRoute.d.ts +27 -0
  94. package/dist/servicesConfigRegistry.d.ts +22 -0
  95. package/dist/sessionProviderRegistry.d.ts +46 -0
  96. package/dist/sessionTypes.d.ts +79 -0
  97. package/dist/sleep.d.ts +1 -0
  98. package/dist/socketEvents.d.ts +37 -0
  99. package/dist/socketMiddlewareRegistry.d.ts +20 -0
  100. package/dist/socketRedisAdapter.d.ts +21 -0
  101. package/dist/socketState.d.ts +5 -0
  102. package/dist/socketStatusTypes.d.ts +6 -0
  103. package/dist/socketTypes.d.ts +34 -0
  104. package/dist/synchronizedEnvHashes.d.ts +66 -0
  105. package/dist/testFileConvention.d.ts +11 -0
  106. package/dist/testResetTargets.d.ts +5 -0
  107. package/dist/tryCatch.d.ts +1 -0
  108. package/dist/tryCatchClient.d.ts +1 -0
  109. package/dist/tryCatchSync.d.ts +1 -0
  110. package/dist/validateRequest.d.ts +50 -0
  111. package/docs/app-bootstrap.md +4 -449
  112. package/docs/config-registry.md +3 -346
  113. package/docs/csrf-config.md +3 -86
  114. package/docs/error-tracker-registry.md +3 -192
  115. package/docs/hooks.md +2 -295
  116. package/docs/rate-limit-strategy.md +3 -168
  117. package/docs/redis-adapter.md +5 -110
  118. package/docs/session-types.md +3 -178
  119. package/docs/socket-bootstrap.md +5 -385
  120. package/package.json +2 -2
  121. package/CLAUDE.md +0 -256
  122. package/dist/pageRouteValidation-B4Cmtfjv.d.ts +0 -280
@@ -1,21 +1,5 @@
1
- type DeepPartial<T> = {
2
- [K in keyof T]?: T[K] extends object | undefined ? DeepPartial<NonNullable<T[K]>> : T[K];
3
- };
4
- /**
5
- * True when `value` is a plain object literal (prototype is `Object.prototype`
6
- * or `null`). Arrays, class instances, Maps, Dates, and `null` are rejected so
7
- * they are treated as opaque replacement values during a merge.
8
- */
9
- declare const isPlainObject: (value: unknown) => value is Record<string, unknown>;
10
- /**
11
- * Deep-merge a deep-partial `override` over a fully-populated `base`,
12
- * returning a new object. See the module header for the exact behaviour
13
- * contract. Arrays and non-plain-object values are replaced wholesale; nested
14
- * plain objects merge recursively; `undefined` override values are ignored.
15
- */
16
- declare const deepMerge: <T>(base: T, override: DeepPartial<T> | undefined) => T;
17
-
18
- interface LoggingConfig {
1
+ import { type DeepPartial } from './configUtils.js';
2
+ export interface LoggingConfig {
19
3
  devLogs: boolean;
20
4
  devNotifications: boolean;
21
5
  socketStatus: boolean;
@@ -32,7 +16,7 @@ interface LoggingConfig {
32
16
  */
33
17
  timestamps: boolean;
34
18
  }
35
- interface RateLimitingConfig {
19
+ export interface RateLimitingConfig {
36
20
  /**
37
21
  * Global kill-switch for all rate limiting. When `false`, every
38
22
  * `checkRateLimit` call short-circuits to "allowed" and counters are not
@@ -94,7 +78,7 @@ interface RateLimitingConfig {
94
78
  auth: AuthRateLimitConfig;
95
79
  }
96
80
  /** Inputs handed to the {@link RateLimitingConfig.identity} callback. */
97
- interface RateLimitIdentityParams {
81
+ export interface RateLimitIdentityParams {
98
82
  /** Resolved route name (`api/billing/getInvoice/v1`, `sync/...`). */
99
83
  routeName: string;
100
84
  /** Session user id when the request is authenticated, else null. */
@@ -105,13 +89,13 @@ interface RateLimitIdentityParams {
105
89
  transport: 'socket' | 'http';
106
90
  }
107
91
  /** Result of a {@link RateLimitingConfig.identity} callback. */
108
- interface RateLimitIdentity {
92
+ export interface RateLimitIdentity {
109
93
  /** Which dimension the key is built from. */
110
94
  scope: 'user' | 'ip' | 'custom';
111
95
  /** The identity value (user id, ip, or a custom string). Never a raw token. */
112
96
  id: string;
113
97
  }
114
- interface AuthRateLimitConfig {
98
+ export interface AuthRateLimitConfig {
115
99
  /** When false (default), no per-account lockout is applied. */
116
100
  enabled: boolean;
117
101
  /** Max failed attempts per account FROM ONE IP within `windowMs` before that
@@ -129,7 +113,7 @@ interface AuthRateLimitConfig {
129
113
  /** Rolling window in ms over which both caps are counted. Default 900000 (15 min). */
130
114
  windowMs: number;
131
115
  }
132
- interface SessionConfig {
116
+ export interface SessionConfig {
133
117
  basedToken: boolean;
134
118
  expiryDays: number;
135
119
  /**
@@ -162,7 +146,7 @@ interface SessionConfig {
162
146
  */
163
147
  projectName: string;
164
148
  }
165
- interface AppConfig {
149
+ export interface AppConfig {
166
150
  /**
167
151
  * Public URL of the app — used by OAuth callback redirects, transactional
168
152
  * email links (when `@luckystack/email` is installed), and any other
@@ -172,7 +156,7 @@ interface AppConfig {
172
156
  */
173
157
  publicUrl: string;
174
158
  }
175
- interface HttpStreamConfig {
159
+ export interface HttpStreamConfig {
176
160
  /** Query-string flag clients pass to opt into SSE streaming. */
177
161
  queryParam: string;
178
162
  /** Value the query-string flag must have. */
@@ -180,13 +164,13 @@ interface HttpStreamConfig {
180
164
  /** Initial comment line written when an SSE stream opens (keeps proxies happy). */
181
165
  connectedComment: string;
182
166
  }
183
- interface SecurityHeadersConfig {
167
+ export interface SecurityHeadersConfig {
184
168
  frameOptions: string;
185
169
  referrerPolicy: string;
186
170
  xssProtection: string;
187
171
  contentTypeOptions: string;
188
172
  }
189
- interface CorsConfig {
173
+ export interface CorsConfig {
190
174
  allowedMethods: string;
191
175
  allowedHeaders: string;
192
176
  exposedHeaders: string;
@@ -224,7 +208,7 @@ interface CorsConfig {
224
208
  */
225
209
  allowOriginless?: boolean;
226
210
  }
227
- interface HttpConfig {
211
+ export interface HttpConfig {
228
212
  sessionCookieName: string;
229
213
  sessionCookieSameSite: 'Strict' | 'Lax' | 'None';
230
214
  sessionCookiePath: string;
@@ -331,7 +315,7 @@ interface HttpConfig {
331
315
  securityHeaders: SecurityHeadersConfig;
332
316
  cors: CorsConfig;
333
317
  }
334
- interface HealthHashConfig {
318
+ export interface HealthHashConfig {
335
319
  /**
336
320
  * - `'plain'` — unsalted `sha256(value)`; the pre-0.2.0 behavior, kept so an
337
321
  * existing router boot handshake keeps comparing successfully. Opt in with
@@ -355,7 +339,7 @@ interface HealthHashConfig {
355
339
  */
356
340
  salt: string;
357
341
  }
358
- interface PasswordPolicyConfig {
342
+ export interface PasswordPolicyConfig {
359
343
  /** Minimum total length. Default 8. */
360
344
  minLength: number;
361
345
  /** Maximum total length. Default 191 (DB column-friendly). */
@@ -381,7 +365,7 @@ interface PasswordPolicyConfig {
381
365
  */
382
366
  customValidator?: (password: string) => string | null;
383
367
  }
384
- interface AuthConfig {
368
+ export interface AuthConfig {
385
369
  /**
386
370
  * Whether email+password (credentials) auth is enabled. When `true` (default)
387
371
  * `@luckystack/login/register` registers the credentials provider, so it shows
@@ -497,7 +481,7 @@ interface AuthConfig {
497
481
  /** Wrong-code attempts before a pending 2FA challenge is burned. DEFAULT 5. */
498
482
  twoFactorMaxAttempts: number;
499
483
  }
500
- interface OfflineQueueConfig {
484
+ export interface OfflineQueueConfig {
501
485
  /** Hard cap on items per queue (api + sync are tracked separately). Default 200. */
502
486
  maxSize: number;
503
487
  /** Drop items older than this many ms when flushing/enqueuing. Default 1 hour. */
@@ -510,7 +494,7 @@ interface OfflineQueueConfig {
510
494
  */
511
495
  dropPolicy: 'drop-oldest' | 'drop-newest' | 'reject';
512
496
  }
513
- interface SyncStreamThrottleConfig {
497
+ export interface SyncStreamThrottleConfig {
514
498
  /** Default `flushAtChars` for `createStreamThrottle({...})`. */
515
499
  flushAtChars: number;
516
500
  /** Default `flushEveryMs` for `createStreamThrottle({...})`. */
@@ -518,7 +502,7 @@ interface SyncStreamThrottleConfig {
518
502
  /** Default `field` (payload key) for emitted chunks. */
519
503
  field: string;
520
504
  }
521
- interface TransportConfig {
505
+ export interface TransportConfig {
522
506
  /**
523
507
  * Transport used for typed API/sync invocations. Socket.io remains connected
524
508
  * in both modes for rooms, presence, reconnect state and realtime callbacks.
@@ -528,7 +512,7 @@ interface TransportConfig {
528
512
  */
529
513
  invocation: 'socket' | 'routed-http';
530
514
  }
531
- interface ApiConfig {
515
+ export interface ApiConfig {
532
516
  /**
533
517
  * Default response timeout (ms) for `apiRequest`. After this elapses with no
534
518
  * response (e.g. server restart/crash between emit and reply) the request
@@ -538,7 +522,7 @@ interface ApiConfig {
538
522
  */
539
523
  requestTimeoutMs: number | false;
540
524
  }
541
- interface ValidationConfig {
525
+ export interface ValidationConfig {
542
526
  /**
543
527
  * Whether runtime input validation runs in PRODUCTION (CORE-01).
544
528
  *
@@ -558,7 +542,7 @@ interface ValidationConfig {
558
542
  */
559
543
  runtimeMode: 'enforce' | 'off';
560
544
  }
561
- interface SyncConfig {
545
+ export interface SyncConfig {
562
546
  streamThrottle: SyncStreamThrottleConfig;
563
547
  /**
564
548
  * Default response timeout (ms) for `syncRequest`'s acknowledgement. Same
@@ -605,7 +589,7 @@ interface SyncConfig {
605
589
  */
606
590
  flushPressure: SyncFlushPressureConfig;
607
591
  }
608
- interface SyncFlushPressureConfig {
592
+ export interface SyncFlushPressureConfig {
609
593
  /** Max queued chunks before the stream applies backpressure (pauses). Default 1000. */
610
594
  highWaterMarkChunks: number;
611
595
  /** Resume emitting once the queue drains below this. Default 250. */
@@ -613,7 +597,7 @@ interface SyncFlushPressureConfig {
613
597
  /** Hard cap on bytes buffered for a single stream before dropping/erroring. Default 5_242_880 (5 MiB). */
614
598
  maxBufferedBytes: number;
615
599
  }
616
- interface SocketConfig {
600
+ export interface SocketConfig {
617
601
  /**
618
602
  * Maximum payload size (bytes) for any single Socket.io message (whole-frame
619
603
  * buffer). DEFAULT 5 MiB (5 * 1024 * 1024). This is the SOCKET-transport cap; the
@@ -642,7 +626,7 @@ interface SocketConfig {
642
626
  */
643
627
  maxRoomsPerSession: number | false;
644
628
  }
645
- interface DevConfig {
629
+ export interface DevConfig {
646
630
  /** Debounce window for hot-reload triggers. */
647
631
  hotReloadDebounceMs: number;
648
632
  /** Chokidar `awaitWriteFinish.stabilityThreshold`. */
@@ -658,7 +642,7 @@ interface DevConfig {
658
642
  */
659
643
  warnOnMissingInputType?: boolean;
660
644
  }
661
- interface PathsConfig {
645
+ export interface PathsConfig {
662
646
  /** Frontend source root. */
663
647
  srcDir: string;
664
648
  /** Server-side source root. */
@@ -698,7 +682,7 @@ interface PathsConfig {
698
682
  /** Generated API docs JSON output path. */
699
683
  generatedApiDocs: string;
700
684
  }
701
- interface ProjectConfig {
685
+ export interface ProjectConfig {
702
686
  app: AppConfig;
703
687
  logging: LoggingConfig;
704
688
  rateLimiting: RateLimitingConfig;
@@ -734,90 +718,9 @@ interface ProjectConfig {
734
718
  */
735
719
  oauthCallbackBase?: string;
736
720
  }
737
- type ProjectConfigInput = DeepPartial<ProjectConfig>;
738
- declare const DEFAULT_PROJECT_CONFIG: ProjectConfig;
739
- declare const registerProjectConfig: (config: ProjectConfigInput) => void;
740
- declare const getProjectConfig: () => ProjectConfig;
741
- declare const getProjectName: () => string;
742
- declare const isProjectConfigRegistered: () => boolean;
743
-
744
- /**
745
- * What `T` becomes after `JSON.stringify` → `JSON.parse`, at the type level.
746
- *
747
- * The mirror of what the codegen does to route OUTPUTS. Use it wherever you
748
- * hand-write a type for something that CROSSES THE WIRE — the client never
749
- * receives your server type, it receives JSON, and JSON has no `Date`:
750
- *
751
- * type ClientUser = Jsonify<User>; // createdAt: string, not Date
752
- *
753
- * Without it a type says `Date`, the client gets `"2026-07-15T15:20:15.553Z"`,
754
- * and `user.createdAt.getTime()` compiles and then throws at runtime.
755
- *
756
- * Deliberately SHALLOW — one level of properties, descending only into arrays.
757
- * A fully recursive object mapper does not survive contact with Prisma's
758
- * self-referential `JsonValue`. Already-JSON-stable values are therefore an
759
- * identity boundary: do not recursively rebuild what JSON already represents.
760
- * The NON-DISTRIBUTIVE tuple guard is load-bearing — it recognizes the complete
761
- * `JsonValue` union before the Date branch distributes `Date | null` into
762
- * `string | null`.
763
- */
764
- type JsonStableValue = string | number | boolean | null | JsonStableValue[] | {
765
- [key: string]: JsonStableValue | undefined;
766
- };
767
- type JsonifyValue<TValue> = [TValue] extends [JsonStableValue] ? TValue : TValue extends Date ? string : TValue extends readonly (infer TElement)[] ? JsonifyValue<TElement>[] : TValue;
768
- type Jsonify<T> = {
769
- [TKey in keyof T]: JsonifyValue<T[TKey]>;
770
- };
771
- interface SessionLocation {
772
- pathName: string;
773
- searchParams?: Record<string, string>;
774
- }
775
- interface BaseSessionLayout {
776
- id: string;
777
- token?: string;
778
- email?: string | null;
779
- name?: string | null;
780
- avatar?: string | null;
781
- avatarFallback?: string | null;
782
- admin?: boolean | null;
783
- language?: string | null;
784
- location?: SessionLocation;
785
- roomCodes?: string[];
786
- /**
787
- * CSRF token bound to this session. Generated by `saveSession` on first
788
- * write. Validated by the HTTP handler on state-changing requests when
789
- * the session is in cookie mode. Token-mode sessions don't need this
790
- * (cross-origin requests don't auto-attach a sessionStorage value).
791
- */
792
- csrfToken?: string;
793
- /**
794
- * Most recent successful login time (set to "now" on each login). Optional
795
- * — populated only when the User schema has a `lastLogin` column.
796
- */
797
- lastLogin?: Date | string | null;
798
- /**
799
- * The previous successful login time, captured at this login's moment.
800
- * Useful for "Last sign-in: X ago" UI on the post-login dashboard.
801
- * Null on first-ever login. Runtime-only — not persisted to the DB.
802
- */
803
- previousLogin?: Date | string | null;
804
- }
805
- interface AuthProps {
806
- /** If true, user must have a valid session with an ID */
807
- login: boolean;
808
- /** Additional validation rules for session properties */
809
- additional?: {
810
- /** The session property to check (e.g., 'admin', 'email') */
811
- key: keyof BaseSessionLayout;
812
- /** Exact value the property must equal (strict comparison) */
813
- value?: unknown;
814
- /** Type the property must be */
815
- type?: 'string' | 'number' | 'boolean';
816
- /** If true, property must be null/undefined. If false, must NOT be null/undefined */
817
- nullish?: boolean;
818
- /** If true, property must be falsy. If false, must be truthy */
819
- mustBeFalsy?: boolean;
820
- }[];
821
- }
822
-
823
- export { type AppConfig as A, type BaseSessionLayout as B, type CorsConfig as C, type DeepPartial as D, type HealthHashConfig as H, type Jsonify as J, type LoggingConfig as L, type OfflineQueueConfig as O, type ProjectConfig as P, type RateLimitingConfig as R, type SessionConfig as S, type TransportConfig as T, type ValidationConfig as V, type AuthProps as a, type SessionLocation as b, type ApiConfig as c, type AuthConfig as d, type AuthRateLimitConfig as e, DEFAULT_PROJECT_CONFIG as f, getProjectConfig as g, type DevConfig as h, isProjectConfigRegistered as i, type HttpConfig as j, type HttpStreamConfig as k, type PasswordPolicyConfig as l, type PathsConfig as m, type ProjectConfigInput as n, type RateLimitIdentity as o, type RateLimitIdentityParams as p, type SecurityHeadersConfig as q, registerProjectConfig as r, type SocketConfig as s, type SyncConfig as t, type SyncFlushPressureConfig as u, type SyncStreamThrottleConfig as v, deepMerge as w, getProjectName as x, isPlainObject as y };
721
+ export type ProjectConfigInput = DeepPartial<ProjectConfig>;
722
+ export declare const DEFAULT_PROJECT_CONFIG: ProjectConfig;
723
+ export declare const registerProjectConfig: (config: ProjectConfigInput) => void;
724
+ export declare const getProjectConfig: () => ProjectConfig;
725
+ export declare const getProjectName: () => string;
726
+ export declare const isProjectConfigRegistered: () => boolean;
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Rate Limiter
3
+ *
4
+ * The framework's rate-limit dispatch is pluggable via
5
+ * `registerRateLimitStrategy()`. The built-in `defaultRateLimitStrategy`
6
+ * implements in-memory counters with optional Redis backing for
7
+ * multi-instance deployments. Consumers can plug in alternative strategies
8
+ * (token-bucket, sliding-window, per-user tiers, edge-KV-backed, etc.) by
9
+ * implementing the `RateLimitStrategy` interface.
10
+ */
11
+ export interface CheckRateLimitParams {
12
+ /** Unique key for rate limiting (e.g., "user:123:api:getData") */
13
+ key: string;
14
+ /** Maximum requests allowed in window */
15
+ limit: number;
16
+ /** Time window in milliseconds (default: 60_000 = 1 minute) */
17
+ windowMs?: number;
18
+ }
19
+ export interface RateLimitResult {
20
+ /** Whether the request is allowed */
21
+ allowed: boolean;
22
+ /** Remaining requests in current window */
23
+ remaining: number;
24
+ /** Seconds until rate limit resets */
25
+ resetIn: number;
26
+ }
27
+ /**
28
+ * Pluggable rate-limit backend. The framework calls these methods through
29
+ * the active strategy slot. Default implementation is memory + Redis.
30
+ * Replace via `registerRateLimitStrategy(...)` at boot.
31
+ */
32
+ export interface RateLimitStrategy {
33
+ /** Human-readable identifier (used in logs/diagnostics). */
34
+ name: string;
35
+ /** Increment the counter for `key` and return whether the request is allowed. */
36
+ check(params: CheckRateLimitParams): Promise<RateLimitResult>;
37
+ /** Read current status without incrementing — useful for response headers. */
38
+ getStatus(key: string, limit: number): Promise<RateLimitResult>;
39
+ /** Reset the counter for a single key. */
40
+ clear(key: string): Promise<void>;
41
+ /** Reset all counters under this strategy's namespace. */
42
+ clearAll(): Promise<void>;
43
+ }
44
+ /**
45
+ * Built-in strategy: in-memory counters with optional Redis backing for
46
+ * multi-instance deployments. Mode is controlled by
47
+ * `projectConfig.rateLimiting.store` (`'memory'` or `'redis'`). When `redis`
48
+ * mode fails (e.g. Redis disconnect), the strategy silently degrades to
49
+ * in-memory and logs a one-shot warning.
50
+ */
51
+ export declare const defaultRateLimitStrategy: RateLimitStrategy;
52
+ /**
53
+ * Replace the active rate-limit backend. Pass a strategy that implements
54
+ * `RateLimitStrategy`. Common use cases:
55
+ *
56
+ * - Token-bucket / sliding-window algorithms for smoother burst handling.
57
+ * - Per-user tier limits (premium accounts get higher caps).
58
+ * - Cloudflare-Workers-KV or upstash-rest-backed counters for edge.
59
+ * - No-op strategy for test environments.
60
+ *
61
+ * Call this once at boot (typically from `luckystack/server/index.ts`)
62
+ * BEFORE the first request lands. Subsequent calls overwrite the previous
63
+ * registration (last-write-wins).
64
+ */
65
+ export declare const registerRateLimitStrategy: (strategy: RateLimitStrategy) => void;
66
+ /** Read the currently-active strategy (defaults to the built-in). */
67
+ export declare const getRateLimitStrategy: () => RateLimitStrategy;
68
+ /**
69
+ * Check if a request is allowed under rate limiting rules.
70
+ * Increments the counter for the key if allowed.
71
+ */
72
+ export declare const checkRateLimit: (params: CheckRateLimitParams) => Promise<RateLimitResult>;
73
+ /**
74
+ * Get current rate limit status without incrementing counter.
75
+ * Useful for rate limit headers in responses.
76
+ */
77
+ export declare const getRateLimitStatus: (key: string, limit: number) => Promise<RateLimitResult>;
78
+ /**
79
+ * Clear rate limit for a specific key.
80
+ * Useful for admin overrides or testing.
81
+ */
82
+ export declare const clearRateLimit: (key: string) => Promise<void>;
83
+ /**
84
+ * Clear all rate limits.
85
+ * Intended for testing or dev-mode server reset only.
86
+ * CORE-O8: throws in production to prevent accidental global counter wipe across
87
+ * all tenants. If you need a production-safe reset, pass a scoped key to
88
+ * `clearRateLimit` or implement a tenant-namespaced strategy.
89
+ */
90
+ export declare const clearAllRateLimits: () => Promise<void>;
@@ -0,0 +1,11 @@
1
+ import { ReactNode } from 'react';
2
+ export type AvatarStatus = 'avatar' | 'fallback';
3
+ interface AvatarContextType {
4
+ avatarStatuses: Record<string, AvatarStatus>;
5
+ setAvatarStatus: (key: string, status: AvatarStatus) => void;
6
+ }
7
+ export declare const AvatarProvider: ({ children }: {
8
+ children: ReactNode;
9
+ }) => import("react").JSX.Element;
10
+ export declare function useAvatarContext(): AvatarContextType;
11
+ export {};
@@ -0,0 +1,10 @@
1
+ import { ReactNode } from 'react';
2
+ export default function Middleware({ children, denied, }: {
3
+ children: ReactNode;
4
+ /**
5
+ * Rendered instead of the page when middleware denies IN PLACE
6
+ * (`{ success: false, status }`) — the URL is kept. Pass a node, or a
7
+ * function to vary by status. Defaults to a minimal built-in view.
8
+ */
9
+ denied?: ReactNode | ((status: number) => ReactNode);
10
+ }): import("react").JSX.Element | null;
@@ -0,0 +1 @@
1
+ export default function useRouter(): (path: string) => Promise<void>;
@@ -0,0 +1,22 @@
1
+ import { ReactNode, Dispatch, SetStateAction } from 'react';
2
+ import { getActiveLanguage, getDefaultLocale, getRegisteredLocales } from '../localesRegistry.js';
3
+ export type TranslationRecord = Record<string, unknown>;
4
+ export declare function TranslationProvider({ children }: {
5
+ children: ReactNode;
6
+ }): import("react").JSX.Element;
7
+ export declare function useTranslation(): TranslationRecord;
8
+ export declare function useUpdateLanguage(): Dispatch<SetStateAction<string>>;
9
+ export interface TranslateParam {
10
+ key: string;
11
+ value: string | number | boolean;
12
+ }
13
+ export declare function translate({ translationList, key, params }: {
14
+ translationList: TranslationRecord;
15
+ key: unknown;
16
+ params?: TranslateParam[];
17
+ }): string;
18
+ export declare const useTranslator: () => ({ key, params }: {
19
+ key: string;
20
+ params?: TranslateParam[];
21
+ }) => string;
22
+ export { getActiveLanguage, getDefaultLocale, getRegisteredLocales };
@@ -0,0 +1,12 @@
1
+ import { type TranslateParam } from './TranslationProvider.js';
2
+ interface NotifyInput {
3
+ key: string;
4
+ params?: TranslateParam[];
5
+ }
6
+ declare const notify: {
7
+ success: (input: NotifyInput) => void;
8
+ error: (input: NotifyInput) => void;
9
+ info: (input: NotifyInput) => void;
10
+ warning: (input: NotifyInput) => void;
11
+ };
12
+ export default notify;
@@ -0,0 +1,31 @@
1
+ import type { BaseSessionLayout } from '../sessionTypes.js';
2
+ import { type ClientHookStopSignal } from '../clientHookBus.js';
3
+ export interface SessionContextValue<TSession extends BaseSessionLayout = BaseSessionLayout> {
4
+ session: TSession | null;
5
+ sessionLoaded: boolean;
6
+ }
7
+ export declare const SessionContext: import("react").Context<SessionContextValue<BaseSessionLayout>>;
8
+ export declare function useSession<TSession extends BaseSessionLayout = BaseSessionLayout>(): SessionContextValue<TSession>;
9
+ export declare const setLatestSession: (session: BaseSessionLayout | null) => void;
10
+ export declare const getCurrentSession: <TSession extends BaseSessionLayout = BaseSessionLayout>() => TSession | null;
11
+ /**
12
+ * Vetoable entry point for committing a null → session transition (i.e. a
13
+ * fresh login or a page-load session restore). Dispatches the `preLogin`
14
+ * client hook to every registered handler in parallel; if any returns a
15
+ * stop signal, the transition is aborted and `{ committed: false, signal }`
16
+ * is returned. Otherwise, `setLatestSession(session)` runs synchronously
17
+ * (which fires `postLogin`) and `{ committed: true }` comes back.
18
+ *
19
+ * Consumer's `SessionProvider` should `await` this for non-null session
20
+ * commits and roll local React state back to `null` when a veto comes
21
+ * through, so the UI doesn't render a half-logged-in state.
22
+ */
23
+ export type ProposeLoginResult = {
24
+ stopped: false;
25
+ committed: true;
26
+ } | {
27
+ stopped: true;
28
+ signal: ClientHookStopSignal;
29
+ committed: false;
30
+ };
31
+ export declare const proposeLogin: (session: BaseSessionLayout) => Promise<ProposeLoginResult>;
@@ -0,0 +1,5 @@
1
+ export type Theme = 'light' | 'dark';
2
+ export declare const useTheme: () => {
3
+ theme: Theme;
4
+ setTheme: (next: Theme) => void;
5
+ };
@@ -0,0 +1,22 @@
1
+ export declare const DEFAULT_REDACTED_LOG_KEYS: readonly string[];
2
+ export declare const registerRedactedLogKeys: (keys: readonly string[]) => void;
3
+ export declare const getRedactedLogKeys: () => readonly string[];
4
+ export declare const isRedactedLogKey: (key: string) => boolean;
5
+ export declare const resetRedactedLogKeysForTests: () => void;
6
+ /** Placeholder substituted for a redacted value. */
7
+ export declare const REDACTED_PLACEHOLDER = "[redacted]";
8
+ export declare const DEPTH_TRUNCATED_PLACEHOLDER = "[truncated: max depth]";
9
+ /**
10
+ * Recursively deep-copy `value`, replacing any property whose key matches the
11
+ * registered redacted-keys set (case-insensitive) with {@link REDACTED_PLACEHOLDER}
12
+ * (SYNC-17 defense-in-depth). Use it at raw log / `captureException` context
13
+ * sites so a token/password nested anywhere in a payload never lands verbatim in
14
+ * a logger sink or error-tracker breadcrumb.
15
+ *
16
+ * - Primitives pass through unchanged.
17
+ * - Arrays are mapped element-wise (array indices are never "keys", so an
18
+ * element is only redacted by its own nested object keys).
19
+ * - Cycles / over-deep graphs are cut at {@link MAX_SANITIZE_DEPTH} with
20
+ * {@link DEPTH_TRUNCATED_PLACEHOLDER}; the input is never mutated.
21
+ */
22
+ export declare const sanitizeForLog: (value: unknown, depth?: number, _seen?: WeakSet<object>) => unknown;
@@ -0,0 +1,15 @@
1
+ import Redis from 'ioredis';
2
+ import type { Redis as RedisClient } from 'ioredis';
3
+ export declare const resetDefaultRedisClient: () => void;
4
+ export declare const rebuildDefaultRedisClient: () => RedisClient;
5
+ export interface RedisConnectionOptions {
6
+ host: string;
7
+ port: number;
8
+ username?: string;
9
+ password?: string;
10
+ }
11
+ export declare const getRedisConnectionOptions: () => RedisConnectionOptions;
12
+ export declare const registerStrayPrefixCommand: (...commands: string[]) => void;
13
+ declare const redisProxy: Redis;
14
+ export declare const redis: Redis;
15
+ export default redisProxy;
@@ -0,0 +1,7 @@
1
+ export type RedisKeyFormatter = (namespace: string, suffix: string) => string;
2
+ export declare const defaultRedisKeyFormatter: RedisKeyFormatter;
3
+ export declare const registerRedisKeyFormatter: (fn: RedisKeyFormatter) => void;
4
+ export declare const getRedisKeyFormatter: () => RedisKeyFormatter;
5
+ export declare const resetRedisKeyFormatterForTests: () => void;
6
+ export declare const formatKey: (namespace: string, suffix?: string) => string;
7
+ export declare const applyStrayKeyPrefix: (key: string) => string;
@@ -0,0 +1,51 @@
1
+ import type { IncomingHttpHeaders } from 'node:http';
2
+ /**
3
+ * Stable sentinel returned when no usable client address could be resolved.
4
+ * Kept as a single exported constant so every rate-limit key-site funnels
5
+ * missing addresses into one deterministic bucket instead of ad-hoc literals.
6
+ */
7
+ export declare const UNKNOWN_CLIENT_IP = "unknown";
8
+ export interface ResolveClientIpParams {
9
+ /**
10
+ * The raw transport peer address — `socket.handshake.address` (Socket.io)
11
+ * or `req.socket.remoteAddress` (raw HTTP). May be `undefined`/`null` when
12
+ * the connection has already been torn down.
13
+ */
14
+ rawAddress: string | null | undefined;
15
+ /** Request / handshake headers. Only consulted when `trustProxy` is true. */
16
+ headers: IncomingHttpHeaders;
17
+ /**
18
+ * Whether a known reverse proxy sits in front of this server. DEFAULT false.
19
+ * When false the raw peer address is returned verbatim (only IPv4-mapped
20
+ * IPv6 canonicalization is applied), preserving historical behaviour. Only
21
+ * enable when a trusted proxy populates `X-Forwarded-For` / `X-Real-IP`,
22
+ * otherwise a client can spoof its own IP via those headers.
23
+ */
24
+ trustProxy?: boolean;
25
+ /**
26
+ * Number of proxy hops to skip from the RIGHT of `X-Forwarded-For` when
27
+ * `trustProxy` is on (CORE-O3). The rightmost entries are appended by your own
28
+ * trusted proxies; the resolved client IP is the entry that many hops in from
29
+ * the end. The leftmost hop is client-controlled and is never trusted. DEFAULT
30
+ * 1 (the immediate upstream proxy). Clamped to the list length so an over-large
31
+ * count falls back to the leftmost trusted hop rather than the spoofable entry.
32
+ */
33
+ trustedProxyHopCount?: number;
34
+ }
35
+ /**
36
+ * Whether a resolved client IP is a loopback / unresolved address — used by the
37
+ * api/sync transports to honour `rateLimiting.skipLoopbackInDev` (api F5/F11).
38
+ * Matches IPv4 loopback (`127.0.0.0/8`), IPv6 loopback (`::1`), the IPv4-mapped
39
+ * form (`::ffff:127.0.0.1`, already canonicalized away by {@link resolveClientIp}),
40
+ * and the {@link UNKNOWN_CLIENT_IP} sentinel. Pass an already-resolved IP.
41
+ */
42
+ export declare const isLoopbackIp: (ip: string) => boolean;
43
+ /**
44
+ * Derive the deterministic, non-reversible bucket component for a session
45
+ * token. The same token always yields the same value (bucket identity is
46
+ * preserved) while the raw token never appears in the returned string.
47
+ * Used by api AND sync rate-limit key builders — lives in core so both
48
+ * packages can share one implementation without a cross-package dependency.
49
+ */
50
+ export declare const deriveTokenBucketId: (token: string) => string;
51
+ export declare const resolveClientIp: ({ rawAddress, headers, trustProxy, trustedProxyHopCount }: ResolveClientIpParams) => string;
@@ -0,0 +1,33 @@
1
+ export interface ErrorParam {
2
+ key: string;
3
+ value: string | number | boolean;
4
+ }
5
+ export interface ErrorResponseInput {
6
+ status?: unknown;
7
+ errorCode?: unknown;
8
+ errorParams?: unknown;
9
+ httpStatus?: unknown;
10
+ }
11
+ export interface NormalizedErrorResponse {
12
+ status: 'error';
13
+ message: string;
14
+ errorCode: string;
15
+ errorParams?: ErrorParam[];
16
+ httpStatus?: number;
17
+ }
18
+ export declare const INVALID_ERROR_RESPONSE_CODE = "error.invalidResponse";
19
+ export declare const isErrorParamArray: (value: unknown) => value is ErrorParam[];
20
+ export declare const normalizeErrorResponseCore: ({ response, fallbackHttpStatus, fallbackErrorCode, resolveMessage, }: {
21
+ response: ErrorResponseInput;
22
+ fallbackHttpStatus?: number;
23
+ fallbackErrorCode?: string;
24
+ resolveMessage?: (args: {
25
+ errorCode: string;
26
+ errorParams?: ErrorParam[];
27
+ }) => string;
28
+ }) => NormalizedErrorResponse;
29
+ export declare const defaultHttpStatusForResponse: ({ status, explicitHttpStatus, fallbackErrorStatus, }: {
30
+ status: 'success' | 'error';
31
+ explicitHttpStatus?: number;
32
+ fallbackErrorStatus?: number;
33
+ }) => number;