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