@rstreamlabs/tunnels 3.9.0 → 3.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -48,7 +48,7 @@ import {
48
48
  webTTYTransportSchema,
49
49
  webhookEventsSchema,
50
50
  wsEventsSchema
51
- } from "./chunk-3VBF43NB.mjs";
51
+ } from "./chunk-DQN5T7CI.mjs";
52
52
 
53
53
  // src/auth-resource.ts
54
54
  import { createAuthTokenParamsSchema } from "@rstreamlabs/rstream/auth-token";
@@ -230,6 +230,113 @@ var RstreamClientsResource = class {
230
230
 
231
231
  // src/resolution.ts
232
232
  import { DEFAULT_API_URL } from "@rstreamlabs/rstream";
233
+
234
+ // src/http-lifetime.ts
235
+ var requestLifetime = (parents, milliseconds) => {
236
+ const controller = new AbortController();
237
+ const signals = parents.filter((signal) => signal != null);
238
+ const listeners = signals.map((signal) => ({
239
+ signal,
240
+ abort: () => controller.abort(signal.reason)
241
+ }));
242
+ for (const listener of listeners) {
243
+ if (listener.signal.aborted) listener.abort();
244
+ else
245
+ listener.signal.addEventListener("abort", listener.abort, { once: true });
246
+ }
247
+ const timer = setTimeout(
248
+ () => controller.abort(
249
+ new DOMException("Engine request timed out.", "TimeoutError")
250
+ ),
251
+ milliseconds
252
+ );
253
+ return {
254
+ signal: controller.signal,
255
+ stopTimer: () => clearTimeout(timer),
256
+ abort: () => controller.abort(
257
+ new DOMException("Engine request closed.", "AbortError")
258
+ ),
259
+ close: () => {
260
+ clearTimeout(timer);
261
+ for (const listener of listeners)
262
+ listener.signal.removeEventListener("abort", listener.abort);
263
+ }
264
+ };
265
+ };
266
+ var readChunks = async (reader, limit, chunks, size) => {
267
+ const result = await reader.read();
268
+ if (result.done) return chunks;
269
+ const total = size + result.value.byteLength;
270
+ if (total > limit) {
271
+ await reader.cancel();
272
+ throw new Error("HTTP response exceeds the size limit.");
273
+ }
274
+ chunks.push(result.value);
275
+ return readChunks(reader, limit, chunks, total);
276
+ };
277
+ var readBoundedText = async (response, limit) => {
278
+ if (Number(response.headers.get("content-length")) > limit) {
279
+ await response.body?.cancel();
280
+ throw new Error("HTTP response exceeds the size limit.");
281
+ }
282
+ if (!response.body) return "";
283
+ const reader = response.body.getReader();
284
+ const chunks = await readChunks(reader, limit, [], 0).finally(
285
+ () => reader.releaseLock()
286
+ );
287
+ const bytes = new Uint8Array(
288
+ chunks.reduce((size, chunk) => size + chunk.byteLength, 0)
289
+ );
290
+ chunks.reduce((offset, chunk) => {
291
+ bytes.set(chunk, offset);
292
+ return offset + chunk.byteLength;
293
+ }, 0);
294
+ return new TextDecoder().decode(bytes);
295
+ };
296
+ var waitWithSignal = async (promise, signal) => {
297
+ if (!signal) return promise;
298
+ if (signal.aborted) {
299
+ void promise.catch(() => void 0);
300
+ signal.throwIfAborted();
301
+ }
302
+ return new Promise((resolve, reject) => {
303
+ const abort = () => reject(signal.reason);
304
+ signal.addEventListener("abort", abort, { once: true });
305
+ void promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
306
+ });
307
+ };
308
+
309
+ // src/control-plane-fetch.ts
310
+ function createBoundedControlPlaneFetch(timeoutMilliseconds, parent, fetch2 = globalThis.fetch, maxResponseBytes = 16384) {
311
+ return async (input, init) => {
312
+ const request = new Request(input, init);
313
+ const headers = new Headers(request.headers);
314
+ headers.set("Connection", "close");
315
+ const lifetime = requestLifetime(
316
+ [parent, request.signal],
317
+ timeoutMilliseconds
318
+ );
319
+ try {
320
+ lifetime.signal.throwIfAborted();
321
+ const response = await fetch2(input, {
322
+ ...init,
323
+ redirect: "manual",
324
+ headers,
325
+ signal: lifetime.signal
326
+ });
327
+ const responseBody = response.body === null ? null : await readBoundedText(response, maxResponseBytes);
328
+ return new Response(responseBody, {
329
+ headers: Object.fromEntries(response.headers.entries()),
330
+ status: response.status,
331
+ statusText: response.statusText
332
+ });
333
+ } finally {
334
+ lifetime.close();
335
+ }
336
+ };
337
+ }
338
+
339
+ // src/resolution.ts
233
340
  import { getTunnelsProjectEngine } from "@rstreamlabs/rstream";
234
341
  import { isTokenCredentials } from "@rstreamlabs/rstream";
235
342
  import { readEnvironment } from "@rstreamlabs/rstream";
@@ -363,6 +470,12 @@ async function resolveManagedTunnelsProject(config) {
363
470
  );
364
471
  }
365
472
  const controlPlane = new RstreamClient({
473
+ fetch: createBoundedControlPlaneFetch(
474
+ 1e4,
475
+ void 0,
476
+ globalThis.fetch,
477
+ 1024 * 1024
478
+ ),
366
479
  apiUrl: resolveTunnelsAPIURL(config.apiUrl),
367
480
  controlPlaneHeaders: config.controlPlaneHeaders,
368
481
  credentials
@@ -404,61 +517,230 @@ var RstreamTunnelsResource = class {
404
517
 
405
518
  // src/tunnels.ts
406
519
  import { getTunnelsProjectEngine as getTunnelsProjectEngine2 } from "@rstreamlabs/rstream";
407
- import { isClientCredentials as isClientCredentials4 } from "@rstreamlabs/rstream";
408
- import { readEnvironment as readEnvironment2 } from "@rstreamlabs/rstream";
409
-
410
- // src/turn.ts
411
- import { isClientCredentials as isClientCredentials2 } from "@rstreamlabs/rstream";
412
- import { isTokenCredentials as isTokenCredentials2 } from "@rstreamlabs/rstream";
413
- import { RstreamClient as RstreamClient2 } from "@rstreamlabs/rstream";
414
- import { turnCredentialsSchema } from "@rstreamlabs/rstream/turn";
520
+ import { createClientCredentialsToken as createClientCredentialsToken3 } from "@rstreamlabs/rstream";
415
521
 
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);
522
+ // src/engine-http.ts
523
+ import { RstreamHTTPError } from "@rstreamlabs/rstream";
524
+ import { z } from "zod";
525
+ var discoverySchema = z.object({
526
+ version: z.literal(1),
527
+ projectEndpoint: z.string(),
528
+ mtlsApiUrl: z.string(),
529
+ capabilities: z.array(z.string()).max(32)
530
+ });
531
+ var hasClientCertificate = (tls) => tls?.cert !== void 0 || tls?.key !== void 0;
532
+ var validateMTLSURL = (target, source) => {
533
+ const url = new URL(target);
534
+ if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash || url.pathname !== "/api" || url.hostname.split(".")[0] !== source.hostname.split(".")[0])
535
+ throw new Error("Invalid discovered mTLS Engine API URL.");
536
+ normalizeEngineAddress(url.host);
537
+ return url.href;
538
+ };
539
+ var discoveryCacheMilliseconds = (response) => {
540
+ const cache = response.headers.get("cache-control") ?? "";
541
+ if (/\bno-(?:store|cache)\b/i.test(cache)) return 0;
542
+ const seconds = cache.match(/\bmax-age\s*=\s*"?(\d+)/i)?.[1];
543
+ return Math.min(seconds === void 0 ? 3600 : Number(seconds), 86400) * 1e3;
544
+ };
545
+ var EngineHTTP = class {
546
+ constructor(options) {
547
+ this.options = options;
548
+ if (hasClientCertificate(options.tls) && (!options.tls?.cert || !options.tls?.key))
549
+ throw new Error("mTLS requires both a certificate and its private key.");
550
+ }
551
+ options;
552
+ closed = new AbortController();
553
+ authenticated;
554
+ anonymous;
555
+ discovery;
556
+ flight;
557
+ get usesMTLS() {
558
+ return hasClientCertificate(this.options.tls);
559
+ }
560
+ async close() {
561
+ this.closed.abort(new Error("Engine HTTP client is closed."));
562
+ this.discovery = void 0;
563
+ await Promise.all([
564
+ this.authenticated?.then((client) => client.close()),
565
+ this.anonymous?.then((client) => client.close())
566
+ ]);
567
+ }
568
+ async transport(anonymous) {
569
+ this.closed.signal.throwIfAborted();
570
+ if (!anonymous && this.options.fetch) return this.options.fetch;
571
+ if (!this.options.tls) return globalThis.fetch;
572
+ if (anonymous) {
573
+ this.anonymous ??= import("./node-engine-http-6DJZZEME.mjs").then(
574
+ ({ NodeEngineHTTP }) => new NodeEngineHTTP({
575
+ ca: this.options.tls?.ca,
576
+ rejectUnauthorized: true
577
+ })
578
+ );
579
+ return (await this.anonymous).fetch;
580
+ }
581
+ this.authenticated ??= import("./node-engine-http-6DJZZEME.mjs").then(
582
+ ({ NodeEngineHTTP }) => new NodeEngineHTTP(this.options.tls ?? {})
583
+ );
584
+ return (await this.authenticated).fetch;
585
+ }
586
+ async apiURL(engine, signal) {
587
+ this.closed.signal.throwIfAborted();
588
+ signal?.throwIfAborted();
589
+ const source = new URL(`https://${normalizeEngineAddress(engine)}`);
590
+ if (!this.usesMTLS) return `https://${normalizeEngineAddress(engine)}/api`;
591
+ if (this.options.mtlsApiUrl)
592
+ return validateMTLSURL(this.options.mtlsApiUrl, source);
593
+ if (this.discovery?.source === source.origin && this.discovery.expiresAt > Date.now())
594
+ return this.discovery.url;
595
+ const flight = this.flight?.source === source.origin ? this.flight : { source: source.origin, promise: this.discover(source) };
596
+ this.flight = flight;
597
+ try {
598
+ const discovery = await waitWithSignal(flight.promise, signal);
599
+ this.discovery = discovery;
600
+ return discovery.url;
601
+ } finally {
602
+ void flight.promise.then(
603
+ () => {
604
+ if (this.flight === flight) this.flight = void 0;
605
+ },
606
+ () => {
607
+ if (this.flight === flight) this.flight = void 0;
608
+ }
609
+ );
610
+ }
611
+ }
612
+ async discover(source) {
613
+ const lifetime = requestLifetime([this.closed.signal], 5e3);
614
+ try {
615
+ const fetch2 = await this.transport(true);
616
+ const response = await fetch2(
617
+ new URL("/.well-known/rstream/engine", source),
618
+ {
619
+ method: "GET",
620
+ credentials: "omit",
621
+ redirect: "manual",
622
+ signal: lifetime.signal
623
+ }
624
+ );
625
+ if (!response.ok) {
626
+ await response.body?.cancel();
627
+ throw new RstreamHTTPError(
628
+ response.status,
629
+ "mTLS Engine discovery failed."
630
+ );
631
+ }
632
+ const text = await readBoundedText(response, 16384);
633
+ const value = JSON.parse(text);
634
+ const document = discoverySchema.parse(value);
635
+ if (document.projectEndpoint !== source.hostname.split(".")[0] || !document.capabilities.includes("engine-api-mtls"))
636
+ throw new Error(
637
+ "Engine discovery does not advertise mTLS for this project."
638
+ );
639
+ return {
640
+ source: source.origin,
641
+ url: validateMTLSURL(document.mtlsApiUrl, source),
642
+ expiresAt: Date.now() + discoveryCacheMilliseconds(response)
643
+ };
644
+ } finally {
645
+ lifetime.close();
646
+ }
647
+ }
648
+ async eventStream(engine, options) {
649
+ const lifetime = requestLifetime(
650
+ [this.closed.signal, options.signal],
651
+ 1e4
652
+ );
443
653
  try {
444
- const response = await globalThis.fetch(request, {
654
+ const base = await this.apiURL(engine, lifetime.signal);
655
+ const headers = new Headers(options.headers);
656
+ if (this.usesMTLS && headers.has("authorization"))
657
+ throw new Error("Use a token or mTLS, not both.");
658
+ headers.set("Accept", "text/event-stream");
659
+ const fetch2 = await this.transport(false);
660
+ const response = await fetch2(`${base}/sse`, {
661
+ method: "GET",
445
662
  headers,
663
+ credentials: "omit",
664
+ redirect: "manual",
446
665
  signal: lifetime.signal
447
666
  });
448
- const responseBody = response.body === null ? null : new Uint8Array(await response.arrayBuffer());
449
- return new Response(responseBody, {
450
- headers: Object.fromEntries(response.headers.entries()),
667
+ if (!response.ok)
668
+ throw new RstreamHTTPError(
669
+ response.status,
670
+ await readBoundedText(response, 16384)
671
+ );
672
+ if (!response.headers.get("content-type")?.startsWith("text/event-stream"))
673
+ throw new Error("Engine returned an invalid event stream.");
674
+ lifetime.stopTimer();
675
+ return {
676
+ response,
677
+ close: () => {
678
+ lifetime.abort();
679
+ lifetime.close();
680
+ }
681
+ };
682
+ } catch (error) {
683
+ lifetime.abort();
684
+ lifetime.close();
685
+ if (!(error instanceof RstreamHTTPError)) this.discovery = void 0;
686
+ throw error;
687
+ }
688
+ }
689
+ async response(engine, path, options = {}, maxBytes = 16 * 1024 * 1024) {
690
+ if (!path.startsWith("/") || path.startsWith("//") || path.includes("\\"))
691
+ throw new Error("Engine request path must be a relative absolute path.");
692
+ const lifetime = requestLifetime(
693
+ [this.closed.signal, options.signal],
694
+ 1e4
695
+ );
696
+ try {
697
+ const base = await this.apiURL(engine, lifetime.signal);
698
+ const url = new URL(`${base}${path}`);
699
+ if (url.origin !== new URL(base).origin || !url.pathname.startsWith("/api/") || url.username || url.password)
700
+ throw new Error(
701
+ "Engine request path must remain inside the Engine API."
702
+ );
703
+ const headers = new Headers(options.headers);
704
+ if (this.usesMTLS && (headers.has("authorization") || url.searchParams.has("rstream.token")))
705
+ throw new Error("Use a token or mTLS, not both.");
706
+ const fetch2 = await this.transport(false);
707
+ const response = await fetch2(`${base}${path}`, {
708
+ ...options,
709
+ headers,
710
+ credentials: "omit",
711
+ redirect: "manual",
712
+ signal: lifetime.signal
713
+ });
714
+ const text = await readBoundedText(
715
+ response,
716
+ response.ok ? maxBytes : 16384
717
+ );
718
+ if (!response.ok) throw new RstreamHTTPError(response.status, text);
719
+ return new Response([204, 205].includes(response.status) ? null : text, {
451
720
  status: response.status,
452
- statusText: response.statusText
721
+ headers: response.headers
453
722
  });
723
+ } catch (error) {
724
+ if (!(error instanceof RstreamHTTPError)) this.discovery = void 0;
725
+ throw error;
454
726
  } finally {
455
727
  lifetime.close();
456
728
  }
457
- };
458
- }
729
+ }
730
+ };
731
+
732
+ // src/tunnels.ts
733
+ import { isClientCredentials as isClientCredentials3 } from "@rstreamlabs/rstream";
734
+ import { readEnvironment as readEnvironment2 } from "@rstreamlabs/rstream";
459
735
 
460
736
  // src/turn.ts
461
- import * as z from "zod";
737
+ import { isClientCredentials as isClientCredentials2 } from "@rstreamlabs/rstream";
738
+ import { createClientCredentialsToken as createClientCredentialsToken2 } from "@rstreamlabs/rstream";
739
+ import { isTokenCredentials as isTokenCredentials2 } from "@rstreamlabs/rstream";
740
+ import { RstreamClient as RstreamClient2 } from "@rstreamlabs/rstream";
741
+ import { RstreamHTTPError as RstreamHTTPError2 } from "@rstreamlabs/rstream";
742
+ import { turnCredentialsSchema } from "@rstreamlabs/rstream/turn";
743
+ import * as z2 from "zod";
462
744
  import crypto from "crypto";
463
745
  var defaultTURNPort = 3478;
464
746
  var defaultTURNSPort = 5349;
@@ -467,20 +749,31 @@ var maxTURNCredentialTTLSeconds = 60 * 60;
467
749
  var maxTURNKeyringBytes = 8 * 1024;
468
750
  var defaultAPIRequestTimeoutMilliseconds = 1e4;
469
751
  var maxAPIRequestTimeoutMilliseconds = 6e4;
470
- var turnTokenClaimsSchema = z.object({
471
- exp: z.number().int().optional(),
472
- iat: z.number().int().optional(),
473
- token_endpoint: z.string().regex(/^[0-9a-f]{8}$/i).optional(),
474
- tokendpoint: z.string().regex(/^[0-9a-f]{8}$/i).optional(),
475
- type: z.enum(["app", "auth", "pat"])
752
+ var turnTokenClaimsSchema = z2.object({
753
+ permissions: z2.array(z2.string()).nullable().optional(),
754
+ exp: z2.number().int().optional(),
755
+ iat: z2.number().int().optional(),
756
+ token_endpoint: z2.string().regex(/^[A-Za-z0-9_.-]{1,128}$/).optional(),
757
+ tokendpoint: z2.string().regex(/^[A-Za-z0-9_.-]{1,128}$/).optional(),
758
+ type: z2.enum(["app", "auth", "pat"])
476
759
  });
477
- var turnCredentialModeSchema = z.enum(["api", "app", "pat"]);
760
+ var turnCredentialModeSchema = z2.enum([
761
+ "auto",
762
+ "api",
763
+ "engine",
764
+ "app",
765
+ "pat"
766
+ ]);
478
767
  function normalizeOptionalString2(value) {
479
768
  const normalized = value?.trim();
480
769
  return normalized ? normalized : void 0;
481
770
  }
482
771
  function normalizeClusterDomain(clusterDomain) {
483
772
  const normalized = normalizeOptionalString2(clusterDomain);
773
+ if (normalized && (normalized.length > 253 || !normalized.split(".").every(
774
+ (label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i.test(label)
775
+ )))
776
+ throw new Error("TURN domain and realm must be valid DNS names.");
484
777
  return normalized?.toLowerCase();
485
778
  }
486
779
  function normalizeNow(now) {
@@ -595,14 +888,27 @@ function requireOption(value, message) {
595
888
  }
596
889
  throw new Error(message);
597
890
  }
598
- function resolveMode(credentials, mode) {
599
- if (mode !== void 0) {
600
- return turnCredentialModeSchema.parse(mode);
601
- }
602
- if (isClientCredentials2(credentials)) {
603
- return "app";
891
+ function automaticPATClaims(token) {
892
+ try {
893
+ return parseTURNTokenClaims(token);
894
+ } catch {
895
+ return void 0;
604
896
  }
605
- return "api";
897
+ }
898
+ function resolveMode(options) {
899
+ const requested = turnCredentialModeSchema.parse(options.mode ?? "auto");
900
+ if (requested !== "auto") return requested;
901
+ if (options.client?.usesMTLS || hasClientCertificate(options.tls))
902
+ return "engine";
903
+ const claims = isTokenCredentials2(options.credentials) ? automaticPATClaims(options.credentials.token) : void 0;
904
+ if (claims?.token_endpoint && claims.tokendpoint && claims.token_endpoint !== claims.tokendpoint)
905
+ throw new Error("Conflicting PAT token endpoints.");
906
+ const localTarget = options.projectEndpoint && (options.clusterDomain || options.turnDomain && options.turnRealm);
907
+ if (!localTarget) return "auto";
908
+ if (isClientCredentials2(options.credentials)) return "app";
909
+ if (claims?.type === "pat" && claims.exp !== void 0 && (claims.token_endpoint || claims.tokendpoint) && (claims.permissions === null || claims.permissions?.includes("turn.relay.allocate")))
910
+ return "pat";
911
+ return "auto";
606
912
  }
607
913
  async function readBoundedTURNKeyring(reader, chunks = [], size = 0) {
608
914
  const result = await reader.read();
@@ -622,7 +928,7 @@ async function readBoundedTURNKeyring(reader, chunks = [], size = 0) {
622
928
  }
623
929
  function parseTURNServerPublicKeyHex(value) {
624
930
  const normalized = value.trim();
625
- if (normalized.length === 0 || normalized.length % 2 !== 0 || !/^[0-9a-f]+$/i.test(normalized)) {
931
+ if (normalized.length === 0 || normalized.length > maxTURNKeyringBytes || normalized.length % 2 !== 0 || !/^[0-9a-f]+$/i.test(normalized)) {
626
932
  throw new Error("TURN keyring must contain hexadecimal DER key material.");
627
933
  }
628
934
  try {
@@ -645,6 +951,10 @@ function parseTURNServerPublicKeyHex(value) {
645
951
  }
646
952
  }
647
953
  function parseAPPPrivateKey(value) {
954
+ if (value.length > 16384 || value.length % 2 !== 0 || !/^[0-9a-f]+$/i.test(value))
955
+ throw new Error(
956
+ "TURN APP client secret must contain hexadecimal DER key material."
957
+ );
648
958
  try {
649
959
  const key = crypto.createPrivateKey({
650
960
  key: Buffer.from(value, "hex"),
@@ -680,19 +990,38 @@ async function loadTURNServerPublicKeyHex(clusterDomain, options) {
680
990
  `/keyrings/turn/${clusterDomain}.spki.der.hex`,
681
991
  keyringBaseURL
682
992
  );
683
- const response = await fetch(url, { redirect: "manual" });
684
- if (response.status >= 300 && response.status < 400) {
993
+ if (url.protocol !== "https:" || url.username || url.password)
685
994
  throw new Error(
686
- "TURN keyring request was redirected; redirects are refused."
995
+ "TURN keyring requests require HTTPS without URL credentials."
687
996
  );
997
+ const lifetime = requestLifetime(
998
+ [options.signal],
999
+ normalizeAPIRequestTimeoutMilliseconds(options.requestTimeoutMilliseconds)
1000
+ );
1001
+ try {
1002
+ const response = await fetch(url, {
1003
+ redirect: "manual",
1004
+ credentials: "omit",
1005
+ signal: lifetime.signal
1006
+ });
1007
+ if (response.status >= 300 && response.status < 400) {
1008
+ await response.body?.cancel();
1009
+ throw new Error(
1010
+ "TURN keyring request was redirected; redirects are refused."
1011
+ );
1012
+ }
1013
+ if (!response.ok) {
1014
+ await response.body?.cancel();
1015
+ throw new Error(`TURN keyring request returned HTTP ${response.status}.`);
1016
+ }
1017
+ if (!response.body) {
1018
+ throw new Error("TURN keyring response body is missing.");
1019
+ }
1020
+ const reader = response.body.getReader();
1021
+ return (await readBoundedTURNKeyring(reader).finally(() => reader.releaseLock())).trim();
1022
+ } finally {
1023
+ lifetime.close();
688
1024
  }
689
- if (!response.ok) {
690
- throw new Error(`TURN keyring request returned HTTP ${response.status}.`);
691
- }
692
- if (!response.body) {
693
- throw new Error("TURN keyring response body is missing.");
694
- }
695
- return (await readBoundedTURNKeyring(response.body.getReader())).trim();
696
1025
  }
697
1026
  async function createAPITURNCredentials(options) {
698
1027
  const projectId = normalizeOptionalString2(options.projectId);
@@ -706,10 +1035,20 @@ async function createAPITURNCredentials(options) {
706
1035
  options.requestTimeoutMilliseconds
707
1036
  );
708
1037
  const params = createAPITURNCredentialsParams(options.ttlSeconds);
709
- const request = options.fetch ?? createBoundedControlPlaneFetch(requestTimeoutMilliseconds);
1038
+ const request = createBoundedControlPlaneFetch(
1039
+ requestTimeoutMilliseconds,
1040
+ options.signal,
1041
+ options.fetch
1042
+ );
710
1043
  const client = new RstreamClient2({
1044
+ controlPlaneHeaders: options.controlPlaneHeaders,
711
1045
  apiUrl: options.apiUrl,
712
- credentials: options.credentials,
1046
+ credentials: isClientCredentials2(options.credentials) ? {
1047
+ token: createClientCredentialsToken2(options.credentials, {
1048
+ expiresInSeconds: 60,
1049
+ claims: { permissions: ["turn.credentials.create"] }
1050
+ }).token
1051
+ } : options.credentials,
713
1052
  fetch: request
714
1053
  });
715
1054
  if (projectId) {
@@ -753,6 +1092,8 @@ function createPATTURNCredentials(options) {
753
1092
  const claimsTokenEndpoint = normalizeOptionalString2(
754
1093
  claims.token_endpoint ?? claims.tokendpoint
755
1094
  );
1095
+ if (claims.token_endpoint && claims.tokendpoint && claims.token_endpoint !== claims.tokendpoint)
1096
+ throw new Error("Conflicting PAT token endpoints.");
756
1097
  const configuredTokenEndpoint = normalizeOptionalString2(
757
1098
  options.tokenEndpoint
758
1099
  );
@@ -762,6 +1103,8 @@ function createPATTURNCredentials(options) {
762
1103
  "TURN PAT mode requires a PAT token carrying a token endpoint."
763
1104
  );
764
1105
  }
1106
+ requireTURNReference(projectEndpoint);
1107
+ requireTURNReference(tokenEndpoint);
765
1108
  if (configuredTokenEndpoint !== void 0 && claimsTokenEndpoint !== void 0 && claimsTokenEndpoint !== configuredTokenEndpoint) {
766
1109
  throw new Error("Token endpoint does not match the PAT token.");
767
1110
  }
@@ -806,6 +1149,8 @@ async function createAPPTURNCredentials(options) {
806
1149
  if (!clientId) {
807
1150
  throw new Error("Client ID is required for TURN APP mode.");
808
1151
  }
1152
+ requireTURNReference(projectEndpoint);
1153
+ requireTURNReference(clientId);
809
1154
  const clientSecret = normalizeOptionalString2(options.clientSecret);
810
1155
  if (!clientSecret) {
811
1156
  throw new Error("Client secret is required for TURN APP mode.");
@@ -838,26 +1183,41 @@ async function createAPPTURNCredentials(options) {
838
1183
  username
839
1184
  });
840
1185
  }
841
- async function createTURNCredentials(options) {
1186
+ async function createTURNCredentialsForLifetime(options) {
842
1187
  const credentials = options.credentials;
843
- const mode = resolveMode(credentials, options.mode);
1188
+ const mode = resolveMode(options);
844
1189
  const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
845
- if (mode === "api") {
1190
+ if (mode === "engine") return createEngineTURNCredentials(options);
1191
+ if (options.client?.usesMTLS || hasClientCertificate(options.tls))
1192
+ throw new Error(
1193
+ "Certificate authentication requires automatic or Engine TURN mode."
1194
+ );
1195
+ if (mode === "api" || mode === "auto") {
846
1196
  const controlPlaneCredentials = options.controlPlaneCredentials ?? credentials;
847
1197
  if (!controlPlaneCredentials) {
1198
+ if (mode === "auto" && (options.client || options.engine))
1199
+ return createEngineTURNCredentials(options);
848
1200
  throw new Error(
849
1201
  "Control-plane credentials are required for TURN API mode."
850
1202
  );
851
1203
  }
852
- return await createAPITURNCredentials({
853
- apiUrl: options.apiUrl,
854
- credentials: controlPlaneCredentials,
855
- fetch: options.fetch,
856
- projectEndpoint,
857
- projectId: options.projectId,
858
- requestTimeoutMilliseconds: options.requestTimeoutMilliseconds,
859
- ttlSeconds: options.ttlSeconds
860
- });
1204
+ try {
1205
+ return await createAPITURNCredentials({
1206
+ controlPlaneHeaders: options.controlPlaneHeaders,
1207
+ apiUrl: options.apiUrl,
1208
+ credentials: controlPlaneCredentials,
1209
+ fetch: options.fetch,
1210
+ projectEndpoint,
1211
+ projectId: options.projectId,
1212
+ requestTimeoutMilliseconds: mode === "auto" ? Math.min(options.requestTimeoutMilliseconds ?? 1e4, 5e3) : options.requestTimeoutMilliseconds,
1213
+ signal: options.signal,
1214
+ ttlSeconds: options.ttlSeconds
1215
+ });
1216
+ } catch (error) {
1217
+ if (mode !== "auto" || !projectEndpoint || !(options.client?.configuredEngine || options.engine) || !turnEngineFallbackAllowed(error, options.signal))
1218
+ throw error;
1219
+ return createEngineTURNCredentials(options);
1220
+ }
861
1221
  }
862
1222
  const legacyDomain = normalizeClusterDomain(options.clusterDomain);
863
1223
  const turnDomain = normalizeClusterDomain(options.turnDomain) ?? legacyDomain ?? (projectEndpoint && options.engine ? deriveClusterDomainFromEngine(projectEndpoint, options.engine) : void 0);
@@ -891,6 +1251,8 @@ async function createTURNCredentials(options) {
891
1251
  throw new Error("Client credentials are required for TURN APP mode.");
892
1252
  }
893
1253
  return await createAPPTURNCredentials({
1254
+ signal: options.signal,
1255
+ requestTimeoutMilliseconds: options.requestTimeoutMilliseconds,
894
1256
  apiUrl: options.apiUrl,
895
1257
  clientId: credentials.clientId,
896
1258
  clientSecret: credentials.clientSecret,
@@ -914,25 +1276,139 @@ async function createTURNCredentials(options) {
914
1276
  turnsPort: options.turnsPort
915
1277
  });
916
1278
  }
1279
+ function requireTURNReference(value) {
1280
+ if (!/^[A-Za-z0-9_.-]{1,128}$/.test(value) || value === "-")
1281
+ throw new Error("Invalid TURN project or credential reference.");
1282
+ }
1283
+ function turnEngineFallbackAllowed(error, signal) {
1284
+ if (signal?.aborted) return false;
1285
+ if (error instanceof RstreamHTTPError2)
1286
+ return [502, 503, 504].includes(error.status);
1287
+ if (error instanceof DOMException && error.name === "TimeoutError")
1288
+ return true;
1289
+ if (typeof error !== "object" || error === null) return false;
1290
+ if ("code" in error && typeof error.code === "string")
1291
+ return [
1292
+ "ENOTFOUND",
1293
+ "EAI_AGAIN",
1294
+ "ECONNREFUSED",
1295
+ "ECONNRESET",
1296
+ "ETIMEDOUT",
1297
+ "EHOSTUNREACH",
1298
+ "ENETUNREACH",
1299
+ "UND_ERR_CONNECT_TIMEOUT",
1300
+ "UND_ERR_HEADERS_TIMEOUT",
1301
+ "UND_ERR_SOCKET"
1302
+ ].includes(error.code);
1303
+ return "cause" in error && error.cause !== error ? turnEngineFallbackAllowed(error.cause, signal) : false;
1304
+ }
1305
+ async function createEngineTURNCredentials(options) {
1306
+ const client = options.client ?? new RstreamTunnelsClient({
1307
+ credentials: options.credentials,
1308
+ tls: options.tls,
1309
+ mtlsApiUrl: options.mtlsApiUrl,
1310
+ engine: options.engine,
1311
+ projectEndpoint: options.projectEndpoint,
1312
+ projectId: options.projectId
1313
+ });
1314
+ try {
1315
+ const engine = await client.getEngine();
1316
+ const endpoint = new URL(`https://${engine}`).hostname.split(".")[0];
1317
+ if (options.projectEndpoint && options.projectEndpoint !== endpoint || options.projectId && !options.projectEndpoint)
1318
+ throw new Error("TURN project endpoint must match the Engine client.");
1319
+ if (options.engine && options.engine !== engine)
1320
+ throw new Error("TURN Engine must match the reusable client.");
1321
+ const value = await client.request(
1322
+ "/turn-server/credentials",
1323
+ {
1324
+ method: "POST",
1325
+ signal: options.signal,
1326
+ headers: { "Content-Type": "application/json" },
1327
+ body: JSON.stringify(
1328
+ createAPITURNCredentialsParams(options.ttlSeconds)
1329
+ )
1330
+ },
1331
+ 16384
1332
+ );
1333
+ return turnCredentialsSchema.parse(value);
1334
+ } finally {
1335
+ if (!options.client) await client.close();
1336
+ }
1337
+ }
1338
+ function validateTURNCredentialsRequest(options) {
1339
+ normalizeTTLSeconds(options.ttlSeconds);
1340
+ turnCredentialModeSchema.parse(options.mode ?? "auto");
1341
+ return normalizeAPIRequestTimeoutMilliseconds(
1342
+ options.requestTimeoutMilliseconds
1343
+ );
1344
+ }
1345
+ async function createTURNCredentials(options) {
1346
+ const lifetime = requestLifetime(
1347
+ [options.signal],
1348
+ validateTURNCredentialsRequest(options)
1349
+ );
1350
+ try {
1351
+ lifetime.signal.throwIfAborted();
1352
+ if ((options.client?.usesMTLS || hasClientCertificate(options.tls)) && options.credentials)
1353
+ throw new Error("Use a token or mTLS, not both.");
1354
+ if (options.client && options.credentials && !sameTURNCredentials(options.client.credentials, options.credentials))
1355
+ throw new Error(
1356
+ "TURN credentials must match the reusable Engine client."
1357
+ );
1358
+ return await waitWithSignal(
1359
+ createTURNCredentialsForLifetime({
1360
+ ...options,
1361
+ credentials: options.credentials ?? options.client?.credentials,
1362
+ signal: lifetime.signal
1363
+ }),
1364
+ lifetime.signal
1365
+ );
1366
+ } finally {
1367
+ lifetime.close();
1368
+ }
1369
+ }
1370
+ function sameTURNCredentials(left, right) {
1371
+ if (isTokenCredentials2(left) && isTokenCredentials2(right))
1372
+ return left.token === right.token;
1373
+ return isClientCredentials2(left) && isClientCredentials2(right) && left.clientId === right.clientId && left.clientSecret === right.clientSecret;
1374
+ }
917
1375
 
918
1376
  // src/turn-resource.ts
919
- import { isClientCredentials as isClientCredentials3 } from "@rstreamlabs/rstream";
920
1377
  var RstreamTURNResource = class {
921
1378
  client;
922
1379
  constructor(client) {
923
1380
  this.client = client;
924
1381
  }
925
1382
  async createCredentials(options = {}) {
1383
+ const lifetime = requestLifetime(
1384
+ [options.signal],
1385
+ validateTURNCredentialsRequest(options)
1386
+ );
1387
+ try {
1388
+ lifetime.signal.throwIfAborted();
1389
+ return await waitWithSignal(
1390
+ this.createForLifetime({ ...options, signal: lifetime.signal }),
1391
+ lifetime.signal
1392
+ );
1393
+ } finally {
1394
+ lifetime.close();
1395
+ }
1396
+ }
1397
+ async createForLifetime(options) {
926
1398
  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);
1399
+ const localMode = options.mode === "app" || options.mode === "pat";
1400
+ const target = localMode && !this.client.usesMTLS && (options.turnDomain === void 0 || options.turnRealm === void 0) && options.clusterDomain === void 0 ? await this.client.getTURNTarget() : void 0;
930
1401
  return await createTURNCredentials({
1402
+ client: this.client,
1403
+ signal: options.signal,
931
1404
  apiUrl: options.apiUrl ?? this.client.apiUrl,
1405
+ fetch: options.fetch,
1406
+ requestTimeoutMilliseconds: options.requestTimeoutMilliseconds,
932
1407
  clusterDomain: options.clusterDomain,
1408
+ controlPlaneHeaders: options.controlPlaneHeaders ?? this.client.controlPlaneHeaders,
933
1409
  controlPlaneCredentials: options.controlPlaneCredentials ?? this.client.controlPlaneCredentials,
934
1410
  credentials,
935
- engine,
1411
+ engine: options.engine,
936
1412
  keyringBaseUrl: options.keyringBaseUrl,
937
1413
  mode: options.mode,
938
1414
  now: options.now,
@@ -1101,24 +1577,45 @@ var engineAuthTokenScopes = {
1101
1577
  var RstreamTunnelsClient = class {
1102
1578
  config;
1103
1579
  managedProject;
1580
+ http;
1104
1581
  constructor(config) {
1105
1582
  this.config = config;
1583
+ if (hasClientCertificate(config?.tls) && config?.credentials)
1584
+ throw new Error("Use a token or mTLS, not both.");
1585
+ this.http = new EngineHTTP(config ?? {});
1106
1586
  }
1107
1587
  get apiUrl() {
1108
1588
  return resolveTunnelsAPIURL(this.config?.apiUrl);
1109
1589
  }
1110
1590
  get credentials() {
1591
+ if (this.http.usesMTLS) return void 0;
1111
1592
  return resolveTunnelsCredentials(this.config?.credentials);
1112
1593
  }
1113
1594
  get controlPlaneCredentials() {
1595
+ if (this.http.usesMTLS) return void 0;
1114
1596
  return resolveControlPlaneCredentials(
1115
1597
  this.config?.controlPlaneCredentials,
1116
1598
  this.credentials
1117
1599
  );
1118
1600
  }
1601
+ get controlPlaneHeaders() {
1602
+ return this.config?.controlPlaneHeaders;
1603
+ }
1119
1604
  get projectEndpoint() {
1120
1605
  return this.config?.projectEndpoint;
1121
1606
  }
1607
+ get configuredEngine() {
1608
+ return this.config?.engine ?? readEnvironment2().engine;
1609
+ }
1610
+ get usesMTLS() {
1611
+ return this.http.usesMTLS;
1612
+ }
1613
+ async close() {
1614
+ await this.http.close();
1615
+ }
1616
+ async getEngineAPIURL(signal) {
1617
+ return this.http.apiURL(await this.getEngine(), signal);
1618
+ }
1122
1619
  async getEngine() {
1123
1620
  const region = resolveTunnelsRegion(this.config?.region);
1124
1621
  const projectEndpoint = normalizeOptionalString3(
@@ -1188,6 +1685,10 @@ var RstreamTunnelsClient = class {
1188
1685
  };
1189
1686
  }
1190
1687
  async getManagedProject() {
1688
+ if (this.usesMTLS)
1689
+ throw new Error(
1690
+ "mTLS project resolution requires the configured Engine address; the Control plane does not accept certificates."
1691
+ );
1191
1692
  const projectEndpoint = normalizeOptionalString3(
1192
1693
  this.config?.projectEndpoint
1193
1694
  );
@@ -1263,7 +1764,7 @@ var RstreamTunnelsClient = class {
1263
1764
  if (!credentials) {
1264
1765
  return void 0;
1265
1766
  }
1266
- if (!isClientCredentials4(credentials)) {
1767
+ if (!isClientCredentials3(credentials)) {
1267
1768
  return credentials.token;
1268
1769
  }
1269
1770
  const tokenParams = await this.getEngineAuthTokenParams();
@@ -1272,26 +1773,37 @@ var RstreamTunnelsClient = class {
1272
1773
  engine
1273
1774
  })).token;
1274
1775
  }
1275
- async request(path, options) {
1776
+ async openEventStream(signal) {
1777
+ const engine = await this.getEngine();
1778
+ const token = await this.getToken(engine);
1779
+ return this.http.eventStream(engine, {
1780
+ signal,
1781
+ headers: token ? { Authorization: `Bearer ${token}` } : void 0
1782
+ });
1783
+ }
1784
+ async request(path, options, maxResponseBytes) {
1276
1785
  const engine = normalizeEngineAddress(await this.getEngine());
1277
1786
  if (!path.startsWith("/") || path.startsWith("//")) {
1278
1787
  throw new Error("Engine request path must be a relative absolute path.");
1279
1788
  }
1280
- const url = `https://${engine}/api${path}`;
1281
1789
  const headers = new Headers(options?.headers || {});
1282
- const token = await this.getToken(engine);
1790
+ const credentials = this.credentials;
1791
+ const token = path === "/turn-server/credentials" && isClientCredentials3(credentials) ? createClientCredentialsToken3(credentials, {
1792
+ expiresInSeconds: 60,
1793
+ claims: { permissions: ["turn.credentials.create"] }
1794
+ }).token : await this.getToken(engine);
1283
1795
  if (token) {
1284
1796
  headers.set("Authorization", `Bearer ${token}`);
1285
1797
  }
1286
- const response = await fetch(url, {
1287
- ...options,
1288
- headers,
1289
- redirect: options?.redirect ?? "manual"
1290
- });
1291
- if (!response.ok) {
1292
- const errorText = await response.text();
1293
- throw new Error(`HTTP error ${response.status}: ${errorText}`);
1294
- }
1798
+ const response = await this.http.response(
1799
+ engine,
1800
+ path,
1801
+ {
1802
+ ...options,
1803
+ headers
1804
+ },
1805
+ maxResponseBytes
1806
+ );
1295
1807
  return await response.json();
1296
1808
  }
1297
1809
  };
@@ -1388,6 +1900,8 @@ function hasWatchOnlyPermissions(permissions) {
1388
1900
  );
1389
1901
  }
1390
1902
  var Watch = class {
1903
+ abort = new AbortController();
1904
+ closeStream;
1391
1905
  connection = null;
1392
1906
  connectionState = "preparing";
1393
1907
  config;
@@ -1402,7 +1916,32 @@ var Watch = class {
1402
1916
  }
1403
1917
  this.connectionState = "connecting";
1404
1918
  try {
1919
+ if (this.config.client?.usesMTLS) {
1920
+ if (this.config.auth !== void 0)
1921
+ throw new Error("Watch: Use a token or mTLS, not both.");
1922
+ if (normalizeTransport(this.config.transport) !== "sse")
1923
+ throw new Error(
1924
+ "Watch: Node.js certificate authentication currently requires SSE."
1925
+ );
1926
+ const stream = await this.config.client.openEventStream(
1927
+ this.abort.signal
1928
+ );
1929
+ if (this.abort.signal.aborted) {
1930
+ stream.close();
1931
+ this.abort.signal.throwIfAborted();
1932
+ }
1933
+ this.closeStream = stream.close;
1934
+ if (!stream.response.body)
1935
+ throw new Error("Watch: Engine event stream is empty.");
1936
+ this.connectionState = "connected";
1937
+ this.events.onConnect?.();
1938
+ void this.readCertificateEvents(stream.response.body).finally(
1939
+ () => this.disconnect()
1940
+ );
1941
+ return;
1942
+ }
1405
1943
  const token = typeof this.config.auth === "function" ? await this.config.auth() : this.config.auth;
1944
+ if (!token) throw new Error("Watch: Authentication is required.");
1406
1945
  validateWatchQueryToken(token);
1407
1946
  const engine = await resolveTunnelsEngine({
1408
1947
  apiUrl: this.config.apiUrl,
@@ -1441,10 +1980,39 @@ var Watch = class {
1441
1980
  };
1442
1981
  }
1443
1982
  } catch (error) {
1444
- this.connectionState = "preparing";
1983
+ this.closeStream?.();
1984
+ this.closeStream = void 0;
1985
+ this.connectionState = this.abort.signal.aborted ? "closed" : "preparing";
1445
1986
  throw error;
1446
1987
  }
1447
1988
  }
1989
+ async readCertificateEvents(body) {
1990
+ const reader = body.getReader();
1991
+ const decoder = new TextDecoder();
1992
+ const state = { buffered: "" };
1993
+ try {
1994
+ while (!this.abort.signal.aborted) {
1995
+ const chunk = await reader.read();
1996
+ if (chunk.done) return;
1997
+ state.buffered += decoder.decode(chunk.value, { stream: true });
1998
+ const frames = state.buffered.split(/\r?\n\r?\n/);
1999
+ state.buffered = frames.pop() ?? "";
2000
+ if (state.buffered.length > 1024 * 1024)
2001
+ throw new Error("Watch event exceeds the size limit.");
2002
+ for (const frame of frames) {
2003
+ if (frame.length > 1024 * 1024)
2004
+ throw new Error("Watch event exceeds the size limit.");
2005
+ const data = frame.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).replace(/^ /, "")).join("\n");
2006
+ if (data) this.handleMessage(data);
2007
+ if (this.abort.signal.aborted) return;
2008
+ }
2009
+ }
2010
+ } catch {
2011
+ this.disconnect();
2012
+ } finally {
2013
+ reader.releaseLock();
2014
+ }
2015
+ }
1448
2016
  handleMessage(data) {
1449
2017
  try {
1450
2018
  const parsed = wsEventsSchema.parse(JSON.parse(data));
@@ -1454,6 +2022,9 @@ var Watch = class {
1454
2022
  }
1455
2023
  }
1456
2024
  disconnect() {
2025
+ this.abort.abort();
2026
+ this.closeStream?.();
2027
+ this.closeStream = void 0;
1457
2028
  if (this.connection) {
1458
2029
  this.connection.onerror = null;
1459
2030
  this.connection.onmessage = null;
@@ -1469,8 +2040,8 @@ var Watch = class {
1469
2040
  };
1470
2041
 
1471
2042
  // src/webtty.ts
1472
- import * as z2 from "zod";
1473
- var osFamilySchema = z2.enum([
2043
+ import * as z3 from "zod";
2044
+ var osFamilySchema = z3.enum([
1474
2045
  "linux",
1475
2046
  "macos",
1476
2047
  "windows",
@@ -1478,55 +2049,55 @@ var osFamilySchema = z2.enum([
1478
2049
  "openbsd",
1479
2050
  "freebsd"
1480
2051
  ]);
1481
- var webttyCapabilitySchema = z2.enum(["exec", "fs"]);
1482
- var webttyFSModeSchema = z2.enum(["read-write", "read-only"]);
1483
- var webttyEncryptionPolicySchema = z2.enum([
2052
+ var webttyCapabilitySchema = z3.enum(["exec", "fs"]);
2053
+ var webttyFSModeSchema = z3.enum(["read-write", "read-only"]);
2054
+ var webttyEncryptionPolicySchema = z3.enum([
1484
2055
  "disabled",
1485
2056
  "explicit_key",
1486
2057
  "workspace_managed"
1487
2058
  ]);
1488
- var webttyTransportSchema = z2.enum([
2059
+ var webttyTransportSchema = z3.enum([
1489
2060
  "plain",
1490
2061
  "websocket",
1491
2062
  "webtransport"
1492
2063
  ]);
1493
- var webttyServerSchema = z2.object({
1494
- tunnel_id: z2.string(),
1495
- tunnel_name: z2.string().optional(),
1496
- target: z2.string().optional(),
1497
- rstream_url: z2.string().optional(),
1498
- status: z2.string().optional(),
1499
- publish: z2.boolean().optional(),
1500
- workspace_id: z2.string().optional(),
1501
- project_id: z2.string().optional(),
1502
- tunnel_protocol: z2.enum(["http", "webtty"]).optional(),
1503
- tunnel_type: z2.enum(["bytestream", "datagram"]).optional(),
1504
- http_version: z2.string().optional(),
2064
+ var webttyServerSchema = z3.object({
2065
+ tunnel_id: z3.string(),
2066
+ tunnel_name: z3.string().optional(),
2067
+ target: z3.string().optional(),
2068
+ rstream_url: z3.string().optional(),
2069
+ status: z3.string().optional(),
2070
+ publish: z3.boolean().optional(),
2071
+ workspace_id: z3.string().optional(),
2072
+ project_id: z3.string().optional(),
2073
+ tunnel_protocol: z3.enum(["http", "webtty"]).optional(),
2074
+ tunnel_type: z3.enum(["bytestream", "datagram"]).optional(),
2075
+ http_version: z3.string().optional(),
1505
2076
  transport: webttyTransportSchema.optional(),
1506
- transport_error: z2.string().optional(),
1507
- managed: z2.boolean(),
1508
- host: z2.string(),
1509
- token_auth: z2.boolean(),
1510
- server_id: z2.string().optional(),
1511
- server_name: z2.string().optional(),
1512
- host_key_id: z2.string().optional(),
1513
- e2e: z2.enum(["disabled", "required"]).optional(),
1514
- client_proof: z2.enum(["none", "required"]).optional(),
2077
+ transport_error: z3.string().optional(),
2078
+ managed: z3.boolean(),
2079
+ host: z3.string(),
2080
+ token_auth: z3.boolean(),
2081
+ server_id: z3.string().optional(),
2082
+ server_name: z3.string().optional(),
2083
+ host_key_id: z3.string().optional(),
2084
+ e2e: z3.enum(["disabled", "required"]).optional(),
2085
+ client_proof: z3.enum(["none", "required"]).optional(),
1515
2086
  encryption_policy: webttyEncryptionPolicySchema.optional(),
1516
- capabilities: z2.array(webttyCapabilitySchema).optional(),
1517
- exec_path: z2.string().optional(),
1518
- fs_path: z2.string().optional(),
2087
+ capabilities: z3.array(webttyCapabilitySchema).optional(),
2088
+ exec_path: z3.string().optional(),
2089
+ fs_path: z3.string().optional(),
1519
2090
  fs_mode: webttyFSModeSchema.optional(),
1520
- fs_backend: z2.enum(["webdav", "webrtc"]).optional(),
2091
+ fs_backend: z3.enum(["webdav", "webrtc"]).optional(),
1521
2092
  os_family: osFamilySchema.optional(),
1522
- arch: z2.string().optional(),
1523
- os_id: z2.string().optional(),
1524
- os_version_id: z2.string().optional(),
1525
- os_version_codename: z2.string().optional(),
1526
- os_pretty_name: z2.string().optional(),
1527
- kernel_release: z2.string().optional(),
1528
- hostname: z2.string().optional(),
1529
- labels: z2.record(z2.string(), z2.string().optional()).optional()
2093
+ arch: z3.string().optional(),
2094
+ os_id: z3.string().optional(),
2095
+ os_version_id: z3.string().optional(),
2096
+ os_version_codename: z3.string().optional(),
2097
+ os_pretty_name: z3.string().optional(),
2098
+ kernel_release: z3.string().optional(),
2099
+ hostname: z3.string().optional(),
2100
+ labels: z3.record(z3.string(), z3.string().optional()).optional()
1530
2101
  });
1531
2102
  function isWebTTYCapability(value) {
1532
2103
  return webttyCapabilitySchema.safeParse(value).success;
@@ -1724,6 +2295,7 @@ export {
1724
2295
  streamWebTTYMetadataSchema,
1725
2296
  tunnelFilterSchema,
1726
2297
  turnCredentialModeSchema,
2298
+ validateTURNCredentialsRequest,
1727
2299
  webTTYCapabilitiesSchema,
1728
2300
  webTTYContextMetadataSchema,
1729
2301
  webTTYControlRequestSchema,