@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/README.md +46 -0
- package/dist/{chunk-3VBF43NB.mjs → chunk-DQN5T7CI.mjs} +207 -9
- package/dist/index.d.mts +154 -117
- package/dist/index.d.ts +154 -117
- package/dist/index.js +1045 -156
- package/dist/index.mjs +713 -141
- package/dist/node-engine-http-6DJZZEME.mjs +102 -0
- package/dist/openapi.d.mts +260 -10
- package/dist/openapi.d.ts +260 -10
- package/dist/openapi.js +207 -9
- package/dist/openapi.mjs +1 -1
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -48,7 +48,7 @@ import {
|
|
|
48
48
|
webTTYTransportSchema,
|
|
49
49
|
webhookEventsSchema,
|
|
50
50
|
wsEventsSchema
|
|
51
|
-
} from "./chunk-
|
|
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 {
|
|
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/
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
)
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
|
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
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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 =
|
|
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
|
|
599
|
-
|
|
600
|
-
return
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
return "app";
|
|
891
|
+
function automaticPATClaims(token) {
|
|
892
|
+
try {
|
|
893
|
+
return parseTURNTokenClaims(token);
|
|
894
|
+
} catch {
|
|
895
|
+
return void 0;
|
|
604
896
|
}
|
|
605
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
1186
|
+
async function createTURNCredentialsForLifetime(options) {
|
|
842
1187
|
const credentials = options.credentials;
|
|
843
|
-
const mode = resolveMode(
|
|
1188
|
+
const mode = resolveMode(options);
|
|
844
1189
|
const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
|
|
845
|
-
if (mode === "
|
|
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
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
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"
|
|
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 (!
|
|
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
|
|
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
|
|
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
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
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.
|
|
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
|
|
1473
|
-
var osFamilySchema =
|
|
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 =
|
|
1482
|
-
var webttyFSModeSchema =
|
|
1483
|
-
var webttyEncryptionPolicySchema =
|
|
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 =
|
|
2059
|
+
var webttyTransportSchema = z3.enum([
|
|
1489
2060
|
"plain",
|
|
1490
2061
|
"websocket",
|
|
1491
2062
|
"webtransport"
|
|
1492
2063
|
]);
|
|
1493
|
-
var webttyServerSchema =
|
|
1494
|
-
tunnel_id:
|
|
1495
|
-
tunnel_name:
|
|
1496
|
-
target:
|
|
1497
|
-
rstream_url:
|
|
1498
|
-
status:
|
|
1499
|
-
publish:
|
|
1500
|
-
workspace_id:
|
|
1501
|
-
project_id:
|
|
1502
|
-
tunnel_protocol:
|
|
1503
|
-
tunnel_type:
|
|
1504
|
-
http_version:
|
|
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:
|
|
1507
|
-
managed:
|
|
1508
|
-
host:
|
|
1509
|
-
token_auth:
|
|
1510
|
-
server_id:
|
|
1511
|
-
server_name:
|
|
1512
|
-
host_key_id:
|
|
1513
|
-
e2e:
|
|
1514
|
-
client_proof:
|
|
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:
|
|
1517
|
-
exec_path:
|
|
1518
|
-
fs_path:
|
|
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:
|
|
2091
|
+
fs_backend: z3.enum(["webdav", "webrtc"]).optional(),
|
|
1521
2092
|
os_family: osFamilySchema.optional(),
|
|
1522
|
-
arch:
|
|
1523
|
-
os_id:
|
|
1524
|
-
os_version_id:
|
|
1525
|
-
os_version_codename:
|
|
1526
|
-
os_pretty_name:
|
|
1527
|
-
kernel_release:
|
|
1528
|
-
hostname:
|
|
1529
|
-
labels:
|
|
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,
|