dsh-mobile 0.5.3 → 0.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.mjs CHANGED
@@ -16,6 +16,7 @@ import { promisify } from "node:util";
16
16
  import { createGzip, gzip } from "node:zlib";
17
17
  import Bonjour from "bonjour-service";
18
18
  import * as QRCode from "qrcode";
19
+ import WebSocket, { WebSocketServer } from "ws";
19
20
  import { Logger, Service } from "@deepseek-ai/cordis";
20
21
  import { connect as connect$1 } from "node:tls";
21
22
  import { generate } from "selfsigned";
@@ -241,15 +242,10 @@ var AccessController = class {
241
242
  /** Exchange a valid persistent device credential for a new short Session. */
242
243
  async renew(deviceToken) {
243
244
  this.requireInitialized();
244
- if (deviceToken.length > 512) throw new AccessError(401, "authentication_failed");
245
245
  return this.exclusive(async () => {
246
246
  const now = this.now();
247
- const tokenDigest = digest(deviceToken);
248
- const index = this.devices.findIndex((device) => timingSafeEqual(Buffer.from(device.tokenDigest, "hex"), tokenDigest));
249
- const device = this.devices[index];
250
- if (device === void 0) throw new AccessError(401, "authentication_failed");
251
- if (device.revokedAt !== void 0) throw new AccessError(401, "device_revoked");
252
- if (device.expiresAt <= now) throw new AccessError(401, "device_expired");
247
+ const device = this.requireDevice(deviceToken, now);
248
+ const index = this.devices.indexOf(device);
253
249
  const updated = Object.freeze({
254
250
  ...device,
255
251
  lastSeenAt: now
@@ -264,15 +260,10 @@ var AccessController = class {
264
260
  /** Validate a persistent device credential without consuming a Session slot. */
265
261
  async probe(deviceToken) {
266
262
  this.requireInitialized();
267
- if (deviceToken.length > 512) throw new AccessError(401, "authentication_failed");
268
263
  return this.exclusive(async () => {
269
264
  const now = this.now();
270
- const tokenDigest = digest(deviceToken);
271
- const index = this.devices.findIndex((device) => timingSafeEqual(Buffer.from(device.tokenDigest, "hex"), tokenDigest));
272
- const device = this.devices[index];
273
- if (device === void 0) throw new AccessError(401, "authentication_failed");
274
- if (device.revokedAt !== void 0) throw new AccessError(401, "device_revoked");
275
- if (device.expiresAt <= now) throw new AccessError(401, "device_expired");
265
+ const device = this.requireDevice(deviceToken, now);
266
+ const index = this.devices.indexOf(device);
276
267
  const updated = Object.freeze({
277
268
  ...device,
278
269
  lastSeenAt: now
@@ -287,6 +278,24 @@ var AccessController = class {
287
278
  });
288
279
  });
289
280
  }
281
+ /** Validate a device credential without writing state or opening a Session. */
282
+ authorizeDevice(deviceToken) {
283
+ this.requireInitialized();
284
+ const device = this.requireDevice(deviceToken, this.now());
285
+ return Object.freeze({
286
+ deviceId: device.id,
287
+ deviceExpiresAt: device.expiresAt
288
+ });
289
+ }
290
+ requireDevice(deviceToken, now) {
291
+ if (deviceToken.length > 512) throw new AccessError(401, "authentication_failed");
292
+ const tokenDigest = digest(deviceToken);
293
+ const device = this.devices.find((candidate) => timingSafeEqual(Buffer.from(candidate.tokenDigest, "hex"), tokenDigest));
294
+ if (device === void 0) throw new AccessError(401, "authentication_failed");
295
+ if (device.revokedAt !== void 0) throw new AccessError(401, "device_revoked");
296
+ if (device.expiresAt <= now) throw new AccessError(401, "device_expired");
297
+ return device;
298
+ }
290
299
  /** Resolve a short Session Cookie without revealing whether device or Session failed. */
291
300
  authorizeSession(sessionToken) {
292
301
  this.requireInitialized();
@@ -745,6 +754,112 @@ async function probeTcpReachable(host, port, timeoutMs = 1500) {
745
754
  });
746
755
  }
747
756
  //#endregion
757
+ //#region src/websocket-paths.ts
758
+ /** Cap the admin-approved extra WebSocket upgrade paths (exact pathnames). */
759
+ const MAX_EXTRA_WEBSOCKET_PATHS = 16;
760
+ const MAX_WEBSOCKET_PATH_LENGTH = 256;
761
+ function fail(code) {
762
+ throw new Error(code);
763
+ }
764
+ /**
765
+ * Validate one exact pathname for proxying. Query strings are matched at
766
+ * upgrade time, so only the pathname is stored. Rejects anything the
767
+ * gateway cannot match exactly.
768
+ */
769
+ function validateWebSocketPath(value) {
770
+ if (typeof value !== "string") fail("websocket_path_invalid");
771
+ const path = value;
772
+ if (path.length === 0 || path.length > 256) fail("websocket_path_invalid");
773
+ if (!path.startsWith("/")) fail("websocket_path_invalid");
774
+ if (/[\s\u0000-\u001f\u007f#?]/u.test(path)) fail("websocket_path_invalid");
775
+ if (path.includes("..")) fail("websocket_path_invalid");
776
+ return path;
777
+ }
778
+ /** Validate a whole replacement list all-or-nothing; duplicates collapse. */
779
+ function normalizeWebSocketPaths(value) {
780
+ if (!Array.isArray(value)) fail("websocket_paths_invalid");
781
+ if (value.length > 16) fail("websocket_paths_invalid");
782
+ const seen = /* @__PURE__ */ new Set();
783
+ for (const entry of value) {
784
+ const path = validateWebSocketPath(entry);
785
+ seen.add(path);
786
+ }
787
+ return [...seen];
788
+ }
789
+ /** Cap remembered rejected upgrade paths offered for one-click approval. */
790
+ const MAX_BLOCKED_UPGRADE_PATHS = 32;
791
+ /**
792
+ * In-memory log of rejected third-party upgrade paths, shared by every
793
+ * gateway instance so the approval UI sees attempts on any listener.
794
+ * Bounded and newest-first; a restart clears it (attempts reappear on
795
+ * the next blocked handshake).
796
+ */
797
+ var BlockedUpgradePathLog = class {
798
+ entries = /* @__PURE__ */ new Map();
799
+ record(pathname) {
800
+ const now = Date.now();
801
+ const stats = this.entries.get(pathname);
802
+ if (stats === void 0) {
803
+ if (this.entries.size >= 32) {
804
+ let oldest;
805
+ for (const [path, entry] of this.entries) if (oldest === void 0 || entry.lastSeen < (this.entries.get(oldest)?.lastSeen ?? 0)) oldest = path;
806
+ if (oldest !== void 0) this.entries.delete(oldest);
807
+ }
808
+ this.entries.set(pathname, {
809
+ attempts: 1,
810
+ firstSeen: now,
811
+ lastSeen: now
812
+ });
813
+ return;
814
+ }
815
+ stats.attempts += 1;
816
+ stats.lastSeen = now;
817
+ }
818
+ report() {
819
+ return [...this.entries].map(([path, stats]) => ({
820
+ path,
821
+ ...stats
822
+ })).sort((a, b) => b.lastSeen - a.lastSeen);
823
+ }
824
+ };
825
+ /** File-backed store shared by every gateway instance (LAN and remote). */
826
+ var WebSocketPathStore = class {
827
+ file;
828
+ paths = /* @__PURE__ */ new Set();
829
+ loaded = false;
830
+ constructor(file) {
831
+ this.file = file;
832
+ }
833
+ /** Snapshot for the upgrade check. */
834
+ has(pathname) {
835
+ return this.paths.has(pathname);
836
+ }
837
+ list() {
838
+ return [...this.paths];
839
+ }
840
+ async load() {
841
+ if (!this.loaded) {
842
+ try {
843
+ const raw = JSON.parse(await readFile(this.file, "utf8"));
844
+ this.paths = new Set(normalizeWebSocketPaths(raw.paths ?? []));
845
+ } catch {
846
+ this.paths = /* @__PURE__ */ new Set();
847
+ }
848
+ this.loaded = true;
849
+ }
850
+ return this.list();
851
+ }
852
+ /** Replace the whole list after validating; persists atomically. */
853
+ async replace(paths) {
854
+ const next = normalizeWebSocketPaths([...paths]);
855
+ await mkdir(dirname(this.file), { recursive: true });
856
+ await writeFile(this.file, `${JSON.stringify({ paths: next })}\n`, "utf8");
857
+ this.paths = new Set(next);
858
+ this.loaded = true;
859
+ return this.list();
860
+ }
861
+ };
862
+ //#endregion
748
863
  //#region src/config.ts
749
864
  /** Loader-facing defaults; {@link parseGatewayConfig} enforces cross-field security rules. */
750
865
  const Config = z.object({
@@ -762,6 +877,14 @@ const Config = z.object({
762
877
  mobileLayoutFile: z.string().hidden(),
763
878
  mobileCompatibilityFile: z.string().hidden(),
764
879
  excludedClientModules: z.array(String).default([]),
880
+ websocketCompression: z.object({
881
+ paths: z.array(String).default([]),
882
+ maxMessageBytes: z.natural(),
883
+ maxQueuedBytes: z.natural(),
884
+ thresholdBytes: z.natural(),
885
+ concurrencyLimit: z.natural(),
886
+ level: z.natural().max(9)
887
+ }),
765
888
  instanceId: z.string().hidden(),
766
889
  pairingCaFile: z.string().hidden(),
767
890
  initiallyEnabled: z.boolean().hidden().required(),
@@ -790,6 +913,21 @@ function integer(value, name, fallback, minimum, maximum) {
790
913
  if (typeof resolved !== "number" || !Number.isSafeInteger(resolved) || resolved < minimum || resolved > maximum) throw new Error(`${name} must be an integer from ${String(minimum)} through ${String(maximum)}`);
791
914
  return resolved;
792
915
  }
916
+ function parseWebSocketCompression(raw) {
917
+ if (raw !== void 0 && (typeof raw !== "object" || raw === null || Array.isArray(raw))) throw new Error("websocketCompression must be an object");
918
+ const value = raw ?? {};
919
+ const maxMessageBytes = integer(value.maxMessageBytes, "websocketCompression.maxMessageBytes", 33554432, 1024, 268435456);
920
+ const maxQueuedBytes = integer(value.maxQueuedBytes, "websocketCompression.maxQueuedBytes", 67108864, 1024, 268435456);
921
+ if (maxQueuedBytes < maxMessageBytes + 14) throw new Error("websocketCompression.maxQueuedBytes must cover maxMessageBytes and the frame header");
922
+ return Object.freeze({
923
+ paths: Object.freeze(normalizeWebSocketPaths(value.paths ?? [])),
924
+ maxMessageBytes,
925
+ maxQueuedBytes,
926
+ thresholdBytes: integer(value.thresholdBytes, "websocketCompression.thresholdBytes", 1024, 0, maxMessageBytes),
927
+ concurrencyLimit: integer(value.concurrencyLimit, "websocketCompression.concurrencyLimit", 4, 1, 32),
928
+ level: integer(value.level, "websocketCompression.level", 3, 0, 9)
929
+ });
930
+ }
793
931
  function stringArray(value, name) {
794
932
  if (!Array.isArray(value) || value.length === 0 || value.some((entry) => typeof entry !== "string")) throw new Error(`${name} must be a non-empty string array`);
795
933
  return value;
@@ -896,6 +1034,7 @@ function parseGatewayConfig(raw) {
896
1034
  mobileLayoutFile: value.mobileLayoutFile === void 0 ? fileURLToPath(new URL("./mobile-layout.js", import.meta.url)) : absoluteFile(value.mobileLayoutFile, "mobileLayoutFile"),
897
1035
  mobileCompatibilityFile: value.mobileCompatibilityFile === void 0 ? fileURLToPath(new URL("./mobile-compat.js", import.meta.url)) : absoluteFile(value.mobileCompatibilityFile, "mobileCompatibilityFile"),
898
1036
  excludedClientModules: excludedClientModules(value.excludedClientModules),
1037
+ websocketCompression: parseWebSocketCompression(value.websocketCompression),
899
1038
  instanceId: value.instanceId === void 0 ? createHash("sha256").update(absoluteFile(value.stateFile, "stateFile")).digest("hex") : /^[a-f\d]{64}$/u.test(value.instanceId) ? value.instanceId : (() => {
900
1039
  throw new Error("instanceId must be a lowercase SHA-256 value");
901
1040
  })(),
@@ -1402,21 +1541,6 @@ async function readJsonObject(request, maximumBytes) {
1402
1541
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new HttpError(400, "bad_request");
1403
1542
  return parsed;
1404
1543
  }
1405
- /** Strict cookie parser: malformed or duplicate names invalidate the whole header. */
1406
- function parseCookies(header) {
1407
- if (header === void 0) return /* @__PURE__ */ new Map();
1408
- if (header.length > 8192) return void 0;
1409
- const cookies = /* @__PURE__ */ new Map();
1410
- for (const part of header.split(";")) {
1411
- const equals = part.indexOf("=");
1412
- if (equals <= 0) return void 0;
1413
- const name = part.slice(0, equals).trim();
1414
- const value = part.slice(equals + 1).trim();
1415
- if (!/^[!#$%&'*+\-.^_`|~\dA-Za-z]+$/u.test(name) || !/^[\w\-.~+/=]*$/u.test(value) || cookies.has(name)) return;
1416
- cookies.set(name, value);
1417
- }
1418
- return cookies;
1419
- }
1420
1544
  /** Serialize a host-only Cookie with no Domain attribute. */
1421
1545
  function cookie(name, value, options) {
1422
1546
  const parts = [
@@ -1516,6 +1640,280 @@ function ensureMobileCompatibility(html) {
1516
1640
  return `${html.slice(0, first.index)}${script}${html.slice(first.index)}`;
1517
1641
  }
1518
1642
  //#endregion
1643
+ //#region src/browser-auth-cookies.ts
1644
+ const HOST_SESSION_COOKIE = "__Host-dsh_ma_session";
1645
+ const HOST_CSRF_COOKIE = "__Host-dsh_ma_csrf";
1646
+ /** Non-secret browser identity; the persistent device credential keeps its narrow renewal path. */
1647
+ const HOST_DEVICE_COOKIE = "__Host-dsh_ma_device_id";
1648
+ const AUTH_COOKIE_NAMES = /* @__PURE__ */ new Set([
1649
+ SESSION_COOKIE,
1650
+ DEVICE_COOKIE,
1651
+ CSRF_COOKIE,
1652
+ HOST_SESSION_COOKIE,
1653
+ HOST_CSRF_COOKIE,
1654
+ HOST_DEVICE_COOKIE
1655
+ ]);
1656
+ /** Read only gateway authentication cookies, retaining distinct values from different scopes. */
1657
+ function browserAuthCookies(header) {
1658
+ if (header === void 0) return /* @__PURE__ */ new Map();
1659
+ if (header.length > 8192) throw new HttpError(401, "authentication_failed");
1660
+ const cookies = /* @__PURE__ */ new Map();
1661
+ for (const part of header.split(";")) {
1662
+ const equals = part.indexOf("=");
1663
+ if (equals <= 0) continue;
1664
+ const name = part.slice(0, equals).trim();
1665
+ if (!AUTH_COOKIE_NAMES.has(name)) continue;
1666
+ const values = cookies.get(name) ?? [];
1667
+ cookies.set(name, values);
1668
+ const value = part.slice(equals + 1).trim();
1669
+ if (!/^[\w\-.~+/=]{1,512}$/u.test(value) || values.includes(value)) continue;
1670
+ values.push(value);
1671
+ }
1672
+ return cookies;
1673
+ }
1674
+ /** Select exactly one authenticated value; never let cookie order choose between valid identities. */
1675
+ function authenticatedCookie(values, authenticate, accepts = () => true) {
1676
+ let selected;
1677
+ for (const value of values) {
1678
+ let result;
1679
+ try {
1680
+ result = authenticate(value);
1681
+ } catch (error) {
1682
+ if (error instanceof AccessError && error.status === 401) continue;
1683
+ throw error;
1684
+ }
1685
+ if (!accepts(result)) continue;
1686
+ if (selected !== void 0) throw new HttpError(401, "authentication_failed");
1687
+ selected = {
1688
+ value,
1689
+ result
1690
+ };
1691
+ }
1692
+ if (selected === void 0) throw new HttpError(401, "authentication_failed");
1693
+ return selected;
1694
+ }
1695
+ //#endregion
1696
+ //#region src/websocket-compression.ts
1697
+ function payloadBytes(data) {
1698
+ if (Array.isArray(data)) return data.reduce((bytes, part) => bytes + part.byteLength, 0);
1699
+ return data.byteLength;
1700
+ }
1701
+ function forwardableClose(code) {
1702
+ return code >= 1e3 && code <= 1014 && code !== 1004 && code !== 1005 && code !== 1006 || code >= 3e3 && code <= 4999;
1703
+ }
1704
+ /**
1705
+ * Bridge an authorized HTTP upgrade, negotiating compression only with the client.
1706
+ * Errors after acceptance close both legs. Socket destruction by the gateway also
1707
+ * releases the ws wrappers, pending compression, and handshake/close timers.
1708
+ * @param request - validated browser upgrade request.
1709
+ * @param client - gateway-owned client socket, paused during authorization.
1710
+ * @param upstream - gateway-owned loopback socket, connecting or connected.
1711
+ * @param head - bytes the HTTP parser read after the client's upgrade headers.
1712
+ * @param target - validated upstream URL, including the original query.
1713
+ * @param upstreamCookie - gateway's signed upstream cookie; browser cookies are discarded.
1714
+ * @param options - resolved message, queue, and compression limits.
1715
+ * @param handshakeTimeoutMs - maximum handshake and close-handshake duration.
1716
+ * @returns fulfillment after both HTTP upgrades are accepted, or rejection before acceptance.
1717
+ */
1718
+ function bridgeCompressedWebSocket(request, client, upstream, head, target, upstreamCookie, options, handshakeTimeoutMs) {
1719
+ return new Promise((resolve, reject) => {
1720
+ let incoming;
1721
+ let outgoing;
1722
+ let server;
1723
+ let accepted = false;
1724
+ let stopped = false;
1725
+ let closing = false;
1726
+ let closeTimer;
1727
+ const handshakeTimer = setTimeout(() => {
1728
+ stop(/* @__PURE__ */ new Error("WebSocket bridge handshake timed out"));
1729
+ }, handshakeTimeoutMs);
1730
+ handshakeTimer.unref();
1731
+ const stop = (error) => {
1732
+ if (stopped) return;
1733
+ stopped = true;
1734
+ clearTimeout(handshakeTimer);
1735
+ clearTimeout(closeTimer);
1736
+ if (!accepted) reject(error);
1737
+ incoming?.terminate();
1738
+ outgoing?.terminate();
1739
+ client.destroy();
1740
+ upstream.destroy();
1741
+ server?.close();
1742
+ };
1743
+ const finish = () => {
1744
+ if (incoming?.readyState === WebSocket.CLOSED && outgoing?.readyState === WebSocket.CLOSED) stop(/* @__PURE__ */ new Error("WebSocket bridge closed"));
1745
+ };
1746
+ const beginClose = (code, reason) => {
1747
+ if (!accepted) {
1748
+ stop(new Error(reason));
1749
+ return;
1750
+ }
1751
+ if (stopped || closing) return;
1752
+ closing = true;
1753
+ incoming?.resume();
1754
+ outgoing?.resume();
1755
+ for (const socket of [incoming, outgoing]) if (socket?.readyState === WebSocket.OPEN) socket.close(code, reason);
1756
+ closeTimer = setTimeout(() => {
1757
+ stop(/* @__PURE__ */ new Error("WebSocket bridge close timed out"));
1758
+ }, handshakeTimeoutMs);
1759
+ closeTimer.unref();
1760
+ };
1761
+ const socketError = (error) => {
1762
+ stop(error);
1763
+ };
1764
+ const socketClosed = () => {
1765
+ if (!accepted) stop(/* @__PURE__ */ new Error("WebSocket socket closed before acceptance"));
1766
+ };
1767
+ client.on("error", socketError);
1768
+ upstream.on("error", socketError);
1769
+ client.once("close", socketClosed);
1770
+ upstream.once("close", socketClosed);
1771
+ const wsError = (error) => {
1772
+ if (!accepted) {
1773
+ stop(error);
1774
+ return;
1775
+ }
1776
+ beginClose(error.code === "WS_ERR_UNSUPPORTED_MESSAGE_LENGTH" ? 1009 : 1011, error.code === "WS_ERR_UNSUPPORTED_MESSAGE_LENGTH" ? "Message too large" : "WebSocket bridge failed");
1777
+ };
1778
+ const attachRelay = (source, destination) => {
1779
+ let queuedBytes = 0;
1780
+ const send = (bytes, operation) => {
1781
+ if (stopped || closing) return;
1782
+ if (destination.readyState !== WebSocket.OPEN) {
1783
+ beginClose(1011, "WebSocket peer unavailable");
1784
+ return;
1785
+ }
1786
+ const queued = bytes + 14;
1787
+ if (queued > options.maxQueuedBytes - queuedBytes) {
1788
+ beginClose(1009, "WebSocket queue limit");
1789
+ return;
1790
+ }
1791
+ queuedBytes += queued;
1792
+ source.pause();
1793
+ const complete = (error) => {
1794
+ queuedBytes -= queued;
1795
+ if (error != null) {
1796
+ wsError(error);
1797
+ return;
1798
+ }
1799
+ if (!stopped && !closing && queuedBytes === 0 && source.readyState === WebSocket.OPEN) source.resume();
1800
+ };
1801
+ try {
1802
+ operation(complete);
1803
+ } catch (error) {
1804
+ complete(error instanceof Error ? error : /* @__PURE__ */ new Error("WebSocket forwarding failed"));
1805
+ }
1806
+ };
1807
+ source.on("message", (data, binary) => {
1808
+ const bytes = payloadBytes(data);
1809
+ if (bytes > options.maxMessageBytes) {
1810
+ beginClose(1009, "Message too large");
1811
+ return;
1812
+ }
1813
+ send(bytes, (callback) => {
1814
+ destination.send(data, {
1815
+ binary,
1816
+ compress: destination === incoming
1817
+ }, callback);
1818
+ });
1819
+ });
1820
+ source.on("ping", (data) => {
1821
+ send(data.byteLength, (callback) => {
1822
+ destination.ping(data, void 0, callback);
1823
+ });
1824
+ });
1825
+ source.on("pong", (data) => {
1826
+ send(data.byteLength, (callback) => {
1827
+ destination.pong(data, void 0, callback);
1828
+ });
1829
+ });
1830
+ source.on("close", (code, reason) => {
1831
+ if (stopped) return;
1832
+ if (code === 1006 || !forwardableClose(code) && code !== 1005) {
1833
+ stop(/* @__PURE__ */ new Error("WebSocket peer closed without a valid close frame"));
1834
+ return;
1835
+ }
1836
+ closing = true;
1837
+ destination.resume();
1838
+ if (destination.readyState === WebSocket.OPEN) {
1839
+ if (code === 1005) destination.close();
1840
+ else destination.close(code, reason);
1841
+ }
1842
+ closeTimer ??= setTimeout(() => {
1843
+ stop(/* @__PURE__ */ new Error("WebSocket bridge close timed out"));
1844
+ }, handshakeTimeoutMs);
1845
+ closeTimer.unref();
1846
+ finish();
1847
+ });
1848
+ };
1849
+ try {
1850
+ if (client.destroyed || upstream.destroyed) throw new Error("WebSocket socket is already closed");
1851
+ const protocolHeader = request.headers["sec-websocket-protocol"];
1852
+ const protocols = typeof protocolHeader === "string" ? protocolHeader.split(",").map((protocol) => protocol.trim()) : [];
1853
+ const url = new URL(target);
1854
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
1855
+ outgoing = new WebSocket(url, protocols, {
1856
+ createConnection: () => upstream,
1857
+ handshakeTimeout: handshakeTimeoutMs,
1858
+ perMessageDeflate: false,
1859
+ autoPong: false,
1860
+ followRedirects: false,
1861
+ maxPayload: options.maxMessageBytes,
1862
+ headers: {
1863
+ host: target.host,
1864
+ origin: target.origin,
1865
+ "sec-fetch-site": "same-origin",
1866
+ ...upstreamCookie === void 0 ? {} : { cookie: upstreamCookie }
1867
+ }
1868
+ });
1869
+ outgoing.on("error", wsError);
1870
+ outgoing.once("close", () => {
1871
+ if (!accepted) stop(/* @__PURE__ */ new Error("Upstream WebSocket closed before acceptance"));
1872
+ else finish();
1873
+ });
1874
+ outgoing.once("open", () => {
1875
+ if (stopped || client.destroyed) {
1876
+ stop(/* @__PURE__ */ new Error("Client closed before WebSocket acceptance"));
1877
+ return;
1878
+ }
1879
+ upstream.setTimeout(0);
1880
+ server = new WebSocketServer({
1881
+ noServer: true,
1882
+ clientTracking: false,
1883
+ autoPong: false,
1884
+ maxPayload: options.maxMessageBytes,
1885
+ handleProtocols: () => outgoing?.protocol || false,
1886
+ perMessageDeflate: {
1887
+ clientNoContextTakeover: true,
1888
+ serverNoContextTakeover: true,
1889
+ threshold: options.thresholdBytes,
1890
+ concurrencyLimit: options.concurrencyLimit,
1891
+ zlibDeflateOptions: { level: options.level }
1892
+ }
1893
+ });
1894
+ server.on("error", wsError);
1895
+ try {
1896
+ server.handleUpgrade(request, client, head, (socket) => {
1897
+ incoming = socket;
1898
+ incoming.on("error", wsError);
1899
+ incoming.once("close", finish);
1900
+ attachRelay(incoming, outgoing);
1901
+ attachRelay(outgoing, incoming);
1902
+ accepted = true;
1903
+ clearTimeout(handshakeTimer);
1904
+ incoming.resume();
1905
+ resolve();
1906
+ });
1907
+ } catch (error) {
1908
+ stop(error instanceof Error ? error : /* @__PURE__ */ new Error("Client WebSocket upgrade failed"));
1909
+ }
1910
+ });
1911
+ } catch (error) {
1912
+ stop(error instanceof Error ? error : /* @__PURE__ */ new Error("WebSocket bridge setup failed"));
1913
+ }
1914
+ });
1915
+ }
1916
+ //#endregion
1519
1917
  //#region src/version.ts
1520
1918
  const manifest = createRequire(import.meta.url)("../package.json");
1521
1919
  /** Version of the installed DSH Mobile plugin package. */
@@ -2469,6 +2867,7 @@ function renderPairPage(locale) {
2469
2867
  <html lang="${copy.lang}">
2470
2868
  <meta charset="utf-8">
2471
2869
  <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
2870
+ <style>input,textarea{font-size:16px}</style>
2472
2871
  <title>${copy.pairTitle}</title>
2473
2872
  <main>
2474
2873
  <h1>${copy.pairHeading}</h1>
@@ -2577,8 +2976,6 @@ function renderLoginScript(locale) {
2577
2976
  //#region src/gateway.ts
2578
2977
  const MAX_CONTROL_BODY_BYTES = 16384;
2579
2978
  const MAX_HEADER_BYTES = 16384;
2580
- const MOBILE_HISTORY_PAGE_MESSAGES = 10;
2581
- const SESSION_HISTORY_PATH = "/api/session.history";
2582
2979
  const DISCOVERY_QUERY = Buffer.from("DSH_MOBILE_DISCOVER_V1", "ascii");
2583
2980
  const DISCOVERY_PROTOCOL = 1;
2584
2981
  const DISCOVERY_INTERVAL_MS = 3e3;
@@ -2664,7 +3061,9 @@ const MOBILE_LAYOUT_DEPENDENCY_PROFILES = Object.freeze([Object.freeze({
2664
3061
  "@deepseek-ai/dsh-client-ui-theme"
2665
3062
  ])
2666
3063
  })]);
2667
- const MOBILE_CSRF_FETCH_BOOTSTRAP = `(()=>{const nativeFetch=window.fetch.bind(window);window.fetch=(input,init)=>{const source=input instanceof Request?input:undefined;const method=String(init?.method??source?.method??'GET').toUpperCase();if(method==='GET'||method==='HEAD')return nativeFetch(input,init);const raw=typeof input==='string'?input:input instanceof URL?input.href:source?.url;if(raw===undefined||new URL(raw,location.href).origin!==location.origin)return nativeFetch(input,init);const headers=new Headers(init?.headers??source?.headers);if(!headers.has(${JSON.stringify(CSRF_HEADER)})){const prefix=${JSON.stringify(`${CSRF_COOKIE}=`)};const token=document.cookie.split(';').map(value=>value.trim()).find(value=>value.startsWith(prefix))?.slice(prefix.length);if(token!==undefined)headers.set(${JSON.stringify(CSRF_HEADER)},token)}return nativeFetch(input,{...init,headers})};})();`;
3064
+ function mobileCsrfFetchBootstrap(cookieName) {
3065
+ return `(()=>{const nativeFetch=window.fetch.bind(window);window.fetch=(input,init)=>{const source=input instanceof Request?input:undefined;const method=String(init?.method??source?.method??'GET').toUpperCase();if(method==='GET'||method==='HEAD')return nativeFetch(input,init);const raw=typeof input==='string'?input:input instanceof URL?input.href:source?.url;if(raw===undefined||new URL(raw,location.href).origin!==location.origin)return nativeFetch(input,init);const headers=new Headers(init?.headers??source?.headers);if(!headers.has(${JSON.stringify(CSRF_HEADER)})){const prefix=${JSON.stringify(`${cookieName}=`)};const values=[...new Set(document.cookie.split(';').map(value=>value.trim()).filter(value=>value.startsWith(prefix)).map(value=>value.slice(prefix.length)))];if(values.length===1)headers.set(${JSON.stringify(CSRF_HEADER)},values[0])}return nativeFetch(input,{...init,headers})};})();`;
3066
+ }
2668
3067
  const MOBILE_AUTHENTICATED_TRANSPORT_BOOTSTRAP = `(()=>{if(window.__DSH_TRANSPORT__!==undefined)throw new Error('DSH Mobile cannot replace an existing transport override');window.__DSH_TRANSPORT__={fetch:(input,init)=>window.fetch(input,init),ownsHost:true},window.__DSH_FILE_UPLOAD__={fetch:(input,init)=>window.fetch(input,init)}})();`;
2669
3068
  /** Use this page's gateway health, rather than OS internet validation, for DSH reconnects. */
2670
3069
  const MOBILE_GATEWAY_REACHABILITY_BOOTSTRAP = `(()=>{
@@ -2976,7 +3375,7 @@ function rewriteMobileIndexWithBatch(html, options = {}) {
2976
3375
  batches: plan.batches
2977
3376
  })).digest("hex").slice(0, 16);
2978
3377
  }
2979
- const replacement = `${MOBILE_GATEWAY_REACHABILITY_BOOTSTRAP}${remoteSettings ? MOBILE_AUTHENTICATED_TRANSPORT_BOOTSTRAP : ""}${MOBILE_CSRF_FETCH_BOOTSTRAP}window.__DSH_MOBILE_FRONTEND__="dedicated";${plan.assignment}${JSON.stringify(plan.parsed)};`;
3378
+ const replacement = `${MOBILE_GATEWAY_REACHABILITY_BOOTSTRAP}${remoteSettings ? MOBILE_AUTHENTICATED_TRANSPORT_BOOTSTRAP : ""}${mobileCsrfFetchBootstrap(options.csrfCookie ?? "dsh_ma_csrf")}window.__DSH_MOBILE_FRONTEND__="dedicated";${plan.assignment}${JSON.stringify(plan.parsed)};`;
2980
3379
  const rewritten = `${html.slice(0, plan.replaceStart)}${replacement}${html.slice(plan.replaceEnd)}`;
2981
3380
  return Object.freeze({
2982
3381
  html: ensureMobileViewport(ensureMobileCompatibility(rewriteMobileApplicationPreloads(rewritten, preloadReplacements))),
@@ -3177,7 +3576,7 @@ function isCompressibleContentType(value) {
3177
3576
  }
3178
3577
  function shouldCompressResponse(request, response) {
3179
3578
  const pathname = request.url?.split("?", 1)[0] ?? "";
3180
- return (request.method === "GET" && (pathname.startsWith("/plugins/") || pathname.startsWith("/assets/")) || request.method === "POST" && pathname === SESSION_HISTORY_PATH) && response.statusCode === 200 && request.headers.range === void 0 && response.headers["content-range"] === void 0 && response.headers["content-encoding"] === void 0 && acceptsGzip(request.headers["accept-encoding"]) && isCompressibleContentType(response.headers["content-type"]);
3579
+ return request.method === "GET" && (pathname.startsWith("/plugins/") || pathname.startsWith("/assets/")) && response.statusCode === 200 && request.headers.range === void 0 && response.headers["content-range"] === void 0 && response.headers["content-encoding"] === void 0 && acceptsGzip(request.headers["accept-encoding"]) && isCompressibleContentType(response.headers["content-type"]);
3181
3580
  }
3182
3581
  function revisionedStaticCacheControl(request, statusCode) {
3183
3582
  if (request.method !== "GET" && request.method !== "HEAD") return void 0;
@@ -3194,39 +3593,12 @@ function revisionedStaticCacheControl(request, statusCode) {
3194
3593
  if (!(target.pathname.startsWith("/plugins/") && hasRevision) && !(target.pathname.startsWith("/assets/") && (hasRevision || hashedAsset))) return void 0;
3195
3594
  return "private, max-age=31536000, immutable";
3196
3595
  }
3197
- function isJsonRecord(value) {
3198
- return typeof value === "object" && value !== null && !Array.isArray(value);
3199
- }
3200
- function mobileHistoryRequestBody(request, body) {
3201
- if (request.method !== "POST" || request.url?.split("?", 1)[0] !== SESSION_HISTORY_PATH) return body;
3202
- let parsed;
3203
- try {
3204
- parsed = JSON.parse(body.toString("utf8"));
3205
- } catch {
3206
- return body;
3207
- }
3208
- if (!isJsonRecord(parsed) || parsed.method !== "session.history" || !isJsonRecord(parsed.payload)) return body;
3209
- const requested = parsed.payload.maxMessages;
3210
- if (typeof requested === "number" && Number.isInteger(requested) && requested > 0 && requested <= MOBILE_HISTORY_PAGE_MESSAGES) return body;
3211
- return Buffer.from(JSON.stringify({
3212
- ...parsed,
3213
- payload: {
3214
- ...parsed.payload,
3215
- maxMessages: MOBILE_HISTORY_PAGE_MESSAGES
3216
- }
3217
- }));
3218
- }
3219
3596
  function addVaryAcceptEncoding(headers) {
3220
3597
  const existing = headers.vary;
3221
3598
  const values = (Array.isArray(existing) ? existing.map((value) => String(value)) : existing === void 0 ? [] : [String(existing)]).flatMap((value) => value.split(",").map((part) => part.trim()).filter(Boolean));
3222
3599
  if (!values.some((value) => value.toLowerCase() === "accept-encoding")) values.push("Accept-Encoding");
3223
3600
  headers.vary = values.join(", ");
3224
3601
  }
3225
- function requestCookies(request) {
3226
- const cookies = parseCookies(request.headers.cookie);
3227
- if (cookies === void 0) throw new HttpError(401, "authentication_failed");
3228
- return cookies;
3229
- }
3230
3602
  function mapError(error) {
3231
3603
  if (error instanceof HttpError) return error;
3232
3604
  if (error instanceof AccessError) return new HttpError(error.status, error.code);
@@ -3755,9 +4127,9 @@ var MobileAccessGateway = class {
3755
4127
  return this.policy;
3756
4128
  }
3757
4129
  authorize(request) {
3758
- const sessionToken = requestCookies(request).get(SESSION_COOKIE);
3759
- if (sessionToken === void 0) throw new HttpError(401, "authentication_failed");
3760
- return this.access.authorizeSession(sessionToken);
4130
+ const cookies = browserAuthCookies(request.headers.cookie);
4131
+ const name = this.tlsEnabled && cookies.has("__Host-dsh_ma_session") ? HOST_SESSION_COOKIE : SESSION_COOKIE;
4132
+ return authenticatedCookie(cookies.get(name) ?? [], (token) => this.access.authorizeSession(token)).result;
3761
4133
  }
3762
4134
  requireCsrf(request, authorization) {
3763
4135
  const value = headerValue(request.headers, CSRF_HEADER);
@@ -3765,16 +4137,40 @@ var MobileAccessGateway = class {
3765
4137
  }
3766
4138
  setSessionCookies(response, result, now) {
3767
4139
  const maxAge = (result.sessionExpiresAt - now) / 1e3;
3768
- response.setHeader("Set-Cookie", [cookie(SESSION_COOKIE, result.sessionToken, {
3769
- tls: this.tlsEnabled,
4140
+ response.setHeader("Set-Cookie", [
4141
+ cookie(SESSION_COOKIE, result.sessionToken, {
4142
+ tls: this.tlsEnabled,
4143
+ httpOnly: true,
4144
+ path: "/",
4145
+ maxAgeSeconds: maxAge
4146
+ }),
4147
+ cookie(CSRF_COOKIE, result.csrfToken, {
4148
+ tls: this.tlsEnabled,
4149
+ httpOnly: false,
4150
+ path: "/",
4151
+ maxAgeSeconds: maxAge
4152
+ }),
4153
+ ...this.tlsEnabled ? [cookie(HOST_SESSION_COOKIE, result.sessionToken, {
4154
+ tls: true,
4155
+ httpOnly: true,
4156
+ path: "/",
4157
+ maxAgeSeconds: maxAge
4158
+ }), cookie(HOST_CSRF_COOKIE, result.csrfToken, {
4159
+ tls: true,
4160
+ httpOnly: false,
4161
+ path: "/",
4162
+ maxAgeSeconds: maxAge
4163
+ })] : []
4164
+ ]);
4165
+ }
4166
+ setBrowserDeviceBinding(response, deviceId, deviceExpiresAt) {
4167
+ if (!this.tlsEnabled) return;
4168
+ const existing = response.getHeader("Set-Cookie");
4169
+ response.setHeader("Set-Cookie", [...existing, cookie(HOST_DEVICE_COOKIE, deviceId, {
4170
+ tls: true,
3770
4171
  httpOnly: true,
3771
4172
  path: "/",
3772
- maxAgeSeconds: maxAge
3773
- }), cookie(CSRF_COOKIE, result.csrfToken, {
3774
- tls: this.tlsEnabled,
3775
- httpOnly: false,
3776
- path: "/",
3777
- maxAgeSeconds: maxAge
4173
+ maxAgeSeconds: (deviceExpiresAt - Date.now()) / 1e3
3778
4174
  })]);
3779
4175
  }
3780
4176
  async handlePair(request, response) {
@@ -3790,6 +4186,7 @@ var MobileAccessGateway = class {
3790
4186
  path: "/mobile-access/auth/renew",
3791
4187
  maxAgeSeconds: (result.deviceExpiresAt - now) / 1e3
3792
4188
  })]);
4189
+ this.setBrowserDeviceBinding(response, result.deviceId, result.deviceExpiresAt);
3793
4190
  sendJson(response, 201, {
3794
4191
  paired: true,
3795
4192
  deviceId: result.deviceId,
@@ -3800,21 +4197,13 @@ var MobileAccessGateway = class {
3800
4197
  async handleRenew(request, response) {
3801
4198
  if (!this.renewLimiter.take(request.socket.remoteAddress ?? "unknown", Date.now())) throw new HttpError(429, "rate_limited");
3802
4199
  await readJsonObject(request, MAX_CONTROL_BODY_BYTES);
3803
- const deviceToken = requestCookies(request).get(DEVICE_COOKIE);
3804
- if (deviceToken === void 0) throw new HttpError(401, "authentication_failed");
3805
- let result;
3806
- try {
3807
- result = await this.access.renew(deviceToken);
3808
- } catch (error) {
3809
- if (error instanceof AccessError && error.status === 401) response.setHeader("Set-Cookie", cookie(DEVICE_COOKIE, "", {
3810
- tls: this.tlsEnabled,
3811
- httpOnly: true,
3812
- path: "/mobile-access/auth/renew",
3813
- maxAgeSeconds: 0
3814
- }));
3815
- throw error;
3816
- }
4200
+ const cookies = browserAuthCookies(request.headers.cookie);
4201
+ const bindings = this.tlsEnabled ? cookies.get(HOST_DEVICE_COOKIE) : void 0;
4202
+ if (bindings !== void 0 && bindings.length !== 1) throw new HttpError(401, "authentication_failed");
4203
+ const device = authenticatedCookie(cookies.get("dsh_ma_device") ?? [], (token) => this.access.authorizeDevice(token), (candidate) => bindings === void 0 || candidate.deviceId === bindings[0]);
4204
+ const result = await this.access.renew(device.value);
3817
4205
  this.setSessionCookies(response, result, Date.now());
4206
+ this.setBrowserDeviceBinding(response, result.deviceId, device.result.deviceExpiresAt);
3818
4207
  sendJson(response, 200, {
3819
4208
  renewed: true,
3820
4209
  deviceId: result.deviceId,
@@ -3865,17 +4254,31 @@ var MobileAccessGateway = class {
3865
4254
  const authorization = this.authorize(request);
3866
4255
  this.requireCsrf(request, authorization);
3867
4256
  this.access.logout(authorization);
3868
- response.setHeader("Set-Cookie", [cookie(SESSION_COOKIE, "", {
3869
- tls: this.tlsEnabled,
3870
- httpOnly: true,
3871
- path: "/",
3872
- maxAgeSeconds: 0
3873
- }), cookie(CSRF_COOKIE, "", {
3874
- tls: this.tlsEnabled,
3875
- httpOnly: false,
3876
- path: "/",
3877
- maxAgeSeconds: 0
3878
- })]);
4257
+ response.setHeader("Set-Cookie", [
4258
+ cookie(SESSION_COOKIE, "", {
4259
+ tls: this.tlsEnabled,
4260
+ httpOnly: true,
4261
+ path: "/",
4262
+ maxAgeSeconds: 0
4263
+ }),
4264
+ cookie(CSRF_COOKIE, "", {
4265
+ tls: this.tlsEnabled,
4266
+ httpOnly: false,
4267
+ path: "/",
4268
+ maxAgeSeconds: 0
4269
+ }),
4270
+ ...this.tlsEnabled ? [cookie(HOST_SESSION_COOKIE, "", {
4271
+ tls: true,
4272
+ httpOnly: true,
4273
+ path: "/",
4274
+ maxAgeSeconds: 0
4275
+ }), cookie(HOST_CSRF_COOKIE, "", {
4276
+ tls: true,
4277
+ httpOnly: false,
4278
+ path: "/",
4279
+ maxAgeSeconds: 0
4280
+ })] : []
4281
+ ]);
3879
4282
  sendJson(response, 200, { loggedOut: true }, this.tlsEnabled);
3880
4283
  }
3881
4284
  async handleExternalRequest(request, response) {
@@ -3994,7 +4397,14 @@ var MobileAccessGateway = class {
3994
4397
  fallback: void 0
3995
4398
  } : void 0 : void 0;
3996
4399
  if (customAsset === void 0 && requestedMobileBootBatch === void 0 && !computerImages && !computerImage && extensionTarget(target.decodedPathname) === void 0 && (target.decodedPathname === "/mobile-access" || target.decodedPathname.startsWith(`/mobile-access/`))) throw new HttpError(404, "not_found");
3997
- if (request.method !== "GET" && request.method !== "HEAD" && request.method !== "POST" && requestedExtension?.kind !== "route") throw new HttpError(405, "method_not_allowed");
4400
+ if (![
4401
+ "GET",
4402
+ "HEAD",
4403
+ "POST",
4404
+ "PUT",
4405
+ "PATCH",
4406
+ "DELETE"
4407
+ ].includes(request.method ?? "") && requestedExtension?.kind !== "route") throw new HttpError(405, "method_not_allowed");
3998
4408
  let authorization;
3999
4409
  try {
4000
4410
  authorization = this.authorize(request);
@@ -4400,9 +4810,15 @@ var MobileAccessGateway = class {
4400
4810
  try {
4401
4811
  const html = Buffer.concat(chunks).toString("utf8");
4402
4812
  const plan = parseMobileBootPlan(html, this.config.excludedClientModules);
4403
- const rewritten = rewriteMobileIndexWithBatch(html, plan.rewrittenBatchEntries === void 0 ? { excludedClientModules: this.config.excludedClientModules } : {
4813
+ const options = plan.rewrittenBatchEntries === void 0 ? { excludedClientModules: this.config.excludedClientModules } : {
4404
4814
  ...await this.resolveMobileBootSizes(plan.rewrittenBatchEntries.flatMap((batch) => batch.entries)),
4405
4815
  excludedClientModules: this.config.excludedClientModules
4816
+ };
4817
+ const cookies = browserAuthCookies(request$2.headers.cookie);
4818
+ const csrfCookie = this.tlsEnabled && cookies.has("__Host-dsh_ma_session") ? HOST_CSRF_COOKIE : CSRF_COOKIE;
4819
+ const rewritten = rewriteMobileIndexWithBatch(html, {
4820
+ ...options,
4821
+ csrfCookie
4406
4822
  });
4407
4823
  for (const batch of rewritten.batches) this.rememberMobileBootBatch(batch);
4408
4824
  body = Buffer.from(rewritten.html);
@@ -4771,11 +5187,9 @@ var MobileAccessGateway = class {
4771
5187
  const operation = this.allocateRequest(authorization, response, holder);
4772
5188
  let bodyDone;
4773
5189
  try {
4774
- const bufferedBody = request$3.method === "POST" && request$3.url?.split("?", 1)[0] === SESSION_HISTORY_PATH ? mobileHistoryRequestBody(request$3, await readBoundedBody(request$3, this.config.maxBodyBytes)) : void 0;
4775
5190
  const upstreamHeaders = sanitizeRequestHeaders(request$3, this.config.upstreamOrigin);
4776
5191
  const upstreamCookie = await this.upstreamCookieHeader();
4777
5192
  if (upstreamCookie !== void 0) upstreamHeaders.cookie = upstreamCookie;
4778
- if (bufferedBody !== void 0) upstreamHeaders["content-length"] = String(bufferedBody.byteLength);
4779
5193
  const proxied = await new Promise((resolve, reject) => {
4780
5194
  const upstreamRequest = request({
4781
5195
  protocol: "http:",
@@ -4792,11 +5206,7 @@ var MobileAccessGateway = class {
4792
5206
  });
4793
5207
  upstreamRequest.once("response", resolve);
4794
5208
  upstreamRequest.once("error", reject);
4795
- if (bufferedBody === void 0) bodyDone = pipeline(request$3, new ByteLimitTransform(this.config.maxBodyBytes), upstreamRequest);
4796
- else {
4797
- upstreamRequest.end(bufferedBody);
4798
- bodyDone = Promise.resolve();
4799
- }
5209
+ bodyDone = pipeline(request$3, new ByteLimitTransform(this.config.maxBodyBytes), upstreamRequest);
4800
5210
  bodyDone.catch(reject);
4801
5211
  });
4802
5212
  setSecurityHeaders(response, this.tlsEnabled, "proxied");
@@ -5019,10 +5429,13 @@ var MobileAccessGateway = class {
5019
5429
  throw new HttpError(400, "bad_request");
5020
5430
  }
5021
5431
  if (decodedKey.length !== 16 || decodedKey.toString("base64") !== key) throw new HttpError(400, "bad_request");
5022
- const authorization = this.authorize(request);
5432
+ let authorization = this.authorize(request);
5023
5433
  if (this.activeWebSockets.size >= this.config.maxWebSockets) throw new HttpError(429, "busy");
5024
5434
  const upstreamCookie = await this.upstreamCookieHeader();
5025
5435
  if (client.destroyed) return;
5436
+ authorization = this.authorize(request);
5437
+ if (this.activeWebSockets.size >= this.config.maxWebSockets) throw new HttpError(429, "busy");
5438
+ const compressed = this.config.websocketCompression.paths.includes(target.decodedPathname);
5026
5439
  const upstream = connect({
5027
5440
  host: stripIpv6Brackets(this.config.upstreamOrigin.hostname),
5028
5441
  port: Number(this.config.upstreamOrigin.port)
@@ -5045,20 +5458,28 @@ var MobileAccessGateway = class {
5045
5458
  });
5046
5459
  this.activeWebSockets.set(id, record);
5047
5460
  const cleanup = () => {
5461
+ if (compressed && (!client.destroyed || !upstream.destroyed)) return;
5048
5462
  const active = this.activeWebSockets.get(id);
5049
5463
  if (active !== void 0) clearTimeout(active.timer);
5050
5464
  this.activeWebSockets.delete(id);
5051
5465
  };
5052
5466
  client.once("close", () => {
5053
- upstream.destroy();
5467
+ if (!compressed) upstream.destroy();
5054
5468
  cleanup();
5055
5469
  });
5056
5470
  upstream.once("close", () => {
5057
- client.destroy();
5471
+ if (!compressed) client.destroy();
5058
5472
  cleanup();
5059
5473
  });
5060
5474
  upstream.setTimeout(this.config.upstreamTimeoutMs, closeBoth);
5061
5475
  try {
5476
+ if (compressed) {
5477
+ const compressionTarget = new URL(this.config.upstreamOrigin.href);
5478
+ compressionTarget.pathname = target.pathname;
5479
+ compressionTarget.search = target.search;
5480
+ await bridgeCompressedWebSocket(request, client, upstream, head, compressionTarget, upstreamCookie, this.config.websocketCompression, this.config.upstreamTimeoutMs);
5481
+ return;
5482
+ }
5062
5483
  await new Promise((resolve, reject) => {
5063
5484
  const connected = () => {
5064
5485
  upstream.off("error", failed);
@@ -5357,8 +5778,8 @@ var MemoryDeviceStore = class {
5357
5778
  //#endregion
5358
5779
  //#region src/frp-component.ts
5359
5780
  const FRP_VERSION$1 = "0.70.1";
5360
- const MAX_ARCHIVE_ENTRIES = 128;
5361
- const MAX_ARCHIVE_LIST_BYTES = 262144;
5781
+ const MAX_ARCHIVE_ENTRIES$1 = 128;
5782
+ const MAX_ARCHIVE_LIST_BYTES$1 = 262144;
5362
5783
  /** Pinned official FRP release metadata for supported desktop targets. */
5363
5784
  const FRP_COMPONENT_RELEASES = Object.freeze(Object.fromEntries([
5364
5785
  {
@@ -5463,12 +5884,12 @@ async function replaceDirectory(target, candidate) {
5463
5884
  function sha256$2(bytes) {
5464
5885
  return createHash("sha256").update(bytes).digest("hex");
5465
5886
  }
5466
- async function runCapture(file, args) {
5887
+ async function runCapture$1(file, args) {
5467
5888
  return new Promise((resolveRun, reject) => {
5468
5889
  execFile(file, [...args], {
5469
5890
  windowsHide: true,
5470
5891
  timeout: 12e4,
5471
- maxBuffer: MAX_ARCHIVE_LIST_BYTES,
5892
+ maxBuffer: MAX_ARCHIVE_LIST_BYTES$1,
5472
5893
  encoding: "utf8"
5473
5894
  }, (error, stdout) => {
5474
5895
  if (error === null) resolveRun(stdout);
@@ -5476,7 +5897,7 @@ async function runCapture(file, args) {
5476
5897
  });
5477
5898
  });
5478
5899
  }
5479
- function validatedArchiveEntry(rawEntry) {
5900
+ function validatedArchiveEntry$1(rawEntry) {
5480
5901
  if (rawEntry.length === 0 || rawEntry.includes("\\") || rawEntry.includes("\0") || rawEntry.startsWith("/") || /^[a-zA-Z]:/u.test(rawEntry)) throw new Error("frp_archive_path_invalid");
5481
5902
  const segments = rawEntry.replace(/\/$/u, "").split("/");
5482
5903
  if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) throw new Error("frp_archive_path_invalid");
@@ -5484,10 +5905,10 @@ function validatedArchiveEntry(rawEntry) {
5484
5905
  }
5485
5906
  /** Select exactly one nested frpc executable from a safe archive listing. */
5486
5907
  function selectFrpExecutableEntry(entries, executableName) {
5487
- if (entries.length === 0 || entries.length > MAX_ARCHIVE_ENTRIES) throw new Error("frp_archive_entries_invalid");
5908
+ if (entries.length === 0 || entries.length > MAX_ARCHIVE_ENTRIES$1) throw new Error("frp_archive_entries_invalid");
5488
5909
  let executableEntry;
5489
5910
  for (const entry of entries) {
5490
- const segments = validatedArchiveEntry(entry);
5911
+ const segments = validatedArchiveEntry$1(entry);
5491
5912
  if (segments.length >= 2 && segments.at(-1) === executableName) {
5492
5913
  if (executableEntry !== void 0) throw new Error("frp_archive_executable_ambiguous");
5493
5914
  executableEntry = entry.replace(/\/$/u, "");
@@ -5496,22 +5917,22 @@ function selectFrpExecutableEntry(entries, executableName) {
5496
5917
  if (executableEntry === void 0) throw new Error("frp_archive_executable_missing");
5497
5918
  return executableEntry;
5498
5919
  }
5499
- async function defaultExtractArtifact$1(archive, destination, executableName) {
5920
+ async function defaultExtractArtifact$2(archive, destination, executableName) {
5500
5921
  const tar = process.platform === "win32" ? "tar.exe" : "tar";
5501
- const executableEntry = selectFrpExecutableEntry((await runCapture(tar, ["-tf", archive])).split(/\r?\n/u).filter((entry) => entry.length > 0), executableName);
5922
+ const executableEntry = selectFrpExecutableEntry((await runCapture$1(tar, ["-tf", archive])).split(/\r?\n/u).filter((entry) => entry.length > 0), executableName);
5502
5923
  const unpacked = join(destination, "archive");
5503
5924
  await mkdir(unpacked, {
5504
5925
  recursive: true,
5505
5926
  mode: 448
5506
5927
  });
5507
- await runCapture(tar, [
5928
+ await runCapture$1(tar, [
5508
5929
  "-xf",
5509
5930
  archive,
5510
5931
  "-C",
5511
5932
  unpacked,
5512
5933
  executableEntry
5513
5934
  ]);
5514
- const extracted = join(unpacked, ...validatedArchiveEntry(executableEntry));
5935
+ const extracted = join(unpacked, ...validatedArchiveEntry$1(executableEntry));
5515
5936
  if (!await regularFile$2(extracted)) throw new Error("frp_archive_executable_invalid");
5516
5937
  await copyFile(extracted, join(destination, executableName));
5517
5938
  }
@@ -5551,7 +5972,7 @@ async function defaultFetchArtifact$2(artifact, signal) {
5551
5972
  return bytes;
5552
5973
  }
5553
5974
  async function defaultInspectExecutable(executable) {
5554
- return (await runCapture(executable, ["--version"])).trim();
5975
+ return (await runCapture$1(executable, ["--version"])).trim();
5555
5976
  }
5556
5977
  /** Owns the optional official frpc binary inside the DSH Mobile state directory. */
5557
5978
  var FrpComponentManager = class {
@@ -5586,7 +6007,7 @@ var FrpComponentManager = class {
5586
6007
  this.stagingRoot
5587
6008
  ]) if (!inside$2(stateDirectory, child)) throw new Error("frp component path escaped its state directory");
5588
6009
  this.fetchArtifact = options.fetchArtifact ?? defaultFetchArtifact$2;
5589
- this.extractArtifact = options.extractArtifact ?? defaultExtractArtifact$1;
6010
+ this.extractArtifact = options.extractArtifact ?? defaultExtractArtifact$2;
5590
6011
  this.inspectExecutable = options.inspectExecutable ?? defaultInspectExecutable;
5591
6012
  }
5592
6013
  /** Inspect the managed executable without relying on global FRP installations. */
@@ -5698,112 +6119,6 @@ var FrpComponentManager = class {
5698
6119
  }
5699
6120
  };
5700
6121
  //#endregion
5701
- //#region src/websocket-paths.ts
5702
- /** Cap the admin-approved extra WebSocket upgrade paths (exact pathnames). */
5703
- const MAX_EXTRA_WEBSOCKET_PATHS = 16;
5704
- const MAX_WEBSOCKET_PATH_LENGTH = 256;
5705
- function fail(code) {
5706
- throw new Error(code);
5707
- }
5708
- /**
5709
- * Validate one exact pathname for proxying. Query strings are matched at
5710
- * upgrade time, so only the pathname is stored. Rejects anything the
5711
- * gateway cannot match exactly.
5712
- */
5713
- function validateWebSocketPath(value) {
5714
- if (typeof value !== "string") fail("websocket_path_invalid");
5715
- const path = value;
5716
- if (path.length === 0 || path.length > 256) fail("websocket_path_invalid");
5717
- if (!path.startsWith("/")) fail("websocket_path_invalid");
5718
- if (/[\s\u0000-\u001f\u007f#?]/u.test(path)) fail("websocket_path_invalid");
5719
- if (path.includes("..")) fail("websocket_path_invalid");
5720
- return path;
5721
- }
5722
- /** Validate a whole replacement list all-or-nothing; duplicates collapse. */
5723
- function normalizeWebSocketPaths(value) {
5724
- if (!Array.isArray(value)) fail("websocket_paths_invalid");
5725
- if (value.length > 16) fail("websocket_paths_invalid");
5726
- const seen = /* @__PURE__ */ new Set();
5727
- for (const entry of value) {
5728
- const path = validateWebSocketPath(entry);
5729
- seen.add(path);
5730
- }
5731
- return [...seen];
5732
- }
5733
- /** Cap remembered rejected upgrade paths offered for one-click approval. */
5734
- const MAX_BLOCKED_UPGRADE_PATHS = 32;
5735
- /**
5736
- * In-memory log of rejected third-party upgrade paths, shared by every
5737
- * gateway instance so the approval UI sees attempts on any listener.
5738
- * Bounded and newest-first; a restart clears it (attempts reappear on
5739
- * the next blocked handshake).
5740
- */
5741
- var BlockedUpgradePathLog = class {
5742
- entries = /* @__PURE__ */ new Map();
5743
- record(pathname) {
5744
- const now = Date.now();
5745
- const stats = this.entries.get(pathname);
5746
- if (stats === void 0) {
5747
- if (this.entries.size >= 32) {
5748
- let oldest;
5749
- for (const [path, entry] of this.entries) if (oldest === void 0 || entry.lastSeen < (this.entries.get(oldest)?.lastSeen ?? 0)) oldest = path;
5750
- if (oldest !== void 0) this.entries.delete(oldest);
5751
- }
5752
- this.entries.set(pathname, {
5753
- attempts: 1,
5754
- firstSeen: now,
5755
- lastSeen: now
5756
- });
5757
- return;
5758
- }
5759
- stats.attempts += 1;
5760
- stats.lastSeen = now;
5761
- }
5762
- report() {
5763
- return [...this.entries].map(([path, stats]) => ({
5764
- path,
5765
- ...stats
5766
- })).sort((a, b) => b.lastSeen - a.lastSeen);
5767
- }
5768
- };
5769
- /** File-backed store shared by every gateway instance (LAN and remote). */
5770
- var WebSocketPathStore = class {
5771
- file;
5772
- paths = /* @__PURE__ */ new Set();
5773
- loaded = false;
5774
- constructor(file) {
5775
- this.file = file;
5776
- }
5777
- /** Snapshot for the upgrade check. */
5778
- has(pathname) {
5779
- return this.paths.has(pathname);
5780
- }
5781
- list() {
5782
- return [...this.paths];
5783
- }
5784
- async load() {
5785
- if (!this.loaded) {
5786
- try {
5787
- const raw = JSON.parse(await readFile(this.file, "utf8"));
5788
- this.paths = new Set(normalizeWebSocketPaths(raw.paths ?? []));
5789
- } catch {
5790
- this.paths = /* @__PURE__ */ new Set();
5791
- }
5792
- this.loaded = true;
5793
- }
5794
- return this.list();
5795
- }
5796
- /** Replace the whole list after validating; persists atomically. */
5797
- async replace(paths) {
5798
- const next = normalizeWebSocketPaths([...paths]);
5799
- await mkdir(dirname(this.file), { recursive: true });
5800
- await writeFile(this.file, `${JSON.stringify({ paths: next })}\n`, "utf8");
5801
- this.paths = new Set(next);
5802
- this.loaded = true;
5803
- return this.list();
5804
- }
5805
- };
5806
- //#endregion
5807
6122
  //#region src/frp-template.ts
5808
6123
  /** Loopback-only HTTP vhost port used between Caddy and frps. */
5809
6124
  const FRP_VHOST_HTTP_PORT = 7080;
@@ -8595,6 +8910,10 @@ async function downloadPinnedArtifact(request) {
8595
8910
  }
8596
8911
  //#endregion
8597
8912
  //#region src/cloudflared-component.ts
8913
+ /** Upper bound for `tar -t` output; a pinned release listing is a few lines. */
8914
+ const MAX_ARCHIVE_LIST_BYTES = 1048576;
8915
+ const MAX_ARCHIVE_ENTRIES = 256;
8916
+ const DOWNLOAD_TIMEOUT_MS = 3e5;
8598
8917
  const CLOUDFLARED_VERSION = "2026.9.1";
8599
8918
  /** Pinned official cloudflared release metadata for supported desktop targets. */
8600
8919
  const CLOUDFLARED_COMPONENT_RELEASES = Object.freeze(Object.fromEntries([
@@ -8605,7 +8924,9 @@ const CLOUDFLARED_COMPONENT_RELEASES = Object.freeze(Object.fromEntries([
8605
8924
  downloadUrl: `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-windows-amd64.exe`,
8606
8925
  downloadBytes: 54976432,
8607
8926
  downloadSha256: "2837888cc0f5d58f15b6dc478376de90b4d3ba5241c7947455d1e0a0df429712",
8608
- executableName: "cloudflared.exe"
8927
+ executableName: "cloudflared.exe",
8928
+ executableBytes: 54976432,
8929
+ executableSha256: "2837888cc0f5d58f15b6dc478376de90b4d3ba5241c7947455d1e0a0df429712"
8609
8930
  },
8610
8931
  {
8611
8932
  version: CLOUDFLARED_VERSION,
@@ -8614,7 +8935,9 @@ const CLOUDFLARED_COMPONENT_RELEASES = Object.freeze(Object.fromEntries([
8614
8935
  downloadUrl: `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64`,
8615
8936
  downloadBytes: 39838488,
8616
8937
  downloadSha256: "03f1f25d1cc93b9ad6c60569d44060bc4f17ed97075760ed8cfca4b12dcd68cc",
8617
- executableName: "cloudflared"
8938
+ executableName: "cloudflared",
8939
+ executableBytes: 39838488,
8940
+ executableSha256: "03f1f25d1cc93b9ad6c60569d44060bc4f17ed97075760ed8cfca4b12dcd68cc"
8618
8941
  },
8619
8942
  {
8620
8943
  version: CLOUDFLARED_VERSION,
@@ -8623,7 +8946,39 @@ const CLOUDFLARED_COMPONENT_RELEASES = Object.freeze(Object.fromEntries([
8623
8946
  downloadUrl: `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64`,
8624
8947
  downloadBytes: 37466252,
8625
8948
  downloadSha256: "3d97437c71848bd8df68041e12436b484a661d95073ea1937f01a845ce88faa3",
8626
- executableName: "cloudflared"
8949
+ executableName: "cloudflared",
8950
+ executableBytes: 37466252,
8951
+ executableSha256: "3d97437c71848bd8df68041e12436b484a661d95073ea1937f01a845ce88faa3"
8952
+ },
8953
+ {
8954
+ version: CLOUDFLARED_VERSION,
8955
+ platform: "darwin",
8956
+ arch: "x64",
8957
+ downloadUrl: `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-darwin-amd64.tgz`,
8958
+ downloadBytes: 21118723,
8959
+ downloadSha256: "ff0d3b51d5ff70eceef89d6b32145fee985018a2174596a5dbe405e2766e2ac4",
8960
+ executableName: "cloudflared",
8961
+ executableBytes: 41731488,
8962
+ executableSha256: "1ea07ae775b03236bd6be18ca1848d6bdc4af2f4f3bce398823b5a36e5761b75",
8963
+ archive: {
8964
+ format: "tgz",
8965
+ member: "cloudflared"
8966
+ }
8967
+ },
8968
+ {
8969
+ version: CLOUDFLARED_VERSION,
8970
+ platform: "darwin",
8971
+ arch: "arm64",
8972
+ downloadUrl: `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-darwin-arm64.tgz`,
8973
+ downloadBytes: 19217478,
8974
+ downloadSha256: "c27ab8fd0aa489449e3d201eb02f957ef460a13b613662928b1b23394bf1bcfe",
8975
+ executableName: "cloudflared",
8976
+ executableBytes: 38893072,
8977
+ executableSha256: "9a0b19f67dc7a3011bc6b972c7ce06a5fcea8784ac6bd599ffa382ea4aeb5a6e",
8978
+ archive: {
8979
+ format: "tgz",
8980
+ member: "cloudflared"
8981
+ }
8627
8982
  }
8628
8983
  ].map((release) => [`${release.platform}-${release.arch}`, Object.freeze(release)])));
8629
8984
  /**
@@ -8650,6 +9005,60 @@ async function regularFile$1(file, expectedBytes) {
8650
9005
  throw error;
8651
9006
  }
8652
9007
  }
9008
+ async function runCapture(file, args) {
9009
+ return new Promise((resolveRun, reject) => {
9010
+ execFile(file, [...args], {
9011
+ windowsHide: true,
9012
+ timeout: 12e4,
9013
+ maxBuffer: MAX_ARCHIVE_LIST_BYTES,
9014
+ encoding: "utf8"
9015
+ }, (error, stdout) => {
9016
+ if (error === null) resolveRun(stdout);
9017
+ else reject(error);
9018
+ });
9019
+ });
9020
+ }
9021
+ /** Reject absolute, traversing, or Windows-shaped archive members. */
9022
+ function validatedArchiveEntry(rawEntry) {
9023
+ if (rawEntry.length === 0 || rawEntry.includes("\\") || rawEntry.includes("\0") || rawEntry.startsWith("/") || /^[a-zA-Z]:/u.test(rawEntry)) throw new Error("cloudflared_archive_path_invalid");
9024
+ const segments = rawEntry.replace(/\/$/u, "").split("/");
9025
+ if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) throw new Error("cloudflared_archive_path_invalid");
9026
+ return segments;
9027
+ }
9028
+ /** Select exactly one archive member named `member`, wherever it sits in the tree. */
9029
+ function selectCloudflaredArchiveEntry(entries, member) {
9030
+ if (entries.length === 0 || entries.length > MAX_ARCHIVE_ENTRIES) throw new Error("cloudflared_archive_entries_invalid");
9031
+ let selected;
9032
+ for (const entry of entries) if (validatedArchiveEntry(entry).at(-1) === member) {
9033
+ if (selected !== void 0) throw new Error("cloudflared_archive_executable_ambiguous");
9034
+ selected = entry.replace(/\/$/u, "");
9035
+ }
9036
+ if (selected === void 0) throw new Error("cloudflared_archive_executable_missing");
9037
+ return selected;
9038
+ }
9039
+ /**
9040
+ * Unpack the pinned member out of a `.tgz` using the system `tar`, which ships
9041
+ * with Windows 10+, macOS, and every supported Linux base.
9042
+ */
9043
+ async function defaultExtractArtifact$1(archive, destination, member) {
9044
+ const tar = process.platform === "win32" ? "tar.exe" : "tar";
9045
+ const selected = selectCloudflaredArchiveEntry((await runCapture(tar, ["-tzf", archive])).split(/\r?\n/u).filter((entry) => entry.length > 0), member);
9046
+ const unpacked = join(destination, "archive");
9047
+ await mkdir(unpacked, {
9048
+ recursive: true,
9049
+ mode: 448
9050
+ });
9051
+ await runCapture(tar, [
9052
+ "-xzf",
9053
+ archive,
9054
+ "-C",
9055
+ unpacked,
9056
+ selected
9057
+ ]);
9058
+ const extracted = join(unpacked, ...validatedArchiveEntry(selected));
9059
+ if (!await regularFile$1(extracted)) throw new Error("cloudflared_archive_executable_invalid");
9060
+ await copyFile(extracted, join(destination, member));
9061
+ }
8653
9062
  async function defaultFetchArtifact$1(url, signal, expectedBytes) {
8654
9063
  return downloadPinnedArtifact({
8655
9064
  url,
@@ -8674,6 +9083,7 @@ var CloudflaredComponentManager = class {
8674
9083
  arch;
8675
9084
  release;
8676
9085
  fetchArtifact;
9086
+ extractArtifact;
8677
9087
  installed = false;
8678
9088
  errorCode;
8679
9089
  queue = Promise.resolve();
@@ -8698,12 +9108,14 @@ var CloudflaredComponentManager = class {
8698
9108
  ]) if (!inside$1(stateDirectory, child)) throw new Error("cloudflared component path escaped its state directory");
8699
9109
  const release = this.release;
8700
9110
  this.fetchArtifact = options.fetchArtifact ?? ((url, signal) => defaultFetchArtifact$1(url, signal, release?.downloadBytes ?? 0));
9111
+ this.extractArtifact = options.extractArtifact ?? defaultExtractArtifact$1;
8701
9112
  }
8702
9113
  /** Inspect the managed binary without using any global cloudflared state. */
8703
9114
  async initialize() {
8704
9115
  const release = this.release;
8705
- this.installed = release !== void 0 && await regularFile$1(this.executable, release.downloadBytes);
8706
- if (this.installed && release !== void 0 && await sha256$1(this.executable) !== release.downloadSha256) {
9116
+ this.errorCode = void 0;
9117
+ this.installed = release !== void 0 && await regularFile$1(this.executable, release.executableBytes);
9118
+ if (this.installed && release !== void 0 && await sha256$1(this.executable) !== release.executableSha256) {
8707
9119
  this.installed = false;
8708
9120
  this.errorCode = "cloudflared_component_invalid";
8709
9121
  }
@@ -8716,7 +9128,7 @@ var CloudflaredComponentManager = class {
8716
9128
  installed: this.installed,
8717
9129
  version: release.version,
8718
9130
  downloadBytes: release.downloadBytes,
8719
- installedBytes: release.downloadBytes,
9131
+ installedBytes: release.executableBytes,
8720
9132
  sourceUrl: release.downloadUrl,
8721
9133
  downloadPage: DOWNLOAD_PAGE$1,
8722
9134
  termsUrl: TERMS_URL$1,
@@ -8738,7 +9150,7 @@ var CloudflaredComponentManager = class {
8738
9150
  const controller = new AbortController();
8739
9151
  const timeout = setTimeout(() => {
8740
9152
  controller.abort();
8741
- }, 3e5);
9153
+ }, DOWNLOAD_TIMEOUT_MS);
8742
9154
  timeout.unref();
8743
9155
  let bytes;
8744
9156
  try {
@@ -8749,12 +9161,24 @@ var CloudflaredComponentManager = class {
8749
9161
  if (bytes.byteLength !== release.downloadBytes) throw new Error("cloudflared_download_size_mismatch");
8750
9162
  if (createHash("sha256").update(bytes).digest("hex") !== release.downloadSha256) throw new Error("cloudflared_download_hash_mismatch");
8751
9163
  const staged = join(staging, release.executableName);
8752
- await writeFile(staged, bytes, {
9164
+ if (release.archive === void 0) await writeFile(staged, bytes, {
8753
9165
  flag: "wx",
8754
9166
  mode: 384
8755
9167
  });
9168
+ else {
9169
+ const downloaded = join(staging, `${release.executableName}.download`);
9170
+ await writeFile(downloaded, bytes, {
9171
+ flag: "wx",
9172
+ mode: 384
9173
+ });
9174
+ try {
9175
+ await this.extractArtifact(downloaded, staging, release.archive.member);
9176
+ } finally {
9177
+ await rm(downloaded, { force: true });
9178
+ }
9179
+ }
8756
9180
  await chmod(staged, 448);
8757
- if (!await regularFile$1(staged, release.downloadBytes) || await sha256$1(staged) !== release.downloadSha256) throw new Error("cloudflared_executable_hash_mismatch");
9181
+ if (!await regularFile$1(staged, release.executableBytes) || await sha256$1(staged) !== release.executableSha256) throw new Error("cloudflared_executable_hash_mismatch");
8758
9182
  const candidate = join(this.componentRoot, `.install-${randomBytes(12).toString("hex")}`);
8759
9183
  await mkdir(candidate, {
8760
9184
  recursive: true,
@@ -13946,6 +14370,6 @@ async function apply(ctx, config) {
13946
14370
  }, "dsh-mobile: independent LAN and selectable remote providers with /mobile command");
13947
14371
  }
13948
14372
  //#endregion
13949
- export { AUTH_PREFIX, AccessController, AccessError, BlockedUpgradePathLog, BoundedRateLimiter, CERT_EXPIRING_DAYS, CLOUDFLARED_COMPONENT_RELEASE, CSRF_COOKIE, CSRF_HEADER, CloudflaredComponentManager, CloudflaredController, CloudflaredTunnelStore, Config, DEFAULT_ORIGIN_LISTEN_PORT, FRP_VHOST_HTTP_PORT as DEFAULT_VHOST_HTTP_PORT, FRP_VHOST_HTTP_PORT, DEVICE_COOKIE, EXTENSION_LIMITS, FRP_ATTACH_DISCOVERY_PATH, FRP_ATTACH_LOCAL_DECLARATION, FRP_ATTACH_SELF_SIGNED_DECLARATION, FRP_ATTACH_VPS_DECLARATION, FRP_CADDY_IMPORT_LINE, FRP_CADDY_SNIPPET_MARKER, FRP_CADDY_SNIPPET_PATH, FRP_COMPONENT_RELEASES, FRP_DEFAULT_PROXY_NAME, FRP_DEFAULT_PUBLIC_PORT, FRP_RESERVED_PORTS, FrpComponentManager, FrpConfigStore, FrpController, JsonDeviceStore, JsonMobileAccessControlStore, JsonRemoteProviderStore, LOCAL_ADMIN_PREFIX, MAX_BLOCKED_UPGRADE_PATHS, MAX_EXTRA_WEBSOCKET_PATHS, MAX_WEBSOCKET_PATH_LENGTH, MemoryDeviceStore, MobileAccessGateway, MobileAccessGatewayController, MobileAccessService, MobileExtensionError, OriginConfigStore, OriginController, REMOTE_PROVIDERS, RequestTrustPolicy, SESSION_COOKIE, TASK_EVENT_DEBOUNCE_MS, TaskEventHub, WS_PATHS, WebSocketPathStore, addressAllowed, apply, assertExtensionId, configuredRemoteProvider, createCaddySite, createFrpAttachPlan, createFrpAttachTemplate, createFrpAttachTemplateParts, createFrpServerTemplate, createFrpcToml, createMobileAccessService, createRestrictedFrpServerTemplate, ensureFrpIngressCertificate, evaluateCertificateLifetime, frpAttachVpsParts, frpIngressPaths, frpIngressSelfCheck, frpProxyName, inject, isCloudflaredRegistration, isFrpSelfSignedIngress, isGloballyRoutableIpv4, isLoopbackAddress, issueServerCertificate, mergeSavedCloudflaredTunnelSettings, mergeSavedFrpSettings, mergeSavedFrpTarget, name, normalizeWebSocketPaths, originGatewayConfig, parseAuthority, parseCidr, parseCloudflaredOrigin, parseCloudflaredTunnelSettings, parseControlFile, parseDeviceSnapshot, parseExtensionManifest, parseFrpSettings, parseGatewayConfig, parseMobileAccessControlState, parseOriginSettings, parseRemoteProviderState, probeOriginCertificate, readCertificateRenewal, resolveAuthority, resolveFrpEntryTls, resolveFrpMode, resolveFrpPublicPort, resolveFrpVhostHttpPort, rewriteMobileIndex, validateAttachSettings, validateCloudflaredTunnelHostname, validateCloudflaredTunnelPort, validateCloudflaredTunnelToken, validateFrpEntryTls, validateFrpMode, validateFrpProxyName, validateFrpPublicOrigin, validateFrpPublicPort, validateFrpServerAddress, validateFrpServerPort, validateFrpToken, validateFrpVhostHttpPort, validateOriginAllowedCidrs, validateOriginListenHost, validateOriginListenPort, validateOriginPublicOrigin, validateWebSocketPath, watchTaskCompletions };
14373
+ export { AUTH_PREFIX, AccessController, AccessError, BlockedUpgradePathLog, BoundedRateLimiter, CERT_EXPIRING_DAYS, CLOUDFLARED_COMPONENT_RELEASE, CLOUDFLARED_COMPONENT_RELEASES, CSRF_COOKIE, CSRF_HEADER, CloudflaredComponentManager, CloudflaredController, CloudflaredTunnelStore, Config, DEFAULT_ORIGIN_LISTEN_PORT, FRP_VHOST_HTTP_PORT as DEFAULT_VHOST_HTTP_PORT, FRP_VHOST_HTTP_PORT, DEVICE_COOKIE, EXTENSION_LIMITS, FRP_ATTACH_DISCOVERY_PATH, FRP_ATTACH_LOCAL_DECLARATION, FRP_ATTACH_SELF_SIGNED_DECLARATION, FRP_ATTACH_VPS_DECLARATION, FRP_CADDY_IMPORT_LINE, FRP_CADDY_SNIPPET_MARKER, FRP_CADDY_SNIPPET_PATH, FRP_COMPONENT_RELEASES, FRP_DEFAULT_PROXY_NAME, FRP_DEFAULT_PUBLIC_PORT, FRP_RESERVED_PORTS, FrpComponentManager, FrpConfigStore, FrpController, JsonDeviceStore, JsonMobileAccessControlStore, JsonRemoteProviderStore, LOCAL_ADMIN_PREFIX, MAX_BLOCKED_UPGRADE_PATHS, MAX_EXTRA_WEBSOCKET_PATHS, MAX_WEBSOCKET_PATH_LENGTH, MemoryDeviceStore, MobileAccessGateway, MobileAccessGatewayController, MobileAccessService, MobileExtensionError, OriginConfigStore, OriginController, REMOTE_PROVIDERS, RequestTrustPolicy, SESSION_COOKIE, TASK_EVENT_DEBOUNCE_MS, TaskEventHub, WS_PATHS, WebSocketPathStore, addressAllowed, apply, assertExtensionId, configuredRemoteProvider, createCaddySite, createFrpAttachPlan, createFrpAttachTemplate, createFrpAttachTemplateParts, createFrpServerTemplate, createFrpcToml, createMobileAccessService, createRestrictedFrpServerTemplate, ensureFrpIngressCertificate, evaluateCertificateLifetime, frpAttachVpsParts, frpIngressPaths, frpIngressSelfCheck, frpProxyName, inject, isCloudflaredRegistration, isFrpSelfSignedIngress, isGloballyRoutableIpv4, isLoopbackAddress, issueServerCertificate, mergeSavedCloudflaredTunnelSettings, mergeSavedFrpSettings, mergeSavedFrpTarget, name, normalizeWebSocketPaths, originGatewayConfig, parseAuthority, parseCidr, parseCloudflaredOrigin, parseCloudflaredTunnelSettings, parseControlFile, parseDeviceSnapshot, parseExtensionManifest, parseFrpSettings, parseGatewayConfig, parseMobileAccessControlState, parseOriginSettings, parseRemoteProviderState, probeOriginCertificate, readCertificateRenewal, resolveAuthority, resolveFrpEntryTls, resolveFrpMode, resolveFrpPublicPort, resolveFrpVhostHttpPort, rewriteMobileIndex, validateAttachSettings, validateCloudflaredTunnelHostname, validateCloudflaredTunnelPort, validateCloudflaredTunnelToken, validateFrpEntryTls, validateFrpMode, validateFrpProxyName, validateFrpPublicOrigin, validateFrpPublicPort, validateFrpServerAddress, validateFrpServerPort, validateFrpToken, validateFrpVhostHttpPort, validateOriginAllowedCidrs, validateOriginListenHost, validateOriginListenPort, validateOriginPublicOrigin, validateWebSocketPath, watchTaskCompletions };
13950
14374
 
13951
14375
  //# sourceMappingURL=index.mjs.map