dsh-claude-slides 2.7.2 → 2.8.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.
@@ -1,14 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  // claude-slides: local editor server (prebuilt; do not edit).
3
3
 
4
- import fs12 from "node:fs";
5
- import path12 from "node:path";
4
+ import fs13 from "node:fs";
5
+ import path13 from "node:path";
6
6
 
7
- import { createHash as createHash3, randomBytes as randomBytes2, randomUUID } from "node:crypto";
8
- import fs11 from "node:fs";
7
+ import { createHash as createHash5, randomBytes as randomBytes2, randomUUID } from "node:crypto";
8
+ import fs12 from "node:fs";
9
9
  import fsp4 from "node:fs/promises";
10
- import http from "node:http";
11
- import path11 from "node:path";
10
+ import path12 from "node:path";
12
11
  import { fileURLToPath, pathToFileURL } from "node:url";
13
12
 
14
13
  import { spawn } from "node:child_process";
@@ -768,6 +767,7 @@ data: ${JSON.stringify(snap)}
768
767
  return { handle, presence, deliver, sendRequest, requestStates, close };
769
768
  }
770
769
 
770
+ import { createHash } from "node:crypto";
771
771
  import fs3 from "node:fs";
772
772
  import net from "node:net";
773
773
  import os2 from "node:os";
@@ -800,17 +800,15 @@ async function runningServer(root, kind, probe) {
800
800
  return null;
801
801
  }
802
802
  }
803
- function recordServer(state, close, signals = ["SIGINT", "SIGTERM"]) {
804
- const file = path2.join(state.root, STATE_FILE);
805
- fs3.writeFileSync(file, `${JSON.stringify(state, null, 2)}
803
+ function writeState(state) {
804
+ fs3.writeFileSync(path2.join(state.root, STATE_FILE), `${JSON.stringify(state, null, 2)}
806
805
  `);
807
806
  writeGitignore(state.root, state.kind);
808
- const shutdown = () => {
809
- if (readState(state.root)?.pid === process.pid) fs3.rmSync(file, { force: true });
810
- close();
811
- process.exit(0);
812
- };
813
- for (const signal of signals) process.on(signal, shutdown);
807
+ }
808
+ function removeState(root) {
809
+ const state = readState(root);
810
+ if (state && state.pid !== process.pid && alive(state.pid)) return;
811
+ fs3.rmSync(path2.join(root, STATE_FILE), { force: true });
814
812
  }
815
813
  var GITIGNORE_FILE = ".gitignore";
816
814
  var gitignoreLines = (kind) => [STATE_FILE, LOG_FILE, ".sharing.json", ...kind === "docs" ? [] : ["history/"], ".*.tmp", ".DS_Store"];
@@ -909,6 +907,7 @@ function serverOrigin(host, port) {
909
907
  const shown2 = host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host.includes(":") ? `[${host}]` : host;
910
908
  return `http://${shown2}:${port}`;
911
909
  }
910
+ var pagePathOf = (kind, root) => `/${kind}/${createHash("sha256").update(path2.resolve(root)).digest("base64url").slice(0, 22)}/`;
912
911
  function sendJson2(response, status, body, headers = {}) {
913
912
  const text = JSON.stringify(body);
914
913
  response.writeHead(status, {
@@ -1496,8 +1495,764 @@ ${sections || '<p class="muted">This copy has no tokens, components or guide.</p
1496
1495
  `;
1497
1496
  }
1498
1497
 
1499
- import fs5 from "node:fs";
1500
- import path5 from "node:path";
1498
+ import { EventEmitter } from "node:events";
1499
+ import fs6 from "node:fs";
1500
+ import http from "node:http";
1501
+ import os3 from "node:os";
1502
+ import path6 from "node:path";
1503
+
1504
+ import crypto from "node:crypto";
1505
+ import fs5 from "node:fs";
1506
+ import path5 from "node:path";
1507
+ var GUEST_ROLES = ["editor", "commenter", "viewer"];
1508
+ var isGuestRole = (value) => typeof value === "string" && GUEST_ROLES.includes(value);
1509
+ var SHARING_FILE = ".sharing.json";
1510
+ var MAX_GUESTS = 200;
1511
+ var MAX_NAME = 40;
1512
+ var token = (bytes = 18) => crypto.randomBytes(bytes).toString("base64url");
1513
+ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1514
+ function fresh() {
1515
+ return { lan: false, links: { editor: { token: token(), on: true }, commenter: { token: token(), on: true }, viewer: { token: token(), on: true } }, guests: [] };
1516
+ }
1517
+ function cleanName(raw) {
1518
+ const name = typeof raw === "string" ? raw.replace(/[\u0000-\u001f\u007f@<>]/g, "").replace(/\s+/g, " ").trim() : "";
1519
+ return [...name].slice(0, MAX_NAME).join("");
1520
+ }
1521
+ function guestUser(guest) {
1522
+ return { id: `guest-${guest.id}`, name: guest.name, email: `${guest.name}@${guest.id}.guest` };
1523
+ }
1524
+ function isLoopback(address) {
1525
+ if (!address) return false;
1526
+ const a = address.startsWith("::ffff:") ? address.slice(7) : address;
1527
+ return a === "::1" || a.startsWith("127.");
1528
+ }
1529
+ var Sharing = class _Sharing {
1530
+ file;
1531
+ state;
1532
+ constructor(root) {
1533
+ this.file = path5.join(root, SHARING_FILE);
1534
+ this.state = _Sharing.read(this.file);
1535
+ }
1536
+ static read(file) {
1537
+ let raw;
1538
+ try {
1539
+ raw = JSON.parse(fs5.readFileSync(file, "utf8"));
1540
+ } catch {
1541
+ return fresh();
1542
+ }
1543
+ const state = fresh();
1544
+ if (!isRecord2(raw)) return state;
1545
+ state.lan = raw.lan === true;
1546
+ if (typeof raw.port === "number" && Number.isInteger(raw.port) && raw.port > 0 && raw.port < 65536) state.port = raw.port;
1547
+ if (isRecord2(raw.links)) {
1548
+ for (const role of GUEST_ROLES) {
1549
+ const link = raw.links[role];
1550
+ if (isRecord2(link) && typeof link.token === "string" && link.token.length >= 16) state.links[role] = { token: link.token, on: link.on !== false };
1551
+ }
1552
+ }
1553
+ if (Array.isArray(raw.guests)) {
1554
+ for (const g of raw.guests) {
1555
+ if (!isRecord2(g) || typeof g.id !== "string" || typeof g.secret !== "string" || !isGuestRole(g.role) || typeof g.link !== "string") continue;
1556
+ state.guests.push({
1557
+ id: g.id,
1558
+ secret: g.secret,
1559
+ name: cleanName(g.name) || "Guest",
1560
+ role: g.role,
1561
+ link: g.link,
1562
+ joined: typeof g.joined === "string" ? g.joined : (/* @__PURE__ */ new Date(0)).toISOString(),
1563
+ ...typeof g.seen === "string" ? { seen: g.seen } : {}
1564
+ });
1565
+ }
1566
+ }
1567
+ return state;
1568
+ }
1569
+ save() {
1570
+ const tmp = `${this.file}.${process.pid}.tmp`;
1571
+ fs5.writeFileSync(tmp, `${JSON.stringify(this.state, null, 2)}
1572
+ `, { mode: 384 });
1573
+ fs5.renameSync(tmp, this.file);
1574
+ }
1575
+ /** The role whose link has this token, while that link is on. */
1576
+ linkRole(tokenGiven) {
1577
+ for (const role of GUEST_ROLES) {
1578
+ const link = this.state.links[role];
1579
+ if (link.on && tokenGiven.length === link.token.length && crypto.timingSafeEqual(Buffer.from(tokenGiven), Buffer.from(link.token))) return role;
1580
+ }
1581
+ return null;
1582
+ }
1583
+ /** The guest a cookie names, if they still have access (their link on and not reset). */
1584
+ guest(secret) {
1585
+ if (!secret) return null;
1586
+ const guest = this.state.guests.find((g) => g.secret === secret);
1587
+ return guest && this.valid(guest) ? guest : null;
1588
+ }
1589
+ valid(guest) {
1590
+ const link = this.state.links[guest.role];
1591
+ return link.on && link.token === guest.link && this.state.guests.includes(guest);
1592
+ }
1593
+ join(role, name) {
1594
+ const guest = { id: token(4), secret: token(24), name: cleanName(name) || "Guest", role, link: this.state.links[role].token, joined: (/* @__PURE__ */ new Date()).toISOString() };
1595
+ this.state.guests.push(guest);
1596
+ if (this.state.guests.length > MAX_GUESTS) this.state.guests.splice(0, this.state.guests.length - MAX_GUESTS);
1597
+ this.save();
1598
+ return guest;
1599
+ }
1600
+ /** Marks a guest seen now (saved at most once a minute per guest). */
1601
+ seen(guest) {
1602
+ const now = /* @__PURE__ */ new Date();
1603
+ if (guest.seen && now.getTime() - Date.parse(guest.seen) < 6e4) return;
1604
+ guest.seen = now.toISOString();
1605
+ this.save();
1606
+ }
1607
+ setLan(on, port) {
1608
+ this.state.lan = on;
1609
+ if (on && port) this.state.port = port;
1610
+ this.save();
1611
+ }
1612
+ setLink(role, on) {
1613
+ this.state.links[role].on = on;
1614
+ this.save();
1615
+ }
1616
+ /** A new token for the link: the old link stops working and the guests who came through it are dropped. */
1617
+ resetLink(role) {
1618
+ const old = this.state.links[role].token;
1619
+ this.state.links[role] = { token: token(), on: this.state.links[role].on };
1620
+ this.state.guests = this.state.guests.filter((g) => g.link !== old);
1621
+ this.save();
1622
+ }
1623
+ /**
1624
+ * Gives a guest another role. They now hang on that role's link (turning it off or resetting it drops them), so a role
1625
+ * whose link is off would shut them out: false then, as for a guest who isn't there.
1626
+ */
1627
+ setRole(id, role) {
1628
+ const guest = this.state.guests.find((g) => g.id === id);
1629
+ if (!guest || !this.state.links[role].on) return false;
1630
+ guest.role = role;
1631
+ guest.link = this.state.links[role].token;
1632
+ this.save();
1633
+ return true;
1634
+ }
1635
+ removeGuest(id) {
1636
+ const before = this.state.guests.length;
1637
+ this.state.guests = this.state.guests.filter((g) => g.id !== id);
1638
+ if (this.state.guests.length === before) return false;
1639
+ this.save();
1640
+ return true;
1641
+ }
1642
+ };
1643
+
1644
+ import { createHash as createHash2 } from "node:crypto";
1645
+ var GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
1646
+ var MAX_MESSAGE = 64 * 1024 * 1024;
1647
+ var WebSocketConnection = class {
1648
+ socket;
1649
+ buffer = Buffer.alloc(0);
1650
+ parts = [];
1651
+ partOpcode = 0;
1652
+ closed = false;
1653
+ handlers = {};
1654
+ constructor(socket, head) {
1655
+ this.socket = socket;
1656
+ socket.on("data", (chunk) => this.receive(chunk));
1657
+ socket.on("close", () => this.finish());
1658
+ socket.on("error", () => this.finish());
1659
+ if (head.length) this.receive(head);
1660
+ }
1661
+ get open() {
1662
+ return !this.closed;
1663
+ }
1664
+ sendText(text) {
1665
+ this.send(1, Buffer.from(text, "utf8"));
1666
+ }
1667
+ sendBinary(data) {
1668
+ this.send(2, data);
1669
+ }
1670
+ close(code = 1e3, reason = "") {
1671
+ if (this.closed) return;
1672
+ const body = Buffer.alloc(2 + Buffer.byteLength(reason));
1673
+ body.writeUInt16BE(code, 0);
1674
+ body.write(reason, 2);
1675
+ this.send(8, body);
1676
+ this.socket.end();
1677
+ this.finish();
1678
+ }
1679
+ finish() {
1680
+ if (this.closed) return;
1681
+ this.closed = true;
1682
+ this.socket.destroy();
1683
+ this.handlers.close?.();
1684
+ }
1685
+ send(opcode, payload) {
1686
+ if (this.closed) return;
1687
+ const length = payload.length;
1688
+ let header;
1689
+ if (length < 126) {
1690
+ header = Buffer.from([128 | opcode, length]);
1691
+ } else if (length < 65536) {
1692
+ header = Buffer.alloc(4);
1693
+ header[0] = 128 | opcode;
1694
+ header[1] = 126;
1695
+ header.writeUInt16BE(length, 2);
1696
+ } else {
1697
+ header = Buffer.alloc(10);
1698
+ header[0] = 128 | opcode;
1699
+ header[1] = 127;
1700
+ header.writeBigUInt64BE(BigInt(length), 2);
1701
+ }
1702
+ this.socket.write(Buffer.concat([header, payload]));
1703
+ }
1704
+ receive(chunk) {
1705
+ this.buffer = this.buffer.length ? Buffer.concat([this.buffer, chunk]) : chunk;
1706
+ for (; ; ) {
1707
+ if (this.buffer.length < 2) return;
1708
+ const first = this.buffer[0];
1709
+ const second = this.buffer[1];
1710
+ const fin = (first & 128) !== 0;
1711
+ const opcode = first & 15;
1712
+ const masked = (second & 128) !== 0;
1713
+ let length = second & 127;
1714
+ let offset = 2;
1715
+ if (length === 126) {
1716
+ if (this.buffer.length < 4) return;
1717
+ length = this.buffer.readUInt16BE(2);
1718
+ offset = 4;
1719
+ } else if (length === 127) {
1720
+ if (this.buffer.length < 10) return;
1721
+ const big = this.buffer.readBigUInt64BE(2);
1722
+ if (big > BigInt(MAX_MESSAGE)) return this.close(1009, "message too big");
1723
+ length = Number(big);
1724
+ offset = 10;
1725
+ }
1726
+ const maskLength = masked ? 4 : 0;
1727
+ if (this.buffer.length < offset + maskLength + length) return;
1728
+ const mask = masked ? this.buffer.subarray(offset, offset + 4) : null;
1729
+ const payload = Buffer.from(this.buffer.subarray(offset + maskLength, offset + maskLength + length));
1730
+ if (mask) for (let index = 0; index < payload.length; index++) payload[index] = payload[index] ^ mask[index & 3];
1731
+ this.buffer = this.buffer.subarray(offset + maskLength + length);
1732
+ this.frame(fin, opcode, payload);
1733
+ if (this.closed) return;
1734
+ }
1735
+ }
1736
+ frame(fin, opcode, payload) {
1737
+ switch (opcode) {
1738
+ case 8:
1739
+ this.close(payload.length >= 2 ? payload.readUInt16BE(0) : 1e3);
1740
+ return;
1741
+ case 9:
1742
+ this.send(10, payload);
1743
+ return;
1744
+ case 10:
1745
+ return;
1746
+ case 0:
1747
+ case 1:
1748
+ case 2: {
1749
+ if (opcode !== 0) {
1750
+ this.parts = [];
1751
+ this.partOpcode = opcode;
1752
+ }
1753
+ this.parts.push(payload);
1754
+ if (this.parts.reduce((total, part) => total + part.length, 0) > MAX_MESSAGE) return this.close(1009, "message too big");
1755
+ if (!fin) return;
1756
+ const message = Buffer.concat(this.parts);
1757
+ this.parts = [];
1758
+ if (this.partOpcode === 1) this.handlers.text?.(message.toString("utf8"));
1759
+ else this.handlers.binary?.(message);
1760
+ return;
1761
+ }
1762
+ default:
1763
+ this.close(1002, "unknown opcode");
1764
+ }
1765
+ }
1766
+ };
1767
+ function acceptWebSocket(request, socket, head, protocol) {
1768
+ const key = request.headers["sec-websocket-key"];
1769
+ if (typeof key !== "string" || String(request.headers.upgrade).toLowerCase() !== "websocket") {
1770
+ socket.end("HTTP/1.1 400 Bad Request\r\n\r\n");
1771
+ return null;
1772
+ }
1773
+ const accept = createHash2("sha1").update(key + GUID).digest("base64");
1774
+ socket.write(["HTTP/1.1 101 Switching Protocols", "Upgrade: websocket", "Connection: Upgrade", `Sec-WebSocket-Accept: ${accept}`, ...protocol ? [`Sec-WebSocket-Protocol: ${protocol}`] : [], "", ""].join("\r\n"));
1775
+ return new WebSocketConnection(socket, head);
1776
+ }
1777
+
1778
+ var HUB_ROUTE = "/__hub";
1779
+ var STREAM_PROTOCOL = "baoyu-sse";
1780
+ var STREAM_REFUSED = 4e3;
1781
+ function buildOf(script2) {
1782
+ const real = fs6.realpathSync(script2);
1783
+ const { mtimeMs, size } = fs6.statSync(real);
1784
+ return `${real}:${mtimeMs}:${size}`;
1785
+ }
1786
+ var SiteClash = class extends Error {
1787
+ };
1788
+ var StreamResponse = class extends EventEmitter {
1789
+ status = 200;
1790
+ headersSent = false;
1791
+ writableEnded = false;
1792
+ destroyed = false;
1793
+ ws;
1794
+ constructor(ws) {
1795
+ super();
1796
+ this.ws = ws;
1797
+ }
1798
+ writeHead(status) {
1799
+ this.status = status;
1800
+ this.headersSent = true;
1801
+ return this;
1802
+ }
1803
+ setHeader() {
1804
+ return this;
1805
+ }
1806
+ flushHeaders() {
1807
+ }
1808
+ write(chunk) {
1809
+ if (this.status === 200 && !this.writableEnded) this.ws.sendText(String(chunk));
1810
+ return true;
1811
+ }
1812
+ end(chunk) {
1813
+ if (this.writableEnded) return this;
1814
+ if (chunk !== void 0) this.write(chunk);
1815
+ this.writableEnded = true;
1816
+ this.ws.close(this.status === 200 ? 1e3 : STREAM_REFUSED + Math.min(this.status, 999));
1817
+ return this;
1818
+ }
1819
+ destroy() {
1820
+ this.destroyed = true;
1821
+ return this.end();
1822
+ }
1823
+ closed() {
1824
+ this.writableEnded = true;
1825
+ this.emit("close");
1826
+ }
1827
+ };
1828
+ var Hub = class {
1829
+ port = 0;
1830
+ kind;
1831
+ host;
1832
+ build;
1833
+ factory;
1834
+ /** Sites' logs go to its stderr too: it runs in someone's terminal, not in the background. */
1835
+ echo;
1836
+ server;
1837
+ /** page → site. */
1838
+ sites = /* @__PURE__ */ new Map();
1839
+ adding = /* @__PURE__ */ new Map();
1840
+ startedAt = /* @__PURE__ */ new Map();
1841
+ hosts = /* @__PURE__ */ new Set();
1842
+ /** One listener per LAN address while any site is open to the network. */
1843
+ lanServers = /* @__PURE__ */ new Map();
1844
+ network;
1845
+ constructor(options) {
1846
+ this.echo = options.echo;
1847
+ this.kind = options.kind;
1848
+ this.build = options.build;
1849
+ this.host = options.host;
1850
+ this.factory = options.site;
1851
+ this.server = this.createServer();
1852
+ const hub = this;
1853
+ this.network = {
1854
+ get port() {
1855
+ return hub.port;
1856
+ },
1857
+ addresses: () => [...this.lanServers.keys()],
1858
+ open: () => this.openLan(),
1859
+ release: () => this.releaseLan()
1860
+ };
1861
+ }
1862
+ createServer() {
1863
+ const server2 = http.createServer((request, response) => {
1864
+ this.onRequest(request, response).catch((error) => {
1865
+ process.stderr.write(`[${(/* @__PURE__ */ new Date()).toISOString()}] request failed: ${error instanceof Error ? error.stack ?? error.message : String(error)}
1866
+ `);
1867
+ if (!response.headersSent) sendJson2(response, 500, { code: "internal", message: "internal error" });
1868
+ else response.destroy();
1869
+ });
1870
+ });
1871
+ server2.on("upgrade", (request, socket, head) => this.onUpgrade(request, socket, head));
1872
+ return server2;
1873
+ }
1874
+ /** Listens at the first free port from `port`; `first` is tried before them. */
1875
+ async listen(port, first) {
1876
+ this.port = await listenFree(this.server, port, this.host, first);
1877
+ const named = this.host === "0.0.0.0" || this.host === "::" ? [] : [this.host.includes(":") ? `[${this.host}]:${this.port}` : `${this.host}:${this.port}`];
1878
+ this.hosts = /* @__PURE__ */ new Set([`127.0.0.1:${this.port}`, `localhost:${this.port}`, `[::1]:${this.port}`, ...named]);
1879
+ return this.port;
1880
+ }
1881
+ /** Ends every site and the listeners; what the folders' state files say goes with them. */
1882
+ close() {
1883
+ for (const site of [...this.sites.values()]) this.drop(site);
1884
+ this.closeLan();
1885
+ this.server.close();
1886
+ this.server.closeAllConnections?.();
1887
+ }
1888
+ /** On a signal (or a stray error, logged): every site's record goes and the process exits. */
1889
+ ownProcess() {
1890
+ const shutdown = () => {
1891
+ this.close();
1892
+ process.exit(0);
1893
+ };
1894
+ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) process.on(signal, shutdown);
1895
+ const stray = (error) => process.stderr.write(`[${(/* @__PURE__ */ new Date()).toISOString()}] uncaught: ${error instanceof Error ? error.stack ?? error.message : String(error)}
1896
+ `);
1897
+ process.on("uncaughtException", stray);
1898
+ process.on("unhandledRejection", stray);
1899
+ }
1900
+ // ---- the sites -------------------------------------------------------------------------------------------------
1901
+ urlOf(site) {
1902
+ return `${serverOrigin(this.host, this.port)}${site.page}`;
1903
+ }
1904
+ stateOf(site) {
1905
+ const lan = site.share.lanUrls();
1906
+ return { pid: process.pid, url: this.urlOf(site), port: this.port, root: site.root, kind: this.kind, startedAt: this.startedAt.get(site.page) ?? (/* @__PURE__ */ new Date()).toISOString(), ...lan.length ? { lan } : {} };
1907
+ }
1908
+ /** Mounts a folder (already mounted: the same site), writing its <root>/.artifact-server.json. */
1909
+ add(root, options) {
1910
+ const mounted = [...this.sites.values()].find((site) => site.root === root);
1911
+ if (mounted) return (options.lan && !mounted.share.shared ? mounted.share.setLan(true) : Promise.resolve()).then(() => this.stateOf(mounted));
1912
+ const adding = this.adding.get(root);
1913
+ if (adding) return adding;
1914
+ const work = this.mount(root, options).finally(() => this.adding.delete(root));
1915
+ this.adding.set(root, work);
1916
+ return work;
1917
+ }
1918
+ async mount(root, { agentHook, lan = false }) {
1919
+ const file = path6.join(root, LOG_FILE);
1920
+ const log = (line) => {
1921
+ const text = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${line}
1922
+ `;
1923
+ try {
1924
+ fs6.appendFileSync(file, text);
1925
+ } catch {
1926
+ }
1927
+ if (this.echo) process.stderr.write(text);
1928
+ };
1929
+ const site = await this.factory(root, { log, network: this.network, ...agentHook ? { agentHook } : {} });
1930
+ const other = this.sites.get(site.page);
1931
+ if (other) {
1932
+ site.close();
1933
+ throw new SiteClash(`${other.root} is already open at ${site.page}.`);
1934
+ }
1935
+ this.sites.set(site.page, site);
1936
+ this.startedAt.set(site.page, (/* @__PURE__ */ new Date()).toISOString());
1937
+ try {
1938
+ await site.share.start(lan);
1939
+ } catch (error) {
1940
+ this.drop(site);
1941
+ throw error;
1942
+ }
1943
+ const state = this.stateOf(site);
1944
+ const { lan: _lan, ...record } = state;
1945
+ writeState(record);
1946
+ log(`serving ${root} at ${state.url}${agentHook ? `; each comment sent to the agent runs: ${agentHook}` : ""}`);
1947
+ return state;
1948
+ }
1949
+ drop(site) {
1950
+ this.sites.delete(site.page);
1951
+ this.startedAt.delete(site.page);
1952
+ removeState(site.root);
1953
+ site.close();
1954
+ }
1955
+ /** Ends one site; true when one was mounted there. The process ends with its last site. */
1956
+ remove(root) {
1957
+ const site = [...this.sites.values()].find((candidate) => candidate.root === root);
1958
+ if (site) this.drop(site);
1959
+ return site !== void 0;
1960
+ }
1961
+ // ---- the network side ------------------------------------------------------------------------------------------
1962
+ async openLan() {
1963
+ for (const address of lanAddresses()) {
1964
+ if (this.lanServers.has(address)) continue;
1965
+ const server2 = this.createServer();
1966
+ try {
1967
+ await listen(server2, this.port, address);
1968
+ this.lanServers.set(address, server2);
1969
+ } catch (error) {
1970
+ server2.close();
1971
+ process.stderr.write(`[${(/* @__PURE__ */ new Date()).toISOString()}] not listening on ${address}:${this.port}: ${errorText(error)}
1972
+ `);
1973
+ }
1974
+ }
1975
+ return [...this.lanServers.keys()];
1976
+ }
1977
+ /** A site stopped sharing: the listeners stay while another is shared. */
1978
+ releaseLan() {
1979
+ if (![...this.sites.values()].some((site) => site.share.shared)) this.closeLan();
1980
+ }
1981
+ closeLan() {
1982
+ for (const server2 of this.lanServers.values()) {
1983
+ server2.close();
1984
+ server2.closeAllConnections?.();
1985
+ }
1986
+ this.lanServers.clear();
1987
+ }
1988
+ /** A Host header this hub answers: its own names, and while something is shared, its LAN addresses and machine name. */
1989
+ hostAllowed(request) {
1990
+ const host = request.headers.host ?? "";
1991
+ if (this.hosts.has(host)) return true;
1992
+ if (!this.lanServers.size) return false;
1993
+ const suffix = `:${this.port}`;
1994
+ if (!host.endsWith(suffix)) return false;
1995
+ const name = host.slice(0, -suffix.length).toLowerCase();
1996
+ const machine = os3.hostname().toLowerCase();
1997
+ return this.lanServers.has(name) || name === machine || name === `${machine.replace(/\.local$/, "")}.local`;
1998
+ }
1999
+ // ---- requests --------------------------------------------------------------------------------------------------
2000
+ /** The site whose address `pathname` is under, with the path below it. */
2001
+ under(pathname) {
2002
+ const [, first, second] = pathname.split("/");
2003
+ const site = first && second ? this.sites.get(`/${first}/${second}/`) : void 0;
2004
+ if (site) return { site, rest: pathname.slice(site.page.length - 1) };
2005
+ for (const candidate of this.sites.values()) if (candidate.claims?.(pathname)) return { site: candidate, rest: pathname };
2006
+ return null;
2007
+ }
2008
+ /** The site a page at this request's Referer belongs to. */
2009
+ referred(request) {
2010
+ const referer = request.headers.referer;
2011
+ if (typeof referer !== "string") return null;
2012
+ try {
2013
+ const at = new URL(referer);
2014
+ return at.host === request.headers.host ? this.under(at.pathname)?.site ?? null : null;
2015
+ } catch {
2016
+ return null;
2017
+ }
2018
+ }
2019
+ /** The sites this request's person may see. */
2020
+ seenBy(request) {
2021
+ return [...this.sites.values()].filter((site) => site.share.viewerOf(request));
2022
+ }
2023
+ async onRequest(request, response) {
2024
+ if (!this.hostAllowed(request)) {
2025
+ request.resume();
2026
+ response.writeHead(421, { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store" }).end("Misdirected request");
2027
+ return;
2028
+ }
2029
+ const url = new URL(request.url ?? "/", "http://local");
2030
+ const pathname = url.pathname;
2031
+ if (pathname === HUB_ROUTE) return this.control(request, response);
2032
+ if (pathname === "/favicon.ico") {
2033
+ response.writeHead(204).end();
2034
+ return;
2035
+ }
2036
+ if (pathname === "/" || pathname === "/index.html") return this.landing(request, response, url.search);
2037
+ const at = this.under(pathname);
2038
+ if (at) {
2039
+ if (at.rest === "") {
2040
+ request.resume();
2041
+ response.writeHead(308, { Location: `${pathname}/${url.search}`, "Cache-Control": "no-store" }).end();
2042
+ return;
2043
+ }
2044
+ request.url = `${at.rest}${url.search}`;
2045
+ return at.site.handle(request, response);
2046
+ }
2047
+ const site = await this.answering(request, pathname);
2048
+ if (site) return site.handle(request, response);
2049
+ request.resume();
2050
+ response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store" }).end("Not found");
2051
+ }
2052
+ /** The site that answers a path outside every site's address. */
2053
+ async answering(request, pathname) {
2054
+ const link = /^\/s\/([A-Za-z0-9_-]{16,64})$/.exec(pathname);
2055
+ if (link) return [...this.sites.values()].find((site) => site.share.sharing.linkRole(link[1])) ?? null;
2056
+ const blob = /^\/_blob\/([0-9a-f]{32})$/.exec(pathname);
2057
+ if (blob) {
2058
+ const referred = this.referred(request);
2059
+ const seen = this.seenBy(request);
2060
+ for (const site of referred && seen.includes(referred) ? [referred, ...seen] : seen) if (await site.hasBlob?.(blob[1])) return site;
2061
+ return null;
2062
+ }
2063
+ for (const site of this.sites.values()) if (site.common?.(pathname)) return site;
2064
+ return this.referred(request);
2065
+ }
2066
+ /** `/`: where this person goes. */
2067
+ landing(request, response, search) {
2068
+ const seen = this.seenBy(request);
2069
+ const only = seen[0];
2070
+ if (seen.length === 1 && only) {
2071
+ request.resume();
2072
+ response.writeHead(302, { Location: `${only.page}${search}`, "Cache-Control": "no-store" }).end();
2073
+ return;
2074
+ }
2075
+ const any = only ?? [...this.sites.values()][0];
2076
+ if (!any) {
2077
+ request.resume();
2078
+ response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store" }).end("Nothing is open here.");
2079
+ return;
2080
+ }
2081
+ if (!only) return any.share.denied(request, response);
2082
+ any.share.sendList(response, seen.map((site) => ({ page: site.page, title: site.share.title() })));
2083
+ }
2084
+ /** The CLIs' route: over loopback, from a process (a browser's POST carries an Origin). */
2085
+ async control(request, response) {
2086
+ const info = { hub: 1, kind: this.kind, build: this.build, host: this.host, pid: process.pid };
2087
+ if (!isLoopback(request.socket.remoteAddress) || request.headers.origin !== void 0) {
2088
+ request.resume();
2089
+ return sendJson2(response, 403, { code: "not_granted", message: "Only a local process can do this." });
2090
+ }
2091
+ if (request.method === "GET") return sendJson2(response, 200, info);
2092
+ if (request.method !== "POST") {
2093
+ request.resume();
2094
+ return sendJson2(response, 405, { code: "method_not_allowed", message: "GET or POST." });
2095
+ }
2096
+ let body;
2097
+ try {
2098
+ body = JSON.parse((await readBody(request, 64 << 10)).toString("utf8"));
2099
+ } catch {
2100
+ return sendJson2(response, 400, { code: "invalid_argument", message: "The call is not JSON." });
2101
+ }
2102
+ if (typeof body.root !== "string" || !path6.isAbsolute(body.root)) return sendJson2(response, 400, { code: "invalid_argument", message: "root: an absolute path." });
2103
+ if (body.action === "mount") {
2104
+ try {
2105
+ const state = await this.add(body.root, { ...typeof body.agentHook === "string" && body.agentHook ? { agentHook: body.agentHook } : {}, lan: body.lan === true });
2106
+ return sendJson2(response, 200, { ...info, state });
2107
+ } catch (error) {
2108
+ sendJson2(response, error instanceof SiteClash ? 409 : 400, { ...info, code: error.code ?? "SERVER_FAILED", message: errorText(error) });
2109
+ return this.endIfEmpty();
2110
+ }
2111
+ }
2112
+ if (body.action === "unmount") {
2113
+ const removed = this.remove(body.root);
2114
+ const remaining = this.sites.size;
2115
+ sendJson2(response, 200, { ...info, removed, remaining });
2116
+ return this.endIfEmpty();
2117
+ }
2118
+ return sendJson2(response, 400, { code: "invalid_argument", message: "action: mount or unmount." });
2119
+ }
2120
+ /** The process ends with its last site. */
2121
+ endIfEmpty() {
2122
+ if (this.sites.size > 0 || this.adding.size > 0) return;
2123
+ this.close();
2124
+ setImmediate(() => process.exit(0));
2125
+ }
2126
+ onUpgrade(request, socket, head) {
2127
+ socket.on("error", () => {
2128
+ });
2129
+ const url = new URL(request.url ?? "/", "http://local");
2130
+ const at = this.hostAllowed(request) && this.originOk(request) ? this.under(url.pathname) : null;
2131
+ if (!at) {
2132
+ socket.end("HTTP/1.1 403 Forbidden\r\n\r\n");
2133
+ return;
2134
+ }
2135
+ request.url = `${at.rest}${url.search}`;
2136
+ const asked = String(request.headers["sec-websocket-protocol"] ?? "").split(",").map((p) => p.trim());
2137
+ if (asked.includes(STREAM_PROTOCOL)) return this.stream(at.site, request, socket, head, url);
2138
+ if (at.site.upgrade) return at.site.upgrade(request, socket, head);
2139
+ socket.end("HTTP/1.1 404 Not Found\r\n\r\n");
2140
+ }
2141
+ originOk(request) {
2142
+ const origin = request.headers.origin;
2143
+ if (origin === void 0) return true;
2144
+ try {
2145
+ return new URL(origin).host === request.headers.host;
2146
+ } catch {
2147
+ return false;
2148
+ }
2149
+ }
2150
+ /** An event stream over a WebSocket: the site's handler answers it as a GET. */
2151
+ stream(site, request, socket, head, url) {
2152
+ const ws = acceptWebSocket(request, socket, head, STREAM_PROTOCOL);
2153
+ if (!ws) return;
2154
+ const last = url.searchParams.get("__lastEventId");
2155
+ if (last !== null) {
2156
+ request.headers["last-event-id"] = last;
2157
+ url.searchParams.delete("__lastEventId");
2158
+ request.url = `${request.url?.split("?")[0]}${url.search}`;
2159
+ }
2160
+ request.method = "GET";
2161
+ const response = new StreamResponse(ws);
2162
+ ws.handlers = {
2163
+ close: () => {
2164
+ response.closed();
2165
+ request.emit("close");
2166
+ }
2167
+ };
2168
+ Promise.resolve(site.handle(request, response)).catch(() => ws.close(STREAM_REFUSED + 500));
2169
+ }
2170
+ };
2171
+ async function hubAt(origin) {
2172
+ try {
2173
+ const response = await fetch(`${origin}${HUB_ROUTE}`, { signal: AbortSignal.timeout(1e3) });
2174
+ if (!response.ok) return null;
2175
+ const info = await response.json();
2176
+ return info.hub === 1 ? info : null;
2177
+ } catch {
2178
+ return null;
2179
+ }
2180
+ }
2181
+ async function joinHub(call, below = Infinity) {
2182
+ if (call.port === 0) return null;
2183
+ const ports = call.portGiven ? [call.port] : Array.from({ length: PORT_TRIES }, (_, i) => call.port + i).filter((port) => port <= 65535 && port < below);
2184
+ const build = buildOf(call.script);
2185
+ const origins = ports.map((port) => serverOrigin(call.host, port));
2186
+ const infos = await Promise.all(origins.map(hubAt));
2187
+ for (const [i, info] of infos.entries()) {
2188
+ if (!info || info.kind !== call.kind || info.build !== build || info.host !== call.host) continue;
2189
+ const response = await fetch(`${origins[i]}${HUB_ROUTE}`, {
2190
+ method: "POST",
2191
+ headers: { "Content-Type": "application/json" },
2192
+ body: JSON.stringify({ action: "mount", root: call.root, ...call.agentHook ? { agentHook: call.agentHook } : {}, lan: call.lan === true }),
2193
+ signal: AbortSignal.timeout(2e4)
2194
+ }).catch(() => null);
2195
+ if (!response) continue;
2196
+ const body = await response.json().catch(() => ({}));
2197
+ if (response.ok && body.state) return body.state;
2198
+ if (response.status === 409) continue;
2199
+ throw Object.assign(new Error(body.message ?? `the server answered ${response.status}`), { code: body.code ?? "SERVER_FAILED" });
2200
+ }
2201
+ return null;
2202
+ }
2203
+ async function leaveHub(state) {
2204
+ let remaining = 0;
2205
+ const response = await fetch(`${new URL(state.url).origin}${HUB_ROUTE}`, {
2206
+ method: "POST",
2207
+ headers: { "Content-Type": "application/json" },
2208
+ body: JSON.stringify({ action: "unmount", root: state.root }),
2209
+ signal: AbortSignal.timeout(5e3)
2210
+ }).catch(() => null);
2211
+ const body = response ? await response.json().catch(() => null) : null;
2212
+ if (body?.hub === 1) remaining = Number(body.remaining ?? 0);
2213
+ else if (alive(state.pid)) process.kill(state.pid, "SIGTERM");
2214
+ const deadline = Date.now() + 5e3;
2215
+ while (remaining === 0 && alive(state.pid) && Date.now() < deadline) await new Promise((resolve) => setTimeout(resolve, 100));
2216
+ if (remaining === 0 && alive(state.pid)) return false;
2217
+ fs6.rmSync(path6.join(state.root, STATE_FILE), { force: true });
2218
+ return true;
2219
+ }
2220
+ function holdSite(state) {
2221
+ const leave = () => {
2222
+ void leaveHub(state).finally(() => process.exit(0));
2223
+ };
2224
+ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) process.on(signal, leave);
2225
+ setInterval(() => {
2226
+ if (!alive(state.pid) || readState(state.root)?.pid !== state.pid) process.exit(0);
2227
+ }, 2e3);
2228
+ }
2229
+ async function serveHub(call) {
2230
+ const joined = await joinHub(call);
2231
+ const stayed = (state) => {
2232
+ if (!call.background) holdSite(state);
2233
+ return { ...state, joined: true };
2234
+ };
2235
+ if (joined) return stayed(joined);
2236
+ const hub = new Hub({ kind: call.kind, build: buildOf(call.script), host: call.host, site: call.site, echo: !call.background });
2237
+ const { state: sharing } = new Sharing(call.root);
2238
+ const linksPort = !call.portGiven && (call.lan || sharing.lan) ? sharing.port : void 0;
2239
+ const port = await hub.listen(call.port, linksPort);
2240
+ const lower = call.portGiven ? null : await joinHub(call, port);
2241
+ if (lower) {
2242
+ hub.close();
2243
+ return stayed(lower);
2244
+ }
2245
+ hub.ownProcess();
2246
+ try {
2247
+ return { ...await hub.add(call.root, { ...call.agentHook ? { agentHook: call.agentHook } : {}, lan: call.lan === true }), joined: false };
2248
+ } catch (error) {
2249
+ hub.close();
2250
+ throw error;
2251
+ }
2252
+ }
2253
+
2254
+ import fs7 from "node:fs";
2255
+ import path7 from "node:path";
1501
2256
  var FOLDER2 = /^[a-z0-9][a-z0-9_-]{0,63}$/;
1502
2257
  var MAX_FILES = 2e3;
1503
2258
  var MAX_FILE_BYTES2 = 64 * 1024 * 1024;
@@ -1505,7 +2260,7 @@ var DESIGN_SYSTEM_FILE = "design-system.json";
1505
2260
  function readRecords(project, indexFile) {
1506
2261
  let index;
1507
2262
  try {
1508
- index = JSON.parse(fs5.readFileSync(path5.join(project, indexFile), "utf8"));
2263
+ index = JSON.parse(fs7.readFileSync(path7.join(project, indexFile), "utf8"));
1509
2264
  } catch {
1510
2265
  return [];
1511
2266
  }
@@ -1524,11 +2279,11 @@ function readRecords(project, indexFile) {
1524
2279
  return out;
1525
2280
  }
1526
2281
  function plain(p, want) {
1527
- const stat = fs5.lstatSync(p, { throwIfNoEntry: false });
2282
+ const stat = fs7.lstatSync(p, { throwIfNoEntry: false });
1528
2283
  return Boolean(stat && !stat.isSymbolicLink() && (want === "file" ? stat.isFile() : stat.isDirectory()));
1529
2284
  }
1530
2285
  function walk(dir, prefix = "", out = []) {
1531
- for (const entry of fs5.readdirSync(path5.join(dir, prefix), { withFileTypes: true })) {
2286
+ for (const entry of fs7.readdirSync(path7.join(dir, prefix), { withFileTypes: true })) {
1532
2287
  if (out.length >= MAX_FILES) break;
1533
2288
  const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
1534
2289
  if (entry.isDirectory()) walk(dir, rel, out);
@@ -1536,22 +2291,22 @@ function walk(dir, prefix = "", out = []) {
1536
2291
  }
1537
2292
  return out;
1538
2293
  }
1539
- var projectOf = (candidate) => path5.basename(candidate) === "project" ? candidate : path5.join(candidate, "project");
2294
+ var projectOf = (candidate) => path7.basename(candidate) === "project" ? candidate : path7.join(candidate, "project");
1540
2295
  function sourceProject(artifact, root, from) {
1541
2296
  const given = from ?? artifact;
1542
2297
  if (!given) return { reason: "the record names no source folder", kind: "none" };
1543
2298
  if (!from && /^[a-z][a-z0-9+.-]*:\/\//i.test(given)) return { reason: `its source is ${given}, which is not a folder on this machine`, kind: "remote" };
1544
- const candidates = path5.isAbsolute(given) ? [given] : [path5.resolve(given), path5.resolve(root, given), path5.resolve(root, "..", given)];
2299
+ const candidates = path7.isAbsolute(given) ? [given] : [path7.resolve(given), path7.resolve(root, given), path7.resolve(root, "..", given)];
1545
2300
  for (const candidate of candidates) {
1546
2301
  const project = projectOf(candidate);
1547
2302
  if (plain(project, "dir")) return { project };
1548
2303
  }
1549
- if (!from && !path5.isAbsolute(given)) {
1550
- let dir = path5.resolve(root, "..");
1551
- for (let up = path5.dirname(dir); up !== dir; up = path5.dirname(dir)) {
2304
+ if (!from && !path7.isAbsolute(given)) {
2305
+ let dir = path7.resolve(root, "..");
2306
+ for (let up = path7.dirname(dir); up !== dir; up = path7.dirname(dir)) {
1552
2307
  dir = up;
1553
- const project = projectOf(path5.resolve(dir, given));
1554
- if (plain(path5.join(project, DESIGN_SYSTEM_FILE), "file")) return { project };
2308
+ const project = projectOf(path7.resolve(dir, given));
2309
+ if (plain(path7.join(project, DESIGN_SYSTEM_FILE), "file")) return { project };
1555
2310
  }
1556
2311
  }
1557
2312
  return { reason: `its source folder ${given} was not found${from ? "" : ` (tried from ${process.cwd()} and the folders above the artifact); pass --from <folder>`}`, kind: "not-found" };
@@ -1560,21 +2315,21 @@ function compareSystem(project, root, record, from) {
1560
2315
  const diff = { namespace: record.namespace, title: record.title, changed: [], removed: [] };
1561
2316
  const found = sourceProject(record.artifact, root, from);
1562
2317
  if (!found.project) return { ...diff, skipped: found.reason, skippedKind: found.kind };
1563
- diff.source = path5.dirname(found.project);
1564
- const copy = path5.join(project, "ds", record.namespace);
2318
+ diff.source = path7.dirname(found.project);
2319
+ const copy = path7.join(project, "ds", record.namespace);
1565
2320
  if (!plain(copy, "dir")) return { ...diff, missing: true };
1566
2321
  for (const rel of walk(copy)) {
1567
- const mine = path5.join(copy, rel);
1568
- const theirs = path5.join(found.project, rel);
2322
+ const mine = path7.join(copy, rel);
2323
+ const theirs = path7.join(found.project, rel);
1569
2324
  if (!plain(mine, "file")) continue;
1570
2325
  if (!plain(theirs, "file")) {
1571
2326
  diff.removed.push(rel);
1572
2327
  continue;
1573
2328
  }
1574
- const a = fs5.statSync(mine);
1575
- const b = fs5.statSync(theirs);
2329
+ const a = fs7.statSync(mine);
2330
+ const b = fs7.statSync(theirs);
1576
2331
  if (a.size > MAX_FILE_BYTES2 || b.size > MAX_FILE_BYTES2) continue;
1577
- if (a.size !== b.size || !fs5.readFileSync(mine).equals(fs5.readFileSync(theirs))) diff.changed.push(rel);
2332
+ if (a.size !== b.size || !fs7.readFileSync(mine).equals(fs7.readFileSync(theirs))) diff.changed.push(rel);
1578
2333
  }
1579
2334
  return diff;
1580
2335
  }
@@ -1600,8 +2355,8 @@ function compareSystems(project, root, indexFile, namespace, from) {
1600
2355
  return pick(readRecords(project, indexFile), namespace).map((r) => compareSystem(project, root, r, from));
1601
2356
  }
1602
2357
  function stamp(project, indexFile, namespaces, now) {
1603
- const file = path5.join(project, indexFile);
1604
- const text = fs5.readFileSync(file, "utf8");
2358
+ const file = path7.join(project, indexFile);
2359
+ const text = fs7.readFileSync(file, "utf8");
1605
2360
  const index = JSON.parse(text);
1606
2361
  const list = index.designSystems;
1607
2362
  const records = Array.isArray(list) ? list : list && typeof list === "object" ? Object.values(list) : [];
@@ -1612,9 +2367,9 @@ function stamp(project, indexFile, namespaces, now) {
1612
2367
  const indent = /^\{\n/.test(text) ? /\n( +|\t)"/.exec(text)?.[1] ?? 2 : void 0;
1613
2368
  const out = JSON.stringify(index, null, indent);
1614
2369
  const tmp = `${file}.${process.pid}.tmp`;
1615
- fs5.writeFileSync(tmp, text.endsWith("\n") ? `${out}
2370
+ fs7.writeFileSync(tmp, text.endsWith("\n") ? `${out}
1616
2371
  ` : out);
1617
- fs5.renameSync(tmp, file);
2372
+ fs7.renameSync(tmp, file);
1618
2373
  }
1619
2374
  function syncSystems(project, root, indexFile, opts) {
1620
2375
  const diffs = compareSystems(project, root, indexFile, opts.namespace, opts.from);
@@ -1625,10 +2380,10 @@ function syncSystems(project, root, indexFile, opts) {
1625
2380
  const result = { ...diff, copied: [] };
1626
2381
  if (!opts.check && diff.source && !diff.missing) {
1627
2382
  for (const rel of diff.changed) {
1628
- const target = path5.join(project, "ds", diff.namespace, rel);
2383
+ const target = path7.join(project, "ds", diff.namespace, rel);
1629
2384
  const tmp = `${target}.${process.pid}.tmp`;
1630
- fs5.copyFileSync(path5.join(diff.source, "project", rel), tmp);
1631
- fs5.renameSync(tmp, target);
2385
+ fs7.copyFileSync(path7.join(diff.source, "project", rel), tmp);
2386
+ fs7.renameSync(tmp, target);
1632
2387
  result.copied.push(rel);
1633
2388
  }
1634
2389
  if (diff.changed.length > 0) synced.add(diff.namespace);
@@ -1642,9 +2397,9 @@ function syncSystems(project, root, indexFile, opts) {
1642
2397
  return results;
1643
2398
  }
1644
2399
 
1645
- import { createHash, randomBytes } from "node:crypto";
1646
- import fs6 from "node:fs";
1647
- import path6 from "node:path";
2400
+ import { createHash as createHash3, randomBytes } from "node:crypto";
2401
+ import fs8 from "node:fs";
2402
+ import path8 from "node:path";
1648
2403
  var HISTORY_DIR = "history";
1649
2404
  var INDEX = "index.json";
1650
2405
  var OBJECTS = "objects";
@@ -1655,34 +2410,34 @@ var MAX_NOTICES = 50;
1655
2410
  var NOTICE_DAYS = 7;
1656
2411
  var AGENT = { by: "agent", user: { id: "agent", name: "Agent" } };
1657
2412
  var SYSTEM = { by: "system", user: { id: "system", name: "" } };
1658
- var sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
2413
+ var sha256 = (bytes) => createHash3("sha256").update(bytes).digest("hex");
1659
2414
  var SHA = /^[0-9a-f]{64}$/;
1660
- var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2415
+ var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1661
2416
  function writeAtomic2(file, data) {
1662
- fs6.mkdirSync(path6.dirname(file), { recursive: true });
1663
- const tmp = path6.join(path6.dirname(file), `.${path6.basename(file)}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`);
2417
+ fs8.mkdirSync(path8.dirname(file), { recursive: true });
2418
+ const tmp = path8.join(path8.dirname(file), `.${path8.basename(file)}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`);
1664
2419
  try {
1665
- fs6.writeFileSync(tmp, data);
1666
- fs6.renameSync(tmp, file);
2420
+ fs8.writeFileSync(tmp, data);
2421
+ fs8.renameSync(tmp, file);
1667
2422
  } catch (error) {
1668
- fs6.rmSync(tmp, { force: true });
2423
+ fs8.rmSync(tmp, { force: true });
1669
2424
  throw error;
1670
2425
  }
1671
2426
  }
1672
2427
  var emptyIndex = () => ({ format: 1, next: 1, base: {}, versions: [], notices: [] });
1673
2428
  function readUser(value) {
1674
- return isRecord2(value) && typeof value.id === "string" && typeof value.name === "string" ? { id: value.id, name: value.name } : null;
2429
+ return isRecord3(value) && typeof value.id === "string" && typeof value.name === "string" ? { id: value.id, name: value.name } : null;
1675
2430
  }
1676
2431
  var isAuthor = (value) => value === "human" || value === "agent" || value === "system";
1677
2432
  function parseIndex(raw) {
1678
2433
  const index = emptyIndex();
1679
- if (!isRecord2(raw)) return index;
1680
- if (isRecord2(raw.base)) {
2434
+ if (!isRecord3(raw)) return index;
2435
+ if (isRecord3(raw.base)) {
1681
2436
  for (const [p, s] of Object.entries(raw.base)) if (typeof s === "string" && SHA.test(s)) index.base[p] = s;
1682
2437
  }
1683
2438
  if (Array.isArray(raw.versions)) {
1684
2439
  for (const v of raw.versions) {
1685
- if (!isRecord2(v) || typeof v.n !== "number" || !isAuthor(v.by) || typeof v.at !== "string" || !isRecord2(v.changes)) continue;
2440
+ if (!isRecord3(v) || typeof v.n !== "number" || !isAuthor(v.by) || typeof v.at !== "string" || !isRecord3(v.changes)) continue;
1686
2441
  const user = readUser(v.user);
1687
2442
  if (!user) continue;
1688
2443
  const changes = {};
@@ -1694,8 +2449,8 @@ function parseIndex(raw) {
1694
2449
  if (Array.isArray(v.overwrote)) {
1695
2450
  const list = [];
1696
2451
  for (const o of v.overwrote) {
1697
- const who = isRecord2(o) ? readUser(o.user) : null;
1698
- if (!isRecord2(o) || !who || typeof o.path !== "string" || o.lost !== null && typeof o.lost !== "string" || typeof o.n !== "number" || !isAuthor(o.by)) continue;
2452
+ const who = isRecord3(o) ? readUser(o.user) : null;
2453
+ if (!isRecord3(o) || !who || typeof o.path !== "string" || o.lost !== null && typeof o.lost !== "string" || typeof o.n !== "number" || !isAuthor(o.by)) continue;
1699
2454
  list.push({
1700
2455
  path: o.path,
1701
2456
  lost: o.lost,
@@ -1717,10 +2472,10 @@ function parseIndex(raw) {
1717
2472
  index.next = Math.max(typeof raw.next === "number" ? raw.next : 1, (last?.n ?? 0) + 1);
1718
2473
  if (Array.isArray(raw.notices)) {
1719
2474
  for (const n of raw.notices) {
1720
- if (!isRecord2(n) || typeof n.id !== "string" || typeof n.to !== "string" || typeof n.path !== "string" || typeof n.n !== "number") continue;
2475
+ if (!isRecord3(n) || typeof n.id !== "string" || typeof n.to !== "string" || typeof n.path !== "string" || typeof n.n !== "number") continue;
1721
2476
  if (n.kind !== "overwritten" && n.kind !== "overwrote") continue;
1722
2477
  const by = readUser(n.by);
1723
- const lost = isRecord2(n.lost) ? n.lost : null;
2478
+ const lost = isRecord3(n.lost) ? n.lost : null;
1724
2479
  const lostUser = lost ? readUser(lost.user) : null;
1725
2480
  if (!by || !lost || !lostUser || lost.sha !== null && typeof lost.sha !== "string" || typeof lost.n !== "number" || !isAuthor(lost.by)) continue;
1726
2481
  index.notices.push({
@@ -1746,10 +2501,10 @@ var History = class {
1746
2501
  /** path → sha: what the files hold after the newest version. */
1747
2502
  headState;
1748
2503
  constructor(root) {
1749
- this.dir = path6.join(root, HISTORY_DIR);
2504
+ this.dir = path8.join(root, HISTORY_DIR);
1750
2505
  let raw = null;
1751
2506
  try {
1752
- raw = JSON.parse(fs6.readFileSync(path6.join(this.dir, INDEX), "utf8"));
2507
+ raw = JSON.parse(fs8.readFileSync(path8.join(this.dir, INDEX), "utf8"));
1753
2508
  } catch {
1754
2509
  }
1755
2510
  this.index = parseIndex(raw);
@@ -1793,7 +2548,7 @@ var History = class {
1793
2548
  read(sha) {
1794
2549
  if (!SHA.test(sha)) return null;
1795
2550
  try {
1796
- return fs6.readFileSync(path6.join(this.dir, OBJECTS, sha));
2551
+ return fs8.readFileSync(path8.join(this.dir, OBJECTS, sha));
1797
2552
  } catch {
1798
2553
  return null;
1799
2554
  }
@@ -1807,7 +2562,7 @@ var History = class {
1807
2562
  size(sha) {
1808
2563
  if (!SHA.test(sha)) return null;
1809
2564
  try {
1810
- return fs6.statSync(path6.join(this.dir, OBJECTS, sha)).size;
2565
+ return fs8.statSync(path8.join(this.dir, OBJECTS, sha)).size;
1811
2566
  } catch {
1812
2567
  return null;
1813
2568
  }
@@ -1941,8 +2696,8 @@ var History = class {
1941
2696
  return out;
1942
2697
  }
1943
2698
  putObject(sha, bytes) {
1944
- const file = path6.join(this.dir, OBJECTS, sha);
1945
- if (fs6.existsSync(file)) return;
2699
+ const file = path8.join(this.dir, OBJECTS, sha);
2700
+ if (fs8.existsSync(file)) return;
1946
2701
  writeAtomic2(file, bytes);
1947
2702
  }
1948
2703
  /** Past MAX_VERSIONS the oldest fold into the base; old notices go; contents no one holds are removed. */
@@ -1979,14 +2734,14 @@ var History = class {
1979
2734
  }
1980
2735
  let names;
1981
2736
  try {
1982
- names = fs6.readdirSync(path6.join(this.dir, OBJECTS));
2737
+ names = fs8.readdirSync(path8.join(this.dir, OBJECTS));
1983
2738
  } catch {
1984
2739
  return;
1985
2740
  }
1986
- for (const name of names) if (SHA.test(name) && !keep.has(name)) fs6.rmSync(path6.join(this.dir, OBJECTS, name), { force: true });
2741
+ for (const name of names) if (SHA.test(name) && !keep.has(name)) fs8.rmSync(path8.join(this.dir, OBJECTS, name), { force: true });
1987
2742
  }
1988
2743
  save() {
1989
- writeAtomic2(path6.join(this.dir, INDEX), `${JSON.stringify(this.index)}
2744
+ writeAtomic2(path8.join(this.dir, INDEX), `${JSON.stringify(this.index)}
1990
2745
  `);
1991
2746
  }
1992
2747
  };
@@ -2055,157 +2810,16 @@ function diffContents(before, after) {
2055
2810
  return { added, removed, lines, ...truncated ? { truncated: true } : {} };
2056
2811
  }
2057
2812
 
2058
- import { createHash as createHash2 } from "node:crypto";
2059
- import fs10 from "node:fs";
2060
- import os4 from "node:os";
2061
- import path10 from "node:path";
2813
+ import { createHash as createHash4 } from "node:crypto";
2814
+ import fs11 from "node:fs";
2815
+ import os5 from "node:os";
2816
+ import path11 from "node:path";
2062
2817
 
2063
- import fs8 from "node:fs";
2818
+ import fs9 from "node:fs";
2064
2819
  import fsp2 from "node:fs/promises";
2065
- import path8 from "node:path";
2820
+ import path9 from "node:path";
2066
2821
  import { promisify } from "node:util";
2067
2822
  import zlib from "node:zlib";
2068
-
2069
- import crypto from "node:crypto";
2070
- import fs7 from "node:fs";
2071
- import path7 from "node:path";
2072
- var GUEST_ROLES = ["editor", "commenter", "viewer"];
2073
- var isGuestRole = (value) => typeof value === "string" && GUEST_ROLES.includes(value);
2074
- var SHARING_FILE = ".sharing.json";
2075
- var MAX_GUESTS = 200;
2076
- var MAX_NAME = 40;
2077
- var token = (bytes = 18) => crypto.randomBytes(bytes).toString("base64url");
2078
- var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2079
- function fresh() {
2080
- return { lan: false, links: { editor: { token: token(), on: true }, commenter: { token: token(), on: true }, viewer: { token: token(), on: true } }, guests: [] };
2081
- }
2082
- function cleanName(raw) {
2083
- const name = typeof raw === "string" ? raw.replace(/[\u0000-\u001f\u007f@<>]/g, "").replace(/\s+/g, " ").trim() : "";
2084
- return [...name].slice(0, MAX_NAME).join("");
2085
- }
2086
- function guestUser(guest) {
2087
- return { id: `guest-${guest.id}`, name: guest.name, email: `${guest.name}@${guest.id}.guest` };
2088
- }
2089
- function isLoopback(address) {
2090
- if (!address) return false;
2091
- const a = address.startsWith("::ffff:") ? address.slice(7) : address;
2092
- return a === "::1" || a.startsWith("127.");
2093
- }
2094
- var Sharing = class _Sharing {
2095
- file;
2096
- state;
2097
- constructor(root) {
2098
- this.file = path7.join(root, SHARING_FILE);
2099
- this.state = _Sharing.read(this.file);
2100
- }
2101
- static read(file) {
2102
- let raw;
2103
- try {
2104
- raw = JSON.parse(fs7.readFileSync(file, "utf8"));
2105
- } catch {
2106
- return fresh();
2107
- }
2108
- const state = fresh();
2109
- if (!isRecord3(raw)) return state;
2110
- state.lan = raw.lan === true;
2111
- if (typeof raw.port === "number" && Number.isInteger(raw.port) && raw.port > 0 && raw.port < 65536) state.port = raw.port;
2112
- if (isRecord3(raw.links)) {
2113
- for (const role of GUEST_ROLES) {
2114
- const link = raw.links[role];
2115
- if (isRecord3(link) && typeof link.token === "string" && link.token.length >= 16) state.links[role] = { token: link.token, on: link.on !== false };
2116
- }
2117
- }
2118
- if (Array.isArray(raw.guests)) {
2119
- for (const g of raw.guests) {
2120
- if (!isRecord3(g) || typeof g.id !== "string" || typeof g.secret !== "string" || !isGuestRole(g.role) || typeof g.link !== "string") continue;
2121
- state.guests.push({
2122
- id: g.id,
2123
- secret: g.secret,
2124
- name: cleanName(g.name) || "Guest",
2125
- role: g.role,
2126
- link: g.link,
2127
- joined: typeof g.joined === "string" ? g.joined : (/* @__PURE__ */ new Date(0)).toISOString(),
2128
- ...typeof g.seen === "string" ? { seen: g.seen } : {}
2129
- });
2130
- }
2131
- }
2132
- return state;
2133
- }
2134
- save() {
2135
- const tmp = `${this.file}.${process.pid}.tmp`;
2136
- fs7.writeFileSync(tmp, `${JSON.stringify(this.state, null, 2)}
2137
- `, { mode: 384 });
2138
- fs7.renameSync(tmp, this.file);
2139
- }
2140
- /** The role whose link has this token, while that link is on. */
2141
- linkRole(tokenGiven) {
2142
- for (const role of GUEST_ROLES) {
2143
- const link = this.state.links[role];
2144
- if (link.on && tokenGiven.length === link.token.length && crypto.timingSafeEqual(Buffer.from(tokenGiven), Buffer.from(link.token))) return role;
2145
- }
2146
- return null;
2147
- }
2148
- /** The guest a cookie names, if they still have access (their link on and not reset). */
2149
- guest(secret) {
2150
- if (!secret) return null;
2151
- const guest = this.state.guests.find((g) => g.secret === secret);
2152
- return guest && this.valid(guest) ? guest : null;
2153
- }
2154
- valid(guest) {
2155
- const link = this.state.links[guest.role];
2156
- return link.on && link.token === guest.link && this.state.guests.includes(guest);
2157
- }
2158
- join(role, name) {
2159
- const guest = { id: token(4), secret: token(24), name: cleanName(name) || "Guest", role, link: this.state.links[role].token, joined: (/* @__PURE__ */ new Date()).toISOString() };
2160
- this.state.guests.push(guest);
2161
- if (this.state.guests.length > MAX_GUESTS) this.state.guests.splice(0, this.state.guests.length - MAX_GUESTS);
2162
- this.save();
2163
- return guest;
2164
- }
2165
- /** Marks a guest seen now (saved at most once a minute per guest). */
2166
- seen(guest) {
2167
- const now = /* @__PURE__ */ new Date();
2168
- if (guest.seen && now.getTime() - Date.parse(guest.seen) < 6e4) return;
2169
- guest.seen = now.toISOString();
2170
- this.save();
2171
- }
2172
- setLan(on, port) {
2173
- this.state.lan = on;
2174
- if (on && port) this.state.port = port;
2175
- this.save();
2176
- }
2177
- setLink(role, on) {
2178
- this.state.links[role].on = on;
2179
- this.save();
2180
- }
2181
- /** A new token for the link: the old link stops working and the guests who came through it are dropped. */
2182
- resetLink(role) {
2183
- const old = this.state.links[role].token;
2184
- this.state.links[role] = { token: token(), on: this.state.links[role].on };
2185
- this.state.guests = this.state.guests.filter((g) => g.link !== old);
2186
- this.save();
2187
- }
2188
- /**
2189
- * Gives a guest another role. They now hang on that role's link (turning it off or resetting it drops them), so a role
2190
- * whose link is off would shut them out: false then, as for a guest who isn't there.
2191
- */
2192
- setRole(id, role) {
2193
- const guest = this.state.guests.find((g) => g.id === id);
2194
- if (!guest || !this.state.links[role].on) return false;
2195
- guest.role = role;
2196
- guest.link = this.state.links[role].token;
2197
- this.save();
2198
- return true;
2199
- }
2200
- removeGuest(id) {
2201
- const before = this.state.guests.length;
2202
- this.state.guests = this.state.guests.filter((g) => g.id !== id);
2203
- if (this.state.guests.length === before) return false;
2204
- this.save();
2205
- return true;
2206
- }
2207
- };
2208
-
2209
2823
  var LOCAL_FILES = /* @__PURE__ */ new Set([STATE_FILE, LOG_FILE, SHARING_FILE]);
2210
2824
  var LOCAL_DIRS = /* @__PURE__ */ new Set([EXPORTS_DIR]);
2211
2825
  var JUNK_FILES = /* @__PURE__ */ new Set([".DS_Store", "Thumbs.db", "desktop.ini"]);
@@ -2217,8 +2831,8 @@ var ArchiveTooLarge = class extends Error {
2217
2831
  code = "TOO_LARGE";
2218
2832
  };
2219
2833
  async function archiveEntries(root, skip = []) {
2220
- const folder = path8.basename(path8.resolve(root)) || "artifact";
2221
- const skipped = new Set(skip.map((file) => path8.resolve(file)));
2834
+ const folder = path9.basename(path9.resolve(root)) || "artifact";
2835
+ const skipped = new Set(skip.map((file) => path9.resolve(file)));
2222
2836
  const entries = [];
2223
2837
  const walk2 = async (dir, rel) => {
2224
2838
  let names;
@@ -2231,7 +2845,7 @@ async function archiveEntries(root, skip = []) {
2231
2845
  for (const dirent of names.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
2232
2846
  const name = dirent.name;
2233
2847
  if (JUNK_FILES.has(name) || isTemp(name) || !rel && (LOCAL_FILES.has(name) || LOCAL_DIRS.has(name) && dirent.isDirectory())) continue;
2234
- const file = path8.join(dir, name);
2848
+ const file = path9.join(dir, name);
2235
2849
  if (skipped.has(file)) continue;
2236
2850
  const inner = rel ? `${rel}/${name}` : name;
2237
2851
  if (dirent.isDirectory()) await walk2(file, inner);
@@ -2241,7 +2855,7 @@ async function archiveEntries(root, skip = []) {
2241
2855
  }
2242
2856
  }
2243
2857
  };
2244
- await walk2(path8.resolve(root), "");
2858
+ await walk2(path9.resolve(root), "");
2245
2859
  if (entries.length > MAX_ENTRIES) throw new ArchiveTooLarge(`The folder holds ${entries.length} files, more than a zip holds (${MAX_ENTRIES}); copy the folder itself instead.`);
2246
2860
  const bound = entries.reduce((sum, entry) => sum + entry.size + 2 * (46 + Buffer.byteLength(entry.name)), 22);
2247
2861
  if (bound > MAX_BYTES) throw new ArchiveTooLarge(`The folder holds ${(bound / 2 ** 30).toFixed(1)} GiB, more than a zip holds (4 GiB); copy the folder itself instead.`);
@@ -2290,7 +2904,7 @@ async function writeZip(entries, write) {
2290
2904
  const crc = crc32(data);
2291
2905
  let method = 0;
2292
2906
  let body = data;
2293
- if (data.length > 0 && !STORED.has(path8.extname(entry.name).toLowerCase())) {
2907
+ if (data.length > 0 && !STORED.has(path9.extname(entry.name).toLowerCase())) {
2294
2908
  const deflated = await deflateRaw(data);
2295
2909
  if (deflated.length < data.length) {
2296
2910
  method = 8;
@@ -2344,12 +2958,12 @@ async function writeZip(entries, write) {
2344
2958
  }
2345
2959
  function archiveFileName(root, now = /* @__PURE__ */ new Date()) {
2346
2960
  const day = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
2347
- return `${path8.basename(path8.resolve(root)) || "artifact"}-${day}.zip`;
2961
+ return `${path9.basename(path9.resolve(root)) || "artifact"}-${day}.zip`;
2348
2962
  }
2349
2963
  async function writeArchive(root, out) {
2350
- const file = path8.resolve(out);
2964
+ const file = path9.resolve(out);
2351
2965
  const entries = await archiveEntries(root, [file]);
2352
- const tmp = path8.join(path8.dirname(file), `.${path8.basename(file)}.${process.pid}.tmp`);
2966
+ const tmp = path9.join(path9.dirname(file), `.${path9.basename(file)}.${process.pid}.tmp`);
2353
2967
  const handle = await fsp2.open(tmp, "w");
2354
2968
  let bytes;
2355
2969
  try {
@@ -2367,10 +2981,10 @@ async function writeArchive(root, out) {
2367
2981
  return { file, files: entries.length, bytes };
2368
2982
  }
2369
2983
  async function archiveCommand(root, out) {
2370
- const where = out ? path8.resolve(out) : path8.dirname(path8.resolve(root));
2371
- const target = fs8.statSync(where, { throwIfNoEntry: false })?.isDirectory() ? path8.join(where, archiveFileName(root)) : where;
2984
+ const where = out ? path9.resolve(out) : path9.dirname(path9.resolve(root));
2985
+ const target = fs9.statSync(where, { throwIfNoEntry: false })?.isDirectory() ? path9.join(where, archiveFileName(root)) : where;
2372
2986
  const { file, files, bytes } = await writeArchive(root, target);
2373
- return { file, folder: path8.basename(path8.resolve(root)), files, bytes, leftOut: [...LOCAL_FILES] };
2987
+ return { file, folder: path9.basename(path9.resolve(root)), files, bytes, leftOut: [...LOCAL_FILES] };
2374
2988
  }
2375
2989
  function attachment(name) {
2376
2990
  const ascii = name.replace(/[^\x20-\x7e]|["\\]/g, "_");
@@ -2418,31 +3032,31 @@ async function sendArchive(request, response, root) {
2418
3032
  }
2419
3033
  }
2420
3034
 
2421
- import fs9 from "node:fs";
3035
+ import fs10 from "node:fs";
2422
3036
  import fsp3 from "node:fs/promises";
2423
- import os3 from "node:os";
2424
- import path9 from "node:path";
3037
+ import os4 from "node:os";
3038
+ import path10 from "node:path";
2425
3039
  var MAX_BYTES2 = 4294967295;
2426
3040
  function shown(file) {
2427
- const home = os3.homedir();
2428
- return home && (file === home || file.startsWith(home + path9.sep)) ? `~${file.slice(home.length)}` : file;
3041
+ const home = os4.homedir();
3042
+ return home && (file === home || file.startsWith(home + path10.sep)) ? `~${file.slice(home.length)}` : file;
2429
3043
  }
2430
3044
  function saved(file, bytes) {
2431
- return { path: file, name: path9.basename(file), folder: path9.dirname(file), shown: shown(file), bytes };
3045
+ return { path: file, name: path10.basename(file), folder: path10.dirname(file), shown: shown(file), bytes };
2432
3046
  }
2433
3047
  function safeFileName(raw) {
2434
3048
  let name = (raw.replace(/\\/g, "/").split("/").pop() ?? "").replace(/[\u0000-\u001f\u007f<>:"|?*]/g, "_").replace(/^[.\s]+|[.\s]+$/g, "");
2435
3049
  if (name.length > 200) {
2436
- const ext = path9.extname(name).slice(0, 16);
3050
+ const ext = path10.extname(name).slice(0, 16);
2437
3051
  name = name.slice(0, 200 - ext.length) + ext;
2438
3052
  }
2439
3053
  return name || "download";
2440
3054
  }
2441
3055
  async function openFree(dir, name) {
2442
- const ext = path9.extname(name);
3056
+ const ext = path10.extname(name);
2443
3057
  const stem = name.slice(0, name.length - ext.length);
2444
3058
  for (let n = 0; ; n++) {
2445
- const file = path9.join(dir, n === 0 ? name : `${stem} (${n})${ext}`);
3059
+ const file = path10.join(dir, n === 0 ? name : `${stem} (${n})${ext}`);
2446
3060
  try {
2447
3061
  return { file, handle: await fsp3.open(file, "wx") };
2448
3062
  } catch (error) {
@@ -2461,7 +3075,7 @@ async function saveDownload(request, response, root) {
2461
3075
  let handle = null;
2462
3076
  let bytes = 0;
2463
3077
  try {
2464
- const dir = path9.join(root, EXPORTS_DIR);
3078
+ const dir = path10.join(root, EXPORTS_DIR);
2465
3079
  await fsp3.mkdir(dir, { recursive: true });
2466
3080
  ({ file, handle } = await openFree(dir, name));
2467
3081
  for await (const chunk of request) {
@@ -2486,7 +3100,7 @@ async function saveArchive(request, response, root) {
2486
3100
  request.resume();
2487
3101
  try {
2488
3102
  const { file } = await archiveCommand(root, void 0);
2489
- sendJson2(response, 200, saved(file, fs9.statSync(file).size));
3103
+ sendJson2(response, 200, saved(file, fs10.statSync(file).size));
2490
3104
  } catch (error) {
2491
3105
  const tooLarge = error instanceof ArchiveTooLarge;
2492
3106
  sendJson2(response, tooLarge ? 413 : 500, { code: tooLarge ? "too_large" : "upstream_error", message: error instanceof Error ? error.message : String(error) });
@@ -2515,108 +3129,73 @@ function commentsViewer(viewer) {
2515
3129
  var isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2516
3130
  var esc2 = (text) => text.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
2517
3131
  var script = (value) => JSON.stringify(value).replace(/</g, "\\u003c");
2518
- function ownHosts(host, port) {
2519
- const named = host === "0.0.0.0" || host === "::" ? [] : [host.includes(":") ? `[${host}]:${port}` : `${host}:${port}`];
2520
- return [.../* @__PURE__ */ new Set([`127.0.0.1:${port}`, `localhost:${port}`, `[::1]:${port}`, ...named])];
2521
- }
2522
3132
  function localOwner() {
2523
3133
  let name = "You";
2524
3134
  try {
2525
- name = os4.userInfo().username.trim() || name;
3135
+ name = os5.userInfo().username.trim() || name;
2526
3136
  } catch {
2527
3137
  }
2528
3138
  return { id: "local", name, email: `${name}@localhost` };
2529
3139
  }
2530
- var guestCookieName = (kind, root) => `${kind.replace(/-/g, "_")}_guest_${createHash2("sha256").update(path10.resolve(root)).digest("hex").slice(0, 12)}`;
3140
+ var guestCookieName = (kind, root) => `${kind.replace(/-/g, "_")}_guest_${createHash4("sha256").update(path11.resolve(root)).digest("hex").slice(0, 12)}`;
2531
3141
  var ShareHost = class {
2532
3142
  sharing;
2533
3143
  options;
2534
- /** The port the server listens on (set by `listening`); the LAN listeners take the same one. */
2535
- port = 0;
2536
- hosts = /* @__PURE__ */ new Set();
2537
- /** One listener per LAN address while the artifact is open to the network. */
2538
- lanServers = /* @__PURE__ */ new Map();
3144
+ network;
3145
+ page;
3146
+ /** Open to the local network now (the hub's listeners are up, for this artifact too). */
3147
+ on = false;
2539
3148
  live = /* @__PURE__ */ new Set();
2540
3149
  constructor(options) {
2541
3150
  this.options = options;
3151
+ this.network = options.network;
3152
+ this.page = options.page;
2542
3153
  this.sharing = new Sharing(options.root);
2543
3154
  }
2544
- /**
2545
- * Starts `server` on the first free port from `port` (a shared artifact tries the port its links were given out with
2546
- * first, so they keep working, unless the port was given), shares it on the network with `lan` (or as it was left)
2547
- * and answers the port taken. If this throws, the caller closes what it started.
2548
- */
2549
- async listen(server2, { port, host, lan = false, portGiven = false }) {
3155
+ get port() {
3156
+ return this.network.port;
3157
+ }
3158
+ /** The hub took the artifact: open to the network now with `lan`, or if it was left shared. */
3159
+ async start(lan = false) {
2550
3160
  const { state } = this.sharing;
2551
- const linksPort = !portGiven && (lan || state.lan) && state.port ? state.port : void 0;
2552
- const taken = await listenFree(server2, port, host, linksPort);
2553
- if (linksPort && taken !== linksPort) this.options.log(`port ${linksPort} (the shared links') is in use; took ${taken}: give the new links out again`);
2554
- else if (!linksPort && port && taken !== port) this.options.log(`port ${port} is in use; took ${taken}`);
2555
- if (lan && !this.sharing.state.lan) this.sharing.setLan(true, taken);
2556
- await this.listening(taken, ownHosts(host, taken));
2557
- return taken;
3161
+ if (!lan && !state.lan) return;
3162
+ if (state.port && state.port !== this.port) this.options.log(`port ${state.port} (the shared links') is not this server's; it is on ${this.port}: give the new links out again`);
3163
+ await this.setLan(true);
2558
3164
  }
2559
3165
  /** The artifact at each LAN address it is shared on ([] while it is not). */
2560
3166
  lanUrls() {
2561
- return [...this.lanServers.keys()].map((a) => `http://${a}:${this.port}/`);
2562
- }
2563
- /** The main listener is up on `port` (answering `hosts`): open to the network now if the artifact was left shared. */
2564
- async listening(port, hosts) {
2565
- this.port = port;
2566
- this.hosts = new Set(hosts);
2567
- if (this.sharing.state.lan) await this.setLan(true);
3167
+ return this.on ? this.network.addresses().map((a) => `http://${a}:${this.port}${this.page}`) : [];
2568
3168
  }
2569
3169
  /** Open to the local network now. */
2570
3170
  get shared() {
2571
- return this.lanServers.size > 0;
3171
+ return this.on;
2572
3172
  }
2573
- /** A Host header this server answers: its own names, and while shared, its LAN addresses and machine name. */
2574
- hostAllowed(request) {
2575
- const host = request.headers.host ?? "";
2576
- if (this.hosts.has(host)) return true;
2577
- if (!this.lanServers.size) return false;
2578
- const suffix = `:${this.port}`;
2579
- if (!host.endsWith(suffix)) return false;
2580
- const name = host.slice(0, -suffix.length).toLowerCase();
2581
- const machine = os4.hostname().toLowerCase();
2582
- return this.lanServers.has(name) || name === machine || name === `${machine.replace(/\.local$/, "")}.local`;
3173
+ /** The artifact's name, for the join page and the list of what is open at the server's root. */
3174
+ title() {
3175
+ return this.options.title();
2583
3176
  }
2584
3177
  /**
2585
- * Opens the artifact to the local network (a listener on each LAN address, on the server's port) or closes it.
2586
- * Returns the addresses it listens on. Guests already in keep their place for when it opens again.
3178
+ * Opens the artifact to the local network or closes it, and returns the addresses it is on. Guests already in keep
3179
+ * their place for when it opens again.
2587
3180
  */
2588
3181
  async setLan(on) {
2589
3182
  if (!on) {
2590
- this.closeLan();
3183
+ this.on = false;
3184
+ this.network.release();
2591
3185
  for (const live of [...this.live]) if (live.viewer.guest) live.close("unshared");
2592
3186
  if (this.sharing.state.lan) this.sharing.setLan(false);
2593
3187
  return [];
2594
3188
  }
2595
- for (const address of lanAddresses()) {
2596
- if (this.lanServers.has(address)) continue;
2597
- const server2 = this.options.createServer();
2598
- try {
2599
- await listen(server2, this.port, address);
2600
- this.lanServers.set(address, server2);
2601
- } catch (error) {
2602
- server2.close();
2603
- this.options.log(`not listening on ${address}:${this.port}: ${error.message}`);
2604
- }
2605
- }
3189
+ const addresses = await this.network.open();
3190
+ this.on = addresses.length > 0;
2606
3191
  if (!this.sharing.state.lan || this.sharing.state.port !== this.port) this.sharing.setLan(true, this.port);
2607
- this.options.log(`shared on the local network: ${[...this.lanServers.keys()].map((a) => `http://${a}:${this.port}/`).join(", ") || "(no LAN address)"}`);
2608
- return [...this.lanServers.keys()];
2609
- }
2610
- closeLan() {
2611
- for (const server2 of this.lanServers.values()) {
2612
- server2.close();
2613
- server2.closeAllConnections?.();
2614
- }
2615
- this.lanServers.clear();
3192
+ this.options.log(`shared on the local network: ${addresses.map((a) => `http://${a}:${this.port}${this.page}`).join(", ") || "(no LAN address)"}`);
3193
+ return addresses;
2616
3194
  }
2617
- /** Stops the LAN listeners (the server closes its own). */
3195
+ /** The artifact left the hub: it stays shared in .sharing.json, for the next time it opens. */
2618
3196
  close() {
2619
- this.closeLan();
3197
+ this.on = false;
3198
+ this.network.release();
2620
3199
  }
2621
3200
  // ---- who is asking ---------------------------------------------------------------------------------------------
2622
3201
  cookie(request) {
@@ -2634,7 +3213,7 @@ var ShareHost = class {
2634
3213
  */
2635
3214
  viewerOf(request) {
2636
3215
  if (isLoopback(request.socket.remoteAddress)) return { role: "owner", user: this.options.owner };
2637
- const guest = this.lanServers.size ? this.sharing.guest(this.cookie(request)) : null;
3216
+ const guest = this.on ? this.sharing.guest(this.cookie(request)) : null;
2638
3217
  if (!guest) return null;
2639
3218
  this.sharing.seen(guest);
2640
3219
  return { role: guest.role, user: guestUser(guest), guest };
@@ -2652,7 +3231,7 @@ var ShareHost = class {
2652
3231
  }
2653
3232
  /** Still allowed in: the owner, or a guest whose link is on and who was not removed. */
2654
3233
  stillIn(viewer) {
2655
- return !viewer.guest || this.shared && this.sharing.valid(viewer.guest);
3234
+ return !viewer.guest || this.on && this.sharing.valid(viewer.guest);
2656
3235
  }
2657
3236
  /** Ends the streams of guests who lost access (a link turned off or reset, a guest removed). */
2658
3237
  dropRevoked() {
@@ -2692,7 +3271,7 @@ var ShareHost = class {
2692
3271
  }
2693
3272
  if (method === "GET" && p === "/__share/invite") return sendJson2(response, 200, this.invites(viewer, request.headers.host ?? "")), true;
2694
3273
  if (method === "GET" && p === "/__share/ui.js") {
2695
- fs10.stat(this.options.uiFile, (error, stat) => {
3274
+ fs11.stat(this.options.uiFile, (error, stat) => {
2696
3275
  if (error || !stat.isFile()) return sendJson2(response, 404, { code: "not_found", message: "The Share panel is missing." });
2697
3276
  sendFile(request, response, this.options.uiFile, { "Content-Type": "text/javascript; charset=utf-8", "Content-Length": stat.size, "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" });
2698
3277
  });
@@ -2711,8 +3290,8 @@ var ShareHost = class {
2711
3290
  * ("?…") goes to the boot script, for `globals`.
2712
3291
  */
2713
3292
  headTags(_viewer, query = "") {
2714
- let tags = `<script src="/__share/boot.js${query.replace(/[<>"']/g, "").replace(/&/g, "&amp;")}"></script>`;
2715
- if (fs10.existsSync(this.options.uiFile)) tags += '<script type="module" src="/__share/ui.js"></script>';
3293
+ let tags = `<script src="${this.page}__share/boot.js${query.replace(/[<>"']/g, "").replace(/&/g, "&amp;")}"></script>`;
3294
+ if (fs11.existsSync(this.options.uiFile)) tags += `<script type="module" src="${this.page}__share/ui.js"></script>`;
2716
3295
  return tags;
2717
3296
  }
2718
3297
  /** `html` with the head tags first in its <head>; null when it has none. */
@@ -2722,10 +3301,10 @@ var ShareHost = class {
2722
3301
  const end = html.indexOf(">", at) + 1;
2723
3302
  return html.slice(0, end) + this.headTags(viewer, query) + html.slice(end);
2724
3303
  }
2725
- /** What the page runs before anything else (see SECURE_CONTEXT_SHIM and GUEST_WATCH). */
3304
+ /** What the page runs before anything else (see ROOT_SHIM, SECURE_CONTEXT_SHIM and GUEST_WATCH). */
2726
3305
  bootScript(viewer, query = new URLSearchParams()) {
2727
3306
  const share = { ...this.me(viewer), noun: this.options.noun, title: this.options.title(), ...this.options.reactions ? { reactions: true } : {} };
2728
- let js = `${SECURE_CONTEXT_SHIM}window.__baoyuShare=${script(share)};`;
3307
+ let js = `(function(BASE){${ROOT_SHIM}})(${script(this.page)});${SECURE_CONTEXT_SHIM}window.__baoyuShare=${script(share)};`;
2729
3308
  for (const [name, value] of Object.entries(this.options.globals?.(viewer, query) ?? {})) js += `window[${script(name)}]=${script(value)};`;
2730
3309
  if (viewer.guest) js += GUEST_WATCH;
2731
3310
  return js;
@@ -2738,6 +3317,11 @@ var ShareHost = class {
2738
3317
  const t = this.words(request);
2739
3318
  this.sendPage(response, 403, t.noAccessTitle, `<h1>${esc2(t.noAccessTitle)}</h1><p>${esc2(t.noAccess)}</p>${this.ownerHint(request, t)}`);
2740
3319
  }
3320
+ /** `/` on the server for someone who may see more than one artifact: a page of links. */
3321
+ sendList(response, sites) {
3322
+ const noun = this.options.noun.en;
3323
+ this.sendPage(response, 200, `Open ${noun}s`, `<h1>Open ${esc2(noun)}s</h1><ul>${sites.map((site) => `<li><a href="${esc2(site.page)}">${esc2(site.title)}</a></li>`).join("")}</ul>`);
3324
+ }
2741
3325
  /** A small page of our own (the join form, the refusal), in the reader's language. */
2742
3326
  sendPage(response, status, title, body, headers = {}) {
2743
3327
  const html = `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${esc2(title)}</title><style>${PAGE_CSS}</style></head><body><main>${body}</main></body></html>`;
@@ -2752,8 +3336,8 @@ var ShareHost = class {
2752
3336
  /** On the computer that shares the artifact, over its LAN address: the way back to the owner's page. */
2753
3337
  ownerHint(request, t) {
2754
3338
  const from = (request.socket.remoteAddress ?? "").replace(/^::ffff:/, "");
2755
- if (!this.lanServers.has(from)) return "";
2756
- const url = `http://127.0.0.1:${this.port}/`;
3339
+ if (!this.network.addresses().includes(from)) return "";
3340
+ const url = `http://127.0.0.1:${this.port}${this.page}`;
2757
3341
  return `<p class="note">${esc2(t.ownerHere)} <a href="${esc2(url)}">${esc2(url)}</a></p>`;
2758
3342
  }
2759
3343
  /** GET /s/<token>: the name form (or straight in, for a guest already in through this link); POST: join. */
@@ -2763,16 +3347,16 @@ var ShareHost = class {
2763
3347
  const local = isLoopback(request.socket.remoteAddress);
2764
3348
  const form = method === "POST" ? await readBody(request, 8 << 10).then((body) => body.toString("utf8"), () => "") : "";
2765
3349
  const title = this.options.title();
2766
- if (!role || !local && !this.lanServers.size) return this.sendPage(response, 404, t.linkGoneTitle, `<h1>${esc2(t.linkGoneTitle)}</h1><p>${esc2(t.linkGone)}</p>`);
3350
+ if (!role || !local && !this.on) return this.sendPage(response, 404, t.linkGoneTitle, `<h1>${esc2(t.linkGoneTitle)}</h1><p>${esc2(t.linkGone)}</p>`);
2767
3351
  if (local) {
2768
- const lan = [...this.lanServers.keys()][0];
2769
- const there = lan ? `http://${lan}:${this.port}/s/${token2}` : "";
3352
+ const lan = this.on ? this.network.addresses()[0] : void 0;
3353
+ const there = lan ? `http://${lan}:${this.port}${this.page}s/${token2}` : "";
2770
3354
  const tryIt = there ? `<p>${esc2(t.tryRole(t.roles[role]))} <a href="${esc2(there)}">${esc2(there)}</a></p>` : "";
2771
- return this.sendPage(response, 200, title, `<h1>${esc2(title)}</h1><p>${esc2(t.youOwn)} <a href="/">${esc2(t.openAsOwner)}</a></p>${tryIt}`);
3355
+ return this.sendPage(response, 200, title, `<h1>${esc2(title)}</h1><p>${esc2(t.youOwn)} <a href="${esc2(this.page)}">${esc2(t.openAsOwner)}</a></p>${tryIt}`);
2772
3356
  }
2773
3357
  const current = this.sharing.guest(this.cookie(request));
2774
3358
  if (method === "GET" && current && current.link === this.sharing.state.links[role].token) {
2775
- response.writeHead(303, { Location: "/", "Cache-Control": "no-store" }).end();
3359
+ response.writeHead(303, { Location: this.page, "Cache-Control": "no-store" }).end();
2776
3360
  return;
2777
3361
  }
2778
3362
  if (method === "GET") {
@@ -2787,21 +3371,21 @@ var ShareHost = class {
2787
3371
  const guest = this.sharing.join(role, name);
2788
3372
  this.options.log(`share: ${guest.name} joined as ${role} (${request.socket.remoteAddress})`);
2789
3373
  const cookie = `${this.options.cookieName}=${guest.secret}; Path=/; HttpOnly; SameSite=Lax; Max-Age=31536000`;
2790
- response.writeHead(303, { Location: "/", "Set-Cookie": cookie, "Cache-Control": "no-store" }).end();
3374
+ response.writeHead(303, { Location: this.page, "Set-Cookie": cookie, "Cache-Control": "no-store" }).end();
2791
3375
  }
2792
3376
  /**
2793
3377
  * The links this person may pass on: their own role's and the ones below it that are on (the owner: every one on), at
2794
- * the address they came by (`host`, a Host header hostAllowed let in). A guest could forward the link they joined
3378
+ * the address they came by (`host`, a Host header the hub let in). A guest could forward the link they joined
2795
3379
  * with anyway; a lower one gives no more than that. Who joins through one shows in the owner's panel like anyone else.
2796
3380
  */
2797
3381
  invites(viewer, host) {
2798
3382
  const { links } = this.sharing.state;
2799
3383
  const from = viewer.role === "owner" ? 0 : GUEST_ROLES.indexOf(viewer.role);
2800
- return { links: GUEST_ROLES.slice(from).filter((role) => links[role].on).map((role) => ({ role, url: `http://${host}/s/${links[role].token}` })) };
3384
+ return { links: GUEST_ROLES.slice(from).filter((role) => links[role].on).map((role) => ({ role, url: `http://${host}${this.page}s/${links[role].token}` })) };
2801
3385
  }
2802
3386
  /** Links, addresses and people, for the share panel and `artifact.mjs share`. */
2803
3387
  shareView() {
2804
- const addresses = [...this.lanServers.keys()].sort((a, b) => Number(isTailnet(a)) - Number(isTailnet(b)));
3388
+ const addresses = (this.on ? this.network.addresses() : []).sort((a, b) => Number(isTailnet(a)) - Number(isTailnet(b)));
2805
3389
  const local = addresses.filter((a) => !isTailnet(a));
2806
3390
  const base = local.length ? `http://${local[0]}:${this.port}` : addresses.length ? `http://${addresses[0]}:${this.port}` : `http://127.0.0.1:${this.port}`;
2807
3391
  const tailnet = addresses.find(isTailnet);
@@ -2812,12 +3396,13 @@ var ShareHost = class {
2812
3396
  const network = lan.length ? `http://${lan[0]}:${this.port}` : base;
2813
3397
  return {
2814
3398
  title: this.options.title(),
2815
- lan: this.lanServers.size > 0,
3399
+ lan: this.on,
2816
3400
  port: this.port,
3401
+ page: this.page,
2817
3402
  addresses,
2818
- local: `http://127.0.0.1:${this.port}/`,
2819
- links: Object.fromEntries(GUEST_ROLES.map((role) => [role, { on: links[role].on, url: `${base}/s/${links[role].token}`, networkUrl: `${network}/s/${links[role].token}` }])),
2820
- ...tailnet ? { tailnet: { address: tailnet, links: Object.fromEntries(GUEST_ROLES.map((role) => [role, { on: links[role].on, url: `http://${tailnet}:${this.port}/s/${links[role].token}` }])) } } : {},
3403
+ local: `http://127.0.0.1:${this.port}${this.page}`,
3404
+ links: Object.fromEntries(GUEST_ROLES.map((role) => [role, { on: links[role].on, url: `${base}${this.page}s/${links[role].token}`, networkUrl: `${network}${this.page}s/${links[role].token}` }])),
3405
+ ...tailnet ? { tailnet: { address: tailnet, links: Object.fromEntries(GUEST_ROLES.map((role) => [role, { on: links[role].on, url: `http://${tailnet}:${this.port}${this.page}s/${links[role].token}` }])) } } : {},
2821
3406
  guests: guests.filter((g) => this.sharing.valid(g) || links[g.role].token === g.link).map((g) => ({ id: g.id, name: g.name, role: g.role, joined: g.joined, ...g.seen ? { seen: g.seen } : {}, online: online.has(g.id), active: this.sharing.valid(g) }))
2822
3407
  };
2823
3408
  }
@@ -2881,6 +3466,31 @@ var ShareHost = class {
2881
3466
  return (zh ? ZH : EN)(this.options.noun);
2882
3467
  }
2883
3468
  };
3469
+ var ROOT_SHIM = `
3470
+ var P=location.protocol==='https:'?'wss:':'ws:';
3471
+ function under(u){try{var a=new URL(String(u),location.href);if(a.host===location.host&&/^(https?|wss?):$/.test(a.protocol)&&a.pathname.indexOf(BASE)!==0)a.pathname=BASE+a.pathname.slice(1);return a.href}catch(e){return u}}
3472
+ var F=window.fetch;
3473
+ window.fetch=function(i,o){if(typeof Request==='function'&&i instanceof Request){var r=under(i.url);return F.call(this,r===i.url?i:new Request(r,i),o)}return F.call(this,under(i&&i.href||i),o)};
3474
+ var X=XMLHttpRequest.prototype.open;
3475
+ XMLHttpRequest.prototype.open=function(m,u){var a=[].slice.call(arguments);a[1]=under(u);return X.apply(this,a)};
3476
+ var W=window.WebSocket;
3477
+ function WS(u,p){return p===undefined?new W(under(u)):new W(under(u),p)}
3478
+ WS.prototype=W.prototype;WS.CONNECTING=0;WS.OPEN=1;WS.CLOSING=2;WS.CLOSED=3;
3479
+ window.WebSocket=WS;
3480
+ if(navigator.sendBeacon){var B=navigator.sendBeacon;navigator.sendBeacon=function(u,d){return B.call(navigator,under(u),d)}}
3481
+ class S extends EventTarget{
3482
+ constructor(u){super();this.url=under(u);this.withCredentials=false;this.readyState=0;this.onopen=this.onmessage=this.onerror=null;this.l='';this.c()}
3483
+ c(){var t=this,a=new URL(t.url);a.protocol=P;if(t.l)a.searchParams.set('__lastEventId',t.l);var w=t.w=new W(a.href,'baoyu-sse'),b='';
3484
+ w.onopen=function(){t.readyState=1;t.f(new Event('open'))};
3485
+ w.onmessage=function(e){b+=e.data;var i;while((i=b.indexOf('\\n\\n'))>=0){t.b(b.slice(0,i));b=b.slice(i+2)}};
3486
+ w.onclose=function(e){if(t.readyState===2)return;if(e.code>=4000){t.readyState=2;t.f(new Event('error'));return}t.readyState=0;t.f(new Event('error'));t.r=setTimeout(function(){t.c()},1000)}}
3487
+ b(s){var n='message',d=[],h=false;s.split('\\n').forEach(function(x){if(!x||x[0]===':')return;var i=x.indexOf(':'),k=i<0?x:x.slice(0,i),v=i<0?'':x.slice(i+1).replace(/^ /,'');if(k==='event')n=v;else if(k==='data'){d.push(v);h=true}else if(k==='id')this.l=v}.bind(this));if(h)this.f(new MessageEvent(n,{data:d.join('\\n'),origin:location.origin,lastEventId:this.l}))}
3488
+ f(e){this.dispatchEvent(e);var h=this['on'+e.type];if(typeof h==='function')h.call(this,e)}
3489
+ close(){this.readyState=2;clearTimeout(this.r);if(this.w)this.w.close()}
3490
+ }
3491
+ S.CONNECTING=0;S.OPEN=1;S.CLOSED=2;
3492
+ window.EventSource=S;
3493
+ `;
2884
3494
  var SECURE_CONTEXT_SHIM = "if(window.crypto&&!crypto.randomUUID)crypto.randomUUID=function(){var b=crypto.getRandomValues(new Uint8Array(16)),h='',i;b[6]=b[6]&15|64;b[8]=b[8]&63|128;for(i=0;i<16;i++){h+=(b[i]+256).toString(16).slice(1);if(i==3||i==5||i==7||i==9)h+='-'}return h};";
2885
3495
  var GUEST_WATCH = "setInterval(function(){fetch('/__share/me',{cache:'no-store'}).then(function(r){if(r.status===403)return location.reload();return r.json().then(function(m){if(m&&m.role&&m.role!==window.__baoyuShare.role)location.reload()})},function(){})},5000);";
2886
3496
  var PAGE_CSS = `
@@ -3355,7 +3965,7 @@ var OVERWRITE_WINDOW_MS = 6e4;
3355
3965
  var filesOf = (listing, paths) => Object.fromEntries(paths.map((p) => [p, listing.files[p]?.sha256 ?? null]));
3356
3966
  var authorOf = (who) => ({ id: who.user.id, name: who.user.name, ...who.by === "agent" ? { agent: true } : {} });
3357
3967
  var CLIENT_ID = /^[A-Za-z0-9_-]{8,64}$/;
3358
- var sha2562 = (bytes) => createHash3("sha256").update(bytes).digest("hex");
3968
+ var sha2562 = (bytes) => createHash5("sha256").update(bytes).digest("hex");
3359
3969
  function contentTypeOf(file) {
3360
3970
  const ext = file.slice(file.lastIndexOf(".") + 1).toLowerCase();
3361
3971
  return MIME[ext] ?? "application/octet-stream";
@@ -3379,7 +3989,7 @@ function inlineScriptHashes(html) {
3379
3989
  const hashes = [];
3380
3990
  for (const match of html.matchAll(/<script(\s[^>]*)?>([\s\S]*?)<\/script>/gi)) {
3381
3991
  if (/\ssrc\s*=/i.test(match[1] ?? "")) continue;
3382
- hashes.push("sha256-" + createHash3("sha256").update(match[2] ?? "", "utf8").digest("base64"));
3992
+ hashes.push("sha256-" + createHash5("sha256").update(match[2] ?? "", "utf8").digest("base64"));
3383
3993
  }
3384
3994
  return hashes;
3385
3995
  }
@@ -3412,7 +4022,7 @@ async function collectFiles(root, limit) {
3412
4022
  const seen = /* @__PURE__ */ new Set();
3413
4023
  let truncated = false;
3414
4024
  const visit = async (relDir, descend) => {
3415
- const dir = relDir === "" ? root : path11.join(root, ...relDir.split("/"));
4025
+ const dir = relDir === "" ? root : path12.join(root, ...relDir.split("/"));
3416
4026
  let entries;
3417
4027
  try {
3418
4028
  entries = await fsp4.readdir(dir, { withFileTypes: true });
@@ -3443,8 +4053,8 @@ async function collectFiles(root, limit) {
3443
4053
  return { rels, truncated };
3444
4054
  }
3445
4055
  async function writeFileAtomic(file, bytes) {
3446
- await fsp4.mkdir(path11.dirname(file), { recursive: true });
3447
- const tmp = path11.join(path11.dirname(file), `.${path11.basename(file)}.${randomUUID()}.tmp`);
4056
+ await fsp4.mkdir(path12.dirname(file), { recursive: true });
4057
+ const tmp = path12.join(path12.dirname(file), `.${path12.basename(file)}.${randomUUID()}.tmp`);
3448
4058
  try {
3449
4059
  await fsp4.writeFile(tmp, bytes);
3450
4060
  await fsp4.rename(tmp, file);
@@ -3500,11 +4110,11 @@ var Store = class {
3500
4110
  async diskPath(rel) {
3501
4111
  const realRoot = await fsp4.realpath(this.root).catch(() => null);
3502
4112
  if (realRoot === null) return null;
3503
- const file = path11.join(realRoot, ...rel.split("/"));
3504
- for (let dir = path11.dirname(file); ; dir = path11.dirname(dir)) {
4113
+ const file = path12.join(realRoot, ...rel.split("/"));
4114
+ for (let dir = path12.dirname(file); ; dir = path12.dirname(dir)) {
3505
4115
  const real = await fsp4.realpath(dir).catch(() => null);
3506
4116
  if (real === null) {
3507
- if (path11.dirname(dir) === dir) return null;
4117
+ if (path12.dirname(dir) === dir) return null;
3508
4118
  continue;
3509
4119
  }
3510
4120
  if (real !== dir) return null;
@@ -3734,7 +4344,7 @@ var Store = class {
3734
4344
  }
3735
4345
  async findBlobFile(id) {
3736
4346
  try {
3737
- const names = await fsp4.readdir(path11.join(this.root, LAYOUT.blobDir));
4347
+ const names = await fsp4.readdir(path12.join(this.root, LAYOUT.blobDir));
3738
4348
  const name = names.find((n) => n.startsWith(`${id}.`) && !n.endsWith(".tmp"));
3739
4349
  if (name === void 0) return null;
3740
4350
  const file = await this.diskPath(`${LAYOUT.blobDir}/${name}`);
@@ -3789,13 +4399,13 @@ function watchArtifact(store, onChange, onError) {
3789
4399
  const directories = /* @__PURE__ */ new Map();
3790
4400
  const visit = (rel, descend) => {
3791
4401
  if (directories.size >= maxDirectories) return;
3792
- const dir = rel === "" ? store.root : path11.join(store.root, ...rel.split("/"));
3793
- if (directories.has(dir) || !fs11.existsSync(dir)) return;
3794
- const info = fs11.lstatSync(dir);
4402
+ const dir = rel === "" ? store.root : path12.join(store.root, ...rel.split("/"));
4403
+ if (directories.has(dir) || !fs12.existsSync(dir)) return;
4404
+ const info = fs12.lstatSync(dir);
3795
4405
  if (!info.isDirectory()) return;
3796
4406
  directories.set(dir, `${info.dev}:${info.ino}`);
3797
4407
  if (!descend) return;
3798
- for (const entry of fs11.readdirSync(dir, { withFileTypes: true })) {
4408
+ for (const entry of fs12.readdirSync(dir, { withFileTypes: true })) {
3799
4409
  if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
3800
4410
  const child = rel === "" ? entry.name : `${rel}/${entry.name}`;
3801
4411
  if (LAYOUT.descendInto(child)) visit(child, true);
@@ -3812,19 +4422,19 @@ function watchArtifact(store, onChange, onError) {
3812
4422
  if (watchers.has(dir)) continue;
3813
4423
  let watcher;
3814
4424
  try {
3815
- watcher = fs11.watch(dir, (event, name) => {
4425
+ watcher = fs12.watch(dir, (event, name) => {
3816
4426
  if (stopped) return;
3817
4427
  if (name) {
3818
- const changed = path11.join(dir, String(name));
4428
+ const changed = path12.join(dir, String(name));
3819
4429
  if (event === "rename" && watchers.has(changed)) {
3820
4430
  for (const [watchedDir, item] of watchers) {
3821
- if (watchedDir === changed || watchedDir.startsWith(changed + path11.sep)) {
4431
+ if (watchedDir === changed || watchedDir.startsWith(changed + path12.sep)) {
3822
4432
  item.watcher.close();
3823
4433
  watchers.delete(watchedDir);
3824
4434
  }
3825
4435
  }
3826
4436
  }
3827
- const rel = path11.relative(store.root, changed).split(path11.sep).join("/");
4437
+ const rel = path12.relative(store.root, changed).split(path12.sep).join("/");
3828
4438
  if (isIgnoredPath(rel) || !LAYOUT.watches(rel)) return;
3829
4439
  }
3830
4440
  schedule();
@@ -3860,7 +4470,7 @@ function watchArtifact(store, onChange, onError) {
3860
4470
  const { rels } = await collectFiles(store.root, LAYOUT.maxFiles);
3861
4471
  const parts = [];
3862
4472
  for (const rel of rels) {
3863
- const info = await fsp4.lstat(path11.join(store.root, ...rel.split("/"))).catch(() => null);
4473
+ const info = await fsp4.lstat(path12.join(store.root, ...rel.split("/"))).catch(() => null);
3864
4474
  parts.push(info ? `${rel}\0${info.size}\0${info.mtimeMs}\0${info.ino}` : `${rel}\0-`);
3865
4475
  }
3866
4476
  return parts.join("\n");
@@ -3889,23 +4499,23 @@ function watchArtifact(store, onChange, onError) {
3889
4499
  };
3890
4500
  }
3891
4501
  var publishError = (response, status, code, message) => sendJson2(response, status, { code, message });
3892
- async function startServer({ root, port = DEFAULT_PORTS[KIND], host = "127.0.0.1", log = (line) => process.stderr.write(`${line}
3893
- `), agentHook, lan = false, portGiven = false }) {
3894
- const project = path11.join(root, "project");
3895
- const stamp2 = (line) => log(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${line}`);
4502
+ async function mountSite(root, { log: stamp2, network, agentHook }) {
4503
+ const project = path12.join(root, "project");
4504
+ const editorPath = pagePathOf(KIND, root);
3896
4505
  const projectDir = await fsp4.realpath(project);
3897
4506
  const history = new History(root);
3898
4507
  const store = new Store(projectDir, history, stamp2);
3899
4508
  const owner = localOwner();
3900
4509
  const share = new ShareHost({
3901
4510
  root,
4511
+ page: editorPath,
4512
+ network,
3902
4513
  cookieName: guestCookieName(KIND, root),
3903
4514
  owner,
3904
4515
  noun: NOUNS[KIND] ?? { en: NOUN, zh: NOUN, zhThis: NOUN },
3905
4516
  title: () => artifactTitle(root, projectDir),
3906
4517
  log: stamp2,
3907
- createServer: () => http.createServer(handle),
3908
- uiFile: path11.join(EDITOR_DIR, "share-ui.js"),
4518
+ uiFile: path12.join(EDITOR_DIR, "share-ui.js"),
3909
4519
  // The History panel's address and what this person may do there; on a version's page (?at=n), which version.
3910
4520
  globals: (viewer, query) => {
3911
4521
  const at = Number(query.get("at") ?? "");
@@ -3924,8 +4534,6 @@ async function startServer({ root, port = DEFAULT_PORTS[KIND], host = "127.0.0.1
3924
4534
  return viewer ? commentsViewer(viewer) : null;
3925
4535
  }
3926
4536
  });
3927
- const grant = randomBytes2(24).toString("base64url");
3928
- const editorPath = `/${KIND}/${grant}/`;
3929
4537
  const boot = randomBytes2(6).toString("hex");
3930
4538
  const clients = /* @__PURE__ */ new Map();
3931
4539
  const presence = new PresenceHub();
@@ -4056,7 +4664,7 @@ data: ${JSON.stringify(event)}
4056
4664
  const parts = rest === "" ? ["index.html"] : rest.split("/");
4057
4665
  const invalid = parts.some((p) => p === "" || p === "." || p === ".." || p.startsWith(".") || p.includes("\\")) || parts[0] === "node";
4058
4666
  const realDist = invalid ? null : await fsp4.realpath(EDITOR_DIR).catch(() => null);
4059
- const file = realDist === null ? null : path11.join(realDist, ...parts);
4667
+ const file = realDist === null ? null : path12.join(realDist, ...parts);
4060
4668
  const real = file === null ? null : await fsp4.realpath(file).catch(() => null);
4061
4669
  const info = real === null ? null : await fsp4.lstat(real).catch(() => null);
4062
4670
  if (file === null || real !== file || !info?.isFile()) {
@@ -4067,7 +4675,7 @@ data: ${JSON.stringify(event)}
4067
4675
  response.writeHead(404, { "Cache-Control": "no-store" }).end();
4068
4676
  return;
4069
4677
  }
4070
- const ext = path11.extname(file).toLowerCase();
4678
+ const ext = path12.extname(file).toLowerCase();
4071
4679
  const type = STATIC_TYPES[ext] ?? "application/octet-stream";
4072
4680
  if (ext === ".html") {
4073
4681
  const key = parts.join("/");
@@ -4083,7 +4691,8 @@ data: ${JSON.stringify(event)}
4083
4691
  "Content-Length": body.length,
4084
4692
  "Cache-Control": "no-cache",
4085
4693
  "X-Content-Type-Options": "nosniff",
4086
- "Referrer-Policy": "no-referrer",
4694
+ // A root-relative <link> or <a href> finds its way to this artifact by the page's address (hub.ts).
4695
+ "Referrer-Policy": "same-origin",
4087
4696
  "Content-Security-Policy": page.csp
4088
4697
  });
4089
4698
  response.end(request.method === "HEAD" ? void 0 : body);
@@ -4294,7 +4903,7 @@ data: ${JSON.stringify(event)}
4294
4903
  "Content-Length": body.length,
4295
4904
  "Cache-Control": "no-store",
4296
4905
  "X-Content-Type-Options": "nosniff",
4297
- // The address holds the grant, and a README may link anywhere.
4906
+ // A README may link anywhere, and the page (no script, its own origin) asks for nothing by its address.
4298
4907
  "Referrer-Policy": "no-referrer",
4299
4908
  "Content-Security-Policy": DATA_CSP
4300
4909
  });
@@ -4335,48 +4944,18 @@ data: ${JSON.stringify(event)}
4335
4944
  }
4336
4945
  };
4337
4946
  async function handle(request, response) {
4338
- if (!share.hostAllowed(request)) {
4339
- request.resume();
4340
- response.writeHead(421, { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store" }).end("Misdirected request");
4341
- return;
4342
- }
4343
- const url2 = new URL(request.url ?? "/", "http://localhost");
4344
- const pathname = url2.pathname;
4345
- if (pathname === "/favicon.ico") {
4346
- response.writeHead(204).end();
4347
- return;
4348
- }
4349
4947
  if (await share.route(request, response)) return;
4350
4948
  const viewer = share.viewerOf(request);
4351
4949
  if (!viewer) return share.denied(request, response);
4352
- if (pathname === "/" || pathname === "/index.html") {
4353
- response.writeHead(302, { Location: editorPath, "Cache-Control": "no-store" }).end();
4354
- return;
4355
- }
4356
- const rootBlob = BLOB_PATH.exec(pathname.slice(1));
4357
- if (rootBlob) {
4358
- void guard(response, () => serveBlob(request, response, rootBlob[1]));
4359
- return;
4360
- }
4361
- const match = /^\/([a-z-]+)\/([^/]+)(\/.*)?$/.exec(pathname);
4362
- if (!match || match[1] !== KIND || match[2] !== grant) {
4363
- response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store" }).end("Not found");
4364
- return;
4365
- }
4366
- if (match[3] === void 0) {
4367
- response.writeHead(308, { Location: `${editorPath}${url2.search}`, "Cache-Control": "no-store" }).end();
4368
- return;
4369
- }
4370
4950
  let rest;
4371
4951
  try {
4372
- rest = decodeURIComponent(match[3].slice(1));
4952
+ rest = decodeURIComponent(new URL(request.url ?? "/", "http://localhost").pathname.slice(1));
4373
4953
  } catch {
4374
4954
  response.writeHead(400).end();
4375
4955
  return;
4376
4956
  }
4377
- void guard(response, () => route(request, response, rest, viewer));
4957
+ return route(request, response, rest, viewer);
4378
4958
  }
4379
- const server2 = http.createServer((request, response) => void guard(response, () => handle(request, response)));
4380
4959
  const close = () => {
4381
4960
  stopWatch();
4382
4961
  comments.close();
@@ -4384,32 +4963,27 @@ data: ${JSON.stringify(event)}
4384
4963
  presence.close();
4385
4964
  for (const client of clients.keys()) client.end();
4386
4965
  clients.clear();
4387
- server2.close();
4388
- server2.closeAllConnections?.();
4389
4966
  };
4390
- const actualPort = await share.listen(server2, { port, host, lan, portGiven }).catch((error) => {
4391
- close();
4392
- throw error;
4393
- });
4394
- const url = `${serverOrigin(host, actualPort)}${editorPath}`;
4395
- stamp2(`serving ${project} at ${url}`);
4396
- const lanUrls = share.lanUrls();
4397
- return { url, port: actualPort, grant, close, ...lanUrls.length ? { lan: lanUrls } : {} };
4967
+ return {
4968
+ root,
4969
+ page: editorPath,
4970
+ share,
4971
+ handle: (request, response) => guard(response, () => handle(request, response)),
4972
+ hasBlob: async (id) => await store.findBlobFile(id) !== null,
4973
+ close
4974
+ };
4398
4975
  }
4399
4976
  function artifactTitle(root, projectDir) {
4400
4977
  try {
4401
- const index = JSON.parse(fs11.readFileSync(path11.join(projectDir, INDEX_FILE), "utf8"));
4978
+ const index = JSON.parse(fs12.readFileSync(path12.join(projectDir, INDEX_FILE), "utf8"));
4402
4979
  for (const value of [index.title, index.name]) if (typeof value === "string" && value.trim() && value.trim() !== "project") return value.trim();
4403
4980
  } catch {
4404
4981
  }
4405
- return path11.basename(root);
4982
+ return path12.basename(root);
4406
4983
  }
4407
4984
  var runningServer2 = (root) => runningServer(root, KIND, ROUTES.session);
4408
- async function serveForeground({ root, port, host, agentHook, lan, portGiven }) {
4409
- const running = await startServer({ root, port, host, agentHook, lan, portGiven });
4410
- const state = { pid: process.pid, url: running.url, port: running.port, root, kind: KIND, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
4411
- recordServer(state, running.close);
4412
- return { ...state, ...running.lan ? { lan: running.lan } : {} };
4985
+ function serveForeground(options) {
4986
+ return serveHub({ ...options, kind: KIND, script: SCRIPT, portGiven: options.portGiven === true, site: mountSite });
4413
4987
  }
4414
4988
  async function main(argv) {
4415
4989
  const positional = [];
@@ -4418,23 +4992,23 @@ async function main(argv) {
4418
4992
  const arg = argv[i];
4419
4993
  if (!arg.startsWith("--")) positional.push(arg);
4420
4994
  else if (arg.includes("=")) flags.set(arg.slice(2, arg.indexOf("=")), arg.slice(arg.indexOf("=") + 1));
4421
- else if (arg === "--lan") flags.set("lan", "true");
4995
+ else if (arg === "--lan" || arg === "--background") flags.set(arg.slice(2), "true");
4422
4996
  else flags.set(arg.slice(2), argv[++i] ?? "");
4423
4997
  }
4424
4998
  if (positional.length !== 1 || flags.has("help")) {
4425
4999
  process.stderr.write(
4426
- `Usage: node ${path11.basename(SCRIPT)} <root> [--port n] [--host h] [--lan] [--agent-hook "<command>"]
5000
+ `Usage: node ${path12.basename(SCRIPT)} <root> [--port n] [--host h] [--lan] [--agent-hook "<command>"]
4427
5001
  <root> holds project/${INDEX_FILE}.
4428
5002
  `
4429
5003
  );
4430
5004
  return 2;
4431
5005
  }
4432
- let root = path11.resolve(positional[0]);
4433
- if (path11.basename(root) === "project" && fs11.existsSync(path11.join(root, INDEX_FILE)) && !fs11.existsSync(path11.join(root, "project"))) {
4434
- root = path11.dirname(root);
5006
+ let root = path12.resolve(positional[0]);
5007
+ if (path12.basename(root) === "project" && fs12.existsSync(path12.join(root, INDEX_FILE)) && !fs12.existsSync(path12.join(root, "project"))) {
5008
+ root = path12.dirname(root);
4435
5009
  }
4436
- if (!fs11.statSync(path11.join(root, "project", INDEX_FILE), { throwIfNoEntry: false })?.isFile()) {
4437
- process.stderr.write(`${JSON.stringify({ error: "NOT_FOUND", message: `${path11.join(root, "project", INDEX_FILE)} does not exist.` })}
5010
+ if (!fs12.statSync(path12.join(root, "project", INDEX_FILE), { throwIfNoEntry: false })?.isFile()) {
5011
+ process.stderr.write(`${JSON.stringify({ error: "NOT_FOUND", message: `${path12.join(root, "project", INDEX_FILE)} does not exist.` })}
4438
5012
  `);
4439
5013
  return 2;
4440
5014
  }
@@ -4451,14 +5025,14 @@ async function main(argv) {
4451
5025
  return 0;
4452
5026
  }
4453
5027
  const agentHook = flags.get("agent-hook") || void 0;
4454
- const state = await serveForeground({ root, port, host: flags.get("host") ?? "127.0.0.1", agentHook, lan: flags.has("lan"), portGiven: flags.has("port") });
5028
+ const state = await serveForeground({ root, port, host: flags.get("host") ?? "127.0.0.1", agentHook, lan: flags.has("lan"), portGiven: flags.has("port"), background: flags.has("background") });
4455
5029
  process.stdout.write(`${JSON.stringify({ url: state.url, root, pid: state.pid, ...state.lan ? { lan: state.lan } : {} }, null, 2)}
4456
5030
  `);
4457
- return -1;
5031
+ return state.joined && flags.has("background") ? 0 : -1;
4458
5032
  }
4459
5033
  function isMain(moduleUrl) {
4460
5034
  try {
4461
- return Boolean(process.argv[1]) && pathToFileURL(fs11.realpathSync(process.argv[1])).href === moduleUrl;
5035
+ return Boolean(process.argv[1]) && pathToFileURL(fs12.realpathSync(process.argv[1])).href === moduleUrl;
4462
5036
  } catch {
4463
5037
  return false;
4464
5038
  }
@@ -4475,7 +5049,7 @@ function createArtifactServer(kind, moduleUrl) {
4475
5049
  kindLabels = kind.labels;
4476
5050
  kindAgentContext = kind.agentContext;
4477
5051
  SCRIPT = fileURLToPath(moduleUrl);
4478
- EDITOR_DIR = path11.join(path11.dirname(SCRIPT), "editor");
5052
+ EDITOR_DIR = path12.join(path12.dirname(SCRIPT), "editor");
4479
5053
  const runIfMain = () => {
4480
5054
  if (!isMain(moduleUrl)) return;
4481
5055
  main(process.argv.slice(2)).then(
@@ -4490,7 +5064,7 @@ function createArtifactServer(kind, moduleUrl) {
4490
5064
  }
4491
5065
  );
4492
5066
  };
4493
- return { startServer, runningServer: runningServer2, serveForeground, readState, runIfMain };
5067
+ return { mountSite, runningServer: runningServer2, serveForeground, readState, runIfMain };
4494
5068
  }
4495
5069
 
4496
5070
  var KIND2 = "slides";
@@ -4526,19 +5100,19 @@ function commentLocation2(input, projectDir) {
4526
5100
  let slideId = match[2];
4527
5101
  if (!slideId) {
4528
5102
  try {
4529
- const order = JSON.parse(fs12.readFileSync(path12.join(projectDir, INDEX_FILE2), "utf8")).order;
5103
+ const order = JSON.parse(fs13.readFileSync(path13.join(projectDir, INDEX_FILE2), "utf8")).order;
4530
5104
  if (Array.isArray(order) && typeof order[slide - 1] === "string") slideId = order[slide - 1];
4531
5105
  } catch {
4532
5106
  }
4533
5107
  }
4534
5108
  const file = slideId ? projectFile(`slides/${slideId}.html`) : void 0;
4535
- if (slideId && file && fs12.existsSync(path12.join(projectDir, "slides", `${slideId}.html`))) return { file, slide, slideId };
5109
+ if (slideId && file && fs13.existsSync(path13.join(projectDir, "slides", `${slideId}.html`))) return { file, slide, slideId };
4536
5110
  return { file: "project/deck.json", slide };
4537
5111
  }
4538
5112
  function labels(projectDir) {
4539
5113
  let order = [];
4540
5114
  try {
4541
- const listed = JSON.parse(fs12.readFileSync(path12.join(projectDir, INDEX_FILE2), "utf8")).order;
5115
+ const listed = JSON.parse(fs13.readFileSync(path13.join(projectDir, INDEX_FILE2), "utf8")).order;
4542
5116
  if (Array.isArray(listed)) order = listed.filter((id) => typeof id === "string");
4543
5117
  } catch {
4544
5118
  }
@@ -4558,7 +5132,7 @@ function agentContext(projectDir, rels) {
4558
5132
  if (!slideId) return null;
4559
5133
  let order = [];
4560
5134
  try {
4561
- const listed = JSON.parse(fs12.readFileSync(path12.join(projectDir, INDEX_FILE2), "utf8")).order;
5135
+ const listed = JSON.parse(fs13.readFileSync(path13.join(projectDir, INDEX_FILE2), "utf8")).order;
4562
5136
  if (Array.isArray(listed)) order = listed.filter((id) => typeof id === "string");
4563
5137
  } catch {
4564
5138
  }
@@ -4585,15 +5159,15 @@ var server = createArtifactServer(
4585
5159
  },
4586
5160
  import.meta.url
4587
5161
  );
4588
- var { startServer: startServer2, serveForeground: serveForeground2, runningServer: runningServer3, readState: readState2 } = server;
5162
+ var { mountSite: mountSite2, serveForeground: serveForeground2, runningServer: runningServer3, readState: readState2 } = server;
4589
5163
  server.runIfMain();
4590
5164
  export {
4591
5165
  INDEX_FILE2 as INDEX_FILE,
4592
5166
  KIND2 as KIND,
4593
5167
  LAYOUT2 as LAYOUT,
4594
5168
  NOUN2 as NOUN,
5169
+ mountSite2 as mountSite,
4595
5170
  readState2 as readState,
4596
5171
  runningServer3 as runningServer,
4597
- serveForeground2 as serveForeground,
4598
- startServer2 as startServer
5172
+ serveForeground2 as serveForeground
4599
5173
  };