@rstreamlabs/tunnels 3.8.1 → 3.8.3

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.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
@@ -412,6 +412,52 @@ import { isClientCredentials as isClientCredentials2 } from "@rstreamlabs/rstrea
412
412
  import { isTokenCredentials as isTokenCredentials2 } from "@rstreamlabs/rstream";
413
413
  import { RstreamClient as RstreamClient2 } from "@rstreamlabs/rstream";
414
414
  import { turnCredentialsSchema } from "@rstreamlabs/rstream/turn";
415
+
416
+ // src/control-plane-fetch.ts
417
+ function createRequestLifetime(parent, timeoutMilliseconds) {
418
+ const controller = new AbortController();
419
+ const abort = () => controller.abort(parent.reason);
420
+ if (parent.aborted) {
421
+ abort();
422
+ } else {
423
+ parent.addEventListener("abort", abort, { once: true });
424
+ }
425
+ const timeout = setTimeout(
426
+ () => controller.abort(new Error("Control plane request timed out.")),
427
+ timeoutMilliseconds
428
+ );
429
+ return {
430
+ close: () => {
431
+ clearTimeout(timeout);
432
+ parent.removeEventListener("abort", abort);
433
+ },
434
+ signal: controller.signal
435
+ };
436
+ }
437
+ function createBoundedControlPlaneFetch(timeoutMilliseconds) {
438
+ return async (input, init) => {
439
+ const request = new Request(input, init);
440
+ const headers = new Headers(request.headers);
441
+ headers.set("Connection", "close");
442
+ const lifetime = createRequestLifetime(request.signal, timeoutMilliseconds);
443
+ try {
444
+ const response = await globalThis.fetch(request, {
445
+ headers,
446
+ signal: lifetime.signal
447
+ });
448
+ const responseBody = response.body === null ? null : new Uint8Array(await response.arrayBuffer());
449
+ return new Response(responseBody, {
450
+ headers: Object.fromEntries(response.headers.entries()),
451
+ status: response.status,
452
+ statusText: response.statusText
453
+ });
454
+ } finally {
455
+ lifetime.close();
456
+ }
457
+ };
458
+ }
459
+
460
+ // src/turn.ts
415
461
  import * as z from "zod";
416
462
  import crypto from "crypto";
417
463
  var defaultTURNPort = 3478;
@@ -419,6 +465,8 @@ var defaultTURNSPort = 5349;
419
465
  var defaultTURNCredentialTTLSeconds = 10 * 60;
420
466
  var maxTURNCredentialTTLSeconds = 60 * 60;
421
467
  var maxTURNKeyringBytes = 8 * 1024;
468
+ var defaultAPIRequestTimeoutMilliseconds = 1e4;
469
+ var maxAPIRequestTimeoutMilliseconds = 6e4;
422
470
  var turnTokenClaimsSchema = z.object({
423
471
  exp: z.number().int().optional(),
424
472
  iat: z.number().int().optional(),
@@ -452,6 +500,15 @@ function normalizeTTLSeconds(ttlSeconds) {
452
500
  }
453
501
  return ttl;
454
502
  }
503
+ function normalizeAPIRequestTimeoutMilliseconds(timeoutMilliseconds) {
504
+ const timeout = timeoutMilliseconds ?? defaultAPIRequestTimeoutMilliseconds;
505
+ if (!Number.isInteger(timeout) || timeout < 1 || timeout > maxAPIRequestTimeoutMilliseconds) {
506
+ throw new Error(
507
+ "TURN API request timeout must be an integer between 1 and 60000 milliseconds."
508
+ );
509
+ }
510
+ return timeout;
511
+ }
455
512
  function createAPITURNCredentialsParams(ttlSeconds) {
456
513
  return ttlSeconds === void 0 ? {} : { ttlSeconds: normalizeTTLSeconds(ttlSeconds) };
457
514
  }
@@ -468,14 +525,14 @@ function normalizeTURNPort(port) {
468
525
  function normalizeTURNSPort(port) {
469
526
  return normalizePort(port, defaultTURNSPort, "TURNS port");
470
527
  }
471
- function createTURNURLs(clusterDomain, turnPort, turnsPort) {
528
+ function createTURNURLs(turnDomain, turnPort, turnsPort) {
472
529
  const normalizedTURNPort = normalizeTURNPort(turnPort);
473
530
  const normalizedTURNSPort = normalizeTURNSPort(turnsPort);
474
531
  return [
475
- `turn:${clusterDomain}:${normalizedTURNPort}?transport=udp`,
476
- `turn:${clusterDomain}:${normalizedTURNPort}?transport=tcp`,
477
- `turns:${clusterDomain}:${normalizedTURNSPort}?transport=udp`,
478
- `turns:${clusterDomain}:${normalizedTURNSPort}?transport=tcp`
532
+ `turn:${turnDomain}:${normalizedTURNPort}?transport=udp`,
533
+ `turn:${turnDomain}:${normalizedTURNPort}?transport=tcp`,
534
+ `turns:${turnDomain}:${normalizedTURNSPort}?transport=udp`,
535
+ `turns:${turnDomain}:${normalizedTURNSPort}?transport=tcp`
479
536
  ];
480
537
  }
481
538
  function parseTURNTokenClaims(token) {
@@ -645,11 +702,16 @@ async function createAPITURNCredentials(options) {
645
702
  "Project ID or project endpoint is required for TURN API mode."
646
703
  );
647
704
  }
705
+ const requestTimeoutMilliseconds = normalizeAPIRequestTimeoutMilliseconds(
706
+ options.requestTimeoutMilliseconds
707
+ );
708
+ const params = createAPITURNCredentialsParams(options.ttlSeconds);
709
+ const request = options.fetch ?? createBoundedControlPlaneFetch(requestTimeoutMilliseconds);
648
710
  const client = new RstreamClient2({
649
711
  apiUrl: options.apiUrl,
650
- credentials: options.credentials
712
+ credentials: options.credentials,
713
+ fetch: request
651
714
  });
652
- const params = createAPITURNCredentialsParams(options.ttlSeconds);
653
715
  if (projectId) {
654
716
  return await client.tunnels.projects.createTurnCredentials(
655
717
  projectId,
@@ -667,9 +729,14 @@ async function createAPITURNCredentials(options) {
667
729
  );
668
730
  }
669
731
  function createPATTURNCredentials(options) {
670
- const clusterDomain = normalizeClusterDomain(options.clusterDomain);
671
- if (!clusterDomain) {
672
- throw new Error("Cluster domain is required for TURN PAT mode.");
732
+ const legacyDomain = normalizeClusterDomain(options.clusterDomain);
733
+ const turnDomain = normalizeClusterDomain(options.turnDomain) ?? legacyDomain;
734
+ const turnRealm = normalizeClusterDomain(options.turnRealm) ?? legacyDomain;
735
+ if (!turnDomain) {
736
+ throw new Error("TURN domain is required for TURN PAT mode.");
737
+ }
738
+ if (!turnRealm) {
739
+ throw new Error("TURN realm is required for TURN PAT mode.");
673
740
  }
674
741
  const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
675
742
  if (!projectEndpoint) {
@@ -683,12 +750,19 @@ function createPATTURNCredentials(options) {
683
750
  if (claims.type !== "pat") {
684
751
  throw new Error("TURN PAT mode requires a PAT token.");
685
752
  }
686
- const tokenEndpoint = normalizeOptionalString2(options.tokenEndpoint);
753
+ const claimsTokenEndpoint = normalizeOptionalString2(
754
+ claims.token_endpoint ?? claims.tokendpoint
755
+ );
756
+ const configuredTokenEndpoint = normalizeOptionalString2(
757
+ options.tokenEndpoint
758
+ );
759
+ const tokenEndpoint = configuredTokenEndpoint ?? claimsTokenEndpoint;
687
760
  if (!tokenEndpoint) {
688
- throw new Error("Token endpoint is required for TURN PAT mode.");
761
+ throw new Error(
762
+ "TURN PAT mode requires a PAT token carrying a token endpoint."
763
+ );
689
764
  }
690
- const claimsTokenEndpoint = claims.token_endpoint ?? claims.tokendpoint;
691
- if (claimsTokenEndpoint !== void 0 && claimsTokenEndpoint !== tokenEndpoint) {
765
+ if (configuredTokenEndpoint !== void 0 && claimsTokenEndpoint !== void 0 && claimsTokenEndpoint !== configuredTokenEndpoint) {
692
766
  throw new Error("Token endpoint does not match the PAT token.");
693
767
  }
694
768
  const now = normalizeNow(options.now);
@@ -701,7 +775,7 @@ function createPATTURNCredentials(options) {
701
775
  crypto.hkdfSync(
702
776
  "sha256",
703
777
  tokenHash,
704
- Buffer.from(clusterDomain, "utf8"),
778
+ Buffer.from(turnRealm, "utf8"),
705
779
  Buffer.from("turn-pat-v1", "utf8"),
706
780
  32
707
781
  )
@@ -710,14 +784,19 @@ function createPATTURNCredentials(options) {
710
784
  return turnCredentialsSchema.parse({
711
785
  credential,
712
786
  ttl,
713
- urls: createTURNURLs(clusterDomain, options.turnPort, options.turnsPort),
787
+ urls: createTURNURLs(turnDomain, options.turnPort, options.turnsPort),
714
788
  username
715
789
  });
716
790
  }
717
791
  async function createAPPTURNCredentials(options) {
718
- const clusterDomain = normalizeClusterDomain(options.clusterDomain);
719
- if (!clusterDomain) {
720
- throw new Error("Cluster domain is required for TURN APP mode.");
792
+ const legacyDomain = normalizeClusterDomain(options.clusterDomain);
793
+ const turnDomain = normalizeClusterDomain(options.turnDomain) ?? legacyDomain;
794
+ const turnRealm = normalizeClusterDomain(options.turnRealm) ?? legacyDomain;
795
+ if (!turnDomain) {
796
+ throw new Error("TURN domain is required for TURN APP mode.");
797
+ }
798
+ if (!turnRealm) {
799
+ throw new Error("TURN realm is required for TURN APP mode.");
721
800
  }
722
801
  const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
723
802
  if (!projectEndpoint) {
@@ -735,7 +814,7 @@ async function createAPPTURNCredentials(options) {
735
814
  const exp = normalizeNow(options.now) + ttlSeconds;
736
815
  const username = `v1:${exp}:app:${projectEndpoint}:${clientId}`;
737
816
  const clientPrivateKey = parseAPPPrivateKey(clientSecret);
738
- const serverPublicKeyHex = normalizeOptionalString2(options.serverPublicKeyHex) ?? await loadTURNServerPublicKeyHex(clusterDomain, options);
817
+ const serverPublicKeyHex = normalizeOptionalString2(options.serverPublicKeyHex) ?? await loadTURNServerPublicKeyHex(turnRealm, options);
739
818
  const serverPublicKey = parseTURNServerPublicKeyHex(serverPublicKeyHex);
740
819
  requireMatchingCurve(clientPrivateKey, serverPublicKey);
741
820
  const sharedSecret = crypto.diffieHellman({
@@ -746,7 +825,7 @@ async function createAPPTURNCredentials(options) {
746
825
  crypto.hkdfSync(
747
826
  "sha256",
748
827
  sharedSecret,
749
- Buffer.from(clusterDomain, "utf8"),
828
+ Buffer.from(turnRealm, "utf8"),
750
829
  Buffer.from("turn-app-v1", "utf8"),
751
830
  32
752
831
  )
@@ -755,7 +834,7 @@ async function createAPPTURNCredentials(options) {
755
834
  return turnCredentialsSchema.parse({
756
835
  credential,
757
836
  ttl: ttlSeconds,
758
- urls: createTURNURLs(clusterDomain, options.turnPort, options.turnsPort),
837
+ urls: createTURNURLs(turnDomain, options.turnPort, options.turnsPort),
759
838
  username
760
839
  });
761
840
  }
@@ -773,33 +852,38 @@ async function createTURNCredentials(options) {
773
852
  return await createAPITURNCredentials({
774
853
  apiUrl: options.apiUrl,
775
854
  credentials: controlPlaneCredentials,
855
+ fetch: options.fetch,
776
856
  projectEndpoint,
777
857
  projectId: options.projectId,
858
+ requestTimeoutMilliseconds: options.requestTimeoutMilliseconds,
778
859
  ttlSeconds: options.ttlSeconds
779
860
  });
780
861
  }
781
- const clusterDomain = normalizeClusterDomain(options.clusterDomain) ?? (projectEndpoint && options.engine ? deriveClusterDomainFromEngine(projectEndpoint, options.engine) : void 0);
862
+ const legacyDomain = normalizeClusterDomain(options.clusterDomain);
863
+ const turnDomain = normalizeClusterDomain(options.turnDomain) ?? legacyDomain ?? (projectEndpoint && options.engine ? deriveClusterDomainFromEngine(projectEndpoint, options.engine) : void 0);
864
+ const turnRealm = normalizeClusterDomain(options.turnRealm) ?? legacyDomain;
782
865
  if (mode === "pat") {
783
866
  if (!isTokenCredentials2(credentials)) {
784
867
  throw new Error("A PAT token is required for TURN PAT mode.");
785
868
  }
786
869
  return createPATTURNCredentials({
787
- clusterDomain: requireOption(
788
- clusterDomain,
789
- "Cluster domain is required for TURN PAT mode."
790
- ),
791
870
  now: options.now,
792
871
  projectEndpoint: requireOption(
793
872
  projectEndpoint,
794
873
  "Project endpoint is required for TURN PAT mode."
795
874
  ),
796
875
  token: credentials.token,
797
- tokenEndpoint: requireOption(
798
- normalizeOptionalString2(options.tokenEndpoint),
799
- "Token endpoint is required for TURN PAT mode."
800
- ),
876
+ tokenEndpoint: options.tokenEndpoint,
801
877
  ttlSeconds: options.ttlSeconds,
878
+ turnDomain: requireOption(
879
+ turnDomain,
880
+ "TURN domain is required for TURN PAT mode."
881
+ ),
802
882
  turnPort: options.turnPort,
883
+ turnRealm: requireOption(
884
+ turnRealm,
885
+ "TURN realm is required for TURN PAT mode."
886
+ ),
803
887
  turnsPort: options.turnsPort
804
888
  });
805
889
  }
@@ -810,10 +894,6 @@ async function createTURNCredentials(options) {
810
894
  apiUrl: options.apiUrl,
811
895
  clientId: credentials.clientId,
812
896
  clientSecret: credentials.clientSecret,
813
- clusterDomain: requireOption(
814
- clusterDomain,
815
- "Cluster domain is required for TURN APP mode."
816
- ),
817
897
  keyringBaseUrl: options.keyringBaseUrl,
818
898
  now: options.now,
819
899
  projectEndpoint: requireOption(
@@ -822,24 +902,36 @@ async function createTURNCredentials(options) {
822
902
  ),
823
903
  serverPublicKeyHex: options.serverPublicKeyHex,
824
904
  ttlSeconds: options.ttlSeconds,
905
+ turnDomain: requireOption(
906
+ turnDomain,
907
+ "TURN domain is required for TURN APP mode."
908
+ ),
825
909
  turnPort: options.turnPort,
910
+ turnRealm: requireOption(
911
+ turnRealm,
912
+ "TURN realm is required for TURN APP mode."
913
+ ),
826
914
  turnsPort: options.turnsPort
827
915
  });
828
916
  }
829
917
 
830
918
  // src/turn-resource.ts
919
+ import { isClientCredentials as isClientCredentials3 } from "@rstreamlabs/rstream";
831
920
  var RstreamTURNResource = class {
832
921
  client;
833
922
  constructor(client) {
834
923
  this.client = client;
835
924
  }
836
925
  async createCredentials(options = {}) {
837
- const engine = options.engine ?? (options.clusterDomain === void 0 ? await this.client.getEngine().catch(() => void 0) : void 0);
926
+ const credentials = options.credentials ?? this.client.credentials;
927
+ const localMode = options.mode === "app" || options.mode === "pat" || options.mode === void 0 && isClientCredentials3(credentials);
928
+ const target = localMode && (options.turnDomain === void 0 || options.turnRealm === void 0) && options.clusterDomain === void 0 ? await this.client.getTURNTarget() : void 0;
929
+ const engine = options.engine ?? (options.clusterDomain === void 0 && options.turnDomain === void 0 && target === void 0 ? await this.client.getEngine().catch(() => void 0) : void 0);
838
930
  return await createTURNCredentials({
839
931
  apiUrl: options.apiUrl ?? this.client.apiUrl,
840
932
  clusterDomain: options.clusterDomain,
841
933
  controlPlaneCredentials: options.controlPlaneCredentials ?? this.client.controlPlaneCredentials,
842
- credentials: options.credentials ?? this.client.credentials,
934
+ credentials,
843
935
  engine,
844
936
  keyringBaseUrl: options.keyringBaseUrl,
845
937
  mode: options.mode,
@@ -849,8 +941,10 @@ var RstreamTURNResource = class {
849
941
  serverPublicKeyHex: options.serverPublicKeyHex,
850
942
  tokenEndpoint: options.tokenEndpoint,
851
943
  ttlSeconds: options.ttlSeconds,
852
- turnPort: options.turnPort,
853
- turnsPort: options.turnsPort
944
+ turnDomain: options.turnDomain ?? target?.turnDomain,
945
+ turnPort: options.turnPort ?? target?.turnPort,
946
+ turnRealm: options.turnRealm ?? target?.turnRealm,
947
+ turnsPort: options.turnsPort ?? target?.turnsPort
854
948
  });
855
949
  }
856
950
  };
@@ -1072,6 +1166,27 @@ var RstreamTunnelsClient = class {
1072
1166
  region: this.config?.region
1073
1167
  });
1074
1168
  }
1169
+ async getTURNTarget() {
1170
+ const project = await this.getManagedProject();
1171
+ if (project === void 0) {
1172
+ throw new Error(
1173
+ "Managed project endpoint is required to resolve the TURN target."
1174
+ );
1175
+ }
1176
+ const turnDomain = project.turnDomain?.trim() || project.domain.trim();
1177
+ const turnRealm = project.turnRealm?.trim();
1178
+ if (!turnRealm) {
1179
+ throw new Error(
1180
+ "Managed project metadata does not expose the TURN realm; use API credential mode or provide turnRealm explicitly."
1181
+ );
1182
+ }
1183
+ return {
1184
+ turnDomain,
1185
+ turnPort: project.turnPort,
1186
+ turnRealm,
1187
+ turnsPort: project.turnsPort
1188
+ };
1189
+ }
1075
1190
  async getManagedProject() {
1076
1191
  const projectEndpoint = normalizeOptionalString3(
1077
1192
  this.config?.projectEndpoint
@@ -1148,7 +1263,7 @@ var RstreamTunnelsClient = class {
1148
1263
  if (!credentials) {
1149
1264
  return void 0;
1150
1265
  }
1151
- if (!isClientCredentials3(credentials)) {
1266
+ if (!isClientCredentials4(credentials)) {
1152
1267
  return credentials.token;
1153
1268
  }
1154
1269
  const tokenParams = await this.getEngineAuthTokenParams();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rstreamlabs/tunnels",
3
- "version": "3.8.1",
3
+ "version": "3.8.3",
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.2",
51
51
  "@types/jsonwebtoken": "^9.0.10",
52
52
  "jsonwebtoken": "^9.0.3",
53
53
  "zod": "^4.4.3"