@rstreamlabs/tunnels 3.8.0 → 3.8.2
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 +7 -2
- package/dist/index.d.mts +15 -3
- package/dist/index.d.ts +15 -3
- package/dist/index.js +178 -56
- package/dist/index.mjs +174 -52
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -161,14 +161,19 @@ const turn = await createTURNCredentials({
|
|
|
161
161
|
clientSecret: process.env.RSTREAM_CLIENT_SECRET!,
|
|
162
162
|
},
|
|
163
163
|
projectEndpoint: "project-endpoint",
|
|
164
|
-
|
|
164
|
+
turnDomain: "regional.example.rstream.test",
|
|
165
|
+
turnRealm: "global.example.rstream.test",
|
|
165
166
|
ttlSeconds: 600,
|
|
166
167
|
});
|
|
167
168
|
```
|
|
168
169
|
|
|
169
170
|
TURN credentials are short-lived. `ttlSeconds` is optional, defaults to 600 for
|
|
170
171
|
local derivation, and must be an integer between 1 and 3600 seconds. PAT-backed
|
|
171
|
-
credentials are additionally capped by the PAT expiration.
|
|
172
|
+
credentials are additionally capped by the PAT expiration. `turnDomain` is the
|
|
173
|
+
relay hostname used in ICE URLs; `turnRealm` is the authentication scope used
|
|
174
|
+
for key derivation. A managed client resolves both values from project
|
|
175
|
+
metadata. Lower-level local derivation keeps them explicit because a regional
|
|
176
|
+
relay can belong to a different global realm.
|
|
172
177
|
|
|
173
178
|
## Webhooks
|
|
174
179
|
|
package/dist/index.d.mts
CHANGED
|
@@ -239,26 +239,30 @@ interface CreateAPITURNCredentialsOptions {
|
|
|
239
239
|
ttlSeconds?: number;
|
|
240
240
|
}
|
|
241
241
|
interface CreatePATTURNCredentialsOptions {
|
|
242
|
-
clusterDomain
|
|
242
|
+
clusterDomain?: string;
|
|
243
243
|
now?: Date | number;
|
|
244
244
|
projectEndpoint: string;
|
|
245
245
|
token: string;
|
|
246
|
-
tokenEndpoint
|
|
246
|
+
tokenEndpoint?: string;
|
|
247
247
|
ttlSeconds?: number;
|
|
248
|
+
turnDomain?: string;
|
|
248
249
|
turnPort?: number;
|
|
250
|
+
turnRealm?: string;
|
|
249
251
|
turnsPort?: number;
|
|
250
252
|
}
|
|
251
253
|
interface CreateAPPTURNCredentialsOptions {
|
|
252
254
|
apiUrl?: string;
|
|
253
255
|
clientId: string;
|
|
254
256
|
clientSecret: string;
|
|
255
|
-
clusterDomain
|
|
257
|
+
clusterDomain?: string;
|
|
256
258
|
keyringBaseUrl?: string;
|
|
257
259
|
now?: Date | number;
|
|
258
260
|
projectEndpoint: string;
|
|
259
261
|
serverPublicKeyHex?: string;
|
|
260
262
|
ttlSeconds?: number;
|
|
263
|
+
turnDomain?: string;
|
|
261
264
|
turnPort?: number;
|
|
265
|
+
turnRealm?: string;
|
|
262
266
|
turnsPort?: number;
|
|
263
267
|
}
|
|
264
268
|
interface CreateTURNCredentialsOptions {
|
|
@@ -275,7 +279,9 @@ interface CreateTURNCredentialsOptions {
|
|
|
275
279
|
serverPublicKeyHex?: string;
|
|
276
280
|
tokenEndpoint?: string;
|
|
277
281
|
ttlSeconds?: number;
|
|
282
|
+
turnDomain?: string;
|
|
278
283
|
turnPort?: number;
|
|
284
|
+
turnRealm?: string;
|
|
279
285
|
turnsPort?: number;
|
|
280
286
|
}
|
|
281
287
|
declare function createAPITURNCredentials(options: CreateAPITURNCredentialsOptions): Promise<TURNCredentials>;
|
|
@@ -5782,6 +5788,12 @@ declare class RstreamTunnelsClient {
|
|
|
5782
5788
|
get controlPlaneCredentials(): RstreamCredentials | undefined;
|
|
5783
5789
|
get projectEndpoint(): string | undefined;
|
|
5784
5790
|
getEngine(): Promise<string>;
|
|
5791
|
+
getTURNTarget(): Promise<{
|
|
5792
|
+
turnDomain: string;
|
|
5793
|
+
turnPort: number;
|
|
5794
|
+
turnRealm: string;
|
|
5795
|
+
turnsPort: number;
|
|
5796
|
+
}>;
|
|
5785
5797
|
private getManagedProject;
|
|
5786
5798
|
private getEngineAuthTokenParams;
|
|
5787
5799
|
getEngineAuthTokenTarget(): Promise<{
|
package/dist/index.d.ts
CHANGED
|
@@ -239,26 +239,30 @@ interface CreateAPITURNCredentialsOptions {
|
|
|
239
239
|
ttlSeconds?: number;
|
|
240
240
|
}
|
|
241
241
|
interface CreatePATTURNCredentialsOptions {
|
|
242
|
-
clusterDomain
|
|
242
|
+
clusterDomain?: string;
|
|
243
243
|
now?: Date | number;
|
|
244
244
|
projectEndpoint: string;
|
|
245
245
|
token: string;
|
|
246
|
-
tokenEndpoint
|
|
246
|
+
tokenEndpoint?: string;
|
|
247
247
|
ttlSeconds?: number;
|
|
248
|
+
turnDomain?: string;
|
|
248
249
|
turnPort?: number;
|
|
250
|
+
turnRealm?: string;
|
|
249
251
|
turnsPort?: number;
|
|
250
252
|
}
|
|
251
253
|
interface CreateAPPTURNCredentialsOptions {
|
|
252
254
|
apiUrl?: string;
|
|
253
255
|
clientId: string;
|
|
254
256
|
clientSecret: string;
|
|
255
|
-
clusterDomain
|
|
257
|
+
clusterDomain?: string;
|
|
256
258
|
keyringBaseUrl?: string;
|
|
257
259
|
now?: Date | number;
|
|
258
260
|
projectEndpoint: string;
|
|
259
261
|
serverPublicKeyHex?: string;
|
|
260
262
|
ttlSeconds?: number;
|
|
263
|
+
turnDomain?: string;
|
|
261
264
|
turnPort?: number;
|
|
265
|
+
turnRealm?: string;
|
|
262
266
|
turnsPort?: number;
|
|
263
267
|
}
|
|
264
268
|
interface CreateTURNCredentialsOptions {
|
|
@@ -275,7 +279,9 @@ interface CreateTURNCredentialsOptions {
|
|
|
275
279
|
serverPublicKeyHex?: string;
|
|
276
280
|
tokenEndpoint?: string;
|
|
277
281
|
ttlSeconds?: number;
|
|
282
|
+
turnDomain?: string;
|
|
278
283
|
turnPort?: number;
|
|
284
|
+
turnRealm?: string;
|
|
279
285
|
turnsPort?: number;
|
|
280
286
|
}
|
|
281
287
|
declare function createAPITURNCredentials(options: CreateAPITURNCredentialsOptions): Promise<TURNCredentials>;
|
|
@@ -5782,6 +5788,12 @@ declare class RstreamTunnelsClient {
|
|
|
5782
5788
|
get controlPlaneCredentials(): RstreamCredentials | undefined;
|
|
5783
5789
|
get projectEndpoint(): string | undefined;
|
|
5784
5790
|
getEngine(): Promise<string>;
|
|
5791
|
+
getTURNTarget(): Promise<{
|
|
5792
|
+
turnDomain: string;
|
|
5793
|
+
turnPort: number;
|
|
5794
|
+
turnRealm: string;
|
|
5795
|
+
turnsPort: number;
|
|
5796
|
+
}>;
|
|
5785
5797
|
private getManagedProject;
|
|
5786
5798
|
private getEngineAuthTokenParams;
|
|
5787
5799
|
getEngineAuthTokenTarget(): Promise<{
|
package/dist/index.js
CHANGED
|
@@ -1766,9 +1766,9 @@ var RstreamTunnelsResource = class {
|
|
|
1766
1766
|
};
|
|
1767
1767
|
|
|
1768
1768
|
// src/tunnels.ts
|
|
1769
|
-
var import_rstream11 = require("@rstreamlabs/rstream");
|
|
1770
1769
|
var import_rstream12 = require("@rstreamlabs/rstream");
|
|
1771
1770
|
var import_rstream13 = require("@rstreamlabs/rstream");
|
|
1771
|
+
var import_rstream14 = require("@rstreamlabs/rstream");
|
|
1772
1772
|
|
|
1773
1773
|
// src/turn.ts
|
|
1774
1774
|
var import_rstream8 = require("@rstreamlabs/rstream");
|
|
@@ -1781,6 +1781,7 @@ var defaultTURNPort = 3478;
|
|
|
1781
1781
|
var defaultTURNSPort = 5349;
|
|
1782
1782
|
var defaultTURNCredentialTTLSeconds = 10 * 60;
|
|
1783
1783
|
var maxTURNCredentialTTLSeconds = 60 * 60;
|
|
1784
|
+
var maxTURNKeyringBytes = 8 * 1024;
|
|
1784
1785
|
var turnTokenClaimsSchema = z8.object({
|
|
1785
1786
|
exp: z8.number().int().optional(),
|
|
1786
1787
|
iat: z8.number().int().optional(),
|
|
@@ -1830,14 +1831,14 @@ function normalizeTURNPort(port) {
|
|
|
1830
1831
|
function normalizeTURNSPort(port) {
|
|
1831
1832
|
return normalizePort(port, defaultTURNSPort, "TURNS port");
|
|
1832
1833
|
}
|
|
1833
|
-
function createTURNURLs(
|
|
1834
|
+
function createTURNURLs(turnDomain, turnPort, turnsPort) {
|
|
1834
1835
|
const normalizedTURNPort = normalizeTURNPort(turnPort);
|
|
1835
1836
|
const normalizedTURNSPort = normalizeTURNSPort(turnsPort);
|
|
1836
1837
|
return [
|
|
1837
|
-
`turn:${
|
|
1838
|
-
`turn:${
|
|
1839
|
-
`turns:${
|
|
1840
|
-
`turns:${
|
|
1838
|
+
`turn:${turnDomain}:${normalizedTURNPort}?transport=udp`,
|
|
1839
|
+
`turn:${turnDomain}:${normalizedTURNPort}?transport=tcp`,
|
|
1840
|
+
`turns:${turnDomain}:${normalizedTURNSPort}?transport=udp`,
|
|
1841
|
+
`turns:${turnDomain}:${normalizedTURNSPort}?transport=tcp`
|
|
1841
1842
|
];
|
|
1842
1843
|
}
|
|
1843
1844
|
function parseTURNTokenClaims(token) {
|
|
@@ -1909,6 +1910,75 @@ function resolveMode(credentials, mode) {
|
|
|
1909
1910
|
}
|
|
1910
1911
|
return "api";
|
|
1911
1912
|
}
|
|
1913
|
+
async function readBoundedTURNKeyring(reader, chunks = [], size = 0) {
|
|
1914
|
+
const result = await reader.read();
|
|
1915
|
+
if (result.done) {
|
|
1916
|
+
return Buffer.concat(chunks, size).toString("utf8");
|
|
1917
|
+
}
|
|
1918
|
+
const nextSize = size + result.value.byteLength;
|
|
1919
|
+
if (nextSize > maxTURNKeyringBytes) {
|
|
1920
|
+
await reader.cancel();
|
|
1921
|
+
throw new Error("TURN keyring response exceeds the 8 KiB limit.");
|
|
1922
|
+
}
|
|
1923
|
+
return await readBoundedTURNKeyring(
|
|
1924
|
+
reader,
|
|
1925
|
+
[...chunks, result.value],
|
|
1926
|
+
nextSize
|
|
1927
|
+
);
|
|
1928
|
+
}
|
|
1929
|
+
function parseTURNServerPublicKeyHex(value) {
|
|
1930
|
+
const normalized = value.trim();
|
|
1931
|
+
if (normalized.length === 0 || normalized.length % 2 !== 0 || !/^[0-9a-f]+$/i.test(normalized)) {
|
|
1932
|
+
throw new Error("TURN keyring must contain hexadecimal DER key material.");
|
|
1933
|
+
}
|
|
1934
|
+
try {
|
|
1935
|
+
const key = import_crypto.default.createPublicKey({
|
|
1936
|
+
key: Buffer.from(normalized, "hex"),
|
|
1937
|
+
format: "der",
|
|
1938
|
+
type: "spki"
|
|
1939
|
+
});
|
|
1940
|
+
if (key.asymmetricKeyType !== "ec") {
|
|
1941
|
+
throw new Error("TURN keyring public key must be an EC key.");
|
|
1942
|
+
}
|
|
1943
|
+
return key;
|
|
1944
|
+
} catch (error) {
|
|
1945
|
+
if (error instanceof Error && error.message.startsWith("TURN keyring")) {
|
|
1946
|
+
throw error;
|
|
1947
|
+
}
|
|
1948
|
+
throw new Error("TURN keyring contains an invalid DER public key.", {
|
|
1949
|
+
cause: error
|
|
1950
|
+
});
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
function parseAPPPrivateKey(value) {
|
|
1954
|
+
try {
|
|
1955
|
+
const key = import_crypto.default.createPrivateKey({
|
|
1956
|
+
key: Buffer.from(value, "hex"),
|
|
1957
|
+
format: "der",
|
|
1958
|
+
type: "pkcs8"
|
|
1959
|
+
});
|
|
1960
|
+
if (key.asymmetricKeyType !== "ec") {
|
|
1961
|
+
throw new Error("TURN APP client secret must contain an EC private key.");
|
|
1962
|
+
}
|
|
1963
|
+
return key;
|
|
1964
|
+
} catch (error) {
|
|
1965
|
+
if (error instanceof Error && error.message.startsWith("TURN APP")) {
|
|
1966
|
+
throw error;
|
|
1967
|
+
}
|
|
1968
|
+
throw new Error("TURN APP client secret contains an invalid private key.", {
|
|
1969
|
+
cause: error
|
|
1970
|
+
});
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
function requireMatchingCurve(privateKey, publicKey) {
|
|
1974
|
+
const privateCurve = privateKey.asymmetricKeyDetails?.namedCurve;
|
|
1975
|
+
const publicCurve = publicKey.asymmetricKeyDetails?.namedCurve;
|
|
1976
|
+
if (!privateCurve || privateCurve !== publicCurve) {
|
|
1977
|
+
throw new Error(
|
|
1978
|
+
"TURN APP client and server keys must use the same named curve."
|
|
1979
|
+
);
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1912
1982
|
async function loadTURNServerPublicKeyHex(clusterDomain, options) {
|
|
1913
1983
|
const configuredKey = normalizeOptionalString2(options.keyringBaseUrl);
|
|
1914
1984
|
const keyringBaseURL = configuredKey ?? normalizeOptionalString2(options.apiUrl) ?? "https://rstream.io";
|
|
@@ -1916,11 +1986,19 @@ async function loadTURNServerPublicKeyHex(clusterDomain, options) {
|
|
|
1916
1986
|
`/keyrings/turn/${clusterDomain}.spki.der.hex`,
|
|
1917
1987
|
keyringBaseURL
|
|
1918
1988
|
);
|
|
1919
|
-
const response = await fetch(url);
|
|
1989
|
+
const response = await fetch(url, { redirect: "manual" });
|
|
1990
|
+
if (response.status >= 300 && response.status < 400) {
|
|
1991
|
+
throw new Error(
|
|
1992
|
+
"TURN keyring request was redirected; redirects are refused."
|
|
1993
|
+
);
|
|
1994
|
+
}
|
|
1920
1995
|
if (!response.ok) {
|
|
1921
|
-
throw new Error(
|
|
1996
|
+
throw new Error(`TURN keyring request returned HTTP ${response.status}.`);
|
|
1922
1997
|
}
|
|
1923
|
-
|
|
1998
|
+
if (!response.body) {
|
|
1999
|
+
throw new Error("TURN keyring response body is missing.");
|
|
2000
|
+
}
|
|
2001
|
+
return (await readBoundedTURNKeyring(response.body.getReader())).trim();
|
|
1924
2002
|
}
|
|
1925
2003
|
async function createAPITURNCredentials(options) {
|
|
1926
2004
|
const projectId = normalizeOptionalString2(options.projectId);
|
|
@@ -1952,9 +2030,14 @@ async function createAPITURNCredentials(options) {
|
|
|
1952
2030
|
);
|
|
1953
2031
|
}
|
|
1954
2032
|
function createPATTURNCredentials(options) {
|
|
1955
|
-
const
|
|
1956
|
-
|
|
1957
|
-
|
|
2033
|
+
const legacyDomain = normalizeClusterDomain(options.clusterDomain);
|
|
2034
|
+
const turnDomain = normalizeClusterDomain(options.turnDomain) ?? legacyDomain;
|
|
2035
|
+
const turnRealm = normalizeClusterDomain(options.turnRealm) ?? legacyDomain;
|
|
2036
|
+
if (!turnDomain) {
|
|
2037
|
+
throw new Error("TURN domain is required for TURN PAT mode.");
|
|
2038
|
+
}
|
|
2039
|
+
if (!turnRealm) {
|
|
2040
|
+
throw new Error("TURN realm is required for TURN PAT mode.");
|
|
1958
2041
|
}
|
|
1959
2042
|
const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
|
|
1960
2043
|
if (!projectEndpoint) {
|
|
@@ -1968,12 +2051,19 @@ function createPATTURNCredentials(options) {
|
|
|
1968
2051
|
if (claims.type !== "pat") {
|
|
1969
2052
|
throw new Error("TURN PAT mode requires a PAT token.");
|
|
1970
2053
|
}
|
|
1971
|
-
const
|
|
2054
|
+
const claimsTokenEndpoint = normalizeOptionalString2(
|
|
2055
|
+
claims.token_endpoint ?? claims.tokendpoint
|
|
2056
|
+
);
|
|
2057
|
+
const configuredTokenEndpoint = normalizeOptionalString2(
|
|
2058
|
+
options.tokenEndpoint
|
|
2059
|
+
);
|
|
2060
|
+
const tokenEndpoint = configuredTokenEndpoint ?? claimsTokenEndpoint;
|
|
1972
2061
|
if (!tokenEndpoint) {
|
|
1973
|
-
throw new Error(
|
|
2062
|
+
throw new Error(
|
|
2063
|
+
"TURN PAT mode requires a PAT token carrying a token endpoint."
|
|
2064
|
+
);
|
|
1974
2065
|
}
|
|
1975
|
-
|
|
1976
|
-
if (claimsTokenEndpoint !== void 0 && claimsTokenEndpoint !== tokenEndpoint) {
|
|
2066
|
+
if (configuredTokenEndpoint !== void 0 && claimsTokenEndpoint !== void 0 && claimsTokenEndpoint !== configuredTokenEndpoint) {
|
|
1977
2067
|
throw new Error("Token endpoint does not match the PAT token.");
|
|
1978
2068
|
}
|
|
1979
2069
|
const now = normalizeNow(options.now);
|
|
@@ -1986,7 +2076,7 @@ function createPATTURNCredentials(options) {
|
|
|
1986
2076
|
import_crypto.default.hkdfSync(
|
|
1987
2077
|
"sha256",
|
|
1988
2078
|
tokenHash,
|
|
1989
|
-
Buffer.from(
|
|
2079
|
+
Buffer.from(turnRealm, "utf8"),
|
|
1990
2080
|
Buffer.from("turn-pat-v1", "utf8"),
|
|
1991
2081
|
32
|
|
1992
2082
|
)
|
|
@@ -1995,14 +2085,19 @@ function createPATTURNCredentials(options) {
|
|
|
1995
2085
|
return import_turn.turnCredentialsSchema.parse({
|
|
1996
2086
|
credential,
|
|
1997
2087
|
ttl,
|
|
1998
|
-
urls: createTURNURLs(
|
|
2088
|
+
urls: createTURNURLs(turnDomain, options.turnPort, options.turnsPort),
|
|
1999
2089
|
username
|
|
2000
2090
|
});
|
|
2001
2091
|
}
|
|
2002
2092
|
async function createAPPTURNCredentials(options) {
|
|
2003
|
-
const
|
|
2004
|
-
|
|
2005
|
-
|
|
2093
|
+
const legacyDomain = normalizeClusterDomain(options.clusterDomain);
|
|
2094
|
+
const turnDomain = normalizeClusterDomain(options.turnDomain) ?? legacyDomain;
|
|
2095
|
+
const turnRealm = normalizeClusterDomain(options.turnRealm) ?? legacyDomain;
|
|
2096
|
+
if (!turnDomain) {
|
|
2097
|
+
throw new Error("TURN domain is required for TURN APP mode.");
|
|
2098
|
+
}
|
|
2099
|
+
if (!turnRealm) {
|
|
2100
|
+
throw new Error("TURN realm is required for TURN APP mode.");
|
|
2006
2101
|
}
|
|
2007
2102
|
const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
|
|
2008
2103
|
if (!projectEndpoint) {
|
|
@@ -2019,17 +2114,10 @@ async function createAPPTURNCredentials(options) {
|
|
|
2019
2114
|
const ttlSeconds = normalizeTTLSeconds(options.ttlSeconds);
|
|
2020
2115
|
const exp = normalizeNow(options.now) + ttlSeconds;
|
|
2021
2116
|
const username = `v1:${exp}:app:${projectEndpoint}:${clientId}`;
|
|
2022
|
-
const clientPrivateKey =
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
});
|
|
2027
|
-
const serverPublicKeyHex = normalizeOptionalString2(options.serverPublicKeyHex) ?? await loadTURNServerPublicKeyHex(clusterDomain, options);
|
|
2028
|
-
const serverPublicKey = import_crypto.default.createPublicKey({
|
|
2029
|
-
key: Buffer.from(serverPublicKeyHex, "hex"),
|
|
2030
|
-
format: "der",
|
|
2031
|
-
type: "spki"
|
|
2032
|
-
});
|
|
2117
|
+
const clientPrivateKey = parseAPPPrivateKey(clientSecret);
|
|
2118
|
+
const serverPublicKeyHex = normalizeOptionalString2(options.serverPublicKeyHex) ?? await loadTURNServerPublicKeyHex(turnRealm, options);
|
|
2119
|
+
const serverPublicKey = parseTURNServerPublicKeyHex(serverPublicKeyHex);
|
|
2120
|
+
requireMatchingCurve(clientPrivateKey, serverPublicKey);
|
|
2033
2121
|
const sharedSecret = import_crypto.default.diffieHellman({
|
|
2034
2122
|
privateKey: clientPrivateKey,
|
|
2035
2123
|
publicKey: serverPublicKey
|
|
@@ -2038,7 +2126,7 @@ async function createAPPTURNCredentials(options) {
|
|
|
2038
2126
|
import_crypto.default.hkdfSync(
|
|
2039
2127
|
"sha256",
|
|
2040
2128
|
sharedSecret,
|
|
2041
|
-
Buffer.from(
|
|
2129
|
+
Buffer.from(turnRealm, "utf8"),
|
|
2042
2130
|
Buffer.from("turn-app-v1", "utf8"),
|
|
2043
2131
|
32
|
|
2044
2132
|
)
|
|
@@ -2047,7 +2135,7 @@ async function createAPPTURNCredentials(options) {
|
|
|
2047
2135
|
return import_turn.turnCredentialsSchema.parse({
|
|
2048
2136
|
credential,
|
|
2049
2137
|
ttl: ttlSeconds,
|
|
2050
|
-
urls: createTURNURLs(
|
|
2138
|
+
urls: createTURNURLs(turnDomain, options.turnPort, options.turnsPort),
|
|
2051
2139
|
username
|
|
2052
2140
|
});
|
|
2053
2141
|
}
|
|
@@ -2070,28 +2158,31 @@ async function createTURNCredentials(options) {
|
|
|
2070
2158
|
ttlSeconds: options.ttlSeconds
|
|
2071
2159
|
});
|
|
2072
2160
|
}
|
|
2073
|
-
const
|
|
2161
|
+
const legacyDomain = normalizeClusterDomain(options.clusterDomain);
|
|
2162
|
+
const turnDomain = normalizeClusterDomain(options.turnDomain) ?? legacyDomain ?? (projectEndpoint && options.engine ? deriveClusterDomainFromEngine(projectEndpoint, options.engine) : void 0);
|
|
2163
|
+
const turnRealm = normalizeClusterDomain(options.turnRealm) ?? legacyDomain;
|
|
2074
2164
|
if (mode === "pat") {
|
|
2075
2165
|
if (!(0, import_rstream9.isTokenCredentials)(credentials)) {
|
|
2076
2166
|
throw new Error("A PAT token is required for TURN PAT mode.");
|
|
2077
2167
|
}
|
|
2078
2168
|
return createPATTURNCredentials({
|
|
2079
|
-
clusterDomain: requireOption(
|
|
2080
|
-
clusterDomain,
|
|
2081
|
-
"Cluster domain is required for TURN PAT mode."
|
|
2082
|
-
),
|
|
2083
2169
|
now: options.now,
|
|
2084
2170
|
projectEndpoint: requireOption(
|
|
2085
2171
|
projectEndpoint,
|
|
2086
2172
|
"Project endpoint is required for TURN PAT mode."
|
|
2087
2173
|
),
|
|
2088
2174
|
token: credentials.token,
|
|
2089
|
-
tokenEndpoint:
|
|
2090
|
-
normalizeOptionalString2(options.tokenEndpoint),
|
|
2091
|
-
"Token endpoint is required for TURN PAT mode."
|
|
2092
|
-
),
|
|
2175
|
+
tokenEndpoint: options.tokenEndpoint,
|
|
2093
2176
|
ttlSeconds: options.ttlSeconds,
|
|
2177
|
+
turnDomain: requireOption(
|
|
2178
|
+
turnDomain,
|
|
2179
|
+
"TURN domain is required for TURN PAT mode."
|
|
2180
|
+
),
|
|
2094
2181
|
turnPort: options.turnPort,
|
|
2182
|
+
turnRealm: requireOption(
|
|
2183
|
+
turnRealm,
|
|
2184
|
+
"TURN realm is required for TURN PAT mode."
|
|
2185
|
+
),
|
|
2095
2186
|
turnsPort: options.turnsPort
|
|
2096
2187
|
});
|
|
2097
2188
|
}
|
|
@@ -2102,10 +2193,6 @@ async function createTURNCredentials(options) {
|
|
|
2102
2193
|
apiUrl: options.apiUrl,
|
|
2103
2194
|
clientId: credentials.clientId,
|
|
2104
2195
|
clientSecret: credentials.clientSecret,
|
|
2105
|
-
clusterDomain: requireOption(
|
|
2106
|
-
clusterDomain,
|
|
2107
|
-
"Cluster domain is required for TURN APP mode."
|
|
2108
|
-
),
|
|
2109
2196
|
keyringBaseUrl: options.keyringBaseUrl,
|
|
2110
2197
|
now: options.now,
|
|
2111
2198
|
projectEndpoint: requireOption(
|
|
@@ -2114,24 +2201,36 @@ async function createTURNCredentials(options) {
|
|
|
2114
2201
|
),
|
|
2115
2202
|
serverPublicKeyHex: options.serverPublicKeyHex,
|
|
2116
2203
|
ttlSeconds: options.ttlSeconds,
|
|
2204
|
+
turnDomain: requireOption(
|
|
2205
|
+
turnDomain,
|
|
2206
|
+
"TURN domain is required for TURN APP mode."
|
|
2207
|
+
),
|
|
2117
2208
|
turnPort: options.turnPort,
|
|
2209
|
+
turnRealm: requireOption(
|
|
2210
|
+
turnRealm,
|
|
2211
|
+
"TURN realm is required for TURN APP mode."
|
|
2212
|
+
),
|
|
2118
2213
|
turnsPort: options.turnsPort
|
|
2119
2214
|
});
|
|
2120
2215
|
}
|
|
2121
2216
|
|
|
2122
2217
|
// src/turn-resource.ts
|
|
2218
|
+
var import_rstream11 = require("@rstreamlabs/rstream");
|
|
2123
2219
|
var RstreamTURNResource = class {
|
|
2124
2220
|
client;
|
|
2125
2221
|
constructor(client) {
|
|
2126
2222
|
this.client = client;
|
|
2127
2223
|
}
|
|
2128
2224
|
async createCredentials(options = {}) {
|
|
2129
|
-
const
|
|
2225
|
+
const credentials = options.credentials ?? this.client.credentials;
|
|
2226
|
+
const localMode = options.mode === "app" || options.mode === "pat" || options.mode === void 0 && (0, import_rstream11.isClientCredentials)(credentials);
|
|
2227
|
+
const target = localMode && (options.turnDomain === void 0 || options.turnRealm === void 0) && options.clusterDomain === void 0 ? await this.client.getTURNTarget() : void 0;
|
|
2228
|
+
const engine = options.engine ?? (options.clusterDomain === void 0 && options.turnDomain === void 0 && target === void 0 ? await this.client.getEngine().catch(() => void 0) : void 0);
|
|
2130
2229
|
return await createTURNCredentials({
|
|
2131
2230
|
apiUrl: options.apiUrl ?? this.client.apiUrl,
|
|
2132
2231
|
clusterDomain: options.clusterDomain,
|
|
2133
2232
|
controlPlaneCredentials: options.controlPlaneCredentials ?? this.client.controlPlaneCredentials,
|
|
2134
|
-
credentials
|
|
2233
|
+
credentials,
|
|
2135
2234
|
engine,
|
|
2136
2235
|
keyringBaseUrl: options.keyringBaseUrl,
|
|
2137
2236
|
mode: options.mode,
|
|
@@ -2141,8 +2240,10 @@ var RstreamTURNResource = class {
|
|
|
2141
2240
|
serverPublicKeyHex: options.serverPublicKeyHex,
|
|
2142
2241
|
tokenEndpoint: options.tokenEndpoint,
|
|
2143
2242
|
ttlSeconds: options.ttlSeconds,
|
|
2144
|
-
|
|
2145
|
-
|
|
2243
|
+
turnDomain: options.turnDomain ?? target?.turnDomain,
|
|
2244
|
+
turnPort: options.turnPort ?? target?.turnPort,
|
|
2245
|
+
turnRealm: options.turnRealm ?? target?.turnRealm,
|
|
2246
|
+
turnsPort: options.turnsPort ?? target?.turnsPort
|
|
2146
2247
|
});
|
|
2147
2248
|
}
|
|
2148
2249
|
};
|
|
@@ -2323,7 +2424,7 @@ var RstreamTunnelsClient = class {
|
|
|
2323
2424
|
this.config?.projectEndpoint
|
|
2324
2425
|
);
|
|
2325
2426
|
if (region !== void 0) {
|
|
2326
|
-
if (normalizeOptionalString3(this.config?.engine) !== void 0 || (0,
|
|
2427
|
+
if (normalizeOptionalString3(this.config?.engine) !== void 0 || (0, import_rstream14.readEnvironment)().engine !== void 0) {
|
|
2327
2428
|
throw new Error(
|
|
2328
2429
|
"Region selection cannot be combined with an explicit engine override."
|
|
2329
2430
|
);
|
|
@@ -2335,17 +2436,17 @@ var RstreamTunnelsClient = class {
|
|
|
2335
2436
|
}
|
|
2336
2437
|
const project = await this.getManagedProject();
|
|
2337
2438
|
if (project !== void 0) {
|
|
2338
|
-
const engine = (0,
|
|
2439
|
+
const engine = (0, import_rstream12.getTunnelsProjectEngine)(project, region);
|
|
2339
2440
|
if (engine) return normalizeEngineAddress(engine);
|
|
2340
2441
|
}
|
|
2341
2442
|
throw new Error(
|
|
2342
2443
|
"Failed to resolve the selected regional engine from the managed tunnels project."
|
|
2343
2444
|
);
|
|
2344
2445
|
}
|
|
2345
|
-
if (projectEndpoint !== void 0 && normalizeOptionalString3(this.config?.engine) === void 0 && (0,
|
|
2446
|
+
if (projectEndpoint !== void 0 && normalizeOptionalString3(this.config?.engine) === void 0 && (0, import_rstream14.readEnvironment)().engine === void 0) {
|
|
2346
2447
|
const project = await this.getManagedProject();
|
|
2347
2448
|
if (project !== void 0) {
|
|
2348
|
-
const engine = (0,
|
|
2449
|
+
const engine = (0, import_rstream12.getTunnelsProjectEngine)(project);
|
|
2349
2450
|
if (engine) {
|
|
2350
2451
|
return normalizeEngineAddress(engine);
|
|
2351
2452
|
}
|
|
@@ -2364,6 +2465,27 @@ var RstreamTunnelsClient = class {
|
|
|
2364
2465
|
region: this.config?.region
|
|
2365
2466
|
});
|
|
2366
2467
|
}
|
|
2468
|
+
async getTURNTarget() {
|
|
2469
|
+
const project = await this.getManagedProject();
|
|
2470
|
+
if (project === void 0) {
|
|
2471
|
+
throw new Error(
|
|
2472
|
+
"Managed project endpoint is required to resolve the TURN target."
|
|
2473
|
+
);
|
|
2474
|
+
}
|
|
2475
|
+
const turnDomain = project.turnDomain?.trim() || project.domain.trim();
|
|
2476
|
+
const turnRealm = project.turnRealm?.trim();
|
|
2477
|
+
if (!turnRealm) {
|
|
2478
|
+
throw new Error(
|
|
2479
|
+
"Managed project metadata does not expose the TURN realm; use API credential mode or provide turnRealm explicitly."
|
|
2480
|
+
);
|
|
2481
|
+
}
|
|
2482
|
+
return {
|
|
2483
|
+
turnDomain,
|
|
2484
|
+
turnPort: project.turnPort,
|
|
2485
|
+
turnRealm,
|
|
2486
|
+
turnsPort: project.turnsPort
|
|
2487
|
+
};
|
|
2488
|
+
}
|
|
2367
2489
|
async getManagedProject() {
|
|
2368
2490
|
const projectEndpoint = normalizeOptionalString3(
|
|
2369
2491
|
this.config?.projectEndpoint
|
|
@@ -2440,7 +2562,7 @@ var RstreamTunnelsClient = class {
|
|
|
2440
2562
|
if (!credentials) {
|
|
2441
2563
|
return void 0;
|
|
2442
2564
|
}
|
|
2443
|
-
if (!(0,
|
|
2565
|
+
if (!(0, import_rstream13.isClientCredentials)(credentials)) {
|
|
2444
2566
|
return credentials.token;
|
|
2445
2567
|
}
|
|
2446
2568
|
const tokenParams = await this.getEngineAuthTokenParams();
|
package/dist/index.mjs
CHANGED
|
@@ -404,7 +404,7 @@ var RstreamTunnelsResource = class {
|
|
|
404
404
|
|
|
405
405
|
// src/tunnels.ts
|
|
406
406
|
import { getTunnelsProjectEngine as getTunnelsProjectEngine2 } from "@rstreamlabs/rstream";
|
|
407
|
-
import { isClientCredentials as
|
|
407
|
+
import { isClientCredentials as isClientCredentials4 } from "@rstreamlabs/rstream";
|
|
408
408
|
import { readEnvironment as readEnvironment2 } from "@rstreamlabs/rstream";
|
|
409
409
|
|
|
410
410
|
// src/turn.ts
|
|
@@ -418,6 +418,7 @@ var defaultTURNPort = 3478;
|
|
|
418
418
|
var defaultTURNSPort = 5349;
|
|
419
419
|
var defaultTURNCredentialTTLSeconds = 10 * 60;
|
|
420
420
|
var maxTURNCredentialTTLSeconds = 60 * 60;
|
|
421
|
+
var maxTURNKeyringBytes = 8 * 1024;
|
|
421
422
|
var turnTokenClaimsSchema = z.object({
|
|
422
423
|
exp: z.number().int().optional(),
|
|
423
424
|
iat: z.number().int().optional(),
|
|
@@ -467,14 +468,14 @@ function normalizeTURNPort(port) {
|
|
|
467
468
|
function normalizeTURNSPort(port) {
|
|
468
469
|
return normalizePort(port, defaultTURNSPort, "TURNS port");
|
|
469
470
|
}
|
|
470
|
-
function createTURNURLs(
|
|
471
|
+
function createTURNURLs(turnDomain, turnPort, turnsPort) {
|
|
471
472
|
const normalizedTURNPort = normalizeTURNPort(turnPort);
|
|
472
473
|
const normalizedTURNSPort = normalizeTURNSPort(turnsPort);
|
|
473
474
|
return [
|
|
474
|
-
`turn:${
|
|
475
|
-
`turn:${
|
|
476
|
-
`turns:${
|
|
477
|
-
`turns:${
|
|
475
|
+
`turn:${turnDomain}:${normalizedTURNPort}?transport=udp`,
|
|
476
|
+
`turn:${turnDomain}:${normalizedTURNPort}?transport=tcp`,
|
|
477
|
+
`turns:${turnDomain}:${normalizedTURNSPort}?transport=udp`,
|
|
478
|
+
`turns:${turnDomain}:${normalizedTURNSPort}?transport=tcp`
|
|
478
479
|
];
|
|
479
480
|
}
|
|
480
481
|
function parseTURNTokenClaims(token) {
|
|
@@ -546,6 +547,75 @@ function resolveMode(credentials, mode) {
|
|
|
546
547
|
}
|
|
547
548
|
return "api";
|
|
548
549
|
}
|
|
550
|
+
async function readBoundedTURNKeyring(reader, chunks = [], size = 0) {
|
|
551
|
+
const result = await reader.read();
|
|
552
|
+
if (result.done) {
|
|
553
|
+
return Buffer.concat(chunks, size).toString("utf8");
|
|
554
|
+
}
|
|
555
|
+
const nextSize = size + result.value.byteLength;
|
|
556
|
+
if (nextSize > maxTURNKeyringBytes) {
|
|
557
|
+
await reader.cancel();
|
|
558
|
+
throw new Error("TURN keyring response exceeds the 8 KiB limit.");
|
|
559
|
+
}
|
|
560
|
+
return await readBoundedTURNKeyring(
|
|
561
|
+
reader,
|
|
562
|
+
[...chunks, result.value],
|
|
563
|
+
nextSize
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
function parseTURNServerPublicKeyHex(value) {
|
|
567
|
+
const normalized = value.trim();
|
|
568
|
+
if (normalized.length === 0 || normalized.length % 2 !== 0 || !/^[0-9a-f]+$/i.test(normalized)) {
|
|
569
|
+
throw new Error("TURN keyring must contain hexadecimal DER key material.");
|
|
570
|
+
}
|
|
571
|
+
try {
|
|
572
|
+
const key = crypto.createPublicKey({
|
|
573
|
+
key: Buffer.from(normalized, "hex"),
|
|
574
|
+
format: "der",
|
|
575
|
+
type: "spki"
|
|
576
|
+
});
|
|
577
|
+
if (key.asymmetricKeyType !== "ec") {
|
|
578
|
+
throw new Error("TURN keyring public key must be an EC key.");
|
|
579
|
+
}
|
|
580
|
+
return key;
|
|
581
|
+
} catch (error) {
|
|
582
|
+
if (error instanceof Error && error.message.startsWith("TURN keyring")) {
|
|
583
|
+
throw error;
|
|
584
|
+
}
|
|
585
|
+
throw new Error("TURN keyring contains an invalid DER public key.", {
|
|
586
|
+
cause: error
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
function parseAPPPrivateKey(value) {
|
|
591
|
+
try {
|
|
592
|
+
const key = crypto.createPrivateKey({
|
|
593
|
+
key: Buffer.from(value, "hex"),
|
|
594
|
+
format: "der",
|
|
595
|
+
type: "pkcs8"
|
|
596
|
+
});
|
|
597
|
+
if (key.asymmetricKeyType !== "ec") {
|
|
598
|
+
throw new Error("TURN APP client secret must contain an EC private key.");
|
|
599
|
+
}
|
|
600
|
+
return key;
|
|
601
|
+
} catch (error) {
|
|
602
|
+
if (error instanceof Error && error.message.startsWith("TURN APP")) {
|
|
603
|
+
throw error;
|
|
604
|
+
}
|
|
605
|
+
throw new Error("TURN APP client secret contains an invalid private key.", {
|
|
606
|
+
cause: error
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
function requireMatchingCurve(privateKey, publicKey) {
|
|
611
|
+
const privateCurve = privateKey.asymmetricKeyDetails?.namedCurve;
|
|
612
|
+
const publicCurve = publicKey.asymmetricKeyDetails?.namedCurve;
|
|
613
|
+
if (!privateCurve || privateCurve !== publicCurve) {
|
|
614
|
+
throw new Error(
|
|
615
|
+
"TURN APP client and server keys must use the same named curve."
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
549
619
|
async function loadTURNServerPublicKeyHex(clusterDomain, options) {
|
|
550
620
|
const configuredKey = normalizeOptionalString2(options.keyringBaseUrl);
|
|
551
621
|
const keyringBaseURL = configuredKey ?? normalizeOptionalString2(options.apiUrl) ?? "https://rstream.io";
|
|
@@ -553,11 +623,19 @@ async function loadTURNServerPublicKeyHex(clusterDomain, options) {
|
|
|
553
623
|
`/keyrings/turn/${clusterDomain}.spki.der.hex`,
|
|
554
624
|
keyringBaseURL
|
|
555
625
|
);
|
|
556
|
-
const response = await fetch(url);
|
|
626
|
+
const response = await fetch(url, { redirect: "manual" });
|
|
627
|
+
if (response.status >= 300 && response.status < 400) {
|
|
628
|
+
throw new Error(
|
|
629
|
+
"TURN keyring request was redirected; redirects are refused."
|
|
630
|
+
);
|
|
631
|
+
}
|
|
557
632
|
if (!response.ok) {
|
|
558
|
-
throw new Error(
|
|
633
|
+
throw new Error(`TURN keyring request returned HTTP ${response.status}.`);
|
|
559
634
|
}
|
|
560
|
-
|
|
635
|
+
if (!response.body) {
|
|
636
|
+
throw new Error("TURN keyring response body is missing.");
|
|
637
|
+
}
|
|
638
|
+
return (await readBoundedTURNKeyring(response.body.getReader())).trim();
|
|
561
639
|
}
|
|
562
640
|
async function createAPITURNCredentials(options) {
|
|
563
641
|
const projectId = normalizeOptionalString2(options.projectId);
|
|
@@ -589,9 +667,14 @@ async function createAPITURNCredentials(options) {
|
|
|
589
667
|
);
|
|
590
668
|
}
|
|
591
669
|
function createPATTURNCredentials(options) {
|
|
592
|
-
const
|
|
593
|
-
|
|
594
|
-
|
|
670
|
+
const legacyDomain = normalizeClusterDomain(options.clusterDomain);
|
|
671
|
+
const turnDomain = normalizeClusterDomain(options.turnDomain) ?? legacyDomain;
|
|
672
|
+
const turnRealm = normalizeClusterDomain(options.turnRealm) ?? legacyDomain;
|
|
673
|
+
if (!turnDomain) {
|
|
674
|
+
throw new Error("TURN domain is required for TURN PAT mode.");
|
|
675
|
+
}
|
|
676
|
+
if (!turnRealm) {
|
|
677
|
+
throw new Error("TURN realm is required for TURN PAT mode.");
|
|
595
678
|
}
|
|
596
679
|
const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
|
|
597
680
|
if (!projectEndpoint) {
|
|
@@ -605,12 +688,19 @@ function createPATTURNCredentials(options) {
|
|
|
605
688
|
if (claims.type !== "pat") {
|
|
606
689
|
throw new Error("TURN PAT mode requires a PAT token.");
|
|
607
690
|
}
|
|
608
|
-
const
|
|
691
|
+
const claimsTokenEndpoint = normalizeOptionalString2(
|
|
692
|
+
claims.token_endpoint ?? claims.tokendpoint
|
|
693
|
+
);
|
|
694
|
+
const configuredTokenEndpoint = normalizeOptionalString2(
|
|
695
|
+
options.tokenEndpoint
|
|
696
|
+
);
|
|
697
|
+
const tokenEndpoint = configuredTokenEndpoint ?? claimsTokenEndpoint;
|
|
609
698
|
if (!tokenEndpoint) {
|
|
610
|
-
throw new Error(
|
|
699
|
+
throw new Error(
|
|
700
|
+
"TURN PAT mode requires a PAT token carrying a token endpoint."
|
|
701
|
+
);
|
|
611
702
|
}
|
|
612
|
-
|
|
613
|
-
if (claimsTokenEndpoint !== void 0 && claimsTokenEndpoint !== tokenEndpoint) {
|
|
703
|
+
if (configuredTokenEndpoint !== void 0 && claimsTokenEndpoint !== void 0 && claimsTokenEndpoint !== configuredTokenEndpoint) {
|
|
614
704
|
throw new Error("Token endpoint does not match the PAT token.");
|
|
615
705
|
}
|
|
616
706
|
const now = normalizeNow(options.now);
|
|
@@ -623,7 +713,7 @@ function createPATTURNCredentials(options) {
|
|
|
623
713
|
crypto.hkdfSync(
|
|
624
714
|
"sha256",
|
|
625
715
|
tokenHash,
|
|
626
|
-
Buffer.from(
|
|
716
|
+
Buffer.from(turnRealm, "utf8"),
|
|
627
717
|
Buffer.from("turn-pat-v1", "utf8"),
|
|
628
718
|
32
|
|
629
719
|
)
|
|
@@ -632,14 +722,19 @@ function createPATTURNCredentials(options) {
|
|
|
632
722
|
return turnCredentialsSchema.parse({
|
|
633
723
|
credential,
|
|
634
724
|
ttl,
|
|
635
|
-
urls: createTURNURLs(
|
|
725
|
+
urls: createTURNURLs(turnDomain, options.turnPort, options.turnsPort),
|
|
636
726
|
username
|
|
637
727
|
});
|
|
638
728
|
}
|
|
639
729
|
async function createAPPTURNCredentials(options) {
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
730
|
+
const legacyDomain = normalizeClusterDomain(options.clusterDomain);
|
|
731
|
+
const turnDomain = normalizeClusterDomain(options.turnDomain) ?? legacyDomain;
|
|
732
|
+
const turnRealm = normalizeClusterDomain(options.turnRealm) ?? legacyDomain;
|
|
733
|
+
if (!turnDomain) {
|
|
734
|
+
throw new Error("TURN domain is required for TURN APP mode.");
|
|
735
|
+
}
|
|
736
|
+
if (!turnRealm) {
|
|
737
|
+
throw new Error("TURN realm is required for TURN APP mode.");
|
|
643
738
|
}
|
|
644
739
|
const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
|
|
645
740
|
if (!projectEndpoint) {
|
|
@@ -656,17 +751,10 @@ async function createAPPTURNCredentials(options) {
|
|
|
656
751
|
const ttlSeconds = normalizeTTLSeconds(options.ttlSeconds);
|
|
657
752
|
const exp = normalizeNow(options.now) + ttlSeconds;
|
|
658
753
|
const username = `v1:${exp}:app:${projectEndpoint}:${clientId}`;
|
|
659
|
-
const clientPrivateKey =
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
});
|
|
664
|
-
const serverPublicKeyHex = normalizeOptionalString2(options.serverPublicKeyHex) ?? await loadTURNServerPublicKeyHex(clusterDomain, options);
|
|
665
|
-
const serverPublicKey = crypto.createPublicKey({
|
|
666
|
-
key: Buffer.from(serverPublicKeyHex, "hex"),
|
|
667
|
-
format: "der",
|
|
668
|
-
type: "spki"
|
|
669
|
-
});
|
|
754
|
+
const clientPrivateKey = parseAPPPrivateKey(clientSecret);
|
|
755
|
+
const serverPublicKeyHex = normalizeOptionalString2(options.serverPublicKeyHex) ?? await loadTURNServerPublicKeyHex(turnRealm, options);
|
|
756
|
+
const serverPublicKey = parseTURNServerPublicKeyHex(serverPublicKeyHex);
|
|
757
|
+
requireMatchingCurve(clientPrivateKey, serverPublicKey);
|
|
670
758
|
const sharedSecret = crypto.diffieHellman({
|
|
671
759
|
privateKey: clientPrivateKey,
|
|
672
760
|
publicKey: serverPublicKey
|
|
@@ -675,7 +763,7 @@ async function createAPPTURNCredentials(options) {
|
|
|
675
763
|
crypto.hkdfSync(
|
|
676
764
|
"sha256",
|
|
677
765
|
sharedSecret,
|
|
678
|
-
Buffer.from(
|
|
766
|
+
Buffer.from(turnRealm, "utf8"),
|
|
679
767
|
Buffer.from("turn-app-v1", "utf8"),
|
|
680
768
|
32
|
|
681
769
|
)
|
|
@@ -684,7 +772,7 @@ async function createAPPTURNCredentials(options) {
|
|
|
684
772
|
return turnCredentialsSchema.parse({
|
|
685
773
|
credential,
|
|
686
774
|
ttl: ttlSeconds,
|
|
687
|
-
urls: createTURNURLs(
|
|
775
|
+
urls: createTURNURLs(turnDomain, options.turnPort, options.turnsPort),
|
|
688
776
|
username
|
|
689
777
|
});
|
|
690
778
|
}
|
|
@@ -707,28 +795,31 @@ async function createTURNCredentials(options) {
|
|
|
707
795
|
ttlSeconds: options.ttlSeconds
|
|
708
796
|
});
|
|
709
797
|
}
|
|
710
|
-
const
|
|
798
|
+
const legacyDomain = normalizeClusterDomain(options.clusterDomain);
|
|
799
|
+
const turnDomain = normalizeClusterDomain(options.turnDomain) ?? legacyDomain ?? (projectEndpoint && options.engine ? deriveClusterDomainFromEngine(projectEndpoint, options.engine) : void 0);
|
|
800
|
+
const turnRealm = normalizeClusterDomain(options.turnRealm) ?? legacyDomain;
|
|
711
801
|
if (mode === "pat") {
|
|
712
802
|
if (!isTokenCredentials2(credentials)) {
|
|
713
803
|
throw new Error("A PAT token is required for TURN PAT mode.");
|
|
714
804
|
}
|
|
715
805
|
return createPATTURNCredentials({
|
|
716
|
-
clusterDomain: requireOption(
|
|
717
|
-
clusterDomain,
|
|
718
|
-
"Cluster domain is required for TURN PAT mode."
|
|
719
|
-
),
|
|
720
806
|
now: options.now,
|
|
721
807
|
projectEndpoint: requireOption(
|
|
722
808
|
projectEndpoint,
|
|
723
809
|
"Project endpoint is required for TURN PAT mode."
|
|
724
810
|
),
|
|
725
811
|
token: credentials.token,
|
|
726
|
-
tokenEndpoint:
|
|
727
|
-
normalizeOptionalString2(options.tokenEndpoint),
|
|
728
|
-
"Token endpoint is required for TURN PAT mode."
|
|
729
|
-
),
|
|
812
|
+
tokenEndpoint: options.tokenEndpoint,
|
|
730
813
|
ttlSeconds: options.ttlSeconds,
|
|
814
|
+
turnDomain: requireOption(
|
|
815
|
+
turnDomain,
|
|
816
|
+
"TURN domain is required for TURN PAT mode."
|
|
817
|
+
),
|
|
731
818
|
turnPort: options.turnPort,
|
|
819
|
+
turnRealm: requireOption(
|
|
820
|
+
turnRealm,
|
|
821
|
+
"TURN realm is required for TURN PAT mode."
|
|
822
|
+
),
|
|
732
823
|
turnsPort: options.turnsPort
|
|
733
824
|
});
|
|
734
825
|
}
|
|
@@ -739,10 +830,6 @@ async function createTURNCredentials(options) {
|
|
|
739
830
|
apiUrl: options.apiUrl,
|
|
740
831
|
clientId: credentials.clientId,
|
|
741
832
|
clientSecret: credentials.clientSecret,
|
|
742
|
-
clusterDomain: requireOption(
|
|
743
|
-
clusterDomain,
|
|
744
|
-
"Cluster domain is required for TURN APP mode."
|
|
745
|
-
),
|
|
746
833
|
keyringBaseUrl: options.keyringBaseUrl,
|
|
747
834
|
now: options.now,
|
|
748
835
|
projectEndpoint: requireOption(
|
|
@@ -751,24 +838,36 @@ async function createTURNCredentials(options) {
|
|
|
751
838
|
),
|
|
752
839
|
serverPublicKeyHex: options.serverPublicKeyHex,
|
|
753
840
|
ttlSeconds: options.ttlSeconds,
|
|
841
|
+
turnDomain: requireOption(
|
|
842
|
+
turnDomain,
|
|
843
|
+
"TURN domain is required for TURN APP mode."
|
|
844
|
+
),
|
|
754
845
|
turnPort: options.turnPort,
|
|
846
|
+
turnRealm: requireOption(
|
|
847
|
+
turnRealm,
|
|
848
|
+
"TURN realm is required for TURN APP mode."
|
|
849
|
+
),
|
|
755
850
|
turnsPort: options.turnsPort
|
|
756
851
|
});
|
|
757
852
|
}
|
|
758
853
|
|
|
759
854
|
// src/turn-resource.ts
|
|
855
|
+
import { isClientCredentials as isClientCredentials3 } from "@rstreamlabs/rstream";
|
|
760
856
|
var RstreamTURNResource = class {
|
|
761
857
|
client;
|
|
762
858
|
constructor(client) {
|
|
763
859
|
this.client = client;
|
|
764
860
|
}
|
|
765
861
|
async createCredentials(options = {}) {
|
|
766
|
-
const
|
|
862
|
+
const credentials = options.credentials ?? this.client.credentials;
|
|
863
|
+
const localMode = options.mode === "app" || options.mode === "pat" || options.mode === void 0 && isClientCredentials3(credentials);
|
|
864
|
+
const target = localMode && (options.turnDomain === void 0 || options.turnRealm === void 0) && options.clusterDomain === void 0 ? await this.client.getTURNTarget() : void 0;
|
|
865
|
+
const engine = options.engine ?? (options.clusterDomain === void 0 && options.turnDomain === void 0 && target === void 0 ? await this.client.getEngine().catch(() => void 0) : void 0);
|
|
767
866
|
return await createTURNCredentials({
|
|
768
867
|
apiUrl: options.apiUrl ?? this.client.apiUrl,
|
|
769
868
|
clusterDomain: options.clusterDomain,
|
|
770
869
|
controlPlaneCredentials: options.controlPlaneCredentials ?? this.client.controlPlaneCredentials,
|
|
771
|
-
credentials
|
|
870
|
+
credentials,
|
|
772
871
|
engine,
|
|
773
872
|
keyringBaseUrl: options.keyringBaseUrl,
|
|
774
873
|
mode: options.mode,
|
|
@@ -778,8 +877,10 @@ var RstreamTURNResource = class {
|
|
|
778
877
|
serverPublicKeyHex: options.serverPublicKeyHex,
|
|
779
878
|
tokenEndpoint: options.tokenEndpoint,
|
|
780
879
|
ttlSeconds: options.ttlSeconds,
|
|
781
|
-
|
|
782
|
-
|
|
880
|
+
turnDomain: options.turnDomain ?? target?.turnDomain,
|
|
881
|
+
turnPort: options.turnPort ?? target?.turnPort,
|
|
882
|
+
turnRealm: options.turnRealm ?? target?.turnRealm,
|
|
883
|
+
turnsPort: options.turnsPort ?? target?.turnsPort
|
|
783
884
|
});
|
|
784
885
|
}
|
|
785
886
|
};
|
|
@@ -1001,6 +1102,27 @@ var RstreamTunnelsClient = class {
|
|
|
1001
1102
|
region: this.config?.region
|
|
1002
1103
|
});
|
|
1003
1104
|
}
|
|
1105
|
+
async getTURNTarget() {
|
|
1106
|
+
const project = await this.getManagedProject();
|
|
1107
|
+
if (project === void 0) {
|
|
1108
|
+
throw new Error(
|
|
1109
|
+
"Managed project endpoint is required to resolve the TURN target."
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
const turnDomain = project.turnDomain?.trim() || project.domain.trim();
|
|
1113
|
+
const turnRealm = project.turnRealm?.trim();
|
|
1114
|
+
if (!turnRealm) {
|
|
1115
|
+
throw new Error(
|
|
1116
|
+
"Managed project metadata does not expose the TURN realm; use API credential mode or provide turnRealm explicitly."
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1119
|
+
return {
|
|
1120
|
+
turnDomain,
|
|
1121
|
+
turnPort: project.turnPort,
|
|
1122
|
+
turnRealm,
|
|
1123
|
+
turnsPort: project.turnsPort
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1004
1126
|
async getManagedProject() {
|
|
1005
1127
|
const projectEndpoint = normalizeOptionalString3(
|
|
1006
1128
|
this.config?.projectEndpoint
|
|
@@ -1077,7 +1199,7 @@ var RstreamTunnelsClient = class {
|
|
|
1077
1199
|
if (!credentials) {
|
|
1078
1200
|
return void 0;
|
|
1079
1201
|
}
|
|
1080
|
-
if (!
|
|
1202
|
+
if (!isClientCredentials4(credentials)) {
|
|
1081
1203
|
return credentials.token;
|
|
1082
1204
|
}
|
|
1083
1205
|
const tokenParams = await this.getEngineAuthTokenParams();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rstreamlabs/tunnels",
|
|
3
|
-
"version": "3.8.
|
|
3
|
+
"version": "3.8.2",
|
|
4
4
|
"description": "JS/TS SDK for the rstream Engine API and tunnel runtime APIs.",
|
|
5
5
|
"author": "@uartnet <hello@rstream.io>",
|
|
6
6
|
"repository": {
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"typescript-config": "*"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@rstreamlabs/rstream": "3.8.
|
|
50
|
+
"@rstreamlabs/rstream": "3.8.1",
|
|
51
51
|
"@types/jsonwebtoken": "^9.0.10",
|
|
52
52
|
"jsonwebtoken": "^9.0.3",
|
|
53
53
|
"zod": "^4.4.3"
|