@rstreamlabs/tunnels 1.0.7 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -65,8 +65,10 @@ __export(index_exports, {
65
65
  listClientsResponseSchema: () => listClientsResponseSchema,
66
66
  listTunnelsParamsSchema: () => listTunnelsParamsSchema,
67
67
  listTunnelsResponseSchema: () => listTunnelsResponseSchema,
68
+ normalizeEngineAddress: () => normalizeEngineAddress,
68
69
  parseWebTTYServers: () => parseWebTTYServers,
69
70
  resolveControlPlaneCredentials: () => resolveControlPlaneCredentials,
71
+ resolveManagedTunnelsProject: () => resolveManagedTunnelsProject,
70
72
  resolveTunnelsAPIURL: () => resolveTunnelsAPIURL,
71
73
  resolveTunnelsCredentials: () => resolveTunnelsCredentials,
72
74
  resolveTunnelsEngine: () => resolveTunnelsEngine,
@@ -90,9 +92,9 @@ var import_rstream = require("@rstreamlabs/rstream");
90
92
  var z = __toESM(require("zod"));
91
93
  var StringFilter = z.union([
92
94
  z.string(),
93
- z.object({ exact: z.string() }),
94
- z.object({ oneof: z.array(z.string()) }),
95
- z.object({ regex: z.string() })
95
+ z.object({ exact: z.string() }).strict(),
96
+ z.object({ oneof: z.array(z.string()) }).strict(),
97
+ z.object({ regex: z.string() }).strict()
96
98
  ]);
97
99
  function transform(field) {
98
100
  if (field instanceof z.ZodOptional) {
@@ -126,7 +128,7 @@ function select(base) {
126
128
  return [key, z.boolean().optional()];
127
129
  });
128
130
  const shape = Object.fromEntries(entries);
129
- return z.object(shape);
131
+ return z.object(shape).strict();
130
132
  }
131
133
 
132
134
  // src/tunnel.ts
@@ -219,7 +221,7 @@ var authTokenTunnelsScopesSchema = z3.object({
219
221
  creation_date: true
220
222
  })
221
223
  ).optional()
222
- })
224
+ }).strict()
223
225
  ]).optional(),
224
226
  // Scopes for connecting to tunnels
225
227
  connect: z3.union([
@@ -231,7 +233,7 @@ var authTokenTunnelsScopesSchema = z3.object({
231
233
  path: z3.string().optional()
232
234
  })
233
235
  ).optional()
234
- })
236
+ }).strict()
235
237
  ]).optional(),
236
238
  // Scopes for listing tunnels
237
239
  list: z3.union([
@@ -239,17 +241,32 @@ var authTokenTunnelsScopesSchema = z3.object({
239
241
  z3.object({
240
242
  filters: filters(tunnelSchema).optional(),
241
243
  select: select(tunnelSchema).optional()
242
- })
244
+ }).strict()
243
245
  ]).optional()
246
+ }).strict().superRefine((scopes, ctx) => {
247
+ if (scopes.create === void 0 && scopes.connect === void 0 && scopes.list === void 0) {
248
+ ctx.addIssue({
249
+ code: z3.ZodIssueCode.custom,
250
+ message: "At least one tunnel scope action is required."
251
+ });
252
+ }
244
253
  });
245
254
  var authTokenScopesSchema = z3.object({
246
255
  tunnels: authTokenTunnelsScopesSchema.optional()
247
256
  // Scopes related to tunnels
257
+ }).strict().superRefine((scopes, ctx) => {
258
+ if (scopes.tunnels === void 0) {
259
+ ctx.addIssue({
260
+ code: z3.ZodIssueCode.custom,
261
+ message: "Tunnel scopes are required.",
262
+ path: ["tunnels"]
263
+ });
264
+ }
248
265
  });
249
266
  var authTokenTunnelGrantSchema = z3.object({
250
267
  workspaces: z3.array(nonEmptyStringSchema).min(1).optional(),
251
268
  projects: z3.array(nonEmptyStringSchema).min(1).optional(),
252
- scopes: authTokenScopesSchema.optional()
269
+ scopes: authTokenScopesSchema
253
270
  }).strict().superRefine((grant, ctx) => {
254
271
  if (grant.workspaces !== void 0 && grant.projects !== void 0) {
255
272
  ctx.addIssue({
@@ -258,6 +275,13 @@ var authTokenTunnelGrantSchema = z3.object({
258
275
  path: ["projects"]
259
276
  });
260
277
  }
278
+ if (grant.workspaces === void 0 && grant.projects === void 0) {
279
+ ctx.addIssue({
280
+ code: z3.ZodIssueCode.custom,
281
+ message: "A grant must target workspaces or projects.",
282
+ path: ["workspaces"]
283
+ });
284
+ }
261
285
  });
262
286
  var authTokenCommonSchema = {
263
287
  aud: z3.union([z3.string(), z3.array(z3.string())]).optional(),
@@ -293,10 +317,9 @@ var authTokenSchema = z3.union([
293
317
  ]);
294
318
  var tokenSchema = authTokenSchema;
295
319
  var createAuthTokenParamsSchema = z3.object({
296
- expires_in: z3.number().default(60),
297
- // 1 minute
320
+ expires_in: z3.number().int().min(1).max(3600).default(60),
298
321
  scopes: authTokenScopesSchema.optional(),
299
- tunnelsGrants: z3.array(authTokenTunnelGrantSchema).optional(),
322
+ tunnelsGrants: z3.array(authTokenTunnelGrantSchema).min(1).optional(),
300
323
  metadata: z3.unknown().optional()
301
324
  // Additional metadata
302
325
  }).strict().superRefine((params, ctx) => {
@@ -307,6 +330,13 @@ var createAuthTokenParamsSchema = z3.object({
307
330
  path: ["tunnelsGrants"]
308
331
  });
309
332
  }
333
+ if (params.scopes === void 0 && params.tunnelsGrants === void 0) {
334
+ ctx.addIssue({
335
+ code: z3.ZodIssueCode.custom,
336
+ message: "Explicit scopes or tunnelsGrants are required.",
337
+ path: ["scopes"]
338
+ });
339
+ }
310
340
  });
311
341
  var createAuthTokenResponseSchema = z3.object({
312
342
  token: z3.string()
@@ -333,12 +363,12 @@ var RstreamAuthResource = class {
333
363
  }
334
364
  };
335
365
  function createAuthTokenFromClientCredentials(credentials, params, options) {
336
- const tokenParams = createAuthTokenParamsSchema.parse(params ?? {});
366
+ const tokenParams = createAuthTokenParamsSchema.parse(params);
337
367
  const payload = {
338
368
  metadata: {
339
369
  engine: options?.engine
340
370
  },
341
- tunnelsGrants: normalizeAuthTokenTunnelGrants(tokenParams),
371
+ tunnelsGrants: normalizeAuthTokenTunnelGrants(tokenParams, options),
342
372
  permissions: null
343
373
  };
344
374
  const { token } = (0, import_rstream.createClientCredentialsToken)(credentials, {
@@ -347,19 +377,36 @@ function createAuthTokenFromClientCredentials(credentials, params, options) {
347
377
  });
348
378
  return { token };
349
379
  }
350
- function normalizeAuthTokenTunnelGrants(params) {
351
- if (!params) {
352
- return void 0;
353
- }
380
+ function normalizeAuthTokenTunnelGrants(params, options) {
354
381
  const tunnelsGrants = params.tunnelsGrants;
355
382
  if (tunnelsGrants !== void 0) {
356
383
  return [...tunnelsGrants];
357
384
  }
358
385
  if (params.scopes) {
359
- return [{ scopes: params.scopes }];
386
+ return [{ ...normalizeTunnelGrantTarget(options), scopes: params.scopes }];
360
387
  }
361
388
  return void 0;
362
389
  }
390
+ function normalizeTunnelGrantTarget(options) {
391
+ const projectId = normalizeOptionalString(options?.projectId);
392
+ const workspaceId = normalizeOptionalString(options?.workspaceId);
393
+ if (projectId !== void 0 && workspaceId !== void 0) {
394
+ throw new Error("Use either projectId or workspaceId, not both.");
395
+ }
396
+ if (projectId !== void 0) {
397
+ return { projects: [projectId] };
398
+ }
399
+ if (workspaceId !== void 0) {
400
+ return { workspaces: [workspaceId] };
401
+ }
402
+ throw new Error(
403
+ "Project ID or workspace ID is required when using scope-only tunnel grants."
404
+ );
405
+ }
406
+ function normalizeOptionalString(value) {
407
+ const normalized = value?.trim();
408
+ return normalized ? normalized : void 0;
409
+ }
363
410
 
364
411
  // src/client.ts
365
412
  var z4 = __toESM(require("zod"));
@@ -400,6 +447,13 @@ var listClientsParamsSchema = z4.object({
400
447
  var listClientsResponseSchema = z4.array(clientSchema);
401
448
 
402
449
  // src/clients-resource.ts
450
+ function clientPathId(id) {
451
+ const normalized = id.trim();
452
+ if (!normalized) {
453
+ throw new Error("Client ID is required.");
454
+ }
455
+ return encodeURIComponent(normalized);
456
+ }
403
457
  var RstreamClientsResource = class {
404
458
  client;
405
459
  constructor(client) {
@@ -413,9 +467,12 @@ var RstreamClientsResource = class {
413
467
  return listClientsResponseSchema.parse(response);
414
468
  }
415
469
  async get(id) {
416
- const response = await this.client.request(`/clients/${id}`, {
417
- method: "GET"
418
- });
470
+ const response = await this.client.request(
471
+ `/clients/${clientPathId(id)}`,
472
+ {
473
+ method: "GET"
474
+ }
475
+ );
419
476
  return clientSchema.parse(response);
420
477
  }
421
478
  };
@@ -831,12 +888,6 @@ var engineOpenApiDocument = {
831
888
  scheme: "bearer",
832
889
  bearerFormat: "rstream token",
833
890
  description: "Preferred authentication mechanism. Send an OAuth-issued rstream access token or compatible rstream credential in Authorization: Bearer <token>."
834
- },
835
- queryToken: {
836
- type: "apiKey",
837
- in: "query",
838
- name: "rstream.token",
839
- description: "Fallback authentication mechanism for transports where setting Authorization is impractical. Prefer bearerAuth whenever possible."
840
891
  }
841
892
  },
842
893
  parameters: {
@@ -956,7 +1007,7 @@ var engineOpenApiDocument = {
956
1007
  summary: "List visible engine clients.",
957
1008
  operationId: "list_engine_clients",
958
1009
  parameters: [{ $ref: "#/components/parameters/ListClientsParams" }],
959
- security: [{ bearerAuth: [] }, { queryToken: [] }],
1010
+ security: [{ bearerAuth: [] }],
960
1011
  responses: {
961
1012
  "200": jsonArrayResponse(
962
1013
  "Visible clients.",
@@ -975,7 +1026,7 @@ var engineOpenApiDocument = {
975
1026
  summary: "List visible engine tunnels.",
976
1027
  operationId: "list_engine_tunnels",
977
1028
  parameters: [{ $ref: "#/components/parameters/ListTunnelsParams" }],
978
- security: [{ bearerAuth: [] }, { queryToken: [] }],
1029
+ security: [{ bearerAuth: [] }],
979
1030
  responses: {
980
1031
  "200": jsonArrayResponse(
981
1032
  "Visible tunnels. Fine-grained list scopes may filter objects and project fields.",
@@ -995,7 +1046,7 @@ var engineOpenApiDocument = {
995
1046
  summary: "Subscribe to visible engine state through Server-Sent Events.",
996
1047
  operationId: "watch_engine_events_sse",
997
1048
  parameters: [{ $ref: "#/components/parameters/WatchParams" }],
998
- security: [{ bearerAuth: [] }, { queryToken: [] }],
1049
+ security: [{ bearerAuth: [] }],
999
1050
  responses: {
1000
1051
  "200": streamResponse(
1001
1052
  "SSE stream. Each event is emitted as a data line containing a JSON Event object."
@@ -1014,7 +1065,7 @@ var engineOpenApiDocument = {
1014
1065
  summary: "Subscribe to visible engine state through a WebSocket upgrade.",
1015
1066
  operationId: "watch_engine_events_websocket",
1016
1067
  parameters: [{ $ref: "#/components/parameters/WatchParams" }],
1017
- security: [{ bearerAuth: [] }, { queryToken: [] }],
1068
+ security: [{ bearerAuth: [] }],
1018
1069
  responses: {
1019
1070
  "101": {
1020
1071
  description: "WebSocket connection accepted. Frames contain JSON Event objects."
@@ -1042,22 +1093,37 @@ var import_rstream4 = require("@rstreamlabs/rstream");
1042
1093
  var import_rstream5 = require("@rstreamlabs/rstream");
1043
1094
  var import_rstream6 = require("@rstreamlabs/rstream");
1044
1095
  var import_rstream7 = require("@rstreamlabs/rstream");
1045
- var import_jsonwebtoken = __toESM(require("jsonwebtoken"));
1046
1096
  function trimOptionalString(value) {
1047
1097
  const normalized = value?.trim();
1048
1098
  return normalized ? normalized : void 0;
1049
1099
  }
1050
- function getEngineFromToken(token) {
1051
- if (!token) {
1052
- return void 0;
1100
+ var dnsLabelPattern = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i;
1101
+ function validateHostname(hostname) {
1102
+ if (!hostname || hostname.length > 253 || hostname.includes("..")) {
1103
+ return false;
1053
1104
  }
1054
- const parsed = authTokenSchema.safeParse(
1055
- import_jsonwebtoken.default.decode(token, { complete: false })
1056
- );
1057
- if (!parsed.success) {
1058
- return void 0;
1105
+ return hostname.split(".").every((label) => label === "localhost" || dnsLabelPattern.test(label));
1106
+ }
1107
+ function normalizeEngineAddress(engine) {
1108
+ const normalized = engine.trim();
1109
+ if (!normalized || /^[a-z][a-z0-9+.-]*:\/\//i.test(normalized) || /[/?#@\\\s]/.test(normalized)) {
1110
+ throw new Error("Engine must be a host[:port] value.");
1111
+ }
1112
+ const explicitPort = normalized.match(/:(\d+)$/)?.[1];
1113
+ const url = new URL(`https://${normalized}`);
1114
+ if (url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
1115
+ throw new Error("Engine must be a host[:port] value.");
1116
+ }
1117
+ if (!validateHostname(url.hostname)) {
1118
+ throw new Error("Engine must use a valid hostname.");
1119
+ }
1120
+ if (explicitPort) {
1121
+ const port = Number(explicitPort);
1122
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
1123
+ throw new Error("Engine port must be an integer between 1 and 65535.");
1124
+ }
1059
1125
  }
1060
- return trimOptionalString(parsed.data.metadata?.engine);
1126
+ return `${url.hostname.toLowerCase()}${explicitPort ? `:${explicitPort}` : ""}`;
1061
1127
  }
1062
1128
  function resolveTunnelsAPIURL(apiUrl) {
1063
1129
  return trimOptionalString(apiUrl) ?? (0, import_rstream6.readEnvironment)().apiUrl ?? import_rstream3.DEFAULT_API_URL;
@@ -1091,37 +1157,22 @@ function resolveControlPlaneCredentials(controlPlaneCredentials, credentials, au
1091
1157
  async function resolveTunnelsEngine(config) {
1092
1158
  const explicitEngine = trimOptionalString(config.engine);
1093
1159
  if (explicitEngine) {
1094
- return explicitEngine;
1160
+ return normalizeEngineAddress(explicitEngine);
1095
1161
  }
1096
1162
  const environmentEngine = (0, import_rstream6.readEnvironment)().engine;
1097
1163
  if (environmentEngine) {
1098
- return environmentEngine;
1164
+ return normalizeEngineAddress(environmentEngine);
1099
1165
  }
1100
1166
  const token = trimOptionalString(config.token) ?? ((0, import_rstream5.isTokenCredentials)(config.credentials) ? config.credentials.token : void 0);
1101
- const tokenEngine = getEngineFromToken(token);
1102
- if (tokenEngine) {
1103
- return tokenEngine;
1104
- }
1105
1167
  const projectEndpoint = trimOptionalString(config.projectEndpoint);
1106
1168
  if (projectEndpoint) {
1107
- const credentials = resolveControlPlaneCredentials(
1108
- config.controlPlaneCredentials,
1109
- config.credentials,
1169
+ const project = await resolveManagedTunnelsProject({
1170
+ ...config,
1110
1171
  token
1111
- );
1112
- if (credentials === void 0) {
1113
- throw new Error(
1114
- "Control-plane credentials are required to resolve a managed project endpoint."
1115
- );
1116
- }
1117
- const controlPlane = new import_rstream7.RstreamClient({
1118
- apiUrl: resolveTunnelsAPIURL(config.apiUrl),
1119
- credentials
1120
1172
  });
1121
- const project = await controlPlane.tunnels.projects.resolveByEndpoint(projectEndpoint);
1122
1173
  const engine = (0, import_rstream4.getTunnelsProjectEngine)(project);
1123
1174
  if (engine) {
1124
- return engine;
1175
+ return normalizeEngineAddress(engine);
1125
1176
  }
1126
1177
  throw new Error(
1127
1178
  "Failed to resolve the engine address from the managed tunnels project."
@@ -1131,8 +1182,36 @@ async function resolveTunnelsEngine(config) {
1131
1182
  "Engine URL is not defined. Provide an engine, use RSTREAM_ENGINE, or configure a managed project endpoint with control-plane access."
1132
1183
  );
1133
1184
  }
1185
+ async function resolveManagedTunnelsProject(config) {
1186
+ const projectEndpoint = trimOptionalString(config.projectEndpoint);
1187
+ if (!projectEndpoint) {
1188
+ throw new Error("Managed project endpoint is required.");
1189
+ }
1190
+ const credentials = resolveControlPlaneCredentials(
1191
+ config.controlPlaneCredentials,
1192
+ config.credentials,
1193
+ config.token
1194
+ );
1195
+ if (credentials === void 0) {
1196
+ throw new Error(
1197
+ "Control-plane credentials are required to resolve a managed project endpoint."
1198
+ );
1199
+ }
1200
+ const controlPlane = new import_rstream7.RstreamClient({
1201
+ apiUrl: resolveTunnelsAPIURL(config.apiUrl),
1202
+ credentials
1203
+ });
1204
+ return await controlPlane.tunnels.projects.resolveByEndpoint(projectEndpoint);
1205
+ }
1134
1206
 
1135
1207
  // src/tunnels-resource.ts
1208
+ function tunnelPathId(id) {
1209
+ const normalized = id.trim();
1210
+ if (!normalized) {
1211
+ throw new Error("Tunnel ID is required.");
1212
+ }
1213
+ return encodeURIComponent(normalized);
1214
+ }
1136
1215
  var RstreamTunnelsResource = class {
1137
1216
  client;
1138
1217
  constructor(client) {
@@ -1146,15 +1225,20 @@ var RstreamTunnelsResource = class {
1146
1225
  return listTunnelsResponseSchema.parse(response);
1147
1226
  }
1148
1227
  async get(id) {
1149
- const response = await this.client.request(`/tunnels/${id}`, {
1150
- method: "GET"
1151
- });
1228
+ const response = await this.client.request(
1229
+ `/tunnels/${tunnelPathId(id)}`,
1230
+ {
1231
+ method: "GET"
1232
+ }
1233
+ );
1152
1234
  return tunnelSchema.parse(response);
1153
1235
  }
1154
1236
  };
1155
1237
 
1156
1238
  // src/tunnels.ts
1157
1239
  var import_rstream12 = require("@rstreamlabs/rstream");
1240
+ var import_rstream13 = require("@rstreamlabs/rstream");
1241
+ var import_rstream14 = require("@rstreamlabs/rstream");
1158
1242
 
1159
1243
  // src/turn.ts
1160
1244
  var import_rstream8 = require("@rstreamlabs/rstream");
@@ -1165,46 +1249,52 @@ var z8 = __toESM(require("zod"));
1165
1249
  var import_crypto = __toESM(require("crypto"));
1166
1250
  var defaultTURNPort = 3478;
1167
1251
  var defaultTURNSPort = 5349;
1168
- var defaultTURNCredentialTTLSeconds = 24 * 60 * 60;
1252
+ var defaultTURNCredentialTTLSeconds = 10 * 60;
1253
+ var maxTURNCredentialTTLSeconds = 60 * 60;
1169
1254
  var turnTokenClaimsSchema = z8.object({
1170
- token_endpoint: z8.string().optional(),
1171
- type: z8.string()
1255
+ exp: z8.number().int().optional(),
1256
+ iat: z8.number().int().optional(),
1257
+ token_endpoint: z8.string().regex(/^[0-9a-f]{8}$/i).optional(),
1258
+ type: z8.enum(["app", "auth", "pat"])
1172
1259
  });
1173
1260
  var turnCredentialModeSchema = z8.enum(["api", "app", "pat"]);
1174
- function normalizeOptionalString(value) {
1261
+ function normalizeOptionalString2(value) {
1175
1262
  const normalized = value?.trim();
1176
1263
  return normalized ? normalized : void 0;
1177
1264
  }
1178
1265
  function normalizeClusterDomain(clusterDomain) {
1179
- const normalized = normalizeOptionalString(clusterDomain);
1266
+ const normalized = normalizeOptionalString2(clusterDomain);
1180
1267
  return normalized?.toLowerCase();
1181
1268
  }
1182
1269
  function normalizeNow(now) {
1183
- if (now instanceof Date) {
1184
- return Math.floor(now.getTime() / 1e3);
1270
+ if (now === void 0) {
1271
+ return Math.floor(Date.now() / 1e3);
1185
1272
  }
1186
- if (typeof now === "number" && Number.isFinite(now)) {
1187
- return Math.floor(now);
1273
+ const timestamp = now instanceof Date ? now.getTime() / 1e3 : now;
1274
+ if (!Number.isFinite(timestamp)) {
1275
+ throw new Error("TURN timestamp must be finite.");
1188
1276
  }
1189
- return Math.floor(Date.now() / 1e3);
1277
+ return Math.floor(timestamp);
1190
1278
  }
1191
1279
  function normalizeTTLSeconds(ttlSeconds) {
1192
- if (typeof ttlSeconds === "number" && Number.isFinite(ttlSeconds) && ttlSeconds > 0) {
1193
- return Math.floor(ttlSeconds);
1280
+ const ttl = ttlSeconds ?? defaultTURNCredentialTTLSeconds;
1281
+ if (!Number.isInteger(ttl) || ttl < 1 || ttl > maxTURNCredentialTTLSeconds) {
1282
+ throw new Error("TURN TTL must be an integer between 1 and 3600 seconds.");
1194
1283
  }
1195
- return defaultTURNCredentialTTLSeconds;
1284
+ return ttl;
1196
1285
  }
1197
- function normalizeTURNPort(port) {
1198
- if (typeof port === "number" && Number.isFinite(port) && port > 0) {
1199
- return Math.floor(port);
1286
+ function normalizePort(port, fallback, name) {
1287
+ const normalized = port ?? fallback;
1288
+ if (!Number.isInteger(normalized) || normalized < 1 || normalized > 65535) {
1289
+ throw new Error(`${name} must be an integer between 1 and 65535.`);
1200
1290
  }
1201
- return defaultTURNPort;
1291
+ return normalized;
1292
+ }
1293
+ function normalizeTURNPort(port) {
1294
+ return normalizePort(port, defaultTURNPort, "TURN port");
1202
1295
  }
1203
1296
  function normalizeTURNSPort(port) {
1204
- if (typeof port === "number" && Number.isFinite(port) && port > 0) {
1205
- return Math.floor(port);
1206
- }
1207
- return defaultTURNSPort;
1297
+ return normalizePort(port, defaultTURNSPort, "TURNS port");
1208
1298
  }
1209
1299
  function createTURNURLs(clusterDomain, turnPort, turnsPort) {
1210
1300
  const normalizedTURNPort = normalizeTURNPort(turnPort);
@@ -1222,16 +1312,32 @@ function parseTURNTokenClaims(token) {
1222
1312
  if (!payloadPart) {
1223
1313
  throw new Error("Invalid token format.");
1224
1314
  }
1225
- const payload = Buffer.from(payloadPart, "base64url").toString("utf8");
1226
- const parsed = turnTokenClaimsSchema.safeParse(JSON.parse(payload));
1315
+ let decoded;
1316
+ try {
1317
+ const payload = Buffer.from(payloadPart, "base64url").toString("utf8");
1318
+ decoded = JSON.parse(payload);
1319
+ } catch {
1320
+ throw new Error("Invalid token format.");
1321
+ }
1322
+ const parsed = turnTokenClaimsSchema.safeParse(decoded);
1227
1323
  if (!parsed.success) {
1228
1324
  throw new Error("Invalid token format.");
1229
1325
  }
1230
1326
  return parsed.data;
1231
1327
  }
1328
+ function requirePATExpiration(claims, now) {
1329
+ const exp = claims.exp;
1330
+ if (typeof exp !== "number") {
1331
+ throw new Error("TURN PAT mode requires a PAT token with expiration.");
1332
+ }
1333
+ if (exp <= now) {
1334
+ throw new Error("TURN PAT mode requires a non-expired PAT token.");
1335
+ }
1336
+ return exp;
1337
+ }
1232
1338
  function deriveClusterDomainFromEngine(projectEndpoint, engine) {
1233
- const normalizedProjectEndpoint = normalizeOptionalString(projectEndpoint);
1234
- const normalizedEngine = normalizeOptionalString(engine);
1339
+ const normalizedProjectEndpoint = normalizeOptionalString2(projectEndpoint);
1340
+ const normalizedEngine = normalizeOptionalString2(engine);
1235
1341
  if (!normalizedProjectEndpoint || !normalizedEngine) {
1236
1342
  throw new Error(
1237
1343
  "Failed to derive the cluster domain from the engine address."
@@ -1265,17 +1371,11 @@ function resolveMode(credentials, mode) {
1265
1371
  if ((0, import_rstream8.isClientCredentials)(credentials)) {
1266
1372
  return "app";
1267
1373
  }
1268
- if ((0, import_rstream9.isTokenCredentials)(credentials)) {
1269
- const claims = parseTURNTokenClaims(credentials.token);
1270
- if (claims.type === "pat" && normalizeOptionalString(claims.token_endpoint)) {
1271
- return "pat";
1272
- }
1273
- }
1274
1374
  return "api";
1275
1375
  }
1276
1376
  async function loadTURNServerPublicKeyHex(clusterDomain, options) {
1277
- const configuredKey = normalizeOptionalString(options.keyringBaseUrl);
1278
- const keyringBaseURL = configuredKey ?? normalizeOptionalString(options.apiUrl) ?? "https://rstream.io";
1377
+ const configuredKey = normalizeOptionalString2(options.keyringBaseUrl);
1378
+ const keyringBaseURL = configuredKey ?? normalizeOptionalString2(options.apiUrl) ?? "https://rstream.io";
1279
1379
  const url = new URL(
1280
1380
  `/keyrings/turn/${clusterDomain}.spki.der.hex`,
1281
1381
  keyringBaseURL
@@ -1287,8 +1387,8 @@ async function loadTURNServerPublicKeyHex(clusterDomain, options) {
1287
1387
  return (await response.text()).trim();
1288
1388
  }
1289
1389
  async function createAPITURNCredentials(options) {
1290
- const projectId = normalizeOptionalString(options.projectId);
1291
- const projectEndpoint = normalizeOptionalString(options.projectEndpoint);
1390
+ const projectId = normalizeOptionalString2(options.projectId);
1391
+ const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
1292
1392
  if (!projectId && !projectEndpoint) {
1293
1393
  throw new Error(
1294
1394
  "Project ID or project endpoint is required for TURN API mode."
@@ -1315,11 +1415,11 @@ function createPATTURNCredentials(options) {
1315
1415
  if (!clusterDomain) {
1316
1416
  throw new Error("Cluster domain is required for TURN PAT mode.");
1317
1417
  }
1318
- const projectEndpoint = normalizeOptionalString(options.projectEndpoint);
1418
+ const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
1319
1419
  if (!projectEndpoint) {
1320
1420
  throw new Error("Project endpoint is required for TURN PAT mode.");
1321
1421
  }
1322
- const token = normalizeOptionalString(options.token);
1422
+ const token = normalizeOptionalString2(options.token);
1323
1423
  if (!token) {
1324
1424
  throw new Error("Token is required for TURN PAT mode.");
1325
1425
  }
@@ -1327,14 +1427,17 @@ function createPATTURNCredentials(options) {
1327
1427
  if (claims.type !== "pat") {
1328
1428
  throw new Error("TURN PAT mode requires a PAT token.");
1329
1429
  }
1330
- const tokenEndpoint = normalizeOptionalString(claims.token_endpoint);
1430
+ const tokenEndpoint = normalizeOptionalString2(options.tokenEndpoint);
1331
1431
  if (!tokenEndpoint) {
1332
- throw new Error(
1333
- "TURN PAT mode requires a PAT token carrying a token endpoint."
1334
- );
1432
+ throw new Error("Token endpoint is required for TURN PAT mode.");
1335
1433
  }
1434
+ if (claims.token_endpoint !== void 0 && claims.token_endpoint !== tokenEndpoint) {
1435
+ throw new Error("Token endpoint does not match the PAT token.");
1436
+ }
1437
+ const now = normalizeNow(options.now);
1336
1438
  const ttlSeconds = normalizeTTLSeconds(options.ttlSeconds);
1337
- const exp = normalizeNow(options.now) + ttlSeconds;
1439
+ const exp = Math.min(now + ttlSeconds, requirePATExpiration(claims, now));
1440
+ const ttl = exp - now;
1338
1441
  const username = `v1:${exp}:pat:${projectEndpoint}:${tokenEndpoint}`;
1339
1442
  const tokenHash = import_crypto.default.createHash("sha256").update(token, "utf8").digest();
1340
1443
  const key = Buffer.from(
@@ -1349,7 +1452,7 @@ function createPATTURNCredentials(options) {
1349
1452
  const credential = import_crypto.default.createHmac("sha256", key).update(username, "utf8").digest("base64");
1350
1453
  return import_rstream11.turnCredentialsSchema.parse({
1351
1454
  credential,
1352
- ttl: ttlSeconds,
1455
+ ttl,
1353
1456
  urls: createTURNURLs(clusterDomain, options.turnPort, options.turnsPort),
1354
1457
  username
1355
1458
  });
@@ -1359,15 +1462,15 @@ async function createAPPTURNCredentials(options) {
1359
1462
  if (!clusterDomain) {
1360
1463
  throw new Error("Cluster domain is required for TURN APP mode.");
1361
1464
  }
1362
- const projectEndpoint = normalizeOptionalString(options.projectEndpoint);
1465
+ const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
1363
1466
  if (!projectEndpoint) {
1364
1467
  throw new Error("Project endpoint is required for TURN APP mode.");
1365
1468
  }
1366
- const clientId = normalizeOptionalString(options.clientId);
1469
+ const clientId = normalizeOptionalString2(options.clientId);
1367
1470
  if (!clientId) {
1368
1471
  throw new Error("Client ID is required for TURN APP mode.");
1369
1472
  }
1370
- const clientSecret = normalizeOptionalString(options.clientSecret);
1473
+ const clientSecret = normalizeOptionalString2(options.clientSecret);
1371
1474
  if (!clientSecret) {
1372
1475
  throw new Error("Client secret is required for TURN APP mode.");
1373
1476
  }
@@ -1379,7 +1482,7 @@ async function createAPPTURNCredentials(options) {
1379
1482
  format: "der",
1380
1483
  type: "pkcs8"
1381
1484
  });
1382
- const serverPublicKeyHex = normalizeOptionalString(options.serverPublicKeyHex) ?? await loadTURNServerPublicKeyHex(clusterDomain, options);
1485
+ const serverPublicKeyHex = normalizeOptionalString2(options.serverPublicKeyHex) ?? await loadTURNServerPublicKeyHex(clusterDomain, options);
1383
1486
  const serverPublicKey = import_crypto.default.createPublicKey({
1384
1487
  key: Buffer.from(serverPublicKeyHex, "hex"),
1385
1488
  format: "der",
@@ -1409,7 +1512,7 @@ async function createAPPTURNCredentials(options) {
1409
1512
  async function createTURNCredentials(options) {
1410
1513
  const credentials = options.credentials;
1411
1514
  const mode = resolveMode(credentials, options.mode);
1412
- const projectEndpoint = normalizeOptionalString(options.projectEndpoint);
1515
+ const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
1413
1516
  if (mode === "api") {
1414
1517
  const controlPlaneCredentials = options.controlPlaneCredentials ?? credentials;
1415
1518
  if (!controlPlaneCredentials) {
@@ -1440,6 +1543,10 @@ async function createTURNCredentials(options) {
1440
1543
  "Project endpoint is required for TURN PAT mode."
1441
1544
  ),
1442
1545
  token: credentials.token,
1546
+ tokenEndpoint: requireOption(
1547
+ normalizeOptionalString2(options.tokenEndpoint),
1548
+ "Token endpoint is required for TURN PAT mode."
1549
+ ),
1443
1550
  ttlSeconds: options.ttlSeconds,
1444
1551
  turnPort: options.turnPort,
1445
1552
  turnsPort: options.turnsPort
@@ -1489,6 +1596,7 @@ var RstreamTURNResource = class {
1489
1596
  projectEndpoint: options.projectEndpoint ?? this.client.projectEndpoint,
1490
1597
  projectId: options.projectId,
1491
1598
  serverPublicKeyHex: options.serverPublicKeyHex,
1599
+ tokenEndpoint: options.tokenEndpoint,
1492
1600
  ttlSeconds: options.ttlSeconds,
1493
1601
  turnPort: options.turnPort,
1494
1602
  turnsPort: options.turnsPort
@@ -1498,6 +1606,41 @@ var RstreamTURNResource = class {
1498
1606
 
1499
1607
  // src/webhooks-resource.ts
1500
1608
  var import_crypto2 = __toESM(require("crypto"));
1609
+ var SIGNATURE_HEX_BYTES = 32;
1610
+ var signatureHexSchema = /^[0-9a-fA-F]{64}$/;
1611
+ var timestampSchema = /^\d+$/;
1612
+ function parseSignatureTimestamp(timestamp) {
1613
+ if (!timestampSchema.test(timestamp)) {
1614
+ throw new Error("Invalid signature timestamp");
1615
+ }
1616
+ const parsed = Number(timestamp);
1617
+ if (!Number.isSafeInteger(parsed)) {
1618
+ throw new Error("Invalid signature timestamp");
1619
+ }
1620
+ return parsed;
1621
+ }
1622
+ function normalizeTolerance(tolerance) {
1623
+ if (!Number.isFinite(tolerance) || !Number.isInteger(tolerance) || tolerance < 0) {
1624
+ throw new Error("Invalid signature tolerance");
1625
+ }
1626
+ return tolerance;
1627
+ }
1628
+ function readSignature(signature) {
1629
+ if (!signatureHexSchema.test(signature)) {
1630
+ return void 0;
1631
+ }
1632
+ const parsed = Buffer.from(signature, "hex");
1633
+ return parsed.length === SIGNATURE_HEX_BYTES ? parsed : void 0;
1634
+ }
1635
+ function signatureMatches(signature, expected) {
1636
+ const parsed = readSignature(signature);
1637
+ return parsed === void 0 ? false : import_crypto2.default.timingSafeEqual(parsed, expected);
1638
+ }
1639
+ function assertSecretConfigured(secret) {
1640
+ if (secret.trim().length === 0) {
1641
+ throw new Error("Webhook signing secret is required");
1642
+ }
1643
+ }
1501
1644
  var RstreamWebhookResource = class {
1502
1645
  async event(payload, header, secret, tolerance = 300, receivedAt = Date.now()) {
1503
1646
  const payloadBuffer = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
@@ -1505,31 +1648,26 @@ var RstreamWebhookResource = class {
1505
1648
  if (!signatureHeader) {
1506
1649
  throw new Error("No signature header");
1507
1650
  }
1651
+ assertSecretConfigured(secret);
1508
1652
  const elements = signatureHeader.split(",").map((element) => element.trim());
1509
1653
  const timestamp = elements.find((element) => element.startsWith("t="))?.substring(2);
1510
1654
  const signatures = elements.filter((element) => element.startsWith("v1=")).map((element) => element.substring(3));
1511
1655
  if (!timestamp || signatures.length === 0) {
1512
1656
  throw new Error("Invalid signature header format");
1513
1657
  }
1514
- const timestampNum = parseInt(timestamp, 10);
1658
+ const timestampNum = parseSignatureTimestamp(timestamp);
1659
+ const signatureTolerance = normalizeTolerance(tolerance);
1515
1660
  const now = Math.floor(receivedAt / 1e3);
1516
- if (Math.abs(now - timestampNum) > tolerance) {
1661
+ if (Math.abs(now - timestampNum) > signatureTolerance) {
1517
1662
  throw new Error(
1518
1663
  `Webhook signature timestamp outside tolerance: ${now}, ${timestampNum}`
1519
1664
  );
1520
1665
  }
1521
1666
  const signedPayload = `${timestamp}.${payloadBuffer.toString("utf8")}`;
1522
- const expectedSignature = import_crypto2.default.createHmac("sha256", secret).update(signedPayload).digest("hex");
1523
- const signatureMatched = signatures.some((signature) => {
1524
- try {
1525
- return import_crypto2.default.timingSafeEqual(
1526
- Buffer.from(signature),
1527
- Buffer.from(expectedSignature)
1528
- );
1529
- } catch {
1530
- return false;
1531
- }
1532
- });
1667
+ const expectedSignature = import_crypto2.default.createHmac("sha256", secret).update(signedPayload).digest();
1668
+ const signatureMatched = signatures.some(
1669
+ (signature) => signatureMatches(signature, expectedSignature)
1670
+ );
1533
1671
  if (!signatureMatched) {
1534
1672
  throw new Error("Signature verification failed");
1535
1673
  }
@@ -1547,8 +1685,16 @@ var RstreamWebhookResource = class {
1547
1685
  };
1548
1686
 
1549
1687
  // src/tunnels.ts
1688
+ var engineAuthTokenScopes = {
1689
+ tunnels: {
1690
+ connect: true,
1691
+ create: true,
1692
+ list: true
1693
+ }
1694
+ };
1550
1695
  var RstreamTunnelsClient = class {
1551
1696
  config;
1697
+ managedProject;
1552
1698
  constructor(config) {
1553
1699
  this.config = config;
1554
1700
  }
@@ -1568,6 +1714,21 @@ var RstreamTunnelsClient = class {
1568
1714
  return this.config?.projectEndpoint;
1569
1715
  }
1570
1716
  async getEngine() {
1717
+ const projectEndpoint = normalizeOptionalString3(
1718
+ this.config?.projectEndpoint
1719
+ );
1720
+ if (projectEndpoint !== void 0 && normalizeOptionalString3(this.config?.engine) === void 0 && (0, import_rstream14.readEnvironment)().engine === void 0) {
1721
+ const project = await this.getManagedProject();
1722
+ if (project !== void 0) {
1723
+ const engine = (0, import_rstream12.getTunnelsProjectEngine)(project);
1724
+ if (engine) {
1725
+ return normalizeEngineAddress(engine);
1726
+ }
1727
+ }
1728
+ throw new Error(
1729
+ "Failed to resolve the engine address from the managed tunnels project."
1730
+ );
1731
+ }
1571
1732
  return await resolveTunnelsEngine({
1572
1733
  apiUrl: this.config?.apiUrl,
1573
1734
  controlPlaneCredentials: this.controlPlaneCredentials,
@@ -1576,6 +1737,57 @@ var RstreamTunnelsClient = class {
1576
1737
  projectEndpoint: this.config?.projectEndpoint
1577
1738
  });
1578
1739
  }
1740
+ async getManagedProject() {
1741
+ const projectEndpoint = normalizeOptionalString3(
1742
+ this.config?.projectEndpoint
1743
+ );
1744
+ if (projectEndpoint === void 0) {
1745
+ return void 0;
1746
+ }
1747
+ if (this.managedProject === void 0) {
1748
+ this.managedProject = resolveManagedTunnelsProject({
1749
+ apiUrl: this.config?.apiUrl,
1750
+ controlPlaneCredentials: this.controlPlaneCredentials,
1751
+ credentials: this.credentials,
1752
+ projectEndpoint
1753
+ }).catch((error) => {
1754
+ this.managedProject = void 0;
1755
+ throw error;
1756
+ });
1757
+ }
1758
+ return await this.managedProject;
1759
+ }
1760
+ async getEngineAuthTokenParams() {
1761
+ const target = await this.getEngineAuthTokenTarget();
1762
+ return {
1763
+ tunnelsGrants: [
1764
+ {
1765
+ ...target,
1766
+ scopes: engineAuthTokenScopes
1767
+ }
1768
+ ]
1769
+ };
1770
+ }
1771
+ async getEngineAuthTokenTarget() {
1772
+ const projectId = normalizeOptionalString3(this.config?.projectId);
1773
+ const workspaceId = normalizeOptionalString3(this.config?.workspaceId);
1774
+ if (projectId !== void 0 && workspaceId !== void 0) {
1775
+ throw new Error("Use either projectId or workspaceId, not both.");
1776
+ }
1777
+ if (projectId !== void 0) {
1778
+ return { projects: [projectId] };
1779
+ }
1780
+ if (workspaceId !== void 0) {
1781
+ return { workspaces: [workspaceId] };
1782
+ }
1783
+ const project = await this.getManagedProject();
1784
+ if (project !== void 0) {
1785
+ return { projects: [project.id] };
1786
+ }
1787
+ throw new Error(
1788
+ "Application credentials require projectId, workspaceId, or projectEndpoint to create scoped engine tokens."
1789
+ );
1790
+ }
1579
1791
  get auth() {
1580
1792
  return new RstreamAuthResource(this);
1581
1793
  }
@@ -1596,16 +1808,20 @@ var RstreamTunnelsClient = class {
1596
1808
  if (!credentials) {
1597
1809
  return void 0;
1598
1810
  }
1599
- if (!(0, import_rstream12.isClientCredentials)(credentials)) {
1811
+ if (!(0, import_rstream13.isClientCredentials)(credentials)) {
1600
1812
  return credentials.token;
1601
1813
  }
1602
- return (await this.auth.createAuthToken(void 0, {
1814
+ const tokenParams = await this.getEngineAuthTokenParams();
1815
+ return (await this.auth.createAuthToken(tokenParams, {
1603
1816
  credentials,
1604
1817
  engine
1605
1818
  })).token;
1606
1819
  }
1607
1820
  async request(path, options) {
1608
- const engine = await this.getEngine();
1821
+ const engine = normalizeEngineAddress(await this.getEngine());
1822
+ if (!path.startsWith("/") || path.startsWith("//")) {
1823
+ throw new Error("Engine request path must be a relative absolute path.");
1824
+ }
1609
1825
  const url = `https://${engine}/api${path}`;
1610
1826
  const headers = new Headers(options?.headers || {});
1611
1827
  const token = await this.getToken(engine);
@@ -1614,7 +1830,8 @@ var RstreamTunnelsClient = class {
1614
1830
  }
1615
1831
  const response = await fetch(url, {
1616
1832
  ...options,
1617
- headers
1833
+ headers,
1834
+ redirect: options?.redirect ?? "manual"
1618
1835
  });
1619
1836
  if (!response.ok) {
1620
1837
  const errorText = await response.text();
@@ -1623,8 +1840,68 @@ var RstreamTunnelsClient = class {
1623
1840
  return await response.json();
1624
1841
  }
1625
1842
  };
1843
+ function normalizeOptionalString3(value) {
1844
+ const normalized = value?.trim();
1845
+ return normalized ? normalized : void 0;
1846
+ }
1626
1847
 
1627
1848
  // src/watch.ts
1849
+ var import_jsonwebtoken = __toESM(require("jsonwebtoken"));
1850
+ var maxWatchTokenLifetimeSeconds = 3600;
1851
+ var watchTokenIssuedAtSkewSeconds = 300;
1852
+ function normalizeTransport(transport) {
1853
+ if (transport === void 0) {
1854
+ return "sse";
1855
+ }
1856
+ if (transport === "sse" || transport === "websocket") {
1857
+ return transport;
1858
+ }
1859
+ throw new Error("Watch: Unsupported transport.");
1860
+ }
1861
+ function validateWatchQueryToken(token) {
1862
+ const parsed = authTokenSchema.safeParse(import_jsonwebtoken.default.decode(token));
1863
+ if (!parsed.success || parsed.data.type === "pat") {
1864
+ throw new Error(
1865
+ "Watch requires a short-lived auth or app token for URL-based authentication."
1866
+ );
1867
+ }
1868
+ const iat = parsed.data.iat;
1869
+ const exp = parsed.data.exp;
1870
+ const now = Math.floor(Date.now() / 1e3);
1871
+ 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) {
1872
+ throw new Error(
1873
+ "Watch requires a token with a bounded lifetime of at most 3600 seconds."
1874
+ );
1875
+ }
1876
+ if (!hasWatchOnlyTunnelGrants(parsed.data)) {
1877
+ throw new Error(
1878
+ "Watch requires a fine-grained token limited to tunnel list grants for URL-based authentication."
1879
+ );
1880
+ }
1881
+ }
1882
+ function hasWatchOnlyTunnelGrants(token) {
1883
+ if (!hasWatchOnlyPermissions(token.permissions)) {
1884
+ return false;
1885
+ }
1886
+ if (!Array.isArray(token.tunnelsGrants) || token.tunnelsGrants.length === 0) {
1887
+ return false;
1888
+ }
1889
+ return token.tunnelsGrants.every((grant) => {
1890
+ const tunnels = grant.scopes?.tunnels;
1891
+ if (tunnels === void 0) {
1892
+ return false;
1893
+ }
1894
+ return tunnels.list !== void 0 && tunnels.create === void 0 && tunnels.connect === void 0;
1895
+ });
1896
+ }
1897
+ function hasWatchOnlyPermissions(permissions) {
1898
+ if (!Array.isArray(permissions)) {
1899
+ return true;
1900
+ }
1901
+ return permissions.every(
1902
+ (permission) => permission === "tunnels.resources.read-only" || permission === "network.resources.read-only"
1903
+ );
1904
+ }
1628
1905
  var Watch = class {
1629
1906
  connection = null;
1630
1907
  connectionState = "preparing";
@@ -1639,45 +1916,56 @@ var Watch = class {
1639
1916
  throw new Error("Watch: Connection already started or closed.");
1640
1917
  }
1641
1918
  this.connectionState = "connecting";
1642
- const token = typeof this.config.auth === "function" ? await this.config.auth() : this.config.auth;
1643
- const engine = await resolveTunnelsEngine({
1644
- apiUrl: this.config.apiUrl,
1645
- controlPlaneCredentials: this.config.controlPlaneCredentials,
1646
- engine: this.config.engine,
1647
- projectEndpoint: this.config.projectEndpoint,
1648
- token
1649
- });
1650
- const transport = this.config.transport ?? "sse";
1651
- const base = `https://${engine}`;
1652
- if (transport === "sse") {
1653
- const url = new URL(`/api/sse`, base);
1654
- url.searchParams.set("rstream.token", token);
1655
- this.connection = new EventSource(url.toString());
1656
- } else {
1657
- const url = new URL(`/api/websocket`, base);
1658
- url.searchParams.set("rstream.token", token);
1659
- url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
1660
- this.connection = new WebSocket(url.toString());
1661
- }
1662
- this.connection.onopen = () => {
1663
- this.connectionState = "connected";
1664
- this.events.onConnect?.();
1665
- };
1666
- this.connection.onmessage = (msg) => {
1667
- const parsed = wsEventsSchema.parse(JSON.parse(msg.data));
1668
- this.events.onEvent?.(parsed);
1669
- };
1670
- this.connection.onerror = () => {
1671
- if (this.connectionState !== "closed") {
1672
- this.disconnect();
1919
+ try {
1920
+ const token = typeof this.config.auth === "function" ? await this.config.auth() : this.config.auth;
1921
+ validateWatchQueryToken(token);
1922
+ const engine = await resolveTunnelsEngine({
1923
+ apiUrl: this.config.apiUrl,
1924
+ controlPlaneCredentials: this.config.controlPlaneCredentials,
1925
+ engine: this.config.engine,
1926
+ projectEndpoint: this.config.projectEndpoint,
1927
+ token
1928
+ });
1929
+ const transport = normalizeTransport(this.config.transport);
1930
+ const base = `https://${engine}`;
1931
+ if (transport === "sse") {
1932
+ const url = new URL(`/api/sse`, base);
1933
+ url.searchParams.set("rstream.token", token);
1934
+ this.connection = new EventSource(url.toString());
1935
+ } else {
1936
+ const url = new URL(`/api/websocket`, base);
1937
+ url.searchParams.set("rstream.token", token);
1938
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
1939
+ this.connection = new WebSocket(url.toString());
1673
1940
  }
1674
- };
1675
- if (this.connection instanceof WebSocket) {
1676
- this.connection.onclose = () => {
1941
+ this.connection.onopen = () => {
1942
+ this.connectionState = "connected";
1943
+ this.events.onConnect?.();
1944
+ };
1945
+ this.connection.onmessage = (msg) => this.handleMessage(msg.data);
1946
+ this.connection.onerror = () => {
1677
1947
  if (this.connectionState !== "closed") {
1678
1948
  this.disconnect();
1679
1949
  }
1680
1950
  };
1951
+ if (this.connection instanceof WebSocket) {
1952
+ this.connection.onclose = () => {
1953
+ if (this.connectionState !== "closed") {
1954
+ this.disconnect();
1955
+ }
1956
+ };
1957
+ }
1958
+ } catch (error) {
1959
+ this.connectionState = "preparing";
1960
+ throw error;
1961
+ }
1962
+ }
1963
+ handleMessage(data) {
1964
+ try {
1965
+ const parsed = wsEventsSchema.parse(JSON.parse(data));
1966
+ this.events.onEvent?.(parsed);
1967
+ } catch {
1968
+ this.disconnect();
1681
1969
  }
1682
1970
  }
1683
1971
  disconnect() {
@@ -1798,8 +2086,10 @@ function parseWebTTYServers(tunnels) {
1798
2086
  listClientsResponseSchema,
1799
2087
  listTunnelsParamsSchema,
1800
2088
  listTunnelsResponseSchema,
2089
+ normalizeEngineAddress,
1801
2090
  parseWebTTYServers,
1802
2091
  resolveControlPlaneCredentials,
2092
+ resolveManagedTunnelsProject,
1803
2093
  resolveTunnelsAPIURL,
1804
2094
  resolveTunnelsCredentials,
1805
2095
  resolveTunnelsEngine,