@rstreamlabs/tunnels 1.0.6 → 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.mjs CHANGED
@@ -1,3 +1,23 @@
1
+ import {
2
+ clientFilterSchema,
3
+ clientSchema,
4
+ engineOpenApiDocument,
5
+ engineOpenApiJson,
6
+ eventSchema,
7
+ formatTunnelHost,
8
+ listClientsParamsSchema,
9
+ listClientsResponseSchema,
10
+ listTunnelsParamsSchema,
11
+ listTunnelsResponseSchema,
12
+ streamEndpointSchema,
13
+ streamSummarySchema,
14
+ streamTLSInfoSchema,
15
+ tunnelFilterSchema,
16
+ tunnelSchema,
17
+ webhookEventsSchema,
18
+ wsEventsSchema
19
+ } from "./chunk-7RZ743EF.mjs";
20
+
1
21
  // src/auth-resource.ts
2
22
  import { createClientCredentialsToken } from "@rstreamlabs/rstream";
3
23
 
@@ -5,9 +25,9 @@ import { createClientCredentialsToken } from "@rstreamlabs/rstream";
5
25
  import * as z from "zod";
6
26
  var StringFilter = z.union([
7
27
  z.string(),
8
- z.object({ exact: z.string() }),
9
- z.object({ oneof: z.array(z.string()) }),
10
- z.object({ regex: z.string() })
28
+ z.object({ exact: z.string() }).strict(),
29
+ z.object({ oneof: z.array(z.string()) }).strict(),
30
+ z.object({ regex: z.string() }).strict()
11
31
  ]);
12
32
  function transform(field) {
13
33
  if (field instanceof z.ZodOptional) {
@@ -41,78 +61,18 @@ function select(base) {
41
61
  return [key, z.boolean().optional()];
42
62
  });
43
63
  const shape = Object.fromEntries(entries);
44
- return z.object(shape);
45
- }
46
-
47
- // src/tunnel.ts
48
- import * as z2 from "zod";
49
- var DEFAULT_PUBLISHED_PORT = 443;
50
- var tunnelSchema = z2.object({
51
- // common properties
52
- client_id: z2.string(),
53
- user_id: z2.string().optional(),
54
- workspace_id: z2.string().optional(),
55
- project_id: z2.string().optional(),
56
- cluster_id: z2.string().optional(),
57
- plan: z2.string().optional(),
58
- provider: z2.string().optional(),
59
- region: z2.string().optional(),
60
- status: z2.enum(["online", "offline"]),
61
- // tunnel properties
62
- id: z2.string(),
63
- creation_date: z2.union([z2.string().datetime(), z2.date()]).optional(),
64
- name: z2.string().optional(),
65
- type: z2.enum(["bytestream", "datagram"]).optional(),
66
- publish: z2.boolean().optional(),
67
- protocol: z2.enum(["tls", "dtls", "quic", "http"]).optional(),
68
- labels: z2.record(z2.string(), z2.string()).optional(),
69
- geo_ip: z2.array(z2.string()).optional(),
70
- trusted_ips: z2.array(z2.string()).optional(),
71
- host: z2.string().optional(),
72
- hostname: z2.string().optional(),
73
- port: z2.number().int().min(1).max(65535).optional(),
74
- tls_mode: z2.enum(["passthrough", "terminated"]).optional(),
75
- tls_alpns: z2.array(z2.string()).optional(),
76
- tls_min_version: z2.string().optional(),
77
- tls_ciphers: z2.array(z2.string()).optional(),
78
- mtls: z2.boolean().optional(),
79
- mtls_ca_cert_pem: z2.string().optional(),
80
- http_version: z2.enum(["http/1.1", "h2", "h2c", "h3"]).optional(),
81
- http_use_tls: z2.boolean().optional(),
82
- upstream_tls: z2.boolean().optional(),
83
- token_auth: z2.boolean().optional(),
84
- rstream_auth: z2.boolean().optional(),
85
- challenge_mode: z2.boolean().optional()
86
- });
87
- var listTunnelsParamsSchema = z2.object({
88
- limit: z2.number().optional(),
89
- filters: tunnelSchema.pick({
90
- status: true,
91
- client_id: true,
92
- hostname: true,
93
- protocol: true,
94
- publish: true,
95
- labels: true
96
- }).partial().optional()
97
- });
98
- var listTunnelsResponseSchema = z2.array(tunnelSchema);
99
- function formatTunnelHost(tunnel) {
100
- if (!tunnel.hostname) return tunnel.host;
101
- if (!tunnel.port || tunnel.port === DEFAULT_PUBLISHED_PORT) {
102
- return tunnel.hostname;
103
- }
104
- return `${tunnel.hostname}:${tunnel.port}`;
64
+ return z.object(shape).strict();
105
65
  }
106
66
 
107
67
  // src/auth.ts
108
- import * as z3 from "zod";
109
- var nonEmptyStringSchema = z3.string().trim().min(1);
110
- var authTokenPermissionsSchema = z3.array(z3.string());
111
- var authTokenTunnelsScopesSchema = z3.object({
68
+ import * as z2 from "zod";
69
+ var nonEmptyStringSchema = z2.string().trim().min(1);
70
+ var authTokenPermissionsSchema = z2.array(z2.string());
71
+ var authTokenTunnelsScopesSchema = z2.object({
112
72
  // Scopes for creating tunnels
113
- create: z3.union([
114
- z3.boolean(),
115
- z3.object({
73
+ create: z2.union([
74
+ z2.boolean(),
75
+ z2.object({
116
76
  filters: filters(
117
77
  tunnelSchema.omit({
118
78
  client_id: true,
@@ -128,97 +88,125 @@ var authTokenTunnelsScopesSchema = z3.object({
128
88
  creation_date: true
129
89
  })
130
90
  ).optional()
131
- })
91
+ }).strict()
132
92
  ]).optional(),
133
93
  // Scopes for connecting to tunnels
134
- connect: z3.union([
135
- z3.boolean(),
136
- z3.object({
94
+ connect: z2.union([
95
+ z2.boolean(),
96
+ z2.object({
137
97
  filters: filters(tunnelSchema).optional(),
138
98
  params: filters(
139
- z3.object({
140
- path: z3.string().optional()
99
+ z2.object({
100
+ path: z2.string().optional()
141
101
  })
142
102
  ).optional()
143
- })
103
+ }).strict()
144
104
  ]).optional(),
145
105
  // Scopes for listing tunnels
146
- list: z3.union([
147
- z3.boolean(),
148
- z3.object({
106
+ list: z2.union([
107
+ z2.boolean(),
108
+ z2.object({
149
109
  filters: filters(tunnelSchema).optional(),
150
110
  select: select(tunnelSchema).optional()
151
- })
111
+ }).strict()
152
112
  ]).optional()
113
+ }).strict().superRefine((scopes, ctx) => {
114
+ if (scopes.create === void 0 && scopes.connect === void 0 && scopes.list === void 0) {
115
+ ctx.addIssue({
116
+ code: z2.ZodIssueCode.custom,
117
+ message: "At least one tunnel scope action is required."
118
+ });
119
+ }
153
120
  });
154
- var authTokenScopesSchema = z3.object({
121
+ var authTokenScopesSchema = z2.object({
155
122
  tunnels: authTokenTunnelsScopesSchema.optional()
156
123
  // Scopes related to tunnels
124
+ }).strict().superRefine((scopes, ctx) => {
125
+ if (scopes.tunnels === void 0) {
126
+ ctx.addIssue({
127
+ code: z2.ZodIssueCode.custom,
128
+ message: "Tunnel scopes are required.",
129
+ path: ["tunnels"]
130
+ });
131
+ }
157
132
  });
158
- var authTokenTunnelGrantSchema = z3.object({
159
- workspaces: z3.array(nonEmptyStringSchema).min(1).optional(),
160
- projects: z3.array(nonEmptyStringSchema).min(1).optional(),
161
- scopes: authTokenScopesSchema.optional()
133
+ var authTokenTunnelGrantSchema = z2.object({
134
+ workspaces: z2.array(nonEmptyStringSchema).min(1).optional(),
135
+ projects: z2.array(nonEmptyStringSchema).min(1).optional(),
136
+ scopes: authTokenScopesSchema
162
137
  }).strict().superRefine((grant, ctx) => {
163
138
  if (grant.workspaces !== void 0 && grant.projects !== void 0) {
164
139
  ctx.addIssue({
165
- code: z3.ZodIssueCode.custom,
140
+ code: z2.ZodIssueCode.custom,
166
141
  message: "A grant cannot target workspaces and projects at the same time.",
167
142
  path: ["projects"]
168
143
  });
169
144
  }
145
+ if (grant.workspaces === void 0 && grant.projects === void 0) {
146
+ ctx.addIssue({
147
+ code: z2.ZodIssueCode.custom,
148
+ message: "A grant must target workspaces or projects.",
149
+ path: ["workspaces"]
150
+ });
151
+ }
170
152
  });
171
153
  var authTokenCommonSchema = {
172
- aud: z3.union([z3.string(), z3.array(z3.string())]).optional(),
173
- exp: z3.number().optional(),
174
- tunnelsGrants: z3.array(authTokenTunnelGrantSchema).optional(),
175
- iat: z3.number().optional(),
176
- iss: z3.string().optional(),
177
- jti: z3.string().optional(),
178
- metadata: z3.object({
179
- engine: z3.string().optional()
154
+ aud: z2.union([z2.string(), z2.array(z2.string())]).optional(),
155
+ exp: z2.number().optional(),
156
+ tunnelsGrants: z2.array(authTokenTunnelGrantSchema).optional(),
157
+ iat: z2.number().optional(),
158
+ iss: z2.string().optional(),
159
+ jti: z2.string().optional(),
160
+ metadata: z2.object({
161
+ engine: z2.string().optional()
180
162
  }).strict().optional(),
181
- nbf: z3.number().optional(),
182
- sub: z3.string().optional()
163
+ nbf: z2.number().optional(),
164
+ sub: z2.string().optional()
183
165
  };
184
- var authTokenSchema = z3.union([
185
- z3.object({
186
- type: z3.literal("auth"),
187
- userId: z3.string(),
166
+ var authTokenSchema = z2.union([
167
+ z2.object({
168
+ type: z2.literal("auth"),
169
+ userId: z2.string(),
188
170
  permissions: authTokenPermissionsSchema.nullable(),
189
171
  ...authTokenCommonSchema
190
172
  }).strict(),
191
- z3.object({
192
- type: z3.literal("pat"),
193
- token_endpoint: z3.string().optional(),
173
+ z2.object({
174
+ type: z2.literal("pat"),
175
+ token_endpoint: z2.string().optional(),
194
176
  ...authTokenCommonSchema
195
177
  }).strict(),
196
- z3.object({
197
- type: z3.literal("app"),
198
- clientId: z3.string(),
178
+ z2.object({
179
+ type: z2.literal("app"),
180
+ clientId: z2.string(),
199
181
  permissions: authTokenPermissionsSchema.nullable(),
200
182
  ...authTokenCommonSchema
201
183
  }).strict()
202
184
  ]);
203
185
  var tokenSchema = authTokenSchema;
204
- var createAuthTokenParamsSchema = z3.object({
205
- expires_in: z3.number().default(60),
206
- // 1 minute
186
+ var createAuthTokenParamsSchema = z2.object({
187
+ expires_in: z2.number().int().min(1).max(3600).default(60),
207
188
  scopes: authTokenScopesSchema.optional(),
208
- tunnelsGrants: z3.array(authTokenTunnelGrantSchema).optional(),
209
- metadata: z3.unknown().optional()
189
+ tunnelsGrants: z2.array(authTokenTunnelGrantSchema).min(1).optional(),
190
+ metadata: z2.unknown().optional()
210
191
  // Additional metadata
211
192
  }).strict().superRefine((params, ctx) => {
212
193
  if (params.scopes !== void 0 && params.tunnelsGrants !== void 0) {
213
194
  ctx.addIssue({
214
- code: z3.ZodIssueCode.custom,
195
+ code: z2.ZodIssueCode.custom,
215
196
  message: "Use either scopes or tunnelsGrants, not both.",
216
197
  path: ["tunnelsGrants"]
217
198
  });
218
199
  }
200
+ if (params.scopes === void 0 && params.tunnelsGrants === void 0) {
201
+ ctx.addIssue({
202
+ code: z2.ZodIssueCode.custom,
203
+ message: "Explicit scopes or tunnelsGrants are required.",
204
+ path: ["scopes"]
205
+ });
206
+ }
219
207
  });
220
- var createAuthTokenResponseSchema = z3.object({
221
- token: z3.string()
208
+ var createAuthTokenResponseSchema = z2.object({
209
+ token: z2.string()
222
210
  });
223
211
 
224
212
  // src/auth-resource.ts
@@ -242,12 +230,12 @@ var RstreamAuthResource = class {
242
230
  }
243
231
  };
244
232
  function createAuthTokenFromClientCredentials(credentials, params, options) {
245
- const tokenParams = createAuthTokenParamsSchema.parse(params ?? {});
233
+ const tokenParams = createAuthTokenParamsSchema.parse(params);
246
234
  const payload = {
247
235
  metadata: {
248
236
  engine: options?.engine
249
237
  },
250
- tunnelsGrants: normalizeAuthTokenTunnelGrants(tokenParams),
238
+ tunnelsGrants: normalizeAuthTokenTunnelGrants(tokenParams, options),
251
239
  permissions: null
252
240
  };
253
241
  const { token } = createClientCredentialsToken(credentials, {
@@ -256,49 +244,45 @@ function createAuthTokenFromClientCredentials(credentials, params, options) {
256
244
  });
257
245
  return { token };
258
246
  }
259
- function normalizeAuthTokenTunnelGrants(params) {
260
- if (!params) {
261
- return void 0;
262
- }
247
+ function normalizeAuthTokenTunnelGrants(params, options) {
263
248
  const tunnelsGrants = params.tunnelsGrants;
264
249
  if (tunnelsGrants !== void 0) {
265
250
  return [...tunnelsGrants];
266
251
  }
267
252
  if (params.scopes) {
268
- return [{ scopes: params.scopes }];
253
+ return [{ ...normalizeTunnelGrantTarget(options), scopes: params.scopes }];
269
254
  }
270
255
  return void 0;
271
256
  }
272
-
273
- // src/client.ts
274
- import * as z4 from "zod";
275
- var clientSchema = z4.object({
276
- id: z4.string(),
277
- status: z4.enum(["online", "offline"]),
278
- user_id: z4.string().optional(),
279
- workspace_id: z4.string().optional(),
280
- project_id: z4.string().optional(),
281
- cluster_id: z4.string().optional(),
282
- plan: z4.string().optional(),
283
- provider: z4.string().optional(),
284
- region: z4.string().optional(),
285
- agent: z4.string().optional(),
286
- channel: z4.string().optional(),
287
- version: z4.string().optional(),
288
- os: z4.string().optional(),
289
- arch: z4.string().optional(),
290
- protocol_version: z4.string().optional(),
291
- labels: z4.record(z4.string(), z4.string()).optional()
292
- });
293
- var listClientsParamsSchema = z4.object({
294
- limit: z4.number().optional(),
295
- filters: clientSchema.pick({
296
- labels: true
297
- }).partial().optional()
298
- });
299
- var listClientsResponseSchema = z4.array(clientSchema);
257
+ function normalizeTunnelGrantTarget(options) {
258
+ const projectId = normalizeOptionalString(options?.projectId);
259
+ const workspaceId = normalizeOptionalString(options?.workspaceId);
260
+ if (projectId !== void 0 && workspaceId !== void 0) {
261
+ throw new Error("Use either projectId or workspaceId, not both.");
262
+ }
263
+ if (projectId !== void 0) {
264
+ return { projects: [projectId] };
265
+ }
266
+ if (workspaceId !== void 0) {
267
+ return { workspaces: [workspaceId] };
268
+ }
269
+ throw new Error(
270
+ "Project ID or workspace ID is required when using scope-only tunnel grants."
271
+ );
272
+ }
273
+ function normalizeOptionalString(value) {
274
+ const normalized = value?.trim();
275
+ return normalized ? normalized : void 0;
276
+ }
300
277
 
301
278
  // src/clients-resource.ts
279
+ function clientPathId(id) {
280
+ const normalized = id.trim();
281
+ if (!normalized) {
282
+ throw new Error("Client ID is required.");
283
+ }
284
+ return encodeURIComponent(normalized);
285
+ }
302
286
  var RstreamClientsResource = class {
303
287
  client;
304
288
  constructor(client) {
@@ -312,299 +296,53 @@ var RstreamClientsResource = class {
312
296
  return listClientsResponseSchema.parse(response);
313
297
  }
314
298
  async get(id) {
315
- const response = await this.client.request(`/clients/${id}`, {
316
- method: "GET"
317
- });
299
+ const response = await this.client.request(
300
+ `/clients/${clientPathId(id)}`,
301
+ {
302
+ method: "GET"
303
+ }
304
+ );
318
305
  return clientSchema.parse(response);
319
306
  }
320
307
  };
321
308
 
322
- // src/stream.ts
323
- import * as z5 from "zod";
324
- var nonEmptyStringSchema2 = z5.string().min(1);
325
- var isoDateTimeSchema = z5.string().datetime({ offset: true });
326
- var ipAddressSchema = z5.string();
327
- var intNonNegSchema = z5.number().int().nonnegative();
328
- var numNonNegSchema = z5.number().nonnegative();
329
- var geolocationSchema = z5.object({
330
- country: z5.string().optional(),
331
- region: z5.string().optional()
332
- });
333
- var streamTLSInfoSchema = z5.object({
334
- version: z5.string().min(1).optional(),
335
- cipher: z5.string().min(1).optional(),
336
- protocol: z5.string().min(1).optional(),
337
- curve: z5.string().min(1).optional(),
338
- ech: z5.boolean().optional()
339
- });
340
- var streamEndpointSchema = z5.object({
341
- ip: ipAddressSchema.optional(),
342
- geolocation: geolocationSchema.optional(),
343
- tls: streamTLSInfoSchema.optional()
344
- });
345
- var httpHeadersSchema = z5.record(
346
- z5.string(),
347
- z5.union([z5.string(), z5.array(z5.string())])
348
- );
349
- var httpProtocolSchema = z5.enum(["http/1.1", "h2", "h2c", "h3"]);
350
- var httpRequestSchema = z5.object({
351
- method: nonEmptyStringSchema2,
352
- scheme: z5.string().min(1).optional(),
353
- host: nonEmptyStringSchema2.optional(),
354
- path: nonEmptyStringSchema2.optional(),
355
- query: z5.string().optional(),
356
- url: nonEmptyStringSchema2.optional(),
357
- headers: httpHeadersSchema.optional(),
358
- userAgent: z5.string().optional()
359
- });
360
- var httpResponseSchema = z5.object({
361
- status: z5.number().int().min(100).max(999),
362
- headers: httpHeadersSchema.optional()
363
- });
364
- var httpRequestStageSchema = z5.object({
365
- protocol: httpProtocolSchema.optional(),
366
- request: httpRequestSchema
367
- });
368
- var httpResponseStageSchema = z5.object({
369
- protocol: httpProtocolSchema.optional(),
370
- response: httpResponseSchema
371
- });
372
- var tokenSourceSchema = z5.enum([
373
- "authorization",
374
- "query",
375
- "client",
376
- "unknown"
377
- ]);
378
- var tokenTypeSchema = z5.enum(["auth", "pat", "app", "unknown"]);
379
- var firewallRuleSchema = z5.discriminatedUnion("kind", [
380
- z5.object({ kind: z5.literal("none") }),
381
- z5.object({ kind: z5.literal("mtls") }),
382
- z5.object({
383
- kind: z5.literal("rstream_auth"),
384
- source: z5.literal("rstream_auth")
385
- }),
386
- z5.object({
387
- kind: z5.literal("token"),
388
- source: tokenSourceSchema.optional(),
389
- tokenType: tokenTypeSchema.optional()
390
- })
391
- ]);
392
- var unauthorizedSourceSchema = z5.object({
393
- source: tokenSourceSchema
394
- });
395
- var unauthorizedReasonSchema = (shape) => z5.object(shape).merge(unauthorizedSourceSchema);
396
- var unauthorizedSchema = z5.discriminatedUnion("reason", [
397
- unauthorizedReasonSchema({
398
- reason: z5.literal("missing_token")
399
- }),
400
- unauthorizedReasonSchema({
401
- reason: z5.literal("invalid_token")
402
- }),
403
- unauthorizedReasonSchema({
404
- reason: z5.literal("expired_token")
405
- }),
406
- unauthorizedReasonSchema({
407
- reason: z5.literal("invalid_secret")
408
- }),
409
- unauthorizedReasonSchema({
410
- reason: z5.literal("user_mismatch")
411
- }),
412
- unauthorizedReasonSchema({
413
- reason: z5.literal("insufficient_scope")
414
- }),
415
- unauthorizedReasonSchema({
416
- reason: z5.literal("policy_denied"),
417
- policy: z5.enum(["ip", "geo", "mtls", "challenge", "other"]),
418
- value: z5.string().optional()
419
- })
420
- ]);
421
- var failureSchema = z5.discriminatedUnion("kind", [
422
- z5.object({
423
- kind: z5.literal("timeout"),
424
- timeoutMs: intNonNegSchema.optional(),
425
- message: z5.string().optional()
426
- }),
427
- z5.object({
428
- kind: z5.literal("unauthorized"),
429
- why: unauthorizedSchema,
430
- message: z5.string().optional()
431
- }),
432
- z5.object({
433
- kind: z5.literal("not_found"),
434
- resource: z5.enum(["tunnel", "client", "route"]),
435
- message: z5.string().optional()
436
- }),
437
- z5.object({
438
- kind: z5.literal("refused"),
439
- reason: z5.enum(["capacity", "policy", "unsupported", "other"]),
440
- message: z5.string().optional()
441
- }),
442
- z5.object({ kind: z5.literal("io"), message: z5.string().optional() }),
443
- z5.object({ kind: z5.literal("internal"), message: z5.string().optional() })
444
- ]);
445
- var firewallSchema = z5.discriminatedUnion("decision", [
446
- z5.object({ decision: z5.literal("allowed"), rule: firewallRuleSchema }),
447
- z5.object({
448
- decision: z5.literal("denied"),
449
- rule: firewallRuleSchema,
450
- failure: failureSchema
451
- })
452
- ]);
453
- var routedTunnelSchema = z5.object({
454
- tunnelId: nonEmptyStringSchema2.optional(),
455
- tunnelName: z5.string().optional(),
456
- hostname: z5.string().optional(),
457
- protocol: z5.string().optional(),
458
- published: z5.boolean().optional(),
459
- type: z5.string().optional(),
460
- tlsMode: z5.string().optional(),
461
- httpVersion: z5.string().optional(),
462
- httpUseTls: z5.boolean().optional(),
463
- upstreamTls: z5.boolean().optional(),
464
- tokenAuth: z5.boolean().optional(),
465
- mtls: z5.boolean().optional()
466
- });
467
- var routingSchema = z5.discriminatedUnion("decision", [
468
- z5.object({ decision: z5.literal("routed"), tunnel: routedTunnelSchema }),
469
- z5.object({ decision: z5.literal("not_routed"), failure: failureSchema })
470
- ]);
471
- var streamConnectedMetricsSchema = z5.object({
472
- establish_ms: numNonNegSchema,
473
- duration_ms: numNonNegSchema,
474
- upstream_bytes: intNonNegSchema,
475
- downstream_bytes: intNonNegSchema
476
- });
477
- var streamFailedMetricsSchema = z5.object({
478
- time_to_fail_ms: numNonNegSchema.optional(),
479
- establish_ms: numNonNegSchema.optional()
480
- });
481
- var terminationSchema = z5.object({
482
- by: z5.enum(["downstream", "upstream", "server", "unknown"]),
483
- reason: z5.enum(["closed", "reset", "timeout", "error", "unknown"]),
484
- message: z5.string().optional()
485
- });
486
- var responseConnectedSchema = z5.object({
487
- outcome: z5.literal("connected"),
488
- established_at: isoDateTimeSchema,
489
- upstream: streamEndpointSchema.optional(),
490
- metrics: streamConnectedMetricsSchema,
491
- termination: terminationSchema.optional(),
492
- http: httpResponseStageSchema.optional()
493
- });
494
- var responseFailedSchema = z5.object({
495
- outcome: z5.literal("failed"),
496
- failure: failureSchema,
497
- upstream: streamEndpointSchema.optional(),
498
- metrics: streamFailedMetricsSchema.optional(),
499
- termination: terminationSchema.optional(),
500
- http: httpResponseStageSchema.optional()
501
- });
502
- var responseSchema = z5.discriminatedUnion("outcome", [
503
- responseConnectedSchema,
504
- responseFailedSchema
505
- ]);
506
- var streamSummarySchema = z5.object({
507
- workspace_id: nonEmptyStringSchema2.optional(),
508
- project_id: nonEmptyStringSchema2.optional(),
509
- cluster_id: nonEmptyStringSchema2.optional(),
510
- user_id: nonEmptyStringSchema2.optional(),
511
- client_id: nonEmptyStringSchema2.optional(),
512
- tunnel_id: nonEmptyStringSchema2.optional(),
513
- stream_id: nonEmptyStringSchema2,
514
- created_at: isoDateTimeSchema,
515
- terminated_at: isoDateTimeSchema,
516
- request: z5.object({
517
- downstream: streamEndpointSchema,
518
- http: httpRequestStageSchema.optional()
519
- }),
520
- firewall: firewallSchema,
521
- routing: routingSchema,
522
- response: responseSchema
523
- });
524
-
525
- // src/event.ts
526
- import * as z6 from "zod";
527
- var isoDateTimeSchema2 = z6.string().datetime({ offset: true });
528
- var initialStateSchema = z6.object({
529
- snapshot_at: isoDateTimeSchema2,
530
- clients: z6.array(clientSchema),
531
- tunnels: z6.array(tunnelSchema)
532
- });
533
- var eventBaseSchema = z6.object({
534
- id: z6.string().optional(),
535
- created_at: isoDateTimeSchema2.optional(),
536
- user_id: z6.string().optional(),
537
- workspace_id: z6.string().optional(),
538
- project_id: z6.string().optional(),
539
- cluster_id: z6.string().optional()
540
- });
541
- var withEventBase = (shape) => z6.object(shape).merge(eventBaseSchema).passthrough();
542
- var stateInitialEventSchema = withEventBase({
543
- type: z6.literal("state.initial"),
544
- object: initialStateSchema
545
- });
546
- var streamSummaryEventSchema = withEventBase({
547
- type: z6.literal("stream.summary"),
548
- object: streamSummarySchema
549
- });
550
- var commonEventsSchema = [
551
- withEventBase({
552
- type: z6.literal("client.created"),
553
- object: clientSchema
554
- }),
555
- withEventBase({
556
- type: z6.literal("client.updated"),
557
- object: clientSchema
558
- }),
559
- withEventBase({
560
- type: z6.literal("client.deleted"),
561
- object: clientSchema
562
- }),
563
- withEventBase({
564
- type: z6.literal("tunnel.created"),
565
- object: tunnelSchema
566
- }),
567
- withEventBase({
568
- type: z6.literal("tunnel.updated"),
569
- object: tunnelSchema
570
- }),
571
- withEventBase({
572
- type: z6.literal("tunnel.deleted"),
573
- object: tunnelSchema
574
- })
575
- ];
576
- var wsEventsSchema = z6.union([
577
- stateInitialEventSchema,
578
- ...commonEventsSchema
579
- ]);
580
- var webhookEventsSchema = z6.union([
581
- ...commonEventsSchema,
582
- streamSummaryEventSchema
583
- ]);
584
- var eventSchema = z6.union([wsEventsSchema, webhookEventsSchema]);
585
-
586
309
  // src/resolution.ts
587
310
  import { DEFAULT_API_URL } from "@rstreamlabs/rstream";
588
311
  import { getTunnelsProjectEngine } from "@rstreamlabs/rstream";
589
312
  import { isTokenCredentials } from "@rstreamlabs/rstream";
590
313
  import { readEnvironment } from "@rstreamlabs/rstream";
591
314
  import { RstreamClient } from "@rstreamlabs/rstream";
592
- import jwt from "jsonwebtoken";
593
315
  function trimOptionalString(value) {
594
316
  const normalized = value?.trim();
595
317
  return normalized ? normalized : void 0;
596
318
  }
597
- function getEngineFromToken(token) {
598
- if (!token) {
599
- return void 0;
319
+ var dnsLabelPattern = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i;
320
+ function validateHostname(hostname) {
321
+ if (!hostname || hostname.length > 253 || hostname.includes("..")) {
322
+ return false;
600
323
  }
601
- const parsed = authTokenSchema.safeParse(
602
- jwt.decode(token, { complete: false })
603
- );
604
- if (!parsed.success) {
605
- return void 0;
324
+ return hostname.split(".").every((label) => label === "localhost" || dnsLabelPattern.test(label));
325
+ }
326
+ function normalizeEngineAddress(engine) {
327
+ const normalized = engine.trim();
328
+ if (!normalized || /^[a-z][a-z0-9+.-]*:\/\//i.test(normalized) || /[/?#@\\\s]/.test(normalized)) {
329
+ throw new Error("Engine must be a host[:port] value.");
330
+ }
331
+ const explicitPort = normalized.match(/:(\d+)$/)?.[1];
332
+ const url = new URL(`https://${normalized}`);
333
+ if (url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
334
+ throw new Error("Engine must be a host[:port] value.");
335
+ }
336
+ if (!validateHostname(url.hostname)) {
337
+ throw new Error("Engine must use a valid hostname.");
338
+ }
339
+ if (explicitPort) {
340
+ const port = Number(explicitPort);
341
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
342
+ throw new Error("Engine port must be an integer between 1 and 65535.");
343
+ }
606
344
  }
607
- return trimOptionalString(parsed.data.metadata?.engine);
345
+ return `${url.hostname.toLowerCase()}${explicitPort ? `:${explicitPort}` : ""}`;
608
346
  }
609
347
  function resolveTunnelsAPIURL(apiUrl) {
610
348
  return trimOptionalString(apiUrl) ?? readEnvironment().apiUrl ?? DEFAULT_API_URL;
@@ -638,37 +376,22 @@ function resolveControlPlaneCredentials(controlPlaneCredentials, credentials, au
638
376
  async function resolveTunnelsEngine(config) {
639
377
  const explicitEngine = trimOptionalString(config.engine);
640
378
  if (explicitEngine) {
641
- return explicitEngine;
379
+ return normalizeEngineAddress(explicitEngine);
642
380
  }
643
381
  const environmentEngine = readEnvironment().engine;
644
382
  if (environmentEngine) {
645
- return environmentEngine;
383
+ return normalizeEngineAddress(environmentEngine);
646
384
  }
647
385
  const token = trimOptionalString(config.token) ?? (isTokenCredentials(config.credentials) ? config.credentials.token : void 0);
648
- const tokenEngine = getEngineFromToken(token);
649
- if (tokenEngine) {
650
- return tokenEngine;
651
- }
652
386
  const projectEndpoint = trimOptionalString(config.projectEndpoint);
653
387
  if (projectEndpoint) {
654
- const credentials = resolveControlPlaneCredentials(
655
- config.controlPlaneCredentials,
656
- config.credentials,
388
+ const project = await resolveManagedTunnelsProject({
389
+ ...config,
657
390
  token
658
- );
659
- if (credentials === void 0) {
660
- throw new Error(
661
- "Control-plane credentials are required to resolve a managed project endpoint."
662
- );
663
- }
664
- const controlPlane = new RstreamClient({
665
- apiUrl: resolveTunnelsAPIURL(config.apiUrl),
666
- credentials
667
391
  });
668
- const project = await controlPlane.tunnels.projects.resolveByEndpoint(projectEndpoint);
669
392
  const engine = getTunnelsProjectEngine(project);
670
393
  if (engine) {
671
- return engine;
394
+ return normalizeEngineAddress(engine);
672
395
  }
673
396
  throw new Error(
674
397
  "Failed to resolve the engine address from the managed tunnels project."
@@ -678,8 +401,36 @@ async function resolveTunnelsEngine(config) {
678
401
  "Engine URL is not defined. Provide an engine, use RSTREAM_ENGINE, or configure a managed project endpoint with control-plane access."
679
402
  );
680
403
  }
404
+ async function resolveManagedTunnelsProject(config) {
405
+ const projectEndpoint = trimOptionalString(config.projectEndpoint);
406
+ if (!projectEndpoint) {
407
+ throw new Error("Managed project endpoint is required.");
408
+ }
409
+ const credentials = resolveControlPlaneCredentials(
410
+ config.controlPlaneCredentials,
411
+ config.credentials,
412
+ config.token
413
+ );
414
+ if (credentials === void 0) {
415
+ throw new Error(
416
+ "Control-plane credentials are required to resolve a managed project endpoint."
417
+ );
418
+ }
419
+ const controlPlane = new RstreamClient({
420
+ apiUrl: resolveTunnelsAPIURL(config.apiUrl),
421
+ credentials
422
+ });
423
+ return await controlPlane.tunnels.projects.resolveByEndpoint(projectEndpoint);
424
+ }
681
425
 
682
426
  // src/tunnels-resource.ts
427
+ function tunnelPathId(id) {
428
+ const normalized = id.trim();
429
+ if (!normalized) {
430
+ throw new Error("Tunnel ID is required.");
431
+ }
432
+ return encodeURIComponent(normalized);
433
+ }
683
434
  var RstreamTunnelsResource = class {
684
435
  client;
685
436
  constructor(client) {
@@ -693,65 +444,76 @@ var RstreamTunnelsResource = class {
693
444
  return listTunnelsResponseSchema.parse(response);
694
445
  }
695
446
  async get(id) {
696
- const response = await this.client.request(`/tunnels/${id}`, {
697
- method: "GET"
698
- });
447
+ const response = await this.client.request(
448
+ `/tunnels/${tunnelPathId(id)}`,
449
+ {
450
+ method: "GET"
451
+ }
452
+ );
699
453
  return tunnelSchema.parse(response);
700
454
  }
701
455
  };
702
456
 
703
457
  // src/tunnels.ts
458
+ import { getTunnelsProjectEngine as getTunnelsProjectEngine2 } from "@rstreamlabs/rstream";
704
459
  import { isClientCredentials as isClientCredentials3 } from "@rstreamlabs/rstream";
460
+ import { readEnvironment as readEnvironment2 } from "@rstreamlabs/rstream";
705
461
 
706
462
  // src/turn.ts
707
463
  import { isClientCredentials as isClientCredentials2 } from "@rstreamlabs/rstream";
708
464
  import { isTokenCredentials as isTokenCredentials2 } from "@rstreamlabs/rstream";
709
465
  import { RstreamClient as RstreamClient2 } from "@rstreamlabs/rstream";
710
466
  import { turnCredentialsSchema } from "@rstreamlabs/rstream";
711
- import * as z7 from "zod";
467
+ import * as z3 from "zod";
712
468
  import crypto from "crypto";
713
469
  var defaultTURNPort = 3478;
714
470
  var defaultTURNSPort = 5349;
715
- var defaultTURNCredentialTTLSeconds = 24 * 60 * 60;
716
- var turnTokenClaimsSchema = z7.object({
717
- token_endpoint: z7.string().optional(),
718
- type: z7.string()
471
+ var defaultTURNCredentialTTLSeconds = 10 * 60;
472
+ var maxTURNCredentialTTLSeconds = 60 * 60;
473
+ var turnTokenClaimsSchema = z3.object({
474
+ exp: z3.number().int().optional(),
475
+ iat: z3.number().int().optional(),
476
+ token_endpoint: z3.string().regex(/^[0-9a-f]{8}$/i).optional(),
477
+ type: z3.enum(["app", "auth", "pat"])
719
478
  });
720
- var turnCredentialModeSchema = z7.enum(["api", "app", "pat"]);
721
- function normalizeOptionalString(value) {
479
+ var turnCredentialModeSchema = z3.enum(["api", "app", "pat"]);
480
+ function normalizeOptionalString2(value) {
722
481
  const normalized = value?.trim();
723
482
  return normalized ? normalized : void 0;
724
483
  }
725
484
  function normalizeClusterDomain(clusterDomain) {
726
- const normalized = normalizeOptionalString(clusterDomain);
485
+ const normalized = normalizeOptionalString2(clusterDomain);
727
486
  return normalized?.toLowerCase();
728
487
  }
729
488
  function normalizeNow(now) {
730
- if (now instanceof Date) {
731
- return Math.floor(now.getTime() / 1e3);
489
+ if (now === void 0) {
490
+ return Math.floor(Date.now() / 1e3);
732
491
  }
733
- if (typeof now === "number" && Number.isFinite(now)) {
734
- return Math.floor(now);
492
+ const timestamp = now instanceof Date ? now.getTime() / 1e3 : now;
493
+ if (!Number.isFinite(timestamp)) {
494
+ throw new Error("TURN timestamp must be finite.");
735
495
  }
736
- return Math.floor(Date.now() / 1e3);
496
+ return Math.floor(timestamp);
737
497
  }
738
498
  function normalizeTTLSeconds(ttlSeconds) {
739
- if (typeof ttlSeconds === "number" && Number.isFinite(ttlSeconds) && ttlSeconds > 0) {
740
- return Math.floor(ttlSeconds);
499
+ const ttl = ttlSeconds ?? defaultTURNCredentialTTLSeconds;
500
+ if (!Number.isInteger(ttl) || ttl < 1 || ttl > maxTURNCredentialTTLSeconds) {
501
+ throw new Error("TURN TTL must be an integer between 1 and 3600 seconds.");
741
502
  }
742
- return defaultTURNCredentialTTLSeconds;
503
+ return ttl;
743
504
  }
744
- function normalizeTURNPort(port) {
745
- if (typeof port === "number" && Number.isFinite(port) && port > 0) {
746
- return Math.floor(port);
505
+ function normalizePort(port, fallback, name) {
506
+ const normalized = port ?? fallback;
507
+ if (!Number.isInteger(normalized) || normalized < 1 || normalized > 65535) {
508
+ throw new Error(`${name} must be an integer between 1 and 65535.`);
747
509
  }
748
- return defaultTURNPort;
510
+ return normalized;
511
+ }
512
+ function normalizeTURNPort(port) {
513
+ return normalizePort(port, defaultTURNPort, "TURN port");
749
514
  }
750
515
  function normalizeTURNSPort(port) {
751
- if (typeof port === "number" && Number.isFinite(port) && port > 0) {
752
- return Math.floor(port);
753
- }
754
- return defaultTURNSPort;
516
+ return normalizePort(port, defaultTURNSPort, "TURNS port");
755
517
  }
756
518
  function createTURNURLs(clusterDomain, turnPort, turnsPort) {
757
519
  const normalizedTURNPort = normalizeTURNPort(turnPort);
@@ -769,16 +531,32 @@ function parseTURNTokenClaims(token) {
769
531
  if (!payloadPart) {
770
532
  throw new Error("Invalid token format.");
771
533
  }
772
- const payload = Buffer.from(payloadPart, "base64url").toString("utf8");
773
- const parsed = turnTokenClaimsSchema.safeParse(JSON.parse(payload));
534
+ let decoded;
535
+ try {
536
+ const payload = Buffer.from(payloadPart, "base64url").toString("utf8");
537
+ decoded = JSON.parse(payload);
538
+ } catch {
539
+ throw new Error("Invalid token format.");
540
+ }
541
+ const parsed = turnTokenClaimsSchema.safeParse(decoded);
774
542
  if (!parsed.success) {
775
543
  throw new Error("Invalid token format.");
776
544
  }
777
545
  return parsed.data;
778
546
  }
547
+ function requirePATExpiration(claims, now) {
548
+ const exp = claims.exp;
549
+ if (typeof exp !== "number") {
550
+ throw new Error("TURN PAT mode requires a PAT token with expiration.");
551
+ }
552
+ if (exp <= now) {
553
+ throw new Error("TURN PAT mode requires a non-expired PAT token.");
554
+ }
555
+ return exp;
556
+ }
779
557
  function deriveClusterDomainFromEngine(projectEndpoint, engine) {
780
- const normalizedProjectEndpoint = normalizeOptionalString(projectEndpoint);
781
- const normalizedEngine = normalizeOptionalString(engine);
558
+ const normalizedProjectEndpoint = normalizeOptionalString2(projectEndpoint);
559
+ const normalizedEngine = normalizeOptionalString2(engine);
782
560
  if (!normalizedProjectEndpoint || !normalizedEngine) {
783
561
  throw new Error(
784
562
  "Failed to derive the cluster domain from the engine address."
@@ -812,17 +590,11 @@ function resolveMode(credentials, mode) {
812
590
  if (isClientCredentials2(credentials)) {
813
591
  return "app";
814
592
  }
815
- if (isTokenCredentials2(credentials)) {
816
- const claims = parseTURNTokenClaims(credentials.token);
817
- if (claims.type === "pat" && normalizeOptionalString(claims.token_endpoint)) {
818
- return "pat";
819
- }
820
- }
821
593
  return "api";
822
594
  }
823
595
  async function loadTURNServerPublicKeyHex(clusterDomain, options) {
824
- const configuredKey = normalizeOptionalString(options.keyringBaseUrl);
825
- const keyringBaseURL = configuredKey ?? normalizeOptionalString(options.apiUrl) ?? "https://rstream.io";
596
+ const configuredKey = normalizeOptionalString2(options.keyringBaseUrl);
597
+ const keyringBaseURL = configuredKey ?? normalizeOptionalString2(options.apiUrl) ?? "https://rstream.io";
826
598
  const url = new URL(
827
599
  `/keyrings/turn/${clusterDomain}.spki.der.hex`,
828
600
  keyringBaseURL
@@ -834,8 +606,8 @@ async function loadTURNServerPublicKeyHex(clusterDomain, options) {
834
606
  return (await response.text()).trim();
835
607
  }
836
608
  async function createAPITURNCredentials(options) {
837
- const projectId = normalizeOptionalString(options.projectId);
838
- const projectEndpoint = normalizeOptionalString(options.projectEndpoint);
609
+ const projectId = normalizeOptionalString2(options.projectId);
610
+ const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
839
611
  if (!projectId && !projectEndpoint) {
840
612
  throw new Error(
841
613
  "Project ID or project endpoint is required for TURN API mode."
@@ -862,11 +634,11 @@ function createPATTURNCredentials(options) {
862
634
  if (!clusterDomain) {
863
635
  throw new Error("Cluster domain is required for TURN PAT mode.");
864
636
  }
865
- const projectEndpoint = normalizeOptionalString(options.projectEndpoint);
637
+ const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
866
638
  if (!projectEndpoint) {
867
639
  throw new Error("Project endpoint is required for TURN PAT mode.");
868
640
  }
869
- const token = normalizeOptionalString(options.token);
641
+ const token = normalizeOptionalString2(options.token);
870
642
  if (!token) {
871
643
  throw new Error("Token is required for TURN PAT mode.");
872
644
  }
@@ -874,14 +646,17 @@ function createPATTURNCredentials(options) {
874
646
  if (claims.type !== "pat") {
875
647
  throw new Error("TURN PAT mode requires a PAT token.");
876
648
  }
877
- const tokenEndpoint = normalizeOptionalString(claims.token_endpoint);
649
+ const tokenEndpoint = normalizeOptionalString2(options.tokenEndpoint);
878
650
  if (!tokenEndpoint) {
879
- throw new Error(
880
- "TURN PAT mode requires a PAT token carrying a token endpoint."
881
- );
651
+ throw new Error("Token endpoint is required for TURN PAT mode.");
652
+ }
653
+ if (claims.token_endpoint !== void 0 && claims.token_endpoint !== tokenEndpoint) {
654
+ throw new Error("Token endpoint does not match the PAT token.");
882
655
  }
656
+ const now = normalizeNow(options.now);
883
657
  const ttlSeconds = normalizeTTLSeconds(options.ttlSeconds);
884
- const exp = normalizeNow(options.now) + ttlSeconds;
658
+ const exp = Math.min(now + ttlSeconds, requirePATExpiration(claims, now));
659
+ const ttl = exp - now;
885
660
  const username = `v1:${exp}:pat:${projectEndpoint}:${tokenEndpoint}`;
886
661
  const tokenHash = crypto.createHash("sha256").update(token, "utf8").digest();
887
662
  const key = Buffer.from(
@@ -896,7 +671,7 @@ function createPATTURNCredentials(options) {
896
671
  const credential = crypto.createHmac("sha256", key).update(username, "utf8").digest("base64");
897
672
  return turnCredentialsSchema.parse({
898
673
  credential,
899
- ttl: ttlSeconds,
674
+ ttl,
900
675
  urls: createTURNURLs(clusterDomain, options.turnPort, options.turnsPort),
901
676
  username
902
677
  });
@@ -906,15 +681,15 @@ async function createAPPTURNCredentials(options) {
906
681
  if (!clusterDomain) {
907
682
  throw new Error("Cluster domain is required for TURN APP mode.");
908
683
  }
909
- const projectEndpoint = normalizeOptionalString(options.projectEndpoint);
684
+ const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
910
685
  if (!projectEndpoint) {
911
686
  throw new Error("Project endpoint is required for TURN APP mode.");
912
687
  }
913
- const clientId = normalizeOptionalString(options.clientId);
688
+ const clientId = normalizeOptionalString2(options.clientId);
914
689
  if (!clientId) {
915
690
  throw new Error("Client ID is required for TURN APP mode.");
916
691
  }
917
- const clientSecret = normalizeOptionalString(options.clientSecret);
692
+ const clientSecret = normalizeOptionalString2(options.clientSecret);
918
693
  if (!clientSecret) {
919
694
  throw new Error("Client secret is required for TURN APP mode.");
920
695
  }
@@ -926,7 +701,7 @@ async function createAPPTURNCredentials(options) {
926
701
  format: "der",
927
702
  type: "pkcs8"
928
703
  });
929
- const serverPublicKeyHex = normalizeOptionalString(options.serverPublicKeyHex) ?? await loadTURNServerPublicKeyHex(clusterDomain, options);
704
+ const serverPublicKeyHex = normalizeOptionalString2(options.serverPublicKeyHex) ?? await loadTURNServerPublicKeyHex(clusterDomain, options);
930
705
  const serverPublicKey = crypto.createPublicKey({
931
706
  key: Buffer.from(serverPublicKeyHex, "hex"),
932
707
  format: "der",
@@ -956,7 +731,7 @@ async function createAPPTURNCredentials(options) {
956
731
  async function createTURNCredentials(options) {
957
732
  const credentials = options.credentials;
958
733
  const mode = resolveMode(credentials, options.mode);
959
- const projectEndpoint = normalizeOptionalString(options.projectEndpoint);
734
+ const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
960
735
  if (mode === "api") {
961
736
  const controlPlaneCredentials = options.controlPlaneCredentials ?? credentials;
962
737
  if (!controlPlaneCredentials) {
@@ -987,6 +762,10 @@ async function createTURNCredentials(options) {
987
762
  "Project endpoint is required for TURN PAT mode."
988
763
  ),
989
764
  token: credentials.token,
765
+ tokenEndpoint: requireOption(
766
+ normalizeOptionalString2(options.tokenEndpoint),
767
+ "Token endpoint is required for TURN PAT mode."
768
+ ),
990
769
  ttlSeconds: options.ttlSeconds,
991
770
  turnPort: options.turnPort,
992
771
  turnsPort: options.turnsPort
@@ -1036,6 +815,7 @@ var RstreamTURNResource = class {
1036
815
  projectEndpoint: options.projectEndpoint ?? this.client.projectEndpoint,
1037
816
  projectId: options.projectId,
1038
817
  serverPublicKeyHex: options.serverPublicKeyHex,
818
+ tokenEndpoint: options.tokenEndpoint,
1039
819
  ttlSeconds: options.ttlSeconds,
1040
820
  turnPort: options.turnPort,
1041
821
  turnsPort: options.turnsPort
@@ -1045,6 +825,41 @@ var RstreamTURNResource = class {
1045
825
 
1046
826
  // src/webhooks-resource.ts
1047
827
  import crypto2 from "crypto";
828
+ var SIGNATURE_HEX_BYTES = 32;
829
+ var signatureHexSchema = /^[0-9a-fA-F]{64}$/;
830
+ var timestampSchema = /^\d+$/;
831
+ function parseSignatureTimestamp(timestamp) {
832
+ if (!timestampSchema.test(timestamp)) {
833
+ throw new Error("Invalid signature timestamp");
834
+ }
835
+ const parsed = Number(timestamp);
836
+ if (!Number.isSafeInteger(parsed)) {
837
+ throw new Error("Invalid signature timestamp");
838
+ }
839
+ return parsed;
840
+ }
841
+ function normalizeTolerance(tolerance) {
842
+ if (!Number.isFinite(tolerance) || !Number.isInteger(tolerance) || tolerance < 0) {
843
+ throw new Error("Invalid signature tolerance");
844
+ }
845
+ return tolerance;
846
+ }
847
+ function readSignature(signature) {
848
+ if (!signatureHexSchema.test(signature)) {
849
+ return void 0;
850
+ }
851
+ const parsed = Buffer.from(signature, "hex");
852
+ return parsed.length === SIGNATURE_HEX_BYTES ? parsed : void 0;
853
+ }
854
+ function signatureMatches(signature, expected) {
855
+ const parsed = readSignature(signature);
856
+ return parsed === void 0 ? false : crypto2.timingSafeEqual(parsed, expected);
857
+ }
858
+ function assertSecretConfigured(secret) {
859
+ if (secret.trim().length === 0) {
860
+ throw new Error("Webhook signing secret is required");
861
+ }
862
+ }
1048
863
  var RstreamWebhookResource = class {
1049
864
  async event(payload, header, secret, tolerance = 300, receivedAt = Date.now()) {
1050
865
  const payloadBuffer = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
@@ -1052,31 +867,26 @@ var RstreamWebhookResource = class {
1052
867
  if (!signatureHeader) {
1053
868
  throw new Error("No signature header");
1054
869
  }
870
+ assertSecretConfigured(secret);
1055
871
  const elements = signatureHeader.split(",").map((element) => element.trim());
1056
872
  const timestamp = elements.find((element) => element.startsWith("t="))?.substring(2);
1057
873
  const signatures = elements.filter((element) => element.startsWith("v1=")).map((element) => element.substring(3));
1058
874
  if (!timestamp || signatures.length === 0) {
1059
875
  throw new Error("Invalid signature header format");
1060
876
  }
1061
- const timestampNum = parseInt(timestamp, 10);
877
+ const timestampNum = parseSignatureTimestamp(timestamp);
878
+ const signatureTolerance = normalizeTolerance(tolerance);
1062
879
  const now = Math.floor(receivedAt / 1e3);
1063
- if (Math.abs(now - timestampNum) > tolerance) {
880
+ if (Math.abs(now - timestampNum) > signatureTolerance) {
1064
881
  throw new Error(
1065
882
  `Webhook signature timestamp outside tolerance: ${now}, ${timestampNum}`
1066
883
  );
1067
884
  }
1068
885
  const signedPayload = `${timestamp}.${payloadBuffer.toString("utf8")}`;
1069
- const expectedSignature = crypto2.createHmac("sha256", secret).update(signedPayload).digest("hex");
1070
- const signatureMatched = signatures.some((signature) => {
1071
- try {
1072
- return crypto2.timingSafeEqual(
1073
- Buffer.from(signature),
1074
- Buffer.from(expectedSignature)
1075
- );
1076
- } catch {
1077
- return false;
1078
- }
1079
- });
886
+ const expectedSignature = crypto2.createHmac("sha256", secret).update(signedPayload).digest();
887
+ const signatureMatched = signatures.some(
888
+ (signature) => signatureMatches(signature, expectedSignature)
889
+ );
1080
890
  if (!signatureMatched) {
1081
891
  throw new Error("Signature verification failed");
1082
892
  }
@@ -1094,8 +904,16 @@ var RstreamWebhookResource = class {
1094
904
  };
1095
905
 
1096
906
  // src/tunnels.ts
907
+ var engineAuthTokenScopes = {
908
+ tunnels: {
909
+ connect: true,
910
+ create: true,
911
+ list: true
912
+ }
913
+ };
1097
914
  var RstreamTunnelsClient = class {
1098
915
  config;
916
+ managedProject;
1099
917
  constructor(config) {
1100
918
  this.config = config;
1101
919
  }
@@ -1115,6 +933,21 @@ var RstreamTunnelsClient = class {
1115
933
  return this.config?.projectEndpoint;
1116
934
  }
1117
935
  async getEngine() {
936
+ const projectEndpoint = normalizeOptionalString3(
937
+ this.config?.projectEndpoint
938
+ );
939
+ if (projectEndpoint !== void 0 && normalizeOptionalString3(this.config?.engine) === void 0 && readEnvironment2().engine === void 0) {
940
+ const project = await this.getManagedProject();
941
+ if (project !== void 0) {
942
+ const engine = getTunnelsProjectEngine2(project);
943
+ if (engine) {
944
+ return normalizeEngineAddress(engine);
945
+ }
946
+ }
947
+ throw new Error(
948
+ "Failed to resolve the engine address from the managed tunnels project."
949
+ );
950
+ }
1118
951
  return await resolveTunnelsEngine({
1119
952
  apiUrl: this.config?.apiUrl,
1120
953
  controlPlaneCredentials: this.controlPlaneCredentials,
@@ -1123,6 +956,57 @@ var RstreamTunnelsClient = class {
1123
956
  projectEndpoint: this.config?.projectEndpoint
1124
957
  });
1125
958
  }
959
+ async getManagedProject() {
960
+ const projectEndpoint = normalizeOptionalString3(
961
+ this.config?.projectEndpoint
962
+ );
963
+ if (projectEndpoint === void 0) {
964
+ return void 0;
965
+ }
966
+ if (this.managedProject === void 0) {
967
+ this.managedProject = resolveManagedTunnelsProject({
968
+ apiUrl: this.config?.apiUrl,
969
+ controlPlaneCredentials: this.controlPlaneCredentials,
970
+ credentials: this.credentials,
971
+ projectEndpoint
972
+ }).catch((error) => {
973
+ this.managedProject = void 0;
974
+ throw error;
975
+ });
976
+ }
977
+ return await this.managedProject;
978
+ }
979
+ async getEngineAuthTokenParams() {
980
+ const target = await this.getEngineAuthTokenTarget();
981
+ return {
982
+ tunnelsGrants: [
983
+ {
984
+ ...target,
985
+ scopes: engineAuthTokenScopes
986
+ }
987
+ ]
988
+ };
989
+ }
990
+ async getEngineAuthTokenTarget() {
991
+ const projectId = normalizeOptionalString3(this.config?.projectId);
992
+ const workspaceId = normalizeOptionalString3(this.config?.workspaceId);
993
+ if (projectId !== void 0 && workspaceId !== void 0) {
994
+ throw new Error("Use either projectId or workspaceId, not both.");
995
+ }
996
+ if (projectId !== void 0) {
997
+ return { projects: [projectId] };
998
+ }
999
+ if (workspaceId !== void 0) {
1000
+ return { workspaces: [workspaceId] };
1001
+ }
1002
+ const project = await this.getManagedProject();
1003
+ if (project !== void 0) {
1004
+ return { projects: [project.id] };
1005
+ }
1006
+ throw new Error(
1007
+ "Application credentials require projectId, workspaceId, or projectEndpoint to create scoped engine tokens."
1008
+ );
1009
+ }
1126
1010
  get auth() {
1127
1011
  return new RstreamAuthResource(this);
1128
1012
  }
@@ -1146,13 +1030,17 @@ var RstreamTunnelsClient = class {
1146
1030
  if (!isClientCredentials3(credentials)) {
1147
1031
  return credentials.token;
1148
1032
  }
1149
- return (await this.auth.createAuthToken(void 0, {
1033
+ const tokenParams = await this.getEngineAuthTokenParams();
1034
+ return (await this.auth.createAuthToken(tokenParams, {
1150
1035
  credentials,
1151
1036
  engine
1152
1037
  })).token;
1153
1038
  }
1154
1039
  async request(path, options) {
1155
- const engine = await this.getEngine();
1040
+ const engine = normalizeEngineAddress(await this.getEngine());
1041
+ if (!path.startsWith("/") || path.startsWith("//")) {
1042
+ throw new Error("Engine request path must be a relative absolute path.");
1043
+ }
1156
1044
  const url = `https://${engine}/api${path}`;
1157
1045
  const headers = new Headers(options?.headers || {});
1158
1046
  const token = await this.getToken(engine);
@@ -1161,7 +1049,8 @@ var RstreamTunnelsClient = class {
1161
1049
  }
1162
1050
  const response = await fetch(url, {
1163
1051
  ...options,
1164
- headers
1052
+ headers,
1053
+ redirect: options?.redirect ?? "manual"
1165
1054
  });
1166
1055
  if (!response.ok) {
1167
1056
  const errorText = await response.text();
@@ -1170,8 +1059,68 @@ var RstreamTunnelsClient = class {
1170
1059
  return await response.json();
1171
1060
  }
1172
1061
  };
1062
+ function normalizeOptionalString3(value) {
1063
+ const normalized = value?.trim();
1064
+ return normalized ? normalized : void 0;
1065
+ }
1173
1066
 
1174
1067
  // src/watch.ts
1068
+ import jwt from "jsonwebtoken";
1069
+ var maxWatchTokenLifetimeSeconds = 3600;
1070
+ var watchTokenIssuedAtSkewSeconds = 300;
1071
+ function normalizeTransport(transport) {
1072
+ if (transport === void 0) {
1073
+ return "sse";
1074
+ }
1075
+ if (transport === "sse" || transport === "websocket") {
1076
+ return transport;
1077
+ }
1078
+ throw new Error("Watch: Unsupported transport.");
1079
+ }
1080
+ function validateWatchQueryToken(token) {
1081
+ const parsed = authTokenSchema.safeParse(jwt.decode(token));
1082
+ if (!parsed.success || parsed.data.type === "pat") {
1083
+ throw new Error(
1084
+ "Watch requires a short-lived auth or app token for URL-based authentication."
1085
+ );
1086
+ }
1087
+ const iat = parsed.data.iat;
1088
+ const exp = parsed.data.exp;
1089
+ const now = Math.floor(Date.now() / 1e3);
1090
+ 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) {
1091
+ throw new Error(
1092
+ "Watch requires a token with a bounded lifetime of at most 3600 seconds."
1093
+ );
1094
+ }
1095
+ if (!hasWatchOnlyTunnelGrants(parsed.data)) {
1096
+ throw new Error(
1097
+ "Watch requires a fine-grained token limited to tunnel list grants for URL-based authentication."
1098
+ );
1099
+ }
1100
+ }
1101
+ function hasWatchOnlyTunnelGrants(token) {
1102
+ if (!hasWatchOnlyPermissions(token.permissions)) {
1103
+ return false;
1104
+ }
1105
+ if (!Array.isArray(token.tunnelsGrants) || token.tunnelsGrants.length === 0) {
1106
+ return false;
1107
+ }
1108
+ return token.tunnelsGrants.every((grant) => {
1109
+ const tunnels = grant.scopes?.tunnels;
1110
+ if (tunnels === void 0) {
1111
+ return false;
1112
+ }
1113
+ return tunnels.list !== void 0 && tunnels.create === void 0 && tunnels.connect === void 0;
1114
+ });
1115
+ }
1116
+ function hasWatchOnlyPermissions(permissions) {
1117
+ if (!Array.isArray(permissions)) {
1118
+ return true;
1119
+ }
1120
+ return permissions.every(
1121
+ (permission) => permission === "tunnels.resources.read-only" || permission === "network.resources.read-only"
1122
+ );
1123
+ }
1175
1124
  var Watch = class {
1176
1125
  connection = null;
1177
1126
  connectionState = "preparing";
@@ -1186,45 +1135,56 @@ var Watch = class {
1186
1135
  throw new Error("Watch: Connection already started or closed.");
1187
1136
  }
1188
1137
  this.connectionState = "connecting";
1189
- const token = typeof this.config.auth === "function" ? await this.config.auth() : this.config.auth;
1190
- const engine = await resolveTunnelsEngine({
1191
- apiUrl: this.config.apiUrl,
1192
- controlPlaneCredentials: this.config.controlPlaneCredentials,
1193
- engine: this.config.engine,
1194
- projectEndpoint: this.config.projectEndpoint,
1195
- token
1196
- });
1197
- const transport = this.config.transport ?? "sse";
1198
- const base = `https://${engine}`;
1199
- if (transport === "sse") {
1200
- const url = new URL(`/api/sse`, base);
1201
- url.searchParams.set("rstream.token", token);
1202
- this.connection = new EventSource(url.toString());
1203
- } else {
1204
- const url = new URL(`/api/websocket`, base);
1205
- url.searchParams.set("rstream.token", token);
1206
- url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
1207
- this.connection = new WebSocket(url.toString());
1208
- }
1209
- this.connection.onopen = () => {
1210
- this.connectionState = "connected";
1211
- this.events.onConnect?.();
1212
- };
1213
- this.connection.onmessage = (msg) => {
1214
- const parsed = wsEventsSchema.parse(JSON.parse(msg.data));
1215
- this.events.onEvent?.(parsed);
1216
- };
1217
- this.connection.onerror = () => {
1218
- if (this.connectionState !== "closed") {
1219
- this.disconnect();
1138
+ try {
1139
+ const token = typeof this.config.auth === "function" ? await this.config.auth() : this.config.auth;
1140
+ validateWatchQueryToken(token);
1141
+ const engine = await resolveTunnelsEngine({
1142
+ apiUrl: this.config.apiUrl,
1143
+ controlPlaneCredentials: this.config.controlPlaneCredentials,
1144
+ engine: this.config.engine,
1145
+ projectEndpoint: this.config.projectEndpoint,
1146
+ token
1147
+ });
1148
+ const transport = normalizeTransport(this.config.transport);
1149
+ const base = `https://${engine}`;
1150
+ if (transport === "sse") {
1151
+ const url = new URL(`/api/sse`, base);
1152
+ url.searchParams.set("rstream.token", token);
1153
+ this.connection = new EventSource(url.toString());
1154
+ } else {
1155
+ const url = new URL(`/api/websocket`, base);
1156
+ url.searchParams.set("rstream.token", token);
1157
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
1158
+ this.connection = new WebSocket(url.toString());
1220
1159
  }
1221
- };
1222
- if (this.connection instanceof WebSocket) {
1223
- this.connection.onclose = () => {
1160
+ this.connection.onopen = () => {
1161
+ this.connectionState = "connected";
1162
+ this.events.onConnect?.();
1163
+ };
1164
+ this.connection.onmessage = (msg) => this.handleMessage(msg.data);
1165
+ this.connection.onerror = () => {
1224
1166
  if (this.connectionState !== "closed") {
1225
1167
  this.disconnect();
1226
1168
  }
1227
1169
  };
1170
+ if (this.connection instanceof WebSocket) {
1171
+ this.connection.onclose = () => {
1172
+ if (this.connectionState !== "closed") {
1173
+ this.disconnect();
1174
+ }
1175
+ };
1176
+ }
1177
+ } catch (error) {
1178
+ this.connectionState = "preparing";
1179
+ throw error;
1180
+ }
1181
+ }
1182
+ handleMessage(data) {
1183
+ try {
1184
+ const parsed = wsEventsSchema.parse(JSON.parse(data));
1185
+ this.events.onEvent?.(parsed);
1186
+ } catch {
1187
+ this.disconnect();
1228
1188
  }
1229
1189
  }
1230
1190
  disconnect() {
@@ -1243,7 +1203,7 @@ var Watch = class {
1243
1203
  };
1244
1204
 
1245
1205
  // src/webtty.ts
1246
- import * as z8 from "zod";
1206
+ import * as z4 from "zod";
1247
1207
  var osFamilies = [
1248
1208
  "linux",
1249
1209
  "macos",
@@ -1252,25 +1212,25 @@ var osFamilies = [
1252
1212
  "openbsd",
1253
1213
  "freebsd"
1254
1214
  ];
1255
- var webttyServerSchema = z8.object({
1256
- tunnel_id: z8.string(),
1257
- host: z8.string(),
1258
- token_auth: z8.boolean(),
1259
- os_family: z8.enum(osFamilies).optional(),
1260
- arch: z8.string().optional(),
1261
- os_id: z8.string().optional(),
1215
+ var webttyServerSchema = z4.object({
1216
+ tunnel_id: z4.string(),
1217
+ host: z4.string(),
1218
+ token_auth: z4.boolean(),
1219
+ os_family: z4.enum(osFamilies).optional(),
1220
+ arch: z4.string().optional(),
1221
+ os_id: z4.string().optional(),
1262
1222
  // /etc/os-release::ID (ubuntu, debian, rocky, etc.)
1263
- os_version_id: z8.string().optional(),
1223
+ os_version_id: z4.string().optional(),
1264
1224
  // /etc/os-release::VERSION_ID (24.04, 22.04, 11, 10.0.19045, ...)
1265
- os_version_codename: z8.string().optional(),
1225
+ os_version_codename: z4.string().optional(),
1266
1226
  // /etc/os-release::VERSION_CODENAME (jammy, noble...)
1267
- os_pretty_name: z8.string().optional(),
1227
+ os_pretty_name: z4.string().optional(),
1268
1228
  // /etc/os-release::PRETTY_NAME
1269
- kernel_release: z8.string().optional(),
1229
+ kernel_release: z4.string().optional(),
1270
1230
  // uname -r
1271
- hostname: z8.string().optional(),
1231
+ hostname: z4.string().optional(),
1272
1232
  // uname -n
1273
- labels: z8.record(z8.string(), z8.string().optional()).optional()
1233
+ labels: z4.record(z4.string(), z4.string().optional()).optional()
1274
1234
  });
1275
1235
  function parser(tunnel) {
1276
1236
  if (tunnel.status !== "online") return null;
@@ -1327,6 +1287,7 @@ export {
1327
1287
  authTokenScopesSchema,
1328
1288
  authTokenTunnelGrantSchema,
1329
1289
  authTokenTunnelsScopesSchema,
1290
+ clientFilterSchema,
1330
1291
  clientSchema,
1331
1292
  createAPITURNCredentials,
1332
1293
  createAPPTURNCredentials,
@@ -1335,14 +1296,18 @@ export {
1335
1296
  createAuthTokenResponseSchema,
1336
1297
  createPATTURNCredentials,
1337
1298
  createTURNCredentials,
1299
+ engineOpenApiDocument,
1300
+ engineOpenApiJson,
1338
1301
  eventSchema,
1339
1302
  formatTunnelHost,
1340
1303
  listClientsParamsSchema,
1341
1304
  listClientsResponseSchema,
1342
1305
  listTunnelsParamsSchema,
1343
1306
  listTunnelsResponseSchema,
1307
+ normalizeEngineAddress,
1344
1308
  parseWebTTYServers,
1345
1309
  resolveControlPlaneCredentials,
1310
+ resolveManagedTunnelsProject,
1346
1311
  resolveTunnelsAPIURL,
1347
1312
  resolveTunnelsCredentials,
1348
1313
  resolveTunnelsEngine,
@@ -1350,6 +1315,7 @@ export {
1350
1315
  streamSummarySchema,
1351
1316
  streamTLSInfoSchema,
1352
1317
  tokenSchema,
1318
+ tunnelFilterSchema,
1353
1319
  tunnelSchema,
1354
1320
  turnCredentialModeSchema,
1355
1321
  webhookEventsSchema,