@luckystack/server 0.7.2 → 0.7.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/CHANGELOG.md +37 -0
- package/CLAUDE.md +3 -3
- package/dist/{chunk-X3OSC5W3.js → chunk-QLVQ75QL.js} +29 -2
- package/dist/chunk-QLVQ75QL.js.map +1 -0
- package/dist/index.js +69 -27
- package/dist/index.js.map +1 -1
- package/dist/parseArgv.js +1 -1
- package/docs/create-server.md +9 -8
- package/docs/http-routes.md +3 -2
- package/package.json +10 -10
- package/dist/chunk-X3OSC5W3.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.7.4] - 2026-07-22
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- `/readyz` now includes the recorded dev-tool initialization state. When
|
|
15
|
+
devkit boot failed and API/sync routes deliberately return 503, readiness is
|
|
16
|
+
503 too instead of advertising a hollow-green instance.
|
|
17
|
+
- Port resolution is now explicitly validated and regression-tested as
|
|
18
|
+
`options.port > argv > options.defaultPort > SERVER_PORT > 80`; invalid values
|
|
19
|
+
fail before `listen`, `65535` never retries to `65536`, and `listen(0)` registers,
|
|
20
|
+
advertises, and logs the OS-assigned port from `httpServer.address()`.
|
|
21
|
+
- Auto-increment, dev advertisement, and OAuth now use the same canonical
|
|
22
|
+
`resolveEnvKey()` environment classification (`LUCKYSTACK_ENV` first).
|
|
23
|
+
- Dev port-file cleanup is PID-owned, so an exiting old backend cannot remove a
|
|
24
|
+
newer backend's advertisement.
|
|
25
|
+
|
|
26
|
+
## [0.7.3] - 2026-07-20
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- **Stale-port bug class after a dev auto-increment hop.** When the dev server
|
|
31
|
+
auto-incremented off a busy port (`:80` → `:81`), the bind-address registry
|
|
32
|
+
still held the INTENDED port, so `checkOrigin`'s same-origin CORS entry pointed
|
|
33
|
+
at a port nothing listened on. `listenLuckyStackServer` now re-registers the
|
|
34
|
+
ACTUALLY-bound port inside the listen callback, so every `getBindAddress()`
|
|
35
|
+
reader (CORS foremost) sees reality.
|
|
36
|
+
- **OAuth now targets the actually-bound port after a hop.** The `/auth/api/<provider>`
|
|
37
|
+
authorize route rewrites a `localhost` OAuth `redirect_uri` to the port the
|
|
38
|
+
server bound (via core's `resolveDevCallbackUrl`), matching the token-exchange
|
|
39
|
+
side in `@luckystack/login` — so the OAuth round-trip reaches the live dev
|
|
40
|
+
server even after an auto-increment hop, instead of a frozen dead port.
|
|
41
|
+
- **Loud OAuth port-drift warning on a hop.** The framework auto-targets the bound
|
|
42
|
+
port for OAuth, but a provider console still exact-matches its registered
|
|
43
|
+
redirect URI — so the server now warns (naming both ports) to add the bound port
|
|
44
|
+
to the provider's authorized redirect URIs, or pin the port with
|
|
45
|
+
`SERVER_PORT_AUTO_INCREMENT=0`.
|
|
46
|
+
|
|
10
47
|
## [0.7.1] - 2026-07-18
|
|
11
48
|
|
|
12
49
|
### Fixed
|
package/CLAUDE.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## What this package does
|
|
6
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/*`, `/csrf-token`), 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.
|
|
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/*`, `/csrf-token`), 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
8
|
|
|
9
9
|
## When to USE this package
|
|
10
10
|
|
|
@@ -49,7 +49,7 @@ One-call server bootstrap for a LuckyStack project. Wires together a raw Node.js
|
|
|
49
49
|
| `registerErrorFormatter(formatter)` | Override the JSON error shape returned by framework error responses. | -> docs/security-defaults.md |
|
|
50
50
|
| `getErrorFormatter()` | Read the currently registered formatter. | -> docs/security-defaults.md |
|
|
51
51
|
| Route handler: `handleLivezRoute` | Liveness probe at `projectConfig.http.liveEndpoint`. Always 200 when reachable. | -> docs/http-routes.md |
|
|
52
|
-
| Route handler: `handleReadyzRoute` | Readiness probe: boot UUID + 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). Response `checks.database` = tri-state, `checks.prisma` kept for compat. | -> docs/http-routes.md |
|
|
52
|
+
| 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 |
|
|
53
53
|
| Route handler: `handleHealthRoute` | Health endpoint: returns boot UUID + env hashes for router topology checks. | -> docs/http-routes.md |
|
|
54
54
|
| Route handler: `handleTestResetRoute` | Destructive test reset. Fail-closed on `NODE_ENV` and `TEST_RESET_TOKEN`. | -> docs/security-defaults.md |
|
|
55
55
|
| Hook payload: `OnSocketConnectPayload` | Payload type for `onSocketConnect` lifecycle hook handlers. | -> docs/create-server.md |
|
|
@@ -63,7 +63,7 @@ One-call server bootstrap for a LuckyStack project. Wires together a raw Node.js
|
|
|
63
63
|
|
|
64
64
|
## Config keys (env vars + registerProjectConfig slots)
|
|
65
65
|
|
|
66
|
-
-
|
|
66
|
+
- Port precedence — `options.port` → positional argv → `options.defaultPort` (the scaffold passes `config.ports.backend`) → legacy `SERVER_PORT` → `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.
|
|
67
67
|
- `SERVER_IP` (env, optional, default `127.0.0.1`) — bind address fallback when `options.ip` is omitted.
|
|
68
68
|
- `NODE_ENV` (env, required for security-sensitive branches) — `development` / `test` toggle devkit hot reload + REPL and gate `/_test/reset`.
|
|
69
69
|
- `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.
|
|
@@ -1,3 +1,28 @@
|
|
|
1
|
+
// src/portResolution.ts
|
|
2
|
+
var normalizeServerPort = (value, source = "server port") => {
|
|
3
|
+
let normalized;
|
|
4
|
+
if (typeof value === "number") normalized = value;
|
|
5
|
+
else normalized = /^\d+$/.test(value) ? Number(value) : Number.NaN;
|
|
6
|
+
if (!Number.isInteger(normalized) || normalized < 0 || normalized > 65535) {
|
|
7
|
+
throw new RangeError(
|
|
8
|
+
`[luckystack:port] ${source} must be an integer from 0 through 65535, got: "${String(value)}".`
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
return normalized;
|
|
12
|
+
};
|
|
13
|
+
var resolveServerPort = ({
|
|
14
|
+
optionsPort,
|
|
15
|
+
parsedPort: parsedPort2,
|
|
16
|
+
defaultPort,
|
|
17
|
+
envPort
|
|
18
|
+
}) => {
|
|
19
|
+
if (optionsPort !== void 0) return normalizeServerPort(optionsPort, "options.port");
|
|
20
|
+
if (parsedPort2 !== void 0 && parsedPort2 !== null) return normalizeServerPort(parsedPort2, "argv port");
|
|
21
|
+
if (defaultPort !== void 0) return normalizeServerPort(defaultPort, "options.defaultPort");
|
|
22
|
+
if (envPort !== void 0) return normalizeServerPort(envPort, "SERVER_PORT");
|
|
23
|
+
return 80;
|
|
24
|
+
};
|
|
25
|
+
|
|
1
26
|
// src/argv.ts
|
|
2
27
|
var parsedBundles = [];
|
|
3
28
|
var parsedPort = null;
|
|
@@ -18,7 +43,7 @@ var parseServerArgv = (argv) => {
|
|
|
18
43
|
`[luckystack:argv] port argument must be numeric, got: "${portArg}". Usage: npm run server -- <bundle[,bundle...]> [port]`
|
|
19
44
|
);
|
|
20
45
|
}
|
|
21
|
-
port =
|
|
46
|
+
port = normalizeServerPort(portArg, "port argument");
|
|
22
47
|
}
|
|
23
48
|
return { bundles, port };
|
|
24
49
|
};
|
|
@@ -36,9 +61,11 @@ var getParsedBundles = () => parsedBundles;
|
|
|
36
61
|
var getParsedPort = () => parsedPort;
|
|
37
62
|
|
|
38
63
|
export {
|
|
64
|
+
normalizeServerPort,
|
|
65
|
+
resolveServerPort,
|
|
39
66
|
parseServerArgv,
|
|
40
67
|
applyServerArgv,
|
|
41
68
|
getParsedBundles,
|
|
42
69
|
getParsedPort
|
|
43
70
|
};
|
|
44
|
-
//# sourceMappingURL=chunk-
|
|
71
|
+
//# sourceMappingURL=chunk-QLVQ75QL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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"]}
|
package/dist/index.js
CHANGED
|
@@ -2,12 +2,14 @@ import {
|
|
|
2
2
|
applyServerArgv,
|
|
3
3
|
getParsedBundles,
|
|
4
4
|
getParsedPort,
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
normalizeServerPort,
|
|
6
|
+
parseServerArgv,
|
|
7
|
+
resolveServerPort
|
|
8
|
+
} from "./chunk-QLVQ75QL.js";
|
|
7
9
|
|
|
8
10
|
// src/createServer.ts
|
|
9
11
|
import http from "http";
|
|
10
|
-
import { registerBindAddress, writeBootUuid, getLogger as getLogger13, getProjectConfig as getProjectConfig15, tryCatch as tryCatch10,
|
|
12
|
+
import { registerBindAddress, registerBoundAddress, writeBootUuid, getLogger as getLogger13, getProjectConfig as getProjectConfig15, tryCatch as tryCatch10, tryCatchSync as tryCatchSync2, resolveEnvKey as resolveEnvKey5, dispatchHook as dispatchHook8 } from "@luckystack/core";
|
|
11
13
|
|
|
12
14
|
// src/httpHandler.ts
|
|
13
15
|
import { randomUUID } from "crypto";
|
|
@@ -299,6 +301,15 @@ import {
|
|
|
299
301
|
resolveEnvKey,
|
|
300
302
|
tryCatch
|
|
301
303
|
} from "@luckystack/core";
|
|
304
|
+
|
|
305
|
+
// src/devToolsStatus.ts
|
|
306
|
+
var devToolsInitError = null;
|
|
307
|
+
var markDevToolsInitFailed = (error) => {
|
|
308
|
+
devToolsInitError = error;
|
|
309
|
+
};
|
|
310
|
+
var getDevToolsInitError = () => devToolsInitError;
|
|
311
|
+
|
|
312
|
+
// src/httpRoutes/healthRoutes.ts
|
|
302
313
|
var pingPrisma = async () => {
|
|
303
314
|
const client = prisma;
|
|
304
315
|
const queryRaw = client.$queryRaw;
|
|
@@ -336,7 +347,8 @@ var handleReadyzRoute = async ({ res, routePath }) => {
|
|
|
336
347
|
const [redisError, pong] = await tryCatch(() => redis.ping());
|
|
337
348
|
const redisOk = !redisError && (pong === "PONG" || Boolean(pong));
|
|
338
349
|
const databaseResult = await checkDatabaseReady();
|
|
339
|
-
const
|
|
350
|
+
const devToolsReady = getDevToolsInitError() === null;
|
|
351
|
+
const ready = Boolean(bootUuid) && redisOk && databaseResult !== false && devToolsReady;
|
|
340
352
|
res.statusCode = ready ? 200 : 503;
|
|
341
353
|
res.setHeader("Content-Type", "application/json");
|
|
342
354
|
res.end(JSON.stringify({
|
|
@@ -348,7 +360,8 @@ var handleReadyzRoute = async ({ res, routePath }) => {
|
|
|
348
360
|
bootUuid: Boolean(bootUuid),
|
|
349
361
|
redis: redisOk,
|
|
350
362
|
database: databaseResult,
|
|
351
|
-
prisma: databaseResult !== false
|
|
363
|
+
prisma: databaseResult !== false,
|
|
364
|
+
devTools: devToolsReady
|
|
352
365
|
}
|
|
353
366
|
}));
|
|
354
367
|
return true;
|
|
@@ -457,7 +470,8 @@ import {
|
|
|
457
470
|
dispatchHook as dispatchHook2,
|
|
458
471
|
getLogger,
|
|
459
472
|
getProjectConfig as getProjectConfig5,
|
|
460
|
-
resolveClientIp
|
|
473
|
+
resolveClientIp,
|
|
474
|
+
resolveDevCallbackUrl
|
|
461
475
|
} from "@luckystack/core";
|
|
462
476
|
|
|
463
477
|
// src/httpRoutes/sessionCookie.ts
|
|
@@ -561,7 +575,11 @@ var handleAuthApiRoute = async ({
|
|
|
561
575
|
);
|
|
562
576
|
const authParams = new URLSearchParams({
|
|
563
577
|
client_id: provider.clientID,
|
|
564
|
-
|
|
578
|
+
//? In dev, target the port the server ACTUALLY bound (auto-increment may
|
|
579
|
+
//? have moved it off the frozen `oauthCallbackBase` port). The token
|
|
580
|
+
//? exchange applies the SAME rewrite, so the two redirect_uri values stay
|
|
581
|
+
//? byte-identical as OAuth requires. Prod / non-localhost: unchanged.
|
|
582
|
+
redirect_uri: resolveDevCallbackUrl(provider.callbackURL),
|
|
565
583
|
scope: provider.scope.join(" "),
|
|
566
584
|
response_type: "code",
|
|
567
585
|
prompt: "select_account"
|
|
@@ -963,13 +981,6 @@ var resolveRequesterIp = (req) => {
|
|
|
963
981
|
return rawRemoteAddress || trustProxy && (req.headers["x-forwarded-for"] || req.headers["x-real-ip"]) ? resolveClientIp3({ rawAddress: rawRemoteAddress, headers: req.headers, trustProxy, trustedProxyHopCount }) : void 0;
|
|
964
982
|
};
|
|
965
983
|
|
|
966
|
-
// src/devToolsStatus.ts
|
|
967
|
-
var devToolsInitError = null;
|
|
968
|
-
var markDevToolsInitFailed = (error) => {
|
|
969
|
-
devToolsInitError = error;
|
|
970
|
-
};
|
|
971
|
-
var getDevToolsInitError = () => devToolsInitError;
|
|
972
|
-
|
|
973
984
|
// src/httpRoutes/apiRoute.ts
|
|
974
985
|
var handleApiRoute = async ({
|
|
975
986
|
req,
|
|
@@ -2293,7 +2304,10 @@ var writeDevServerInfo = (ip, port) => {
|
|
|
2293
2304
|
};
|
|
2294
2305
|
var clearDevServerInfo = () => {
|
|
2295
2306
|
try {
|
|
2296
|
-
|
|
2307
|
+
const file = devServerInfoPath();
|
|
2308
|
+
const info = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
2309
|
+
if (info.pid !== process.pid) return;
|
|
2310
|
+
fs.rmSync(file, { force: true });
|
|
2297
2311
|
} catch {
|
|
2298
2312
|
}
|
|
2299
2313
|
};
|
|
@@ -2338,25 +2352,36 @@ var initDevTools = async () => {
|
|
|
2338
2352
|
}
|
|
2339
2353
|
};
|
|
2340
2354
|
var listenLuckyStackServer = (httpServer, ip, port, callback) => new Promise((resolve, reject) => {
|
|
2341
|
-
const startPort =
|
|
2355
|
+
const startPort = normalizeServerPort(port);
|
|
2356
|
+
const environmentKey = resolveEnvKey5();
|
|
2357
|
+
const production = environmentKey === "production";
|
|
2358
|
+
const test = environmentKey === "test";
|
|
2342
2359
|
const autoIncrementEnv = (process.env.SERVER_PORT_AUTO_INCREMENT ?? "").toLowerCase();
|
|
2343
2360
|
let autoIncrement;
|
|
2344
2361
|
if (["0", "false"].includes(autoIncrementEnv)) autoIncrement = false;
|
|
2345
2362
|
else if (["1", "true"].includes(autoIncrementEnv)) autoIncrement = true;
|
|
2346
|
-
else autoIncrement = !
|
|
2363
|
+
else autoIncrement = !production;
|
|
2347
2364
|
const tryListen = (attemptPort) => {
|
|
2348
2365
|
const onError = (err) => {
|
|
2349
2366
|
if (err.code !== "EADDRINUSE") {
|
|
2350
2367
|
reject(err);
|
|
2351
2368
|
return;
|
|
2352
2369
|
}
|
|
2353
|
-
if (autoIncrement) {
|
|
2370
|
+
if (autoIncrement && attemptPort < 65535) {
|
|
2354
2371
|
getLogger13().warn(
|
|
2355
|
-
`Port ${String(attemptPort)} is in use \u2014 trying ${String(attemptPort + 1)} (auto-increment; set SERVER_PORT_AUTO_INCREMENT=0 to disable). A previous/zombie dev server is still holding :${String(attemptPort)}:
|
|
2372
|
+
`Port ${String(attemptPort)} is in use \u2014 trying ${String(attemptPort + 1)} (auto-increment; set SERVER_PORT_AUTO_INCREMENT=0 to disable). A previous/zombie dev server is still holding :${String(attemptPort)}: a manual client pinned there will keep talking to the OLD process, while the Vite proxy follows this restart's advertised port. If this restart was meant to replace it, stop the process on :${String(attemptPort)} first.`
|
|
2356
2373
|
);
|
|
2357
2374
|
tryListen(attemptPort + 1);
|
|
2358
2375
|
return;
|
|
2359
2376
|
}
|
|
2377
|
+
if (autoIncrement) {
|
|
2378
|
+
const rangeError = new RangeError(
|
|
2379
|
+
"Port 65535 is already in use and auto-increment cannot select a valid higher TCP port."
|
|
2380
|
+
);
|
|
2381
|
+
getLogger13().error(rangeError.message);
|
|
2382
|
+
reject(rangeError);
|
|
2383
|
+
return;
|
|
2384
|
+
}
|
|
2360
2385
|
getLogger13().error(
|
|
2361
2386
|
`Port ${String(attemptPort)} is already in use \u2014 the server did NOT start. Another \`npm run server\` is probably still running (stop it), or set SERVER_PORT to a free port, or set SERVER_PORT_AUTO_INCREMENT=1 to auto-pick the next free port.`
|
|
2362
2387
|
);
|
|
@@ -2365,13 +2390,28 @@ var listenLuckyStackServer = (httpServer, ip, port, callback) => new Promise((re
|
|
|
2365
2390
|
httpServer.once("error", onError);
|
|
2366
2391
|
httpServer.listen(attemptPort, ip, () => {
|
|
2367
2392
|
httpServer.off("error", onError);
|
|
2368
|
-
|
|
2369
|
-
|
|
2393
|
+
const address = httpServer.address();
|
|
2394
|
+
const boundPort = typeof address === "object" && address !== null ? address.port : attemptPort;
|
|
2395
|
+
registerBoundAddress({ ip, port: boundPort });
|
|
2396
|
+
if (!production && !test) {
|
|
2397
|
+
writeDevServerInfo(ip, boundPort);
|
|
2370
2398
|
process.once("exit", clearDevServerInfo);
|
|
2371
2399
|
}
|
|
2400
|
+
if (attemptPort !== startPort && !production) {
|
|
2401
|
+
const configuredCallbackBase = tryCatchSync2(() => getProjectConfig15().oauthCallbackBase)[1] ?? "";
|
|
2402
|
+
const callbackPort = tryCatchSync2(() => {
|
|
2403
|
+
const callbackUrl = new URL(configuredCallbackBase);
|
|
2404
|
+
return callbackUrl.port || (callbackUrl.protocol === "https:" ? "443" : "80");
|
|
2405
|
+
})[1] ?? "";
|
|
2406
|
+
if (callbackPort === String(startPort) && callbackPort !== String(boundPort)) {
|
|
2407
|
+
getLogger13().warn(
|
|
2408
|
+
`OAuth port drift: the server bound :${String(boundPort)} but your OAuth callback base is configured for :${callbackPort} (${configuredCallbackBase}). The framework now auto-targets :${String(boundPort)} for OAuth so the callback reaches THIS server \u2014 but your provider (Google/GitHub/\u2026) still exact-matches its registered redirect URI, so add :${String(boundPort)} to the authorized redirect URIs, OR set SERVER_PORT_AUTO_INCREMENT=0 to pin :${callbackPort} (stop whatever holds it) instead of hopping.`
|
|
2409
|
+
);
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2372
2412
|
const config = getProjectConfig15();
|
|
2373
2413
|
if (config.logging.socketStartup || config.logging.devLogs) {
|
|
2374
|
-
getLogger13().info(`Server is running on http://${ip}:${String(
|
|
2414
|
+
getLogger13().info(`Server is running on http://${ip}:${String(boundPort)}/`);
|
|
2375
2415
|
}
|
|
2376
2416
|
callback?.();
|
|
2377
2417
|
resolve(httpServer);
|
|
@@ -2391,13 +2431,15 @@ var createLuckyStackServer = async (options = {}) => {
|
|
|
2391
2431
|
requireServicesConfig: options.requireServicesConfig,
|
|
2392
2432
|
requireOAuthProviders: options.requireOAuthProviders
|
|
2393
2433
|
});
|
|
2394
|
-
const port =
|
|
2434
|
+
const port = resolveServerPort({
|
|
2435
|
+
optionsPort: options.port,
|
|
2436
|
+
parsedPort: getParsedPort(),
|
|
2437
|
+
defaultPort: options.defaultPort,
|
|
2438
|
+
envPort: process.env.SERVER_PORT
|
|
2439
|
+
});
|
|
2395
2440
|
const ip = options.ip ?? process.env.SERVER_IP ?? "127.0.0.1";
|
|
2396
2441
|
const enableDevTools = options.enableDevTools ?? resolveEnvKey5() !== "production";
|
|
2397
|
-
registerBindAddress({
|
|
2398
|
-
ip,
|
|
2399
|
-
port: typeof port === "string" ? Number.parseInt(port, 10) : port
|
|
2400
|
-
});
|
|
2442
|
+
registerBindAddress({ ip, port });
|
|
2401
2443
|
if (enableDevTools) {
|
|
2402
2444
|
await initDevTools();
|
|
2403
2445
|
}
|