@luckystack/core 0.1.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@luckystack/core` are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0]
11
+
12
+ ### Added
13
+
14
+ - Initial public release as part of the LuckyStack package split.
package/CLAUDE.md ADDED
@@ -0,0 +1,210 @@
1
+ # @luckystack/core
2
+
3
+ > AI summary + function INDEX. For deep specs see `docs/` next to this file.
4
+
5
+ ## What this package does
6
+
7
+ Foundation package for LuckyStack. Owns the socket-first transport contracts (`apiMessage`, `syncMessage`, io-instance slot), the registries that every other framework package reads from (project config, deploy/services config, runtime maps, prisma/redis clients, notifier, email senders, error trackers, logger), and the cross-cutting primitives (rate limiter, CORS check, validateRequest, hook bus, response normalizer, offline queue, CSRF, cookies). Every other `@luckystack/*` package depends on this one and never reaches around it.
8
+
9
+ ## When to USE
10
+
11
+ - Reading the active runtime config (`getProjectConfig()`, `getProjectName()`, `getBindAddress()`).
12
+ - Registering DI slots from the project's boot entry (`registerProjectConfig`, `registerPrismaClient`, `registerRedisClient`, `registerLogger`, `registerNotifier`, `registerEmailSender(s)`, `registerErrorTracker(s)`, `registerRuntimeMapsProvider`, `registerApiMethodMap`, `registerAvatarConfig`, `registerBindAddress`, `registerRateLimitStrategy`).
13
+ - Subscribing to lifecycle/transport events via `registerHook(...)` / `registerSyncHook(...)`.
14
+ - Calling the cross-cutting primitives directly when a feature package would be overkill (`tryCatch`, `notify`, `checkRateLimit`, `allowedOrigin`, `validateRequest`, `extractTokenFromSocket`, `extractTokenFromRequest`, the offline queue API).
15
+ - Reading shared transport types (`BaseSessionLayout`, `AuthProps`, `apiMessage`, `syncMessage`, `SOCKETSTATUS`).
16
+
17
+ ## When NOT to suggest yet
18
+
19
+ - Building a feature endpoint: import `@luckystack/api` / `@luckystack/sync` instead — they wrap core's hooks and validation in the file-based routing contract.
20
+ - Sending email: use `@luckystack/email`'s `sendEmail(...)` (it routes through `getEmailSenderByName`).
21
+ - Booting an HTTP server: use `createLuckyStackServer` from `@luckystack/server` — it wires `registerBindAddress`, `setIoInstance`, `attachSocketRedisAdapter`, security headers, etc. for you.
22
+ - Replacing Sentry: register an adapter via `@luckystack/error-tracking` (which calls `registerErrorTracker(s)` here). Do not call `initSharedSentry` in new code.
23
+ - Anything React-coupled: import from `@luckystack/core/client` (browser-safe subpath) or from the consuming feature package's `/client` entry. The server barrel intentionally does NOT export `apiRequest`.
24
+ - Defining project session types: extend `BaseSessionLayout` in your project's own `config.ts` — do not edit the core type.
25
+
26
+ ## Function Index
27
+
28
+ | Function / Export | 1-line | Deep doc |
29
+ | --- | --- | --- |
30
+ | `registerProjectConfig(config: ProjectConfigInput): void` | Deep-merge installer config over `DEFAULT_PROJECT_CONFIG`; call once at boot before any framework code runs. | -> docs/config-registry.md |
31
+ | `getProjectConfig(): ProjectConfig` | Read the active config at call time (never at module load). | -> docs/config-registry.md |
32
+ | `getProjectName(): string` | Resolve the Redis-prefix project name (config -> `PROJECT_NAME` env -> `'luckystack'`). | -> docs/config-registry.md |
33
+ | `isProjectConfigRegistered(): boolean` | Boot-time guard for code paths that must run after registration. | -> docs/config-registry.md |
34
+ | `DEFAULT_PROJECT_CONFIG: ProjectConfig` | Built-in defaults exported so consumers can spread + override structurally. | -> docs/config-registry.md |
35
+ | `registerDeployConfig(config: DeployConfigShape): DeployConfigShape` | Register the project's deploy topology (resources, environments, routing, development). | -> docs/config-registry.md |
36
+ | `getDeployConfig(): DeployConfigShape` | Read deploy topology (defaults to `{ resources: {} }`). | -> docs/config-registry.md |
37
+ | `isDeployConfigRegistered(): boolean` | Guard for router/boot probes that need a registered deploy config. | -> docs/config-registry.md |
38
+ | `registerServicesConfig(config: ServicesConfigShape): ServicesConfigShape` | Register services + presets topology (consumed by router/presetLoader). | -> docs/config-registry.md |
39
+ | `getServicesConfig(): ServicesConfigShape` | Throws when no services config has been registered. | -> docs/config-registry.md |
40
+ | `isServicesConfigRegistered(): boolean` | Non-throwing guard. | -> docs/config-registry.md |
41
+ | `registerAvatarConfig(config: AvatarConfigInput): void` | Override avatar disk format(s) + Cache-Control header. | -> docs/config-registry.md |
42
+ | `getAvatarConfig(): AvatarConfig` | Read active avatar config. | -> docs/config-registry.md |
43
+ | `DEFAULT_AVATAR_CONFIG: AvatarConfig` | Default formats `[{ extension: 'webp', contentType: 'image/webp' }]` + 24h cache. | -> docs/config-registry.md |
44
+ | `registerBindAddress(address: { ip: string; port: number }): void` | `createLuckyStackServer` writes the resolved listen address here at boot. | -> docs/app-bootstrap.md |
45
+ | `getBindAddress(): { ip: string; port: string }` | Resolve at call time (registry -> `SERVER_IP`/`SERVER_PORT` -> `'127.0.0.1'`/`''`). | -> docs/app-bootstrap.md |
46
+ | `registerRuntimeMapsProvider(provider: RuntimeMapsProvider): void` | DI slot for generated api/sync maps; called by project `server/prod/runtimeMaps.ts`. | -> docs/app-bootstrap.md |
47
+ | `getRuntimeApiMaps(): Promise<RuntimeApiMapsResult>` | Async accessor for `{ apisObject, functionsObject }`. | -> docs/app-bootstrap.md |
48
+ | `getRuntimeSyncMaps(): Promise<RuntimeSyncMapsResult>` | Async accessor for `{ syncObject, functionsObject }`. | -> docs/app-bootstrap.md |
49
+ | `isRuntimeMapsProviderRegistered(): boolean` | Boot-fail-mode detector for `verifyBootstrap`. | -> docs/app-bootstrap.md |
50
+ | `registerApiMethodMap(map: ApiMethodMap): void` | Register generated `apiMethodMap` so `apiRequest` can look up real HTTP methods. | -> docs/app-bootstrap.md |
51
+ | `getRegisteredApiMethod(pagePath: string, apiName: string, version: string): HttpMethodLiteral \| undefined` | Lookup helper used by `isGetMethod`. | -> docs/app-bootstrap.md |
52
+ | `isApiMethodMapRegistered(): boolean` | Detect whether the prefix-heuristic fallback is active. | -> docs/app-bootstrap.md |
53
+ | `registerPrismaClient(client: PrismaClient, key?: string): PrismaClient` | Register a Prisma client into a slot (default `'default'`). Pass a `key` for graded credentials (e.g. `'ro'`/`'rw'`) or per-tenant clients. | -> docs/app-bootstrap.md |
54
+ | `registerRedisClient(client: RedisClient, key?: string): RedisClient` | Register an ioredis client into a slot (default `'default'`). | -> docs/app-bootstrap.md |
55
+ | `getPrismaClient(): PrismaClient` | Read the `'default'` slot (registered client or lazy default resolver). | -> docs/app-bootstrap.md |
56
+ | `getRedisClient(): RedisClient` | Read the `'default'` slot (registered client or lazy default resolver). | -> docs/app-bootstrap.md |
57
+ | `getPrismaClientFor(key?: string): PrismaClient` | Read a specific slot. `'default'` falls back to the resolver; any other unregistered slot throws (never silently returns the privileged default). | -> docs/app-bootstrap.md |
58
+ | `getRedisClientFor(key?: string): RedisClient` | Read a specific Redis slot (same semantics as `getPrismaClientFor`). | -> docs/app-bootstrap.md |
59
+ | `getPrismaClientKeys(): string[]` / `getRedisClientKeys(): string[]` | Diagnostic — which slots have an explicitly-registered client. | -> docs/app-bootstrap.md |
60
+ | `DEFAULT_CLIENT_KEY: 'default'` | The reserved slot backing every framework internal + the `prisma`/`redis` proxies. | -> docs/app-bootstrap.md |
61
+ | `isPrismaClientRegistered(): boolean` | Boot guard — true when the `'default'` slot is registered. | -> docs/app-bootstrap.md |
62
+ | `isRedisClientRegistered(): boolean` | Boot guard — true when the `'default'` slot is registered. | -> docs/app-bootstrap.md |
63
+ | `resetClientsForTests(): void` | Test-only — drop every registered slot (default resolvers stay set). | -> docs/app-bootstrap.md |
64
+ | `redis` | Proxy that forwards every call to the currently registered ioredis client. Applies `applyStrayKeyPrefix` to single-key commands as a best-effort namespace net (keys containing `:` pass through untouched). | -> docs/redis-adapter.md |
65
+ | `getRedisConnectionOptions(): RedisConnectionOptions` | Single source of truth for `{ host, port, username?, password? }` (router reuses for cross-env probes). | -> docs/redis-adapter.md |
66
+ | `formatKey(namespace: string, suffix?: string): string` | Build a namespaced Redis key through the active formatter. The single authority every framework key-site routes through (`-session`, `-activeUsers`, `-pwreset`, `-email-change`, `-oauth-state`, `:rate-limit`). Default reproduces historical key bytes (zero migration). | -> docs/redis-adapter.md |
67
+ | `registerRedisKeyFormatter(fn: RedisKeyFormatter): void` | Override how every framework Redis key is namespaced (multi-tenant per-tenant prefixing). Must preserve the `<namespace-root>:<suffix>` join so `SCAN` enumeration still works. | -> docs/redis-adapter.md |
68
+ | `getRedisKeyFormatter(): RedisKeyFormatter` / `defaultRedisKeyFormatter` / `resetRedisKeyFormatterForTests()` | Read the active formatter / the built-in default / test reset. | -> docs/redis-adapter.md |
69
+ | `applyStrayKeyPrefix(key: string): string` | Project-prefix an un-namespaced (colon-free) key; pass-through for already-namespaced keys. Used by the `redis` proxy net. | -> docs/redis-adapter.md |
70
+ | `acquireLease(name, ttlMs): Promise<string \| null>` | Acquire an exclusive Redis lease (`SET NX PX`). Returns an owner token, or null if held. Single-Redis best-effort (not Redlock) — the lease is a primitive; the renew loop is app code. | -> docs/redis-adapter.md |
71
+ | `renewLease(name, token, ttlMs): Promise<boolean>` | Owner-checked compare-and-pexpire (Lua); extends only if `token` still holds the lease. | -> docs/redis-adapter.md |
72
+ | `releaseLease(name, token): Promise<boolean>` | Owner-checked compare-and-delete (Lua); releasing another owner's lease is a no-op. | -> docs/redis-adapter.md |
73
+ | `attachSocketRedisAdapter(io: SocketIOServer): void` | Wire `@socket.io/redis-adapter` with duplicated pub/sub clients so room broadcasts span instances. | -> docs/redis-adapter.md |
74
+ | `setIoInstance(io: SocketIOServer \| null): void` | Module-level slot for the running Socket.io server. | -> docs/socket-bootstrap.md |
75
+ | `getIoInstance(): SocketIOServer \| null` | Read the slot from framework code that needs to broadcast. | -> docs/socket-bootstrap.md |
76
+ | `socket`, `setSocket`, `incrementResponseIndex`, `waitForSocket` | Client-side socket singleton + response-index counter. | -> docs/socket-bootstrap.md |
77
+ | `extractTokenFromSocket(socket): string \| null` | Read session token from Socket.io handshake (cookie + header). | -> docs/socket-bootstrap.md |
78
+ | `extractTokenFromRequest(request): string \| null` | Read session token from a Node IncomingMessage (cookie + `Authorization: Bearer`). | -> docs/socket-bootstrap.md |
79
+ | `allowedOrigin(origin: string): boolean` | Same-origin + project-configured allow-list CORS check; dispatches `corsRejected` hook on miss. | -> docs/socket-bootstrap.md |
80
+ | `validateRequest({ data, user, auth }): ValidationResult` | Auth gate driven by `AuthProps`; checks login + `additional[]` predicates. | -> docs/session-types.md |
81
+ | `isFalsy(value): boolean` | Helper used inside `validateRequest`. | -> docs/session-types.md |
82
+ | `validateInputByType(value, type)` | Runtime input validation; lazy-loads `@luckystack/devkit` in dev. | -> docs/session-types.md |
83
+ | `registerNotifier(notifier: Notifier): void` | DI for client-side toast notifier (success/error/info/warning). | -> docs/app-bootstrap.md |
84
+ | `getNotifier(): Notifier` | Read active notifier (no-op default). | -> docs/app-bootstrap.md |
85
+ | `notify: Notifier` | Delegating wrapper used by framework hot paths. | -> docs/app-bootstrap.md |
86
+ | `registerEmailSender(sender: EmailSender): void` | Legacy single-sender registration; mirrors into `default` slot. | -> docs/app-bootstrap.md |
87
+ | `registerEmailSenders(senders: EmailSenderRegistry): void` | Multi-adapter registration (`'default'`, `'transactional'`, `'marketing'`, custom). | -> docs/app-bootstrap.md |
88
+ | `getEmailSender(): EmailSender \| null` | Read legacy/`default` sender. | -> docs/app-bootstrap.md |
89
+ | `getEmailSenderByName(name: string): EmailSender \| null` | Read a specific slot (falls back to legacy sender for `'default'`). | -> docs/app-bootstrap.md |
90
+ | `listEmailSenderNames(): string[]` | Diagnostic helper. | -> docs/app-bootstrap.md |
91
+ | `isEmailSenderRegistered(): boolean` | Boot guard. | -> docs/app-bootstrap.md |
92
+ | `registerLogger(logger: Logger): void` | Plug in Pino/Winston/Datadog (or `createDevLogger()` for colored dev output). | -> docs/app-bootstrap.md |
93
+ | `getLogger(): Logger` | Read at call time (default = console.{debug,info,warn,error}). | -> docs/app-bootstrap.md |
94
+ | `isLoggerRegistered(): boolean` | Boot guard. | -> docs/app-bootstrap.md |
95
+ | `createDevLogger(): Logger` | Factory for the dev-mode colored logger (depends on `initConsolelog()`). | -> docs/app-bootstrap.md |
96
+ | `resetLoggerForTests(): void` | Test-only — restore default logger. | -> docs/app-bootstrap.md |
97
+ | `registerRedactedLogKeys(keys: Iterable<string>): void` | Add keys that the framework will redact from log payloads. | -> docs/app-bootstrap.md |
98
+ | `getRedactedLogKeys(): Set<string>` | Read current redacted-keys set. | -> docs/app-bootstrap.md |
99
+ | `isRedactedLogKey(key: string): boolean` | Hot-path lookup used inside the framework. | -> docs/app-bootstrap.md |
100
+ | `resetRedactedLogKeysForTests(): void` | Test-only reset. | -> docs/app-bootstrap.md |
101
+ | `initConsolelog(): void` | Monkey-patch `console.*` to render trailing color string (dev only). | -> docs/app-bootstrap.md |
102
+ | `registerLocaleReloader(reloader: LocaleReloader): void` | DI for the dev-only i18n hot-reload trigger. | -> docs/app-bootstrap.md |
103
+ | `getLocaleReloader(): LocaleReloader \| null` | Read active reloader (returns null when no project supplied one). | -> docs/app-bootstrap.md |
104
+ | `registerErrorTracker(tracker: ErrorTracker): void` | Single-tracker registration (replaces previous). | -> docs/error-tracker-registry.md |
105
+ | `registerErrorTrackers(trackers: ErrorTracker[]): void` | Multi-tracker registration (replaces the list). | -> docs/error-tracker-registry.md |
106
+ | `getActiveErrorTrackers(): ErrorTracker[]` | Read active trackers. | -> docs/error-tracker-registry.md |
107
+ | `captureExceptionAcrossTrackers(error, context?): void` | Fan-out exception capture; per-tracker errors are swallowed. | -> docs/error-tracker-registry.md |
108
+ | `captureMessageAcrossTrackers(message, level, context?): void` | Fan-out message capture. | -> docs/error-tracker-registry.md |
109
+ | `setErrorTrackerUser(user: ErrorTrackerUser \| null): void` | Fan-out user identification. | -> docs/error-tracker-registry.md |
110
+ | `recordMetricAcrossTrackers(name, value, tags?): void` | Optional metric fan-out (skips trackers without `recordMetric`). | -> docs/error-tracker-registry.md |
111
+ | `startSpanAcrossTrackers<T>(name, op, fn): T` | First tracker that supports spans wins; falls back to direct `fn()`. | -> docs/error-tracker-registry.md |
112
+ | `initSharedSentry(instance)` | Legacy Sentry slot (kept for backwards compatibility). | -> docs/error-tracker-registry.md |
113
+ | `captureException(error, context?): void` | Legacy fan-out: Sentry slot + adapter list. | -> docs/error-tracker-registry.md |
114
+ | `captureMessage(message, level?, context?): void` | Legacy fan-out: Sentry slot + adapter list. | -> docs/error-tracker-registry.md |
115
+ | `setSentryUser(user): void` | Legacy fan-out: Sentry slot + adapter list. | -> docs/error-tracker-registry.md |
116
+ | `startSpan(name, op): unknown` | Legacy span helper. | -> docs/error-tracker-registry.md |
117
+ | `checkRateLimit(params: CheckRateLimitParams): Promise<RateLimitResult>` | Delegates to the active strategy's `check()`. | -> docs/rate-limit-strategy.md |
118
+ | `getRateLimitStatus(key, limit): Promise<RateLimitResult>` | Read-only status (does not increment). | -> docs/rate-limit-strategy.md |
119
+ | `clearRateLimit(key): Promise<void>` | Reset a single key. | -> docs/rate-limit-strategy.md |
120
+ | `clearAllRateLimits(): Promise<void>` | Reset every key under the active strategy's namespace. | -> docs/rate-limit-strategy.md |
121
+ | `registerRateLimitStrategy(strategy: RateLimitStrategy): void` | Swap the backend (token-bucket, edge-KV, per-tier, no-op). | -> docs/rate-limit-strategy.md |
122
+ | `getRateLimitStrategy(): RateLimitStrategy` | Read the currently active strategy. | -> docs/rate-limit-strategy.md |
123
+ | `defaultRateLimitStrategy: RateLimitStrategy` | Built-in memory-or-redis backend. | -> docs/rate-limit-strategy.md |
124
+ | `registerHook<TName>(name, handler): void` | Subscribe an async handler to a lifecycle event. | -> docs/hooks.md |
125
+ | `registerMiddlewareHandler(handler: MiddlewareHandler): void` | Register the GLOBAL route-guard fallback. Called by `<Middleware>` / `useRouter` only when no per-page middleware is registered for the visited route. Optional — framework default allows by-default. | -> docs/hooks.md |
126
+ | `getMiddlewareHandler(): MiddlewareHandler` | Read the active global handler. | -> docs/hooks.md |
127
+ | `registerPageMiddleware(path: string, fn: PageMiddleware): void` | Register a per-page route guard. Auto-called by `src/main.tsx`'s `getRoutes()` for every page that exports `middleware`. Per-page takes precedence over the global handler. | -> docs/hooks.md |
128
+ | `getPageMiddleware(path: string): PageMiddleware \| undefined` | Lookup the per-page guard for a given route. Framework-internal. | -> docs/hooks.md |
129
+ | `hasPageMiddleware(path: string): boolean` | Existence check. | -> docs/hooks.md |
130
+ | `MiddlewareInput`, `MiddlewareResult`, `MiddlewareHandler`, `PageMiddleware` (types) | Route-guard contract types. Both handler shapes share the same signature. | -> docs/hooks.md |
131
+ | `validatePagePath(srcRelativePath, rules?): PagePathValidationResult` | Pure validator for the invisible-parent folder convention used by `src/main.tsx`'s page auto-discovery AND the `scaffold:page` CLI. Returns `{ valid, route?, reason? }`. | -> docs/hooks.md |
132
+ | `DEFAULT_PAGE_ROUTE_RULES: PageRouteRules` | Default config (private-folder prefix `'_'` + reserved framework folders). | -> docs/hooks.md |
133
+ | `dispatchHook<TName>(name, payload): Promise<DispatchResult>` | Internal: framework code invokes registered handlers in order; first stop signal aborts. | -> docs/hooks.md |
134
+ | `clearAllHooks(): void` | Test-only — drop every registered handler (sync + async). | -> docs/hooks.md |
135
+ | `registerSyncHook<TName>(name, handler): void` | Subscribe a synchronous mutator to a hot-path hook (e.g. error normalization). | -> docs/hooks.md |
136
+ | `dispatchSyncHook<TName>(name, payload): void` | Internal: framework code invokes sync handlers; payload is mutated in place. | -> docs/hooks.md |
137
+ | `BaseSessionLayout`, `SessionLocation`, `AuthProps` (types) | Foundational session-shape types; project `SessionLayout` extends `BaseSessionLayout`. | -> docs/session-types.md |
138
+ | `HookSessionShape`, `HookName`, `HookHandler`, `HookResult`, `HookStopSignal`, `HookPayloads` (types) | Hook contract types (augmentable via TS module augmentation). | -> docs/hooks.md |
139
+ | `isOnline()` / `enqueueApiRequest` / `enqueueSyncRequest` / `removeApiQueueItem` / `removeSyncQueueItem` / `removeApiQueueItemsByKey` / `flushApiQueue` / `flushSyncQueue` / `getApiQueueSize` / `getSyncQueueSize` | Client-side offline queue with per-item `dropPolicy` and global max-size/max-age caps. | -> docs/socket-bootstrap.md |
140
+ | `getCsrfToken()`, `clearCsrfToken()`, `httpFetch(...)` | CSRF-aware fetch wrapper used by the client transport. | -> docs/socket-bootstrap.md |
141
+ | `registerCsrfConfig(input: Partial<CsrfConfig>): void` | Override the CSRF cookie name, header name, token length, or cookie options. | -> docs/csrf-config.md |
142
+ | `getCsrfConfig(): CsrfConfig` | Read the active CSRF config at call time (defaults to `DEFAULT_CSRF_CONFIG`). | -> docs/csrf-config.md |
143
+ | `DEFAULT_CSRF_CONFIG: CsrfConfig` | Built-in defaults (`csrf-token` cookie, `x-csrf-token` header, 32-byte token). | -> docs/csrf-config.md |
144
+ | `resetCsrfConfigForTests(): void` | Test-only — restore CSRF defaults between scenarios. | -> docs/csrf-config.md |
145
+ | `registerSocketMiddleware(mw: SocketMiddleware): void` | Wedge an `io.use(...)` middleware into the framework's socket bootstrap; runs before any `connect` handler. | -> docs/socket-bootstrap.md |
146
+ | `getSocketMiddlewares(): readonly SocketMiddleware[]` | Read the registered middleware list. | -> docs/socket-bootstrap.md |
147
+ | `clearSocketMiddlewares(): void` | Drop every registered middleware (test/hot-reload helper). | -> docs/socket-bootstrap.md |
148
+ | `applySocketMiddlewares(io: SocketIOServer): void` | Wire every registered middleware into the running Socket.io server (called from `@luckystack/server`'s `loadSocket`). | -> docs/socket-bootstrap.md |
149
+ | `writeBootUuid(envKey?): Promise<string>` | Write a fresh UUID to `luckystack:boot:<envKey>` with the configured TTL. | -> docs/app-bootstrap.md |
150
+ | `readBootUuid(envKey?): Promise<string \| null>` | Read the boot UUID (router cross-checks against `/_health`). | -> docs/app-bootstrap.md |
151
+ | `resolveEnvKey(): string` | `LUCKYSTACK_ENV` -> `NODE_ENV` -> `'development'`. | -> docs/app-bootstrap.md |
152
+ | `BOOT_KEY_PREFIX: 'luckystack:boot:'` | Constant — single source of truth so router can't drift. | -> docs/app-bootstrap.md |
153
+ | `collectSynchronizedEnvKeys()` / `computeSynchronizedEnvHashes()` / `hashSynchronizedValue()` | Cross-env drift detection helpers for the router boot handshake. | -> docs/app-bootstrap.md |
154
+ | `tryCatch<T>(fn): Promise<[Error \| null, T \| null]>` | Tuple-style async error handling used everywhere in the framework. | -> docs/app-bootstrap.md |
155
+ | `sleep(ms): Promise<void>` | `setTimeout`-based delay. | -> docs/app-bootstrap.md |
156
+ | `getParams(request)` | Parse URL params from a Node request. | -> docs/app-bootstrap.md |
157
+ | `serveAvatar(...)` | HTTP handler for `/avatars/:fileId`; rejects path-traversal, applies `AvatarConfig`. | -> docs/app-bootstrap.md |
158
+ | `processUpload(input: ProcessUploadInput): Promise<ProcessUploadResult>` | Wraps a project encode/save callback with the `onUploadStart`/`onUploadComplete` hook contract. | -> docs/hooks.md |
159
+ | Response normalizers (`normalizeErrorResponse`, `extractLanguageFromHeader`, ...) | Shared error-shape + i18n helpers; dispatches `preErrorNormalize` / `postErrorNormalize` sync hooks. | -> docs/hooks.md |
160
+ | `serviceRoute` exports | Helpers to build router-routable URLs across services. | -> docs/socket-bootstrap.md |
161
+ | `socketEvents` exports | Wire-protocol constants for socket events. | -> docs/socket-bootstrap.md |
162
+ | `paths` exports (`getGeneratedApiDocsPath`, `getApiMethodMapPath`, ...) | Paths resolved through `projectConfig.paths`. | -> docs/config-registry.md |
163
+ | `cookies` exports | Cookie parse/serialise helpers used by the HTTP layer. | -> docs/socket-bootstrap.md |
164
+ | `httpApiUtils` exports | Shared HTTP helpers (status codes, header writers). | -> docs/socket-bootstrap.md |
165
+
166
+ ## Config keys
167
+
168
+ Env vars read directly by core (via `env.ts` and call-time helpers):
169
+
170
+ | Env var | Default | Consumer |
171
+ | --- | --- | --- |
172
+ | `NODE_ENV` | `'development'` | Zod-validated, mirrored to `process.env`. |
173
+ | `SERVER_IP` | `'127.0.0.1'` | `getBindAddress()` fallback. |
174
+ | `SERVER_PORT` | `'80'` | `getBindAddress()` fallback. |
175
+ | `SECURE` | `'false'` | `allowedOrigin` scheme selection. |
176
+ | `DNS` | `''` | Reserved for downstream packages. |
177
+ | `REDIS_HOST` | `'127.0.0.1'` | `redis.ts` default client + `getRedisConnectionOptions`. |
178
+ | `REDIS_PORT` | `'6379'` | Same. |
179
+ | `REDIS_USERNAME` | (unset) | Optional ioredis auth. |
180
+ | `REDIS_PASSWORD` | (unset) | Optional ioredis auth. |
181
+ | `PROJECT_NAME` | `'luckystack'` | Redis-prefix fallback in `getProjectName()`. |
182
+ | `LUCKYSTACK_ENV` | (unset) | `resolveEnvKey()` first preference (boot UUID, router handshake). |
183
+ | `LUCKYSTACK_ENV_FILES` | `.env,.env.local` | Ambient override for `getEnvFiles()` / `loadEnvFiles()` — comma-separated list of env files to load, "later overrides earlier". |
184
+
185
+ `registerProjectConfig` slots (see `ProjectConfig` for full surface): `app.publicUrl`, `logging.*`, `rateLimiting.{enabled, store, redisKeyPrefix, defaultApiLimit, defaultIpLimit, windowMs, cleanupIntervalMs}`, `session.{basedToken, expiryDays, perBrowser, perUser, maxConcurrentPerUser, onConflict, notifyOldDeviceOnRevoke, projectName}`, `http.{sessionCookie*, requestBodyMaxBytes, healthEndpoint, liveEndpoint, readyEndpoint, testResetEndpoint, stream, securityHeaders, cors}`, `auth.{passwordPolicy, emailMaxLength, nameMaxLength, bcryptRounds, providerAccountStrategy, forgotPassword, passwordResetTtlSeconds, passwordResetBrand}`, `socket.{maxHttpBufferSize, pingTimeout, pingInterval}`, `sync.{streamThrottle, fanoutYieldEvery, fanoutYieldMs}`, `offlineQueue.{maxSize, maxAgeMs, dropPolicy}`, `dev.{hotReloadDebounceMs, watcherStabilityThresholdMs, watcherPollIntervalMs, warnOnMissingInputType}`, `paths.*`, `defaultLanguage`, `defaultTheme`, `socketActivityBroadcaster`, `socketStatusIndicator`, `locationProviderEnabled`, `loginRedirectUrl`.
186
+
187
+ Other registries: `registerDeployConfig(DeployConfigShape)`, `registerServicesConfig(ServicesConfigShape)`, `registerAvatarConfig(AvatarConfigInput)`, `registerBindAddress({ ip, port })`.
188
+
189
+ ## Peer dependencies
190
+
191
+ Required:
192
+
193
+ - `@prisma/client@^6.19.0` — DB client proxied through `getPrismaClient()`.
194
+ - `ioredis@^5.10.0` — Redis client proxied through `getRedisClient()`; backs the rate limiter, session store, boot-UUID, and offline-queue.
195
+ - `socket.io@^4.8.0` — server-side `SocketIOServer` consumed by `setIoInstance` and `attachSocketRedisAdapter`.
196
+ - `socket.io-client@^4.8.0` — client-side socket types used by the offline queue.
197
+ - `zod@^4.0.0` — `env.ts` schema parsing.
198
+
199
+ Optional (mark in `peerDependenciesMeta`):
200
+
201
+ - `react@^19.0.0`, `react-dom@^19.0.0`, `react-router-dom@^7.0.0` — only consumed by `@luckystack/core/client` and the `react/*` subpath helpers. Pure server boots can skip these.
202
+ - `sonner@^2.0.0` — only needed if you wire the default sonner-backed notifier from the project's React entry; the core notifier slot itself is library-agnostic.
203
+
204
+ Runtime (bundled): `@socket.io/redis-adapter`, `dotenv`.
205
+
206
+ ## Related
207
+
208
+ - README: `packages/core/README.md`
209
+ - Architecture deep-dives: `docs/ARCHITECTURE_EXTENSION_POINTS.md`, `docs/ARCHITECTURE_PACKAGING.md`, `docs/ARCHITECTURE_SOCKET.md`, `docs/ARCHITECTURE_MULTI_INSTANCE.md`, `docs/ARCHITECTURE_API.md`, `docs/ARCHITECTURE_SYNC.md`, `docs/ARCHITECTURE_SESSION.md`, `docs/ARCHITECTURE_AUTH.md`, `docs/ARCHITECTURE_ROUTING.md`
210
+ - Project-side glue: `config.ts`, `server/server.ts`, `server/prod/runtimeMaps.ts`, `src/_sockets/socketInitializer.ts`
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mathijs van Melick
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,93 @@
1
+ # @luckystack/core
2
+
3
+ > Foundation package for [LuckyStack](https://github.com/ItsLucky23/LuckyStack-v2). Socket-first transport contracts, Redis adapter, project-config registry, hooks registry, response normalization, rate limiting, and shared session types.
4
+
5
+ Every other `@luckystack/*` package depends on this one.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @luckystack/core
11
+ ```
12
+
13
+ Peer dependencies you must install separately:
14
+
15
+ ```bash
16
+ npm install @prisma/client ioredis socket.io socket.io-client zod
17
+ ```
18
+
19
+ ## Quickstart
20
+
21
+ The core package is mostly consumed transitively through `@luckystack/server`, `@luckystack/api`, etc. You typically only touch it directly to register your project config and to call low-level utilities.
22
+
23
+ ```ts
24
+ import { registerProjectConfig, getProjectConfig, notify, redis, prisma } from '@luckystack/core';
25
+
26
+ registerProjectConfig({
27
+ defaultLanguage: 'en',
28
+ session: { expiryDays: 30, basedToken: false, allowMultiple: true },
29
+ auth: {
30
+ emailMaxLength: 254,
31
+ passwordMinLength: 8,
32
+ passwordMaxLength: 128,
33
+ nameMaxLength: 64,
34
+ oauthStateTtlSeconds: 600,
35
+ },
36
+ // ...see ProjectConfig type for the full surface
37
+ });
38
+
39
+ const config = getProjectConfig();
40
+ ```
41
+
42
+ ## Subpath: `@luckystack/core/client`
43
+
44
+ Browser-safe surface for the React/Vite bundle. Re-exports the pieces consumed by `@luckystack/sync`'s client transport (`projectConfig`, `notifier`, `socketState`, `offlineQueue`, `responseNormalizer`, `serviceRoute`, `socketEvents`, `socketStatusTypes`, `apiTypeStubs`, `sessionTypes`).
45
+
46
+ ```ts
47
+ import { socket, isOnline, enqueueApiRequest } from '@luckystack/core/client';
48
+ ```
49
+
50
+ ## Public API (server entry)
51
+
52
+ | Group | Exports |
53
+ | --- | --- |
54
+ | Project config | `registerProjectConfig`, `getProjectConfig`, `getProjectName`, `isProjectConfigRegistered`, `DEFAULT_PROJECT_CONFIG`. Types: `ProjectConfig`, `ProjectConfigInput`, `LoggingConfig`, `RateLimitingConfig`, `SessionConfig`, `SentryConfig`, `SentrySampleRates`, `HttpConfig`, `HttpStreamConfig`, `SecurityHeadersConfig`, `CorsConfig`, `AuthConfig`, `SocketConfig`, `SyncConfig`, `SyncStreamThrottleConfig`, `OfflineQueueConfig`, `DevConfig`, `PathsConfig`, `EmailConfig`, `EmailLoggingConfig`, `EmailEnvVarsConfig`, `EmailDefaultsConfig`. **`getProjectName()`** is the canonical way to read the project's Redis-prefix string — it resolves at call time in this order: (1) `projectConfig.session.projectName` if a consumer set it, (2) `process.env.PROJECT_NAME` (read after `dotenv` has loaded), (3) literal `'luckystack'` as the absolute fallback. Reach for it from any framework or project code that needs the prefix instead of duplicating the env-read pattern. |
55
+ | Deploy + services config | `registerDeployConfig`, `getDeployConfig`, `isDeployConfigRegistered`, `registerServicesConfig`, `getServicesConfig`, `isServicesConfigRegistered`. Types: `DeployConfigShape`, `DeployResourceShape`, `DeployEnvironmentShape`, `DeployRoutingShape`, `DeployDevelopmentShape`, `ServicesConfigShape`, `ServiceDefinition`, `PresetDefinition` |
56
+ | Runtime maps | `registerRuntimeMapsProvider`, `getRuntimeApiMaps`, `getRuntimeSyncMaps`. Types: `RuntimeMapsProvider`, `RuntimeApiMapsResult`, `RuntimeSyncMapsResult` |
57
+ | DI clients | `registerPrismaClient`, `registerRedisClient`, `getPrismaClient`, `getRedisClient`, `isPrismaClientRegistered`, `isRedisClientRegistered` |
58
+ | Notifier | `registerNotifier`, `getNotifier`, `notify`. Types: `Notifier`, `NotifyInput`, `NotifyParam` |
59
+ | Email registry | `registerEmailSender`, `getEmailSender`, `isEmailSenderRegistered`. Types: `EmailSender`, `EmailMessage`, `EmailResult` (re-exported from `@luckystack/email` for convenience) |
60
+ | Logger | `registerLogger`, `getLogger`, `isLoggerRegistered`, `resetLoggerForTests`, `createDevLogger`. Types: `Logger`, `LoggerContext`. Default logger is a thin wrapper over `console.{debug,info,warn,error}` with no color codes — safe in production. Call `registerLogger(createDevLogger())` from a dev-only entry to opt into the colored terminal output (which still depends on `initConsolelog()` to render the trailing color string). Plug in Pino / Winston / Datadog by registering an object that satisfies `Logger`. |
61
+ | Redacted log keys | `registerRedactedLogKeys`, `getRedactedLogKeys`, `isRedactedLogKey`, `resetRedactedLogKeysForTests` |
62
+ | Avatar / uploads | `registerAvatarConfig`, `getAvatarConfig`, `DEFAULT_AVATAR_CONFIG`, `serveAvatar`, `processUpload`. Types: `AvatarConfig`, `AvatarConfigInput`, `ProcessUploadInput`, `ProcessUploadResult`. **Path-traversal guard:** `serveAvatar` rejects any `fileId` that does not match `/^[A-Za-z0-9_-]{1,128}$/` with a 400 before touching the filesystem — `..`, slashes, and other path-separator characters cannot escape the configured upload directory. **`processUpload`** wraps the project-supplied encode/save callback with the framework's `onUploadStart` (stoppable) and `onUploadComplete` hook contract so consumer upload routes don't need to plumb the hooks themselves. Returns `{ status: 'success', sizeBytes }`, `{ status: 'rejected', errorCode }` (when `onUploadStart` returned a stop signal), or `{ status: 'error', reason, cause? }`. See [`src/settings/_api/updateUser_v1.ts`](../../src/settings/_api/updateUser_v1.ts) for the canonical avatar usage. |
63
+ | Bind address | `registerBindAddress({ ip, port })`, `getBindAddress() → { ip, port: string }`. `createLuckyStackServer` registers the resolved listen address at boot so framework code (e.g. `checkOrigin` building the same-origin entry) reads it from the registry instead of `SERVER_IP` / `SERVER_PORT` env vars — no more drift when consumers configure programmatically via `createLuckyStackServer({ ip, port })`. Resolution at call time: (1) `registerBindAddress(...)` value, (2) `process.env.SERVER_IP` / `SERVER_PORT` (legacy), (3) `'127.0.0.1'` / `''` fallback. |
64
+ | Locale reloader | `registerLocaleReloader`, `getLocaleReloader`. Type: `LocaleReloader` |
65
+ | Hooks (async) | `registerHook`, `dispatchHook`, `clearAllHooks`. Types: `HookName`, `HookHandler`, `HookResult`, `HookStopSignal`, `HookPayloads`, `DispatchResult`, `HookSessionShape`. Canonical payload types: `PreApiValidatePayload`, `PostApiValidatePayload`, `PreApiExecutePayload`, `PostApiExecutePayload`, `PreApiRespondPayload`, `PostApiRespondPayload`, `ApiResponseEnvelope`, `PreSyncFanoutPayload`, `PostSyncFanoutPayload`, `ApiErrorPayload`, `SyncErrorPayload`, `RateLimitExceededPayload`, `CorsRejectedPayload`, `CsrfMismatchPayload`, `PreSessionRefreshPayload`, `PostSessionRefreshPayload`, `OnUploadStartPayload`, `OnUploadCompletePayload`. Feature-package payloads (`Pre/PostLogin`, `Pre/PostRegister`, `Pre/PostLogout`, `Pre/PostSessionCreate`, `Pre/PostSessionDelete`, `Pre/PostEmailSend`, `Pre/PostPresenceUpdate`, socket lifecycle, etc.) augment `HookPayloads` from their owning packages — see those packages' READMEs. |
66
+ | Hooks (sync, mutators) | `registerSyncHook`, `dispatchSyncHook`. Types: `SyncHookName`, `SyncHookHandler`, `SyncHookPayloads`, `PreErrorNormalizePayload`, `PostErrorNormalizePayload`. Sync hook handlers must be synchronous (no `async`), mutate the payload object in place, and cannot stop the flow. Use them only for hot-path mutators like error-code remapping. |
67
+ | Session types | `BaseSessionLayout`, `SessionLocation`, `AuthProps` |
68
+ | Socket | `setIoInstance`, `getIoInstance`, `attachSocketRedisAdapter`, `socket`, `setSocket`, `incrementResponseIndex`, `waitForSocket`. Types: `SOCKETSTATUS`, `statusContent`, `apiMessage`, `syncMessage` |
69
+ | Auth helpers | `extractTokenFromSocket`, `extractTokenFromRequest`, `validateRequest`, `isFalsy`, `allowedOrigin`. Types: `ValidationResult` |
70
+ | Rate limiting | `checkRateLimit`, `getRateLimitStatus`, `clearRateLimit`, `clearAllRateLimits` |
71
+ | Validation | `validateInputByType` (resolves to `@luckystack/devkit` lazily in dev) |
72
+ | Persistence + env | `redis`, `prisma`, `db` exports, `env` exports |
73
+ | Boot | `writeBootUuid`, `readBootUuid`, `resolveEnvKey`, `collectSynchronizedEnvKeys`, `computeSynchronizedEnvHashes`, `hashSynchronizedValue`, `initConsolelog` |
74
+ | Offline queue | `isOnline`, `enqueueApiRequest`, `enqueueSyncRequest`, `removeApiQueueItem`, `removeSyncQueueItem`, `removeApiQueueItemsByKey`, `flushApiQueue`, `flushSyncQueue`, `getApiQueueSize`, `getSyncQueueSize` |
75
+ | CSRF | `getCsrfToken`, `clearCsrfToken`, `httpFetch` |
76
+ | Cookies + paths | All exports of `./cookies`, `./httpApiUtils`, and `./paths` (`getGeneratedApiDocsPath`, `getApiMethodMapPath`, etc.) |
77
+ | Misc | `tryCatch`, `sleep`, `getParams`, response normalizers (`normalizeErrorResponse`, `extractLanguageFromHeader`, …), service-route helpers, socket-event constants |
78
+
79
+ > Note: `apiRequest` is exported from `@luckystack/core/client`, not from the server entry. The server barrel does not pull React-coupled code.
80
+
81
+ ## Dependencies
82
+
83
+ - Runtime: `@socket.io/redis-adapter`, `dotenv`
84
+ - Peer (canonical ranges, standardized 2026-05-07):
85
+ - `@prisma/client@^6.19.0`
86
+ - `ioredis@^5.10.0`
87
+ - `socket.io@^4.8.0`
88
+ - `socket.io-client@^4.8.0`
89
+ - `zod@^3.25.0`
90
+
91
+ ## License
92
+
93
+ MIT — see [LICENSE](../../LICENSE).
@@ -0,0 +1,7 @@
1
+ interface ApiTypeMap {
2
+ }
3
+ interface SyncTypeMap {
4
+ }
5
+ type StreamPayload = Record<string, unknown>;
6
+
7
+ export type { ApiTypeMap, StreamPayload, SyncTypeMap };
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=apiTypeStubs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,236 @@
1
+ import { StreamPayload, ApiTypeMap } from './apiTypeStubs.js';
2
+ export { SyncTypeMap } from './apiTypeStubs.js';
3
+ import { B as BaseSessionLayout } from './pageRouteValidation-CxZJVwZh.js';
4
+ export { A as ApiMethodMap, a as AppConfig, b as AuthProps, D as DEFAULT_PAGE_ROUTE_RULES, H as HttpMethodLiteral, L as Logger, c as LoggerContext, d as LoggingConfig, N as Notifier, e as NotifyInput, f as NotifyParam, P as PagePathValidationResult, g as PageRouteRules, h as ProjectConfig, R as RateLimitingConfig, S as SOCKETSTATUS, i as SessionConfig, j as SessionLocation, k as buildApiResponseEventName, l as buildApiStreamEventName, m as buildGetJoinedRoomsResponseEventName, n as buildJoinRoomResponseEventName, o as buildLeaveRoomResponseEventName, p as buildSyncProgressEventName, q as buildSyncResponseEventName, r as clearCsrfToken, s as enqueueApiRequest, t as enqueueSyncRequest, u as flushApiQueue, v as flushSyncQueue, w as getApiQueueSize, x as getCsrfToken, y as getLogger, z as getNotifier, C as getProjectConfig, E as getRegisteredApiMethod, F as getSyncQueueSize, G as httpFetch, I as incrementResponseIndex, J as isApiMethodMapRegistered, K as isOnline, M as isProjectConfigRegistered, O as normalizeErrorResponseCore, Q as notify, T as parseServiceRouteName, U as registerApiMethodMap, V as registerLogger, W as registerNotifier, X as registerProjectConfig, Y as removeApiQueueItem, Z as removeApiQueueItemsByKey, _ as removeSyncQueueItem, $ as setSocket, a0 as sleep, a1 as socket, a2 as socketEventNames, a3 as statusContent, a4 as tryCatch, a5 as validatePagePath, a6 as waitForSocket } from './pageRouteValidation-CxZJVwZh.js';
5
+ import * as react from 'react';
6
+ import { ReactNode, Dispatch, SetStateAction } from 'react';
7
+ import 'socket.io-client';
8
+
9
+ type ApiStreamEvent<T extends StreamPayload = StreamPayload> = T;
10
+ type DataRequired<T> = Record<string, never> extends T ? false : true;
11
+ type UnionToIntersection<U> = (U extends unknown ? (arg: U) => void : never) extends ((arg: infer I) => void) ? I : never;
12
+ type ApiRouteRecord = UnionToIntersection<{
13
+ [P in keyof ApiTypeMap]: {
14
+ [N in keyof ApiTypeMap[P] as P extends 'root' ? `system/${Extract<N, string>}` : `${Extract<P, string>}/${Extract<N, string>}`]: ApiTypeMap[P][N];
15
+ };
16
+ }[keyof ApiTypeMap]>;
17
+ type ApiFullName = Extract<keyof ApiRouteRecord, string>;
18
+ type VersionsForFullName<F extends ApiFullName> = keyof ApiRouteRecord[F] & string;
19
+ type Prettify<T> = {
20
+ [K in keyof T]: T[K];
21
+ } & {};
22
+ type InputForFullName<F extends ApiFullName, V extends VersionsForFullName<F>> = ApiRouteRecord[F][V] extends {
23
+ input: infer I;
24
+ } ? I : never;
25
+ type OutputForFullName<F extends ApiFullName, V extends VersionsForFullName<F>> = ApiRouteRecord[F][V] extends {
26
+ output: infer O;
27
+ } ? O : never;
28
+ type StreamForFullName<F extends ApiFullName, V extends VersionsForFullName<F>> = ApiRouteRecord[F][V] extends {
29
+ stream: infer S;
30
+ } ? S : never;
31
+ type ApiStreamCallbackForFullName<F extends ApiFullName, V extends VersionsForFullName<F>> = [
32
+ StreamForFullName<F, V>
33
+ ] extends [never] ? never : (event: ApiStreamEvent<Prettify<StreamForFullName<F, V> extends StreamPayload ? StreamForFullName<F, V> : StreamPayload>>) => void;
34
+ type ApiParamsForFullName<F extends ApiFullName, V extends VersionsForFullName<F>> = DataRequired<InputForFullName<F, V>> extends true ? {
35
+ name: F;
36
+ version: V;
37
+ data: Prettify<InputForFullName<F, V>>;
38
+ abortable?: boolean;
39
+ disableErrorMessage?: boolean;
40
+ onStream?: ApiStreamCallbackForFullName<F, V>;
41
+ signal?: AbortSignal;
42
+ } : {
43
+ name: F;
44
+ version: V;
45
+ data?: Prettify<InputForFullName<F, V>>;
46
+ abortable?: boolean;
47
+ disableErrorMessage?: boolean;
48
+ onStream?: ApiStreamCallbackForFullName<F, V>;
49
+ signal?: AbortSignal;
50
+ };
51
+ /**
52
+ * Type-safe API request function.
53
+ *
54
+ * @example
55
+ * ```typescript
56
+ * // Full name usage - includes page in the name
57
+ * const result = await apiRequest({ name: 'examples/publicApi', version: 'v1', data: { message: 'hello' } });
58
+ * // result is typed correctly for publicApi
59
+ *
60
+ * // Global APIs use service-first naming
61
+ * await apiRequest({ name: 'system/session', version: 'v1' });
62
+ * ```
63
+ */
64
+ declare function apiRequest<F extends ApiFullName, V extends VersionsForFullName<F>>(params: ApiParamsForFullName<F, V>): Promise<Prettify<OutputForFullName<F, V>>>;
65
+
66
+ type LocalesMap = Record<string, unknown>;
67
+ type LanguageSource = () => string | null | undefined;
68
+ declare const registerLocales: (locales: LocalesMap) => void;
69
+ declare const getRegisteredLocales: () => LocalesMap;
70
+ declare const getDefaultLocale: () => string;
71
+ declare const registerLanguageSource: (fn: LanguageSource) => void;
72
+ declare const getActiveLanguage: () => string;
73
+ declare const getLocaleByCode: (code: string) => unknown;
74
+
75
+ interface MiddlewareInput {
76
+ location: string;
77
+ searchParams: Record<string, string>;
78
+ session: BaseSessionLayout | null;
79
+ }
80
+ type MiddlewareResult = {
81
+ success: true;
82
+ redirect?: undefined;
83
+ } | {
84
+ success: false;
85
+ redirect: string;
86
+ } | undefined;
87
+ type MiddlewareHandler = (input: MiddlewareInput) => MiddlewareResult | Promise<MiddlewareResult>;
88
+ declare const registerMiddlewareHandler: (handler: MiddlewareHandler) => void;
89
+ declare const getMiddlewareHandler: () => MiddlewareHandler;
90
+ type PageMiddleware<TSession extends BaseSessionLayout = BaseSessionLayout> = (input: {
91
+ location: string;
92
+ searchParams: Record<string, string>;
93
+ session: TSession | null;
94
+ }) => MiddlewareResult | Promise<MiddlewareResult>;
95
+ declare const registerPageMiddleware: (path: string, middleware: PageMiddleware) => void;
96
+ declare const getPageMiddleware: (path: string) => PageMiddleware | undefined;
97
+ declare const hasPageMiddleware: (path: string) => boolean;
98
+
99
+ interface ClientHookPayloadMap {
100
+ preLogin: {
101
+ candidateSession: BaseSessionLayout;
102
+ };
103
+ postLogin: {
104
+ session: BaseSessionLayout;
105
+ };
106
+ postLogout: {
107
+ previousSession: BaseSessionLayout;
108
+ };
109
+ }
110
+ type ClientHookName = keyof ClientHookPayloadMap;
111
+ interface ClientHookStopSignal {
112
+ stop: true;
113
+ errorCode: string;
114
+ }
115
+ type ClientHookResult = void | undefined | ClientHookStopSignal;
116
+ type ClientDispatchResult = {
117
+ stopped: false;
118
+ } | {
119
+ stopped: true;
120
+ signal: ClientHookStopSignal;
121
+ };
122
+ type ClientHookHandler<N extends ClientHookName> = (payload: ClientHookPayloadMap[N]) => ClientHookResult | Promise<ClientHookResult>;
123
+ /**
124
+ * Subscribe to a client-side lifecycle hook. Returns an unsubscribe function
125
+ * — call it to remove the handler (e.g. inside a React effect cleanup).
126
+ *
127
+ * Multiple handlers can be registered for the same hook; they all fire on
128
+ * each dispatch. A handler that throws (sync) or rejects (async) is logged
129
+ * to `console.error` and does not affect other handlers.
130
+ */
131
+ declare function registerClientHook<N extends ClientHookName>(name: N, handler: ClientHookHandler<N>): () => void;
132
+ /**
133
+ * Fire a client-side hook (fire-and-forget). Framework-internal — consumers
134
+ * should not call this directly. Used for `post*` hooks where any stop-signal
135
+ * returned by a handler is intentionally ignored: a `postLogin` listener that
136
+ * tried to "stop" the framework would be too late — the transition already
137
+ * happened. Use `dispatchVetoableClientHook` for `pre*` hooks where the
138
+ * caller actually awaits the veto decision.
139
+ */
140
+ declare function dispatchClientHook<N extends ClientHookName>(name: N, payload: ClientHookPayloadMap[N]): void;
141
+ /**
142
+ * Awaits every registered handler for a `pre*` client hook. If any handler
143
+ * returns a `ClientHookStopSignal`, dispatch short-circuits and returns
144
+ * `{ stopped: true, signal }`; callers must respect the veto (skip the
145
+ * pending action). Handlers that throw or reject are logged but treated as
146
+ * `undefined` — a buggy handler must not silently block the user flow.
147
+ *
148
+ * Mirrors `dispatchHook` from `@luckystack/core` so consumer/framework code
149
+ * can use the same stop-signal pattern on both sides of the wire.
150
+ */
151
+ declare function dispatchVetoableClientHook<N extends ClientHookName>(name: N, payload: ClientHookPayloadMap[N]): Promise<ClientDispatchResult>;
152
+
153
+ interface SessionContextValue<TSession extends BaseSessionLayout = BaseSessionLayout> {
154
+ session: TSession | null;
155
+ sessionLoaded: boolean;
156
+ }
157
+ declare const SessionContext: react.Context<SessionContextValue<BaseSessionLayout>>;
158
+ declare function useSession<TSession extends BaseSessionLayout = BaseSessionLayout>(): SessionContextValue<TSession>;
159
+ declare const setLatestSession: (session: BaseSessionLayout | null) => void;
160
+ declare const getCurrentSession: <TSession extends BaseSessionLayout = BaseSessionLayout>() => TSession | null;
161
+ /**
162
+ * Vetoable entry point for committing a null → session transition (i.e. a
163
+ * fresh login or a page-load session restore). Dispatches the `preLogin`
164
+ * client hook to every registered handler in parallel; if any returns a
165
+ * stop signal, the transition is aborted and `{ committed: false, signal }`
166
+ * is returned. Otherwise, `setLatestSession(session)` runs synchronously
167
+ * (which fires `postLogin`) and `{ committed: true }` comes back.
168
+ *
169
+ * Consumer's `SessionProvider` should `await` this for non-null session
170
+ * commits and roll local React state back to `null` when a veto comes
171
+ * through, so the UI doesn't render a half-logged-in state.
172
+ */
173
+ type ProposeLoginResult = {
174
+ stopped: false;
175
+ committed: true;
176
+ } | {
177
+ stopped: true;
178
+ signal: ClientHookStopSignal;
179
+ committed: false;
180
+ };
181
+ declare const proposeLogin: (session: BaseSessionLayout) => Promise<ProposeLoginResult>;
182
+
183
+ type AvatarStatus = 'avatar' | 'fallback';
184
+ interface AvatarContextType {
185
+ avatarStatuses: Record<string, AvatarStatus>;
186
+ setAvatarStatus: (key: string, status: AvatarStatus) => void;
187
+ }
188
+ declare const AvatarProvider: ({ children }: {
189
+ children: ReactNode;
190
+ }) => react.JSX.Element;
191
+ declare function useAvatarContext(): AvatarContextType;
192
+
193
+ type Theme = 'light' | 'dark';
194
+ declare const useTheme: () => {
195
+ theme: Theme;
196
+ setTheme: (next: Theme) => void;
197
+ };
198
+
199
+ type TranslationRecord = Record<string, unknown>;
200
+ declare function TranslationProvider({ children }: {
201
+ children: ReactNode;
202
+ }): react.JSX.Element;
203
+ declare function useTranslation(): TranslationRecord;
204
+ declare function useUpdateLanguage(): Dispatch<SetStateAction<string>>;
205
+ interface TranslateParam {
206
+ key: string;
207
+ value: string | number | boolean;
208
+ }
209
+ declare function translate({ translationList, key, params }: {
210
+ translationList: TranslationRecord;
211
+ key: unknown;
212
+ params?: TranslateParam[];
213
+ }): string;
214
+ declare const useTranslator: () => ({ key, params }: {
215
+ key: string;
216
+ params?: TranslateParam[];
217
+ }) => string;
218
+
219
+ interface NotifyInput {
220
+ key: string;
221
+ params?: TranslateParam[];
222
+ }
223
+ declare const notify: {
224
+ success: (input: NotifyInput) => void;
225
+ error: (input: NotifyInput) => void;
226
+ info: (input: NotifyInput) => void;
227
+ warning: (input: NotifyInput) => void;
228
+ };
229
+
230
+ declare function Middleware({ children }: {
231
+ children: ReactNode;
232
+ }): react.JSX.Element | null;
233
+
234
+ declare function useRouter(): (path: string) => Promise<void>;
235
+
236
+ export { type ApiStreamEvent, ApiTypeMap, AvatarProvider, type AvatarStatus, BaseSessionLayout, type ClientDispatchResult, type ClientHookHandler, type ClientHookName, type ClientHookPayloadMap, type ClientHookResult, type ClientHookStopSignal, type LanguageSource, type LocalesMap, Middleware, type MiddlewareHandler, type MiddlewareInput, type MiddlewareResult, type PageMiddleware, SessionContext, type SessionContextValue, StreamPayload, type Theme, type TranslateParam, TranslationProvider, type TranslationRecord, apiRequest, dispatchClientHook, dispatchVetoableClientHook, getActiveLanguage, getCurrentSession, getDefaultLocale, getLocaleByCode, getMiddlewareHandler, getPageMiddleware, getRegisteredLocales, hasPageMiddleware, notify as i18nNotify, proposeLogin, registerClientHook, registerLanguageSource, registerLocales, registerMiddlewareHandler, registerPageMiddleware, setLatestSession, translate, useAvatarContext, useRouter, useSession, useTheme, useTranslation, useTranslator, useUpdateLanguage };