@maintainer-pro/ai-bridge 0.1.12 → 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 +113 -8
- 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();
|
|
@@ -2306,6 +2313,50 @@ function replyProxyHttp(id, status, headers, body) {
|
|
|
2306
2313
|
bridgeSend({ type: "proxy.http.chunk", id, stream, data: "", eof: true });
|
|
2307
2314
|
}
|
|
2308
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
|
+
|
|
2309
2360
|
function bridgeEmbedConfigJs(ws) {
|
|
2310
2361
|
const store = ws?.store && typeof ws.store === "object" ? ws.store : {};
|
|
2311
2362
|
const aiApp = { id: "ai-server", role: "ai-server" };
|
|
@@ -2331,7 +2382,16 @@ async function handleAiProxyHttpFromAdmin(msg, ws) {
|
|
|
2331
2382
|
const method = String(msg.method || "GET").toUpperCase();
|
|
2332
2383
|
const reqPath = safeProxyPath(msg.path);
|
|
2333
2384
|
const pathname = reqPath.split("?")[0] || "/";
|
|
2334
|
-
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;
|
|
2335
2395
|
const body =
|
|
2336
2396
|
typeof msg.body === "string" && msg.body
|
|
2337
2397
|
? Buffer.from(msg.body, "base64")
|
|
@@ -2340,20 +2400,21 @@ async function handleAiProxyHttpFromAdmin(msg, ws) {
|
|
|
2340
2400
|
if (pathname === "/ai-ui.iife.js") {
|
|
2341
2401
|
const file = findIife();
|
|
2342
2402
|
if (!file || !fs.existsSync(file)) {
|
|
2343
|
-
|
|
2403
|
+
sendProcessedProxyHttp(
|
|
2344
2404
|
id,
|
|
2405
|
+
ctx,
|
|
2345
2406
|
404,
|
|
2346
2407
|
{ "content-type": "text/plain; charset=utf-8" },
|
|
2347
2408
|
"Not found"
|
|
2348
2409
|
);
|
|
2349
2410
|
return;
|
|
2350
2411
|
}
|
|
2351
|
-
|
|
2412
|
+
sendProcessedProxyHttp(
|
|
2352
2413
|
id,
|
|
2414
|
+
ctx,
|
|
2353
2415
|
200,
|
|
2354
2416
|
{
|
|
2355
2417
|
"content-type": "text/javascript; charset=utf-8",
|
|
2356
|
-
"cache-control": "no-store",
|
|
2357
2418
|
},
|
|
2358
2419
|
fs.readFileSync(file)
|
|
2359
2420
|
);
|
|
@@ -2363,8 +2424,9 @@ async function handleAiProxyHttpFromAdmin(msg, ws) {
|
|
|
2363
2424
|
if (pathname === "/embed-config.js") {
|
|
2364
2425
|
const cfg = bridgeCfg || loadConfig();
|
|
2365
2426
|
await loadSandboxStoreEnv(ws, cfg);
|
|
2366
|
-
|
|
2427
|
+
sendProcessedProxyHttp(
|
|
2367
2428
|
id,
|
|
2429
|
+
ctx,
|
|
2368
2430
|
200,
|
|
2369
2431
|
{
|
|
2370
2432
|
"content-type": "text/javascript; charset=utf-8",
|
|
@@ -2387,7 +2449,7 @@ async function handleAiProxyHttpFromAdmin(msg, ws) {
|
|
|
2387
2449
|
headers,
|
|
2388
2450
|
body,
|
|
2389
2451
|
});
|
|
2390
|
-
|
|
2452
|
+
sendProcessedProxyHttp(id, ctx, result.status, result.headers, result.body);
|
|
2391
2453
|
} catch (err) {
|
|
2392
2454
|
bridgeSend({
|
|
2393
2455
|
type: "proxy.http.error",
|
|
@@ -2443,7 +2505,29 @@ function handleProxyHttpFromAdmin(msg) {
|
|
|
2443
2505
|
}
|
|
2444
2506
|
const method = String(msg.method || "GET").toUpperCase();
|
|
2445
2507
|
const path = safeProxyPath(msg.path);
|
|
2446
|
-
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;
|
|
2447
2531
|
headers.host = `127.0.0.1:${port}`;
|
|
2448
2532
|
// Next.js dev 403s `/_next` when Origin/sec-fetch look cross-site.
|
|
2449
2533
|
// This hop is server-to-server; drop those so chunks always load.
|
|
@@ -2479,11 +2563,31 @@ function handleProxyHttpFromAdmin(msg) {
|
|
|
2479
2563
|
if (value == null) continue;
|
|
2480
2564
|
outHeaders[key] = Array.isArray(value) ? value.join(", ") : String(value);
|
|
2481
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
|
+
}
|
|
2482
2586
|
bridgeSend({
|
|
2483
2587
|
type: "proxy.http.start",
|
|
2484
2588
|
id,
|
|
2485
2589
|
stream,
|
|
2486
|
-
status
|
|
2590
|
+
status,
|
|
2487
2591
|
headers: outHeaders,
|
|
2488
2592
|
});
|
|
2489
2593
|
res.on("data", (chunk) => {
|
|
@@ -5265,6 +5369,7 @@ async function main() {
|
|
|
5265
5369
|
senderType: msg.senderType === "client" ? "client" : undefined,
|
|
5266
5370
|
senderName:
|
|
5267
5371
|
typeof msg.senderName === "string" ? msg.senderName : undefined,
|
|
5372
|
+
context: msg.context && typeof msg.context === "object" ? msg.context : undefined,
|
|
5268
5373
|
};
|
|
5269
5374
|
const embedded = embeddedChat.get(sandboxId);
|
|
5270
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
|
+
}
|