@abstractframework/code 0.4.0 → 0.4.2

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/bin/server.js ADDED
@@ -0,0 +1,715 @@
1
+ import * as http from "node:http";
2
+ import * as https from "node:https";
3
+ import { existsSync, readFileSync, statSync } from "node:fs";
4
+ import { isIP } from "node:net";
5
+ import { dirname, extname, join, resolve, sep } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const BIN_DIR = dirname(fileURLToPath(import.meta.url));
9
+ const DEFAULT_DIST_DIR = resolve(BIN_DIR, "..", "dist");
10
+ const FALLBACK_GATEWAY_URL = "http://127.0.0.1:8080";
11
+ const URL_COOKIE = "abstractcode_gateway_url";
12
+ const SESSION_COOKIE = "abstractcode_gateway_session";
13
+ const CSRF_COOKIE = "abstractcode_gateway_csrf";
14
+ const TRUE_VALUES = new Set(["1", "true", "yes", "y", "on"]);
15
+ const HOP_HEADERS = new Set([
16
+ "connection",
17
+ "keep-alive",
18
+ "proxy-authenticate",
19
+ "proxy-authorization",
20
+ "te",
21
+ "trailer",
22
+ "trailers",
23
+ "transfer-encoding",
24
+ "upgrade",
25
+ ]);
26
+ const MIME_TYPES = {
27
+ ".html": "text/html",
28
+ ".js": "application/javascript",
29
+ ".css": "text/css",
30
+ ".json": "application/json",
31
+ ".png": "image/png",
32
+ ".jpg": "image/jpeg",
33
+ ".jpeg": "image/jpeg",
34
+ ".gif": "image/gif",
35
+ ".svg": "image/svg+xml",
36
+ ".ico": "image/x-icon",
37
+ ".webmanifest": "application/manifest+json",
38
+ };
39
+
40
+ function normalizeGatewayUrl(value) {
41
+ return String(value || "")
42
+ .trim()
43
+ .replace(/\/+$/, "");
44
+ }
45
+
46
+ function serverConfig(options = {}) {
47
+ const env = options.env || process.env;
48
+ const defaultGatewayUrl =
49
+ normalizeGatewayUrl(
50
+ options.defaultGatewayUrl ||
51
+ env.ABSTRACTCODE_GATEWAY_URL ||
52
+ env.ABSTRACTGATEWAY_URL ||
53
+ FALLBACK_GATEWAY_URL,
54
+ ) || FALLBACK_GATEWAY_URL;
55
+ return { env, defaultGatewayUrl };
56
+ }
57
+
58
+ function envBool(env, name) {
59
+ const raw = env[name];
60
+ return typeof raw === "string" && TRUE_VALUES.has(raw.trim().toLowerCase());
61
+ }
62
+
63
+ function requestHostname(req, config) {
64
+ const trusted =
65
+ envBool(config.env, "ABSTRACTCODE_TRUST_PROXY_HEADERS") ||
66
+ envBool(config.env, "ABSTRACTGATEWAY_TRUST_PROXY_HEADERS");
67
+ const value = trusted
68
+ ? req.headers["x-forwarded-host"] || req.headers.host
69
+ : req.headers.host;
70
+ const raw = String(value || "")
71
+ .split(",", 1)[0]
72
+ .trim();
73
+ if (!raw) return "";
74
+ if (raw.startsWith("["))
75
+ return raw.slice(1).split("]", 1)[0].trim().toLowerCase();
76
+ if ((raw.match(/:/g) || []).length === 1)
77
+ return raw.split(":")[0].trim().toLowerCase();
78
+ return raw.toLowerCase();
79
+ }
80
+
81
+ function isLoopbackHostname(hostname) {
82
+ const value = String(hostname || "")
83
+ .trim()
84
+ .toLowerCase();
85
+ if (value === "localhost" || value.endsWith(".localhost") || value === "::1") return true;
86
+ return isIP(value) === 4 && Number(value.split(".", 1)[0]) === 127;
87
+ }
88
+
89
+ function isLoopbackPeer(req) {
90
+ let address = String(req.socket?.remoteAddress || "")
91
+ .trim()
92
+ .toLowerCase();
93
+ if (address.startsWith("::ffff:")) address = address.slice("::ffff:".length);
94
+ return (
95
+ address === "::1" || address === "127.0.0.1" || address.startsWith("127.")
96
+ );
97
+ }
98
+
99
+ function connectionConfigAllowed(req, config) {
100
+ if (envBool(config.env, "ABSTRACTCODE_ALLOW_REMOTE_BROWSER_GATEWAY_CONFIG"))
101
+ return true;
102
+ if (
103
+ envBool(config.env, "ABSTRACTGATEWAY_ALLOW_REMOTE_BROWSER_GATEWAY_CONFIG")
104
+ )
105
+ return true;
106
+ const trusted =
107
+ envBool(config.env, "ABSTRACTCODE_TRUST_PROXY_HEADERS") ||
108
+ envBool(config.env, "ABSTRACTGATEWAY_TRUST_PROXY_HEADERS");
109
+ // A trusted proxy makes its own socket address meaningless. Hosted
110
+ // deployments must explicitly opt in before a browser can choose an SSRF
111
+ // destination; direct loopback development remains convenient.
112
+ return (
113
+ !trusted &&
114
+ isLoopbackPeer(req) &&
115
+ isLoopbackHostname(requestHostname(req, config))
116
+ );
117
+ }
118
+
119
+ function connectionConfigDenial(req, config) {
120
+ const host = requestHostname(req, config) || "unknown host";
121
+ return (
122
+ `Browser-supplied Gateway URL changes are disabled for this non-local Code host (${host}). ` +
123
+ "Use the server-configured Gateway URL, or set ABSTRACTCODE_ALLOW_REMOTE_BROWSER_GATEWAY_CONFIG=1 behind your own access control."
124
+ );
125
+ }
126
+
127
+ function parseCookies(req) {
128
+ const out = {};
129
+ for (const part of String(req.headers.cookie || "").split(";")) {
130
+ const index = part.indexOf("=");
131
+ if (index < 0) continue;
132
+ const key = part.slice(0, index).trim();
133
+ const value = part.slice(index + 1).trim();
134
+ if (!key) continue;
135
+ try {
136
+ out[key] = decodeURIComponent(value);
137
+ } catch {
138
+ out[key] = value;
139
+ }
140
+ }
141
+ return out;
142
+ }
143
+
144
+ function cookieSecure(req, config) {
145
+ if (req.socket?.encrypted) return "; Secure";
146
+ const trusted =
147
+ envBool(config.env, "ABSTRACTCODE_TRUST_PROXY_HEADERS") ||
148
+ envBool(config.env, "ABSTRACTGATEWAY_TRUST_PROXY_HEADERS");
149
+ return trusted && firstHeader(req.headers["x-forwarded-proto"]).toLowerCase() === "https"
150
+ ? "; Secure"
151
+ : "";
152
+ }
153
+
154
+ function setSessionCookies(
155
+ res,
156
+ req,
157
+ gatewayUrl,
158
+ sessionId,
159
+ csrfToken,
160
+ persist,
161
+ config,
162
+ ) {
163
+ const secure = cookieSecure(req, config);
164
+ const maxAge = persist ? "; Max-Age=2592000" : "";
165
+ const privateAttrs = `; Path=/; HttpOnly; SameSite=Lax${secure}${maxAge}`;
166
+ const csrfAttrs = `; Path=/; SameSite=Lax${secure}${maxAge}`;
167
+ res.setHeader("Set-Cookie", [
168
+ `${URL_COOKIE}=${encodeURIComponent(gatewayUrl)}${privateAttrs}`,
169
+ `${SESSION_COOKIE}=${encodeURIComponent(sessionId)}${privateAttrs}`,
170
+ `${CSRF_COOKIE}=${encodeURIComponent(csrfToken)}${csrfAttrs}`,
171
+ ]);
172
+ }
173
+
174
+ function clearSessionCookies(res, req, config) {
175
+ const secure = cookieSecure(req, config);
176
+ res.setHeader("Set-Cookie", [
177
+ `${URL_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0${secure}`,
178
+ `${SESSION_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0${secure}`,
179
+ `${CSRF_COOKIE}=; Path=/; SameSite=Lax; Max-Age=0${secure}`,
180
+ ]);
181
+ }
182
+
183
+ function browserSession(req, config) {
184
+ const cookies = parseCookies(req);
185
+ const cookieUrl = normalizeGatewayUrl(cookies[URL_COOKIE]);
186
+ const allowCookieUrl =
187
+ envBool(config.env, "ABSTRACTCODE_ALLOW_BROWSER_GATEWAY_URL_COOKIE") ||
188
+ envBool(config.env, "ABSTRACTCODE_ALLOW_REMOTE_BROWSER_GATEWAY_CONFIG") ||
189
+ envBool(
190
+ config.env,
191
+ "ABSTRACTGATEWAY_ALLOW_REMOTE_BROWSER_GATEWAY_CONFIG",
192
+ ) ||
193
+ connectionConfigAllowed(req, config);
194
+ return {
195
+ gatewayUrl:
196
+ cookieUrl && (allowCookieUrl || cookieUrl === config.defaultGatewayUrl)
197
+ ? cookieUrl
198
+ : config.defaultGatewayUrl,
199
+ sessionId: String(cookies[SESSION_COOKIE] || "").trim(),
200
+ csrfToken: String(cookies[CSRF_COOKIE] || "").trim(),
201
+ };
202
+ }
203
+
204
+ function resolveBackend(gatewayUrl, config) {
205
+ const url = new URL(
206
+ normalizeGatewayUrl(gatewayUrl) || config.defaultGatewayUrl,
207
+ );
208
+ if (url.protocol !== "http:" && url.protocol !== "https:")
209
+ throw new Error("Gateway URL must use http or https");
210
+ if (!url.port) url.port = url.protocol === "https:" ? "443" : "80";
211
+ return {
212
+ url,
213
+ origin: `${url.protocol}//${url.host}`,
214
+ client: url.protocol === "https:" ? https : http,
215
+ };
216
+ }
217
+
218
+ function mutatingMethod(method) {
219
+ return ["POST", "PUT", "PATCH", "DELETE"].includes(
220
+ String(method || "GET").toUpperCase(),
221
+ );
222
+ }
223
+
224
+ function firstHeader(value) {
225
+ return String(Array.isArray(value) ? value[0] : value || "")
226
+ .split(",", 1)[0]
227
+ .trim();
228
+ }
229
+
230
+ function requestOrigin(req, config) {
231
+ const trusted =
232
+ envBool(config.env, "ABSTRACTCODE_TRUST_PROXY_HEADERS") ||
233
+ envBool(config.env, "ABSTRACTGATEWAY_TRUST_PROXY_HEADERS");
234
+ const host = firstHeader(
235
+ trusted
236
+ ? req.headers["x-forwarded-host"] || req.headers.host
237
+ : req.headers.host,
238
+ );
239
+ const protocol =
240
+ firstHeader(trusted ? req.headers["x-forwarded-proto"] : "") ||
241
+ (req.socket?.encrypted ? "https" : "http");
242
+ if (!host || !["http", "https"].includes(protocol.toLowerCase())) return "";
243
+ try {
244
+ return new URL(`${protocol.toLowerCase()}://${host}`).origin;
245
+ } catch {
246
+ return "";
247
+ }
248
+ }
249
+
250
+ function browserMutationAllowed(req, config) {
251
+ const origin = firstHeader(req.headers.origin);
252
+ if (origin) {
253
+ let normalized;
254
+ try {
255
+ normalized = new URL(origin).origin;
256
+ } catch {
257
+ return false;
258
+ }
259
+ return normalized === requestOrigin(req, config);
260
+ }
261
+ return (
262
+ firstHeader(req.headers["sec-fetch-site"]).toLowerCase() !== "cross-site"
263
+ );
264
+ }
265
+
266
+ function jsonRequest(req) {
267
+ return (
268
+ firstHeader(req.headers["content-type"])
269
+ .split(";", 1)[0]
270
+ .trim()
271
+ .toLowerCase() === "application/json"
272
+ );
273
+ }
274
+
275
+ function sendJson(res, status, payload) {
276
+ if (res.headersSent) {
277
+ res.destroy();
278
+ return;
279
+ }
280
+ res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
281
+ res.end(JSON.stringify(payload));
282
+ }
283
+
284
+ function readRequestJson(req) {
285
+ return new Promise((resolveValue) => {
286
+ const chunks = [];
287
+ req.on("data", (chunk) => chunks.push(chunk));
288
+ req.on("end", () => {
289
+ try {
290
+ const raw = Buffer.concat(chunks).toString("utf8");
291
+ resolveValue(raw ? JSON.parse(raw) : {});
292
+ } catch {
293
+ resolveValue({});
294
+ }
295
+ });
296
+ req.on("error", () => resolveValue({}));
297
+ });
298
+ }
299
+
300
+ function gatewayRequest(gatewayUrl, options, body, config) {
301
+ return new Promise((resolveValue) => {
302
+ let backend;
303
+ try {
304
+ backend = resolveBackend(gatewayUrl, config);
305
+ } catch (error) {
306
+ resolveValue({
307
+ ok: false,
308
+ status: 0,
309
+ payload: {
310
+ detail: `Invalid gateway URL: ${String(error?.message || error)}`,
311
+ },
312
+ });
313
+ return;
314
+ }
315
+ const request = backend.client.request(
316
+ {
317
+ protocol: backend.url.protocol,
318
+ hostname: backend.url.hostname,
319
+ port: backend.url.port,
320
+ timeout: options.timeout || 4000,
321
+ ...options,
322
+ },
323
+ (response) => {
324
+ const chunks = [];
325
+ response.on("data", (chunk) => chunks.push(chunk));
326
+ response.on("end", () => {
327
+ const raw = Buffer.concat(chunks).toString("utf8");
328
+ let payload;
329
+ try {
330
+ payload = raw ? JSON.parse(raw) : {};
331
+ } catch {
332
+ payload = { detail: raw };
333
+ }
334
+ const status = response.statusCode || 0;
335
+ resolveValue({
336
+ ok: status >= 200 && status < 300,
337
+ status,
338
+ payload,
339
+ headers: response.headers,
340
+ origin: backend.origin,
341
+ });
342
+ });
343
+ },
344
+ );
345
+ request.on("timeout", () => {
346
+ request.destroy();
347
+ resolveValue({
348
+ ok: false,
349
+ status: 0,
350
+ payload: { detail: "Gateway request timed out" },
351
+ origin: backend.origin,
352
+ });
353
+ });
354
+ request.on("error", (error) =>
355
+ resolveValue({
356
+ ok: false,
357
+ status: 0,
358
+ payload: { detail: String(error?.message || error) },
359
+ origin: backend.origin,
360
+ }),
361
+ );
362
+ if (body) request.write(body);
363
+ request.end();
364
+ });
365
+ }
366
+
367
+ function cookieValueFromSetCookie(rawHeaders, name) {
368
+ const headers = Array.isArray(rawHeaders)
369
+ ? rawHeaders
370
+ : rawHeaders
371
+ ? [rawHeaders]
372
+ : [];
373
+ for (const header of headers) {
374
+ for (const candidate of String(header || "").split(/,(?=\s*[^;,=]+=)/)) {
375
+ const first = candidate.split(";", 1)[0];
376
+ const index = first.indexOf("=");
377
+ if (index < 0 || first.slice(0, index).trim() !== name) continue;
378
+ const raw = first.slice(index + 1).trim();
379
+ try {
380
+ return decodeURIComponent(raw);
381
+ } catch {
382
+ return raw;
383
+ }
384
+ }
385
+ }
386
+ return "";
387
+ }
388
+
389
+ async function handleConnectionApi(req, res, config) {
390
+ if (req.method === "GET") {
391
+ const session = browserSession(req, config);
392
+ if (!session.sessionId) {
393
+ sendJson(res, 200, {
394
+ ok: false,
395
+ gateway_url: session.gatewayUrl,
396
+ has_session: false,
397
+ gateway: { ok: false, error: "Gateway sign-in required" },
398
+ });
399
+ return;
400
+ }
401
+ const checked = await gatewayRequest(
402
+ session.gatewayUrl,
403
+ {
404
+ method: "GET",
405
+ path: "/api/gateway/me",
406
+ headers: {
407
+ Accept: "application/json",
408
+ "X-AbstractGateway-Session": session.sessionId,
409
+ },
410
+ },
411
+ undefined,
412
+ config,
413
+ );
414
+ sendJson(res, 200, {
415
+ ok: checked.ok,
416
+ gateway_url: session.gatewayUrl,
417
+ has_session: true,
418
+ gateway: checked.payload,
419
+ });
420
+ return;
421
+ }
422
+ if (req.method === "POST") {
423
+ if (!jsonRequest(req)) {
424
+ sendJson(res, 415, {
425
+ detail:
426
+ "Gateway connection requests require Content-Type: application/json",
427
+ });
428
+ return;
429
+ }
430
+ const payload = await readRequestJson(req);
431
+ const gatewayUrl =
432
+ normalizeGatewayUrl(payload.gateway_url || config.defaultGatewayUrl) ||
433
+ config.defaultGatewayUrl;
434
+ if (
435
+ !connectionConfigAllowed(req, config) &&
436
+ gatewayUrl !== config.defaultGatewayUrl
437
+ ) {
438
+ sendJson(res, 403, { detail: connectionConfigDenial(req, config) });
439
+ return;
440
+ }
441
+ const body = Buffer.from(
442
+ JSON.stringify({
443
+ user_id: String(payload.gateway_user_id || "").trim(),
444
+ token: String(payload.gateway_token || "").trim(),
445
+ remember: payload.persist === true,
446
+ }),
447
+ );
448
+ const login = await gatewayRequest(
449
+ gatewayUrl,
450
+ {
451
+ method: "POST",
452
+ path: "/api/gateway/session/login",
453
+ headers: {
454
+ Accept: "application/json",
455
+ "Content-Type": "application/json",
456
+ "Content-Length": String(body.length),
457
+ },
458
+ },
459
+ body,
460
+ config,
461
+ );
462
+ const session =
463
+ login.payload && typeof login.payload.session === "object"
464
+ ? login.payload.session
465
+ : {};
466
+ const setCookie = login.headers?.["set-cookie"];
467
+ const sessionId =
468
+ cookieValueFromSetCookie(setCookie, "abstractgateway_session") ||
469
+ String(session.session_id || "").trim();
470
+ const csrfToken =
471
+ cookieValueFromSetCookie(setCookie, "abstractgateway_csrf") ||
472
+ String(session.csrf_token || "").trim();
473
+ if (!login.ok || !sessionId || !csrfToken) {
474
+ sendJson(res, login.status || 401, {
475
+ ok: false,
476
+ detail: login.payload?.detail || "Gateway browser session failed",
477
+ gateway: login.payload,
478
+ });
479
+ return;
480
+ }
481
+ setSessionCookies(
482
+ res,
483
+ req,
484
+ gatewayUrl,
485
+ sessionId,
486
+ csrfToken,
487
+ payload.persist === true,
488
+ config,
489
+ );
490
+ sendJson(res, 200, {
491
+ ok: true,
492
+ gateway_url: gatewayUrl,
493
+ has_session: true,
494
+ gateway: login.payload,
495
+ });
496
+ return;
497
+ }
498
+ if (req.method === "DELETE") {
499
+ const session = browserSession(req, config);
500
+ if (session.sessionId) {
501
+ const body = Buffer.from("{}");
502
+ await gatewayRequest(
503
+ session.gatewayUrl,
504
+ {
505
+ method: "POST",
506
+ path: "/api/gateway/session/logout",
507
+ timeout: 2000,
508
+ headers: {
509
+ Accept: "application/json",
510
+ "Content-Type": "application/json",
511
+ "Content-Length": String(body.length),
512
+ "X-AbstractGateway-Session": session.sessionId,
513
+ "X-AbstractGateway-CSRF": session.csrfToken,
514
+ },
515
+ },
516
+ body,
517
+ config,
518
+ );
519
+ }
520
+ clearSessionCookies(res, req, config);
521
+ sendJson(res, 200, { ok: true });
522
+ return;
523
+ }
524
+ sendJson(res, 405, { detail: "Method not allowed" });
525
+ }
526
+
527
+ function proxyHeaders(headers) {
528
+ const out = {};
529
+ for (const [key, value] of Object.entries(headers || {})) {
530
+ const normalized = String(key).toLowerCase();
531
+ if (HOP_HEADERS.has(normalized) || normalized === "content-length")
532
+ continue;
533
+ out[key] = value;
534
+ }
535
+ return out;
536
+ }
537
+
538
+ function proxyGatewayRequest(req, res, config) {
539
+ const session = browserSession(req, config);
540
+ if (!session.sessionId) {
541
+ sendJson(res, 401, { detail: "Gateway sign-in required" });
542
+ return;
543
+ }
544
+ if (mutatingMethod(req.method)) {
545
+ const presented = String(req.headers["x-abstractcode-csrf"] || "").trim();
546
+ if (!session.csrfToken || presented !== session.csrfToken) {
547
+ sendJson(res, 403, {
548
+ detail: "Gateway browser session CSRF token missing or invalid",
549
+ reason_code: "csrf_required",
550
+ });
551
+ return;
552
+ }
553
+ }
554
+ let backend;
555
+ try {
556
+ backend = resolveBackend(session.gatewayUrl, config);
557
+ } catch (error) {
558
+ sendJson(res, 500, {
559
+ detail: `Invalid gateway URL: ${String(error?.message || error)}`,
560
+ });
561
+ return;
562
+ }
563
+ const headers = { ...req.headers, host: backend.url.host };
564
+ delete headers.cookie;
565
+ delete headers.authorization;
566
+ delete headers["x-forwarded-for"];
567
+ delete headers["x-forwarded-host"];
568
+ delete headers["x-forwarded-proto"];
569
+ delete headers["x-abstractcode-csrf"];
570
+ headers["x-abstractgateway-session"] = session.sessionId;
571
+ if (mutatingMethod(req.method))
572
+ headers["x-abstractgateway-csrf"] = session.csrfToken;
573
+ const proxyRequest = backend.client.request(
574
+ {
575
+ protocol: backend.url.protocol,
576
+ hostname: backend.url.hostname,
577
+ port: backend.url.port,
578
+ method: req.method,
579
+ path: req.url,
580
+ headers,
581
+ },
582
+ (proxyResponse) => {
583
+ res.writeHead(
584
+ proxyResponse.statusCode || 502,
585
+ proxyHeaders(proxyResponse.headers),
586
+ );
587
+ proxyResponse.pipe(res);
588
+ },
589
+ );
590
+ proxyRequest.on("error", (error) =>
591
+ sendJson(res, 502, {
592
+ detail: `Backend not reachable at ${backend.origin} (${String(error?.message || error)})`,
593
+ }),
594
+ );
595
+ req.pipe(proxyRequest);
596
+ }
597
+
598
+ function requestPathname(req) {
599
+ return new URL(req.url || "/", "http://abstractcode.local").pathname;
600
+ }
601
+
602
+ /** Connect-compatible middleware shared by Vite and the packaged server. */
603
+ export function createGatewayMiddleware(options = {}) {
604
+ const config = serverConfig(options);
605
+ return function codeGatewayMiddleware(req, res, next) {
606
+ let pathname;
607
+ try {
608
+ pathname = requestPathname(req);
609
+ } catch {
610
+ sendJson(res, 400, { detail: "Invalid request URL" });
611
+ return;
612
+ }
613
+ if (pathname === "/api/connection/gateway") {
614
+ if (mutatingMethod(req.method) && !browserMutationAllowed(req, config)) {
615
+ sendJson(res, 403, {
616
+ detail: "Cross-origin browser requests are not allowed",
617
+ reason_code: "origin_required",
618
+ });
619
+ return;
620
+ }
621
+ void handleConnectionApi(req, res, config).catch((error) => {
622
+ sendJson(res, 500, {
623
+ detail: `Gateway connection request failed (${String(error?.message || error)})`,
624
+ });
625
+ });
626
+ return;
627
+ }
628
+ if (pathname === "/api/gateway" || pathname.startsWith("/api/gateway/")) {
629
+ if (mutatingMethod(req.method) && !browserMutationAllowed(req, config)) {
630
+ sendJson(res, 403, {
631
+ detail: "Cross-origin browser requests are not allowed",
632
+ reason_code: "origin_required",
633
+ });
634
+ return;
635
+ }
636
+ proxyGatewayRequest(req, res, config);
637
+ return;
638
+ }
639
+ if (typeof next === "function") next();
640
+ else sendJson(res, 404, { detail: "Not found" });
641
+ };
642
+ }
643
+
644
+ function unsafeStaticPath(req) {
645
+ const rawPath = String(req.url || "/").split("?", 1)[0];
646
+ let decoded;
647
+ try {
648
+ decoded = decodeURIComponent(rawPath);
649
+ } catch {
650
+ return true;
651
+ }
652
+ return decoded.includes("\0") || decoded.split(/[\\/]+/).includes("..");
653
+ }
654
+
655
+ function serveFile(res, filePath) {
656
+ try {
657
+ if (!existsSync(filePath) || !statSync(filePath).isFile()) return false;
658
+ res.writeHead(200, {
659
+ "Content-Type":
660
+ MIME_TYPES[extname(filePath).toLowerCase()] ||
661
+ "application/octet-stream",
662
+ "Cache-Control": "no-cache",
663
+ });
664
+ res.end(readFileSync(filePath));
665
+ return true;
666
+ } catch {
667
+ return false;
668
+ }
669
+ }
670
+
671
+ export function createCodeRequestHandler(options = {}) {
672
+ const distDir = resolve(options.distDir || DEFAULT_DIST_DIR);
673
+ const gatewayMiddleware = createGatewayMiddleware(options);
674
+ return function codeRequestHandler(req, res) {
675
+ gatewayMiddleware(req, res, () => {
676
+ let pathname;
677
+ try {
678
+ pathname = decodeURIComponent(requestPathname(req));
679
+ } catch {
680
+ res.writeHead(400);
681
+ res.end("Bad Request");
682
+ return;
683
+ }
684
+ if (
685
+ pathname === "/api" ||
686
+ pathname.startsWith("/api/") ||
687
+ unsafeStaticPath(req)
688
+ ) {
689
+ if (pathname === "/api" || pathname.startsWith("/api/"))
690
+ sendJson(res, 404, { detail: "Not found" });
691
+ else {
692
+ res.writeHead(400);
693
+ res.end("Bad Request");
694
+ }
695
+ return;
696
+ }
697
+ const filePath = resolve(distDir, pathname.replace(/^\/+/, ""));
698
+ if (filePath !== distDir && !filePath.startsWith(`${distDir}${sep}`)) {
699
+ res.writeHead(400);
700
+ res.end("Bad Request");
701
+ return;
702
+ }
703
+ if (serveFile(res, filePath)) return;
704
+ if (serveFile(res, `${filePath}.html`)) return;
705
+ if (serveFile(res, join(filePath, "index.html"))) return;
706
+ if (serveFile(res, join(distDir, "index.html"))) return;
707
+ res.writeHead(404);
708
+ res.end("Not Found");
709
+ });
710
+ };
711
+ }
712
+
713
+ export function createCodeServer(options = {}) {
714
+ return http.createServer(createCodeRequestHandler(options));
715
+ }