@luckystack/server 0.10.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/AGENTS.md +7 -0
  2. package/CHANGELOG.md +11 -1
  3. package/README.md +2 -200
  4. package/dist/afterListen.d.ts +2 -0
  5. package/dist/argv.d.ts +8 -0
  6. package/dist/bootstrap.d.ts +36 -0
  7. package/dist/capabilities.d.ts +10 -0
  8. package/dist/createServer.d.ts +4 -0
  9. package/dist/customRoutesRegistry.d.ts +9 -0
  10. package/dist/devServerInfo.d.ts +7 -0
  11. package/dist/devToolsStatus.d.ts +3 -0
  12. package/dist/errorFormatterRegistry.d.ts +2 -0
  13. package/dist/hookPayloads.d.ts +62 -0
  14. package/dist/httpHandler.d.ts +3 -0
  15. package/dist/httpRoutes/apiRoute.d.ts +2 -0
  16. package/dist/httpRoutes/authApiRoute.d.ts +2 -0
  17. package/dist/httpRoutes/authCallbackRoute.d.ts +2 -0
  18. package/dist/httpRoutes/authLogoutRoute.d.ts +2 -0
  19. package/dist/httpRoutes/authProvidersRoute.d.ts +2 -0
  20. package/dist/httpRoutes/authSecondFactorRoutes.d.ts +3 -0
  21. package/dist/httpRoutes/csrfMiddleware.d.ts +8 -0
  22. package/dist/httpRoutes/csrfRoute.d.ts +2 -0
  23. package/dist/httpRoutes/customRoutes.d.ts +3 -0
  24. package/dist/httpRoutes/faviconRoute.d.ts +2 -0
  25. package/dist/httpRoutes/healthRoutes.d.ts +4 -0
  26. package/dist/httpRoutes/resolveRequesterIp.d.ts +27 -0
  27. package/dist/httpRoutes/sessionCookie.d.ts +1 -0
  28. package/dist/httpRoutes/staticRoutes.d.ts +2 -0
  29. package/dist/httpRoutes/syncRoute.d.ts +2 -0
  30. package/dist/httpRoutes/testResetRoute.d.ts +2 -0
  31. package/dist/httpRoutes/timingSafeEqual.d.ts +1 -0
  32. package/dist/httpRoutes/types.d.ts +15 -0
  33. package/dist/httpRoutes/uploadsRoute.d.ts +2 -0
  34. package/dist/index.d.ts +21 -352
  35. package/dist/index.js +32 -7
  36. package/dist/index.js.map +1 -1
  37. package/dist/loadSocket.d.ts +21 -0
  38. package/dist/logSanitize.d.ts +1 -0
  39. package/dist/originExemptRegistry.d.ts +13 -0
  40. package/dist/parseArgv.d.ts +1 -2
  41. package/dist/portResolution.d.ts +7 -0
  42. package/dist/runtimeMapsLoader.d.ts +46 -0
  43. package/dist/securityHeadersRegistry.d.ts +13 -0
  44. package/dist/sse.d.ts +11 -0
  45. package/dist/stopServer.d.ts +28 -0
  46. package/dist/types.d.ts +138 -0
  47. package/dist/verifyBootstrap.d.ts +18 -0
  48. package/docs/argv-parsing.md +3 -244
  49. package/docs/create-server.md +3 -327
  50. package/docs/http-routes.md +3 -319
  51. package/docs/request-pipeline.md +3 -196
  52. package/docs/runtime-maps.md +3 -207
  53. package/docs/security-defaults.md +7 -236
  54. package/package.json +12 -12
  55. package/CLAUDE.md +0 -89
@@ -0,0 +1,21 @@
1
+ import type { Server as HttpServer } from 'node:http';
2
+ import type { Redis as RedisClient } from 'ioredis';
3
+ import { Server as SocketIOServer } from 'socket.io';
4
+ export interface LoadSocketOptions {
5
+ maxHttpBufferSize?: number;
6
+ }
7
+ export interface LoadSocketResult {
8
+ io: SocketIOServer;
9
+ /**
10
+ * The duplicated Redis pub/sub clients backing the Socket.io adapter. The
11
+ * server bootstrap owns them so graceful shutdown can `quit()` them — they are
12
+ * NOT closed by `io.close()` (which only tears down the engine + connections).
13
+ * Created here (instead of letting `attachSocketRedisAdapter` duplicate
14
+ * internally) precisely so the server holds a handle to disconnect them.
15
+ */
16
+ adapterClients: {
17
+ pubClient: RedisClient;
18
+ subClient: RedisClient;
19
+ };
20
+ }
21
+ export declare const loadSocket: (httpServer: HttpServer, options?: LoadSocketOptions) => LoadSocketResult;
@@ -0,0 +1 @@
1
+ export declare const sanitizeForLog: (value: unknown) => unknown;
@@ -0,0 +1,13 @@
1
+ export interface OriginExemptMatcher {
2
+ /**
3
+ * A route is exempt when its path equals this prefix OR continues past it on a
4
+ * path-SEGMENT boundary (i.e. `<prefix>/...`). Matching is boundary-aware so a
5
+ * prefix can't bleed into a sibling route — `/webhooks` exempts `/webhooks` and
6
+ * `/webhooks/stripe` but NOT `/webhooksadmin`.
7
+ */
8
+ pathPrefix: string;
9
+ }
10
+ export declare const registerOriginExemptPath: (matcher: OriginExemptMatcher) => void;
11
+ export declare const getOriginExemptPaths: () => readonly OriginExemptMatcher[];
12
+ export declare const clearOriginExemptPaths: () => void;
13
+ export declare const isOriginExemptPath: (routePath: string) => boolean;
@@ -1,2 +1 @@
1
-
2
- export { }
1
+ export {};
@@ -0,0 +1,7 @@
1
+ export interface ResolveServerPortInput {
2
+ optionsPort?: number | string;
3
+ parsedPort?: number | null;
4
+ defaultPort?: number | string;
5
+ }
6
+ export declare const normalizeServerPort: (value: number | string, source?: string) => number;
7
+ export declare const resolveServerPort: ({ optionsPort, parsedPort, defaultPort, }: ResolveServerPortInput) => number;
@@ -0,0 +1,46 @@
1
+ import { type RuntimeMapsProvider } from '@luckystack/core';
2
+ type RuntimeMapRecord = Record<string, unknown>;
3
+ /**
4
+ * The shape that the generated maps module must export. `scripts/generateServerRequests.ts`
5
+ * emits exactly this shape: `{ apis, syncs, functions }` where each value is a
6
+ * `Record<string, Handler>`. Consumers can import this type to validate their
7
+ * generated file in CI or type-check the `loadGenerated` callback return.
8
+ */
9
+ export interface GeneratedRuntimeMapsModule {
10
+ apis: RuntimeMapRecord;
11
+ syncs: RuntimeMapRecord;
12
+ functions: RuntimeMapRecord;
13
+ }
14
+ export interface ProdRuntimeMapsLoaderOptions {
15
+ /**
16
+ * Dynamic-import callback for the generated maps module of a given preset.
17
+ * Called once per preset per process lifetime; the result is cached.
18
+ *
19
+ * The resolved module must have shape `{ apis, syncs, functions }` (the
20
+ * shape `scripts/generateServerRequests.ts` emits — see
21
+ * {@link GeneratedRuntimeMapsModule}). A mis-shaped module logs a warning
22
+ * and results in every route for that preset returning `notFound`.
23
+ *
24
+ * Pass a function that calls `import()` with a path relative to YOUR
25
+ * server-side module — the framework cannot resolve a relative path on
26
+ * your behalf because dynamic-import resolution is module-scoped.
27
+ *
28
+ * @example
29
+ * loadGenerated: (preset) => import(`./prod/generatedApis.${preset}`)
30
+ */
31
+ loadGenerated: (preset: string) => Promise<unknown>;
32
+ /**
33
+ * Override the preset(s) to load. Skips the argv lookup. Accepts a single
34
+ * preset name or an array. Useful in tests or when the preset list comes
35
+ * from a non-argv source.
36
+ */
37
+ preset?: string | string[];
38
+ }
39
+ export declare const createProdRuntimeMapsProvider: (options: ProdRuntimeMapsLoaderOptions) => RuntimeMapsProvider;
40
+ /**
41
+ * Convenience wrapper around `createProdRuntimeMapsProvider` +
42
+ * `registerRuntimeMapsProvider`. Most consumers want this — pass the
43
+ * loader callback once and the runtime is wired.
44
+ */
45
+ export declare const registerProdRuntimeMapsProvider: (options: ProdRuntimeMapsLoaderOptions) => RuntimeMapsProvider;
46
+ export {};
@@ -0,0 +1,13 @@
1
+ import type { IncomingMessage } from 'node:http';
2
+ export type SecurityHeadersBuilder = (req: IncomingMessage) => Record<string, string> | null | undefined;
3
+ /**
4
+ * Register a custom security-headers builder. Called for every HTTP
5
+ * request; return a plain object that gets merged on top of the framework
6
+ * defaults. Use for Content-Security-Policy, HSTS, Permissions-Policy.
7
+ *
8
+ * Last-write-wins: subsequent calls replace the previous builder. Pass
9
+ * `null` to unregister.
10
+ */
11
+ export declare const registerSecurityHeaders: (builder: SecurityHeadersBuilder | null) => void;
12
+ /** Read the active builder (or null). */
13
+ export declare const getSecurityHeadersBuilder: () => SecurityHeadersBuilder | null;
package/dist/sse.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import type { ServerResponse } from 'node:http';
2
+ export declare const shouldUseHttpStream: ({ acceptHeader, queryString, }: {
3
+ acceptHeader: string | string[] | undefined;
4
+ queryString: string | undefined;
5
+ }) => boolean;
6
+ export declare const initSseResponse: (res: ServerResponse) => void;
7
+ export declare const sendSseEvent: ({ res, event, data, }: {
8
+ res: ServerResponse;
9
+ event: string;
10
+ data: unknown;
11
+ }) => void;
@@ -0,0 +1,28 @@
1
+ import type { Server as HttpServer } from 'node:http';
2
+ import type { Server as SocketIOServer } from 'socket.io';
3
+ import type { Redis as RedisClient } from 'ioredis';
4
+ import type { StopLuckyStackServerOptions } from './types.js';
5
+ export interface GracefulShutdownDeps {
6
+ httpServer: HttpServer;
7
+ ioServer: SocketIOServer;
8
+ adapterClients: {
9
+ pubClient: RedisClient;
10
+ subClient: RedisClient;
11
+ };
12
+ }
13
+ /**
14
+ * Run the graceful-shutdown sequence (MIS-016). Ordered + isolated:
15
+ * 1. Stop accepting NEW HTTP connections (`httpServer.close()` is kicked off
16
+ * immediately; we await it last so in-flight requests can drain).
17
+ * 2. Dispatch the core `preServerStop` hook so consumers can flush queues /
18
+ * release leases / close pools.
19
+ * 3. `flushErrorTrackers()` so buffered events aren't lost — bounded by the
20
+ * timeout race.
21
+ * 4. Close the Socket.io server, then the HTTP server, then `quit()` the
22
+ * Redis-adapter pub/sub clients.
23
+ *
24
+ * Every step is wrapped in {@link withTimeout} so a single failing/hanging step
25
+ * cannot hang the whole shutdown — the sequence always settles within
26
+ * `timeoutMs` per step. Never rejects.
27
+ */
28
+ export declare const runGracefulShutdown: (deps: GracefulShutdownDeps, options?: StopLuckyStackServerOptions) => Promise<void>;
@@ -0,0 +1,138 @@
1
+ import type { IncomingMessage, ServerResponse, Server as HttpServer } from 'node:http';
2
+ import type { Server as SocketIOServer } from 'socket.io';
3
+ export interface RouteContext {
4
+ routePath: string;
5
+ method: string;
6
+ queryString: string | undefined;
7
+ token: string | null;
8
+ }
9
+ export type StaticFileHandler = (req: IncomingMessage, res: ServerResponse) => unknown;
10
+ export type FaviconHandler = (res: ServerResponse) => unknown;
11
+ export type CustomRouteHandler = (req: IncomingMessage, res: ServerResponse, ctx: RouteContext) => Promise<boolean> | boolean;
12
+ export type CustomRoutePhase = 'pre-params' | 'post-params';
13
+ export interface CreateLuckyStackServerOptions {
14
+ /**
15
+ * Port the HTTP server listens on. Resolution order:
16
+ * 1. This option (`options.port`)
17
+ * 2. Second positional argv (`npm run server -- <bundles> <port>`), parsed
18
+ * by `@luckystack/server/parseArgv`
19
+ * 3. `options.defaultPort` (the project's single-instance default, e.g. the
20
+ * `backend` port from `config.ports.ts`)
21
+ * 4. `80` for generic consumers that supplied no port source
22
+ */
23
+ port?: number | string;
24
+ /**
25
+ * Project's single-instance default backend port. Sits BELOW positional argv
26
+ * so a multi-instance boot (`npm run server -- <preset> <port>`) still wins.
27
+ * The scaffold passes the `backend` value from `config.ports.ts` here, so the
28
+ * listen port has one static source of truth.
29
+ */
30
+ defaultPort?: number;
31
+ /** Bind address. Defaults to process.env.SERVER_IP or '127.0.0.1'. */
32
+ ip?: string;
33
+ /** Project-side static file handler (Vite output, etc.). Used as the catch-all. */
34
+ serveFile?: StaticFileHandler;
35
+ /** Project-side favicon handler. */
36
+ serveFavicon?: FaviconHandler;
37
+ /**
38
+ * Optional pre-fallback hook for project-specific HTTP routes. Return `true`
39
+ * if the route was handled (response ended). Return `false` (or omit return)
40
+ * to fall through to the framework's static file serving.
41
+ */
42
+ customRoutes?: CustomRouteHandler;
43
+ /**
44
+ * Enable dev-mode tooling (devkit hot reload, REPL, console init).
45
+ * Defaults to `resolveRuntimeMode() !== 'production'` (NODE_ENV-based).
46
+ */
47
+ enableDevTools?: boolean;
48
+ /**
49
+ * Maximum HTTP buffer size for socket.io. Defaults to 5 MB. Adjust if you
50
+ * stream large payloads through sockets.
51
+ */
52
+ maxHttpBufferSize?: number;
53
+ /** Fail boot if no DeployConfig has been registered. Default: false. */
54
+ requireDeployConfig?: boolean;
55
+ /** Fail boot if no ServicesConfig has been registered. Default: false. */
56
+ requireServicesConfig?: boolean;
57
+ /** Fail boot if no OAuth providers have been registered. Default: false. */
58
+ requireOAuthProviders?: boolean;
59
+ /**
60
+ * Dynamic-import callback for production runtime maps. When provided, the
61
+ * framework registers its built-in `RuntimeMapsProvider` and the consumer
62
+ * no longer needs a hand-rolled `server/prod/runtimeMaps.ts`. Pass a
63
+ * function that calls `import()` with a path relative to the consumer's
64
+ * server module — the framework cannot resolve that path on the
65
+ * consumer's behalf because dynamic-import resolution is module-scoped.
66
+ *
67
+ * @example
68
+ * loadGeneratedMaps: (preset) => import(`./prod/generatedApis.${preset}`)
69
+ */
70
+ loadGeneratedMaps?: (preset: string) => Promise<unknown>;
71
+ /**
72
+ * Override the preset(s) to load (skips argv lookup). Pass a single preset
73
+ * name or an array for multi-preset boots. Default: the first positional
74
+ * argv arg parsed by `@luckystack/server/parseArgv`, falling back to
75
+ * `'default'`.
76
+ */
77
+ runtimeMapsPreset?: string | string[];
78
+ }
79
+ export interface StopLuckyStackServerOptions {
80
+ /**
81
+ * Why the server is stopping — threaded into the `preServerStop` hook payload.
82
+ * Defaults to `'manual'` for a programmatic `stop()`; the signal handlers pass
83
+ * the received signal name.
84
+ */
85
+ reason?: 'SIGTERM' | 'SIGINT' | 'SIGHUP' | 'manual';
86
+ /**
87
+ * Soft budget (ms) for the whole shutdown sequence — the `preServerStop` hook
88
+ * fan-out, the bounded `flushErrorTrackers()` race, and closing the
89
+ * http/io/redis-adapter handles. Each step is wrapped so a single hanging step
90
+ * cannot stall the others past this deadline. Defaults to `10000`.
91
+ */
92
+ timeoutMs?: number;
93
+ }
94
+ export interface AfterListenOptions {
95
+ /**
96
+ * Re-throw instead of swallowing. Use only when the process is genuinely
97
+ * useless without this task. Default `false` — a listening server survives a
98
+ * failing post-listen task.
99
+ */
100
+ fatal?: boolean;
101
+ /** Label used in the failure log. Defaults to `post-listen task`. */
102
+ label?: string;
103
+ }
104
+ export interface RunningLuckyStackServer {
105
+ httpServer: HttpServer;
106
+ ioServer: SocketIOServer;
107
+ /**
108
+ * Start listening. Resolves when the HTTP server is ready. Logs the bound
109
+ * URL on success.
110
+ */
111
+ listen: (callback?: () => void) => Promise<HttpServer>;
112
+ /**
113
+ * Run work that belongs AFTER the server is accepting traffic — durable queue
114
+ * workers, warm-up passes, reconciliation jobs.
115
+ *
116
+ * A rejection here is logged loudly and swallowed by default, because the
117
+ * server is already up: killing a healthy process over one broken dependency
118
+ * turns an outage into a crash-loop (supervisor restarts, dependency is still
119
+ * down, repeat). Pass `{ fatal: true }` when the process genuinely has no
120
+ * purpose without the task — the rejection then propagates to the caller.
121
+ *
122
+ * Never resolves to the task's value; it is fire-and-report, not a pipeline
123
+ * stage. Read what you need inside the callback.
124
+ */
125
+ afterListen: (task: () => void | Promise<void>, options?: AfterListenOptions) => Promise<void>;
126
+ /**
127
+ * Graceful shutdown (MIS-016). Stops accepting new connections, dispatches the
128
+ * core `preServerStop` hook, flushes error-trackers (bounded by a timeout
129
+ * race), and closes the Socket.io server, the HTTP server, and the
130
+ * Redis-adapter pub/sub clients. Every step is isolated + time-bounded so one
131
+ * failing/hanging step cannot hang the whole shutdown. Idempotent — a second
132
+ * call returns the in-flight shutdown promise. Resolves once shutdown
133
+ * completes (or the timeout forces it to finish).
134
+ */
135
+ stop: (options?: StopLuckyStackServerOptions) => Promise<void>;
136
+ /** Alias of {@link stop} matching the `net.Server`-style `close({ timeoutMs })` API. */
137
+ close: (options?: StopLuckyStackServerOptions) => Promise<void>;
138
+ }
@@ -0,0 +1,18 @@
1
+ export interface BootstrapRequirements {
2
+ /**
3
+ * If true, require a deploy config to have been registered. Defaults to
4
+ * false because single-instance deployments don't need one.
5
+ */
6
+ requireDeployConfig?: boolean;
7
+ /**
8
+ * If true, require a services config to have been registered. Defaults to
9
+ * false; only needed when running the router.
10
+ */
11
+ requireServicesConfig?: boolean;
12
+ /**
13
+ * If true, require an OAuth provider list to have been registered.
14
+ * Defaults to false (an app may use credentials only).
15
+ */
16
+ requireOAuthProviders?: boolean;
17
+ }
18
+ export declare const verifyBootstrap: (requirements?: BootstrapRequirements) => Promise<void>;
@@ -1,246 +1,5 @@
1
- # Argv Parsing (`parseServerArgv` + `applyServerArgv`)
1
+ # Runtime arguments
2
2
 
3
- > Deep specs. Bron: `packages/server/src/argv.ts`, `packages/server/src/parseArgv.ts`. Bijgewerkt: 2026-08-16.
3
+ Explicit runtime arguments let deployment choose bundles and bind port without editing generated source. Validate them before boot rather than continuing with an ambiguous or partial map; fail loudly on invalid input. Source: `../src/argv.ts`, `../src/parseArgv.ts`, `../src/portResolution.ts`.
4
4
 
5
- ## Overview
6
-
7
- `@luckystack/server` accepts two positional CLI arguments on boot:
8
-
9
- ```
10
- npm run server -- <bundle[,bundle...]> [port]
11
- ```
12
-
13
- - Arg 0 — preset list. Comma-separated; duplicates collapsed; runtime maps from each preset are shallow-merged at boot.
14
- - Arg 1 — listen port. Numeric. Optional.
15
-
16
- Argv provides one shape consumed by `createProdRuntimeMapsProvider` (preset), `createLuckyStackServer` (port), and the browser-safe `@luckystack/core/config` port-override registry.
17
-
18
- The module exposes:
19
-
20
- - A pure parser: `parseServerArgv(argv)`.
21
- - A side-effect runner that reads `process.argv.slice(2)` once and registers the result: `applyServerArgv()`.
22
- - Read accessors: `getParsedBundles()`, `getParsedPort()`.
23
- - A side-effect-only entrypoint `@luckystack/server/parseArgv` that simply imports `applyServerArgv` and runs it.
24
-
25
- The side-effect entry MUST be the FIRST import in the consumer's `server.ts` because it registers the parsed port in `@luckystack/core/config`, and the consumer's config reads that override at top-level evaluation time. When no CLI port was supplied, the registry stays empty and `config.ts` uses the consumer-owned `config.ports.ts` backend value.
26
-
27
- Importing the consumer config before `parseArgv` runs can lock the static default into its OAuth callback base. The server listen path still reads the same registry through `getParsedPort()`, so both surfaces share one override without an environment writeback.
28
-
29
- ## API Reference
30
-
31
- ### `parseServerArgv(argv: string[]): ParsedServerArgv`
32
-
33
- **Signature:**
34
-
35
- ```typescript
36
- export interface ParsedServerArgv {
37
- bundles: string[];
38
- port: number | null;
39
- }
40
-
41
- export const parseServerArgv = (argv: string[]): ParsedServerArgv;
42
- ```
43
-
44
- **Parameters:**
45
-
46
- | Field | Type | Purpose |
47
- | --- | --- | --- |
48
- | `argv` | `string[]` | Positional args (typically `process.argv.slice(2)`). |
49
-
50
- **Returns:** `{ bundles, port }`:
51
-
52
- - `bundles: string[]` — deduplicated, trimmed, non-empty entries from arg 0. Empty array when arg 0 is missing or empty.
53
- - `port: number | null` — `parseInt(argv[1], 10)` when arg 1 is supplied; `null` otherwise.
54
-
55
- **Behavior:**
56
-
57
- - Reject more than 2 positional arguments by throwing `Error('[luckystack:argv] unexpected positional argument(s): "<rest>". Usage: npm run server -- <bundle[,bundle...]> [port]')`.
58
- - For arg 0: split on `,`, `trim()` each piece, drop falsy, collapse via `Array.from(new Set(...))`.
59
- - For arg 1: must match `/^\d+$/`. Otherwise throws `Error('[luckystack:argv] port argument must be numeric, got: "<value>". Usage: npm run server -- <bundle[,bundle...]> [port]')`.
60
-
61
- **Errors / Edge cases:**
62
-
63
- - Whitespace in arg 0 (`"billing, vehicles"`) is supported — trimmed.
64
- - A trailing comma (`"billing,"`) is silently dropped.
65
- - A leading `0` in the port string is accepted (`/^\d+$/`) and parsed normally.
66
- - An empty arg 0 (`""`) yields `bundles: []`; the downstream resolver falls back to `['default']`.
67
-
68
- **Example:**
69
-
70
- ```typescript
71
- parseServerArgv(['billing,vehicles', '4001']);
72
- // => { bundles: ['billing', 'vehicles'], port: 4001 }
73
-
74
- parseServerArgv([]);
75
- // => { bundles: [], port: null }
76
-
77
- parseServerArgv(['billing', '4001', 'oops']);
78
- // => throws (too many positionals)
79
-
80
- parseServerArgv(['billing', 'PORT']);
81
- // => throws (non-numeric port)
82
- ```
83
-
84
- ---
85
-
86
- ### `applyServerArgv(): void`
87
-
88
- **Signature:**
89
-
90
- ```typescript
91
- export const applyServerArgv = (): void;
92
- ```
93
-
94
- **Parameters:** none. Reads `process.argv.slice(2)`.
95
-
96
- **Returns:** `void`.
97
-
98
- **Behavior:**
99
-
100
- - Idempotent. Subsequent calls return immediately via the module-level `hasRun` latch.
101
- - First call:
102
- 1. `parseServerArgv(process.argv.slice(2))` (throws on malformed input).
103
- 2. Caches `bundles` in module state.
104
- 3. Calls `registerPortOverride(port)`. A numeric port becomes visible through the browser-safe core/config entry; `null` clears the override.
105
-
106
- **Errors / Edge cases:**
107
-
108
- - Throwing during this call aborts boot before consumer config evaluates.
109
- - Calling `applyServerArgv` after consumer config has evaluated is too late — that module has already captured its callback base.
110
-
111
- **Example:**
112
-
113
- ```typescript
114
- // server.ts — first line
115
- import '@luckystack/server/parseArgv';
116
- // rest of bootstrap...
117
- ```
118
-
119
- Or call it explicitly when you control the boot timing:
120
-
121
- ```typescript
122
- import { applyServerArgv } from '@luckystack/server';
123
- applyServerArgv();
124
- ```
125
-
126
- ---
127
-
128
- ### `getParsedBundles(): string[]`
129
-
130
- **Signature:**
131
-
132
- ```typescript
133
- export const getParsedBundles = (): string[];
134
- ```
135
-
136
- **Returns:** the cached `bundles` array. Empty until `applyServerArgv()` has run.
137
-
138
- **Behavior:**
139
-
140
- - Read-only; never throws.
141
- - Used by `createProdRuntimeMapsProvider` to resolve which preset(s) to load when neither `options.preset` nor a literal string is supplied.
142
-
143
- **Example:**
144
-
145
- ```typescript
146
- import { applyServerArgv, getParsedBundles } from '@luckystack/server';
147
-
148
- applyServerArgv();
149
- console.log(getParsedBundles()); // e.g. ['billing', 'vehicles']
150
- ```
151
-
152
- ---
153
-
154
- ### `getParsedPort(): number | null`
155
-
156
- **Signature:**
157
-
158
- ```typescript
159
- export const getParsedPort = (): number | null;
160
- ```
161
-
162
- **Returns:** the port currently held in the core/config override registry. `null` when no positional port was registered.
163
-
164
- **Behavior:**
165
-
166
- - Read-only; never throws.
167
- - Consumed by `createLuckyStackServer` as one of the port-resolution fallbacks:
168
- 1. `options.port`
169
- 2. `getParsedPort()`
170
- 3. `options.defaultPort` (the scaffold's `config.ports.ts` backend)
171
- 4. `80` (generic final fallback)
172
-
173
- **Example:**
174
-
175
- ```typescript
176
- import { getParsedPort } from '@luckystack/server';
177
-
178
- const port = getParsedPort();
179
- if (port !== null) {
180
- console.log(`argv supplied port ${port}`);
181
- }
182
- ```
183
-
184
- ---
185
-
186
- ### Side-effect entrypoint: `@luckystack/server/parseArgv`
187
-
188
- **Module body (verbatim):**
189
-
190
- ```typescript
191
- import { applyServerArgv } from './argv';
192
-
193
- applyServerArgv();
194
- ```
195
-
196
- **Usage:** import as the FIRST line of your `server.ts`:
197
-
198
- ```typescript
199
- import '@luckystack/server/parseArgv';
200
- ```
201
-
202
- Consumer `config.ts` MUST be imported below this line because it reads `getPortOverride()` at module evaluation time. Importing core itself first is safe: the registry is mutable and browser-safe. With no CLI port, scaffold config intentionally falls back to its pure-data `config.ports.ts` value.
203
-
204
- ## Resolution order summary
205
-
206
- | Consumer | Source of port |
207
- | --- | --- |
208
- | `createLuckyStackServer` | `options.port` -> `getParsedPort()` -> `options.defaultPort` -> `80` |
209
- | `createLuckyStackServer` IP | `options.ip` -> `SERVER_IP` -> `127.0.0.1` |
210
- | `createProdRuntimeMapsProvider` | `options.preset` (string -> single-entry array; non-empty array -> dedup) -> `getParsedBundles()` -> `['default']` |
211
-
212
- ## CLI examples
213
-
214
- ```bash
215
- # Default preset, config.ports.ts backend
216
- npm run server
217
-
218
- # Single bundle, config.ports.ts backend
219
- npm run server -- billing
220
-
221
- # Two bundles merged, port 4001
222
- npm run server -- billing,vehicles 4001
223
-
224
- # Whitespace in bundle list is fine
225
- npm run server -- "billing , vehicles" 4001
226
-
227
- # Invalid port — boot aborts with descriptive error
228
- npm run server -- billing PORT
229
- # Error: [luckystack:argv] port argument must be numeric, got: "PORT".
230
-
231
- # Extra positional — boot aborts
232
- npm run server -- billing 4001 oops
233
- # Error: [luckystack:argv] unexpected positional argument(s): "oops".
234
- ```
235
-
236
- ## Interaction with runtime maps
237
-
238
- `createProdRuntimeMapsProvider({ loadGenerated, preset? })` calls `getParsedBundles()` when `preset` is omitted or empty. Each resolved preset is dynamically imported via the consumer-supplied `loadGenerated` callback, then shallow-merged into one runtime view. Key collisions across presets throw at boot. See `runtime-maps.md` for the merge semantics.
239
-
240
- ## Related
241
-
242
- - Function INDEX: `packages/server/CLAUDE.md`
243
- - Runtime maps: `packages/server/docs/runtime-maps.md`
244
- - Create server: `packages/server/docs/create-server.md`
245
- - Architecture: `docs/ARCHITECTURE_PACKAGING.md` (preset bundles, multi-service builds)
246
- - README: `packages/server/README.md`
5
+ A selected bundle controls which generated routes load, not who may invoke them. Keep runtime map isolation and application authorization independent. See `./runtime-maps.md`, `../src/verifyBootstrap.ts`; consult source for current argument spelling and precedence.