leglas 0.9.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 +291 -379
- package/dist/bin.js +1645 -157
- package/dist/index.js +1645 -157
- 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-BMpAHiSA.js +0 -14
- package/dist/shell/assets/index-pfmBGQQ0.css +0 -1
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);
|
|
@@ -2637,13 +2650,13 @@ async function attachRequest(cwd, requestId, input, deps) {
|
|
|
2637
2650
|
const references = [];
|
|
2638
2651
|
requestedWidths.set(captured, Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, Math.round(input.width))));
|
|
2639
2652
|
const controller = new AbortController();
|
|
2640
|
-
let
|
|
2653
|
+
let expired2 = false;
|
|
2641
2654
|
let finishDeadline;
|
|
2642
2655
|
const deadline = new Promise((resolve5) => {
|
|
2643
2656
|
finishDeadline = resolve5;
|
|
2644
2657
|
});
|
|
2645
2658
|
const timer = setTimeout(() => {
|
|
2646
|
-
|
|
2659
|
+
expired2 = true;
|
|
2647
2660
|
controller.abort();
|
|
2648
2661
|
finishDeadline();
|
|
2649
2662
|
}, deadlineMs);
|
|
@@ -2656,7 +2669,7 @@ async function attachRequest(cwd, requestId, input, deps) {
|
|
|
2656
2669
|
const work = (async () => {
|
|
2657
2670
|
try {
|
|
2658
2671
|
const browser = await deps.pool.acquire();
|
|
2659
|
-
if (
|
|
2672
|
+
if (expired2)
|
|
2660
2673
|
return;
|
|
2661
2674
|
if (browser === null) {
|
|
2662
2675
|
captured.skipped = deps.pool.reason() ?? NO_BROWSER;
|
|
@@ -2670,7 +2683,7 @@ async function attachRequest(cwd, requestId, input, deps) {
|
|
|
2670
2683
|
signal: controller.signal
|
|
2671
2684
|
};
|
|
2672
2685
|
const direction = await capture(browser, directionInput);
|
|
2673
|
-
if (
|
|
2686
|
+
if (expired2)
|
|
2674
2687
|
return;
|
|
2675
2688
|
await mkdir3(destination, { recursive: true });
|
|
2676
2689
|
await writeFile3(join5(destination, "frame.png"), direction.frame.png);
|
|
@@ -2688,7 +2701,7 @@ async function attachRequest(cwd, requestId, input, deps) {
|
|
|
2688
2701
|
for (let index = 0; index < direction.crops.length; index += 1) {
|
|
2689
2702
|
const crop = direction.crops[index];
|
|
2690
2703
|
const note = input.notes[index];
|
|
2691
|
-
if (crop === null || crop === void 0 || note === void 0 ||
|
|
2704
|
+
if (crop === null || crop === void 0 || note === void 0 || expired2)
|
|
2692
2705
|
continue;
|
|
2693
2706
|
const name = `note-${index + 1}.png`;
|
|
2694
2707
|
await writeFile3(join5(destination, name), crop.shot.png);
|
|
@@ -2702,7 +2715,7 @@ async function attachRequest(cwd, requestId, input, deps) {
|
|
|
2702
2715
|
viewport: direction.frame.width
|
|
2703
2716
|
});
|
|
2704
2717
|
}
|
|
2705
|
-
if (input.compare !== null && !
|
|
2718
|
+
if (input.compare !== null && !expired2) {
|
|
2706
2719
|
const compareInput = {
|
|
2707
2720
|
url: previewUrl(input.origin, input.compare),
|
|
2708
2721
|
width: input.width,
|
|
@@ -2710,7 +2723,7 @@ async function attachRequest(cwd, requestId, input, deps) {
|
|
|
2710
2723
|
signal: controller.signal
|
|
2711
2724
|
};
|
|
2712
2725
|
const comparison = await capture(browser, compareInput);
|
|
2713
|
-
if (
|
|
2726
|
+
if (expired2)
|
|
2714
2727
|
return;
|
|
2715
2728
|
await writeFile3(join5(destination, "compare.png"), comparison.frame.png);
|
|
2716
2729
|
captured.attachments.push({
|
|
@@ -2723,14 +2736,14 @@ async function attachRequest(cwd, requestId, input, deps) {
|
|
|
2723
2736
|
});
|
|
2724
2737
|
}
|
|
2725
2738
|
} catch (error) {
|
|
2726
|
-
if (!
|
|
2739
|
+
if (!expired2) {
|
|
2727
2740
|
captured.skipped = error instanceof Error ? error.message : `The page did not load: ${String(error)}`;
|
|
2728
2741
|
}
|
|
2729
2742
|
}
|
|
2730
2743
|
})();
|
|
2731
2744
|
await Promise.race([work, deadline]);
|
|
2732
2745
|
clearTimeout(timer);
|
|
2733
|
-
if (
|
|
2746
|
+
if (expired2)
|
|
2734
2747
|
captured.skipped = "The design could not be captured in time.";
|
|
2735
2748
|
captured.attachments.push(...references);
|
|
2736
2749
|
return captured;
|
|
@@ -5017,10 +5030,14 @@ function createCoalescer(emit, options = {}) {
|
|
|
5017
5030
|
}
|
|
5018
5031
|
};
|
|
5019
5032
|
}
|
|
5020
|
-
function createLiveHub(
|
|
5033
|
+
function createLiveHub(options = {}) {
|
|
5021
5034
|
const listeners = /* @__PURE__ */ new Set();
|
|
5035
|
+
let viewers = 0;
|
|
5022
5036
|
const drop = (listener) => {
|
|
5023
|
-
listeners.delete(listener)
|
|
5037
|
+
if (!listeners.delete(listener) || !listener.viewer)
|
|
5038
|
+
return;
|
|
5039
|
+
viewers = Math.max(0, viewers - 1);
|
|
5040
|
+
options.onViewers?.(viewers);
|
|
5024
5041
|
};
|
|
5025
5042
|
const write2 = (listener, opcode, payload) => {
|
|
5026
5043
|
if (listener.socket.destroyed || !listener.socket.writable) {
|
|
@@ -5105,7 +5122,7 @@ function createLiveHub(_options = {}) {
|
|
|
5105
5122
|
}
|
|
5106
5123
|
}
|
|
5107
5124
|
},
|
|
5108
|
-
upgrade: (req, socket, head) => {
|
|
5125
|
+
upgrade: (req, socket, head, upgradeOptions = {}) => {
|
|
5109
5126
|
const path = (req.url ?? "/").split("?")[0] ?? "/";
|
|
5110
5127
|
if (req.method !== "GET" || path !== LIVE_PATH)
|
|
5111
5128
|
return false;
|
|
@@ -5127,8 +5144,16 @@ Sec-WebSocket-Accept: ${accept}\r
|
|
|
5127
5144
|
socket.destroy();
|
|
5128
5145
|
return false;
|
|
5129
5146
|
}
|
|
5130
|
-
const listener = {
|
|
5147
|
+
const listener = {
|
|
5148
|
+
socket,
|
|
5149
|
+
buffered: Buffer.alloc(0),
|
|
5150
|
+
viewer: upgradeOptions.viewer === true
|
|
5151
|
+
};
|
|
5131
5152
|
listeners.add(listener);
|
|
5153
|
+
if (listener.viewer) {
|
|
5154
|
+
viewers += 1;
|
|
5155
|
+
options.onViewers?.(viewers);
|
|
5156
|
+
}
|
|
5132
5157
|
socket.on("data", (chunk) => read(listener, chunk));
|
|
5133
5158
|
socket.once("error", () => drop(listener));
|
|
5134
5159
|
socket.once("end", () => drop(listener));
|
|
@@ -5146,8 +5171,1328 @@ Sec-WebSocket-Accept: ${accept}\r
|
|
|
5146
5171
|
},
|
|
5147
5172
|
get listening() {
|
|
5148
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;
|
|
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();
|
|
5149
5980
|
}
|
|
5150
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
|
|
6495
|
+
};
|
|
5151
6496
|
}
|
|
5152
6497
|
|
|
5153
6498
|
// ../server/dist/renames.js
|
|
@@ -5186,7 +6531,7 @@ function resolveTitle(input, titles, renames) {
|
|
|
5186
6531
|
import { createReadStream, existsSync as existsSync3, statSync, unwatchFile, watch as watchFs, watchFile } from "fs";
|
|
5187
6532
|
import { createHash as createHash2 } from "crypto";
|
|
5188
6533
|
import { mkdir as mkdir8, readdir as readdir3, writeFile as writeFile8 } from "fs/promises";
|
|
5189
|
-
import
|
|
6534
|
+
import http4 from "http";
|
|
5190
6535
|
import net3 from "net";
|
|
5191
6536
|
import { basename as basename3, dirname as dirname8, extname as extname3, join as join12, normalize, relative as relative3 } from "path";
|
|
5192
6537
|
|
|
@@ -5255,7 +6600,7 @@ var CONTENT_TYPES = {
|
|
|
5255
6600
|
".woff2": "font/woff2"
|
|
5256
6601
|
};
|
|
5257
6602
|
var FILES_PREFIX = `${LEGLAS_PREFIX}/files`;
|
|
5258
|
-
function
|
|
6603
|
+
function sendJson2(res, status, body) {
|
|
5259
6604
|
const payload = JSON.stringify(body);
|
|
5260
6605
|
res.writeHead(status, {
|
|
5261
6606
|
"content-type": "application/json; charset=utf-8",
|
|
@@ -5592,7 +6937,8 @@ function watchHealth(target, live) {
|
|
|
5592
6937
|
close: () => {
|
|
5593
6938
|
closed = true;
|
|
5594
6939
|
clearInterval(timer);
|
|
5595
|
-
}
|
|
6940
|
+
},
|
|
6941
|
+
reachable: () => previous
|
|
5596
6942
|
};
|
|
5597
6943
|
}
|
|
5598
6944
|
function snapshotConfig(cwd) {
|
|
@@ -5649,7 +6995,7 @@ function listen(server, port) {
|
|
|
5649
6995
|
server.listen(port, "127.0.0.1");
|
|
5650
6996
|
});
|
|
5651
6997
|
}
|
|
5652
|
-
async function
|
|
6998
|
+
async function bind2(server, requested) {
|
|
5653
6999
|
if (requested === 0)
|
|
5654
7000
|
return listen(server, 0);
|
|
5655
7001
|
for (let attempt = 0; attempt < PORT_ATTEMPTS; attempt += 1) {
|
|
@@ -5665,6 +7011,8 @@ async function bind(server, requested) {
|
|
|
5665
7011
|
async function startServer(options) {
|
|
5666
7012
|
const { config, configErrors = [], configWarnings = [], shellDir = null, project = "", cwd = process.cwd(), leglasCommand = "npx -y leglas", fileMounts = /* @__PURE__ */ new Map(), detect = () => detectAgents() } = options;
|
|
5667
7013
|
const browserPool = options.pool ?? createBrowserPool();
|
|
7014
|
+
let shares = null;
|
|
7015
|
+
let liveHealth = null;
|
|
5668
7016
|
const live = options.live ?? createLiveHub();
|
|
5669
7017
|
const branches = createBranchRegistry({
|
|
5670
7018
|
cwd,
|
|
@@ -5739,12 +7087,103 @@ async function startServer(options) {
|
|
|
5739
7087
|
const livePreviews = async () => (await livePreviewDefinitions()).map(readyPreview).filter((preview) => preview !== null);
|
|
5740
7088
|
void probeAgents().catch(() => {
|
|
5741
7089
|
});
|
|
5742
|
-
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) => {
|
|
5743
7106
|
const url = req.url ?? "/";
|
|
5744
7107
|
const path = url.split("?")[0] ?? "/";
|
|
5745
7108
|
const query = new URLSearchParams(url.includes("?") ? url.slice(url.indexOf("?") + 1) : "");
|
|
5746
|
-
if (req.method === "POST" && path.startsWith(`${LEGLAS_PREFIX}/api/`) && !isTrustedMutation(req)) {
|
|
5747
|
-
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 }));
|
|
5748
7187
|
}
|
|
5749
7188
|
if (path === `${LEGLAS_PREFIX}/api/config`) {
|
|
5750
7189
|
const boot = config?.previews ?? [];
|
|
@@ -5790,23 +7229,23 @@ async function startServer(options) {
|
|
|
5790
7229
|
return void req.on("end", async () => {
|
|
5791
7230
|
const parsed2 = jsonBody(body);
|
|
5792
7231
|
if (parsed2 === null) {
|
|
5793
|
-
return
|
|
7232
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
5794
7233
|
}
|
|
5795
7234
|
if (typeof parsed2.title !== "string" || parsed2.title.trim() === "") {
|
|
5796
|
-
return
|
|
7235
|
+
return sendJson2(res, 400, { ok: false, error: "Body needs a direction title." });
|
|
5797
7236
|
}
|
|
5798
7237
|
const preview = (await livePreviewDefinitions()).find((entry) => entry.title === parsed2.title);
|
|
5799
7238
|
if (preview === void 0) {
|
|
5800
|
-
return
|
|
7239
|
+
return sendJson2(res, 404, { ok: false, error: "No such direction." });
|
|
5801
7240
|
}
|
|
5802
7241
|
if (preview.branch === void 0) {
|
|
5803
|
-
return
|
|
7242
|
+
return sendJson2(res, 400, {
|
|
5804
7243
|
ok: false,
|
|
5805
7244
|
error: `"${preview.title}" is not a branch preview.`
|
|
5806
7245
|
});
|
|
5807
7246
|
}
|
|
5808
7247
|
if (config?.devCommand === void 0) {
|
|
5809
|
-
return
|
|
7248
|
+
return sendJson2(res, 400, {
|
|
5810
7249
|
ok: false,
|
|
5811
7250
|
error: `"${preview.title}" cannot start because the config sets no devCommand.`
|
|
5812
7251
|
});
|
|
@@ -5814,9 +7253,9 @@ async function startServer(options) {
|
|
|
5814
7253
|
void branches.start(preview.title);
|
|
5815
7254
|
const state = branches.state(preview.title);
|
|
5816
7255
|
if (state === void 0) {
|
|
5817
|
-
return
|
|
7256
|
+
return sendJson2(res, 404, { ok: false, error: "No such branch preview." });
|
|
5818
7257
|
}
|
|
5819
|
-
return
|
|
7258
|
+
return sendJson2(res, 200, { ok: true, state: publicBranchState(state) });
|
|
5820
7259
|
});
|
|
5821
7260
|
}
|
|
5822
7261
|
if (path === `${LEGLAS_PREFIX}/api/previews/delete` && req.method === "POST") {
|
|
@@ -5825,11 +7264,11 @@ async function startServer(options) {
|
|
|
5825
7264
|
return void req.on("end", async () => {
|
|
5826
7265
|
const parsed2 = jsonBody(body);
|
|
5827
7266
|
if (parsed2 === null) {
|
|
5828
|
-
return
|
|
7267
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
5829
7268
|
}
|
|
5830
7269
|
const titles = parsed2.titles;
|
|
5831
7270
|
if (!Array.isArray(titles) || titles.length === 0 || titles.some((title) => typeof title !== "string" || title.trim() === "")) {
|
|
5832
|
-
return
|
|
7271
|
+
return sendJson2(res, 400, {
|
|
5833
7272
|
ok: false,
|
|
5834
7273
|
error: "Body needs a non-empty array of direction titles."
|
|
5835
7274
|
});
|
|
@@ -5838,20 +7277,20 @@ async function startServer(options) {
|
|
|
5838
7277
|
try {
|
|
5839
7278
|
const local = await readLocalPreviews(cwd);
|
|
5840
7279
|
if (local.errors.length > 0) {
|
|
5841
|
-
return
|
|
7280
|
+
return sendJson2(res, 409, { ok: false, error: local.errors.join(" ") });
|
|
5842
7281
|
}
|
|
5843
7282
|
const localTitles = new Set(local.previews.map((preview) => preview.title));
|
|
5844
7283
|
const unknown = unique.filter((title) => !localTitles.has(title));
|
|
5845
7284
|
if (unknown.length > 0) {
|
|
5846
|
-
return
|
|
7285
|
+
return sendJson2(res, 400, {
|
|
5847
7286
|
ok: false,
|
|
5848
7287
|
error: "Only machine-local directions can be deleted from the registry."
|
|
5849
7288
|
});
|
|
5850
7289
|
}
|
|
5851
7290
|
const deleted = await dropLocalPreviews(cwd, unique);
|
|
5852
|
-
return
|
|
7291
|
+
return sendJson2(res, 200, { ok: true, deleted });
|
|
5853
7292
|
} catch {
|
|
5854
|
-
return
|
|
7293
|
+
return sendJson2(res, 500, {
|
|
5855
7294
|
ok: false,
|
|
5856
7295
|
error: "The directions could not be deleted from Leglas."
|
|
5857
7296
|
});
|
|
@@ -5861,7 +7300,7 @@ async function startServer(options) {
|
|
|
5861
7300
|
if (path === `${LEGLAS_PREFIX}/api/references` && req.method === "POST") {
|
|
5862
7301
|
const declaredLength = req.headers["content-length"];
|
|
5863
7302
|
if (typeof declaredLength === "string" && Number(declaredLength) > REFERENCE_MAX_BYTES) {
|
|
5864
|
-
return
|
|
7303
|
+
return sendJson2(res, 413, { ok: false, error: "That image is over 10MB." });
|
|
5865
7304
|
}
|
|
5866
7305
|
const chunks = [];
|
|
5867
7306
|
let bytes = 0;
|
|
@@ -5875,7 +7314,7 @@ async function startServer(options) {
|
|
|
5875
7314
|
refused = true;
|
|
5876
7315
|
req.pause();
|
|
5877
7316
|
res.once("finish", () => req.socket.destroy());
|
|
5878
|
-
|
|
7317
|
+
sendJson2(res, 413, { ok: false, error: "That image is over 10MB." });
|
|
5879
7318
|
return;
|
|
5880
7319
|
}
|
|
5881
7320
|
chunks.push(buffer);
|
|
@@ -5884,12 +7323,12 @@ async function startServer(options) {
|
|
|
5884
7323
|
if (refused)
|
|
5885
7324
|
return;
|
|
5886
7325
|
if (bytes === 0) {
|
|
5887
|
-
return
|
|
7326
|
+
return sendJson2(res, 400, { ok: false, error: "The upload was empty." });
|
|
5888
7327
|
}
|
|
5889
7328
|
const body = Buffer.concat(chunks, bytes);
|
|
5890
7329
|
const image = sniffImage(body);
|
|
5891
7330
|
if (image === null) {
|
|
5892
|
-
return
|
|
7331
|
+
return sendJson2(res, 415, {
|
|
5893
7332
|
ok: false,
|
|
5894
7333
|
error: "Only PNG, JPEG, WebP and GIF images can be attached."
|
|
5895
7334
|
});
|
|
@@ -5901,7 +7340,7 @@ async function startServer(options) {
|
|
|
5901
7340
|
await writeFile8(join12(cwd, file), body);
|
|
5902
7341
|
void pruneReferences(cwd).catch(() => {
|
|
5903
7342
|
});
|
|
5904
|
-
return
|
|
7343
|
+
return sendJson2(res, 200, {
|
|
5905
7344
|
ok: true,
|
|
5906
7345
|
reference: {
|
|
5907
7346
|
id,
|
|
@@ -5913,7 +7352,7 @@ async function startServer(options) {
|
|
|
5913
7352
|
}
|
|
5914
7353
|
});
|
|
5915
7354
|
} catch {
|
|
5916
|
-
return
|
|
7355
|
+
return sendJson2(res, 500, {
|
|
5917
7356
|
ok: false,
|
|
5918
7357
|
error: "The image could not be attached."
|
|
5919
7358
|
});
|
|
@@ -5926,24 +7365,24 @@ async function startServer(options) {
|
|
|
5926
7365
|
return void req.on("end", async () => {
|
|
5927
7366
|
const parsed2 = jsonBody(body);
|
|
5928
7367
|
if (parsed2 === null) {
|
|
5929
|
-
return
|
|
7368
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
5930
7369
|
}
|
|
5931
7370
|
if (parsed2.mode !== void 0 && parsed2.mode !== "variant" && parsed2.mode !== "replace") {
|
|
5932
|
-
return
|
|
7371
|
+
return sendJson2(res, 400, {
|
|
5933
7372
|
ok: false,
|
|
5934
7373
|
error: 'mode must be "variant" or "replace".'
|
|
5935
7374
|
});
|
|
5936
7375
|
}
|
|
5937
7376
|
const mode = parsed2.mode === "replace" ? "replace" : "variant";
|
|
5938
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)))) {
|
|
5939
|
-
return
|
|
7378
|
+
return sendJson2(res, 400, { ok: false, error: "references must be uploaded image ids." });
|
|
5940
7379
|
}
|
|
5941
7380
|
const references = parsed2.references ?? [];
|
|
5942
7381
|
if (references.length > 0) {
|
|
5943
7382
|
const present = new Set((await readdir3(join12(cwd, REFERENCES_DIR)).catch(() => [])).map((name) => name.slice(0, name.indexOf(".") === -1 ? name.length : name.indexOf("."))));
|
|
5944
7383
|
const gone = references.filter((id2) => !present.has(id2));
|
|
5945
7384
|
if (gone.length > 0) {
|
|
5946
|
-
return
|
|
7385
|
+
return sendJson2(res, 410, {
|
|
5947
7386
|
ok: false,
|
|
5948
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."
|
|
5949
7388
|
});
|
|
@@ -5953,11 +7392,11 @@ async function startServer(options) {
|
|
|
5953
7392
|
const previews = await livePreviews();
|
|
5954
7393
|
const preview = previews.find((entry) => entry.title === parsed2.title);
|
|
5955
7394
|
if (!preview) {
|
|
5956
|
-
return
|
|
7395
|
+
return sendJson2(res, 400, { ok: false, error: "Unknown preview, or empty request." });
|
|
5957
7396
|
}
|
|
5958
7397
|
const notes = annotationsFor(await readAnnotations(cwd).catch(() => []), preview.title);
|
|
5959
7398
|
if (!parsed2.intent?.trim() && notes.length === 0) {
|
|
5960
|
-
return
|
|
7399
|
+
return sendJson2(res, 400, { ok: false, error: "Unknown preview, or empty request." });
|
|
5961
7400
|
}
|
|
5962
7401
|
const intent = (parsed2.intent ?? "").trim();
|
|
5963
7402
|
const live2 = (await readRequests(cwd).catch(() => [])).filter((entry) => entry.status === "queued" || entry.status === "picked-up");
|
|
@@ -5971,7 +7410,7 @@ async function startServer(options) {
|
|
|
5971
7410
|
// one forks the direction and the other rewrites it. Only a
|
|
5972
7411
|
// genuine repeat is refused.
|
|
5973
7412
|
(entry.mode ?? "replace") === mode && sameNotes(entry) && sameContext(entry))) {
|
|
5974
|
-
return
|
|
7413
|
+
return sendJson2(res, 409, {
|
|
5975
7414
|
ok: false,
|
|
5976
7415
|
duplicate: true,
|
|
5977
7416
|
error: `That exact change to ${preview.title} is already waiting.`
|
|
@@ -6005,13 +7444,13 @@ async function startServer(options) {
|
|
|
6005
7444
|
...composed
|
|
6006
7445
|
}, id);
|
|
6007
7446
|
runner?.nudge();
|
|
6008
|
-
return
|
|
7447
|
+
return sendJson2(res, 200, {
|
|
6009
7448
|
ok: true,
|
|
6010
7449
|
...composed,
|
|
6011
7450
|
attachments: captured.attachments
|
|
6012
7451
|
});
|
|
6013
7452
|
} catch {
|
|
6014
|
-
return
|
|
7453
|
+
return sendJson2(res, 200, {
|
|
6015
7454
|
ok: true,
|
|
6016
7455
|
...composed,
|
|
6017
7456
|
attachments: captured.attachments,
|
|
@@ -6022,29 +7461,29 @@ async function startServer(options) {
|
|
|
6022
7461
|
}
|
|
6023
7462
|
if (path === `${LEGLAS_PREFIX}/api/capture` && req.method === "POST") {
|
|
6024
7463
|
if (!hasJsonBody(req)) {
|
|
6025
|
-
return
|
|
7464
|
+
return sendJson2(res, 400, { ok: false, error: "Capture must be JSON." });
|
|
6026
7465
|
}
|
|
6027
7466
|
let body = "";
|
|
6028
7467
|
req.on("data", (chunk) => body += chunk);
|
|
6029
7468
|
return void req.on("end", async () => {
|
|
6030
7469
|
const parsed2 = jsonBody(body);
|
|
6031
7470
|
if (parsed2 === null) {
|
|
6032
|
-
return
|
|
7471
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6033
7472
|
}
|
|
6034
7473
|
if (typeof parsed2.title !== "string" || parsed2.title === "") {
|
|
6035
|
-
return
|
|
7474
|
+
return sendJson2(res, 400, { ok: false, error: "Capture needs a direction title." });
|
|
6036
7475
|
}
|
|
6037
7476
|
if (parsed2.note !== void 0 && typeof parsed2.note !== "string") {
|
|
6038
|
-
return
|
|
7477
|
+
return sendJson2(res, 400, { ok: false, error: "The note id must be a string." });
|
|
6039
7478
|
}
|
|
6040
7479
|
const preview = (await livePreviews()).find((entry) => entry.title === parsed2.title);
|
|
6041
7480
|
if (preview === void 0) {
|
|
6042
|
-
return
|
|
7481
|
+
return sendJson2(res, 404, { ok: false, error: "No such direction." });
|
|
6043
7482
|
}
|
|
6044
7483
|
const width = typeof parsed2.width === "number" && Number.isFinite(parsed2.width) ? Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, Math.round(parsed2.width))) : 1440;
|
|
6045
7484
|
const browser = await browserPool.acquire();
|
|
6046
7485
|
if (browser === null) {
|
|
6047
|
-
return
|
|
7486
|
+
return sendJson2(res, 503, {
|
|
6048
7487
|
ok: false,
|
|
6049
7488
|
error: browserPool.reason() ?? NO_BROWSER
|
|
6050
7489
|
});
|
|
@@ -6081,7 +7520,7 @@ async function startServer(options) {
|
|
|
6081
7520
|
});
|
|
6082
7521
|
const result2 = await Promise.race([work, timeout]);
|
|
6083
7522
|
if (result2 === timeoutMarker) {
|
|
6084
|
-
return
|
|
7523
|
+
return sendJson2(res, 504, { ok: false, error: "The page did not load in time." });
|
|
6085
7524
|
}
|
|
6086
7525
|
clearTimeout(timer);
|
|
6087
7526
|
const crop = annotations.length > 0 ? result2.crops[0] : null;
|
|
@@ -6091,7 +7530,7 @@ async function startServer(options) {
|
|
|
6091
7530
|
const relativeFile = `${CAPTURES_DIR}/show/${name}`;
|
|
6092
7531
|
await mkdir8(join12(cwd, CAPTURES_DIR, "show"), { recursive: true });
|
|
6093
7532
|
await writeFile8(join12(cwd, relativeFile), shot.png);
|
|
6094
|
-
return
|
|
7533
|
+
return sendJson2(res, 200, {
|
|
6095
7534
|
ok: true,
|
|
6096
7535
|
file: relativeFile,
|
|
6097
7536
|
width: shot.width,
|
|
@@ -6103,7 +7542,7 @@ async function startServer(options) {
|
|
|
6103
7542
|
});
|
|
6104
7543
|
} catch (error) {
|
|
6105
7544
|
clearTimeout(timer);
|
|
6106
|
-
return
|
|
7545
|
+
return sendJson2(res, 502, {
|
|
6107
7546
|
ok: false,
|
|
6108
7547
|
error: error instanceof Error ? error.message : String(error)
|
|
6109
7548
|
});
|
|
@@ -6114,14 +7553,14 @@ async function startServer(options) {
|
|
|
6114
7553
|
return void readAgentChoice(cwd).then((choice) => {
|
|
6115
7554
|
if (choice.agent !== null)
|
|
6116
7555
|
runner?.prepare(choice.agent);
|
|
6117
|
-
|
|
6118
|
-
}, () =>
|
|
7556
|
+
sendJson2(res, 200, { ok: true });
|
|
7557
|
+
}, () => sendJson2(res, 200, { ok: true }));
|
|
6119
7558
|
}
|
|
6120
7559
|
if (path === `${LEGLAS_PREFIX}/api/agents` && req.method === "GET") {
|
|
6121
7560
|
return void Promise.all([
|
|
6122
7561
|
currentAgents(query.get("refresh") === "1"),
|
|
6123
7562
|
readAgentChoice(cwd)
|
|
6124
|
-
]).then(([agents, choice]) =>
|
|
7563
|
+
]).then(([agents, choice]) => sendJson2(res, 200, {
|
|
6125
7564
|
agents,
|
|
6126
7565
|
choice: choice.agent,
|
|
6127
7566
|
customRun: choice.run,
|
|
@@ -6130,48 +7569,48 @@ async function startServer(options) {
|
|
|
6130
7569
|
}
|
|
6131
7570
|
if (path === `${LEGLAS_PREFIX}/api/agent` && req.method === "POST") {
|
|
6132
7571
|
if (!isLoopbackAddress(req.socket.remoteAddress)) {
|
|
6133
|
-
return
|
|
7572
|
+
return sendJson2(res, 403, {
|
|
6134
7573
|
ok: false,
|
|
6135
7574
|
error: "The agent choice can only be made from the machine running Leglas."
|
|
6136
7575
|
});
|
|
6137
7576
|
}
|
|
6138
7577
|
if (!hasJsonBody(req)) {
|
|
6139
|
-
return
|
|
7578
|
+
return sendJson2(res, 400, { ok: false, error: "Agent choice must be JSON." });
|
|
6140
7579
|
}
|
|
6141
7580
|
let body = "";
|
|
6142
7581
|
req.on("data", (chunk) => body += chunk);
|
|
6143
7582
|
return void req.on("end", () => {
|
|
6144
7583
|
const parsed2 = jsonBody(body);
|
|
6145
7584
|
if (parsed2 === null) {
|
|
6146
|
-
return
|
|
7585
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6147
7586
|
}
|
|
6148
7587
|
if (!isKnownAgent(parsed2.agent) && parsed2.agent !== "custom") {
|
|
6149
|
-
return
|
|
7588
|
+
return sendJson2(res, 400, { ok: false, error: "Body needs a known agent." });
|
|
6150
7589
|
}
|
|
6151
7590
|
if (parsed2.run !== void 0 && typeof parsed2.run !== "string") {
|
|
6152
|
-
return
|
|
7591
|
+
return sendJson2(res, 400, { ok: false, error: "The custom run command must be a string." });
|
|
6153
7592
|
}
|
|
6154
7593
|
const effort = parsed2.effort === null || isAgentEffort(parsed2.effort) ? parsed2.effort : void 0;
|
|
6155
7594
|
if (parsed2.effort !== void 0 && effort === void 0) {
|
|
6156
|
-
return
|
|
7595
|
+
return sendJson2(res, 400, { ok: false, error: "Effort must be a supported level or null." });
|
|
6157
7596
|
}
|
|
6158
7597
|
if (parsed2.agent === "custom") {
|
|
6159
7598
|
if (effort !== void 0) {
|
|
6160
|
-
return
|
|
7599
|
+
return sendJson2(res, 400, {
|
|
6161
7600
|
ok: false,
|
|
6162
7601
|
error: "Custom agents manage effort in their own command."
|
|
6163
7602
|
});
|
|
6164
7603
|
}
|
|
6165
7604
|
if (typeof parsed2.run !== "string") {
|
|
6166
|
-
return
|
|
7605
|
+
return sendJson2(res, 400, { ok: false, error: "A custom agent needs a run command." });
|
|
6167
7606
|
}
|
|
6168
7607
|
const template = parseTemplate(parsed2.run);
|
|
6169
7608
|
if (!template.ok)
|
|
6170
|
-
return
|
|
6171
|
-
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." }));
|
|
6172
7611
|
}
|
|
6173
7612
|
if (effort !== void 0 && effort !== null && !KNOWN_AGENTS[parsed2.agent].efforts.includes(effort)) {
|
|
6174
|
-
return
|
|
7613
|
+
return sendJson2(res, 400, {
|
|
6175
7614
|
ok: false,
|
|
6176
7615
|
error: `${KNOWN_AGENTS[parsed2.agent].name} does not expose an effort override.`
|
|
6177
7616
|
});
|
|
@@ -6181,8 +7620,8 @@ async function startServer(options) {
|
|
|
6181
7620
|
...effort === void 0 ? {} : { effort }
|
|
6182
7621
|
}).then(() => {
|
|
6183
7622
|
runner?.prepare(parsed2.agent);
|
|
6184
|
-
|
|
6185
|
-
}, () =>
|
|
7623
|
+
sendJson2(res, 200, { ok: true });
|
|
7624
|
+
}, () => sendJson2(res, 500, { ok: false, error: "Agent choice could not be saved." }));
|
|
6186
7625
|
});
|
|
6187
7626
|
}
|
|
6188
7627
|
if (path === `${LEGLAS_PREFIX}/api/watch` && req.method === "POST") {
|
|
@@ -6191,13 +7630,13 @@ async function startServer(options) {
|
|
|
6191
7630
|
return void req.on("end", () => {
|
|
6192
7631
|
const parsed2 = jsonBody(body);
|
|
6193
7632
|
if (parsed2 === null) {
|
|
6194
|
-
return
|
|
7633
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6195
7634
|
}
|
|
6196
7635
|
if (typeof parsed2.watching !== "boolean") {
|
|
6197
|
-
return
|
|
7636
|
+
return sendJson2(res, 400, { ok: false, error: "Body needs a watching boolean." });
|
|
6198
7637
|
}
|
|
6199
7638
|
lastSeen = parsed2.watching ? Date.now() : null;
|
|
6200
|
-
|
|
7639
|
+
sendJson2(res, 200, { ok: true });
|
|
6201
7640
|
});
|
|
6202
7641
|
}
|
|
6203
7642
|
if (path === `${LEGLAS_PREFIX}/api/requests` && req.method === "GET") {
|
|
@@ -6246,47 +7685,47 @@ async function startServer(options) {
|
|
|
6246
7685
|
}
|
|
6247
7686
|
if (path === `${LEGLAS_PREFIX}/api/requests/cancel` && req.method === "POST") {
|
|
6248
7687
|
if (!hasJsonBody(req)) {
|
|
6249
|
-
return
|
|
7688
|
+
return sendJson2(res, 200, { ok: true, cancelled: runner?.cancel() ?? false });
|
|
6250
7689
|
}
|
|
6251
7690
|
let body = "";
|
|
6252
7691
|
req.on("data", (chunk) => body += chunk);
|
|
6253
7692
|
return void req.on("end", () => {
|
|
6254
7693
|
const parsed2 = jsonBody(body);
|
|
6255
7694
|
if (parsed2 === null) {
|
|
6256
|
-
return
|
|
7695
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6257
7696
|
}
|
|
6258
7697
|
if (parsed2.id !== void 0 && typeof parsed2.id !== "string") {
|
|
6259
|
-
return
|
|
7698
|
+
return sendJson2(res, 400, { ok: false, error: "The request id must be a string." });
|
|
6260
7699
|
}
|
|
6261
|
-
return
|
|
7700
|
+
return sendJson2(res, 200, { ok: true, cancelled: runner?.cancel(parsed2.id) ?? false });
|
|
6262
7701
|
});
|
|
6263
7702
|
}
|
|
6264
7703
|
if (path === `${LEGLAS_PREFIX}/api/requests/retry` && req.method === "POST") {
|
|
6265
7704
|
if (!hasJsonBody(req)) {
|
|
6266
|
-
return
|
|
7705
|
+
return sendJson2(res, 400, { ok: false, error: "Retry must be JSON." });
|
|
6267
7706
|
}
|
|
6268
7707
|
let body = "";
|
|
6269
7708
|
req.on("data", (chunk) => body += chunk);
|
|
6270
7709
|
return void req.on("end", async () => {
|
|
6271
7710
|
const parsed2 = jsonBody(body);
|
|
6272
7711
|
if (parsed2 === null) {
|
|
6273
|
-
return
|
|
7712
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6274
7713
|
}
|
|
6275
7714
|
if (typeof parsed2.id !== "string") {
|
|
6276
|
-
return
|
|
7715
|
+
return sendJson2(res, 400, { ok: false, error: "Body needs a request id." });
|
|
6277
7716
|
}
|
|
6278
7717
|
const request = (await readRequests(cwd)).find((entry) => entry.id === parsed2.id);
|
|
6279
7718
|
if (request === void 0) {
|
|
6280
|
-
return
|
|
7719
|
+
return sendJson2(res, 404, { ok: false, error: "No such request." });
|
|
6281
7720
|
}
|
|
6282
7721
|
if (!isEnded(request, runner?.snapshot().failedIds ?? [])) {
|
|
6283
|
-
return
|
|
7722
|
+
return sendJson2(res, 400, { ok: false, error: "Only an ended request can be run again." });
|
|
6284
7723
|
}
|
|
6285
7724
|
try {
|
|
6286
7725
|
const retryId = newRequestId();
|
|
6287
7726
|
const attachments = await rehomeCaptures(cwd, request.id, retryId, request.attachments ?? []).catch(() => []);
|
|
6288
7727
|
if (!await removeRequest(cwd, request.id)) {
|
|
6289
|
-
return
|
|
7728
|
+
return sendJson2(res, 404, { ok: false, error: "No such request." });
|
|
6290
7729
|
}
|
|
6291
7730
|
await appendRequest(cwd, {
|
|
6292
7731
|
title: request.title,
|
|
@@ -6307,9 +7746,9 @@ async function startServer(options) {
|
|
|
6307
7746
|
...request.references === void 0 ? {} : { references: request.references }
|
|
6308
7747
|
}, retryId);
|
|
6309
7748
|
runner?.nudge();
|
|
6310
|
-
return
|
|
7749
|
+
return sendJson2(res, 200, { ok: true });
|
|
6311
7750
|
} catch {
|
|
6312
|
-
return
|
|
7751
|
+
return sendJson2(res, 500, { ok: false, error: "The request could not be retried." });
|
|
6313
7752
|
}
|
|
6314
7753
|
});
|
|
6315
7754
|
}
|
|
@@ -6318,21 +7757,21 @@ async function startServer(options) {
|
|
|
6318
7757
|
}
|
|
6319
7758
|
if (path === `${LEGLAS_PREFIX}/api/annotations` && req.method === "POST") {
|
|
6320
7759
|
if (!hasJsonBody(req)) {
|
|
6321
|
-
return
|
|
7760
|
+
return sendJson2(res, 400, { ok: false, error: "A note must be JSON." });
|
|
6322
7761
|
}
|
|
6323
7762
|
let body = "";
|
|
6324
7763
|
req.on("data", (chunk) => body += chunk);
|
|
6325
7764
|
return void req.on("end", async () => {
|
|
6326
7765
|
const parsed2 = jsonBody(body);
|
|
6327
7766
|
if (parsed2 === null) {
|
|
6328
|
-
return
|
|
7767
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6329
7768
|
}
|
|
6330
7769
|
if (typeof parsed2.title !== "string" || parsed2.title.trim() === "") {
|
|
6331
|
-
return
|
|
7770
|
+
return sendJson2(res, 400, { ok: false, error: "A note needs a direction." });
|
|
6332
7771
|
}
|
|
6333
7772
|
const anchor = anchorFrom(parsed2.anchor);
|
|
6334
7773
|
if (anchor === null) {
|
|
6335
|
-
return
|
|
7774
|
+
return sendJson2(res, 400, { ok: false, error: "A note needs something to point at." });
|
|
6336
7775
|
}
|
|
6337
7776
|
try {
|
|
6338
7777
|
const annotation = await addAnnotation(cwd, {
|
|
@@ -6340,87 +7779,87 @@ async function startServer(options) {
|
|
|
6340
7779
|
note: typeof parsed2.note === "string" ? parsed2.note.trim() : "",
|
|
6341
7780
|
title: parsed2.title
|
|
6342
7781
|
});
|
|
6343
|
-
return
|
|
7782
|
+
return sendJson2(res, 200, { ok: true, annotation });
|
|
6344
7783
|
} catch {
|
|
6345
|
-
return
|
|
7784
|
+
return sendJson2(res, 500, { ok: false, error: "The note could not be kept." });
|
|
6346
7785
|
}
|
|
6347
7786
|
});
|
|
6348
7787
|
}
|
|
6349
7788
|
if (path === `${LEGLAS_PREFIX}/api/annotations/update` && req.method === "POST") {
|
|
6350
7789
|
if (!hasJsonBody(req)) {
|
|
6351
|
-
return
|
|
7790
|
+
return sendJson2(res, 400, { ok: false, error: "A note must be JSON." });
|
|
6352
7791
|
}
|
|
6353
7792
|
let body = "";
|
|
6354
7793
|
req.on("data", (chunk) => body += chunk);
|
|
6355
7794
|
return void req.on("end", async () => {
|
|
6356
7795
|
const parsed2 = jsonBody(body);
|
|
6357
7796
|
if (parsed2 === null) {
|
|
6358
|
-
return
|
|
7797
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6359
7798
|
}
|
|
6360
7799
|
if (typeof parsed2.id !== "string" || parsed2.id === "") {
|
|
6361
|
-
return
|
|
7800
|
+
return sendJson2(res, 400, { ok: false, error: "Body needs the note to reword." });
|
|
6362
7801
|
}
|
|
6363
7802
|
if (typeof parsed2.note !== "string") {
|
|
6364
|
-
return
|
|
7803
|
+
return sendJson2(res, 400, { ok: false, error: "A reworded note needs its words." });
|
|
6365
7804
|
}
|
|
6366
7805
|
try {
|
|
6367
7806
|
const annotation = await updateAnnotation(cwd, parsed2.id, parsed2.note);
|
|
6368
7807
|
if (annotation === null) {
|
|
6369
|
-
return
|
|
7808
|
+
return sendJson2(res, 404, { ok: false, error: "That note has gone." });
|
|
6370
7809
|
}
|
|
6371
|
-
return
|
|
7810
|
+
return sendJson2(res, 200, { ok: true, annotation });
|
|
6372
7811
|
} catch {
|
|
6373
|
-
return
|
|
7812
|
+
return sendJson2(res, 500, { ok: false, error: "The note could not be reworded." });
|
|
6374
7813
|
}
|
|
6375
7814
|
});
|
|
6376
7815
|
}
|
|
6377
7816
|
if (path === `${LEGLAS_PREFIX}/api/annotations/delete` && req.method === "POST") {
|
|
6378
7817
|
if (!hasJsonBody(req)) {
|
|
6379
|
-
return
|
|
7818
|
+
return sendJson2(res, 400, { ok: false, error: "Delete must be JSON." });
|
|
6380
7819
|
}
|
|
6381
7820
|
let body = "";
|
|
6382
7821
|
req.on("data", (chunk) => body += chunk);
|
|
6383
7822
|
return void req.on("end", async () => {
|
|
6384
7823
|
const parsed2 = jsonBody(body);
|
|
6385
7824
|
if (parsed2 === null) {
|
|
6386
|
-
return
|
|
7825
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6387
7826
|
}
|
|
6388
7827
|
const ids = Array.isArray(parsed2.ids) ? parsed2.ids.filter((entry) => typeof entry === "string") : [];
|
|
6389
7828
|
if (ids.length === 0) {
|
|
6390
|
-
return
|
|
7829
|
+
return sendJson2(res, 400, { ok: false, error: "Body needs the notes to forget." });
|
|
6391
7830
|
}
|
|
6392
7831
|
try {
|
|
6393
|
-
return
|
|
7832
|
+
return sendJson2(res, 200, { ok: true, deleted: await removeAnnotations(cwd, ids) });
|
|
6394
7833
|
} catch {
|
|
6395
|
-
return
|
|
7834
|
+
return sendJson2(res, 500, { ok: false, error: "The notes could not be forgotten." });
|
|
6396
7835
|
}
|
|
6397
7836
|
});
|
|
6398
7837
|
}
|
|
6399
7838
|
if (path === `${LEGLAS_PREFIX}/api/requests/dismiss` && req.method === "POST") {
|
|
6400
7839
|
if (!hasJsonBody(req)) {
|
|
6401
|
-
return
|
|
7840
|
+
return sendJson2(res, 400, { ok: false, error: "Dismiss must be JSON." });
|
|
6402
7841
|
}
|
|
6403
7842
|
let body = "";
|
|
6404
7843
|
req.on("data", (chunk) => body += chunk);
|
|
6405
7844
|
return void req.on("end", async () => {
|
|
6406
7845
|
const parsed2 = jsonBody(body);
|
|
6407
7846
|
if (parsed2 === null) {
|
|
6408
|
-
return
|
|
7847
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6409
7848
|
}
|
|
6410
7849
|
if (typeof parsed2.id !== "string") {
|
|
6411
|
-
return
|
|
7850
|
+
return sendJson2(res, 400, { ok: false, error: "Body needs a request id." });
|
|
6412
7851
|
}
|
|
6413
7852
|
const target2 = (await readRequests(cwd)).find((entry) => entry.id === parsed2.id);
|
|
6414
7853
|
if (target2 === void 0 || !isEnded(target2, runner?.snapshot().failedIds ?? [])) {
|
|
6415
|
-
return
|
|
7854
|
+
return sendJson2(res, 400, { ok: false, error: "Only an ended request can be dismissed." });
|
|
6416
7855
|
}
|
|
6417
7856
|
try {
|
|
6418
7857
|
if (!await removeRequest(cwd, parsed2.id)) {
|
|
6419
|
-
return
|
|
7858
|
+
return sendJson2(res, 404, { ok: false, error: "No such request." });
|
|
6420
7859
|
}
|
|
6421
|
-
return
|
|
7860
|
+
return sendJson2(res, 200, { ok: true });
|
|
6422
7861
|
} catch {
|
|
6423
|
-
return
|
|
7862
|
+
return sendJson2(res, 500, { ok: false, error: "The request could not be dismissed." });
|
|
6424
7863
|
}
|
|
6425
7864
|
});
|
|
6426
7865
|
}
|
|
@@ -6430,13 +7869,13 @@ async function startServer(options) {
|
|
|
6430
7869
|
return void req.on("end", () => {
|
|
6431
7870
|
const parsed2 = jsonBody(body);
|
|
6432
7871
|
if (parsed2 === null) {
|
|
6433
|
-
return
|
|
7872
|
+
return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
|
|
6434
7873
|
}
|
|
6435
7874
|
if (parsed2.renames === null || typeof parsed2.renames !== "object") {
|
|
6436
|
-
return
|
|
7875
|
+
return sendJson2(res, 400, { ok: false, error: "Body needs a renames object." });
|
|
6437
7876
|
}
|
|
6438
7877
|
const renames = Object.fromEntries(Object.entries(parsed2.renames).filter((entry) => typeof entry[1] === "string" && entry[1] !== ""));
|
|
6439
|
-
void writeRenames(cwd, renames).then(() =>
|
|
7878
|
+
void writeRenames(cwd, renames).then(() => sendJson2(res, 200, { ok: true }), () => sendJson2(res, 200, { ok: false }));
|
|
6440
7879
|
});
|
|
6441
7880
|
}
|
|
6442
7881
|
if (path === `${LEGLAS_PREFIX}/api/health`) {
|
|
@@ -6453,42 +7892,86 @@ async function startServer(options) {
|
|
|
6453
7892
|
relative6 = "";
|
|
6454
7893
|
}
|
|
6455
7894
|
const dir = fileMounts.get(slug);
|
|
6456
|
-
|
|
6457
|
-
|
|
6458
|
-
|
|
6459
|
-
|
|
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
|
+
});
|
|
6460
7911
|
}
|
|
6461
7912
|
if (path.startsWith(`${LEGLAS_PREFIX}/api/`)) {
|
|
6462
|
-
return
|
|
7913
|
+
return sendJson2(res, 404, { error: "No such Leglas API path." });
|
|
6463
7914
|
}
|
|
6464
7915
|
if (path === LEGLAS_PREFIX || path.startsWith(`${LEGLAS_PREFIX}/`)) {
|
|
6465
7916
|
if (shellDir !== null && serveShellFile(res, shellDir, path))
|
|
6466
7917
|
return;
|
|
6467
7918
|
if (shellDir !== null) {
|
|
6468
7919
|
res.writeHead(404, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
|
|
6469
|
-
|
|
7920
|
+
res.end("Leglas: no such path.");
|
|
7921
|
+
return;
|
|
6470
7922
|
}
|
|
6471
7923
|
res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
6472
|
-
|
|
7924
|
+
res.end(PLACEHOLDER);
|
|
7925
|
+
return;
|
|
6473
7926
|
}
|
|
6474
|
-
return proxy.request(req, res,
|
|
6475
|
-
}
|
|
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
|
+
}));
|
|
6476
7934
|
const sockets = /* @__PURE__ */ new Set();
|
|
6477
7935
|
server.on("connection", (socket) => {
|
|
6478
7936
|
sockets.add(socket);
|
|
6479
7937
|
socket.once("close", () => sockets.delete(socket));
|
|
6480
7938
|
});
|
|
6481
|
-
|
|
6482
|
-
|
|
6483
|
-
|
|
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;
|
|
6484
7943
|
const path = (req.url ?? "/").split("?")[0] ?? "/";
|
|
6485
|
-
if (path.startsWith(`${LEGLAS_PREFIX}/`))
|
|
6486
|
-
|
|
7944
|
+
if (path.startsWith(`${LEGLAS_PREFIX}/`)) {
|
|
7945
|
+
socket.destroy();
|
|
7946
|
+
return false;
|
|
7947
|
+
}
|
|
6487
7948
|
proxy.upgrade(req, socket, head);
|
|
7949
|
+
return false;
|
|
7950
|
+
};
|
|
7951
|
+
server.on("upgrade", (req, socket, head) => {
|
|
7952
|
+
handleUpgrade(req, socket, head, { remote: false });
|
|
6488
7953
|
});
|
|
6489
|
-
|
|
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 }
|
|
7971
|
+
});
|
|
7972
|
+
port = await bind2(server, options.port ?? DEFAULT_PORT);
|
|
6490
7973
|
const liveFiles = watchLiveFiles(cwd, bootConfigPath, live);
|
|
6491
|
-
|
|
7974
|
+
liveHealth = watchHealth(target, live);
|
|
6492
7975
|
await pruneCaptures(cwd, (await readRequests(cwd).catch(() => [])).map((request) => request.id)).catch(() => {
|
|
6493
7976
|
});
|
|
6494
7977
|
await writeServerInfo(cwd, {
|
|
@@ -6512,21 +7995,26 @@ async function startServer(options) {
|
|
|
6512
7995
|
close: () => {
|
|
6513
7996
|
if (closePromise !== null)
|
|
6514
7997
|
return closePromise;
|
|
6515
|
-
|
|
6516
|
-
|
|
6517
|
-
|
|
6518
|
-
|
|
6519
|
-
|
|
6520
|
-
|
|
6521
|
-
|
|
6522
|
-
|
|
6523
|
-
|
|
6524
|
-
|
|
6525
|
-
|
|
6526
|
-
|
|
6527
|
-
|
|
6528
|
-
|
|
6529
|
-
|
|
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
|
+
})();
|
|
6530
8018
|
return closePromise;
|
|
6531
8019
|
}
|
|
6532
8020
|
};
|