@rstreamlabs/tunnels 1.0.7 → 1.0.9

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
@@ -16,7 +16,7 @@ import {
16
16
  tunnelSchema,
17
17
  webhookEventsSchema,
18
18
  wsEventsSchema
19
- } from "./chunk-OPJKEDVS.mjs";
19
+ } from "./chunk-TVDZNVSF.mjs";
20
20
 
21
21
  // src/auth-resource.ts
22
22
  import { createClientCredentialsToken } from "@rstreamlabs/rstream";
@@ -25,9 +25,9 @@ import { createClientCredentialsToken } from "@rstreamlabs/rstream";
25
25
  import * as z from "zod";
26
26
  var StringFilter = z.union([
27
27
  z.string(),
28
- z.object({ exact: z.string() }),
29
- z.object({ oneof: z.array(z.string()) }),
30
- z.object({ regex: z.string() })
28
+ z.object({ exact: z.string() }).strict(),
29
+ z.object({ oneof: z.array(z.string()) }).strict(),
30
+ z.object({ regex: z.string() }).strict()
31
31
  ]);
32
32
  function transform(field) {
33
33
  if (field instanceof z.ZodOptional) {
@@ -61,7 +61,7 @@ function select(base) {
61
61
  return [key, z.boolean().optional()];
62
62
  });
63
63
  const shape = Object.fromEntries(entries);
64
- return z.object(shape);
64
+ return z.object(shape).strict();
65
65
  }
66
66
 
67
67
  // src/auth.ts
@@ -88,7 +88,7 @@ var authTokenTunnelsScopesSchema = z2.object({
88
88
  creation_date: true
89
89
  })
90
90
  ).optional()
91
- })
91
+ }).strict()
92
92
  ]).optional(),
93
93
  // Scopes for connecting to tunnels
94
94
  connect: z2.union([
@@ -100,7 +100,7 @@ var authTokenTunnelsScopesSchema = z2.object({
100
100
  path: z2.string().optional()
101
101
  })
102
102
  ).optional()
103
- })
103
+ }).strict()
104
104
  ]).optional(),
105
105
  // Scopes for listing tunnels
106
106
  list: z2.union([
@@ -108,14 +108,29 @@ var authTokenTunnelsScopesSchema = z2.object({
108
108
  z2.object({
109
109
  filters: filters(tunnelSchema).optional(),
110
110
  select: select(tunnelSchema).optional()
111
- })
111
+ }).strict()
112
112
  ]).optional()
113
+ }).strict().superRefine((scopes, ctx) => {
114
+ if (scopes.create === void 0 && scopes.connect === void 0 && scopes.list === void 0) {
115
+ ctx.addIssue({
116
+ code: z2.ZodIssueCode.custom,
117
+ message: "At least one tunnel scope action is required."
118
+ });
119
+ }
113
120
  });
114
121
  var authTokenScopesSchema = z2.object({
115
122
  tunnels: authTokenTunnelsScopesSchema.optional()
116
123
  // Scopes related to tunnels
124
+ }).strict().superRefine((scopes, ctx) => {
125
+ if (scopes.tunnels === void 0) {
126
+ ctx.addIssue({
127
+ code: z2.ZodIssueCode.custom,
128
+ message: "Tunnel scopes are required.",
129
+ path: ["tunnels"]
130
+ });
131
+ }
117
132
  });
118
- var authTokenTunnelGrantSchema = z2.object({
133
+ var authTokenTunnelGrantLeafSchema = z2.object({
119
134
  workspaces: z2.array(nonEmptyStringSchema).min(1).optional(),
120
135
  projects: z2.array(nonEmptyStringSchema).min(1).optional(),
121
136
  scopes: authTokenScopesSchema.optional()
@@ -127,11 +142,25 @@ var authTokenTunnelGrantSchema = z2.object({
127
142
  path: ["projects"]
128
143
  });
129
144
  }
145
+ if (grant.workspaces === void 0 && grant.projects === void 0 && grant.scopes === void 0) {
146
+ ctx.addIssue({
147
+ code: z2.ZodIssueCode.custom,
148
+ message: "A grant must target workspaces, projects, or scopes.",
149
+ path: ["workspaces"]
150
+ });
151
+ }
130
152
  });
153
+ var authTokenTunnelGrantSchema = z2.lazy(
154
+ () => z2.union([
155
+ authTokenTunnelGrantLeafSchema,
156
+ z2.object({ AND: z2.array(authTokenTunnelGrantSchema).min(1) }).strict(),
157
+ z2.object({ OR: z2.array(authTokenTunnelGrantSchema).min(1) }).strict()
158
+ ])
159
+ );
131
160
  var authTokenCommonSchema = {
132
161
  aud: z2.union([z2.string(), z2.array(z2.string())]).optional(),
133
162
  exp: z2.number().optional(),
134
- tunnelsGrants: z2.array(authTokenTunnelGrantSchema).optional(),
163
+ tunnelsGrants: authTokenTunnelGrantSchema.optional(),
135
164
  iat: z2.number().optional(),
136
165
  iss: z2.string().optional(),
137
166
  jti: z2.string().optional(),
@@ -150,6 +179,7 @@ var authTokenSchema = z2.union([
150
179
  }).strict(),
151
180
  z2.object({
152
181
  type: z2.literal("pat"),
182
+ tokendpoint: z2.string().optional(),
153
183
  token_endpoint: z2.string().optional(),
154
184
  ...authTokenCommonSchema
155
185
  }).strict(),
@@ -162,10 +192,9 @@ var authTokenSchema = z2.union([
162
192
  ]);
163
193
  var tokenSchema = authTokenSchema;
164
194
  var createAuthTokenParamsSchema = z2.object({
165
- expires_in: z2.number().default(60),
166
- // 1 minute
195
+ expires_in: z2.number().int().min(1).max(3600).default(60),
167
196
  scopes: authTokenScopesSchema.optional(),
168
- tunnelsGrants: z2.array(authTokenTunnelGrantSchema).optional(),
197
+ tunnelsGrants: authTokenTunnelGrantSchema.optional(),
169
198
  metadata: z2.unknown().optional()
170
199
  // Additional metadata
171
200
  }).strict().superRefine((params, ctx) => {
@@ -176,6 +205,13 @@ var createAuthTokenParamsSchema = z2.object({
176
205
  path: ["tunnelsGrants"]
177
206
  });
178
207
  }
208
+ if (params.scopes === void 0 && params.tunnelsGrants === void 0) {
209
+ ctx.addIssue({
210
+ code: z2.ZodIssueCode.custom,
211
+ message: "Explicit scopes or tunnelsGrants are required.",
212
+ path: ["scopes"]
213
+ });
214
+ }
179
215
  });
180
216
  var createAuthTokenResponseSchema = z2.object({
181
217
  token: z2.string()
@@ -196,18 +232,38 @@ var RstreamAuthResource = class {
196
232
  );
197
233
  }
198
234
  const engine = options?.engine ?? await this.client.getEngine().catch(() => void 0);
235
+ const scopedOptions = await this.resolveScopeOptions(params, options);
199
236
  return createAuthTokenFromClientCredentials(credentials, params, {
200
- engine
237
+ engine,
238
+ ...scopedOptions
201
239
  });
202
240
  }
241
+ async resolveScopeOptions(params, options) {
242
+ const tokenParams = createAuthTokenParamsSchema.parse(params);
243
+ if (options?.projectScoped === false) {
244
+ return options ?? {};
245
+ }
246
+ if (options?.projectId !== void 0 || options?.workspaceId !== void 0) {
247
+ return options;
248
+ }
249
+ if (tokenParams.scopes === void 0 && !tunnelGrantsNeedDefaultTarget(tokenParams.tunnelsGrants)) {
250
+ return options ?? {};
251
+ }
252
+ const target = await this.client.getEngineAuthTokenTarget();
253
+ return {
254
+ ...options,
255
+ projectId: target.projects?.[0],
256
+ workspaceId: target.workspaces?.[0]
257
+ };
258
+ }
203
259
  };
204
260
  function createAuthTokenFromClientCredentials(credentials, params, options) {
205
- const tokenParams = createAuthTokenParamsSchema.parse(params ?? {});
261
+ const tokenParams = createAuthTokenParamsSchema.parse(params);
206
262
  const payload = {
207
263
  metadata: {
208
264
  engine: options?.engine
209
265
  },
210
- tunnelsGrants: normalizeAuthTokenTunnelGrants(tokenParams),
266
+ tunnelsGrants: normalizeAuthTokenTunnelGrants(tokenParams, options),
211
267
  permissions: null
212
268
  };
213
269
  const { token } = createClientCredentialsToken(credentials, {
@@ -216,21 +272,101 @@ function createAuthTokenFromClientCredentials(credentials, params, options) {
216
272
  });
217
273
  return { token };
218
274
  }
219
- function normalizeAuthTokenTunnelGrants(params) {
220
- if (!params) {
221
- return void 0;
222
- }
275
+ function normalizeAuthTokenTunnelGrants(params, options) {
223
276
  const tunnelsGrants = params.tunnelsGrants;
224
277
  if (tunnelsGrants !== void 0) {
225
- return [...tunnelsGrants];
278
+ return applyDefaultTunnelGrantTarget(tunnelsGrants, options);
226
279
  }
227
280
  if (params.scopes) {
228
- return [{ scopes: params.scopes }];
281
+ return { ...normalizeTunnelGrantTarget(options), scopes: params.scopes };
229
282
  }
230
283
  return void 0;
231
284
  }
285
+ function applyDefaultTunnelGrantTarget(grants, options, inheritedTarget = false) {
286
+ if (options?.projectScoped === false) {
287
+ return grants;
288
+ }
289
+ if ("AND" in grants) {
290
+ const branchHasTarget = inheritedTarget || grants.AND.some((grant) => tunnelGrantTreeHasTarget(grant));
291
+ return {
292
+ AND: grants.AND.map(
293
+ (grant) => applyDefaultTunnelGrantTarget(grant, options, branchHasTarget)
294
+ )
295
+ };
296
+ }
297
+ if ("OR" in grants) {
298
+ return {
299
+ OR: grants.OR.map(
300
+ (grant) => applyDefaultTunnelGrantTarget(grant, options, inheritedTarget)
301
+ )
302
+ };
303
+ }
304
+ if (grants.scopes === void 0 || tunnelGrantHasTarget(grants) || inheritedTarget) {
305
+ return grants;
306
+ }
307
+ return { ...normalizeTunnelGrantTarget(options), scopes: grants.scopes };
308
+ }
309
+ function tunnelGrantsNeedDefaultTarget(grants, inheritedTarget = false) {
310
+ if (grants === void 0) {
311
+ return false;
312
+ }
313
+ if ("AND" in grants) {
314
+ const branchHasTarget = inheritedTarget || grants.AND.some((grant) => tunnelGrantTreeHasTarget(grant));
315
+ return grants.AND.some(
316
+ (grant) => tunnelGrantsNeedDefaultTarget(grant, branchHasTarget)
317
+ );
318
+ }
319
+ if ("OR" in grants) {
320
+ return grants.OR.some(
321
+ (grant) => tunnelGrantsNeedDefaultTarget(grant, inheritedTarget)
322
+ );
323
+ }
324
+ return grants.scopes !== void 0 && !tunnelGrantHasTarget(grants) && !inheritedTarget;
325
+ }
326
+ function tunnelGrantHasTarget(grant) {
327
+ return grant.projects !== void 0 || grant.workspaces !== void 0;
328
+ }
329
+ function tunnelGrantTreeHasTarget(grants) {
330
+ if ("AND" in grants) {
331
+ return grants.AND.some((grant) => tunnelGrantTreeHasTarget(grant));
332
+ }
333
+ if ("OR" in grants) {
334
+ return grants.OR.some((grant) => tunnelGrantTreeHasTarget(grant));
335
+ }
336
+ return tunnelGrantHasTarget(grants);
337
+ }
338
+ function normalizeTunnelGrantTarget(options) {
339
+ const projectId = normalizeOptionalString(options?.projectId);
340
+ const workspaceId = normalizeOptionalString(options?.workspaceId);
341
+ if (projectId !== void 0 && workspaceId !== void 0) {
342
+ throw new Error("Use either projectId or workspaceId, not both.");
343
+ }
344
+ if (projectId !== void 0) {
345
+ return { projects: [projectId] };
346
+ }
347
+ if (workspaceId !== void 0) {
348
+ return { workspaces: [workspaceId] };
349
+ }
350
+ if (options?.projectScoped === false) {
351
+ return {};
352
+ }
353
+ throw new Error(
354
+ "Project ID or workspace ID is required when using scope-only tunnel grants."
355
+ );
356
+ }
357
+ function normalizeOptionalString(value) {
358
+ const normalized = value?.trim();
359
+ return normalized ? normalized : void 0;
360
+ }
232
361
 
233
362
  // src/clients-resource.ts
363
+ function clientPathId(id) {
364
+ const normalized = id.trim();
365
+ if (!normalized) {
366
+ throw new Error("Client ID is required.");
367
+ }
368
+ return encodeURIComponent(normalized);
369
+ }
234
370
  var RstreamClientsResource = class {
235
371
  client;
236
372
  constructor(client) {
@@ -244,9 +380,12 @@ var RstreamClientsResource = class {
244
380
  return listClientsResponseSchema.parse(response);
245
381
  }
246
382
  async get(id) {
247
- const response = await this.client.request(`/clients/${id}`, {
248
- method: "GET"
249
- });
383
+ const response = await this.client.request(
384
+ `/clients/${clientPathId(id)}`,
385
+ {
386
+ method: "GET"
387
+ }
388
+ );
250
389
  return clientSchema.parse(response);
251
390
  }
252
391
  };
@@ -257,22 +396,37 @@ import { getTunnelsProjectEngine } from "@rstreamlabs/rstream";
257
396
  import { isTokenCredentials } from "@rstreamlabs/rstream";
258
397
  import { readEnvironment } from "@rstreamlabs/rstream";
259
398
  import { RstreamClient } from "@rstreamlabs/rstream";
260
- import jwt from "jsonwebtoken";
261
399
  function trimOptionalString(value) {
262
400
  const normalized = value?.trim();
263
401
  return normalized ? normalized : void 0;
264
402
  }
265
- function getEngineFromToken(token) {
266
- if (!token) {
267
- return void 0;
403
+ var dnsLabelPattern = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i;
404
+ function validateHostname(hostname) {
405
+ if (!hostname || hostname.length > 253 || hostname.includes("..")) {
406
+ return false;
268
407
  }
269
- const parsed = authTokenSchema.safeParse(
270
- jwt.decode(token, { complete: false })
271
- );
272
- if (!parsed.success) {
273
- return void 0;
408
+ return hostname.split(".").every((label) => label === "localhost" || dnsLabelPattern.test(label));
409
+ }
410
+ function normalizeEngineAddress(engine) {
411
+ const normalized = engine.trim();
412
+ if (!normalized || /^[a-z][a-z0-9+.-]*:\/\//i.test(normalized) || /[/?#@\\\s]/.test(normalized)) {
413
+ throw new Error("Engine must be a host[:port] value.");
414
+ }
415
+ const explicitPort = normalized.match(/:(\d+)$/)?.[1];
416
+ const url = new URL(`https://${normalized}`);
417
+ if (url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
418
+ throw new Error("Engine must be a host[:port] value.");
419
+ }
420
+ if (!validateHostname(url.hostname)) {
421
+ throw new Error("Engine must use a valid hostname.");
422
+ }
423
+ if (explicitPort) {
424
+ const port = Number(explicitPort);
425
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
426
+ throw new Error("Engine port must be an integer between 1 and 65535.");
427
+ }
274
428
  }
275
- return trimOptionalString(parsed.data.metadata?.engine);
429
+ return `${url.hostname.toLowerCase()}${explicitPort ? `:${explicitPort}` : ""}`;
276
430
  }
277
431
  function resolveTunnelsAPIURL(apiUrl) {
278
432
  return trimOptionalString(apiUrl) ?? readEnvironment().apiUrl ?? DEFAULT_API_URL;
@@ -306,48 +460,61 @@ function resolveControlPlaneCredentials(controlPlaneCredentials, credentials, au
306
460
  async function resolveTunnelsEngine(config) {
307
461
  const explicitEngine = trimOptionalString(config.engine);
308
462
  if (explicitEngine) {
309
- return explicitEngine;
463
+ return normalizeEngineAddress(explicitEngine);
310
464
  }
311
465
  const environmentEngine = readEnvironment().engine;
312
466
  if (environmentEngine) {
313
- return environmentEngine;
467
+ return normalizeEngineAddress(environmentEngine);
314
468
  }
315
469
  const token = trimOptionalString(config.token) ?? (isTokenCredentials(config.credentials) ? config.credentials.token : void 0);
316
- const tokenEngine = getEngineFromToken(token);
317
- if (tokenEngine) {
318
- return tokenEngine;
319
- }
320
470
  const projectEndpoint = trimOptionalString(config.projectEndpoint);
321
471
  if (projectEndpoint) {
322
- const credentials = resolveControlPlaneCredentials(
323
- config.controlPlaneCredentials,
324
- config.credentials,
472
+ const project = await resolveManagedTunnelsProject({
473
+ ...config,
325
474
  token
326
- );
327
- if (credentials === void 0) {
328
- throw new Error(
329
- "Control-plane credentials are required to resolve a managed project endpoint."
330
- );
331
- }
332
- const controlPlane = new RstreamClient({
333
- apiUrl: resolveTunnelsAPIURL(config.apiUrl),
334
- credentials
335
475
  });
336
- const project = await controlPlane.tunnels.projects.resolveByEndpoint(projectEndpoint);
337
476
  const engine = getTunnelsProjectEngine(project);
338
477
  if (engine) {
339
- return engine;
478
+ return normalizeEngineAddress(engine);
340
479
  }
341
480
  throw new Error(
342
481
  "Failed to resolve the engine address from the managed tunnels project."
343
482
  );
344
483
  }
345
484
  throw new Error(
346
- "Engine URL is not defined. Provide an engine, use RSTREAM_ENGINE, or configure a managed project endpoint with control-plane access."
485
+ "Engine URL is not defined. Provide an engine, use RSTREAM_ENGINE, or configure a managed project endpoint with Control plane API access."
347
486
  );
348
487
  }
488
+ async function resolveManagedTunnelsProject(config) {
489
+ const projectEndpoint = trimOptionalString(config.projectEndpoint);
490
+ if (!projectEndpoint) {
491
+ throw new Error("Managed project endpoint is required.");
492
+ }
493
+ const credentials = resolveControlPlaneCredentials(
494
+ config.controlPlaneCredentials,
495
+ config.credentials,
496
+ config.token
497
+ );
498
+ if (credentials === void 0) {
499
+ throw new Error(
500
+ "Control-plane credentials are required to resolve a managed project endpoint."
501
+ );
502
+ }
503
+ const controlPlane = new RstreamClient({
504
+ apiUrl: resolveTunnelsAPIURL(config.apiUrl),
505
+ credentials
506
+ });
507
+ return await controlPlane.tunnels.projects.resolveByEndpoint(projectEndpoint);
508
+ }
349
509
 
350
510
  // src/tunnels-resource.ts
511
+ function tunnelPathId(id) {
512
+ const normalized = id.trim();
513
+ if (!normalized) {
514
+ throw new Error("Tunnel ID is required.");
515
+ }
516
+ return encodeURIComponent(normalized);
517
+ }
351
518
  var RstreamTunnelsResource = class {
352
519
  client;
353
520
  constructor(client) {
@@ -361,15 +528,20 @@ var RstreamTunnelsResource = class {
361
528
  return listTunnelsResponseSchema.parse(response);
362
529
  }
363
530
  async get(id) {
364
- const response = await this.client.request(`/tunnels/${id}`, {
365
- method: "GET"
366
- });
531
+ const response = await this.client.request(
532
+ `/tunnels/${tunnelPathId(id)}`,
533
+ {
534
+ method: "GET"
535
+ }
536
+ );
367
537
  return tunnelSchema.parse(response);
368
538
  }
369
539
  };
370
540
 
371
541
  // src/tunnels.ts
542
+ import { getTunnelsProjectEngine as getTunnelsProjectEngine2 } from "@rstreamlabs/rstream";
372
543
  import { isClientCredentials as isClientCredentials3 } from "@rstreamlabs/rstream";
544
+ import { readEnvironment as readEnvironment2 } from "@rstreamlabs/rstream";
373
545
 
374
546
  // src/turn.ts
375
547
  import { isClientCredentials as isClientCredentials2 } from "@rstreamlabs/rstream";
@@ -380,46 +552,53 @@ import * as z3 from "zod";
380
552
  import crypto from "crypto";
381
553
  var defaultTURNPort = 3478;
382
554
  var defaultTURNSPort = 5349;
383
- var defaultTURNCredentialTTLSeconds = 24 * 60 * 60;
555
+ var defaultTURNCredentialTTLSeconds = 10 * 60;
556
+ var maxTURNCredentialTTLSeconds = 60 * 60;
384
557
  var turnTokenClaimsSchema = z3.object({
385
- token_endpoint: z3.string().optional(),
386
- type: z3.string()
558
+ exp: z3.number().int().optional(),
559
+ iat: z3.number().int().optional(),
560
+ token_endpoint: z3.string().regex(/^[0-9a-f]{8}$/i).optional(),
561
+ tokendpoint: z3.string().regex(/^[0-9a-f]{8}$/i).optional(),
562
+ type: z3.enum(["app", "auth", "pat"])
387
563
  });
388
564
  var turnCredentialModeSchema = z3.enum(["api", "app", "pat"]);
389
- function normalizeOptionalString(value) {
565
+ function normalizeOptionalString2(value) {
390
566
  const normalized = value?.trim();
391
567
  return normalized ? normalized : void 0;
392
568
  }
393
569
  function normalizeClusterDomain(clusterDomain) {
394
- const normalized = normalizeOptionalString(clusterDomain);
570
+ const normalized = normalizeOptionalString2(clusterDomain);
395
571
  return normalized?.toLowerCase();
396
572
  }
397
573
  function normalizeNow(now) {
398
- if (now instanceof Date) {
399
- return Math.floor(now.getTime() / 1e3);
574
+ if (now === void 0) {
575
+ return Math.floor(Date.now() / 1e3);
400
576
  }
401
- if (typeof now === "number" && Number.isFinite(now)) {
402
- return Math.floor(now);
577
+ const timestamp = now instanceof Date ? now.getTime() / 1e3 : now;
578
+ if (!Number.isFinite(timestamp)) {
579
+ throw new Error("TURN timestamp must be finite.");
403
580
  }
404
- return Math.floor(Date.now() / 1e3);
581
+ return Math.floor(timestamp);
405
582
  }
406
583
  function normalizeTTLSeconds(ttlSeconds) {
407
- if (typeof ttlSeconds === "number" && Number.isFinite(ttlSeconds) && ttlSeconds > 0) {
408
- return Math.floor(ttlSeconds);
584
+ const ttl = ttlSeconds ?? defaultTURNCredentialTTLSeconds;
585
+ if (!Number.isInteger(ttl) || ttl < 1 || ttl > maxTURNCredentialTTLSeconds) {
586
+ throw new Error("TURN TTL must be an integer between 1 and 3600 seconds.");
409
587
  }
410
- return defaultTURNCredentialTTLSeconds;
588
+ return ttl;
411
589
  }
412
- function normalizeTURNPort(port) {
413
- if (typeof port === "number" && Number.isFinite(port) && port > 0) {
414
- return Math.floor(port);
590
+ function normalizePort(port, fallback, name) {
591
+ const normalized = port ?? fallback;
592
+ if (!Number.isInteger(normalized) || normalized < 1 || normalized > 65535) {
593
+ throw new Error(`${name} must be an integer between 1 and 65535.`);
415
594
  }
416
- return defaultTURNPort;
595
+ return normalized;
596
+ }
597
+ function normalizeTURNPort(port) {
598
+ return normalizePort(port, defaultTURNPort, "TURN port");
417
599
  }
418
600
  function normalizeTURNSPort(port) {
419
- if (typeof port === "number" && Number.isFinite(port) && port > 0) {
420
- return Math.floor(port);
421
- }
422
- return defaultTURNSPort;
601
+ return normalizePort(port, defaultTURNSPort, "TURNS port");
423
602
  }
424
603
  function createTURNURLs(clusterDomain, turnPort, turnsPort) {
425
604
  const normalizedTURNPort = normalizeTURNPort(turnPort);
@@ -437,16 +616,32 @@ function parseTURNTokenClaims(token) {
437
616
  if (!payloadPart) {
438
617
  throw new Error("Invalid token format.");
439
618
  }
440
- const payload = Buffer.from(payloadPart, "base64url").toString("utf8");
441
- const parsed = turnTokenClaimsSchema.safeParse(JSON.parse(payload));
619
+ let decoded;
620
+ try {
621
+ const payload = Buffer.from(payloadPart, "base64url").toString("utf8");
622
+ decoded = JSON.parse(payload);
623
+ } catch {
624
+ throw new Error("Invalid token format.");
625
+ }
626
+ const parsed = turnTokenClaimsSchema.safeParse(decoded);
442
627
  if (!parsed.success) {
443
628
  throw new Error("Invalid token format.");
444
629
  }
445
630
  return parsed.data;
446
631
  }
632
+ function requirePATExpiration(claims, now) {
633
+ const exp = claims.exp;
634
+ if (typeof exp !== "number") {
635
+ throw new Error("TURN PAT mode requires a PAT token with expiration.");
636
+ }
637
+ if (exp <= now) {
638
+ throw new Error("TURN PAT mode requires a non-expired PAT token.");
639
+ }
640
+ return exp;
641
+ }
447
642
  function deriveClusterDomainFromEngine(projectEndpoint, engine) {
448
- const normalizedProjectEndpoint = normalizeOptionalString(projectEndpoint);
449
- const normalizedEngine = normalizeOptionalString(engine);
643
+ const normalizedProjectEndpoint = normalizeOptionalString2(projectEndpoint);
644
+ const normalizedEngine = normalizeOptionalString2(engine);
450
645
  if (!normalizedProjectEndpoint || !normalizedEngine) {
451
646
  throw new Error(
452
647
  "Failed to derive the cluster domain from the engine address."
@@ -480,17 +675,11 @@ function resolveMode(credentials, mode) {
480
675
  if (isClientCredentials2(credentials)) {
481
676
  return "app";
482
677
  }
483
- if (isTokenCredentials2(credentials)) {
484
- const claims = parseTURNTokenClaims(credentials.token);
485
- if (claims.type === "pat" && normalizeOptionalString(claims.token_endpoint)) {
486
- return "pat";
487
- }
488
- }
489
678
  return "api";
490
679
  }
491
680
  async function loadTURNServerPublicKeyHex(clusterDomain, options) {
492
- const configuredKey = normalizeOptionalString(options.keyringBaseUrl);
493
- const keyringBaseURL = configuredKey ?? normalizeOptionalString(options.apiUrl) ?? "https://rstream.io";
681
+ const configuredKey = normalizeOptionalString2(options.keyringBaseUrl);
682
+ const keyringBaseURL = configuredKey ?? normalizeOptionalString2(options.apiUrl) ?? "https://rstream.io";
494
683
  const url = new URL(
495
684
  `/keyrings/turn/${clusterDomain}.spki.der.hex`,
496
685
  keyringBaseURL
@@ -502,8 +691,8 @@ async function loadTURNServerPublicKeyHex(clusterDomain, options) {
502
691
  return (await response.text()).trim();
503
692
  }
504
693
  async function createAPITURNCredentials(options) {
505
- const projectId = normalizeOptionalString(options.projectId);
506
- const projectEndpoint = normalizeOptionalString(options.projectEndpoint);
694
+ const projectId = normalizeOptionalString2(options.projectId);
695
+ const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
507
696
  if (!projectId && !projectEndpoint) {
508
697
  throw new Error(
509
698
  "Project ID or project endpoint is required for TURN API mode."
@@ -530,11 +719,11 @@ function createPATTURNCredentials(options) {
530
719
  if (!clusterDomain) {
531
720
  throw new Error("Cluster domain is required for TURN PAT mode.");
532
721
  }
533
- const projectEndpoint = normalizeOptionalString(options.projectEndpoint);
722
+ const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
534
723
  if (!projectEndpoint) {
535
724
  throw new Error("Project endpoint is required for TURN PAT mode.");
536
725
  }
537
- const token = normalizeOptionalString(options.token);
726
+ const token = normalizeOptionalString2(options.token);
538
727
  if (!token) {
539
728
  throw new Error("Token is required for TURN PAT mode.");
540
729
  }
@@ -542,14 +731,18 @@ function createPATTURNCredentials(options) {
542
731
  if (claims.type !== "pat") {
543
732
  throw new Error("TURN PAT mode requires a PAT token.");
544
733
  }
545
- const tokenEndpoint = normalizeOptionalString(claims.token_endpoint);
734
+ const tokenEndpoint = normalizeOptionalString2(options.tokenEndpoint);
546
735
  if (!tokenEndpoint) {
547
- throw new Error(
548
- "TURN PAT mode requires a PAT token carrying a token endpoint."
549
- );
736
+ throw new Error("Token endpoint is required for TURN PAT mode.");
737
+ }
738
+ const claimsTokenEndpoint = claims.token_endpoint ?? claims.tokendpoint;
739
+ if (claimsTokenEndpoint !== void 0 && claimsTokenEndpoint !== tokenEndpoint) {
740
+ throw new Error("Token endpoint does not match the PAT token.");
550
741
  }
742
+ const now = normalizeNow(options.now);
551
743
  const ttlSeconds = normalizeTTLSeconds(options.ttlSeconds);
552
- const exp = normalizeNow(options.now) + ttlSeconds;
744
+ const exp = Math.min(now + ttlSeconds, requirePATExpiration(claims, now));
745
+ const ttl = exp - now;
553
746
  const username = `v1:${exp}:pat:${projectEndpoint}:${tokenEndpoint}`;
554
747
  const tokenHash = crypto.createHash("sha256").update(token, "utf8").digest();
555
748
  const key = Buffer.from(
@@ -564,7 +757,7 @@ function createPATTURNCredentials(options) {
564
757
  const credential = crypto.createHmac("sha256", key).update(username, "utf8").digest("base64");
565
758
  return turnCredentialsSchema.parse({
566
759
  credential,
567
- ttl: ttlSeconds,
760
+ ttl,
568
761
  urls: createTURNURLs(clusterDomain, options.turnPort, options.turnsPort),
569
762
  username
570
763
  });
@@ -574,15 +767,15 @@ async function createAPPTURNCredentials(options) {
574
767
  if (!clusterDomain) {
575
768
  throw new Error("Cluster domain is required for TURN APP mode.");
576
769
  }
577
- const projectEndpoint = normalizeOptionalString(options.projectEndpoint);
770
+ const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
578
771
  if (!projectEndpoint) {
579
772
  throw new Error("Project endpoint is required for TURN APP mode.");
580
773
  }
581
- const clientId = normalizeOptionalString(options.clientId);
774
+ const clientId = normalizeOptionalString2(options.clientId);
582
775
  if (!clientId) {
583
776
  throw new Error("Client ID is required for TURN APP mode.");
584
777
  }
585
- const clientSecret = normalizeOptionalString(options.clientSecret);
778
+ const clientSecret = normalizeOptionalString2(options.clientSecret);
586
779
  if (!clientSecret) {
587
780
  throw new Error("Client secret is required for TURN APP mode.");
588
781
  }
@@ -594,7 +787,7 @@ async function createAPPTURNCredentials(options) {
594
787
  format: "der",
595
788
  type: "pkcs8"
596
789
  });
597
- const serverPublicKeyHex = normalizeOptionalString(options.serverPublicKeyHex) ?? await loadTURNServerPublicKeyHex(clusterDomain, options);
790
+ const serverPublicKeyHex = normalizeOptionalString2(options.serverPublicKeyHex) ?? await loadTURNServerPublicKeyHex(clusterDomain, options);
598
791
  const serverPublicKey = crypto.createPublicKey({
599
792
  key: Buffer.from(serverPublicKeyHex, "hex"),
600
793
  format: "der",
@@ -624,7 +817,7 @@ async function createAPPTURNCredentials(options) {
624
817
  async function createTURNCredentials(options) {
625
818
  const credentials = options.credentials;
626
819
  const mode = resolveMode(credentials, options.mode);
627
- const projectEndpoint = normalizeOptionalString(options.projectEndpoint);
820
+ const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
628
821
  if (mode === "api") {
629
822
  const controlPlaneCredentials = options.controlPlaneCredentials ?? credentials;
630
823
  if (!controlPlaneCredentials) {
@@ -655,6 +848,10 @@ async function createTURNCredentials(options) {
655
848
  "Project endpoint is required for TURN PAT mode."
656
849
  ),
657
850
  token: credentials.token,
851
+ tokenEndpoint: requireOption(
852
+ normalizeOptionalString2(options.tokenEndpoint),
853
+ "Token endpoint is required for TURN PAT mode."
854
+ ),
658
855
  ttlSeconds: options.ttlSeconds,
659
856
  turnPort: options.turnPort,
660
857
  turnsPort: options.turnsPort
@@ -704,6 +901,7 @@ var RstreamTURNResource = class {
704
901
  projectEndpoint: options.projectEndpoint ?? this.client.projectEndpoint,
705
902
  projectId: options.projectId,
706
903
  serverPublicKeyHex: options.serverPublicKeyHex,
904
+ tokenEndpoint: options.tokenEndpoint,
707
905
  ttlSeconds: options.ttlSeconds,
708
906
  turnPort: options.turnPort,
709
907
  turnsPort: options.turnsPort
@@ -713,6 +911,41 @@ var RstreamTURNResource = class {
713
911
 
714
912
  // src/webhooks-resource.ts
715
913
  import crypto2 from "crypto";
914
+ var SIGNATURE_HEX_BYTES = 32;
915
+ var signatureHexSchema = /^[0-9a-fA-F]{64}$/;
916
+ var timestampSchema = /^\d+$/;
917
+ function parseSignatureTimestamp(timestamp) {
918
+ if (!timestampSchema.test(timestamp)) {
919
+ throw new Error("Invalid signature timestamp");
920
+ }
921
+ const parsed = Number(timestamp);
922
+ if (!Number.isSafeInteger(parsed)) {
923
+ throw new Error("Invalid signature timestamp");
924
+ }
925
+ return parsed;
926
+ }
927
+ function normalizeTolerance(tolerance) {
928
+ if (!Number.isFinite(tolerance) || !Number.isInteger(tolerance) || tolerance < 0) {
929
+ throw new Error("Invalid signature tolerance");
930
+ }
931
+ return tolerance;
932
+ }
933
+ function readSignature(signature) {
934
+ if (!signatureHexSchema.test(signature)) {
935
+ return void 0;
936
+ }
937
+ const parsed = Buffer.from(signature, "hex");
938
+ return parsed.length === SIGNATURE_HEX_BYTES ? parsed : void 0;
939
+ }
940
+ function signatureMatches(signature, expected) {
941
+ const parsed = readSignature(signature);
942
+ return parsed === void 0 ? false : crypto2.timingSafeEqual(parsed, expected);
943
+ }
944
+ function assertSecretConfigured(secret) {
945
+ if (secret.trim().length === 0) {
946
+ throw new Error("Webhook signing secret is required");
947
+ }
948
+ }
716
949
  var RstreamWebhookResource = class {
717
950
  async event(payload, header, secret, tolerance = 300, receivedAt = Date.now()) {
718
951
  const payloadBuffer = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
@@ -720,31 +953,26 @@ var RstreamWebhookResource = class {
720
953
  if (!signatureHeader) {
721
954
  throw new Error("No signature header");
722
955
  }
956
+ assertSecretConfigured(secret);
723
957
  const elements = signatureHeader.split(",").map((element) => element.trim());
724
958
  const timestamp = elements.find((element) => element.startsWith("t="))?.substring(2);
725
959
  const signatures = elements.filter((element) => element.startsWith("v1=")).map((element) => element.substring(3));
726
960
  if (!timestamp || signatures.length === 0) {
727
961
  throw new Error("Invalid signature header format");
728
962
  }
729
- const timestampNum = parseInt(timestamp, 10);
963
+ const timestampNum = parseSignatureTimestamp(timestamp);
964
+ const signatureTolerance = normalizeTolerance(tolerance);
730
965
  const now = Math.floor(receivedAt / 1e3);
731
- if (Math.abs(now - timestampNum) > tolerance) {
966
+ if (Math.abs(now - timestampNum) > signatureTolerance) {
732
967
  throw new Error(
733
968
  `Webhook signature timestamp outside tolerance: ${now}, ${timestampNum}`
734
969
  );
735
970
  }
736
971
  const signedPayload = `${timestamp}.${payloadBuffer.toString("utf8")}`;
737
- const expectedSignature = crypto2.createHmac("sha256", secret).update(signedPayload).digest("hex");
738
- const signatureMatched = signatures.some((signature) => {
739
- try {
740
- return crypto2.timingSafeEqual(
741
- Buffer.from(signature),
742
- Buffer.from(expectedSignature)
743
- );
744
- } catch {
745
- return false;
746
- }
747
- });
972
+ const expectedSignature = crypto2.createHmac("sha256", secret).update(signedPayload).digest();
973
+ const signatureMatched = signatures.some(
974
+ (signature) => signatureMatches(signature, expectedSignature)
975
+ );
748
976
  if (!signatureMatched) {
749
977
  throw new Error("Signature verification failed");
750
978
  }
@@ -762,8 +990,16 @@ var RstreamWebhookResource = class {
762
990
  };
763
991
 
764
992
  // src/tunnels.ts
993
+ var engineAuthTokenScopes = {
994
+ tunnels: {
995
+ connect: true,
996
+ create: true,
997
+ list: true
998
+ }
999
+ };
765
1000
  var RstreamTunnelsClient = class {
766
1001
  config;
1002
+ managedProject;
767
1003
  constructor(config) {
768
1004
  this.config = config;
769
1005
  }
@@ -783,6 +1019,21 @@ var RstreamTunnelsClient = class {
783
1019
  return this.config?.projectEndpoint;
784
1020
  }
785
1021
  async getEngine() {
1022
+ const projectEndpoint = normalizeOptionalString3(
1023
+ this.config?.projectEndpoint
1024
+ );
1025
+ if (projectEndpoint !== void 0 && normalizeOptionalString3(this.config?.engine) === void 0 && readEnvironment2().engine === void 0) {
1026
+ const project = await this.getManagedProject();
1027
+ if (project !== void 0) {
1028
+ const engine = getTunnelsProjectEngine2(project);
1029
+ if (engine) {
1030
+ return normalizeEngineAddress(engine);
1031
+ }
1032
+ }
1033
+ throw new Error(
1034
+ "Failed to resolve the engine address from the managed tunnels project."
1035
+ );
1036
+ }
786
1037
  return await resolveTunnelsEngine({
787
1038
  apiUrl: this.config?.apiUrl,
788
1039
  controlPlaneCredentials: this.controlPlaneCredentials,
@@ -791,6 +1042,55 @@ var RstreamTunnelsClient = class {
791
1042
  projectEndpoint: this.config?.projectEndpoint
792
1043
  });
793
1044
  }
1045
+ async getManagedProject() {
1046
+ const projectEndpoint = normalizeOptionalString3(
1047
+ this.config?.projectEndpoint
1048
+ );
1049
+ if (projectEndpoint === void 0) {
1050
+ return void 0;
1051
+ }
1052
+ if (this.managedProject === void 0) {
1053
+ this.managedProject = resolveManagedTunnelsProject({
1054
+ apiUrl: this.config?.apiUrl,
1055
+ controlPlaneCredentials: this.controlPlaneCredentials,
1056
+ credentials: this.credentials,
1057
+ projectEndpoint
1058
+ }).catch((error) => {
1059
+ this.managedProject = void 0;
1060
+ throw error;
1061
+ });
1062
+ }
1063
+ return await this.managedProject;
1064
+ }
1065
+ async getEngineAuthTokenParams() {
1066
+ const target = await this.getEngineAuthTokenTarget();
1067
+ return {
1068
+ tunnelsGrants: {
1069
+ ...target,
1070
+ scopes: engineAuthTokenScopes
1071
+ }
1072
+ };
1073
+ }
1074
+ async getEngineAuthTokenTarget() {
1075
+ const projectId = normalizeOptionalString3(this.config?.projectId);
1076
+ const workspaceId = normalizeOptionalString3(this.config?.workspaceId);
1077
+ if (projectId !== void 0 && workspaceId !== void 0) {
1078
+ throw new Error("Use either projectId or workspaceId, not both.");
1079
+ }
1080
+ if (projectId !== void 0) {
1081
+ return { projects: [projectId] };
1082
+ }
1083
+ if (workspaceId !== void 0) {
1084
+ return { workspaces: [workspaceId] };
1085
+ }
1086
+ const project = await this.getManagedProject();
1087
+ if (project !== void 0) {
1088
+ return { projects: [project.id] };
1089
+ }
1090
+ throw new Error(
1091
+ "Application credentials require projectId, workspaceId, or projectEndpoint to create scoped engine tokens."
1092
+ );
1093
+ }
794
1094
  get auth() {
795
1095
  return new RstreamAuthResource(this);
796
1096
  }
@@ -814,13 +1114,17 @@ var RstreamTunnelsClient = class {
814
1114
  if (!isClientCredentials3(credentials)) {
815
1115
  return credentials.token;
816
1116
  }
817
- return (await this.auth.createAuthToken(void 0, {
1117
+ const tokenParams = await this.getEngineAuthTokenParams();
1118
+ return (await this.auth.createAuthToken(tokenParams, {
818
1119
  credentials,
819
1120
  engine
820
1121
  })).token;
821
1122
  }
822
1123
  async request(path, options) {
823
- const engine = await this.getEngine();
1124
+ const engine = normalizeEngineAddress(await this.getEngine());
1125
+ if (!path.startsWith("/") || path.startsWith("//")) {
1126
+ throw new Error("Engine request path must be a relative absolute path.");
1127
+ }
824
1128
  const url = `https://${engine}/api${path}`;
825
1129
  const headers = new Headers(options?.headers || {});
826
1130
  const token = await this.getToken(engine);
@@ -829,7 +1133,8 @@ var RstreamTunnelsClient = class {
829
1133
  }
830
1134
  const response = await fetch(url, {
831
1135
  ...options,
832
- headers
1136
+ headers,
1137
+ redirect: options?.redirect ?? "manual"
833
1138
  });
834
1139
  if (!response.ok) {
835
1140
  const errorText = await response.text();
@@ -838,8 +1143,96 @@ var RstreamTunnelsClient = class {
838
1143
  return await response.json();
839
1144
  }
840
1145
  };
1146
+ function normalizeOptionalString3(value) {
1147
+ const normalized = value?.trim();
1148
+ return normalized ? normalized : void 0;
1149
+ }
841
1150
 
842
1151
  // src/watch.ts
1152
+ import jwt from "jsonwebtoken";
1153
+ var maxWatchTokenLifetimeSeconds = 3600;
1154
+ var watchTokenIssuedAtSkewSeconds = 300;
1155
+ function normalizeTransport(transport) {
1156
+ if (transport === void 0) {
1157
+ return "sse";
1158
+ }
1159
+ if (transport === "sse" || transport === "websocket") {
1160
+ return transport;
1161
+ }
1162
+ throw new Error("Watch: Unsupported transport.");
1163
+ }
1164
+ function validateWatchQueryToken(token) {
1165
+ const parsed = authTokenSchema.safeParse(jwt.decode(token));
1166
+ if (!parsed.success || parsed.data.type === "pat") {
1167
+ throw new Error(
1168
+ "Watch requires a short-lived auth or app token for URL-based authentication."
1169
+ );
1170
+ }
1171
+ const iat = parsed.data.iat;
1172
+ const exp = parsed.data.exp;
1173
+ const now = Math.floor(Date.now() / 1e3);
1174
+ if (typeof iat !== "number" || typeof exp !== "number" || !Number.isInteger(iat) || !Number.isInteger(exp) || exp <= iat || exp <= now || exp > now + maxWatchTokenLifetimeSeconds || iat > now + watchTokenIssuedAtSkewSeconds || exp - iat > maxWatchTokenLifetimeSeconds) {
1175
+ throw new Error(
1176
+ "Watch requires a token with a bounded lifetime of at most 3600 seconds."
1177
+ );
1178
+ }
1179
+ if (!hasWatchOnlyTunnelGrants(parsed.data)) {
1180
+ throw new Error(
1181
+ "Watch requires a fine-grained token limited to tunnel list grants for URL-based authentication."
1182
+ );
1183
+ }
1184
+ }
1185
+ function hasWatchOnlyTunnelGrants(token) {
1186
+ if (!hasWatchOnlyPermissions(token.permissions)) {
1187
+ return false;
1188
+ }
1189
+ if (token.tunnelsGrants === void 0) {
1190
+ return false;
1191
+ }
1192
+ return hasWatchOnlyTunnelGrant(token.tunnelsGrants);
1193
+ }
1194
+ function hasWatchOnlyTunnelGrant(grant) {
1195
+ return watchGrantBranches(grant).every(
1196
+ (branch) => branch.hasList && !branch.hasMutationOrConnect
1197
+ );
1198
+ }
1199
+ function watchGrantBranches(grant) {
1200
+ if ("AND" in grant) {
1201
+ return grant.AND.reduce(
1202
+ (branches, child) => combineWatchGrantBranches(branches, child),
1203
+ [{ hasList: false, hasMutationOrConnect: false }]
1204
+ );
1205
+ }
1206
+ if ("OR" in grant) {
1207
+ return grant.OR.flatMap(watchGrantBranches);
1208
+ }
1209
+ const tunnels = grant.scopes?.tunnels;
1210
+ if (tunnels === void 0) {
1211
+ return [{ hasList: false, hasMutationOrConnect: false }];
1212
+ }
1213
+ return [
1214
+ {
1215
+ hasList: tunnels.list !== void 0,
1216
+ hasMutationOrConnect: tunnels.create !== void 0 || tunnels.connect !== void 0
1217
+ }
1218
+ ];
1219
+ }
1220
+ function combineWatchGrantBranches(branches, child) {
1221
+ return branches.flatMap(
1222
+ (branch) => watchGrantBranches(child).map((childBranch) => ({
1223
+ hasList: branch.hasList || childBranch.hasList,
1224
+ hasMutationOrConnect: branch.hasMutationOrConnect || childBranch.hasMutationOrConnect
1225
+ }))
1226
+ );
1227
+ }
1228
+ function hasWatchOnlyPermissions(permissions) {
1229
+ if (!Array.isArray(permissions)) {
1230
+ return true;
1231
+ }
1232
+ return permissions.every(
1233
+ (permission) => permission === "tunnels.resources.read-only" || permission === "network.resources.read-only"
1234
+ );
1235
+ }
843
1236
  var Watch = class {
844
1237
  connection = null;
845
1238
  connectionState = "preparing";
@@ -854,45 +1247,56 @@ var Watch = class {
854
1247
  throw new Error("Watch: Connection already started or closed.");
855
1248
  }
856
1249
  this.connectionState = "connecting";
857
- const token = typeof this.config.auth === "function" ? await this.config.auth() : this.config.auth;
858
- const engine = await resolveTunnelsEngine({
859
- apiUrl: this.config.apiUrl,
860
- controlPlaneCredentials: this.config.controlPlaneCredentials,
861
- engine: this.config.engine,
862
- projectEndpoint: this.config.projectEndpoint,
863
- token
864
- });
865
- const transport = this.config.transport ?? "sse";
866
- const base = `https://${engine}`;
867
- if (transport === "sse") {
868
- const url = new URL(`/api/sse`, base);
869
- url.searchParams.set("rstream.token", token);
870
- this.connection = new EventSource(url.toString());
871
- } else {
872
- const url = new URL(`/api/websocket`, base);
873
- url.searchParams.set("rstream.token", token);
874
- url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
875
- this.connection = new WebSocket(url.toString());
876
- }
877
- this.connection.onopen = () => {
878
- this.connectionState = "connected";
879
- this.events.onConnect?.();
880
- };
881
- this.connection.onmessage = (msg) => {
882
- const parsed = wsEventsSchema.parse(JSON.parse(msg.data));
883
- this.events.onEvent?.(parsed);
884
- };
885
- this.connection.onerror = () => {
886
- if (this.connectionState !== "closed") {
887
- this.disconnect();
1250
+ try {
1251
+ const token = typeof this.config.auth === "function" ? await this.config.auth() : this.config.auth;
1252
+ validateWatchQueryToken(token);
1253
+ const engine = await resolveTunnelsEngine({
1254
+ apiUrl: this.config.apiUrl,
1255
+ controlPlaneCredentials: this.config.controlPlaneCredentials,
1256
+ engine: this.config.engine,
1257
+ projectEndpoint: this.config.projectEndpoint,
1258
+ token
1259
+ });
1260
+ const transport = normalizeTransport(this.config.transport);
1261
+ const base = `https://${engine}`;
1262
+ if (transport === "sse") {
1263
+ const url = new URL(`/api/sse`, base);
1264
+ url.searchParams.set("rstream.token", token);
1265
+ this.connection = new EventSource(url.toString());
1266
+ } else {
1267
+ const url = new URL(`/api/websocket`, base);
1268
+ url.searchParams.set("rstream.token", token);
1269
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
1270
+ this.connection = new WebSocket(url.toString());
888
1271
  }
889
- };
890
- if (this.connection instanceof WebSocket) {
891
- this.connection.onclose = () => {
1272
+ this.connection.onopen = () => {
1273
+ this.connectionState = "connected";
1274
+ this.events.onConnect?.();
1275
+ };
1276
+ this.connection.onmessage = (msg) => this.handleMessage(msg.data);
1277
+ this.connection.onerror = () => {
892
1278
  if (this.connectionState !== "closed") {
893
1279
  this.disconnect();
894
1280
  }
895
1281
  };
1282
+ if (this.connection instanceof WebSocket) {
1283
+ this.connection.onclose = () => {
1284
+ if (this.connectionState !== "closed") {
1285
+ this.disconnect();
1286
+ }
1287
+ };
1288
+ }
1289
+ } catch (error) {
1290
+ this.connectionState = "preparing";
1291
+ throw error;
1292
+ }
1293
+ }
1294
+ handleMessage(data) {
1295
+ try {
1296
+ const parsed = wsEventsSchema.parse(JSON.parse(data));
1297
+ this.events.onEvent?.(parsed);
1298
+ } catch {
1299
+ this.disconnect();
896
1300
  }
897
1301
  }
898
1302
  disconnect() {
@@ -1012,8 +1416,10 @@ export {
1012
1416
  listClientsResponseSchema,
1013
1417
  listTunnelsParamsSchema,
1014
1418
  listTunnelsResponseSchema,
1419
+ normalizeEngineAddress,
1015
1420
  parseWebTTYServers,
1016
1421
  resolveControlPlaneCredentials,
1422
+ resolveManagedTunnelsProject,
1017
1423
  resolveTunnelsAPIURL,
1018
1424
  resolveTunnelsCredentials,
1019
1425
  resolveTunnelsEngine,