leglas 0.8.0 → 1.0.0
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/README.md +292 -371
- package/dist/bin.js +1707 -162
- package/dist/index.js +1707 -162
- package/dist/shell/assets/index-BCvek69-.css +1 -0
- package/dist/shell/assets/index-BjY8sKEQ.js +14 -0
- package/dist/shell/index.html +2 -2
- package/package.json +1 -1
- package/dist/shell/assets/index-CpYCCElH.css +0 -1
- package/dist/shell/assets/index-Ct9pp-D-.js +0 -14
package/dist/index.js
CHANGED
|
@@ -688,6 +688,14 @@ When asked for design variations, alternatives, or "a few options":
|
|
|
688
688
|
and register it with \`npx leglas add --title "\u2026" --url "/" --branch <branch>\`
|
|
689
689
|
(the config needs \`devCommand\` with \`{port}\`). Everything below is the
|
|
690
690
|
ordinary, in-app path.
|
|
691
|
+
A page the app rebuilds in the browser after load (anything that hydrates:
|
|
692
|
+
Next, Nuxt, SvelteKit, a captured production site) is not its served HTML.
|
|
693
|
+
Markup edited there shows for a moment and is then replaced from the app's
|
|
694
|
+
own JavaScript and data, so make the change where that JavaScript gets what
|
|
695
|
+
it renders. When that is a script other directions share, give it a
|
|
696
|
+
per-direction override that defaults to what it renders today: every other
|
|
697
|
+
direction renders exactly as before, which is adding beside, not rewriting.
|
|
698
|
+
\`npx leglas show\` says when a page was rebuilt after load.
|
|
691
699
|
2. Run \`npx leglas explore <surface> --count <n>\` first, adding
|
|
692
700
|
\`--based-on "<title>"\` when the user wants variations of a direction they
|
|
693
701
|
already like. It prints what the set needs and how to register it. In
|
|
@@ -1350,9 +1358,9 @@ function agentSearchPath(env = process.env, platform = process.platform) {
|
|
|
1350
1358
|
function agentEnvironment(env = process.env) {
|
|
1351
1359
|
return { ...env, PATH: agentSearchPath(env) };
|
|
1352
1360
|
}
|
|
1353
|
-
async function pathLookup(binary) {
|
|
1354
|
-
const entries = agentSearchPath().split(delimiter).filter((entry) => entry !== "");
|
|
1355
|
-
const extensions =
|
|
1361
|
+
async function pathLookup(binary, env = process.env, platform = process.platform) {
|
|
1362
|
+
const entries = agentSearchPath(env, platform).split(delimiter).filter((entry) => entry !== "");
|
|
1363
|
+
const extensions = platform === "win32" ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter((entry) => entry !== "") : [""];
|
|
1356
1364
|
for (const entry of entries) {
|
|
1357
1365
|
for (const extension of extensions) {
|
|
1358
1366
|
try {
|
|
@@ -1364,10 +1372,10 @@ async function pathLookup(binary) {
|
|
|
1364
1372
|
}
|
|
1365
1373
|
return false;
|
|
1366
1374
|
}
|
|
1367
|
-
async function detectAgents(
|
|
1375
|
+
async function detectAgents(lookup2 = pathLookup, probe2 = execProbe) {
|
|
1368
1376
|
const entries = Object.entries(KNOWN_AGENTS);
|
|
1369
1377
|
return Promise.all(entries.map(async ([id, adapter]) => {
|
|
1370
|
-
const available = await
|
|
1378
|
+
const available = await lookup2(adapter.binary).catch(() => false);
|
|
1371
1379
|
if (!available) {
|
|
1372
1380
|
return {
|
|
1373
1381
|
id,
|
|
@@ -1796,6 +1804,13 @@ async function dropLocalPreviews(cwd, titles) {
|
|
|
1796
1804
|
// ../server/dist/proxy.js
|
|
1797
1805
|
import http, {} from "http";
|
|
1798
1806
|
import net from "net";
|
|
1807
|
+
var SHARE_COOKIE = "leglas-share";
|
|
1808
|
+
function withoutShareCookie(cookie) {
|
|
1809
|
+
if (cookie === void 0)
|
|
1810
|
+
return void 0;
|
|
1811
|
+
const kept = (Array.isArray(cookie) ? cookie.join("; ") : cookie).split(";").map((entry) => entry.trim()).filter((entry) => entry !== "" && !entry.startsWith(`${SHARE_COOKIE}=`));
|
|
1812
|
+
return kept.length === 0 ? void 0 : kept.join("; ");
|
|
1813
|
+
}
|
|
1799
1814
|
function createProxyHandler(options) {
|
|
1800
1815
|
const target = new URL(options.target);
|
|
1801
1816
|
const host = target.hostname;
|
|
@@ -1803,19 +1818,25 @@ function createProxyHandler(options) {
|
|
|
1803
1818
|
const port = Number(target.port || (target.protocol === "https:" ? 443 : 80));
|
|
1804
1819
|
const authority = target.port ? `${host}:${target.port}` : host;
|
|
1805
1820
|
function upstreamHeaders(req) {
|
|
1806
|
-
|
|
1821
|
+
const headers = { ...req.headers, host: authority };
|
|
1822
|
+
const cookie = withoutShareCookie(headers.cookie);
|
|
1823
|
+
if (cookie === void 0)
|
|
1824
|
+
delete headers.cookie;
|
|
1825
|
+
else
|
|
1826
|
+
headers.cookie = cookie;
|
|
1827
|
+
return headers;
|
|
1807
1828
|
}
|
|
1808
|
-
function rewriteLocation(location,
|
|
1829
|
+
function rewriteLocation(location, publicOrigin2) {
|
|
1809
1830
|
if (location === void 0)
|
|
1810
1831
|
return void 0;
|
|
1811
1832
|
for (const origin of [`${target.protocol}//${authority}`, `${target.protocol}//localhost:${port}`]) {
|
|
1812
1833
|
if (location.startsWith(origin))
|
|
1813
|
-
return
|
|
1834
|
+
return publicOrigin2 + location.slice(origin.length);
|
|
1814
1835
|
}
|
|
1815
1836
|
return location;
|
|
1816
1837
|
}
|
|
1817
1838
|
return {
|
|
1818
|
-
request(req, res,
|
|
1839
|
+
request(req, res, publicOrigin2) {
|
|
1819
1840
|
options.onActivity?.();
|
|
1820
1841
|
options.onOpen?.();
|
|
1821
1842
|
let open = true;
|
|
@@ -1830,7 +1851,7 @@ function createProxyHandler(options) {
|
|
|
1830
1851
|
res.once("close", close);
|
|
1831
1852
|
const upstream = http.request({ host: dialHost, port, method: req.method, path: req.url, headers: upstreamHeaders(req) }, (upstreamRes) => {
|
|
1832
1853
|
const headers = { ...upstreamRes.headers };
|
|
1833
|
-
const location = rewriteLocation(typeof headers.location === "string" ? headers.location : void 0,
|
|
1854
|
+
const location = rewriteLocation(typeof headers.location === "string" ? headers.location : void 0, publicOrigin2);
|
|
1834
1855
|
if (location !== void 0)
|
|
1835
1856
|
headers.location = location;
|
|
1836
1857
|
res.writeHead(upstreamRes.statusCode ?? 502, headers);
|
|
@@ -2593,6 +2614,32 @@ function createBrowserPool(options = {}) {
|
|
|
2593
2614
|
};
|
|
2594
2615
|
}
|
|
2595
2616
|
|
|
2617
|
+
// ../server/dist/hydration.js
|
|
2618
|
+
function hydrationEvidence(messages) {
|
|
2619
|
+
for (const raw of messages) {
|
|
2620
|
+
const message2 = raw.split("\n", 1)[0]?.trim() ?? "";
|
|
2621
|
+
if (/Minified React error #(418|419|422|423|425)\b/.test(message2)) {
|
|
2622
|
+
return { framework: "React", message: message2 };
|
|
2623
|
+
}
|
|
2624
|
+
if (/Hydration failed because/.test(message2) || /error while hydrating/i.test(message2) || /Text content (did not|does not) match/i.test(message2) || /Expected server HTML to contain/i.test(message2) || /did not match\. Server:/.test(message2)) {
|
|
2625
|
+
return { framework: "React", message: message2 };
|
|
2626
|
+
}
|
|
2627
|
+
if (/Hydration (node|text|children|class|style|attribute) mismatch/i.test(message2) || /Hydration completed but contains mismatches/i.test(message2)) {
|
|
2628
|
+
return { framework: "Vue", message: message2 };
|
|
2629
|
+
}
|
|
2630
|
+
if (/hydration_mismatch/.test(message2)) {
|
|
2631
|
+
return { framework: "Svelte", message: message2 };
|
|
2632
|
+
}
|
|
2633
|
+
if (/Hydration Mismatch\. Unable to find DOM nodes/.test(message2)) {
|
|
2634
|
+
return { framework: "Solid", message: message2 };
|
|
2635
|
+
}
|
|
2636
|
+
if (/hydrat/i.test(message2) && (/expected .+ but found/i.test(message2) || /mismatch/i.test(message2) && /(node|element|markup|dom|tag|text|attribute|server|client)/i.test(message2))) {
|
|
2637
|
+
return { framework: "the app", message: message2 };
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
return null;
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2596
2643
|
// ../server/dist/capture.js
|
|
2597
2644
|
var FRAME_MAX_HEIGHT = 4e3;
|
|
2598
2645
|
var MIN_WIDTH = 320;
|
|
@@ -2686,10 +2733,12 @@ function locatorExpression(focus) {
|
|
|
2686
2733
|
async function render(page, input) {
|
|
2687
2734
|
const width = clamp(Math.round(input.width), MIN_WIDTH, MAX_WIDTH);
|
|
2688
2735
|
const errors = [];
|
|
2736
|
+
let hydration = null;
|
|
2689
2737
|
const remember = (value) => {
|
|
2738
|
+
const message2 = String(value ?? "").trim().slice(0, 240);
|
|
2739
|
+
hydration ??= hydrationEvidence([message2]);
|
|
2690
2740
|
if (errors.length >= 10)
|
|
2691
2741
|
return;
|
|
2692
|
-
const message2 = String(value ?? "").slice(0, 240);
|
|
2693
2742
|
if (message2 === "" || /favicon/i.test(message2))
|
|
2694
2743
|
return;
|
|
2695
2744
|
errors.push(message2);
|
|
@@ -2837,7 +2886,7 @@ async function render(page, input) {
|
|
|
2837
2886
|
resolved
|
|
2838
2887
|
});
|
|
2839
2888
|
}
|
|
2840
|
-
return { frame, crops, errors, cut };
|
|
2889
|
+
return { frame, crops, errors, hydration, cut };
|
|
2841
2890
|
} finally {
|
|
2842
2891
|
for (const stop of unlisten)
|
|
2843
2892
|
stop();
|
|
@@ -2981,17 +3030,23 @@ async function attachRequest(cwd, requestId, input, deps) {
|
|
|
2981
3030
|
const capture = deps.capture ?? capturePage;
|
|
2982
3031
|
const deadlineMs = deps.deadlineMs ?? 12e3;
|
|
2983
3032
|
const destination = join5(cwd, CAPTURES_DIR, requestId);
|
|
2984
|
-
const captured = {
|
|
3033
|
+
const captured = {
|
|
3034
|
+
attachments: [],
|
|
3035
|
+
errors: [],
|
|
3036
|
+
hydration: null,
|
|
3037
|
+
cut: false,
|
|
3038
|
+
skipped: null
|
|
3039
|
+
};
|
|
2985
3040
|
const references = [];
|
|
2986
3041
|
requestedWidths.set(captured, Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, Math.round(input.width))));
|
|
2987
3042
|
const controller = new AbortController();
|
|
2988
|
-
let
|
|
3043
|
+
let expired2 = false;
|
|
2989
3044
|
let finishDeadline;
|
|
2990
3045
|
const deadline = new Promise((resolve5) => {
|
|
2991
3046
|
finishDeadline = resolve5;
|
|
2992
3047
|
});
|
|
2993
3048
|
const timer = setTimeout(() => {
|
|
2994
|
-
|
|
3049
|
+
expired2 = true;
|
|
2995
3050
|
controller.abort();
|
|
2996
3051
|
finishDeadline();
|
|
2997
3052
|
}, deadlineMs);
|
|
@@ -3004,7 +3059,7 @@ async function attachRequest(cwd, requestId, input, deps) {
|
|
|
3004
3059
|
const work = (async () => {
|
|
3005
3060
|
try {
|
|
3006
3061
|
const browser = await deps.pool.acquire();
|
|
3007
|
-
if (
|
|
3062
|
+
if (expired2)
|
|
3008
3063
|
return;
|
|
3009
3064
|
if (browser === null) {
|
|
3010
3065
|
captured.skipped = deps.pool.reason() ?? NO_BROWSER;
|
|
@@ -3018,7 +3073,7 @@ async function attachRequest(cwd, requestId, input, deps) {
|
|
|
3018
3073
|
signal: controller.signal
|
|
3019
3074
|
};
|
|
3020
3075
|
const direction = await capture(browser, directionInput);
|
|
3021
|
-
if (
|
|
3076
|
+
if (expired2)
|
|
3022
3077
|
return;
|
|
3023
3078
|
await mkdir3(destination, { recursive: true });
|
|
3024
3079
|
await writeFile3(join5(destination, "frame.png"), direction.frame.png);
|
|
@@ -3031,11 +3086,12 @@ async function attachRequest(cwd, requestId, input, deps) {
|
|
|
3031
3086
|
viewport: direction.frame.width
|
|
3032
3087
|
});
|
|
3033
3088
|
captured.errors = direction.errors;
|
|
3089
|
+
captured.hydration = direction.hydration;
|
|
3034
3090
|
captured.cut = direction.cut;
|
|
3035
3091
|
for (let index = 0; index < direction.crops.length; index += 1) {
|
|
3036
3092
|
const crop = direction.crops[index];
|
|
3037
3093
|
const note = input.notes[index];
|
|
3038
|
-
if (crop === null || crop === void 0 || note === void 0 ||
|
|
3094
|
+
if (crop === null || crop === void 0 || note === void 0 || expired2)
|
|
3039
3095
|
continue;
|
|
3040
3096
|
const name = `note-${index + 1}.png`;
|
|
3041
3097
|
await writeFile3(join5(destination, name), crop.shot.png);
|
|
@@ -3049,7 +3105,7 @@ async function attachRequest(cwd, requestId, input, deps) {
|
|
|
3049
3105
|
viewport: direction.frame.width
|
|
3050
3106
|
});
|
|
3051
3107
|
}
|
|
3052
|
-
if (input.compare !== null && !
|
|
3108
|
+
if (input.compare !== null && !expired2) {
|
|
3053
3109
|
const compareInput = {
|
|
3054
3110
|
url: previewUrl(input.origin, input.compare),
|
|
3055
3111
|
width: input.width,
|
|
@@ -3057,7 +3113,7 @@ async function attachRequest(cwd, requestId, input, deps) {
|
|
|
3057
3113
|
signal: controller.signal
|
|
3058
3114
|
};
|
|
3059
3115
|
const comparison = await capture(browser, compareInput);
|
|
3060
|
-
if (
|
|
3116
|
+
if (expired2)
|
|
3061
3117
|
return;
|
|
3062
3118
|
await writeFile3(join5(destination, "compare.png"), comparison.frame.png);
|
|
3063
3119
|
captured.attachments.push({
|
|
@@ -3070,14 +3126,14 @@ async function attachRequest(cwd, requestId, input, deps) {
|
|
|
3070
3126
|
});
|
|
3071
3127
|
}
|
|
3072
3128
|
} catch (error) {
|
|
3073
|
-
if (!
|
|
3129
|
+
if (!expired2) {
|
|
3074
3130
|
captured.skipped = error instanceof Error ? error.message : `The page did not load: ${String(error)}`;
|
|
3075
3131
|
}
|
|
3076
3132
|
}
|
|
3077
3133
|
})();
|
|
3078
3134
|
await Promise.race([work, deadline]);
|
|
3079
3135
|
clearTimeout(timer);
|
|
3080
|
-
if (
|
|
3136
|
+
if (expired2)
|
|
3081
3137
|
captured.skipped = "The design could not be captured in time.";
|
|
3082
3138
|
captured.attachments.push(...references);
|
|
3083
3139
|
return captured;
|
|
@@ -3726,10 +3782,10 @@ function scope(leglasCommand, quotedTitle) {
|
|
|
3726
3782
|
|
|
3727
3783
|
This is a scoped design change: no test run, no build, and no survey of the rest of the project is needed. The result is checked visually in a live preview, not by tooling.
|
|
3728
3784
|
|
|
3729
|
-
Leave every other direction exactly as it is; they are alternatives being compared side by side, so changing a sibling destroys the comparison. Keep the change additive: do not rewrite shared components that other directions rely on.`;
|
|
3785
|
+
Leave every other direction exactly as it is; they are alternatives being compared side by side, so changing a sibling destroys the comparison. Keep the change additive: do not rewrite shared components that other directions rely on. A shared script may gain one small per-direction override, at the point it reads what it renders, that defaults to what it renders today; every other direction then renders exactly as before, so that counts as additive.`;
|
|
3730
3786
|
}
|
|
3731
3787
|
function capturedBlock(captured) {
|
|
3732
|
-
if (captured === null || captured.attachments.length === 0 && captured.errors.length === 0 && captured.skipped === null)
|
|
3788
|
+
if (captured === null || captured.attachments.length === 0 && captured.errors.length === 0 && captured.hydration === null && captured.skipped === null)
|
|
3733
3789
|
return "";
|
|
3734
3790
|
const lines2 = [];
|
|
3735
3791
|
const frames = captured.attachments.filter((attachment) => attachment.kind === "frame" || attachment.kind === "note");
|
|
@@ -3761,6 +3817,9 @@ function capturedBlock(captured) {
|
|
|
3761
3817
|
for (const error of captured.errors)
|
|
3762
3818
|
lines2.push(` - ${error}`);
|
|
3763
3819
|
}
|
|
3820
|
+
if (captured.hydration !== null) {
|
|
3821
|
+
lines2.push(`After load, ${captured.hydration.framework} rebuilt this page in the browser from the app's own JavaScript and data (${captured.hydration.message}). Markup edited in the served HTML shows for a moment and is then replaced, so make the change where that JavaScript gets what it renders: the data or source it reads, or a per-direction override that a shared script reads with the original as its default. Look at the result a few seconds after load, not at first paint.`);
|
|
3822
|
+
}
|
|
3764
3823
|
if (captured.skipped !== null) {
|
|
3765
3824
|
lines2.push(`(${captured.skipped} Use the live preview instead.)`);
|
|
3766
3825
|
}
|
|
@@ -5361,10 +5420,14 @@ function createCoalescer(emit, options = {}) {
|
|
|
5361
5420
|
}
|
|
5362
5421
|
};
|
|
5363
5422
|
}
|
|
5364
|
-
function createLiveHub(
|
|
5423
|
+
function createLiveHub(options = {}) {
|
|
5365
5424
|
const listeners = /* @__PURE__ */ new Set();
|
|
5425
|
+
let viewers = 0;
|
|
5366
5426
|
const drop = (listener) => {
|
|
5367
|
-
listeners.delete(listener)
|
|
5427
|
+
if (!listeners.delete(listener) || !listener.viewer)
|
|
5428
|
+
return;
|
|
5429
|
+
viewers = Math.max(0, viewers - 1);
|
|
5430
|
+
options.onViewers?.(viewers);
|
|
5368
5431
|
};
|
|
5369
5432
|
const write2 = (listener, opcode, payload) => {
|
|
5370
5433
|
if (listener.socket.destroyed || !listener.socket.writable) {
|
|
@@ -5449,7 +5512,7 @@ function createLiveHub(_options = {}) {
|
|
|
5449
5512
|
}
|
|
5450
5513
|
}
|
|
5451
5514
|
},
|
|
5452
|
-
upgrade: (req, socket, head) => {
|
|
5515
|
+
upgrade: (req, socket, head, upgradeOptions = {}) => {
|
|
5453
5516
|
const path = (req.url ?? "/").split("?")[0] ?? "/";
|
|
5454
5517
|
if (req.method !== "GET" || path !== LIVE_PATH)
|
|
5455
5518
|
return false;
|
|
@@ -5471,8 +5534,16 @@ Sec-WebSocket-Accept: ${accept}\r
|
|
|
5471
5534
|
socket.destroy();
|
|
5472
5535
|
return false;
|
|
5473
5536
|
}
|
|
5474
|
-
const listener = {
|
|
5537
|
+
const listener = {
|
|
5538
|
+
socket,
|
|
5539
|
+
buffered: Buffer.alloc(0),
|
|
5540
|
+
viewer: upgradeOptions.viewer === true
|
|
5541
|
+
};
|
|
5475
5542
|
listeners.add(listener);
|
|
5543
|
+
if (listener.viewer) {
|
|
5544
|
+
viewers += 1;
|
|
5545
|
+
options.onViewers?.(viewers);
|
|
5546
|
+
}
|
|
5476
5547
|
socket.on("data", (chunk) => read(listener, chunk));
|
|
5477
5548
|
socket.once("error", () => drop(listener));
|
|
5478
5549
|
socket.once("end", () => drop(listener));
|
|
@@ -5490,7 +5561,1327 @@ Sec-WebSocket-Accept: ${accept}\r
|
|
|
5490
5561
|
},
|
|
5491
5562
|
get listening() {
|
|
5492
5563
|
return listeners.size;
|
|
5564
|
+
},
|
|
5565
|
+
get viewers() {
|
|
5566
|
+
return viewers;
|
|
5567
|
+
}
|
|
5568
|
+
};
|
|
5569
|
+
}
|
|
5570
|
+
|
|
5571
|
+
// ../server/dist/share.js
|
|
5572
|
+
import { randomBytes as randomBytes4, randomUUID, timingSafeEqual } from "crypto";
|
|
5573
|
+
import http3 from "http";
|
|
5574
|
+
import { posix } from "path";
|
|
5575
|
+
|
|
5576
|
+
// ../server/dist/tunnel.js
|
|
5577
|
+
import { spawn as spawnChild } from "child_process";
|
|
5578
|
+
import { Resolver, lookup } from "dns/promises";
|
|
5579
|
+
import http2 from "http";
|
|
5580
|
+
import https from "https";
|
|
5581
|
+
var URL_DEADLINE_MS = 3e4;
|
|
5582
|
+
var PROBE_DEADLINE_MS = 3e4;
|
|
5583
|
+
var PROBE_INTERVAL_MS = 1500;
|
|
5584
|
+
var SLOW_PROBE_INTERVAL_MS = 4e3;
|
|
5585
|
+
var SLOW_PROBE_CAP_MS = 3e4;
|
|
5586
|
+
var STOP_GRACE_MS = 3e3;
|
|
5587
|
+
var STOP_LIMIT_MS = STOP_GRACE_MS + 2e3;
|
|
5588
|
+
var PROBE_TIMEOUT_MS2 = 3e3;
|
|
5589
|
+
async function askLink(resolver, url, entryPath) {
|
|
5590
|
+
let target;
|
|
5591
|
+
try {
|
|
5592
|
+
target = new URL(url);
|
|
5593
|
+
} catch {
|
|
5594
|
+
return false;
|
|
5595
|
+
}
|
|
5596
|
+
let address;
|
|
5597
|
+
try {
|
|
5598
|
+
[address] = await resolver.resolve4(target.hostname);
|
|
5599
|
+
} catch {
|
|
5600
|
+
try {
|
|
5601
|
+
address = (await lookup(target.hostname, { family: 4 })).address;
|
|
5602
|
+
} catch {
|
|
5603
|
+
return false;
|
|
5604
|
+
}
|
|
5605
|
+
}
|
|
5606
|
+
if (address === void 0)
|
|
5607
|
+
return false;
|
|
5608
|
+
const secure = target.protocol === "https:";
|
|
5609
|
+
return new Promise((resolve5) => {
|
|
5610
|
+
const request = (secure ? https : http2).request({
|
|
5611
|
+
host: address,
|
|
5612
|
+
port: Number(target.port || (secure ? 443 : 80)),
|
|
5613
|
+
path: entryPath,
|
|
5614
|
+
method: "GET",
|
|
5615
|
+
headers: { host: target.host },
|
|
5616
|
+
...secure ? { servername: target.hostname } : {},
|
|
5617
|
+
timeout: PROBE_TIMEOUT_MS2
|
|
5618
|
+
}, (response) => {
|
|
5619
|
+
response.resume();
|
|
5620
|
+
const status = response.statusCode ?? 0;
|
|
5621
|
+
resolve5(status >= 200 && status < 400);
|
|
5622
|
+
});
|
|
5623
|
+
request.once("timeout", () => request.destroy());
|
|
5624
|
+
request.once("error", () => resolve5(false));
|
|
5625
|
+
request.end();
|
|
5626
|
+
});
|
|
5627
|
+
}
|
|
5628
|
+
async function detectTunnels(env = process.env) {
|
|
5629
|
+
const providers = ["cloudflared", "ngrok"];
|
|
5630
|
+
const found = await Promise.all(providers.map((provider) => pathLookup(provider, env).catch(() => false)));
|
|
5631
|
+
return providers.filter((_provider, index) => found[index] === true);
|
|
5632
|
+
}
|
|
5633
|
+
function duration(ms) {
|
|
5634
|
+
return ms % 1e3 === 0 ? `${ms / 1e3}s` : `${ms}ms`;
|
|
5635
|
+
}
|
|
5636
|
+
function withOutput(sentence, output) {
|
|
5637
|
+
if (output === "")
|
|
5638
|
+
return sentence;
|
|
5639
|
+
const stem = sentence.endsWith(".") ? sentence.slice(0, -1) : sentence;
|
|
5640
|
+
return `${stem} (${output.slice(0, 160)}).`;
|
|
5641
|
+
}
|
|
5642
|
+
function startTunnel(options, deps = {}) {
|
|
5643
|
+
const spawn4 = deps.spawn ?? spawnChild;
|
|
5644
|
+
const now = deps.now ?? Date.now;
|
|
5645
|
+
const urlDeadlineMs = deps.urlDeadlineMs ?? URL_DEADLINE_MS;
|
|
5646
|
+
const probeDeadlineMs = deps.probeDeadlineMs ?? PROBE_DEADLINE_MS;
|
|
5647
|
+
const resolver = deps.probe === void 0 ? new Resolver() : null;
|
|
5648
|
+
const probe2 = deps.probe ?? ((url2) => askLink(resolver, url2, options.entryPath));
|
|
5649
|
+
const timers = /* @__PURE__ */ new Set();
|
|
5650
|
+
const later = (callback, ms) => {
|
|
5651
|
+
const timer = setTimeout(() => {
|
|
5652
|
+
timers.delete(timer);
|
|
5653
|
+
callback();
|
|
5654
|
+
}, ms);
|
|
5655
|
+
timer.unref?.();
|
|
5656
|
+
timers.add(timer);
|
|
5657
|
+
return timer;
|
|
5658
|
+
};
|
|
5659
|
+
const clearTimers = () => {
|
|
5660
|
+
for (const timer of timers)
|
|
5661
|
+
clearTimeout(timer);
|
|
5662
|
+
timers.clear();
|
|
5663
|
+
};
|
|
5664
|
+
let lastState = "";
|
|
5665
|
+
let state = { status: "starting", provider: options.provider };
|
|
5666
|
+
let terminal = false;
|
|
5667
|
+
let stopping = false;
|
|
5668
|
+
let exited = false;
|
|
5669
|
+
let stopPromise = null;
|
|
5670
|
+
let settleStop = null;
|
|
5671
|
+
let url = null;
|
|
5672
|
+
let urlAt = 0;
|
|
5673
|
+
let urlTimer = null;
|
|
5674
|
+
let lastLine = "";
|
|
5675
|
+
const partial = { stdout: "", stderr: "" };
|
|
5676
|
+
const report = (next) => {
|
|
5677
|
+
const serialized = JSON.stringify(next);
|
|
5678
|
+
if (serialized === lastState)
|
|
5679
|
+
return;
|
|
5680
|
+
lastState = serialized;
|
|
5681
|
+
state = next;
|
|
5682
|
+
options.onState(next);
|
|
5683
|
+
};
|
|
5684
|
+
const fail = (reason) => {
|
|
5685
|
+
if (terminal)
|
|
5686
|
+
return;
|
|
5687
|
+
terminal = true;
|
|
5688
|
+
clearTimers();
|
|
5689
|
+
report({
|
|
5690
|
+
status: "failed",
|
|
5691
|
+
provider: options.provider,
|
|
5692
|
+
reason,
|
|
5693
|
+
...url === null ? {} : { url }
|
|
5694
|
+
});
|
|
5695
|
+
};
|
|
5696
|
+
report(state);
|
|
5697
|
+
const args = options.provider === "cloudflared" ? [
|
|
5698
|
+
"tunnel",
|
|
5699
|
+
"--url",
|
|
5700
|
+
`http://127.0.0.1:${options.port}`,
|
|
5701
|
+
"--no-autoupdate"
|
|
5702
|
+
] : ["http", String(options.port), "--log", "stdout", "--log-format", "json"];
|
|
5703
|
+
let child;
|
|
5704
|
+
try {
|
|
5705
|
+
child = spawn4(options.provider, args, {
|
|
5706
|
+
env: agentEnvironment(),
|
|
5707
|
+
shell: false,
|
|
5708
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
5709
|
+
});
|
|
5710
|
+
} catch {
|
|
5711
|
+
fail(`${options.provider} exited before the tunnel came up.`);
|
|
5712
|
+
return { settle: () => {
|
|
5713
|
+
}, stop: async () => {
|
|
5714
|
+
} };
|
|
5715
|
+
}
|
|
5716
|
+
const beginProbe = (found) => {
|
|
5717
|
+
if (url !== null || terminal || stopping)
|
|
5718
|
+
return;
|
|
5719
|
+
url = found;
|
|
5720
|
+
urlAt = now();
|
|
5721
|
+
if (urlTimer !== null) {
|
|
5722
|
+
clearTimeout(urlTimer);
|
|
5723
|
+
timers.delete(urlTimer);
|
|
5724
|
+
urlTimer = null;
|
|
5725
|
+
}
|
|
5726
|
+
report({ status: "starting", provider: options.provider, url });
|
|
5727
|
+
later(() => {
|
|
5728
|
+
if (terminal || stopping || url === null)
|
|
5729
|
+
return;
|
|
5730
|
+
report({ status: "starting", provider: options.provider, url, slow: true });
|
|
5731
|
+
}, probeDeadlineMs);
|
|
5732
|
+
let slowWait = SLOW_PROBE_INTERVAL_MS;
|
|
5733
|
+
const again = () => {
|
|
5734
|
+
if (terminal || stopping || url === null)
|
|
5735
|
+
return;
|
|
5736
|
+
if (now() - urlAt < probeDeadlineMs) {
|
|
5737
|
+
later(poll, PROBE_INTERVAL_MS);
|
|
5738
|
+
return;
|
|
5739
|
+
}
|
|
5740
|
+
later(poll, slowWait);
|
|
5741
|
+
slowWait = Math.min(SLOW_PROBE_CAP_MS, slowWait * 2);
|
|
5742
|
+
};
|
|
5743
|
+
const poll = () => {
|
|
5744
|
+
if (terminal || stopping || url === null)
|
|
5745
|
+
return;
|
|
5746
|
+
void probe2(url).then((reachable) => {
|
|
5747
|
+
if (terminal || stopping || url === null)
|
|
5748
|
+
return;
|
|
5749
|
+
if (reachable) {
|
|
5750
|
+
terminal = true;
|
|
5751
|
+
clearTimers();
|
|
5752
|
+
report({ status: "ready", provider: options.provider, url });
|
|
5753
|
+
return;
|
|
5754
|
+
}
|
|
5755
|
+
again();
|
|
5756
|
+
}, again);
|
|
5757
|
+
};
|
|
5758
|
+
poll();
|
|
5759
|
+
};
|
|
5760
|
+
const inspect = (stream, line) => {
|
|
5761
|
+
const trimmed = line.trim();
|
|
5762
|
+
if (trimmed !== "")
|
|
5763
|
+
lastLine = trimmed;
|
|
5764
|
+
if (url !== null || trimmed === "")
|
|
5765
|
+
return;
|
|
5766
|
+
if (options.provider === "cloudflared") {
|
|
5767
|
+
const found = /https:\/\/(?!api\.)[a-z0-9-]+\.trycloudflare\.com/.exec(trimmed)?.[0];
|
|
5768
|
+
if (found !== void 0)
|
|
5769
|
+
beginProbe(found);
|
|
5770
|
+
return;
|
|
5771
|
+
}
|
|
5772
|
+
if (stream !== "stdout")
|
|
5773
|
+
return;
|
|
5774
|
+
try {
|
|
5775
|
+
const event = JSON.parse(trimmed);
|
|
5776
|
+
const candidate = typeof event.url === "string" && event.url.startsWith("https://") ? event.url : null;
|
|
5777
|
+
if (candidate !== null)
|
|
5778
|
+
beginProbe(candidate);
|
|
5779
|
+
} catch {
|
|
5780
|
+
}
|
|
5781
|
+
};
|
|
5782
|
+
const read = (stream, chunk) => {
|
|
5783
|
+
const combined = partial[stream] + (Buffer.isBuffer(chunk) ? chunk.toString() : chunk);
|
|
5784
|
+
const lines2 = combined.split(/\r?\n/);
|
|
5785
|
+
partial[stream] = lines2.pop() ?? "";
|
|
5786
|
+
for (const line of lines2)
|
|
5787
|
+
inspect(stream, line);
|
|
5788
|
+
if (partial[stream] !== "")
|
|
5789
|
+
inspect(stream, partial[stream]);
|
|
5790
|
+
};
|
|
5791
|
+
child.stdout?.on("data", (chunk) => read("stdout", chunk));
|
|
5792
|
+
child.stderr?.on("data", (chunk) => read("stderr", chunk));
|
|
5793
|
+
const onExit = () => {
|
|
5794
|
+
if (exited)
|
|
5795
|
+
return;
|
|
5796
|
+
exited = true;
|
|
5797
|
+
clearTimers();
|
|
5798
|
+
settleStop?.();
|
|
5799
|
+
settleStop = null;
|
|
5800
|
+
if (stopping)
|
|
5801
|
+
return;
|
|
5802
|
+
if (state.status === "ready") {
|
|
5803
|
+
terminal = true;
|
|
5804
|
+
report({
|
|
5805
|
+
status: "failed",
|
|
5806
|
+
provider: options.provider,
|
|
5807
|
+
url: state.url,
|
|
5808
|
+
reason: "The tunnel process exited."
|
|
5809
|
+
});
|
|
5810
|
+
return;
|
|
5811
|
+
}
|
|
5812
|
+
fail(withOutput(`${options.provider} exited before the tunnel came up.`, lastLine));
|
|
5813
|
+
};
|
|
5814
|
+
child.once("error", onExit);
|
|
5815
|
+
child.once("exit", onExit);
|
|
5816
|
+
urlTimer = later(() => fail(withOutput(`${options.provider} did not report a URL within ${duration(urlDeadlineMs)}.`, lastLine)), urlDeadlineMs);
|
|
5817
|
+
return {
|
|
5818
|
+
settle() {
|
|
5819
|
+
if (terminal || stopping || url === null)
|
|
5820
|
+
return;
|
|
5821
|
+
terminal = true;
|
|
5822
|
+
clearTimers();
|
|
5823
|
+
report({ status: "ready", provider: options.provider, url });
|
|
5824
|
+
},
|
|
5825
|
+
stop() {
|
|
5826
|
+
if (stopPromise !== null)
|
|
5827
|
+
return stopPromise;
|
|
5828
|
+
stopping = true;
|
|
5829
|
+
clearTimers();
|
|
5830
|
+
if (exited)
|
|
5831
|
+
return Promise.resolve();
|
|
5832
|
+
stopPromise = new Promise((resolve5) => {
|
|
5833
|
+
let settled = false;
|
|
5834
|
+
const done = () => {
|
|
5835
|
+
if (settled)
|
|
5836
|
+
return;
|
|
5837
|
+
settled = true;
|
|
5838
|
+
clearTimers();
|
|
5839
|
+
settleStop = null;
|
|
5840
|
+
resolve5();
|
|
5841
|
+
};
|
|
5842
|
+
settleStop = done;
|
|
5843
|
+
try {
|
|
5844
|
+
child.kill("SIGTERM");
|
|
5845
|
+
} catch {
|
|
5846
|
+
done();
|
|
5847
|
+
return;
|
|
5848
|
+
}
|
|
5849
|
+
later(() => {
|
|
5850
|
+
if (exited)
|
|
5851
|
+
return done();
|
|
5852
|
+
try {
|
|
5853
|
+
child.kill("SIGKILL");
|
|
5854
|
+
} catch {
|
|
5855
|
+
}
|
|
5856
|
+
}, STOP_GRACE_MS);
|
|
5857
|
+
later(done, STOP_LIMIT_MS);
|
|
5858
|
+
});
|
|
5859
|
+
return stopPromise;
|
|
5860
|
+
}
|
|
5861
|
+
};
|
|
5862
|
+
}
|
|
5863
|
+
|
|
5864
|
+
// ../server/dist/share.js
|
|
5865
|
+
var MAX_REFUSED = 40;
|
|
5866
|
+
function routeAllowed(routes, url) {
|
|
5867
|
+
const [rawPath = "/"] = url.split("?", 2);
|
|
5868
|
+
const path = canonical(rawPath);
|
|
5869
|
+
return routes.some((route) => {
|
|
5870
|
+
const prefix = route.endsWith("/") && route !== "/";
|
|
5871
|
+
return prefix ? path.startsWith(route) || `${path}/` === route : path === route || path === `${route}/`;
|
|
5872
|
+
});
|
|
5873
|
+
}
|
|
5874
|
+
var OWN_PREFIX = "/leglas";
|
|
5875
|
+
var ENTRY_PREFIX = `${OWN_PREFIX}/s/`;
|
|
5876
|
+
var DEFAULT_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
5877
|
+
var MAX_GRANTS = 16;
|
|
5878
|
+
var MAX_TOMBSTONES = 32;
|
|
5879
|
+
var VIEWER_CONCURRENCY = 12;
|
|
5880
|
+
var VIEWER_QUEUE = 128;
|
|
5881
|
+
var VIEWER_DEADLINE_MS = 3e4;
|
|
5882
|
+
var DEV_CONTROL_ROUTES = [
|
|
5883
|
+
/** read, Vite 8.2.2: opens `?file=` in the machine's editor. Rsbuild 2.2.3 too. */
|
|
5884
|
+
"/__open-in-editor",
|
|
5885
|
+
/** read, react-dev-utils 12.0.1: the same, older name. */
|
|
5886
|
+
"/__open-stack-frame-in-editor",
|
|
5887
|
+
/** read, react-dev-utils 12.0.1: serves a module's source through the overlay. */
|
|
5888
|
+
"/__get-internal-source",
|
|
5889
|
+
/** read, vite-plugin-inspect 12.0.2: the module graph and every transformed source. */
|
|
5890
|
+
"/__inspect",
|
|
5891
|
+
/** read, vite-plugin-vue-devtools 8.2.1: its whole interface and RPC surface. */
|
|
5892
|
+
"/__devtools__",
|
|
5893
|
+
/** read, browser-sync 3.0.4: its client surface and server metadata. */
|
|
5894
|
+
"/__browser_sync__",
|
|
5895
|
+
/**
|
|
5896
|
+
* read, webpack-dev-server 6.0.0, which mounts its own surface here: the
|
|
5897
|
+
* file listing, `/webpack-dev-server/invalidate`, which forces a rebuild,
|
|
5898
|
+
* and `/webpack-dev-server/open-editor`, which calls the same launch-editor
|
|
5899
|
+
* package Vite does. The subtree match below takes all three.
|
|
5900
|
+
*/
|
|
5901
|
+
"/webpack-dev-server",
|
|
5902
|
+
/** reported: Rails Web Console, an interactive server-side REPL. */
|
|
5903
|
+
"/__web_console",
|
|
5904
|
+
/** reported: the Better Errors gem, likewise. */
|
|
5905
|
+
"/__better_errors",
|
|
5906
|
+
/** reported: Laravel Ignition, whose solutions endpoint runs code. */
|
|
5907
|
+
"/_ignition",
|
|
5908
|
+
/** reported: Symfony's profiler, which serves traces, config and source. */
|
|
5909
|
+
"/_profiler",
|
|
5910
|
+
/** reported: Symfony's web debug toolbar. */
|
|
5911
|
+
"/_wdt",
|
|
5912
|
+
/** reported: Django Debug Toolbar, which serves settings, SQL and templates. */
|
|
5913
|
+
"/__debug__",
|
|
5914
|
+
/** reported: Go's pprof, where some GETs start expensive profiling. */
|
|
5915
|
+
"/debug/pprof",
|
|
5916
|
+
/** reported: Spring Boot Actuator, which can serve env, beans and heap dumps. */
|
|
5917
|
+
"/actuator",
|
|
5918
|
+
/** reported: Gatsby's development GraphQL surface, schema and content. */
|
|
5919
|
+
"/___graphql"
|
|
5920
|
+
];
|
|
5921
|
+
var DEV_CONTROL_PREFIXES = [
|
|
5922
|
+
/** read, Next 16.3.1. Its app assets sit at `/_next/` and stay allowed. */
|
|
5923
|
+
"/__nextjs_",
|
|
5924
|
+
/**
|
|
5925
|
+
* read, Nuxt DevTools 4.0.0-alpha.16. Nuxt's own bundle is at `/_nuxt/`,
|
|
5926
|
+
* one underscore and a different prefix, so the app is untouched.
|
|
5927
|
+
*/
|
|
5928
|
+
"/__nuxt_devtools__",
|
|
5929
|
+
/**
|
|
5930
|
+
* read, Parcel 2.16.4: `__parcel_launch_editor` reads a `file` parameter
|
|
5931
|
+
* and calls the same launch-editor code Vite does. Beside it sit
|
|
5932
|
+
* `__parcel_source_map`, `__parcel_source_root` and `__parcel_code_frame`,
|
|
5933
|
+
* which serve source, and the HMR and health routes, which a viewer has no
|
|
5934
|
+
* use for: their live-reload socket is already refused.
|
|
5935
|
+
*/
|
|
5936
|
+
"/__parcel_"
|
|
5937
|
+
];
|
|
5938
|
+
var DEV_CONTROL_QUERY_KEYS = ["__debugger__"];
|
|
5939
|
+
function canonical(path) {
|
|
5940
|
+
let form = path;
|
|
5941
|
+
try {
|
|
5942
|
+
form = decodeURIComponent(path);
|
|
5943
|
+
} catch {
|
|
5944
|
+
}
|
|
5945
|
+
return posix.normalize(form.replaceAll("\\", "/").replace(/\/{2,}/g, "/"));
|
|
5946
|
+
}
|
|
5947
|
+
function spellings(path) {
|
|
5948
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5949
|
+
const add = (value) => {
|
|
5950
|
+
seen.add(value);
|
|
5951
|
+
seen.add(value.toLowerCase());
|
|
5952
|
+
};
|
|
5953
|
+
add(path);
|
|
5954
|
+
const forms = [path];
|
|
5955
|
+
try {
|
|
5956
|
+
forms.push(decodeURIComponent(path));
|
|
5957
|
+
} catch {
|
|
5958
|
+
}
|
|
5959
|
+
for (const value of forms) {
|
|
5960
|
+
add(value);
|
|
5961
|
+
for (const slashed of [value, value.replaceAll("\\", "/")]) {
|
|
5962
|
+
const collapsed = slashed.replace(/\/{2,}/g, "/");
|
|
5963
|
+
add(collapsed);
|
|
5964
|
+
add(posix.normalize(collapsed));
|
|
5965
|
+
}
|
|
5966
|
+
}
|
|
5967
|
+
return [...seen];
|
|
5968
|
+
}
|
|
5969
|
+
function isDevControlRequest(url) {
|
|
5970
|
+
const [rawPath = "/", query] = url.split("?", 2);
|
|
5971
|
+
for (const path of spellings(rawPath)) {
|
|
5972
|
+
if (DEV_CONTROL_PREFIXES.some((prefix) => path.startsWith(prefix)))
|
|
5973
|
+
return true;
|
|
5974
|
+
if (DEV_CONTROL_ROUTES.some((route) => path === route || path.startsWith(`${route}/`))) {
|
|
5975
|
+
return true;
|
|
5976
|
+
}
|
|
5977
|
+
}
|
|
5978
|
+
if (query === void 0)
|
|
5979
|
+
return false;
|
|
5980
|
+
const keys = new URLSearchParams(query);
|
|
5981
|
+
return DEV_CONTROL_QUERY_KEYS.some((key) => keys.has(key) || keys.has(key.toUpperCase()));
|
|
5982
|
+
}
|
|
5983
|
+
function isHiddenPath(path) {
|
|
5984
|
+
return spellings(path).some((form) => {
|
|
5985
|
+
const segments = form.split("/");
|
|
5986
|
+
const modules = segments.indexOf("node_modules");
|
|
5987
|
+
return segments.some((segment, at) => {
|
|
5988
|
+
if (!segment.startsWith(".") || segment === "." || segment === "..")
|
|
5989
|
+
return false;
|
|
5990
|
+
const last = at === segments.length - 1;
|
|
5991
|
+
return last || !(modules >= 0 && at > modules);
|
|
5992
|
+
});
|
|
5993
|
+
});
|
|
5994
|
+
}
|
|
5995
|
+
var FILES_PREFIX_PATH = "/leglas/files/";
|
|
5996
|
+
var DETECT_TTL_MS = 1e4;
|
|
5997
|
+
function isRecord3(value) {
|
|
5998
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5999
|
+
}
|
|
6000
|
+
function stringArray(value) {
|
|
6001
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
6002
|
+
}
|
|
6003
|
+
function stringRecord(value) {
|
|
6004
|
+
return isRecord3(value) && Object.values(value).every((entry) => typeof entry === "string");
|
|
6005
|
+
}
|
|
6006
|
+
function layoutFrom(value) {
|
|
6007
|
+
if (!isRecord3(value))
|
|
6008
|
+
return null;
|
|
6009
|
+
if (!stringArray(value.order) || !stringRecord(value.renames) || !stringArray(value.collapsedFamilies) || value.compare !== null && typeof value.compare !== "string" || value.viewport !== null && (typeof value.viewport !== "number" || !Number.isFinite(value.viewport))) {
|
|
6010
|
+
return null;
|
|
6011
|
+
}
|
|
6012
|
+
return {
|
|
6013
|
+
order: [...value.order],
|
|
6014
|
+
renames: { ...value.renames },
|
|
6015
|
+
collapsedFamilies: [...value.collapsedFamilies],
|
|
6016
|
+
compare: value.compare,
|
|
6017
|
+
viewport: value.viewport
|
|
6018
|
+
};
|
|
6019
|
+
}
|
|
6020
|
+
function manifestFrom(value, previews) {
|
|
6021
|
+
if (!isRecord3(value)) {
|
|
6022
|
+
return { ok: false, error: "Share details must be a JSON object." };
|
|
6023
|
+
}
|
|
6024
|
+
const scope2 = value.scope;
|
|
6025
|
+
const layout = layoutFrom(value.layout);
|
|
6026
|
+
if (scope2 !== "direction" && scope2 !== "compare" && scope2 !== "rail" || !stringArray(value.titles) || layout === null) {
|
|
6027
|
+
return { ok: false, error: "Share details need a scope, directions and a complete layout." };
|
|
6028
|
+
}
|
|
6029
|
+
const titles = [...value.titles];
|
|
6030
|
+
if (titles.length === 0) {
|
|
6031
|
+
return { ok: false, error: "Choose at least one direction to share." };
|
|
6032
|
+
}
|
|
6033
|
+
const byTitle = new Map(previews.map((preview) => [preview.title, preview]));
|
|
6034
|
+
const unknown = [...new Set(titles.filter((title) => !byTitle.has(title)))];
|
|
6035
|
+
if (unknown.length > 0) {
|
|
6036
|
+
return {
|
|
6037
|
+
ok: false,
|
|
6038
|
+
error: `Directions are not available to share: ${unknown.join(", ")}.`
|
|
6039
|
+
};
|
|
6040
|
+
}
|
|
6041
|
+
const branches = [
|
|
6042
|
+
...new Set(titles.filter((title) => byTitle.get(title)?.branch !== void 0))
|
|
6043
|
+
];
|
|
6044
|
+
if (branches.length > 0) {
|
|
6045
|
+
return {
|
|
6046
|
+
ok: false,
|
|
6047
|
+
error: `Branch directions can't be shared yet: ${branches.join(", ")}.`
|
|
6048
|
+
};
|
|
6049
|
+
}
|
|
6050
|
+
if (scope2 === "direction" && titles.length > 1) {
|
|
6051
|
+
return { ok: false, error: "A direction share can contain only one direction." };
|
|
6052
|
+
}
|
|
6053
|
+
if (scope2 === "compare" && (titles.length !== 2 || new Set(titles).size !== 2 || layout.compare === null || !titles.includes(layout.compare))) {
|
|
6054
|
+
return {
|
|
6055
|
+
ok: false,
|
|
6056
|
+
error: "A comparison share needs exactly two directions and one of them on the right."
|
|
6057
|
+
};
|
|
6058
|
+
}
|
|
6059
|
+
if (scope2 !== "compare" && layout.compare !== null) {
|
|
6060
|
+
return { ok: false, error: "Only a comparison share can name a right pane." };
|
|
6061
|
+
}
|
|
6062
|
+
const reach = value.reach === "listed" ? "listed" : "open";
|
|
6063
|
+
if (value.reach !== void 0 && value.reach !== "open" && value.reach !== "listed") {
|
|
6064
|
+
return { ok: false, error: "Reach is either open or listed." };
|
|
6065
|
+
}
|
|
6066
|
+
if (value.routes !== void 0 && !stringArray(value.routes)) {
|
|
6067
|
+
return { ok: false, error: "The route list must be an array of paths." };
|
|
6068
|
+
}
|
|
6069
|
+
const routes = [...new Set((value.routes ?? []).map((route) => route.split("?", 1)[0] ?? ""))].filter((route) => route !== "").slice(0, 400);
|
|
6070
|
+
if (routes.some((route) => !route.startsWith("/"))) {
|
|
6071
|
+
return { ok: false, error: "Every route must be a path beginning with a slash." };
|
|
6072
|
+
}
|
|
6073
|
+
const own = titles.flatMap((title) => {
|
|
6074
|
+
const url = byTitle.get(title)?.url;
|
|
6075
|
+
return url === void 0 ? [] : [url.split("?", 1)[0] ?? ""];
|
|
6076
|
+
});
|
|
6077
|
+
return {
|
|
6078
|
+
ok: true,
|
|
6079
|
+
manifest: { scope: scope2, titles, layout, reach, routes: [.../* @__PURE__ */ new Set([...routes, ...own])] }
|
|
6080
|
+
};
|
|
6081
|
+
}
|
|
6082
|
+
function matchOne(candidate, grants) {
|
|
6083
|
+
const received = Buffer.from(candidate, "utf8");
|
|
6084
|
+
let found = null;
|
|
6085
|
+
for (const grant of grants) {
|
|
6086
|
+
const expected = Buffer.from(grant.token, "utf8");
|
|
6087
|
+
if (expected.length !== received.length)
|
|
6088
|
+
continue;
|
|
6089
|
+
if (timingSafeEqual(expected, received))
|
|
6090
|
+
found = grant;
|
|
6091
|
+
}
|
|
6092
|
+
return found;
|
|
6093
|
+
}
|
|
6094
|
+
function grantFor(share, candidate) {
|
|
6095
|
+
return matchOne(candidate, share.grants.values());
|
|
6096
|
+
}
|
|
6097
|
+
function endedGrantFor(share, candidate) {
|
|
6098
|
+
return matchOne(candidate, share.tombstones);
|
|
6099
|
+
}
|
|
6100
|
+
function expired(grant, now, nowMono) {
|
|
6101
|
+
return now >= grant.expiresAt || nowMono >= grant.expiresAtMono;
|
|
6102
|
+
}
|
|
6103
|
+
function cookieToken(req) {
|
|
6104
|
+
const raw = req.headers.cookie;
|
|
6105
|
+
const cookies = (Array.isArray(raw) ? raw.join(";") : raw ?? "").split(";");
|
|
6106
|
+
for (const cookie of cookies) {
|
|
6107
|
+
const separator = cookie.indexOf("=");
|
|
6108
|
+
if (separator === -1)
|
|
6109
|
+
continue;
|
|
6110
|
+
if (cookie.slice(0, separator).trim() !== SHARE_COOKIE)
|
|
6111
|
+
continue;
|
|
6112
|
+
return cookie.slice(separator + 1).trim();
|
|
6113
|
+
}
|
|
6114
|
+
return null;
|
|
6115
|
+
}
|
|
6116
|
+
function forwardedProto(req) {
|
|
6117
|
+
const forwarded = req.headers["x-forwarded-proto"];
|
|
6118
|
+
const first = Array.isArray(forwarded) ? forwarded[0] : forwarded;
|
|
6119
|
+
return first?.split(",", 1)[0]?.trim() || "http";
|
|
6120
|
+
}
|
|
6121
|
+
function publicOrigin(req) {
|
|
6122
|
+
return `${forwardedProto(req)}://${req.headers.host ?? "127.0.0.1"}`;
|
|
6123
|
+
}
|
|
6124
|
+
function throughTunnel(req) {
|
|
6125
|
+
return req.headers["x-forwarded-for"] !== void 0 || req.headers["cf-connecting-ip"] !== void 0 || req.headers["x-forwarded-proto"] !== void 0;
|
|
6126
|
+
}
|
|
6127
|
+
function cloneLayout(layout) {
|
|
6128
|
+
return {
|
|
6129
|
+
...layout,
|
|
6130
|
+
order: [...layout.order],
|
|
6131
|
+
renames: { ...layout.renames },
|
|
6132
|
+
collapsedFamilies: [...layout.collapsedFamilies]
|
|
6133
|
+
};
|
|
6134
|
+
}
|
|
6135
|
+
function sendJson(res, status, body) {
|
|
6136
|
+
res.writeHead(status, {
|
|
6137
|
+
"content-type": "application/json; charset=utf-8",
|
|
6138
|
+
"cache-control": "no-store"
|
|
6139
|
+
});
|
|
6140
|
+
res.end(JSON.stringify(body));
|
|
6141
|
+
}
|
|
6142
|
+
var REFUSALS = {
|
|
6143
|
+
inactive: {
|
|
6144
|
+
status: 403,
|
|
6145
|
+
sentence: "This link isn't active.",
|
|
6146
|
+
title: "This Leglas link isn't active"
|
|
6147
|
+
},
|
|
6148
|
+
expiry: {
|
|
6149
|
+
status: 410,
|
|
6150
|
+
sentence: "This link expired. The person sharing it can send a new one.",
|
|
6151
|
+
title: "This Leglas link expired"
|
|
6152
|
+
},
|
|
6153
|
+
revoke: {
|
|
6154
|
+
status: 410,
|
|
6155
|
+
sentence: "This link was turned off.",
|
|
6156
|
+
title: "This Leglas link was turned off"
|
|
6157
|
+
}
|
|
6158
|
+
};
|
|
6159
|
+
function refuse(req, res, cause = "inactive") {
|
|
6160
|
+
const refusal = REFUSALS[cause];
|
|
6161
|
+
const accept = req.headers.accept;
|
|
6162
|
+
const html = (Array.isArray(accept) ? accept.join(",") : accept ?? "").includes("text/html");
|
|
6163
|
+
if (!html) {
|
|
6164
|
+
return sendJson(res, refusal.status, { ok: false, error: refusal.sentence });
|
|
6165
|
+
}
|
|
6166
|
+
res.writeHead(refusal.status, {
|
|
6167
|
+
"content-type": "text/html; charset=utf-8",
|
|
6168
|
+
"cache-control": "no-store"
|
|
6169
|
+
});
|
|
6170
|
+
res.end(`<!doctype html>
|
|
6171
|
+
<meta charset="utf-8">
|
|
6172
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6173
|
+
<title>${refusal.title}</title>
|
|
6174
|
+
<body style="margin:0;background:#1C1C20;color:#f5f4f1;font:16px/1.5 ui-sans-serif,system-ui;display:grid;min-height:100vh;place-items:center">
|
|
6175
|
+
<main style="max-width:34rem;padding:2rem"><h1 style="font-size:1.25rem">${refusal.title}</h1>
|
|
6176
|
+
<p>${refusal.sentence}</p></main>
|
|
6177
|
+
</body>`);
|
|
6178
|
+
}
|
|
6179
|
+
function bind(server) {
|
|
6180
|
+
return new Promise((resolve5, reject) => {
|
|
6181
|
+
const onError = (error) => {
|
|
6182
|
+
server.removeListener("listening", onListening);
|
|
6183
|
+
reject(error);
|
|
6184
|
+
};
|
|
6185
|
+
const onListening = () => {
|
|
6186
|
+
server.removeListener("error", onError);
|
|
6187
|
+
const address = server.address();
|
|
6188
|
+
resolve5(typeof address === "object" && address !== null ? address.port : 0);
|
|
6189
|
+
};
|
|
6190
|
+
server.once("error", onError);
|
|
6191
|
+
server.once("listening", onListening);
|
|
6192
|
+
server.listen(0, "127.0.0.1");
|
|
6193
|
+
});
|
|
6194
|
+
}
|
|
6195
|
+
function closeListener(share) {
|
|
6196
|
+
return new Promise((resolve5) => {
|
|
6197
|
+
if (share.expiryTimer !== null) {
|
|
6198
|
+
clearTimeout(share.expiryTimer);
|
|
6199
|
+
share.expiryTimer = null;
|
|
5493
6200
|
}
|
|
6201
|
+
for (const socket of share.sockets)
|
|
6202
|
+
socket.destroy();
|
|
6203
|
+
share.sockets.clear();
|
|
6204
|
+
share.server.closeAllConnections();
|
|
6205
|
+
share.server.close(() => resolve5());
|
|
6206
|
+
});
|
|
6207
|
+
}
|
|
6208
|
+
function createShareManager(options) {
|
|
6209
|
+
const detect = options.detectTunnels ?? detectTunnels;
|
|
6210
|
+
const runTunnel = options.startTunnel ?? startTunnel;
|
|
6211
|
+
const now = options.now ?? Date.now;
|
|
6212
|
+
const nowMono = options.nowMono ?? process.hrtime.bigint;
|
|
6213
|
+
const deadlineMs = options.viewerDeadlineMs ?? VIEWER_DEADLINE_MS;
|
|
6214
|
+
let detected = null;
|
|
6215
|
+
let active = null;
|
|
6216
|
+
let creating = false;
|
|
6217
|
+
let stops = 0;
|
|
6218
|
+
let closed = false;
|
|
6219
|
+
let stopPromise = null;
|
|
6220
|
+
let detectedAt = 0;
|
|
6221
|
+
const tunnels = () => {
|
|
6222
|
+
if (detected === null || Date.now() - detectedAt > DETECT_TTL_MS) {
|
|
6223
|
+
detectedAt = Date.now();
|
|
6224
|
+
detected = detect().catch(() => []);
|
|
6225
|
+
}
|
|
6226
|
+
return detected;
|
|
6227
|
+
};
|
|
6228
|
+
const status = () => {
|
|
6229
|
+
const share = active;
|
|
6230
|
+
if (share === null)
|
|
6231
|
+
return null;
|
|
6232
|
+
const tunnelUrl = "url" in share.tunnel ? share.tunnel.url : void 0;
|
|
6233
|
+
const origin = tunnelUrl === void 0 ? null : tunnelUrl.replace(/\/$/, "");
|
|
6234
|
+
return {
|
|
6235
|
+
id: share.id,
|
|
6236
|
+
scope: share.scope,
|
|
6237
|
+
titles: [...share.titles],
|
|
6238
|
+
layout: cloneLayout(share.layout),
|
|
6239
|
+
sharePort: share.port,
|
|
6240
|
+
grants: [...share.grants.values()].toSorted((a, b) => a.createdAt - b.createdAt).map((grant) => {
|
|
6241
|
+
const entryPath = `${ENTRY_PREFIX}${grant.token}`;
|
|
6242
|
+
return {
|
|
6243
|
+
id: grant.id,
|
|
6244
|
+
name: grant.name,
|
|
6245
|
+
url: origin === null ? null : `${origin}${entryPath}`,
|
|
6246
|
+
localUrl: `http://127.0.0.1:${share.port}${entryPath}`,
|
|
6247
|
+
viewers: grant.viewers,
|
|
6248
|
+
createdAt: grant.createdAt,
|
|
6249
|
+
expiresAt: grant.expiresAt
|
|
6250
|
+
};
|
|
6251
|
+
}),
|
|
6252
|
+
reach: share.reach,
|
|
6253
|
+
routes: [...share.routes],
|
|
6254
|
+
refused: [...share.refused],
|
|
6255
|
+
tunnel: { ...share.tunnel },
|
|
6256
|
+
startedAt: share.startedAt
|
|
6257
|
+
};
|
|
6258
|
+
};
|
|
6259
|
+
const endGrant = (share, grant, why) => {
|
|
6260
|
+
if (!share.grants.delete(grant.id))
|
|
6261
|
+
return;
|
|
6262
|
+
grant.endedAt = now();
|
|
6263
|
+
grant.endedBy = why;
|
|
6264
|
+
grant.viewers = 0;
|
|
6265
|
+
share.tombstones.push(grant);
|
|
6266
|
+
while (share.tombstones.length > MAX_TOMBSTONES)
|
|
6267
|
+
share.tombstones.shift();
|
|
6268
|
+
for (const socket of share.grantSockets.get(grant.id) ?? [])
|
|
6269
|
+
socket.destroy();
|
|
6270
|
+
share.grantSockets.delete(grant.id);
|
|
6271
|
+
for (const held of share.grantRequests.get(grant.id) ?? []) {
|
|
6272
|
+
held.res.destroy();
|
|
6273
|
+
held.req.destroy();
|
|
6274
|
+
}
|
|
6275
|
+
share.grantRequests.delete(grant.id);
|
|
6276
|
+
for (const held of [...share.waiting.get(grant.id) ?? []]) {
|
|
6277
|
+
if (held.drop())
|
|
6278
|
+
refuse(held.req, held.res, why);
|
|
6279
|
+
}
|
|
6280
|
+
share.waiting.delete(grant.id);
|
|
6281
|
+
const turn = share.rota.indexOf(grant.id);
|
|
6282
|
+
if (turn >= 0)
|
|
6283
|
+
share.rota.splice(turn, 1);
|
|
6284
|
+
};
|
|
6285
|
+
const sweepExpiry = () => {
|
|
6286
|
+
const share = active;
|
|
6287
|
+
if (share === null)
|
|
6288
|
+
return;
|
|
6289
|
+
if (share.expiryTimer !== null) {
|
|
6290
|
+
clearTimeout(share.expiryTimer);
|
|
6291
|
+
share.expiryTimer = null;
|
|
6292
|
+
}
|
|
6293
|
+
const at = now();
|
|
6294
|
+
const mono = nowMono();
|
|
6295
|
+
let ended = false;
|
|
6296
|
+
for (const grant of [...share.grants.values()]) {
|
|
6297
|
+
if (!expired(grant, at, mono))
|
|
6298
|
+
continue;
|
|
6299
|
+
endGrant(share, grant, "expiry");
|
|
6300
|
+
ended = true;
|
|
6301
|
+
}
|
|
6302
|
+
const next = [...share.grants.values()].reduce((soonest, grant) => soonest === null ? grant.expiresAt : Math.min(soonest, grant.expiresAt), null);
|
|
6303
|
+
if (next !== null) {
|
|
6304
|
+
share.expiryTimer = setTimeout(sweepExpiry, Math.max(1, next - at));
|
|
6305
|
+
share.expiryTimer.unref?.();
|
|
6306
|
+
}
|
|
6307
|
+
if (ended)
|
|
6308
|
+
options.live.nudge("share");
|
|
6309
|
+
};
|
|
6310
|
+
const mintGrant = (share, name) => {
|
|
6311
|
+
let token = randomBytes4(24).toString("base64url");
|
|
6312
|
+
const taken = new Set([...share.grants.values(), ...share.tombstones].map((g) => g.token));
|
|
6313
|
+
while (taken.has(token))
|
|
6314
|
+
token = randomBytes4(24).toString("base64url");
|
|
6315
|
+
const at = now();
|
|
6316
|
+
const grant = {
|
|
6317
|
+
id: randomUUID(),
|
|
6318
|
+
name,
|
|
6319
|
+
token,
|
|
6320
|
+
createdAt: at,
|
|
6321
|
+
expiresAt: at + DEFAULT_TTL_MS,
|
|
6322
|
+
expiresAtMono: nowMono() + BigInt(DEFAULT_TTL_MS) * 1000000n,
|
|
6323
|
+
endedAt: null,
|
|
6324
|
+
endedBy: null,
|
|
6325
|
+
viewers: 0
|
|
6326
|
+
};
|
|
6327
|
+
share.grants.set(grant.id, grant);
|
|
6328
|
+
return grant;
|
|
6329
|
+
};
|
|
6330
|
+
const resolve5 = (share, candidate) => {
|
|
6331
|
+
const grant = grantFor(share, candidate);
|
|
6332
|
+
if (grant !== null) {
|
|
6333
|
+
if (!expired(grant, now(), nowMono()))
|
|
6334
|
+
return { grant };
|
|
6335
|
+
endGrant(share, grant, "expiry");
|
|
6336
|
+
options.live.nudge("share");
|
|
6337
|
+
return { refusal: "expiry" };
|
|
6338
|
+
}
|
|
6339
|
+
const ended = endedGrantFor(share, candidate);
|
|
6340
|
+
if (ended !== null)
|
|
6341
|
+
return { refusal: ended.endedBy === "revoke" ? "revoke" : "expiry" };
|
|
6342
|
+
return { refusal: "inactive" };
|
|
6343
|
+
};
|
|
6344
|
+
const pump = (share) => {
|
|
6345
|
+
while (share.running < VIEWER_CONCURRENCY && share.rota.length > 0) {
|
|
6346
|
+
const grantId = share.rota[0];
|
|
6347
|
+
if (grantId === void 0)
|
|
6348
|
+
return;
|
|
6349
|
+
const next = share.waiting.get(grantId)?.[0];
|
|
6350
|
+
if (next === void 0) {
|
|
6351
|
+
share.rota.shift();
|
|
6352
|
+
share.waiting.delete(grantId);
|
|
6353
|
+
continue;
|
|
6354
|
+
}
|
|
6355
|
+
next.drop();
|
|
6356
|
+
const turn = share.rota.indexOf(grantId);
|
|
6357
|
+
if (turn >= 0) {
|
|
6358
|
+
share.rota.splice(turn, 1);
|
|
6359
|
+
share.rota.push(grantId);
|
|
6360
|
+
}
|
|
6361
|
+
if (Date.now() >= next.spentAt) {
|
|
6362
|
+
next.shed();
|
|
6363
|
+
continue;
|
|
6364
|
+
}
|
|
6365
|
+
if (!share.grants.has(grantId)) {
|
|
6366
|
+
refuse(next.req, next.res, "revoke");
|
|
6367
|
+
continue;
|
|
6368
|
+
}
|
|
6369
|
+
next.start();
|
|
6370
|
+
}
|
|
6371
|
+
};
|
|
6372
|
+
const admit = (share, grant, req, res, run4) => {
|
|
6373
|
+
const queue = share.waiting.get(grant.id) ?? [];
|
|
6374
|
+
if (queue.length >= VIEWER_QUEUE) {
|
|
6375
|
+
return sendJson(res, 503, { ok: false, error: "Too much at once. Try again." });
|
|
6376
|
+
}
|
|
6377
|
+
let queued = true;
|
|
6378
|
+
let running = false;
|
|
6379
|
+
let over = false;
|
|
6380
|
+
const spentAt = Date.now() + deadlineMs;
|
|
6381
|
+
const shed = () => {
|
|
6382
|
+
if (over)
|
|
6383
|
+
return;
|
|
6384
|
+
over = true;
|
|
6385
|
+
sendJson(res, 503, { ok: false, error: "The dev server is busy. Try again." });
|
|
6386
|
+
};
|
|
6387
|
+
const deadline = setTimeout(() => {
|
|
6388
|
+
if (over)
|
|
6389
|
+
return;
|
|
6390
|
+
const waited = held.drop();
|
|
6391
|
+
if (running) {
|
|
6392
|
+
over = true;
|
|
6393
|
+
res.destroy();
|
|
6394
|
+
release();
|
|
6395
|
+
return;
|
|
6396
|
+
}
|
|
6397
|
+
if (waited)
|
|
6398
|
+
shed();
|
|
6399
|
+
}, deadlineMs);
|
|
6400
|
+
deadline.unref?.();
|
|
6401
|
+
const release = () => {
|
|
6402
|
+
if (!running)
|
|
6403
|
+
return;
|
|
6404
|
+
running = false;
|
|
6405
|
+
over = true;
|
|
6406
|
+
clearTimeout(deadline);
|
|
6407
|
+
share.running = Math.max(0, share.running - 1);
|
|
6408
|
+
pump(share);
|
|
6409
|
+
};
|
|
6410
|
+
const start = () => {
|
|
6411
|
+
running = true;
|
|
6412
|
+
share.running += 1;
|
|
6413
|
+
const writeHead = res.writeHead.bind(res);
|
|
6414
|
+
res.writeHead = ((...args) => {
|
|
6415
|
+
release();
|
|
6416
|
+
return writeHead(...args);
|
|
6417
|
+
});
|
|
6418
|
+
res.once("finish", release);
|
|
6419
|
+
res.once("close", release);
|
|
6420
|
+
run4();
|
|
6421
|
+
};
|
|
6422
|
+
const held = {
|
|
6423
|
+
req,
|
|
6424
|
+
res,
|
|
6425
|
+
grantId: grant.id,
|
|
6426
|
+
spentAt,
|
|
6427
|
+
start,
|
|
6428
|
+
shed,
|
|
6429
|
+
drop: () => {
|
|
6430
|
+
if (!queued)
|
|
6431
|
+
return false;
|
|
6432
|
+
queued = false;
|
|
6433
|
+
const rest = share.waiting.get(grant.id);
|
|
6434
|
+
const at = rest?.indexOf(held) ?? -1;
|
|
6435
|
+
if (rest !== void 0 && at >= 0)
|
|
6436
|
+
rest.splice(at, 1);
|
|
6437
|
+
if (rest !== void 0 && rest.length === 0) {
|
|
6438
|
+
share.waiting.delete(grant.id);
|
|
6439
|
+
const turn = share.rota.indexOf(grant.id);
|
|
6440
|
+
if (turn >= 0)
|
|
6441
|
+
share.rota.splice(turn, 1);
|
|
6442
|
+
}
|
|
6443
|
+
return true;
|
|
6444
|
+
}
|
|
6445
|
+
};
|
|
6446
|
+
queue.push(held);
|
|
6447
|
+
share.waiting.set(grant.id, queue);
|
|
6448
|
+
if (!share.rota.includes(grant.id))
|
|
6449
|
+
share.rota.push(grant.id);
|
|
6450
|
+
pump(share);
|
|
6451
|
+
if (!queued)
|
|
6452
|
+
return;
|
|
6453
|
+
res.once("close", () => {
|
|
6454
|
+
held.drop();
|
|
6455
|
+
if (running)
|
|
6456
|
+
return;
|
|
6457
|
+
over = true;
|
|
6458
|
+
clearTimeout(deadline);
|
|
6459
|
+
});
|
|
6460
|
+
};
|
|
6461
|
+
const request = (req, res) => {
|
|
6462
|
+
const share = active;
|
|
6463
|
+
if (share === null)
|
|
6464
|
+
return refuse(req, res);
|
|
6465
|
+
const path = (req.url ?? "/").split("?")[0] ?? "/";
|
|
6466
|
+
if (path.startsWith(ENTRY_PREFIX)) {
|
|
6467
|
+
const candidate = path.slice(ENTRY_PREFIX.length);
|
|
6468
|
+
if (req.method !== "GET" && req.method !== "HEAD" || candidate.includes("/")) {
|
|
6469
|
+
return refuse(req, res);
|
|
6470
|
+
}
|
|
6471
|
+
const found2 = resolve5(share, candidate);
|
|
6472
|
+
if ("refusal" in found2)
|
|
6473
|
+
return refuse(req, res, found2.refusal);
|
|
6474
|
+
const secure = forwardedProto(req) === "https" ? "; Secure" : "";
|
|
6475
|
+
res.writeHead(302, {
|
|
6476
|
+
location: "/leglas/",
|
|
6477
|
+
"set-cookie": `${SHARE_COOKIE}=${found2.grant.token}; Path=/; HttpOnly; SameSite=Lax${secure}`,
|
|
6478
|
+
"cache-control": "no-store"
|
|
6479
|
+
});
|
|
6480
|
+
res.end();
|
|
6481
|
+
return;
|
|
6482
|
+
}
|
|
6483
|
+
const cookie = cookieToken(req);
|
|
6484
|
+
if (cookie === null)
|
|
6485
|
+
return refuse(req, res);
|
|
6486
|
+
const found = resolve5(share, cookie);
|
|
6487
|
+
if ("refusal" in found)
|
|
6488
|
+
return refuse(req, res, found.refusal);
|
|
6489
|
+
const grant = found.grant;
|
|
6490
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
6491
|
+
return sendJson(res, 403, {
|
|
6492
|
+
ok: false,
|
|
6493
|
+
error: "Viewers can look, not change what runs."
|
|
6494
|
+
});
|
|
6495
|
+
}
|
|
6496
|
+
if (isDevControlRequest(req.url ?? "/")) {
|
|
6497
|
+
return sendJson(res, 403, { ok: false, error: "Not available to viewers." });
|
|
6498
|
+
}
|
|
6499
|
+
if (isHiddenPath(path)) {
|
|
6500
|
+
return sendJson(res, 403, { ok: false, error: "Not available to viewers." });
|
|
6501
|
+
}
|
|
6502
|
+
if (req.headers["sec-fetch-dest"] === "serviceworker") {
|
|
6503
|
+
return sendJson(res, 403, { ok: false, error: "Not available to viewers." });
|
|
6504
|
+
}
|
|
6505
|
+
const url = req.url ?? "/";
|
|
6506
|
+
const interfaceOwn = spellings(path).every((form) => form === OWN_PREFIX || form.startsWith(`${OWN_PREFIX}/`));
|
|
6507
|
+
if (share.reach === "listed" && !interfaceOwn && !routeAllowed(share.routes, url)) {
|
|
6508
|
+
const asked = canonical(url.split("?", 1)[0] ?? "/");
|
|
6509
|
+
if (!share.refused.includes(asked)) {
|
|
6510
|
+
share.refused.push(asked);
|
|
6511
|
+
while (share.refused.length > MAX_REFUSED)
|
|
6512
|
+
share.refused.shift();
|
|
6513
|
+
options.live.nudge("share");
|
|
6514
|
+
}
|
|
6515
|
+
return sendJson(res, 403, { ok: false, error: "Not shared." });
|
|
6516
|
+
}
|
|
6517
|
+
const run4 = () => {
|
|
6518
|
+
const held = { req, res };
|
|
6519
|
+
const inFlight = share.grantRequests.get(grant.id) ?? /* @__PURE__ */ new Set();
|
|
6520
|
+
inFlight.add(held);
|
|
6521
|
+
share.grantRequests.set(grant.id, inFlight);
|
|
6522
|
+
let released = false;
|
|
6523
|
+
const release = () => {
|
|
6524
|
+
if (released)
|
|
6525
|
+
return;
|
|
6526
|
+
released = true;
|
|
6527
|
+
share.grantRequests.get(grant.id)?.delete(held);
|
|
6528
|
+
};
|
|
6529
|
+
res.once("finish", release);
|
|
6530
|
+
res.once("close", release);
|
|
6531
|
+
options.request(req, res, { publicOrigin: publicOrigin(req), grantId: grant.id });
|
|
6532
|
+
};
|
|
6533
|
+
if (interfaceOwn)
|
|
6534
|
+
return run4();
|
|
6535
|
+
admit(share, grant, req, res, run4);
|
|
6536
|
+
};
|
|
6537
|
+
const upgrade = (req, socket, head) => {
|
|
6538
|
+
const share = active;
|
|
6539
|
+
const cookie = cookieToken(req);
|
|
6540
|
+
if (share === null || cookie === null) {
|
|
6541
|
+
socket.destroy();
|
|
6542
|
+
return;
|
|
6543
|
+
}
|
|
6544
|
+
const found = resolve5(share, cookie);
|
|
6545
|
+
if ("refusal" in found) {
|
|
6546
|
+
socket.destroy();
|
|
6547
|
+
return;
|
|
6548
|
+
}
|
|
6549
|
+
const grant = found.grant;
|
|
6550
|
+
const path = (req.url ?? "/").split("?")[0] ?? "/";
|
|
6551
|
+
if (path !== LIVE_PATH) {
|
|
6552
|
+
socket.destroy();
|
|
6553
|
+
return;
|
|
6554
|
+
}
|
|
6555
|
+
if (throughTunnel(req))
|
|
6556
|
+
share.runningTunnel?.settle();
|
|
6557
|
+
if (!options.upgrade(req, socket, head))
|
|
6558
|
+
return;
|
|
6559
|
+
const held = share.grantSockets.get(grant.id) ?? /* @__PURE__ */ new Set();
|
|
6560
|
+
held.add(socket);
|
|
6561
|
+
share.grantSockets.set(grant.id, held);
|
|
6562
|
+
grant.viewers += 1;
|
|
6563
|
+
options.live.nudge("share");
|
|
6564
|
+
let gone = false;
|
|
6565
|
+
const letGo = () => {
|
|
6566
|
+
if (gone)
|
|
6567
|
+
return;
|
|
6568
|
+
gone = true;
|
|
6569
|
+
share.grantSockets.get(grant.id)?.delete(socket);
|
|
6570
|
+
grant.viewers = Math.max(0, grant.viewers - 1);
|
|
6571
|
+
options.live.nudge("share");
|
|
6572
|
+
};
|
|
6573
|
+
socket.once("close", letGo);
|
|
6574
|
+
socket.once("end", letGo);
|
|
6575
|
+
socket.once("error", letGo);
|
|
6576
|
+
};
|
|
6577
|
+
const create = async (input) => {
|
|
6578
|
+
if (closed)
|
|
6579
|
+
return { ok: false, status: 409, error: "Leglas is shutting down." };
|
|
6580
|
+
if (active !== null || creating) {
|
|
6581
|
+
return { ok: false, status: 409, error: "Stop the current share first." };
|
|
6582
|
+
}
|
|
6583
|
+
creating = true;
|
|
6584
|
+
const stopsAtStart = stops;
|
|
6585
|
+
try {
|
|
6586
|
+
const previews = await options.previews();
|
|
6587
|
+
const parsed = manifestFrom(input, previews);
|
|
6588
|
+
if (!parsed.ok)
|
|
6589
|
+
return { ok: false, status: 400, error: parsed.error };
|
|
6590
|
+
const providers = await tunnels();
|
|
6591
|
+
const requested = isRecord3(input) ? input.tunnel : void 0;
|
|
6592
|
+
if (requested !== void 0 && requested !== "none" && requested !== "cloudflared" && requested !== "ngrok") {
|
|
6593
|
+
return { ok: false, status: 400, error: "That tunnel provider is not supported." };
|
|
6594
|
+
}
|
|
6595
|
+
if (requested !== void 0 && requested !== "none" && !providers.includes(requested)) {
|
|
6596
|
+
return {
|
|
6597
|
+
ok: false,
|
|
6598
|
+
status: 400,
|
|
6599
|
+
error: `${requested} is not available on this machine.`
|
|
6600
|
+
};
|
|
6601
|
+
}
|
|
6602
|
+
const provider = requested ?? providers[0] ?? "none";
|
|
6603
|
+
if (closed)
|
|
6604
|
+
return { ok: false, status: 409, error: "Leglas is shutting down." };
|
|
6605
|
+
const server = http3.createServer(request);
|
|
6606
|
+
const sockets = /* @__PURE__ */ new Set();
|
|
6607
|
+
server.on("connection", (socket) => {
|
|
6608
|
+
sockets.add(socket);
|
|
6609
|
+
socket.once("close", () => sockets.delete(socket));
|
|
6610
|
+
});
|
|
6611
|
+
server.on("upgrade", upgrade);
|
|
6612
|
+
let port;
|
|
6613
|
+
try {
|
|
6614
|
+
port = await bind(server);
|
|
6615
|
+
} catch (error) {
|
|
6616
|
+
return {
|
|
6617
|
+
ok: false,
|
|
6618
|
+
status: 500,
|
|
6619
|
+
error: `Leglas could not open a listener for the share (${error instanceof Error ? error.message : String(error)}).`
|
|
6620
|
+
};
|
|
6621
|
+
}
|
|
6622
|
+
if (closed) {
|
|
6623
|
+
await new Promise((resolve6) => server.close(() => resolve6()));
|
|
6624
|
+
return { ok: false, status: 409, error: "Leglas is shutting down." };
|
|
6625
|
+
}
|
|
6626
|
+
if (stops !== stopsAtStart) {
|
|
6627
|
+
await new Promise((resolve6) => server.close(() => resolve6()));
|
|
6628
|
+
return { ok: false, status: 409, error: "Sharing was stopped while it was starting." };
|
|
6629
|
+
}
|
|
6630
|
+
const share = {
|
|
6631
|
+
...parsed.manifest,
|
|
6632
|
+
id: randomUUID(),
|
|
6633
|
+
grants: /* @__PURE__ */ new Map(),
|
|
6634
|
+
tombstones: [],
|
|
6635
|
+
port,
|
|
6636
|
+
startedAt: now(),
|
|
6637
|
+
tunnel: provider === "none" ? { status: "none" } : { status: "starting", provider },
|
|
6638
|
+
runningTunnel: null,
|
|
6639
|
+
tunnelGeneration: 0,
|
|
6640
|
+
server,
|
|
6641
|
+
sockets,
|
|
6642
|
+
grantSockets: /* @__PURE__ */ new Map(),
|
|
6643
|
+
grantRequests: /* @__PURE__ */ new Map(),
|
|
6644
|
+
launch: null,
|
|
6645
|
+
expiryTimer: null,
|
|
6646
|
+
refused: [],
|
|
6647
|
+
running: 0,
|
|
6648
|
+
waiting: /* @__PURE__ */ new Map(),
|
|
6649
|
+
rota: []
|
|
6650
|
+
};
|
|
6651
|
+
active = share;
|
|
6652
|
+
mintGrant(share, "");
|
|
6653
|
+
sweepExpiry();
|
|
6654
|
+
options.live.nudge("share");
|
|
6655
|
+
if (provider !== "none") {
|
|
6656
|
+
share.launch = setImmediate(() => {
|
|
6657
|
+
share.launch = null;
|
|
6658
|
+
if (active !== share)
|
|
6659
|
+
return;
|
|
6660
|
+
share.runningTunnel = runTunnel({
|
|
6661
|
+
provider,
|
|
6662
|
+
port,
|
|
6663
|
+
// Whichever link exists when the tunnel starts: the probe only
|
|
6664
|
+
// needs a path the listener answers, and a share always has one.
|
|
6665
|
+
entryPath: `${ENTRY_PREFIX}${[...share.grants.values()][0]?.token ?? ""}`,
|
|
6666
|
+
onState: (next) => {
|
|
6667
|
+
if (active !== share || JSON.stringify(share.tunnel) === JSON.stringify(next))
|
|
6668
|
+
return;
|
|
6669
|
+
share.tunnel = next;
|
|
6670
|
+
options.live.nudge("share");
|
|
6671
|
+
}
|
|
6672
|
+
});
|
|
6673
|
+
});
|
|
6674
|
+
share.launch.unref?.();
|
|
6675
|
+
}
|
|
6676
|
+
return { ok: true, share: status() };
|
|
6677
|
+
} finally {
|
|
6678
|
+
creating = false;
|
|
6679
|
+
}
|
|
6680
|
+
};
|
|
6681
|
+
const createGrant = (input) => {
|
|
6682
|
+
const share = active;
|
|
6683
|
+
if (share === null)
|
|
6684
|
+
return { ok: false, status: 404, error: "Nothing is being shared." };
|
|
6685
|
+
const name = isRecord3(input) && typeof input.name === "string" ? input.name.trim() : "";
|
|
6686
|
+
if (name.length > 60) {
|
|
6687
|
+
return { ok: false, status: 400, error: "That name is too long for a link." };
|
|
6688
|
+
}
|
|
6689
|
+
sweepExpiry();
|
|
6690
|
+
if (share.grants.size >= MAX_GRANTS) {
|
|
6691
|
+
return {
|
|
6692
|
+
ok: false,
|
|
6693
|
+
status: 409,
|
|
6694
|
+
error: `A share can hold ${MAX_GRANTS} links. Revoke one to make another.`
|
|
6695
|
+
};
|
|
6696
|
+
}
|
|
6697
|
+
mintGrant(share, name);
|
|
6698
|
+
sweepExpiry();
|
|
6699
|
+
options.live.nudge("share");
|
|
6700
|
+
return { ok: true, share: status() };
|
|
6701
|
+
};
|
|
6702
|
+
const revokeGrant = (input) => {
|
|
6703
|
+
const share = active;
|
|
6704
|
+
if (share === null)
|
|
6705
|
+
return { ok: false, status: 404, error: "Nothing is being shared." };
|
|
6706
|
+
const id = isRecord3(input) && typeof input.id === "string" ? input.id : "";
|
|
6707
|
+
const grant = share.grants.get(id);
|
|
6708
|
+
if (grant === void 0)
|
|
6709
|
+
return { ok: false, status: 404, error: "No such link." };
|
|
6710
|
+
endGrant(share, grant, "revoke");
|
|
6711
|
+
sweepExpiry();
|
|
6712
|
+
options.live.nudge("share");
|
|
6713
|
+
return { ok: true, share: status() };
|
|
6714
|
+
};
|
|
6715
|
+
const extendGrant = (input) => {
|
|
6716
|
+
const share = active;
|
|
6717
|
+
if (share === null)
|
|
6718
|
+
return { ok: false, status: 404, error: "Nothing is being shared." };
|
|
6719
|
+
const id = isRecord3(input) && typeof input.id === "string" ? input.id : "";
|
|
6720
|
+
sweepExpiry();
|
|
6721
|
+
const grant = share.grants.get(id);
|
|
6722
|
+
if (grant === void 0) {
|
|
6723
|
+
return { ok: false, status: 404, error: "That link has ended. Make a new one." };
|
|
6724
|
+
}
|
|
6725
|
+
const at = now();
|
|
6726
|
+
grant.expiresAt = at + DEFAULT_TTL_MS;
|
|
6727
|
+
grant.expiresAtMono = nowMono() + BigInt(DEFAULT_TTL_MS) * 1000000n;
|
|
6728
|
+
sweepExpiry();
|
|
6729
|
+
options.live.nudge("share");
|
|
6730
|
+
return { ok: true, share: status() };
|
|
6731
|
+
};
|
|
6732
|
+
const rotate = async () => {
|
|
6733
|
+
const share = active;
|
|
6734
|
+
if (share === null)
|
|
6735
|
+
return { ok: false, status: 404, error: "Nothing is being shared." };
|
|
6736
|
+
for (const grant of [...share.grants.values()])
|
|
6737
|
+
endGrant(share, grant, "revoke");
|
|
6738
|
+
const provider = "provider" in share.tunnel ? share.tunnel.provider : null;
|
|
6739
|
+
await share.runningTunnel?.stop().catch(() => {
|
|
6740
|
+
});
|
|
6741
|
+
share.runningTunnel = null;
|
|
6742
|
+
if (active !== share)
|
|
6743
|
+
return { ok: false, status: 404, error: "Nothing is being shared." };
|
|
6744
|
+
mintGrant(share, "");
|
|
6745
|
+
sweepExpiry();
|
|
6746
|
+
if (provider !== null) {
|
|
6747
|
+
share.tunnelGeneration += 1;
|
|
6748
|
+
const generation = share.tunnelGeneration;
|
|
6749
|
+
share.tunnel = { status: "starting", provider };
|
|
6750
|
+
share.runningTunnel = runTunnel({
|
|
6751
|
+
provider,
|
|
6752
|
+
port: share.port,
|
|
6753
|
+
entryPath: `${ENTRY_PREFIX}${[...share.grants.values()][0]?.token ?? ""}`,
|
|
6754
|
+
onState: (next) => {
|
|
6755
|
+
if (active !== share || share.tunnelGeneration !== generation)
|
|
6756
|
+
return;
|
|
6757
|
+
if (JSON.stringify(share.tunnel) === JSON.stringify(next))
|
|
6758
|
+
return;
|
|
6759
|
+
share.tunnel = next;
|
|
6760
|
+
options.live.nudge("share");
|
|
6761
|
+
}
|
|
6762
|
+
});
|
|
6763
|
+
}
|
|
6764
|
+
options.live.nudge("share");
|
|
6765
|
+
return { ok: true, share: status() };
|
|
6766
|
+
};
|
|
6767
|
+
const allowRoute = (input) => {
|
|
6768
|
+
const share = active;
|
|
6769
|
+
if (share === null)
|
|
6770
|
+
return { ok: false, status: 404, error: "Nothing is being shared." };
|
|
6771
|
+
const given = isRecord3(input) && typeof input.path === "string" ? input.path.trim() : "";
|
|
6772
|
+
if (!given.startsWith("/")) {
|
|
6773
|
+
return { ok: false, status: 400, error: "A route is a path beginning with a slash." };
|
|
6774
|
+
}
|
|
6775
|
+
const subtree = isRecord3(input) && input.subtree === true;
|
|
6776
|
+
if (subtree && given.replace(/\/+$/, "") === "") {
|
|
6777
|
+
return { ok: false, status: 400, error: "The root is a page, not a folder." };
|
|
6778
|
+
}
|
|
6779
|
+
const asked = subtree ? `${given.replace(/\/+$/, "")}/` : given === "/" ? given : given.replace(/\/+$/, "");
|
|
6780
|
+
if (share.routes.length >= 400) {
|
|
6781
|
+
return { ok: false, status: 409, error: "That share is holding as many routes as it can." };
|
|
6782
|
+
}
|
|
6783
|
+
if (!share.routes.includes(asked))
|
|
6784
|
+
share.routes.push(asked);
|
|
6785
|
+
share.refused = share.refused.filter((path) => !routeAllowed([asked], path));
|
|
6786
|
+
options.live.nudge("share");
|
|
6787
|
+
return { ok: true, share: status() };
|
|
6788
|
+
};
|
|
6789
|
+
const update = async (input) => {
|
|
6790
|
+
const share = active;
|
|
6791
|
+
if (share === null) {
|
|
6792
|
+
return { ok: false, status: 404, error: "Nothing is being shared." };
|
|
6793
|
+
}
|
|
6794
|
+
const parsed = manifestFrom(input, await options.previews());
|
|
6795
|
+
if (!parsed.ok)
|
|
6796
|
+
return { ok: false, status: 400, error: parsed.error };
|
|
6797
|
+
if (active !== share) {
|
|
6798
|
+
return { ok: false, status: 404, error: "Nothing is being shared." };
|
|
6799
|
+
}
|
|
6800
|
+
share.scope = parsed.manifest.scope;
|
|
6801
|
+
share.titles = parsed.manifest.titles;
|
|
6802
|
+
share.layout = parsed.manifest.layout;
|
|
6803
|
+
options.live.nudge("share");
|
|
6804
|
+
options.live.nudge("config");
|
|
6805
|
+
return { ok: true, share: status() };
|
|
6806
|
+
};
|
|
6807
|
+
const viewerConfig = async (grantId) => {
|
|
6808
|
+
const share = active;
|
|
6809
|
+
if (share === null || !share.grants.has(grantId))
|
|
6810
|
+
return null;
|
|
6811
|
+
const titles = new Set(share.titles);
|
|
6812
|
+
const previews = (await options.previews()).filter((preview) => titles.has(preview.title));
|
|
6813
|
+
if (active !== share)
|
|
6814
|
+
return null;
|
|
6815
|
+
return {
|
|
6816
|
+
...options.viewerConfig,
|
|
6817
|
+
// The project id is the config's absolute path, which keys the sharer's
|
|
6818
|
+
// saved layout and names their machine's directories, and the dev
|
|
6819
|
+
// server's address names their network. A viewer keeps no layout and
|
|
6820
|
+
// never dials the dev server, so the share's own id does the job and
|
|
6821
|
+
// the address is left blank.
|
|
6822
|
+
project: `share:${share.id}`,
|
|
6823
|
+
devServer: "",
|
|
6824
|
+
previews: options.previewsForConfig(previews),
|
|
6825
|
+
errors: [],
|
|
6826
|
+
warnings: [],
|
|
6827
|
+
viewer: { scope: share.scope, layout: cloneLayout(share.layout) }
|
|
6828
|
+
};
|
|
6829
|
+
};
|
|
6830
|
+
const fileSlugAllowed = async (slug, grantId) => {
|
|
6831
|
+
const share = active;
|
|
6832
|
+
if (share === null || !share.grants.has(grantId))
|
|
6833
|
+
return false;
|
|
6834
|
+
const titles = new Set(share.titles);
|
|
6835
|
+
const previews = await options.previews();
|
|
6836
|
+
return previews.some((preview) => {
|
|
6837
|
+
if (!titles.has(preview.title) || preview.file === void 0)
|
|
6838
|
+
return false;
|
|
6839
|
+
const rest = preview.url.startsWith(FILES_PREFIX_PATH) ? preview.url.slice(FILES_PREFIX_PATH.length) : "";
|
|
6840
|
+
const slash = rest.indexOf("/");
|
|
6841
|
+
return (slash === -1 ? rest : rest.slice(0, slash)) === slug;
|
|
6842
|
+
});
|
|
6843
|
+
};
|
|
6844
|
+
const stop = () => {
|
|
6845
|
+
stops += 1;
|
|
6846
|
+
if (stopPromise !== null)
|
|
6847
|
+
return stopPromise;
|
|
6848
|
+
const share = active;
|
|
6849
|
+
if (share === null)
|
|
6850
|
+
return Promise.resolve();
|
|
6851
|
+
if (share.launch !== null) {
|
|
6852
|
+
clearImmediate(share.launch);
|
|
6853
|
+
share.launch = null;
|
|
6854
|
+
}
|
|
6855
|
+
stopPromise = (async () => {
|
|
6856
|
+
await share.runningTunnel?.stop().catch(() => {
|
|
6857
|
+
});
|
|
6858
|
+
await closeListener(share);
|
|
6859
|
+
if (active === share)
|
|
6860
|
+
active = null;
|
|
6861
|
+
options.live.nudge("share");
|
|
6862
|
+
})().finally(() => {
|
|
6863
|
+
stopPromise = null;
|
|
6864
|
+
});
|
|
6865
|
+
return stopPromise;
|
|
6866
|
+
};
|
|
6867
|
+
const close = async () => {
|
|
6868
|
+
closed = true;
|
|
6869
|
+
await stop();
|
|
6870
|
+
};
|
|
6871
|
+
return {
|
|
6872
|
+
tunnels,
|
|
6873
|
+
status,
|
|
6874
|
+
fileSlugAllowed,
|
|
6875
|
+
allowRoute,
|
|
6876
|
+
create,
|
|
6877
|
+
createGrant,
|
|
6878
|
+
revokeGrant,
|
|
6879
|
+
extendGrant,
|
|
6880
|
+
rotate,
|
|
6881
|
+
update,
|
|
6882
|
+
viewerConfig,
|
|
6883
|
+
stop,
|
|
6884
|
+
close
|
|
5494
6885
|
};
|
|
5495
6886
|
}
|
|
5496
6887
|
|
|
@@ -5530,7 +6921,7 @@ function resolveTitle(input, titles, renames) {
|
|
|
5530
6921
|
import { createReadStream, existsSync as existsSync3, statSync, unwatchFile, watch as watchFs, watchFile } from "fs";
|
|
5531
6922
|
import { createHash as createHash2 } from "crypto";
|
|
5532
6923
|
import { mkdir as mkdir8, readdir as readdir3, writeFile as writeFile8 } from "fs/promises";
|
|
5533
|
-
import
|
|
6924
|
+
import http4 from "http";
|
|
5534
6925
|
import net3 from "net";
|
|
5535
6926
|
import { basename as basename3, dirname as dirname8, extname as extname3, join as join12, normalize, relative as relative3 } from "path";
|
|
5536
6927
|
|
|
@@ -5599,7 +6990,7 @@ var CONTENT_TYPES = {
|
|
|
5599
6990
|
".woff2": "font/woff2"
|
|
5600
6991
|
};
|
|
5601
6992
|
var FILES_PREFIX = `${LEGLAS_PREFIX}/files`;
|
|
5602
|
-
function
|
|
6993
|
+
function sendJson2(res, status, body) {
|
|
5603
6994
|
const payload = JSON.stringify(body);
|
|
5604
6995
|
res.writeHead(status, {
|
|
5605
6996
|
"content-type": "application/json; charset=utf-8",
|
|
@@ -5936,7 +7327,8 @@ function watchHealth(target, live) {
|
|
|
5936
7327
|
close: () => {
|
|
5937
7328
|
closed = true;
|
|
5938
7329
|
clearInterval(timer);
|
|
5939
|
-
}
|
|
7330
|
+
},
|
|
7331
|
+
reachable: () => previous
|
|
5940
7332
|
};
|
|
5941
7333
|
}
|
|
5942
7334
|
function snapshotConfig(cwd) {
|
|
@@ -5993,7 +7385,7 @@ function listen(server, port) {
|
|
|
5993
7385
|
server.listen(port, "127.0.0.1");
|
|
5994
7386
|
});
|
|
5995
7387
|
}
|
|
5996
|
-
async function
|
|
7388
|
+
async function bind2(server, requested) {
|
|
5997
7389
|
if (requested === 0)
|
|
5998
7390
|
return listen(server, 0);
|
|
5999
7391
|
for (let attempt = 0; attempt < PORT_ATTEMPTS; attempt += 1) {
|
|
@@ -6009,6 +7401,8 @@ async function bind(server, requested) {
|
|
|
6009
7401
|
async function startServer(options) {
|
|
6010
7402
|
const { config, configErrors = [], configWarnings = [], shellDir = null, project = "", cwd = process.cwd(), leglasCommand = "npx -y leglas", fileMounts = /* @__PURE__ */ new Map(), detect = () => detectAgents() } = options;
|
|
6011
7403
|
const browserPool = options.pool ?? createBrowserPool();
|
|
7404
|
+
let shares = null;
|
|
7405
|
+
let liveHealth = null;
|
|
6012
7406
|
const live = options.live ?? createLiveHub();
|
|
6013
7407
|
const branches = createBranchRegistry({
|
|
6014
7408
|
cwd,
|
|
@@ -6083,12 +7477,103 @@ async function startServer(options) {
|
|
|
6083
7477
|
const livePreviews = async () => (await livePreviewDefinitions()).map(readyPreview).filter((preview) => preview !== null);
|
|
6084
7478
|
void probeAgents().catch(() => {
|
|
6085
7479
|
});
|
|
6086
|
-
const
|
|
7480
|
+
const readShareBody = (req, res, run4) => {
|
|
7481
|
+
let body = "";
|
|
7482
|
+
req.on("data", (chunk) => body += chunk);
|
|
7483
|
+
req.on("end", () => {
|
|
7484
|
+
const parsed = jsonBody(body);
|
|
7485
|
+
if (parsed === null)
|
|
7486
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
7487
|
+
void Promise.resolve(run4(parsed)).then((result) => {
|
|
7488
|
+
if (result === void 0) {
|
|
7489
|
+
return sendJson2(res, 500, { ok: false, error: "Sharing is not available." });
|
|
7490
|
+
}
|
|
7491
|
+
return result.ok ? sendJson2(res, 200, result) : sendJson2(res, result.status, { ok: false, error: result.error });
|
|
7492
|
+
});
|
|
7493
|
+
});
|
|
7494
|
+
};
|
|
7495
|
+
const handleRequest = (req, res, context) => {
|
|
6087
7496
|
const url = req.url ?? "/";
|
|
6088
7497
|
const path = url.split("?")[0] ?? "/";
|
|
6089
7498
|
const query = new URLSearchParams(url.includes("?") ? url.slice(url.indexOf("?") + 1) : "");
|
|
6090
|
-
if (req.method === "POST" && path.startsWith(`${LEGLAS_PREFIX}/api/`) && !isTrustedMutation(req)) {
|
|
6091
|
-
return
|
|
7499
|
+
if (!context.remote && req.method === "POST" && path.startsWith(`${LEGLAS_PREFIX}/api/`) && !isTrustedMutation(req)) {
|
|
7500
|
+
return sendJson2(res, 403, { ok: false, error: "Cross-origin API mutations are refused." });
|
|
7501
|
+
}
|
|
7502
|
+
if (context.remote && path === `${LEGLAS_PREFIX}/api/config`) {
|
|
7503
|
+
return void shares?.viewerConfig(context.grantId ?? "").then((payload) => {
|
|
7504
|
+
if (payload === null) {
|
|
7505
|
+
return sendJson2(res, 403, { ok: false, error: "This link isn't active." });
|
|
7506
|
+
}
|
|
7507
|
+
sendConditionalJson(req, res, payload);
|
|
7508
|
+
});
|
|
7509
|
+
}
|
|
7510
|
+
if (context.remote && path === `${LEGLAS_PREFIX}/api/health`) {
|
|
7511
|
+
const known = liveHealth?.reachable() ?? null;
|
|
7512
|
+
return void (known === null ? probe(target) : Promise.resolve(known)).then((reachable) => sendConditionalJson(req, res, { reachable }));
|
|
7513
|
+
}
|
|
7514
|
+
if (context.remote && path.startsWith(`${LEGLAS_PREFIX}/api/`)) {
|
|
7515
|
+
return sendJson2(res, 403, { error: "Not available to viewers." });
|
|
7516
|
+
}
|
|
7517
|
+
if (!context.remote && path === `${LEGLAS_PREFIX}/api/share` && req.method === "GET") {
|
|
7518
|
+
return void shares?.tunnels().then((tunnels) => sendJson2(res, 200, { share: shares?.status() ?? null, tunnels }));
|
|
7519
|
+
}
|
|
7520
|
+
if (!context.remote && path === `${LEGLAS_PREFIX}/api/share` && req.method === "POST") {
|
|
7521
|
+
if (!hasJsonBody(req)) {
|
|
7522
|
+
return sendJson2(res, 400, { ok: false, error: "Share details must be JSON." });
|
|
7523
|
+
}
|
|
7524
|
+
let body = "";
|
|
7525
|
+
req.on("data", (chunk) => body += chunk);
|
|
7526
|
+
return void req.on("end", async () => {
|
|
7527
|
+
const parsed = jsonBody(body);
|
|
7528
|
+
if (parsed === null) {
|
|
7529
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
7530
|
+
}
|
|
7531
|
+
const result = await shares?.create(parsed).catch((error) => ({
|
|
7532
|
+
ok: false,
|
|
7533
|
+
status: 500,
|
|
7534
|
+
error: `Leglas could not start the share (${error instanceof Error ? error.message : String(error)}).`
|
|
7535
|
+
}));
|
|
7536
|
+
if (result === void 0) {
|
|
7537
|
+
return sendJson2(res, 500, { ok: false, error: "Sharing is not available." });
|
|
7538
|
+
}
|
|
7539
|
+
return result.ok ? sendJson2(res, 200, result) : sendJson2(res, result.status, { ok: false, error: result.error });
|
|
7540
|
+
});
|
|
7541
|
+
}
|
|
7542
|
+
if (!context.remote && path === `${LEGLAS_PREFIX}/api/share/update` && req.method === "POST") {
|
|
7543
|
+
if (!hasJsonBody(req)) {
|
|
7544
|
+
return sendJson2(res, 400, { ok: false, error: "Share details must be JSON." });
|
|
7545
|
+
}
|
|
7546
|
+
let body = "";
|
|
7547
|
+
req.on("data", (chunk) => body += chunk);
|
|
7548
|
+
return void req.on("end", async () => {
|
|
7549
|
+
const parsed = jsonBody(body);
|
|
7550
|
+
if (parsed === null) {
|
|
7551
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
7552
|
+
}
|
|
7553
|
+
const result = await shares?.update(parsed);
|
|
7554
|
+
if (result === void 0) {
|
|
7555
|
+
return sendJson2(res, 500, { ok: false, error: "Sharing is not available." });
|
|
7556
|
+
}
|
|
7557
|
+
return result.ok ? sendJson2(res, 200, result) : sendJson2(res, result.status, { ok: false, error: result.error });
|
|
7558
|
+
});
|
|
7559
|
+
}
|
|
7560
|
+
if (!context.remote && path === `${LEGLAS_PREFIX}/api/share/grants` && req.method === "POST") {
|
|
7561
|
+
return void readShareBody(req, res, (body) => shares?.createGrant(body));
|
|
7562
|
+
}
|
|
7563
|
+
if (!context.remote && path === `${LEGLAS_PREFIX}/api/share/grants/revoke` && req.method === "POST") {
|
|
7564
|
+
return void readShareBody(req, res, (body) => shares?.revokeGrant(body));
|
|
7565
|
+
}
|
|
7566
|
+
if (!context.remote && path === `${LEGLAS_PREFIX}/api/share/grants/extend` && req.method === "POST") {
|
|
7567
|
+
return void readShareBody(req, res, (body) => shares?.extendGrant(body));
|
|
7568
|
+
}
|
|
7569
|
+
if (!context.remote && path === `${LEGLAS_PREFIX}/api/share/allow` && req.method === "POST") {
|
|
7570
|
+
return void readShareBody(req, res, (body) => shares?.allowRoute(body));
|
|
7571
|
+
}
|
|
7572
|
+
if (!context.remote && path === `${LEGLAS_PREFIX}/api/share/rotate` && req.method === "POST") {
|
|
7573
|
+
return void readShareBody(req, res, () => shares?.rotate());
|
|
7574
|
+
}
|
|
7575
|
+
if (!context.remote && path === `${LEGLAS_PREFIX}/api/share/stop` && req.method === "POST") {
|
|
7576
|
+
return void (shares?.stop() ?? Promise.resolve()).then(() => sendJson2(res, 200, { ok: true }));
|
|
6092
7577
|
}
|
|
6093
7578
|
if (path === `${LEGLAS_PREFIX}/api/config`) {
|
|
6094
7579
|
const boot = config?.previews ?? [];
|
|
@@ -6134,23 +7619,23 @@ async function startServer(options) {
|
|
|
6134
7619
|
return void req.on("end", async () => {
|
|
6135
7620
|
const parsed = jsonBody(body);
|
|
6136
7621
|
if (parsed === null) {
|
|
6137
|
-
return
|
|
7622
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6138
7623
|
}
|
|
6139
7624
|
if (typeof parsed.title !== "string" || parsed.title.trim() === "") {
|
|
6140
|
-
return
|
|
7625
|
+
return sendJson2(res, 400, { ok: false, error: "Body needs a direction title." });
|
|
6141
7626
|
}
|
|
6142
7627
|
const preview = (await livePreviewDefinitions()).find((entry) => entry.title === parsed.title);
|
|
6143
7628
|
if (preview === void 0) {
|
|
6144
|
-
return
|
|
7629
|
+
return sendJson2(res, 404, { ok: false, error: "No such direction." });
|
|
6145
7630
|
}
|
|
6146
7631
|
if (preview.branch === void 0) {
|
|
6147
|
-
return
|
|
7632
|
+
return sendJson2(res, 400, {
|
|
6148
7633
|
ok: false,
|
|
6149
7634
|
error: `"${preview.title}" is not a branch preview.`
|
|
6150
7635
|
});
|
|
6151
7636
|
}
|
|
6152
7637
|
if (config?.devCommand === void 0) {
|
|
6153
|
-
return
|
|
7638
|
+
return sendJson2(res, 400, {
|
|
6154
7639
|
ok: false,
|
|
6155
7640
|
error: `"${preview.title}" cannot start because the config sets no devCommand.`
|
|
6156
7641
|
});
|
|
@@ -6158,9 +7643,9 @@ async function startServer(options) {
|
|
|
6158
7643
|
void branches.start(preview.title);
|
|
6159
7644
|
const state = branches.state(preview.title);
|
|
6160
7645
|
if (state === void 0) {
|
|
6161
|
-
return
|
|
7646
|
+
return sendJson2(res, 404, { ok: false, error: "No such branch preview." });
|
|
6162
7647
|
}
|
|
6163
|
-
return
|
|
7648
|
+
return sendJson2(res, 200, { ok: true, state: publicBranchState(state) });
|
|
6164
7649
|
});
|
|
6165
7650
|
}
|
|
6166
7651
|
if (path === `${LEGLAS_PREFIX}/api/previews/delete` && req.method === "POST") {
|
|
@@ -6169,11 +7654,11 @@ async function startServer(options) {
|
|
|
6169
7654
|
return void req.on("end", async () => {
|
|
6170
7655
|
const parsed = jsonBody(body);
|
|
6171
7656
|
if (parsed === null) {
|
|
6172
|
-
return
|
|
7657
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6173
7658
|
}
|
|
6174
7659
|
const titles = parsed.titles;
|
|
6175
7660
|
if (!Array.isArray(titles) || titles.length === 0 || titles.some((title) => typeof title !== "string" || title.trim() === "")) {
|
|
6176
|
-
return
|
|
7661
|
+
return sendJson2(res, 400, {
|
|
6177
7662
|
ok: false,
|
|
6178
7663
|
error: "Body needs a non-empty array of direction titles."
|
|
6179
7664
|
});
|
|
@@ -6182,20 +7667,20 @@ async function startServer(options) {
|
|
|
6182
7667
|
try {
|
|
6183
7668
|
const local = await readLocalPreviews(cwd);
|
|
6184
7669
|
if (local.errors.length > 0) {
|
|
6185
|
-
return
|
|
7670
|
+
return sendJson2(res, 409, { ok: false, error: local.errors.join(" ") });
|
|
6186
7671
|
}
|
|
6187
7672
|
const localTitles = new Set(local.previews.map((preview) => preview.title));
|
|
6188
7673
|
const unknown = unique.filter((title) => !localTitles.has(title));
|
|
6189
7674
|
if (unknown.length > 0) {
|
|
6190
|
-
return
|
|
7675
|
+
return sendJson2(res, 400, {
|
|
6191
7676
|
ok: false,
|
|
6192
7677
|
error: "Only machine-local directions can be deleted from the registry."
|
|
6193
7678
|
});
|
|
6194
7679
|
}
|
|
6195
7680
|
const deleted = await dropLocalPreviews(cwd, unique);
|
|
6196
|
-
return
|
|
7681
|
+
return sendJson2(res, 200, { ok: true, deleted });
|
|
6197
7682
|
} catch {
|
|
6198
|
-
return
|
|
7683
|
+
return sendJson2(res, 500, {
|
|
6199
7684
|
ok: false,
|
|
6200
7685
|
error: "The directions could not be deleted from Leglas."
|
|
6201
7686
|
});
|
|
@@ -6205,7 +7690,7 @@ async function startServer(options) {
|
|
|
6205
7690
|
if (path === `${LEGLAS_PREFIX}/api/references` && req.method === "POST") {
|
|
6206
7691
|
const declaredLength = req.headers["content-length"];
|
|
6207
7692
|
if (typeof declaredLength === "string" && Number(declaredLength) > REFERENCE_MAX_BYTES) {
|
|
6208
|
-
return
|
|
7693
|
+
return sendJson2(res, 413, { ok: false, error: "That image is over 10MB." });
|
|
6209
7694
|
}
|
|
6210
7695
|
const chunks = [];
|
|
6211
7696
|
let bytes = 0;
|
|
@@ -6219,7 +7704,7 @@ async function startServer(options) {
|
|
|
6219
7704
|
refused = true;
|
|
6220
7705
|
req.pause();
|
|
6221
7706
|
res.once("finish", () => req.socket.destroy());
|
|
6222
|
-
|
|
7707
|
+
sendJson2(res, 413, { ok: false, error: "That image is over 10MB." });
|
|
6223
7708
|
return;
|
|
6224
7709
|
}
|
|
6225
7710
|
chunks.push(buffer);
|
|
@@ -6228,12 +7713,12 @@ async function startServer(options) {
|
|
|
6228
7713
|
if (refused)
|
|
6229
7714
|
return;
|
|
6230
7715
|
if (bytes === 0) {
|
|
6231
|
-
return
|
|
7716
|
+
return sendJson2(res, 400, { ok: false, error: "The upload was empty." });
|
|
6232
7717
|
}
|
|
6233
7718
|
const body = Buffer.concat(chunks, bytes);
|
|
6234
7719
|
const image = sniffImage(body);
|
|
6235
7720
|
if (image === null) {
|
|
6236
|
-
return
|
|
7721
|
+
return sendJson2(res, 415, {
|
|
6237
7722
|
ok: false,
|
|
6238
7723
|
error: "Only PNG, JPEG, WebP and GIF images can be attached."
|
|
6239
7724
|
});
|
|
@@ -6245,7 +7730,7 @@ async function startServer(options) {
|
|
|
6245
7730
|
await writeFile8(join12(cwd, file), body);
|
|
6246
7731
|
void pruneReferences(cwd).catch(() => {
|
|
6247
7732
|
});
|
|
6248
|
-
return
|
|
7733
|
+
return sendJson2(res, 200, {
|
|
6249
7734
|
ok: true,
|
|
6250
7735
|
reference: {
|
|
6251
7736
|
id,
|
|
@@ -6257,7 +7742,7 @@ async function startServer(options) {
|
|
|
6257
7742
|
}
|
|
6258
7743
|
});
|
|
6259
7744
|
} catch {
|
|
6260
|
-
return
|
|
7745
|
+
return sendJson2(res, 500, {
|
|
6261
7746
|
ok: false,
|
|
6262
7747
|
error: "The image could not be attached."
|
|
6263
7748
|
});
|
|
@@ -6270,24 +7755,24 @@ async function startServer(options) {
|
|
|
6270
7755
|
return void req.on("end", async () => {
|
|
6271
7756
|
const parsed = jsonBody(body);
|
|
6272
7757
|
if (parsed === null) {
|
|
6273
|
-
return
|
|
7758
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6274
7759
|
}
|
|
6275
7760
|
if (parsed.mode !== void 0 && parsed.mode !== "variant" && parsed.mode !== "replace") {
|
|
6276
|
-
return
|
|
7761
|
+
return sendJson2(res, 400, {
|
|
6277
7762
|
ok: false,
|
|
6278
7763
|
error: 'mode must be "variant" or "replace".'
|
|
6279
7764
|
});
|
|
6280
7765
|
}
|
|
6281
7766
|
const mode = parsed.mode === "replace" ? "replace" : "variant";
|
|
6282
7767
|
if (parsed.references !== void 0 && (!Array.isArray(parsed.references) || parsed.references.some((reference) => typeof reference !== "string" || !/^[A-Za-z0-9_-]{1,32}$/.test(reference)))) {
|
|
6283
|
-
return
|
|
7768
|
+
return sendJson2(res, 400, { ok: false, error: "references must be uploaded image ids." });
|
|
6284
7769
|
}
|
|
6285
7770
|
const references = parsed.references ?? [];
|
|
6286
7771
|
if (references.length > 0) {
|
|
6287
7772
|
const present = new Set((await readdir3(join12(cwd, REFERENCES_DIR)).catch(() => [])).map((name) => name.slice(0, name.indexOf(".") === -1 ? name.length : name.indexOf("."))));
|
|
6288
7773
|
const gone = references.filter((id2) => !present.has(id2));
|
|
6289
7774
|
if (gone.length > 0) {
|
|
6290
|
-
return
|
|
7775
|
+
return sendJson2(res, 410, {
|
|
6291
7776
|
ok: false,
|
|
6292
7777
|
error: gone.length === 1 ? "An attached image is gone: it was pasted over an hour ago and never sent. Attach it again." : "Some attached images are gone: they were pasted over an hour ago and never sent. Attach them again."
|
|
6293
7778
|
});
|
|
@@ -6297,11 +7782,11 @@ async function startServer(options) {
|
|
|
6297
7782
|
const previews = await livePreviews();
|
|
6298
7783
|
const preview = previews.find((entry) => entry.title === parsed.title);
|
|
6299
7784
|
if (!preview) {
|
|
6300
|
-
return
|
|
7785
|
+
return sendJson2(res, 400, { ok: false, error: "Unknown preview, or empty request." });
|
|
6301
7786
|
}
|
|
6302
7787
|
const notes = annotationsFor(await readAnnotations(cwd).catch(() => []), preview.title);
|
|
6303
7788
|
if (!parsed.intent?.trim() && notes.length === 0) {
|
|
6304
|
-
return
|
|
7789
|
+
return sendJson2(res, 400, { ok: false, error: "Unknown preview, or empty request." });
|
|
6305
7790
|
}
|
|
6306
7791
|
const intent = (parsed.intent ?? "").trim();
|
|
6307
7792
|
const live2 = (await readRequests(cwd).catch(() => [])).filter((entry) => entry.status === "queued" || entry.status === "picked-up");
|
|
@@ -6315,7 +7800,7 @@ async function startServer(options) {
|
|
|
6315
7800
|
// one forks the direction and the other rewrites it. Only a
|
|
6316
7801
|
// genuine repeat is refused.
|
|
6317
7802
|
(entry.mode ?? "replace") === mode && sameNotes(entry) && sameContext(entry))) {
|
|
6318
|
-
return
|
|
7803
|
+
return sendJson2(res, 409, {
|
|
6319
7804
|
ok: false,
|
|
6320
7805
|
duplicate: true,
|
|
6321
7806
|
error: `That exact change to ${preview.title} is already waiting.`
|
|
@@ -6349,13 +7834,13 @@ async function startServer(options) {
|
|
|
6349
7834
|
...composed
|
|
6350
7835
|
}, id);
|
|
6351
7836
|
runner?.nudge();
|
|
6352
|
-
return
|
|
7837
|
+
return sendJson2(res, 200, {
|
|
6353
7838
|
ok: true,
|
|
6354
7839
|
...composed,
|
|
6355
7840
|
attachments: captured.attachments
|
|
6356
7841
|
});
|
|
6357
7842
|
} catch {
|
|
6358
|
-
return
|
|
7843
|
+
return sendJson2(res, 200, {
|
|
6359
7844
|
ok: true,
|
|
6360
7845
|
...composed,
|
|
6361
7846
|
attachments: captured.attachments,
|
|
@@ -6366,29 +7851,29 @@ async function startServer(options) {
|
|
|
6366
7851
|
}
|
|
6367
7852
|
if (path === `${LEGLAS_PREFIX}/api/capture` && req.method === "POST") {
|
|
6368
7853
|
if (!hasJsonBody(req)) {
|
|
6369
|
-
return
|
|
7854
|
+
return sendJson2(res, 400, { ok: false, error: "Capture must be JSON." });
|
|
6370
7855
|
}
|
|
6371
7856
|
let body = "";
|
|
6372
7857
|
req.on("data", (chunk) => body += chunk);
|
|
6373
7858
|
return void req.on("end", async () => {
|
|
6374
7859
|
const parsed = jsonBody(body);
|
|
6375
7860
|
if (parsed === null) {
|
|
6376
|
-
return
|
|
7861
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6377
7862
|
}
|
|
6378
7863
|
if (typeof parsed.title !== "string" || parsed.title === "") {
|
|
6379
|
-
return
|
|
7864
|
+
return sendJson2(res, 400, { ok: false, error: "Capture needs a direction title." });
|
|
6380
7865
|
}
|
|
6381
7866
|
if (parsed.note !== void 0 && typeof parsed.note !== "string") {
|
|
6382
|
-
return
|
|
7867
|
+
return sendJson2(res, 400, { ok: false, error: "The note id must be a string." });
|
|
6383
7868
|
}
|
|
6384
7869
|
const preview = (await livePreviews()).find((entry) => entry.title === parsed.title);
|
|
6385
7870
|
if (preview === void 0) {
|
|
6386
|
-
return
|
|
7871
|
+
return sendJson2(res, 404, { ok: false, error: "No such direction." });
|
|
6387
7872
|
}
|
|
6388
7873
|
const width = typeof parsed.width === "number" && Number.isFinite(parsed.width) ? Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, Math.round(parsed.width))) : 1440;
|
|
6389
7874
|
const browser = await browserPool.acquire();
|
|
6390
7875
|
if (browser === null) {
|
|
6391
|
-
return
|
|
7876
|
+
return sendJson2(res, 503, {
|
|
6392
7877
|
ok: false,
|
|
6393
7878
|
error: browserPool.reason() ?? NO_BROWSER
|
|
6394
7879
|
});
|
|
@@ -6425,7 +7910,7 @@ async function startServer(options) {
|
|
|
6425
7910
|
});
|
|
6426
7911
|
const result = await Promise.race([work, timeout]);
|
|
6427
7912
|
if (result === timeoutMarker) {
|
|
6428
|
-
return
|
|
7913
|
+
return sendJson2(res, 504, { ok: false, error: "The page did not load in time." });
|
|
6429
7914
|
}
|
|
6430
7915
|
clearTimeout(timer);
|
|
6431
7916
|
const crop = annotations.length > 0 ? result.crops[0] : null;
|
|
@@ -6435,18 +7920,19 @@ async function startServer(options) {
|
|
|
6435
7920
|
const relativeFile = `${CAPTURES_DIR}/show/${name}`;
|
|
6436
7921
|
await mkdir8(join12(cwd, CAPTURES_DIR, "show"), { recursive: true });
|
|
6437
7922
|
await writeFile8(join12(cwd, relativeFile), shot.png);
|
|
6438
|
-
return
|
|
7923
|
+
return sendJson2(res, 200, {
|
|
6439
7924
|
ok: true,
|
|
6440
7925
|
file: relativeFile,
|
|
6441
7926
|
width: shot.width,
|
|
6442
7927
|
height: shot.height,
|
|
6443
7928
|
viewport: result.frame.width,
|
|
6444
7929
|
errors: result.errors,
|
|
7930
|
+
hydration: result.hydration,
|
|
6445
7931
|
cut: result.cut
|
|
6446
7932
|
});
|
|
6447
7933
|
} catch (error) {
|
|
6448
7934
|
clearTimeout(timer);
|
|
6449
|
-
return
|
|
7935
|
+
return sendJson2(res, 502, {
|
|
6450
7936
|
ok: false,
|
|
6451
7937
|
error: error instanceof Error ? error.message : String(error)
|
|
6452
7938
|
});
|
|
@@ -6457,14 +7943,14 @@ async function startServer(options) {
|
|
|
6457
7943
|
return void readAgentChoice(cwd).then((choice) => {
|
|
6458
7944
|
if (choice.agent !== null)
|
|
6459
7945
|
runner?.prepare(choice.agent);
|
|
6460
|
-
|
|
6461
|
-
}, () =>
|
|
7946
|
+
sendJson2(res, 200, { ok: true });
|
|
7947
|
+
}, () => sendJson2(res, 200, { ok: true }));
|
|
6462
7948
|
}
|
|
6463
7949
|
if (path === `${LEGLAS_PREFIX}/api/agents` && req.method === "GET") {
|
|
6464
7950
|
return void Promise.all([
|
|
6465
7951
|
currentAgents(query.get("refresh") === "1"),
|
|
6466
7952
|
readAgentChoice(cwd)
|
|
6467
|
-
]).then(([agents, choice]) =>
|
|
7953
|
+
]).then(([agents, choice]) => sendJson2(res, 200, {
|
|
6468
7954
|
agents,
|
|
6469
7955
|
choice: choice.agent,
|
|
6470
7956
|
customRun: choice.run,
|
|
@@ -6473,48 +7959,48 @@ async function startServer(options) {
|
|
|
6473
7959
|
}
|
|
6474
7960
|
if (path === `${LEGLAS_PREFIX}/api/agent` && req.method === "POST") {
|
|
6475
7961
|
if (!isLoopbackAddress(req.socket.remoteAddress)) {
|
|
6476
|
-
return
|
|
7962
|
+
return sendJson2(res, 403, {
|
|
6477
7963
|
ok: false,
|
|
6478
7964
|
error: "The agent choice can only be made from the machine running Leglas."
|
|
6479
7965
|
});
|
|
6480
7966
|
}
|
|
6481
7967
|
if (!hasJsonBody(req)) {
|
|
6482
|
-
return
|
|
7968
|
+
return sendJson2(res, 400, { ok: false, error: "Agent choice must be JSON." });
|
|
6483
7969
|
}
|
|
6484
7970
|
let body = "";
|
|
6485
7971
|
req.on("data", (chunk) => body += chunk);
|
|
6486
7972
|
return void req.on("end", () => {
|
|
6487
7973
|
const parsed = jsonBody(body);
|
|
6488
7974
|
if (parsed === null) {
|
|
6489
|
-
return
|
|
7975
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6490
7976
|
}
|
|
6491
7977
|
if (!isKnownAgent(parsed.agent) && parsed.agent !== "custom") {
|
|
6492
|
-
return
|
|
7978
|
+
return sendJson2(res, 400, { ok: false, error: "Body needs a known agent." });
|
|
6493
7979
|
}
|
|
6494
7980
|
if (parsed.run !== void 0 && typeof parsed.run !== "string") {
|
|
6495
|
-
return
|
|
7981
|
+
return sendJson2(res, 400, { ok: false, error: "The custom run command must be a string." });
|
|
6496
7982
|
}
|
|
6497
7983
|
const effort = parsed.effort === null || isAgentEffort(parsed.effort) ? parsed.effort : void 0;
|
|
6498
7984
|
if (parsed.effort !== void 0 && effort === void 0) {
|
|
6499
|
-
return
|
|
7985
|
+
return sendJson2(res, 400, { ok: false, error: "Effort must be a supported level or null." });
|
|
6500
7986
|
}
|
|
6501
7987
|
if (parsed.agent === "custom") {
|
|
6502
7988
|
if (effort !== void 0) {
|
|
6503
|
-
return
|
|
7989
|
+
return sendJson2(res, 400, {
|
|
6504
7990
|
ok: false,
|
|
6505
7991
|
error: "Custom agents manage effort in their own command."
|
|
6506
7992
|
});
|
|
6507
7993
|
}
|
|
6508
7994
|
if (typeof parsed.run !== "string") {
|
|
6509
|
-
return
|
|
7995
|
+
return sendJson2(res, 400, { ok: false, error: "A custom agent needs a run command." });
|
|
6510
7996
|
}
|
|
6511
7997
|
const template = parseTemplate(parsed.run);
|
|
6512
7998
|
if (!template.ok)
|
|
6513
|
-
return
|
|
6514
|
-
return void saveAgentChoice(cwd, { agent: "custom", run: parsed.run }).then(() =>
|
|
7999
|
+
return sendJson2(res, 400, { ok: false, error: template.error });
|
|
8000
|
+
return void saveAgentChoice(cwd, { agent: "custom", run: parsed.run }).then(() => sendJson2(res, 200, { ok: true }), () => sendJson2(res, 500, { ok: false, error: "Agent choice could not be saved." }));
|
|
6515
8001
|
}
|
|
6516
8002
|
if (effort !== void 0 && effort !== null && !KNOWN_AGENTS[parsed.agent].efforts.includes(effort)) {
|
|
6517
|
-
return
|
|
8003
|
+
return sendJson2(res, 400, {
|
|
6518
8004
|
ok: false,
|
|
6519
8005
|
error: `${KNOWN_AGENTS[parsed.agent].name} does not expose an effort override.`
|
|
6520
8006
|
});
|
|
@@ -6524,8 +8010,8 @@ async function startServer(options) {
|
|
|
6524
8010
|
...effort === void 0 ? {} : { effort }
|
|
6525
8011
|
}).then(() => {
|
|
6526
8012
|
runner?.prepare(parsed.agent);
|
|
6527
|
-
|
|
6528
|
-
}, () =>
|
|
8013
|
+
sendJson2(res, 200, { ok: true });
|
|
8014
|
+
}, () => sendJson2(res, 500, { ok: false, error: "Agent choice could not be saved." }));
|
|
6529
8015
|
});
|
|
6530
8016
|
}
|
|
6531
8017
|
if (path === `${LEGLAS_PREFIX}/api/watch` && req.method === "POST") {
|
|
@@ -6534,13 +8020,13 @@ async function startServer(options) {
|
|
|
6534
8020
|
return void req.on("end", () => {
|
|
6535
8021
|
const parsed = jsonBody(body);
|
|
6536
8022
|
if (parsed === null) {
|
|
6537
|
-
return
|
|
8023
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6538
8024
|
}
|
|
6539
8025
|
if (typeof parsed.watching !== "boolean") {
|
|
6540
|
-
return
|
|
8026
|
+
return sendJson2(res, 400, { ok: false, error: "Body needs a watching boolean." });
|
|
6541
8027
|
}
|
|
6542
8028
|
lastSeen = parsed.watching ? Date.now() : null;
|
|
6543
|
-
|
|
8029
|
+
sendJson2(res, 200, { ok: true });
|
|
6544
8030
|
});
|
|
6545
8031
|
}
|
|
6546
8032
|
if (path === `${LEGLAS_PREFIX}/api/requests` && req.method === "GET") {
|
|
@@ -6589,47 +8075,47 @@ async function startServer(options) {
|
|
|
6589
8075
|
}
|
|
6590
8076
|
if (path === `${LEGLAS_PREFIX}/api/requests/cancel` && req.method === "POST") {
|
|
6591
8077
|
if (!hasJsonBody(req)) {
|
|
6592
|
-
return
|
|
8078
|
+
return sendJson2(res, 200, { ok: true, cancelled: runner?.cancel() ?? false });
|
|
6593
8079
|
}
|
|
6594
8080
|
let body = "";
|
|
6595
8081
|
req.on("data", (chunk) => body += chunk);
|
|
6596
8082
|
return void req.on("end", () => {
|
|
6597
8083
|
const parsed = jsonBody(body);
|
|
6598
8084
|
if (parsed === null) {
|
|
6599
|
-
return
|
|
8085
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6600
8086
|
}
|
|
6601
8087
|
if (parsed.id !== void 0 && typeof parsed.id !== "string") {
|
|
6602
|
-
return
|
|
8088
|
+
return sendJson2(res, 400, { ok: false, error: "The request id must be a string." });
|
|
6603
8089
|
}
|
|
6604
|
-
return
|
|
8090
|
+
return sendJson2(res, 200, { ok: true, cancelled: runner?.cancel(parsed.id) ?? false });
|
|
6605
8091
|
});
|
|
6606
8092
|
}
|
|
6607
8093
|
if (path === `${LEGLAS_PREFIX}/api/requests/retry` && req.method === "POST") {
|
|
6608
8094
|
if (!hasJsonBody(req)) {
|
|
6609
|
-
return
|
|
8095
|
+
return sendJson2(res, 400, { ok: false, error: "Retry must be JSON." });
|
|
6610
8096
|
}
|
|
6611
8097
|
let body = "";
|
|
6612
8098
|
req.on("data", (chunk) => body += chunk);
|
|
6613
8099
|
return void req.on("end", async () => {
|
|
6614
8100
|
const parsed = jsonBody(body);
|
|
6615
8101
|
if (parsed === null) {
|
|
6616
|
-
return
|
|
8102
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6617
8103
|
}
|
|
6618
8104
|
if (typeof parsed.id !== "string") {
|
|
6619
|
-
return
|
|
8105
|
+
return sendJson2(res, 400, { ok: false, error: "Body needs a request id." });
|
|
6620
8106
|
}
|
|
6621
8107
|
const request = (await readRequests(cwd)).find((entry) => entry.id === parsed.id);
|
|
6622
8108
|
if (request === void 0) {
|
|
6623
|
-
return
|
|
8109
|
+
return sendJson2(res, 404, { ok: false, error: "No such request." });
|
|
6624
8110
|
}
|
|
6625
8111
|
if (!isEnded(request, runner?.snapshot().failedIds ?? [])) {
|
|
6626
|
-
return
|
|
8112
|
+
return sendJson2(res, 400, { ok: false, error: "Only an ended request can be run again." });
|
|
6627
8113
|
}
|
|
6628
8114
|
try {
|
|
6629
8115
|
const retryId = newRequestId();
|
|
6630
8116
|
const attachments = await rehomeCaptures(cwd, request.id, retryId, request.attachments ?? []).catch(() => []);
|
|
6631
8117
|
if (!await removeRequest(cwd, request.id)) {
|
|
6632
|
-
return
|
|
8118
|
+
return sendJson2(res, 404, { ok: false, error: "No such request." });
|
|
6633
8119
|
}
|
|
6634
8120
|
await appendRequest(cwd, {
|
|
6635
8121
|
title: request.title,
|
|
@@ -6650,9 +8136,9 @@ async function startServer(options) {
|
|
|
6650
8136
|
...request.references === void 0 ? {} : { references: request.references }
|
|
6651
8137
|
}, retryId);
|
|
6652
8138
|
runner?.nudge();
|
|
6653
|
-
return
|
|
8139
|
+
return sendJson2(res, 200, { ok: true });
|
|
6654
8140
|
} catch {
|
|
6655
|
-
return
|
|
8141
|
+
return sendJson2(res, 500, { ok: false, error: "The request could not be retried." });
|
|
6656
8142
|
}
|
|
6657
8143
|
});
|
|
6658
8144
|
}
|
|
@@ -6661,21 +8147,21 @@ async function startServer(options) {
|
|
|
6661
8147
|
}
|
|
6662
8148
|
if (path === `${LEGLAS_PREFIX}/api/annotations` && req.method === "POST") {
|
|
6663
8149
|
if (!hasJsonBody(req)) {
|
|
6664
|
-
return
|
|
8150
|
+
return sendJson2(res, 400, { ok: false, error: "A note must be JSON." });
|
|
6665
8151
|
}
|
|
6666
8152
|
let body = "";
|
|
6667
8153
|
req.on("data", (chunk) => body += chunk);
|
|
6668
8154
|
return void req.on("end", async () => {
|
|
6669
8155
|
const parsed = jsonBody(body);
|
|
6670
8156
|
if (parsed === null) {
|
|
6671
|
-
return
|
|
8157
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6672
8158
|
}
|
|
6673
8159
|
if (typeof parsed.title !== "string" || parsed.title.trim() === "") {
|
|
6674
|
-
return
|
|
8160
|
+
return sendJson2(res, 400, { ok: false, error: "A note needs a direction." });
|
|
6675
8161
|
}
|
|
6676
8162
|
const anchor = anchorFrom(parsed.anchor);
|
|
6677
8163
|
if (anchor === null) {
|
|
6678
|
-
return
|
|
8164
|
+
return sendJson2(res, 400, { ok: false, error: "A note needs something to point at." });
|
|
6679
8165
|
}
|
|
6680
8166
|
try {
|
|
6681
8167
|
const annotation = await addAnnotation(cwd, {
|
|
@@ -6683,87 +8169,87 @@ async function startServer(options) {
|
|
|
6683
8169
|
note: typeof parsed.note === "string" ? parsed.note.trim() : "",
|
|
6684
8170
|
title: parsed.title
|
|
6685
8171
|
});
|
|
6686
|
-
return
|
|
8172
|
+
return sendJson2(res, 200, { ok: true, annotation });
|
|
6687
8173
|
} catch {
|
|
6688
|
-
return
|
|
8174
|
+
return sendJson2(res, 500, { ok: false, error: "The note could not be kept." });
|
|
6689
8175
|
}
|
|
6690
8176
|
});
|
|
6691
8177
|
}
|
|
6692
8178
|
if (path === `${LEGLAS_PREFIX}/api/annotations/update` && req.method === "POST") {
|
|
6693
8179
|
if (!hasJsonBody(req)) {
|
|
6694
|
-
return
|
|
8180
|
+
return sendJson2(res, 400, { ok: false, error: "A note must be JSON." });
|
|
6695
8181
|
}
|
|
6696
8182
|
let body = "";
|
|
6697
8183
|
req.on("data", (chunk) => body += chunk);
|
|
6698
8184
|
return void req.on("end", async () => {
|
|
6699
8185
|
const parsed = jsonBody(body);
|
|
6700
8186
|
if (parsed === null) {
|
|
6701
|
-
return
|
|
8187
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6702
8188
|
}
|
|
6703
8189
|
if (typeof parsed.id !== "string" || parsed.id === "") {
|
|
6704
|
-
return
|
|
8190
|
+
return sendJson2(res, 400, { ok: false, error: "Body needs the note to reword." });
|
|
6705
8191
|
}
|
|
6706
8192
|
if (typeof parsed.note !== "string") {
|
|
6707
|
-
return
|
|
8193
|
+
return sendJson2(res, 400, { ok: false, error: "A reworded note needs its words." });
|
|
6708
8194
|
}
|
|
6709
8195
|
try {
|
|
6710
8196
|
const annotation = await updateAnnotation(cwd, parsed.id, parsed.note);
|
|
6711
8197
|
if (annotation === null) {
|
|
6712
|
-
return
|
|
8198
|
+
return sendJson2(res, 404, { ok: false, error: "That note has gone." });
|
|
6713
8199
|
}
|
|
6714
|
-
return
|
|
8200
|
+
return sendJson2(res, 200, { ok: true, annotation });
|
|
6715
8201
|
} catch {
|
|
6716
|
-
return
|
|
8202
|
+
return sendJson2(res, 500, { ok: false, error: "The note could not be reworded." });
|
|
6717
8203
|
}
|
|
6718
8204
|
});
|
|
6719
8205
|
}
|
|
6720
8206
|
if (path === `${LEGLAS_PREFIX}/api/annotations/delete` && req.method === "POST") {
|
|
6721
8207
|
if (!hasJsonBody(req)) {
|
|
6722
|
-
return
|
|
8208
|
+
return sendJson2(res, 400, { ok: false, error: "Delete must be JSON." });
|
|
6723
8209
|
}
|
|
6724
8210
|
let body = "";
|
|
6725
8211
|
req.on("data", (chunk) => body += chunk);
|
|
6726
8212
|
return void req.on("end", async () => {
|
|
6727
8213
|
const parsed = jsonBody(body);
|
|
6728
8214
|
if (parsed === null) {
|
|
6729
|
-
return
|
|
8215
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6730
8216
|
}
|
|
6731
8217
|
const ids = Array.isArray(parsed.ids) ? parsed.ids.filter((entry) => typeof entry === "string") : [];
|
|
6732
8218
|
if (ids.length === 0) {
|
|
6733
|
-
return
|
|
8219
|
+
return sendJson2(res, 400, { ok: false, error: "Body needs the notes to forget." });
|
|
6734
8220
|
}
|
|
6735
8221
|
try {
|
|
6736
|
-
return
|
|
8222
|
+
return sendJson2(res, 200, { ok: true, deleted: await removeAnnotations(cwd, ids) });
|
|
6737
8223
|
} catch {
|
|
6738
|
-
return
|
|
8224
|
+
return sendJson2(res, 500, { ok: false, error: "The notes could not be forgotten." });
|
|
6739
8225
|
}
|
|
6740
8226
|
});
|
|
6741
8227
|
}
|
|
6742
8228
|
if (path === `${LEGLAS_PREFIX}/api/requests/dismiss` && req.method === "POST") {
|
|
6743
8229
|
if (!hasJsonBody(req)) {
|
|
6744
|
-
return
|
|
8230
|
+
return sendJson2(res, 400, { ok: false, error: "Dismiss must be JSON." });
|
|
6745
8231
|
}
|
|
6746
8232
|
let body = "";
|
|
6747
8233
|
req.on("data", (chunk) => body += chunk);
|
|
6748
8234
|
return void req.on("end", async () => {
|
|
6749
8235
|
const parsed = jsonBody(body);
|
|
6750
8236
|
if (parsed === null) {
|
|
6751
|
-
return
|
|
8237
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6752
8238
|
}
|
|
6753
8239
|
if (typeof parsed.id !== "string") {
|
|
6754
|
-
return
|
|
8240
|
+
return sendJson2(res, 400, { ok: false, error: "Body needs a request id." });
|
|
6755
8241
|
}
|
|
6756
8242
|
const target2 = (await readRequests(cwd)).find((entry) => entry.id === parsed.id);
|
|
6757
8243
|
if (target2 === void 0 || !isEnded(target2, runner?.snapshot().failedIds ?? [])) {
|
|
6758
|
-
return
|
|
8244
|
+
return sendJson2(res, 400, { ok: false, error: "Only an ended request can be dismissed." });
|
|
6759
8245
|
}
|
|
6760
8246
|
try {
|
|
6761
8247
|
if (!await removeRequest(cwd, parsed.id)) {
|
|
6762
|
-
return
|
|
8248
|
+
return sendJson2(res, 404, { ok: false, error: "No such request." });
|
|
6763
8249
|
}
|
|
6764
|
-
return
|
|
8250
|
+
return sendJson2(res, 200, { ok: true });
|
|
6765
8251
|
} catch {
|
|
6766
|
-
return
|
|
8252
|
+
return sendJson2(res, 500, { ok: false, error: "The request could not be dismissed." });
|
|
6767
8253
|
}
|
|
6768
8254
|
});
|
|
6769
8255
|
}
|
|
@@ -6773,13 +8259,13 @@ async function startServer(options) {
|
|
|
6773
8259
|
return void req.on("end", () => {
|
|
6774
8260
|
const parsed = jsonBody(body);
|
|
6775
8261
|
if (parsed === null) {
|
|
6776
|
-
return
|
|
8262
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6777
8263
|
}
|
|
6778
8264
|
if (parsed.renames === null || typeof parsed.renames !== "object") {
|
|
6779
|
-
return
|
|
8265
|
+
return sendJson2(res, 400, { ok: false, error: "Body needs a renames object." });
|
|
6780
8266
|
}
|
|
6781
8267
|
const renames = Object.fromEntries(Object.entries(parsed.renames).filter((entry) => typeof entry[1] === "string" && entry[1] !== ""));
|
|
6782
|
-
void writeRenames(cwd, renames).then(() =>
|
|
8268
|
+
void writeRenames(cwd, renames).then(() => sendJson2(res, 200, { ok: true }), () => sendJson2(res, 200, { ok: false }));
|
|
6783
8269
|
});
|
|
6784
8270
|
}
|
|
6785
8271
|
if (path === `${LEGLAS_PREFIX}/api/health`) {
|
|
@@ -6796,42 +8282,86 @@ async function startServer(options) {
|
|
|
6796
8282
|
relative6 = "";
|
|
6797
8283
|
}
|
|
6798
8284
|
const dir = fileMounts.get(slug);
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
6802
|
-
|
|
8285
|
+
const serveMount = () => {
|
|
8286
|
+
if (dir !== void 0 && relative6 !== "" && serveFrom(res, dir, relative6))
|
|
8287
|
+
return;
|
|
8288
|
+
res.writeHead(404, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
|
|
8289
|
+
res.end("Leglas: no such preview file.");
|
|
8290
|
+
};
|
|
8291
|
+
if (!context.remote)
|
|
8292
|
+
return serveMount();
|
|
8293
|
+
if (relative6.split("/").some((segment) => segment.startsWith("."))) {
|
|
8294
|
+
return sendJson2(res, 403, { ok: false, error: "Not available to viewers." });
|
|
8295
|
+
}
|
|
8296
|
+
return void (shares?.fileSlugAllowed(slug, context.grantId ?? "") ?? Promise.resolve(false)).then((allowed) => {
|
|
8297
|
+
if (!allowed)
|
|
8298
|
+
return sendJson2(res, 403, { ok: false, error: "Not available to viewers." });
|
|
8299
|
+
serveMount();
|
|
8300
|
+
});
|
|
6803
8301
|
}
|
|
6804
8302
|
if (path.startsWith(`${LEGLAS_PREFIX}/api/`)) {
|
|
6805
|
-
return
|
|
8303
|
+
return sendJson2(res, 404, { error: "No such Leglas API path." });
|
|
6806
8304
|
}
|
|
6807
8305
|
if (path === LEGLAS_PREFIX || path.startsWith(`${LEGLAS_PREFIX}/`)) {
|
|
6808
8306
|
if (shellDir !== null && serveShellFile(res, shellDir, path))
|
|
6809
8307
|
return;
|
|
6810
8308
|
if (shellDir !== null) {
|
|
6811
8309
|
res.writeHead(404, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
|
|
6812
|
-
|
|
8310
|
+
res.end("Leglas: no such path.");
|
|
8311
|
+
return;
|
|
6813
8312
|
}
|
|
6814
8313
|
res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
6815
|
-
|
|
8314
|
+
res.end(PLACEHOLDER);
|
|
8315
|
+
return;
|
|
6816
8316
|
}
|
|
6817
|
-
return proxy.request(req, res,
|
|
6818
|
-
}
|
|
8317
|
+
return proxy.request(req, res, context.publicOrigin);
|
|
8318
|
+
};
|
|
8319
|
+
let port = 0;
|
|
8320
|
+
const server = http4.createServer((req, res) => handleRequest(req, res, {
|
|
8321
|
+
remote: false,
|
|
8322
|
+
publicOrigin: `http://localhost:${port}`
|
|
8323
|
+
}));
|
|
6819
8324
|
const sockets = /* @__PURE__ */ new Set();
|
|
6820
8325
|
server.on("connection", (socket) => {
|
|
6821
8326
|
sockets.add(socket);
|
|
6822
8327
|
socket.once("close", () => sockets.delete(socket));
|
|
6823
8328
|
});
|
|
6824
|
-
|
|
6825
|
-
|
|
6826
|
-
|
|
8329
|
+
const handleUpgrade = (req, socket, head, context) => {
|
|
8330
|
+
const liveUpgrade = context.remote ? live.upgrade(req, socket, head, { viewer: true }) : live.upgrade(req, socket, head);
|
|
8331
|
+
if (liveUpgrade)
|
|
8332
|
+
return true;
|
|
6827
8333
|
const path = (req.url ?? "/").split("?")[0] ?? "/";
|
|
6828
|
-
if (path.startsWith(`${LEGLAS_PREFIX}/`))
|
|
6829
|
-
|
|
8334
|
+
if (path.startsWith(`${LEGLAS_PREFIX}/`)) {
|
|
8335
|
+
socket.destroy();
|
|
8336
|
+
return false;
|
|
8337
|
+
}
|
|
6830
8338
|
proxy.upgrade(req, socket, head);
|
|
8339
|
+
return false;
|
|
8340
|
+
};
|
|
8341
|
+
server.on("upgrade", (req, socket, head) => {
|
|
8342
|
+
handleUpgrade(req, socket, head, { remote: false });
|
|
8343
|
+
});
|
|
8344
|
+
shares = createShareManager({
|
|
8345
|
+
live,
|
|
8346
|
+
previews: livePreviewDefinitions,
|
|
8347
|
+
previewsForConfig,
|
|
8348
|
+
viewerConfig: {
|
|
8349
|
+
project,
|
|
8350
|
+
devServer: target,
|
|
8351
|
+
scanPreviews: config?.scanPreviews ?? true
|
|
8352
|
+
},
|
|
8353
|
+
request: (req, res, context) => handleRequest(req, res, {
|
|
8354
|
+
remote: true,
|
|
8355
|
+
publicOrigin: context.publicOrigin,
|
|
8356
|
+
grantId: context.grantId
|
|
8357
|
+
}),
|
|
8358
|
+
upgrade: (req, socket, head) => handleUpgrade(req, socket, head, { remote: true }),
|
|
8359
|
+
...options.detectTunnels === void 0 ? {} : { detectTunnels: options.detectTunnels },
|
|
8360
|
+
...options.startTunnel === void 0 ? {} : { startTunnel: options.startTunnel }
|
|
6831
8361
|
});
|
|
6832
|
-
|
|
8362
|
+
port = await bind2(server, options.port ?? DEFAULT_PORT);
|
|
6833
8363
|
const liveFiles = watchLiveFiles(cwd, bootConfigPath, live);
|
|
6834
|
-
|
|
8364
|
+
liveHealth = watchHealth(target, live);
|
|
6835
8365
|
await pruneCaptures(cwd, (await readRequests(cwd).catch(() => [])).map((request) => request.id)).catch(() => {
|
|
6836
8366
|
});
|
|
6837
8367
|
await writeServerInfo(cwd, {
|
|
@@ -6855,21 +8385,26 @@ async function startServer(options) {
|
|
|
6855
8385
|
close: () => {
|
|
6856
8386
|
if (closePromise !== null)
|
|
6857
8387
|
return closePromise;
|
|
6858
|
-
|
|
6859
|
-
|
|
6860
|
-
|
|
6861
|
-
|
|
6862
|
-
|
|
6863
|
-
|
|
6864
|
-
|
|
6865
|
-
|
|
6866
|
-
|
|
6867
|
-
|
|
6868
|
-
|
|
6869
|
-
|
|
6870
|
-
|
|
6871
|
-
|
|
6872
|
-
|
|
8388
|
+
closePromise = (async () => {
|
|
8389
|
+
liveFiles.close();
|
|
8390
|
+
liveHealth?.close();
|
|
8391
|
+
await Promise.all([
|
|
8392
|
+
shares?.close() ?? Promise.resolve(),
|
|
8393
|
+
branches.stop(),
|
|
8394
|
+
runner.stop(),
|
|
8395
|
+
browserPool.close(),
|
|
8396
|
+
live.close()
|
|
8397
|
+
]);
|
|
8398
|
+
await new Promise((done) => {
|
|
8399
|
+
for (const socket of sockets)
|
|
8400
|
+
socket.destroy();
|
|
8401
|
+
sockets.clear();
|
|
8402
|
+
server.closeAllConnections();
|
|
8403
|
+
server.close(() => done());
|
|
8404
|
+
});
|
|
8405
|
+
await removeServerInfo(cwd, { port, pid: process.pid }).catch(() => {
|
|
8406
|
+
});
|
|
8407
|
+
})();
|
|
6873
8408
|
return closePromise;
|
|
6874
8409
|
}
|
|
6875
8410
|
};
|
|
@@ -7474,6 +9009,10 @@ async function runShow(options, deps) {
|
|
|
7474
9009
|
height: captured.height,
|
|
7475
9010
|
viewport: captured.viewport,
|
|
7476
9011
|
errors: Array.isArray(captured.errors) ? captured.errors.filter((error) => typeof error === "string") : [],
|
|
9012
|
+
hydration: typeof captured.hydration === "object" && captured.hydration !== null && typeof captured.hydration.framework === "string" && typeof captured.hydration.message === "string" ? {
|
|
9013
|
+
framework: captured.hydration.framework,
|
|
9014
|
+
message: captured.hydration.message
|
|
9015
|
+
} : null,
|
|
7477
9016
|
cut: captured.cut === true
|
|
7478
9017
|
};
|
|
7479
9018
|
}
|
|
@@ -7501,6 +9040,12 @@ async function runShow(options, deps) {
|
|
|
7501
9040
|
if (envelope2.screenshot.cut) {
|
|
7502
9041
|
deps.log(" the top of the page only; it is taller than one capture");
|
|
7503
9042
|
}
|
|
9043
|
+
if (envelope2.screenshot.hydration !== null) {
|
|
9044
|
+
deps.log(
|
|
9045
|
+
` hydration ${envelope2.screenshot.hydration.framework} rebuilt the page in the browser after load; the served markup is not what is on screen`
|
|
9046
|
+
);
|
|
9047
|
+
deps.log(` ${envelope2.screenshot.hydration.message}`);
|
|
9048
|
+
}
|
|
7504
9049
|
if (envelope2.screenshot.errors.length > 0) {
|
|
7505
9050
|
const count = envelope2.screenshot.errors.length;
|
|
7506
9051
|
deps.log(` console ${count} ${count === 1 ? "error" : "errors"} on load`);
|