@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 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
- clusterDomain: "cluster.example.rstream.test",
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: string;
242
+ clusterDomain?: string;
243
243
  now?: Date | number;
244
244
  projectEndpoint: string;
245
245
  token: string;
246
- tokenEndpoint: string;
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: string;
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: string;
242
+ clusterDomain?: string;
243
243
  now?: Date | number;
244
244
  projectEndpoint: string;
245
245
  token: string;
246
- tokenEndpoint: string;
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: string;
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(clusterDomain, turnPort, turnsPort) {
1834
+ function createTURNURLs(turnDomain, turnPort, turnsPort) {
1834
1835
  const normalizedTURNPort = normalizeTURNPort(turnPort);
1835
1836
  const normalizedTURNSPort = normalizeTURNSPort(turnsPort);
1836
1837
  return [
1837
- `turn:${clusterDomain}:${normalizedTURNPort}?transport=udp`,
1838
- `turn:${clusterDomain}:${normalizedTURNPort}?transport=tcp`,
1839
- `turns:${clusterDomain}:${normalizedTURNSPort}?transport=udp`,
1840
- `turns:${clusterDomain}:${normalizedTURNSPort}?transport=tcp`
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("Failed to load TURN keyring.");
1996
+ throw new Error(`TURN keyring request returned HTTP ${response.status}.`);
1922
1997
  }
1923
- return (await response.text()).trim();
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 clusterDomain = normalizeClusterDomain(options.clusterDomain);
1956
- if (!clusterDomain) {
1957
- throw new Error("Cluster domain is required for TURN PAT mode.");
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 tokenEndpoint = normalizeOptionalString2(options.tokenEndpoint);
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("Token endpoint is required for TURN PAT mode.");
2062
+ throw new Error(
2063
+ "TURN PAT mode requires a PAT token carrying a token endpoint."
2064
+ );
1974
2065
  }
1975
- const claimsTokenEndpoint = claims.token_endpoint ?? claims.tokendpoint;
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(clusterDomain, "utf8"),
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(clusterDomain, options.turnPort, options.turnsPort),
2088
+ urls: createTURNURLs(turnDomain, options.turnPort, options.turnsPort),
1999
2089
  username
2000
2090
  });
2001
2091
  }
2002
2092
  async function createAPPTURNCredentials(options) {
2003
- const clusterDomain = normalizeClusterDomain(options.clusterDomain);
2004
- if (!clusterDomain) {
2005
- throw new Error("Cluster domain is required for TURN APP mode.");
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 = import_crypto.default.createPrivateKey({
2023
- key: Buffer.from(clientSecret, "hex"),
2024
- format: "der",
2025
- type: "pkcs8"
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(clusterDomain, "utf8"),
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(clusterDomain, options.turnPort, options.turnsPort),
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 clusterDomain = normalizeClusterDomain(options.clusterDomain) ?? (projectEndpoint && options.engine ? deriveClusterDomainFromEngine(projectEndpoint, options.engine) : void 0);
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: requireOption(
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 engine = options.engine ?? (options.clusterDomain === void 0 ? await this.client.getEngine().catch(() => void 0) : void 0);
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: options.credentials ?? this.client.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
- turnPort: options.turnPort,
2145
- turnsPort: options.turnsPort
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, import_rstream13.readEnvironment)().engine !== void 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, import_rstream11.getTunnelsProjectEngine)(project, region);
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, import_rstream13.readEnvironment)().engine === void 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, import_rstream11.getTunnelsProjectEngine)(project);
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, import_rstream12.isClientCredentials)(credentials)) {
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 isClientCredentials3 } from "@rstreamlabs/rstream";
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(clusterDomain, turnPort, turnsPort) {
471
+ function createTURNURLs(turnDomain, turnPort, turnsPort) {
471
472
  const normalizedTURNPort = normalizeTURNPort(turnPort);
472
473
  const normalizedTURNSPort = normalizeTURNSPort(turnsPort);
473
474
  return [
474
- `turn:${clusterDomain}:${normalizedTURNPort}?transport=udp`,
475
- `turn:${clusterDomain}:${normalizedTURNPort}?transport=tcp`,
476
- `turns:${clusterDomain}:${normalizedTURNSPort}?transport=udp`,
477
- `turns:${clusterDomain}:${normalizedTURNSPort}?transport=tcp`
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("Failed to load TURN keyring.");
633
+ throw new Error(`TURN keyring request returned HTTP ${response.status}.`);
559
634
  }
560
- return (await response.text()).trim();
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 clusterDomain = normalizeClusterDomain(options.clusterDomain);
593
- if (!clusterDomain) {
594
- throw new Error("Cluster domain is required for TURN PAT mode.");
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 tokenEndpoint = normalizeOptionalString2(options.tokenEndpoint);
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("Token endpoint is required for TURN PAT mode.");
699
+ throw new Error(
700
+ "TURN PAT mode requires a PAT token carrying a token endpoint."
701
+ );
611
702
  }
612
- const claimsTokenEndpoint = claims.token_endpoint ?? claims.tokendpoint;
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(clusterDomain, "utf8"),
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(clusterDomain, options.turnPort, options.turnsPort),
725
+ urls: createTURNURLs(turnDomain, options.turnPort, options.turnsPort),
636
726
  username
637
727
  });
638
728
  }
639
729
  async function createAPPTURNCredentials(options) {
640
- const clusterDomain = normalizeClusterDomain(options.clusterDomain);
641
- if (!clusterDomain) {
642
- throw new Error("Cluster domain is required for TURN APP mode.");
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 = crypto.createPrivateKey({
660
- key: Buffer.from(clientSecret, "hex"),
661
- format: "der",
662
- type: "pkcs8"
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(clusterDomain, "utf8"),
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(clusterDomain, options.turnPort, options.turnsPort),
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 clusterDomain = normalizeClusterDomain(options.clusterDomain) ?? (projectEndpoint && options.engine ? deriveClusterDomainFromEngine(projectEndpoint, options.engine) : void 0);
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: requireOption(
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 engine = options.engine ?? (options.clusterDomain === void 0 ? await this.client.getEngine().catch(() => void 0) : void 0);
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: options.credentials ?? this.client.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
- turnPort: options.turnPort,
782
- turnsPort: options.turnsPort
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 (!isClientCredentials3(credentials)) {
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.0",
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.0",
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"