@rstreamlabs/tunnels 3.8.3 → 3.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -5,6 +5,14 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res, err) => function __init() {
9
+ if (err) throw err[0];
10
+ try {
11
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
12
+ } catch (e) {
13
+ throw err = [e], e;
14
+ }
15
+ };
8
16
  var __export = (target, all) => {
9
17
  for (var name in all)
10
18
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -27,6 +35,116 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
35
  ));
28
36
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
37
 
38
+ // src/node-engine-http.ts
39
+ var node_engine_http_exports = {};
40
+ __export(node_engine_http_exports, {
41
+ NodeEngineHTTP: () => NodeEngineHTTP
42
+ });
43
+ var import_node_https, import_node_https2, responseHeaders, responseBody, NodeEngineHTTP;
44
+ var init_node_engine_http = __esm({
45
+ "src/node-engine-http.ts"() {
46
+ "use strict";
47
+ import_node_https = require("https");
48
+ import_node_https2 = require("https");
49
+ responseHeaders = (response) => {
50
+ const headers = new Headers();
51
+ for (const [name, value] of Object.entries(response.headers)) {
52
+ if (Array.isArray(value))
53
+ for (const item of value) headers.append(name, item);
54
+ else if (value !== void 0) headers.set(name, value);
55
+ }
56
+ return headers;
57
+ };
58
+ responseBody = (response) => {
59
+ const state = { ended: false };
60
+ return new ReadableStream({
61
+ start(controller) {
62
+ response.on("data", (chunk) => {
63
+ controller.enqueue(new Uint8Array(chunk));
64
+ response.pause();
65
+ });
66
+ response.once("end", () => {
67
+ if (!state.ended) {
68
+ state.ended = true;
69
+ controller.close();
70
+ }
71
+ });
72
+ response.once("error", (error) => {
73
+ if (!state.ended) {
74
+ state.ended = true;
75
+ controller.error(error);
76
+ }
77
+ });
78
+ response.once("aborted", () => {
79
+ if (!state.ended) {
80
+ state.ended = true;
81
+ controller.error(new Error("Engine response was interrupted."));
82
+ }
83
+ });
84
+ },
85
+ pull() {
86
+ response.resume();
87
+ },
88
+ cancel() {
89
+ state.ended = true;
90
+ response.destroy();
91
+ }
92
+ });
93
+ };
94
+ NodeEngineHTTP = class {
95
+ constructor(tls) {
96
+ this.tls = tls;
97
+ }
98
+ tls;
99
+ agent = new import_node_https.Agent({
100
+ keepAlive: true,
101
+ maxSockets: 16,
102
+ maxFreeSockets: 4,
103
+ maxTotalSockets: 16
104
+ });
105
+ fetch = async (input, init) => {
106
+ const incoming = new Request(input, init);
107
+ const url = new URL(incoming.url);
108
+ if (url.protocol !== "https:" || url.username || url.password)
109
+ throw new Error(
110
+ "Engine HTTP requests require HTTPS without URL credentials."
111
+ );
112
+ const body = incoming.body ? new Uint8Array(await incoming.arrayBuffer()) : void 0;
113
+ incoming.signal.throwIfAborted();
114
+ return new Promise((resolve, reject) => {
115
+ const outgoing = (0, import_node_https2.request)(
116
+ url,
117
+ {
118
+ ...this.tls,
119
+ servername: url.hostname,
120
+ agent: this.agent,
121
+ method: incoming.method,
122
+ headers: Object.fromEntries(incoming.headers),
123
+ signal: incoming.signal
124
+ },
125
+ (response) => {
126
+ const status = response.statusCode ?? 502;
127
+ const empty = incoming.method === "HEAD" || [204, 205, 304].includes(status);
128
+ if (empty) response.resume();
129
+ resolve(
130
+ new Response(empty ? null : responseBody(response), {
131
+ status,
132
+ headers: responseHeaders(response)
133
+ })
134
+ );
135
+ }
136
+ );
137
+ outgoing.once("error", reject);
138
+ outgoing.end(body);
139
+ });
140
+ };
141
+ close() {
142
+ this.agent.destroy();
143
+ }
144
+ };
145
+ }
146
+ });
147
+
30
148
  // src/index.ts
31
149
  var index_exports = {};
32
150
  __export(index_exports, {
@@ -84,7 +202,9 @@ __export(index_exports, {
84
202
  resolveTunnelsCredentials: () => resolveTunnelsCredentials,
85
203
  resolveTunnelsEngine: () => resolveTunnelsEngine,
86
204
  resolveTunnelsRegion: () => resolveTunnelsRegion,
205
+ resolveWebTTYBrowserEndpoint: () => resolveWebTTYBrowserEndpoint,
87
206
  resolveWebTTYControlRequestParamsSchema: () => resolveWebTTYControlRequestParamsSchema,
207
+ resolveWebTTYServerTransport: () => resolveWebTTYServerTransport,
88
208
  signWebhookPayload: () => signWebhookPayload,
89
209
  streamEndpointSchema: () => streamEndpointSchema,
90
210
  streamEntrySchema: () => streamEntrySchema,
@@ -94,6 +214,7 @@ __export(index_exports, {
94
214
  streamWebTTYMetadataSchema: () => streamWebTTYMetadataSchema,
95
215
  tunnelFilterSchema: () => tunnelFilterSchema,
96
216
  turnCredentialModeSchema: () => turnCredentialModeSchema,
217
+ validateTURNCredentialsRequest: () => validateTURNCredentialsRequest,
97
218
  webTTYCapabilitiesSchema: () => webTTYCapabilitiesSchema,
98
219
  webTTYContextMetadataSchema: () => webTTYContextMetadataSchema,
99
220
  webTTYControlRequestSchema: () => webTTYControlRequestSchema,
@@ -114,6 +235,7 @@ __export(index_exports, {
114
235
  webTTYTransportSchema: () => webTTYTransportSchema,
115
236
  webhookEventsSchema: () => webhookEventsSchema,
116
237
  webttyServerSchema: () => webttyServerSchema,
238
+ webttyTransportSchema: () => webttyTransportSchema,
117
239
  wsEventsSchema: () => wsEventsSchema
118
240
  });
119
241
  module.exports = __toCommonJS(index_exports);
@@ -1298,6 +1420,19 @@ var watchParamsSchema = z7.object({
1298
1420
  tunnels: tunnelFilterSchema.optional()
1299
1421
  });
1300
1422
  var enginePermissions = {
1423
+ "turn.credentials.create": {
1424
+ name: "TURN Credentials: Create",
1425
+ description: "Issue short-lived TURN credentials through either the Control plane or Engine API.",
1426
+ rules: [
1427
+ {
1428
+ resource: [
1429
+ "api.turn-server.credentials",
1430
+ "engine.turn-server.credentials"
1431
+ ],
1432
+ action: "create"
1433
+ }
1434
+ ]
1435
+ },
1301
1436
  "tunnels.resources.read-only": {
1302
1437
  name: "Discovery: Read-Only",
1303
1438
  description: "Read active clients, active tunnels, and live state events.",
@@ -1377,13 +1512,13 @@ var engineOpenApiDocument = {
1377
1512
  }
1378
1513
  }
1379
1514
  ],
1380
- security: [
1381
- {
1382
- bearerAuth: []
1383
- }
1384
- ],
1515
+ security: [{ bearerAuth: [] }, { mutualTLS: [] }],
1385
1516
  components: {
1386
1517
  securitySchemes: {
1518
+ mutualTLS: {
1519
+ type: "mutualTLS",
1520
+ description: "EE project API only, on the dedicated authority returned by anonymous HTTPS discovery. Present one registered client certificate without a bearer or query token. Ordinary browser-facing hosts never request a client certificate."
1521
+ },
1387
1522
  bearerAuth: {
1388
1523
  type: "http",
1389
1524
  scheme: "bearer",
@@ -1451,6 +1586,64 @@ var engineOpenApiDocument = {
1451
1586
  Forbidden: errorResponse("The token is not authorized.")
1452
1587
  },
1453
1588
  schemas: {
1589
+ EngineDiscovery: {
1590
+ type: "object",
1591
+ required: ["version", "projectEndpoint", "mtlsApiUrl", "capabilities"],
1592
+ properties: {
1593
+ version: { type: "integer", const: 1 },
1594
+ projectEndpoint: { type: "string" },
1595
+ mtlsApiUrl: {
1596
+ type: "string",
1597
+ format: "uri",
1598
+ example: "https://project.mtls.engine.example/api"
1599
+ },
1600
+ capabilities: {
1601
+ type: "array",
1602
+ items: { type: "string" },
1603
+ examples: [["engine-api-mtls", "turn-credentials-v2"]]
1604
+ }
1605
+ }
1606
+ },
1607
+ CreateTURNCredentials: {
1608
+ type: "object",
1609
+ additionalProperties: false,
1610
+ properties: {
1611
+ ttlSeconds: {
1612
+ type: "integer",
1613
+ minimum: 1,
1614
+ maximum: 3600,
1615
+ default: 600
1616
+ }
1617
+ }
1618
+ },
1619
+ TURNCredentials: {
1620
+ type: "object",
1621
+ required: ["username", "credential", "urls", "ttl"],
1622
+ properties: {
1623
+ username: {
1624
+ type: "string",
1625
+ maxLength: 512,
1626
+ description: "Opaque short-lived v2 issued identity. Do not parse in clients."
1627
+ },
1628
+ credential: {
1629
+ type: "string",
1630
+ maxLength: 1024,
1631
+ description: "Short-lived TURN password."
1632
+ },
1633
+ urls: {
1634
+ type: "array",
1635
+ minItems: 1,
1636
+ maxItems: 16,
1637
+ items: { type: "string", maxLength: 2048 }
1638
+ },
1639
+ ttl: {
1640
+ type: "integer",
1641
+ minimum: 1,
1642
+ maximum: 3600,
1643
+ description: "Effective remaining lifetime in seconds, clipped to identity, source credential and certificate expiry."
1644
+ }
1645
+ }
1646
+ },
1454
1647
  Client: jsonSchemaFor(clientSchema),
1455
1648
  Event: jsonSchemaFor(wsEventsSchema),
1456
1649
  ListClientsParams: jsonSchemaFor(listClientsParamsSchema),
@@ -1485,6 +1678,133 @@ var engineOpenApiDocument = {
1485
1678
  "read-managed-webtty-recordings": ["webtty.logs.read-only"]
1486
1679
  },
1487
1680
  paths: {
1681
+ "/.well-known/rstream/engine": {
1682
+ get: {
1683
+ tags: ["Discovery"],
1684
+ summary: "Discover the dedicated project mTLS API.",
1685
+ operationId: "get_engine_discovery",
1686
+ description: "Anonymous GET on the ordinary project Engine authority. Follow the returned HTTPS URL; do not infer an mTLS hostname. Cache per client using Cache-Control (normally one hour). No certificate, bearer token, cookie or redirect is required. Available only where an explicit EE mapping is configured.",
1687
+ security: [],
1688
+ responses: {
1689
+ "200": {
1690
+ description: "Project-bound discovery. Capabilities describe enabled server features; callers still need the corresponding rights.",
1691
+ headers: {
1692
+ "Cache-Control": {
1693
+ schema: { type: "string" },
1694
+ example: "public, max-age=3600"
1695
+ }
1696
+ },
1697
+ content: {
1698
+ "application/json": {
1699
+ schema: { $ref: "#/components/schemas/EngineDiscovery" }
1700
+ }
1701
+ }
1702
+ },
1703
+ "404": {
1704
+ description: "Dedicated mTLS API is not configured on this authority."
1705
+ }
1706
+ }
1707
+ }
1708
+ },
1709
+ "/api/turn-server/credentials": {
1710
+ post: {
1711
+ tags: ["TURN"],
1712
+ summary: "Issue short-lived project TURN credentials.",
1713
+ operationId: "create_engine_turn_credentials",
1714
+ description: "EE managed projects. Same request and response contract as Control plane project TURN issuance. Empty body is valid. Issued credentials require turn.credentials.create only; turn.relay.allocate applies to local PAT/APP derivation. Issuance does not allocate a relay and does not bypass relay quotas or revocation checks.",
1715
+ security: [{ bearerAuth: [] }, { mutualTLS: [] }],
1716
+ requestBody: {
1717
+ required: false,
1718
+ content: {
1719
+ "application/json": {
1720
+ schema: { $ref: "#/components/schemas/CreateTURNCredentials" }
1721
+ }
1722
+ }
1723
+ },
1724
+ responses: {
1725
+ "200": {
1726
+ description: "Short-lived project TURN credentials.",
1727
+ headers: {
1728
+ "Cache-Control": {
1729
+ schema: { type: "string" },
1730
+ example: "no-store"
1731
+ }
1732
+ },
1733
+ content: {
1734
+ "application/json": {
1735
+ schema: { $ref: "#/components/schemas/TURNCredentials" }
1736
+ }
1737
+ }
1738
+ },
1739
+ "400": {
1740
+ description: "Malformed, unknown or out-of-range request fields; body exceeds 4 KiB.",
1741
+ headers: {
1742
+ "Cache-Control": {
1743
+ schema: { type: "string" },
1744
+ example: "no-store"
1745
+ }
1746
+ }
1747
+ },
1748
+ "401": {
1749
+ description: "Missing, invalid, expired or mixed authentication proofs.",
1750
+ headers: {
1751
+ "Cache-Control": {
1752
+ schema: { type: "string" },
1753
+ example: "no-store"
1754
+ }
1755
+ }
1756
+ },
1757
+ "403": {
1758
+ description: "Missing turn.credentials.create permission, workspace membership or project resource scope.",
1759
+ headers: {
1760
+ "Cache-Control": {
1761
+ schema: { type: "string" },
1762
+ example: "no-store"
1763
+ }
1764
+ }
1765
+ },
1766
+ "404": {
1767
+ description: "Project not found.",
1768
+ headers: {
1769
+ "Cache-Control": {
1770
+ schema: { type: "string" },
1771
+ example: "no-store"
1772
+ }
1773
+ }
1774
+ },
1775
+ "409": {
1776
+ description: "Project or serving infrastructure is inactive.",
1777
+ headers: {
1778
+ "Cache-Control": {
1779
+ schema: { type: "string" },
1780
+ example: "no-store"
1781
+ }
1782
+ }
1783
+ },
1784
+ "429": {
1785
+ description: "Rate limit exceeded; do not retry through another issuer.",
1786
+ headers: {
1787
+ "Cache-Control": {
1788
+ schema: { type: "string" },
1789
+ example: "no-store"
1790
+ }
1791
+ }
1792
+ },
1793
+ "503": {
1794
+ description: "Authentication backend, serving metadata or issued keyring unavailable.",
1795
+ headers: {
1796
+ "Cache-Control": {
1797
+ schema: { type: "string" },
1798
+ example: "no-store"
1799
+ }
1800
+ }
1801
+ }
1802
+ },
1803
+ "x-rstream-required-rules": [
1804
+ { resource: "engine.turn-server.credentials", action: "create" }
1805
+ ]
1806
+ }
1807
+ },
1488
1808
  "/api/openapi.json": {
1489
1809
  get: {
1490
1810
  tags: ["Discovery"],
@@ -1511,7 +1831,7 @@ var engineOpenApiDocument = {
1511
1831
  summary: "List visible engine clients.",
1512
1832
  operationId: "list_engine_clients",
1513
1833
  parameters: [{ $ref: "#/components/parameters/ListClientsParams" }],
1514
- security: [{ bearerAuth: [] }],
1834
+ security: [{ bearerAuth: [] }, { mutualTLS: [] }],
1515
1835
  responses: {
1516
1836
  "200": jsonArrayResponse(
1517
1837
  "Visible clients.",
@@ -1530,7 +1850,7 @@ var engineOpenApiDocument = {
1530
1850
  summary: "List visible engine tunnels.",
1531
1851
  operationId: "list_engine_tunnels",
1532
1852
  parameters: [{ $ref: "#/components/parameters/ListTunnelsParams" }],
1533
- security: [{ bearerAuth: [] }],
1853
+ security: [{ bearerAuth: [] }, { mutualTLS: [] }],
1534
1854
  responses: {
1535
1855
  "200": jsonArrayResponse(
1536
1856
  "Visible tunnels. Fine-grained list scopes may filter objects and project fields.",
@@ -1550,7 +1870,7 @@ var engineOpenApiDocument = {
1550
1870
  summary: "Subscribe to visible engine state through Server-Sent Events.",
1551
1871
  operationId: "watch_engine_events_sse",
1552
1872
  parameters: [{ $ref: "#/components/parameters/WatchParams" }],
1553
- security: [{ bearerAuth: [] }],
1873
+ security: [{ bearerAuth: [] }, { mutualTLS: [] }],
1554
1874
  responses: {
1555
1875
  "200": streamResponse(
1556
1876
  "SSE stream. Each event is emitted as a data line containing a JSON Event object."
@@ -1569,7 +1889,7 @@ var engineOpenApiDocument = {
1569
1889
  summary: "Subscribe to visible engine state through a WebSocket upgrade.",
1570
1890
  operationId: "watch_engine_events_websocket",
1571
1891
  parameters: [{ $ref: "#/components/parameters/WatchParams" }],
1572
- security: [{ bearerAuth: [] }],
1892
+ security: [{ bearerAuth: [] }, { mutualTLS: [] }],
1573
1893
  responses: {
1574
1894
  "101": {
1575
1895
  description: "WebSocket connection accepted. Frames contain JSON Event objects."
@@ -1593,6 +1913,113 @@ var engineOpenApiJson = `${JSON.stringify(
1593
1913
 
1594
1914
  // src/resolution.ts
1595
1915
  var import_rstream3 = require("@rstreamlabs/rstream");
1916
+
1917
+ // src/http-lifetime.ts
1918
+ var requestLifetime = (parents, milliseconds) => {
1919
+ const controller = new AbortController();
1920
+ const signals = parents.filter((signal) => signal != null);
1921
+ const listeners = signals.map((signal) => ({
1922
+ signal,
1923
+ abort: () => controller.abort(signal.reason)
1924
+ }));
1925
+ for (const listener of listeners) {
1926
+ if (listener.signal.aborted) listener.abort();
1927
+ else
1928
+ listener.signal.addEventListener("abort", listener.abort, { once: true });
1929
+ }
1930
+ const timer = setTimeout(
1931
+ () => controller.abort(
1932
+ new DOMException("Engine request timed out.", "TimeoutError")
1933
+ ),
1934
+ milliseconds
1935
+ );
1936
+ return {
1937
+ signal: controller.signal,
1938
+ stopTimer: () => clearTimeout(timer),
1939
+ abort: () => controller.abort(
1940
+ new DOMException("Engine request closed.", "AbortError")
1941
+ ),
1942
+ close: () => {
1943
+ clearTimeout(timer);
1944
+ for (const listener of listeners)
1945
+ listener.signal.removeEventListener("abort", listener.abort);
1946
+ }
1947
+ };
1948
+ };
1949
+ var readChunks = async (reader, limit, chunks, size) => {
1950
+ const result = await reader.read();
1951
+ if (result.done) return chunks;
1952
+ const total = size + result.value.byteLength;
1953
+ if (total > limit) {
1954
+ await reader.cancel();
1955
+ throw new Error("HTTP response exceeds the size limit.");
1956
+ }
1957
+ chunks.push(result.value);
1958
+ return readChunks(reader, limit, chunks, total);
1959
+ };
1960
+ var readBoundedText = async (response, limit) => {
1961
+ if (Number(response.headers.get("content-length")) > limit) {
1962
+ await response.body?.cancel();
1963
+ throw new Error("HTTP response exceeds the size limit.");
1964
+ }
1965
+ if (!response.body) return "";
1966
+ const reader = response.body.getReader();
1967
+ const chunks = await readChunks(reader, limit, [], 0).finally(
1968
+ () => reader.releaseLock()
1969
+ );
1970
+ const bytes = new Uint8Array(
1971
+ chunks.reduce((size, chunk) => size + chunk.byteLength, 0)
1972
+ );
1973
+ chunks.reduce((offset, chunk) => {
1974
+ bytes.set(chunk, offset);
1975
+ return offset + chunk.byteLength;
1976
+ }, 0);
1977
+ return new TextDecoder().decode(bytes);
1978
+ };
1979
+ var waitWithSignal = async (promise, signal) => {
1980
+ if (!signal) return promise;
1981
+ if (signal.aborted) {
1982
+ void promise.catch(() => void 0);
1983
+ signal.throwIfAborted();
1984
+ }
1985
+ return new Promise((resolve, reject) => {
1986
+ const abort = () => reject(signal.reason);
1987
+ signal.addEventListener("abort", abort, { once: true });
1988
+ void promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
1989
+ });
1990
+ };
1991
+
1992
+ // src/control-plane-fetch.ts
1993
+ function createBoundedControlPlaneFetch(timeoutMilliseconds, parent, fetch2 = globalThis.fetch, maxResponseBytes = 16384) {
1994
+ return async (input, init) => {
1995
+ const request2 = new Request(input, init);
1996
+ const headers = new Headers(request2.headers);
1997
+ headers.set("Connection", "close");
1998
+ const lifetime = requestLifetime(
1999
+ [parent, request2.signal],
2000
+ timeoutMilliseconds
2001
+ );
2002
+ try {
2003
+ lifetime.signal.throwIfAborted();
2004
+ const response = await fetch2(input, {
2005
+ ...init,
2006
+ redirect: "manual",
2007
+ headers,
2008
+ signal: lifetime.signal
2009
+ });
2010
+ const responseBody2 = response.body === null ? null : await readBoundedText(response, maxResponseBytes);
2011
+ return new Response(responseBody2, {
2012
+ headers: Object.fromEntries(response.headers.entries()),
2013
+ status: response.status,
2014
+ statusText: response.statusText
2015
+ });
2016
+ } finally {
2017
+ lifetime.close();
2018
+ }
2019
+ };
2020
+ }
2021
+
2022
+ // src/resolution.ts
1596
2023
  var import_rstream4 = require("@rstreamlabs/rstream");
1597
2024
  var import_rstream5 = require("@rstreamlabs/rstream");
1598
2025
  var import_rstream6 = require("@rstreamlabs/rstream");
@@ -1726,6 +2153,12 @@ async function resolveManagedTunnelsProject(config) {
1726
2153
  );
1727
2154
  }
1728
2155
  const controlPlane = new import_rstream7.RstreamClient({
2156
+ fetch: createBoundedControlPlaneFetch(
2157
+ 1e4,
2158
+ void 0,
2159
+ globalThis.fetch,
2160
+ 1024 * 1024
2161
+ ),
1729
2162
  apiUrl: resolveTunnelsAPIURL(config.apiUrl),
1730
2163
  controlPlaneHeaders: config.controlPlaneHeaders,
1731
2164
  credentials
@@ -1766,62 +2199,231 @@ var RstreamTunnelsResource = class {
1766
2199
  };
1767
2200
 
1768
2201
  // src/tunnels.ts
1769
- var import_rstream12 = require("@rstreamlabs/rstream");
1770
- var import_rstream13 = require("@rstreamlabs/rstream");
1771
2202
  var import_rstream14 = require("@rstreamlabs/rstream");
2203
+ var import_rstream15 = require("@rstreamlabs/rstream");
1772
2204
 
1773
- // src/turn.ts
2205
+ // src/engine-http.ts
1774
2206
  var import_rstream8 = require("@rstreamlabs/rstream");
1775
- var import_rstream9 = require("@rstreamlabs/rstream");
1776
- var import_rstream10 = require("@rstreamlabs/rstream");
1777
- var import_turn = require("@rstreamlabs/rstream/turn");
1778
-
1779
- // src/control-plane-fetch.ts
1780
- function createRequestLifetime(parent, timeoutMilliseconds) {
1781
- const controller = new AbortController();
1782
- const abort = () => controller.abort(parent.reason);
1783
- if (parent.aborted) {
1784
- abort();
1785
- } else {
1786
- parent.addEventListener("abort", abort, { once: true });
1787
- }
1788
- const timeout = setTimeout(
1789
- () => controller.abort(new Error("Control plane request timed out.")),
1790
- timeoutMilliseconds
1791
- );
1792
- return {
1793
- close: () => {
1794
- clearTimeout(timeout);
1795
- parent.removeEventListener("abort", abort);
1796
- },
1797
- signal: controller.signal
1798
- };
1799
- }
1800
- function createBoundedControlPlaneFetch(timeoutMilliseconds) {
1801
- return async (input, init) => {
1802
- const request = new Request(input, init);
1803
- const headers = new Headers(request.headers);
1804
- headers.set("Connection", "close");
1805
- const lifetime = createRequestLifetime(request.signal, timeoutMilliseconds);
2207
+ var import_zod = require("zod");
2208
+ var discoverySchema = import_zod.z.object({
2209
+ version: import_zod.z.literal(1),
2210
+ projectEndpoint: import_zod.z.string(),
2211
+ mtlsApiUrl: import_zod.z.string(),
2212
+ capabilities: import_zod.z.array(import_zod.z.string()).max(32)
2213
+ });
2214
+ var hasClientCertificate = (tls) => tls?.cert !== void 0 || tls?.key !== void 0;
2215
+ var validateMTLSURL = (target, source) => {
2216
+ const url = new URL(target);
2217
+ if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash || url.pathname !== "/api" || url.hostname.split(".")[0] !== source.hostname.split(".")[0])
2218
+ throw new Error("Invalid discovered mTLS Engine API URL.");
2219
+ normalizeEngineAddress(url.host);
2220
+ return url.href;
2221
+ };
2222
+ var discoveryCacheMilliseconds = (response) => {
2223
+ const cache = response.headers.get("cache-control") ?? "";
2224
+ if (/\bno-(?:store|cache)\b/i.test(cache)) return 0;
2225
+ const seconds = cache.match(/\bmax-age\s*=\s*"?(\d+)/i)?.[1];
2226
+ return Math.min(seconds === void 0 ? 3600 : Number(seconds), 86400) * 1e3;
2227
+ };
2228
+ var EngineHTTP = class {
2229
+ constructor(options) {
2230
+ this.options = options;
2231
+ if (hasClientCertificate(options.tls) && (!options.tls?.cert || !options.tls?.key))
2232
+ throw new Error("mTLS requires both a certificate and its private key.");
2233
+ }
2234
+ options;
2235
+ closed = new AbortController();
2236
+ authenticated;
2237
+ anonymous;
2238
+ discovery;
2239
+ flight;
2240
+ get usesMTLS() {
2241
+ return hasClientCertificate(this.options.tls);
2242
+ }
2243
+ async close() {
2244
+ this.closed.abort(new Error("Engine HTTP client is closed."));
2245
+ this.discovery = void 0;
2246
+ await Promise.all([
2247
+ this.authenticated?.then((client) => client.close()),
2248
+ this.anonymous?.then((client) => client.close())
2249
+ ]);
2250
+ }
2251
+ async transport(anonymous) {
2252
+ this.closed.signal.throwIfAborted();
2253
+ if (!anonymous && this.options.fetch) return this.options.fetch;
2254
+ if (!this.options.tls) return globalThis.fetch;
2255
+ if (anonymous) {
2256
+ this.anonymous ??= Promise.resolve().then(() => (init_node_engine_http(), node_engine_http_exports)).then(
2257
+ ({ NodeEngineHTTP: NodeEngineHTTP2 }) => new NodeEngineHTTP2({
2258
+ ca: this.options.tls?.ca,
2259
+ rejectUnauthorized: true
2260
+ })
2261
+ );
2262
+ return (await this.anonymous).fetch;
2263
+ }
2264
+ this.authenticated ??= Promise.resolve().then(() => (init_node_engine_http(), node_engine_http_exports)).then(
2265
+ ({ NodeEngineHTTP: NodeEngineHTTP2 }) => new NodeEngineHTTP2(this.options.tls ?? {})
2266
+ );
2267
+ return (await this.authenticated).fetch;
2268
+ }
2269
+ async apiURL(engine, signal) {
2270
+ this.closed.signal.throwIfAborted();
2271
+ signal?.throwIfAborted();
2272
+ const source = new URL(`https://${normalizeEngineAddress(engine)}`);
2273
+ if (!this.usesMTLS) return `https://${normalizeEngineAddress(engine)}/api`;
2274
+ if (this.options.mtlsApiUrl)
2275
+ return validateMTLSURL(this.options.mtlsApiUrl, source);
2276
+ if (this.discovery?.source === source.origin && this.discovery.expiresAt > Date.now())
2277
+ return this.discovery.url;
2278
+ const flight = this.flight?.source === source.origin ? this.flight : { source: source.origin, promise: this.discover(source) };
2279
+ this.flight = flight;
2280
+ try {
2281
+ const discovery = await waitWithSignal(flight.promise, signal);
2282
+ this.discovery = discovery;
2283
+ return discovery.url;
2284
+ } finally {
2285
+ void flight.promise.then(
2286
+ () => {
2287
+ if (this.flight === flight) this.flight = void 0;
2288
+ },
2289
+ () => {
2290
+ if (this.flight === flight) this.flight = void 0;
2291
+ }
2292
+ );
2293
+ }
2294
+ }
2295
+ async discover(source) {
2296
+ const lifetime = requestLifetime([this.closed.signal], 5e3);
2297
+ try {
2298
+ const fetch2 = await this.transport(true);
2299
+ const response = await fetch2(
2300
+ new URL("/.well-known/rstream/engine", source),
2301
+ {
2302
+ method: "GET",
2303
+ credentials: "omit",
2304
+ redirect: "manual",
2305
+ signal: lifetime.signal
2306
+ }
2307
+ );
2308
+ if (!response.ok) {
2309
+ await response.body?.cancel();
2310
+ throw new import_rstream8.RstreamHTTPError(
2311
+ response.status,
2312
+ "mTLS Engine discovery failed."
2313
+ );
2314
+ }
2315
+ const text = await readBoundedText(response, 16384);
2316
+ const value = JSON.parse(text);
2317
+ const document = discoverySchema.parse(value);
2318
+ if (document.projectEndpoint !== source.hostname.split(".")[0] || !document.capabilities.includes("engine-api-mtls"))
2319
+ throw new Error(
2320
+ "Engine discovery does not advertise mTLS for this project."
2321
+ );
2322
+ return {
2323
+ source: source.origin,
2324
+ url: validateMTLSURL(document.mtlsApiUrl, source),
2325
+ expiresAt: Date.now() + discoveryCacheMilliseconds(response)
2326
+ };
2327
+ } finally {
2328
+ lifetime.close();
2329
+ }
2330
+ }
2331
+ async eventStream(engine, options) {
2332
+ const lifetime = requestLifetime(
2333
+ [this.closed.signal, options.signal],
2334
+ 1e4
2335
+ );
1806
2336
  try {
1807
- const response = await globalThis.fetch(request, {
2337
+ const base = await this.apiURL(engine, lifetime.signal);
2338
+ const headers = new Headers(options.headers);
2339
+ if (this.usesMTLS && headers.has("authorization"))
2340
+ throw new Error("Use a token or mTLS, not both.");
2341
+ headers.set("Accept", "text/event-stream");
2342
+ const fetch2 = await this.transport(false);
2343
+ const response = await fetch2(`${base}/sse`, {
2344
+ method: "GET",
1808
2345
  headers,
2346
+ credentials: "omit",
2347
+ redirect: "manual",
1809
2348
  signal: lifetime.signal
1810
2349
  });
1811
- const responseBody = response.body === null ? null : new Uint8Array(await response.arrayBuffer());
1812
- return new Response(responseBody, {
1813
- headers: Object.fromEntries(response.headers.entries()),
2350
+ if (!response.ok)
2351
+ throw new import_rstream8.RstreamHTTPError(
2352
+ response.status,
2353
+ await readBoundedText(response, 16384)
2354
+ );
2355
+ if (!response.headers.get("content-type")?.startsWith("text/event-stream"))
2356
+ throw new Error("Engine returned an invalid event stream.");
2357
+ lifetime.stopTimer();
2358
+ return {
2359
+ response,
2360
+ close: () => {
2361
+ lifetime.abort();
2362
+ lifetime.close();
2363
+ }
2364
+ };
2365
+ } catch (error) {
2366
+ lifetime.abort();
2367
+ lifetime.close();
2368
+ if (!(error instanceof import_rstream8.RstreamHTTPError)) this.discovery = void 0;
2369
+ throw error;
2370
+ }
2371
+ }
2372
+ async response(engine, path, options = {}, maxBytes = 16 * 1024 * 1024) {
2373
+ if (!path.startsWith("/") || path.startsWith("//") || path.includes("\\"))
2374
+ throw new Error("Engine request path must be a relative absolute path.");
2375
+ const lifetime = requestLifetime(
2376
+ [this.closed.signal, options.signal],
2377
+ 1e4
2378
+ );
2379
+ try {
2380
+ const base = await this.apiURL(engine, lifetime.signal);
2381
+ const url = new URL(`${base}${path}`);
2382
+ if (url.origin !== new URL(base).origin || !url.pathname.startsWith("/api/") || url.username || url.password)
2383
+ throw new Error(
2384
+ "Engine request path must remain inside the Engine API."
2385
+ );
2386
+ const headers = new Headers(options.headers);
2387
+ if (this.usesMTLS && (headers.has("authorization") || url.searchParams.has("rstream.token")))
2388
+ throw new Error("Use a token or mTLS, not both.");
2389
+ const fetch2 = await this.transport(false);
2390
+ const response = await fetch2(`${base}${path}`, {
2391
+ ...options,
2392
+ headers,
2393
+ credentials: "omit",
2394
+ redirect: "manual",
2395
+ signal: lifetime.signal
2396
+ });
2397
+ const text = await readBoundedText(
2398
+ response,
2399
+ response.ok ? maxBytes : 16384
2400
+ );
2401
+ if (!response.ok) throw new import_rstream8.RstreamHTTPError(response.status, text);
2402
+ return new Response([204, 205].includes(response.status) ? null : text, {
1814
2403
  status: response.status,
1815
- statusText: response.statusText
2404
+ headers: response.headers
1816
2405
  });
2406
+ } catch (error) {
2407
+ if (!(error instanceof import_rstream8.RstreamHTTPError)) this.discovery = void 0;
2408
+ throw error;
1817
2409
  } finally {
1818
2410
  lifetime.close();
1819
2411
  }
1820
- };
1821
- }
2412
+ }
2413
+ };
2414
+
2415
+ // src/tunnels.ts
2416
+ var import_rstream16 = require("@rstreamlabs/rstream");
2417
+ var import_rstream17 = require("@rstreamlabs/rstream");
1822
2418
 
1823
2419
  // src/turn.ts
1824
- var z8 = __toESM(require("zod"));
2420
+ var import_rstream9 = require("@rstreamlabs/rstream");
2421
+ var import_rstream10 = require("@rstreamlabs/rstream");
2422
+ var import_rstream11 = require("@rstreamlabs/rstream");
2423
+ var import_rstream12 = require("@rstreamlabs/rstream");
2424
+ var import_rstream13 = require("@rstreamlabs/rstream");
2425
+ var import_turn = require("@rstreamlabs/rstream/turn");
2426
+ var z9 = __toESM(require("zod"));
1825
2427
  var import_crypto = __toESM(require("crypto"));
1826
2428
  var defaultTURNPort = 3478;
1827
2429
  var defaultTURNSPort = 5349;
@@ -1830,20 +2432,31 @@ var maxTURNCredentialTTLSeconds = 60 * 60;
1830
2432
  var maxTURNKeyringBytes = 8 * 1024;
1831
2433
  var defaultAPIRequestTimeoutMilliseconds = 1e4;
1832
2434
  var maxAPIRequestTimeoutMilliseconds = 6e4;
1833
- var turnTokenClaimsSchema = z8.object({
1834
- exp: z8.number().int().optional(),
1835
- iat: z8.number().int().optional(),
1836
- token_endpoint: z8.string().regex(/^[0-9a-f]{8}$/i).optional(),
1837
- tokendpoint: z8.string().regex(/^[0-9a-f]{8}$/i).optional(),
1838
- type: z8.enum(["app", "auth", "pat"])
2435
+ var turnTokenClaimsSchema = z9.object({
2436
+ permissions: z9.array(z9.string()).nullable().optional(),
2437
+ exp: z9.number().int().optional(),
2438
+ iat: z9.number().int().optional(),
2439
+ token_endpoint: z9.string().regex(/^[A-Za-z0-9_.-]{1,128}$/).optional(),
2440
+ tokendpoint: z9.string().regex(/^[A-Za-z0-9_.-]{1,128}$/).optional(),
2441
+ type: z9.enum(["app", "auth", "pat"])
1839
2442
  });
1840
- var turnCredentialModeSchema = z8.enum(["api", "app", "pat"]);
2443
+ var turnCredentialModeSchema = z9.enum([
2444
+ "auto",
2445
+ "api",
2446
+ "engine",
2447
+ "app",
2448
+ "pat"
2449
+ ]);
1841
2450
  function normalizeOptionalString2(value) {
1842
2451
  const normalized = value?.trim();
1843
2452
  return normalized ? normalized : void 0;
1844
2453
  }
1845
2454
  function normalizeClusterDomain(clusterDomain) {
1846
2455
  const normalized = normalizeOptionalString2(clusterDomain);
2456
+ if (normalized && (normalized.length > 253 || !normalized.split(".").every(
2457
+ (label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i.test(label)
2458
+ )))
2459
+ throw new Error("TURN domain and realm must be valid DNS names.");
1847
2460
  return normalized?.toLowerCase();
1848
2461
  }
1849
2462
  function normalizeNow(now) {
@@ -1958,14 +2571,27 @@ function requireOption(value, message) {
1958
2571
  }
1959
2572
  throw new Error(message);
1960
2573
  }
1961
- function resolveMode(credentials, mode) {
1962
- if (mode !== void 0) {
1963
- return turnCredentialModeSchema.parse(mode);
1964
- }
1965
- if ((0, import_rstream8.isClientCredentials)(credentials)) {
1966
- return "app";
2574
+ function automaticPATClaims(token) {
2575
+ try {
2576
+ return parseTURNTokenClaims(token);
2577
+ } catch {
2578
+ return void 0;
1967
2579
  }
1968
- return "api";
2580
+ }
2581
+ function resolveMode(options) {
2582
+ const requested = turnCredentialModeSchema.parse(options.mode ?? "auto");
2583
+ if (requested !== "auto") return requested;
2584
+ if (options.client?.usesMTLS || hasClientCertificate(options.tls))
2585
+ return "engine";
2586
+ const claims = (0, import_rstream11.isTokenCredentials)(options.credentials) ? automaticPATClaims(options.credentials.token) : void 0;
2587
+ if (claims?.token_endpoint && claims.tokendpoint && claims.token_endpoint !== claims.tokendpoint)
2588
+ throw new Error("Conflicting PAT token endpoints.");
2589
+ const localTarget = options.projectEndpoint && (options.clusterDomain || options.turnDomain && options.turnRealm);
2590
+ if (!localTarget) return "auto";
2591
+ if ((0, import_rstream9.isClientCredentials)(options.credentials)) return "app";
2592
+ if (claims?.type === "pat" && claims.exp !== void 0 && (claims.token_endpoint || claims.tokendpoint) && (claims.permissions === null || claims.permissions?.includes("turn.relay.allocate")))
2593
+ return "pat";
2594
+ return "auto";
1969
2595
  }
1970
2596
  async function readBoundedTURNKeyring(reader, chunks = [], size = 0) {
1971
2597
  const result = await reader.read();
@@ -1985,7 +2611,7 @@ async function readBoundedTURNKeyring(reader, chunks = [], size = 0) {
1985
2611
  }
1986
2612
  function parseTURNServerPublicKeyHex(value) {
1987
2613
  const normalized = value.trim();
1988
- if (normalized.length === 0 || normalized.length % 2 !== 0 || !/^[0-9a-f]+$/i.test(normalized)) {
2614
+ if (normalized.length === 0 || normalized.length > maxTURNKeyringBytes || normalized.length % 2 !== 0 || !/^[0-9a-f]+$/i.test(normalized)) {
1989
2615
  throw new Error("TURN keyring must contain hexadecimal DER key material.");
1990
2616
  }
1991
2617
  try {
@@ -2008,6 +2634,10 @@ function parseTURNServerPublicKeyHex(value) {
2008
2634
  }
2009
2635
  }
2010
2636
  function parseAPPPrivateKey(value) {
2637
+ if (value.length > 16384 || value.length % 2 !== 0 || !/^[0-9a-f]+$/i.test(value))
2638
+ throw new Error(
2639
+ "TURN APP client secret must contain hexadecimal DER key material."
2640
+ );
2011
2641
  try {
2012
2642
  const key = import_crypto.default.createPrivateKey({
2013
2643
  key: Buffer.from(value, "hex"),
@@ -2043,19 +2673,38 @@ async function loadTURNServerPublicKeyHex(clusterDomain, options) {
2043
2673
  `/keyrings/turn/${clusterDomain}.spki.der.hex`,
2044
2674
  keyringBaseURL
2045
2675
  );
2046
- const response = await fetch(url, { redirect: "manual" });
2047
- if (response.status >= 300 && response.status < 400) {
2676
+ if (url.protocol !== "https:" || url.username || url.password)
2048
2677
  throw new Error(
2049
- "TURN keyring request was redirected; redirects are refused."
2678
+ "TURN keyring requests require HTTPS without URL credentials."
2050
2679
  );
2680
+ const lifetime = requestLifetime(
2681
+ [options.signal],
2682
+ normalizeAPIRequestTimeoutMilliseconds(options.requestTimeoutMilliseconds)
2683
+ );
2684
+ try {
2685
+ const response = await fetch(url, {
2686
+ redirect: "manual",
2687
+ credentials: "omit",
2688
+ signal: lifetime.signal
2689
+ });
2690
+ if (response.status >= 300 && response.status < 400) {
2691
+ await response.body?.cancel();
2692
+ throw new Error(
2693
+ "TURN keyring request was redirected; redirects are refused."
2694
+ );
2695
+ }
2696
+ if (!response.ok) {
2697
+ await response.body?.cancel();
2698
+ throw new Error(`TURN keyring request returned HTTP ${response.status}.`);
2699
+ }
2700
+ if (!response.body) {
2701
+ throw new Error("TURN keyring response body is missing.");
2702
+ }
2703
+ const reader = response.body.getReader();
2704
+ return (await readBoundedTURNKeyring(reader).finally(() => reader.releaseLock())).trim();
2705
+ } finally {
2706
+ lifetime.close();
2051
2707
  }
2052
- if (!response.ok) {
2053
- throw new Error(`TURN keyring request returned HTTP ${response.status}.`);
2054
- }
2055
- if (!response.body) {
2056
- throw new Error("TURN keyring response body is missing.");
2057
- }
2058
- return (await readBoundedTURNKeyring(response.body.getReader())).trim();
2059
2708
  }
2060
2709
  async function createAPITURNCredentials(options) {
2061
2710
  const projectId = normalizeOptionalString2(options.projectId);
@@ -2069,11 +2718,21 @@ async function createAPITURNCredentials(options) {
2069
2718
  options.requestTimeoutMilliseconds
2070
2719
  );
2071
2720
  const params = createAPITURNCredentialsParams(options.ttlSeconds);
2072
- const request = options.fetch ?? createBoundedControlPlaneFetch(requestTimeoutMilliseconds);
2073
- const client = new import_rstream10.RstreamClient({
2721
+ const request2 = createBoundedControlPlaneFetch(
2722
+ requestTimeoutMilliseconds,
2723
+ options.signal,
2724
+ options.fetch
2725
+ );
2726
+ const client = new import_rstream12.RstreamClient({
2727
+ controlPlaneHeaders: options.controlPlaneHeaders,
2074
2728
  apiUrl: options.apiUrl,
2075
- credentials: options.credentials,
2076
- fetch: request
2729
+ credentials: (0, import_rstream9.isClientCredentials)(options.credentials) ? {
2730
+ token: (0, import_rstream10.createClientCredentialsToken)(options.credentials, {
2731
+ expiresInSeconds: 60,
2732
+ claims: { permissions: ["turn.credentials.create"] }
2733
+ }).token
2734
+ } : options.credentials,
2735
+ fetch: request2
2077
2736
  });
2078
2737
  if (projectId) {
2079
2738
  return await client.tunnels.projects.createTurnCredentials(
@@ -2116,6 +2775,8 @@ function createPATTURNCredentials(options) {
2116
2775
  const claimsTokenEndpoint = normalizeOptionalString2(
2117
2776
  claims.token_endpoint ?? claims.tokendpoint
2118
2777
  );
2778
+ if (claims.token_endpoint && claims.tokendpoint && claims.token_endpoint !== claims.tokendpoint)
2779
+ throw new Error("Conflicting PAT token endpoints.");
2119
2780
  const configuredTokenEndpoint = normalizeOptionalString2(
2120
2781
  options.tokenEndpoint
2121
2782
  );
@@ -2125,6 +2786,8 @@ function createPATTURNCredentials(options) {
2125
2786
  "TURN PAT mode requires a PAT token carrying a token endpoint."
2126
2787
  );
2127
2788
  }
2789
+ requireTURNReference(projectEndpoint);
2790
+ requireTURNReference(tokenEndpoint);
2128
2791
  if (configuredTokenEndpoint !== void 0 && claimsTokenEndpoint !== void 0 && claimsTokenEndpoint !== configuredTokenEndpoint) {
2129
2792
  throw new Error("Token endpoint does not match the PAT token.");
2130
2793
  }
@@ -2169,6 +2832,8 @@ async function createAPPTURNCredentials(options) {
2169
2832
  if (!clientId) {
2170
2833
  throw new Error("Client ID is required for TURN APP mode.");
2171
2834
  }
2835
+ requireTURNReference(projectEndpoint);
2836
+ requireTURNReference(clientId);
2172
2837
  const clientSecret = normalizeOptionalString2(options.clientSecret);
2173
2838
  if (!clientSecret) {
2174
2839
  throw new Error("Client secret is required for TURN APP mode.");
@@ -2201,32 +2866,47 @@ async function createAPPTURNCredentials(options) {
2201
2866
  username
2202
2867
  });
2203
2868
  }
2204
- async function createTURNCredentials(options) {
2869
+ async function createTURNCredentialsForLifetime(options) {
2205
2870
  const credentials = options.credentials;
2206
- const mode = resolveMode(credentials, options.mode);
2871
+ const mode = resolveMode(options);
2207
2872
  const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
2208
- if (mode === "api") {
2873
+ if (mode === "engine") return createEngineTURNCredentials(options);
2874
+ if (options.client?.usesMTLS || hasClientCertificate(options.tls))
2875
+ throw new Error(
2876
+ "Certificate authentication requires automatic or Engine TURN mode."
2877
+ );
2878
+ if (mode === "api" || mode === "auto") {
2209
2879
  const controlPlaneCredentials = options.controlPlaneCredentials ?? credentials;
2210
2880
  if (!controlPlaneCredentials) {
2881
+ if (mode === "auto" && (options.client || options.engine))
2882
+ return createEngineTURNCredentials(options);
2211
2883
  throw new Error(
2212
2884
  "Control-plane credentials are required for TURN API mode."
2213
2885
  );
2214
2886
  }
2215
- return await createAPITURNCredentials({
2216
- apiUrl: options.apiUrl,
2217
- credentials: controlPlaneCredentials,
2218
- fetch: options.fetch,
2219
- projectEndpoint,
2220
- projectId: options.projectId,
2221
- requestTimeoutMilliseconds: options.requestTimeoutMilliseconds,
2222
- ttlSeconds: options.ttlSeconds
2223
- });
2887
+ try {
2888
+ return await createAPITURNCredentials({
2889
+ controlPlaneHeaders: options.controlPlaneHeaders,
2890
+ apiUrl: options.apiUrl,
2891
+ credentials: controlPlaneCredentials,
2892
+ fetch: options.fetch,
2893
+ projectEndpoint,
2894
+ projectId: options.projectId,
2895
+ requestTimeoutMilliseconds: mode === "auto" ? Math.min(options.requestTimeoutMilliseconds ?? 1e4, 5e3) : options.requestTimeoutMilliseconds,
2896
+ signal: options.signal,
2897
+ ttlSeconds: options.ttlSeconds
2898
+ });
2899
+ } catch (error) {
2900
+ if (mode !== "auto" || !projectEndpoint || !(options.client?.configuredEngine || options.engine) || !turnEngineFallbackAllowed(error, options.signal))
2901
+ throw error;
2902
+ return createEngineTURNCredentials(options);
2903
+ }
2224
2904
  }
2225
2905
  const legacyDomain = normalizeClusterDomain(options.clusterDomain);
2226
2906
  const turnDomain = normalizeClusterDomain(options.turnDomain) ?? legacyDomain ?? (projectEndpoint && options.engine ? deriveClusterDomainFromEngine(projectEndpoint, options.engine) : void 0);
2227
2907
  const turnRealm = normalizeClusterDomain(options.turnRealm) ?? legacyDomain;
2228
2908
  if (mode === "pat") {
2229
- if (!(0, import_rstream9.isTokenCredentials)(credentials)) {
2909
+ if (!(0, import_rstream11.isTokenCredentials)(credentials)) {
2230
2910
  throw new Error("A PAT token is required for TURN PAT mode.");
2231
2911
  }
2232
2912
  return createPATTURNCredentials({
@@ -2250,10 +2930,12 @@ async function createTURNCredentials(options) {
2250
2930
  turnsPort: options.turnsPort
2251
2931
  });
2252
2932
  }
2253
- if (!(0, import_rstream8.isClientCredentials)(credentials)) {
2933
+ if (!(0, import_rstream9.isClientCredentials)(credentials)) {
2254
2934
  throw new Error("Client credentials are required for TURN APP mode.");
2255
2935
  }
2256
2936
  return await createAPPTURNCredentials({
2937
+ signal: options.signal,
2938
+ requestTimeoutMilliseconds: options.requestTimeoutMilliseconds,
2257
2939
  apiUrl: options.apiUrl,
2258
2940
  clientId: credentials.clientId,
2259
2941
  clientSecret: credentials.clientSecret,
@@ -2277,25 +2959,139 @@ async function createTURNCredentials(options) {
2277
2959
  turnsPort: options.turnsPort
2278
2960
  });
2279
2961
  }
2962
+ function requireTURNReference(value) {
2963
+ if (!/^[A-Za-z0-9_.-]{1,128}$/.test(value) || value === "-")
2964
+ throw new Error("Invalid TURN project or credential reference.");
2965
+ }
2966
+ function turnEngineFallbackAllowed(error, signal) {
2967
+ if (signal?.aborted) return false;
2968
+ if (error instanceof import_rstream13.RstreamHTTPError)
2969
+ return [502, 503, 504].includes(error.status);
2970
+ if (error instanceof DOMException && error.name === "TimeoutError")
2971
+ return true;
2972
+ if (typeof error !== "object" || error === null) return false;
2973
+ if ("code" in error && typeof error.code === "string")
2974
+ return [
2975
+ "ENOTFOUND",
2976
+ "EAI_AGAIN",
2977
+ "ECONNREFUSED",
2978
+ "ECONNRESET",
2979
+ "ETIMEDOUT",
2980
+ "EHOSTUNREACH",
2981
+ "ENETUNREACH",
2982
+ "UND_ERR_CONNECT_TIMEOUT",
2983
+ "UND_ERR_HEADERS_TIMEOUT",
2984
+ "UND_ERR_SOCKET"
2985
+ ].includes(error.code);
2986
+ return "cause" in error && error.cause !== error ? turnEngineFallbackAllowed(error.cause, signal) : false;
2987
+ }
2988
+ async function createEngineTURNCredentials(options) {
2989
+ const client = options.client ?? new RstreamTunnelsClient({
2990
+ credentials: options.credentials,
2991
+ tls: options.tls,
2992
+ mtlsApiUrl: options.mtlsApiUrl,
2993
+ engine: options.engine,
2994
+ projectEndpoint: options.projectEndpoint,
2995
+ projectId: options.projectId
2996
+ });
2997
+ try {
2998
+ const engine = await client.getEngine();
2999
+ const endpoint = new URL(`https://${engine}`).hostname.split(".")[0];
3000
+ if (options.projectEndpoint && options.projectEndpoint !== endpoint || options.projectId && !options.projectEndpoint)
3001
+ throw new Error("TURN project endpoint must match the Engine client.");
3002
+ if (options.engine && options.engine !== engine)
3003
+ throw new Error("TURN Engine must match the reusable client.");
3004
+ const value = await client.request(
3005
+ "/turn-server/credentials",
3006
+ {
3007
+ method: "POST",
3008
+ signal: options.signal,
3009
+ headers: { "Content-Type": "application/json" },
3010
+ body: JSON.stringify(
3011
+ createAPITURNCredentialsParams(options.ttlSeconds)
3012
+ )
3013
+ },
3014
+ 16384
3015
+ );
3016
+ return import_turn.turnCredentialsSchema.parse(value);
3017
+ } finally {
3018
+ if (!options.client) await client.close();
3019
+ }
3020
+ }
3021
+ function validateTURNCredentialsRequest(options) {
3022
+ normalizeTTLSeconds(options.ttlSeconds);
3023
+ turnCredentialModeSchema.parse(options.mode ?? "auto");
3024
+ return normalizeAPIRequestTimeoutMilliseconds(
3025
+ options.requestTimeoutMilliseconds
3026
+ );
3027
+ }
3028
+ async function createTURNCredentials(options) {
3029
+ const lifetime = requestLifetime(
3030
+ [options.signal],
3031
+ validateTURNCredentialsRequest(options)
3032
+ );
3033
+ try {
3034
+ lifetime.signal.throwIfAborted();
3035
+ if ((options.client?.usesMTLS || hasClientCertificate(options.tls)) && options.credentials)
3036
+ throw new Error("Use a token or mTLS, not both.");
3037
+ if (options.client && options.credentials && !sameTURNCredentials(options.client.credentials, options.credentials))
3038
+ throw new Error(
3039
+ "TURN credentials must match the reusable Engine client."
3040
+ );
3041
+ return await waitWithSignal(
3042
+ createTURNCredentialsForLifetime({
3043
+ ...options,
3044
+ credentials: options.credentials ?? options.client?.credentials,
3045
+ signal: lifetime.signal
3046
+ }),
3047
+ lifetime.signal
3048
+ );
3049
+ } finally {
3050
+ lifetime.close();
3051
+ }
3052
+ }
3053
+ function sameTURNCredentials(left, right) {
3054
+ if ((0, import_rstream11.isTokenCredentials)(left) && (0, import_rstream11.isTokenCredentials)(right))
3055
+ return left.token === right.token;
3056
+ return (0, import_rstream9.isClientCredentials)(left) && (0, import_rstream9.isClientCredentials)(right) && left.clientId === right.clientId && left.clientSecret === right.clientSecret;
3057
+ }
2280
3058
 
2281
3059
  // src/turn-resource.ts
2282
- var import_rstream11 = require("@rstreamlabs/rstream");
2283
3060
  var RstreamTURNResource = class {
2284
3061
  client;
2285
3062
  constructor(client) {
2286
3063
  this.client = client;
2287
3064
  }
2288
3065
  async createCredentials(options = {}) {
3066
+ const lifetime = requestLifetime(
3067
+ [options.signal],
3068
+ validateTURNCredentialsRequest(options)
3069
+ );
3070
+ try {
3071
+ lifetime.signal.throwIfAborted();
3072
+ return await waitWithSignal(
3073
+ this.createForLifetime({ ...options, signal: lifetime.signal }),
3074
+ lifetime.signal
3075
+ );
3076
+ } finally {
3077
+ lifetime.close();
3078
+ }
3079
+ }
3080
+ async createForLifetime(options) {
2289
3081
  const credentials = options.credentials ?? this.client.credentials;
2290
- const localMode = options.mode === "app" || options.mode === "pat" || options.mode === void 0 && (0, import_rstream11.isClientCredentials)(credentials);
2291
- const target = localMode && (options.turnDomain === void 0 || options.turnRealm === void 0) && options.clusterDomain === void 0 ? await this.client.getTURNTarget() : void 0;
2292
- const engine = options.engine ?? (options.clusterDomain === void 0 && options.turnDomain === void 0 && target === void 0 ? await this.client.getEngine().catch(() => void 0) : void 0);
3082
+ const localMode = options.mode === "app" || options.mode === "pat";
3083
+ const target = localMode && !this.client.usesMTLS && (options.turnDomain === void 0 || options.turnRealm === void 0) && options.clusterDomain === void 0 ? await this.client.getTURNTarget() : void 0;
2293
3084
  return await createTURNCredentials({
3085
+ client: this.client,
3086
+ signal: options.signal,
2294
3087
  apiUrl: options.apiUrl ?? this.client.apiUrl,
3088
+ fetch: options.fetch,
3089
+ requestTimeoutMilliseconds: options.requestTimeoutMilliseconds,
2295
3090
  clusterDomain: options.clusterDomain,
3091
+ controlPlaneHeaders: options.controlPlaneHeaders ?? this.client.controlPlaneHeaders,
2296
3092
  controlPlaneCredentials: options.controlPlaneCredentials ?? this.client.controlPlaneCredentials,
2297
3093
  credentials,
2298
- engine,
3094
+ engine: options.engine,
2299
3095
  keyringBaseUrl: options.keyringBaseUrl,
2300
3096
  mode: options.mode,
2301
3097
  now: options.now,
@@ -2464,31 +3260,52 @@ var engineAuthTokenScopes = {
2464
3260
  var RstreamTunnelsClient = class {
2465
3261
  config;
2466
3262
  managedProject;
3263
+ http;
2467
3264
  constructor(config) {
2468
3265
  this.config = config;
3266
+ if (hasClientCertificate(config?.tls) && config?.credentials)
3267
+ throw new Error("Use a token or mTLS, not both.");
3268
+ this.http = new EngineHTTP(config ?? {});
2469
3269
  }
2470
3270
  get apiUrl() {
2471
3271
  return resolveTunnelsAPIURL(this.config?.apiUrl);
2472
3272
  }
2473
3273
  get credentials() {
3274
+ if (this.http.usesMTLS) return void 0;
2474
3275
  return resolveTunnelsCredentials(this.config?.credentials);
2475
3276
  }
2476
3277
  get controlPlaneCredentials() {
3278
+ if (this.http.usesMTLS) return void 0;
2477
3279
  return resolveControlPlaneCredentials(
2478
3280
  this.config?.controlPlaneCredentials,
2479
3281
  this.credentials
2480
3282
  );
2481
3283
  }
3284
+ get controlPlaneHeaders() {
3285
+ return this.config?.controlPlaneHeaders;
3286
+ }
2482
3287
  get projectEndpoint() {
2483
3288
  return this.config?.projectEndpoint;
2484
3289
  }
3290
+ get configuredEngine() {
3291
+ return this.config?.engine ?? (0, import_rstream17.readEnvironment)().engine;
3292
+ }
3293
+ get usesMTLS() {
3294
+ return this.http.usesMTLS;
3295
+ }
3296
+ async close() {
3297
+ await this.http.close();
3298
+ }
3299
+ async getEngineAPIURL(signal) {
3300
+ return this.http.apiURL(await this.getEngine(), signal);
3301
+ }
2485
3302
  async getEngine() {
2486
3303
  const region = resolveTunnelsRegion(this.config?.region);
2487
3304
  const projectEndpoint = normalizeOptionalString3(
2488
3305
  this.config?.projectEndpoint
2489
3306
  );
2490
3307
  if (region !== void 0) {
2491
- if (normalizeOptionalString3(this.config?.engine) !== void 0 || (0, import_rstream14.readEnvironment)().engine !== void 0) {
3308
+ if (normalizeOptionalString3(this.config?.engine) !== void 0 || (0, import_rstream17.readEnvironment)().engine !== void 0) {
2492
3309
  throw new Error(
2493
3310
  "Region selection cannot be combined with an explicit engine override."
2494
3311
  );
@@ -2500,17 +3317,17 @@ var RstreamTunnelsClient = class {
2500
3317
  }
2501
3318
  const project = await this.getManagedProject();
2502
3319
  if (project !== void 0) {
2503
- const engine = (0, import_rstream12.getTunnelsProjectEngine)(project, region);
3320
+ const engine = (0, import_rstream14.getTunnelsProjectEngine)(project, region);
2504
3321
  if (engine) return normalizeEngineAddress(engine);
2505
3322
  }
2506
3323
  throw new Error(
2507
3324
  "Failed to resolve the selected regional engine from the managed tunnels project."
2508
3325
  );
2509
3326
  }
2510
- if (projectEndpoint !== void 0 && normalizeOptionalString3(this.config?.engine) === void 0 && (0, import_rstream14.readEnvironment)().engine === void 0) {
3327
+ if (projectEndpoint !== void 0 && normalizeOptionalString3(this.config?.engine) === void 0 && (0, import_rstream17.readEnvironment)().engine === void 0) {
2511
3328
  const project = await this.getManagedProject();
2512
3329
  if (project !== void 0) {
2513
- const engine = (0, import_rstream12.getTunnelsProjectEngine)(project);
3330
+ const engine = (0, import_rstream14.getTunnelsProjectEngine)(project);
2514
3331
  if (engine) {
2515
3332
  return normalizeEngineAddress(engine);
2516
3333
  }
@@ -2551,6 +3368,10 @@ var RstreamTunnelsClient = class {
2551
3368
  };
2552
3369
  }
2553
3370
  async getManagedProject() {
3371
+ if (this.usesMTLS)
3372
+ throw new Error(
3373
+ "mTLS project resolution requires the configured Engine address; the Control plane does not accept certificates."
3374
+ );
2554
3375
  const projectEndpoint = normalizeOptionalString3(
2555
3376
  this.config?.projectEndpoint
2556
3377
  );
@@ -2626,7 +3447,7 @@ var RstreamTunnelsClient = class {
2626
3447
  if (!credentials) {
2627
3448
  return void 0;
2628
3449
  }
2629
- if (!(0, import_rstream13.isClientCredentials)(credentials)) {
3450
+ if (!(0, import_rstream16.isClientCredentials)(credentials)) {
2630
3451
  return credentials.token;
2631
3452
  }
2632
3453
  const tokenParams = await this.getEngineAuthTokenParams();
@@ -2635,26 +3456,37 @@ var RstreamTunnelsClient = class {
2635
3456
  engine
2636
3457
  })).token;
2637
3458
  }
2638
- async request(path, options) {
3459
+ async openEventStream(signal) {
3460
+ const engine = await this.getEngine();
3461
+ const token = await this.getToken(engine);
3462
+ return this.http.eventStream(engine, {
3463
+ signal,
3464
+ headers: token ? { Authorization: `Bearer ${token}` } : void 0
3465
+ });
3466
+ }
3467
+ async request(path, options, maxResponseBytes) {
2639
3468
  const engine = normalizeEngineAddress(await this.getEngine());
2640
3469
  if (!path.startsWith("/") || path.startsWith("//")) {
2641
3470
  throw new Error("Engine request path must be a relative absolute path.");
2642
3471
  }
2643
- const url = `https://${engine}/api${path}`;
2644
3472
  const headers = new Headers(options?.headers || {});
2645
- const token = await this.getToken(engine);
3473
+ const credentials = this.credentials;
3474
+ const token = path === "/turn-server/credentials" && (0, import_rstream16.isClientCredentials)(credentials) ? (0, import_rstream15.createClientCredentialsToken)(credentials, {
3475
+ expiresInSeconds: 60,
3476
+ claims: { permissions: ["turn.credentials.create"] }
3477
+ }).token : await this.getToken(engine);
2646
3478
  if (token) {
2647
3479
  headers.set("Authorization", `Bearer ${token}`);
2648
3480
  }
2649
- const response = await fetch(url, {
2650
- ...options,
2651
- headers,
2652
- redirect: options?.redirect ?? "manual"
2653
- });
2654
- if (!response.ok) {
2655
- const errorText = await response.text();
2656
- throw new Error(`HTTP error ${response.status}: ${errorText}`);
2657
- }
3481
+ const response = await this.http.response(
3482
+ engine,
3483
+ path,
3484
+ {
3485
+ ...options,
3486
+ headers
3487
+ },
3488
+ maxResponseBytes
3489
+ );
2658
3490
  return await response.json();
2659
3491
  }
2660
3492
  };
@@ -2751,6 +3583,8 @@ function hasWatchOnlyPermissions(permissions) {
2751
3583
  );
2752
3584
  }
2753
3585
  var Watch = class {
3586
+ abort = new AbortController();
3587
+ closeStream;
2754
3588
  connection = null;
2755
3589
  connectionState = "preparing";
2756
3590
  config;
@@ -2765,7 +3599,32 @@ var Watch = class {
2765
3599
  }
2766
3600
  this.connectionState = "connecting";
2767
3601
  try {
3602
+ if (this.config.client?.usesMTLS) {
3603
+ if (this.config.auth !== void 0)
3604
+ throw new Error("Watch: Use a token or mTLS, not both.");
3605
+ if (normalizeTransport(this.config.transport) !== "sse")
3606
+ throw new Error(
3607
+ "Watch: Node.js certificate authentication currently requires SSE."
3608
+ );
3609
+ const stream = await this.config.client.openEventStream(
3610
+ this.abort.signal
3611
+ );
3612
+ if (this.abort.signal.aborted) {
3613
+ stream.close();
3614
+ this.abort.signal.throwIfAborted();
3615
+ }
3616
+ this.closeStream = stream.close;
3617
+ if (!stream.response.body)
3618
+ throw new Error("Watch: Engine event stream is empty.");
3619
+ this.connectionState = "connected";
3620
+ this.events.onConnect?.();
3621
+ void this.readCertificateEvents(stream.response.body).finally(
3622
+ () => this.disconnect()
3623
+ );
3624
+ return;
3625
+ }
2768
3626
  const token = typeof this.config.auth === "function" ? await this.config.auth() : this.config.auth;
3627
+ if (!token) throw new Error("Watch: Authentication is required.");
2769
3628
  validateWatchQueryToken(token);
2770
3629
  const engine = await resolveTunnelsEngine({
2771
3630
  apiUrl: this.config.apiUrl,
@@ -2804,10 +3663,39 @@ var Watch = class {
2804
3663
  };
2805
3664
  }
2806
3665
  } catch (error) {
2807
- this.connectionState = "preparing";
3666
+ this.closeStream?.();
3667
+ this.closeStream = void 0;
3668
+ this.connectionState = this.abort.signal.aborted ? "closed" : "preparing";
2808
3669
  throw error;
2809
3670
  }
2810
3671
  }
3672
+ async readCertificateEvents(body) {
3673
+ const reader = body.getReader();
3674
+ const decoder = new TextDecoder();
3675
+ const state = { buffered: "" };
3676
+ try {
3677
+ while (!this.abort.signal.aborted) {
3678
+ const chunk = await reader.read();
3679
+ if (chunk.done) return;
3680
+ state.buffered += decoder.decode(chunk.value, { stream: true });
3681
+ const frames = state.buffered.split(/\r?\n\r?\n/);
3682
+ state.buffered = frames.pop() ?? "";
3683
+ if (state.buffered.length > 1024 * 1024)
3684
+ throw new Error("Watch event exceeds the size limit.");
3685
+ for (const frame of frames) {
3686
+ if (frame.length > 1024 * 1024)
3687
+ throw new Error("Watch event exceeds the size limit.");
3688
+ const data = frame.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).replace(/^ /, "")).join("\n");
3689
+ if (data) this.handleMessage(data);
3690
+ if (this.abort.signal.aborted) return;
3691
+ }
3692
+ }
3693
+ } catch {
3694
+ this.disconnect();
3695
+ } finally {
3696
+ reader.releaseLock();
3697
+ }
3698
+ }
2811
3699
  handleMessage(data) {
2812
3700
  try {
2813
3701
  const parsed = wsEventsSchema.parse(JSON.parse(data));
@@ -2817,6 +3705,9 @@ var Watch = class {
2817
3705
  }
2818
3706
  }
2819
3707
  disconnect() {
3708
+ this.abort.abort();
3709
+ this.closeStream?.();
3710
+ this.closeStream = void 0;
2820
3711
  if (this.connection) {
2821
3712
  this.connection.onerror = null;
2822
3713
  this.connection.onmessage = null;
@@ -2832,8 +3723,8 @@ var Watch = class {
2832
3723
  };
2833
3724
 
2834
3725
  // src/webtty.ts
2835
- var z9 = __toESM(require("zod"));
2836
- var osFamilySchema = z9.enum([
3726
+ var z10 = __toESM(require("zod"));
3727
+ var osFamilySchema = z10.enum([
2837
3728
  "linux",
2838
3729
  "macos",
2839
3730
  "windows",
@@ -2841,45 +3732,55 @@ var osFamilySchema = z9.enum([
2841
3732
  "openbsd",
2842
3733
  "freebsd"
2843
3734
  ]);
2844
- var webttyCapabilitySchema = z9.enum(["exec", "fs"]);
2845
- var webttyFSModeSchema = z9.enum(["read-write", "read-only"]);
2846
- var webttyEncryptionPolicySchema = z9.enum([
3735
+ var webttyCapabilitySchema = z10.enum(["exec", "fs"]);
3736
+ var webttyFSModeSchema = z10.enum(["read-write", "read-only"]);
3737
+ var webttyEncryptionPolicySchema = z10.enum([
2847
3738
  "disabled",
2848
3739
  "explicit_key",
2849
3740
  "workspace_managed"
2850
3741
  ]);
2851
- var webttyServerSchema = z9.object({
2852
- tunnel_id: z9.string(),
2853
- tunnel_name: z9.string().optional(),
2854
- target: z9.string().optional(),
2855
- rstream_url: z9.string().optional(),
2856
- status: z9.string().optional(),
2857
- publish: z9.boolean().optional(),
2858
- workspace_id: z9.string().optional(),
2859
- project_id: z9.string().optional(),
2860
- tunnel_protocol: z9.enum(["http", "webtty"]),
2861
- managed: z9.boolean(),
2862
- host: z9.string(),
2863
- token_auth: z9.boolean(),
2864
- server_id: z9.string().optional(),
2865
- server_name: z9.string().optional(),
2866
- host_key_id: z9.string().optional(),
2867
- e2e: z9.enum(["disabled", "required"]).optional(),
2868
- client_proof: z9.enum(["none", "required"]).optional(),
3742
+ var webttyTransportSchema = z10.enum([
3743
+ "plain",
3744
+ "websocket",
3745
+ "webtransport"
3746
+ ]);
3747
+ var webttyServerSchema = z10.object({
3748
+ tunnel_id: z10.string(),
3749
+ tunnel_name: z10.string().optional(),
3750
+ target: z10.string().optional(),
3751
+ rstream_url: z10.string().optional(),
3752
+ status: z10.string().optional(),
3753
+ publish: z10.boolean().optional(),
3754
+ workspace_id: z10.string().optional(),
3755
+ project_id: z10.string().optional(),
3756
+ tunnel_protocol: z10.enum(["http", "webtty"]).optional(),
3757
+ tunnel_type: z10.enum(["bytestream", "datagram"]).optional(),
3758
+ http_version: z10.string().optional(),
3759
+ transport: webttyTransportSchema.optional(),
3760
+ transport_error: z10.string().optional(),
3761
+ managed: z10.boolean(),
3762
+ host: z10.string(),
3763
+ token_auth: z10.boolean(),
3764
+ server_id: z10.string().optional(),
3765
+ server_name: z10.string().optional(),
3766
+ host_key_id: z10.string().optional(),
3767
+ e2e: z10.enum(["disabled", "required"]).optional(),
3768
+ client_proof: z10.enum(["none", "required"]).optional(),
2869
3769
  encryption_policy: webttyEncryptionPolicySchema.optional(),
2870
- capabilities: z9.array(webttyCapabilitySchema).optional(),
2871
- exec_path: z9.string().optional(),
2872
- fs_path: z9.string().optional(),
3770
+ capabilities: z10.array(webttyCapabilitySchema).optional(),
3771
+ exec_path: z10.string().optional(),
3772
+ fs_path: z10.string().optional(),
2873
3773
  fs_mode: webttyFSModeSchema.optional(),
3774
+ fs_backend: z10.enum(["webdav", "webrtc"]).optional(),
2874
3775
  os_family: osFamilySchema.optional(),
2875
- arch: z9.string().optional(),
2876
- os_id: z9.string().optional(),
2877
- os_version_id: z9.string().optional(),
2878
- os_version_codename: z9.string().optional(),
2879
- os_pretty_name: z9.string().optional(),
2880
- kernel_release: z9.string().optional(),
2881
- hostname: z9.string().optional(),
2882
- labels: z9.record(z9.string(), z9.string().optional()).optional()
3776
+ arch: z10.string().optional(),
3777
+ os_id: z10.string().optional(),
3778
+ os_version_id: z10.string().optional(),
3779
+ os_version_codename: z10.string().optional(),
3780
+ os_pretty_name: z10.string().optional(),
3781
+ kernel_release: z10.string().optional(),
3782
+ hostname: z10.string().optional(),
3783
+ labels: z10.record(z10.string(), z10.string().optional()).optional()
2883
3784
  });
2884
3785
  function isWebTTYCapability(value) {
2885
3786
  return webttyCapabilitySchema.safeParse(value).success;
@@ -2897,11 +3798,12 @@ function webTTYCapabilitiesForLabels(labels) {
2897
3798
  function webTTYHasCapability(capabilities, capability) {
2898
3799
  return capabilities.includes(capability);
2899
3800
  }
2900
- function parser(tunnel) {
3801
+ function parser(tunnel, includePrivate) {
2901
3802
  if (tunnel.status !== "online") return null;
2902
- if (tunnel.publish !== true) return null;
3803
+ if (tunnel.publish !== true && !includePrivate) return null;
2903
3804
  const managedProtocol = tunnel.protocol === "webtty";
2904
- if (tunnel.protocol !== "http" && !managedProtocol) return null;
3805
+ if (tunnel.protocol !== void 0 && tunnel.protocol !== "http" && !managedProtocol)
3806
+ return null;
2905
3807
  const tunnelLabels = tunnel.labels ?? {};
2906
3808
  if (!managedProtocol && tunnelLabels["application-protocol"] !== "rstream.webtty") {
2907
3809
  return null;
@@ -2929,8 +3831,11 @@ function parser(tunnel) {
2929
3831
  workspace_id: tunnel.workspace_id,
2930
3832
  project_id: tunnel.project_id,
2931
3833
  tunnel_protocol: tunnel.protocol,
3834
+ tunnel_type: tunnel.type,
3835
+ http_version: tunnel.http_version,
3836
+ ...webTTYTransportMetadata(tunnelLabels["rstream.webtty.transport"]),
2932
3837
  managed: managedProtocol,
2933
- host: formatTunnelHost(tunnel),
3838
+ host: formatTunnelHost(tunnel) ?? "",
2934
3839
  token_auth: tunnel.token_auth === true,
2935
3840
  server_id: serverId || void 0,
2936
3841
  server_name: serverName || void 0,
@@ -2941,6 +3846,7 @@ function parser(tunnel) {
2941
3846
  capabilities,
2942
3847
  exec_path: webTTYHasCapability(capabilities, "exec") ? tunnelLabels["rstream.webtty.exec.path"] ?? "/" : void 0,
2943
3848
  fs_path: webTTYHasCapability(capabilities, "fs") ? tunnelLabels["rstream.webtty.fs.path"] ?? "/fs" : void 0,
3849
+ fs_backend: webTTYHasCapability(capabilities, "fs") ? tunnelLabels["rstream.webtty.fs.backend"] ?? "webdav" : void 0,
2944
3850
  fs_mode: webTTYHasCapability(capabilities, "fs") ? tunnelLabels["rstream.webtty.fs.mode"] ?? "read-write" : void 0,
2945
3851
  os_family: tunnelLabels["rstream.webtty.os_family"],
2946
3852
  arch: tunnelLabels["rstream.webtty.arch"],
@@ -2956,8 +3862,54 @@ function parser(tunnel) {
2956
3862
  if (!parsed.success) return null;
2957
3863
  return parsed.data;
2958
3864
  }
2959
- function parseWebTTYServers(tunnels) {
2960
- return tunnels.map((tunnel) => parser(tunnel)).filter((server) => server !== null);
3865
+ function parseWebTTYServers(tunnels, options = {}) {
3866
+ return tunnels.map((tunnel) => parser(tunnel, options.includePrivate === true)).filter((server) => server !== null);
3867
+ }
3868
+ function webTTYTransportMetadata(value) {
3869
+ if (value === void 0) return {};
3870
+ const parsed = webttyTransportSchema.safeParse(value);
3871
+ if (!parsed.success)
3872
+ return {
3873
+ transport_error: "Server advertises an invalid WebTTY transport."
3874
+ };
3875
+ return { transport: parsed.data };
3876
+ }
3877
+ function resolveWebTTYServerTransport(server, requested) {
3878
+ if (server.transport_error !== void 0)
3879
+ throw new Error(server.transport_error);
3880
+ const advertised = server.transport ?? (server.tunnel_type === "datagram" ? "webtransport" : void 0);
3881
+ if (advertised !== void 0 && server.tunnel_type !== void 0 && advertised === "webtransport" !== (server.tunnel_type === "datagram")) {
3882
+ throw new Error(
3883
+ `Server WebTTY transport ${advertised} conflicts with tunnel type ${server.tunnel_type}.`
3884
+ );
3885
+ }
3886
+ if (advertised === "webtransport" && server.http_version !== void 0 && server.http_version !== "h3") {
3887
+ throw new Error("Server WebTransport requires HTTP/3.");
3888
+ }
3889
+ if (requested !== void 0 && advertised !== void 0 && requested !== advertised) {
3890
+ throw new Error(
3891
+ `Requested WebTTY transport ${requested} conflicts with server transport ${advertised}.`
3892
+ );
3893
+ }
3894
+ return advertised ?? requested ?? "websocket";
3895
+ }
3896
+ function resolveWebTTYBrowserEndpoint(server) {
3897
+ const transport = resolveWebTTYServerTransport(server);
3898
+ if (transport === "plain")
3899
+ throw new Error(
3900
+ "Plain WebTTY requires a native client; this server cannot be opened in a browser."
3901
+ );
3902
+ if (server.publish === false || server.host === "")
3903
+ throw new Error(
3904
+ "Private WebTTY requires a native client and its rstrm:// address."
3905
+ );
3906
+ const url = new URL(
3907
+ `${transport === "webtransport" ? "https" : "wss"}://${server.host}`
3908
+ );
3909
+ const execPath = server.exec_path?.trim();
3910
+ if (execPath)
3911
+ url.pathname = execPath.startsWith("/") ? execPath : `/${execPath}`;
3912
+ return { url: url.toString(), transport };
2961
3913
  }
2962
3914
  // Annotate the CommonJS export names for ESM import in node:
2963
3915
  0 && (module.exports = {
@@ -3015,7 +3967,9 @@ function parseWebTTYServers(tunnels) {
3015
3967
  resolveTunnelsCredentials,
3016
3968
  resolveTunnelsEngine,
3017
3969
  resolveTunnelsRegion,
3970
+ resolveWebTTYBrowserEndpoint,
3018
3971
  resolveWebTTYControlRequestParamsSchema,
3972
+ resolveWebTTYServerTransport,
3019
3973
  signWebhookPayload,
3020
3974
  streamEndpointSchema,
3021
3975
  streamEntrySchema,
@@ -3025,6 +3979,7 @@ function parseWebTTYServers(tunnels) {
3025
3979
  streamWebTTYMetadataSchema,
3026
3980
  tunnelFilterSchema,
3027
3981
  turnCredentialModeSchema,
3982
+ validateTURNCredentialsRequest,
3028
3983
  webTTYCapabilitiesSchema,
3029
3984
  webTTYContextMetadataSchema,
3030
3985
  webTTYControlRequestSchema,
@@ -3045,5 +4000,6 @@ function parseWebTTYServers(tunnels) {
3045
4000
  webTTYTransportSchema,
3046
4001
  webhookEventsSchema,
3047
4002
  webttyServerSchema,
4003
+ webttyTransportSchema,
3048
4004
  wsEventsSchema
3049
4005
  });