@rstreamlabs/tunnels 3.8.0 → 3.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -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(),
@@ -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}.`);
1997
+ }
1998
+ if (!response.body) {
1999
+ throw new Error("TURN keyring response body is missing.");
1922
2000
  }
1923
- return (await response.text()).trim();
2001
+ return (await readBoundedTURNKeyring(response.body.getReader())).trim();
1924
2002
  }
1925
2003
  async function createAPITURNCredentials(options) {
1926
2004
  const projectId = normalizeOptionalString2(options.projectId);
@@ -2019,17 +2097,10 @@ async function createAPPTURNCredentials(options) {
2019
2097
  const ttlSeconds = normalizeTTLSeconds(options.ttlSeconds);
2020
2098
  const exp = normalizeNow(options.now) + ttlSeconds;
2021
2099
  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
- });
2100
+ const clientPrivateKey = parseAPPPrivateKey(clientSecret);
2027
2101
  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
- });
2102
+ const serverPublicKey = parseTURNServerPublicKeyHex(serverPublicKeyHex);
2103
+ requireMatchingCurve(clientPrivateKey, serverPublicKey);
2033
2104
  const sharedSecret = import_crypto.default.diffieHellman({
2034
2105
  privateKey: clientPrivateKey,
2035
2106
  publicKey: serverPublicKey
package/dist/index.mjs CHANGED
@@ -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(),
@@ -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}.`);
634
+ }
635
+ if (!response.body) {
636
+ throw new Error("TURN keyring response body is missing.");
559
637
  }
560
- return (await response.text()).trim();
638
+ return (await readBoundedTURNKeyring(response.body.getReader())).trim();
561
639
  }
562
640
  async function createAPITURNCredentials(options) {
563
641
  const projectId = normalizeOptionalString2(options.projectId);
@@ -656,17 +734,10 @@ async function createAPPTURNCredentials(options) {
656
734
  const ttlSeconds = normalizeTTLSeconds(options.ttlSeconds);
657
735
  const exp = normalizeNow(options.now) + ttlSeconds;
658
736
  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
- });
737
+ const clientPrivateKey = parseAPPPrivateKey(clientSecret);
664
738
  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
- });
739
+ const serverPublicKey = parseTURNServerPublicKeyHex(serverPublicKeyHex);
740
+ requireMatchingCurve(clientPrivateKey, serverPublicKey);
670
741
  const sharedSecret = crypto.diffieHellman({
671
742
  privateKey: clientPrivateKey,
672
743
  publicKey: serverPublicKey
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rstreamlabs/tunnels",
3
- "version": "3.8.0",
3
+ "version": "3.8.1",
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": {