@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.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
@@ -160,8 +162,7 @@ var tunnelSchema = z2.object({
160
162
  tls_alpns: z2.array(z2.string()).optional(),
161
163
  tls_min_version: z2.string().optional(),
162
164
  tls_ciphers: z2.array(z2.string()).optional(),
163
- mtls: z2.boolean().optional(),
164
- mtls_ca_cert_pem: z2.string().optional(),
165
+ mtls_auth: z2.boolean().optional(),
165
166
  http_version: z2.enum(["http/1.1", "h2", "h2c", "h3"]).optional(),
166
167
  http_use_tls: z2.boolean().optional(),
167
168
  upstream_tls: z2.boolean().optional(),
@@ -219,7 +220,7 @@ var authTokenTunnelsScopesSchema = z3.object({
219
220
  creation_date: true
220
221
  })
221
222
  ).optional()
222
- })
223
+ }).strict()
223
224
  ]).optional(),
224
225
  // Scopes for connecting to tunnels
225
226
  connect: z3.union([
@@ -231,7 +232,7 @@ var authTokenTunnelsScopesSchema = z3.object({
231
232
  path: z3.string().optional()
232
233
  })
233
234
  ).optional()
234
- })
235
+ }).strict()
235
236
  ]).optional(),
236
237
  // Scopes for listing tunnels
237
238
  list: z3.union([
@@ -239,14 +240,29 @@ var authTokenTunnelsScopesSchema = z3.object({
239
240
  z3.object({
240
241
  filters: filters(tunnelSchema).optional(),
241
242
  select: select(tunnelSchema).optional()
242
- })
243
+ }).strict()
243
244
  ]).optional()
245
+ }).strict().superRefine((scopes, ctx) => {
246
+ if (scopes.create === void 0 && scopes.connect === void 0 && scopes.list === void 0) {
247
+ ctx.addIssue({
248
+ code: z3.ZodIssueCode.custom,
249
+ message: "At least one tunnel scope action is required."
250
+ });
251
+ }
244
252
  });
245
253
  var authTokenScopesSchema = z3.object({
246
254
  tunnels: authTokenTunnelsScopesSchema.optional()
247
255
  // Scopes related to tunnels
256
+ }).strict().superRefine((scopes, ctx) => {
257
+ if (scopes.tunnels === void 0) {
258
+ ctx.addIssue({
259
+ code: z3.ZodIssueCode.custom,
260
+ message: "Tunnel scopes are required.",
261
+ path: ["tunnels"]
262
+ });
263
+ }
248
264
  });
249
- var authTokenTunnelGrantSchema = z3.object({
265
+ var authTokenTunnelGrantLeafSchema = z3.object({
250
266
  workspaces: z3.array(nonEmptyStringSchema).min(1).optional(),
251
267
  projects: z3.array(nonEmptyStringSchema).min(1).optional(),
252
268
  scopes: authTokenScopesSchema.optional()
@@ -258,11 +274,25 @@ var authTokenTunnelGrantSchema = z3.object({
258
274
  path: ["projects"]
259
275
  });
260
276
  }
277
+ if (grant.workspaces === void 0 && grant.projects === void 0 && grant.scopes === void 0) {
278
+ ctx.addIssue({
279
+ code: z3.ZodIssueCode.custom,
280
+ message: "A grant must target workspaces, projects, or scopes.",
281
+ path: ["workspaces"]
282
+ });
283
+ }
261
284
  });
285
+ var authTokenTunnelGrantSchema = z3.lazy(
286
+ () => z3.union([
287
+ authTokenTunnelGrantLeafSchema,
288
+ z3.object({ AND: z3.array(authTokenTunnelGrantSchema).min(1) }).strict(),
289
+ z3.object({ OR: z3.array(authTokenTunnelGrantSchema).min(1) }).strict()
290
+ ])
291
+ );
262
292
  var authTokenCommonSchema = {
263
293
  aud: z3.union([z3.string(), z3.array(z3.string())]).optional(),
264
294
  exp: z3.number().optional(),
265
- tunnelsGrants: z3.array(authTokenTunnelGrantSchema).optional(),
295
+ tunnelsGrants: authTokenTunnelGrantSchema.optional(),
266
296
  iat: z3.number().optional(),
267
297
  iss: z3.string().optional(),
268
298
  jti: z3.string().optional(),
@@ -281,6 +311,7 @@ var authTokenSchema = z3.union([
281
311
  }).strict(),
282
312
  z3.object({
283
313
  type: z3.literal("pat"),
314
+ tokendpoint: z3.string().optional(),
284
315
  token_endpoint: z3.string().optional(),
285
316
  ...authTokenCommonSchema
286
317
  }).strict(),
@@ -293,10 +324,9 @@ var authTokenSchema = z3.union([
293
324
  ]);
294
325
  var tokenSchema = authTokenSchema;
295
326
  var createAuthTokenParamsSchema = z3.object({
296
- expires_in: z3.number().default(60),
297
- // 1 minute
327
+ expires_in: z3.number().int().min(1).max(3600).default(60),
298
328
  scopes: authTokenScopesSchema.optional(),
299
- tunnelsGrants: z3.array(authTokenTunnelGrantSchema).optional(),
329
+ tunnelsGrants: authTokenTunnelGrantSchema.optional(),
300
330
  metadata: z3.unknown().optional()
301
331
  // Additional metadata
302
332
  }).strict().superRefine((params, ctx) => {
@@ -307,6 +337,13 @@ var createAuthTokenParamsSchema = z3.object({
307
337
  path: ["tunnelsGrants"]
308
338
  });
309
339
  }
340
+ if (params.scopes === void 0 && params.tunnelsGrants === void 0) {
341
+ ctx.addIssue({
342
+ code: z3.ZodIssueCode.custom,
343
+ message: "Explicit scopes or tunnelsGrants are required.",
344
+ path: ["scopes"]
345
+ });
346
+ }
310
347
  });
311
348
  var createAuthTokenResponseSchema = z3.object({
312
349
  token: z3.string()
@@ -327,18 +364,38 @@ var RstreamAuthResource = class {
327
364
  );
328
365
  }
329
366
  const engine = options?.engine ?? await this.client.getEngine().catch(() => void 0);
367
+ const scopedOptions = await this.resolveScopeOptions(params, options);
330
368
  return createAuthTokenFromClientCredentials(credentials, params, {
331
- engine
369
+ engine,
370
+ ...scopedOptions
332
371
  });
333
372
  }
373
+ async resolveScopeOptions(params, options) {
374
+ const tokenParams = createAuthTokenParamsSchema.parse(params);
375
+ if (options?.projectScoped === false) {
376
+ return options ?? {};
377
+ }
378
+ if (options?.projectId !== void 0 || options?.workspaceId !== void 0) {
379
+ return options;
380
+ }
381
+ if (tokenParams.scopes === void 0 && !tunnelGrantsNeedDefaultTarget(tokenParams.tunnelsGrants)) {
382
+ return options ?? {};
383
+ }
384
+ const target = await this.client.getEngineAuthTokenTarget();
385
+ return {
386
+ ...options,
387
+ projectId: target.projects?.[0],
388
+ workspaceId: target.workspaces?.[0]
389
+ };
390
+ }
334
391
  };
335
392
  function createAuthTokenFromClientCredentials(credentials, params, options) {
336
- const tokenParams = createAuthTokenParamsSchema.parse(params ?? {});
393
+ const tokenParams = createAuthTokenParamsSchema.parse(params);
337
394
  const payload = {
338
395
  metadata: {
339
396
  engine: options?.engine
340
397
  },
341
- tunnelsGrants: normalizeAuthTokenTunnelGrants(tokenParams),
398
+ tunnelsGrants: normalizeAuthTokenTunnelGrants(tokenParams, options),
342
399
  permissions: null
343
400
  };
344
401
  const { token } = (0, import_rstream.createClientCredentialsToken)(credentials, {
@@ -347,19 +404,92 @@ function createAuthTokenFromClientCredentials(credentials, params, options) {
347
404
  });
348
405
  return { token };
349
406
  }
350
- function normalizeAuthTokenTunnelGrants(params) {
351
- if (!params) {
352
- return void 0;
353
- }
407
+ function normalizeAuthTokenTunnelGrants(params, options) {
354
408
  const tunnelsGrants = params.tunnelsGrants;
355
409
  if (tunnelsGrants !== void 0) {
356
- return [...tunnelsGrants];
410
+ return applyDefaultTunnelGrantTarget(tunnelsGrants, options);
357
411
  }
358
412
  if (params.scopes) {
359
- return [{ scopes: params.scopes }];
413
+ return { ...normalizeTunnelGrantTarget(options), scopes: params.scopes };
360
414
  }
361
415
  return void 0;
362
416
  }
417
+ function applyDefaultTunnelGrantTarget(grants, options, inheritedTarget = false) {
418
+ if (options?.projectScoped === false) {
419
+ return grants;
420
+ }
421
+ if ("AND" in grants) {
422
+ const branchHasTarget = inheritedTarget || grants.AND.some((grant) => tunnelGrantTreeHasTarget(grant));
423
+ return {
424
+ AND: grants.AND.map(
425
+ (grant) => applyDefaultTunnelGrantTarget(grant, options, branchHasTarget)
426
+ )
427
+ };
428
+ }
429
+ if ("OR" in grants) {
430
+ return {
431
+ OR: grants.OR.map(
432
+ (grant) => applyDefaultTunnelGrantTarget(grant, options, inheritedTarget)
433
+ )
434
+ };
435
+ }
436
+ if (grants.scopes === void 0 || tunnelGrantHasTarget(grants) || inheritedTarget) {
437
+ return grants;
438
+ }
439
+ return { ...normalizeTunnelGrantTarget(options), scopes: grants.scopes };
440
+ }
441
+ function tunnelGrantsNeedDefaultTarget(grants, inheritedTarget = false) {
442
+ if (grants === void 0) {
443
+ return false;
444
+ }
445
+ if ("AND" in grants) {
446
+ const branchHasTarget = inheritedTarget || grants.AND.some((grant) => tunnelGrantTreeHasTarget(grant));
447
+ return grants.AND.some(
448
+ (grant) => tunnelGrantsNeedDefaultTarget(grant, branchHasTarget)
449
+ );
450
+ }
451
+ if ("OR" in grants) {
452
+ return grants.OR.some(
453
+ (grant) => tunnelGrantsNeedDefaultTarget(grant, inheritedTarget)
454
+ );
455
+ }
456
+ return grants.scopes !== void 0 && !tunnelGrantHasTarget(grants) && !inheritedTarget;
457
+ }
458
+ function tunnelGrantHasTarget(grant) {
459
+ return grant.projects !== void 0 || grant.workspaces !== void 0;
460
+ }
461
+ function tunnelGrantTreeHasTarget(grants) {
462
+ if ("AND" in grants) {
463
+ return grants.AND.some((grant) => tunnelGrantTreeHasTarget(grant));
464
+ }
465
+ if ("OR" in grants) {
466
+ return grants.OR.some((grant) => tunnelGrantTreeHasTarget(grant));
467
+ }
468
+ return tunnelGrantHasTarget(grants);
469
+ }
470
+ function normalizeTunnelGrantTarget(options) {
471
+ const projectId = normalizeOptionalString(options?.projectId);
472
+ const workspaceId = normalizeOptionalString(options?.workspaceId);
473
+ if (projectId !== void 0 && workspaceId !== void 0) {
474
+ throw new Error("Use either projectId or workspaceId, not both.");
475
+ }
476
+ if (projectId !== void 0) {
477
+ return { projects: [projectId] };
478
+ }
479
+ if (workspaceId !== void 0) {
480
+ return { workspaces: [workspaceId] };
481
+ }
482
+ if (options?.projectScoped === false) {
483
+ return {};
484
+ }
485
+ throw new Error(
486
+ "Project ID or workspace ID is required when using scope-only tunnel grants."
487
+ );
488
+ }
489
+ function normalizeOptionalString(value) {
490
+ const normalized = value?.trim();
491
+ return normalized ? normalized : void 0;
492
+ }
363
493
 
364
494
  // src/client.ts
365
495
  var z4 = __toESM(require("zod"));
@@ -400,6 +530,13 @@ var listClientsParamsSchema = z4.object({
400
530
  var listClientsResponseSchema = z4.array(clientSchema);
401
531
 
402
532
  // src/clients-resource.ts
533
+ function clientPathId(id) {
534
+ const normalized = id.trim();
535
+ if (!normalized) {
536
+ throw new Error("Client ID is required.");
537
+ }
538
+ return encodeURIComponent(normalized);
539
+ }
403
540
  var RstreamClientsResource = class {
404
541
  client;
405
542
  constructor(client) {
@@ -413,9 +550,12 @@ var RstreamClientsResource = class {
413
550
  return listClientsResponseSchema.parse(response);
414
551
  }
415
552
  async get(id) {
416
- const response = await this.client.request(`/clients/${id}`, {
417
- method: "GET"
418
- });
553
+ const response = await this.client.request(
554
+ `/clients/${clientPathId(id)}`,
555
+ {
556
+ method: "GET"
557
+ }
558
+ );
419
559
  return clientSchema.parse(response);
420
560
  }
421
561
  };
@@ -563,7 +703,7 @@ var routedTunnelSchema = z5.object({
563
703
  httpUseTls: z5.boolean().optional(),
564
704
  upstreamTls: z5.boolean().optional(),
565
705
  tokenAuth: z5.boolean().optional(),
566
- mtls: z5.boolean().optional()
706
+ mtls_auth: z5.boolean().optional()
567
707
  });
568
708
  var routingSchema = z5.discriminatedUnion("decision", [
569
709
  z5.object({ decision: z5.literal("routed"), tunnel: routedTunnelSchema }),
@@ -805,7 +945,7 @@ var engineOpenApiDocument = {
805
945
  info: {
806
946
  title: "rstream Engine API",
807
947
  version: "1.0.0",
808
- description: "Operational data-plane API exposed by rstream-engine. This surface is scoped to the authenticated engine or project endpoint and covers live clients, live tunnels, and state-change streams."
948
+ description: "Operational Engine API exposed by rstream-engine. This surface is scoped to the authenticated engine or project endpoint and covers live clients, live tunnels, and state-change streams."
809
949
  },
810
950
  servers: [
811
951
  {
@@ -831,12 +971,6 @@ var engineOpenApiDocument = {
831
971
  scheme: "bearer",
832
972
  bearerFormat: "rstream token",
833
973
  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
974
  }
841
975
  },
842
976
  parameters: {
@@ -956,7 +1090,7 @@ var engineOpenApiDocument = {
956
1090
  summary: "List visible engine clients.",
957
1091
  operationId: "list_engine_clients",
958
1092
  parameters: [{ $ref: "#/components/parameters/ListClientsParams" }],
959
- security: [{ bearerAuth: [] }, { queryToken: [] }],
1093
+ security: [{ bearerAuth: [] }],
960
1094
  responses: {
961
1095
  "200": jsonArrayResponse(
962
1096
  "Visible clients.",
@@ -975,7 +1109,7 @@ var engineOpenApiDocument = {
975
1109
  summary: "List visible engine tunnels.",
976
1110
  operationId: "list_engine_tunnels",
977
1111
  parameters: [{ $ref: "#/components/parameters/ListTunnelsParams" }],
978
- security: [{ bearerAuth: [] }, { queryToken: [] }],
1112
+ security: [{ bearerAuth: [] }],
979
1113
  responses: {
980
1114
  "200": jsonArrayResponse(
981
1115
  "Visible tunnels. Fine-grained list scopes may filter objects and project fields.",
@@ -995,7 +1129,7 @@ var engineOpenApiDocument = {
995
1129
  summary: "Subscribe to visible engine state through Server-Sent Events.",
996
1130
  operationId: "watch_engine_events_sse",
997
1131
  parameters: [{ $ref: "#/components/parameters/WatchParams" }],
998
- security: [{ bearerAuth: [] }, { queryToken: [] }],
1132
+ security: [{ bearerAuth: [] }],
999
1133
  responses: {
1000
1134
  "200": streamResponse(
1001
1135
  "SSE stream. Each event is emitted as a data line containing a JSON Event object."
@@ -1014,7 +1148,7 @@ var engineOpenApiDocument = {
1014
1148
  summary: "Subscribe to visible engine state through a WebSocket upgrade.",
1015
1149
  operationId: "watch_engine_events_websocket",
1016
1150
  parameters: [{ $ref: "#/components/parameters/WatchParams" }],
1017
- security: [{ bearerAuth: [] }, { queryToken: [] }],
1151
+ security: [{ bearerAuth: [] }],
1018
1152
  responses: {
1019
1153
  "101": {
1020
1154
  description: "WebSocket connection accepted. Frames contain JSON Event objects."
@@ -1042,22 +1176,37 @@ var import_rstream4 = require("@rstreamlabs/rstream");
1042
1176
  var import_rstream5 = require("@rstreamlabs/rstream");
1043
1177
  var import_rstream6 = require("@rstreamlabs/rstream");
1044
1178
  var import_rstream7 = require("@rstreamlabs/rstream");
1045
- var import_jsonwebtoken = __toESM(require("jsonwebtoken"));
1046
1179
  function trimOptionalString(value) {
1047
1180
  const normalized = value?.trim();
1048
1181
  return normalized ? normalized : void 0;
1049
1182
  }
1050
- function getEngineFromToken(token) {
1051
- if (!token) {
1052
- return void 0;
1183
+ var dnsLabelPattern = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i;
1184
+ function validateHostname(hostname) {
1185
+ if (!hostname || hostname.length > 253 || hostname.includes("..")) {
1186
+ return false;
1053
1187
  }
1054
- const parsed = authTokenSchema.safeParse(
1055
- import_jsonwebtoken.default.decode(token, { complete: false })
1056
- );
1057
- if (!parsed.success) {
1058
- return void 0;
1188
+ return hostname.split(".").every((label) => label === "localhost" || dnsLabelPattern.test(label));
1189
+ }
1190
+ function normalizeEngineAddress(engine) {
1191
+ const normalized = engine.trim();
1192
+ if (!normalized || /^[a-z][a-z0-9+.-]*:\/\//i.test(normalized) || /[/?#@\\\s]/.test(normalized)) {
1193
+ throw new Error("Engine must be a host[:port] value.");
1194
+ }
1195
+ const explicitPort = normalized.match(/:(\d+)$/)?.[1];
1196
+ const url = new URL(`https://${normalized}`);
1197
+ if (url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
1198
+ throw new Error("Engine must be a host[:port] value.");
1199
+ }
1200
+ if (!validateHostname(url.hostname)) {
1201
+ throw new Error("Engine must use a valid hostname.");
1202
+ }
1203
+ if (explicitPort) {
1204
+ const port = Number(explicitPort);
1205
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
1206
+ throw new Error("Engine port must be an integer between 1 and 65535.");
1207
+ }
1059
1208
  }
1060
- return trimOptionalString(parsed.data.metadata?.engine);
1209
+ return `${url.hostname.toLowerCase()}${explicitPort ? `:${explicitPort}` : ""}`;
1061
1210
  }
1062
1211
  function resolveTunnelsAPIURL(apiUrl) {
1063
1212
  return trimOptionalString(apiUrl) ?? (0, import_rstream6.readEnvironment)().apiUrl ?? import_rstream3.DEFAULT_API_URL;
@@ -1091,48 +1240,61 @@ function resolveControlPlaneCredentials(controlPlaneCredentials, credentials, au
1091
1240
  async function resolveTunnelsEngine(config) {
1092
1241
  const explicitEngine = trimOptionalString(config.engine);
1093
1242
  if (explicitEngine) {
1094
- return explicitEngine;
1243
+ return normalizeEngineAddress(explicitEngine);
1095
1244
  }
1096
1245
  const environmentEngine = (0, import_rstream6.readEnvironment)().engine;
1097
1246
  if (environmentEngine) {
1098
- return environmentEngine;
1247
+ return normalizeEngineAddress(environmentEngine);
1099
1248
  }
1100
1249
  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
1250
  const projectEndpoint = trimOptionalString(config.projectEndpoint);
1106
1251
  if (projectEndpoint) {
1107
- const credentials = resolveControlPlaneCredentials(
1108
- config.controlPlaneCredentials,
1109
- config.credentials,
1252
+ const project = await resolveManagedTunnelsProject({
1253
+ ...config,
1110
1254
  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
1255
  });
1121
- const project = await controlPlane.tunnels.projects.resolveByEndpoint(projectEndpoint);
1122
1256
  const engine = (0, import_rstream4.getTunnelsProjectEngine)(project);
1123
1257
  if (engine) {
1124
- return engine;
1258
+ return normalizeEngineAddress(engine);
1125
1259
  }
1126
1260
  throw new Error(
1127
1261
  "Failed to resolve the engine address from the managed tunnels project."
1128
1262
  );
1129
1263
  }
1130
1264
  throw new Error(
1131
- "Engine URL is not defined. Provide an engine, use RSTREAM_ENGINE, or configure a managed project endpoint with control-plane access."
1265
+ "Engine URL is not defined. Provide an engine, use RSTREAM_ENGINE, or configure a managed project endpoint with Control plane API access."
1132
1266
  );
1133
1267
  }
1268
+ async function resolveManagedTunnelsProject(config) {
1269
+ const projectEndpoint = trimOptionalString(config.projectEndpoint);
1270
+ if (!projectEndpoint) {
1271
+ throw new Error("Managed project endpoint is required.");
1272
+ }
1273
+ const credentials = resolveControlPlaneCredentials(
1274
+ config.controlPlaneCredentials,
1275
+ config.credentials,
1276
+ config.token
1277
+ );
1278
+ if (credentials === void 0) {
1279
+ throw new Error(
1280
+ "Control-plane credentials are required to resolve a managed project endpoint."
1281
+ );
1282
+ }
1283
+ const controlPlane = new import_rstream7.RstreamClient({
1284
+ apiUrl: resolveTunnelsAPIURL(config.apiUrl),
1285
+ credentials
1286
+ });
1287
+ return await controlPlane.tunnels.projects.resolveByEndpoint(projectEndpoint);
1288
+ }
1134
1289
 
1135
1290
  // src/tunnels-resource.ts
1291
+ function tunnelPathId(id) {
1292
+ const normalized = id.trim();
1293
+ if (!normalized) {
1294
+ throw new Error("Tunnel ID is required.");
1295
+ }
1296
+ return encodeURIComponent(normalized);
1297
+ }
1136
1298
  var RstreamTunnelsResource = class {
1137
1299
  client;
1138
1300
  constructor(client) {
@@ -1146,15 +1308,20 @@ var RstreamTunnelsResource = class {
1146
1308
  return listTunnelsResponseSchema.parse(response);
1147
1309
  }
1148
1310
  async get(id) {
1149
- const response = await this.client.request(`/tunnels/${id}`, {
1150
- method: "GET"
1151
- });
1311
+ const response = await this.client.request(
1312
+ `/tunnels/${tunnelPathId(id)}`,
1313
+ {
1314
+ method: "GET"
1315
+ }
1316
+ );
1152
1317
  return tunnelSchema.parse(response);
1153
1318
  }
1154
1319
  };
1155
1320
 
1156
1321
  // src/tunnels.ts
1157
1322
  var import_rstream12 = require("@rstreamlabs/rstream");
1323
+ var import_rstream13 = require("@rstreamlabs/rstream");
1324
+ var import_rstream14 = require("@rstreamlabs/rstream");
1158
1325
 
1159
1326
  // src/turn.ts
1160
1327
  var import_rstream8 = require("@rstreamlabs/rstream");
@@ -1165,46 +1332,53 @@ var z8 = __toESM(require("zod"));
1165
1332
  var import_crypto = __toESM(require("crypto"));
1166
1333
  var defaultTURNPort = 3478;
1167
1334
  var defaultTURNSPort = 5349;
1168
- var defaultTURNCredentialTTLSeconds = 24 * 60 * 60;
1335
+ var defaultTURNCredentialTTLSeconds = 10 * 60;
1336
+ var maxTURNCredentialTTLSeconds = 60 * 60;
1169
1337
  var turnTokenClaimsSchema = z8.object({
1170
- token_endpoint: z8.string().optional(),
1171
- type: z8.string()
1338
+ exp: z8.number().int().optional(),
1339
+ iat: z8.number().int().optional(),
1340
+ token_endpoint: z8.string().regex(/^[0-9a-f]{8}$/i).optional(),
1341
+ tokendpoint: z8.string().regex(/^[0-9a-f]{8}$/i).optional(),
1342
+ type: z8.enum(["app", "auth", "pat"])
1172
1343
  });
1173
1344
  var turnCredentialModeSchema = z8.enum(["api", "app", "pat"]);
1174
- function normalizeOptionalString(value) {
1345
+ function normalizeOptionalString2(value) {
1175
1346
  const normalized = value?.trim();
1176
1347
  return normalized ? normalized : void 0;
1177
1348
  }
1178
1349
  function normalizeClusterDomain(clusterDomain) {
1179
- const normalized = normalizeOptionalString(clusterDomain);
1350
+ const normalized = normalizeOptionalString2(clusterDomain);
1180
1351
  return normalized?.toLowerCase();
1181
1352
  }
1182
1353
  function normalizeNow(now) {
1183
- if (now instanceof Date) {
1184
- return Math.floor(now.getTime() / 1e3);
1354
+ if (now === void 0) {
1355
+ return Math.floor(Date.now() / 1e3);
1185
1356
  }
1186
- if (typeof now === "number" && Number.isFinite(now)) {
1187
- return Math.floor(now);
1357
+ const timestamp = now instanceof Date ? now.getTime() / 1e3 : now;
1358
+ if (!Number.isFinite(timestamp)) {
1359
+ throw new Error("TURN timestamp must be finite.");
1188
1360
  }
1189
- return Math.floor(Date.now() / 1e3);
1361
+ return Math.floor(timestamp);
1190
1362
  }
1191
1363
  function normalizeTTLSeconds(ttlSeconds) {
1192
- if (typeof ttlSeconds === "number" && Number.isFinite(ttlSeconds) && ttlSeconds > 0) {
1193
- return Math.floor(ttlSeconds);
1364
+ const ttl = ttlSeconds ?? defaultTURNCredentialTTLSeconds;
1365
+ if (!Number.isInteger(ttl) || ttl < 1 || ttl > maxTURNCredentialTTLSeconds) {
1366
+ throw new Error("TURN TTL must be an integer between 1 and 3600 seconds.");
1194
1367
  }
1195
- return defaultTURNCredentialTTLSeconds;
1368
+ return ttl;
1196
1369
  }
1197
- function normalizeTURNPort(port) {
1198
- if (typeof port === "number" && Number.isFinite(port) && port > 0) {
1199
- return Math.floor(port);
1370
+ function normalizePort(port, fallback, name) {
1371
+ const normalized = port ?? fallback;
1372
+ if (!Number.isInteger(normalized) || normalized < 1 || normalized > 65535) {
1373
+ throw new Error(`${name} must be an integer between 1 and 65535.`);
1200
1374
  }
1201
- return defaultTURNPort;
1375
+ return normalized;
1376
+ }
1377
+ function normalizeTURNPort(port) {
1378
+ return normalizePort(port, defaultTURNPort, "TURN port");
1202
1379
  }
1203
1380
  function normalizeTURNSPort(port) {
1204
- if (typeof port === "number" && Number.isFinite(port) && port > 0) {
1205
- return Math.floor(port);
1206
- }
1207
- return defaultTURNSPort;
1381
+ return normalizePort(port, defaultTURNSPort, "TURNS port");
1208
1382
  }
1209
1383
  function createTURNURLs(clusterDomain, turnPort, turnsPort) {
1210
1384
  const normalizedTURNPort = normalizeTURNPort(turnPort);
@@ -1222,16 +1396,32 @@ function parseTURNTokenClaims(token) {
1222
1396
  if (!payloadPart) {
1223
1397
  throw new Error("Invalid token format.");
1224
1398
  }
1225
- const payload = Buffer.from(payloadPart, "base64url").toString("utf8");
1226
- const parsed = turnTokenClaimsSchema.safeParse(JSON.parse(payload));
1399
+ let decoded;
1400
+ try {
1401
+ const payload = Buffer.from(payloadPart, "base64url").toString("utf8");
1402
+ decoded = JSON.parse(payload);
1403
+ } catch {
1404
+ throw new Error("Invalid token format.");
1405
+ }
1406
+ const parsed = turnTokenClaimsSchema.safeParse(decoded);
1227
1407
  if (!parsed.success) {
1228
1408
  throw new Error("Invalid token format.");
1229
1409
  }
1230
1410
  return parsed.data;
1231
1411
  }
1412
+ function requirePATExpiration(claims, now) {
1413
+ const exp = claims.exp;
1414
+ if (typeof exp !== "number") {
1415
+ throw new Error("TURN PAT mode requires a PAT token with expiration.");
1416
+ }
1417
+ if (exp <= now) {
1418
+ throw new Error("TURN PAT mode requires a non-expired PAT token.");
1419
+ }
1420
+ return exp;
1421
+ }
1232
1422
  function deriveClusterDomainFromEngine(projectEndpoint, engine) {
1233
- const normalizedProjectEndpoint = normalizeOptionalString(projectEndpoint);
1234
- const normalizedEngine = normalizeOptionalString(engine);
1423
+ const normalizedProjectEndpoint = normalizeOptionalString2(projectEndpoint);
1424
+ const normalizedEngine = normalizeOptionalString2(engine);
1235
1425
  if (!normalizedProjectEndpoint || !normalizedEngine) {
1236
1426
  throw new Error(
1237
1427
  "Failed to derive the cluster domain from the engine address."
@@ -1265,17 +1455,11 @@ function resolveMode(credentials, mode) {
1265
1455
  if ((0, import_rstream8.isClientCredentials)(credentials)) {
1266
1456
  return "app";
1267
1457
  }
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
1458
  return "api";
1275
1459
  }
1276
1460
  async function loadTURNServerPublicKeyHex(clusterDomain, options) {
1277
- const configuredKey = normalizeOptionalString(options.keyringBaseUrl);
1278
- const keyringBaseURL = configuredKey ?? normalizeOptionalString(options.apiUrl) ?? "https://rstream.io";
1461
+ const configuredKey = normalizeOptionalString2(options.keyringBaseUrl);
1462
+ const keyringBaseURL = configuredKey ?? normalizeOptionalString2(options.apiUrl) ?? "https://rstream.io";
1279
1463
  const url = new URL(
1280
1464
  `/keyrings/turn/${clusterDomain}.spki.der.hex`,
1281
1465
  keyringBaseURL
@@ -1287,8 +1471,8 @@ async function loadTURNServerPublicKeyHex(clusterDomain, options) {
1287
1471
  return (await response.text()).trim();
1288
1472
  }
1289
1473
  async function createAPITURNCredentials(options) {
1290
- const projectId = normalizeOptionalString(options.projectId);
1291
- const projectEndpoint = normalizeOptionalString(options.projectEndpoint);
1474
+ const projectId = normalizeOptionalString2(options.projectId);
1475
+ const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
1292
1476
  if (!projectId && !projectEndpoint) {
1293
1477
  throw new Error(
1294
1478
  "Project ID or project endpoint is required for TURN API mode."
@@ -1315,11 +1499,11 @@ function createPATTURNCredentials(options) {
1315
1499
  if (!clusterDomain) {
1316
1500
  throw new Error("Cluster domain is required for TURN PAT mode.");
1317
1501
  }
1318
- const projectEndpoint = normalizeOptionalString(options.projectEndpoint);
1502
+ const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
1319
1503
  if (!projectEndpoint) {
1320
1504
  throw new Error("Project endpoint is required for TURN PAT mode.");
1321
1505
  }
1322
- const token = normalizeOptionalString(options.token);
1506
+ const token = normalizeOptionalString2(options.token);
1323
1507
  if (!token) {
1324
1508
  throw new Error("Token is required for TURN PAT mode.");
1325
1509
  }
@@ -1327,14 +1511,18 @@ function createPATTURNCredentials(options) {
1327
1511
  if (claims.type !== "pat") {
1328
1512
  throw new Error("TURN PAT mode requires a PAT token.");
1329
1513
  }
1330
- const tokenEndpoint = normalizeOptionalString(claims.token_endpoint);
1514
+ const tokenEndpoint = normalizeOptionalString2(options.tokenEndpoint);
1331
1515
  if (!tokenEndpoint) {
1332
- throw new Error(
1333
- "TURN PAT mode requires a PAT token carrying a token endpoint."
1334
- );
1516
+ throw new Error("Token endpoint is required for TURN PAT mode.");
1517
+ }
1518
+ const claimsTokenEndpoint = claims.token_endpoint ?? claims.tokendpoint;
1519
+ if (claimsTokenEndpoint !== void 0 && claimsTokenEndpoint !== tokenEndpoint) {
1520
+ throw new Error("Token endpoint does not match the PAT token.");
1335
1521
  }
1522
+ const now = normalizeNow(options.now);
1336
1523
  const ttlSeconds = normalizeTTLSeconds(options.ttlSeconds);
1337
- const exp = normalizeNow(options.now) + ttlSeconds;
1524
+ const exp = Math.min(now + ttlSeconds, requirePATExpiration(claims, now));
1525
+ const ttl = exp - now;
1338
1526
  const username = `v1:${exp}:pat:${projectEndpoint}:${tokenEndpoint}`;
1339
1527
  const tokenHash = import_crypto.default.createHash("sha256").update(token, "utf8").digest();
1340
1528
  const key = Buffer.from(
@@ -1349,7 +1537,7 @@ function createPATTURNCredentials(options) {
1349
1537
  const credential = import_crypto.default.createHmac("sha256", key).update(username, "utf8").digest("base64");
1350
1538
  return import_rstream11.turnCredentialsSchema.parse({
1351
1539
  credential,
1352
- ttl: ttlSeconds,
1540
+ ttl,
1353
1541
  urls: createTURNURLs(clusterDomain, options.turnPort, options.turnsPort),
1354
1542
  username
1355
1543
  });
@@ -1359,15 +1547,15 @@ async function createAPPTURNCredentials(options) {
1359
1547
  if (!clusterDomain) {
1360
1548
  throw new Error("Cluster domain is required for TURN APP mode.");
1361
1549
  }
1362
- const projectEndpoint = normalizeOptionalString(options.projectEndpoint);
1550
+ const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
1363
1551
  if (!projectEndpoint) {
1364
1552
  throw new Error("Project endpoint is required for TURN APP mode.");
1365
1553
  }
1366
- const clientId = normalizeOptionalString(options.clientId);
1554
+ const clientId = normalizeOptionalString2(options.clientId);
1367
1555
  if (!clientId) {
1368
1556
  throw new Error("Client ID is required for TURN APP mode.");
1369
1557
  }
1370
- const clientSecret = normalizeOptionalString(options.clientSecret);
1558
+ const clientSecret = normalizeOptionalString2(options.clientSecret);
1371
1559
  if (!clientSecret) {
1372
1560
  throw new Error("Client secret is required for TURN APP mode.");
1373
1561
  }
@@ -1379,7 +1567,7 @@ async function createAPPTURNCredentials(options) {
1379
1567
  format: "der",
1380
1568
  type: "pkcs8"
1381
1569
  });
1382
- const serverPublicKeyHex = normalizeOptionalString(options.serverPublicKeyHex) ?? await loadTURNServerPublicKeyHex(clusterDomain, options);
1570
+ const serverPublicKeyHex = normalizeOptionalString2(options.serverPublicKeyHex) ?? await loadTURNServerPublicKeyHex(clusterDomain, options);
1383
1571
  const serverPublicKey = import_crypto.default.createPublicKey({
1384
1572
  key: Buffer.from(serverPublicKeyHex, "hex"),
1385
1573
  format: "der",
@@ -1409,7 +1597,7 @@ async function createAPPTURNCredentials(options) {
1409
1597
  async function createTURNCredentials(options) {
1410
1598
  const credentials = options.credentials;
1411
1599
  const mode = resolveMode(credentials, options.mode);
1412
- const projectEndpoint = normalizeOptionalString(options.projectEndpoint);
1600
+ const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
1413
1601
  if (mode === "api") {
1414
1602
  const controlPlaneCredentials = options.controlPlaneCredentials ?? credentials;
1415
1603
  if (!controlPlaneCredentials) {
@@ -1440,6 +1628,10 @@ async function createTURNCredentials(options) {
1440
1628
  "Project endpoint is required for TURN PAT mode."
1441
1629
  ),
1442
1630
  token: credentials.token,
1631
+ tokenEndpoint: requireOption(
1632
+ normalizeOptionalString2(options.tokenEndpoint),
1633
+ "Token endpoint is required for TURN PAT mode."
1634
+ ),
1443
1635
  ttlSeconds: options.ttlSeconds,
1444
1636
  turnPort: options.turnPort,
1445
1637
  turnsPort: options.turnsPort
@@ -1489,6 +1681,7 @@ var RstreamTURNResource = class {
1489
1681
  projectEndpoint: options.projectEndpoint ?? this.client.projectEndpoint,
1490
1682
  projectId: options.projectId,
1491
1683
  serverPublicKeyHex: options.serverPublicKeyHex,
1684
+ tokenEndpoint: options.tokenEndpoint,
1492
1685
  ttlSeconds: options.ttlSeconds,
1493
1686
  turnPort: options.turnPort,
1494
1687
  turnsPort: options.turnsPort
@@ -1498,6 +1691,41 @@ var RstreamTURNResource = class {
1498
1691
 
1499
1692
  // src/webhooks-resource.ts
1500
1693
  var import_crypto2 = __toESM(require("crypto"));
1694
+ var SIGNATURE_HEX_BYTES = 32;
1695
+ var signatureHexSchema = /^[0-9a-fA-F]{64}$/;
1696
+ var timestampSchema = /^\d+$/;
1697
+ function parseSignatureTimestamp(timestamp) {
1698
+ if (!timestampSchema.test(timestamp)) {
1699
+ throw new Error("Invalid signature timestamp");
1700
+ }
1701
+ const parsed = Number(timestamp);
1702
+ if (!Number.isSafeInteger(parsed)) {
1703
+ throw new Error("Invalid signature timestamp");
1704
+ }
1705
+ return parsed;
1706
+ }
1707
+ function normalizeTolerance(tolerance) {
1708
+ if (!Number.isFinite(tolerance) || !Number.isInteger(tolerance) || tolerance < 0) {
1709
+ throw new Error("Invalid signature tolerance");
1710
+ }
1711
+ return tolerance;
1712
+ }
1713
+ function readSignature(signature) {
1714
+ if (!signatureHexSchema.test(signature)) {
1715
+ return void 0;
1716
+ }
1717
+ const parsed = Buffer.from(signature, "hex");
1718
+ return parsed.length === SIGNATURE_HEX_BYTES ? parsed : void 0;
1719
+ }
1720
+ function signatureMatches(signature, expected) {
1721
+ const parsed = readSignature(signature);
1722
+ return parsed === void 0 ? false : import_crypto2.default.timingSafeEqual(parsed, expected);
1723
+ }
1724
+ function assertSecretConfigured(secret) {
1725
+ if (secret.trim().length === 0) {
1726
+ throw new Error("Webhook signing secret is required");
1727
+ }
1728
+ }
1501
1729
  var RstreamWebhookResource = class {
1502
1730
  async event(payload, header, secret, tolerance = 300, receivedAt = Date.now()) {
1503
1731
  const payloadBuffer = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
@@ -1505,31 +1733,26 @@ var RstreamWebhookResource = class {
1505
1733
  if (!signatureHeader) {
1506
1734
  throw new Error("No signature header");
1507
1735
  }
1736
+ assertSecretConfigured(secret);
1508
1737
  const elements = signatureHeader.split(",").map((element) => element.trim());
1509
1738
  const timestamp = elements.find((element) => element.startsWith("t="))?.substring(2);
1510
1739
  const signatures = elements.filter((element) => element.startsWith("v1=")).map((element) => element.substring(3));
1511
1740
  if (!timestamp || signatures.length === 0) {
1512
1741
  throw new Error("Invalid signature header format");
1513
1742
  }
1514
- const timestampNum = parseInt(timestamp, 10);
1743
+ const timestampNum = parseSignatureTimestamp(timestamp);
1744
+ const signatureTolerance = normalizeTolerance(tolerance);
1515
1745
  const now = Math.floor(receivedAt / 1e3);
1516
- if (Math.abs(now - timestampNum) > tolerance) {
1746
+ if (Math.abs(now - timestampNum) > signatureTolerance) {
1517
1747
  throw new Error(
1518
1748
  `Webhook signature timestamp outside tolerance: ${now}, ${timestampNum}`
1519
1749
  );
1520
1750
  }
1521
1751
  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
- });
1752
+ const expectedSignature = import_crypto2.default.createHmac("sha256", secret).update(signedPayload).digest();
1753
+ const signatureMatched = signatures.some(
1754
+ (signature) => signatureMatches(signature, expectedSignature)
1755
+ );
1533
1756
  if (!signatureMatched) {
1534
1757
  throw new Error("Signature verification failed");
1535
1758
  }
@@ -1547,8 +1770,16 @@ var RstreamWebhookResource = class {
1547
1770
  };
1548
1771
 
1549
1772
  // src/tunnels.ts
1773
+ var engineAuthTokenScopes = {
1774
+ tunnels: {
1775
+ connect: true,
1776
+ create: true,
1777
+ list: true
1778
+ }
1779
+ };
1550
1780
  var RstreamTunnelsClient = class {
1551
1781
  config;
1782
+ managedProject;
1552
1783
  constructor(config) {
1553
1784
  this.config = config;
1554
1785
  }
@@ -1568,6 +1799,21 @@ var RstreamTunnelsClient = class {
1568
1799
  return this.config?.projectEndpoint;
1569
1800
  }
1570
1801
  async getEngine() {
1802
+ const projectEndpoint = normalizeOptionalString3(
1803
+ this.config?.projectEndpoint
1804
+ );
1805
+ if (projectEndpoint !== void 0 && normalizeOptionalString3(this.config?.engine) === void 0 && (0, import_rstream14.readEnvironment)().engine === void 0) {
1806
+ const project = await this.getManagedProject();
1807
+ if (project !== void 0) {
1808
+ const engine = (0, import_rstream12.getTunnelsProjectEngine)(project);
1809
+ if (engine) {
1810
+ return normalizeEngineAddress(engine);
1811
+ }
1812
+ }
1813
+ throw new Error(
1814
+ "Failed to resolve the engine address from the managed tunnels project."
1815
+ );
1816
+ }
1571
1817
  return await resolveTunnelsEngine({
1572
1818
  apiUrl: this.config?.apiUrl,
1573
1819
  controlPlaneCredentials: this.controlPlaneCredentials,
@@ -1576,6 +1822,55 @@ var RstreamTunnelsClient = class {
1576
1822
  projectEndpoint: this.config?.projectEndpoint
1577
1823
  });
1578
1824
  }
1825
+ async getManagedProject() {
1826
+ const projectEndpoint = normalizeOptionalString3(
1827
+ this.config?.projectEndpoint
1828
+ );
1829
+ if (projectEndpoint === void 0) {
1830
+ return void 0;
1831
+ }
1832
+ if (this.managedProject === void 0) {
1833
+ this.managedProject = resolveManagedTunnelsProject({
1834
+ apiUrl: this.config?.apiUrl,
1835
+ controlPlaneCredentials: this.controlPlaneCredentials,
1836
+ credentials: this.credentials,
1837
+ projectEndpoint
1838
+ }).catch((error) => {
1839
+ this.managedProject = void 0;
1840
+ throw error;
1841
+ });
1842
+ }
1843
+ return await this.managedProject;
1844
+ }
1845
+ async getEngineAuthTokenParams() {
1846
+ const target = await this.getEngineAuthTokenTarget();
1847
+ return {
1848
+ tunnelsGrants: {
1849
+ ...target,
1850
+ scopes: engineAuthTokenScopes
1851
+ }
1852
+ };
1853
+ }
1854
+ async getEngineAuthTokenTarget() {
1855
+ const projectId = normalizeOptionalString3(this.config?.projectId);
1856
+ const workspaceId = normalizeOptionalString3(this.config?.workspaceId);
1857
+ if (projectId !== void 0 && workspaceId !== void 0) {
1858
+ throw new Error("Use either projectId or workspaceId, not both.");
1859
+ }
1860
+ if (projectId !== void 0) {
1861
+ return { projects: [projectId] };
1862
+ }
1863
+ if (workspaceId !== void 0) {
1864
+ return { workspaces: [workspaceId] };
1865
+ }
1866
+ const project = await this.getManagedProject();
1867
+ if (project !== void 0) {
1868
+ return { projects: [project.id] };
1869
+ }
1870
+ throw new Error(
1871
+ "Application credentials require projectId, workspaceId, or projectEndpoint to create scoped engine tokens."
1872
+ );
1873
+ }
1579
1874
  get auth() {
1580
1875
  return new RstreamAuthResource(this);
1581
1876
  }
@@ -1596,16 +1891,20 @@ var RstreamTunnelsClient = class {
1596
1891
  if (!credentials) {
1597
1892
  return void 0;
1598
1893
  }
1599
- if (!(0, import_rstream12.isClientCredentials)(credentials)) {
1894
+ if (!(0, import_rstream13.isClientCredentials)(credentials)) {
1600
1895
  return credentials.token;
1601
1896
  }
1602
- return (await this.auth.createAuthToken(void 0, {
1897
+ const tokenParams = await this.getEngineAuthTokenParams();
1898
+ return (await this.auth.createAuthToken(tokenParams, {
1603
1899
  credentials,
1604
1900
  engine
1605
1901
  })).token;
1606
1902
  }
1607
1903
  async request(path, options) {
1608
- const engine = await this.getEngine();
1904
+ const engine = normalizeEngineAddress(await this.getEngine());
1905
+ if (!path.startsWith("/") || path.startsWith("//")) {
1906
+ throw new Error("Engine request path must be a relative absolute path.");
1907
+ }
1609
1908
  const url = `https://${engine}/api${path}`;
1610
1909
  const headers = new Headers(options?.headers || {});
1611
1910
  const token = await this.getToken(engine);
@@ -1614,7 +1913,8 @@ var RstreamTunnelsClient = class {
1614
1913
  }
1615
1914
  const response = await fetch(url, {
1616
1915
  ...options,
1617
- headers
1916
+ headers,
1917
+ redirect: options?.redirect ?? "manual"
1618
1918
  });
1619
1919
  if (!response.ok) {
1620
1920
  const errorText = await response.text();
@@ -1623,8 +1923,96 @@ var RstreamTunnelsClient = class {
1623
1923
  return await response.json();
1624
1924
  }
1625
1925
  };
1926
+ function normalizeOptionalString3(value) {
1927
+ const normalized = value?.trim();
1928
+ return normalized ? normalized : void 0;
1929
+ }
1626
1930
 
1627
1931
  // src/watch.ts
1932
+ var import_jsonwebtoken = __toESM(require("jsonwebtoken"));
1933
+ var maxWatchTokenLifetimeSeconds = 3600;
1934
+ var watchTokenIssuedAtSkewSeconds = 300;
1935
+ function normalizeTransport(transport) {
1936
+ if (transport === void 0) {
1937
+ return "sse";
1938
+ }
1939
+ if (transport === "sse" || transport === "websocket") {
1940
+ return transport;
1941
+ }
1942
+ throw new Error("Watch: Unsupported transport.");
1943
+ }
1944
+ function validateWatchQueryToken(token) {
1945
+ const parsed = authTokenSchema.safeParse(import_jsonwebtoken.default.decode(token));
1946
+ if (!parsed.success || parsed.data.type === "pat") {
1947
+ throw new Error(
1948
+ "Watch requires a short-lived auth or app token for URL-based authentication."
1949
+ );
1950
+ }
1951
+ const iat = parsed.data.iat;
1952
+ const exp = parsed.data.exp;
1953
+ const now = Math.floor(Date.now() / 1e3);
1954
+ 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) {
1955
+ throw new Error(
1956
+ "Watch requires a token with a bounded lifetime of at most 3600 seconds."
1957
+ );
1958
+ }
1959
+ if (!hasWatchOnlyTunnelGrants(parsed.data)) {
1960
+ throw new Error(
1961
+ "Watch requires a fine-grained token limited to tunnel list grants for URL-based authentication."
1962
+ );
1963
+ }
1964
+ }
1965
+ function hasWatchOnlyTunnelGrants(token) {
1966
+ if (!hasWatchOnlyPermissions(token.permissions)) {
1967
+ return false;
1968
+ }
1969
+ if (token.tunnelsGrants === void 0) {
1970
+ return false;
1971
+ }
1972
+ return hasWatchOnlyTunnelGrant(token.tunnelsGrants);
1973
+ }
1974
+ function hasWatchOnlyTunnelGrant(grant) {
1975
+ return watchGrantBranches(grant).every(
1976
+ (branch) => branch.hasList && !branch.hasMutationOrConnect
1977
+ );
1978
+ }
1979
+ function watchGrantBranches(grant) {
1980
+ if ("AND" in grant) {
1981
+ return grant.AND.reduce(
1982
+ (branches, child) => combineWatchGrantBranches(branches, child),
1983
+ [{ hasList: false, hasMutationOrConnect: false }]
1984
+ );
1985
+ }
1986
+ if ("OR" in grant) {
1987
+ return grant.OR.flatMap(watchGrantBranches);
1988
+ }
1989
+ const tunnels = grant.scopes?.tunnels;
1990
+ if (tunnels === void 0) {
1991
+ return [{ hasList: false, hasMutationOrConnect: false }];
1992
+ }
1993
+ return [
1994
+ {
1995
+ hasList: tunnels.list !== void 0,
1996
+ hasMutationOrConnect: tunnels.create !== void 0 || tunnels.connect !== void 0
1997
+ }
1998
+ ];
1999
+ }
2000
+ function combineWatchGrantBranches(branches, child) {
2001
+ return branches.flatMap(
2002
+ (branch) => watchGrantBranches(child).map((childBranch) => ({
2003
+ hasList: branch.hasList || childBranch.hasList,
2004
+ hasMutationOrConnect: branch.hasMutationOrConnect || childBranch.hasMutationOrConnect
2005
+ }))
2006
+ );
2007
+ }
2008
+ function hasWatchOnlyPermissions(permissions) {
2009
+ if (!Array.isArray(permissions)) {
2010
+ return true;
2011
+ }
2012
+ return permissions.every(
2013
+ (permission) => permission === "tunnels.resources.read-only" || permission === "network.resources.read-only"
2014
+ );
2015
+ }
1628
2016
  var Watch = class {
1629
2017
  connection = null;
1630
2018
  connectionState = "preparing";
@@ -1639,45 +2027,56 @@ var Watch = class {
1639
2027
  throw new Error("Watch: Connection already started or closed.");
1640
2028
  }
1641
2029
  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();
2030
+ try {
2031
+ const token = typeof this.config.auth === "function" ? await this.config.auth() : this.config.auth;
2032
+ validateWatchQueryToken(token);
2033
+ const engine = await resolveTunnelsEngine({
2034
+ apiUrl: this.config.apiUrl,
2035
+ controlPlaneCredentials: this.config.controlPlaneCredentials,
2036
+ engine: this.config.engine,
2037
+ projectEndpoint: this.config.projectEndpoint,
2038
+ token
2039
+ });
2040
+ const transport = normalizeTransport(this.config.transport);
2041
+ const base = `https://${engine}`;
2042
+ if (transport === "sse") {
2043
+ const url = new URL(`/api/sse`, base);
2044
+ url.searchParams.set("rstream.token", token);
2045
+ this.connection = new EventSource(url.toString());
2046
+ } else {
2047
+ const url = new URL(`/api/websocket`, base);
2048
+ url.searchParams.set("rstream.token", token);
2049
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
2050
+ this.connection = new WebSocket(url.toString());
1673
2051
  }
1674
- };
1675
- if (this.connection instanceof WebSocket) {
1676
- this.connection.onclose = () => {
2052
+ this.connection.onopen = () => {
2053
+ this.connectionState = "connected";
2054
+ this.events.onConnect?.();
2055
+ };
2056
+ this.connection.onmessage = (msg) => this.handleMessage(msg.data);
2057
+ this.connection.onerror = () => {
1677
2058
  if (this.connectionState !== "closed") {
1678
2059
  this.disconnect();
1679
2060
  }
1680
2061
  };
2062
+ if (this.connection instanceof WebSocket) {
2063
+ this.connection.onclose = () => {
2064
+ if (this.connectionState !== "closed") {
2065
+ this.disconnect();
2066
+ }
2067
+ };
2068
+ }
2069
+ } catch (error) {
2070
+ this.connectionState = "preparing";
2071
+ throw error;
2072
+ }
2073
+ }
2074
+ handleMessage(data) {
2075
+ try {
2076
+ const parsed = wsEventsSchema.parse(JSON.parse(data));
2077
+ this.events.onEvent?.(parsed);
2078
+ } catch {
2079
+ this.disconnect();
1681
2080
  }
1682
2081
  }
1683
2082
  disconnect() {
@@ -1798,8 +2197,10 @@ function parseWebTTYServers(tunnels) {
1798
2197
  listClientsResponseSchema,
1799
2198
  listTunnelsParamsSchema,
1800
2199
  listTunnelsResponseSchema,
2200
+ normalizeEngineAddress,
1801
2201
  parseWebTTYServers,
1802
2202
  resolveControlPlaneCredentials,
2203
+ resolveManagedTunnelsProject,
1803
2204
  resolveTunnelsAPIURL,
1804
2205
  resolveTunnelsCredentials,
1805
2206
  resolveTunnelsEngine,