@rstreamlabs/tunnels 3.9.0 → 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/README.md +46 -0
- package/dist/{chunk-3VBF43NB.mjs → chunk-DQN5T7CI.mjs} +207 -9
- package/dist/index.d.mts +154 -117
- package/dist/index.d.ts +154 -117
- package/dist/index.js +1045 -156
- package/dist/index.mjs +713 -141
- package/dist/node-engine-http-6DJZZEME.mjs +102 -0
- package/dist/openapi.d.mts +260 -10
- package/dist/openapi.d.ts +260 -10
- package/dist/openapi.js +207 -9
- package/dist/openapi.mjs +1 -1
- package/package.json +2 -2
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, {
|
|
@@ -96,6 +214,7 @@ __export(index_exports, {
|
|
|
96
214
|
streamWebTTYMetadataSchema: () => streamWebTTYMetadataSchema,
|
|
97
215
|
tunnelFilterSchema: () => tunnelFilterSchema,
|
|
98
216
|
turnCredentialModeSchema: () => turnCredentialModeSchema,
|
|
217
|
+
validateTURNCredentialsRequest: () => validateTURNCredentialsRequest,
|
|
99
218
|
webTTYCapabilitiesSchema: () => webTTYCapabilitiesSchema,
|
|
100
219
|
webTTYContextMetadataSchema: () => webTTYContextMetadataSchema,
|
|
101
220
|
webTTYControlRequestSchema: () => webTTYControlRequestSchema,
|
|
@@ -1301,6 +1420,19 @@ var watchParamsSchema = z7.object({
|
|
|
1301
1420
|
tunnels: tunnelFilterSchema.optional()
|
|
1302
1421
|
});
|
|
1303
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
|
+
},
|
|
1304
1436
|
"tunnels.resources.read-only": {
|
|
1305
1437
|
name: "Discovery: Read-Only",
|
|
1306
1438
|
description: "Read active clients, active tunnels, and live state events.",
|
|
@@ -1380,13 +1512,13 @@ var engineOpenApiDocument = {
|
|
|
1380
1512
|
}
|
|
1381
1513
|
}
|
|
1382
1514
|
],
|
|
1383
|
-
security: [
|
|
1384
|
-
{
|
|
1385
|
-
bearerAuth: []
|
|
1386
|
-
}
|
|
1387
|
-
],
|
|
1515
|
+
security: [{ bearerAuth: [] }, { mutualTLS: [] }],
|
|
1388
1516
|
components: {
|
|
1389
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
|
+
},
|
|
1390
1522
|
bearerAuth: {
|
|
1391
1523
|
type: "http",
|
|
1392
1524
|
scheme: "bearer",
|
|
@@ -1454,6 +1586,64 @@ var engineOpenApiDocument = {
|
|
|
1454
1586
|
Forbidden: errorResponse("The token is not authorized.")
|
|
1455
1587
|
},
|
|
1456
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
|
+
},
|
|
1457
1647
|
Client: jsonSchemaFor(clientSchema),
|
|
1458
1648
|
Event: jsonSchemaFor(wsEventsSchema),
|
|
1459
1649
|
ListClientsParams: jsonSchemaFor(listClientsParamsSchema),
|
|
@@ -1488,6 +1678,133 @@ var engineOpenApiDocument = {
|
|
|
1488
1678
|
"read-managed-webtty-recordings": ["webtty.logs.read-only"]
|
|
1489
1679
|
},
|
|
1490
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
|
+
},
|
|
1491
1808
|
"/api/openapi.json": {
|
|
1492
1809
|
get: {
|
|
1493
1810
|
tags: ["Discovery"],
|
|
@@ -1514,7 +1831,7 @@ var engineOpenApiDocument = {
|
|
|
1514
1831
|
summary: "List visible engine clients.",
|
|
1515
1832
|
operationId: "list_engine_clients",
|
|
1516
1833
|
parameters: [{ $ref: "#/components/parameters/ListClientsParams" }],
|
|
1517
|
-
security: [{ bearerAuth: [] }],
|
|
1834
|
+
security: [{ bearerAuth: [] }, { mutualTLS: [] }],
|
|
1518
1835
|
responses: {
|
|
1519
1836
|
"200": jsonArrayResponse(
|
|
1520
1837
|
"Visible clients.",
|
|
@@ -1533,7 +1850,7 @@ var engineOpenApiDocument = {
|
|
|
1533
1850
|
summary: "List visible engine tunnels.",
|
|
1534
1851
|
operationId: "list_engine_tunnels",
|
|
1535
1852
|
parameters: [{ $ref: "#/components/parameters/ListTunnelsParams" }],
|
|
1536
|
-
security: [{ bearerAuth: [] }],
|
|
1853
|
+
security: [{ bearerAuth: [] }, { mutualTLS: [] }],
|
|
1537
1854
|
responses: {
|
|
1538
1855
|
"200": jsonArrayResponse(
|
|
1539
1856
|
"Visible tunnels. Fine-grained list scopes may filter objects and project fields.",
|
|
@@ -1553,7 +1870,7 @@ var engineOpenApiDocument = {
|
|
|
1553
1870
|
summary: "Subscribe to visible engine state through Server-Sent Events.",
|
|
1554
1871
|
operationId: "watch_engine_events_sse",
|
|
1555
1872
|
parameters: [{ $ref: "#/components/parameters/WatchParams" }],
|
|
1556
|
-
security: [{ bearerAuth: [] }],
|
|
1873
|
+
security: [{ bearerAuth: [] }, { mutualTLS: [] }],
|
|
1557
1874
|
responses: {
|
|
1558
1875
|
"200": streamResponse(
|
|
1559
1876
|
"SSE stream. Each event is emitted as a data line containing a JSON Event object."
|
|
@@ -1572,7 +1889,7 @@ var engineOpenApiDocument = {
|
|
|
1572
1889
|
summary: "Subscribe to visible engine state through a WebSocket upgrade.",
|
|
1573
1890
|
operationId: "watch_engine_events_websocket",
|
|
1574
1891
|
parameters: [{ $ref: "#/components/parameters/WatchParams" }],
|
|
1575
|
-
security: [{ bearerAuth: [] }],
|
|
1892
|
+
security: [{ bearerAuth: [] }, { mutualTLS: [] }],
|
|
1576
1893
|
responses: {
|
|
1577
1894
|
"101": {
|
|
1578
1895
|
description: "WebSocket connection accepted. Frames contain JSON Event objects."
|
|
@@ -1596,6 +1913,113 @@ var engineOpenApiJson = `${JSON.stringify(
|
|
|
1596
1913
|
|
|
1597
1914
|
// src/resolution.ts
|
|
1598
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
|
|
1599
2023
|
var import_rstream4 = require("@rstreamlabs/rstream");
|
|
1600
2024
|
var import_rstream5 = require("@rstreamlabs/rstream");
|
|
1601
2025
|
var import_rstream6 = require("@rstreamlabs/rstream");
|
|
@@ -1729,6 +2153,12 @@ async function resolveManagedTunnelsProject(config) {
|
|
|
1729
2153
|
);
|
|
1730
2154
|
}
|
|
1731
2155
|
const controlPlane = new import_rstream7.RstreamClient({
|
|
2156
|
+
fetch: createBoundedControlPlaneFetch(
|
|
2157
|
+
1e4,
|
|
2158
|
+
void 0,
|
|
2159
|
+
globalThis.fetch,
|
|
2160
|
+
1024 * 1024
|
|
2161
|
+
),
|
|
1732
2162
|
apiUrl: resolveTunnelsAPIURL(config.apiUrl),
|
|
1733
2163
|
controlPlaneHeaders: config.controlPlaneHeaders,
|
|
1734
2164
|
credentials
|
|
@@ -1769,62 +2199,231 @@ var RstreamTunnelsResource = class {
|
|
|
1769
2199
|
};
|
|
1770
2200
|
|
|
1771
2201
|
// src/tunnels.ts
|
|
1772
|
-
var import_rstream12 = require("@rstreamlabs/rstream");
|
|
1773
|
-
var import_rstream13 = require("@rstreamlabs/rstream");
|
|
1774
2202
|
var import_rstream14 = require("@rstreamlabs/rstream");
|
|
2203
|
+
var import_rstream15 = require("@rstreamlabs/rstream");
|
|
1775
2204
|
|
|
1776
|
-
// src/
|
|
2205
|
+
// src/engine-http.ts
|
|
1777
2206
|
var import_rstream8 = require("@rstreamlabs/rstream");
|
|
1778
|
-
var
|
|
1779
|
-
var
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
);
|
|
1795
|
-
return
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
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;
|
|
1809
2280
|
try {
|
|
1810
|
-
const
|
|
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
|
+
);
|
|
2336
|
+
try {
|
|
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",
|
|
1811
2345
|
headers,
|
|
2346
|
+
credentials: "omit",
|
|
2347
|
+
redirect: "manual",
|
|
1812
2348
|
signal: lifetime.signal
|
|
1813
2349
|
});
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
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, {
|
|
1817
2403
|
status: response.status,
|
|
1818
|
-
|
|
2404
|
+
headers: response.headers
|
|
1819
2405
|
});
|
|
2406
|
+
} catch (error) {
|
|
2407
|
+
if (!(error instanceof import_rstream8.RstreamHTTPError)) this.discovery = void 0;
|
|
2408
|
+
throw error;
|
|
1820
2409
|
} finally {
|
|
1821
2410
|
lifetime.close();
|
|
1822
2411
|
}
|
|
1823
|
-
}
|
|
1824
|
-
}
|
|
2412
|
+
}
|
|
2413
|
+
};
|
|
2414
|
+
|
|
2415
|
+
// src/tunnels.ts
|
|
2416
|
+
var import_rstream16 = require("@rstreamlabs/rstream");
|
|
2417
|
+
var import_rstream17 = require("@rstreamlabs/rstream");
|
|
1825
2418
|
|
|
1826
2419
|
// src/turn.ts
|
|
1827
|
-
var
|
|
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"));
|
|
1828
2427
|
var import_crypto = __toESM(require("crypto"));
|
|
1829
2428
|
var defaultTURNPort = 3478;
|
|
1830
2429
|
var defaultTURNSPort = 5349;
|
|
@@ -1833,20 +2432,31 @@ var maxTURNCredentialTTLSeconds = 60 * 60;
|
|
|
1833
2432
|
var maxTURNKeyringBytes = 8 * 1024;
|
|
1834
2433
|
var defaultAPIRequestTimeoutMilliseconds = 1e4;
|
|
1835
2434
|
var maxAPIRequestTimeoutMilliseconds = 6e4;
|
|
1836
|
-
var turnTokenClaimsSchema =
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
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"])
|
|
1842
2442
|
});
|
|
1843
|
-
var turnCredentialModeSchema =
|
|
2443
|
+
var turnCredentialModeSchema = z9.enum([
|
|
2444
|
+
"auto",
|
|
2445
|
+
"api",
|
|
2446
|
+
"engine",
|
|
2447
|
+
"app",
|
|
2448
|
+
"pat"
|
|
2449
|
+
]);
|
|
1844
2450
|
function normalizeOptionalString2(value) {
|
|
1845
2451
|
const normalized = value?.trim();
|
|
1846
2452
|
return normalized ? normalized : void 0;
|
|
1847
2453
|
}
|
|
1848
2454
|
function normalizeClusterDomain(clusterDomain) {
|
|
1849
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.");
|
|
1850
2460
|
return normalized?.toLowerCase();
|
|
1851
2461
|
}
|
|
1852
2462
|
function normalizeNow(now) {
|
|
@@ -1961,14 +2571,27 @@ function requireOption(value, message) {
|
|
|
1961
2571
|
}
|
|
1962
2572
|
throw new Error(message);
|
|
1963
2573
|
}
|
|
1964
|
-
function
|
|
1965
|
-
|
|
1966
|
-
return
|
|
1967
|
-
}
|
|
1968
|
-
|
|
1969
|
-
return "app";
|
|
2574
|
+
function automaticPATClaims(token) {
|
|
2575
|
+
try {
|
|
2576
|
+
return parseTURNTokenClaims(token);
|
|
2577
|
+
} catch {
|
|
2578
|
+
return void 0;
|
|
1970
2579
|
}
|
|
1971
|
-
|
|
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";
|
|
1972
2595
|
}
|
|
1973
2596
|
async function readBoundedTURNKeyring(reader, chunks = [], size = 0) {
|
|
1974
2597
|
const result = await reader.read();
|
|
@@ -1988,7 +2611,7 @@ async function readBoundedTURNKeyring(reader, chunks = [], size = 0) {
|
|
|
1988
2611
|
}
|
|
1989
2612
|
function parseTURNServerPublicKeyHex(value) {
|
|
1990
2613
|
const normalized = value.trim();
|
|
1991
|
-
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)) {
|
|
1992
2615
|
throw new Error("TURN keyring must contain hexadecimal DER key material.");
|
|
1993
2616
|
}
|
|
1994
2617
|
try {
|
|
@@ -2011,6 +2634,10 @@ function parseTURNServerPublicKeyHex(value) {
|
|
|
2011
2634
|
}
|
|
2012
2635
|
}
|
|
2013
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
|
+
);
|
|
2014
2641
|
try {
|
|
2015
2642
|
const key = import_crypto.default.createPrivateKey({
|
|
2016
2643
|
key: Buffer.from(value, "hex"),
|
|
@@ -2046,19 +2673,38 @@ async function loadTURNServerPublicKeyHex(clusterDomain, options) {
|
|
|
2046
2673
|
`/keyrings/turn/${clusterDomain}.spki.der.hex`,
|
|
2047
2674
|
keyringBaseURL
|
|
2048
2675
|
);
|
|
2049
|
-
|
|
2050
|
-
if (response.status >= 300 && response.status < 400) {
|
|
2676
|
+
if (url.protocol !== "https:" || url.username || url.password)
|
|
2051
2677
|
throw new Error(
|
|
2052
|
-
"TURN keyring
|
|
2678
|
+
"TURN keyring requests require HTTPS without URL credentials."
|
|
2053
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();
|
|
2054
2707
|
}
|
|
2055
|
-
if (!response.ok) {
|
|
2056
|
-
throw new Error(`TURN keyring request returned HTTP ${response.status}.`);
|
|
2057
|
-
}
|
|
2058
|
-
if (!response.body) {
|
|
2059
|
-
throw new Error("TURN keyring response body is missing.");
|
|
2060
|
-
}
|
|
2061
|
-
return (await readBoundedTURNKeyring(response.body.getReader())).trim();
|
|
2062
2708
|
}
|
|
2063
2709
|
async function createAPITURNCredentials(options) {
|
|
2064
2710
|
const projectId = normalizeOptionalString2(options.projectId);
|
|
@@ -2072,11 +2718,21 @@ async function createAPITURNCredentials(options) {
|
|
|
2072
2718
|
options.requestTimeoutMilliseconds
|
|
2073
2719
|
);
|
|
2074
2720
|
const params = createAPITURNCredentialsParams(options.ttlSeconds);
|
|
2075
|
-
const
|
|
2076
|
-
|
|
2721
|
+
const request2 = createBoundedControlPlaneFetch(
|
|
2722
|
+
requestTimeoutMilliseconds,
|
|
2723
|
+
options.signal,
|
|
2724
|
+
options.fetch
|
|
2725
|
+
);
|
|
2726
|
+
const client = new import_rstream12.RstreamClient({
|
|
2727
|
+
controlPlaneHeaders: options.controlPlaneHeaders,
|
|
2077
2728
|
apiUrl: options.apiUrl,
|
|
2078
|
-
credentials: options.credentials
|
|
2079
|
-
|
|
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
|
|
2080
2736
|
});
|
|
2081
2737
|
if (projectId) {
|
|
2082
2738
|
return await client.tunnels.projects.createTurnCredentials(
|
|
@@ -2119,6 +2775,8 @@ function createPATTURNCredentials(options) {
|
|
|
2119
2775
|
const claimsTokenEndpoint = normalizeOptionalString2(
|
|
2120
2776
|
claims.token_endpoint ?? claims.tokendpoint
|
|
2121
2777
|
);
|
|
2778
|
+
if (claims.token_endpoint && claims.tokendpoint && claims.token_endpoint !== claims.tokendpoint)
|
|
2779
|
+
throw new Error("Conflicting PAT token endpoints.");
|
|
2122
2780
|
const configuredTokenEndpoint = normalizeOptionalString2(
|
|
2123
2781
|
options.tokenEndpoint
|
|
2124
2782
|
);
|
|
@@ -2128,6 +2786,8 @@ function createPATTURNCredentials(options) {
|
|
|
2128
2786
|
"TURN PAT mode requires a PAT token carrying a token endpoint."
|
|
2129
2787
|
);
|
|
2130
2788
|
}
|
|
2789
|
+
requireTURNReference(projectEndpoint);
|
|
2790
|
+
requireTURNReference(tokenEndpoint);
|
|
2131
2791
|
if (configuredTokenEndpoint !== void 0 && claimsTokenEndpoint !== void 0 && claimsTokenEndpoint !== configuredTokenEndpoint) {
|
|
2132
2792
|
throw new Error("Token endpoint does not match the PAT token.");
|
|
2133
2793
|
}
|
|
@@ -2172,6 +2832,8 @@ async function createAPPTURNCredentials(options) {
|
|
|
2172
2832
|
if (!clientId) {
|
|
2173
2833
|
throw new Error("Client ID is required for TURN APP mode.");
|
|
2174
2834
|
}
|
|
2835
|
+
requireTURNReference(projectEndpoint);
|
|
2836
|
+
requireTURNReference(clientId);
|
|
2175
2837
|
const clientSecret = normalizeOptionalString2(options.clientSecret);
|
|
2176
2838
|
if (!clientSecret) {
|
|
2177
2839
|
throw new Error("Client secret is required for TURN APP mode.");
|
|
@@ -2204,32 +2866,47 @@ async function createAPPTURNCredentials(options) {
|
|
|
2204
2866
|
username
|
|
2205
2867
|
});
|
|
2206
2868
|
}
|
|
2207
|
-
async function
|
|
2869
|
+
async function createTURNCredentialsForLifetime(options) {
|
|
2208
2870
|
const credentials = options.credentials;
|
|
2209
|
-
const mode = resolveMode(
|
|
2871
|
+
const mode = resolveMode(options);
|
|
2210
2872
|
const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
|
|
2211
|
-
if (mode === "
|
|
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") {
|
|
2212
2879
|
const controlPlaneCredentials = options.controlPlaneCredentials ?? credentials;
|
|
2213
2880
|
if (!controlPlaneCredentials) {
|
|
2881
|
+
if (mode === "auto" && (options.client || options.engine))
|
|
2882
|
+
return createEngineTURNCredentials(options);
|
|
2214
2883
|
throw new Error(
|
|
2215
2884
|
"Control-plane credentials are required for TURN API mode."
|
|
2216
2885
|
);
|
|
2217
2886
|
}
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
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
|
+
}
|
|
2227
2904
|
}
|
|
2228
2905
|
const legacyDomain = normalizeClusterDomain(options.clusterDomain);
|
|
2229
2906
|
const turnDomain = normalizeClusterDomain(options.turnDomain) ?? legacyDomain ?? (projectEndpoint && options.engine ? deriveClusterDomainFromEngine(projectEndpoint, options.engine) : void 0);
|
|
2230
2907
|
const turnRealm = normalizeClusterDomain(options.turnRealm) ?? legacyDomain;
|
|
2231
2908
|
if (mode === "pat") {
|
|
2232
|
-
if (!(0,
|
|
2909
|
+
if (!(0, import_rstream11.isTokenCredentials)(credentials)) {
|
|
2233
2910
|
throw new Error("A PAT token is required for TURN PAT mode.");
|
|
2234
2911
|
}
|
|
2235
2912
|
return createPATTURNCredentials({
|
|
@@ -2253,10 +2930,12 @@ async function createTURNCredentials(options) {
|
|
|
2253
2930
|
turnsPort: options.turnsPort
|
|
2254
2931
|
});
|
|
2255
2932
|
}
|
|
2256
|
-
if (!(0,
|
|
2933
|
+
if (!(0, import_rstream9.isClientCredentials)(credentials)) {
|
|
2257
2934
|
throw new Error("Client credentials are required for TURN APP mode.");
|
|
2258
2935
|
}
|
|
2259
2936
|
return await createAPPTURNCredentials({
|
|
2937
|
+
signal: options.signal,
|
|
2938
|
+
requestTimeoutMilliseconds: options.requestTimeoutMilliseconds,
|
|
2260
2939
|
apiUrl: options.apiUrl,
|
|
2261
2940
|
clientId: credentials.clientId,
|
|
2262
2941
|
clientSecret: credentials.clientSecret,
|
|
@@ -2280,25 +2959,139 @@ async function createTURNCredentials(options) {
|
|
|
2280
2959
|
turnsPort: options.turnsPort
|
|
2281
2960
|
});
|
|
2282
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
|
+
}
|
|
2283
3058
|
|
|
2284
3059
|
// src/turn-resource.ts
|
|
2285
|
-
var import_rstream11 = require("@rstreamlabs/rstream");
|
|
2286
3060
|
var RstreamTURNResource = class {
|
|
2287
3061
|
client;
|
|
2288
3062
|
constructor(client) {
|
|
2289
3063
|
this.client = client;
|
|
2290
3064
|
}
|
|
2291
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) {
|
|
2292
3081
|
const credentials = options.credentials ?? this.client.credentials;
|
|
2293
|
-
const localMode = options.mode === "app" || options.mode === "pat"
|
|
2294
|
-
const target = localMode && (options.turnDomain === void 0 || options.turnRealm === void 0) && options.clusterDomain === void 0 ? await this.client.getTURNTarget() : void 0;
|
|
2295
|
-
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;
|
|
2296
3084
|
return await createTURNCredentials({
|
|
3085
|
+
client: this.client,
|
|
3086
|
+
signal: options.signal,
|
|
2297
3087
|
apiUrl: options.apiUrl ?? this.client.apiUrl,
|
|
3088
|
+
fetch: options.fetch,
|
|
3089
|
+
requestTimeoutMilliseconds: options.requestTimeoutMilliseconds,
|
|
2298
3090
|
clusterDomain: options.clusterDomain,
|
|
3091
|
+
controlPlaneHeaders: options.controlPlaneHeaders ?? this.client.controlPlaneHeaders,
|
|
2299
3092
|
controlPlaneCredentials: options.controlPlaneCredentials ?? this.client.controlPlaneCredentials,
|
|
2300
3093
|
credentials,
|
|
2301
|
-
engine,
|
|
3094
|
+
engine: options.engine,
|
|
2302
3095
|
keyringBaseUrl: options.keyringBaseUrl,
|
|
2303
3096
|
mode: options.mode,
|
|
2304
3097
|
now: options.now,
|
|
@@ -2467,31 +3260,52 @@ var engineAuthTokenScopes = {
|
|
|
2467
3260
|
var RstreamTunnelsClient = class {
|
|
2468
3261
|
config;
|
|
2469
3262
|
managedProject;
|
|
3263
|
+
http;
|
|
2470
3264
|
constructor(config) {
|
|
2471
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 ?? {});
|
|
2472
3269
|
}
|
|
2473
3270
|
get apiUrl() {
|
|
2474
3271
|
return resolveTunnelsAPIURL(this.config?.apiUrl);
|
|
2475
3272
|
}
|
|
2476
3273
|
get credentials() {
|
|
3274
|
+
if (this.http.usesMTLS) return void 0;
|
|
2477
3275
|
return resolveTunnelsCredentials(this.config?.credentials);
|
|
2478
3276
|
}
|
|
2479
3277
|
get controlPlaneCredentials() {
|
|
3278
|
+
if (this.http.usesMTLS) return void 0;
|
|
2480
3279
|
return resolveControlPlaneCredentials(
|
|
2481
3280
|
this.config?.controlPlaneCredentials,
|
|
2482
3281
|
this.credentials
|
|
2483
3282
|
);
|
|
2484
3283
|
}
|
|
3284
|
+
get controlPlaneHeaders() {
|
|
3285
|
+
return this.config?.controlPlaneHeaders;
|
|
3286
|
+
}
|
|
2485
3287
|
get projectEndpoint() {
|
|
2486
3288
|
return this.config?.projectEndpoint;
|
|
2487
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
|
+
}
|
|
2488
3302
|
async getEngine() {
|
|
2489
3303
|
const region = resolveTunnelsRegion(this.config?.region);
|
|
2490
3304
|
const projectEndpoint = normalizeOptionalString3(
|
|
2491
3305
|
this.config?.projectEndpoint
|
|
2492
3306
|
);
|
|
2493
3307
|
if (region !== void 0) {
|
|
2494
|
-
if (normalizeOptionalString3(this.config?.engine) !== void 0 || (0,
|
|
3308
|
+
if (normalizeOptionalString3(this.config?.engine) !== void 0 || (0, import_rstream17.readEnvironment)().engine !== void 0) {
|
|
2495
3309
|
throw new Error(
|
|
2496
3310
|
"Region selection cannot be combined with an explicit engine override."
|
|
2497
3311
|
);
|
|
@@ -2503,17 +3317,17 @@ var RstreamTunnelsClient = class {
|
|
|
2503
3317
|
}
|
|
2504
3318
|
const project = await this.getManagedProject();
|
|
2505
3319
|
if (project !== void 0) {
|
|
2506
|
-
const engine = (0,
|
|
3320
|
+
const engine = (0, import_rstream14.getTunnelsProjectEngine)(project, region);
|
|
2507
3321
|
if (engine) return normalizeEngineAddress(engine);
|
|
2508
3322
|
}
|
|
2509
3323
|
throw new Error(
|
|
2510
3324
|
"Failed to resolve the selected regional engine from the managed tunnels project."
|
|
2511
3325
|
);
|
|
2512
3326
|
}
|
|
2513
|
-
if (projectEndpoint !== void 0 && normalizeOptionalString3(this.config?.engine) === void 0 && (0,
|
|
3327
|
+
if (projectEndpoint !== void 0 && normalizeOptionalString3(this.config?.engine) === void 0 && (0, import_rstream17.readEnvironment)().engine === void 0) {
|
|
2514
3328
|
const project = await this.getManagedProject();
|
|
2515
3329
|
if (project !== void 0) {
|
|
2516
|
-
const engine = (0,
|
|
3330
|
+
const engine = (0, import_rstream14.getTunnelsProjectEngine)(project);
|
|
2517
3331
|
if (engine) {
|
|
2518
3332
|
return normalizeEngineAddress(engine);
|
|
2519
3333
|
}
|
|
@@ -2554,6 +3368,10 @@ var RstreamTunnelsClient = class {
|
|
|
2554
3368
|
};
|
|
2555
3369
|
}
|
|
2556
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
|
+
);
|
|
2557
3375
|
const projectEndpoint = normalizeOptionalString3(
|
|
2558
3376
|
this.config?.projectEndpoint
|
|
2559
3377
|
);
|
|
@@ -2629,7 +3447,7 @@ var RstreamTunnelsClient = class {
|
|
|
2629
3447
|
if (!credentials) {
|
|
2630
3448
|
return void 0;
|
|
2631
3449
|
}
|
|
2632
|
-
if (!(0,
|
|
3450
|
+
if (!(0, import_rstream16.isClientCredentials)(credentials)) {
|
|
2633
3451
|
return credentials.token;
|
|
2634
3452
|
}
|
|
2635
3453
|
const tokenParams = await this.getEngineAuthTokenParams();
|
|
@@ -2638,26 +3456,37 @@ var RstreamTunnelsClient = class {
|
|
|
2638
3456
|
engine
|
|
2639
3457
|
})).token;
|
|
2640
3458
|
}
|
|
2641
|
-
async
|
|
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) {
|
|
2642
3468
|
const engine = normalizeEngineAddress(await this.getEngine());
|
|
2643
3469
|
if (!path.startsWith("/") || path.startsWith("//")) {
|
|
2644
3470
|
throw new Error("Engine request path must be a relative absolute path.");
|
|
2645
3471
|
}
|
|
2646
|
-
const url = `https://${engine}/api${path}`;
|
|
2647
3472
|
const headers = new Headers(options?.headers || {});
|
|
2648
|
-
const
|
|
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);
|
|
2649
3478
|
if (token) {
|
|
2650
3479
|
headers.set("Authorization", `Bearer ${token}`);
|
|
2651
3480
|
}
|
|
2652
|
-
const response = await
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
3481
|
+
const response = await this.http.response(
|
|
3482
|
+
engine,
|
|
3483
|
+
path,
|
|
3484
|
+
{
|
|
3485
|
+
...options,
|
|
3486
|
+
headers
|
|
3487
|
+
},
|
|
3488
|
+
maxResponseBytes
|
|
3489
|
+
);
|
|
2661
3490
|
return await response.json();
|
|
2662
3491
|
}
|
|
2663
3492
|
};
|
|
@@ -2754,6 +3583,8 @@ function hasWatchOnlyPermissions(permissions) {
|
|
|
2754
3583
|
);
|
|
2755
3584
|
}
|
|
2756
3585
|
var Watch = class {
|
|
3586
|
+
abort = new AbortController();
|
|
3587
|
+
closeStream;
|
|
2757
3588
|
connection = null;
|
|
2758
3589
|
connectionState = "preparing";
|
|
2759
3590
|
config;
|
|
@@ -2768,7 +3599,32 @@ var Watch = class {
|
|
|
2768
3599
|
}
|
|
2769
3600
|
this.connectionState = "connecting";
|
|
2770
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
|
+
}
|
|
2771
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.");
|
|
2772
3628
|
validateWatchQueryToken(token);
|
|
2773
3629
|
const engine = await resolveTunnelsEngine({
|
|
2774
3630
|
apiUrl: this.config.apiUrl,
|
|
@@ -2807,10 +3663,39 @@ var Watch = class {
|
|
|
2807
3663
|
};
|
|
2808
3664
|
}
|
|
2809
3665
|
} catch (error) {
|
|
2810
|
-
this.
|
|
3666
|
+
this.closeStream?.();
|
|
3667
|
+
this.closeStream = void 0;
|
|
3668
|
+
this.connectionState = this.abort.signal.aborted ? "closed" : "preparing";
|
|
2811
3669
|
throw error;
|
|
2812
3670
|
}
|
|
2813
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
|
+
}
|
|
2814
3699
|
handleMessage(data) {
|
|
2815
3700
|
try {
|
|
2816
3701
|
const parsed = wsEventsSchema.parse(JSON.parse(data));
|
|
@@ -2820,6 +3705,9 @@ var Watch = class {
|
|
|
2820
3705
|
}
|
|
2821
3706
|
}
|
|
2822
3707
|
disconnect() {
|
|
3708
|
+
this.abort.abort();
|
|
3709
|
+
this.closeStream?.();
|
|
3710
|
+
this.closeStream = void 0;
|
|
2823
3711
|
if (this.connection) {
|
|
2824
3712
|
this.connection.onerror = null;
|
|
2825
3713
|
this.connection.onmessage = null;
|
|
@@ -2835,8 +3723,8 @@ var Watch = class {
|
|
|
2835
3723
|
};
|
|
2836
3724
|
|
|
2837
3725
|
// src/webtty.ts
|
|
2838
|
-
var
|
|
2839
|
-
var osFamilySchema =
|
|
3726
|
+
var z10 = __toESM(require("zod"));
|
|
3727
|
+
var osFamilySchema = z10.enum([
|
|
2840
3728
|
"linux",
|
|
2841
3729
|
"macos",
|
|
2842
3730
|
"windows",
|
|
@@ -2844,55 +3732,55 @@ var osFamilySchema = z9.enum([
|
|
|
2844
3732
|
"openbsd",
|
|
2845
3733
|
"freebsd"
|
|
2846
3734
|
]);
|
|
2847
|
-
var webttyCapabilitySchema =
|
|
2848
|
-
var webttyFSModeSchema =
|
|
2849
|
-
var webttyEncryptionPolicySchema =
|
|
3735
|
+
var webttyCapabilitySchema = z10.enum(["exec", "fs"]);
|
|
3736
|
+
var webttyFSModeSchema = z10.enum(["read-write", "read-only"]);
|
|
3737
|
+
var webttyEncryptionPolicySchema = z10.enum([
|
|
2850
3738
|
"disabled",
|
|
2851
3739
|
"explicit_key",
|
|
2852
3740
|
"workspace_managed"
|
|
2853
3741
|
]);
|
|
2854
|
-
var webttyTransportSchema =
|
|
3742
|
+
var webttyTransportSchema = z10.enum([
|
|
2855
3743
|
"plain",
|
|
2856
3744
|
"websocket",
|
|
2857
3745
|
"webtransport"
|
|
2858
3746
|
]);
|
|
2859
|
-
var webttyServerSchema =
|
|
2860
|
-
tunnel_id:
|
|
2861
|
-
tunnel_name:
|
|
2862
|
-
target:
|
|
2863
|
-
rstream_url:
|
|
2864
|
-
status:
|
|
2865
|
-
publish:
|
|
2866
|
-
workspace_id:
|
|
2867
|
-
project_id:
|
|
2868
|
-
tunnel_protocol:
|
|
2869
|
-
tunnel_type:
|
|
2870
|
-
http_version:
|
|
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(),
|
|
2871
3759
|
transport: webttyTransportSchema.optional(),
|
|
2872
|
-
transport_error:
|
|
2873
|
-
managed:
|
|
2874
|
-
host:
|
|
2875
|
-
token_auth:
|
|
2876
|
-
server_id:
|
|
2877
|
-
server_name:
|
|
2878
|
-
host_key_id:
|
|
2879
|
-
e2e:
|
|
2880
|
-
client_proof:
|
|
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(),
|
|
2881
3769
|
encryption_policy: webttyEncryptionPolicySchema.optional(),
|
|
2882
|
-
capabilities:
|
|
2883
|
-
exec_path:
|
|
2884
|
-
fs_path:
|
|
3770
|
+
capabilities: z10.array(webttyCapabilitySchema).optional(),
|
|
3771
|
+
exec_path: z10.string().optional(),
|
|
3772
|
+
fs_path: z10.string().optional(),
|
|
2885
3773
|
fs_mode: webttyFSModeSchema.optional(),
|
|
2886
|
-
fs_backend:
|
|
3774
|
+
fs_backend: z10.enum(["webdav", "webrtc"]).optional(),
|
|
2887
3775
|
os_family: osFamilySchema.optional(),
|
|
2888
|
-
arch:
|
|
2889
|
-
os_id:
|
|
2890
|
-
os_version_id:
|
|
2891
|
-
os_version_codename:
|
|
2892
|
-
os_pretty_name:
|
|
2893
|
-
kernel_release:
|
|
2894
|
-
hostname:
|
|
2895
|
-
labels:
|
|
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()
|
|
2896
3784
|
});
|
|
2897
3785
|
function isWebTTYCapability(value) {
|
|
2898
3786
|
return webttyCapabilitySchema.safeParse(value).success;
|
|
@@ -3091,6 +3979,7 @@ function resolveWebTTYBrowserEndpoint(server) {
|
|
|
3091
3979
|
streamWebTTYMetadataSchema,
|
|
3092
3980
|
tunnelFilterSchema,
|
|
3093
3981
|
turnCredentialModeSchema,
|
|
3982
|
+
validateTURNCredentialsRequest,
|
|
3094
3983
|
webTTYCapabilitiesSchema,
|
|
3095
3984
|
webTTYContextMetadataSchema,
|
|
3096
3985
|
webTTYControlRequestSchema,
|