@luckystack/server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,206 @@
1
+ # Runtime Maps Loader
2
+
3
+ > Deep specs. Bron: `packages/server/src/runtimeMapsLoader.ts`. Bijgewerkt: 2026-05-20.
4
+
5
+ ## Overview
6
+
7
+ A `RuntimeMapsProvider` (defined in `@luckystack/core`) is the runtime source of truth for which `apis`, `syncs`, and `functions` exist in the running process. Without one, every `/api/*` and `/sync/*` request silently resolves to `notFound`.
8
+
9
+ `@luckystack/server` ships two helpers so consumers no longer need to hand-roll their own `server/prod/runtimeMaps.ts`:
10
+
11
+ - `createProdRuntimeMapsProvider(options)` — builds and returns the provider without registering.
12
+ - `registerProdRuntimeMapsProvider(options)` — convenience wrapper: builds AND calls `registerRuntimeMapsProvider(provider)`. Most consumers want this.
13
+
14
+ In production, the provider loads generated maps via the consumer-supplied `loadGenerated(preset)` callback. In dev it transparently delegates to `@luckystack/devkit`'s in-memory discovery (`devApis` / `devSyncs` / `devFunctions`), which is populated by the devkit watchers.
15
+
16
+ Why does the consumer supply `loadGenerated`? Dynamic-import path resolution in ESM is module-scoped — the framework cannot resolve a relative path on the consumer's behalf. Passing a function that calls `import(\`./prod/generatedApis.${preset}\`)` lets the consumer-side module own resolution.
17
+
18
+ Preset list resolution (in `resolvePresets`):
19
+
20
+ 1. `options.preset` if it is a non-empty string -> `[preset]`.
21
+ 2. `options.preset` if it is a non-empty array -> dedup via `Array.from(new Set(...))`.
22
+ 3. `getParsedBundles()` from `argv.ts` when non-empty.
23
+ 4. Final fallback: `['default']`.
24
+
25
+ Multiple presets are loaded in parallel, normalized to `{ apisObject, syncObject, functionsObject }`, then shallow-merged into a single view. Key collisions across presets throw at boot — services must own exactly one preset (see `docs/ARCHITECTURE_PACKAGING.md` §10).
26
+
27
+ ## API Reference
28
+
29
+ ### `createProdRuntimeMapsProvider(options: ProdRuntimeMapsLoaderOptions): RuntimeMapsProvider`
30
+
31
+ **Signature:**
32
+
33
+ ```typescript
34
+ export interface ProdRuntimeMapsLoaderOptions {
35
+ loadGenerated: (preset: string) => Promise<unknown>;
36
+ preset?: string | string[];
37
+ }
38
+
39
+ export const createProdRuntimeMapsProvider = (
40
+ options: ProdRuntimeMapsLoaderOptions,
41
+ ): RuntimeMapsProvider;
42
+ ```
43
+
44
+ **Parameters:**
45
+
46
+ | Field | Type | Default | Purpose |
47
+ | --- | --- | --- | --- |
48
+ | `loadGenerated` | `(preset: string) => Promise<unknown>` | required | Dynamic-import callback that resolves the generated maps module for a given preset. The resolved module must have shape `{ apis, syncs, functions }` (what `scripts/generateServerRequests.ts` emits). Called once per preset per process lifetime; the result is cached. |
49
+ | `preset` | `string \| string[]` | argv -> `['default']` | Override for the preset list. String -> single-entry array; array -> deduplicated. Skips the argv lookup. |
50
+
51
+ **Returns:** a `RuntimeMapsProvider`:
52
+
53
+ ```typescript
54
+ interface RuntimeMapsProvider {
55
+ getRuntimeApiMaps: () => Promise<{ apisObject: Record<string, unknown>; functionsObject: Record<string, unknown> }>;
56
+ getRuntimeSyncMaps: () => Promise<{ syncObject: Record<string, unknown>; functionsObject: Record<string, unknown> }>;
57
+ }
58
+ ```
59
+
60
+ **Behavior:**
61
+
62
+ - Builds two getters (`getRuntimeApiMaps`, `getRuntimeSyncMaps`) that switch on `process.env.NODE_ENV === 'production'`:
63
+ - **Production:** lazy-loads + caches the prod maps via `loadProdRuntimeMaps`. Subsequent calls await the same `prodMapsPromise`.
64
+ - **Dev:** lazy-imports `@luckystack/devkit` and returns `{ devApis, devSyncs, devFunctions }`. Devkit is excluded from production bundles so this dynamic import is never reached when `NODE_ENV === 'production'`.
65
+ - `loadProdRuntimeMaps`:
66
+ 1. Resolve presets via `resolvePresets(options)`.
67
+ 2. Concurrently `loadGenerated(preset)` for each preset; failures resolve to `null` (caught with `.catch(() => null)`).
68
+ 3. For each loaded module, run `normalizeGeneratedModule` (defends against missing or malformed export shape: anything other than an object becomes `{}`).
69
+ 4. Merge `apis`, `syncs`, `functions` into the combined view with `mergeInto`, tracking origin per key in three `Map<string, string>`s.
70
+ 5. Skipped presets emit `console.warn('[luckystack:runtimeMaps] preset "<name>" failed to load — skipping. ...')`.
71
+ 6. If no presets loaded successfully, emit `console.warn('[luckystack:runtimeMaps] no presets resolved (tried: ...). Every api/sync request will return notFound until at least one generated module loads.')` and return empty maps.
72
+ - The dev branch caches the devkit module promise so repeated calls share one resolved module.
73
+
74
+ **Errors / Edge cases:**
75
+
76
+ - Key collision across presets throws synchronously inside `loadProdRuntimeMaps`:
77
+
78
+ ```
79
+ [luckystack:runtimeMaps] <kind> key collision: "<key>" present in both preset "<previous>" and preset "<current>". Services must belong to exactly one preset (see docs/ARCHITECTURE_PACKAGING.md §10).
80
+ ```
81
+
82
+ - Failed `import()` (e.g. preset module not built) is swallowed and logged as a warning; the provider continues with whatever did load.
83
+ - A preset module whose default export is not an object normalizes to `{}` for all three maps — no throw, just empty.
84
+ - The dev branch dynamic-imports `'@luckystack/devkit'` even when only one of the two getters is invoked; the module promise is cached.
85
+
86
+ **Example:**
87
+
88
+ ```typescript
89
+ import { createProdRuntimeMapsProvider, registerRuntimeMapsProvider } from '@luckystack/server';
90
+
91
+ const provider = createProdRuntimeMapsProvider({
92
+ loadGenerated: (preset) => import(`./prod/generatedApis.${preset}`),
93
+ preset: 'billing',
94
+ });
95
+ registerRuntimeMapsProvider(provider);
96
+ ```
97
+
98
+ ---
99
+
100
+ ### `registerProdRuntimeMapsProvider(options: ProdRuntimeMapsLoaderOptions): RuntimeMapsProvider`
101
+
102
+ **Signature:**
103
+
104
+ ```typescript
105
+ export const registerProdRuntimeMapsProvider = (
106
+ options: ProdRuntimeMapsLoaderOptions,
107
+ ): RuntimeMapsProvider;
108
+ ```
109
+
110
+ **Parameters:** identical to `createProdRuntimeMapsProvider`.
111
+
112
+ **Returns:** the same `RuntimeMapsProvider` instance that was registered.
113
+
114
+ **Behavior:**
115
+
116
+ - Internally:
117
+ ```typescript
118
+ const provider = createProdRuntimeMapsProvider(options);
119
+ registerRuntimeMapsProvider(provider);
120
+ return provider;
121
+ ```
122
+ - Calling twice replaces the active provider (last-write-wins) — `registerRuntimeMapsProvider` is the registry function.
123
+
124
+ **Errors / Edge cases:** same as `createProdRuntimeMapsProvider`. `registerRuntimeMapsProvider` itself does not throw on duplicate calls.
125
+
126
+ **Example — direct use:**
127
+
128
+ ```typescript
129
+ import { registerProdRuntimeMapsProvider } from '@luckystack/server';
130
+
131
+ registerProdRuntimeMapsProvider({
132
+ loadGenerated: (preset) => import(`./prod/generatedApis.${preset}`),
133
+ });
134
+ ```
135
+
136
+ **Example — via `createLuckyStackServer.loadGeneratedMaps`:** when the option is supplied, `createLuckyStackServer` calls `registerProdRuntimeMapsProvider` for you before `verifyBootstrap` runs.
137
+
138
+ ```typescript
139
+ import { createLuckyStackServer } from '@luckystack/server';
140
+
141
+ const server = await createLuckyStackServer({
142
+ serveFile,
143
+ serveFavicon,
144
+ loadGeneratedMaps: (preset) => import(`./prod/generatedApis.${preset}`),
145
+ runtimeMapsPreset: ['billing', 'vehicles'],
146
+ });
147
+ await server.listen();
148
+ ```
149
+
150
+ ## Expected module shape
151
+
152
+ `loadGenerated(preset)` must resolve to a module that has these named exports:
153
+
154
+ ```typescript
155
+ export const apis: Record<string, ApiRouteEntry>;
156
+ export const syncs: Record<string, SyncRouteEntry>;
157
+ export const functions: Record<string, FunctionEntry>;
158
+ ```
159
+
160
+ This matches what `scripts/generateServerRequests.ts` emits. Missing exports default to `{}` — useful when a preset has no syncs or functions.
161
+
162
+ ## Merge semantics
163
+
164
+ `mergeInto(target, source, kind, fromPreset, keyOrigin)`:
165
+
166
+ - Iterate every key in `source`.
167
+ - If `keyOrigin` already records a different preset for that key -> throw the collision error.
168
+ - Otherwise record the origin and shallow-assign `target[key] = source[key]`.
169
+
170
+ Three independent origin maps are used so an api named `users/get` and a sync named `users/get` do not collide.
171
+
172
+ ## Failure modes
173
+
174
+ | Situation | Behavior |
175
+ | --- | --- |
176
+ | `loadGenerated(preset)` rejects | Warn (`preset "<name>" failed to load — skipping`); continue with other presets. |
177
+ | All presets failed | Warn (`no presets resolved (tried: ...)`); return empty maps; every request returns `notFound`. |
178
+ | Preset module has wrong shape | Normalize to `{}`. Same effect: requests for those routes return `notFound`. |
179
+ | Key collision across presets | Throw at boot — services must own exactly one preset. |
180
+ | Provider is never registered at all | `verifyBootstrap` hard-fails in production, loud-warns in dev. |
181
+
182
+ ## Dev vs prod branch
183
+
184
+ | Environment | Source of maps |
185
+ | --- | --- |
186
+ | `NODE_ENV === 'production'` | `loadGenerated(preset)` for each resolved preset; cached `prodMapsPromise`. |
187
+ | Anything else | Dynamic import of `'@luckystack/devkit'` -> `{ devApis, devSyncs, devFunctions }`. Devkit watchers keep these maps live. |
188
+
189
+ The dynamic import to `'@luckystack/devkit'` is intentionally written as `import('@luckystack/devkit')` and not statically referenced, so bundlers tree-shake it out of production builds.
190
+
191
+ ## Config keys
192
+
193
+ | Source | Key | Effect |
194
+ | --- | --- | --- |
195
+ | env | `NODE_ENV` | Switches dev (devkit) vs prod (`loadGenerated`) branch. |
196
+ | argv | `<bundles>` | Preset list when `options.preset` is omitted. |
197
+ | options | `options.preset` | Overrides argv-derived preset list. |
198
+ | options | `options.loadGenerated` | Required prod loader callback. |
199
+
200
+ ## Related
201
+
202
+ - Function INDEX: `packages/server/CLAUDE.md`
203
+ - Argv parsing: `packages/server/docs/argv-parsing.md`
204
+ - Create server: `packages/server/docs/create-server.md`
205
+ - Architecture: `docs/ARCHITECTURE_PACKAGING.md` (§10 preset bundles + multi-service builds)
206
+ - README: `packages/server/README.md`
@@ -0,0 +1,236 @@
1
+ # Security Defaults
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.
4
+
5
+ ## Overview
6
+
7
+ `@luckystack/server` ships four fail-closed defenses out of the box:
8
+
9
+ 1. **Origin policy.** No `Host` fallback — non-browser callers cannot silently bypass CORS on state-changing methods.
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.
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
+
14
+ Plus two extension seams:
15
+
16
+ - `registerSecurityHeaders(builder)` — append / override headers per request.
17
+ - `registerErrorFormatter(formatter)` — shape the JSON error envelope globally.
18
+
19
+ ## CORS / origin policy
20
+
21
+ Lives in `enforceOriginPolicy` (`httpHandler.ts`):
22
+
23
+ - `origin = req.headers.origin ?? req.headers.referer ?? ''`. There is intentionally NO `Host` fallback.
24
+ - For state-changing methods (anything other than GET / HEAD / OPTIONS) with no `Origin` and no `Referer`: respond `403 'Forbidden'` (text/plain) and stop dispatch.
25
+ - For any method when an explicit `Origin` / `Referer` is supplied but `allowedOrigin(origin)` returns false: respond `403 'Forbidden'` and stop.
26
+ - Read-only methods (GET / HEAD / OPTIONS) without an origin header pass through — this keeps health probes and asset fetches from non-browser tooling working.
27
+
28
+ When `curl`-testing a write endpoint, attach `-H 'Origin: https://your-allowed-origin'` (the allow-list lives in `projectConfig.http.cors.allowedOrigins` and is enforced by `allowedOrigin` in `@luckystack/core`).
29
+
30
+ ## Security headers
31
+
32
+ `setSecurityHeaders` in `httpHandler.ts` writes the framework defaults from `projectConfig.http`:
33
+
34
+ | Header | Source | Notes |
35
+ | --- | --- | --- |
36
+ | `Access-Control-Allow-Origin` | resolved `origin` (or `''`) | Mirrors the origin policy's resolved value. |
37
+ | `Access-Control-Allow-Methods` | `cors.allowedMethods` | |
38
+ | `Access-Control-Allow-Headers` | `cors.allowedHeaders` | |
39
+ | `Access-Control-Expose-Headers` | `cors.exposedHeaders` | |
40
+ | `Access-Control-Allow-Credentials` | `cors.credentials ? 'true' : (omitted)` | |
41
+ | `Referrer-Policy` | `securityHeaders.referrerPolicy` | |
42
+ | `X-Frame-Options` | `securityHeaders.frameOptions` | |
43
+ | `X-XSS-Protection` | `securityHeaders.xssProtection` | |
44
+ | `X-Content-Type-Options` | `securityHeaders.contentTypeOptions` | |
45
+
46
+ After defaults, the registered builder (if any) runs. Errors in the builder are caught and logged via `getLogger().warn('securityHeadersBuilder threw — falling back to defaults', { err })`; the request continues with defaults intact.
47
+
48
+ ### API — `registerSecurityHeaders(builder | null)`
49
+
50
+ **Signature:**
51
+
52
+ ```typescript
53
+ export type SecurityHeadersBuilder = (
54
+ req: IncomingMessage,
55
+ ) => Record<string, string> | null | undefined;
56
+
57
+ export const registerSecurityHeaders = (builder: SecurityHeadersBuilder | null): void;
58
+ export const getSecurityHeadersBuilder = (): SecurityHeadersBuilder | null;
59
+ ```
60
+
61
+ **Behavior:**
62
+
63
+ - Last-write-wins; subsequent calls replace the prior builder. Pass `null` to unregister.
64
+ - Builder is invoked for every request after framework defaults. A returned object's entries are written via `res.setHeader(name, value)` so they REPLACE same-name defaults (per Node semantics).
65
+ - A nullish return (`null` / `undefined`) means "use defaults only".
66
+
67
+ **Example:**
68
+
69
+ ```typescript
70
+ import { registerSecurityHeaders } from '@luckystack/server';
71
+
72
+ registerSecurityHeaders((req) => ({
73
+ 'Content-Security-Policy': "default-src 'self'; img-src 'self' data:; script-src 'self'",
74
+ 'Strict-Transport-Security': 'max-age=63072000; includeSubDomains; preload',
75
+ 'Permissions-Policy': 'camera=(), microphone=()',
76
+ }));
77
+ ```
78
+
79
+ ## CSRF middleware
80
+
81
+ Lives in `enforceCsrfOnStateChangingRequest` (`csrfMiddleware.ts`). Runs after origin/security-header setup but BEFORE route dispatch.
82
+
83
+ **Activation predicate (all required):**
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.
90
+
91
+ When the predicate is true:
92
+
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.' }`.
97
+
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.
99
+
100
+ ## `/_test/reset` fail-closed contract
101
+
102
+ Source: `httpRoutes/testResetRoute.ts`.
103
+
104
+ **Activation predicate (both required):**
105
+
106
+ 1. `process.env.NODE_ENV` is exactly `'development'` OR `'test'`. Any other value (including unset / `'production'` / `'staging'`) returns `404 { status: 'error', errorCode: 'notFound' }`. This is the fail-closed gate — `NODE_ENV !== 'production'` is NOT a sufficient check.
107
+ 2. `process.env.TEST_RESET_TOKEN` is non-empty AND `req.headers['x-test-reset-token'] === process.env.TEST_RESET_TOKEN`. Any mismatch (including an unset token) returns `403 { status: 'error', errorCode: 'auth.forbidden' }`.
108
+
109
+ **Authorization header — actual source-of-truth:**
110
+
111
+ The handler reads `req.headers['x-test-reset-token']`. Wire your test client to send:
112
+
113
+ ```
114
+ x-test-reset-token: <value of TEST_RESET_TOKEN>
115
+ ```
116
+
117
+ (The README quickstart mentions `Authorization: Bearer ${TOKEN}` — the running code reads `x-test-reset-token`. Use that header.)
118
+
119
+ **What the reset clears:**
120
+
121
+ | Step | Action |
122
+ | --- | --- |
123
+ | 1 | `clearAllRateLimits()` — purges in-memory + Redis rate-limit counters. Always reported in `cleared`. |
124
+ | 2 | `redis SCAN` + `DEL` keys matching `<projectName>-session:*`. Reported as `'sessions'` when at least one key is deleted. |
125
+ | 3 | `redis SCAN` + `DEL` keys matching `<projectName>-activeUsers:*`. Reported as `'activeUsers'` when at least one key is deleted. |
126
+ | 4 (opt-in) | When `?include=hooks` is supplied, `clearAllHooks()` empties every framework hook registration. Reported as `'hooks'`. |
127
+
128
+ `getProjectName()` (from `@luckystack/core`) is the single source of truth for the key prefix — same helper used by `session.ts` and `rateLimiter.ts`.
129
+
130
+ **Response:** `200 application/json { status: 'success', cleared: string[] }`. The `cleared` array reflects which subsystems reported deletions.
131
+
132
+ **Errors / edge cases:**
133
+
134
+ - Malformed `req.url` is detected with `URL.canParse(rawUrl, base)`. When false, `?include=hooks` is treated as absent.
135
+ - Redis `SCAN`+`DEL` errors are swallowed per pattern — the cleared array simply omits the label.
136
+ - The hook-clear is opt-in because clearing every hook also removes framework-internal registrations (e.g. presence's `postLogout`).
137
+
138
+ **Example — invoking from a test runner:**
139
+
140
+ ```bash
141
+ curl -X POST http://127.0.0.1:80/_test/reset \
142
+ -H 'x-test-reset-token: dev-secret-123' \
143
+ -H 'Origin: http://127.0.0.1:80'
144
+ ```
145
+
146
+ `.env.local` (dev / test only):
147
+
148
+ ```
149
+ NODE_ENV=development
150
+ TEST_RESET_TOKEN=dev-secret-123
151
+ ```
152
+
153
+ `@luckystack/test-runner`'s `resetServerState` reads the same env var.
154
+
155
+ ## Error formatter
156
+
157
+ Lives in `errorFormatterRegistry.ts`. The framework normalizes errors via `normalizeErrorResponse` from `@luckystack/core` (i18n + envelope shape). The registered formatter is the GLOBAL hook — per-endpoint `errorFormatter` exports on route files take precedence.
158
+
159
+ **Resolution order at error time:**
160
+
161
+ 1. Per-endpoint `errorFormatter` export (when the route file declares one).
162
+ 2. Global formatter from `registerErrorFormatter(...)`.
163
+ 3. Framework default `normalizeErrorResponse`.
164
+
165
+ ### API — `registerErrorFormatter(formatter | null)`
166
+
167
+ **Signature:**
168
+
169
+ ```typescript
170
+ export interface ErrorFormatterContext {
171
+ routeName: string;
172
+ transport: 'socket' | 'http';
173
+ userId?: string | null;
174
+ }
175
+
176
+ export type ErrorFormatter = (
177
+ error: {
178
+ status: 'error';
179
+ errorCode: string;
180
+ message?: string;
181
+ httpStatus?: number;
182
+ [key: string]: unknown;
183
+ },
184
+ ctx: ErrorFormatterContext,
185
+ ) => Record<string, unknown>;
186
+
187
+ export const registerErrorFormatter = (formatter: ErrorFormatter | null): void;
188
+ export const getErrorFormatter = (): ErrorFormatter | null;
189
+ ```
190
+
191
+ **Behavior:**
192
+
193
+ - Receives the already-normalized envelope plus `{ routeName, transport, userId? }`.
194
+ - Return a (possibly extended) object that the framework emits verbatim.
195
+ - Pass `null` to unregister.
196
+
197
+ **Example — add correlation IDs + a legacy alias key:**
198
+
199
+ ```typescript
200
+ import { registerErrorFormatter } from '@luckystack/server';
201
+
202
+ registerErrorFormatter((error, ctx) => ({
203
+ ...error,
204
+ // legacy clients expect `reason`; new clients read `errorCode`.
205
+ reason: error.errorCode,
206
+ correlationId: ctx.routeName + ':' + (ctx.userId ?? 'anon'),
207
+ }));
208
+ ```
209
+
210
+ ## Hooks dispatched
211
+
212
+ | Hook | Payload | When |
213
+ | --- | --- | --- |
214
+ | `csrfMismatch` | `{ route, method, requestId, userId, providedToken: boolean }` | CSRF middleware rejects a write. `providedToken` is `Boolean(value)` — never the token itself. |
215
+ | `preHttpRequest` | `{ method, url, requestId, origin, headers }` | Every request before dispatch; can stop with `HookStopSignal`. `headers` excludes `authorization`, `cookie`, `set-cookie`, `x-csrf-token`. |
216
+ | `rateLimitExceeded` | `{ scope, key, limit, windowMs, count, route, ip }` | Credentials login (`handleAuthApiRoute`). |
217
+
218
+ ## Config keys
219
+
220
+ | Source | Key | Effect |
221
+ | --- | --- | --- |
222
+ | env | `NODE_ENV` | Gates `/_test/reset` — must be exactly `'development'` or `'test'`. |
223
+ | env | `TEST_RESET_TOKEN` | Required for `/_test/reset` to be reachable. Compared against `x-test-reset-token`. |
224
+ | env | `SECURE` | When `'true'`, session cookies are emitted with `Secure;`. |
225
+ | config | `projectConfig.http.cors.*` | `Access-Control-*` headers. |
226
+ | config | `projectConfig.http.securityHeaders.*` | `Referrer-Policy`, `X-Frame-Options`, `X-XSS-Protection`, `X-Content-Type-Options`. |
227
+ | config | `projectConfig.session.basedToken` | Switches cookie-mode (CSRF enforced) vs header-mode (CSRF skipped — header transport already binds to the request). |
228
+ | config | `projectConfig.http.testResetEndpoint` | Path for `handleTestResetRoute`. Default `/_test/reset`. |
229
+
230
+ ## Related
231
+
232
+ - Function INDEX: `packages/server/CLAUDE.md`
233
+ - Request pipeline: `packages/server/docs/request-pipeline.md`
234
+ - HTTP routes: `packages/server/docs/http-routes.md`
235
+ - Architecture: `docs/ARCHITECTURE_API.md`, `docs/ARCHITECTURE_AUTH.md`
236
+ - README: `packages/server/README.md`
package/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "@luckystack/server",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "type": "module",
9
+ "description": "One-call server bootstrap for LuckyStack: HTTP + Socket.io + framework routes (api, sync, _health, /auth, uploads) wired together. Consumer's server.ts shrinks to ~20 lines.",
10
+ "license": "MIT",
11
+ "author": "Mathijs van Melick <mathijsvanmelick3@gmail.com>",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/ItsLucky23/LuckyStack-v2.git",
15
+ "directory": "packages/server"
16
+ },
17
+ "homepage": "https://github.com/ItsLucky23/LuckyStack-v2/tree/master/packages/server#readme",
18
+ "bugs": {
19
+ "url": "https://github.com/ItsLucky23/LuckyStack-v2/issues"
20
+ },
21
+ "keywords": [
22
+ "luckystack",
23
+ "server",
24
+ "bootstrap",
25
+ "socket.io",
26
+ "http",
27
+ "framework",
28
+ "fullstack"
29
+ ],
30
+ "engines": {
31
+ "node": ">=20.0.0"
32
+ },
33
+ "main": "./dist/index.js",
34
+ "types": "./dist/index.d.ts",
35
+ "exports": {
36
+ ".": {
37
+ "types": "./dist/index.d.ts",
38
+ "import": "./dist/index.js"
39
+ },
40
+ "./parseArgv": {
41
+ "types": "./dist/parseArgv.d.ts",
42
+ "import": "./dist/parseArgv.js"
43
+ }
44
+ },
45
+ "files": [
46
+ "dist",
47
+ "README.md",
48
+ "CLAUDE.md",
49
+ "docs",
50
+ "LICENSE",
51
+ "CHANGELOG.md"
52
+ ],
53
+ "scripts": {
54
+ "build": "tsup",
55
+ "build:watch": "tsup --watch",
56
+ "test": "vitest run"
57
+ },
58
+ "dependencies": {
59
+ "@luckystack/api": "^0.1.0",
60
+ "@luckystack/core": "^0.1.0",
61
+ "@luckystack/login": "^0.1.0",
62
+ "@luckystack/presence": "^0.1.0",
63
+ "@luckystack/sync": "^0.1.0"
64
+ },
65
+ "peerDependencies": {
66
+ "@prisma/client": "^6.19.0",
67
+ "socket.io": "^4.8.0",
68
+ "@luckystack/devkit": "^0.1.0",
69
+ "@luckystack/docs-ui": "^0.1.0",
70
+ "@luckystack/email": "^0.1.0",
71
+ "@luckystack/error-tracking": "^0.1.0"
72
+ },
73
+ "peerDependenciesMeta": {
74
+ "@luckystack/devkit": { "optional": true },
75
+ "@luckystack/docs-ui": { "optional": true },
76
+ "@luckystack/email": { "optional": true },
77
+ "@luckystack/error-tracking": { "optional": true }
78
+ }
79
+ }