@luckystack/server 0.8.2 → 0.8.4

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/dist/parseArgv.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  applyServerArgv
3
- } from "./chunk-QLVQ75QL.js";
3
+ } from "./chunk-HJXOI6LY.js";
4
4
 
5
5
  // src/parseArgv.ts
6
6
  applyServerArgv();
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/parseArgv.ts"],"sourcesContent":["//? Side-effect-only entrypoint. Import this as the FIRST line of your\n//? `server.ts` so the positional CLI args (`<bundles> <port>`) are parsed\n//? and written into `process.env.SERVER_PORT` before any other module load\n//? reads it (notably `config.ts` which builds `backendUrl` at top level).\n//?\n//? Usage:\n//? import '@luckystack/server/parseArgv';\n\nimport { applyServerArgv } from './argv';\n\napplyServerArgv();\n"],"mappings":";;;;;AAUA,gBAAgB;","names":[]}
1
+ {"version":3,"sources":["../src/parseArgv.ts"],"sourcesContent":["//? Side-effect-only entrypoint. Import this as the FIRST line of your\n//? `server.ts` so the positional CLI args (`<bundles> <port>`) are registered\n//? before any other module evaluates the consumer config (notably its top-level\n//? OAuth callback base).\n//?\n//? Usage:\n//? import '@luckystack/server/parseArgv';\n\nimport { applyServerArgv } from './argv';\n\napplyServerArgv();\n"],"mappings":";;;;;AAUA,gBAAgB;","names":[]}
@@ -1,6 +1,6 @@
1
1
  # Argv Parsing (`parseServerArgv` + `applyServerArgv`)
2
2
 
3
- > Deep specs. Bron: `packages/server/src/argv.ts`, `packages/server/src/parseArgv.ts`. Bijgewerkt: 2026-05-20.
3
+ > Deep specs. Bron: `packages/server/src/argv.ts`, `packages/server/src/parseArgv.ts`. Bijgewerkt: 2026-08-16.
4
4
 
5
5
  ## Overview
6
6
 
@@ -13,23 +13,18 @@ npm run server -- <bundle[,bundle...]> [port]
13
13
  - Arg 0 — preset list. Comma-separated; duplicates collapsed; runtime maps from each preset are shallow-merged at boot.
14
14
  - Arg 1 — listen port. Numeric. Optional.
15
15
 
16
- Argv replaces the legacy `LUCKYSTACK_BUNDLE` + `SERVER_PORT` environment toggles with one shape consumed by `createProdRuntimeMapsProvider` (preset) and `createLuckyStackServer` (port).
16
+ Argv provides one shape consumed by `createProdRuntimeMapsProvider` (preset), `createLuckyStackServer` (port), and the browser-safe `@luckystack/core/config` port-override registry.
17
17
 
18
18
  The module exposes:
19
19
 
20
20
  - A pure parser: `parseServerArgv(argv)`.
21
- - A side-effect runner that reads `process.argv.slice(2)` once and caches: `applyServerArgv()`.
21
+ - A side-effect runner that reads `process.argv.slice(2)` once and registers the result: `applyServerArgv()`.
22
22
  - Read accessors: `getParsedBundles()`, `getParsedPort()`.
23
23
  - A side-effect-only entrypoint `@luckystack/server/parseArgv` that simply imports `applyServerArgv` and runs it.
24
24
 
25
- The side-effect entry MUST be the FIRST import in the consumer's `server.ts` because the parsed port is written back to `process.env.SERVER_PORT`, and downstream modules read that variable at top-level evaluation time:
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
26
 
27
- - `@luckystack/core` env Zod schema
28
- - `@luckystack/core` `bindAddress.ts` fallback
29
- - The consumer's `config.ts` `backendUrl` constant
30
- - `@luckystack/login` `oauthProviders.ts` callback URL builder
31
-
32
- Importing anything that pulls one of those four before `parseArgv` runs will lock in the wrong port.
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.
33
28
 
34
29
  ## API Reference
35
30
 
@@ -105,13 +100,13 @@ export const applyServerArgv = (): void;
105
100
  - Idempotent. Subsequent calls return immediately via the module-level `hasRun` latch.
106
101
  - First call:
107
102
  1. `parseServerArgv(process.argv.slice(2))` (throws on malformed input).
108
- 2. Caches `bundles` + `port` in module state.
109
- 3. When `port !== null`, writes `process.env.SERVER_PORT = String(port)`. This is the writeback that lets the four downstream env-readers (listed in Overview) see the resolved port without per-call refactoring.
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.
110
105
 
111
106
  **Errors / Edge cases:**
112
107
 
113
- - Throwing during this call aborts boot before any other module has a chance to read `SERVER_PORT`.
114
- - Calling `applyServerArgv` after another module has already read `SERVER_PORT` is too late — that consumer has already captured the old value.
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.
115
110
 
116
111
  **Example:**
117
112
 
@@ -164,7 +159,7 @@ console.log(getParsedBundles()); // e.g. ['billing', 'vehicles']
164
159
  export const getParsedPort = (): number | null;
165
160
  ```
166
161
 
167
- **Returns:** the cached `port`. `null` until `applyServerArgv()` has run with a numeric arg 1.
162
+ **Returns:** the port currently held in the core/config override registry. `null` when no positional port was registered.
168
163
 
169
164
  **Behavior:**
170
165
 
@@ -172,8 +167,8 @@ export const getParsedPort = (): number | null;
172
167
  - Consumed by `createLuckyStackServer` as one of the port-resolution fallbacks:
173
168
  1. `options.port`
174
169
  2. `getParsedPort()`
175
- 3. `process.env.SERVER_PORT`
176
- 4. `80`
170
+ 3. `options.defaultPort` (the scaffold's `config.ports.ts` backend)
171
+ 4. `80` (generic final fallback)
177
172
 
178
173
  **Example:**
179
174
 
@@ -204,28 +199,23 @@ applyServerArgv();
204
199
  import '@luckystack/server/parseArgv';
205
200
  ```
206
201
 
207
- Anything that depends on `process.env.SERVER_PORT` MUST be imported below this line. Common pitfalls:
208
-
209
- - Importing `'./config'` (which evaluates `backendUrl` at top level) before `parseArgv`.
210
- - Importing `@luckystack/core` modules that pull the env Zod schema before `parseArgv`.
211
-
212
- Both will freeze the wrong port into module-level constants.
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.
213
203
 
214
204
  ## Resolution order summary
215
205
 
216
206
  | Consumer | Source of port |
217
207
  | --- | --- |
218
- | `createLuckyStackServer` | `options.port` -> `getParsedPort()` -> `SERVER_PORT` -> `80` |
208
+ | `createLuckyStackServer` | `options.port` -> `getParsedPort()` -> `options.defaultPort` -> `80` |
219
209
  | `createLuckyStackServer` IP | `options.ip` -> `SERVER_IP` -> `127.0.0.1` |
220
210
  | `createProdRuntimeMapsProvider` | `options.preset` (string -> single-entry array; non-empty array -> dedup) -> `getParsedBundles()` -> `['default']` |
221
211
 
222
212
  ## CLI examples
223
213
 
224
214
  ```bash
225
- # Default preset, port 80
215
+ # Default preset, config.ports.ts backend
226
216
  npm run server
227
217
 
228
- # Single bundle, port 80
218
+ # Single bundle, config.ports.ts backend
229
219
  npm run server -- billing
230
220
 
231
221
  # Two bundles merged, port 4001
@@ -13,10 +13,10 @@ Both honor an explicit pre-flight check via `verifyBootstrap(requirements?)`, wh
13
13
 
14
14
  Boot order (effective for both entries):
15
15
 
16
- 1. `bootstrapLuckyStack` only: load overlay files (`core` -> `deploy` -> `login` -> `sentry` -> `presence` -> `docs-ui` -> `server`, each folder topologically followed by alphabetical `*.ts`).
16
+ 1. `bootstrapLuckyStack` only: load overlay files (`core` -> `deploy` -> `login` -> `email` -> `sentry` -> `presence` -> `cron` -> `docs-ui` -> `server`, each folder topologically followed by alphabetical `*.ts`).
17
17
  2. If `options.loadGeneratedMaps` was supplied, register the framework-shipped runtime-maps provider before `verifyBootstrap` so the boot check sees it.
18
18
  3. `verifyBootstrap` runs (using the per-call `requireDeployConfig` / `requireServicesConfig` / `requireOAuthProviders` flags). Throws a single descriptive `Error` if anything is missing.
19
- 4. Resolve `port` (`options.port` -> argv-parsed -> `options.defaultPort` -> `SERVER_PORT` -> `80`) and `ip` (`options.ip` -> `SERVER_IP` -> `127.0.0.1`); validate the port in `0..65535` and register the intended address. After listen succeeds, register `httpServer.address().port` as the actually-bound address.
19
+ 4. Resolve `port` (`options.port` -> argv-registry override -> `options.defaultPort` -> `80`) and `ip` (`options.ip` -> `SERVER_IP` -> `127.0.0.1`); validate the port in `0..65535` and register the intended address plus configured-default metadata. After listen succeeds, register `httpServer.address().port` as the actually-bound address.
20
20
  5. In dev mode (`enableDevTools !== false` and `NODE_ENV !== 'production'`): `initConsolelog()` + dynamic-import `@luckystack/devkit` -> `initializeAll()` + `setupWatchers()`. Initialization status is shared with API/sync dispatchers and `/readyz`; a fatal devkit error keeps all three fail-closed with 503. Install `SIGINT` / `SIGTERM` handlers that force-exit.
21
21
  6. `writeBootUuid()` writes a fresh boot UUID to Redis so `/_health` becomes truthful and the router can detect rolling restarts.
22
22
  7. Construct `http.createServer(handleHttpRequest)` and `loadSocket(httpServer, { maxHttpBufferSize })`.
@@ -38,8 +38,8 @@ export const createLuckyStackServer = async (
38
38
 
39
39
  | Field | Type | Default | Purpose |
40
40
  | --- | --- | --- | --- |
41
- | `port` | `number \| string` | `getParsedPort()` -> `defaultPort` -> `SERVER_PORT` -> `80` | Explicit HTTP listen port. Numeric strings are accepted; partial/non-integer/out-of-range values fail before `listen`. |
42
- | `defaultPort` | `number` | `SERVER_PORT` -> `80` | Config-level fallback below explicit `port` + argv; the scaffold passes `config.ports.backend`. |
41
+ | `port` | `number \| string` | `getParsedPort()` -> `defaultPort` -> `80` | Explicit HTTP listen port. Numeric strings are accepted; partial/non-integer/out-of-range values fail before `listen`. |
42
+ | `defaultPort` | `number` | `80` | Config-level fallback below explicit `port` + argv; the scaffold passes `config.ports.backend`. It is also recorded so a programmatic `port` can update a callback that still names this default without overriding an unrelated local router ingress. |
43
43
  | `ip` | `string` | `process.env.SERVER_IP ?? '127.0.0.1'` | Bind address. Registered so `checkOrigin` and other framework code see the resolved value. |
44
44
  | `serveFile` | `StaticFileHandler` | none | Catch-all GET handler (Vite output, SPA `index.html`). Called for `/assets/*`, known static extensions, and the final SPA fallback. Without it the static fallback returns `404`. |
45
45
  | `serveFavicon` | `FaviconHandler` | none | Handler for `/favicon.ico`. Without it the route returns `404`. |
@@ -129,7 +129,7 @@ export const bootstrapLuckyStack = async (
129
129
 
130
130
  **Behavior:**
131
131
 
132
- - If `skipOverlayLoad !== true` and `<ROOT_DIR>/<overlayRoot>` exists, walk the canonical package order (`core`, `deploy`, `login`, `sentry`, `presence`, `docs-ui`, `server`).
132
+ - If `skipOverlayLoad !== true` and `<ROOT_DIR>/<overlayRoot>` exists, walk the canonical package order (`core`, `deploy`, `login`, `email`, `sentry`, `presence`, `cron`, `docs-ui`, `server`).
133
133
  - Inside each package folder: import `index.ts`/`index.js` first if present, then every remaining `*.ts` / `*.js` file in alphabetical order. Each file is responsible for its own side-effect registration.
134
134
  - Then delegate to `createLuckyStackServer(options)`.
135
135
 
@@ -276,7 +276,7 @@ export type CustomRouteHandler = (
276
276
 
277
277
  | Source | Key | Effect |
278
278
  | --- | --- | --- |
279
- | env | `SERVER_PORT` | Legacy port fallback below `options.port`, argv, and `options.defaultPort`. |
279
+ | config/argv | `config.ports.ts` backend / positional `[port]` | Static consumer default and per-boot override; no environment port bridge. |
280
280
  | env | `SERVER_IP` | IP fallback. |
281
281
  | env | `NODE_ENV` | Switches `enableDevTools` default, dynamic-import branch for devkit, and `verifyBootstrap` warn-vs-throw for runtime-maps / localized normalizer. |
282
282
  | env | `SECURE` | When `'true'`, session cookies are emitted with the `Secure;` flag (see `request-pipeline.md`). |
@@ -1,6 +1,6 @@
1
1
  # HTTP Routes
2
2
 
3
- > Deep specs. Bron: `packages/server/src/httpHandler.ts`, `packages/server/src/httpRoutes/*.ts`, `packages/server/src/customRoutesRegistry.ts`. Bijgewerkt: 2026-05-20.
3
+ > Deep specs. Bron: `packages/server/src/httpHandler.ts`, `packages/server/src/httpRoutes/*.ts`, `packages/server/src/customRoutesRegistry.ts`.
4
4
 
5
5
  ## Overview
6
6
 
@@ -33,7 +33,7 @@ A custom-route registry sits in front of the static fallback. Both the legacy `c
33
33
 
34
34
  | Route | Method(s) | Handler | Response (success) | Response (failure) |
35
35
  | --- | --- | --- | --- | --- |
36
- | `/auth/csrf` | GET | `handleCsrfRoute` | `200 { status: 'success', csrfToken }` | `401 auth.unauthenticated` |
36
+ | `/auth/csrf` | GET | `handleCsrfRoute` | `200 { status: 'success', csrfToken }`; without login it also sets the double-submit cookie | `401 auth.unauthenticated` only when login is installed and no valid session exists |
37
37
  | `/favicon.ico` | GET | `handleFaviconRoute` | Whatever `options.serveFavicon(res)` writes | `404` empty when no handler supplied |
38
38
  | `/livez` | GET | `handleLivezRoute` | `200 { status: 'live' }` | Always 200 when reachable |
39
39
  | `/readyz` | GET | `handleReadyzRoute` | `200 { status: 'ready', checks: {...} }` | `503 { status: 'not-ready', checks: {...} }` |
@@ -52,7 +52,9 @@ A custom-route registry sits in front of the static fallback. Both the legacy `c
52
52
  | `/assets/*`, `*.{png,jpg,jpeg,gif,svg,html,css,js}` | GET | `handleStaticAndSpaFallback` | Whatever `options.serveFile` writes (URL temporarily rewritten) | `404` when no `serveFile` is wired |
53
53
  | Any other extension-less path | GET | `handleStaticAndSpaFallback` | SPA fallback (rewrite URL to `/`, call `serveFile`) | `404` when no `serveFile` |
54
54
 
55
- > The CSRF header name clients must send (for the `/auth/csrf` route above) is configurable: it comes from `getCsrfConfig().headerName` (default `x-csrf-token`), renamable via `registerCsrfConfig({ headerName })` from `@luckystack/core`.
55
+ > `GET /auth/csrf` issues the token; state-changing requests send it using `getCsrfConfig().headerName` (default `x-csrf-token`). `registerCsrfConfig(...)` also controls the login-absent cookie name/options and token length.
56
+ >
57
+ > `/_test/reset` clears rate limits and only the framework's default Redis session/active-user namespaces. It does not clear application database rows or invoke a custom `SessionAdapter`; custom-storage tests own that cleanup.
56
58
 
57
59
  ## API Reference — handlers
58
60
 
@@ -172,7 +174,7 @@ A handler returns `true` (or simply ends `res`) to short-circuit dispatch; retur
172
174
  - Calls `handleHttpApiRequest({ name, data, token, requesterIp, xLanguageHeader, acceptLanguageHeader, method, stream? })` from `@luckystack/api`. The `stream` callback emits `event: stream` SSE frames during long-running calls.
173
175
  - Emits the result (`event: final` for SSE, or `JSON.stringify(result)` + `res.writeHead(result.httpStatus)` for non-SSE).
174
176
 
175
- **Errors:** any thrown error is logged + captured to Sentry, `apiError` hook dispatched, and the envelope `500 api.invalidRequestFormat` is returned (over SSE or JSON).
177
+ **Errors:** any thrown error is logged + captured by the registered error tracker(s), `apiError` hook dispatched, and the envelope `500 api.invalidRequestFormat` is returned (over SSE or JSON).
176
178
 
177
179
  ### `handleSyncRoute(ctx)`
178
180
 
@@ -184,7 +186,7 @@ A handler returns `true` (or simply ends `res`) to short-circuit dispatch; retur
184
186
  - Calls `handleHttpSyncRequest({ name: 'sync/<rest>', cb, data, receiver, ignoreSelf, token, requesterIp, xLanguageHeader, acceptLanguageHeader, stream? })` from `@luckystack/sync`.
185
187
  - SSE handling mirrors `handleApiRoute`.
186
188
 
187
- **Errors:** mirrors `handleApiRoute` — logs, Sentry, `syncError` hook, returns `500 sync.invalidRequestFormat`.
189
+ **Errors:** mirrors `handleApiRoute` — logs, registered error tracker(s), `syncError` hook, returns `500 sync.invalidRequestFormat`.
188
190
 
189
191
  ### `handleCustomRoutes(ctx)`
190
192
 
@@ -1,6 +1,6 @@
1
1
  # Request Pipeline
2
2
 
3
- > Deep specs. Bron: `packages/server/src/httpHandler.ts`, `packages/server/src/loadSocket.ts`, `packages/server/src/sse.ts`. Bijgewerkt: 2026-05-20.
3
+ > Deep specs. Bron: `packages/server/src/httpHandler.ts`, `packages/server/src/loadSocket.ts`, `packages/server/src/sse.ts`.
4
4
 
5
5
  ## Overview
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Runtime Maps Loader
2
2
 
3
- > Deep specs. Bron: `packages/server/src/runtimeMapsLoader.ts`. Bijgewerkt: 2026-05-20.
3
+ > Deep specs. Bron: `packages/server/src/runtimeMapsLoader.ts`.
4
4
 
5
5
  ## Overview
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Security Defaults
2
2
 
3
- > Deep specs. Bron: `packages/server/src/httpHandler.ts`, `packages/server/src/httpRoutes/csrfMiddleware.ts`, `packages/server/src/httpRoutes/testResetRoute.ts`, `packages/server/src/securityHeadersRegistry.ts`, `packages/server/src/errorFormatterRegistry.ts`. Bijgewerkt: 2026-05-20.
3
+ > Deep specs. Sources: `packages/server/src/httpHandler.ts`, `packages/server/src/httpRoutes/csrfRoute.ts`, `packages/server/src/httpRoutes/csrfMiddleware.ts`, `packages/server/src/httpRoutes/testResetRoute.ts`, `packages/server/src/securityHeadersRegistry.ts`, and `packages/server/src/errorFormatterRegistry.ts`.
4
4
 
5
5
  ## Overview
6
6
 
@@ -8,7 +8,7 @@
8
8
 
9
9
  1. **Origin policy.** No `Host` fallback — non-browser callers cannot silently bypass CORS on state-changing methods.
10
10
  2. **Security headers.** Framework defaults are emitted for every response. A consumer-registered builder may override or extend.
11
- 3. **CSRF middleware.** Cookie-mode state-changing requests to `/api/*`, `/sync/*`, and `/auth/api/*` require a CSRF header (default `x-csrf-token`, renamable via `registerCsrfConfig({ headerName })` from `@luckystack/core`) matching the session-bound token; mismatches return 403 and dispatch the `csrfMismatch` hook.
11
+ 3. **CSRF middleware.** Cookie-mode state-changing framework and custom routes require the configured CSRF header unless they are an auth bootstrap/callback route or explicitly origin-exempt. With login installed it validates a session-bound token; without login it uses a stateless double-submit cookie/header pair. Mismatches return 403 and dispatch `csrfMismatch`.
12
12
  4. **`/_test/reset` fail-closed.** Requires `NODE_ENV` to be exactly `development` or `test` AND a non-empty `TEST_RESET_TOKEN` env var. An unset token does NOT mean "no auth"; it means the route is permanently 403.
13
13
 
14
14
  Plus two extension seams:
@@ -82,20 +82,24 @@ Lives in `enforceCsrfOnStateChangingRequest` (`csrfMiddleware.ts`). Runs after o
82
82
 
83
83
  **Activation predicate (all required):**
84
84
 
85
- - `projectConfig.session.basedToken === false` (cookie mode).
86
- - HTTP method is NOT `GET` and NOT `OPTIONS`.
87
- - `routePath` starts with `/api/`, `/sync/`, or `/auth/api/`.
88
- - `routePath` does NOT start with `/auth/callback`.
89
- - A session token was extracted from the request.
85
+ - `projectConfig.session.basedToken === false` (cookie mode; the public scaffold flag is `config.sessionBasedToken`).
86
+ - HTTP method is state-changing: not `GET`, `HEAD`, or `OPTIONS`.
87
+ - The path is a framework API/sync/auth-API route, or any custom path outside `/auth/*` and `/assets/*`.
88
+ - The path is not an auth callback, credentials/email-code/2FA login-bootstrap route, or a prefix registered through `registerOriginExemptPath(...)`.
90
89
 
91
- When the predicate is true:
90
+ The validation branch then depends on whether `@luckystack/login` is installed:
92
91
 
93
- 1. `getSession(token)` resolves the session. If no session, skip middleware (downstream auth check rejects).
94
- 2. Read the configured CSRF header (`getCsrfConfig().headerName`, default `x-csrf-token`; first value when array).
95
- 3. If provided and equal to `session.csrfToken`, allow.
96
- 4. Otherwise: dispatch the `csrfMismatch` hook with `{ route, method, requestId, userId, providedToken: Boolean(provided) }` (note: presence-only, never the value), then respond `403 application/json { status: 'error', errorCode: 'auth.csrfMismatch', message: 'CSRF token missing or invalid. Fetch /auth/csrf first.' }`.
92
+ 1. Read `getCsrfConfig().headerName` (default `x-csrf-token`; first value when the Node header is an array).
93
+ 2. **Login present:** when a valid session token/session exists, compare the header with `session.csrfToken`. Requests without a valid session continue to the downstream auth policy instead of being misclassified as CSRF failures.
94
+ 3. **Login absent:** read the configured CSRF cookie (default `csrf-token`) and compare it with the header using the timing-safe helper. No session read occurs.
95
+ 4. On mismatch, dispatch `csrfMismatch` with `{ route, method, requestId, userId, providedToken: Boolean(provided) }`—presence only, never the token—then return `403 auth.csrfMismatch`.
97
96
 
98
- Token issue: `GET /auth/csrf` (`csrfRoute.ts`) returns `{ status: 'success', csrfToken }`. The `apiRequest` helper in `@luckystack/core/client` fetches it once per session and attaches it to every state-changing request automatically.
97
+ **Token issue:** `GET /auth/csrf` is the single issuing endpoint.
98
+
99
+ - With login present it requires a valid session and returns that session's CSRF token.
100
+ - Without login it returns a new random token and sets the same value in the configurable double-submit cookie; `httpOnly: true` is supported because the client reads the matching token from the JSON response. The cookie's `Secure` attribute follows explicit CSRF config or `SECURE=true`.
101
+
102
+ The client helpers fetch/cache the token per invocation origin and attach it to state-changing requests. Token mode (`sessionBasedToken=true`, internally `session.basedToken=true`) deliberately skips CSRF because JavaScript explicitly supplies the bearer/handshake token; the browser does not ambiently attach it like a cookie.
99
103
 
100
104
  ## `/_test/reset` fail-closed contract
101
105
 
@@ -125,7 +129,9 @@ x-test-reset-token: <value of TEST_RESET_TOKEN>
125
129
  | 3 | `redis SCAN` + `DEL` keys matching `<projectName>-activeUsers:*`. Reported as `'activeUsers'` when at least one key is deleted. |
126
130
  | 4 (opt-in) | When `?include=hooks` is supplied, `clearAllHooks()` empties every framework hook registration. Reported as `'hooks'`. |
127
131
 
128
- `getProjectName()` (from `@luckystack/core`) is the single source of truth for the key prefix — same helper used by `session.ts` and `rateLimiter.ts`.
132
+ `getProjectName()` (from `@luckystack/core`) is the single source of truth for the default Redis key prefix—same helper used by the Redis session adapter and rate limiter.
133
+
134
+ > **Custom `SessionAdapter` caveat:** `/_test/reset` clears only the framework's default Redis session and active-user namespaces. It does not call or discover a custom adapter, so sessions stored in DynamoDB, Postgres, an in-memory test adapter, or another backend survive this endpoint. Custom-adapter test suites must clear their own store in setup/teardown.
129
135
 
130
136
  **Response:** `200 application/json { status: 'success', cleared: string[] }`. The `cleared` array reflects which subsystems reported deletions.
131
137
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luckystack/server",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -57,24 +57,28 @@
57
57
  "test": "vitest run"
58
58
  },
59
59
  "dependencies": {
60
- "@luckystack/api": "^0.8.2",
61
- "@luckystack/core": "^0.8.2"
60
+ "@luckystack/api": "^0.8.4",
61
+ "@luckystack/core": "^0.8.4"
62
62
  },
63
63
  "peerDependencies": {
64
64
  "@prisma/client": "^6.19.0",
65
65
  "socket.io": "^4.8.0",
66
- "@luckystack/devkit": "^0.8.2",
67
- "@luckystack/docs-ui": "^0.8.2",
68
- "@luckystack/email": "^0.8.2",
69
- "@luckystack/error-tracking": "^0.8.2",
70
- "@luckystack/login": "^0.8.2",
71
- "@luckystack/presence": "^0.8.2",
72
- "@luckystack/sync": "^0.8.2"
66
+ "@luckystack/cron": "^0.8.4",
67
+ "@luckystack/devkit": "^0.8.4",
68
+ "@luckystack/docs-ui": "^0.8.4",
69
+ "@luckystack/email": "^0.8.4",
70
+ "@luckystack/error-tracking": "^0.8.4",
71
+ "@luckystack/login": "^0.8.4",
72
+ "@luckystack/presence": "^0.8.4",
73
+ "@luckystack/sync": "^0.8.4"
73
74
  },
74
75
  "peerDependenciesMeta": {
75
76
  "@prisma/client": {
76
77
  "optional": true
77
78
  },
79
+ "@luckystack/cron": {
80
+ "optional": true
81
+ },
78
82
  "@luckystack/devkit": {
79
83
  "optional": true
80
84
  },
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/portResolution.ts","../src/argv.ts"],"sourcesContent":["export interface ResolveServerPortInput {\n optionsPort?: number | string;\n parsedPort?: number | null;\n defaultPort?: number | string;\n envPort?: string;\n}\n\nexport const normalizeServerPort = (\n value: number | string,\n source = 'server port',\n): number => {\n let normalized: number;\n if (typeof value === 'number') normalized = value;\n else normalized = /^\\d+$/.test(value) ? Number(value) : Number.NaN;\n\n if (!Number.isInteger(normalized) || normalized < 0 || normalized > 65_535) {\n throw new RangeError(\n `[luckystack:port] ${source} must be an integer from 0 through 65535, got: \"${String(value)}\".`,\n );\n }\n\n return normalized;\n};\n\n//? One explicit, testable precedence chain. The scaffold passes ports.backend as\n//? `defaultPort`; SERVER_PORT remains a final compatibility fallback.\nexport const resolveServerPort = ({\n optionsPort,\n parsedPort,\n defaultPort,\n envPort,\n}: ResolveServerPortInput): number => {\n if (optionsPort !== undefined) return normalizeServerPort(optionsPort, 'options.port');\n if (parsedPort !== undefined && parsedPort !== null) return normalizeServerPort(parsedPort, 'argv port');\n if (defaultPort !== undefined) return normalizeServerPort(defaultPort, 'options.defaultPort');\n if (envPort !== undefined) return normalizeServerPort(envPort, 'SERVER_PORT');\n return 80;\n};\n","//? 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\nimport { normalizeServerPort } from './portResolution';\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 = normalizeServerPort(portArg, 'port argument');\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":";AAOO,IAAM,sBAAsB,CACjC,OACA,SAAS,kBACE;AACX,MAAI;AACJ,MAAI,OAAO,UAAU,SAAU,cAAa;AAAA,MACvC,cAAa,QAAQ,KAAK,KAAK,IAAI,OAAO,KAAK,IAAI,OAAO;AAE/D,MAAI,CAAC,OAAO,UAAU,UAAU,KAAK,aAAa,KAAK,aAAa,OAAQ;AAC1E,UAAM,IAAI;AAAA,MACR,qBAAqB,MAAM,mDAAmD,OAAO,KAAK,CAAC;AAAA,IAC7F;AAAA,EACF;AAEA,SAAO;AACT;AAIO,IAAM,oBAAoB,CAAC;AAAA,EAChC;AAAA,EACA,YAAAA;AAAA,EACA;AAAA,EACA;AACF,MAAsC;AACpC,MAAI,gBAAgB,OAAW,QAAO,oBAAoB,aAAa,cAAc;AACrF,MAAIA,gBAAe,UAAaA,gBAAe,KAAM,QAAO,oBAAoBA,aAAY,WAAW;AACvG,MAAI,gBAAgB,OAAW,QAAO,oBAAoB,aAAa,qBAAqB;AAC5F,MAAI,YAAY,OAAW,QAAO,oBAAoB,SAAS,aAAa;AAC5E,SAAO;AACT;;;ACjBA,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,oBAAoB,SAAS,eAAe;AAAA,EACrD;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":["parsedPort"]}