@luckystack/core 0.10.1 → 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 (123) hide show
  1. package/AGENTS.md +11 -0
  2. package/CHANGELOG.md +13 -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 -1886
  54. package/dist/index.js +8 -0
  55. package/dist/index.js.map +1 -1
  56. package/dist/lease.d.ts +18 -0
  57. package/dist/localSocketEnumerationGuard.d.ts +22 -0
  58. package/dist/localeReloader.d.ts +3 -0
  59. package/dist/localesRegistry.d.ts +8 -0
  60. package/dist/localizedNormalizer.d.ts +50 -0
  61. package/dist/loggerRegistry.d.ts +12 -0
  62. package/dist/middlewareRegistry.d.ts +45 -0
  63. package/dist/notifier.d.ts +17 -0
  64. package/dist/offlineQueue.d.ts +48 -0
  65. package/dist/oneTimeToken.d.ts +43 -0
  66. package/dist/pageRouteValidation.d.ts +37 -0
  67. package/dist/paths.d.ts +35 -0
  68. package/dist/peerDeps.d.ts +23 -0
  69. package/dist/portOverrideRegistry.d.ts +3 -0
  70. package/dist/processUpload.d.ts +44 -0
  71. package/dist/{sessionTypes-DanjuPaE.d.ts → projectConfig.d.ts} +32 -129
  72. package/dist/rateLimiter.d.ts +90 -0
  73. package/dist/react/AvatarProvider.d.ts +11 -0
  74. package/dist/react/Middleware.d.ts +10 -0
  75. package/dist/react/Router.d.ts +1 -0
  76. package/dist/react/TranslationProvider.d.ts +22 -0
  77. package/dist/react/notify.d.ts +12 -0
  78. package/dist/react/sessionContext.d.ts +31 -0
  79. package/dist/react/useTheme.d.ts +5 -0
  80. package/dist/redactedLogKeys.d.ts +22 -0
  81. package/dist/redis.d.ts +15 -0
  82. package/dist/redisKeyFormatter.d.ts +7 -0
  83. package/dist/resolveClientIp.d.ts +51 -0
  84. package/dist/responseNormalizer.d.ts +33 -0
  85. package/dist/roomNameFormatterRegistry.d.ts +36 -0
  86. package/dist/roomSockets.d.ts +23 -0
  87. package/dist/routedDataQuery.d.ts +7 -0
  88. package/dist/routedHttpInvocation.d.ts +20 -0
  89. package/dist/runtimeMapsRegistry.d.ts +18 -0
  90. package/dist/runtimeTypeValidation.d.ts +13 -0
  91. package/dist/secretsResolved.d.ts +3 -0
  92. package/dist/sentrySetup.d.ts +21 -0
  93. package/dist/serveAvatars.d.ts +5 -0
  94. package/dist/serviceRoute.d.ts +27 -0
  95. package/dist/servicesConfigRegistry.d.ts +22 -0
  96. package/dist/sessionProviderRegistry.d.ts +46 -0
  97. package/dist/sessionTypes.d.ts +79 -0
  98. package/dist/sleep.d.ts +1 -0
  99. package/dist/socketEvents.d.ts +37 -0
  100. package/dist/socketMiddlewareRegistry.d.ts +20 -0
  101. package/dist/socketRedisAdapter.d.ts +21 -0
  102. package/dist/socketState.d.ts +5 -0
  103. package/dist/socketStatusTypes.d.ts +6 -0
  104. package/dist/socketTypes.d.ts +34 -0
  105. package/dist/synchronizedEnvHashes.d.ts +66 -0
  106. package/dist/testFileConvention.d.ts +11 -0
  107. package/dist/testResetTargets.d.ts +5 -0
  108. package/dist/tryCatch.d.ts +1 -0
  109. package/dist/tryCatchClient.d.ts +1 -0
  110. package/dist/tryCatchSync.d.ts +1 -0
  111. package/dist/validateRequest.d.ts +50 -0
  112. package/docs/app-bootstrap.md +4 -449
  113. package/docs/config-registry.md +3 -346
  114. package/docs/csrf-config.md +3 -86
  115. package/docs/error-tracker-registry.md +3 -192
  116. package/docs/hooks.md +2 -295
  117. package/docs/rate-limit-strategy.md +3 -168
  118. package/docs/redis-adapter.md +5 -110
  119. package/docs/session-types.md +3 -178
  120. package/docs/socket-bootstrap.md +5 -385
  121. package/package.json +2 -2
  122. package/CLAUDE.md +0 -256
  123. package/dist/pageRouteValidation-B4Cmtfjv.d.ts +0 -280
@@ -1,452 +1,7 @@
1
- # App Bootstrap
1
+ # App bootstrap
2
2
 
3
- > Deep specs for the registries + utilities a project entry wires at boot: bind address, runtime maps, api-method map, prisma/redis clients, notifier, email senders, logger + redacted log keys, locale reloader, boot UUID, synchronized env hashes, plus the cross-cutting helpers `tryCatch`, `sleep`, `getParams`, `serveAvatar`. Source: `packages/core/src/`. Bijgewerkt: 2026-07-20.
3
+ The registry pattern separates framework packages from each application's infrastructure. Register dependencies during boot and read them when needed, rather than importing consumer configuration or caching stale values at module load. The server entry performs standard wiring; take over registration manually only when owning boot order yourself. See `../src/createRegistry.ts`, `../src/clients.ts`, `../../server/src/bootstrap.ts`.
4
4
 
5
- ## Overview
5
+ A consumer-supplied client is intentional ownership: automatic secret refresh must not replace a custom Redis or database connection without its owner's consent. Logging and tracking contexts must redact credentials before fanout; production should not install stack-trace-heavy development logging. The runtime/deploy-environment distinction matters: deployment identity does not authorize development behavior. See `../src/redis.ts`, `../src/env.ts`, `../src/redactedLogKeys.ts`.
6
6
 
7
- The core package exposes a uniform "register at boot, read at call time" pattern across many DI slots. A consumer's boot entry calls every relevant `register*` function once, before framework hot paths run. Framework packages then read via `get*` accessors and never reach around the registry.
8
-
9
- This doc groups every slot into sections:
10
-
11
- - **Bind address** — `createLuckyStackServer` writes the resolved listen address so CORS code reads it from one source.
12
- - **Runtime maps + api method map** — the project's generated route maps are injected via a provider, so the api/sync packages can resolve route handlers without relative imports into the consumer tree.
13
- - **Prisma + Redis clients** — DI slots with lazy defaults; `prisma` and `redis` are proxies that defer resolution to call time.
14
- - **Notifier** — client-side toast slot used by `apiRequest`, `syncRequest`, and the offline-queue flush.
15
- - **Email senders** — multi-adapter registry (`'default'`, `'transactional'`, `'marketing'`, custom) with a legacy single-sender path.
16
- - **Logger + redacted log keys** — pluggable logger (default = console; dev factory available) plus a set of keys masked when printing payloads.
17
- - **Locale reloader** — devkit watcher trigger point.
18
- - **Boot UUID + synchronized env hashes** — router cross-env handshake primitives.
19
- - **Misc utilities** — `tryCatch`, `sleep`, `getParams`, `serveAvatar`.
20
-
21
- ## API Reference — Runtime Port Override + Bind Address
22
-
23
- ### `registerPortOverride(port: number | null): void` / `getPortOverride(): number | undefined`
24
-
25
- Browser-safe registry exported from both `@luckystack/core/config` and the main barrel. `@luckystack/server/parseArgv` registers the positional CLI port before consumer `config.ts` evaluates; `null` clears it. This is runtime override state only — the static default remains consumer-owned `config.ports.ts`. Values outside integer `0..65535` throw.
26
-
27
- ### `registerBindAddress(address: { ip: string; port: number; configuredPort?: number }): void`
28
-
29
- Stores the intended pre-listen address used by `createLuckyStackServer` and resets the OAuth pre-hop baseline. Optional `configuredPort` records the consumer default below a programmatic/argv override, so OAuth can update a callback that still names that default without bypassing an unrelated local router ingress. Framework code never needs to infer the listen port from env vars.
30
-
31
- ### `registerBoundAddress(address: { ip: string; port: number }): void`
32
-
33
- Called from the successful `node:http.listen` callback with `httpServer.address().port`. Updates the address all runtime readers see while preserving the intended address. The distinction matters for `listen(0)` and for a dev auto-increment hop.
34
-
35
- ### `getBindAddress(): { ip: string; port: string }`
36
-
37
- **Behavior (resolution order):**
38
- 1. Actually-bound registered value (returns `{ ip: registered.ip, port: String(registered.port) }`).
39
- 2. `process.env.SERVER_IP` for the pre-bootstrap bind address.
40
- 3. Fallback to `'127.0.0.1'` / the generic server port `'80'`. A real server registers its intended `options.port` / argv / `defaultPort` before request-time CORS or OAuth resolution.
41
-
42
- ### `resolveDevCallbackUrl(callbackUrl: string): string`
43
-
44
- In non-production environments, rewrites a `localhost`, `127.0.0.1`, or `[::1]` OAuth callback only when its effective port still equals the intended pre-listen port or the registered consumer default and the server uses another port. Authorize and token exchange both call this helper, so their `redirect_uri` remains byte-identical. An explicitly configured local router/reverse-proxy port that matches neither is left untouched. Production, remote hosts, malformed URLs, and already-current callbacks are no-ops.
45
-
46
- ## API Reference — Runtime Maps
47
-
48
- ### Types
49
- ```typescript
50
- export interface RuntimeApiMapsResult {
51
- apisObject: Record<string, unknown>;
52
- functionsObject: Record<string, unknown>;
53
- }
54
-
55
- export interface RuntimeSyncMapsResult {
56
- syncObject: Record<string, unknown>;
57
- functionsObject: Record<string, unknown>;
58
- }
59
-
60
- export interface RuntimeMapsProvider {
61
- getRuntimeApiMaps: () => Promise<RuntimeApiMapsResult>;
62
- getRuntimeSyncMaps: () => Promise<RuntimeSyncMapsResult>;
63
- }
64
- ```
65
-
66
- ### `registerRuntimeMapsProvider(provider: RuntimeMapsProvider): void`
67
-
68
- Stores a provider. The project's `server/prod/runtimeMaps.ts` knows how to dynamically import per-preset `generatedApis.<preset>.ts` files and merge devkit overrides — framework packages don't. They ask core for the current maps and core delegates.
69
-
70
- ### `getRuntimeApiMaps(): Promise<RuntimeApiMapsResult>`
71
- ### `getRuntimeSyncMaps(): Promise<RuntimeSyncMapsResult>`
72
-
73
- Async accessors. Default (unregistered) returns empty `{ apisObject: {}, functionsObject: {} }` / `{ syncObject: {}, functionsObject: {} }` so framework code can boot in tests without crashing — but every api/sync route silently 404s in that mode.
74
-
75
- ### `isRuntimeMapsProviderRegistered(): boolean`
76
-
77
- Used by `verifyBootstrap` (in `@luckystack/server`) to detect the production fail-mode where no provider got registered — typically because the project forgot to import its `server/prod/runtimeMaps.ts` side-effect.
78
-
79
- ## API Reference — API Method Map
80
-
81
- ### Types
82
- ```typescript
83
- export type HttpMethodLiteral = 'GET' | 'POST' | 'PUT' | 'DELETE';
84
- export type ApiMethodMap = Record<string, Record<string, Record<string, HttpMethodLiteral>>>;
85
- ```
86
-
87
- Shape: `apiMethodMap[pagePath][apiName][version] = 'GET' | ...`.
88
-
89
- ### `registerApiMethodMap(map: ApiMethodMap): void`
90
-
91
- Stores the generated method map. The canonical consumer wrapper `src/_sockets/apiRequest.ts` imports `apiMethodMap` from `apiTypes.generated.ts`, registers it at module load, then re-exports the core request helper. This registration is required for routed HTTP because route names alone cannot reliably reveal their declared HTTP method.
92
-
93
- ### `getRegisteredApiMethod(pagePath, apiName, version): HttpMethodLiteral | undefined`
94
-
95
- Returns the registered method or `undefined`. `isGetMethod` (in `apiRequest.ts`) falls back to the `inferHttpMethod` prefix heuristic when undefined.
96
-
97
- ### `isApiMethodMapRegistered(): boolean`
98
-
99
- Detects whether the prefix-heuristic fallback is active.
100
-
101
- ## API Reference — Prisma + Redis Clients
102
-
103
- ### `registerPrismaClient(client: PrismaClient): PrismaClient`
104
- ### `registerRedisClient(client: RedisClient): RedisClient`
105
-
106
- Stores a custom client and returns it for chaining. Replaces (last-write-wins).
107
-
108
- ### `getPrismaClient(): PrismaClient`
109
- ### `getRedisClient(): RedisClient`
110
-
111
- **Behavior (resolution order):**
112
- 1. Registered client.
113
- 2. Default resolver (set by `db.ts` / `redis.ts` at module load with the lazy factory).
114
- 3. Throws when neither exists ("No Prisma/Redis client available...").
115
-
116
- ### `isPrismaClientRegistered(): boolean`
117
- ### `isRedisClientRegistered(): boolean`
118
-
119
- Boot guards.
120
-
121
- ### `setDefaultPrismaResolver(resolver)` / `setDefaultRedisResolver(resolver)`
122
-
123
- Internal — wired by `db.ts` / `redis.ts`. Splits registration from default construction so this file doesn't depend on `@prisma/client` / `ioredis` types directly.
124
-
125
- ## API Reference — Notifier
126
-
127
- ### Types
128
- ```typescript
129
- export interface NotifyParam { key: string; value: string | number | boolean }
130
- export interface NotifyInput { key: string; params?: NotifyParam[] }
131
- export interface Notifier {
132
- success: (input: NotifyInput) => void;
133
- error: (input: NotifyInput) => void;
134
- info: (input: NotifyInput) => void;
135
- warning: (input: NotifyInput) => void;
136
- }
137
- ```
138
-
139
- ### `registerNotifier(notifier: Notifier): void`
140
-
141
- Replaces the active notifier. Default is a no-op (safe for SSR / tests).
142
-
143
- ### `getNotifier(): Notifier`
144
-
145
- Returns the active notifier.
146
-
147
- ### `notify: Notifier`
148
-
149
- A delegating wrapper used by framework hot paths so framework code can do `notify.error({ key: 'api.failed' })` without dereferencing on every call.
150
-
151
- ## API Reference — Email Senders
152
-
153
- ### Types
154
- ```typescript
155
- export interface EmailMessage {
156
- to: string | string[];
157
- subject: string;
158
- html: string;
159
- text?: string;
160
- from?: string;
161
- replyTo?: string;
162
- cc?: string | string[];
163
- bcc?: string | string[];
164
- }
165
-
166
- export type EmailResult =
167
- | { ok: true; id: string }
168
- | { ok: false; reason: string; cause?: unknown };
169
-
170
- export interface EmailSender {
171
- name: string;
172
- send: (message: EmailMessage) => Promise<EmailResult>;
173
- }
174
-
175
- export type EmailSenderRegistry = Record<string, EmailSender>;
176
- ```
177
-
178
- ### `registerEmailSender(sender: EmailSender): void`
179
-
180
- Legacy single-sender. Mirrors the sender into `registry.default` so `getEmailSenderByName('default')` finds it.
181
-
182
- ### `registerEmailSenders(senders: EmailSenderRegistry): void`
183
-
184
- Multi-adapter. Replaces the entire registry (last-write-wins). If `senders.default` exists, also updates the legacy slot.
185
-
186
- Reserved slot conventions:
187
- - `'default'` — fallback when no specific slot matches.
188
- - `'transactional'` — login password-reset, account-confirmation, security-critical mail.
189
- - `'marketing'` — newsletters, bulk sends.
190
- - `'diagnostics'` — diagnostic endpoints (e.g. playground).
191
-
192
- Custom slots ('billing', 'support') are allowed and resolved explicitly via `sendEmail({ adapter: 'billing', ... })` in `@luckystack/email`.
193
-
194
- ### `getEmailSender(): EmailSender | null`
195
-
196
- Returns the legacy sender, falling back to `registry.default`. `null` when neither is set.
197
-
198
- ### `getEmailSenderByName(name: string): EmailSender | null`
199
-
200
- **Behavior:**
201
- - Returns `registry[name]` when present.
202
- - Falls back to the legacy single sender when `name === 'default'`.
203
- - Returns `null` otherwise.
204
-
205
- ### `listEmailSenderNames(): string[]`
206
-
207
- Returns the registry keys (excludes the legacy single-sender slot).
208
-
209
- ### `isEmailSenderRegistered(): boolean`
210
-
211
- True when either the legacy slot or the registry is populated.
212
-
213
- ## API Reference — Logger
214
-
215
- ### Types
216
- ```typescript
217
- export interface LoggerContext { [key: string]: unknown }
218
-
219
- export interface Logger {
220
- debug: (message: string, context?: LoggerContext) => void;
221
- info: (message: string, context?: LoggerContext) => void;
222
- warn: (message: string, context?: LoggerContext) => void;
223
- error: (message: string, error?: unknown, context?: LoggerContext) => void;
224
- }
225
- ```
226
-
227
- ### `registerLogger(logger: Logger): void`
228
-
229
- Replaces the active logger and sets the registered flag.
230
-
231
- ### `getLogger(): Logger`
232
-
233
- Returns the active logger. Default is a thin wrapper over `console.{debug,info,warn,error}` with no color codes — safe for production.
234
-
235
- ### `isLoggerRegistered(): boolean`
236
-
237
- Boot guard.
238
-
239
- ### `createDevLogger(): Logger`
240
-
241
- Returns a logger that calls `console.log(message[, context], '<color>')`. The trailing color string is only meaningful after `initConsolelog()` installed the console monkey-patch — without it the codes print as literal text.
242
-
243
- ### `resetLoggerForTests(): void`
244
-
245
- Test-only — restores the default logger and clears the registered flag.
246
-
247
- ### `initConsolelog(): void`
248
-
249
- Monkey-patches `console.log` to:
250
- - Capture the caller's frame from the stack (skipping `consoleLog.ts` and `loggerRegistry.ts` frames).
251
- - Strip the platform-specific path prefix and column suffix from the frame, yielding a `<file>:<line>` label.
252
- - Find any color keyword in the args (`black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`, `reset`), remove it, and apply the ANSI color around the file label.
253
- - If the first arg is an object, print the label first then the args on a new line; otherwise concatenate with `' -- '` separator.
254
-
255
- Dev-only. Never install in production — stack-trace per log is expensive.
256
-
257
- ## API Reference — Redacted Log Keys
258
-
259
- Default seed set: `password`, `confirmpassword`, `token`, `newtoken`, `authorization`, `cookie`, `set-cookie`. Stored lowercased.
260
-
261
- ### `registerRedactedLogKeys(keys: readonly string[]): void`
262
-
263
- Additive — adds each lowercased key to the set. Idempotent on duplicates.
264
-
265
- ### `getRedactedLogKeys(): readonly string[]`
266
-
267
- Returns a fresh array of the current set (lowercased).
268
-
269
- ### `isRedactedLogKey(key: string): boolean`
270
-
271
- Case-insensitive lookup used inside framework log-redaction.
272
-
273
- ### `resetRedactedLogKeysForTests(): void`
274
-
275
- Test-only — clears and re-seeds with the default set.
276
-
277
- ## API Reference — Locale Reloader
278
-
279
- ### `registerLocaleReloader(reloader: LocaleReloader): LocaleReloader`
280
-
281
- Stores a `() => void | Promise<void>` callback and returns it. The consumer's i18n loader registers it; `@luckystack/devkit`'s file watcher calls it when `_locales/*.json` changes.
282
-
283
- ### `getLocaleReloader(): LocaleReloader | null`
284
-
285
- Returns the registered reloader, or `null` when none was supplied (watcher no-ops in that case).
286
-
287
- ## API Reference — Boot UUID + Env Sync
288
-
289
- ### Constants
290
-
291
- | Name | Value |
292
- |---|---|
293
- | `BOOT_KEY_PREFIX` | `'luckystack:boot:'` |
294
- | (internal) `DEFAULT_BOOT_KEY_TTL_SECONDS` | `3600` |
295
-
296
- `BOOT_KEY_PREFIX` is exported as the single source of truth so `@luckystack/router`'s `bootHandshake.ts` cannot drift.
297
-
298
- ### Runtime mode versus deploy environment
299
-
300
- - `resolveRuntimeMode()` returns the validated `NODE_ENV` application mode (`development`, `production` or `test`). `isProductionRuntime()` and `isTestRuntime()` are convenience predicates. Security, route-map, cookie and dev-tool behavior uses this axis.
301
- - `resolveEnvKey()` returns `process.env.LUCKYSTACK_ENV` ?? `process.env.NODE_ENV` ?? `'development'`. This is only the deploy-topology identity used by boot UUIDs, health attestation, routing and observability labels. A production process may therefore use `LUCKYSTACK_ENV=staging` without enabling development behavior.
302
-
303
- ### `writeBootUuid(envKey?: string): Promise<string>`
304
-
305
- **Behavior:** Generates a fresh `randomUUID()` and writes it to `luckystack:boot:<envKey>` in Redis with `EX <deployConfig.routing.bootKeyTtlSeconds ?? 3600>`. Returns the UUID.
306
-
307
- ### `readBootUuid(envKey?: string): Promise<string | null>`
308
-
309
- Reads the UUID for the given env key (defaults via `resolveEnvKey()`).
310
-
311
- ### `refreshBootUuid(envKey?: string): Promise<void>`
312
-
313
- Extends the existing key with the configured TTL without changing its UUID. If
314
- Redis recovered after the key had already expired, writes a fresh UUID instead.
315
- The key is environment-level: every healthy backend may renew the current value,
316
- so multi-instance readiness does not depend on the last process that wrote it.
317
-
318
- ### `startBootUuidHeartbeat(envKey?: string): BootUuidHeartbeat`
319
-
320
- Starts an unref'd refresh loop at one third of the configured TTL. Each refresh
321
- must settle before the next timer is scheduled, preventing overlapping Redis
322
- calls during an outage. Failures are logged and retried. Call `stop()` during
323
- server shutdown. `@luckystack/server` starts this only after HTTP listen succeeds.
324
-
325
- ### `collectSynchronizedEnvKeys(): string[]`
326
-
327
- Walks `deployConfig.resources` and collects every `synchronizedEnvKeys[]` entry into a sorted unique list.
328
-
329
- ### `computeSynchronizedEnvHashes(): Record<string, string | null>`
330
-
331
- For each key from `collectSynchronizedEnvKeys`, returns a SHA-256 hex hash of `process.env[key]` (or `null` when the var is unset). Used by `/_health` so the router can compare hashes across environments without leaking secrets.
332
-
333
- ### `hashSynchronizedValue(value: string): string`
334
-
335
- Router-side helper — hash a single value with the same algorithm. Kept separate so the router can import it without loading the core barrel (which opens a Redis connection).
336
-
337
- ## API Reference — Utilities
338
-
339
- ### `tryCatch<T, P>(fn, params?, context?): Promise<[Error | null, T | null]>`
340
-
341
- **Signature:**
342
- ```typescript
343
- export default async function tryCatch<T, P>(
344
- func: (values: P) => Promise<T> | T,
345
- params?: P,
346
- context?: Record<string, unknown>,
347
- ): Promise<[Error | null, T | null]>
348
- ```
349
-
350
- **Behavior:**
351
- - Calls `func(params)`, awaits the result.
352
- - On success → returns `[null, response]`.
353
- - On throw → calls `captureException(error, context)` (legacy + multi-adapter fan-out) and returns `[error as Error, null]`.
354
-
355
- **Usage convention:** check the first tuple element; if truthy, an error occurred.
356
-
357
- ```typescript
358
- import { tryCatch } from '@luckystack/core';
359
-
360
- const [error, result] = await tryCatch(async () => db.user.findUnique({ where: { id } }));
361
- if (error) return { status: 'error', errorCode: 'db.failed' };
362
- const user = result;
363
- ```
364
-
365
- ### `sleep(ms: number): Promise<void>`
366
-
367
- Resolves after `setTimeout(ms)`. Used in fanout-yield loops and dev-mode hot reload debouncing.
368
-
369
- ### `getParams({ method, req, res, queryString? }): Promise<Record<string, unknown> | null>`
370
-
371
- **Behavior:**
372
- - `GET` → returns `Object.fromEntries(new URLSearchParams(queryString))`.
373
- - `POST` / `PUT` / `DELETE`:
374
- - Checks the declared `Content-Length` against `projectConfig.http.requestBodyMaxBytes`. If exceeded → writes a 413 JSON error response with `errorCode: 'api.payloadTooLarge'` and resolves `null`.
375
- - Streams `req.on('data')`; aborts with the same 413 if running body size exceeds the cap.
376
- - Parses `application/x-www-form-urlencoded` via `URLSearchParams`.
377
- - Parses `application/json` via `JSON.parse(body || '{}')`. Rejects array / scalar / null bodies — only plain objects pass.
378
- - Unknown / missing content-type → 415 with `errorCode: 'api.unsupportedMediaType'`.
379
- - Malformed JSON / form → 400 with `errorCode: 'api.invalidRequestFormat'`.
380
-
381
- **Returns:** Parsed object, or `null` when the framework already wrote an error response.
382
-
383
- ### `serveAvatar({ routePath, res }): Promise<void>`
384
-
385
- **Behavior:**
386
- 1. Resolves `uploadsFolder = getUploadsDir()`.
387
- 2. Strips the extension from `routePath` → `fileId`.
388
- 3. Rejects when `!fileId` or `fileId` does not match `/^[A-Za-z0-9_-]{1,128}$/` → 404 plaintext. Belt-and-suspenders against path traversal even though `path.basename` already strips separators.
389
- 4. Reads `getAvatarConfig()` and walks `formats` in order:
390
- - For each `{ extension, contentType }`, checks `access(<uploadsFolder>/<fileId>.<extension>)` via `tryCatch`. First existing file wins.
391
- - Writes 200 with `Content-Type` + `Cache-Control` from the avatar config and pipes the file stream to `res`.
392
- 5. If no format matches → logs `avatar: file not found` at debug and writes 404 plaintext.
393
-
394
- ## Environment variables read by core
395
-
396
- | Var | Default | Where |
397
- |---|---|---|
398
- | `NODE_ENV` | `'development'` | `env.ts` (Zod-validated). |
399
- | `SERVER_IP` | `'127.0.0.1'` | `getBindAddress` fallback. |
400
- | `SECURE` | `'false'` | `allowedOrigin` scheme selection. |
401
- | `REDIS_HOST` | `'127.0.0.1'` | Default redis client. |
402
- | `REDIS_PORT` | `'6379'` | Same. |
403
- | `REDIS_USER` | (unset) | Optional ioredis auth. |
404
- | `REDIS_PASSWORD` | (unset) | Optional ioredis auth. |
405
- | `PROJECT_NAME` | `'luckystack'` | `getProjectName` fallback. |
406
- | `LUCKYSTACK_ENV` | (unset) | `resolveEnvKey` first preference. |
407
-
408
- ## Example — minimal project boot
409
-
410
- ```typescript
411
- import {
412
- registerProjectConfig,
413
- registerPrismaClient,
414
- registerRedisClient,
415
- registerLogger,
416
- registerNotifier,
417
- registerRuntimeMapsProvider,
418
- registerBindAddress,
419
- } from '@luckystack/core';
420
- import { PrismaClient } from '@prisma/client';
421
- import Redis from 'ioredis';
422
-
423
- registerProjectConfig({ defaultLanguage: 'en' });
424
- registerPrismaClient(new PrismaClient({ log: ['warn', 'error'] }));
425
- registerRedisClient(new Redis(process.env.REDIS_URL!));
426
- registerLogger({
427
- debug: (...a) => pino.debug(a),
428
- info: (...a) => pino.info(a),
429
- warn: (...a) => pino.warn(a),
430
- error: (...a) => pino.error(a),
431
- });
432
- registerNotifier(toastNotifier);
433
- registerRuntimeMapsProvider(prodRuntimeMaps);
434
- registerBindAddress({ ip: '0.0.0.0', port: 8080 });
435
- ```
436
-
437
- The browser-side method map is registered separately by the project wrapper:
438
-
439
- ```typescript
440
- import { registerApiMethodMap } from '@luckystack/core/client';
441
- import { apiMethodMap } from './apiTypes.generated';
442
-
443
- registerApiMethodMap(apiMethodMap);
444
- export { apiRequest } from '@luckystack/core/client';
445
- ```
446
-
447
- ## Related
448
-
449
- - Function INDEX: `packages/core/CLAUDE.md`
450
- - Architecture: `docs/ARCHITECTURE_PACKAGING.md`, `docs/ARCHITECTURE_ROUTING.md`, `docs/HOSTING.md`, `docs/ARCHITECTURE_EXTENSION_POINTS.md`
451
- - README: `packages/core/README.md`
452
- - Source: `packages/core/src/bindAddress.ts`, `runtimeMapsRegistry.ts`, `apiMethodMapRegistry.ts`, `clients.ts`, `db.ts`, `redis.ts`, `notifier.ts`, `emailRegistry.ts`, `loggerRegistry.ts`, `redactedLogKeys.ts`, `consoleLog.ts`, `localeReloader.ts`, `bootUuid.ts`, `synchronizedEnvHashes.ts`, `tryCatch.ts`, `sleep.ts`, `getParams.ts`, `serveAvatars.ts`
7
+ For the available registrations and environment variables, consult their types and use sites in `../src/`, not a copied list.