leglas 0.8.0 → 1.0.0

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