leglas 0.8.0 → 1.0.0

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