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/bin.js
CHANGED
|
@@ -968,9 +968,9 @@ function agentSearchPath(env = process.env, platform = process.platform) {
|
|
|
968
968
|
function agentEnvironment(env = process.env) {
|
|
969
969
|
return { ...env, PATH: agentSearchPath(env) };
|
|
970
970
|
}
|
|
971
|
-
async function pathLookup(binary) {
|
|
972
|
-
const entries = agentSearchPath().split(delimiter).filter((entry) => entry !== "");
|
|
973
|
-
const extensions =
|
|
971
|
+
async function pathLookup(binary, env = process.env, platform = process.platform) {
|
|
972
|
+
const entries = agentSearchPath(env, platform).split(delimiter).filter((entry) => entry !== "");
|
|
973
|
+
const extensions = platform === "win32" ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter((entry) => entry !== "") : [""];
|
|
974
974
|
for (const entry of entries) {
|
|
975
975
|
for (const extension of extensions) {
|
|
976
976
|
try {
|
|
@@ -982,10 +982,10 @@ async function pathLookup(binary) {
|
|
|
982
982
|
}
|
|
983
983
|
return false;
|
|
984
984
|
}
|
|
985
|
-
async function detectAgents(
|
|
985
|
+
async function detectAgents(lookup2 = pathLookup, probe2 = execProbe) {
|
|
986
986
|
const entries = Object.entries(KNOWN_AGENTS);
|
|
987
987
|
return Promise.all(entries.map(async ([id, adapter]) => {
|
|
988
|
-
const available = await
|
|
988
|
+
const available = await lookup2(adapter.binary).catch(() => false);
|
|
989
989
|
if (!available) {
|
|
990
990
|
return {
|
|
991
991
|
id,
|
|
@@ -1414,6 +1414,13 @@ async function dropLocalPreviews(cwd, titles) {
|
|
|
1414
1414
|
// ../server/dist/proxy.js
|
|
1415
1415
|
import http, {} from "http";
|
|
1416
1416
|
import net from "net";
|
|
1417
|
+
var SHARE_COOKIE = "leglas-share";
|
|
1418
|
+
function withoutShareCookie(cookie) {
|
|
1419
|
+
if (cookie === void 0)
|
|
1420
|
+
return void 0;
|
|
1421
|
+
const kept = (Array.isArray(cookie) ? cookie.join("; ") : cookie).split(";").map((entry) => entry.trim()).filter((entry) => entry !== "" && !entry.startsWith(`${SHARE_COOKIE}=`));
|
|
1422
|
+
return kept.length === 0 ? void 0 : kept.join("; ");
|
|
1423
|
+
}
|
|
1417
1424
|
function createProxyHandler(options) {
|
|
1418
1425
|
const target = new URL(options.target);
|
|
1419
1426
|
const host = target.hostname;
|
|
@@ -1421,19 +1428,25 @@ function createProxyHandler(options) {
|
|
|
1421
1428
|
const port = Number(target.port || (target.protocol === "https:" ? 443 : 80));
|
|
1422
1429
|
const authority = target.port ? `${host}:${target.port}` : host;
|
|
1423
1430
|
function upstreamHeaders(req) {
|
|
1424
|
-
|
|
1431
|
+
const headers = { ...req.headers, host: authority };
|
|
1432
|
+
const cookie = withoutShareCookie(headers.cookie);
|
|
1433
|
+
if (cookie === void 0)
|
|
1434
|
+
delete headers.cookie;
|
|
1435
|
+
else
|
|
1436
|
+
headers.cookie = cookie;
|
|
1437
|
+
return headers;
|
|
1425
1438
|
}
|
|
1426
|
-
function rewriteLocation(location,
|
|
1439
|
+
function rewriteLocation(location, publicOrigin2) {
|
|
1427
1440
|
if (location === void 0)
|
|
1428
1441
|
return void 0;
|
|
1429
1442
|
for (const origin of [`${target.protocol}//${authority}`, `${target.protocol}//localhost:${port}`]) {
|
|
1430
1443
|
if (location.startsWith(origin))
|
|
1431
|
-
return
|
|
1444
|
+
return publicOrigin2 + location.slice(origin.length);
|
|
1432
1445
|
}
|
|
1433
1446
|
return location;
|
|
1434
1447
|
}
|
|
1435
1448
|
return {
|
|
1436
|
-
request(req, res,
|
|
1449
|
+
request(req, res, publicOrigin2) {
|
|
1437
1450
|
options.onActivity?.();
|
|
1438
1451
|
options.onOpen?.();
|
|
1439
1452
|
let open = true;
|
|
@@ -1448,7 +1461,7 @@ function createProxyHandler(options) {
|
|
|
1448
1461
|
res.once("close", close);
|
|
1449
1462
|
const upstream = http.request({ host: dialHost, port, method: req.method, path: req.url, headers: upstreamHeaders(req) }, (upstreamRes) => {
|
|
1450
1463
|
const headers = { ...upstreamRes.headers };
|
|
1451
|
-
const location = rewriteLocation(typeof headers.location === "string" ? headers.location : void 0,
|
|
1464
|
+
const location = rewriteLocation(typeof headers.location === "string" ? headers.location : void 0, publicOrigin2);
|
|
1452
1465
|
if (location !== void 0)
|
|
1453
1466
|
headers.location = location;
|
|
1454
1467
|
res.writeHead(upstreamRes.statusCode ?? 502, headers);
|
|
@@ -2211,6 +2224,32 @@ function createBrowserPool(options = {}) {
|
|
|
2211
2224
|
};
|
|
2212
2225
|
}
|
|
2213
2226
|
|
|
2227
|
+
// ../server/dist/hydration.js
|
|
2228
|
+
function hydrationEvidence(messages) {
|
|
2229
|
+
for (const raw of messages) {
|
|
2230
|
+
const message2 = raw.split("\n", 1)[0]?.trim() ?? "";
|
|
2231
|
+
if (/Minified React error #(418|419|422|423|425)\b/.test(message2)) {
|
|
2232
|
+
return { framework: "React", message: message2 };
|
|
2233
|
+
}
|
|
2234
|
+
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)) {
|
|
2235
|
+
return { framework: "React", message: message2 };
|
|
2236
|
+
}
|
|
2237
|
+
if (/Hydration (node|text|children|class|style|attribute) mismatch/i.test(message2) || /Hydration completed but contains mismatches/i.test(message2)) {
|
|
2238
|
+
return { framework: "Vue", message: message2 };
|
|
2239
|
+
}
|
|
2240
|
+
if (/hydration_mismatch/.test(message2)) {
|
|
2241
|
+
return { framework: "Svelte", message: message2 };
|
|
2242
|
+
}
|
|
2243
|
+
if (/Hydration Mismatch\. Unable to find DOM nodes/.test(message2)) {
|
|
2244
|
+
return { framework: "Solid", message: message2 };
|
|
2245
|
+
}
|
|
2246
|
+
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))) {
|
|
2247
|
+
return { framework: "the app", message: message2 };
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
return null;
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2214
2253
|
// ../server/dist/capture.js
|
|
2215
2254
|
var FRAME_MAX_HEIGHT = 4e3;
|
|
2216
2255
|
var MIN_WIDTH = 320;
|
|
@@ -2304,10 +2343,12 @@ function locatorExpression(focus) {
|
|
|
2304
2343
|
async function render(page, input) {
|
|
2305
2344
|
const width = clamp(Math.round(input.width), MIN_WIDTH, MAX_WIDTH);
|
|
2306
2345
|
const errors = [];
|
|
2346
|
+
let hydration = null;
|
|
2307
2347
|
const remember = (value) => {
|
|
2348
|
+
const message2 = String(value ?? "").trim().slice(0, 240);
|
|
2349
|
+
hydration ??= hydrationEvidence([message2]);
|
|
2308
2350
|
if (errors.length >= 10)
|
|
2309
2351
|
return;
|
|
2310
|
-
const message2 = String(value ?? "").slice(0, 240);
|
|
2311
2352
|
if (message2 === "" || /favicon/i.test(message2))
|
|
2312
2353
|
return;
|
|
2313
2354
|
errors.push(message2);
|
|
@@ -2455,7 +2496,7 @@ async function render(page, input) {
|
|
|
2455
2496
|
resolved
|
|
2456
2497
|
});
|
|
2457
2498
|
}
|
|
2458
|
-
return { frame, crops, errors, cut };
|
|
2499
|
+
return { frame, crops, errors, hydration, cut };
|
|
2459
2500
|
} finally {
|
|
2460
2501
|
for (const stop of unlisten)
|
|
2461
2502
|
stop();
|
|
@@ -2599,17 +2640,23 @@ async function attachRequest(cwd, requestId, input, deps) {
|
|
|
2599
2640
|
const capture = deps.capture ?? capturePage;
|
|
2600
2641
|
const deadlineMs = deps.deadlineMs ?? 12e3;
|
|
2601
2642
|
const destination = join5(cwd, CAPTURES_DIR, requestId);
|
|
2602
|
-
const captured = {
|
|
2643
|
+
const captured = {
|
|
2644
|
+
attachments: [],
|
|
2645
|
+
errors: [],
|
|
2646
|
+
hydration: null,
|
|
2647
|
+
cut: false,
|
|
2648
|
+
skipped: null
|
|
2649
|
+
};
|
|
2603
2650
|
const references = [];
|
|
2604
2651
|
requestedWidths.set(captured, Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, Math.round(input.width))));
|
|
2605
2652
|
const controller = new AbortController();
|
|
2606
|
-
let
|
|
2653
|
+
let expired2 = false;
|
|
2607
2654
|
let finishDeadline;
|
|
2608
2655
|
const deadline = new Promise((resolve5) => {
|
|
2609
2656
|
finishDeadline = resolve5;
|
|
2610
2657
|
});
|
|
2611
2658
|
const timer = setTimeout(() => {
|
|
2612
|
-
|
|
2659
|
+
expired2 = true;
|
|
2613
2660
|
controller.abort();
|
|
2614
2661
|
finishDeadline();
|
|
2615
2662
|
}, deadlineMs);
|
|
@@ -2622,7 +2669,7 @@ async function attachRequest(cwd, requestId, input, deps) {
|
|
|
2622
2669
|
const work = (async () => {
|
|
2623
2670
|
try {
|
|
2624
2671
|
const browser = await deps.pool.acquire();
|
|
2625
|
-
if (
|
|
2672
|
+
if (expired2)
|
|
2626
2673
|
return;
|
|
2627
2674
|
if (browser === null) {
|
|
2628
2675
|
captured.skipped = deps.pool.reason() ?? NO_BROWSER;
|
|
@@ -2636,7 +2683,7 @@ async function attachRequest(cwd, requestId, input, deps) {
|
|
|
2636
2683
|
signal: controller.signal
|
|
2637
2684
|
};
|
|
2638
2685
|
const direction = await capture(browser, directionInput);
|
|
2639
|
-
if (
|
|
2686
|
+
if (expired2)
|
|
2640
2687
|
return;
|
|
2641
2688
|
await mkdir3(destination, { recursive: true });
|
|
2642
2689
|
await writeFile3(join5(destination, "frame.png"), direction.frame.png);
|
|
@@ -2649,11 +2696,12 @@ async function attachRequest(cwd, requestId, input, deps) {
|
|
|
2649
2696
|
viewport: direction.frame.width
|
|
2650
2697
|
});
|
|
2651
2698
|
captured.errors = direction.errors;
|
|
2699
|
+
captured.hydration = direction.hydration;
|
|
2652
2700
|
captured.cut = direction.cut;
|
|
2653
2701
|
for (let index = 0; index < direction.crops.length; index += 1) {
|
|
2654
2702
|
const crop = direction.crops[index];
|
|
2655
2703
|
const note = input.notes[index];
|
|
2656
|
-
if (crop === null || crop === void 0 || note === void 0 ||
|
|
2704
|
+
if (crop === null || crop === void 0 || note === void 0 || expired2)
|
|
2657
2705
|
continue;
|
|
2658
2706
|
const name = `note-${index + 1}.png`;
|
|
2659
2707
|
await writeFile3(join5(destination, name), crop.shot.png);
|
|
@@ -2667,7 +2715,7 @@ async function attachRequest(cwd, requestId, input, deps) {
|
|
|
2667
2715
|
viewport: direction.frame.width
|
|
2668
2716
|
});
|
|
2669
2717
|
}
|
|
2670
|
-
if (input.compare !== null && !
|
|
2718
|
+
if (input.compare !== null && !expired2) {
|
|
2671
2719
|
const compareInput = {
|
|
2672
2720
|
url: previewUrl(input.origin, input.compare),
|
|
2673
2721
|
width: input.width,
|
|
@@ -2675,7 +2723,7 @@ async function attachRequest(cwd, requestId, input, deps) {
|
|
|
2675
2723
|
signal: controller.signal
|
|
2676
2724
|
};
|
|
2677
2725
|
const comparison = await capture(browser, compareInput);
|
|
2678
|
-
if (
|
|
2726
|
+
if (expired2)
|
|
2679
2727
|
return;
|
|
2680
2728
|
await writeFile3(join5(destination, "compare.png"), comparison.frame.png);
|
|
2681
2729
|
captured.attachments.push({
|
|
@@ -2688,14 +2736,14 @@ async function attachRequest(cwd, requestId, input, deps) {
|
|
|
2688
2736
|
});
|
|
2689
2737
|
}
|
|
2690
2738
|
} catch (error) {
|
|
2691
|
-
if (!
|
|
2739
|
+
if (!expired2) {
|
|
2692
2740
|
captured.skipped = error instanceof Error ? error.message : `The page did not load: ${String(error)}`;
|
|
2693
2741
|
}
|
|
2694
2742
|
}
|
|
2695
2743
|
})();
|
|
2696
2744
|
await Promise.race([work, deadline]);
|
|
2697
2745
|
clearTimeout(timer);
|
|
2698
|
-
if (
|
|
2746
|
+
if (expired2)
|
|
2699
2747
|
captured.skipped = "The design could not be captured in time.";
|
|
2700
2748
|
captured.attachments.push(...references);
|
|
2701
2749
|
return captured;
|
|
@@ -3344,10 +3392,10 @@ function scope(leglasCommand, quotedTitle) {
|
|
|
3344
3392
|
|
|
3345
3393
|
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.
|
|
3346
3394
|
|
|
3347
|
-
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.`;
|
|
3395
|
+
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.`;
|
|
3348
3396
|
}
|
|
3349
3397
|
function capturedBlock(captured) {
|
|
3350
|
-
if (captured === null || captured.attachments.length === 0 && captured.errors.length === 0 && captured.skipped === null)
|
|
3398
|
+
if (captured === null || captured.attachments.length === 0 && captured.errors.length === 0 && captured.hydration === null && captured.skipped === null)
|
|
3351
3399
|
return "";
|
|
3352
3400
|
const lines2 = [];
|
|
3353
3401
|
const frames = captured.attachments.filter((attachment) => attachment.kind === "frame" || attachment.kind === "note");
|
|
@@ -3379,6 +3427,9 @@ function capturedBlock(captured) {
|
|
|
3379
3427
|
for (const error of captured.errors)
|
|
3380
3428
|
lines2.push(` - ${error}`);
|
|
3381
3429
|
}
|
|
3430
|
+
if (captured.hydration !== null) {
|
|
3431
|
+
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.`);
|
|
3432
|
+
}
|
|
3382
3433
|
if (captured.skipped !== null) {
|
|
3383
3434
|
lines2.push(`(${captured.skipped} Use the live preview instead.)`);
|
|
3384
3435
|
}
|
|
@@ -4979,10 +5030,14 @@ function createCoalescer(emit, options = {}) {
|
|
|
4979
5030
|
}
|
|
4980
5031
|
};
|
|
4981
5032
|
}
|
|
4982
|
-
function createLiveHub(
|
|
5033
|
+
function createLiveHub(options = {}) {
|
|
4983
5034
|
const listeners = /* @__PURE__ */ new Set();
|
|
5035
|
+
let viewers = 0;
|
|
4984
5036
|
const drop = (listener) => {
|
|
4985
|
-
listeners.delete(listener)
|
|
5037
|
+
if (!listeners.delete(listener) || !listener.viewer)
|
|
5038
|
+
return;
|
|
5039
|
+
viewers = Math.max(0, viewers - 1);
|
|
5040
|
+
options.onViewers?.(viewers);
|
|
4986
5041
|
};
|
|
4987
5042
|
const write2 = (listener, opcode, payload) => {
|
|
4988
5043
|
if (listener.socket.destroyed || !listener.socket.writable) {
|
|
@@ -5067,7 +5122,7 @@ function createLiveHub(_options = {}) {
|
|
|
5067
5122
|
}
|
|
5068
5123
|
}
|
|
5069
5124
|
},
|
|
5070
|
-
upgrade: (req, socket, head) => {
|
|
5125
|
+
upgrade: (req, socket, head, upgradeOptions = {}) => {
|
|
5071
5126
|
const path = (req.url ?? "/").split("?")[0] ?? "/";
|
|
5072
5127
|
if (req.method !== "GET" || path !== LIVE_PATH)
|
|
5073
5128
|
return false;
|
|
@@ -5089,8 +5144,16 @@ Sec-WebSocket-Accept: ${accept}\r
|
|
|
5089
5144
|
socket.destroy();
|
|
5090
5145
|
return false;
|
|
5091
5146
|
}
|
|
5092
|
-
const listener = {
|
|
5147
|
+
const listener = {
|
|
5148
|
+
socket,
|
|
5149
|
+
buffered: Buffer.alloc(0),
|
|
5150
|
+
viewer: upgradeOptions.viewer === true
|
|
5151
|
+
};
|
|
5093
5152
|
listeners.add(listener);
|
|
5153
|
+
if (listener.viewer) {
|
|
5154
|
+
viewers += 1;
|
|
5155
|
+
options.onViewers?.(viewers);
|
|
5156
|
+
}
|
|
5094
5157
|
socket.on("data", (chunk) => read(listener, chunk));
|
|
5095
5158
|
socket.once("error", () => drop(listener));
|
|
5096
5159
|
socket.once("end", () => drop(listener));
|
|
@@ -5108,7 +5171,1327 @@ Sec-WebSocket-Accept: ${accept}\r
|
|
|
5108
5171
|
},
|
|
5109
5172
|
get listening() {
|
|
5110
5173
|
return listeners.size;
|
|
5174
|
+
},
|
|
5175
|
+
get viewers() {
|
|
5176
|
+
return viewers;
|
|
5177
|
+
}
|
|
5178
|
+
};
|
|
5179
|
+
}
|
|
5180
|
+
|
|
5181
|
+
// ../server/dist/share.js
|
|
5182
|
+
import { randomBytes as randomBytes4, randomUUID, timingSafeEqual } from "crypto";
|
|
5183
|
+
import http3 from "http";
|
|
5184
|
+
import { posix } from "path";
|
|
5185
|
+
|
|
5186
|
+
// ../server/dist/tunnel.js
|
|
5187
|
+
import { spawn as spawnChild } from "child_process";
|
|
5188
|
+
import { Resolver, lookup } from "dns/promises";
|
|
5189
|
+
import http2 from "http";
|
|
5190
|
+
import https from "https";
|
|
5191
|
+
var URL_DEADLINE_MS = 3e4;
|
|
5192
|
+
var PROBE_DEADLINE_MS = 3e4;
|
|
5193
|
+
var PROBE_INTERVAL_MS = 1500;
|
|
5194
|
+
var SLOW_PROBE_INTERVAL_MS = 4e3;
|
|
5195
|
+
var SLOW_PROBE_CAP_MS = 3e4;
|
|
5196
|
+
var STOP_GRACE_MS = 3e3;
|
|
5197
|
+
var STOP_LIMIT_MS = STOP_GRACE_MS + 2e3;
|
|
5198
|
+
var PROBE_TIMEOUT_MS2 = 3e3;
|
|
5199
|
+
async function askLink(resolver, url, entryPath) {
|
|
5200
|
+
let target;
|
|
5201
|
+
try {
|
|
5202
|
+
target = new URL(url);
|
|
5203
|
+
} catch {
|
|
5204
|
+
return false;
|
|
5205
|
+
}
|
|
5206
|
+
let address;
|
|
5207
|
+
try {
|
|
5208
|
+
[address] = await resolver.resolve4(target.hostname);
|
|
5209
|
+
} catch {
|
|
5210
|
+
try {
|
|
5211
|
+
address = (await lookup(target.hostname, { family: 4 })).address;
|
|
5212
|
+
} catch {
|
|
5213
|
+
return false;
|
|
5214
|
+
}
|
|
5215
|
+
}
|
|
5216
|
+
if (address === void 0)
|
|
5217
|
+
return false;
|
|
5218
|
+
const secure = target.protocol === "https:";
|
|
5219
|
+
return new Promise((resolve5) => {
|
|
5220
|
+
const request = (secure ? https : http2).request({
|
|
5221
|
+
host: address,
|
|
5222
|
+
port: Number(target.port || (secure ? 443 : 80)),
|
|
5223
|
+
path: entryPath,
|
|
5224
|
+
method: "GET",
|
|
5225
|
+
headers: { host: target.host },
|
|
5226
|
+
...secure ? { servername: target.hostname } : {},
|
|
5227
|
+
timeout: PROBE_TIMEOUT_MS2
|
|
5228
|
+
}, (response) => {
|
|
5229
|
+
response.resume();
|
|
5230
|
+
const status = response.statusCode ?? 0;
|
|
5231
|
+
resolve5(status >= 200 && status < 400);
|
|
5232
|
+
});
|
|
5233
|
+
request.once("timeout", () => request.destroy());
|
|
5234
|
+
request.once("error", () => resolve5(false));
|
|
5235
|
+
request.end();
|
|
5236
|
+
});
|
|
5237
|
+
}
|
|
5238
|
+
async function detectTunnels(env = process.env) {
|
|
5239
|
+
const providers = ["cloudflared", "ngrok"];
|
|
5240
|
+
const found = await Promise.all(providers.map((provider) => pathLookup(provider, env).catch(() => false)));
|
|
5241
|
+
return providers.filter((_provider, index) => found[index] === true);
|
|
5242
|
+
}
|
|
5243
|
+
function duration(ms) {
|
|
5244
|
+
return ms % 1e3 === 0 ? `${ms / 1e3}s` : `${ms}ms`;
|
|
5245
|
+
}
|
|
5246
|
+
function withOutput(sentence, output) {
|
|
5247
|
+
if (output === "")
|
|
5248
|
+
return sentence;
|
|
5249
|
+
const stem = sentence.endsWith(".") ? sentence.slice(0, -1) : sentence;
|
|
5250
|
+
return `${stem} (${output.slice(0, 160)}).`;
|
|
5251
|
+
}
|
|
5252
|
+
function startTunnel(options, deps = {}) {
|
|
5253
|
+
const spawn5 = deps.spawn ?? spawnChild;
|
|
5254
|
+
const now = deps.now ?? Date.now;
|
|
5255
|
+
const urlDeadlineMs = deps.urlDeadlineMs ?? URL_DEADLINE_MS;
|
|
5256
|
+
const probeDeadlineMs = deps.probeDeadlineMs ?? PROBE_DEADLINE_MS;
|
|
5257
|
+
const resolver = deps.probe === void 0 ? new Resolver() : null;
|
|
5258
|
+
const probe2 = deps.probe ?? ((url2) => askLink(resolver, url2, options.entryPath));
|
|
5259
|
+
const timers = /* @__PURE__ */ new Set();
|
|
5260
|
+
const later = (callback, ms) => {
|
|
5261
|
+
const timer = setTimeout(() => {
|
|
5262
|
+
timers.delete(timer);
|
|
5263
|
+
callback();
|
|
5264
|
+
}, ms);
|
|
5265
|
+
timer.unref?.();
|
|
5266
|
+
timers.add(timer);
|
|
5267
|
+
return timer;
|
|
5268
|
+
};
|
|
5269
|
+
const clearTimers = () => {
|
|
5270
|
+
for (const timer of timers)
|
|
5271
|
+
clearTimeout(timer);
|
|
5272
|
+
timers.clear();
|
|
5273
|
+
};
|
|
5274
|
+
let lastState = "";
|
|
5275
|
+
let state = { status: "starting", provider: options.provider };
|
|
5276
|
+
let terminal = false;
|
|
5277
|
+
let stopping = false;
|
|
5278
|
+
let exited = false;
|
|
5279
|
+
let stopPromise = null;
|
|
5280
|
+
let settleStop = null;
|
|
5281
|
+
let url = null;
|
|
5282
|
+
let urlAt = 0;
|
|
5283
|
+
let urlTimer = null;
|
|
5284
|
+
let lastLine = "";
|
|
5285
|
+
const partial = { stdout: "", stderr: "" };
|
|
5286
|
+
const report = (next) => {
|
|
5287
|
+
const serialized = JSON.stringify(next);
|
|
5288
|
+
if (serialized === lastState)
|
|
5289
|
+
return;
|
|
5290
|
+
lastState = serialized;
|
|
5291
|
+
state = next;
|
|
5292
|
+
options.onState(next);
|
|
5293
|
+
};
|
|
5294
|
+
const fail = (reason) => {
|
|
5295
|
+
if (terminal)
|
|
5296
|
+
return;
|
|
5297
|
+
terminal = true;
|
|
5298
|
+
clearTimers();
|
|
5299
|
+
report({
|
|
5300
|
+
status: "failed",
|
|
5301
|
+
provider: options.provider,
|
|
5302
|
+
reason,
|
|
5303
|
+
...url === null ? {} : { url }
|
|
5304
|
+
});
|
|
5305
|
+
};
|
|
5306
|
+
report(state);
|
|
5307
|
+
const args = options.provider === "cloudflared" ? [
|
|
5308
|
+
"tunnel",
|
|
5309
|
+
"--url",
|
|
5310
|
+
`http://127.0.0.1:${options.port}`,
|
|
5311
|
+
"--no-autoupdate"
|
|
5312
|
+
] : ["http", String(options.port), "--log", "stdout", "--log-format", "json"];
|
|
5313
|
+
let child;
|
|
5314
|
+
try {
|
|
5315
|
+
child = spawn5(options.provider, args, {
|
|
5316
|
+
env: agentEnvironment(),
|
|
5317
|
+
shell: false,
|
|
5318
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
5319
|
+
});
|
|
5320
|
+
} catch {
|
|
5321
|
+
fail(`${options.provider} exited before the tunnel came up.`);
|
|
5322
|
+
return { settle: () => {
|
|
5323
|
+
}, stop: async () => {
|
|
5324
|
+
} };
|
|
5325
|
+
}
|
|
5326
|
+
const beginProbe = (found) => {
|
|
5327
|
+
if (url !== null || terminal || stopping)
|
|
5328
|
+
return;
|
|
5329
|
+
url = found;
|
|
5330
|
+
urlAt = now();
|
|
5331
|
+
if (urlTimer !== null) {
|
|
5332
|
+
clearTimeout(urlTimer);
|
|
5333
|
+
timers.delete(urlTimer);
|
|
5334
|
+
urlTimer = null;
|
|
5335
|
+
}
|
|
5336
|
+
report({ status: "starting", provider: options.provider, url });
|
|
5337
|
+
later(() => {
|
|
5338
|
+
if (terminal || stopping || url === null)
|
|
5339
|
+
return;
|
|
5340
|
+
report({ status: "starting", provider: options.provider, url, slow: true });
|
|
5341
|
+
}, probeDeadlineMs);
|
|
5342
|
+
let slowWait = SLOW_PROBE_INTERVAL_MS;
|
|
5343
|
+
const again = () => {
|
|
5344
|
+
if (terminal || stopping || url === null)
|
|
5345
|
+
return;
|
|
5346
|
+
if (now() - urlAt < probeDeadlineMs) {
|
|
5347
|
+
later(poll, PROBE_INTERVAL_MS);
|
|
5348
|
+
return;
|
|
5349
|
+
}
|
|
5350
|
+
later(poll, slowWait);
|
|
5351
|
+
slowWait = Math.min(SLOW_PROBE_CAP_MS, slowWait * 2);
|
|
5352
|
+
};
|
|
5353
|
+
const poll = () => {
|
|
5354
|
+
if (terminal || stopping || url === null)
|
|
5355
|
+
return;
|
|
5356
|
+
void probe2(url).then((reachable) => {
|
|
5357
|
+
if (terminal || stopping || url === null)
|
|
5358
|
+
return;
|
|
5359
|
+
if (reachable) {
|
|
5360
|
+
terminal = true;
|
|
5361
|
+
clearTimers();
|
|
5362
|
+
report({ status: "ready", provider: options.provider, url });
|
|
5363
|
+
return;
|
|
5364
|
+
}
|
|
5365
|
+
again();
|
|
5366
|
+
}, again);
|
|
5367
|
+
};
|
|
5368
|
+
poll();
|
|
5369
|
+
};
|
|
5370
|
+
const inspect = (stream, line) => {
|
|
5371
|
+
const trimmed = line.trim();
|
|
5372
|
+
if (trimmed !== "")
|
|
5373
|
+
lastLine = trimmed;
|
|
5374
|
+
if (url !== null || trimmed === "")
|
|
5375
|
+
return;
|
|
5376
|
+
if (options.provider === "cloudflared") {
|
|
5377
|
+
const found = /https:\/\/(?!api\.)[a-z0-9-]+\.trycloudflare\.com/.exec(trimmed)?.[0];
|
|
5378
|
+
if (found !== void 0)
|
|
5379
|
+
beginProbe(found);
|
|
5380
|
+
return;
|
|
5381
|
+
}
|
|
5382
|
+
if (stream !== "stdout")
|
|
5383
|
+
return;
|
|
5384
|
+
try {
|
|
5385
|
+
const event = JSON.parse(trimmed);
|
|
5386
|
+
const candidate = typeof event.url === "string" && event.url.startsWith("https://") ? event.url : null;
|
|
5387
|
+
if (candidate !== null)
|
|
5388
|
+
beginProbe(candidate);
|
|
5389
|
+
} catch {
|
|
5390
|
+
}
|
|
5391
|
+
};
|
|
5392
|
+
const read = (stream, chunk) => {
|
|
5393
|
+
const combined = partial[stream] + (Buffer.isBuffer(chunk) ? chunk.toString() : chunk);
|
|
5394
|
+
const lines2 = combined.split(/\r?\n/);
|
|
5395
|
+
partial[stream] = lines2.pop() ?? "";
|
|
5396
|
+
for (const line of lines2)
|
|
5397
|
+
inspect(stream, line);
|
|
5398
|
+
if (partial[stream] !== "")
|
|
5399
|
+
inspect(stream, partial[stream]);
|
|
5400
|
+
};
|
|
5401
|
+
child.stdout?.on("data", (chunk) => read("stdout", chunk));
|
|
5402
|
+
child.stderr?.on("data", (chunk) => read("stderr", chunk));
|
|
5403
|
+
const onExit = () => {
|
|
5404
|
+
if (exited)
|
|
5405
|
+
return;
|
|
5406
|
+
exited = true;
|
|
5407
|
+
clearTimers();
|
|
5408
|
+
settleStop?.();
|
|
5409
|
+
settleStop = null;
|
|
5410
|
+
if (stopping)
|
|
5411
|
+
return;
|
|
5412
|
+
if (state.status === "ready") {
|
|
5413
|
+
terminal = true;
|
|
5414
|
+
report({
|
|
5415
|
+
status: "failed",
|
|
5416
|
+
provider: options.provider,
|
|
5417
|
+
url: state.url,
|
|
5418
|
+
reason: "The tunnel process exited."
|
|
5419
|
+
});
|
|
5420
|
+
return;
|
|
5421
|
+
}
|
|
5422
|
+
fail(withOutput(`${options.provider} exited before the tunnel came up.`, lastLine));
|
|
5423
|
+
};
|
|
5424
|
+
child.once("error", onExit);
|
|
5425
|
+
child.once("exit", onExit);
|
|
5426
|
+
urlTimer = later(() => fail(withOutput(`${options.provider} did not report a URL within ${duration(urlDeadlineMs)}.`, lastLine)), urlDeadlineMs);
|
|
5427
|
+
return {
|
|
5428
|
+
settle() {
|
|
5429
|
+
if (terminal || stopping || url === null)
|
|
5430
|
+
return;
|
|
5431
|
+
terminal = true;
|
|
5432
|
+
clearTimers();
|
|
5433
|
+
report({ status: "ready", provider: options.provider, url });
|
|
5434
|
+
},
|
|
5435
|
+
stop() {
|
|
5436
|
+
if (stopPromise !== null)
|
|
5437
|
+
return stopPromise;
|
|
5438
|
+
stopping = true;
|
|
5439
|
+
clearTimers();
|
|
5440
|
+
if (exited)
|
|
5441
|
+
return Promise.resolve();
|
|
5442
|
+
stopPromise = new Promise((resolve5) => {
|
|
5443
|
+
let settled = false;
|
|
5444
|
+
const done = () => {
|
|
5445
|
+
if (settled)
|
|
5446
|
+
return;
|
|
5447
|
+
settled = true;
|
|
5448
|
+
clearTimers();
|
|
5449
|
+
settleStop = null;
|
|
5450
|
+
resolve5();
|
|
5451
|
+
};
|
|
5452
|
+
settleStop = done;
|
|
5453
|
+
try {
|
|
5454
|
+
child.kill("SIGTERM");
|
|
5455
|
+
} catch {
|
|
5456
|
+
done();
|
|
5457
|
+
return;
|
|
5458
|
+
}
|
|
5459
|
+
later(() => {
|
|
5460
|
+
if (exited)
|
|
5461
|
+
return done();
|
|
5462
|
+
try {
|
|
5463
|
+
child.kill("SIGKILL");
|
|
5464
|
+
} catch {
|
|
5465
|
+
}
|
|
5466
|
+
}, STOP_GRACE_MS);
|
|
5467
|
+
later(done, STOP_LIMIT_MS);
|
|
5468
|
+
});
|
|
5469
|
+
return stopPromise;
|
|
5470
|
+
}
|
|
5471
|
+
};
|
|
5472
|
+
}
|
|
5473
|
+
|
|
5474
|
+
// ../server/dist/share.js
|
|
5475
|
+
var MAX_REFUSED = 40;
|
|
5476
|
+
function routeAllowed(routes, url) {
|
|
5477
|
+
const [rawPath = "/"] = url.split("?", 2);
|
|
5478
|
+
const path = canonical(rawPath);
|
|
5479
|
+
return routes.some((route) => {
|
|
5480
|
+
const prefix = route.endsWith("/") && route !== "/";
|
|
5481
|
+
return prefix ? path.startsWith(route) || `${path}/` === route : path === route || path === `${route}/`;
|
|
5482
|
+
});
|
|
5483
|
+
}
|
|
5484
|
+
var OWN_PREFIX = "/leglas";
|
|
5485
|
+
var ENTRY_PREFIX = `${OWN_PREFIX}/s/`;
|
|
5486
|
+
var DEFAULT_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
5487
|
+
var MAX_GRANTS = 16;
|
|
5488
|
+
var MAX_TOMBSTONES = 32;
|
|
5489
|
+
var VIEWER_CONCURRENCY = 12;
|
|
5490
|
+
var VIEWER_QUEUE = 128;
|
|
5491
|
+
var VIEWER_DEADLINE_MS = 3e4;
|
|
5492
|
+
var DEV_CONTROL_ROUTES = [
|
|
5493
|
+
/** read, Vite 8.2.2: opens `?file=` in the machine's editor. Rsbuild 2.2.3 too. */
|
|
5494
|
+
"/__open-in-editor",
|
|
5495
|
+
/** read, react-dev-utils 12.0.1: the same, older name. */
|
|
5496
|
+
"/__open-stack-frame-in-editor",
|
|
5497
|
+
/** read, react-dev-utils 12.0.1: serves a module's source through the overlay. */
|
|
5498
|
+
"/__get-internal-source",
|
|
5499
|
+
/** read, vite-plugin-inspect 12.0.2: the module graph and every transformed source. */
|
|
5500
|
+
"/__inspect",
|
|
5501
|
+
/** read, vite-plugin-vue-devtools 8.2.1: its whole interface and RPC surface. */
|
|
5502
|
+
"/__devtools__",
|
|
5503
|
+
/** read, browser-sync 3.0.4: its client surface and server metadata. */
|
|
5504
|
+
"/__browser_sync__",
|
|
5505
|
+
/**
|
|
5506
|
+
* read, webpack-dev-server 6.0.0, which mounts its own surface here: the
|
|
5507
|
+
* file listing, `/webpack-dev-server/invalidate`, which forces a rebuild,
|
|
5508
|
+
* and `/webpack-dev-server/open-editor`, which calls the same launch-editor
|
|
5509
|
+
* package Vite does. The subtree match below takes all three.
|
|
5510
|
+
*/
|
|
5511
|
+
"/webpack-dev-server",
|
|
5512
|
+
/** reported: Rails Web Console, an interactive server-side REPL. */
|
|
5513
|
+
"/__web_console",
|
|
5514
|
+
/** reported: the Better Errors gem, likewise. */
|
|
5515
|
+
"/__better_errors",
|
|
5516
|
+
/** reported: Laravel Ignition, whose solutions endpoint runs code. */
|
|
5517
|
+
"/_ignition",
|
|
5518
|
+
/** reported: Symfony's profiler, which serves traces, config and source. */
|
|
5519
|
+
"/_profiler",
|
|
5520
|
+
/** reported: Symfony's web debug toolbar. */
|
|
5521
|
+
"/_wdt",
|
|
5522
|
+
/** reported: Django Debug Toolbar, which serves settings, SQL and templates. */
|
|
5523
|
+
"/__debug__",
|
|
5524
|
+
/** reported: Go's pprof, where some GETs start expensive profiling. */
|
|
5525
|
+
"/debug/pprof",
|
|
5526
|
+
/** reported: Spring Boot Actuator, which can serve env, beans and heap dumps. */
|
|
5527
|
+
"/actuator",
|
|
5528
|
+
/** reported: Gatsby's development GraphQL surface, schema and content. */
|
|
5529
|
+
"/___graphql"
|
|
5530
|
+
];
|
|
5531
|
+
var DEV_CONTROL_PREFIXES = [
|
|
5532
|
+
/** read, Next 16.3.1. Its app assets sit at `/_next/` and stay allowed. */
|
|
5533
|
+
"/__nextjs_",
|
|
5534
|
+
/**
|
|
5535
|
+
* read, Nuxt DevTools 4.0.0-alpha.16. Nuxt's own bundle is at `/_nuxt/`,
|
|
5536
|
+
* one underscore and a different prefix, so the app is untouched.
|
|
5537
|
+
*/
|
|
5538
|
+
"/__nuxt_devtools__",
|
|
5539
|
+
/**
|
|
5540
|
+
* read, Parcel 2.16.4: `__parcel_launch_editor` reads a `file` parameter
|
|
5541
|
+
* and calls the same launch-editor code Vite does. Beside it sit
|
|
5542
|
+
* `__parcel_source_map`, `__parcel_source_root` and `__parcel_code_frame`,
|
|
5543
|
+
* which serve source, and the HMR and health routes, which a viewer has no
|
|
5544
|
+
* use for: their live-reload socket is already refused.
|
|
5545
|
+
*/
|
|
5546
|
+
"/__parcel_"
|
|
5547
|
+
];
|
|
5548
|
+
var DEV_CONTROL_QUERY_KEYS = ["__debugger__"];
|
|
5549
|
+
function canonical(path) {
|
|
5550
|
+
let form = path;
|
|
5551
|
+
try {
|
|
5552
|
+
form = decodeURIComponent(path);
|
|
5553
|
+
} catch {
|
|
5554
|
+
}
|
|
5555
|
+
return posix.normalize(form.replaceAll("\\", "/").replace(/\/{2,}/g, "/"));
|
|
5556
|
+
}
|
|
5557
|
+
function spellings(path) {
|
|
5558
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5559
|
+
const add = (value) => {
|
|
5560
|
+
seen.add(value);
|
|
5561
|
+
seen.add(value.toLowerCase());
|
|
5562
|
+
};
|
|
5563
|
+
add(path);
|
|
5564
|
+
const forms = [path];
|
|
5565
|
+
try {
|
|
5566
|
+
forms.push(decodeURIComponent(path));
|
|
5567
|
+
} catch {
|
|
5568
|
+
}
|
|
5569
|
+
for (const value of forms) {
|
|
5570
|
+
add(value);
|
|
5571
|
+
for (const slashed of [value, value.replaceAll("\\", "/")]) {
|
|
5572
|
+
const collapsed = slashed.replace(/\/{2,}/g, "/");
|
|
5573
|
+
add(collapsed);
|
|
5574
|
+
add(posix.normalize(collapsed));
|
|
5575
|
+
}
|
|
5576
|
+
}
|
|
5577
|
+
return [...seen];
|
|
5578
|
+
}
|
|
5579
|
+
function isDevControlRequest(url) {
|
|
5580
|
+
const [rawPath = "/", query] = url.split("?", 2);
|
|
5581
|
+
for (const path of spellings(rawPath)) {
|
|
5582
|
+
if (DEV_CONTROL_PREFIXES.some((prefix) => path.startsWith(prefix)))
|
|
5583
|
+
return true;
|
|
5584
|
+
if (DEV_CONTROL_ROUTES.some((route) => path === route || path.startsWith(`${route}/`))) {
|
|
5585
|
+
return true;
|
|
5586
|
+
}
|
|
5587
|
+
}
|
|
5588
|
+
if (query === void 0)
|
|
5589
|
+
return false;
|
|
5590
|
+
const keys = new URLSearchParams(query);
|
|
5591
|
+
return DEV_CONTROL_QUERY_KEYS.some((key) => keys.has(key) || keys.has(key.toUpperCase()));
|
|
5592
|
+
}
|
|
5593
|
+
function isHiddenPath(path) {
|
|
5594
|
+
return spellings(path).some((form) => {
|
|
5595
|
+
const segments = form.split("/");
|
|
5596
|
+
const modules = segments.indexOf("node_modules");
|
|
5597
|
+
return segments.some((segment, at) => {
|
|
5598
|
+
if (!segment.startsWith(".") || segment === "." || segment === "..")
|
|
5599
|
+
return false;
|
|
5600
|
+
const last = at === segments.length - 1;
|
|
5601
|
+
return last || !(modules >= 0 && at > modules);
|
|
5602
|
+
});
|
|
5603
|
+
});
|
|
5604
|
+
}
|
|
5605
|
+
var FILES_PREFIX_PATH = "/leglas/files/";
|
|
5606
|
+
var DETECT_TTL_MS = 1e4;
|
|
5607
|
+
function isRecord3(value) {
|
|
5608
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5609
|
+
}
|
|
5610
|
+
function stringArray(value) {
|
|
5611
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
5612
|
+
}
|
|
5613
|
+
function stringRecord(value) {
|
|
5614
|
+
return isRecord3(value) && Object.values(value).every((entry) => typeof entry === "string");
|
|
5615
|
+
}
|
|
5616
|
+
function layoutFrom(value) {
|
|
5617
|
+
if (!isRecord3(value))
|
|
5618
|
+
return null;
|
|
5619
|
+
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))) {
|
|
5620
|
+
return null;
|
|
5621
|
+
}
|
|
5622
|
+
return {
|
|
5623
|
+
order: [...value.order],
|
|
5624
|
+
renames: { ...value.renames },
|
|
5625
|
+
collapsedFamilies: [...value.collapsedFamilies],
|
|
5626
|
+
compare: value.compare,
|
|
5627
|
+
viewport: value.viewport
|
|
5628
|
+
};
|
|
5629
|
+
}
|
|
5630
|
+
function manifestFrom(value, previews) {
|
|
5631
|
+
if (!isRecord3(value)) {
|
|
5632
|
+
return { ok: false, error: "Share details must be a JSON object." };
|
|
5633
|
+
}
|
|
5634
|
+
const scope2 = value.scope;
|
|
5635
|
+
const layout = layoutFrom(value.layout);
|
|
5636
|
+
if (scope2 !== "direction" && scope2 !== "compare" && scope2 !== "rail" || !stringArray(value.titles) || layout === null) {
|
|
5637
|
+
return { ok: false, error: "Share details need a scope, directions and a complete layout." };
|
|
5638
|
+
}
|
|
5639
|
+
const titles = [...value.titles];
|
|
5640
|
+
if (titles.length === 0) {
|
|
5641
|
+
return { ok: false, error: "Choose at least one direction to share." };
|
|
5642
|
+
}
|
|
5643
|
+
const byTitle = new Map(previews.map((preview) => [preview.title, preview]));
|
|
5644
|
+
const unknown = [...new Set(titles.filter((title) => !byTitle.has(title)))];
|
|
5645
|
+
if (unknown.length > 0) {
|
|
5646
|
+
return {
|
|
5647
|
+
ok: false,
|
|
5648
|
+
error: `Directions are not available to share: ${unknown.join(", ")}.`
|
|
5649
|
+
};
|
|
5650
|
+
}
|
|
5651
|
+
const branches = [
|
|
5652
|
+
...new Set(titles.filter((title) => byTitle.get(title)?.branch !== void 0))
|
|
5653
|
+
];
|
|
5654
|
+
if (branches.length > 0) {
|
|
5655
|
+
return {
|
|
5656
|
+
ok: false,
|
|
5657
|
+
error: `Branch directions can't be shared yet: ${branches.join(", ")}.`
|
|
5658
|
+
};
|
|
5659
|
+
}
|
|
5660
|
+
if (scope2 === "direction" && titles.length > 1) {
|
|
5661
|
+
return { ok: false, error: "A direction share can contain only one direction." };
|
|
5662
|
+
}
|
|
5663
|
+
if (scope2 === "compare" && (titles.length !== 2 || new Set(titles).size !== 2 || layout.compare === null || !titles.includes(layout.compare))) {
|
|
5664
|
+
return {
|
|
5665
|
+
ok: false,
|
|
5666
|
+
error: "A comparison share needs exactly two directions and one of them on the right."
|
|
5667
|
+
};
|
|
5668
|
+
}
|
|
5669
|
+
if (scope2 !== "compare" && layout.compare !== null) {
|
|
5670
|
+
return { ok: false, error: "Only a comparison share can name a right pane." };
|
|
5671
|
+
}
|
|
5672
|
+
const reach = value.reach === "listed" ? "listed" : "open";
|
|
5673
|
+
if (value.reach !== void 0 && value.reach !== "open" && value.reach !== "listed") {
|
|
5674
|
+
return { ok: false, error: "Reach is either open or listed." };
|
|
5675
|
+
}
|
|
5676
|
+
if (value.routes !== void 0 && !stringArray(value.routes)) {
|
|
5677
|
+
return { ok: false, error: "The route list must be an array of paths." };
|
|
5678
|
+
}
|
|
5679
|
+
const routes = [...new Set((value.routes ?? []).map((route) => route.split("?", 1)[0] ?? ""))].filter((route) => route !== "").slice(0, 400);
|
|
5680
|
+
if (routes.some((route) => !route.startsWith("/"))) {
|
|
5681
|
+
return { ok: false, error: "Every route must be a path beginning with a slash." };
|
|
5682
|
+
}
|
|
5683
|
+
const own = titles.flatMap((title) => {
|
|
5684
|
+
const url = byTitle.get(title)?.url;
|
|
5685
|
+
return url === void 0 ? [] : [url.split("?", 1)[0] ?? ""];
|
|
5686
|
+
});
|
|
5687
|
+
return {
|
|
5688
|
+
ok: true,
|
|
5689
|
+
manifest: { scope: scope2, titles, layout, reach, routes: [.../* @__PURE__ */ new Set([...routes, ...own])] }
|
|
5690
|
+
};
|
|
5691
|
+
}
|
|
5692
|
+
function matchOne(candidate, grants) {
|
|
5693
|
+
const received = Buffer.from(candidate, "utf8");
|
|
5694
|
+
let found = null;
|
|
5695
|
+
for (const grant of grants) {
|
|
5696
|
+
const expected = Buffer.from(grant.token, "utf8");
|
|
5697
|
+
if (expected.length !== received.length)
|
|
5698
|
+
continue;
|
|
5699
|
+
if (timingSafeEqual(expected, received))
|
|
5700
|
+
found = grant;
|
|
5701
|
+
}
|
|
5702
|
+
return found;
|
|
5703
|
+
}
|
|
5704
|
+
function grantFor(share, candidate) {
|
|
5705
|
+
return matchOne(candidate, share.grants.values());
|
|
5706
|
+
}
|
|
5707
|
+
function endedGrantFor(share, candidate) {
|
|
5708
|
+
return matchOne(candidate, share.tombstones);
|
|
5709
|
+
}
|
|
5710
|
+
function expired(grant, now, nowMono) {
|
|
5711
|
+
return now >= grant.expiresAt || nowMono >= grant.expiresAtMono;
|
|
5712
|
+
}
|
|
5713
|
+
function cookieToken(req) {
|
|
5714
|
+
const raw = req.headers.cookie;
|
|
5715
|
+
const cookies = (Array.isArray(raw) ? raw.join(";") : raw ?? "").split(";");
|
|
5716
|
+
for (const cookie of cookies) {
|
|
5717
|
+
const separator = cookie.indexOf("=");
|
|
5718
|
+
if (separator === -1)
|
|
5719
|
+
continue;
|
|
5720
|
+
if (cookie.slice(0, separator).trim() !== SHARE_COOKIE)
|
|
5721
|
+
continue;
|
|
5722
|
+
return cookie.slice(separator + 1).trim();
|
|
5723
|
+
}
|
|
5724
|
+
return null;
|
|
5725
|
+
}
|
|
5726
|
+
function forwardedProto(req) {
|
|
5727
|
+
const forwarded = req.headers["x-forwarded-proto"];
|
|
5728
|
+
const first = Array.isArray(forwarded) ? forwarded[0] : forwarded;
|
|
5729
|
+
return first?.split(",", 1)[0]?.trim() || "http";
|
|
5730
|
+
}
|
|
5731
|
+
function publicOrigin(req) {
|
|
5732
|
+
return `${forwardedProto(req)}://${req.headers.host ?? "127.0.0.1"}`;
|
|
5733
|
+
}
|
|
5734
|
+
function throughTunnel(req) {
|
|
5735
|
+
return req.headers["x-forwarded-for"] !== void 0 || req.headers["cf-connecting-ip"] !== void 0 || req.headers["x-forwarded-proto"] !== void 0;
|
|
5736
|
+
}
|
|
5737
|
+
function cloneLayout(layout) {
|
|
5738
|
+
return {
|
|
5739
|
+
...layout,
|
|
5740
|
+
order: [...layout.order],
|
|
5741
|
+
renames: { ...layout.renames },
|
|
5742
|
+
collapsedFamilies: [...layout.collapsedFamilies]
|
|
5743
|
+
};
|
|
5744
|
+
}
|
|
5745
|
+
function sendJson(res, status, body) {
|
|
5746
|
+
res.writeHead(status, {
|
|
5747
|
+
"content-type": "application/json; charset=utf-8",
|
|
5748
|
+
"cache-control": "no-store"
|
|
5749
|
+
});
|
|
5750
|
+
res.end(JSON.stringify(body));
|
|
5751
|
+
}
|
|
5752
|
+
var REFUSALS = {
|
|
5753
|
+
inactive: {
|
|
5754
|
+
status: 403,
|
|
5755
|
+
sentence: "This link isn't active.",
|
|
5756
|
+
title: "This Leglas link isn't active"
|
|
5757
|
+
},
|
|
5758
|
+
expiry: {
|
|
5759
|
+
status: 410,
|
|
5760
|
+
sentence: "This link expired. The person sharing it can send a new one.",
|
|
5761
|
+
title: "This Leglas link expired"
|
|
5762
|
+
},
|
|
5763
|
+
revoke: {
|
|
5764
|
+
status: 410,
|
|
5765
|
+
sentence: "This link was turned off.",
|
|
5766
|
+
title: "This Leglas link was turned off"
|
|
5767
|
+
}
|
|
5768
|
+
};
|
|
5769
|
+
function refuse(req, res, cause = "inactive") {
|
|
5770
|
+
const refusal = REFUSALS[cause];
|
|
5771
|
+
const accept = req.headers.accept;
|
|
5772
|
+
const html = (Array.isArray(accept) ? accept.join(",") : accept ?? "").includes("text/html");
|
|
5773
|
+
if (!html) {
|
|
5774
|
+
return sendJson(res, refusal.status, { ok: false, error: refusal.sentence });
|
|
5775
|
+
}
|
|
5776
|
+
res.writeHead(refusal.status, {
|
|
5777
|
+
"content-type": "text/html; charset=utf-8",
|
|
5778
|
+
"cache-control": "no-store"
|
|
5779
|
+
});
|
|
5780
|
+
res.end(`<!doctype html>
|
|
5781
|
+
<meta charset="utf-8">
|
|
5782
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
5783
|
+
<title>${refusal.title}</title>
|
|
5784
|
+
<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">
|
|
5785
|
+
<main style="max-width:34rem;padding:2rem"><h1 style="font-size:1.25rem">${refusal.title}</h1>
|
|
5786
|
+
<p>${refusal.sentence}</p></main>
|
|
5787
|
+
</body>`);
|
|
5788
|
+
}
|
|
5789
|
+
function bind(server) {
|
|
5790
|
+
return new Promise((resolve5, reject) => {
|
|
5791
|
+
const onError = (error) => {
|
|
5792
|
+
server.removeListener("listening", onListening);
|
|
5793
|
+
reject(error);
|
|
5794
|
+
};
|
|
5795
|
+
const onListening = () => {
|
|
5796
|
+
server.removeListener("error", onError);
|
|
5797
|
+
const address = server.address();
|
|
5798
|
+
resolve5(typeof address === "object" && address !== null ? address.port : 0);
|
|
5799
|
+
};
|
|
5800
|
+
server.once("error", onError);
|
|
5801
|
+
server.once("listening", onListening);
|
|
5802
|
+
server.listen(0, "127.0.0.1");
|
|
5803
|
+
});
|
|
5804
|
+
}
|
|
5805
|
+
function closeListener(share) {
|
|
5806
|
+
return new Promise((resolve5) => {
|
|
5807
|
+
if (share.expiryTimer !== null) {
|
|
5808
|
+
clearTimeout(share.expiryTimer);
|
|
5809
|
+
share.expiryTimer = null;
|
|
5111
5810
|
}
|
|
5811
|
+
for (const socket of share.sockets)
|
|
5812
|
+
socket.destroy();
|
|
5813
|
+
share.sockets.clear();
|
|
5814
|
+
share.server.closeAllConnections();
|
|
5815
|
+
share.server.close(() => resolve5());
|
|
5816
|
+
});
|
|
5817
|
+
}
|
|
5818
|
+
function createShareManager(options) {
|
|
5819
|
+
const detect = options.detectTunnels ?? detectTunnels;
|
|
5820
|
+
const runTunnel = options.startTunnel ?? startTunnel;
|
|
5821
|
+
const now = options.now ?? Date.now;
|
|
5822
|
+
const nowMono = options.nowMono ?? process.hrtime.bigint;
|
|
5823
|
+
const deadlineMs = options.viewerDeadlineMs ?? VIEWER_DEADLINE_MS;
|
|
5824
|
+
let detected = null;
|
|
5825
|
+
let active = null;
|
|
5826
|
+
let creating = false;
|
|
5827
|
+
let stops = 0;
|
|
5828
|
+
let closed = false;
|
|
5829
|
+
let stopPromise = null;
|
|
5830
|
+
let detectedAt = 0;
|
|
5831
|
+
const tunnels = () => {
|
|
5832
|
+
if (detected === null || Date.now() - detectedAt > DETECT_TTL_MS) {
|
|
5833
|
+
detectedAt = Date.now();
|
|
5834
|
+
detected = detect().catch(() => []);
|
|
5835
|
+
}
|
|
5836
|
+
return detected;
|
|
5837
|
+
};
|
|
5838
|
+
const status = () => {
|
|
5839
|
+
const share = active;
|
|
5840
|
+
if (share === null)
|
|
5841
|
+
return null;
|
|
5842
|
+
const tunnelUrl = "url" in share.tunnel ? share.tunnel.url : void 0;
|
|
5843
|
+
const origin = tunnelUrl === void 0 ? null : tunnelUrl.replace(/\/$/, "");
|
|
5844
|
+
return {
|
|
5845
|
+
id: share.id,
|
|
5846
|
+
scope: share.scope,
|
|
5847
|
+
titles: [...share.titles],
|
|
5848
|
+
layout: cloneLayout(share.layout),
|
|
5849
|
+
sharePort: share.port,
|
|
5850
|
+
grants: [...share.grants.values()].toSorted((a, b) => a.createdAt - b.createdAt).map((grant) => {
|
|
5851
|
+
const entryPath = `${ENTRY_PREFIX}${grant.token}`;
|
|
5852
|
+
return {
|
|
5853
|
+
id: grant.id,
|
|
5854
|
+
name: grant.name,
|
|
5855
|
+
url: origin === null ? null : `${origin}${entryPath}`,
|
|
5856
|
+
localUrl: `http://127.0.0.1:${share.port}${entryPath}`,
|
|
5857
|
+
viewers: grant.viewers,
|
|
5858
|
+
createdAt: grant.createdAt,
|
|
5859
|
+
expiresAt: grant.expiresAt
|
|
5860
|
+
};
|
|
5861
|
+
}),
|
|
5862
|
+
reach: share.reach,
|
|
5863
|
+
routes: [...share.routes],
|
|
5864
|
+
refused: [...share.refused],
|
|
5865
|
+
tunnel: { ...share.tunnel },
|
|
5866
|
+
startedAt: share.startedAt
|
|
5867
|
+
};
|
|
5868
|
+
};
|
|
5869
|
+
const endGrant = (share, grant, why) => {
|
|
5870
|
+
if (!share.grants.delete(grant.id))
|
|
5871
|
+
return;
|
|
5872
|
+
grant.endedAt = now();
|
|
5873
|
+
grant.endedBy = why;
|
|
5874
|
+
grant.viewers = 0;
|
|
5875
|
+
share.tombstones.push(grant);
|
|
5876
|
+
while (share.tombstones.length > MAX_TOMBSTONES)
|
|
5877
|
+
share.tombstones.shift();
|
|
5878
|
+
for (const socket of share.grantSockets.get(grant.id) ?? [])
|
|
5879
|
+
socket.destroy();
|
|
5880
|
+
share.grantSockets.delete(grant.id);
|
|
5881
|
+
for (const held of share.grantRequests.get(grant.id) ?? []) {
|
|
5882
|
+
held.res.destroy();
|
|
5883
|
+
held.req.destroy();
|
|
5884
|
+
}
|
|
5885
|
+
share.grantRequests.delete(grant.id);
|
|
5886
|
+
for (const held of [...share.waiting.get(grant.id) ?? []]) {
|
|
5887
|
+
if (held.drop())
|
|
5888
|
+
refuse(held.req, held.res, why);
|
|
5889
|
+
}
|
|
5890
|
+
share.waiting.delete(grant.id);
|
|
5891
|
+
const turn = share.rota.indexOf(grant.id);
|
|
5892
|
+
if (turn >= 0)
|
|
5893
|
+
share.rota.splice(turn, 1);
|
|
5894
|
+
};
|
|
5895
|
+
const sweepExpiry = () => {
|
|
5896
|
+
const share = active;
|
|
5897
|
+
if (share === null)
|
|
5898
|
+
return;
|
|
5899
|
+
if (share.expiryTimer !== null) {
|
|
5900
|
+
clearTimeout(share.expiryTimer);
|
|
5901
|
+
share.expiryTimer = null;
|
|
5902
|
+
}
|
|
5903
|
+
const at = now();
|
|
5904
|
+
const mono = nowMono();
|
|
5905
|
+
let ended = false;
|
|
5906
|
+
for (const grant of [...share.grants.values()]) {
|
|
5907
|
+
if (!expired(grant, at, mono))
|
|
5908
|
+
continue;
|
|
5909
|
+
endGrant(share, grant, "expiry");
|
|
5910
|
+
ended = true;
|
|
5911
|
+
}
|
|
5912
|
+
const next = [...share.grants.values()].reduce((soonest, grant) => soonest === null ? grant.expiresAt : Math.min(soonest, grant.expiresAt), null);
|
|
5913
|
+
if (next !== null) {
|
|
5914
|
+
share.expiryTimer = setTimeout(sweepExpiry, Math.max(1, next - at));
|
|
5915
|
+
share.expiryTimer.unref?.();
|
|
5916
|
+
}
|
|
5917
|
+
if (ended)
|
|
5918
|
+
options.live.nudge("share");
|
|
5919
|
+
};
|
|
5920
|
+
const mintGrant = (share, name) => {
|
|
5921
|
+
let token = randomBytes4(24).toString("base64url");
|
|
5922
|
+
const taken = new Set([...share.grants.values(), ...share.tombstones].map((g) => g.token));
|
|
5923
|
+
while (taken.has(token))
|
|
5924
|
+
token = randomBytes4(24).toString("base64url");
|
|
5925
|
+
const at = now();
|
|
5926
|
+
const grant = {
|
|
5927
|
+
id: randomUUID(),
|
|
5928
|
+
name,
|
|
5929
|
+
token,
|
|
5930
|
+
createdAt: at,
|
|
5931
|
+
expiresAt: at + DEFAULT_TTL_MS,
|
|
5932
|
+
expiresAtMono: nowMono() + BigInt(DEFAULT_TTL_MS) * 1000000n,
|
|
5933
|
+
endedAt: null,
|
|
5934
|
+
endedBy: null,
|
|
5935
|
+
viewers: 0
|
|
5936
|
+
};
|
|
5937
|
+
share.grants.set(grant.id, grant);
|
|
5938
|
+
return grant;
|
|
5939
|
+
};
|
|
5940
|
+
const resolve5 = (share, candidate) => {
|
|
5941
|
+
const grant = grantFor(share, candidate);
|
|
5942
|
+
if (grant !== null) {
|
|
5943
|
+
if (!expired(grant, now(), nowMono()))
|
|
5944
|
+
return { grant };
|
|
5945
|
+
endGrant(share, grant, "expiry");
|
|
5946
|
+
options.live.nudge("share");
|
|
5947
|
+
return { refusal: "expiry" };
|
|
5948
|
+
}
|
|
5949
|
+
const ended = endedGrantFor(share, candidate);
|
|
5950
|
+
if (ended !== null)
|
|
5951
|
+
return { refusal: ended.endedBy === "revoke" ? "revoke" : "expiry" };
|
|
5952
|
+
return { refusal: "inactive" };
|
|
5953
|
+
};
|
|
5954
|
+
const pump = (share) => {
|
|
5955
|
+
while (share.running < VIEWER_CONCURRENCY && share.rota.length > 0) {
|
|
5956
|
+
const grantId = share.rota[0];
|
|
5957
|
+
if (grantId === void 0)
|
|
5958
|
+
return;
|
|
5959
|
+
const next = share.waiting.get(grantId)?.[0];
|
|
5960
|
+
if (next === void 0) {
|
|
5961
|
+
share.rota.shift();
|
|
5962
|
+
share.waiting.delete(grantId);
|
|
5963
|
+
continue;
|
|
5964
|
+
}
|
|
5965
|
+
next.drop();
|
|
5966
|
+
const turn = share.rota.indexOf(grantId);
|
|
5967
|
+
if (turn >= 0) {
|
|
5968
|
+
share.rota.splice(turn, 1);
|
|
5969
|
+
share.rota.push(grantId);
|
|
5970
|
+
}
|
|
5971
|
+
if (Date.now() >= next.spentAt) {
|
|
5972
|
+
next.shed();
|
|
5973
|
+
continue;
|
|
5974
|
+
}
|
|
5975
|
+
if (!share.grants.has(grantId)) {
|
|
5976
|
+
refuse(next.req, next.res, "revoke");
|
|
5977
|
+
continue;
|
|
5978
|
+
}
|
|
5979
|
+
next.start();
|
|
5980
|
+
}
|
|
5981
|
+
};
|
|
5982
|
+
const admit = (share, grant, req, res, run4) => {
|
|
5983
|
+
const queue = share.waiting.get(grant.id) ?? [];
|
|
5984
|
+
if (queue.length >= VIEWER_QUEUE) {
|
|
5985
|
+
return sendJson(res, 503, { ok: false, error: "Too much at once. Try again." });
|
|
5986
|
+
}
|
|
5987
|
+
let queued = true;
|
|
5988
|
+
let running = false;
|
|
5989
|
+
let over = false;
|
|
5990
|
+
const spentAt = Date.now() + deadlineMs;
|
|
5991
|
+
const shed = () => {
|
|
5992
|
+
if (over)
|
|
5993
|
+
return;
|
|
5994
|
+
over = true;
|
|
5995
|
+
sendJson(res, 503, { ok: false, error: "The dev server is busy. Try again." });
|
|
5996
|
+
};
|
|
5997
|
+
const deadline = setTimeout(() => {
|
|
5998
|
+
if (over)
|
|
5999
|
+
return;
|
|
6000
|
+
const waited = held.drop();
|
|
6001
|
+
if (running) {
|
|
6002
|
+
over = true;
|
|
6003
|
+
res.destroy();
|
|
6004
|
+
release();
|
|
6005
|
+
return;
|
|
6006
|
+
}
|
|
6007
|
+
if (waited)
|
|
6008
|
+
shed();
|
|
6009
|
+
}, deadlineMs);
|
|
6010
|
+
deadline.unref?.();
|
|
6011
|
+
const release = () => {
|
|
6012
|
+
if (!running)
|
|
6013
|
+
return;
|
|
6014
|
+
running = false;
|
|
6015
|
+
over = true;
|
|
6016
|
+
clearTimeout(deadline);
|
|
6017
|
+
share.running = Math.max(0, share.running - 1);
|
|
6018
|
+
pump(share);
|
|
6019
|
+
};
|
|
6020
|
+
const start = () => {
|
|
6021
|
+
running = true;
|
|
6022
|
+
share.running += 1;
|
|
6023
|
+
const writeHead = res.writeHead.bind(res);
|
|
6024
|
+
res.writeHead = ((...args) => {
|
|
6025
|
+
release();
|
|
6026
|
+
return writeHead(...args);
|
|
6027
|
+
});
|
|
6028
|
+
res.once("finish", release);
|
|
6029
|
+
res.once("close", release);
|
|
6030
|
+
run4();
|
|
6031
|
+
};
|
|
6032
|
+
const held = {
|
|
6033
|
+
req,
|
|
6034
|
+
res,
|
|
6035
|
+
grantId: grant.id,
|
|
6036
|
+
spentAt,
|
|
6037
|
+
start,
|
|
6038
|
+
shed,
|
|
6039
|
+
drop: () => {
|
|
6040
|
+
if (!queued)
|
|
6041
|
+
return false;
|
|
6042
|
+
queued = false;
|
|
6043
|
+
const rest = share.waiting.get(grant.id);
|
|
6044
|
+
const at = rest?.indexOf(held) ?? -1;
|
|
6045
|
+
if (rest !== void 0 && at >= 0)
|
|
6046
|
+
rest.splice(at, 1);
|
|
6047
|
+
if (rest !== void 0 && rest.length === 0) {
|
|
6048
|
+
share.waiting.delete(grant.id);
|
|
6049
|
+
const turn = share.rota.indexOf(grant.id);
|
|
6050
|
+
if (turn >= 0)
|
|
6051
|
+
share.rota.splice(turn, 1);
|
|
6052
|
+
}
|
|
6053
|
+
return true;
|
|
6054
|
+
}
|
|
6055
|
+
};
|
|
6056
|
+
queue.push(held);
|
|
6057
|
+
share.waiting.set(grant.id, queue);
|
|
6058
|
+
if (!share.rota.includes(grant.id))
|
|
6059
|
+
share.rota.push(grant.id);
|
|
6060
|
+
pump(share);
|
|
6061
|
+
if (!queued)
|
|
6062
|
+
return;
|
|
6063
|
+
res.once("close", () => {
|
|
6064
|
+
held.drop();
|
|
6065
|
+
if (running)
|
|
6066
|
+
return;
|
|
6067
|
+
over = true;
|
|
6068
|
+
clearTimeout(deadline);
|
|
6069
|
+
});
|
|
6070
|
+
};
|
|
6071
|
+
const request = (req, res) => {
|
|
6072
|
+
const share = active;
|
|
6073
|
+
if (share === null)
|
|
6074
|
+
return refuse(req, res);
|
|
6075
|
+
const path = (req.url ?? "/").split("?")[0] ?? "/";
|
|
6076
|
+
if (path.startsWith(ENTRY_PREFIX)) {
|
|
6077
|
+
const candidate = path.slice(ENTRY_PREFIX.length);
|
|
6078
|
+
if (req.method !== "GET" && req.method !== "HEAD" || candidate.includes("/")) {
|
|
6079
|
+
return refuse(req, res);
|
|
6080
|
+
}
|
|
6081
|
+
const found2 = resolve5(share, candidate);
|
|
6082
|
+
if ("refusal" in found2)
|
|
6083
|
+
return refuse(req, res, found2.refusal);
|
|
6084
|
+
const secure = forwardedProto(req) === "https" ? "; Secure" : "";
|
|
6085
|
+
res.writeHead(302, {
|
|
6086
|
+
location: "/leglas/",
|
|
6087
|
+
"set-cookie": `${SHARE_COOKIE}=${found2.grant.token}; Path=/; HttpOnly; SameSite=Lax${secure}`,
|
|
6088
|
+
"cache-control": "no-store"
|
|
6089
|
+
});
|
|
6090
|
+
res.end();
|
|
6091
|
+
return;
|
|
6092
|
+
}
|
|
6093
|
+
const cookie = cookieToken(req);
|
|
6094
|
+
if (cookie === null)
|
|
6095
|
+
return refuse(req, res);
|
|
6096
|
+
const found = resolve5(share, cookie);
|
|
6097
|
+
if ("refusal" in found)
|
|
6098
|
+
return refuse(req, res, found.refusal);
|
|
6099
|
+
const grant = found.grant;
|
|
6100
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
6101
|
+
return sendJson(res, 403, {
|
|
6102
|
+
ok: false,
|
|
6103
|
+
error: "Viewers can look, not change what runs."
|
|
6104
|
+
});
|
|
6105
|
+
}
|
|
6106
|
+
if (isDevControlRequest(req.url ?? "/")) {
|
|
6107
|
+
return sendJson(res, 403, { ok: false, error: "Not available to viewers." });
|
|
6108
|
+
}
|
|
6109
|
+
if (isHiddenPath(path)) {
|
|
6110
|
+
return sendJson(res, 403, { ok: false, error: "Not available to viewers." });
|
|
6111
|
+
}
|
|
6112
|
+
if (req.headers["sec-fetch-dest"] === "serviceworker") {
|
|
6113
|
+
return sendJson(res, 403, { ok: false, error: "Not available to viewers." });
|
|
6114
|
+
}
|
|
6115
|
+
const url = req.url ?? "/";
|
|
6116
|
+
const interfaceOwn = spellings(path).every((form) => form === OWN_PREFIX || form.startsWith(`${OWN_PREFIX}/`));
|
|
6117
|
+
if (share.reach === "listed" && !interfaceOwn && !routeAllowed(share.routes, url)) {
|
|
6118
|
+
const asked = canonical(url.split("?", 1)[0] ?? "/");
|
|
6119
|
+
if (!share.refused.includes(asked)) {
|
|
6120
|
+
share.refused.push(asked);
|
|
6121
|
+
while (share.refused.length > MAX_REFUSED)
|
|
6122
|
+
share.refused.shift();
|
|
6123
|
+
options.live.nudge("share");
|
|
6124
|
+
}
|
|
6125
|
+
return sendJson(res, 403, { ok: false, error: "Not shared." });
|
|
6126
|
+
}
|
|
6127
|
+
const run4 = () => {
|
|
6128
|
+
const held = { req, res };
|
|
6129
|
+
const inFlight = share.grantRequests.get(grant.id) ?? /* @__PURE__ */ new Set();
|
|
6130
|
+
inFlight.add(held);
|
|
6131
|
+
share.grantRequests.set(grant.id, inFlight);
|
|
6132
|
+
let released = false;
|
|
6133
|
+
const release = () => {
|
|
6134
|
+
if (released)
|
|
6135
|
+
return;
|
|
6136
|
+
released = true;
|
|
6137
|
+
share.grantRequests.get(grant.id)?.delete(held);
|
|
6138
|
+
};
|
|
6139
|
+
res.once("finish", release);
|
|
6140
|
+
res.once("close", release);
|
|
6141
|
+
options.request(req, res, { publicOrigin: publicOrigin(req), grantId: grant.id });
|
|
6142
|
+
};
|
|
6143
|
+
if (interfaceOwn)
|
|
6144
|
+
return run4();
|
|
6145
|
+
admit(share, grant, req, res, run4);
|
|
6146
|
+
};
|
|
6147
|
+
const upgrade = (req, socket, head) => {
|
|
6148
|
+
const share = active;
|
|
6149
|
+
const cookie = cookieToken(req);
|
|
6150
|
+
if (share === null || cookie === null) {
|
|
6151
|
+
socket.destroy();
|
|
6152
|
+
return;
|
|
6153
|
+
}
|
|
6154
|
+
const found = resolve5(share, cookie);
|
|
6155
|
+
if ("refusal" in found) {
|
|
6156
|
+
socket.destroy();
|
|
6157
|
+
return;
|
|
6158
|
+
}
|
|
6159
|
+
const grant = found.grant;
|
|
6160
|
+
const path = (req.url ?? "/").split("?")[0] ?? "/";
|
|
6161
|
+
if (path !== LIVE_PATH) {
|
|
6162
|
+
socket.destroy();
|
|
6163
|
+
return;
|
|
6164
|
+
}
|
|
6165
|
+
if (throughTunnel(req))
|
|
6166
|
+
share.runningTunnel?.settle();
|
|
6167
|
+
if (!options.upgrade(req, socket, head))
|
|
6168
|
+
return;
|
|
6169
|
+
const held = share.grantSockets.get(grant.id) ?? /* @__PURE__ */ new Set();
|
|
6170
|
+
held.add(socket);
|
|
6171
|
+
share.grantSockets.set(grant.id, held);
|
|
6172
|
+
grant.viewers += 1;
|
|
6173
|
+
options.live.nudge("share");
|
|
6174
|
+
let gone = false;
|
|
6175
|
+
const letGo = () => {
|
|
6176
|
+
if (gone)
|
|
6177
|
+
return;
|
|
6178
|
+
gone = true;
|
|
6179
|
+
share.grantSockets.get(grant.id)?.delete(socket);
|
|
6180
|
+
grant.viewers = Math.max(0, grant.viewers - 1);
|
|
6181
|
+
options.live.nudge("share");
|
|
6182
|
+
};
|
|
6183
|
+
socket.once("close", letGo);
|
|
6184
|
+
socket.once("end", letGo);
|
|
6185
|
+
socket.once("error", letGo);
|
|
6186
|
+
};
|
|
6187
|
+
const create = async (input) => {
|
|
6188
|
+
if (closed)
|
|
6189
|
+
return { ok: false, status: 409, error: "Leglas is shutting down." };
|
|
6190
|
+
if (active !== null || creating) {
|
|
6191
|
+
return { ok: false, status: 409, error: "Stop the current share first." };
|
|
6192
|
+
}
|
|
6193
|
+
creating = true;
|
|
6194
|
+
const stopsAtStart = stops;
|
|
6195
|
+
try {
|
|
6196
|
+
const previews = await options.previews();
|
|
6197
|
+
const parsed2 = manifestFrom(input, previews);
|
|
6198
|
+
if (!parsed2.ok)
|
|
6199
|
+
return { ok: false, status: 400, error: parsed2.error };
|
|
6200
|
+
const providers = await tunnels();
|
|
6201
|
+
const requested = isRecord3(input) ? input.tunnel : void 0;
|
|
6202
|
+
if (requested !== void 0 && requested !== "none" && requested !== "cloudflared" && requested !== "ngrok") {
|
|
6203
|
+
return { ok: false, status: 400, error: "That tunnel provider is not supported." };
|
|
6204
|
+
}
|
|
6205
|
+
if (requested !== void 0 && requested !== "none" && !providers.includes(requested)) {
|
|
6206
|
+
return {
|
|
6207
|
+
ok: false,
|
|
6208
|
+
status: 400,
|
|
6209
|
+
error: `${requested} is not available on this machine.`
|
|
6210
|
+
};
|
|
6211
|
+
}
|
|
6212
|
+
const provider = requested ?? providers[0] ?? "none";
|
|
6213
|
+
if (closed)
|
|
6214
|
+
return { ok: false, status: 409, error: "Leglas is shutting down." };
|
|
6215
|
+
const server = http3.createServer(request);
|
|
6216
|
+
const sockets = /* @__PURE__ */ new Set();
|
|
6217
|
+
server.on("connection", (socket) => {
|
|
6218
|
+
sockets.add(socket);
|
|
6219
|
+
socket.once("close", () => sockets.delete(socket));
|
|
6220
|
+
});
|
|
6221
|
+
server.on("upgrade", upgrade);
|
|
6222
|
+
let port;
|
|
6223
|
+
try {
|
|
6224
|
+
port = await bind(server);
|
|
6225
|
+
} catch (error) {
|
|
6226
|
+
return {
|
|
6227
|
+
ok: false,
|
|
6228
|
+
status: 500,
|
|
6229
|
+
error: `Leglas could not open a listener for the share (${error instanceof Error ? error.message : String(error)}).`
|
|
6230
|
+
};
|
|
6231
|
+
}
|
|
6232
|
+
if (closed) {
|
|
6233
|
+
await new Promise((resolve6) => server.close(() => resolve6()));
|
|
6234
|
+
return { ok: false, status: 409, error: "Leglas is shutting down." };
|
|
6235
|
+
}
|
|
6236
|
+
if (stops !== stopsAtStart) {
|
|
6237
|
+
await new Promise((resolve6) => server.close(() => resolve6()));
|
|
6238
|
+
return { ok: false, status: 409, error: "Sharing was stopped while it was starting." };
|
|
6239
|
+
}
|
|
6240
|
+
const share = {
|
|
6241
|
+
...parsed2.manifest,
|
|
6242
|
+
id: randomUUID(),
|
|
6243
|
+
grants: /* @__PURE__ */ new Map(),
|
|
6244
|
+
tombstones: [],
|
|
6245
|
+
port,
|
|
6246
|
+
startedAt: now(),
|
|
6247
|
+
tunnel: provider === "none" ? { status: "none" } : { status: "starting", provider },
|
|
6248
|
+
runningTunnel: null,
|
|
6249
|
+
tunnelGeneration: 0,
|
|
6250
|
+
server,
|
|
6251
|
+
sockets,
|
|
6252
|
+
grantSockets: /* @__PURE__ */ new Map(),
|
|
6253
|
+
grantRequests: /* @__PURE__ */ new Map(),
|
|
6254
|
+
launch: null,
|
|
6255
|
+
expiryTimer: null,
|
|
6256
|
+
refused: [],
|
|
6257
|
+
running: 0,
|
|
6258
|
+
waiting: /* @__PURE__ */ new Map(),
|
|
6259
|
+
rota: []
|
|
6260
|
+
};
|
|
6261
|
+
active = share;
|
|
6262
|
+
mintGrant(share, "");
|
|
6263
|
+
sweepExpiry();
|
|
6264
|
+
options.live.nudge("share");
|
|
6265
|
+
if (provider !== "none") {
|
|
6266
|
+
share.launch = setImmediate(() => {
|
|
6267
|
+
share.launch = null;
|
|
6268
|
+
if (active !== share)
|
|
6269
|
+
return;
|
|
6270
|
+
share.runningTunnel = runTunnel({
|
|
6271
|
+
provider,
|
|
6272
|
+
port,
|
|
6273
|
+
// Whichever link exists when the tunnel starts: the probe only
|
|
6274
|
+
// needs a path the listener answers, and a share always has one.
|
|
6275
|
+
entryPath: `${ENTRY_PREFIX}${[...share.grants.values()][0]?.token ?? ""}`,
|
|
6276
|
+
onState: (next) => {
|
|
6277
|
+
if (active !== share || JSON.stringify(share.tunnel) === JSON.stringify(next))
|
|
6278
|
+
return;
|
|
6279
|
+
share.tunnel = next;
|
|
6280
|
+
options.live.nudge("share");
|
|
6281
|
+
}
|
|
6282
|
+
});
|
|
6283
|
+
});
|
|
6284
|
+
share.launch.unref?.();
|
|
6285
|
+
}
|
|
6286
|
+
return { ok: true, share: status() };
|
|
6287
|
+
} finally {
|
|
6288
|
+
creating = false;
|
|
6289
|
+
}
|
|
6290
|
+
};
|
|
6291
|
+
const createGrant = (input) => {
|
|
6292
|
+
const share = active;
|
|
6293
|
+
if (share === null)
|
|
6294
|
+
return { ok: false, status: 404, error: "Nothing is being shared." };
|
|
6295
|
+
const name = isRecord3(input) && typeof input.name === "string" ? input.name.trim() : "";
|
|
6296
|
+
if (name.length > 60) {
|
|
6297
|
+
return { ok: false, status: 400, error: "That name is too long for a link." };
|
|
6298
|
+
}
|
|
6299
|
+
sweepExpiry();
|
|
6300
|
+
if (share.grants.size >= MAX_GRANTS) {
|
|
6301
|
+
return {
|
|
6302
|
+
ok: false,
|
|
6303
|
+
status: 409,
|
|
6304
|
+
error: `A share can hold ${MAX_GRANTS} links. Revoke one to make another.`
|
|
6305
|
+
};
|
|
6306
|
+
}
|
|
6307
|
+
mintGrant(share, name);
|
|
6308
|
+
sweepExpiry();
|
|
6309
|
+
options.live.nudge("share");
|
|
6310
|
+
return { ok: true, share: status() };
|
|
6311
|
+
};
|
|
6312
|
+
const revokeGrant = (input) => {
|
|
6313
|
+
const share = active;
|
|
6314
|
+
if (share === null)
|
|
6315
|
+
return { ok: false, status: 404, error: "Nothing is being shared." };
|
|
6316
|
+
const id = isRecord3(input) && typeof input.id === "string" ? input.id : "";
|
|
6317
|
+
const grant = share.grants.get(id);
|
|
6318
|
+
if (grant === void 0)
|
|
6319
|
+
return { ok: false, status: 404, error: "No such link." };
|
|
6320
|
+
endGrant(share, grant, "revoke");
|
|
6321
|
+
sweepExpiry();
|
|
6322
|
+
options.live.nudge("share");
|
|
6323
|
+
return { ok: true, share: status() };
|
|
6324
|
+
};
|
|
6325
|
+
const extendGrant = (input) => {
|
|
6326
|
+
const share = active;
|
|
6327
|
+
if (share === null)
|
|
6328
|
+
return { ok: false, status: 404, error: "Nothing is being shared." };
|
|
6329
|
+
const id = isRecord3(input) && typeof input.id === "string" ? input.id : "";
|
|
6330
|
+
sweepExpiry();
|
|
6331
|
+
const grant = share.grants.get(id);
|
|
6332
|
+
if (grant === void 0) {
|
|
6333
|
+
return { ok: false, status: 404, error: "That link has ended. Make a new one." };
|
|
6334
|
+
}
|
|
6335
|
+
const at = now();
|
|
6336
|
+
grant.expiresAt = at + DEFAULT_TTL_MS;
|
|
6337
|
+
grant.expiresAtMono = nowMono() + BigInt(DEFAULT_TTL_MS) * 1000000n;
|
|
6338
|
+
sweepExpiry();
|
|
6339
|
+
options.live.nudge("share");
|
|
6340
|
+
return { ok: true, share: status() };
|
|
6341
|
+
};
|
|
6342
|
+
const rotate = async () => {
|
|
6343
|
+
const share = active;
|
|
6344
|
+
if (share === null)
|
|
6345
|
+
return { ok: false, status: 404, error: "Nothing is being shared." };
|
|
6346
|
+
for (const grant of [...share.grants.values()])
|
|
6347
|
+
endGrant(share, grant, "revoke");
|
|
6348
|
+
const provider = "provider" in share.tunnel ? share.tunnel.provider : null;
|
|
6349
|
+
await share.runningTunnel?.stop().catch(() => {
|
|
6350
|
+
});
|
|
6351
|
+
share.runningTunnel = null;
|
|
6352
|
+
if (active !== share)
|
|
6353
|
+
return { ok: false, status: 404, error: "Nothing is being shared." };
|
|
6354
|
+
mintGrant(share, "");
|
|
6355
|
+
sweepExpiry();
|
|
6356
|
+
if (provider !== null) {
|
|
6357
|
+
share.tunnelGeneration += 1;
|
|
6358
|
+
const generation = share.tunnelGeneration;
|
|
6359
|
+
share.tunnel = { status: "starting", provider };
|
|
6360
|
+
share.runningTunnel = runTunnel({
|
|
6361
|
+
provider,
|
|
6362
|
+
port: share.port,
|
|
6363
|
+
entryPath: `${ENTRY_PREFIX}${[...share.grants.values()][0]?.token ?? ""}`,
|
|
6364
|
+
onState: (next) => {
|
|
6365
|
+
if (active !== share || share.tunnelGeneration !== generation)
|
|
6366
|
+
return;
|
|
6367
|
+
if (JSON.stringify(share.tunnel) === JSON.stringify(next))
|
|
6368
|
+
return;
|
|
6369
|
+
share.tunnel = next;
|
|
6370
|
+
options.live.nudge("share");
|
|
6371
|
+
}
|
|
6372
|
+
});
|
|
6373
|
+
}
|
|
6374
|
+
options.live.nudge("share");
|
|
6375
|
+
return { ok: true, share: status() };
|
|
6376
|
+
};
|
|
6377
|
+
const allowRoute = (input) => {
|
|
6378
|
+
const share = active;
|
|
6379
|
+
if (share === null)
|
|
6380
|
+
return { ok: false, status: 404, error: "Nothing is being shared." };
|
|
6381
|
+
const given = isRecord3(input) && typeof input.path === "string" ? input.path.trim() : "";
|
|
6382
|
+
if (!given.startsWith("/")) {
|
|
6383
|
+
return { ok: false, status: 400, error: "A route is a path beginning with a slash." };
|
|
6384
|
+
}
|
|
6385
|
+
const subtree = isRecord3(input) && input.subtree === true;
|
|
6386
|
+
if (subtree && given.replace(/\/+$/, "") === "") {
|
|
6387
|
+
return { ok: false, status: 400, error: "The root is a page, not a folder." };
|
|
6388
|
+
}
|
|
6389
|
+
const asked = subtree ? `${given.replace(/\/+$/, "")}/` : given === "/" ? given : given.replace(/\/+$/, "");
|
|
6390
|
+
if (share.routes.length >= 400) {
|
|
6391
|
+
return { ok: false, status: 409, error: "That share is holding as many routes as it can." };
|
|
6392
|
+
}
|
|
6393
|
+
if (!share.routes.includes(asked))
|
|
6394
|
+
share.routes.push(asked);
|
|
6395
|
+
share.refused = share.refused.filter((path) => !routeAllowed([asked], path));
|
|
6396
|
+
options.live.nudge("share");
|
|
6397
|
+
return { ok: true, share: status() };
|
|
6398
|
+
};
|
|
6399
|
+
const update = async (input) => {
|
|
6400
|
+
const share = active;
|
|
6401
|
+
if (share === null) {
|
|
6402
|
+
return { ok: false, status: 404, error: "Nothing is being shared." };
|
|
6403
|
+
}
|
|
6404
|
+
const parsed2 = manifestFrom(input, await options.previews());
|
|
6405
|
+
if (!parsed2.ok)
|
|
6406
|
+
return { ok: false, status: 400, error: parsed2.error };
|
|
6407
|
+
if (active !== share) {
|
|
6408
|
+
return { ok: false, status: 404, error: "Nothing is being shared." };
|
|
6409
|
+
}
|
|
6410
|
+
share.scope = parsed2.manifest.scope;
|
|
6411
|
+
share.titles = parsed2.manifest.titles;
|
|
6412
|
+
share.layout = parsed2.manifest.layout;
|
|
6413
|
+
options.live.nudge("share");
|
|
6414
|
+
options.live.nudge("config");
|
|
6415
|
+
return { ok: true, share: status() };
|
|
6416
|
+
};
|
|
6417
|
+
const viewerConfig = async (grantId) => {
|
|
6418
|
+
const share = active;
|
|
6419
|
+
if (share === null || !share.grants.has(grantId))
|
|
6420
|
+
return null;
|
|
6421
|
+
const titles = new Set(share.titles);
|
|
6422
|
+
const previews = (await options.previews()).filter((preview) => titles.has(preview.title));
|
|
6423
|
+
if (active !== share)
|
|
6424
|
+
return null;
|
|
6425
|
+
return {
|
|
6426
|
+
...options.viewerConfig,
|
|
6427
|
+
// The project id is the config's absolute path, which keys the sharer's
|
|
6428
|
+
// saved layout and names their machine's directories, and the dev
|
|
6429
|
+
// server's address names their network. A viewer keeps no layout and
|
|
6430
|
+
// never dials the dev server, so the share's own id does the job and
|
|
6431
|
+
// the address is left blank.
|
|
6432
|
+
project: `share:${share.id}`,
|
|
6433
|
+
devServer: "",
|
|
6434
|
+
previews: options.previewsForConfig(previews),
|
|
6435
|
+
errors: [],
|
|
6436
|
+
warnings: [],
|
|
6437
|
+
viewer: { scope: share.scope, layout: cloneLayout(share.layout) }
|
|
6438
|
+
};
|
|
6439
|
+
};
|
|
6440
|
+
const fileSlugAllowed = async (slug, grantId) => {
|
|
6441
|
+
const share = active;
|
|
6442
|
+
if (share === null || !share.grants.has(grantId))
|
|
6443
|
+
return false;
|
|
6444
|
+
const titles = new Set(share.titles);
|
|
6445
|
+
const previews = await options.previews();
|
|
6446
|
+
return previews.some((preview) => {
|
|
6447
|
+
if (!titles.has(preview.title) || preview.file === void 0)
|
|
6448
|
+
return false;
|
|
6449
|
+
const rest = preview.url.startsWith(FILES_PREFIX_PATH) ? preview.url.slice(FILES_PREFIX_PATH.length) : "";
|
|
6450
|
+
const slash = rest.indexOf("/");
|
|
6451
|
+
return (slash === -1 ? rest : rest.slice(0, slash)) === slug;
|
|
6452
|
+
});
|
|
6453
|
+
};
|
|
6454
|
+
const stop = () => {
|
|
6455
|
+
stops += 1;
|
|
6456
|
+
if (stopPromise !== null)
|
|
6457
|
+
return stopPromise;
|
|
6458
|
+
const share = active;
|
|
6459
|
+
if (share === null)
|
|
6460
|
+
return Promise.resolve();
|
|
6461
|
+
if (share.launch !== null) {
|
|
6462
|
+
clearImmediate(share.launch);
|
|
6463
|
+
share.launch = null;
|
|
6464
|
+
}
|
|
6465
|
+
stopPromise = (async () => {
|
|
6466
|
+
await share.runningTunnel?.stop().catch(() => {
|
|
6467
|
+
});
|
|
6468
|
+
await closeListener(share);
|
|
6469
|
+
if (active === share)
|
|
6470
|
+
active = null;
|
|
6471
|
+
options.live.nudge("share");
|
|
6472
|
+
})().finally(() => {
|
|
6473
|
+
stopPromise = null;
|
|
6474
|
+
});
|
|
6475
|
+
return stopPromise;
|
|
6476
|
+
};
|
|
6477
|
+
const close = async () => {
|
|
6478
|
+
closed = true;
|
|
6479
|
+
await stop();
|
|
6480
|
+
};
|
|
6481
|
+
return {
|
|
6482
|
+
tunnels,
|
|
6483
|
+
status,
|
|
6484
|
+
fileSlugAllowed,
|
|
6485
|
+
allowRoute,
|
|
6486
|
+
create,
|
|
6487
|
+
createGrant,
|
|
6488
|
+
revokeGrant,
|
|
6489
|
+
extendGrant,
|
|
6490
|
+
rotate,
|
|
6491
|
+
update,
|
|
6492
|
+
viewerConfig,
|
|
6493
|
+
stop,
|
|
6494
|
+
close
|
|
5112
6495
|
};
|
|
5113
6496
|
}
|
|
5114
6497
|
|
|
@@ -5148,7 +6531,7 @@ function resolveTitle(input, titles, renames) {
|
|
|
5148
6531
|
import { createReadStream, existsSync as existsSync3, statSync, unwatchFile, watch as watchFs, watchFile } from "fs";
|
|
5149
6532
|
import { createHash as createHash2 } from "crypto";
|
|
5150
6533
|
import { mkdir as mkdir8, readdir as readdir3, writeFile as writeFile8 } from "fs/promises";
|
|
5151
|
-
import
|
|
6534
|
+
import http4 from "http";
|
|
5152
6535
|
import net3 from "net";
|
|
5153
6536
|
import { basename as basename3, dirname as dirname8, extname as extname3, join as join12, normalize, relative as relative3 } from "path";
|
|
5154
6537
|
|
|
@@ -5217,7 +6600,7 @@ var CONTENT_TYPES = {
|
|
|
5217
6600
|
".woff2": "font/woff2"
|
|
5218
6601
|
};
|
|
5219
6602
|
var FILES_PREFIX = `${LEGLAS_PREFIX}/files`;
|
|
5220
|
-
function
|
|
6603
|
+
function sendJson2(res, status, body) {
|
|
5221
6604
|
const payload = JSON.stringify(body);
|
|
5222
6605
|
res.writeHead(status, {
|
|
5223
6606
|
"content-type": "application/json; charset=utf-8",
|
|
@@ -5554,7 +6937,8 @@ function watchHealth(target, live) {
|
|
|
5554
6937
|
close: () => {
|
|
5555
6938
|
closed = true;
|
|
5556
6939
|
clearInterval(timer);
|
|
5557
|
-
}
|
|
6940
|
+
},
|
|
6941
|
+
reachable: () => previous
|
|
5558
6942
|
};
|
|
5559
6943
|
}
|
|
5560
6944
|
function snapshotConfig(cwd) {
|
|
@@ -5611,7 +6995,7 @@ function listen(server, port) {
|
|
|
5611
6995
|
server.listen(port, "127.0.0.1");
|
|
5612
6996
|
});
|
|
5613
6997
|
}
|
|
5614
|
-
async function
|
|
6998
|
+
async function bind2(server, requested) {
|
|
5615
6999
|
if (requested === 0)
|
|
5616
7000
|
return listen(server, 0);
|
|
5617
7001
|
for (let attempt = 0; attempt < PORT_ATTEMPTS; attempt += 1) {
|
|
@@ -5627,6 +7011,8 @@ async function bind(server, requested) {
|
|
|
5627
7011
|
async function startServer(options) {
|
|
5628
7012
|
const { config, configErrors = [], configWarnings = [], shellDir = null, project = "", cwd = process.cwd(), leglasCommand = "npx -y leglas", fileMounts = /* @__PURE__ */ new Map(), detect = () => detectAgents() } = options;
|
|
5629
7013
|
const browserPool = options.pool ?? createBrowserPool();
|
|
7014
|
+
let shares = null;
|
|
7015
|
+
let liveHealth = null;
|
|
5630
7016
|
const live = options.live ?? createLiveHub();
|
|
5631
7017
|
const branches = createBranchRegistry({
|
|
5632
7018
|
cwd,
|
|
@@ -5701,12 +7087,103 @@ async function startServer(options) {
|
|
|
5701
7087
|
const livePreviews = async () => (await livePreviewDefinitions()).map(readyPreview).filter((preview) => preview !== null);
|
|
5702
7088
|
void probeAgents().catch(() => {
|
|
5703
7089
|
});
|
|
5704
|
-
const
|
|
7090
|
+
const readShareBody = (req, res, run4) => {
|
|
7091
|
+
let body = "";
|
|
7092
|
+
req.on("data", (chunk) => body += chunk);
|
|
7093
|
+
req.on("end", () => {
|
|
7094
|
+
const parsed2 = jsonBody(body);
|
|
7095
|
+
if (parsed2 === null)
|
|
7096
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
7097
|
+
void Promise.resolve(run4(parsed2)).then((result2) => {
|
|
7098
|
+
if (result2 === void 0) {
|
|
7099
|
+
return sendJson2(res, 500, { ok: false, error: "Sharing is not available." });
|
|
7100
|
+
}
|
|
7101
|
+
return result2.ok ? sendJson2(res, 200, result2) : sendJson2(res, result2.status, { ok: false, error: result2.error });
|
|
7102
|
+
});
|
|
7103
|
+
});
|
|
7104
|
+
};
|
|
7105
|
+
const handleRequest = (req, res, context) => {
|
|
5705
7106
|
const url = req.url ?? "/";
|
|
5706
7107
|
const path = url.split("?")[0] ?? "/";
|
|
5707
7108
|
const query = new URLSearchParams(url.includes("?") ? url.slice(url.indexOf("?") + 1) : "");
|
|
5708
|
-
if (req.method === "POST" && path.startsWith(`${LEGLAS_PREFIX}/api/`) && !isTrustedMutation(req)) {
|
|
5709
|
-
return
|
|
7109
|
+
if (!context.remote && req.method === "POST" && path.startsWith(`${LEGLAS_PREFIX}/api/`) && !isTrustedMutation(req)) {
|
|
7110
|
+
return sendJson2(res, 403, { ok: false, error: "Cross-origin API mutations are refused." });
|
|
7111
|
+
}
|
|
7112
|
+
if (context.remote && path === `${LEGLAS_PREFIX}/api/config`) {
|
|
7113
|
+
return void shares?.viewerConfig(context.grantId ?? "").then((payload) => {
|
|
7114
|
+
if (payload === null) {
|
|
7115
|
+
return sendJson2(res, 403, { ok: false, error: "This link isn't active." });
|
|
7116
|
+
}
|
|
7117
|
+
sendConditionalJson(req, res, payload);
|
|
7118
|
+
});
|
|
7119
|
+
}
|
|
7120
|
+
if (context.remote && path === `${LEGLAS_PREFIX}/api/health`) {
|
|
7121
|
+
const known = liveHealth?.reachable() ?? null;
|
|
7122
|
+
return void (known === null ? probe(target) : Promise.resolve(known)).then((reachable) => sendConditionalJson(req, res, { reachable }));
|
|
7123
|
+
}
|
|
7124
|
+
if (context.remote && path.startsWith(`${LEGLAS_PREFIX}/api/`)) {
|
|
7125
|
+
return sendJson2(res, 403, { error: "Not available to viewers." });
|
|
7126
|
+
}
|
|
7127
|
+
if (!context.remote && path === `${LEGLAS_PREFIX}/api/share` && req.method === "GET") {
|
|
7128
|
+
return void shares?.tunnels().then((tunnels) => sendJson2(res, 200, { share: shares?.status() ?? null, tunnels }));
|
|
7129
|
+
}
|
|
7130
|
+
if (!context.remote && path === `${LEGLAS_PREFIX}/api/share` && req.method === "POST") {
|
|
7131
|
+
if (!hasJsonBody(req)) {
|
|
7132
|
+
return sendJson2(res, 400, { ok: false, error: "Share details must be JSON." });
|
|
7133
|
+
}
|
|
7134
|
+
let body = "";
|
|
7135
|
+
req.on("data", (chunk) => body += chunk);
|
|
7136
|
+
return void req.on("end", async () => {
|
|
7137
|
+
const parsed2 = jsonBody(body);
|
|
7138
|
+
if (parsed2 === null) {
|
|
7139
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
7140
|
+
}
|
|
7141
|
+
const result2 = await shares?.create(parsed2).catch((error) => ({
|
|
7142
|
+
ok: false,
|
|
7143
|
+
status: 500,
|
|
7144
|
+
error: `Leglas could not start the share (${error instanceof Error ? error.message : String(error)}).`
|
|
7145
|
+
}));
|
|
7146
|
+
if (result2 === void 0) {
|
|
7147
|
+
return sendJson2(res, 500, { ok: false, error: "Sharing is not available." });
|
|
7148
|
+
}
|
|
7149
|
+
return result2.ok ? sendJson2(res, 200, result2) : sendJson2(res, result2.status, { ok: false, error: result2.error });
|
|
7150
|
+
});
|
|
7151
|
+
}
|
|
7152
|
+
if (!context.remote && path === `${LEGLAS_PREFIX}/api/share/update` && req.method === "POST") {
|
|
7153
|
+
if (!hasJsonBody(req)) {
|
|
7154
|
+
return sendJson2(res, 400, { ok: false, error: "Share details must be JSON." });
|
|
7155
|
+
}
|
|
7156
|
+
let body = "";
|
|
7157
|
+
req.on("data", (chunk) => body += chunk);
|
|
7158
|
+
return void req.on("end", async () => {
|
|
7159
|
+
const parsed2 = jsonBody(body);
|
|
7160
|
+
if (parsed2 === null) {
|
|
7161
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
7162
|
+
}
|
|
7163
|
+
const result2 = await shares?.update(parsed2);
|
|
7164
|
+
if (result2 === void 0) {
|
|
7165
|
+
return sendJson2(res, 500, { ok: false, error: "Sharing is not available." });
|
|
7166
|
+
}
|
|
7167
|
+
return result2.ok ? sendJson2(res, 200, result2) : sendJson2(res, result2.status, { ok: false, error: result2.error });
|
|
7168
|
+
});
|
|
7169
|
+
}
|
|
7170
|
+
if (!context.remote && path === `${LEGLAS_PREFIX}/api/share/grants` && req.method === "POST") {
|
|
7171
|
+
return void readShareBody(req, res, (body) => shares?.createGrant(body));
|
|
7172
|
+
}
|
|
7173
|
+
if (!context.remote && path === `${LEGLAS_PREFIX}/api/share/grants/revoke` && req.method === "POST") {
|
|
7174
|
+
return void readShareBody(req, res, (body) => shares?.revokeGrant(body));
|
|
7175
|
+
}
|
|
7176
|
+
if (!context.remote && path === `${LEGLAS_PREFIX}/api/share/grants/extend` && req.method === "POST") {
|
|
7177
|
+
return void readShareBody(req, res, (body) => shares?.extendGrant(body));
|
|
7178
|
+
}
|
|
7179
|
+
if (!context.remote && path === `${LEGLAS_PREFIX}/api/share/allow` && req.method === "POST") {
|
|
7180
|
+
return void readShareBody(req, res, (body) => shares?.allowRoute(body));
|
|
7181
|
+
}
|
|
7182
|
+
if (!context.remote && path === `${LEGLAS_PREFIX}/api/share/rotate` && req.method === "POST") {
|
|
7183
|
+
return void readShareBody(req, res, () => shares?.rotate());
|
|
7184
|
+
}
|
|
7185
|
+
if (!context.remote && path === `${LEGLAS_PREFIX}/api/share/stop` && req.method === "POST") {
|
|
7186
|
+
return void (shares?.stop() ?? Promise.resolve()).then(() => sendJson2(res, 200, { ok: true }));
|
|
5710
7187
|
}
|
|
5711
7188
|
if (path === `${LEGLAS_PREFIX}/api/config`) {
|
|
5712
7189
|
const boot = config?.previews ?? [];
|
|
@@ -5752,23 +7229,23 @@ async function startServer(options) {
|
|
|
5752
7229
|
return void req.on("end", async () => {
|
|
5753
7230
|
const parsed2 = jsonBody(body);
|
|
5754
7231
|
if (parsed2 === null) {
|
|
5755
|
-
return
|
|
7232
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
5756
7233
|
}
|
|
5757
7234
|
if (typeof parsed2.title !== "string" || parsed2.title.trim() === "") {
|
|
5758
|
-
return
|
|
7235
|
+
return sendJson2(res, 400, { ok: false, error: "Body needs a direction title." });
|
|
5759
7236
|
}
|
|
5760
7237
|
const preview = (await livePreviewDefinitions()).find((entry) => entry.title === parsed2.title);
|
|
5761
7238
|
if (preview === void 0) {
|
|
5762
|
-
return
|
|
7239
|
+
return sendJson2(res, 404, { ok: false, error: "No such direction." });
|
|
5763
7240
|
}
|
|
5764
7241
|
if (preview.branch === void 0) {
|
|
5765
|
-
return
|
|
7242
|
+
return sendJson2(res, 400, {
|
|
5766
7243
|
ok: false,
|
|
5767
7244
|
error: `"${preview.title}" is not a branch preview.`
|
|
5768
7245
|
});
|
|
5769
7246
|
}
|
|
5770
7247
|
if (config?.devCommand === void 0) {
|
|
5771
|
-
return
|
|
7248
|
+
return sendJson2(res, 400, {
|
|
5772
7249
|
ok: false,
|
|
5773
7250
|
error: `"${preview.title}" cannot start because the config sets no devCommand.`
|
|
5774
7251
|
});
|
|
@@ -5776,9 +7253,9 @@ async function startServer(options) {
|
|
|
5776
7253
|
void branches.start(preview.title);
|
|
5777
7254
|
const state = branches.state(preview.title);
|
|
5778
7255
|
if (state === void 0) {
|
|
5779
|
-
return
|
|
7256
|
+
return sendJson2(res, 404, { ok: false, error: "No such branch preview." });
|
|
5780
7257
|
}
|
|
5781
|
-
return
|
|
7258
|
+
return sendJson2(res, 200, { ok: true, state: publicBranchState(state) });
|
|
5782
7259
|
});
|
|
5783
7260
|
}
|
|
5784
7261
|
if (path === `${LEGLAS_PREFIX}/api/previews/delete` && req.method === "POST") {
|
|
@@ -5787,11 +7264,11 @@ async function startServer(options) {
|
|
|
5787
7264
|
return void req.on("end", async () => {
|
|
5788
7265
|
const parsed2 = jsonBody(body);
|
|
5789
7266
|
if (parsed2 === null) {
|
|
5790
|
-
return
|
|
7267
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
5791
7268
|
}
|
|
5792
7269
|
const titles = parsed2.titles;
|
|
5793
7270
|
if (!Array.isArray(titles) || titles.length === 0 || titles.some((title) => typeof title !== "string" || title.trim() === "")) {
|
|
5794
|
-
return
|
|
7271
|
+
return sendJson2(res, 400, {
|
|
5795
7272
|
ok: false,
|
|
5796
7273
|
error: "Body needs a non-empty array of direction titles."
|
|
5797
7274
|
});
|
|
@@ -5800,20 +7277,20 @@ async function startServer(options) {
|
|
|
5800
7277
|
try {
|
|
5801
7278
|
const local = await readLocalPreviews(cwd);
|
|
5802
7279
|
if (local.errors.length > 0) {
|
|
5803
|
-
return
|
|
7280
|
+
return sendJson2(res, 409, { ok: false, error: local.errors.join(" ") });
|
|
5804
7281
|
}
|
|
5805
7282
|
const localTitles = new Set(local.previews.map((preview) => preview.title));
|
|
5806
7283
|
const unknown = unique.filter((title) => !localTitles.has(title));
|
|
5807
7284
|
if (unknown.length > 0) {
|
|
5808
|
-
return
|
|
7285
|
+
return sendJson2(res, 400, {
|
|
5809
7286
|
ok: false,
|
|
5810
7287
|
error: "Only machine-local directions can be deleted from the registry."
|
|
5811
7288
|
});
|
|
5812
7289
|
}
|
|
5813
7290
|
const deleted = await dropLocalPreviews(cwd, unique);
|
|
5814
|
-
return
|
|
7291
|
+
return sendJson2(res, 200, { ok: true, deleted });
|
|
5815
7292
|
} catch {
|
|
5816
|
-
return
|
|
7293
|
+
return sendJson2(res, 500, {
|
|
5817
7294
|
ok: false,
|
|
5818
7295
|
error: "The directions could not be deleted from Leglas."
|
|
5819
7296
|
});
|
|
@@ -5823,7 +7300,7 @@ async function startServer(options) {
|
|
|
5823
7300
|
if (path === `${LEGLAS_PREFIX}/api/references` && req.method === "POST") {
|
|
5824
7301
|
const declaredLength = req.headers["content-length"];
|
|
5825
7302
|
if (typeof declaredLength === "string" && Number(declaredLength) > REFERENCE_MAX_BYTES) {
|
|
5826
|
-
return
|
|
7303
|
+
return sendJson2(res, 413, { ok: false, error: "That image is over 10MB." });
|
|
5827
7304
|
}
|
|
5828
7305
|
const chunks = [];
|
|
5829
7306
|
let bytes = 0;
|
|
@@ -5837,7 +7314,7 @@ async function startServer(options) {
|
|
|
5837
7314
|
refused = true;
|
|
5838
7315
|
req.pause();
|
|
5839
7316
|
res.once("finish", () => req.socket.destroy());
|
|
5840
|
-
|
|
7317
|
+
sendJson2(res, 413, { ok: false, error: "That image is over 10MB." });
|
|
5841
7318
|
return;
|
|
5842
7319
|
}
|
|
5843
7320
|
chunks.push(buffer);
|
|
@@ -5846,12 +7323,12 @@ async function startServer(options) {
|
|
|
5846
7323
|
if (refused)
|
|
5847
7324
|
return;
|
|
5848
7325
|
if (bytes === 0) {
|
|
5849
|
-
return
|
|
7326
|
+
return sendJson2(res, 400, { ok: false, error: "The upload was empty." });
|
|
5850
7327
|
}
|
|
5851
7328
|
const body = Buffer.concat(chunks, bytes);
|
|
5852
7329
|
const image = sniffImage(body);
|
|
5853
7330
|
if (image === null) {
|
|
5854
|
-
return
|
|
7331
|
+
return sendJson2(res, 415, {
|
|
5855
7332
|
ok: false,
|
|
5856
7333
|
error: "Only PNG, JPEG, WebP and GIF images can be attached."
|
|
5857
7334
|
});
|
|
@@ -5863,7 +7340,7 @@ async function startServer(options) {
|
|
|
5863
7340
|
await writeFile8(join12(cwd, file), body);
|
|
5864
7341
|
void pruneReferences(cwd).catch(() => {
|
|
5865
7342
|
});
|
|
5866
|
-
return
|
|
7343
|
+
return sendJson2(res, 200, {
|
|
5867
7344
|
ok: true,
|
|
5868
7345
|
reference: {
|
|
5869
7346
|
id,
|
|
@@ -5875,7 +7352,7 @@ async function startServer(options) {
|
|
|
5875
7352
|
}
|
|
5876
7353
|
});
|
|
5877
7354
|
} catch {
|
|
5878
|
-
return
|
|
7355
|
+
return sendJson2(res, 500, {
|
|
5879
7356
|
ok: false,
|
|
5880
7357
|
error: "The image could not be attached."
|
|
5881
7358
|
});
|
|
@@ -5888,24 +7365,24 @@ async function startServer(options) {
|
|
|
5888
7365
|
return void req.on("end", async () => {
|
|
5889
7366
|
const parsed2 = jsonBody(body);
|
|
5890
7367
|
if (parsed2 === null) {
|
|
5891
|
-
return
|
|
7368
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
5892
7369
|
}
|
|
5893
7370
|
if (parsed2.mode !== void 0 && parsed2.mode !== "variant" && parsed2.mode !== "replace") {
|
|
5894
|
-
return
|
|
7371
|
+
return sendJson2(res, 400, {
|
|
5895
7372
|
ok: false,
|
|
5896
7373
|
error: 'mode must be "variant" or "replace".'
|
|
5897
7374
|
});
|
|
5898
7375
|
}
|
|
5899
7376
|
const mode = parsed2.mode === "replace" ? "replace" : "variant";
|
|
5900
7377
|
if (parsed2.references !== void 0 && (!Array.isArray(parsed2.references) || parsed2.references.some((reference) => typeof reference !== "string" || !/^[A-Za-z0-9_-]{1,32}$/.test(reference)))) {
|
|
5901
|
-
return
|
|
7378
|
+
return sendJson2(res, 400, { ok: false, error: "references must be uploaded image ids." });
|
|
5902
7379
|
}
|
|
5903
7380
|
const references = parsed2.references ?? [];
|
|
5904
7381
|
if (references.length > 0) {
|
|
5905
7382
|
const present = new Set((await readdir3(join12(cwd, REFERENCES_DIR)).catch(() => [])).map((name) => name.slice(0, name.indexOf(".") === -1 ? name.length : name.indexOf("."))));
|
|
5906
7383
|
const gone = references.filter((id2) => !present.has(id2));
|
|
5907
7384
|
if (gone.length > 0) {
|
|
5908
|
-
return
|
|
7385
|
+
return sendJson2(res, 410, {
|
|
5909
7386
|
ok: false,
|
|
5910
7387
|
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."
|
|
5911
7388
|
});
|
|
@@ -5915,11 +7392,11 @@ async function startServer(options) {
|
|
|
5915
7392
|
const previews = await livePreviews();
|
|
5916
7393
|
const preview = previews.find((entry) => entry.title === parsed2.title);
|
|
5917
7394
|
if (!preview) {
|
|
5918
|
-
return
|
|
7395
|
+
return sendJson2(res, 400, { ok: false, error: "Unknown preview, or empty request." });
|
|
5919
7396
|
}
|
|
5920
7397
|
const notes = annotationsFor(await readAnnotations(cwd).catch(() => []), preview.title);
|
|
5921
7398
|
if (!parsed2.intent?.trim() && notes.length === 0) {
|
|
5922
|
-
return
|
|
7399
|
+
return sendJson2(res, 400, { ok: false, error: "Unknown preview, or empty request." });
|
|
5923
7400
|
}
|
|
5924
7401
|
const intent = (parsed2.intent ?? "").trim();
|
|
5925
7402
|
const live2 = (await readRequests(cwd).catch(() => [])).filter((entry) => entry.status === "queued" || entry.status === "picked-up");
|
|
@@ -5933,7 +7410,7 @@ async function startServer(options) {
|
|
|
5933
7410
|
// one forks the direction and the other rewrites it. Only a
|
|
5934
7411
|
// genuine repeat is refused.
|
|
5935
7412
|
(entry.mode ?? "replace") === mode && sameNotes(entry) && sameContext(entry))) {
|
|
5936
|
-
return
|
|
7413
|
+
return sendJson2(res, 409, {
|
|
5937
7414
|
ok: false,
|
|
5938
7415
|
duplicate: true,
|
|
5939
7416
|
error: `That exact change to ${preview.title} is already waiting.`
|
|
@@ -5967,13 +7444,13 @@ async function startServer(options) {
|
|
|
5967
7444
|
...composed
|
|
5968
7445
|
}, id);
|
|
5969
7446
|
runner?.nudge();
|
|
5970
|
-
return
|
|
7447
|
+
return sendJson2(res, 200, {
|
|
5971
7448
|
ok: true,
|
|
5972
7449
|
...composed,
|
|
5973
7450
|
attachments: captured.attachments
|
|
5974
7451
|
});
|
|
5975
7452
|
} catch {
|
|
5976
|
-
return
|
|
7453
|
+
return sendJson2(res, 200, {
|
|
5977
7454
|
ok: true,
|
|
5978
7455
|
...composed,
|
|
5979
7456
|
attachments: captured.attachments,
|
|
@@ -5984,29 +7461,29 @@ async function startServer(options) {
|
|
|
5984
7461
|
}
|
|
5985
7462
|
if (path === `${LEGLAS_PREFIX}/api/capture` && req.method === "POST") {
|
|
5986
7463
|
if (!hasJsonBody(req)) {
|
|
5987
|
-
return
|
|
7464
|
+
return sendJson2(res, 400, { ok: false, error: "Capture must be JSON." });
|
|
5988
7465
|
}
|
|
5989
7466
|
let body = "";
|
|
5990
7467
|
req.on("data", (chunk) => body += chunk);
|
|
5991
7468
|
return void req.on("end", async () => {
|
|
5992
7469
|
const parsed2 = jsonBody(body);
|
|
5993
7470
|
if (parsed2 === null) {
|
|
5994
|
-
return
|
|
7471
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
5995
7472
|
}
|
|
5996
7473
|
if (typeof parsed2.title !== "string" || parsed2.title === "") {
|
|
5997
|
-
return
|
|
7474
|
+
return sendJson2(res, 400, { ok: false, error: "Capture needs a direction title." });
|
|
5998
7475
|
}
|
|
5999
7476
|
if (parsed2.note !== void 0 && typeof parsed2.note !== "string") {
|
|
6000
|
-
return
|
|
7477
|
+
return sendJson2(res, 400, { ok: false, error: "The note id must be a string." });
|
|
6001
7478
|
}
|
|
6002
7479
|
const preview = (await livePreviews()).find((entry) => entry.title === parsed2.title);
|
|
6003
7480
|
if (preview === void 0) {
|
|
6004
|
-
return
|
|
7481
|
+
return sendJson2(res, 404, { ok: false, error: "No such direction." });
|
|
6005
7482
|
}
|
|
6006
7483
|
const width = typeof parsed2.width === "number" && Number.isFinite(parsed2.width) ? Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, Math.round(parsed2.width))) : 1440;
|
|
6007
7484
|
const browser = await browserPool.acquire();
|
|
6008
7485
|
if (browser === null) {
|
|
6009
|
-
return
|
|
7486
|
+
return sendJson2(res, 503, {
|
|
6010
7487
|
ok: false,
|
|
6011
7488
|
error: browserPool.reason() ?? NO_BROWSER
|
|
6012
7489
|
});
|
|
@@ -6043,7 +7520,7 @@ async function startServer(options) {
|
|
|
6043
7520
|
});
|
|
6044
7521
|
const result2 = await Promise.race([work, timeout]);
|
|
6045
7522
|
if (result2 === timeoutMarker) {
|
|
6046
|
-
return
|
|
7523
|
+
return sendJson2(res, 504, { ok: false, error: "The page did not load in time." });
|
|
6047
7524
|
}
|
|
6048
7525
|
clearTimeout(timer);
|
|
6049
7526
|
const crop = annotations.length > 0 ? result2.crops[0] : null;
|
|
@@ -6053,18 +7530,19 @@ async function startServer(options) {
|
|
|
6053
7530
|
const relativeFile = `${CAPTURES_DIR}/show/${name}`;
|
|
6054
7531
|
await mkdir8(join12(cwd, CAPTURES_DIR, "show"), { recursive: true });
|
|
6055
7532
|
await writeFile8(join12(cwd, relativeFile), shot.png);
|
|
6056
|
-
return
|
|
7533
|
+
return sendJson2(res, 200, {
|
|
6057
7534
|
ok: true,
|
|
6058
7535
|
file: relativeFile,
|
|
6059
7536
|
width: shot.width,
|
|
6060
7537
|
height: shot.height,
|
|
6061
7538
|
viewport: result2.frame.width,
|
|
6062
7539
|
errors: result2.errors,
|
|
7540
|
+
hydration: result2.hydration,
|
|
6063
7541
|
cut: result2.cut
|
|
6064
7542
|
});
|
|
6065
7543
|
} catch (error) {
|
|
6066
7544
|
clearTimeout(timer);
|
|
6067
|
-
return
|
|
7545
|
+
return sendJson2(res, 502, {
|
|
6068
7546
|
ok: false,
|
|
6069
7547
|
error: error instanceof Error ? error.message : String(error)
|
|
6070
7548
|
});
|
|
@@ -6075,14 +7553,14 @@ async function startServer(options) {
|
|
|
6075
7553
|
return void readAgentChoice(cwd).then((choice) => {
|
|
6076
7554
|
if (choice.agent !== null)
|
|
6077
7555
|
runner?.prepare(choice.agent);
|
|
6078
|
-
|
|
6079
|
-
}, () =>
|
|
7556
|
+
sendJson2(res, 200, { ok: true });
|
|
7557
|
+
}, () => sendJson2(res, 200, { ok: true }));
|
|
6080
7558
|
}
|
|
6081
7559
|
if (path === `${LEGLAS_PREFIX}/api/agents` && req.method === "GET") {
|
|
6082
7560
|
return void Promise.all([
|
|
6083
7561
|
currentAgents(query.get("refresh") === "1"),
|
|
6084
7562
|
readAgentChoice(cwd)
|
|
6085
|
-
]).then(([agents, choice]) =>
|
|
7563
|
+
]).then(([agents, choice]) => sendJson2(res, 200, {
|
|
6086
7564
|
agents,
|
|
6087
7565
|
choice: choice.agent,
|
|
6088
7566
|
customRun: choice.run,
|
|
@@ -6091,48 +7569,48 @@ async function startServer(options) {
|
|
|
6091
7569
|
}
|
|
6092
7570
|
if (path === `${LEGLAS_PREFIX}/api/agent` && req.method === "POST") {
|
|
6093
7571
|
if (!isLoopbackAddress(req.socket.remoteAddress)) {
|
|
6094
|
-
return
|
|
7572
|
+
return sendJson2(res, 403, {
|
|
6095
7573
|
ok: false,
|
|
6096
7574
|
error: "The agent choice can only be made from the machine running Leglas."
|
|
6097
7575
|
});
|
|
6098
7576
|
}
|
|
6099
7577
|
if (!hasJsonBody(req)) {
|
|
6100
|
-
return
|
|
7578
|
+
return sendJson2(res, 400, { ok: false, error: "Agent choice must be JSON." });
|
|
6101
7579
|
}
|
|
6102
7580
|
let body = "";
|
|
6103
7581
|
req.on("data", (chunk) => body += chunk);
|
|
6104
7582
|
return void req.on("end", () => {
|
|
6105
7583
|
const parsed2 = jsonBody(body);
|
|
6106
7584
|
if (parsed2 === null) {
|
|
6107
|
-
return
|
|
7585
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6108
7586
|
}
|
|
6109
7587
|
if (!isKnownAgent(parsed2.agent) && parsed2.agent !== "custom") {
|
|
6110
|
-
return
|
|
7588
|
+
return sendJson2(res, 400, { ok: false, error: "Body needs a known agent." });
|
|
6111
7589
|
}
|
|
6112
7590
|
if (parsed2.run !== void 0 && typeof parsed2.run !== "string") {
|
|
6113
|
-
return
|
|
7591
|
+
return sendJson2(res, 400, { ok: false, error: "The custom run command must be a string." });
|
|
6114
7592
|
}
|
|
6115
7593
|
const effort = parsed2.effort === null || isAgentEffort(parsed2.effort) ? parsed2.effort : void 0;
|
|
6116
7594
|
if (parsed2.effort !== void 0 && effort === void 0) {
|
|
6117
|
-
return
|
|
7595
|
+
return sendJson2(res, 400, { ok: false, error: "Effort must be a supported level or null." });
|
|
6118
7596
|
}
|
|
6119
7597
|
if (parsed2.agent === "custom") {
|
|
6120
7598
|
if (effort !== void 0) {
|
|
6121
|
-
return
|
|
7599
|
+
return sendJson2(res, 400, {
|
|
6122
7600
|
ok: false,
|
|
6123
7601
|
error: "Custom agents manage effort in their own command."
|
|
6124
7602
|
});
|
|
6125
7603
|
}
|
|
6126
7604
|
if (typeof parsed2.run !== "string") {
|
|
6127
|
-
return
|
|
7605
|
+
return sendJson2(res, 400, { ok: false, error: "A custom agent needs a run command." });
|
|
6128
7606
|
}
|
|
6129
7607
|
const template = parseTemplate(parsed2.run);
|
|
6130
7608
|
if (!template.ok)
|
|
6131
|
-
return
|
|
6132
|
-
return void saveAgentChoice(cwd, { agent: "custom", run: parsed2.run }).then(() =>
|
|
7609
|
+
return sendJson2(res, 400, { ok: false, error: template.error });
|
|
7610
|
+
return void saveAgentChoice(cwd, { agent: "custom", run: parsed2.run }).then(() => sendJson2(res, 200, { ok: true }), () => sendJson2(res, 500, { ok: false, error: "Agent choice could not be saved." }));
|
|
6133
7611
|
}
|
|
6134
7612
|
if (effort !== void 0 && effort !== null && !KNOWN_AGENTS[parsed2.agent].efforts.includes(effort)) {
|
|
6135
|
-
return
|
|
7613
|
+
return sendJson2(res, 400, {
|
|
6136
7614
|
ok: false,
|
|
6137
7615
|
error: `${KNOWN_AGENTS[parsed2.agent].name} does not expose an effort override.`
|
|
6138
7616
|
});
|
|
@@ -6142,8 +7620,8 @@ async function startServer(options) {
|
|
|
6142
7620
|
...effort === void 0 ? {} : { effort }
|
|
6143
7621
|
}).then(() => {
|
|
6144
7622
|
runner?.prepare(parsed2.agent);
|
|
6145
|
-
|
|
6146
|
-
}, () =>
|
|
7623
|
+
sendJson2(res, 200, { ok: true });
|
|
7624
|
+
}, () => sendJson2(res, 500, { ok: false, error: "Agent choice could not be saved." }));
|
|
6147
7625
|
});
|
|
6148
7626
|
}
|
|
6149
7627
|
if (path === `${LEGLAS_PREFIX}/api/watch` && req.method === "POST") {
|
|
@@ -6152,13 +7630,13 @@ async function startServer(options) {
|
|
|
6152
7630
|
return void req.on("end", () => {
|
|
6153
7631
|
const parsed2 = jsonBody(body);
|
|
6154
7632
|
if (parsed2 === null) {
|
|
6155
|
-
return
|
|
7633
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6156
7634
|
}
|
|
6157
7635
|
if (typeof parsed2.watching !== "boolean") {
|
|
6158
|
-
return
|
|
7636
|
+
return sendJson2(res, 400, { ok: false, error: "Body needs a watching boolean." });
|
|
6159
7637
|
}
|
|
6160
7638
|
lastSeen = parsed2.watching ? Date.now() : null;
|
|
6161
|
-
|
|
7639
|
+
sendJson2(res, 200, { ok: true });
|
|
6162
7640
|
});
|
|
6163
7641
|
}
|
|
6164
7642
|
if (path === `${LEGLAS_PREFIX}/api/requests` && req.method === "GET") {
|
|
@@ -6207,47 +7685,47 @@ async function startServer(options) {
|
|
|
6207
7685
|
}
|
|
6208
7686
|
if (path === `${LEGLAS_PREFIX}/api/requests/cancel` && req.method === "POST") {
|
|
6209
7687
|
if (!hasJsonBody(req)) {
|
|
6210
|
-
return
|
|
7688
|
+
return sendJson2(res, 200, { ok: true, cancelled: runner?.cancel() ?? false });
|
|
6211
7689
|
}
|
|
6212
7690
|
let body = "";
|
|
6213
7691
|
req.on("data", (chunk) => body += chunk);
|
|
6214
7692
|
return void req.on("end", () => {
|
|
6215
7693
|
const parsed2 = jsonBody(body);
|
|
6216
7694
|
if (parsed2 === null) {
|
|
6217
|
-
return
|
|
7695
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6218
7696
|
}
|
|
6219
7697
|
if (parsed2.id !== void 0 && typeof parsed2.id !== "string") {
|
|
6220
|
-
return
|
|
7698
|
+
return sendJson2(res, 400, { ok: false, error: "The request id must be a string." });
|
|
6221
7699
|
}
|
|
6222
|
-
return
|
|
7700
|
+
return sendJson2(res, 200, { ok: true, cancelled: runner?.cancel(parsed2.id) ?? false });
|
|
6223
7701
|
});
|
|
6224
7702
|
}
|
|
6225
7703
|
if (path === `${LEGLAS_PREFIX}/api/requests/retry` && req.method === "POST") {
|
|
6226
7704
|
if (!hasJsonBody(req)) {
|
|
6227
|
-
return
|
|
7705
|
+
return sendJson2(res, 400, { ok: false, error: "Retry must be JSON." });
|
|
6228
7706
|
}
|
|
6229
7707
|
let body = "";
|
|
6230
7708
|
req.on("data", (chunk) => body += chunk);
|
|
6231
7709
|
return void req.on("end", async () => {
|
|
6232
7710
|
const parsed2 = jsonBody(body);
|
|
6233
7711
|
if (parsed2 === null) {
|
|
6234
|
-
return
|
|
7712
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6235
7713
|
}
|
|
6236
7714
|
if (typeof parsed2.id !== "string") {
|
|
6237
|
-
return
|
|
7715
|
+
return sendJson2(res, 400, { ok: false, error: "Body needs a request id." });
|
|
6238
7716
|
}
|
|
6239
7717
|
const request = (await readRequests(cwd)).find((entry) => entry.id === parsed2.id);
|
|
6240
7718
|
if (request === void 0) {
|
|
6241
|
-
return
|
|
7719
|
+
return sendJson2(res, 404, { ok: false, error: "No such request." });
|
|
6242
7720
|
}
|
|
6243
7721
|
if (!isEnded(request, runner?.snapshot().failedIds ?? [])) {
|
|
6244
|
-
return
|
|
7722
|
+
return sendJson2(res, 400, { ok: false, error: "Only an ended request can be run again." });
|
|
6245
7723
|
}
|
|
6246
7724
|
try {
|
|
6247
7725
|
const retryId = newRequestId();
|
|
6248
7726
|
const attachments = await rehomeCaptures(cwd, request.id, retryId, request.attachments ?? []).catch(() => []);
|
|
6249
7727
|
if (!await removeRequest(cwd, request.id)) {
|
|
6250
|
-
return
|
|
7728
|
+
return sendJson2(res, 404, { ok: false, error: "No such request." });
|
|
6251
7729
|
}
|
|
6252
7730
|
await appendRequest(cwd, {
|
|
6253
7731
|
title: request.title,
|
|
@@ -6268,9 +7746,9 @@ async function startServer(options) {
|
|
|
6268
7746
|
...request.references === void 0 ? {} : { references: request.references }
|
|
6269
7747
|
}, retryId);
|
|
6270
7748
|
runner?.nudge();
|
|
6271
|
-
return
|
|
7749
|
+
return sendJson2(res, 200, { ok: true });
|
|
6272
7750
|
} catch {
|
|
6273
|
-
return
|
|
7751
|
+
return sendJson2(res, 500, { ok: false, error: "The request could not be retried." });
|
|
6274
7752
|
}
|
|
6275
7753
|
});
|
|
6276
7754
|
}
|
|
@@ -6279,21 +7757,21 @@ async function startServer(options) {
|
|
|
6279
7757
|
}
|
|
6280
7758
|
if (path === `${LEGLAS_PREFIX}/api/annotations` && req.method === "POST") {
|
|
6281
7759
|
if (!hasJsonBody(req)) {
|
|
6282
|
-
return
|
|
7760
|
+
return sendJson2(res, 400, { ok: false, error: "A note must be JSON." });
|
|
6283
7761
|
}
|
|
6284
7762
|
let body = "";
|
|
6285
7763
|
req.on("data", (chunk) => body += chunk);
|
|
6286
7764
|
return void req.on("end", async () => {
|
|
6287
7765
|
const parsed2 = jsonBody(body);
|
|
6288
7766
|
if (parsed2 === null) {
|
|
6289
|
-
return
|
|
7767
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6290
7768
|
}
|
|
6291
7769
|
if (typeof parsed2.title !== "string" || parsed2.title.trim() === "") {
|
|
6292
|
-
return
|
|
7770
|
+
return sendJson2(res, 400, { ok: false, error: "A note needs a direction." });
|
|
6293
7771
|
}
|
|
6294
7772
|
const anchor = anchorFrom(parsed2.anchor);
|
|
6295
7773
|
if (anchor === null) {
|
|
6296
|
-
return
|
|
7774
|
+
return sendJson2(res, 400, { ok: false, error: "A note needs something to point at." });
|
|
6297
7775
|
}
|
|
6298
7776
|
try {
|
|
6299
7777
|
const annotation = await addAnnotation(cwd, {
|
|
@@ -6301,87 +7779,87 @@ async function startServer(options) {
|
|
|
6301
7779
|
note: typeof parsed2.note === "string" ? parsed2.note.trim() : "",
|
|
6302
7780
|
title: parsed2.title
|
|
6303
7781
|
});
|
|
6304
|
-
return
|
|
7782
|
+
return sendJson2(res, 200, { ok: true, annotation });
|
|
6305
7783
|
} catch {
|
|
6306
|
-
return
|
|
7784
|
+
return sendJson2(res, 500, { ok: false, error: "The note could not be kept." });
|
|
6307
7785
|
}
|
|
6308
7786
|
});
|
|
6309
7787
|
}
|
|
6310
7788
|
if (path === `${LEGLAS_PREFIX}/api/annotations/update` && req.method === "POST") {
|
|
6311
7789
|
if (!hasJsonBody(req)) {
|
|
6312
|
-
return
|
|
7790
|
+
return sendJson2(res, 400, { ok: false, error: "A note must be JSON." });
|
|
6313
7791
|
}
|
|
6314
7792
|
let body = "";
|
|
6315
7793
|
req.on("data", (chunk) => body += chunk);
|
|
6316
7794
|
return void req.on("end", async () => {
|
|
6317
7795
|
const parsed2 = jsonBody(body);
|
|
6318
7796
|
if (parsed2 === null) {
|
|
6319
|
-
return
|
|
7797
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6320
7798
|
}
|
|
6321
7799
|
if (typeof parsed2.id !== "string" || parsed2.id === "") {
|
|
6322
|
-
return
|
|
7800
|
+
return sendJson2(res, 400, { ok: false, error: "Body needs the note to reword." });
|
|
6323
7801
|
}
|
|
6324
7802
|
if (typeof parsed2.note !== "string") {
|
|
6325
|
-
return
|
|
7803
|
+
return sendJson2(res, 400, { ok: false, error: "A reworded note needs its words." });
|
|
6326
7804
|
}
|
|
6327
7805
|
try {
|
|
6328
7806
|
const annotation = await updateAnnotation(cwd, parsed2.id, parsed2.note);
|
|
6329
7807
|
if (annotation === null) {
|
|
6330
|
-
return
|
|
7808
|
+
return sendJson2(res, 404, { ok: false, error: "That note has gone." });
|
|
6331
7809
|
}
|
|
6332
|
-
return
|
|
7810
|
+
return sendJson2(res, 200, { ok: true, annotation });
|
|
6333
7811
|
} catch {
|
|
6334
|
-
return
|
|
7812
|
+
return sendJson2(res, 500, { ok: false, error: "The note could not be reworded." });
|
|
6335
7813
|
}
|
|
6336
7814
|
});
|
|
6337
7815
|
}
|
|
6338
7816
|
if (path === `${LEGLAS_PREFIX}/api/annotations/delete` && req.method === "POST") {
|
|
6339
7817
|
if (!hasJsonBody(req)) {
|
|
6340
|
-
return
|
|
7818
|
+
return sendJson2(res, 400, { ok: false, error: "Delete must be JSON." });
|
|
6341
7819
|
}
|
|
6342
7820
|
let body = "";
|
|
6343
7821
|
req.on("data", (chunk) => body += chunk);
|
|
6344
7822
|
return void req.on("end", async () => {
|
|
6345
7823
|
const parsed2 = jsonBody(body);
|
|
6346
7824
|
if (parsed2 === null) {
|
|
6347
|
-
return
|
|
7825
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6348
7826
|
}
|
|
6349
7827
|
const ids = Array.isArray(parsed2.ids) ? parsed2.ids.filter((entry) => typeof entry === "string") : [];
|
|
6350
7828
|
if (ids.length === 0) {
|
|
6351
|
-
return
|
|
7829
|
+
return sendJson2(res, 400, { ok: false, error: "Body needs the notes to forget." });
|
|
6352
7830
|
}
|
|
6353
7831
|
try {
|
|
6354
|
-
return
|
|
7832
|
+
return sendJson2(res, 200, { ok: true, deleted: await removeAnnotations(cwd, ids) });
|
|
6355
7833
|
} catch {
|
|
6356
|
-
return
|
|
7834
|
+
return sendJson2(res, 500, { ok: false, error: "The notes could not be forgotten." });
|
|
6357
7835
|
}
|
|
6358
7836
|
});
|
|
6359
7837
|
}
|
|
6360
7838
|
if (path === `${LEGLAS_PREFIX}/api/requests/dismiss` && req.method === "POST") {
|
|
6361
7839
|
if (!hasJsonBody(req)) {
|
|
6362
|
-
return
|
|
7840
|
+
return sendJson2(res, 400, { ok: false, error: "Dismiss must be JSON." });
|
|
6363
7841
|
}
|
|
6364
7842
|
let body = "";
|
|
6365
7843
|
req.on("data", (chunk) => body += chunk);
|
|
6366
7844
|
return void req.on("end", async () => {
|
|
6367
7845
|
const parsed2 = jsonBody(body);
|
|
6368
7846
|
if (parsed2 === null) {
|
|
6369
|
-
return
|
|
7847
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6370
7848
|
}
|
|
6371
7849
|
if (typeof parsed2.id !== "string") {
|
|
6372
|
-
return
|
|
7850
|
+
return sendJson2(res, 400, { ok: false, error: "Body needs a request id." });
|
|
6373
7851
|
}
|
|
6374
7852
|
const target2 = (await readRequests(cwd)).find((entry) => entry.id === parsed2.id);
|
|
6375
7853
|
if (target2 === void 0 || !isEnded(target2, runner?.snapshot().failedIds ?? [])) {
|
|
6376
|
-
return
|
|
7854
|
+
return sendJson2(res, 400, { ok: false, error: "Only an ended request can be dismissed." });
|
|
6377
7855
|
}
|
|
6378
7856
|
try {
|
|
6379
7857
|
if (!await removeRequest(cwd, parsed2.id)) {
|
|
6380
|
-
return
|
|
7858
|
+
return sendJson2(res, 404, { ok: false, error: "No such request." });
|
|
6381
7859
|
}
|
|
6382
|
-
return
|
|
7860
|
+
return sendJson2(res, 200, { ok: true });
|
|
6383
7861
|
} catch {
|
|
6384
|
-
return
|
|
7862
|
+
return sendJson2(res, 500, { ok: false, error: "The request could not be dismissed." });
|
|
6385
7863
|
}
|
|
6386
7864
|
});
|
|
6387
7865
|
}
|
|
@@ -6391,13 +7869,13 @@ async function startServer(options) {
|
|
|
6391
7869
|
return void req.on("end", () => {
|
|
6392
7870
|
const parsed2 = jsonBody(body);
|
|
6393
7871
|
if (parsed2 === null) {
|
|
6394
|
-
return
|
|
7872
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6395
7873
|
}
|
|
6396
7874
|
if (parsed2.renames === null || typeof parsed2.renames !== "object") {
|
|
6397
|
-
return
|
|
7875
|
+
return sendJson2(res, 400, { ok: false, error: "Body needs a renames object." });
|
|
6398
7876
|
}
|
|
6399
7877
|
const renames = Object.fromEntries(Object.entries(parsed2.renames).filter((entry) => typeof entry[1] === "string" && entry[1] !== ""));
|
|
6400
|
-
void writeRenames(cwd, renames).then(() =>
|
|
7878
|
+
void writeRenames(cwd, renames).then(() => sendJson2(res, 200, { ok: true }), () => sendJson2(res, 200, { ok: false }));
|
|
6401
7879
|
});
|
|
6402
7880
|
}
|
|
6403
7881
|
if (path === `${LEGLAS_PREFIX}/api/health`) {
|
|
@@ -6414,42 +7892,86 @@ async function startServer(options) {
|
|
|
6414
7892
|
relative6 = "";
|
|
6415
7893
|
}
|
|
6416
7894
|
const dir = fileMounts.get(slug);
|
|
6417
|
-
|
|
6418
|
-
|
|
6419
|
-
|
|
6420
|
-
|
|
7895
|
+
const serveMount = () => {
|
|
7896
|
+
if (dir !== void 0 && relative6 !== "" && serveFrom(res, dir, relative6))
|
|
7897
|
+
return;
|
|
7898
|
+
res.writeHead(404, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
|
|
7899
|
+
res.end("Leglas: no such preview file.");
|
|
7900
|
+
};
|
|
7901
|
+
if (!context.remote)
|
|
7902
|
+
return serveMount();
|
|
7903
|
+
if (relative6.split("/").some((segment) => segment.startsWith("."))) {
|
|
7904
|
+
return sendJson2(res, 403, { ok: false, error: "Not available to viewers." });
|
|
7905
|
+
}
|
|
7906
|
+
return void (shares?.fileSlugAllowed(slug, context.grantId ?? "") ?? Promise.resolve(false)).then((allowed) => {
|
|
7907
|
+
if (!allowed)
|
|
7908
|
+
return sendJson2(res, 403, { ok: false, error: "Not available to viewers." });
|
|
7909
|
+
serveMount();
|
|
7910
|
+
});
|
|
6421
7911
|
}
|
|
6422
7912
|
if (path.startsWith(`${LEGLAS_PREFIX}/api/`)) {
|
|
6423
|
-
return
|
|
7913
|
+
return sendJson2(res, 404, { error: "No such Leglas API path." });
|
|
6424
7914
|
}
|
|
6425
7915
|
if (path === LEGLAS_PREFIX || path.startsWith(`${LEGLAS_PREFIX}/`)) {
|
|
6426
7916
|
if (shellDir !== null && serveShellFile(res, shellDir, path))
|
|
6427
7917
|
return;
|
|
6428
7918
|
if (shellDir !== null) {
|
|
6429
7919
|
res.writeHead(404, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
|
|
6430
|
-
|
|
7920
|
+
res.end("Leglas: no such path.");
|
|
7921
|
+
return;
|
|
6431
7922
|
}
|
|
6432
7923
|
res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
6433
|
-
|
|
7924
|
+
res.end(PLACEHOLDER);
|
|
7925
|
+
return;
|
|
6434
7926
|
}
|
|
6435
|
-
return proxy.request(req, res,
|
|
6436
|
-
}
|
|
7927
|
+
return proxy.request(req, res, context.publicOrigin);
|
|
7928
|
+
};
|
|
7929
|
+
let port = 0;
|
|
7930
|
+
const server = http4.createServer((req, res) => handleRequest(req, res, {
|
|
7931
|
+
remote: false,
|
|
7932
|
+
publicOrigin: `http://localhost:${port}`
|
|
7933
|
+
}));
|
|
6437
7934
|
const sockets = /* @__PURE__ */ new Set();
|
|
6438
7935
|
server.on("connection", (socket) => {
|
|
6439
7936
|
sockets.add(socket);
|
|
6440
7937
|
socket.once("close", () => sockets.delete(socket));
|
|
6441
7938
|
});
|
|
6442
|
-
|
|
6443
|
-
|
|
6444
|
-
|
|
7939
|
+
const handleUpgrade = (req, socket, head, context) => {
|
|
7940
|
+
const liveUpgrade = context.remote ? live.upgrade(req, socket, head, { viewer: true }) : live.upgrade(req, socket, head);
|
|
7941
|
+
if (liveUpgrade)
|
|
7942
|
+
return true;
|
|
6445
7943
|
const path = (req.url ?? "/").split("?")[0] ?? "/";
|
|
6446
|
-
if (path.startsWith(`${LEGLAS_PREFIX}/`))
|
|
6447
|
-
|
|
7944
|
+
if (path.startsWith(`${LEGLAS_PREFIX}/`)) {
|
|
7945
|
+
socket.destroy();
|
|
7946
|
+
return false;
|
|
7947
|
+
}
|
|
6448
7948
|
proxy.upgrade(req, socket, head);
|
|
7949
|
+
return false;
|
|
7950
|
+
};
|
|
7951
|
+
server.on("upgrade", (req, socket, head) => {
|
|
7952
|
+
handleUpgrade(req, socket, head, { remote: false });
|
|
7953
|
+
});
|
|
7954
|
+
shares = createShareManager({
|
|
7955
|
+
live,
|
|
7956
|
+
previews: livePreviewDefinitions,
|
|
7957
|
+
previewsForConfig,
|
|
7958
|
+
viewerConfig: {
|
|
7959
|
+
project,
|
|
7960
|
+
devServer: target,
|
|
7961
|
+
scanPreviews: config?.scanPreviews ?? true
|
|
7962
|
+
},
|
|
7963
|
+
request: (req, res, context) => handleRequest(req, res, {
|
|
7964
|
+
remote: true,
|
|
7965
|
+
publicOrigin: context.publicOrigin,
|
|
7966
|
+
grantId: context.grantId
|
|
7967
|
+
}),
|
|
7968
|
+
upgrade: (req, socket, head) => handleUpgrade(req, socket, head, { remote: true }),
|
|
7969
|
+
...options.detectTunnels === void 0 ? {} : { detectTunnels: options.detectTunnels },
|
|
7970
|
+
...options.startTunnel === void 0 ? {} : { startTunnel: options.startTunnel }
|
|
6449
7971
|
});
|
|
6450
|
-
|
|
7972
|
+
port = await bind2(server, options.port ?? DEFAULT_PORT);
|
|
6451
7973
|
const liveFiles = watchLiveFiles(cwd, bootConfigPath, live);
|
|
6452
|
-
|
|
7974
|
+
liveHealth = watchHealth(target, live);
|
|
6453
7975
|
await pruneCaptures(cwd, (await readRequests(cwd).catch(() => [])).map((request) => request.id)).catch(() => {
|
|
6454
7976
|
});
|
|
6455
7977
|
await writeServerInfo(cwd, {
|
|
@@ -6473,21 +7995,26 @@ async function startServer(options) {
|
|
|
6473
7995
|
close: () => {
|
|
6474
7996
|
if (closePromise !== null)
|
|
6475
7997
|
return closePromise;
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6482
|
-
|
|
6483
|
-
|
|
6484
|
-
|
|
6485
|
-
|
|
6486
|
-
|
|
6487
|
-
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
7998
|
+
closePromise = (async () => {
|
|
7999
|
+
liveFiles.close();
|
|
8000
|
+
liveHealth?.close();
|
|
8001
|
+
await Promise.all([
|
|
8002
|
+
shares?.close() ?? Promise.resolve(),
|
|
8003
|
+
branches.stop(),
|
|
8004
|
+
runner.stop(),
|
|
8005
|
+
browserPool.close(),
|
|
8006
|
+
live.close()
|
|
8007
|
+
]);
|
|
8008
|
+
await new Promise((done) => {
|
|
8009
|
+
for (const socket of sockets)
|
|
8010
|
+
socket.destroy();
|
|
8011
|
+
sockets.clear();
|
|
8012
|
+
server.closeAllConnections();
|
|
8013
|
+
server.close(() => done());
|
|
8014
|
+
});
|
|
8015
|
+
await removeServerInfo(cwd, { port, pid: process.pid }).catch(() => {
|
|
8016
|
+
});
|
|
8017
|
+
})();
|
|
6491
8018
|
return closePromise;
|
|
6492
8019
|
}
|
|
6493
8020
|
};
|
|
@@ -6803,6 +8330,14 @@ When asked for design variations, alternatives, or "a few options":
|
|
|
6803
8330
|
and register it with \`npx leglas add --title "\u2026" --url "/" --branch <branch>\`
|
|
6804
8331
|
(the config needs \`devCommand\` with \`{port}\`). Everything below is the
|
|
6805
8332
|
ordinary, in-app path.
|
|
8333
|
+
A page the app rebuilds in the browser after load (anything that hydrates:
|
|
8334
|
+
Next, Nuxt, SvelteKit, a captured production site) is not its served HTML.
|
|
8335
|
+
Markup edited there shows for a moment and is then replaced from the app's
|
|
8336
|
+
own JavaScript and data, so make the change where that JavaScript gets what
|
|
8337
|
+
it renders. When that is a script other directions share, give it a
|
|
8338
|
+
per-direction override that defaults to what it renders today: every other
|
|
8339
|
+
direction renders exactly as before, which is adding beside, not rewriting.
|
|
8340
|
+
\`npx leglas show\` says when a page was rebuilt after load.
|
|
6806
8341
|
2. Run \`npx leglas explore <surface> --count <n>\` first, adding
|
|
6807
8342
|
\`--based-on "<title>"\` when the user wants variations of a direction they
|
|
6808
8343
|
already like. It prints what the set needs and how to register it. In
|
|
@@ -7566,6 +9101,10 @@ async function runShow(options, deps) {
|
|
|
7566
9101
|
height: captured.height,
|
|
7567
9102
|
viewport: captured.viewport,
|
|
7568
9103
|
errors: Array.isArray(captured.errors) ? captured.errors.filter((error) => typeof error === "string") : [],
|
|
9104
|
+
hydration: typeof captured.hydration === "object" && captured.hydration !== null && typeof captured.hydration.framework === "string" && typeof captured.hydration.message === "string" ? {
|
|
9105
|
+
framework: captured.hydration.framework,
|
|
9106
|
+
message: captured.hydration.message
|
|
9107
|
+
} : null,
|
|
7569
9108
|
cut: captured.cut === true
|
|
7570
9109
|
};
|
|
7571
9110
|
}
|
|
@@ -7593,6 +9132,12 @@ async function runShow(options, deps) {
|
|
|
7593
9132
|
if (envelope2.screenshot.cut) {
|
|
7594
9133
|
deps.log(" the top of the page only; it is taller than one capture");
|
|
7595
9134
|
}
|
|
9135
|
+
if (envelope2.screenshot.hydration !== null) {
|
|
9136
|
+
deps.log(
|
|
9137
|
+
` hydration ${envelope2.screenshot.hydration.framework} rebuilt the page in the browser after load; the served markup is not what is on screen`
|
|
9138
|
+
);
|
|
9139
|
+
deps.log(` ${envelope2.screenshot.hydration.message}`);
|
|
9140
|
+
}
|
|
7596
9141
|
if (envelope2.screenshot.errors.length > 0) {
|
|
7597
9142
|
const count = envelope2.screenshot.errors.length;
|
|
7598
9143
|
deps.log(` console ${count} ${count === 1 ? "error" : "errors"} on load`);
|