@luckystack/server 0.6.7 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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).
@@ -41,4 +41,4 @@ export {
41
41
  getParsedBundles,
42
42
  getParsedPort
43
43
  };
44
- //# sourceMappingURL=chunk-RIVFTOTI.js.map
44
+ //# sourceMappingURL=chunk-X3OSC5W3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/argv.ts"],"sourcesContent":["//? Positional argv parser for the LuckyStack server boot. Replaces\n//? `LUCKYSTACK_BUNDLE` + `SERVER_PORT` env-var reads with a single shape:\n//?\n//? npm run server -- billing,vehicles 4001\n//?\n//? Arg 0 = comma-separated preset list (runtime maps merged across them).\n//? Arg 1 = listen port (numeric).\n//?\n//? `applyServerArgv()` is called by the side-effect module\n//? `@luckystack/server/parseArgv`, which consumers import as the FIRST line\n//? of their `server.ts` so the parsed port lands in `process.env.SERVER_PORT`\n//? before `config.ts` (top-level `backendUrl` const) is evaluated.\n\nexport interface ParsedServerArgv {\n bundles: string[];\n port: number | null;\n}\n\nlet parsedBundles: string[] = [];\nlet parsedPort: number | null = null;\nlet hasRun = false;\n\nconst PORT_PATTERN = /^\\d+$/;\n\nexport const parseServerArgv = (argv: string[]): ParsedServerArgv => {\n if (argv.length > 2) {\n throw new Error(\n `[luckystack:argv] unexpected positional argument(s): \"${argv.slice(2).join(' ')}\". ` +\n `Usage: npm run server -- <bundle[,bundle...]> [port]`,\n );\n }\n\n const bundles = argv[0] && argv[0].length > 0\n ? [...new Set(argv[0].split(',').map((s) => s.trim()).filter(Boolean))]\n : [];\n\n let port: number | null = null;\n const portArg = argv[1];\n if (portArg !== undefined) {\n if (!PORT_PATTERN.test(portArg)) {\n throw new Error(\n `[luckystack:argv] port argument must be numeric, got: \"${portArg}\". ` +\n `Usage: npm run server -- <bundle[,bundle...]> [port]`,\n );\n }\n port = Number.parseInt(portArg, 10);\n }\n\n return { bundles, port };\n};\n\nexport const applyServerArgv = (): void => {\n if (hasRun) return;\n hasRun = true;\n\n const result = parseServerArgv(process.argv.slice(2));\n parsedBundles = result.bundles;\n parsedPort = result.port;\n\n //? Writeback so the downstream env-readers (`core/env.ts` Zod schema,\n //? `core/bindAddress.ts` fallback, consumer `config.ts` backendUrl,\n //? `oauthProviders.ts` callback URL) see the resolved port without us\n //? having to refactor those four call sites. This is a deliberate\n //? implementation detail — argv is the public source of truth.\n if (parsedPort !== null) {\n process.env.SERVER_PORT = String(parsedPort);\n }\n};\n\nexport const getParsedBundles = (): string[] => parsedBundles;\nexport const getParsedPort = (): number | null => parsedPort;\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.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  getParsedBundles,
4
4
  getParsedPort,
5
5
  parseServerArgv
6
- } from "./chunk-RIVFTOTI.js";
6
+ } from "./chunk-X3OSC5W3.js";
7
7
 
8
8
  // src/createServer.ts
9
9
  import http from "http";
@@ -68,7 +68,7 @@ import { dispatchHook, getCookieValue, getCsrfConfig, getProjectConfig as getPro
68
68
  // src/capabilities.ts
69
69
  import { createRequire } from "module";
70
70
  var localRequire = createRequire(import.meta.url);
71
- var esmResolve = import.meta.resolve;
71
+ var importMeta = import.meta;
72
72
  var _warnedResolverMissing = false;
73
73
  var warnResolverMissingOnce = () => {
74
74
  if (_warnedResolverMissing) return;
@@ -78,9 +78,9 @@ var warnResolverMissingOnce = () => {
78
78
  );
79
79
  };
80
80
  var has = (pkg) => {
81
- if (typeof esmResolve === "function") {
81
+ if (typeof importMeta.resolve === "function") {
82
82
  try {
83
- esmResolve(pkg);
83
+ importMeta.resolve(pkg);
84
84
  return true;
85
85
  } catch {
86
86
  return false;
@@ -963,6 +963,13 @@ var resolveRequesterIp = (req) => {
963
963
  return rawRemoteAddress || trustProxy && (req.headers["x-forwarded-for"] || req.headers["x-real-ip"]) ? resolveClientIp3({ rawAddress: rawRemoteAddress, headers: req.headers, trustProxy, trustedProxyHopCount }) : void 0;
964
964
  };
965
965
 
966
+ // src/devToolsStatus.ts
967
+ var devToolsInitError = null;
968
+ var markDevToolsInitFailed = (error) => {
969
+ devToolsInitError = error;
970
+ };
971
+ var getDevToolsInitError = () => devToolsInitError;
972
+
966
973
  // src/httpRoutes/apiRoute.ts
967
974
  var handleApiRoute = async ({
968
975
  req,
@@ -1006,6 +1013,20 @@ var handleApiRoute = async ({
1006
1013
  res.end(JSON.stringify(response));
1007
1014
  return true;
1008
1015
  }
1016
+ const devToolsError = getDevToolsInitError();
1017
+ if (devToolsError) {
1018
+ const response = {
1019
+ status: "error",
1020
+ httpStatus: 503,
1021
+ message: "api.devToolsUnavailable",
1022
+ errorCode: "api.devToolsUnavailable",
1023
+ detail: `Dev tooling failed to initialize, so NO API routes are loaded. Fix the cause and RESTART the dev server (hot reload is off). Cause: ${devToolsError.message}`
1024
+ };
1025
+ res.setHeader("Content-Type", "application/json");
1026
+ res.writeHead(503);
1027
+ res.end(JSON.stringify(response));
1028
+ return true;
1029
+ }
1009
1030
  const apiData = typeof params === "object" ? { ...params } : {};
1010
1031
  delete apiData.stream;
1011
1032
  const requesterIp = resolveRequesterIp(req);
@@ -1140,6 +1161,19 @@ var handleSyncRoute = async ({
1140
1161
  res.end(JSON.stringify(response));
1141
1162
  return true;
1142
1163
  }
1164
+ const devToolsError = getDevToolsInitError();
1165
+ if (devToolsError) {
1166
+ const response = {
1167
+ status: "error",
1168
+ message: "sync.devToolsUnavailable",
1169
+ errorCode: "sync.devToolsUnavailable",
1170
+ detail: `Dev tooling failed to initialize, so NO sync routes are loaded. Fix the cause and RESTART the dev server (hot reload is off). Cause: ${devToolsError.message}`
1171
+ };
1172
+ res.setHeader("Content-Type", "application/json");
1173
+ res.writeHead(503);
1174
+ res.end(JSON.stringify(response));
1175
+ return true;
1176
+ }
1143
1177
  const syncParams = normalizeHttpSyncParams(params);
1144
1178
  const requesterIp = resolveRequesterIp(req);
1145
1179
  const result = await sync.handleHttpSyncRequest({
@@ -2296,7 +2330,11 @@ var initDevTools = async () => {
2296
2330
  devkit.setupWatchers();
2297
2331
  });
2298
2332
  if (devkitError) {
2299
- getLogger13().warn("dev tooling failed to initialize \u2014 continuing without hot reload.", { error: devkitError.message });
2333
+ markDevToolsInitFailed(devkitError);
2334
+ getLogger13().error(
2335
+ `dev tooling FAILED to initialize \u2014 the server is running but EVERY /api and /sync route will fail until this is fixed. Hot reload is OFF (the file watchers never started), so fix the cause below and RESTART the server. Cause: ${devkitError.message}`,
2336
+ devkitError
2337
+ );
2300
2338
  }
2301
2339
  };
2302
2340
  var listenLuckyStackServer = (httpServer, ip, port, callback) => new Promise((resolve, reject) => {
@@ -2314,7 +2352,7 @@ var listenLuckyStackServer = (httpServer, ip, port, callback) => new Promise((re
2314
2352
  }
2315
2353
  if (autoIncrement) {
2316
2354
  getLogger13().warn(
2317
- `Port ${String(attemptPort)} is in use \u2014 trying ${String(attemptPort + 1)} (auto-increment; set SERVER_PORT_AUTO_INCREMENT=0 to disable)`
2355
+ `Port ${String(attemptPort)} is in use \u2014 trying ${String(attemptPort + 1)} (auto-increment; set SERVER_PORT_AUTO_INCREMENT=0 to disable). A previous/zombie dev server is still holding :${String(attemptPort)}: anything pinned to that port (an old browser tab, the Vite proxy's cached target, a manual client) will keep talking to the OLD process, NOT this restart. If this restart was meant to replace it, stop the process on :${String(attemptPort)} first.`
2318
2356
  );
2319
2357
  tryListen(attemptPort + 1);
2320
2358
  return;