leglas 0.9.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1358,9 +1358,9 @@ function agentSearchPath(env = process.env, platform = process.platform) {
1358
1358
  function agentEnvironment(env = process.env) {
1359
1359
  return { ...env, PATH: agentSearchPath(env) };
1360
1360
  }
1361
- async function pathLookup(binary) {
1362
- const entries = agentSearchPath().split(delimiter).filter((entry) => entry !== "");
1363
- 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 !== "") : [""];
1364
1364
  for (const entry of entries) {
1365
1365
  for (const extension of extensions) {
1366
1366
  try {
@@ -1372,10 +1372,10 @@ async function pathLookup(binary) {
1372
1372
  }
1373
1373
  return false;
1374
1374
  }
1375
- async function detectAgents(lookup = pathLookup, probe2 = execProbe) {
1375
+ async function detectAgents(lookup2 = pathLookup, probe2 = execProbe) {
1376
1376
  const entries = Object.entries(KNOWN_AGENTS);
1377
1377
  return Promise.all(entries.map(async ([id, adapter]) => {
1378
- const available = await lookup(adapter.binary).catch(() => false);
1378
+ const available = await lookup2(adapter.binary).catch(() => false);
1379
1379
  if (!available) {
1380
1380
  return {
1381
1381
  id,
@@ -1804,6 +1804,13 @@ async function dropLocalPreviews(cwd, titles) {
1804
1804
  // ../server/dist/proxy.js
1805
1805
  import http, {} from "http";
1806
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
+ }
1807
1814
  function createProxyHandler(options) {
1808
1815
  const target = new URL(options.target);
1809
1816
  const host = target.hostname;
@@ -1811,19 +1818,25 @@ function createProxyHandler(options) {
1811
1818
  const port = Number(target.port || (target.protocol === "https:" ? 443 : 80));
1812
1819
  const authority = target.port ? `${host}:${target.port}` : host;
1813
1820
  function upstreamHeaders(req) {
1814
- 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;
1815
1828
  }
1816
- function rewriteLocation(location, publicOrigin) {
1829
+ function rewriteLocation(location, publicOrigin2) {
1817
1830
  if (location === void 0)
1818
1831
  return void 0;
1819
1832
  for (const origin of [`${target.protocol}//${authority}`, `${target.protocol}//localhost:${port}`]) {
1820
1833
  if (location.startsWith(origin))
1821
- return publicOrigin + location.slice(origin.length);
1834
+ return publicOrigin2 + location.slice(origin.length);
1822
1835
  }
1823
1836
  return location;
1824
1837
  }
1825
1838
  return {
1826
- request(req, res, publicOrigin) {
1839
+ request(req, res, publicOrigin2) {
1827
1840
  options.onActivity?.();
1828
1841
  options.onOpen?.();
1829
1842
  let open = true;
@@ -1838,7 +1851,7 @@ function createProxyHandler(options) {
1838
1851
  res.once("close", close);
1839
1852
  const upstream = http.request({ host: dialHost, port, method: req.method, path: req.url, headers: upstreamHeaders(req) }, (upstreamRes) => {
1840
1853
  const headers = { ...upstreamRes.headers };
1841
- 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);
1842
1855
  if (location !== void 0)
1843
1856
  headers.location = location;
1844
1857
  res.writeHead(upstreamRes.statusCode ?? 502, headers);
@@ -3027,13 +3040,13 @@ async function attachRequest(cwd, requestId, input, deps) {
3027
3040
  const references = [];
3028
3041
  requestedWidths.set(captured, Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, Math.round(input.width))));
3029
3042
  const controller = new AbortController();
3030
- let expired = false;
3043
+ let expired2 = false;
3031
3044
  let finishDeadline;
3032
3045
  const deadline = new Promise((resolve5) => {
3033
3046
  finishDeadline = resolve5;
3034
3047
  });
3035
3048
  const timer = setTimeout(() => {
3036
- expired = true;
3049
+ expired2 = true;
3037
3050
  controller.abort();
3038
3051
  finishDeadline();
3039
3052
  }, deadlineMs);
@@ -3046,7 +3059,7 @@ async function attachRequest(cwd, requestId, input, deps) {
3046
3059
  const work = (async () => {
3047
3060
  try {
3048
3061
  const browser = await deps.pool.acquire();
3049
- if (expired)
3062
+ if (expired2)
3050
3063
  return;
3051
3064
  if (browser === null) {
3052
3065
  captured.skipped = deps.pool.reason() ?? NO_BROWSER;
@@ -3060,7 +3073,7 @@ async function attachRequest(cwd, requestId, input, deps) {
3060
3073
  signal: controller.signal
3061
3074
  };
3062
3075
  const direction = await capture(browser, directionInput);
3063
- if (expired)
3076
+ if (expired2)
3064
3077
  return;
3065
3078
  await mkdir3(destination, { recursive: true });
3066
3079
  await writeFile3(join5(destination, "frame.png"), direction.frame.png);
@@ -3078,7 +3091,7 @@ async function attachRequest(cwd, requestId, input, deps) {
3078
3091
  for (let index = 0; index < direction.crops.length; index += 1) {
3079
3092
  const crop = direction.crops[index];
3080
3093
  const note = input.notes[index];
3081
- if (crop === null || crop === void 0 || note === void 0 || expired)
3094
+ if (crop === null || crop === void 0 || note === void 0 || expired2)
3082
3095
  continue;
3083
3096
  const name = `note-${index + 1}.png`;
3084
3097
  await writeFile3(join5(destination, name), crop.shot.png);
@@ -3092,7 +3105,7 @@ async function attachRequest(cwd, requestId, input, deps) {
3092
3105
  viewport: direction.frame.width
3093
3106
  });
3094
3107
  }
3095
- if (input.compare !== null && !expired) {
3108
+ if (input.compare !== null && !expired2) {
3096
3109
  const compareInput = {
3097
3110
  url: previewUrl(input.origin, input.compare),
3098
3111
  width: input.width,
@@ -3100,7 +3113,7 @@ async function attachRequest(cwd, requestId, input, deps) {
3100
3113
  signal: controller.signal
3101
3114
  };
3102
3115
  const comparison = await capture(browser, compareInput);
3103
- if (expired)
3116
+ if (expired2)
3104
3117
  return;
3105
3118
  await writeFile3(join5(destination, "compare.png"), comparison.frame.png);
3106
3119
  captured.attachments.push({
@@ -3113,14 +3126,14 @@ async function attachRequest(cwd, requestId, input, deps) {
3113
3126
  });
3114
3127
  }
3115
3128
  } catch (error) {
3116
- if (!expired) {
3129
+ if (!expired2) {
3117
3130
  captured.skipped = error instanceof Error ? error.message : `The page did not load: ${String(error)}`;
3118
3131
  }
3119
3132
  }
3120
3133
  })();
3121
3134
  await Promise.race([work, deadline]);
3122
3135
  clearTimeout(timer);
3123
- if (expired)
3136
+ if (expired2)
3124
3137
  captured.skipped = "The design could not be captured in time.";
3125
3138
  captured.attachments.push(...references);
3126
3139
  return captured;
@@ -5407,10 +5420,14 @@ function createCoalescer(emit, options = {}) {
5407
5420
  }
5408
5421
  };
5409
5422
  }
5410
- function createLiveHub(_options = {}) {
5423
+ function createLiveHub(options = {}) {
5411
5424
  const listeners = /* @__PURE__ */ new Set();
5425
+ let viewers = 0;
5412
5426
  const drop = (listener) => {
5413
- listeners.delete(listener);
5427
+ if (!listeners.delete(listener) || !listener.viewer)
5428
+ return;
5429
+ viewers = Math.max(0, viewers - 1);
5430
+ options.onViewers?.(viewers);
5414
5431
  };
5415
5432
  const write2 = (listener, opcode, payload) => {
5416
5433
  if (listener.socket.destroyed || !listener.socket.writable) {
@@ -5495,7 +5512,7 @@ function createLiveHub(_options = {}) {
5495
5512
  }
5496
5513
  }
5497
5514
  },
5498
- upgrade: (req, socket, head) => {
5515
+ upgrade: (req, socket, head, upgradeOptions = {}) => {
5499
5516
  const path = (req.url ?? "/").split("?")[0] ?? "/";
5500
5517
  if (req.method !== "GET" || path !== LIVE_PATH)
5501
5518
  return false;
@@ -5517,8 +5534,16 @@ Sec-WebSocket-Accept: ${accept}\r
5517
5534
  socket.destroy();
5518
5535
  return false;
5519
5536
  }
5520
- const listener = { socket, buffered: Buffer.alloc(0) };
5537
+ const listener = {
5538
+ socket,
5539
+ buffered: Buffer.alloc(0),
5540
+ viewer: upgradeOptions.viewer === true
5541
+ };
5521
5542
  listeners.add(listener);
5543
+ if (listener.viewer) {
5544
+ viewers += 1;
5545
+ options.onViewers?.(viewers);
5546
+ }
5522
5547
  socket.on("data", (chunk) => read(listener, chunk));
5523
5548
  socket.once("error", () => drop(listener));
5524
5549
  socket.once("end", () => drop(listener));
@@ -5536,8 +5561,1328 @@ Sec-WebSocket-Accept: ${accept}\r
5536
5561
  },
5537
5562
  get listening() {
5538
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;
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();
5539
6370
  }
5540
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
6885
+ };
5541
6886
  }
5542
6887
 
5543
6888
  // ../server/dist/renames.js
@@ -5576,7 +6921,7 @@ function resolveTitle(input, titles, renames) {
5576
6921
  import { createReadStream, existsSync as existsSync3, statSync, unwatchFile, watch as watchFs, watchFile } from "fs";
5577
6922
  import { createHash as createHash2 } from "crypto";
5578
6923
  import { mkdir as mkdir8, readdir as readdir3, writeFile as writeFile8 } from "fs/promises";
5579
- import http2 from "http";
6924
+ import http4 from "http";
5580
6925
  import net3 from "net";
5581
6926
  import { basename as basename3, dirname as dirname8, extname as extname3, join as join12, normalize, relative as relative3 } from "path";
5582
6927
 
@@ -5645,7 +6990,7 @@ var CONTENT_TYPES = {
5645
6990
  ".woff2": "font/woff2"
5646
6991
  };
5647
6992
  var FILES_PREFIX = `${LEGLAS_PREFIX}/files`;
5648
- function sendJson(res, status, body) {
6993
+ function sendJson2(res, status, body) {
5649
6994
  const payload = JSON.stringify(body);
5650
6995
  res.writeHead(status, {
5651
6996
  "content-type": "application/json; charset=utf-8",
@@ -5982,7 +7327,8 @@ function watchHealth(target, live) {
5982
7327
  close: () => {
5983
7328
  closed = true;
5984
7329
  clearInterval(timer);
5985
- }
7330
+ },
7331
+ reachable: () => previous
5986
7332
  };
5987
7333
  }
5988
7334
  function snapshotConfig(cwd) {
@@ -6039,7 +7385,7 @@ function listen(server, port) {
6039
7385
  server.listen(port, "127.0.0.1");
6040
7386
  });
6041
7387
  }
6042
- async function bind(server, requested) {
7388
+ async function bind2(server, requested) {
6043
7389
  if (requested === 0)
6044
7390
  return listen(server, 0);
6045
7391
  for (let attempt = 0; attempt < PORT_ATTEMPTS; attempt += 1) {
@@ -6055,6 +7401,8 @@ async function bind(server, requested) {
6055
7401
  async function startServer(options) {
6056
7402
  const { config, configErrors = [], configWarnings = [], shellDir = null, project = "", cwd = process.cwd(), leglasCommand = "npx -y leglas", fileMounts = /* @__PURE__ */ new Map(), detect = () => detectAgents() } = options;
6057
7403
  const browserPool = options.pool ?? createBrowserPool();
7404
+ let shares = null;
7405
+ let liveHealth = null;
6058
7406
  const live = options.live ?? createLiveHub();
6059
7407
  const branches = createBranchRegistry({
6060
7408
  cwd,
@@ -6129,12 +7477,103 @@ async function startServer(options) {
6129
7477
  const livePreviews = async () => (await livePreviewDefinitions()).map(readyPreview).filter((preview) => preview !== null);
6130
7478
  void probeAgents().catch(() => {
6131
7479
  });
6132
- 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) => {
6133
7496
  const url = req.url ?? "/";
6134
7497
  const path = url.split("?")[0] ?? "/";
6135
7498
  const query = new URLSearchParams(url.includes("?") ? url.slice(url.indexOf("?") + 1) : "");
6136
- if (req.method === "POST" && path.startsWith(`${LEGLAS_PREFIX}/api/`) && !isTrustedMutation(req)) {
6137
- 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 }));
6138
7577
  }
6139
7578
  if (path === `${LEGLAS_PREFIX}/api/config`) {
6140
7579
  const boot = config?.previews ?? [];
@@ -6180,23 +7619,23 @@ async function startServer(options) {
6180
7619
  return void req.on("end", async () => {
6181
7620
  const parsed = jsonBody(body);
6182
7621
  if (parsed === null) {
6183
- return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
7622
+ return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
6184
7623
  }
6185
7624
  if (typeof parsed.title !== "string" || parsed.title.trim() === "") {
6186
- 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." });
6187
7626
  }
6188
7627
  const preview = (await livePreviewDefinitions()).find((entry) => entry.title === parsed.title);
6189
7628
  if (preview === void 0) {
6190
- return sendJson(res, 404, { ok: false, error: "No such direction." });
7629
+ return sendJson2(res, 404, { ok: false, error: "No such direction." });
6191
7630
  }
6192
7631
  if (preview.branch === void 0) {
6193
- return sendJson(res, 400, {
7632
+ return sendJson2(res, 400, {
6194
7633
  ok: false,
6195
7634
  error: `"${preview.title}" is not a branch preview.`
6196
7635
  });
6197
7636
  }
6198
7637
  if (config?.devCommand === void 0) {
6199
- return sendJson(res, 400, {
7638
+ return sendJson2(res, 400, {
6200
7639
  ok: false,
6201
7640
  error: `"${preview.title}" cannot start because the config sets no devCommand.`
6202
7641
  });
@@ -6204,9 +7643,9 @@ async function startServer(options) {
6204
7643
  void branches.start(preview.title);
6205
7644
  const state = branches.state(preview.title);
6206
7645
  if (state === void 0) {
6207
- return sendJson(res, 404, { ok: false, error: "No such branch preview." });
7646
+ return sendJson2(res, 404, { ok: false, error: "No such branch preview." });
6208
7647
  }
6209
- return sendJson(res, 200, { ok: true, state: publicBranchState(state) });
7648
+ return sendJson2(res, 200, { ok: true, state: publicBranchState(state) });
6210
7649
  });
6211
7650
  }
6212
7651
  if (path === `${LEGLAS_PREFIX}/api/previews/delete` && req.method === "POST") {
@@ -6215,11 +7654,11 @@ async function startServer(options) {
6215
7654
  return void req.on("end", async () => {
6216
7655
  const parsed = jsonBody(body);
6217
7656
  if (parsed === null) {
6218
- return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
7657
+ return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
6219
7658
  }
6220
7659
  const titles = parsed.titles;
6221
7660
  if (!Array.isArray(titles) || titles.length === 0 || titles.some((title) => typeof title !== "string" || title.trim() === "")) {
6222
- return sendJson(res, 400, {
7661
+ return sendJson2(res, 400, {
6223
7662
  ok: false,
6224
7663
  error: "Body needs a non-empty array of direction titles."
6225
7664
  });
@@ -6228,20 +7667,20 @@ async function startServer(options) {
6228
7667
  try {
6229
7668
  const local = await readLocalPreviews(cwd);
6230
7669
  if (local.errors.length > 0) {
6231
- return sendJson(res, 409, { ok: false, error: local.errors.join(" ") });
7670
+ return sendJson2(res, 409, { ok: false, error: local.errors.join(" ") });
6232
7671
  }
6233
7672
  const localTitles = new Set(local.previews.map((preview) => preview.title));
6234
7673
  const unknown = unique.filter((title) => !localTitles.has(title));
6235
7674
  if (unknown.length > 0) {
6236
- return sendJson(res, 400, {
7675
+ return sendJson2(res, 400, {
6237
7676
  ok: false,
6238
7677
  error: "Only machine-local directions can be deleted from the registry."
6239
7678
  });
6240
7679
  }
6241
7680
  const deleted = await dropLocalPreviews(cwd, unique);
6242
- return sendJson(res, 200, { ok: true, deleted });
7681
+ return sendJson2(res, 200, { ok: true, deleted });
6243
7682
  } catch {
6244
- return sendJson(res, 500, {
7683
+ return sendJson2(res, 500, {
6245
7684
  ok: false,
6246
7685
  error: "The directions could not be deleted from Leglas."
6247
7686
  });
@@ -6251,7 +7690,7 @@ async function startServer(options) {
6251
7690
  if (path === `${LEGLAS_PREFIX}/api/references` && req.method === "POST") {
6252
7691
  const declaredLength = req.headers["content-length"];
6253
7692
  if (typeof declaredLength === "string" && Number(declaredLength) > REFERENCE_MAX_BYTES) {
6254
- 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." });
6255
7694
  }
6256
7695
  const chunks = [];
6257
7696
  let bytes = 0;
@@ -6265,7 +7704,7 @@ async function startServer(options) {
6265
7704
  refused = true;
6266
7705
  req.pause();
6267
7706
  res.once("finish", () => req.socket.destroy());
6268
- sendJson(res, 413, { ok: false, error: "That image is over 10MB." });
7707
+ sendJson2(res, 413, { ok: false, error: "That image is over 10MB." });
6269
7708
  return;
6270
7709
  }
6271
7710
  chunks.push(buffer);
@@ -6274,12 +7713,12 @@ async function startServer(options) {
6274
7713
  if (refused)
6275
7714
  return;
6276
7715
  if (bytes === 0) {
6277
- return sendJson(res, 400, { ok: false, error: "The upload was empty." });
7716
+ return sendJson2(res, 400, { ok: false, error: "The upload was empty." });
6278
7717
  }
6279
7718
  const body = Buffer.concat(chunks, bytes);
6280
7719
  const image = sniffImage(body);
6281
7720
  if (image === null) {
6282
- return sendJson(res, 415, {
7721
+ return sendJson2(res, 415, {
6283
7722
  ok: false,
6284
7723
  error: "Only PNG, JPEG, WebP and GIF images can be attached."
6285
7724
  });
@@ -6291,7 +7730,7 @@ async function startServer(options) {
6291
7730
  await writeFile8(join12(cwd, file), body);
6292
7731
  void pruneReferences(cwd).catch(() => {
6293
7732
  });
6294
- return sendJson(res, 200, {
7733
+ return sendJson2(res, 200, {
6295
7734
  ok: true,
6296
7735
  reference: {
6297
7736
  id,
@@ -6303,7 +7742,7 @@ async function startServer(options) {
6303
7742
  }
6304
7743
  });
6305
7744
  } catch {
6306
- return sendJson(res, 500, {
7745
+ return sendJson2(res, 500, {
6307
7746
  ok: false,
6308
7747
  error: "The image could not be attached."
6309
7748
  });
@@ -6316,24 +7755,24 @@ async function startServer(options) {
6316
7755
  return void req.on("end", async () => {
6317
7756
  const parsed = jsonBody(body);
6318
7757
  if (parsed === null) {
6319
- return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
7758
+ return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
6320
7759
  }
6321
7760
  if (parsed.mode !== void 0 && parsed.mode !== "variant" && parsed.mode !== "replace") {
6322
- return sendJson(res, 400, {
7761
+ return sendJson2(res, 400, {
6323
7762
  ok: false,
6324
7763
  error: 'mode must be "variant" or "replace".'
6325
7764
  });
6326
7765
  }
6327
7766
  const mode = parsed.mode === "replace" ? "replace" : "variant";
6328
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)))) {
6329
- 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." });
6330
7769
  }
6331
7770
  const references = parsed.references ?? [];
6332
7771
  if (references.length > 0) {
6333
7772
  const present = new Set((await readdir3(join12(cwd, REFERENCES_DIR)).catch(() => [])).map((name) => name.slice(0, name.indexOf(".") === -1 ? name.length : name.indexOf("."))));
6334
7773
  const gone = references.filter((id2) => !present.has(id2));
6335
7774
  if (gone.length > 0) {
6336
- return sendJson(res, 410, {
7775
+ return sendJson2(res, 410, {
6337
7776
  ok: false,
6338
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."
6339
7778
  });
@@ -6343,11 +7782,11 @@ async function startServer(options) {
6343
7782
  const previews = await livePreviews();
6344
7783
  const preview = previews.find((entry) => entry.title === parsed.title);
6345
7784
  if (!preview) {
6346
- 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." });
6347
7786
  }
6348
7787
  const notes = annotationsFor(await readAnnotations(cwd).catch(() => []), preview.title);
6349
7788
  if (!parsed.intent?.trim() && notes.length === 0) {
6350
- 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." });
6351
7790
  }
6352
7791
  const intent = (parsed.intent ?? "").trim();
6353
7792
  const live2 = (await readRequests(cwd).catch(() => [])).filter((entry) => entry.status === "queued" || entry.status === "picked-up");
@@ -6361,7 +7800,7 @@ async function startServer(options) {
6361
7800
  // one forks the direction and the other rewrites it. Only a
6362
7801
  // genuine repeat is refused.
6363
7802
  (entry.mode ?? "replace") === mode && sameNotes(entry) && sameContext(entry))) {
6364
- return sendJson(res, 409, {
7803
+ return sendJson2(res, 409, {
6365
7804
  ok: false,
6366
7805
  duplicate: true,
6367
7806
  error: `That exact change to ${preview.title} is already waiting.`
@@ -6395,13 +7834,13 @@ async function startServer(options) {
6395
7834
  ...composed
6396
7835
  }, id);
6397
7836
  runner?.nudge();
6398
- return sendJson(res, 200, {
7837
+ return sendJson2(res, 200, {
6399
7838
  ok: true,
6400
7839
  ...composed,
6401
7840
  attachments: captured.attachments
6402
7841
  });
6403
7842
  } catch {
6404
- return sendJson(res, 200, {
7843
+ return sendJson2(res, 200, {
6405
7844
  ok: true,
6406
7845
  ...composed,
6407
7846
  attachments: captured.attachments,
@@ -6412,29 +7851,29 @@ async function startServer(options) {
6412
7851
  }
6413
7852
  if (path === `${LEGLAS_PREFIX}/api/capture` && req.method === "POST") {
6414
7853
  if (!hasJsonBody(req)) {
6415
- return sendJson(res, 400, { ok: false, error: "Capture must be JSON." });
7854
+ return sendJson2(res, 400, { ok: false, error: "Capture must be JSON." });
6416
7855
  }
6417
7856
  let body = "";
6418
7857
  req.on("data", (chunk) => body += chunk);
6419
7858
  return void req.on("end", async () => {
6420
7859
  const parsed = jsonBody(body);
6421
7860
  if (parsed === null) {
6422
- return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
7861
+ return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
6423
7862
  }
6424
7863
  if (typeof parsed.title !== "string" || parsed.title === "") {
6425
- 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." });
6426
7865
  }
6427
7866
  if (parsed.note !== void 0 && typeof parsed.note !== "string") {
6428
- 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." });
6429
7868
  }
6430
7869
  const preview = (await livePreviews()).find((entry) => entry.title === parsed.title);
6431
7870
  if (preview === void 0) {
6432
- return sendJson(res, 404, { ok: false, error: "No such direction." });
7871
+ return sendJson2(res, 404, { ok: false, error: "No such direction." });
6433
7872
  }
6434
7873
  const width = typeof parsed.width === "number" && Number.isFinite(parsed.width) ? Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, Math.round(parsed.width))) : 1440;
6435
7874
  const browser = await browserPool.acquire();
6436
7875
  if (browser === null) {
6437
- return sendJson(res, 503, {
7876
+ return sendJson2(res, 503, {
6438
7877
  ok: false,
6439
7878
  error: browserPool.reason() ?? NO_BROWSER
6440
7879
  });
@@ -6471,7 +7910,7 @@ async function startServer(options) {
6471
7910
  });
6472
7911
  const result = await Promise.race([work, timeout]);
6473
7912
  if (result === timeoutMarker) {
6474
- 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." });
6475
7914
  }
6476
7915
  clearTimeout(timer);
6477
7916
  const crop = annotations.length > 0 ? result.crops[0] : null;
@@ -6481,7 +7920,7 @@ async function startServer(options) {
6481
7920
  const relativeFile = `${CAPTURES_DIR}/show/${name}`;
6482
7921
  await mkdir8(join12(cwd, CAPTURES_DIR, "show"), { recursive: true });
6483
7922
  await writeFile8(join12(cwd, relativeFile), shot.png);
6484
- return sendJson(res, 200, {
7923
+ return sendJson2(res, 200, {
6485
7924
  ok: true,
6486
7925
  file: relativeFile,
6487
7926
  width: shot.width,
@@ -6493,7 +7932,7 @@ async function startServer(options) {
6493
7932
  });
6494
7933
  } catch (error) {
6495
7934
  clearTimeout(timer);
6496
- return sendJson(res, 502, {
7935
+ return sendJson2(res, 502, {
6497
7936
  ok: false,
6498
7937
  error: error instanceof Error ? error.message : String(error)
6499
7938
  });
@@ -6504,14 +7943,14 @@ async function startServer(options) {
6504
7943
  return void readAgentChoice(cwd).then((choice) => {
6505
7944
  if (choice.agent !== null)
6506
7945
  runner?.prepare(choice.agent);
6507
- sendJson(res, 200, { ok: true });
6508
- }, () => sendJson(res, 200, { ok: true }));
7946
+ sendJson2(res, 200, { ok: true });
7947
+ }, () => sendJson2(res, 200, { ok: true }));
6509
7948
  }
6510
7949
  if (path === `${LEGLAS_PREFIX}/api/agents` && req.method === "GET") {
6511
7950
  return void Promise.all([
6512
7951
  currentAgents(query.get("refresh") === "1"),
6513
7952
  readAgentChoice(cwd)
6514
- ]).then(([agents, choice]) => sendJson(res, 200, {
7953
+ ]).then(([agents, choice]) => sendJson2(res, 200, {
6515
7954
  agents,
6516
7955
  choice: choice.agent,
6517
7956
  customRun: choice.run,
@@ -6520,48 +7959,48 @@ async function startServer(options) {
6520
7959
  }
6521
7960
  if (path === `${LEGLAS_PREFIX}/api/agent` && req.method === "POST") {
6522
7961
  if (!isLoopbackAddress(req.socket.remoteAddress)) {
6523
- return sendJson(res, 403, {
7962
+ return sendJson2(res, 403, {
6524
7963
  ok: false,
6525
7964
  error: "The agent choice can only be made from the machine running Leglas."
6526
7965
  });
6527
7966
  }
6528
7967
  if (!hasJsonBody(req)) {
6529
- 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." });
6530
7969
  }
6531
7970
  let body = "";
6532
7971
  req.on("data", (chunk) => body += chunk);
6533
7972
  return void req.on("end", () => {
6534
7973
  const parsed = jsonBody(body);
6535
7974
  if (parsed === null) {
6536
- return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
7975
+ return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
6537
7976
  }
6538
7977
  if (!isKnownAgent(parsed.agent) && parsed.agent !== "custom") {
6539
- 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." });
6540
7979
  }
6541
7980
  if (parsed.run !== void 0 && typeof parsed.run !== "string") {
6542
- 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." });
6543
7982
  }
6544
7983
  const effort = parsed.effort === null || isAgentEffort(parsed.effort) ? parsed.effort : void 0;
6545
7984
  if (parsed.effort !== void 0 && effort === void 0) {
6546
- 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." });
6547
7986
  }
6548
7987
  if (parsed.agent === "custom") {
6549
7988
  if (effort !== void 0) {
6550
- return sendJson(res, 400, {
7989
+ return sendJson2(res, 400, {
6551
7990
  ok: false,
6552
7991
  error: "Custom agents manage effort in their own command."
6553
7992
  });
6554
7993
  }
6555
7994
  if (typeof parsed.run !== "string") {
6556
- 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." });
6557
7996
  }
6558
7997
  const template = parseTemplate(parsed.run);
6559
7998
  if (!template.ok)
6560
- return sendJson(res, 400, { ok: false, error: template.error });
6561
- 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." }));
6562
8001
  }
6563
8002
  if (effort !== void 0 && effort !== null && !KNOWN_AGENTS[parsed.agent].efforts.includes(effort)) {
6564
- return sendJson(res, 400, {
8003
+ return sendJson2(res, 400, {
6565
8004
  ok: false,
6566
8005
  error: `${KNOWN_AGENTS[parsed.agent].name} does not expose an effort override.`
6567
8006
  });
@@ -6571,8 +8010,8 @@ async function startServer(options) {
6571
8010
  ...effort === void 0 ? {} : { effort }
6572
8011
  }).then(() => {
6573
8012
  runner?.prepare(parsed.agent);
6574
- sendJson(res, 200, { ok: true });
6575
- }, () => 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." }));
6576
8015
  });
6577
8016
  }
6578
8017
  if (path === `${LEGLAS_PREFIX}/api/watch` && req.method === "POST") {
@@ -6581,13 +8020,13 @@ async function startServer(options) {
6581
8020
  return void req.on("end", () => {
6582
8021
  const parsed = jsonBody(body);
6583
8022
  if (parsed === null) {
6584
- return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
8023
+ return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
6585
8024
  }
6586
8025
  if (typeof parsed.watching !== "boolean") {
6587
- 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." });
6588
8027
  }
6589
8028
  lastSeen = parsed.watching ? Date.now() : null;
6590
- sendJson(res, 200, { ok: true });
8029
+ sendJson2(res, 200, { ok: true });
6591
8030
  });
6592
8031
  }
6593
8032
  if (path === `${LEGLAS_PREFIX}/api/requests` && req.method === "GET") {
@@ -6636,47 +8075,47 @@ async function startServer(options) {
6636
8075
  }
6637
8076
  if (path === `${LEGLAS_PREFIX}/api/requests/cancel` && req.method === "POST") {
6638
8077
  if (!hasJsonBody(req)) {
6639
- return sendJson(res, 200, { ok: true, cancelled: runner?.cancel() ?? false });
8078
+ return sendJson2(res, 200, { ok: true, cancelled: runner?.cancel() ?? false });
6640
8079
  }
6641
8080
  let body = "";
6642
8081
  req.on("data", (chunk) => body += chunk);
6643
8082
  return void req.on("end", () => {
6644
8083
  const parsed = jsonBody(body);
6645
8084
  if (parsed === null) {
6646
- return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
8085
+ return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
6647
8086
  }
6648
8087
  if (parsed.id !== void 0 && typeof parsed.id !== "string") {
6649
- 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." });
6650
8089
  }
6651
- 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 });
6652
8091
  });
6653
8092
  }
6654
8093
  if (path === `${LEGLAS_PREFIX}/api/requests/retry` && req.method === "POST") {
6655
8094
  if (!hasJsonBody(req)) {
6656
- return sendJson(res, 400, { ok: false, error: "Retry must be JSON." });
8095
+ return sendJson2(res, 400, { ok: false, error: "Retry must be JSON." });
6657
8096
  }
6658
8097
  let body = "";
6659
8098
  req.on("data", (chunk) => body += chunk);
6660
8099
  return void req.on("end", async () => {
6661
8100
  const parsed = jsonBody(body);
6662
8101
  if (parsed === null) {
6663
- return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
8102
+ return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
6664
8103
  }
6665
8104
  if (typeof parsed.id !== "string") {
6666
- 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." });
6667
8106
  }
6668
8107
  const request = (await readRequests(cwd)).find((entry) => entry.id === parsed.id);
6669
8108
  if (request === void 0) {
6670
- return sendJson(res, 404, { ok: false, error: "No such request." });
8109
+ return sendJson2(res, 404, { ok: false, error: "No such request." });
6671
8110
  }
6672
8111
  if (!isEnded(request, runner?.snapshot().failedIds ?? [])) {
6673
- 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." });
6674
8113
  }
6675
8114
  try {
6676
8115
  const retryId = newRequestId();
6677
8116
  const attachments = await rehomeCaptures(cwd, request.id, retryId, request.attachments ?? []).catch(() => []);
6678
8117
  if (!await removeRequest(cwd, request.id)) {
6679
- return sendJson(res, 404, { ok: false, error: "No such request." });
8118
+ return sendJson2(res, 404, { ok: false, error: "No such request." });
6680
8119
  }
6681
8120
  await appendRequest(cwd, {
6682
8121
  title: request.title,
@@ -6697,9 +8136,9 @@ async function startServer(options) {
6697
8136
  ...request.references === void 0 ? {} : { references: request.references }
6698
8137
  }, retryId);
6699
8138
  runner?.nudge();
6700
- return sendJson(res, 200, { ok: true });
8139
+ return sendJson2(res, 200, { ok: true });
6701
8140
  } catch {
6702
- 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." });
6703
8142
  }
6704
8143
  });
6705
8144
  }
@@ -6708,21 +8147,21 @@ async function startServer(options) {
6708
8147
  }
6709
8148
  if (path === `${LEGLAS_PREFIX}/api/annotations` && req.method === "POST") {
6710
8149
  if (!hasJsonBody(req)) {
6711
- 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." });
6712
8151
  }
6713
8152
  let body = "";
6714
8153
  req.on("data", (chunk) => body += chunk);
6715
8154
  return void req.on("end", async () => {
6716
8155
  const parsed = jsonBody(body);
6717
8156
  if (parsed === null) {
6718
- return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
8157
+ return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
6719
8158
  }
6720
8159
  if (typeof parsed.title !== "string" || parsed.title.trim() === "") {
6721
- 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." });
6722
8161
  }
6723
8162
  const anchor = anchorFrom(parsed.anchor);
6724
8163
  if (anchor === null) {
6725
- 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." });
6726
8165
  }
6727
8166
  try {
6728
8167
  const annotation = await addAnnotation(cwd, {
@@ -6730,87 +8169,87 @@ async function startServer(options) {
6730
8169
  note: typeof parsed.note === "string" ? parsed.note.trim() : "",
6731
8170
  title: parsed.title
6732
8171
  });
6733
- return sendJson(res, 200, { ok: true, annotation });
8172
+ return sendJson2(res, 200, { ok: true, annotation });
6734
8173
  } catch {
6735
- 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." });
6736
8175
  }
6737
8176
  });
6738
8177
  }
6739
8178
  if (path === `${LEGLAS_PREFIX}/api/annotations/update` && req.method === "POST") {
6740
8179
  if (!hasJsonBody(req)) {
6741
- 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." });
6742
8181
  }
6743
8182
  let body = "";
6744
8183
  req.on("data", (chunk) => body += chunk);
6745
8184
  return void req.on("end", async () => {
6746
8185
  const parsed = jsonBody(body);
6747
8186
  if (parsed === null) {
6748
- return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
8187
+ return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
6749
8188
  }
6750
8189
  if (typeof parsed.id !== "string" || parsed.id === "") {
6751
- 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." });
6752
8191
  }
6753
8192
  if (typeof parsed.note !== "string") {
6754
- 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." });
6755
8194
  }
6756
8195
  try {
6757
8196
  const annotation = await updateAnnotation(cwd, parsed.id, parsed.note);
6758
8197
  if (annotation === null) {
6759
- return sendJson(res, 404, { ok: false, error: "That note has gone." });
8198
+ return sendJson2(res, 404, { ok: false, error: "That note has gone." });
6760
8199
  }
6761
- return sendJson(res, 200, { ok: true, annotation });
8200
+ return sendJson2(res, 200, { ok: true, annotation });
6762
8201
  } catch {
6763
- 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." });
6764
8203
  }
6765
8204
  });
6766
8205
  }
6767
8206
  if (path === `${LEGLAS_PREFIX}/api/annotations/delete` && req.method === "POST") {
6768
8207
  if (!hasJsonBody(req)) {
6769
- return sendJson(res, 400, { ok: false, error: "Delete must be JSON." });
8208
+ return sendJson2(res, 400, { ok: false, error: "Delete must be JSON." });
6770
8209
  }
6771
8210
  let body = "";
6772
8211
  req.on("data", (chunk) => body += chunk);
6773
8212
  return void req.on("end", async () => {
6774
8213
  const parsed = jsonBody(body);
6775
8214
  if (parsed === null) {
6776
- return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
8215
+ return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
6777
8216
  }
6778
8217
  const ids = Array.isArray(parsed.ids) ? parsed.ids.filter((entry) => typeof entry === "string") : [];
6779
8218
  if (ids.length === 0) {
6780
- 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." });
6781
8220
  }
6782
8221
  try {
6783
- return sendJson(res, 200, { ok: true, deleted: await removeAnnotations(cwd, ids) });
8222
+ return sendJson2(res, 200, { ok: true, deleted: await removeAnnotations(cwd, ids) });
6784
8223
  } catch {
6785
- 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." });
6786
8225
  }
6787
8226
  });
6788
8227
  }
6789
8228
  if (path === `${LEGLAS_PREFIX}/api/requests/dismiss` && req.method === "POST") {
6790
8229
  if (!hasJsonBody(req)) {
6791
- return sendJson(res, 400, { ok: false, error: "Dismiss must be JSON." });
8230
+ return sendJson2(res, 400, { ok: false, error: "Dismiss must be JSON." });
6792
8231
  }
6793
8232
  let body = "";
6794
8233
  req.on("data", (chunk) => body += chunk);
6795
8234
  return void req.on("end", async () => {
6796
8235
  const parsed = jsonBody(body);
6797
8236
  if (parsed === null) {
6798
- return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
8237
+ return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
6799
8238
  }
6800
8239
  if (typeof parsed.id !== "string") {
6801
- 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." });
6802
8241
  }
6803
8242
  const target2 = (await readRequests(cwd)).find((entry) => entry.id === parsed.id);
6804
8243
  if (target2 === void 0 || !isEnded(target2, runner?.snapshot().failedIds ?? [])) {
6805
- 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." });
6806
8245
  }
6807
8246
  try {
6808
8247
  if (!await removeRequest(cwd, parsed.id)) {
6809
- return sendJson(res, 404, { ok: false, error: "No such request." });
8248
+ return sendJson2(res, 404, { ok: false, error: "No such request." });
6810
8249
  }
6811
- return sendJson(res, 200, { ok: true });
8250
+ return sendJson2(res, 200, { ok: true });
6812
8251
  } catch {
6813
- 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." });
6814
8253
  }
6815
8254
  });
6816
8255
  }
@@ -6820,13 +8259,13 @@ async function startServer(options) {
6820
8259
  return void req.on("end", () => {
6821
8260
  const parsed = jsonBody(body);
6822
8261
  if (parsed === null) {
6823
- return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
8262
+ return sendJson2(res, 400, { ok: false, error: "Body must be JSON." });
6824
8263
  }
6825
8264
  if (parsed.renames === null || typeof parsed.renames !== "object") {
6826
- 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." });
6827
8266
  }
6828
8267
  const renames = Object.fromEntries(Object.entries(parsed.renames).filter((entry) => typeof entry[1] === "string" && entry[1] !== ""));
6829
- 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 }));
6830
8269
  });
6831
8270
  }
6832
8271
  if (path === `${LEGLAS_PREFIX}/api/health`) {
@@ -6843,42 +8282,86 @@ async function startServer(options) {
6843
8282
  relative6 = "";
6844
8283
  }
6845
8284
  const dir = fileMounts.get(slug);
6846
- if (dir !== void 0 && relative6 !== "" && serveFrom(res, dir, relative6))
6847
- return;
6848
- res.writeHead(404, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
6849
- 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
+ });
6850
8301
  }
6851
8302
  if (path.startsWith(`${LEGLAS_PREFIX}/api/`)) {
6852
- return sendJson(res, 404, { error: "No such Leglas API path." });
8303
+ return sendJson2(res, 404, { error: "No such Leglas API path." });
6853
8304
  }
6854
8305
  if (path === LEGLAS_PREFIX || path.startsWith(`${LEGLAS_PREFIX}/`)) {
6855
8306
  if (shellDir !== null && serveShellFile(res, shellDir, path))
6856
8307
  return;
6857
8308
  if (shellDir !== null) {
6858
8309
  res.writeHead(404, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
6859
- return res.end("Leglas: no such path.");
8310
+ res.end("Leglas: no such path.");
8311
+ return;
6860
8312
  }
6861
8313
  res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
6862
- return res.end(PLACEHOLDER);
8314
+ res.end(PLACEHOLDER);
8315
+ return;
6863
8316
  }
6864
- return proxy.request(req, res, `http://localhost:${port}`);
6865
- });
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
+ }));
6866
8324
  const sockets = /* @__PURE__ */ new Set();
6867
8325
  server.on("connection", (socket) => {
6868
8326
  sockets.add(socket);
6869
8327
  socket.once("close", () => sockets.delete(socket));
6870
8328
  });
6871
- server.on("upgrade", (req, socket, head) => {
6872
- if (live.upgrade(req, socket, head))
6873
- 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;
6874
8333
  const path = (req.url ?? "/").split("?")[0] ?? "/";
6875
- if (path.startsWith(`${LEGLAS_PREFIX}/`))
6876
- return socket.destroy();
8334
+ if (path.startsWith(`${LEGLAS_PREFIX}/`)) {
8335
+ socket.destroy();
8336
+ return false;
8337
+ }
6877
8338
  proxy.upgrade(req, socket, head);
8339
+ return false;
8340
+ };
8341
+ server.on("upgrade", (req, socket, head) => {
8342
+ handleUpgrade(req, socket, head, { remote: false });
6878
8343
  });
6879
- const port = await bind(server, options.port ?? DEFAULT_PORT);
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 }
8361
+ });
8362
+ port = await bind2(server, options.port ?? DEFAULT_PORT);
6880
8363
  const liveFiles = watchLiveFiles(cwd, bootConfigPath, live);
6881
- const liveHealth = watchHealth(target, live);
8364
+ liveHealth = watchHealth(target, live);
6882
8365
  await pruneCaptures(cwd, (await readRequests(cwd).catch(() => [])).map((request) => request.id)).catch(() => {
6883
8366
  });
6884
8367
  await writeServerInfo(cwd, {
@@ -6902,21 +8385,26 @@ async function startServer(options) {
6902
8385
  close: () => {
6903
8386
  if (closePromise !== null)
6904
8387
  return closePromise;
6905
- liveFiles.close();
6906
- liveHealth.close();
6907
- closePromise = Promise.all([
6908
- branches.stop(),
6909
- runner.stop(),
6910
- browserPool.close(),
6911
- live.close()
6912
- ]).then(() => new Promise((done) => {
6913
- for (const socket of sockets)
6914
- socket.destroy();
6915
- sockets.clear();
6916
- server.closeAllConnections();
6917
- server.close(() => done());
6918
- })).then(() => removeServerInfo(cwd, { port, pid: process.pid }).catch(() => {
6919
- }));
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
+ })();
6920
8408
  return closePromise;
6921
8409
  }
6922
8410
  };