@luckystack/server 0.4.1 → 0.5.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 +21 -0
- package/CLAUDE.md +87 -85
- package/LICENSE +21 -21
- package/README.md +198 -198
- package/dist/{chunk-X3OSC5W3.js → chunk-RIVFTOTI.js} +1 -1
- package/dist/chunk-RIVFTOTI.js.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +61 -10
- package/dist/index.js.map +1 -1
- package/dist/parseArgv.js +1 -1
- package/dist/parseArgv.js.map +1 -1
- package/docs/argv-parsing.md +256 -256
- package/docs/create-server.md +295 -295
- package/docs/http-routes.md +311 -309
- package/docs/request-pipeline.md +198 -198
- package/docs/runtime-maps.md +206 -206
- package/docs/security-defaults.md +236 -236
- package/package.json +13 -10
- package/dist/chunk-X3OSC5W3.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,198 +1,198 @@
|
|
|
1
|
-
# @luckystack/server
|
|
2
|
-
|
|
3
|
-
> One-call server bootstrap for [LuckyStack](https://github.com/ItsLucky23/LuckyStack-v2). HTTP + Socket.io + framework routes (`/api/*`, `/sync/*`, `/_health`, `/_test/reset`, `/auth/*`, `/uploads/*`) wired together. Your `server.ts` shrinks to ~20 lines.
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install @luckystack/server @luckystack/core @luckystack/api @luckystack/login @luckystack/sync @luckystack/presence socket.io
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Quickstart — recommended (`bootstrapLuckyStack`)
|
|
12
|
-
|
|
13
|
-
`bootstrapLuckyStack` is a thin wrapper around `createLuckyStackServer` that auto-imports your `luckystack/<pkg>/*.ts` overlay files in topological order before listening, so registries (DI, hooks, providers) are populated by the time the HTTP server boots. This is what `create-luckystack-app` scaffolds.
|
|
14
|
-
|
|
15
|
-
```ts
|
|
16
|
-
import 'dotenv/config';
|
|
17
|
-
import '../config'; // your registerProjectConfig(...)
|
|
18
|
-
import '../deploy.config'; // your registerDeployConfig(...)
|
|
19
|
-
import '../services.config'; // your registerServicesConfig(...)
|
|
20
|
-
|
|
21
|
-
import { bootstrapLuckyStack } from '@luckystack/server';
|
|
22
|
-
import { serveFile, serveFavicon } from './prod/serveFile';
|
|
23
|
-
|
|
24
|
-
const server = await bootstrapLuckyStack({
|
|
25
|
-
serveFile,
|
|
26
|
-
serveFavicon,
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
await server.listen();
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
The bootstrap call runs (in order):
|
|
33
|
-
|
|
34
|
-
1. Auto-imports `luckystack/core/*.ts`, `luckystack/deploy/*.ts`, `luckystack/login/*.ts`, `luckystack/sentry/*.ts`, `luckystack/presence/*.ts`, `luckystack/docs-ui/*.ts`, `luckystack/server/*.ts` (topologically sorted, then alphabetically inside each folder).
|
|
35
|
-
2. Hands off to `createLuckyStackServer(options)`.
|
|
36
|
-
|
|
37
|
-
Set `skipOverlayLoad: true` to handle every registration yourself, or `overlayRoot: 'custom-folder'` to load from somewhere other than `./luckystack`.
|
|
38
|
-
|
|
39
|
-
### Pre-flight check — `verifyBootstrap`
|
|
40
|
-
|
|
41
|
-
Call `verifyBootstrap` **after** the overlay loads (or after your manual registrations) and **before** `server.listen()` if you want to fail fast on missing registrations instead of silent runtime crashes:
|
|
42
|
-
|
|
43
|
-
```ts
|
|
44
|
-
import { bootstrapLuckyStack, verifyBootstrap } from '@luckystack/server';
|
|
45
|
-
|
|
46
|
-
const server = await bootstrapLuckyStack({ serveFile, serveFavicon });
|
|
47
|
-
|
|
48
|
-
await verifyBootstrap({
|
|
49
|
-
requireDeployConfig: true, // single-instance deploys can omit this
|
|
50
|
-
requireServicesConfig: true, // only true when the router will run
|
|
51
|
-
requireOAuthProviders: true, // false if you only use credentials login
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
await server.listen();
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
`verifyBootstrap` throws a single descriptive `Error` listing every missing registration. The full check covers:
|
|
58
|
-
|
|
59
|
-
- `ProjectConfig` — always required.
|
|
60
|
-
- `DeployConfig` — when `requireDeployConfig: true`.
|
|
61
|
-
- `ServicesConfig` — when `requireServicesConfig: true`.
|
|
62
|
-
- OAuth providers — when `requireOAuthProviders: true` and only the default `credentials` entry has been registered.
|
|
63
|
-
- `RuntimeMapsProvider` — hard-fails in production; loud-warns in dev because devkit hot-reload normally registers one. Without it, every API/sync request silently resolves to `notFound`.
|
|
64
|
-
- `LocalizedNormalizer` — hard-fails in production; warns in dev. Without it, error responses degrade to `errorCode`-as-message (no i18n).
|
|
65
|
-
|
|
66
|
-
## Lower-level entry — `createLuckyStackServer`
|
|
67
|
-
|
|
68
|
-
Use this directly when you want to control the order of imports yourself or skip the overlay convention:
|
|
69
|
-
|
|
70
|
-
```ts
|
|
71
|
-
import { initializeSentry } from '@luckystack/error-tracking';
|
|
72
|
-
import { registerPresenceHooks } from '@luckystack/presence';
|
|
73
|
-
import { createLuckyStackServer } from '@luckystack/server';
|
|
74
|
-
|
|
75
|
-
initializeSentry();
|
|
76
|
-
registerPresenceHooks();
|
|
77
|
-
|
|
78
|
-
const server = await createLuckyStackServer({
|
|
79
|
-
serveFile,
|
|
80
|
-
serveFavicon,
|
|
81
|
-
});
|
|
82
|
-
await server.listen();
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
Both entries handle devkit hot reload + console init in dev mode automatically — opt out with `enableDevTools: false` if you have your own.
|
|
86
|
-
|
|
87
|
-
## Options
|
|
88
|
-
|
|
89
|
-
```ts
|
|
90
|
-
interface CreateLuckyStackServerOptions {
|
|
91
|
-
port?: number | string; // default: process.env.SERVER_PORT ?? 80
|
|
92
|
-
ip?: string; // default: process.env.SERVER_IP ?? '127.0.0.1'
|
|
93
|
-
serveFile?: StaticFileHandler; // catch-all for non-framework GETs (Vite output, SPA fallback)
|
|
94
|
-
serveFavicon?: FaviconHandler; // /favicon.ico
|
|
95
|
-
customRoutes?: CustomRouteHandler; // pre-fallback hook; return true to mark handled
|
|
96
|
-
enableDevTools?: boolean; // default: NODE_ENV !== 'production'
|
|
97
|
-
maxHttpBufferSize?: number; // default: 5 MB
|
|
98
|
-
}
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
`customRoutes` runs before the static file serving, so you can add project-specific HTTP endpoints (webhooks, OG image generation, etc.) without forking the package. Return `true` if you ended the response.
|
|
102
|
-
|
|
103
|
-
You can also register custom routes globally via `registerCustomRoute(handler)` — the bootstrap call composes every registered handler into the running server. This is what `@luckystack/docs-ui` uses to mount `/_docs`.
|
|
104
|
-
|
|
105
|
-
```ts
|
|
106
|
-
import { registerCustomRoute } from '@luckystack/server';
|
|
107
|
-
|
|
108
|
-
registerCustomRoute(async (req, res) => {
|
|
109
|
-
if (req.url !== '/healthz') return false;
|
|
110
|
-
res.statusCode = 200;
|
|
111
|
-
res.end('ok');
|
|
112
|
-
return true;
|
|
113
|
-
});
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
## What it wires
|
|
117
|
-
|
|
118
|
-
- **HTTP server** with CORS + security headers, OPTIONS preflight, method validation, cookie sliding, CSRF middleware.
|
|
119
|
-
- **Socket.io server** attached to the HTTP server, with optional Redis adapter when configured in deploy config.
|
|
120
|
-
- **Framework routes:** `/_health`, `/livez`, `/readyz`, `/_test/reset` (dev/test only), `/api/*` and `/sync/*` (with SSE streaming), `/auth/api`, `/auth/callback/*`, `/uploads/*`, `/assets/*`, `/csrf-token`.
|
|
121
|
-
- **Presence broadcasting** (when enabled in project config): connect / disconnect / reconnect, location updates, peer notifications.
|
|
122
|
-
- **Boot UUID** written on startup so the load-balancer (`@luckystack/router`) can detect rolling restarts.
|
|
123
|
-
- **Dev tools** in non-production: devkit hot reload, console initializer.
|
|
124
|
-
|
|
125
|
-
### HTTP route handler layout
|
|
126
|
-
|
|
127
|
-
`handleHttpRequest` is now a thin (~190-line) orchestrator that sets up CORS / origin / security headers, runs the CSRF middleware, then dispatches a route-handler table. Each handler matches its own route path and returns `boolean` (handled or not). The handlers live in `packages/server/src/httpRoutes/`:
|
|
128
|
-
|
|
129
|
-
| File | Routes |
|
|
130
|
-
| --- | --- |
|
|
131
|
-
| `csrfMiddleware.ts` + `csrfRoute.ts` | CSRF guard on writes + `GET /csrf-token` |
|
|
132
|
-
| `healthRoutes.ts` | `/livez`, `/readyz`, `/_health` |
|
|
133
|
-
| `testResetRoute.ts` | `/_test/reset` (dev/test only — see Security below) |
|
|
134
|
-
| `faviconRoute.ts` | `/favicon.ico` |
|
|
135
|
-
| `uploadsRoute.ts` | `/uploads/*` (avatar serving via `serveAvatar`) |
|
|
136
|
-
| `authApiRoute.ts` | `/auth/api/*` |
|
|
137
|
-
| `authCallbackRoute.ts` | `/auth/callback/*` |
|
|
138
|
-
| `apiRoute.ts` | `/api/*` (delegates to `@luckystack/api`'s `handleHttpApiRequest`) |
|
|
139
|
-
| `syncRoute.ts` | `/sync/*` (delegates to `@luckystack/sync`'s `handleHttpSyncRequest`) |
|
|
140
|
-
| `customRoutes.ts` | Calls every handler registered via `registerCustomRoute(...)` and the inline `customRoutes` option |
|
|
141
|
-
| `staticRoutes.ts` | Final fallback to the consumer's `serveFile(...)` (Vite SPA, etc.) |
|
|
142
|
-
|
|
143
|
-
Top-level `handleHttpRequest` + `dispatchRoutes(handlers, ctx)` are the only orchestration; everything else is a flat list of single-purpose handlers. SSE handling, error fall-through, and dispatch order are preserved.
|
|
144
|
-
|
|
145
|
-
### Security defaults that may surprise you
|
|
146
|
-
|
|
147
|
-
- **CORS fail-closed.** If neither `Origin` nor `Referer` is present, only read-only methods (GET, HEAD, OPTIONS) are allowed; state-changing methods are rejected with 403. Earlier builds fell back to `Host`, which silently bypassed CORS for non-browser callers (`curl`, server-to-server). When you `curl` a write endpoint, set `-H 'Origin: https://your-allowed-origin'`.
|
|
148
|
-
- **`/_test/reset` is fail-closed.** It requires both `NODE_ENV` to be exactly `development` or `test` AND a non-empty `TEST_RESET_TOKEN` env var. Any other state returns 403 (no silent allow-list). Wire `TEST_RESET_TOKEN` in your dev/test `.env.local` and pass it as the `x-test-reset-token: ${TOKEN}` header on the reset call. `@luckystack/test-runner`'s `resetServerState` reads the same env var.
|
|
149
|
-
- **CSRF middleware.** Writes to `/api/*` and `/sync/*` require an `x-csrf-token` header that matches the value minted on the session record (mirrored via `GET /auth/csrf`). The `apiRequest` helper in `@luckystack/core/client` attaches it automatically. Rejections dispatch the `csrfMismatch` hook before returning 403; the payload contains presence-only token info, never the value. The header name, token length, and cookie options are customisable via `registerCsrfConfig({ headerName, tokenLength, cookieOptions })` from `@luckystack/core` — see `packages/core/docs/csrf-config.md`.
|
|
150
|
-
|
|
151
|
-
## Public API
|
|
152
|
-
|
|
153
|
-
| Export | Purpose |
|
|
154
|
-
| --- | --- |
|
|
155
|
-
| `bootstrapLuckyStack(options)` | High-level entry: verifies config, auto-imports overlay, then calls `createLuckyStackServer`. |
|
|
156
|
-
| `createLuckyStackServer(options)` | Lower-level factory that returns `{ httpServer, ioServer, listen, stop, close }` (`stop`/`close` = graceful MIS-016 shutdown). |
|
|
157
|
-
| `verifyBootstrap(requirements?)` | Pre-flight check for project/deploy/services config and required env keys. |
|
|
158
|
-
| `registerCustomRoute(handler)` / `getCustomRoutes()` / `clearCustomRoutes()` | Global custom-route registry composed by `bootstrapLuckyStack`. |
|
|
159
|
-
| Hook payload types: `OnSocketConnectPayload`, `OnSocketDisconnectPayload`, `PreRoomJoinPayload`, `PostRoomJoinPayload`, `PreRoomLeavePayload`, `PostRoomLeavePayload`, `OnLocationUpdatePayload` | For socket-lifecycle hook handlers. |
|
|
160
|
-
| Types: `CreateLuckyStackServerOptions`, `BootstrapLuckyStackOptions`, `BootstrapRequirements`, `RunningLuckyStackServer`, `RouteContext`, `StaticFileHandler`, `FaviconHandler`, `CustomRouteHandler` | Handler typing. |
|
|
161
|
-
|
|
162
|
-
## Dependencies
|
|
163
|
-
|
|
164
|
-
- Runtime: `@luckystack/api`, `@luckystack/core`, `@luckystack/login`, `@luckystack/presence`, `@luckystack/sync`
|
|
165
|
-
- Peer (canonical ranges, standardized 2026-05-07):
|
|
166
|
-
- `@prisma/client@^6.19.0` (transitively required via `@luckystack/core`)
|
|
167
|
-
- `socket.io@^4.8.0`
|
|
168
|
-
- Optional peer: `@luckystack/error-tracking`, `@luckystack/email`, `@luckystack/docs-ui` — bootstrap auto-detects them but does not require them.
|
|
169
|
-
|
|
170
|
-
## Selecting bundles + port at runtime
|
|
171
|
-
|
|
172
|
-
`@luckystack/server/parseArgv` is a side-effect-only entrypoint. Import it as the **first line** of your `server.ts` so positional CLI args are parsed before any module that reads `process.env.SERVER_PORT` at load time (notably your project's `config.ts`).
|
|
173
|
-
|
|
174
|
-
```ts
|
|
175
|
-
// server.ts
|
|
176
|
-
import '@luckystack/server/parseArgv';
|
|
177
|
-
// ...rest of your bootstrap
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
Argv shape: `<bundle[,bundle...]> [port]`
|
|
181
|
-
|
|
182
|
-
```bash
|
|
183
|
-
npm run server # default preset, port 80
|
|
184
|
-
npm run server -- billing # one bundle, port 80
|
|
185
|
-
npm run server -- billing,vehicles 4001 # merge bundles, listen on 4001
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
When multiple presets are passed, the framework loads each preset's generated route map and shallow-merges `apis` / `syncs` / `functions`. Key collisions across presets throw at boot (services must own exactly one preset).
|
|
189
|
-
|
|
190
|
-
## Related architecture docs
|
|
191
|
-
|
|
192
|
-
- [`docs/ARCHITECTURE_PACKAGING.md`](../../docs/ARCHITECTURE_PACKAGING.md) — package split, multi-service builds, preset bundles.
|
|
193
|
-
- [`docs/ARCHITECTURE_SOCKET.md`](../../docs/ARCHITECTURE_SOCKET.md) — Socket.io setup, Redis adapter, room model.
|
|
194
|
-
- [`docs/HOSTING.md`](../../docs/HOSTING.md) — multi-instance deployment, `@luckystack/router`, health probes.
|
|
195
|
-
|
|
196
|
-
## License
|
|
197
|
-
|
|
198
|
-
MIT — see [LICENSE](../../LICENSE).
|
|
1
|
+
# @luckystack/server
|
|
2
|
+
|
|
3
|
+
> One-call server bootstrap for [LuckyStack](https://github.com/ItsLucky23/LuckyStack-v2). HTTP + Socket.io + framework routes (`/api/*`, `/sync/*`, `/_health`, `/_test/reset`, `/auth/*`, `/uploads/*`) wired together. Your `server.ts` shrinks to ~20 lines.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @luckystack/server @luckystack/core @luckystack/api @luckystack/login @luckystack/sync @luckystack/presence socket.io
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quickstart — recommended (`bootstrapLuckyStack`)
|
|
12
|
+
|
|
13
|
+
`bootstrapLuckyStack` is a thin wrapper around `createLuckyStackServer` that auto-imports your `luckystack/<pkg>/*.ts` overlay files in topological order before listening, so registries (DI, hooks, providers) are populated by the time the HTTP server boots. This is what `create-luckystack-app` scaffolds.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import 'dotenv/config';
|
|
17
|
+
import '../config'; // your registerProjectConfig(...)
|
|
18
|
+
import '../deploy.config'; // your registerDeployConfig(...)
|
|
19
|
+
import '../services.config'; // your registerServicesConfig(...)
|
|
20
|
+
|
|
21
|
+
import { bootstrapLuckyStack } from '@luckystack/server';
|
|
22
|
+
import { serveFile, serveFavicon } from './prod/serveFile';
|
|
23
|
+
|
|
24
|
+
const server = await bootstrapLuckyStack({
|
|
25
|
+
serveFile,
|
|
26
|
+
serveFavicon,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
await server.listen();
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The bootstrap call runs (in order):
|
|
33
|
+
|
|
34
|
+
1. Auto-imports `luckystack/core/*.ts`, `luckystack/deploy/*.ts`, `luckystack/login/*.ts`, `luckystack/sentry/*.ts`, `luckystack/presence/*.ts`, `luckystack/docs-ui/*.ts`, `luckystack/server/*.ts` (topologically sorted, then alphabetically inside each folder).
|
|
35
|
+
2. Hands off to `createLuckyStackServer(options)`.
|
|
36
|
+
|
|
37
|
+
Set `skipOverlayLoad: true` to handle every registration yourself, or `overlayRoot: 'custom-folder'` to load from somewhere other than `./luckystack`.
|
|
38
|
+
|
|
39
|
+
### Pre-flight check — `verifyBootstrap`
|
|
40
|
+
|
|
41
|
+
Call `verifyBootstrap` **after** the overlay loads (or after your manual registrations) and **before** `server.listen()` if you want to fail fast on missing registrations instead of silent runtime crashes:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { bootstrapLuckyStack, verifyBootstrap } from '@luckystack/server';
|
|
45
|
+
|
|
46
|
+
const server = await bootstrapLuckyStack({ serveFile, serveFavicon });
|
|
47
|
+
|
|
48
|
+
await verifyBootstrap({
|
|
49
|
+
requireDeployConfig: true, // single-instance deploys can omit this
|
|
50
|
+
requireServicesConfig: true, // only true when the router will run
|
|
51
|
+
requireOAuthProviders: true, // false if you only use credentials login
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
await server.listen();
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`verifyBootstrap` throws a single descriptive `Error` listing every missing registration. The full check covers:
|
|
58
|
+
|
|
59
|
+
- `ProjectConfig` — always required.
|
|
60
|
+
- `DeployConfig` — when `requireDeployConfig: true`.
|
|
61
|
+
- `ServicesConfig` — when `requireServicesConfig: true`.
|
|
62
|
+
- OAuth providers — when `requireOAuthProviders: true` and only the default `credentials` entry has been registered.
|
|
63
|
+
- `RuntimeMapsProvider` — hard-fails in production; loud-warns in dev because devkit hot-reload normally registers one. Without it, every API/sync request silently resolves to `notFound`.
|
|
64
|
+
- `LocalizedNormalizer` — hard-fails in production; warns in dev. Without it, error responses degrade to `errorCode`-as-message (no i18n).
|
|
65
|
+
|
|
66
|
+
## Lower-level entry — `createLuckyStackServer`
|
|
67
|
+
|
|
68
|
+
Use this directly when you want to control the order of imports yourself or skip the overlay convention:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { initializeSentry } from '@luckystack/error-tracking';
|
|
72
|
+
import { registerPresenceHooks } from '@luckystack/presence';
|
|
73
|
+
import { createLuckyStackServer } from '@luckystack/server';
|
|
74
|
+
|
|
75
|
+
initializeSentry();
|
|
76
|
+
registerPresenceHooks();
|
|
77
|
+
|
|
78
|
+
const server = await createLuckyStackServer({
|
|
79
|
+
serveFile,
|
|
80
|
+
serveFavicon,
|
|
81
|
+
});
|
|
82
|
+
await server.listen();
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Both entries handle devkit hot reload + console init in dev mode automatically — opt out with `enableDevTools: false` if you have your own.
|
|
86
|
+
|
|
87
|
+
## Options
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
interface CreateLuckyStackServerOptions {
|
|
91
|
+
port?: number | string; // default: process.env.SERVER_PORT ?? 80
|
|
92
|
+
ip?: string; // default: process.env.SERVER_IP ?? '127.0.0.1'
|
|
93
|
+
serveFile?: StaticFileHandler; // catch-all for non-framework GETs (Vite output, SPA fallback)
|
|
94
|
+
serveFavicon?: FaviconHandler; // /favicon.ico
|
|
95
|
+
customRoutes?: CustomRouteHandler; // pre-fallback hook; return true to mark handled
|
|
96
|
+
enableDevTools?: boolean; // default: NODE_ENV !== 'production'
|
|
97
|
+
maxHttpBufferSize?: number; // default: 5 MB
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`customRoutes` runs before the static file serving, so you can add project-specific HTTP endpoints (webhooks, OG image generation, etc.) without forking the package. Return `true` if you ended the response.
|
|
102
|
+
|
|
103
|
+
You can also register custom routes globally via `registerCustomRoute(handler)` — the bootstrap call composes every registered handler into the running server. This is what `@luckystack/docs-ui` uses to mount `/_docs`.
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import { registerCustomRoute } from '@luckystack/server';
|
|
107
|
+
|
|
108
|
+
registerCustomRoute(async (req, res) => {
|
|
109
|
+
if (req.url !== '/healthz') return false;
|
|
110
|
+
res.statusCode = 200;
|
|
111
|
+
res.end('ok');
|
|
112
|
+
return true;
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## What it wires
|
|
117
|
+
|
|
118
|
+
- **HTTP server** with CORS + security headers, OPTIONS preflight, method validation, cookie sliding, CSRF middleware.
|
|
119
|
+
- **Socket.io server** attached to the HTTP server, with optional Redis adapter when configured in deploy config.
|
|
120
|
+
- **Framework routes:** `/_health`, `/livez`, `/readyz`, `/_test/reset` (dev/test only), `/api/*` and `/sync/*` (with SSE streaming), `/auth/api`, `/auth/callback/*`, `/uploads/*`, `/assets/*`, `/csrf-token`.
|
|
121
|
+
- **Presence broadcasting** (when enabled in project config): connect / disconnect / reconnect, location updates, peer notifications.
|
|
122
|
+
- **Boot UUID** written on startup so the load-balancer (`@luckystack/router`) can detect rolling restarts.
|
|
123
|
+
- **Dev tools** in non-production: devkit hot reload, console initializer.
|
|
124
|
+
|
|
125
|
+
### HTTP route handler layout
|
|
126
|
+
|
|
127
|
+
`handleHttpRequest` is now a thin (~190-line) orchestrator that sets up CORS / origin / security headers, runs the CSRF middleware, then dispatches a route-handler table. Each handler matches its own route path and returns `boolean` (handled or not). The handlers live in `packages/server/src/httpRoutes/`:
|
|
128
|
+
|
|
129
|
+
| File | Routes |
|
|
130
|
+
| --- | --- |
|
|
131
|
+
| `csrfMiddleware.ts` + `csrfRoute.ts` | CSRF guard on writes + `GET /csrf-token` |
|
|
132
|
+
| `healthRoutes.ts` | `/livez`, `/readyz`, `/_health` |
|
|
133
|
+
| `testResetRoute.ts` | `/_test/reset` (dev/test only — see Security below) |
|
|
134
|
+
| `faviconRoute.ts` | `/favicon.ico` |
|
|
135
|
+
| `uploadsRoute.ts` | `/uploads/*` (avatar serving via `serveAvatar`) |
|
|
136
|
+
| `authApiRoute.ts` | `/auth/api/*` |
|
|
137
|
+
| `authCallbackRoute.ts` | `/auth/callback/*` |
|
|
138
|
+
| `apiRoute.ts` | `/api/*` (delegates to `@luckystack/api`'s `handleHttpApiRequest`) |
|
|
139
|
+
| `syncRoute.ts` | `/sync/*` (delegates to `@luckystack/sync`'s `handleHttpSyncRequest`) |
|
|
140
|
+
| `customRoutes.ts` | Calls every handler registered via `registerCustomRoute(...)` and the inline `customRoutes` option |
|
|
141
|
+
| `staticRoutes.ts` | Final fallback to the consumer's `serveFile(...)` (Vite SPA, etc.) |
|
|
142
|
+
|
|
143
|
+
Top-level `handleHttpRequest` + `dispatchRoutes(handlers, ctx)` are the only orchestration; everything else is a flat list of single-purpose handlers. SSE handling, error fall-through, and dispatch order are preserved.
|
|
144
|
+
|
|
145
|
+
### Security defaults that may surprise you
|
|
146
|
+
|
|
147
|
+
- **CORS fail-closed.** If neither `Origin` nor `Referer` is present, only read-only methods (GET, HEAD, OPTIONS) are allowed; state-changing methods are rejected with 403. Earlier builds fell back to `Host`, which silently bypassed CORS for non-browser callers (`curl`, server-to-server). When you `curl` a write endpoint, set `-H 'Origin: https://your-allowed-origin'`.
|
|
148
|
+
- **`/_test/reset` is fail-closed.** It requires both `NODE_ENV` to be exactly `development` or `test` AND a non-empty `TEST_RESET_TOKEN` env var. Any other state returns 403 (no silent allow-list). Wire `TEST_RESET_TOKEN` in your dev/test `.env.local` and pass it as the `x-test-reset-token: ${TOKEN}` header on the reset call. `@luckystack/test-runner`'s `resetServerState` reads the same env var.
|
|
149
|
+
- **CSRF middleware.** Writes to `/api/*` and `/sync/*` require an `x-csrf-token` header that matches the value minted on the session record (mirrored via `GET /auth/csrf`). The `apiRequest` helper in `@luckystack/core/client` attaches it automatically. Rejections dispatch the `csrfMismatch` hook before returning 403; the payload contains presence-only token info, never the value. The header name, token length, and cookie options are customisable via `registerCsrfConfig({ headerName, tokenLength, cookieOptions })` from `@luckystack/core` — see `packages/core/docs/csrf-config.md`.
|
|
150
|
+
|
|
151
|
+
## Public API
|
|
152
|
+
|
|
153
|
+
| Export | Purpose |
|
|
154
|
+
| --- | --- |
|
|
155
|
+
| `bootstrapLuckyStack(options)` | High-level entry: verifies config, auto-imports overlay, then calls `createLuckyStackServer`. |
|
|
156
|
+
| `createLuckyStackServer(options)` | Lower-level factory that returns `{ httpServer, ioServer, listen, stop, close }` (`stop`/`close` = graceful MIS-016 shutdown). |
|
|
157
|
+
| `verifyBootstrap(requirements?)` | Pre-flight check for project/deploy/services config and required env keys. |
|
|
158
|
+
| `registerCustomRoute(handler)` / `getCustomRoutes()` / `clearCustomRoutes()` | Global custom-route registry composed by `bootstrapLuckyStack`. |
|
|
159
|
+
| Hook payload types: `OnSocketConnectPayload`, `OnSocketDisconnectPayload`, `PreRoomJoinPayload`, `PostRoomJoinPayload`, `PreRoomLeavePayload`, `PostRoomLeavePayload`, `OnLocationUpdatePayload` | For socket-lifecycle hook handlers. |
|
|
160
|
+
| Types: `CreateLuckyStackServerOptions`, `BootstrapLuckyStackOptions`, `BootstrapRequirements`, `RunningLuckyStackServer`, `RouteContext`, `StaticFileHandler`, `FaviconHandler`, `CustomRouteHandler` | Handler typing. |
|
|
161
|
+
|
|
162
|
+
## Dependencies
|
|
163
|
+
|
|
164
|
+
- Runtime: `@luckystack/api`, `@luckystack/core`, `@luckystack/login`, `@luckystack/presence`, `@luckystack/sync`
|
|
165
|
+
- Peer (canonical ranges, standardized 2026-05-07):
|
|
166
|
+
- `@prisma/client@^6.19.0` (transitively required via `@luckystack/core`)
|
|
167
|
+
- `socket.io@^4.8.0`
|
|
168
|
+
- Optional peer: `@luckystack/error-tracking`, `@luckystack/email`, `@luckystack/docs-ui` — bootstrap auto-detects them but does not require them.
|
|
169
|
+
|
|
170
|
+
## Selecting bundles + port at runtime
|
|
171
|
+
|
|
172
|
+
`@luckystack/server/parseArgv` is a side-effect-only entrypoint. Import it as the **first line** of your `server.ts` so positional CLI args are parsed before any module that reads `process.env.SERVER_PORT` at load time (notably your project's `config.ts`).
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
// server.ts
|
|
176
|
+
import '@luckystack/server/parseArgv';
|
|
177
|
+
// ...rest of your bootstrap
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Argv shape: `<bundle[,bundle...]> [port]`
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
npm run server # default preset, port 80
|
|
184
|
+
npm run server -- billing # one bundle, port 80
|
|
185
|
+
npm run server -- billing,vehicles 4001 # merge bundles, listen on 4001
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
When multiple presets are passed, the framework loads each preset's generated route map and shallow-merges `apis` / `syncs` / `functions`. Key collisions across presets throw at boot (services must own exactly one preset).
|
|
189
|
+
|
|
190
|
+
## Related architecture docs
|
|
191
|
+
|
|
192
|
+
- [`docs/ARCHITECTURE_PACKAGING.md`](../../docs/ARCHITECTURE_PACKAGING.md) — package split, multi-service builds, preset bundles.
|
|
193
|
+
- [`docs/ARCHITECTURE_SOCKET.md`](../../docs/ARCHITECTURE_SOCKET.md) — Socket.io setup, Redis adapter, room model.
|
|
194
|
+
- [`docs/HOSTING.md`](../../docs/HOSTING.md) — multi-instance deployment, `@luckystack/router`, health probes.
|
|
195
|
+
|
|
196
|
+
## License
|
|
197
|
+
|
|
198
|
+
MIT — see [LICENSE](../../LICENSE).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/argv.ts"],"sourcesContent":["//? Positional argv parser for the LuckyStack server boot. Replaces\r\n//? `LUCKYSTACK_BUNDLE` + `SERVER_PORT` env-var reads with a single shape:\r\n//?\r\n//? npm run server -- billing,vehicles 4001\r\n//?\r\n//? Arg 0 = comma-separated preset list (runtime maps merged across them).\r\n//? Arg 1 = listen port (numeric).\r\n//?\r\n//? `applyServerArgv()` is called by the side-effect module\r\n//? `@luckystack/server/parseArgv`, which consumers import as the FIRST line\r\n//? of their `server.ts` so the parsed port lands in `process.env.SERVER_PORT`\r\n//? before `config.ts` (top-level `backendUrl` const) is evaluated.\r\n\r\nexport interface ParsedServerArgv {\r\n bundles: string[];\r\n port: number | null;\r\n}\r\n\r\nlet parsedBundles: string[] = [];\r\nlet parsedPort: number | null = null;\r\nlet hasRun = false;\r\n\r\nconst PORT_PATTERN = /^\\d+$/;\r\n\r\nexport const parseServerArgv = (argv: string[]): ParsedServerArgv => {\r\n if (argv.length > 2) {\r\n throw new Error(\r\n `[luckystack:argv] unexpected positional argument(s): \"${argv.slice(2).join(' ')}\". ` +\r\n `Usage: npm run server -- <bundle[,bundle...]> [port]`,\r\n );\r\n }\r\n\r\n const bundles = argv[0] && argv[0].length > 0\r\n ? [...new Set(argv[0].split(',').map((s) => s.trim()).filter(Boolean))]\r\n : [];\r\n\r\n let port: number | null = null;\r\n const portArg = argv[1];\r\n if (portArg !== undefined) {\r\n if (!PORT_PATTERN.test(portArg)) {\r\n throw new Error(\r\n `[luckystack:argv] port argument must be numeric, got: \"${portArg}\". ` +\r\n `Usage: npm run server -- <bundle[,bundle...]> [port]`,\r\n );\r\n }\r\n port = Number.parseInt(portArg, 10);\r\n }\r\n\r\n return { bundles, port };\r\n};\r\n\r\nexport const applyServerArgv = (): void => {\r\n if (hasRun) return;\r\n hasRun = true;\r\n\r\n const result = parseServerArgv(process.argv.slice(2));\r\n parsedBundles = result.bundles;\r\n parsedPort = result.port;\r\n\r\n //? Writeback so the downstream env-readers (`core/env.ts` Zod schema,\r\n //? `core/bindAddress.ts` fallback, consumer `config.ts` backendUrl,\r\n //? `oauthProviders.ts` callback URL) see the resolved port without us\r\n //? having to refactor those four call sites. This is a deliberate\r\n //? implementation detail — argv is the public source of truth.\r\n if (parsedPort !== null) {\r\n process.env.SERVER_PORT = String(parsedPort);\r\n }\r\n};\r\n\r\nexport const getParsedBundles = (): string[] => parsedBundles;\r\nexport const getParsedPort = (): number | null => parsedPort;\r\n"],"mappings":";AAkBA,IAAI,gBAA0B,CAAC;AAC/B,IAAI,aAA4B;AAChC,IAAI,SAAS;AAEb,IAAM,eAAe;AAEd,IAAM,kBAAkB,CAAC,SAAqC;AACnE,MAAI,KAAK,SAAS,GAAG;AACnB,UAAM,IAAI;AAAA,MACR,yDAAyD,KAAK,MAAM,CAAC,EAAE,KAAK,GAAG,CAAC;AAAA,IAElF;AAAA,EACF;AAEA,QAAM,UAAU,KAAK,CAAC,KAAK,KAAK,CAAC,EAAE,SAAS,IACxC,CAAC,GAAG,IAAI,IAAI,KAAK,CAAC,EAAE,MAAM,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,OAAO,CAAC,CAAC,IACpE,CAAC;AAEL,MAAI,OAAsB;AAC1B,QAAM,UAAU,KAAK,CAAC;AACtB,MAAI,YAAY,QAAW;AACzB,QAAI,CAAC,aAAa,KAAK,OAAO,GAAG;AAC/B,YAAM,IAAI;AAAA,QACR,0DAA0D,OAAO;AAAA,MAEnE;AAAA,IACF;AACA,WAAO,OAAO,SAAS,SAAS,EAAE;AAAA,EACpC;AAEA,SAAO,EAAE,SAAS,KAAK;AACzB;AAEO,IAAM,kBAAkB,MAAY;AACzC,MAAI,OAAQ;AACZ,WAAS;AAET,QAAM,SAAS,gBAAgB,QAAQ,KAAK,MAAM,CAAC,CAAC;AACpD,kBAAgB,OAAO;AACvB,eAAa,OAAO;AAOpB,MAAI,eAAe,MAAM;AACvB,YAAQ,IAAI,cAAc,OAAO,UAAU;AAAA,EAC7C;AACF;AAEO,IAAM,mBAAmB,MAAgB;AACzC,IAAM,gBAAgB,MAAqB;","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -196,6 +196,9 @@ interface BootstrapLuckyStackOptions extends CreateLuckyStackServerOptions {
|
|
|
196
196
|
*/
|
|
197
197
|
skipOverlayLoad?: boolean;
|
|
198
198
|
}
|
|
199
|
+
declare const OVERLAY_ORDER: string[];
|
|
200
|
+
type OverlayLoader = () => Promise<void>;
|
|
201
|
+
declare const registerOverlayLoader: (loader: OverlayLoader) => void;
|
|
199
202
|
declare const bootstrapLuckyStack: (options?: BootstrapLuckyStackOptions) => Promise<RunningLuckyStackServer>;
|
|
200
203
|
|
|
201
204
|
interface BootstrapRequirements {
|
|
@@ -307,4 +310,4 @@ declare const registerSecurityHeaders: (builder: SecurityHeadersBuilder | null)
|
|
|
307
310
|
/** Read the active builder (or null). */
|
|
308
311
|
declare const getSecurityHeadersBuilder: () => SecurityHeadersBuilder | null;
|
|
309
312
|
|
|
310
|
-
export { type BootstrapLuckyStackOptions, type BootstrapRequirements, type CreateLuckyStackServerOptions, type CustomRouteHandler, type CustomRoutePhase, type FaviconHandler, type GeneratedRuntimeMapsModule, type OnLocationUpdatePayload, type OnSocketConnectPayload, type OnSocketDisconnectPayload, type OriginExemptMatcher, type ParsedServerArgv, type PostRoomJoinPayload, type PostRoomLeavePayload, type PreRoomJoinPayload, type PreRoomLeavePayload, type ProdRuntimeMapsLoaderOptions, type RegisterCustomRouteOptions, type RouteContext, type RunningLuckyStackServer, type SecurityHeadersBuilder, type StaticFileHandler, type StopLuckyStackServerOptions, applyServerArgv, bootstrapLuckyStack, clearCustomRoutes, clearOriginExemptPaths, createLuckyStackServer, createProdRuntimeMapsProvider, getCustomRoutes, getOriginExemptPaths, getParsedBundles, getParsedPort, getPreParamsCustomRoutes, getSecurityHeadersBuilder, isOriginExemptPath, parseServerArgv, registerCustomRoute, registerOriginExemptPath, registerProdRuntimeMapsProvider, registerSecurityHeaders, verifyBootstrap };
|
|
313
|
+
export { type BootstrapLuckyStackOptions, type BootstrapRequirements, type CreateLuckyStackServerOptions, type CustomRouteHandler, type CustomRoutePhase, type FaviconHandler, type GeneratedRuntimeMapsModule, OVERLAY_ORDER, type OnLocationUpdatePayload, type OnSocketConnectPayload, type OnSocketDisconnectPayload, type OriginExemptMatcher, type ParsedServerArgv, type PostRoomJoinPayload, type PostRoomLeavePayload, type PreRoomJoinPayload, type PreRoomLeavePayload, type ProdRuntimeMapsLoaderOptions, type RegisterCustomRouteOptions, type RouteContext, type RunningLuckyStackServer, type SecurityHeadersBuilder, type StaticFileHandler, type StopLuckyStackServerOptions, applyServerArgv, bootstrapLuckyStack, clearCustomRoutes, clearOriginExemptPaths, createLuckyStackServer, createProdRuntimeMapsProvider, getCustomRoutes, getOriginExemptPaths, getParsedBundles, getParsedPort, getPreParamsCustomRoutes, getSecurityHeadersBuilder, isOriginExemptPath, parseServerArgv, registerCustomRoute, registerOriginExemptPath, registerOverlayLoader, registerProdRuntimeMapsProvider, registerSecurityHeaders, verifyBootstrap };
|
package/dist/index.js
CHANGED
|
@@ -3,11 +3,11 @@ import {
|
|
|
3
3
|
getParsedBundles,
|
|
4
4
|
getParsedPort,
|
|
5
5
|
parseServerArgv
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-RIVFTOTI.js";
|
|
7
7
|
|
|
8
8
|
// src/createServer.ts
|
|
9
9
|
import http from "http";
|
|
10
|
-
import { registerBindAddress, writeBootUuid, getLogger as getLogger13, getProjectConfig as getProjectConfig13, tryCatch as tryCatch10, isProduction as isProduction2, resolveEnvKey as resolveEnvKey5 } from "@luckystack/core";
|
|
10
|
+
import { registerBindAddress, writeBootUuid, getLogger as getLogger13, getProjectConfig as getProjectConfig13, tryCatch as tryCatch10, isProduction as isProduction2, resolveEnvKey as resolveEnvKey5, dispatchHook as dispatchHook8 } from "@luckystack/core";
|
|
11
11
|
|
|
12
12
|
// src/httpHandler.ts
|
|
13
13
|
import { randomUUID } from "crypto";
|
|
@@ -107,6 +107,7 @@ var OPTIONAL_PACKAGES = [
|
|
|
107
107
|
"email",
|
|
108
108
|
"error-tracking",
|
|
109
109
|
"presence",
|
|
110
|
+
"cron",
|
|
110
111
|
"docs-ui"
|
|
111
112
|
];
|
|
112
113
|
var canResolve = (specifier) => has(specifier);
|
|
@@ -281,7 +282,10 @@ var handleFaviconRoute = async ({ res, routePath, options }) => {
|
|
|
281
282
|
import {
|
|
282
283
|
computeSynchronizedEnvHashes,
|
|
283
284
|
describeHealthHashConfig,
|
|
285
|
+
getDbHealthCheck,
|
|
284
286
|
getProjectConfig as getProjectConfig3,
|
|
287
|
+
isPrismaClientRegistered,
|
|
288
|
+
isPrismaClientResolvable,
|
|
285
289
|
prisma,
|
|
286
290
|
readBootUuid,
|
|
287
291
|
redis,
|
|
@@ -302,6 +306,16 @@ var pingPrisma = async () => {
|
|
|
302
306
|
}
|
|
303
307
|
return false;
|
|
304
308
|
};
|
|
309
|
+
var checkDatabaseReady = async () => {
|
|
310
|
+
const registered = getDbHealthCheck();
|
|
311
|
+
if (registered) {
|
|
312
|
+
const [error, result] = await tryCatch(async () => registered());
|
|
313
|
+
if (error || result === null) return false;
|
|
314
|
+
return result;
|
|
315
|
+
}
|
|
316
|
+
if (isPrismaClientRegistered() || isPrismaClientResolvable()) return pingPrisma();
|
|
317
|
+
return "skipped";
|
|
318
|
+
};
|
|
305
319
|
var handleLivezRoute = ({ res, routePath }) => {
|
|
306
320
|
if (routePath !== getProjectConfig3().http.liveEndpoint) return Promise.resolve(false);
|
|
307
321
|
res.statusCode = 200;
|
|
@@ -314,13 +328,21 @@ var handleReadyzRoute = async ({ res, routePath }) => {
|
|
|
314
328
|
const bootUuid = await readBootUuid();
|
|
315
329
|
const [redisError, pong] = await tryCatch(() => redis.ping());
|
|
316
330
|
const redisOk = !redisError && (pong === "PONG" || Boolean(pong));
|
|
317
|
-
const
|
|
318
|
-
const ready = Boolean(bootUuid) && redisOk &&
|
|
331
|
+
const databaseResult = await checkDatabaseReady();
|
|
332
|
+
const ready = Boolean(bootUuid) && redisOk && databaseResult !== false;
|
|
319
333
|
res.statusCode = ready ? 200 : 503;
|
|
320
334
|
res.setHeader("Content-Type", "application/json");
|
|
321
335
|
res.end(JSON.stringify({
|
|
322
336
|
status: ready ? "ready" : "not-ready",
|
|
323
|
-
|
|
337
|
+
//? `prisma` kept for backward compatibility with existing probes/dashboards
|
|
338
|
+
//? (true when the database check passed OR was deliberately skipped);
|
|
339
|
+
//? `database` carries the richer tri-state.
|
|
340
|
+
checks: {
|
|
341
|
+
bootUuid: Boolean(bootUuid),
|
|
342
|
+
redis: redisOk,
|
|
343
|
+
database: databaseResult,
|
|
344
|
+
prisma: databaseResult !== false
|
|
345
|
+
}
|
|
324
346
|
}));
|
|
325
347
|
return true;
|
|
326
348
|
};
|
|
@@ -2081,8 +2103,21 @@ var clearDevServerInfo = () => {
|
|
|
2081
2103
|
var initDevTools = async () => {
|
|
2082
2104
|
const { initConsolelog } = await import("@luckystack/core");
|
|
2083
2105
|
initConsolelog();
|
|
2084
|
-
|
|
2085
|
-
|
|
2106
|
+
const devSignalExit = (reason) => {
|
|
2107
|
+
void Promise.race([
|
|
2108
|
+
dispatchHook8("preServerStop", { reason, timeoutMs: 2e3 }),
|
|
2109
|
+
new Promise((resolve) => {
|
|
2110
|
+
setTimeout(resolve, 2e3);
|
|
2111
|
+
})
|
|
2112
|
+
// eslint-disable-next-line unicorn/no-process-exit -- deliberate dev hard-exit once the bounded hook window closes (mirrors the original inline handler)
|
|
2113
|
+
]).finally(() => process.exit(0));
|
|
2114
|
+
};
|
|
2115
|
+
process.once("SIGINT", () => {
|
|
2116
|
+
devSignalExit("SIGINT");
|
|
2117
|
+
});
|
|
2118
|
+
process.once("SIGTERM", () => {
|
|
2119
|
+
devSignalExit("SIGTERM");
|
|
2120
|
+
});
|
|
2086
2121
|
const devkitModuleId = "@luckystack/devkit";
|
|
2087
2122
|
if (!canResolve(devkitModuleId)) {
|
|
2088
2123
|
getLogger13().warn(
|
|
@@ -2207,7 +2242,7 @@ var createLuckyStackServer = async (options = {}) => {
|
|
|
2207
2242
|
import path3 from "path";
|
|
2208
2243
|
import fs2 from "fs";
|
|
2209
2244
|
import { pathToFileURL } from "url";
|
|
2210
|
-
import { ROOT_DIR } from "@luckystack/core";
|
|
2245
|
+
import { ROOT_DIR, tryCatch as tryCatch11 } from "@luckystack/core";
|
|
2211
2246
|
var OVERLAY_ORDER = [
|
|
2212
2247
|
// Core registries first — clients, paths, routing rules. Anything below
|
|
2213
2248
|
// depends on these being in place.
|
|
@@ -2222,6 +2257,9 @@ var OVERLAY_ORDER = [
|
|
|
2222
2257
|
// Sentry / docs-ui / presence sit on top of core but don't block boot.
|
|
2223
2258
|
"sentry",
|
|
2224
2259
|
"presence",
|
|
2260
|
+
// Cron jobs — `registerCronJob(...)` calls in `luckystack/cron/*.ts` lazily
|
|
2261
|
+
// start the leader-elected scheduler (optional @luckystack/cron).
|
|
2262
|
+
"cron",
|
|
2225
2263
|
"docs-ui",
|
|
2226
2264
|
// Server overlay last — typically empty, but a place to wire framework
|
|
2227
2265
|
// hooks (`registerHook('onSocketConnect', ...)` etc.) before listen().
|
|
@@ -2229,7 +2267,18 @@ var OVERLAY_ORDER = [
|
|
|
2229
2267
|
];
|
|
2230
2268
|
var importIfExists = async (filePath) => {
|
|
2231
2269
|
if (!fs2.existsSync(filePath)) return;
|
|
2232
|
-
await
|
|
2270
|
+
const [error] = await tryCatch11(async () => {
|
|
2271
|
+
await import(pathToFileURL(filePath).href);
|
|
2272
|
+
});
|
|
2273
|
+
if (error) {
|
|
2274
|
+
throw new Error(
|
|
2275
|
+
`[luckystack] failed to load overlay file ${filePath}: ${error.message}. If you removed an optional package (e.g. via \`luckystack remove <feature>\` or \`luckystack manage\`), delete or update the overlay files that still import it (the \`luckystack/<feature>/\` folder).`
|
|
2276
|
+
);
|
|
2277
|
+
}
|
|
2278
|
+
};
|
|
2279
|
+
var registeredOverlayLoader = null;
|
|
2280
|
+
var registerOverlayLoader = (loader) => {
|
|
2281
|
+
registeredOverlayLoader = loader;
|
|
2233
2282
|
};
|
|
2234
2283
|
var loadOverlayFolder = async (overlayRoot) => {
|
|
2235
2284
|
const overlayAbs = path3.isAbsolute(overlayRoot) ? overlayRoot : path3.join(ROOT_DIR, overlayRoot);
|
|
@@ -2268,7 +2317,7 @@ var bootstrapLuckyStack = async (options = {}) => {
|
|
|
2268
2317
|
const overlayRoot = options.overlayRoot ?? "luckystack";
|
|
2269
2318
|
if (!options.skipOverlayLoad) {
|
|
2270
2319
|
await importOptionalPackageRegisters();
|
|
2271
|
-
await loadOverlayFolder(overlayRoot);
|
|
2320
|
+
await (registeredOverlayLoader ? registeredOverlayLoader() : loadOverlayFolder(overlayRoot));
|
|
2272
2321
|
}
|
|
2273
2322
|
await getLogin();
|
|
2274
2323
|
const server = await createLuckyStackServer(options);
|
|
@@ -2282,6 +2331,7 @@ import {
|
|
|
2282
2331
|
applyErrorFormatter
|
|
2283
2332
|
} from "@luckystack/core";
|
|
2284
2333
|
export {
|
|
2334
|
+
OVERLAY_ORDER,
|
|
2285
2335
|
applyServerArgv,
|
|
2286
2336
|
bootstrapLuckyStack,
|
|
2287
2337
|
clearCustomRoutes,
|
|
@@ -2300,6 +2350,7 @@ export {
|
|
|
2300
2350
|
registerCustomRoute,
|
|
2301
2351
|
registerErrorFormatter,
|
|
2302
2352
|
registerOriginExemptPath,
|
|
2353
|
+
registerOverlayLoader,
|
|
2303
2354
|
registerProdRuntimeMapsProvider,
|
|
2304
2355
|
registerSecurityHeaders,
|
|
2305
2356
|
verifyBootstrap
|