mslxdff 0.1.2 → 0.1.3

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/src/peers.js ADDED
@@ -0,0 +1,83 @@
1
+ import { loadPeers, savePeers, loadPeerErrors, savePeerErrors } from "./state.js";
2
+
3
+ export const DEFAULT_PEER_COOLDOWN_MS = 30_000;
4
+ export const DEFAULT_MAX_HOPS = 3;
5
+
6
+ export function normalizePeerUrl(url) {
7
+ return String(url || "").trim().replace(/\/+$/, "");
8
+ }
9
+
10
+ export function createPeersService({
11
+ file,
12
+ now = () => Date.now(),
13
+ cooldownMs = DEFAULT_PEER_COOLDOWN_MS,
14
+ peers: seedPeers,
15
+ errors: seedErrors,
16
+ persistPeers = (list, f = file) => savePeers(list, f ? { file: f } : {}),
17
+ persistErrors = (errors, f = file) => savePeerErrors(errors, f ? { file: f } : {}),
18
+ } = {}) {
19
+ const list = (seedPeers ?? loadPeers(file ? { file } : {}))
20
+ .map((p) => ({ ...p, url: normalizePeerUrl(p.url) }))
21
+ .filter((p) => p && p.url);
22
+ const lastErrorAt = { ...(seedErrors ?? loadPeerErrors(file ? { file } : {})) };
23
+
24
+ function all() {
25
+ return [...list];
26
+ }
27
+
28
+ function add(peer) {
29
+ const url = normalizePeerUrl(peer?.url);
30
+ if (!url) return false;
31
+ const existing = list.find((p) => p.url === url);
32
+ const entry = { ...peer, url };
33
+ if (existing) Object.assign(existing, entry);
34
+ else list.push(entry);
35
+ persistPeers([...list]);
36
+ return true;
37
+ }
38
+
39
+ function remove(url) {
40
+ const target = normalizePeerUrl(url);
41
+ const idx = list.findIndex((p) => p.url === target);
42
+ if (idx < 0) return false;
43
+ list.splice(idx, 1);
44
+ persistPeers([...list]);
45
+ return true;
46
+ }
47
+
48
+ function removeByGroup(group) {
49
+ const before = list.length;
50
+ for (let i = list.length - 1; i >= 0; i--) {
51
+ if (list[i].group === group) list.splice(i, 1);
52
+ }
53
+ if (list.length !== before) persistPeers([...list]);
54
+ return before - list.length;
55
+ }
56
+
57
+ function isCooling(url) {
58
+ if (!cooldownMs) return false;
59
+ const err = lastErrorAt[url];
60
+ return typeof err === "number" && now() - err < cooldownMs;
61
+ }
62
+
63
+ function available() {
64
+ return list.filter((p) => !isCooling(p.url));
65
+ }
66
+
67
+ let cursor = 0;
68
+
69
+ function next() {
70
+ const avail = available();
71
+ if (!avail.length) return null;
72
+ cursor = cursor % avail.length;
73
+ return avail[cursor++];
74
+ }
75
+
76
+ async function recordError(url) {
77
+ if (!url) return;
78
+ lastErrorAt[url] = now();
79
+ await persistErrors({ ...lastErrorAt });
80
+ }
81
+
82
+ return { all, add, remove, removeByGroup, isCooling, available, next, recordError, errors: () => ({ ...lastErrorAt }) };
83
+ }
package/src/reasoning.js CHANGED
@@ -1,33 +1,33 @@
1
- const PLACEHOLDER = " ";
2
-
3
- const MODEL_RULES = [
4
- { match: (m) => /^kimi-/i.test(m || ""), scope: "toolCalls" },
5
- { match: (m) => /deepseek/i.test(m || ""), scope: "all" },
6
- ];
7
-
8
- export function normalizeModel(model) {
9
- return model.startsWith("oc/") ? model.slice(3) : model;
10
- }
11
-
12
- function shouldInject(message, scope) {
13
- if (message?.role !== "assistant") return false;
14
- const rc = message.reasoning_content;
15
- if (typeof rc === "string" && rc.length > 0) return false;
16
- if (scope === "toolCalls") {
17
- return Array.isArray(message.tool_calls) && message.tool_calls.length > 0;
18
- }
19
- return true;
20
- }
21
-
22
- function applyRule(body, rule) {
23
- if (!rule || !body?.messages) return body;
24
- const messages = body.messages.map((m) =>
25
- shouldInject(m, rule.scope) ? { ...m, reasoning_content: PLACEHOLDER } : m
26
- );
27
- return { ...body, messages };
28
- }
29
-
30
- export function injectReasoningContent(model, body) {
31
- const rule = MODEL_RULES.find((r) => r.match(model));
32
- return applyRule(body, rule);
1
+ const PLACEHOLDER = " ";
2
+
3
+ const MODEL_RULES = [
4
+ { match: (m) => /^kimi-/i.test(m || ""), scope: "toolCalls" },
5
+ { match: (m) => /deepseek/i.test(m || ""), scope: "all" },
6
+ ];
7
+
8
+ export function normalizeModel(model) {
9
+ return model.startsWith("oc/") ? model.slice(3) : model;
10
+ }
11
+
12
+ function shouldInject(message, scope) {
13
+ if (message?.role !== "assistant") return false;
14
+ const rc = message.reasoning_content;
15
+ if (typeof rc === "string" && rc.length > 0) return false;
16
+ if (scope === "toolCalls") {
17
+ return Array.isArray(message.tool_calls) && message.tool_calls.length > 0;
18
+ }
19
+ return true;
20
+ }
21
+
22
+ function applyRule(body, rule) {
23
+ if (!rule || !body?.messages) return body;
24
+ const messages = body.messages.map((m) =>
25
+ shouldInject(m, rule.scope) ? { ...m, reasoning_content: PLACEHOLDER } : m
26
+ );
27
+ return { ...body, messages };
28
+ }
29
+
30
+ export function injectReasoningContent(model, body) {
31
+ const rule = MODEL_RULES.find((r) => r.match(model));
32
+ return applyRule(body, rule);
33
33
  }
package/src/routes.js CHANGED
@@ -1,125 +1,309 @@
1
- import { timingSafeEqual, createHash } from "node:crypto";
2
- import { injectReasoningContent, normalizeModel } from "./reasoning.js";
3
-
4
- export const errMsg = (err) => String(err?.message || err);
5
-
6
- export function createRouter({ token, upstream, models }) {
7
- return async function router(req, res) {
8
- const method = req.method || "GET";
9
- const path = (req.url || "").split("?")[0];
10
-
11
- const route = ROUTES.find((r) => r.method === method && r.path === path);
12
- if (!route) return notFound(res);
13
-
14
- if (route.requiresAuth && !authorized(req, token)) {
15
- res.statusCode = 401;
16
- res.setHeader("WWW-Authenticate", "Bearer");
17
- return json(res, 401, { error: "Unauthorized" });
18
- }
19
-
20
- await route.handler({ req, res, upstream, models });
21
- };
22
- }
23
-
24
- function authorized(req, token) {
25
- const header = req.headers["authorization"] || "";
26
- const match = /^Bearer (.+)$/.exec(header);
27
- if (!match) return false;
28
- const digests = (s) => createHash("sha256").update(s).digest();
29
- return timingSafeEqual(digests(match[1]), digests(token));
30
- }
31
-
32
- function json(res, status, body) {
33
- res.statusCode = status;
34
- res.setHeader("Content-Type", "application/json");
35
- res.end(JSON.stringify(body));
36
- }
37
-
38
- function notFound(res) {
39
- return json(res, 404, { error: "Not Found" });
40
- }
41
-
42
- function readBody(req) {
43
- return new Promise((resolve, reject) => {
44
- let data = "";
45
- req.on("data", (c) => (data += c));
46
- req.on("end", () => {
47
- try {
48
- resolve(data ? JSON.parse(data) : {});
49
- } catch (err) {
50
- reject(err);
51
- }
52
- });
53
- req.on("error", reject);
54
- });
55
- }
56
-
57
- const ROUTES = [
58
- {
59
- method: "GET",
60
- path: "/health",
61
- handler: ({ res }) => json(res, 200, { status: "ok" }),
62
- },
63
- {
64
- method: "POST",
65
- path: "/v1/chat/completions",
66
- requiresAuth: true,
67
- handler: async ({ req, res, upstream }) => {
68
- let body;
69
- try {
70
- body = await readBody(req);
71
- } catch {
72
- return json(res, 400, { error: "Invalid JSON body" });
73
- }
74
-
75
- const model = normalizeModel(body.model || "");
76
- const forwarded = { ...injectReasoningContent(model, body), model };
77
- let upRes;
78
- try {
79
- upRes = await upstream.chat(forwarded);
80
- } catch (err) {
81
- return json(res, 502, { error: errMsg(err) });
82
- }
83
-
84
- const contentType = upRes.headers.get("content-type") || "";
85
- const isStream = Boolean(body.stream) || contentType.includes("text/event-stream");
86
- res.statusCode = upRes.status;
87
-
88
- if (isStream) {
89
- res.setHeader("Content-Type", "text/event-stream");
90
- res.setHeader("Cache-Control", "no-cache");
91
- res.setHeader("Connection", "keep-alive");
92
- if (upRes.body) {
93
- for await (const chunk of upRes.body) {
94
- res.write(chunk);
95
- }
96
- }
97
- res.end();
98
- return;
99
- }
100
-
101
- const text = await upRes.text();
102
- try {
103
- json(res, upRes.status, JSON.parse(text));
104
- } catch {
105
- res.statusCode = upRes.status;
106
- res.setHeader("Content-Type", contentType || "text/plain");
107
- res.end(text);
108
- }
109
- },
110
- },
111
- {
112
- method: "GET",
113
- path: "/v1/models",
114
- requiresAuth: true,
115
- handler: async ({ res, models }) => {
116
- if (!models) return json(res, 501, { error: "Models service not configured" });
117
- try {
118
- const data = await models.get();
119
- json(res, 200, data);
120
- } catch (err) {
121
- json(res, 502, { error: errMsg(err) });
122
- }
123
- },
124
- },
125
- ];
1
+ import { timingSafeEqual, createHash } from "node:crypto";
2
+ import { injectReasoningContent, normalizeModel } from "./reasoning.js";
3
+ import { isAutoModel } from "./auto.js";
4
+ import { DEFAULT_MAX_HOPS } from "./peers.js";
5
+
6
+ export const errMsg = (err) => String(err?.message || err);
7
+
8
+ export function createRouter({ token, upstream, models, auto, logs, peers, maxHops = DEFAULT_MAX_HOPS, groups, bans }) {
9
+ return async function router(req, res) {
10
+ const method = req.method || "GET";
11
+ const path = (req.url || "").split("?")[0];
12
+
13
+ const route = ROUTES.find((r) => r.method === method && r.path === path);
14
+ if (!route) return notFound(res);
15
+
16
+ if (route.requiresAuth && !authorized(req, token)) {
17
+ res.statusCode = 401;
18
+ res.setHeader("WWW-Authenticate", "Bearer");
19
+ return json(res, 401, { error: "Unauthorized" });
20
+ }
21
+
22
+ await route.handler({ req, res, upstream, models, auto, logs, peers, maxHops, groups, bans, token });
23
+ };
24
+ }
25
+
26
+ function clientIp(req) {
27
+ const fwd = req.headers["x-forwarded-for"];
28
+ const head = typeof fwd === "string" ? fwd.split(",")[0].trim() : "";
29
+ const raw = String(head || req.socket.remoteAddress || "");
30
+ return raw.replace(/^::ffff:/, "").replace(/^::1$/, "127.0.0.1") || null;
31
+ }
32
+
33
+ function authorized(req, token) {
34
+ const header = req.headers["authorization"] || "";
35
+ const match = /^Bearer (.+)$/.exec(header);
36
+ if (!match) return false;
37
+ const digests = (s) => createHash("sha256").update(s).digest();
38
+ return timingSafeEqual(digests(match[1]), digests(token));
39
+ }
40
+
41
+ function json(res, status, body) {
42
+ res.statusCode = status;
43
+ res.setHeader("Content-Type", "application/json");
44
+ res.end(JSON.stringify(body));
45
+ }
46
+
47
+ function notFound(res) {
48
+ return json(res, 404, { error: "Not Found" });
49
+ }
50
+
51
+ function readBody(req) {
52
+ return new Promise((resolve, reject) => {
53
+ let data = "";
54
+ req.on("data", (c) => (data += c));
55
+ req.on("end", () => {
56
+ try {
57
+ resolve(data ? JSON.parse(data) : {});
58
+ } catch (err) {
59
+ reject(err);
60
+ }
61
+ });
62
+ req.on("error", reject);
63
+ });
64
+ }
65
+
66
+ async function relay(res, upRes, body) {
67
+ const contentType = upRes.headers.get("content-type") || "";
68
+ const isStream = Boolean(body?.stream) || contentType.includes("text/event-stream");
69
+ res.statusCode = upRes.status;
70
+
71
+ if (isStream) {
72
+ res.setHeader("Content-Type", "text/event-stream");
73
+ res.setHeader("Cache-Control", "no-cache");
74
+ res.setHeader("Connection", "keep-alive");
75
+ if (upRes.body) {
76
+ for await (const chunk of upRes.body) {
77
+ res.write(chunk);
78
+ }
79
+ }
80
+ res.end();
81
+ return;
82
+ }
83
+
84
+ const text = await upRes.text();
85
+ try {
86
+ json(res, upRes.status, JSON.parse(text));
87
+ } catch {
88
+ res.statusCode = upRes.status;
89
+ res.setHeader("Content-Type", contentType || "text/plain");
90
+ res.end(text);
91
+ }
92
+ }
93
+
94
+ const PEER_TIMEOUT_MS = 30_000;
95
+
96
+ function parseHops(header) {
97
+ const n = Number(header);
98
+ return Number.isInteger(n) && n >= 0 ? n : 0;
99
+ }
100
+
101
+ async function forwardToPeer(peer, body, model, hops) {
102
+ const controller = new AbortController();
103
+ const timer = setTimeout(() => controller.abort(), PEER_TIMEOUT_MS);
104
+ try {
105
+ return await fetch(`${peer.url}/v1/chat/completions`, {
106
+ method: "POST",
107
+ headers: {
108
+ "Content-Type": "application/json",
109
+ "Authorization": `Bearer ${peer.token}`,
110
+ "x-mslxdff-hops": String(hops + 1),
111
+ "x-mslxdff-model-lock": model,
112
+ "Accept": "text/event-stream",
113
+ },
114
+ body: JSON.stringify({ ...body, model }),
115
+ signal: controller.signal,
116
+ });
117
+ } catch (err) {
118
+ return err;
119
+ } finally {
120
+ clearTimeout(timer);
121
+ }
122
+ }
123
+
124
+ const ROUTES = [
125
+ {
126
+ method: "GET",
127
+ path: "/health",
128
+ handler: ({ res }) => json(res, 200, { status: "ok" }),
129
+ },
130
+ {
131
+ method: "POST",
132
+ path: "/v1/chat/completions",
133
+ requiresAuth: true,
134
+ handler: async ({ req, res, upstream, auto, logs, peers, maxHops }) => {
135
+ let body;
136
+ try {
137
+ body = await readBody(req);
138
+ } catch {
139
+ return json(res, 400, { error: "Invalid JSON body" });
140
+ }
141
+
142
+ const startedAt = Date.now();
143
+ const hops = parseHops(req.headers["x-mslxdff-hops"]);
144
+ const lockModel = req.headers["x-mslxdff-model-lock"] || "";
145
+ const requested = normalizeModel(lockModel || body.model || "");
146
+ const useAuto = isAutoModel(requested);
147
+
148
+ let order;
149
+ if (lockModel) {
150
+ order = [requested];
151
+ } else if (useAuto) {
152
+ order = auto ? await auto.candidates() : [""];
153
+ } else {
154
+ order = auto ? await auto.candidatesFor(requested) : [requested];
155
+ }
156
+ if (!order.length) order = [""];
157
+ const canFallback = order.length > 1;
158
+ const canForwardPeers = Boolean(peers) && hops < maxHops;
159
+
160
+ const logCall = (model, status) =>
161
+ logs?.appendCall({ model, auto: useAuto, status, durationMs: Date.now() - startedAt, stream: Boolean(body.stream) });
162
+ const logError = (model, status, message) =>
163
+ logs?.appendError({ model, auto: useAuto, status, message });
164
+
165
+ let lastErr = null;
166
+ for (const model of order) {
167
+ let upRes = null;
168
+ const forwarded = { ...injectReasoningContent(model, body), model };
169
+ try {
170
+ upRes = await upstream.chat(forwarded);
171
+ } catch (err) {
172
+ if (auto) await auto.recordError(model);
173
+ lastErr = { model, upstream: null, status: 502, message: errMsg(err) };
174
+ logError(model, 502, errMsg(err));
175
+ }
176
+ if (upRes && upRes.status >= 400) {
177
+ if (auto) await auto.recordError(model);
178
+ lastErr = { model, upstream: upRes, status: upRes.status, message: null };
179
+ logError(model, upRes.status, `upstream ${upRes.status}`);
180
+ upRes = null;
181
+ }
182
+ if (upRes) {
183
+ logCall(model, upRes.status);
184
+ return relay(res, upRes, body);
185
+ }
186
+
187
+ // local failed for this model: try the same model on peers, round-robin over available ones
188
+ if (canForwardPeers) {
189
+ const count = peers.available().length;
190
+ for (let i = 0; i < count; i++) {
191
+ const peer = peers.next();
192
+ if (!peer) break;
193
+ let peerRes = await forwardToPeer(peer, body, model, hops);
194
+ if (peerRes instanceof Error || peerRes.status >= 400) {
195
+ await peers.recordError(peer.url);
196
+ logError(model, peerRes instanceof Error ? 502 : peerRes.status,
197
+ peerRes instanceof Error ? errMsg(peerRes) : `peer ${peerRes.status}`);
198
+ continue;
199
+ }
200
+ logCall(model, peerRes.status);
201
+ return relay(res, peerRes, body);
202
+ }
203
+ }
204
+
205
+ if (canFallback) continue;
206
+ logCall(lastErr?.model ?? model, lastErr?.status ?? 502);
207
+ if (lastErr?.upstream) return relay(res, lastErr.upstream, body);
208
+ return json(res, 502, { error: lastErr?.message || "all auto models failed" });
209
+ }
210
+
211
+ logCall(lastErr?.model ?? requested, lastErr?.status ?? 502);
212
+ if (lastErr?.upstream) return relay(res, lastErr.upstream, body);
213
+ return json(res, 502, { error: lastErr?.message || "all auto models failed" });
214
+ },
215
+ },
216
+ {
217
+ method: "POST",
218
+ path: "/v1/groups/join",
219
+ requiresAuth: false,
220
+ handler: async ({ req, res, groups, token, bans }) => {
221
+ if (!groups) return json(res, 501, { error: "Groups service not configured" });
222
+ const ip = clientIp(req);
223
+ const banned = bans?.isBanned(ip);
224
+ if (banned) {
225
+ return json(res, 403, { error: `banned until ${new Date(banned.until).toISOString()}` });
226
+ }
227
+ let body;
228
+ try {
229
+ body = await readBody(req);
230
+ } catch {
231
+ return json(res, 400, { error: "Invalid JSON body" });
232
+ }
233
+ if (!body?.name) return json(res, 400, { error: "group name is required" });
234
+
235
+ const fail = (msg) => {
236
+ try {
237
+ if (bans) {
238
+ const b = bans.recordFailure(ip);
239
+ if (b) console.error(`${ip} banned (${bans.threshold} failed joins)`);
240
+ }
241
+ } catch {
242
+ // ban bookkeeping must never break the join endpoint
243
+ }
244
+ return json(res, 403, { error: msg });
245
+ };
246
+
247
+ // Already-registered members re-register (sync) using their bearer token;
248
+ // new members must present the group name (which IS the password).
249
+ if (!body.key) {
250
+ const auth = /^Bearer (.+)$/.exec(req.headers["authorization"] || "");
251
+ const hit = auth && groups.membersForToken(body.name, auth[1]);
252
+ if (!hit) return fail("invalid member token");
253
+ try {
254
+ const refreshed = groups.upsertMember(body.name, {
255
+ memberName: body.memberName,
256
+ url: body.url,
257
+ token: body.token,
258
+ });
259
+ return json(res, 200, { object: "group", name: body.name, members: refreshed });
260
+ } catch (err) {
261
+ return json(res, 400, { error: errMsg(err) });
262
+ }
263
+ }
264
+
265
+ try {
266
+ const youPort = Number(body.myPort);
267
+ const youUrl = Number.isInteger(youPort) && youPort > 0 ? `http://${ip}:${youPort}` : "";
268
+ const memberUrl = String(body.url || youUrl);
269
+ if (!memberUrl) throw new Error("member url is required");
270
+ const members = groups.addMember(body.name, {
271
+ key: body.key,
272
+ memberName: body.memberName,
273
+ url: memberUrl,
274
+ token: body.token,
275
+ });
276
+ // 5 wrong passwords bans the source IP (48h) — see createBansService.
277
+ if (bans) bans.clear(ip);
278
+ // First join seeds the leader's own entry using the addr the joiner saw,
279
+ // so -creategroup needs no address argument.
280
+ if (!members.leader) {
281
+ const leaderUrl = String(body.leaderUrl || "").replace(/\/+$/, "");
282
+ if (leaderUrl) {
283
+ groups.upsertMember(body.name, { memberName: "leader", url: leaderUrl, token });
284
+ Object.assign(members, { leader: { url: leaderUrl, token } });
285
+ }
286
+ }
287
+ // Tell the joiner the url we registered them under (source IP + their port),
288
+ // so they can exclude themselves from their own peer list.
289
+ json(res, 200, { object: "group", name: body.name, members, you: { url: memberUrl } });
290
+ } catch (err) {
291
+ return fail(errMsg(err));
292
+ }
293
+ },
294
+ },
295
+ {
296
+ method: "GET",
297
+ path: "/v1/models",
298
+ requiresAuth: true,
299
+ handler: async ({ res, models }) => {
300
+ if (!models) return json(res, 501, { error: "Models service not configured" });
301
+ try {
302
+ const data = await models.get();
303
+ json(res, 200, data);
304
+ } catch (err) {
305
+ json(res, 502, { error: errMsg(err) });
306
+ }
307
+ },
308
+ },
309
+ ];
package/src/server.js CHANGED
@@ -1,36 +1,37 @@
1
- import { createServer as httpCreateServer } from "node:http";
2
- import { DEFAULT_PORT, getPort } from "./state.js";
3
-
4
- export function startServer({ router }, port = resolvePort()) {
5
- const server = httpCreateServer((req, res) => {
6
- router(req, res).catch((err) => {
7
- res.statusCode = 500;
8
- res.setHeader("Content-Type", "application/json");
9
- res.end(JSON.stringify({ error: String(err?.message || err) }));
10
- });
11
- });
12
-
13
- const ready = () =>
14
- new Promise((resolve, reject) => {
15
- server.on("error", reject);
16
- server.listen(port, resolve);
17
- });
18
-
19
- const close = () =>
20
- new Promise((resolve) => {
21
- server.close(resolve);
22
- });
23
-
24
- process.on("SIGINT", close);
25
- process.on("SIGTERM", close);
26
-
27
- return { server, ready, close };
28
- }
29
-
30
- export function resolvePort() {
31
- const persisted = getPort();
32
- if (persisted) return persisted;
33
- const env = Number(process.env.PORT);
34
- if (Number.isInteger(env) && env > 0) return env;
35
- return DEFAULT_PORT;
36
- }
1
+ import { createServer as httpCreateServer } from "node:http";
2
+ import { DEFAULT_PORT, getPort } from "./state.js";
3
+
4
+ export function startServer({ router }, port = resolvePort()) {
5
+ const server = httpCreateServer((req, res) => {
6
+ router(req, res).catch((err) => {
7
+ res.statusCode = 500;
8
+ res.setHeader("Content-Type", "application/json");
9
+ res.end(JSON.stringify({ error: String(err?.message || err) }));
10
+ });
11
+ });
12
+
13
+ const ready = () =>
14
+ new Promise((resolve, reject) => {
15
+ server.on("error", reject);
16
+ server.listen(port, resolve);
17
+ });
18
+
19
+ const close = () =>
20
+ new Promise((resolve) => {
21
+ server.close(resolve);
22
+ });
23
+
24
+ process.on("SIGINT", close);
25
+ process.on("SIGTERM", close);
26
+
27
+ return { server, ready, close };
28
+ }
29
+
30
+ export function resolvePort() {
31
+ const persisted = getPort();
32
+ if (persisted) return persisted;
33
+ const env = Number(process.env.PORT);
34
+ // 0 = OS-assigned ephemeral port (valid; used by tests/containers)
35
+ if (Number.isInteger(env) && env >= 0) return env;
36
+ return DEFAULT_PORT;
37
+ }