@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.
- package/AGENTS.md +7 -0
- package/CHANGELOG.md +11 -1
- package/README.md +2 -200
- package/dist/afterListen.d.ts +2 -0
- package/dist/argv.d.ts +8 -0
- package/dist/bootstrap.d.ts +36 -0
- package/dist/capabilities.d.ts +10 -0
- package/dist/createServer.d.ts +4 -0
- package/dist/customRoutesRegistry.d.ts +9 -0
- package/dist/devServerInfo.d.ts +7 -0
- package/dist/devToolsStatus.d.ts +3 -0
- package/dist/errorFormatterRegistry.d.ts +2 -0
- package/dist/hookPayloads.d.ts +62 -0
- package/dist/httpHandler.d.ts +3 -0
- package/dist/httpRoutes/apiRoute.d.ts +2 -0
- package/dist/httpRoutes/authApiRoute.d.ts +2 -0
- package/dist/httpRoutes/authCallbackRoute.d.ts +2 -0
- package/dist/httpRoutes/authLogoutRoute.d.ts +2 -0
- package/dist/httpRoutes/authProvidersRoute.d.ts +2 -0
- package/dist/httpRoutes/authSecondFactorRoutes.d.ts +3 -0
- package/dist/httpRoutes/csrfMiddleware.d.ts +8 -0
- package/dist/httpRoutes/csrfRoute.d.ts +2 -0
- package/dist/httpRoutes/customRoutes.d.ts +3 -0
- package/dist/httpRoutes/faviconRoute.d.ts +2 -0
- package/dist/httpRoutes/healthRoutes.d.ts +4 -0
- package/dist/httpRoutes/resolveRequesterIp.d.ts +27 -0
- package/dist/httpRoutes/sessionCookie.d.ts +1 -0
- package/dist/httpRoutes/staticRoutes.d.ts +2 -0
- package/dist/httpRoutes/syncRoute.d.ts +2 -0
- package/dist/httpRoutes/testResetRoute.d.ts +2 -0
- package/dist/httpRoutes/timingSafeEqual.d.ts +1 -0
- package/dist/httpRoutes/types.d.ts +15 -0
- package/dist/httpRoutes/uploadsRoute.d.ts +2 -0
- package/dist/index.d.ts +21 -352
- package/dist/index.js +32 -7
- package/dist/index.js.map +1 -1
- package/dist/loadSocket.d.ts +21 -0
- package/dist/logSanitize.d.ts +1 -0
- package/dist/originExemptRegistry.d.ts +13 -0
- package/dist/parseArgv.d.ts +1 -2
- package/dist/portResolution.d.ts +7 -0
- package/dist/runtimeMapsLoader.d.ts +46 -0
- package/dist/securityHeadersRegistry.d.ts +13 -0
- package/dist/sse.d.ts +11 -0
- package/dist/stopServer.d.ts +28 -0
- package/dist/types.d.ts +138 -0
- package/dist/verifyBootstrap.d.ts +18 -0
- package/docs/argv-parsing.md +3 -244
- package/docs/create-server.md +3 -327
- package/docs/http-routes.md +3 -319
- package/docs/request-pipeline.md +3 -196
- package/docs/runtime-maps.md +3 -207
- package/docs/security-defaults.md +7 -236
- package/package.json +12 -12
- package/CLAUDE.md +0 -89
|
@@ -1,242 +1,13 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Server security boundaries
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Origin is not identity.** HTTP requests with an explicit disallowed Origin/Referer are rejected; state-changing requests without either are rejected rather than trusting the caller-controlled Host header. Origin-less read-only probes remain possible. CORS response headers are not permission to skip route authorization. For testing browser-like writes, supply an allowed `Origin` header. Source: `../src/httpHandler.ts`, `../../core/src/checkOrigin.ts`.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**Cookies need CSRF protection.** Browsers attach cookies automatically; state-changing framework and custom routes must verify a token before execution unless covered by narrowly scoped auth bootstrap or signed-webhook exemption. With a session, compare to its CSRF token; without login use the double-submit contract. Do not log the token in mismatch hooks. Explicit bearer-mode credentials do not use the same ambient-cookie trust model. Source: `../src/httpRoutes/csrfMiddleware.ts`, `../src/httpRoutes/csrfRoute.ts`, `../../core/src/extractToken.ts`.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
**Exemption is not authentication.** A route exempt from origin/CSRF checks must verify a signature or equivalent credential itself; an exemption must not make cookie-protected data readable by arbitrary origins. Keep OAuth state validation and redirect allowlisting intact (`../src/originExemptRegistry.ts`, `../../login/src/login.ts`). Security-header customizations must not silently remove required protection (`../src/securityHeadersRegistry.ts`).
|
|
8
8
|
|
|
9
|
-
|
|
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 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
|
-
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.
|
|
9
|
+
**Boot warnings matter.** Login-bootstrap and OAuth-callback routes cannot present a pre-existing CSRF token; their exemption depends on restrictive session-cookie SameSite policy. Relaxing that policy without a replacement origin/CSRF defense enables login CSRF. The unauthenticated health endpoint can reveal fingerprints of synchronized environment secrets: do not publish plain unsalted hashes, and do not assume a per-boot keyed hash protects low-entropy values from an attacker who can read its public boot identifier. See `../src/verifyBootstrap.ts`, `../src/httpRoutes/healthRoutes.ts`.
|
|
13
10
|
|
|
14
|
-
|
|
11
|
+
**Test reset fails closed.** Its mode and secret checks exist to prevent test-only state deletion in live environments. Do not loosen either condition. Custom session adapters must clean up their own test state; the built-in reset cannot know their storage. See `../src/httpRoutes/testResetRoute.ts`.
|
|
15
12
|
|
|
16
|
-
-
|
|
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; 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(...)`.
|
|
89
|
-
|
|
90
|
-
The validation branch then depends on whether `@luckystack/login` is installed:
|
|
91
|
-
|
|
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`.
|
|
96
|
-
|
|
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.
|
|
103
|
-
|
|
104
|
-
## `/_test/reset` fail-closed contract
|
|
105
|
-
|
|
106
|
-
Source: `httpRoutes/testResetRoute.ts`.
|
|
107
|
-
|
|
108
|
-
**Activation predicate (both required):**
|
|
109
|
-
|
|
110
|
-
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.
|
|
111
|
-
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' }`.
|
|
112
|
-
|
|
113
|
-
**Authorization header — actual source-of-truth:**
|
|
114
|
-
|
|
115
|
-
The handler reads `req.headers['x-test-reset-token']`. Wire your test client to send:
|
|
116
|
-
|
|
117
|
-
```
|
|
118
|
-
x-test-reset-token: <value of TEST_RESET_TOKEN>
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
(The README quickstart mentions `Authorization: Bearer ${TOKEN}` — the running code reads `x-test-reset-token`. Use that header.)
|
|
122
|
-
|
|
123
|
-
**What the reset clears:**
|
|
124
|
-
|
|
125
|
-
| Step | Action |
|
|
126
|
-
| --- | --- |
|
|
127
|
-
| 1 | `clearAllRateLimits()` — purges in-memory + Redis rate-limit counters. Always reported in `cleared`. |
|
|
128
|
-
| 2 | `redis SCAN` + `DEL` keys matching `<projectName>-session:*`. Reported as `'sessions'` when at least one key is deleted. |
|
|
129
|
-
| 3 | `redis SCAN` + `DEL` keys matching `<projectName>-activeUsers:*`. Reported as `'activeUsers'` when at least one key is deleted. |
|
|
130
|
-
| 4 (opt-in) | When `?include=hooks` is supplied, `clearAllHooks()` empties every framework hook registration. Reported as `'hooks'`. |
|
|
131
|
-
|
|
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.
|
|
135
|
-
|
|
136
|
-
**Response:** `200 application/json { status: 'success', cleared: string[] }`. The `cleared` array reflects which subsystems reported deletions.
|
|
137
|
-
|
|
138
|
-
**Errors / edge cases:**
|
|
139
|
-
|
|
140
|
-
- Malformed `req.url` is detected with `URL.canParse(rawUrl, base)`. When false, `?include=hooks` is treated as absent.
|
|
141
|
-
- Redis `SCAN`+`DEL` errors are swallowed per pattern — the cleared array simply omits the label.
|
|
142
|
-
- The hook-clear is opt-in because clearing every hook also removes framework-internal registrations (e.g. presence's `postLogout`).
|
|
143
|
-
|
|
144
|
-
**Example — invoking from a test runner:**
|
|
145
|
-
|
|
146
|
-
```bash
|
|
147
|
-
curl -X POST http://127.0.0.1:80/_test/reset \
|
|
148
|
-
-H 'x-test-reset-token: dev-secret-123' \
|
|
149
|
-
-H 'Origin: http://127.0.0.1:80'
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
`.env.local` (dev / test only):
|
|
153
|
-
|
|
154
|
-
```
|
|
155
|
-
NODE_ENV=development
|
|
156
|
-
TEST_RESET_TOKEN=dev-secret-123
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
`@luckystack/test-runner`'s `resetServerState` reads the same env var.
|
|
160
|
-
|
|
161
|
-
## Error formatter
|
|
162
|
-
|
|
163
|
-
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.
|
|
164
|
-
|
|
165
|
-
**Resolution order at error time:**
|
|
166
|
-
|
|
167
|
-
1. Per-endpoint `errorFormatter` export (when the route file declares one).
|
|
168
|
-
2. Global formatter from `registerErrorFormatter(...)`.
|
|
169
|
-
3. Framework default `normalizeErrorResponse`.
|
|
170
|
-
|
|
171
|
-
### API — `registerErrorFormatter(formatter | null)`
|
|
172
|
-
|
|
173
|
-
**Signature:**
|
|
174
|
-
|
|
175
|
-
```typescript
|
|
176
|
-
export interface ErrorFormatterContext {
|
|
177
|
-
routeName: string;
|
|
178
|
-
transport: 'socket' | 'http';
|
|
179
|
-
userId?: string | null;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
export type ErrorFormatter = (
|
|
183
|
-
error: {
|
|
184
|
-
status: 'error';
|
|
185
|
-
errorCode: string;
|
|
186
|
-
message?: string;
|
|
187
|
-
httpStatus?: number;
|
|
188
|
-
[key: string]: unknown;
|
|
189
|
-
},
|
|
190
|
-
ctx: ErrorFormatterContext,
|
|
191
|
-
) => Record<string, unknown>;
|
|
192
|
-
|
|
193
|
-
export const registerErrorFormatter = (formatter: ErrorFormatter | null): void;
|
|
194
|
-
export const getErrorFormatter = (): ErrorFormatter | null;
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
**Behavior:**
|
|
198
|
-
|
|
199
|
-
- Receives the already-normalized envelope plus `{ routeName, transport, userId? }`.
|
|
200
|
-
- Return a (possibly extended) object that the framework emits verbatim.
|
|
201
|
-
- Pass `null` to unregister.
|
|
202
|
-
|
|
203
|
-
**Example — add correlation IDs + a legacy alias key:**
|
|
204
|
-
|
|
205
|
-
```typescript
|
|
206
|
-
import { registerErrorFormatter } from '@luckystack/server';
|
|
207
|
-
|
|
208
|
-
registerErrorFormatter((error, ctx) => ({
|
|
209
|
-
...error,
|
|
210
|
-
// legacy clients expect `reason`; new clients read `errorCode`.
|
|
211
|
-
reason: error.errorCode,
|
|
212
|
-
correlationId: ctx.routeName + ':' + (ctx.userId ?? 'anon'),
|
|
213
|
-
}));
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
## Hooks dispatched
|
|
217
|
-
|
|
218
|
-
| Hook | Payload | When |
|
|
219
|
-
| --- | --- | --- |
|
|
220
|
-
| `csrfMismatch` | `{ route, method, requestId, userId, providedToken: boolean }` | CSRF middleware rejects a write. `providedToken` is `Boolean(value)` — never the token itself. |
|
|
221
|
-
| `preHttpRequest` | `{ method, url, requestId, origin, headers }` | Every request before dispatch; can stop with `HookStopSignal`. `headers` excludes `authorization`, `cookie`, `set-cookie`, `x-csrf-token`. |
|
|
222
|
-
| `rateLimitExceeded` | `{ scope, key, limit, windowMs, count, route, ip }` | Credentials login (`handleAuthApiRoute`). |
|
|
223
|
-
|
|
224
|
-
## Config keys
|
|
225
|
-
|
|
226
|
-
| Source | Key | Effect |
|
|
227
|
-
| --- | --- | --- |
|
|
228
|
-
| env | `NODE_ENV` | Gates `/_test/reset` — must be exactly `'development'` or `'test'`. |
|
|
229
|
-
| env | `TEST_RESET_TOKEN` | Required for `/_test/reset` to be reachable. Compared against `x-test-reset-token`. |
|
|
230
|
-
| env | `SECURE` | When `'true'`, session cookies are emitted with `Secure;`. |
|
|
231
|
-
| config | `projectConfig.http.cors.*` | `Access-Control-*` headers. |
|
|
232
|
-
| config | `projectConfig.http.securityHeaders.*` | `Referrer-Policy`, `X-Frame-Options`, `X-XSS-Protection`, `X-Content-Type-Options`. |
|
|
233
|
-
| config | `projectConfig.session.basedToken` | Switches cookie-mode (CSRF enforced) vs header-mode (CSRF skipped — header transport already binds to the request). |
|
|
234
|
-
| config | `projectConfig.http.testResetEndpoint` | Path for `handleTestResetRoute`. Default `/_test/reset`. |
|
|
235
|
-
|
|
236
|
-
## Related
|
|
237
|
-
|
|
238
|
-
- Function INDEX: `packages/server/CLAUDE.md`
|
|
239
|
-
- Request pipeline: `packages/server/docs/request-pipeline.md`
|
|
240
|
-
- HTTP routes: `packages/server/docs/http-routes.md`
|
|
241
|
-
- Architecture: `docs/ARCHITECTURE_API.md`, `docs/ARCHITECTURE_AUTH.md`
|
|
242
|
-
- README: `packages/server/README.md`
|
|
13
|
+
**A scoped reset is exact, and a bad selector is an error.** The unscoped reset also deletes every framework session, including the one an authenticated test sweep runs on, so the reset accepts a target selector and the test-runner always narrows it to rate limits. A selected reset runs exactly the listed targets; an unknown or empty target is rejected rather than silently resetting nothing or everything. Changing the unscoped reset instead was rejected because CI setups rely on it to log users out between suites (ADR 0064). The valid targets live in one list in core that server and runner both read; a new target belongs there so the two sides cannot drift. Source: `../../core/src/testResetTargets.ts`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@luckystack/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"files": [
|
|
47
47
|
"dist",
|
|
48
48
|
"README.md",
|
|
49
|
-
"
|
|
49
|
+
"AGENTS.md",
|
|
50
50
|
"docs",
|
|
51
51
|
"LICENSE",
|
|
52
52
|
"CHANGELOG.md"
|
|
@@ -57,20 +57,20 @@
|
|
|
57
57
|
"test": "vitest run"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@luckystack/api": "^0.
|
|
61
|
-
"@luckystack/core": "^0.
|
|
60
|
+
"@luckystack/api": "^0.11.0",
|
|
61
|
+
"@luckystack/core": "^0.11.0"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
64
|
"@prisma/client": "^6.19.0",
|
|
65
65
|
"socket.io": "^4.8.0",
|
|
66
|
-
"@luckystack/cron": "^0.
|
|
67
|
-
"@luckystack/devkit": "^0.
|
|
68
|
-
"@luckystack/docs-ui": "^0.
|
|
69
|
-
"@luckystack/email": "^0.
|
|
70
|
-
"@luckystack/error-tracking": "^0.
|
|
71
|
-
"@luckystack/login": "^0.
|
|
72
|
-
"@luckystack/presence": "^0.
|
|
73
|
-
"@luckystack/sync": "^0.
|
|
66
|
+
"@luckystack/cron": "^0.11.0",
|
|
67
|
+
"@luckystack/devkit": "^0.11.0",
|
|
68
|
+
"@luckystack/docs-ui": "^0.11.0",
|
|
69
|
+
"@luckystack/email": "^0.11.0",
|
|
70
|
+
"@luckystack/error-tracking": "^0.11.0",
|
|
71
|
+
"@luckystack/login": "^0.11.0",
|
|
72
|
+
"@luckystack/presence": "^0.11.0",
|
|
73
|
+
"@luckystack/sync": "^0.11.0"
|
|
74
74
|
},
|
|
75
75
|
"peerDependenciesMeta": {
|
|
76
76
|
"@prisma/client": {
|
package/CLAUDE.md
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
# @luckystack/server
|
|
2
|
-
|
|
3
|
-
> AI summary + function INDEX. For deep specs see `docs/` next to this file.
|
|
4
|
-
|
|
5
|
-
## What this package does
|
|
6
|
-
|
|
7
|
-
One-call server bootstrap for a LuckyStack project. Wires together a raw Node.js HTTP server, Socket.io (with optional Redis adapter), framework routes (`/api/*`, `/sync/*`, `/_health`, `/livez`, `/readyz`, `/_test/reset`, `/auth/*`, `/uploads/*`, including `GET /auth/csrf`), CSRF middleware, CORS + security headers, presence broadcasting, and dev-only hot reload. Consumer's `server.ts` shrinks to roughly twenty lines. Boots are gated by `verifyBootstrap` so missing registrations surface a single readable error instead of mid-request crashes. If devkit initialization fails, API/sync dispatch and `/readyz` all fail closed with 503.
|
|
8
|
-
|
|
9
|
-
## When to USE this package
|
|
10
|
-
|
|
11
|
-
- Project needs the full LuckyStack runtime (HTTP + sockets + framework routes wired in one call).
|
|
12
|
-
- Multi-bundle or multi-service deploys that need positional argv parsing (`npm run server -- <bundles> <port>`) and merged runtime maps across presets.
|
|
13
|
-
- Consumers that want framework-shipped health / liveness / readiness probes, CSRF protection, and dev hot reload without building plumbing themselves.
|
|
14
|
-
- Adding project-specific HTTP endpoints via `registerCustomRoute(...)` or the `customRoutes` option without forking the package.
|
|
15
|
-
- Plugins / framework packages that ship socket-lifecycle behavior via the exported hook payload types (e.g. `@luckystack/presence`).
|
|
16
|
-
|
|
17
|
-
## When to NOT suggest this (yet)
|
|
18
|
-
|
|
19
|
-
- Standalone microservices that do not use the LuckyStack registries (project config, deploy config, runtime maps, localized normalizer) — there is nothing for `verifyBootstrap` to verify, and the wiring overhead buys nothing.
|
|
20
|
-
- A pure router / load-balancer process — use `@luckystack/router` instead; `@luckystack/server` is for the actual service nodes behind the router.
|
|
21
|
-
- Build-time tooling, code generation, or migration scripts that never accept HTTP / socket traffic.
|
|
22
|
-
- Replacing the HTTP layer with Express / Fastify — the package owns the raw Node HTTP server and dispatch table; bringing a second framework on top breaks the route handler contract.
|
|
23
|
-
|
|
24
|
-
## Function Index
|
|
25
|
-
|
|
26
|
-
| Function / Export | One-liner | Deep doc |
|
|
27
|
-
|---|---|---|
|
|
28
|
-
| `createLuckyStackServer(options)` | Lower-level factory that returns `{ httpServer, ioServer, listen, afterListen, stop, close }`. Wires HTTP + Socket.io + framework routes; after a successful listen starts the boot-UUID TTL heartbeat, and stops it during shutdown. Installs a persistent `httpServer.on('error')` listener and (in prod) SIGTERM/SIGINT → graceful `stop()`. | -> docs/create-server.md |
|
|
29
|
-
| `RunningLuckyStackServer.afterListen(task, options?)` | Runs post-listen work (queue workers, warm-up, reconciliation) isolated from the caller's fatal boot chain. A rejection is logged loudly and swallowed by default — the server is already serving, and killing it over one broken dependency turns an outage into a supervisor crash-loop. `{ fatal: true }` re-throws for a task the process cannot run without; `{ label }` names it in the log. | -> docs/create-server.md |
|
|
30
|
-
| `RunningLuckyStackServer.stop(options?)` / `.close(options?)` | Graceful shutdown (MIS-016). Stops accepting new connections, dispatches the core `preServerStop` hook, flushes error-trackers (bounded by a per-step timeout race), then closes io + http + the Redis-adapter pub/sub clients. Each step isolated + time-bounded so one hanging step can't stall shutdown. Idempotent. `options`: `{ reason?, timeoutMs? }` (default `timeoutMs` 10000). | -> docs/create-server.md |
|
|
31
|
-
| `bootstrapLuckyStack(options)` | High-level entry: auto-imports `luckystack/<pkg>/*.ts` overlay in topological order, then delegates to `createLuckyStackServer`. An overlay file that fails to import (classic: it imports a package removed via `luckystack remove`) aborts boot with an ACTIONABLE error naming the file, instead of a raw ERR_MODULE_NOT_FOUND. | -> docs/create-server.md |
|
|
32
|
-
| `OVERLAY_ORDER` (exported const) | The canonical overlay walk order (`core, deploy, login, email, sentry, presence, cron, docs-ui, server`). Exported so the consumer's `scripts/bundleServer.mjs` imports it at build time — a hardcoded bundler copy once silently dropped the cron slot from prod; `overlayOrderParity.test.ts` pins the bundlers' fallback lists to this. | -> docs/create-server.md |
|
|
33
|
-
| `collectOverlayEntries(packageDir)` | Single owner of what one `luckystack/<pkg>/` folder contributes: `{ fileNames, ignoredDirectoryNames }`. `index.*` first, then the rest alphabetically; `.ts`/`.js` only; **test files excluded** (`.tests.`/`.test.`/`.spec.`, plus `__tests__`/`__mocks__` folders) so they never boot or reach the production bundle. The walk is FLAT — any other subdirectory comes back in `ignoredDirectoryNames` and the loader warns, instead of dropping overlay code in silence. Exported so `scripts/bundleServer.mjs` decides identically. ADR 0047. | -> docs/create-server.md |
|
|
34
|
-
| `registerOverlayLoader(loader)` | Production-bundle seam: the server bundler (`scripts/bundleServer.mjs`) generates an entry that statically imports the overlay files and registers this loader; `bootstrapLuckyStack` then runs it INSTEAD of the runtime `.ts` folder walk (plain `node` cannot import `.ts` — ERR_UNKNOWN_FILE_EXTENSION). | -> docs/create-server.md |
|
|
35
|
-
| `verifyBootstrap(requirements?)` | Pre-flight check for ProjectConfig / DeployConfig / ServicesConfig / OAuth / RuntimeMapsProvider / LocalizedNormalizer. Throws one descriptive `Error`. | -> docs/create-server.md |
|
|
36
|
-
| `parseServerArgv(argv)` | Pure parser: validates positional `<bundles> [port]` and returns `{ bundles, port }`. Throws on malformed input. | -> docs/argv-parsing.md |
|
|
37
|
-
| `applyServerArgv()` | Side-effect runner: parses `process.argv.slice(2)`, stores bundles, and registers the port through browser-safe `@luckystack/core/config`. Idempotent. | -> docs/argv-parsing.md |
|
|
38
|
-
| `getParsedBundles()` | Returns the preset list parsed by `applyServerArgv()` (empty array before first call). | -> docs/argv-parsing.md |
|
|
39
|
-
| `getParsedPort()` | Returns the port parsed by `applyServerArgv()` (`null` if argv omitted it). | -> docs/argv-parsing.md |
|
|
40
|
-
| `@luckystack/server/parseArgv` (side-effect import) | First-line import that runs `applyServerArgv()` before consumer `config.ts` reads the typed override registry. | -> docs/argv-parsing.md |
|
|
41
|
-
| `createProdRuntimeMapsProvider(options)` | Build a `RuntimeMapsProvider` that loads generated maps in prod and delegates to devkit discovery in dev. Composed presets reject duplicate API/sync ownership, deduplicate equivalent full-registry function entries, and reject differing function implementations. Returns the provider without registering. | -> docs/runtime-maps.md |
|
|
42
|
-
| `registerProdRuntimeMapsProvider(options)` | Convenience wrapper: builds the provider AND calls `registerRuntimeMapsProvider`. Most consumers want this. | -> docs/runtime-maps.md |
|
|
43
|
-
| `registerCustomRoute(handler, options?)` | Append a custom HTTP route handler. `options.phase`: `'post-params'` (default, runs after body parse) or `'pre-params'` (runs before `getParams`, raw `req` stream intact — webhooks + streaming uploads). | -> /docs/ARCHITECTURE_HTTP.md |
|
|
44
|
-
| `getCustomRoutes()` | Read the `'post-params'` registry snapshot (the default phase). | -> /docs/ARCHITECTURE_HTTP.md |
|
|
45
|
-
| `getPreParamsCustomRoutes()` | Read the `'pre-params'` registry snapshot. | -> /docs/ARCHITECTURE_HTTP.md |
|
|
46
|
-
| `clearCustomRoutes()` | Clear both phases (used by test resets). | -> /docs/ARCHITECTURE_HTTP.md |
|
|
47
|
-
| `registerOriginExemptPath({ pathPrefix })` | Exempt a path prefix from the browser origin gate so a server-to-server webhook (no `Origin`/`Referer`) reaches its handler. Empty/fail-closed by default. **Exemption ≠ auth** — the handler MUST verify a signature/secret. | -> /docs/ARCHITECTURE_HTTP.md |
|
|
48
|
-
| `getOriginExemptPaths()` / `clearOriginExemptPaths()` / `isOriginExemptPath(routePath)` | Read / clear / test the exempt-path registry. | -> /docs/ARCHITECTURE_HTTP.md |
|
|
49
|
-
| `registerSecurityHeaders(builder)` | Override / extend the security-headers builder applied to every HTTP response. | -> docs/security-defaults.md |
|
|
50
|
-
| `getSecurityHeadersBuilder()` | Read the currently registered builder (defaults to framework headers when no override set). | -> docs/security-defaults.md |
|
|
51
|
-
| `registerErrorFormatter(formatter)` | Override the JSON error shape returned by framework error responses. | -> docs/security-defaults.md |
|
|
52
|
-
| `getErrorFormatter()` | Read the currently registered formatter. | -> docs/security-defaults.md |
|
|
53
|
-
| Route handler: `handleLivezRoute` | Liveness probe at `projectConfig.http.liveEndpoint`. Always 200 when reachable. | -> docs/http-routes.md |
|
|
54
|
-
| Route handler: `handleReadyzRoute` | Readiness probe: boot UUID + dev-tool initialization state + Redis ping + database check (ADR 0020: a registered `registerDbHealthCheck` probe wins; else the built-in Prisma ping when Prisma is registered/resolvable; else `'skipped'` so a deliberately DB-less project can go ready). A recorded fatal devkit boot is 503 even when Redis/database are green. Response `checks.database` = tri-state, `checks.prisma` kept for compat. | -> docs/http-routes.md |
|
|
55
|
-
| Route handler: `handleHealthRoute` | Health endpoint: returns boot UUID + env hashes for router topology checks. | -> docs/http-routes.md |
|
|
56
|
-
| Route handler: `handleTestResetRoute` | Destructive test reset. Fail-closed on `NODE_ENV` and `TEST_RESET_TOKEN`. | -> docs/security-defaults.md |
|
|
57
|
-
| Hook payload: `OnSocketConnectPayload` | Payload type for `onSocketConnect` lifecycle hook handlers. | -> docs/create-server.md |
|
|
58
|
-
| Hook payload: `OnSocketDisconnectPayload` | Payload type for `onSocketDisconnect` lifecycle hook handlers. | -> docs/create-server.md |
|
|
59
|
-
| Hook payload: `PreRoomJoinPayload` | Payload type for `preRoomJoin` hook handlers. | -> docs/create-server.md |
|
|
60
|
-
| Hook payload: `PostRoomJoinPayload` | Payload type for `postRoomJoin` hook handlers. | -> docs/create-server.md |
|
|
61
|
-
| Hook payload: `PreRoomLeavePayload` | Payload type for `preRoomLeave` hook handlers. | -> docs/create-server.md |
|
|
62
|
-
| Hook payload: `PostRoomLeavePayload` | Payload type for `postRoomLeave` hook handlers. | -> docs/create-server.md |
|
|
63
|
-
| Hook payload: `OnLocationUpdatePayload` | Payload type for `onLocationUpdate` hook handlers. | -> docs/create-server.md |
|
|
64
|
-
| Types: `CreateLuckyStackServerOptions`, `BootstrapLuckyStackOptions`, `BootstrapRequirements`, `RunningLuckyStackServer`, `StopLuckyStackServerOptions`, `AfterListenOptions`, `RouteContext`, `StaticFileHandler`, `FaviconHandler`, `CustomRouteHandler`, `ProdRuntimeMapsLoaderOptions`, `ParsedServerArgv`, `SecurityHeadersBuilder`, `ErrorFormatter`, `ErrorFormatterContext` | Handler + option typing. | -> docs/create-server.md |
|
|
65
|
-
|
|
66
|
-
## Config keys (env vars + registerProjectConfig slots)
|
|
67
|
-
|
|
68
|
-
- Port precedence — `options.port` → positional argv registry override → `options.defaultPort` (the scaffold passes `config.ports.backend`) → `80`. Inputs must be integer ports in `0..65535`. After bind, `httpServer.address().port` is authoritative (including `listen(0)`). In dev, that port is advertised to `node_modules/.luckystack/dev-server.json`; cleanup removes it only when the exiting PID still owns it. The Vite proxy follows the advertisement for HTTP + WebSocket requests.
|
|
69
|
-
- `SERVER_IP` (env, optional, default `127.0.0.1`) — bind address fallback when `options.ip` is omitted.
|
|
70
|
-
- `NODE_ENV` (env, required for security-sensitive branches) — `development` / `test` toggle devkit hot reload + REPL and gate `/_test/reset`.
|
|
71
|
-
- `TEST_RESET_TOKEN` (env, required for `/_test/reset` to be reachable at all) — must match the `x-test-reset-token` request header. No fallback "no auth" mode.
|
|
72
|
-
- `projectConfig.http.healthEndpoint` (config) — path served by `handleHealthRoute`. Default `/_health`.
|
|
73
|
-
- `projectConfig.http.liveEndpoint` (config) — path served by `handleLivezRoute`. Default `/livez`.
|
|
74
|
-
- `projectConfig.http.readyEndpoint` (config) — path served by `handleReadyzRoute`. Default `/readyz`.
|
|
75
|
-
- `projectConfig.http.testResetEndpoint` (config) — path served by `handleTestResetRoute`. Default `/_test/reset`.
|
|
76
|
-
- `projectConfig.logging.socketStartup` / `projectConfig.logging.devLogs` (config) — gate the boot log line emitted by `listen()`.
|
|
77
|
-
- Positional argv `<bundles> [port]` — preset list (merged in `createProdRuntimeMapsProvider`) and listen port.
|
|
78
|
-
|
|
79
|
-
## Peer dependencies
|
|
80
|
-
|
|
81
|
-
- **Required (runtime deps)**: `@luckystack/api`, `@luckystack/core`.
|
|
82
|
-
- **Required peer**: `socket.io@^4.8.0`.
|
|
83
|
-
- **Optional (all auto-detected by `bootstrapLuckyStack`; the server degrades gracefully when absent — `auth.disabled` / `sync.disabled`, presence skipped)**: `@prisma/client@^6.19.0`, `@luckystack/login`, `@luckystack/presence`, `@luckystack/sync`, `@luckystack/error-tracking`, `@luckystack/email`, `@luckystack/cron`, `@luckystack/docs-ui`, `@luckystack/devkit` (dev-only, dynamically imported by the `enableDevTools` branch).
|
|
84
|
-
- **0.2.0 install-anything-anytime**: `bootstrapLuckyStack` runs an auto-detect phase BEFORE the consumer overlay that imports each installed optional package's `@luckystack/<pkg>/register` side-effect subpath (`OPTIONAL_PACKAGES` in `capabilities.ts` = `login, email, error-tracking, presence, cron, docs-ui`). So `npm i @luckystack/<pkg>` + env + restart self-wires the BACKEND with zero code edits. Detection uses `import.meta.resolve` (the `@luckystack/*` exports maps are import-only — a CJS `require.resolve` reports them absent). Client-side mounts (presence JSX, login pages) need `npx luckystack add <feature>` (`@luckystack/cli`) because Vite can't statically import an uninstalled package and file-routing only scans `src/`.
|
|
85
|
-
|
|
86
|
-
## Related
|
|
87
|
-
|
|
88
|
-
- Architecture deep-dives: `/docs/ARCHITECTURE_API.md`, `/docs/ARCHITECTURE_SOCKET.md`, `/docs/ARCHITECTURE_PACKAGING.md`, `/docs/HOSTING.md`.
|
|
89
|
-
- README (consumer quickstart): `./README.md`.
|