@maintainer-pro/ai-bridge 0.1.11 → 0.1.13
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/package.json +1 -1
- package/src/daemon.mjs +144 -14
- package/src/share-rewrite.mjs +729 -0
package/package.json
CHANGED
package/src/daemon.mjs
CHANGED
|
@@ -18,6 +18,13 @@ import readline from "node:readline";
|
|
|
18
18
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
19
19
|
import { createLogger, ensureProjectDataDir } from "@maintainer-pro/ai-cli";
|
|
20
20
|
import { findIife, startAiServer } from "@maintainer-pro/ai-server";
|
|
21
|
+
import {
|
|
22
|
+
applyShareCacheHeaders,
|
|
23
|
+
lookupShareResponse,
|
|
24
|
+
prepareShareHttpRequest,
|
|
25
|
+
processShareHttpResponse,
|
|
26
|
+
shouldRewriteBody,
|
|
27
|
+
} from "./share-rewrite.mjs";
|
|
21
28
|
|
|
22
29
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
23
30
|
const PACKAGE_VERSION = readPackageVersion();
|
|
@@ -2205,9 +2212,16 @@ const embeddedChat = new Map();
|
|
|
2205
2212
|
/** @type {Map<string, Promise<object | null>>} */
|
|
2206
2213
|
const embeddedChatStarting = new Map();
|
|
2207
2214
|
|
|
2208
|
-
const PROXY_CHUNK_BYTES =
|
|
2215
|
+
const PROXY_CHUNK_BYTES = 256 * 1024;
|
|
2209
2216
|
/** @type {Map<string, import("node:http").ClientRequest>} */
|
|
2210
2217
|
const proxyHttpReqs = new Map();
|
|
2218
|
+
/** Reuse sockets to the local app — Next/Vite fetch many files per page. */
|
|
2219
|
+
const proxyKeepAliveAgent = new http.Agent({
|
|
2220
|
+
keepAlive: true,
|
|
2221
|
+
maxSockets: 64,
|
|
2222
|
+
maxFreeSockets: 16,
|
|
2223
|
+
timeout: 30_000,
|
|
2224
|
+
});
|
|
2211
2225
|
/** @type {Map<string, WebSocket>} */
|
|
2212
2226
|
const proxyLocalSockets = new Map();
|
|
2213
2227
|
/** @type {Record<string, unknown> | null} */
|
|
@@ -2299,6 +2313,50 @@ function replyProxyHttp(id, status, headers, body) {
|
|
|
2299
2313
|
bridgeSend({ type: "proxy.http.chunk", id, stream, data: "", eof: true });
|
|
2300
2314
|
}
|
|
2301
2315
|
|
|
2316
|
+
function shareProxyContext(msg, ws, appId) {
|
|
2317
|
+
const apps = Array.isArray(ws?.hostApps) ? ws.hostApps : [];
|
|
2318
|
+
const app = apps.find((row) => row.id === appId);
|
|
2319
|
+
const slug =
|
|
2320
|
+
(typeof msg.slug === "string" && msg.slug.trim()) ||
|
|
2321
|
+
(isAiServerAppId(appId, ws) ? "ai" : proxySlugForApp(app));
|
|
2322
|
+
const publicBase = String(
|
|
2323
|
+
(typeof msg.publicBase === "string" && msg.publicBase) ||
|
|
2324
|
+
proxyUrlForApp(
|
|
2325
|
+
ws,
|
|
2326
|
+
app || {
|
|
2327
|
+
id: appId,
|
|
2328
|
+
role: slug === "ai" ? "ai-server" : "ui",
|
|
2329
|
+
}
|
|
2330
|
+
) ||
|
|
2331
|
+
""
|
|
2332
|
+
).replace(/\/$/, "");
|
|
2333
|
+
const aiPublicBase = String(
|
|
2334
|
+
(typeof msg.aiPublicBase === "string" && msg.aiPublicBase) ||
|
|
2335
|
+
proxyUrlForApp(ws, { id: "ai-server", role: "ai-server" }) ||
|
|
2336
|
+
""
|
|
2337
|
+
).replace(/\/$/, "");
|
|
2338
|
+
const acceptEncoding =
|
|
2339
|
+
typeof msg.acceptEncoding === "string" ? msg.acceptEncoding : "";
|
|
2340
|
+
return {
|
|
2341
|
+
path: safeProxyPath(msg.path),
|
|
2342
|
+
slug,
|
|
2343
|
+
publicBase,
|
|
2344
|
+
aiPublicBase,
|
|
2345
|
+
acceptEncoding,
|
|
2346
|
+
port: localPortForProxy(ws, appId),
|
|
2347
|
+
};
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
function sendProcessedProxyHttp(id, ctx, status, headers, body) {
|
|
2351
|
+
const processed = processShareHttpResponse({
|
|
2352
|
+
...ctx,
|
|
2353
|
+
status,
|
|
2354
|
+
headers,
|
|
2355
|
+
body,
|
|
2356
|
+
});
|
|
2357
|
+
replyProxyHttp(id, processed.status, processed.headers, processed.body);
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2302
2360
|
function bridgeEmbedConfigJs(ws) {
|
|
2303
2361
|
const store = ws?.store && typeof ws.store === "object" ? ws.store : {};
|
|
2304
2362
|
const aiApp = { id: "ai-server", role: "ai-server" };
|
|
@@ -2324,7 +2382,16 @@ async function handleAiProxyHttpFromAdmin(msg, ws) {
|
|
|
2324
2382
|
const method = String(msg.method || "GET").toUpperCase();
|
|
2325
2383
|
const reqPath = safeProxyPath(msg.path);
|
|
2326
2384
|
const pathname = reqPath.split("?")[0] || "/";
|
|
2327
|
-
const
|
|
2385
|
+
const ctx = shareProxyContext(msg, ws, "ai-server");
|
|
2386
|
+
const prepared = prepareShareHttpRequest(
|
|
2387
|
+
proxyReqHeaders(msg.headers),
|
|
2388
|
+
ctx.publicBase,
|
|
2389
|
+
ctx.acceptEncoding,
|
|
2390
|
+
reqPath
|
|
2391
|
+
);
|
|
2392
|
+
ctx.acceptEncoding = prepared.acceptEncoding;
|
|
2393
|
+
ctx.ifNoneMatch = prepared.ifNoneMatch;
|
|
2394
|
+
const headers = prepared.headers;
|
|
2328
2395
|
const body =
|
|
2329
2396
|
typeof msg.body === "string" && msg.body
|
|
2330
2397
|
? Buffer.from(msg.body, "base64")
|
|
@@ -2333,20 +2400,21 @@ async function handleAiProxyHttpFromAdmin(msg, ws) {
|
|
|
2333
2400
|
if (pathname === "/ai-ui.iife.js") {
|
|
2334
2401
|
const file = findIife();
|
|
2335
2402
|
if (!file || !fs.existsSync(file)) {
|
|
2336
|
-
|
|
2403
|
+
sendProcessedProxyHttp(
|
|
2337
2404
|
id,
|
|
2405
|
+
ctx,
|
|
2338
2406
|
404,
|
|
2339
2407
|
{ "content-type": "text/plain; charset=utf-8" },
|
|
2340
2408
|
"Not found"
|
|
2341
2409
|
);
|
|
2342
2410
|
return;
|
|
2343
2411
|
}
|
|
2344
|
-
|
|
2412
|
+
sendProcessedProxyHttp(
|
|
2345
2413
|
id,
|
|
2414
|
+
ctx,
|
|
2346
2415
|
200,
|
|
2347
2416
|
{
|
|
2348
2417
|
"content-type": "text/javascript; charset=utf-8",
|
|
2349
|
-
"cache-control": "no-store",
|
|
2350
2418
|
},
|
|
2351
2419
|
fs.readFileSync(file)
|
|
2352
2420
|
);
|
|
@@ -2356,8 +2424,9 @@ async function handleAiProxyHttpFromAdmin(msg, ws) {
|
|
|
2356
2424
|
if (pathname === "/embed-config.js") {
|
|
2357
2425
|
const cfg = bridgeCfg || loadConfig();
|
|
2358
2426
|
await loadSandboxStoreEnv(ws, cfg);
|
|
2359
|
-
|
|
2427
|
+
sendProcessedProxyHttp(
|
|
2360
2428
|
id,
|
|
2429
|
+
ctx,
|
|
2361
2430
|
200,
|
|
2362
2431
|
{
|
|
2363
2432
|
"content-type": "text/javascript; charset=utf-8",
|
|
@@ -2380,7 +2449,7 @@ async function handleAiProxyHttpFromAdmin(msg, ws) {
|
|
|
2380
2449
|
headers,
|
|
2381
2450
|
body,
|
|
2382
2451
|
});
|
|
2383
|
-
|
|
2452
|
+
sendProcessedProxyHttp(id, ctx, result.status, result.headers, result.body);
|
|
2384
2453
|
} catch (err) {
|
|
2385
2454
|
bridgeSend({
|
|
2386
2455
|
type: "proxy.http.error",
|
|
@@ -2436,7 +2505,29 @@ function handleProxyHttpFromAdmin(msg) {
|
|
|
2436
2505
|
}
|
|
2437
2506
|
const method = String(msg.method || "GET").toUpperCase();
|
|
2438
2507
|
const path = safeProxyPath(msg.path);
|
|
2439
|
-
const
|
|
2508
|
+
const ctx = shareProxyContext(msg, ws, appId);
|
|
2509
|
+
ctx.port = port;
|
|
2510
|
+
const prepared = prepareShareHttpRequest(
|
|
2511
|
+
proxyReqHeaders(msg.headers),
|
|
2512
|
+
ctx.publicBase,
|
|
2513
|
+
ctx.acceptEncoding,
|
|
2514
|
+
path
|
|
2515
|
+
);
|
|
2516
|
+
ctx.acceptEncoding = prepared.acceptEncoding;
|
|
2517
|
+
ctx.ifNoneMatch = prepared.ifNoneMatch;
|
|
2518
|
+
if (method === "GET" || method === "HEAD") {
|
|
2519
|
+
const cached = lookupShareResponse({
|
|
2520
|
+
publicBase: ctx.publicBase,
|
|
2521
|
+
path,
|
|
2522
|
+
ifNoneMatch: ctx.ifNoneMatch,
|
|
2523
|
+
acceptEncoding: ctx.acceptEncoding,
|
|
2524
|
+
});
|
|
2525
|
+
if (cached) {
|
|
2526
|
+
replyProxyHttp(id, cached.status, cached.headers, cached.body);
|
|
2527
|
+
return;
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
const headers = prepared.headers;
|
|
2440
2531
|
headers.host = `127.0.0.1:${port}`;
|
|
2441
2532
|
// Next.js dev 403s `/_next` when Origin/sec-fetch look cross-site.
|
|
2442
2533
|
// This hop is server-to-server; drop those so chunks always load.
|
|
@@ -2463,7 +2554,7 @@ function handleProxyHttpFromAdmin(msg) {
|
|
|
2463
2554
|
path,
|
|
2464
2555
|
method,
|
|
2465
2556
|
headers,
|
|
2466
|
-
agent:
|
|
2557
|
+
agent: proxyKeepAliveAgent,
|
|
2467
2558
|
},
|
|
2468
2559
|
(res) => {
|
|
2469
2560
|
/** @type {Record<string, string>} */
|
|
@@ -2472,11 +2563,31 @@ function handleProxyHttpFromAdmin(msg) {
|
|
|
2472
2563
|
if (value == null) continue;
|
|
2473
2564
|
outHeaders[key] = Array.isArray(value) ? value.join(", ") : String(value);
|
|
2474
2565
|
}
|
|
2566
|
+
const status = res.statusCode || 502;
|
|
2567
|
+
applyShareCacheHeaders(outHeaders, path);
|
|
2568
|
+
if (shouldRewriteBody(outHeaders)) {
|
|
2569
|
+
/** @type {Buffer[]} */
|
|
2570
|
+
const chunks = [];
|
|
2571
|
+
res.on("data", (chunk) => {
|
|
2572
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
2573
|
+
});
|
|
2574
|
+
res.on("end", () => {
|
|
2575
|
+
proxyHttpReqs.delete(id);
|
|
2576
|
+
sendProcessedProxyHttp(
|
|
2577
|
+
id,
|
|
2578
|
+
ctx,
|
|
2579
|
+
status,
|
|
2580
|
+
outHeaders,
|
|
2581
|
+
Buffer.concat(chunks)
|
|
2582
|
+
);
|
|
2583
|
+
});
|
|
2584
|
+
return;
|
|
2585
|
+
}
|
|
2475
2586
|
bridgeSend({
|
|
2476
2587
|
type: "proxy.http.start",
|
|
2477
2588
|
id,
|
|
2478
2589
|
stream,
|
|
2479
|
-
status
|
|
2590
|
+
status,
|
|
2480
2591
|
headers: outHeaders,
|
|
2481
2592
|
});
|
|
2482
2593
|
res.on("data", (chunk) => {
|
|
@@ -2589,10 +2700,27 @@ function attachProxyLocalWs(id, socket) {
|
|
|
2589
2700
|
});
|
|
2590
2701
|
}
|
|
2591
2702
|
|
|
2592
|
-
function
|
|
2703
|
+
function wsProtocolsFromMsg(msg) {
|
|
2704
|
+
if (Array.isArray(msg?.protocols)) {
|
|
2705
|
+
return msg.protocols.map((p) => String(p || "").trim()).filter(Boolean);
|
|
2706
|
+
}
|
|
2707
|
+
const headers = msg?.headers && typeof msg.headers === "object" ? msg.headers : {};
|
|
2708
|
+
const raw = headers["sec-websocket-protocol"] || headers["Sec-WebSocket-Protocol"] || "";
|
|
2709
|
+
return String(raw)
|
|
2710
|
+
.split(",")
|
|
2711
|
+
.map((part) => part.trim())
|
|
2712
|
+
.filter(Boolean);
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
function openProxyLocalWs(id, port, path, protocols) {
|
|
2593
2716
|
let socket;
|
|
2594
2717
|
try {
|
|
2595
|
-
|
|
2718
|
+
const url = `ws://127.0.0.1:${port}${path}`;
|
|
2719
|
+
const proto = Array.isArray(protocols)
|
|
2720
|
+
? protocols.map((p) => String(p || "").trim()).filter(Boolean)
|
|
2721
|
+
: [];
|
|
2722
|
+
// Vite HMR only accepts upgrades with subprotocol `vite-hmr` / `vite-ping`.
|
|
2723
|
+
socket = proto.length ? new WebSocket(url, proto) : new WebSocket(url);
|
|
2596
2724
|
} catch (err) {
|
|
2597
2725
|
bridgeSend({
|
|
2598
2726
|
type: "proxy.ws.error",
|
|
@@ -2615,6 +2743,7 @@ function handleProxyWsOpenFromAdmin(msg) {
|
|
|
2615
2743
|
return;
|
|
2616
2744
|
}
|
|
2617
2745
|
const path = safeProxyPath(msg.path);
|
|
2746
|
+
const protocols = wsProtocolsFromMsg(msg);
|
|
2618
2747
|
if (isAiServerAppId(appId, ws)) {
|
|
2619
2748
|
void (async () => {
|
|
2620
2749
|
const embedded = await ensureEmbeddedChat(ws);
|
|
@@ -2623,7 +2752,7 @@ function handleProxyWsOpenFromAdmin(msg) {
|
|
|
2623
2752
|
bridgeSend({ type: "proxy.ws.error", id, error: "AI chat is not running" });
|
|
2624
2753
|
return;
|
|
2625
2754
|
}
|
|
2626
|
-
openProxyLocalWs(id, port, path);
|
|
2755
|
+
openProxyLocalWs(id, port, path, protocols);
|
|
2627
2756
|
})();
|
|
2628
2757
|
return;
|
|
2629
2758
|
}
|
|
@@ -2632,7 +2761,7 @@ function handleProxyWsOpenFromAdmin(msg) {
|
|
|
2632
2761
|
bridgeSend({ type: "proxy.ws.error", id, error: "No local port mapped" });
|
|
2633
2762
|
return;
|
|
2634
2763
|
}
|
|
2635
|
-
openProxyLocalWs(id, port, path);
|
|
2764
|
+
openProxyLocalWs(id, port, path, protocols);
|
|
2636
2765
|
}
|
|
2637
2766
|
|
|
2638
2767
|
function handleProxyWsFrameFromAdmin(msg) {
|
|
@@ -5240,6 +5369,7 @@ async function main() {
|
|
|
5240
5369
|
senderType: msg.senderType === "client" ? "client" : undefined,
|
|
5241
5370
|
senderName:
|
|
5242
5371
|
typeof msg.senderName === "string" ? msg.senderName : undefined,
|
|
5372
|
+
context: msg.context && typeof msg.context === "object" ? msg.context : undefined,
|
|
5243
5373
|
};
|
|
5244
5374
|
const embedded = embeddedChat.get(sandboxId);
|
|
5245
5375
|
if (embedded?.runChat) {
|
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Share-URL processing lives on the bridge: path rewrite, widget injection,
|
|
3
|
+
* cache-control, and gzip. Maintainer Pro only forwards the result.
|
|
4
|
+
*/
|
|
5
|
+
import { gzipSync } from "node:zlib";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
|
|
8
|
+
const REWRITE_CACHE_MS = 10 * 60 * 1000;
|
|
9
|
+
const REWRITE_CACHE_MAX = 128;
|
|
10
|
+
|
|
11
|
+
/** @type {Map<string, { at: number, payload: Buffer, status: number, headers: Record<string, string>, etag: string }>} */
|
|
12
|
+
const rewriteCache = new Map();
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {Record<string, string>} headers
|
|
16
|
+
* @returns {Record<string, string>}
|
|
17
|
+
*/
|
|
18
|
+
function lowerHeaders(headers) {
|
|
19
|
+
/** @type {Record<string, string>} */
|
|
20
|
+
const out = {};
|
|
21
|
+
for (const [key, value] of Object.entries(headers || {})) {
|
|
22
|
+
if (value == null) continue;
|
|
23
|
+
out[String(key).toLowerCase()] = String(value);
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function headerGet(headers, name) {
|
|
29
|
+
const lower = String(name).toLowerCase();
|
|
30
|
+
for (const [key, value] of Object.entries(headers || {})) {
|
|
31
|
+
if (String(key).toLowerCase() === lower) return String(value);
|
|
32
|
+
}
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function stripProxyPrefix(urlOrPath, pathPrefix) {
|
|
37
|
+
if (!pathPrefix || !urlOrPath) return urlOrPath;
|
|
38
|
+
try {
|
|
39
|
+
const u = new URL(urlOrPath, "http://mp.invalid");
|
|
40
|
+
let path = u.pathname;
|
|
41
|
+
if (path === pathPrefix) path = "/";
|
|
42
|
+
else if (path.startsWith(`${pathPrefix}/`)) {
|
|
43
|
+
path = path.slice(pathPrefix.length) || "/";
|
|
44
|
+
}
|
|
45
|
+
return `${path}${u.search}${u.hash}`;
|
|
46
|
+
} catch {
|
|
47
|
+
if (urlOrPath === pathPrefix) return "/";
|
|
48
|
+
if (urlOrPath.startsWith(`${pathPrefix}/`)) {
|
|
49
|
+
return urlOrPath.slice(pathPrefix.length) || "/";
|
|
50
|
+
}
|
|
51
|
+
return urlOrPath;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function proxyPathPrefix(publicBase) {
|
|
56
|
+
const trimmed = String(publicBase || "").replace(/\/$/, "");
|
|
57
|
+
if (!trimmed) return "";
|
|
58
|
+
try {
|
|
59
|
+
return new URL(trimmed).pathname.replace(/\/$/, "") || "";
|
|
60
|
+
} catch {
|
|
61
|
+
const start = trimmed.indexOf("://");
|
|
62
|
+
if (start < 0) return trimmed;
|
|
63
|
+
const pathStart = trimmed.indexOf("/", start + 3);
|
|
64
|
+
return pathStart >= 0 ? trimmed.slice(pathStart).replace(/\/$/, "") : "";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Force identity to the local app so HTML/JS can be rewritten, and strip the
|
|
70
|
+
* share prefix from Next's `next-url` header.
|
|
71
|
+
*
|
|
72
|
+
* @param {Record<string, string>} headers
|
|
73
|
+
* @param {string} [publicBase]
|
|
74
|
+
* @param {string} [acceptEncodingHint]
|
|
75
|
+
* @param {string} [path]
|
|
76
|
+
* @returns {{ headers: Record<string, string>, acceptEncoding: string, ifNoneMatch: string }}
|
|
77
|
+
*/
|
|
78
|
+
export function prepareShareHttpRequest(
|
|
79
|
+
headers,
|
|
80
|
+
publicBase,
|
|
81
|
+
acceptEncodingHint,
|
|
82
|
+
path
|
|
83
|
+
) {
|
|
84
|
+
const out = { ...headers };
|
|
85
|
+
const acceptEncoding = String(
|
|
86
|
+
acceptEncodingHint || headerGet(out, "accept-encoding") || ""
|
|
87
|
+
);
|
|
88
|
+
const ifNoneMatch = headerGet(out, "if-none-match");
|
|
89
|
+
const pathPrefix = proxyPathPrefix(publicBase || "");
|
|
90
|
+
if (pathPrefix) {
|
|
91
|
+
for (const key of Object.keys(out)) {
|
|
92
|
+
if (key.toLowerCase() === "next-url") {
|
|
93
|
+
out[key] = stripProxyPrefix(out[key], pathPrefix);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
for (const key of Object.keys(out)) {
|
|
98
|
+
if (key.toLowerCase() === "accept-encoding") delete out[key];
|
|
99
|
+
}
|
|
100
|
+
out["accept-encoding"] = "identity";
|
|
101
|
+
// Rewritten bodies get our ETag. Do not let Next 304 on its own validator
|
|
102
|
+
// or we would have no HTML/JS to rewrite.
|
|
103
|
+
if (willRewritePath(path)) {
|
|
104
|
+
for (const key of Object.keys(out)) {
|
|
105
|
+
const lower = key.toLowerCase();
|
|
106
|
+
if (lower === "if-none-match" || lower === "if-modified-since") {
|
|
107
|
+
delete out[key];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (publicBase && !headerGet(out, "x-forwarded-prefix")) {
|
|
112
|
+
try {
|
|
113
|
+
const prefix = new URL(publicBase).pathname.replace(/\/$/, "") || "/";
|
|
114
|
+
out["x-forwarded-prefix"] = prefix;
|
|
115
|
+
} catch {
|
|
116
|
+
/* ignore */
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { headers: out, acceptEncoding, ifNoneMatch };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function shouldRewriteBody(headers) {
|
|
123
|
+
const enc = headerGet(headers, "content-encoding").toLowerCase();
|
|
124
|
+
if (enc && enc !== "identity") return false;
|
|
125
|
+
const ct = headerGet(headers, "content-type").toLowerCase();
|
|
126
|
+
return /html|javascript|ecmascript|css|json|svg|xml|text\/plain|text\/x-component|text\/x-ref|\brsc\b/.test(
|
|
127
|
+
ct
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function isImmutableAssetPath(path) {
|
|
132
|
+
const p = String(path || "").split("?")[0] || "";
|
|
133
|
+
return (
|
|
134
|
+
/\/_next\/static\//.test(p) ||
|
|
135
|
+
/\/__nextjs_font\//.test(p) ||
|
|
136
|
+
/\/node_modules\/\.vite\//.test(p) ||
|
|
137
|
+
/\.[0-9a-f]{8,}\.(?:js|css|woff2?)$/i.test(p)
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function isNoStorePath(path) {
|
|
142
|
+
const p = String(path || "").split("?")[0] || "";
|
|
143
|
+
return (
|
|
144
|
+
/\/embed-config\.js$/i.test(p) ||
|
|
145
|
+
p === "/api" ||
|
|
146
|
+
p.startsWith("/api/") ||
|
|
147
|
+
/\/_next\/webpack-hmr/i.test(p) ||
|
|
148
|
+
/\/_next\/webpack\/hmr/i.test(p) ||
|
|
149
|
+
/\/__nextjs_original-stack-frames/i.test(p) ||
|
|
150
|
+
/\/@vite(?:\/|$)/.test(p) ||
|
|
151
|
+
/\/@react-refresh/.test(p) ||
|
|
152
|
+
/\/@fs\//.test(p) ||
|
|
153
|
+
/\/@id\//.test(p)
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function isCacheableMediaPath(path) {
|
|
158
|
+
const p = String(path || "").split("?")[0] || "";
|
|
159
|
+
return /\.(?:png|jpe?g|gif|webp|avif|ico|woff2?|ttf|otf|mp4|webm|mp3|wav|wasm)$/i.test(
|
|
160
|
+
p
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function isLongCacheScriptPath(path) {
|
|
165
|
+
const p = String(path || "").split("?")[0] || "";
|
|
166
|
+
return /\/ai-ui\.iife\.js$/i.test(p);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function willRewritePath(path) {
|
|
170
|
+
const p = String(path || "").split("?")[0]?.toLowerCase() || "";
|
|
171
|
+
if (!p || p === "/") return true;
|
|
172
|
+
if (isCacheableMediaPath(p)) return false;
|
|
173
|
+
if (/\.(?:woff2?|ttf|otf|png|jpe?g|gif|webp|avif|ico|mp4|webm|wasm)$/i.test(p)) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function etagFor(payload) {
|
|
180
|
+
const hash = createHash("sha1").update(payload).digest("base64url").slice(0, 27);
|
|
181
|
+
return `"${hash}"`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function etagMatches(ifNoneMatch, etag) {
|
|
185
|
+
if (!ifNoneMatch || !etag) return false;
|
|
186
|
+
const want = String(etag).trim();
|
|
187
|
+
return String(ifNoneMatch)
|
|
188
|
+
.split(",")
|
|
189
|
+
.some((part) => {
|
|
190
|
+
const token = part.trim();
|
|
191
|
+
return token === "*" || token === want || token === `W/${want}`;
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function dropHopCacheNoise(headers) {
|
|
196
|
+
delete headers.pragma;
|
|
197
|
+
delete headers.expires;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Cache-Control for the share URL. Hashed Next/Vite assets can sit in the
|
|
202
|
+
* browser for a year; HTML/RSC revalidate with ETag; secrets and HMR do not
|
|
203
|
+
* cache.
|
|
204
|
+
*
|
|
205
|
+
* @param {Record<string, string>} headers
|
|
206
|
+
* @param {string} path
|
|
207
|
+
* @returns {"no-store" | "immutable" | "store" | "revalidate"}
|
|
208
|
+
*/
|
|
209
|
+
export function applyShareCacheHeaders(headers, path) {
|
|
210
|
+
dropHopCacheNoise(headers);
|
|
211
|
+
if (isNoStorePath(path)) {
|
|
212
|
+
headers["cache-control"] = "private, no-store";
|
|
213
|
+
return "no-store";
|
|
214
|
+
}
|
|
215
|
+
if (isImmutableAssetPath(path)) {
|
|
216
|
+
delete headers["set-cookie"];
|
|
217
|
+
headers["cache-control"] = "public, max-age=31536000, immutable";
|
|
218
|
+
return "immutable";
|
|
219
|
+
}
|
|
220
|
+
if (isCacheableMediaPath(path) || isLongCacheScriptPath(path)) {
|
|
221
|
+
delete headers["set-cookie"];
|
|
222
|
+
headers["cache-control"] = "public, max-age=86400";
|
|
223
|
+
return "store";
|
|
224
|
+
}
|
|
225
|
+
headers["cache-control"] = "no-cache";
|
|
226
|
+
return "revalidate";
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function notModifiedResult(headers, etag) {
|
|
230
|
+
const out = {
|
|
231
|
+
"cache-control": headers["cache-control"] || "no-cache",
|
|
232
|
+
etag,
|
|
233
|
+
};
|
|
234
|
+
if (headers.vary) out.vary = headers.vary;
|
|
235
|
+
return { status: 304, headers: out, body: Buffer.alloc(0) };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function rewriteLocation(value, publicBase, port) {
|
|
239
|
+
if (!publicBase) return value;
|
|
240
|
+
try {
|
|
241
|
+
const u = new URL(value, `http://127.0.0.1:${Number(port) || 0}`);
|
|
242
|
+
if (u.hostname === "127.0.0.1" || u.hostname === "localhost") {
|
|
243
|
+
return `${String(publicBase).replace(/\/$/, "")}${u.pathname}${u.search}${u.hash}`;
|
|
244
|
+
}
|
|
245
|
+
} catch {
|
|
246
|
+
/* keep */
|
|
247
|
+
}
|
|
248
|
+
return value;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function gzipIfRequested(payload, headers, acceptEncoding) {
|
|
252
|
+
if (payload.length < 1024) return payload;
|
|
253
|
+
if (!/\bgzip\b/i.test(acceptEncoding || "")) return payload;
|
|
254
|
+
const enc = headerGet(headers, "content-encoding");
|
|
255
|
+
if (enc && enc !== "identity") return payload;
|
|
256
|
+
try {
|
|
257
|
+
const gzipped = gzipSync(payload, { level: 6 });
|
|
258
|
+
if (gzipped.length >= payload.length) return payload;
|
|
259
|
+
for (const key of Object.keys(headers)) {
|
|
260
|
+
if (key.toLowerCase() === "content-encoding") delete headers[key];
|
|
261
|
+
if (key.toLowerCase() === "content-length") delete headers[key];
|
|
262
|
+
}
|
|
263
|
+
headers["content-encoding"] = "gzip";
|
|
264
|
+
const vary = headerGet(headers, "vary");
|
|
265
|
+
if (!/\baccept-encoding\b/i.test(vary)) {
|
|
266
|
+
for (const key of Object.keys(headers)) {
|
|
267
|
+
if (key.toLowerCase() === "vary") delete headers[key];
|
|
268
|
+
}
|
|
269
|
+
headers.vary = vary ? `${vary}, Accept-Encoding` : "Accept-Encoding";
|
|
270
|
+
}
|
|
271
|
+
return gzipped;
|
|
272
|
+
} catch {
|
|
273
|
+
return payload;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function rewriteCacheKey(publicBase, path) {
|
|
278
|
+
if (!isImmutableAssetPath(path) && !isLongCacheScriptPath(path)) return null;
|
|
279
|
+
return `${publicBase || ""}\0${String(path || "").split("?")[0]}`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function rememberRewrite(key, payload, status, headers, etag) {
|
|
283
|
+
if (rewriteCache.size >= REWRITE_CACHE_MAX) {
|
|
284
|
+
const first = rewriteCache.keys().next().value;
|
|
285
|
+
if (first) rewriteCache.delete(first);
|
|
286
|
+
}
|
|
287
|
+
rewriteCache.set(key, {
|
|
288
|
+
at: Date.now(),
|
|
289
|
+
payload,
|
|
290
|
+
status,
|
|
291
|
+
headers: { ...headers },
|
|
292
|
+
etag,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Serve a previously rewritten immutable asset without hitting Next.
|
|
298
|
+
* Returns a 304 when the browser already has this ETag.
|
|
299
|
+
*
|
|
300
|
+
* @param {{ publicBase?: string, path?: string, ifNoneMatch?: string, acceptEncoding?: string }} opts
|
|
301
|
+
* @returns {{ status: number, headers: Record<string, string>, body: Buffer } | null}
|
|
302
|
+
*/
|
|
303
|
+
export function lookupShareResponse(opts) {
|
|
304
|
+
const path = String(opts?.path || "/");
|
|
305
|
+
const key = rewriteCacheKey(opts?.publicBase || "", path);
|
|
306
|
+
if (!key) return null;
|
|
307
|
+
const hit = rewriteCache.get(key);
|
|
308
|
+
if (!hit || Date.now() - hit.at >= REWRITE_CACHE_MS) return null;
|
|
309
|
+
return finishCachedShareResponse(hit, path, opts?.ifNoneMatch, opts?.acceptEncoding);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function finishCachedShareResponse(hit, path, ifNoneMatch, acceptEncoding) {
|
|
313
|
+
const outHeaders = { ...hit.headers };
|
|
314
|
+
applyShareCacheHeaders(outHeaders, path);
|
|
315
|
+
const etag = hit.etag || etagFor(hit.payload);
|
|
316
|
+
outHeaders.etag = etag;
|
|
317
|
+
if (etagMatches(ifNoneMatch, etag)) {
|
|
318
|
+
return notModifiedResult(outHeaders, etag);
|
|
319
|
+
}
|
|
320
|
+
const gzipped = gzipIfRequested(hit.payload, outHeaders, acceptEncoding || "");
|
|
321
|
+
outHeaders["content-length"] = String(gzipped.length);
|
|
322
|
+
return { status: hit.status, headers: outHeaders, body: gzipped };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function isNextDocument(body) {
|
|
326
|
+
return /\/_next\//.test(body) || /__NEXT_DATA__|self\.__next_f/.test(body);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function isViteDocument(body) {
|
|
330
|
+
return (
|
|
331
|
+
/\/@vite(?:\/client)?(?:["'?]|$)/.test(body) ||
|
|
332
|
+
/\/@react-refresh/.test(body) ||
|
|
333
|
+
/\/node_modules\/\.vite\//.test(body) ||
|
|
334
|
+
/\/node_modules\/\.pnpm\/vite@/.test(body)
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function viteDevBases(body) {
|
|
339
|
+
const bases = [];
|
|
340
|
+
const re =
|
|
341
|
+
/\bconst base(?:\$\d+)?\s*=\s*["'](\/[^"']*)["']\s*\|\|\s*["']\/["']/g;
|
|
342
|
+
let match;
|
|
343
|
+
while ((match = re.exec(body))) {
|
|
344
|
+
const base = match[1];
|
|
345
|
+
if (!base || base === "/") continue;
|
|
346
|
+
bases.push(base.endsWith("/") ? base : `${base}/`);
|
|
347
|
+
}
|
|
348
|
+
return bases;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function viteAssetRoots(body) {
|
|
352
|
+
const roots = new Set([
|
|
353
|
+
"/@vite",
|
|
354
|
+
"/@react-refresh",
|
|
355
|
+
"/@fs",
|
|
356
|
+
"/@id",
|
|
357
|
+
"/node_modules/",
|
|
358
|
+
"/src/",
|
|
359
|
+
"/config.json",
|
|
360
|
+
]);
|
|
361
|
+
const re =
|
|
362
|
+
/["'`](\/[^"'`]*?)(?=\/(?:@vite|@react-refresh|@fs|@id|node_modules\/|src\/))/g;
|
|
363
|
+
let match;
|
|
364
|
+
while ((match = re.exec(body))) {
|
|
365
|
+
const base = match[1];
|
|
366
|
+
if (!base || base === "/") continue;
|
|
367
|
+
roots.add(base.endsWith("/") ? base : `${base}/`);
|
|
368
|
+
}
|
|
369
|
+
for (const base of viteDevBases(body)) roots.add(base);
|
|
370
|
+
return [...roots];
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function joinProxyAndViteBase(pathPrefix, viteBase) {
|
|
374
|
+
const prefix = pathPrefix.replace(/\/$/, "");
|
|
375
|
+
if (!viteBase || viteBase === "/") return `${prefix}/`;
|
|
376
|
+
const base = viteBase.startsWith("/") ? viteBase : `/${viteBase}`;
|
|
377
|
+
return `${prefix}${base.endsWith("/") ? base : `${base}/`}`;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function rewriteViteHmrClient(body, pathPrefix) {
|
|
381
|
+
if (!pathPrefix || !body.includes("vite-hmr")) return body;
|
|
382
|
+
const viteBase = viteDevBases(body)[0] || "/";
|
|
383
|
+
const prefixed = joinProxyAndViteBase(pathPrefix, viteBase);
|
|
384
|
+
const hostPath = JSON.stringify(prefixed).slice(1, -1);
|
|
385
|
+
let out = body;
|
|
386
|
+
out = out.replace(
|
|
387
|
+
/\bconst socketHost = `[\s\S]*?`;/,
|
|
388
|
+
`const socketHost = \`\${importMetaUrl.host}${hostPath}\`;`
|
|
389
|
+
);
|
|
390
|
+
out = out.replace(
|
|
391
|
+
/\bconst directSocketHost = (?:"[^"]*"|'[^']*');/,
|
|
392
|
+
`const directSocketHost = importMetaUrl.host + ${JSON.stringify(prefixed)};`
|
|
393
|
+
);
|
|
394
|
+
return out;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function prefixQuotedRoots(body, pathPrefix, roots) {
|
|
398
|
+
if (!pathPrefix) return body;
|
|
399
|
+
let out = body;
|
|
400
|
+
for (const root of roots) {
|
|
401
|
+
const escaped = root.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
402
|
+
out = out.replace(
|
|
403
|
+
new RegExp(`(["'\`])(${escaped})`, "g"),
|
|
404
|
+
`$1${pathPrefix}$2`
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
return out;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function alreadyPrefixed(path, pathPrefix) {
|
|
411
|
+
return path === pathPrefix || path.startsWith(`${pathPrefix}/`);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function rewriteNextTurbopackBasePath(body, pathPrefix) {
|
|
415
|
+
if (!pathPrefix) return body;
|
|
416
|
+
let out = body;
|
|
417
|
+
if (out.includes("TURBOPACK compile-time value")) {
|
|
418
|
+
const next = JSON.stringify(pathPrefix);
|
|
419
|
+
out = out
|
|
420
|
+
.split(`("TURBOPACK compile-time value", "") || ''`)
|
|
421
|
+
.join(`("TURBOPACK compile-time value", ${next}) || ''`)
|
|
422
|
+
.split(`("TURBOPACK compile-time value", "") || ""`)
|
|
423
|
+
.join(`("TURBOPACK compile-time value", ${next}) || ""`);
|
|
424
|
+
}
|
|
425
|
+
const fromLocation =
|
|
426
|
+
"location ? (0, _createhreffromurl.createHrefFromUrl)(location) : initialCanonicalUrl";
|
|
427
|
+
if (out.includes(fromLocation)) {
|
|
428
|
+
out = out.split(fromLocation).join("initialCanonicalUrl");
|
|
429
|
+
}
|
|
430
|
+
const closeOnDomReady =
|
|
431
|
+
"if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', DOMContentLoaded, false);\n} else {\n // Delayed in marco task to ensure it's executed later than hydration\n setTimeout(DOMContentLoaded);\n}";
|
|
432
|
+
const closeOnLoad =
|
|
433
|
+
"if (document.readyState === 'complete') {\n setTimeout(DOMContentLoaded);\n} else {\n window.addEventListener('load', DOMContentLoaded, false);\n}";
|
|
434
|
+
if (out.includes(closeOnDomReady)) {
|
|
435
|
+
out = out.split(closeOnDomReady).join(closeOnLoad);
|
|
436
|
+
}
|
|
437
|
+
return out;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function contentMime(contentType) {
|
|
441
|
+
return contentType.toLowerCase().split(";")[0]?.trim() || "";
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function prefixRootPaths(body, publicBase, contentType = "") {
|
|
445
|
+
const pathPrefix = proxyPathPrefix(publicBase);
|
|
446
|
+
if (!pathPrefix) return body;
|
|
447
|
+
const mime = contentMime(contentType);
|
|
448
|
+
const isCode =
|
|
449
|
+
/javascript|ecmascript|json|x-component|x-ref|\brsc\b/.test(mime) ||
|
|
450
|
+
mime === "text/plain";
|
|
451
|
+
const html =
|
|
452
|
+
!isCode &&
|
|
453
|
+
(mime === "text/html" ||
|
|
454
|
+
mime === "application/xhtml+xml" ||
|
|
455
|
+
(!mime && /<!doctype html|<html[\s>]/i.test(body)));
|
|
456
|
+
const css = mime === "text/css" || (!isCode && !html && /css/.test(mime));
|
|
457
|
+
const viteAndNext = ["/_next/", "/__nextjs_", ...viteAssetRoots(body)];
|
|
458
|
+
if (!html && !css) {
|
|
459
|
+
let code = rewriteViteHmrClient(body, pathPrefix);
|
|
460
|
+
code = prefixQuotedRoots(code, pathPrefix, viteAndNext);
|
|
461
|
+
code = code.replace(
|
|
462
|
+
/(?<![A-Za-z0-9])\/__nextjs_/g,
|
|
463
|
+
`${pathPrefix}/__nextjs_`
|
|
464
|
+
);
|
|
465
|
+
return rewriteNextTurbopackBasePath(code, pathPrefix);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
let out = body;
|
|
469
|
+
if (
|
|
470
|
+
html &&
|
|
471
|
+
/<head[\s>]/i.test(out) &&
|
|
472
|
+
!/<base\s/i.test(out) &&
|
|
473
|
+
!isNextDocument(out)
|
|
474
|
+
) {
|
|
475
|
+
out = out.replace(
|
|
476
|
+
/<head([^>]*)>/i,
|
|
477
|
+
`<head$1><base href="${pathPrefix}/">`
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
if (html) {
|
|
481
|
+
out = out.replace(
|
|
482
|
+
/(\s(?:src|href|action|poster)=["'])(\/(?!\/)[^"']*)/gi,
|
|
483
|
+
(full, start, path) =>
|
|
484
|
+
alreadyPrefixed(path, pathPrefix) ? full : `${start}${pathPrefix}${path}`
|
|
485
|
+
);
|
|
486
|
+
out = out.replace(/\s+crossorigin(?:\s*=\s*(["']).*?\1)?/gi, "");
|
|
487
|
+
}
|
|
488
|
+
if (isNextDocument(out)) {
|
|
489
|
+
out = prefixQuotedRoots(out, pathPrefix, ["/_next/", "/__nextjs_"]);
|
|
490
|
+
out = out.replace(
|
|
491
|
+
/(url\(\s*['"]?)(\/(?!\/)[^)"']*)/gi,
|
|
492
|
+
(full, start, path) => {
|
|
493
|
+
if (alreadyPrefixed(path, pathPrefix)) return full;
|
|
494
|
+
if (!path.startsWith("/_next/") && !path.startsWith("/__nextjs_")) {
|
|
495
|
+
return full;
|
|
496
|
+
}
|
|
497
|
+
return `${start}${pathPrefix}${path}`;
|
|
498
|
+
}
|
|
499
|
+
);
|
|
500
|
+
} else {
|
|
501
|
+
out = out.replace(
|
|
502
|
+
/(url\(\s*['"]?)(\/(?!\/)[^)"']*)/gi,
|
|
503
|
+
(full, start, path) =>
|
|
504
|
+
alreadyPrefixed(path, pathPrefix) ? full : `${start}${pathPrefix}${path}`
|
|
505
|
+
);
|
|
506
|
+
for (const root of viteAndNext) {
|
|
507
|
+
const escaped = root.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
508
|
+
out = out.replace(
|
|
509
|
+
new RegExp(`(?<![A-Za-z0-9])${escaped}`, "g"),
|
|
510
|
+
`${pathPrefix}${root}`
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return out;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function httpToWsOrigin(httpUrl) {
|
|
518
|
+
const trimmed = httpUrl.replace(/\/$/, "");
|
|
519
|
+
if (trimmed.startsWith("https://")) {
|
|
520
|
+
return `wss://${trimmed.slice("https://".length)}`;
|
|
521
|
+
}
|
|
522
|
+
if (trimmed.startsWith("http://")) {
|
|
523
|
+
return `ws://${trimmed.slice("http://".length)}`;
|
|
524
|
+
}
|
|
525
|
+
return trimmed;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function isHtmlDocument(headers, body) {
|
|
529
|
+
const ct = headerGet(headers, "content-type").toLowerCase();
|
|
530
|
+
if (ct && !/html/.test(ct) && !/text\/plain/.test(ct)) return false;
|
|
531
|
+
return /<!doctype html|<html[\s>]|<body[\s>]|<\/body>|<\/html>/i.test(body);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function isScriptRequestPath(path) {
|
|
535
|
+
const p = String(path || "").split("?")[0]?.toLowerCase() || "";
|
|
536
|
+
return /\.(?:m?js|cjs)$/.test(p);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function rewriteLocalSidecarUrls(html, aiPublicBase, uiPublicBase) {
|
|
540
|
+
const ai = String(aiPublicBase || "").replace(/\/$/, "");
|
|
541
|
+
const ui = String(uiPublicBase || "").replace(/\/$/, "");
|
|
542
|
+
let out = html.split("__AI_SERVER_URL__").join(ai);
|
|
543
|
+
if (ui && ui !== ai) {
|
|
544
|
+
out = out
|
|
545
|
+
.split(`${ui}/embed-config.js`)
|
|
546
|
+
.join(`${ai}/embed-config.js`)
|
|
547
|
+
.split(`${ui}/ai-ui.iife.js`)
|
|
548
|
+
.join(`${ai}/ai-ui.iife.js`);
|
|
549
|
+
}
|
|
550
|
+
out = out.replace(
|
|
551
|
+
/https?:\/\/(?:localhost|127\.0\.0\.1):\d+(?=\/(?:embed-config\.js|ai-ui\.iife\.js|api\/(?:chat|ws)))/gi,
|
|
552
|
+
ai
|
|
553
|
+
);
|
|
554
|
+
return out;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function rewriteEmbedConfigJs(body, publicBase) {
|
|
558
|
+
if (!/^\s*window\.__MAINTAINER_PRO__\s*=/.test(body)) return body;
|
|
559
|
+
const base = String(publicBase || "").replace(/\/$/, "");
|
|
560
|
+
const start = body.indexOf("{");
|
|
561
|
+
const end = body.lastIndexOf("}");
|
|
562
|
+
if (start < 0 || end <= start) return body;
|
|
563
|
+
try {
|
|
564
|
+
const payload = JSON.parse(body.slice(start, end + 1));
|
|
565
|
+
payload.aiServerUrl = base;
|
|
566
|
+
payload.apiUrl = `${base}/api/chat`;
|
|
567
|
+
payload.aiServerWsUrl = `${httpToWsOrigin(base)}/api/ws`;
|
|
568
|
+
try {
|
|
569
|
+
payload.maintainerProUrl = new URL(base).origin;
|
|
570
|
+
} catch {
|
|
571
|
+
/* keep */
|
|
572
|
+
}
|
|
573
|
+
return `window.__MAINTAINER_PRO__=${JSON.stringify(payload)};`;
|
|
574
|
+
} catch {
|
|
575
|
+
return body;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function htmlAlreadyHasWidget(html) {
|
|
580
|
+
return (
|
|
581
|
+
html.includes("data-mp-proxy-embed") ||
|
|
582
|
+
html.includes("AiUi.init") ||
|
|
583
|
+
html.includes("ai-ui.iife.js")
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function injectMaintainerProEmbed(html, aiPublicBase) {
|
|
588
|
+
if (htmlAlreadyHasWidget(html)) return html;
|
|
589
|
+
const ai = String(aiPublicBase || "").replace(/\/$/, "");
|
|
590
|
+
const loader = `<script data-mp-proxy-embed>(function(ai,ws){if(!ai||window.__MP_EMBED_LOADING)return;window.__MP_EMBED_LOADING=1;function load(src){return new Promise(function(resolve,reject){var s=document.createElement("script");s.src=src;s.onload=function(){resolve()};s.onerror=function(){reject(new Error(src))};(document.head||document.documentElement).appendChild(s)})}function boot(){var cfg=window.__MAINTAINER_PRO__||{};if(!window.AiUi||!window.AiUi.init)return false;window.AiUi.init({apiUrl:ai+"/api/chat",aiServerWsUrl:ws,title:"AI Assistant",maintainerProUrl:cfg.maintainerProUrl||undefined,maintainerProApiKey:cfg.maintainerProApiKey||undefined});return true}load(ai+"/embed-config.js").then(function(){return load(ai+"/ai-ui.iife.js")}).then(function(){if(boot())return;var n=0,t=setInterval(function(){n+=1;if(boot()||n>50)clearInterval(t)},100)}).catch(function(){})})(${JSON.stringify(ai)},${JSON.stringify(`${httpToWsOrigin(ai)}/api/ws`)})<\/script>`;
|
|
591
|
+
if (/<head[^>]*>/i.test(html)) {
|
|
592
|
+
return html.replace(/<head([^>]*)>/i, `<head$1>${loader}`);
|
|
593
|
+
}
|
|
594
|
+
if (/<\/html>/i.test(html)) {
|
|
595
|
+
return html.replace(/<\/html>/i, `</html>${loader}`);
|
|
596
|
+
}
|
|
597
|
+
if (/<\/body>/i.test(html)) {
|
|
598
|
+
return html.replace(/<\/body>/i, `${loader}</body>`);
|
|
599
|
+
}
|
|
600
|
+
return html + loader;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function injectNextProxyShim(html, publicBase) {
|
|
604
|
+
const prefix = proxyPathPrefix(publicBase);
|
|
605
|
+
if (
|
|
606
|
+
!prefix ||
|
|
607
|
+
html.includes("data-mp-proxy-next") ||
|
|
608
|
+
(!isNextDocument(html) && !isViteDocument(html))
|
|
609
|
+
) {
|
|
610
|
+
return html;
|
|
611
|
+
}
|
|
612
|
+
const p = JSON.stringify(prefix);
|
|
613
|
+
const script = `<script data-mp-proxy-next>(function(p){if(!p)return;function add(v){if(typeof v!=="string"||!v)return v;if(v.charAt(0)==="/"&&v.charAt(1)!=="/"){if(v===p||v.indexOf(p+"/")===0)return v;var root=p.slice(0,p.lastIndexOf("/"));if(root&&(v===root||v.indexOf(root+"/")===0)return v;if(v==="/api/v1"||v.indexOf("/api/v1/")===0)return v;return p+v}try{var u=new URL(v,location.href);var host=String(u.hostname||"").toLowerCase();if(host.charAt(0)==="["&&host.charAt(host.length-1)==="]")host=host.slice(1,-1);var local=host==="localhost"||host==="127.0.0.1"||host==="::1"||host==="0.0.0.0";if(!local){if(u.pathname===p){u.pathname="/";return u.toString()}if(u.pathname.indexOf(p+"/")===0){u.pathname=u.pathname.slice(p.length)||"/";return u.toString()}return v}var samePort=String(u.port||"")===String(location.port||"");if(samePort){u.pathname=add(u.pathname);return u.toString()}if(u.pathname===p||u.pathname.indexOf(p+"/")===0){u.protocol=location.protocol;u.host=location.host;return u.toString()}}catch(e){}return v}function mapSel(sel){if(typeof sel!=="string")return sel;var pairs=[["src=\\"/_next/","src=\\""+p+"/_next/"],["href=\\"/_next/","href=\\""+p+"/_next/"]];for(var i=0;i<pairs.length;i++){if(sel.indexOf(pairs[i][0])!==-1)return sel.split(pairs[i][0]).join(pairs[i][1])}return sel}function patchQS(proto,name){var orig=proto[name];proto[name]=function(sel){var r=orig.call(this,sel);if(name==="querySelector"){if(r)return r}else if(r.length)return r;var s2=mapSel(sel);return s2===sel?r:orig.call(this,s2)}}patchQS(Document.prototype,"querySelector");patchQS(Document.prototype,"querySelectorAll");patchQS(Element.prototype,"querySelector");patchQS(Element.prototype,"querySelectorAll");var sa=Element.prototype.setAttribute;Element.prototype.setAttribute=function(n,v){if((n==="src"||n==="href")&&typeof v==="string")v=add(v);return sa.call(this,n,v)};function patchUrlProp(ctor,prop){try{var d=Object.getOwnPropertyDescriptor(ctor.prototype,prop);if(!d||!d.set)return;Object.defineProperty(ctor.prototype,prop,{configurable:true,enumerable:true,get:d.get,set:function(v){d.set.call(this,add(v))}})}catch(e){}}patchUrlProp(HTMLScriptElement,"src");patchUrlProp(HTMLLinkElement,"href");var f=window.fetch;window.fetch=function(input,init){if(typeof input==="string")input=add(input);else if(typeof Request!=="undefined"&&input instanceof Request)input=new Request(add(input.url),input);else if(typeof URL!=="undefined"&&input instanceof URL)input=new URL(add(input.href));return f.call(this,input,init)};var xo=XMLHttpRequest.prototype.open;XMLHttpRequest.prototype.open=function(m,u){if(typeof u==="string")arguments[1]=add(u);return xo.apply(this,arguments)};var ps=history.pushState.bind(history);history.pushState=function(s,t,u){if(typeof u==="string")u=add(u);return ps(s,t,u)};var rs=history.replaceState.bind(history);history.replaceState=function(s,t,u){if(typeof u==="string")u=add(u);return rs(s,t,u)};var WS=window.WebSocket;function WrappedWS(url,protocols){if(typeof url==="string")url=add(url);return protocols!==undefined?new WS(url,protocols):new WS(url)}WrappedWS.prototype=WS.prototype;WrappedWS.CONNECTING=WS.CONNECTING;WrappedWS.OPEN=WS.OPEN;WrappedWS.CLOSING=WS.CLOSING;WrappedWS.CLOSED=WS.CLOSED;window.WebSocket=WrappedWS})(${p})</script>`;
|
|
614
|
+
return html.replace(/<head([^>]*)>/i, `<head$1>${script}`);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Rewrite + gzip a local app response before it goes over the bridge WS.
|
|
619
|
+
*
|
|
620
|
+
* @param {{
|
|
621
|
+
* path?: string,
|
|
622
|
+
* slug?: string,
|
|
623
|
+
* publicBase?: string,
|
|
624
|
+
* aiPublicBase?: string,
|
|
625
|
+
* status?: number,
|
|
626
|
+
* headers?: Record<string, string>,
|
|
627
|
+
* body?: Buffer | string,
|
|
628
|
+
* acceptEncoding?: string,
|
|
629
|
+
* ifNoneMatch?: string,
|
|
630
|
+
* port?: number,
|
|
631
|
+
* }} opts
|
|
632
|
+
* @returns {{ status: number, headers: Record<string, string>, body: Buffer }}
|
|
633
|
+
*/
|
|
634
|
+
export function processShareHttpResponse(opts) {
|
|
635
|
+
const path = String(opts?.path || "/");
|
|
636
|
+
const slug = String(opts?.slug || "");
|
|
637
|
+
const publicBase = String(opts?.publicBase || "");
|
|
638
|
+
const aiPublicBase = String(opts?.aiPublicBase || "");
|
|
639
|
+
const acceptEncoding = String(opts?.acceptEncoding || "");
|
|
640
|
+
const ifNoneMatch = String(opts?.ifNoneMatch || "");
|
|
641
|
+
const port = Number(opts?.port) || 0;
|
|
642
|
+
let status =
|
|
643
|
+
typeof opts?.status === "number" && opts.status >= 100 ? opts.status : 200;
|
|
644
|
+
const headers = lowerHeaders(opts?.headers || {});
|
|
645
|
+
let payload = Buffer.isBuffer(opts?.body)
|
|
646
|
+
? opts.body
|
|
647
|
+
: Buffer.from(opts?.body || "");
|
|
648
|
+
|
|
649
|
+
if (headers.location) {
|
|
650
|
+
headers.location = rewriteLocation(headers.location, publicBase, port);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const cachedKey = rewriteCacheKey(publicBase, path);
|
|
654
|
+
const hit =
|
|
655
|
+
cachedKey && Date.now() - (rewriteCache.get(cachedKey)?.at || 0) < REWRITE_CACHE_MS
|
|
656
|
+
? rewriteCache.get(cachedKey)
|
|
657
|
+
: null;
|
|
658
|
+
if (hit) {
|
|
659
|
+
return finishCachedShareResponse(hit, path, ifNoneMatch, acceptEncoding);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (isScriptRequestPath(path) && isHtmlDocument(headers, payload.toString("utf8"))) {
|
|
663
|
+
const notFound = {
|
|
664
|
+
"content-type": "text/plain; charset=utf-8",
|
|
665
|
+
"cache-control": "private, no-store",
|
|
666
|
+
};
|
|
667
|
+
return {
|
|
668
|
+
status: 404,
|
|
669
|
+
headers: notFound,
|
|
670
|
+
body: Buffer.from("Not found"),
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
let text = payload.toString("utf8");
|
|
675
|
+
let mutated = false;
|
|
676
|
+
if (publicBase && shouldRewriteBody(headers)) {
|
|
677
|
+
text = prefixRootPaths(text, publicBase, headers["content-type"] || "");
|
|
678
|
+
mutated = true;
|
|
679
|
+
}
|
|
680
|
+
if (aiPublicBase) {
|
|
681
|
+
const withShareUrls = rewriteLocalSidecarUrls(text, aiPublicBase, publicBase);
|
|
682
|
+
if (withShareUrls !== text) {
|
|
683
|
+
text = withShareUrls;
|
|
684
|
+
mutated = true;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
if (aiPublicBase && /^\s*window\.__MAINTAINER_PRO__\s*=/.test(text)) {
|
|
688
|
+
const next = rewriteEmbedConfigJs(text, aiPublicBase);
|
|
689
|
+
if (next !== text) {
|
|
690
|
+
text = next;
|
|
691
|
+
mutated = true;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
if (
|
|
695
|
+
publicBase &&
|
|
696
|
+
isHtmlDocument(headers, text) &&
|
|
697
|
+
(isNextDocument(text) || isViteDocument(text))
|
|
698
|
+
) {
|
|
699
|
+
const next = injectNextProxyShim(text, publicBase);
|
|
700
|
+
if (next !== text) {
|
|
701
|
+
text = next;
|
|
702
|
+
mutated = true;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (slug !== "ai" && aiPublicBase && isHtmlDocument(headers, text)) {
|
|
706
|
+
const next = injectMaintainerProEmbed(text, aiPublicBase);
|
|
707
|
+
if (next !== text) {
|
|
708
|
+
text = next;
|
|
709
|
+
mutated = true;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (mutated) {
|
|
714
|
+
payload = Buffer.from(text, "utf8");
|
|
715
|
+
delete headers["content-encoding"];
|
|
716
|
+
}
|
|
717
|
+
delete headers.etag;
|
|
718
|
+
delete headers["last-modified"];
|
|
719
|
+
const mode = applyShareCacheHeaders(headers, path);
|
|
720
|
+
const etag = etagFor(payload);
|
|
721
|
+
if (mode !== "no-store") headers.etag = etag;
|
|
722
|
+
if (cachedKey) rememberRewrite(cachedKey, payload, status, headers, etag);
|
|
723
|
+
if (mode !== "no-store" && etagMatches(ifNoneMatch, etag)) {
|
|
724
|
+
return notModifiedResult(headers, etag);
|
|
725
|
+
}
|
|
726
|
+
payload = gzipIfRequested(payload, headers, acceptEncoding);
|
|
727
|
+
headers["content-length"] = String(payload.length);
|
|
728
|
+
return { status, headers, body: payload };
|
|
729
|
+
}
|