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