@luckystack/core 0.6.7 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +148 -107
- package/CLAUDE.md +245 -245
- package/LICENSE +21 -21
- package/README.md +93 -93
- package/dist/{chunk-WFABOO6J.js → chunk-AFG7KKMS.js} +3 -3
- package/dist/chunk-AFG7KKMS.js.map +1 -0
- package/dist/{chunk-3AWP2YEA.js → chunk-J3UOWYT4.js} +2 -28
- package/dist/chunk-J3UOWYT4.js.map +1 -0
- package/dist/{chunk-LU3IPFTR.js → chunk-LTCDQU5Z.js} +1 -1
- package/dist/chunk-LTCDQU5Z.js.map +1 -0
- package/dist/chunk-R7IRJFVC.js +67 -0
- package/dist/chunk-R7IRJFVC.js.map +1 -0
- package/dist/client.d.ts +3 -2
- package/dist/client.js +4 -4
- package/dist/client.js.map +1 -1
- package/dist/config-DZCzBNwP.d.ts +122 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +31 -0
- package/dist/eslint/index.js.map +1 -1
- package/dist/index.d.ts +21 -149
- package/dist/index.js +66 -70
- package/dist/index.js.map +1 -1
- package/dist/pageRouteValidation-sJJCX-YV.d.ts +270 -0
- package/dist/{projectConfig-XL5VB76A.js → projectConfig-DBLSQ3NY.js} +2 -2
- package/dist/{sentrySetup-4NTWLN2A.js → sentrySetup-MF6JSVWA.js} +3 -3
- package/dist/sentrySetup-MF6JSVWA.js.map +1 -0
- package/dist/{pageRouteValidation-gqktrJci.d.ts → sessionTypes-Dzen_rPK.d.ts} +23 -265
- package/docs/app-bootstrap.md +418 -418
- package/docs/config-registry.md +348 -348
- package/docs/csrf-config.md +88 -88
- package/docs/error-tracker-registry.md +194 -174
- package/docs/hooks.md +298 -298
- package/docs/rate-limit-strategy.md +170 -170
- package/docs/redis-adapter.md +111 -111
- package/docs/session-types.md +180 -180
- package/docs/socket-bootstrap.md +366 -366
- package/package.json +6 -2
- package/dist/chunk-3AWP2YEA.js.map +0 -1
- package/dist/chunk-LU3IPFTR.js.map +0 -1
- package/dist/chunk-WFABOO6J.js.map +0 -1
- /package/dist/{projectConfig-XL5VB76A.js.map → config.js.map} +0 -0
- /package/dist/{sentrySetup-4NTWLN2A.js.map → projectConfig-DBLSQ3NY.js.map} +0 -0
package/docs/app-bootstrap.md
CHANGED
|
@@ -1,418 +1,418 @@
|
|
|
1
|
-
# App Bootstrap
|
|
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-05-20.
|
|
4
|
-
|
|
5
|
-
## Overview
|
|
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 — Bind Address
|
|
22
|
-
|
|
23
|
-
### `registerBindAddress(address: { ip: string; port: number }): void`
|
|
24
|
-
|
|
25
|
-
Stores the listen address used by `createLuckyStackServer`. Framework code (e.g. `checkOrigin` building the same-origin allow-list entry) reads from this registry instead of `SERVER_IP`/`SERVER_PORT` env vars so programmatic `createLuckyStackServer({ ip, port })` boots don't drift.
|
|
26
|
-
|
|
27
|
-
### `getBindAddress(): { ip: string; port: string }`
|
|
28
|
-
|
|
29
|
-
**Behavior (resolution order):**
|
|
30
|
-
1. Registered value (returns `{ ip: registered.ip, port: String(registered.port) }`).
|
|
31
|
-
2. `process.env.SERVER_IP` / `process.env.SERVER_PORT`.
|
|
32
|
-
3. Fallback to `'127.0.0.1'` / `''`.
|
|
33
|
-
|
|
34
|
-
## API Reference — Runtime Maps
|
|
35
|
-
|
|
36
|
-
### Types
|
|
37
|
-
```typescript
|
|
38
|
-
export interface RuntimeApiMapsResult {
|
|
39
|
-
apisObject: Record<string, unknown>;
|
|
40
|
-
functionsObject: Record<string, unknown>;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface RuntimeSyncMapsResult {
|
|
44
|
-
syncObject: Record<string, unknown>;
|
|
45
|
-
functionsObject: Record<string, unknown>;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface RuntimeMapsProvider {
|
|
49
|
-
getRuntimeApiMaps: () => Promise<RuntimeApiMapsResult>;
|
|
50
|
-
getRuntimeSyncMaps: () => Promise<RuntimeSyncMapsResult>;
|
|
51
|
-
}
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
### `registerRuntimeMapsProvider(provider: RuntimeMapsProvider): void`
|
|
55
|
-
|
|
56
|
-
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.
|
|
57
|
-
|
|
58
|
-
### `getRuntimeApiMaps(): Promise<RuntimeApiMapsResult>`
|
|
59
|
-
### `getRuntimeSyncMaps(): Promise<RuntimeSyncMapsResult>`
|
|
60
|
-
|
|
61
|
-
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.
|
|
62
|
-
|
|
63
|
-
### `isRuntimeMapsProviderRegistered(): boolean`
|
|
64
|
-
|
|
65
|
-
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.
|
|
66
|
-
|
|
67
|
-
## API Reference — API Method Map
|
|
68
|
-
|
|
69
|
-
### Types
|
|
70
|
-
```typescript
|
|
71
|
-
export type HttpMethodLiteral = 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
72
|
-
export type ApiMethodMap = Record<string, Record<string, Record<string, HttpMethodLiteral>>>;
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
Shape: `apiMethodMap[pagePath][apiName][version] = 'GET' | ...`.
|
|
76
|
-
|
|
77
|
-
### `registerApiMethodMap(map: ApiMethodMap): void`
|
|
78
|
-
|
|
79
|
-
Stores the generated method map (typically wired by `apiTypes.generated.ts`).
|
|
80
|
-
|
|
81
|
-
### `getRegisteredApiMethod(pagePath, apiName, version): HttpMethodLiteral | undefined`
|
|
82
|
-
|
|
83
|
-
Returns the registered method or `undefined`. `isGetMethod` (in `apiRequest.ts`) falls back to the `inferHttpMethod` prefix heuristic when undefined.
|
|
84
|
-
|
|
85
|
-
### `isApiMethodMapRegistered(): boolean`
|
|
86
|
-
|
|
87
|
-
Detects whether the prefix-heuristic fallback is active.
|
|
88
|
-
|
|
89
|
-
## API Reference — Prisma + Redis Clients
|
|
90
|
-
|
|
91
|
-
### `registerPrismaClient(client: PrismaClient): PrismaClient`
|
|
92
|
-
### `registerRedisClient(client: RedisClient): RedisClient`
|
|
93
|
-
|
|
94
|
-
Stores a custom client and returns it for chaining. Replaces (last-write-wins).
|
|
95
|
-
|
|
96
|
-
### `getPrismaClient(): PrismaClient`
|
|
97
|
-
### `getRedisClient(): RedisClient`
|
|
98
|
-
|
|
99
|
-
**Behavior (resolution order):**
|
|
100
|
-
1. Registered client.
|
|
101
|
-
2. Default resolver (set by `db.ts` / `redis.ts` at module load with the lazy factory).
|
|
102
|
-
3. Throws when neither exists ("No Prisma/Redis client available...").
|
|
103
|
-
|
|
104
|
-
### `isPrismaClientRegistered(): boolean`
|
|
105
|
-
### `isRedisClientRegistered(): boolean`
|
|
106
|
-
|
|
107
|
-
Boot guards.
|
|
108
|
-
|
|
109
|
-
### `setDefaultPrismaResolver(resolver)` / `setDefaultRedisResolver(resolver)`
|
|
110
|
-
|
|
111
|
-
Internal — wired by `db.ts` / `redis.ts`. Splits registration from default construction so this file doesn't depend on `@prisma/client` / `ioredis` types directly.
|
|
112
|
-
|
|
113
|
-
## API Reference — Notifier
|
|
114
|
-
|
|
115
|
-
### Types
|
|
116
|
-
```typescript
|
|
117
|
-
export interface NotifyParam { key: string; value: string | number | boolean }
|
|
118
|
-
export interface NotifyInput { key: string; params?: NotifyParam[] }
|
|
119
|
-
export interface Notifier {
|
|
120
|
-
success: (input: NotifyInput) => void;
|
|
121
|
-
error: (input: NotifyInput) => void;
|
|
122
|
-
info: (input: NotifyInput) => void;
|
|
123
|
-
warning: (input: NotifyInput) => void;
|
|
124
|
-
}
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
### `registerNotifier(notifier: Notifier): void`
|
|
128
|
-
|
|
129
|
-
Replaces the active notifier. Default is a no-op (safe for SSR / tests).
|
|
130
|
-
|
|
131
|
-
### `getNotifier(): Notifier`
|
|
132
|
-
|
|
133
|
-
Returns the active notifier.
|
|
134
|
-
|
|
135
|
-
### `notify: Notifier`
|
|
136
|
-
|
|
137
|
-
A delegating wrapper used by framework hot paths so framework code can do `notify.error({ key: 'api.failed' })` without dereferencing on every call.
|
|
138
|
-
|
|
139
|
-
## API Reference — Email Senders
|
|
140
|
-
|
|
141
|
-
### Types
|
|
142
|
-
```typescript
|
|
143
|
-
export interface EmailMessage {
|
|
144
|
-
to: string | string[];
|
|
145
|
-
subject: string;
|
|
146
|
-
html: string;
|
|
147
|
-
text?: string;
|
|
148
|
-
from?: string;
|
|
149
|
-
replyTo?: string;
|
|
150
|
-
cc?: string | string[];
|
|
151
|
-
bcc?: string | string[];
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export type EmailResult =
|
|
155
|
-
| { ok: true; id: string }
|
|
156
|
-
| { ok: false; reason: string; cause?: unknown };
|
|
157
|
-
|
|
158
|
-
export interface EmailSender {
|
|
159
|
-
name: string;
|
|
160
|
-
send: (message: EmailMessage) => Promise<EmailResult>;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export type EmailSenderRegistry = Record<string, EmailSender>;
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
### `registerEmailSender(sender: EmailSender): void`
|
|
167
|
-
|
|
168
|
-
Legacy single-sender. Mirrors the sender into `registry.default` so `getEmailSenderByName('default')` finds it.
|
|
169
|
-
|
|
170
|
-
### `registerEmailSenders(senders: EmailSenderRegistry): void`
|
|
171
|
-
|
|
172
|
-
Multi-adapter. Replaces the entire registry (last-write-wins). If `senders.default` exists, also updates the legacy slot.
|
|
173
|
-
|
|
174
|
-
Reserved slot conventions:
|
|
175
|
-
- `'default'` — fallback when no specific slot matches.
|
|
176
|
-
- `'transactional'` — login password-reset, account-confirmation, security-critical mail.
|
|
177
|
-
- `'marketing'` — newsletters, bulk sends.
|
|
178
|
-
- `'diagnostics'` — diagnostic endpoints (e.g. playground).
|
|
179
|
-
|
|
180
|
-
Custom slots ('billing', 'support') are allowed and resolved explicitly via `sendEmail({ adapter: 'billing', ... })` in `@luckystack/email`.
|
|
181
|
-
|
|
182
|
-
### `getEmailSender(): EmailSender | null`
|
|
183
|
-
|
|
184
|
-
Returns the legacy sender, falling back to `registry.default`. `null` when neither is set.
|
|
185
|
-
|
|
186
|
-
### `getEmailSenderByName(name: string): EmailSender | null`
|
|
187
|
-
|
|
188
|
-
**Behavior:**
|
|
189
|
-
- Returns `registry[name]` when present.
|
|
190
|
-
- Falls back to the legacy single sender when `name === 'default'`.
|
|
191
|
-
- Returns `null` otherwise.
|
|
192
|
-
|
|
193
|
-
### `listEmailSenderNames(): string[]`
|
|
194
|
-
|
|
195
|
-
Returns the registry keys (excludes the legacy single-sender slot).
|
|
196
|
-
|
|
197
|
-
### `isEmailSenderRegistered(): boolean`
|
|
198
|
-
|
|
199
|
-
True when either the legacy slot or the registry is populated.
|
|
200
|
-
|
|
201
|
-
## API Reference — Logger
|
|
202
|
-
|
|
203
|
-
### Types
|
|
204
|
-
```typescript
|
|
205
|
-
export interface LoggerContext { [key: string]: unknown }
|
|
206
|
-
|
|
207
|
-
export interface Logger {
|
|
208
|
-
debug: (message: string, context?: LoggerContext) => void;
|
|
209
|
-
info: (message: string, context?: LoggerContext) => void;
|
|
210
|
-
warn: (message: string, context?: LoggerContext) => void;
|
|
211
|
-
error: (message: string, error?: unknown, context?: LoggerContext) => void;
|
|
212
|
-
}
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
### `registerLogger(logger: Logger): void`
|
|
216
|
-
|
|
217
|
-
Replaces the active logger and sets the registered flag.
|
|
218
|
-
|
|
219
|
-
### `getLogger(): Logger`
|
|
220
|
-
|
|
221
|
-
Returns the active logger. Default is a thin wrapper over `console.{debug,info,warn,error}` with no color codes — safe for production.
|
|
222
|
-
|
|
223
|
-
### `isLoggerRegistered(): boolean`
|
|
224
|
-
|
|
225
|
-
Boot guard.
|
|
226
|
-
|
|
227
|
-
### `createDevLogger(): Logger`
|
|
228
|
-
|
|
229
|
-
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.
|
|
230
|
-
|
|
231
|
-
### `resetLoggerForTests(): void`
|
|
232
|
-
|
|
233
|
-
Test-only — restores the default logger and clears the registered flag.
|
|
234
|
-
|
|
235
|
-
### `initConsolelog(): void`
|
|
236
|
-
|
|
237
|
-
Monkey-patches `console.log` to:
|
|
238
|
-
- Capture the caller's frame from the stack (skipping `consoleLog.ts` and `loggerRegistry.ts` frames).
|
|
239
|
-
- Strip the platform-specific path prefix and column suffix from the frame, yielding a `<file>:<line>` label.
|
|
240
|
-
- 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.
|
|
241
|
-
- If the first arg is an object, print the label first then the args on a new line; otherwise concatenate with `' -- '` separator.
|
|
242
|
-
|
|
243
|
-
Dev-only. Never install in production — stack-trace per log is expensive.
|
|
244
|
-
|
|
245
|
-
## API Reference — Redacted Log Keys
|
|
246
|
-
|
|
247
|
-
Default seed set: `password`, `confirmpassword`, `token`, `newtoken`, `authorization`, `cookie`, `set-cookie`. Stored lowercased.
|
|
248
|
-
|
|
249
|
-
### `registerRedactedLogKeys(keys: readonly string[]): void`
|
|
250
|
-
|
|
251
|
-
Additive — adds each lowercased key to the set. Idempotent on duplicates.
|
|
252
|
-
|
|
253
|
-
### `getRedactedLogKeys(): readonly string[]`
|
|
254
|
-
|
|
255
|
-
Returns a fresh array of the current set (lowercased).
|
|
256
|
-
|
|
257
|
-
### `isRedactedLogKey(key: string): boolean`
|
|
258
|
-
|
|
259
|
-
Case-insensitive lookup used inside framework log-redaction.
|
|
260
|
-
|
|
261
|
-
### `resetRedactedLogKeysForTests(): void`
|
|
262
|
-
|
|
263
|
-
Test-only — clears and re-seeds with the default set.
|
|
264
|
-
|
|
265
|
-
## API Reference — Locale Reloader
|
|
266
|
-
|
|
267
|
-
### `registerLocaleReloader(reloader: LocaleReloader): LocaleReloader`
|
|
268
|
-
|
|
269
|
-
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.
|
|
270
|
-
|
|
271
|
-
### `getLocaleReloader(): LocaleReloader | null`
|
|
272
|
-
|
|
273
|
-
Returns the registered reloader, or `null` when none was supplied (watcher no-ops in that case).
|
|
274
|
-
|
|
275
|
-
## API Reference — Boot UUID + Env Sync
|
|
276
|
-
|
|
277
|
-
### Constants
|
|
278
|
-
|
|
279
|
-
| Name | Value |
|
|
280
|
-
|---|---|
|
|
281
|
-
| `BOOT_KEY_PREFIX` | `'luckystack:boot:'` |
|
|
282
|
-
| (internal) `DEFAULT_BOOT_KEY_TTL_SECONDS` | `3600` |
|
|
283
|
-
|
|
284
|
-
`BOOT_KEY_PREFIX` is exported as the single source of truth so `@luckystack/router`'s `bootHandshake.ts` cannot drift.
|
|
285
|
-
|
|
286
|
-
### `resolveEnvKey(): string`
|
|
287
|
-
|
|
288
|
-
Returns `process.env.LUCKYSTACK_ENV` ?? `process.env.NODE_ENV` ?? `'development'`.
|
|
289
|
-
|
|
290
|
-
### `writeBootUuid(envKey?: string): Promise<string>`
|
|
291
|
-
|
|
292
|
-
**Behavior:** Generates a fresh `randomUUID()` and writes it to `luckystack:boot:<envKey>` in Redis with `EX <deployConfig.routing.bootKeyTtlSeconds ?? 3600>`. Returns the UUID.
|
|
293
|
-
|
|
294
|
-
### `readBootUuid(envKey?: string): Promise<string | null>`
|
|
295
|
-
|
|
296
|
-
Reads the UUID for the given env key (defaults via `resolveEnvKey()`).
|
|
297
|
-
|
|
298
|
-
### `collectSynchronizedEnvKeys(): string[]`
|
|
299
|
-
|
|
300
|
-
Walks `deployConfig.resources` and collects every `synchronizedEnvKeys[]` entry into a sorted unique list.
|
|
301
|
-
|
|
302
|
-
### `computeSynchronizedEnvHashes(): Record<string, string | null>`
|
|
303
|
-
|
|
304
|
-
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.
|
|
305
|
-
|
|
306
|
-
### `hashSynchronizedValue(value: string): string`
|
|
307
|
-
|
|
308
|
-
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).
|
|
309
|
-
|
|
310
|
-
## API Reference — Utilities
|
|
311
|
-
|
|
312
|
-
### `tryCatch<T, P>(fn, params?, context?): Promise<[Error | null, T | null]>`
|
|
313
|
-
|
|
314
|
-
**Signature:**
|
|
315
|
-
```typescript
|
|
316
|
-
export default async function tryCatch<T, P>(
|
|
317
|
-
func: (values: P) => Promise<T> | T,
|
|
318
|
-
params?: P,
|
|
319
|
-
context?: Record<string, unknown>,
|
|
320
|
-
): Promise<[Error | null, T | null]>
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
**Behavior:**
|
|
324
|
-
- Calls `func(params)`, awaits the result.
|
|
325
|
-
- On success → returns `[null, response]`.
|
|
326
|
-
- On throw → calls `captureException(error, context)` (legacy + multi-adapter fan-out) and returns `[error as Error, null]`.
|
|
327
|
-
|
|
328
|
-
**Usage convention:** check the first tuple element; if truthy, an error occurred.
|
|
329
|
-
|
|
330
|
-
```typescript
|
|
331
|
-
import { tryCatch } from '@luckystack/core';
|
|
332
|
-
|
|
333
|
-
const [error, result] = await tryCatch(async () => db.user.findUnique({ where: { id } }));
|
|
334
|
-
if (error) return { status: 'error', errorCode: 'db.failed' };
|
|
335
|
-
const user = result;
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
### `sleep(ms: number): Promise<void>`
|
|
339
|
-
|
|
340
|
-
Resolves after `setTimeout(ms)`. Used in fanout-yield loops and dev-mode hot reload debouncing.
|
|
341
|
-
|
|
342
|
-
### `getParams({ method, req, res, queryString? }): Promise<Record<string, unknown> | null>`
|
|
343
|
-
|
|
344
|
-
**Behavior:**
|
|
345
|
-
- `GET` → returns `Object.fromEntries(new URLSearchParams(queryString))`.
|
|
346
|
-
- `POST` / `PUT` / `DELETE`:
|
|
347
|
-
- Checks the declared `Content-Length` against `projectConfig.http.requestBodyMaxBytes`. If exceeded → writes a 413 JSON error response with `errorCode: 'api.payloadTooLarge'` and resolves `null`.
|
|
348
|
-
- Streams `req.on('data')`; aborts with the same 413 if running body size exceeds the cap.
|
|
349
|
-
- Parses `application/x-www-form-urlencoded` via `URLSearchParams`.
|
|
350
|
-
- Parses `application/json` via `JSON.parse(body || '{}')`. Rejects array / scalar / null bodies — only plain objects pass.
|
|
351
|
-
- Unknown / missing content-type → 415 with `errorCode: 'api.unsupportedMediaType'`.
|
|
352
|
-
- Malformed JSON / form → 400 with `errorCode: 'api.invalidRequestFormat'`.
|
|
353
|
-
|
|
354
|
-
**Returns:** Parsed object, or `null` when the framework already wrote an error response.
|
|
355
|
-
|
|
356
|
-
### `serveAvatar({ routePath, res }): Promise<void>`
|
|
357
|
-
|
|
358
|
-
**Behavior:**
|
|
359
|
-
1. Resolves `uploadsFolder = getUploadsDir()`.
|
|
360
|
-
2. Strips the extension from `routePath` → `fileId`.
|
|
361
|
-
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.
|
|
362
|
-
4. Reads `getAvatarConfig()` and walks `formats` in order:
|
|
363
|
-
- For each `{ extension, contentType }`, checks `access(<uploadsFolder>/<fileId>.<extension>)` via `tryCatch`. First existing file wins.
|
|
364
|
-
- Writes 200 with `Content-Type` + `Cache-Control` from the avatar config and pipes the file stream to `res`.
|
|
365
|
-
5. If no format matches → logs `avatar: file not found` at debug and writes 404 plaintext.
|
|
366
|
-
|
|
367
|
-
## Environment variables read by core
|
|
368
|
-
|
|
369
|
-
| Var | Default | Where |
|
|
370
|
-
|---|---|---|
|
|
371
|
-
| `NODE_ENV` | `'development'` | `env.ts` (Zod-validated). |
|
|
372
|
-
| `SERVER_IP` | `'127.0.0.1'` | `getBindAddress` fallback. |
|
|
373
|
-
| `SERVER_PORT` | `'80'` | `getBindAddress` fallback. |
|
|
374
|
-
| `SECURE` | `'false'` | `allowedOrigin` scheme selection. |
|
|
375
|
-
| `REDIS_HOST` | `'127.0.0.1'` | Default redis client. |
|
|
376
|
-
| `REDIS_PORT` | `'6379'` | Same. |
|
|
377
|
-
| `REDIS_USER` | (unset) | Optional ioredis auth. |
|
|
378
|
-
| `REDIS_PASSWORD` | (unset) | Optional ioredis auth. |
|
|
379
|
-
| `PROJECT_NAME` | `'luckystack'` | `getProjectName` fallback. |
|
|
380
|
-
| `LUCKYSTACK_ENV` | (unset) | `resolveEnvKey` first preference. |
|
|
381
|
-
|
|
382
|
-
## Example — minimal project boot
|
|
383
|
-
|
|
384
|
-
```typescript
|
|
385
|
-
import {
|
|
386
|
-
registerProjectConfig,
|
|
387
|
-
registerPrismaClient,
|
|
388
|
-
registerRedisClient,
|
|
389
|
-
registerLogger,
|
|
390
|
-
registerNotifier,
|
|
391
|
-
registerRuntimeMapsProvider,
|
|
392
|
-
registerApiMethodMap,
|
|
393
|
-
registerBindAddress,
|
|
394
|
-
} from '@luckystack/core';
|
|
395
|
-
import { PrismaClient } from '@prisma/client';
|
|
396
|
-
import Redis from 'ioredis';
|
|
397
|
-
|
|
398
|
-
registerProjectConfig({ defaultLanguage: 'en' });
|
|
399
|
-
registerPrismaClient(new PrismaClient({ log: ['warn', 'error'] }));
|
|
400
|
-
registerRedisClient(new Redis(process.env.REDIS_URL!));
|
|
401
|
-
registerLogger({
|
|
402
|
-
debug: (...a) => pino.debug(a),
|
|
403
|
-
info: (...a) => pino.info(a),
|
|
404
|
-
warn: (...a) => pino.warn(a),
|
|
405
|
-
error: (...a) => pino.error(a),
|
|
406
|
-
});
|
|
407
|
-
registerNotifier(toastNotifier);
|
|
408
|
-
registerRuntimeMapsProvider(prodRuntimeMaps);
|
|
409
|
-
registerApiMethodMap(generatedApiMethodMap);
|
|
410
|
-
registerBindAddress({ ip: '0.0.0.0', port: 8080 });
|
|
411
|
-
```
|
|
412
|
-
|
|
413
|
-
## Related
|
|
414
|
-
|
|
415
|
-
- Function INDEX: `packages/core/CLAUDE.md`
|
|
416
|
-
- Architecture: `docs/ARCHITECTURE_PACKAGING.md`, `docs/ARCHITECTURE_ROUTING.md`, `docs/HOSTING.md`, `docs/ARCHITECTURE_EXTENSION_POINTS.md`
|
|
417
|
-
- README: `packages/core/README.md`
|
|
418
|
-
- 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`
|
|
1
|
+
# App Bootstrap
|
|
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-05-20.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
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 — Bind Address
|
|
22
|
+
|
|
23
|
+
### `registerBindAddress(address: { ip: string; port: number }): void`
|
|
24
|
+
|
|
25
|
+
Stores the listen address used by `createLuckyStackServer`. Framework code (e.g. `checkOrigin` building the same-origin allow-list entry) reads from this registry instead of `SERVER_IP`/`SERVER_PORT` env vars so programmatic `createLuckyStackServer({ ip, port })` boots don't drift.
|
|
26
|
+
|
|
27
|
+
### `getBindAddress(): { ip: string; port: string }`
|
|
28
|
+
|
|
29
|
+
**Behavior (resolution order):**
|
|
30
|
+
1. Registered value (returns `{ ip: registered.ip, port: String(registered.port) }`).
|
|
31
|
+
2. `process.env.SERVER_IP` / `process.env.SERVER_PORT`.
|
|
32
|
+
3. Fallback to `'127.0.0.1'` / `''`.
|
|
33
|
+
|
|
34
|
+
## API Reference — Runtime Maps
|
|
35
|
+
|
|
36
|
+
### Types
|
|
37
|
+
```typescript
|
|
38
|
+
export interface RuntimeApiMapsResult {
|
|
39
|
+
apisObject: Record<string, unknown>;
|
|
40
|
+
functionsObject: Record<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface RuntimeSyncMapsResult {
|
|
44
|
+
syncObject: Record<string, unknown>;
|
|
45
|
+
functionsObject: Record<string, unknown>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface RuntimeMapsProvider {
|
|
49
|
+
getRuntimeApiMaps: () => Promise<RuntimeApiMapsResult>;
|
|
50
|
+
getRuntimeSyncMaps: () => Promise<RuntimeSyncMapsResult>;
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### `registerRuntimeMapsProvider(provider: RuntimeMapsProvider): void`
|
|
55
|
+
|
|
56
|
+
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.
|
|
57
|
+
|
|
58
|
+
### `getRuntimeApiMaps(): Promise<RuntimeApiMapsResult>`
|
|
59
|
+
### `getRuntimeSyncMaps(): Promise<RuntimeSyncMapsResult>`
|
|
60
|
+
|
|
61
|
+
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.
|
|
62
|
+
|
|
63
|
+
### `isRuntimeMapsProviderRegistered(): boolean`
|
|
64
|
+
|
|
65
|
+
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.
|
|
66
|
+
|
|
67
|
+
## API Reference — API Method Map
|
|
68
|
+
|
|
69
|
+
### Types
|
|
70
|
+
```typescript
|
|
71
|
+
export type HttpMethodLiteral = 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
72
|
+
export type ApiMethodMap = Record<string, Record<string, Record<string, HttpMethodLiteral>>>;
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Shape: `apiMethodMap[pagePath][apiName][version] = 'GET' | ...`.
|
|
76
|
+
|
|
77
|
+
### `registerApiMethodMap(map: ApiMethodMap): void`
|
|
78
|
+
|
|
79
|
+
Stores the generated method map (typically wired by `apiTypes.generated.ts`).
|
|
80
|
+
|
|
81
|
+
### `getRegisteredApiMethod(pagePath, apiName, version): HttpMethodLiteral | undefined`
|
|
82
|
+
|
|
83
|
+
Returns the registered method or `undefined`. `isGetMethod` (in `apiRequest.ts`) falls back to the `inferHttpMethod` prefix heuristic when undefined.
|
|
84
|
+
|
|
85
|
+
### `isApiMethodMapRegistered(): boolean`
|
|
86
|
+
|
|
87
|
+
Detects whether the prefix-heuristic fallback is active.
|
|
88
|
+
|
|
89
|
+
## API Reference — Prisma + Redis Clients
|
|
90
|
+
|
|
91
|
+
### `registerPrismaClient(client: PrismaClient): PrismaClient`
|
|
92
|
+
### `registerRedisClient(client: RedisClient): RedisClient`
|
|
93
|
+
|
|
94
|
+
Stores a custom client and returns it for chaining. Replaces (last-write-wins).
|
|
95
|
+
|
|
96
|
+
### `getPrismaClient(): PrismaClient`
|
|
97
|
+
### `getRedisClient(): RedisClient`
|
|
98
|
+
|
|
99
|
+
**Behavior (resolution order):**
|
|
100
|
+
1. Registered client.
|
|
101
|
+
2. Default resolver (set by `db.ts` / `redis.ts` at module load with the lazy factory).
|
|
102
|
+
3. Throws when neither exists ("No Prisma/Redis client available...").
|
|
103
|
+
|
|
104
|
+
### `isPrismaClientRegistered(): boolean`
|
|
105
|
+
### `isRedisClientRegistered(): boolean`
|
|
106
|
+
|
|
107
|
+
Boot guards.
|
|
108
|
+
|
|
109
|
+
### `setDefaultPrismaResolver(resolver)` / `setDefaultRedisResolver(resolver)`
|
|
110
|
+
|
|
111
|
+
Internal — wired by `db.ts` / `redis.ts`. Splits registration from default construction so this file doesn't depend on `@prisma/client` / `ioredis` types directly.
|
|
112
|
+
|
|
113
|
+
## API Reference — Notifier
|
|
114
|
+
|
|
115
|
+
### Types
|
|
116
|
+
```typescript
|
|
117
|
+
export interface NotifyParam { key: string; value: string | number | boolean }
|
|
118
|
+
export interface NotifyInput { key: string; params?: NotifyParam[] }
|
|
119
|
+
export interface Notifier {
|
|
120
|
+
success: (input: NotifyInput) => void;
|
|
121
|
+
error: (input: NotifyInput) => void;
|
|
122
|
+
info: (input: NotifyInput) => void;
|
|
123
|
+
warning: (input: NotifyInput) => void;
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### `registerNotifier(notifier: Notifier): void`
|
|
128
|
+
|
|
129
|
+
Replaces the active notifier. Default is a no-op (safe for SSR / tests).
|
|
130
|
+
|
|
131
|
+
### `getNotifier(): Notifier`
|
|
132
|
+
|
|
133
|
+
Returns the active notifier.
|
|
134
|
+
|
|
135
|
+
### `notify: Notifier`
|
|
136
|
+
|
|
137
|
+
A delegating wrapper used by framework hot paths so framework code can do `notify.error({ key: 'api.failed' })` without dereferencing on every call.
|
|
138
|
+
|
|
139
|
+
## API Reference — Email Senders
|
|
140
|
+
|
|
141
|
+
### Types
|
|
142
|
+
```typescript
|
|
143
|
+
export interface EmailMessage {
|
|
144
|
+
to: string | string[];
|
|
145
|
+
subject: string;
|
|
146
|
+
html: string;
|
|
147
|
+
text?: string;
|
|
148
|
+
from?: string;
|
|
149
|
+
replyTo?: string;
|
|
150
|
+
cc?: string | string[];
|
|
151
|
+
bcc?: string | string[];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export type EmailResult =
|
|
155
|
+
| { ok: true; id: string }
|
|
156
|
+
| { ok: false; reason: string; cause?: unknown };
|
|
157
|
+
|
|
158
|
+
export interface EmailSender {
|
|
159
|
+
name: string;
|
|
160
|
+
send: (message: EmailMessage) => Promise<EmailResult>;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export type EmailSenderRegistry = Record<string, EmailSender>;
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### `registerEmailSender(sender: EmailSender): void`
|
|
167
|
+
|
|
168
|
+
Legacy single-sender. Mirrors the sender into `registry.default` so `getEmailSenderByName('default')` finds it.
|
|
169
|
+
|
|
170
|
+
### `registerEmailSenders(senders: EmailSenderRegistry): void`
|
|
171
|
+
|
|
172
|
+
Multi-adapter. Replaces the entire registry (last-write-wins). If `senders.default` exists, also updates the legacy slot.
|
|
173
|
+
|
|
174
|
+
Reserved slot conventions:
|
|
175
|
+
- `'default'` — fallback when no specific slot matches.
|
|
176
|
+
- `'transactional'` — login password-reset, account-confirmation, security-critical mail.
|
|
177
|
+
- `'marketing'` — newsletters, bulk sends.
|
|
178
|
+
- `'diagnostics'` — diagnostic endpoints (e.g. playground).
|
|
179
|
+
|
|
180
|
+
Custom slots ('billing', 'support') are allowed and resolved explicitly via `sendEmail({ adapter: 'billing', ... })` in `@luckystack/email`.
|
|
181
|
+
|
|
182
|
+
### `getEmailSender(): EmailSender | null`
|
|
183
|
+
|
|
184
|
+
Returns the legacy sender, falling back to `registry.default`. `null` when neither is set.
|
|
185
|
+
|
|
186
|
+
### `getEmailSenderByName(name: string): EmailSender | null`
|
|
187
|
+
|
|
188
|
+
**Behavior:**
|
|
189
|
+
- Returns `registry[name]` when present.
|
|
190
|
+
- Falls back to the legacy single sender when `name === 'default'`.
|
|
191
|
+
- Returns `null` otherwise.
|
|
192
|
+
|
|
193
|
+
### `listEmailSenderNames(): string[]`
|
|
194
|
+
|
|
195
|
+
Returns the registry keys (excludes the legacy single-sender slot).
|
|
196
|
+
|
|
197
|
+
### `isEmailSenderRegistered(): boolean`
|
|
198
|
+
|
|
199
|
+
True when either the legacy slot or the registry is populated.
|
|
200
|
+
|
|
201
|
+
## API Reference — Logger
|
|
202
|
+
|
|
203
|
+
### Types
|
|
204
|
+
```typescript
|
|
205
|
+
export interface LoggerContext { [key: string]: unknown }
|
|
206
|
+
|
|
207
|
+
export interface Logger {
|
|
208
|
+
debug: (message: string, context?: LoggerContext) => void;
|
|
209
|
+
info: (message: string, context?: LoggerContext) => void;
|
|
210
|
+
warn: (message: string, context?: LoggerContext) => void;
|
|
211
|
+
error: (message: string, error?: unknown, context?: LoggerContext) => void;
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### `registerLogger(logger: Logger): void`
|
|
216
|
+
|
|
217
|
+
Replaces the active logger and sets the registered flag.
|
|
218
|
+
|
|
219
|
+
### `getLogger(): Logger`
|
|
220
|
+
|
|
221
|
+
Returns the active logger. Default is a thin wrapper over `console.{debug,info,warn,error}` with no color codes — safe for production.
|
|
222
|
+
|
|
223
|
+
### `isLoggerRegistered(): boolean`
|
|
224
|
+
|
|
225
|
+
Boot guard.
|
|
226
|
+
|
|
227
|
+
### `createDevLogger(): Logger`
|
|
228
|
+
|
|
229
|
+
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.
|
|
230
|
+
|
|
231
|
+
### `resetLoggerForTests(): void`
|
|
232
|
+
|
|
233
|
+
Test-only — restores the default logger and clears the registered flag.
|
|
234
|
+
|
|
235
|
+
### `initConsolelog(): void`
|
|
236
|
+
|
|
237
|
+
Monkey-patches `console.log` to:
|
|
238
|
+
- Capture the caller's frame from the stack (skipping `consoleLog.ts` and `loggerRegistry.ts` frames).
|
|
239
|
+
- Strip the platform-specific path prefix and column suffix from the frame, yielding a `<file>:<line>` label.
|
|
240
|
+
- 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.
|
|
241
|
+
- If the first arg is an object, print the label first then the args on a new line; otherwise concatenate with `' -- '` separator.
|
|
242
|
+
|
|
243
|
+
Dev-only. Never install in production — stack-trace per log is expensive.
|
|
244
|
+
|
|
245
|
+
## API Reference — Redacted Log Keys
|
|
246
|
+
|
|
247
|
+
Default seed set: `password`, `confirmpassword`, `token`, `newtoken`, `authorization`, `cookie`, `set-cookie`. Stored lowercased.
|
|
248
|
+
|
|
249
|
+
### `registerRedactedLogKeys(keys: readonly string[]): void`
|
|
250
|
+
|
|
251
|
+
Additive — adds each lowercased key to the set. Idempotent on duplicates.
|
|
252
|
+
|
|
253
|
+
### `getRedactedLogKeys(): readonly string[]`
|
|
254
|
+
|
|
255
|
+
Returns a fresh array of the current set (lowercased).
|
|
256
|
+
|
|
257
|
+
### `isRedactedLogKey(key: string): boolean`
|
|
258
|
+
|
|
259
|
+
Case-insensitive lookup used inside framework log-redaction.
|
|
260
|
+
|
|
261
|
+
### `resetRedactedLogKeysForTests(): void`
|
|
262
|
+
|
|
263
|
+
Test-only — clears and re-seeds with the default set.
|
|
264
|
+
|
|
265
|
+
## API Reference — Locale Reloader
|
|
266
|
+
|
|
267
|
+
### `registerLocaleReloader(reloader: LocaleReloader): LocaleReloader`
|
|
268
|
+
|
|
269
|
+
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.
|
|
270
|
+
|
|
271
|
+
### `getLocaleReloader(): LocaleReloader | null`
|
|
272
|
+
|
|
273
|
+
Returns the registered reloader, or `null` when none was supplied (watcher no-ops in that case).
|
|
274
|
+
|
|
275
|
+
## API Reference — Boot UUID + Env Sync
|
|
276
|
+
|
|
277
|
+
### Constants
|
|
278
|
+
|
|
279
|
+
| Name | Value |
|
|
280
|
+
|---|---|
|
|
281
|
+
| `BOOT_KEY_PREFIX` | `'luckystack:boot:'` |
|
|
282
|
+
| (internal) `DEFAULT_BOOT_KEY_TTL_SECONDS` | `3600` |
|
|
283
|
+
|
|
284
|
+
`BOOT_KEY_PREFIX` is exported as the single source of truth so `@luckystack/router`'s `bootHandshake.ts` cannot drift.
|
|
285
|
+
|
|
286
|
+
### `resolveEnvKey(): string`
|
|
287
|
+
|
|
288
|
+
Returns `process.env.LUCKYSTACK_ENV` ?? `process.env.NODE_ENV` ?? `'development'`.
|
|
289
|
+
|
|
290
|
+
### `writeBootUuid(envKey?: string): Promise<string>`
|
|
291
|
+
|
|
292
|
+
**Behavior:** Generates a fresh `randomUUID()` and writes it to `luckystack:boot:<envKey>` in Redis with `EX <deployConfig.routing.bootKeyTtlSeconds ?? 3600>`. Returns the UUID.
|
|
293
|
+
|
|
294
|
+
### `readBootUuid(envKey?: string): Promise<string | null>`
|
|
295
|
+
|
|
296
|
+
Reads the UUID for the given env key (defaults via `resolveEnvKey()`).
|
|
297
|
+
|
|
298
|
+
### `collectSynchronizedEnvKeys(): string[]`
|
|
299
|
+
|
|
300
|
+
Walks `deployConfig.resources` and collects every `synchronizedEnvKeys[]` entry into a sorted unique list.
|
|
301
|
+
|
|
302
|
+
### `computeSynchronizedEnvHashes(): Record<string, string | null>`
|
|
303
|
+
|
|
304
|
+
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.
|
|
305
|
+
|
|
306
|
+
### `hashSynchronizedValue(value: string): string`
|
|
307
|
+
|
|
308
|
+
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).
|
|
309
|
+
|
|
310
|
+
## API Reference — Utilities
|
|
311
|
+
|
|
312
|
+
### `tryCatch<T, P>(fn, params?, context?): Promise<[Error | null, T | null]>`
|
|
313
|
+
|
|
314
|
+
**Signature:**
|
|
315
|
+
```typescript
|
|
316
|
+
export default async function tryCatch<T, P>(
|
|
317
|
+
func: (values: P) => Promise<T> | T,
|
|
318
|
+
params?: P,
|
|
319
|
+
context?: Record<string, unknown>,
|
|
320
|
+
): Promise<[Error | null, T | null]>
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
**Behavior:**
|
|
324
|
+
- Calls `func(params)`, awaits the result.
|
|
325
|
+
- On success → returns `[null, response]`.
|
|
326
|
+
- On throw → calls `captureException(error, context)` (legacy + multi-adapter fan-out) and returns `[error as Error, null]`.
|
|
327
|
+
|
|
328
|
+
**Usage convention:** check the first tuple element; if truthy, an error occurred.
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
import { tryCatch } from '@luckystack/core';
|
|
332
|
+
|
|
333
|
+
const [error, result] = await tryCatch(async () => db.user.findUnique({ where: { id } }));
|
|
334
|
+
if (error) return { status: 'error', errorCode: 'db.failed' };
|
|
335
|
+
const user = result;
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### `sleep(ms: number): Promise<void>`
|
|
339
|
+
|
|
340
|
+
Resolves after `setTimeout(ms)`. Used in fanout-yield loops and dev-mode hot reload debouncing.
|
|
341
|
+
|
|
342
|
+
### `getParams({ method, req, res, queryString? }): Promise<Record<string, unknown> | null>`
|
|
343
|
+
|
|
344
|
+
**Behavior:**
|
|
345
|
+
- `GET` → returns `Object.fromEntries(new URLSearchParams(queryString))`.
|
|
346
|
+
- `POST` / `PUT` / `DELETE`:
|
|
347
|
+
- Checks the declared `Content-Length` against `projectConfig.http.requestBodyMaxBytes`. If exceeded → writes a 413 JSON error response with `errorCode: 'api.payloadTooLarge'` and resolves `null`.
|
|
348
|
+
- Streams `req.on('data')`; aborts with the same 413 if running body size exceeds the cap.
|
|
349
|
+
- Parses `application/x-www-form-urlencoded` via `URLSearchParams`.
|
|
350
|
+
- Parses `application/json` via `JSON.parse(body || '{}')`. Rejects array / scalar / null bodies — only plain objects pass.
|
|
351
|
+
- Unknown / missing content-type → 415 with `errorCode: 'api.unsupportedMediaType'`.
|
|
352
|
+
- Malformed JSON / form → 400 with `errorCode: 'api.invalidRequestFormat'`.
|
|
353
|
+
|
|
354
|
+
**Returns:** Parsed object, or `null` when the framework already wrote an error response.
|
|
355
|
+
|
|
356
|
+
### `serveAvatar({ routePath, res }): Promise<void>`
|
|
357
|
+
|
|
358
|
+
**Behavior:**
|
|
359
|
+
1. Resolves `uploadsFolder = getUploadsDir()`.
|
|
360
|
+
2. Strips the extension from `routePath` → `fileId`.
|
|
361
|
+
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.
|
|
362
|
+
4. Reads `getAvatarConfig()` and walks `formats` in order:
|
|
363
|
+
- For each `{ extension, contentType }`, checks `access(<uploadsFolder>/<fileId>.<extension>)` via `tryCatch`. First existing file wins.
|
|
364
|
+
- Writes 200 with `Content-Type` + `Cache-Control` from the avatar config and pipes the file stream to `res`.
|
|
365
|
+
5. If no format matches → logs `avatar: file not found` at debug and writes 404 plaintext.
|
|
366
|
+
|
|
367
|
+
## Environment variables read by core
|
|
368
|
+
|
|
369
|
+
| Var | Default | Where |
|
|
370
|
+
|---|---|---|
|
|
371
|
+
| `NODE_ENV` | `'development'` | `env.ts` (Zod-validated). |
|
|
372
|
+
| `SERVER_IP` | `'127.0.0.1'` | `getBindAddress` fallback. |
|
|
373
|
+
| `SERVER_PORT` | `'80'` | `getBindAddress` fallback. |
|
|
374
|
+
| `SECURE` | `'false'` | `allowedOrigin` scheme selection. |
|
|
375
|
+
| `REDIS_HOST` | `'127.0.0.1'` | Default redis client. |
|
|
376
|
+
| `REDIS_PORT` | `'6379'` | Same. |
|
|
377
|
+
| `REDIS_USER` | (unset) | Optional ioredis auth. |
|
|
378
|
+
| `REDIS_PASSWORD` | (unset) | Optional ioredis auth. |
|
|
379
|
+
| `PROJECT_NAME` | `'luckystack'` | `getProjectName` fallback. |
|
|
380
|
+
| `LUCKYSTACK_ENV` | (unset) | `resolveEnvKey` first preference. |
|
|
381
|
+
|
|
382
|
+
## Example — minimal project boot
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
import {
|
|
386
|
+
registerProjectConfig,
|
|
387
|
+
registerPrismaClient,
|
|
388
|
+
registerRedisClient,
|
|
389
|
+
registerLogger,
|
|
390
|
+
registerNotifier,
|
|
391
|
+
registerRuntimeMapsProvider,
|
|
392
|
+
registerApiMethodMap,
|
|
393
|
+
registerBindAddress,
|
|
394
|
+
} from '@luckystack/core';
|
|
395
|
+
import { PrismaClient } from '@prisma/client';
|
|
396
|
+
import Redis from 'ioredis';
|
|
397
|
+
|
|
398
|
+
registerProjectConfig({ defaultLanguage: 'en' });
|
|
399
|
+
registerPrismaClient(new PrismaClient({ log: ['warn', 'error'] }));
|
|
400
|
+
registerRedisClient(new Redis(process.env.REDIS_URL!));
|
|
401
|
+
registerLogger({
|
|
402
|
+
debug: (...a) => pino.debug(a),
|
|
403
|
+
info: (...a) => pino.info(a),
|
|
404
|
+
warn: (...a) => pino.warn(a),
|
|
405
|
+
error: (...a) => pino.error(a),
|
|
406
|
+
});
|
|
407
|
+
registerNotifier(toastNotifier);
|
|
408
|
+
registerRuntimeMapsProvider(prodRuntimeMaps);
|
|
409
|
+
registerApiMethodMap(generatedApiMethodMap);
|
|
410
|
+
registerBindAddress({ ip: '0.0.0.0', port: 8080 });
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
## Related
|
|
414
|
+
|
|
415
|
+
- Function INDEX: `packages/core/CLAUDE.md`
|
|
416
|
+
- Architecture: `docs/ARCHITECTURE_PACKAGING.md`, `docs/ARCHITECTURE_ROUTING.md`, `docs/HOSTING.md`, `docs/ARCHITECTURE_EXTENSION_POINTS.md`
|
|
417
|
+
- README: `packages/core/README.md`
|
|
418
|
+
- 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`
|