@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.js CHANGED
@@ -48,6 +48,7 @@ __export(index_exports, {
48
48
  authTokenScopesSchema: () => authTokenScopesSchema,
49
49
  authTokenTunnelGrantSchema: () => authTokenTunnelGrantSchema,
50
50
  authTokenTunnelsScopesSchema: () => authTokenTunnelsScopesSchema,
51
+ clientFilterSchema: () => clientFilterSchema,
51
52
  clientSchema: () => clientSchema,
52
53
  createAPITURNCredentials: () => createAPITURNCredentials,
53
54
  createAPPTURNCredentials: () => createAPPTURNCredentials,
@@ -56,14 +57,18 @@ __export(index_exports, {
56
57
  createAuthTokenResponseSchema: () => createAuthTokenResponseSchema,
57
58
  createPATTURNCredentials: () => createPATTURNCredentials,
58
59
  createTURNCredentials: () => createTURNCredentials,
60
+ engineOpenApiDocument: () => engineOpenApiDocument,
61
+ engineOpenApiJson: () => engineOpenApiJson,
59
62
  eventSchema: () => eventSchema,
60
63
  formatTunnelHost: () => formatTunnelHost,
61
64
  listClientsParamsSchema: () => listClientsParamsSchema,
62
65
  listClientsResponseSchema: () => listClientsResponseSchema,
63
66
  listTunnelsParamsSchema: () => listTunnelsParamsSchema,
64
67
  listTunnelsResponseSchema: () => listTunnelsResponseSchema,
68
+ normalizeEngineAddress: () => normalizeEngineAddress,
65
69
  parseWebTTYServers: () => parseWebTTYServers,
66
70
  resolveControlPlaneCredentials: () => resolveControlPlaneCredentials,
71
+ resolveManagedTunnelsProject: () => resolveManagedTunnelsProject,
67
72
  resolveTunnelsAPIURL: () => resolveTunnelsAPIURL,
68
73
  resolveTunnelsCredentials: () => resolveTunnelsCredentials,
69
74
  resolveTunnelsEngine: () => resolveTunnelsEngine,
@@ -71,6 +76,7 @@ __export(index_exports, {
71
76
  streamSummarySchema: () => streamSummarySchema,
72
77
  streamTLSInfoSchema: () => streamTLSInfoSchema,
73
78
  tokenSchema: () => tokenSchema,
79
+ tunnelFilterSchema: () => tunnelFilterSchema,
74
80
  tunnelSchema: () => tunnelSchema,
75
81
  turnCredentialModeSchema: () => turnCredentialModeSchema,
76
82
  webhookEventsSchema: () => webhookEventsSchema,
@@ -86,9 +92,9 @@ var import_rstream = require("@rstreamlabs/rstream");
86
92
  var z = __toESM(require("zod"));
87
93
  var StringFilter = z.union([
88
94
  z.string(),
89
- z.object({ exact: z.string() }),
90
- z.object({ oneof: z.array(z.string()) }),
91
- 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()
92
98
  ]);
93
99
  function transform(field) {
94
100
  if (field instanceof z.ZodOptional) {
@@ -122,7 +128,7 @@ function select(base) {
122
128
  return [key, z.boolean().optional()];
123
129
  });
124
130
  const shape = Object.fromEntries(entries);
125
- return z.object(shape);
131
+ return z.object(shape).strict();
126
132
  }
127
133
 
128
134
  // src/tunnel.ts
@@ -165,16 +171,22 @@ var tunnelSchema = z2.object({
165
171
  rstream_auth: z2.boolean().optional(),
166
172
  challenge_mode: z2.boolean().optional()
167
173
  });
174
+ var tunnelFilterSchema = tunnelSchema.pick({
175
+ id: true,
176
+ name: true,
177
+ type: true,
178
+ status: true,
179
+ client_id: true,
180
+ user_id: true,
181
+ publish: true,
182
+ protocol: true,
183
+ hostname: true,
184
+ http_version: true,
185
+ labels: true
186
+ }).partial();
168
187
  var listTunnelsParamsSchema = z2.object({
169
- limit: z2.number().optional(),
170
- filters: tunnelSchema.pick({
171
- status: true,
172
- client_id: true,
173
- hostname: true,
174
- protocol: true,
175
- publish: true,
176
- labels: true
177
- }).partial().optional()
188
+ limit: z2.number().int().min(1).optional(),
189
+ filters: tunnelFilterSchema.optional()
178
190
  });
179
191
  var listTunnelsResponseSchema = z2.array(tunnelSchema);
180
192
  function formatTunnelHost(tunnel) {
@@ -209,7 +221,7 @@ var authTokenTunnelsScopesSchema = z3.object({
209
221
  creation_date: true
210
222
  })
211
223
  ).optional()
212
- })
224
+ }).strict()
213
225
  ]).optional(),
214
226
  // Scopes for connecting to tunnels
215
227
  connect: z3.union([
@@ -221,7 +233,7 @@ var authTokenTunnelsScopesSchema = z3.object({
221
233
  path: z3.string().optional()
222
234
  })
223
235
  ).optional()
224
- })
236
+ }).strict()
225
237
  ]).optional(),
226
238
  // Scopes for listing tunnels
227
239
  list: z3.union([
@@ -229,17 +241,32 @@ var authTokenTunnelsScopesSchema = z3.object({
229
241
  z3.object({
230
242
  filters: filters(tunnelSchema).optional(),
231
243
  select: select(tunnelSchema).optional()
232
- })
244
+ }).strict()
233
245
  ]).optional()
246
+ }).strict().superRefine((scopes, ctx) => {
247
+ if (scopes.create === void 0 && scopes.connect === void 0 && scopes.list === void 0) {
248
+ ctx.addIssue({
249
+ code: z3.ZodIssueCode.custom,
250
+ message: "At least one tunnel scope action is required."
251
+ });
252
+ }
234
253
  });
235
254
  var authTokenScopesSchema = z3.object({
236
255
  tunnels: authTokenTunnelsScopesSchema.optional()
237
256
  // Scopes related to tunnels
257
+ }).strict().superRefine((scopes, ctx) => {
258
+ if (scopes.tunnels === void 0) {
259
+ ctx.addIssue({
260
+ code: z3.ZodIssueCode.custom,
261
+ message: "Tunnel scopes are required.",
262
+ path: ["tunnels"]
263
+ });
264
+ }
238
265
  });
239
266
  var authTokenTunnelGrantSchema = z3.object({
240
267
  workspaces: z3.array(nonEmptyStringSchema).min(1).optional(),
241
268
  projects: z3.array(nonEmptyStringSchema).min(1).optional(),
242
- scopes: authTokenScopesSchema.optional()
269
+ scopes: authTokenScopesSchema
243
270
  }).strict().superRefine((grant, ctx) => {
244
271
  if (grant.workspaces !== void 0 && grant.projects !== void 0) {
245
272
  ctx.addIssue({
@@ -248,6 +275,13 @@ var authTokenTunnelGrantSchema = z3.object({
248
275
  path: ["projects"]
249
276
  });
250
277
  }
278
+ if (grant.workspaces === void 0 && grant.projects === void 0) {
279
+ ctx.addIssue({
280
+ code: z3.ZodIssueCode.custom,
281
+ message: "A grant must target workspaces or projects.",
282
+ path: ["workspaces"]
283
+ });
284
+ }
251
285
  });
252
286
  var authTokenCommonSchema = {
253
287
  aud: z3.union([z3.string(), z3.array(z3.string())]).optional(),
@@ -283,10 +317,9 @@ var authTokenSchema = z3.union([
283
317
  ]);
284
318
  var tokenSchema = authTokenSchema;
285
319
  var createAuthTokenParamsSchema = z3.object({
286
- expires_in: z3.number().default(60),
287
- // 1 minute
320
+ expires_in: z3.number().int().min(1).max(3600).default(60),
288
321
  scopes: authTokenScopesSchema.optional(),
289
- tunnelsGrants: z3.array(authTokenTunnelGrantSchema).optional(),
322
+ tunnelsGrants: z3.array(authTokenTunnelGrantSchema).min(1).optional(),
290
323
  metadata: z3.unknown().optional()
291
324
  // Additional metadata
292
325
  }).strict().superRefine((params, ctx) => {
@@ -297,6 +330,13 @@ var createAuthTokenParamsSchema = z3.object({
297
330
  path: ["tunnelsGrants"]
298
331
  });
299
332
  }
333
+ if (params.scopes === void 0 && params.tunnelsGrants === void 0) {
334
+ ctx.addIssue({
335
+ code: z3.ZodIssueCode.custom,
336
+ message: "Explicit scopes or tunnelsGrants are required.",
337
+ path: ["scopes"]
338
+ });
339
+ }
300
340
  });
301
341
  var createAuthTokenResponseSchema = z3.object({
302
342
  token: z3.string()
@@ -323,12 +363,12 @@ var RstreamAuthResource = class {
323
363
  }
324
364
  };
325
365
  function createAuthTokenFromClientCredentials(credentials, params, options) {
326
- const tokenParams = createAuthTokenParamsSchema.parse(params ?? {});
366
+ const tokenParams = createAuthTokenParamsSchema.parse(params);
327
367
  const payload = {
328
368
  metadata: {
329
369
  engine: options?.engine
330
370
  },
331
- tunnelsGrants: normalizeAuthTokenTunnelGrants(tokenParams),
371
+ tunnelsGrants: normalizeAuthTokenTunnelGrants(tokenParams, options),
332
372
  permissions: null
333
373
  };
334
374
  const { token } = (0, import_rstream.createClientCredentialsToken)(credentials, {
@@ -337,19 +377,36 @@ function createAuthTokenFromClientCredentials(credentials, params, options) {
337
377
  });
338
378
  return { token };
339
379
  }
340
- function normalizeAuthTokenTunnelGrants(params) {
341
- if (!params) {
342
- return void 0;
343
- }
380
+ function normalizeAuthTokenTunnelGrants(params, options) {
344
381
  const tunnelsGrants = params.tunnelsGrants;
345
382
  if (tunnelsGrants !== void 0) {
346
383
  return [...tunnelsGrants];
347
384
  }
348
385
  if (params.scopes) {
349
- return [{ scopes: params.scopes }];
386
+ return [{ ...normalizeTunnelGrantTarget(options), scopes: params.scopes }];
350
387
  }
351
388
  return void 0;
352
389
  }
390
+ function normalizeTunnelGrantTarget(options) {
391
+ const projectId = normalizeOptionalString(options?.projectId);
392
+ const workspaceId = normalizeOptionalString(options?.workspaceId);
393
+ if (projectId !== void 0 && workspaceId !== void 0) {
394
+ throw new Error("Use either projectId or workspaceId, not both.");
395
+ }
396
+ if (projectId !== void 0) {
397
+ return { projects: [projectId] };
398
+ }
399
+ if (workspaceId !== void 0) {
400
+ return { workspaces: [workspaceId] };
401
+ }
402
+ throw new Error(
403
+ "Project ID or workspace ID is required when using scope-only tunnel grants."
404
+ );
405
+ }
406
+ function normalizeOptionalString(value) {
407
+ const normalized = value?.trim();
408
+ return normalized ? normalized : void 0;
409
+ }
353
410
 
354
411
  // src/client.ts
355
412
  var z4 = __toESM(require("zod"));
@@ -371,15 +428,32 @@ var clientSchema = z4.object({
371
428
  protocol_version: z4.string().optional(),
372
429
  labels: z4.record(z4.string(), z4.string()).optional()
373
430
  });
431
+ var clientFilterSchema = clientSchema.pick({
432
+ id: true,
433
+ status: true,
434
+ user_id: true,
435
+ agent: true,
436
+ channel: true,
437
+ version: true,
438
+ os: true,
439
+ arch: true,
440
+ protocol_version: true,
441
+ labels: true
442
+ }).partial();
374
443
  var listClientsParamsSchema = z4.object({
375
- limit: z4.number().optional(),
376
- filters: clientSchema.pick({
377
- labels: true
378
- }).partial().optional()
444
+ limit: z4.number().int().min(1).optional(),
445
+ filters: clientFilterSchema.optional()
379
446
  });
380
447
  var listClientsResponseSchema = z4.array(clientSchema);
381
448
 
382
449
  // src/clients-resource.ts
450
+ function clientPathId(id) {
451
+ const normalized = id.trim();
452
+ if (!normalized) {
453
+ throw new Error("Client ID is required.");
454
+ }
455
+ return encodeURIComponent(normalized);
456
+ }
383
457
  var RstreamClientsResource = class {
384
458
  client;
385
459
  constructor(client) {
@@ -393,9 +467,12 @@ var RstreamClientsResource = class {
393
467
  return listClientsResponseSchema.parse(response);
394
468
  }
395
469
  async get(id) {
396
- const response = await this.client.request(`/clients/${id}`, {
397
- method: "GET"
398
- });
470
+ const response = await this.client.request(
471
+ `/clients/${clientPathId(id)}`,
472
+ {
473
+ method: "GET"
474
+ }
475
+ );
399
476
  return clientSchema.parse(response);
400
477
  }
401
478
  };
@@ -664,28 +741,389 @@ var webhookEventsSchema = z6.union([
664
741
  ]);
665
742
  var eventSchema = z6.union([wsEventsSchema, webhookEventsSchema]);
666
743
 
744
+ // src/openapi.ts
745
+ var z7 = __toESM(require("zod"));
746
+ var jsonContentType = "application/json";
747
+ var openApiContentType = "application/vnd.oai.openapi+json;version=3.1";
748
+ function isRecord(value) {
749
+ return typeof value === "object" && value !== null && !Array.isArray(value);
750
+ }
751
+ function dropSchemaKeyword(value) {
752
+ if (!isRecord(value)) return value;
753
+ return Object.fromEntries(
754
+ Object.entries(value).filter(([key]) => key !== "$schema")
755
+ );
756
+ }
757
+ function jsonSchemaFor(schema) {
758
+ return dropSchemaKeyword(
759
+ z7.toJSONSchema(schema, {
760
+ unrepresentable: "any",
761
+ override: (ctx) => {
762
+ if (ctx.zodSchema instanceof z7.ZodDate) {
763
+ ctx.jsonSchema.type = "string";
764
+ ctx.jsonSchema.format = "date-time";
765
+ }
766
+ }
767
+ })
768
+ );
769
+ }
770
+ function jsonArrayResponse(description, itemSchemaRef) {
771
+ return {
772
+ description,
773
+ content: {
774
+ [jsonContentType]: {
775
+ schema: {
776
+ type: "array",
777
+ items: {
778
+ $ref: itemSchemaRef
779
+ }
780
+ }
781
+ }
782
+ }
783
+ };
784
+ }
785
+ function streamResponse(description) {
786
+ return {
787
+ description,
788
+ content: {
789
+ "text/event-stream": {
790
+ schema: {
791
+ type: "string"
792
+ }
793
+ }
794
+ }
795
+ };
796
+ }
797
+ function errorResponse(description) {
798
+ return {
799
+ description
800
+ };
801
+ }
802
+ function queryJsonParameter(name, description, schemaRef, examples) {
803
+ return {
804
+ name,
805
+ in: "query",
806
+ required: false,
807
+ description,
808
+ content: {
809
+ [jsonContentType]: {
810
+ schema: {
811
+ $ref: schemaRef
812
+ },
813
+ ...examples ? { examples } : {}
814
+ }
815
+ }
816
+ };
817
+ }
818
+ var readEngineStateRule = {
819
+ resource: ["engine.tunnels", "engine.streams"],
820
+ action: "read"
821
+ };
822
+ var readEngineStateRules = [readEngineStateRule];
823
+ var readEngineStreamsRules = [
824
+ {
825
+ resource: "engine.streams",
826
+ action: "read"
827
+ }
828
+ ];
829
+ var watchParamsSchema = z7.object({
830
+ clients: clientFilterSchema.optional(),
831
+ tunnels: tunnelFilterSchema.optional()
832
+ });
833
+ var enginePermissions = {
834
+ "tunnels.resources.read-only": {
835
+ name: "Discovery: Read-Only",
836
+ description: "Read active clients, active tunnels, and live state events.",
837
+ rules: [readEngineStateRule]
838
+ },
839
+ "tunnels.tunnels.create-delete": {
840
+ name: "Tunnels: Create and Delete",
841
+ description: "Create and close tunnels through the CLI, SDK, or engine control channel.",
842
+ rules: [
843
+ {
844
+ resource: "engine.tunnels",
845
+ action: ["create", "delete"]
846
+ }
847
+ ]
848
+ },
849
+ "tunnels.streams.create-delete": {
850
+ name: "Connections: Create and Delete",
851
+ description: "Open and close private tunnel streams and WebTTY connections.",
852
+ rules: [
853
+ {
854
+ resource: "engine.streams",
855
+ action: ["create", "delete"]
856
+ }
857
+ ]
858
+ }
859
+ };
860
+ var engineOpenApiDocument = {
861
+ openapi: "3.1.0",
862
+ info: {
863
+ title: "rstream Engine API",
864
+ version: "1.0.0",
865
+ 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."
866
+ },
867
+ servers: [
868
+ {
869
+ url: "https://{engine}",
870
+ description: "Project-scoped rstream engine endpoint.",
871
+ variables: {
872
+ engine: {
873
+ default: "project-endpoint.cluster-host:443",
874
+ description: "Use the engine value returned by project discovery or configured in RSTREAM_ENGINE."
875
+ }
876
+ }
877
+ }
878
+ ],
879
+ security: [
880
+ {
881
+ bearerAuth: []
882
+ }
883
+ ],
884
+ components: {
885
+ securitySchemes: {
886
+ bearerAuth: {
887
+ type: "http",
888
+ scheme: "bearer",
889
+ bearerFormat: "rstream token",
890
+ description: "Preferred authentication mechanism. Send an OAuth-issued rstream access token or compatible rstream credential in Authorization: Bearer <token>."
891
+ }
892
+ },
893
+ parameters: {
894
+ ListClientsParams: queryJsonParameter(
895
+ "params",
896
+ "JSON-encoded ListClientsParams object.",
897
+ "#/components/schemas/ListClientsParams",
898
+ {
899
+ filterByAgent: {
900
+ value: {
901
+ limit: 10,
902
+ filters: {
903
+ agent: "rstream",
904
+ channel: "stable"
905
+ }
906
+ }
907
+ }
908
+ }
909
+ ),
910
+ ListTunnelsParams: queryJsonParameter(
911
+ "params",
912
+ "JSON-encoded ListTunnelsParams object.",
913
+ "#/components/schemas/ListTunnelsParams",
914
+ {
915
+ filterByLabels: {
916
+ value: {
917
+ filters: {
918
+ labels: {
919
+ service: "ssh",
920
+ env: "prod"
921
+ }
922
+ }
923
+ }
924
+ }
925
+ }
926
+ ),
927
+ WatchParams: queryJsonParameter(
928
+ "params",
929
+ "JSON-encoded WatchParams object. The same shape is accepted by SSE and WebSocket transports.",
930
+ "#/components/schemas/WatchParams",
931
+ {
932
+ watchTaggedTunnels: {
933
+ value: {
934
+ clients: {
935
+ agent: "rstream"
936
+ },
937
+ tunnels: {
938
+ labels: {
939
+ service: "ssh"
940
+ }
941
+ }
942
+ }
943
+ }
944
+ }
945
+ )
946
+ },
947
+ responses: {
948
+ BadRequest: errorResponse("Request validation failed."),
949
+ Unauthorized: errorResponse("Authentication is required."),
950
+ Forbidden: errorResponse("The token is not authorized.")
951
+ },
952
+ schemas: {
953
+ Client: jsonSchemaFor(clientSchema),
954
+ Event: jsonSchemaFor(wsEventsSchema),
955
+ ListClientsParams: jsonSchemaFor(listClientsParamsSchema),
956
+ ListTunnelsParams: jsonSchemaFor(listTunnelsParamsSchema),
957
+ TunnelListItem: jsonSchemaFor(tunnelSchema),
958
+ WatchParams: jsonSchemaFor(watchParamsSchema)
959
+ }
960
+ },
961
+ "x-rstream-agent-authentication": {
962
+ oauth_authorization_server_metadata: "https://rstream.io/.well-known/oauth-authorization-server",
963
+ oauth_protected_resource_metadata: "https://rstream.io/.well-known/oauth-protected-resource",
964
+ scope_selection: "Use x-rstream-required-rules to understand backend authorization checks. Choose OAuth scopes from x-rstream-permissions or use the task bundles in x-rstream-use-case-permissions."
965
+ },
966
+ "x-rstream-permissions": enginePermissions,
967
+ "x-rstream-use-case-permissions": {
968
+ "inspect-engine-state": ["tunnels.resources.read-only"],
969
+ "expose-service-from-cli": [
970
+ "tunnels.resources.read-only",
971
+ "tunnels.tunnels.create-delete"
972
+ ],
973
+ "connect-private-tunnel": [
974
+ "tunnels.resources.read-only",
975
+ "tunnels.streams.create-delete"
976
+ ],
977
+ "operate-webtty": [
978
+ "tunnels.resources.read-only",
979
+ "tunnels.tunnels.create-delete",
980
+ "tunnels.streams.create-delete"
981
+ ]
982
+ },
983
+ paths: {
984
+ "/api/openapi.json": {
985
+ get: {
986
+ tags: ["Discovery"],
987
+ summary: "Return this OpenAPI description.",
988
+ operationId: "get_engine_openapi",
989
+ security: [],
990
+ responses: {
991
+ "200": {
992
+ description: "OpenAPI document.",
993
+ content: {
994
+ [openApiContentType]: {
995
+ schema: {
996
+ type: "object"
997
+ }
998
+ }
999
+ }
1000
+ }
1001
+ }
1002
+ }
1003
+ },
1004
+ "/api/clients": {
1005
+ get: {
1006
+ tags: ["Runtime inventory"],
1007
+ summary: "List visible engine clients.",
1008
+ operationId: "list_engine_clients",
1009
+ parameters: [{ $ref: "#/components/parameters/ListClientsParams" }],
1010
+ security: [{ bearerAuth: [] }],
1011
+ responses: {
1012
+ "200": jsonArrayResponse(
1013
+ "Visible clients.",
1014
+ "#/components/schemas/Client"
1015
+ ),
1016
+ "400": { $ref: "#/components/responses/BadRequest" },
1017
+ "401": { $ref: "#/components/responses/Unauthorized" },
1018
+ "403": { $ref: "#/components/responses/Forbidden" }
1019
+ },
1020
+ "x-rstream-required-rules": readEngineStreamsRules
1021
+ }
1022
+ },
1023
+ "/api/tunnels": {
1024
+ get: {
1025
+ tags: ["Runtime inventory"],
1026
+ summary: "List visible engine tunnels.",
1027
+ operationId: "list_engine_tunnels",
1028
+ parameters: [{ $ref: "#/components/parameters/ListTunnelsParams" }],
1029
+ security: [{ bearerAuth: [] }],
1030
+ responses: {
1031
+ "200": jsonArrayResponse(
1032
+ "Visible tunnels. Fine-grained list scopes may filter objects and project fields.",
1033
+ "#/components/schemas/TunnelListItem"
1034
+ ),
1035
+ "400": { $ref: "#/components/responses/BadRequest" },
1036
+ "401": { $ref: "#/components/responses/Unauthorized" },
1037
+ "403": { $ref: "#/components/responses/Forbidden" }
1038
+ },
1039
+ "x-rstream-required-rules": readEngineStateRules,
1040
+ "x-rstream-token-metadata-scope": "metadata.scopes.tunnels.list"
1041
+ }
1042
+ },
1043
+ "/api/sse": {
1044
+ get: {
1045
+ tags: ["Runtime watch"],
1046
+ summary: "Subscribe to visible engine state through Server-Sent Events.",
1047
+ operationId: "watch_engine_events_sse",
1048
+ parameters: [{ $ref: "#/components/parameters/WatchParams" }],
1049
+ security: [{ bearerAuth: [] }],
1050
+ responses: {
1051
+ "200": streamResponse(
1052
+ "SSE stream. Each event is emitted as a data line containing a JSON Event object."
1053
+ ),
1054
+ "400": { $ref: "#/components/responses/BadRequest" },
1055
+ "401": { $ref: "#/components/responses/Unauthorized" },
1056
+ "403": { $ref: "#/components/responses/Forbidden" }
1057
+ },
1058
+ "x-rstream-event-schema": "#/components/schemas/Event",
1059
+ "x-rstream-required-rules": readEngineStateRules
1060
+ }
1061
+ },
1062
+ "/api/websocket": {
1063
+ get: {
1064
+ tags: ["Runtime watch"],
1065
+ summary: "Subscribe to visible engine state through a WebSocket upgrade.",
1066
+ operationId: "watch_engine_events_websocket",
1067
+ parameters: [{ $ref: "#/components/parameters/WatchParams" }],
1068
+ security: [{ bearerAuth: [] }],
1069
+ responses: {
1070
+ "101": {
1071
+ description: "WebSocket connection accepted. Frames contain JSON Event objects."
1072
+ },
1073
+ "400": { $ref: "#/components/responses/BadRequest" },
1074
+ "401": { $ref: "#/components/responses/Unauthorized" },
1075
+ "403": { $ref: "#/components/responses/Forbidden" }
1076
+ },
1077
+ "x-rstream-event-schema": "#/components/schemas/Event",
1078
+ "x-rstream-required-rules": readEngineStateRules
1079
+ }
1080
+ }
1081
+ }
1082
+ };
1083
+ var engineOpenApiJson = `${JSON.stringify(
1084
+ engineOpenApiDocument,
1085
+ null,
1086
+ 2
1087
+ )}
1088
+ `;
1089
+
667
1090
  // src/resolution.ts
668
1091
  var import_rstream3 = require("@rstreamlabs/rstream");
669
1092
  var import_rstream4 = require("@rstreamlabs/rstream");
670
1093
  var import_rstream5 = require("@rstreamlabs/rstream");
671
1094
  var import_rstream6 = require("@rstreamlabs/rstream");
672
1095
  var import_rstream7 = require("@rstreamlabs/rstream");
673
- var import_jsonwebtoken = __toESM(require("jsonwebtoken"));
674
1096
  function trimOptionalString(value) {
675
1097
  const normalized = value?.trim();
676
1098
  return normalized ? normalized : void 0;
677
1099
  }
678
- function getEngineFromToken(token) {
679
- if (!token) {
680
- return void 0;
1100
+ var dnsLabelPattern = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i;
1101
+ function validateHostname(hostname) {
1102
+ if (!hostname || hostname.length > 253 || hostname.includes("..")) {
1103
+ return false;
681
1104
  }
682
- const parsed = authTokenSchema.safeParse(
683
- import_jsonwebtoken.default.decode(token, { complete: false })
684
- );
685
- if (!parsed.success) {
686
- return void 0;
1105
+ return hostname.split(".").every((label) => label === "localhost" || dnsLabelPattern.test(label));
1106
+ }
1107
+ function normalizeEngineAddress(engine) {
1108
+ const normalized = engine.trim();
1109
+ if (!normalized || /^[a-z][a-z0-9+.-]*:\/\//i.test(normalized) || /[/?#@\\\s]/.test(normalized)) {
1110
+ throw new Error("Engine must be a host[:port] value.");
1111
+ }
1112
+ const explicitPort = normalized.match(/:(\d+)$/)?.[1];
1113
+ const url = new URL(`https://${normalized}`);
1114
+ if (url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
1115
+ throw new Error("Engine must be a host[:port] value.");
1116
+ }
1117
+ if (!validateHostname(url.hostname)) {
1118
+ throw new Error("Engine must use a valid hostname.");
1119
+ }
1120
+ if (explicitPort) {
1121
+ const port = Number(explicitPort);
1122
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
1123
+ throw new Error("Engine port must be an integer between 1 and 65535.");
1124
+ }
687
1125
  }
688
- return trimOptionalString(parsed.data.metadata?.engine);
1126
+ return `${url.hostname.toLowerCase()}${explicitPort ? `:${explicitPort}` : ""}`;
689
1127
  }
690
1128
  function resolveTunnelsAPIURL(apiUrl) {
691
1129
  return trimOptionalString(apiUrl) ?? (0, import_rstream6.readEnvironment)().apiUrl ?? import_rstream3.DEFAULT_API_URL;
@@ -719,37 +1157,22 @@ function resolveControlPlaneCredentials(controlPlaneCredentials, credentials, au
719
1157
  async function resolveTunnelsEngine(config) {
720
1158
  const explicitEngine = trimOptionalString(config.engine);
721
1159
  if (explicitEngine) {
722
- return explicitEngine;
1160
+ return normalizeEngineAddress(explicitEngine);
723
1161
  }
724
1162
  const environmentEngine = (0, import_rstream6.readEnvironment)().engine;
725
1163
  if (environmentEngine) {
726
- return environmentEngine;
1164
+ return normalizeEngineAddress(environmentEngine);
727
1165
  }
728
1166
  const token = trimOptionalString(config.token) ?? ((0, import_rstream5.isTokenCredentials)(config.credentials) ? config.credentials.token : void 0);
729
- const tokenEngine = getEngineFromToken(token);
730
- if (tokenEngine) {
731
- return tokenEngine;
732
- }
733
1167
  const projectEndpoint = trimOptionalString(config.projectEndpoint);
734
1168
  if (projectEndpoint) {
735
- const credentials = resolveControlPlaneCredentials(
736
- config.controlPlaneCredentials,
737
- config.credentials,
1169
+ const project = await resolveManagedTunnelsProject({
1170
+ ...config,
738
1171
  token
739
- );
740
- if (credentials === void 0) {
741
- throw new Error(
742
- "Control-plane credentials are required to resolve a managed project endpoint."
743
- );
744
- }
745
- const controlPlane = new import_rstream7.RstreamClient({
746
- apiUrl: resolveTunnelsAPIURL(config.apiUrl),
747
- credentials
748
1172
  });
749
- const project = await controlPlane.tunnels.projects.resolveByEndpoint(projectEndpoint);
750
1173
  const engine = (0, import_rstream4.getTunnelsProjectEngine)(project);
751
1174
  if (engine) {
752
- return engine;
1175
+ return normalizeEngineAddress(engine);
753
1176
  }
754
1177
  throw new Error(
755
1178
  "Failed to resolve the engine address from the managed tunnels project."
@@ -759,8 +1182,36 @@ async function resolveTunnelsEngine(config) {
759
1182
  "Engine URL is not defined. Provide an engine, use RSTREAM_ENGINE, or configure a managed project endpoint with control-plane access."
760
1183
  );
761
1184
  }
1185
+ async function resolveManagedTunnelsProject(config) {
1186
+ const projectEndpoint = trimOptionalString(config.projectEndpoint);
1187
+ if (!projectEndpoint) {
1188
+ throw new Error("Managed project endpoint is required.");
1189
+ }
1190
+ const credentials = resolveControlPlaneCredentials(
1191
+ config.controlPlaneCredentials,
1192
+ config.credentials,
1193
+ config.token
1194
+ );
1195
+ if (credentials === void 0) {
1196
+ throw new Error(
1197
+ "Control-plane credentials are required to resolve a managed project endpoint."
1198
+ );
1199
+ }
1200
+ const controlPlane = new import_rstream7.RstreamClient({
1201
+ apiUrl: resolveTunnelsAPIURL(config.apiUrl),
1202
+ credentials
1203
+ });
1204
+ return await controlPlane.tunnels.projects.resolveByEndpoint(projectEndpoint);
1205
+ }
762
1206
 
763
1207
  // src/tunnels-resource.ts
1208
+ function tunnelPathId(id) {
1209
+ const normalized = id.trim();
1210
+ if (!normalized) {
1211
+ throw new Error("Tunnel ID is required.");
1212
+ }
1213
+ return encodeURIComponent(normalized);
1214
+ }
764
1215
  var RstreamTunnelsResource = class {
765
1216
  client;
766
1217
  constructor(client) {
@@ -774,65 +1225,76 @@ var RstreamTunnelsResource = class {
774
1225
  return listTunnelsResponseSchema.parse(response);
775
1226
  }
776
1227
  async get(id) {
777
- const response = await this.client.request(`/tunnels/${id}`, {
778
- method: "GET"
779
- });
1228
+ const response = await this.client.request(
1229
+ `/tunnels/${tunnelPathId(id)}`,
1230
+ {
1231
+ method: "GET"
1232
+ }
1233
+ );
780
1234
  return tunnelSchema.parse(response);
781
1235
  }
782
1236
  };
783
1237
 
784
1238
  // src/tunnels.ts
785
1239
  var import_rstream12 = require("@rstreamlabs/rstream");
1240
+ var import_rstream13 = require("@rstreamlabs/rstream");
1241
+ var import_rstream14 = require("@rstreamlabs/rstream");
786
1242
 
787
1243
  // src/turn.ts
788
1244
  var import_rstream8 = require("@rstreamlabs/rstream");
789
1245
  var import_rstream9 = require("@rstreamlabs/rstream");
790
1246
  var import_rstream10 = require("@rstreamlabs/rstream");
791
1247
  var import_rstream11 = require("@rstreamlabs/rstream");
792
- var z7 = __toESM(require("zod"));
1248
+ var z8 = __toESM(require("zod"));
793
1249
  var import_crypto = __toESM(require("crypto"));
794
1250
  var defaultTURNPort = 3478;
795
1251
  var defaultTURNSPort = 5349;
796
- var defaultTURNCredentialTTLSeconds = 24 * 60 * 60;
797
- var turnTokenClaimsSchema = z7.object({
798
- token_endpoint: z7.string().optional(),
799
- type: z7.string()
1252
+ var defaultTURNCredentialTTLSeconds = 10 * 60;
1253
+ var maxTURNCredentialTTLSeconds = 60 * 60;
1254
+ var turnTokenClaimsSchema = z8.object({
1255
+ exp: z8.number().int().optional(),
1256
+ iat: z8.number().int().optional(),
1257
+ token_endpoint: z8.string().regex(/^[0-9a-f]{8}$/i).optional(),
1258
+ type: z8.enum(["app", "auth", "pat"])
800
1259
  });
801
- var turnCredentialModeSchema = z7.enum(["api", "app", "pat"]);
802
- function normalizeOptionalString(value) {
1260
+ var turnCredentialModeSchema = z8.enum(["api", "app", "pat"]);
1261
+ function normalizeOptionalString2(value) {
803
1262
  const normalized = value?.trim();
804
1263
  return normalized ? normalized : void 0;
805
1264
  }
806
1265
  function normalizeClusterDomain(clusterDomain) {
807
- const normalized = normalizeOptionalString(clusterDomain);
1266
+ const normalized = normalizeOptionalString2(clusterDomain);
808
1267
  return normalized?.toLowerCase();
809
1268
  }
810
1269
  function normalizeNow(now) {
811
- if (now instanceof Date) {
812
- return Math.floor(now.getTime() / 1e3);
1270
+ if (now === void 0) {
1271
+ return Math.floor(Date.now() / 1e3);
813
1272
  }
814
- if (typeof now === "number" && Number.isFinite(now)) {
815
- return Math.floor(now);
1273
+ const timestamp = now instanceof Date ? now.getTime() / 1e3 : now;
1274
+ if (!Number.isFinite(timestamp)) {
1275
+ throw new Error("TURN timestamp must be finite.");
816
1276
  }
817
- return Math.floor(Date.now() / 1e3);
1277
+ return Math.floor(timestamp);
818
1278
  }
819
1279
  function normalizeTTLSeconds(ttlSeconds) {
820
- if (typeof ttlSeconds === "number" && Number.isFinite(ttlSeconds) && ttlSeconds > 0) {
821
- return Math.floor(ttlSeconds);
1280
+ const ttl = ttlSeconds ?? defaultTURNCredentialTTLSeconds;
1281
+ if (!Number.isInteger(ttl) || ttl < 1 || ttl > maxTURNCredentialTTLSeconds) {
1282
+ throw new Error("TURN TTL must be an integer between 1 and 3600 seconds.");
822
1283
  }
823
- return defaultTURNCredentialTTLSeconds;
1284
+ return ttl;
824
1285
  }
825
- function normalizeTURNPort(port) {
826
- if (typeof port === "number" && Number.isFinite(port) && port > 0) {
827
- return Math.floor(port);
1286
+ function normalizePort(port, fallback, name) {
1287
+ const normalized = port ?? fallback;
1288
+ if (!Number.isInteger(normalized) || normalized < 1 || normalized > 65535) {
1289
+ throw new Error(`${name} must be an integer between 1 and 65535.`);
828
1290
  }
829
- return defaultTURNPort;
1291
+ return normalized;
1292
+ }
1293
+ function normalizeTURNPort(port) {
1294
+ return normalizePort(port, defaultTURNPort, "TURN port");
830
1295
  }
831
1296
  function normalizeTURNSPort(port) {
832
- if (typeof port === "number" && Number.isFinite(port) && port > 0) {
833
- return Math.floor(port);
834
- }
835
- return defaultTURNSPort;
1297
+ return normalizePort(port, defaultTURNSPort, "TURNS port");
836
1298
  }
837
1299
  function createTURNURLs(clusterDomain, turnPort, turnsPort) {
838
1300
  const normalizedTURNPort = normalizeTURNPort(turnPort);
@@ -850,16 +1312,32 @@ function parseTURNTokenClaims(token) {
850
1312
  if (!payloadPart) {
851
1313
  throw new Error("Invalid token format.");
852
1314
  }
853
- const payload = Buffer.from(payloadPart, "base64url").toString("utf8");
854
- const parsed = turnTokenClaimsSchema.safeParse(JSON.parse(payload));
1315
+ let decoded;
1316
+ try {
1317
+ const payload = Buffer.from(payloadPart, "base64url").toString("utf8");
1318
+ decoded = JSON.parse(payload);
1319
+ } catch {
1320
+ throw new Error("Invalid token format.");
1321
+ }
1322
+ const parsed = turnTokenClaimsSchema.safeParse(decoded);
855
1323
  if (!parsed.success) {
856
1324
  throw new Error("Invalid token format.");
857
1325
  }
858
1326
  return parsed.data;
859
1327
  }
1328
+ function requirePATExpiration(claims, now) {
1329
+ const exp = claims.exp;
1330
+ if (typeof exp !== "number") {
1331
+ throw new Error("TURN PAT mode requires a PAT token with expiration.");
1332
+ }
1333
+ if (exp <= now) {
1334
+ throw new Error("TURN PAT mode requires a non-expired PAT token.");
1335
+ }
1336
+ return exp;
1337
+ }
860
1338
  function deriveClusterDomainFromEngine(projectEndpoint, engine) {
861
- const normalizedProjectEndpoint = normalizeOptionalString(projectEndpoint);
862
- const normalizedEngine = normalizeOptionalString(engine);
1339
+ const normalizedProjectEndpoint = normalizeOptionalString2(projectEndpoint);
1340
+ const normalizedEngine = normalizeOptionalString2(engine);
863
1341
  if (!normalizedProjectEndpoint || !normalizedEngine) {
864
1342
  throw new Error(
865
1343
  "Failed to derive the cluster domain from the engine address."
@@ -893,17 +1371,11 @@ function resolveMode(credentials, mode) {
893
1371
  if ((0, import_rstream8.isClientCredentials)(credentials)) {
894
1372
  return "app";
895
1373
  }
896
- if ((0, import_rstream9.isTokenCredentials)(credentials)) {
897
- const claims = parseTURNTokenClaims(credentials.token);
898
- if (claims.type === "pat" && normalizeOptionalString(claims.token_endpoint)) {
899
- return "pat";
900
- }
901
- }
902
1374
  return "api";
903
1375
  }
904
1376
  async function loadTURNServerPublicKeyHex(clusterDomain, options) {
905
- const configuredKey = normalizeOptionalString(options.keyringBaseUrl);
906
- const keyringBaseURL = configuredKey ?? normalizeOptionalString(options.apiUrl) ?? "https://rstream.io";
1377
+ const configuredKey = normalizeOptionalString2(options.keyringBaseUrl);
1378
+ const keyringBaseURL = configuredKey ?? normalizeOptionalString2(options.apiUrl) ?? "https://rstream.io";
907
1379
  const url = new URL(
908
1380
  `/keyrings/turn/${clusterDomain}.spki.der.hex`,
909
1381
  keyringBaseURL
@@ -915,8 +1387,8 @@ async function loadTURNServerPublicKeyHex(clusterDomain, options) {
915
1387
  return (await response.text()).trim();
916
1388
  }
917
1389
  async function createAPITURNCredentials(options) {
918
- const projectId = normalizeOptionalString(options.projectId);
919
- const projectEndpoint = normalizeOptionalString(options.projectEndpoint);
1390
+ const projectId = normalizeOptionalString2(options.projectId);
1391
+ const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
920
1392
  if (!projectId && !projectEndpoint) {
921
1393
  throw new Error(
922
1394
  "Project ID or project endpoint is required for TURN API mode."
@@ -943,11 +1415,11 @@ function createPATTURNCredentials(options) {
943
1415
  if (!clusterDomain) {
944
1416
  throw new Error("Cluster domain is required for TURN PAT mode.");
945
1417
  }
946
- const projectEndpoint = normalizeOptionalString(options.projectEndpoint);
1418
+ const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
947
1419
  if (!projectEndpoint) {
948
1420
  throw new Error("Project endpoint is required for TURN PAT mode.");
949
1421
  }
950
- const token = normalizeOptionalString(options.token);
1422
+ const token = normalizeOptionalString2(options.token);
951
1423
  if (!token) {
952
1424
  throw new Error("Token is required for TURN PAT mode.");
953
1425
  }
@@ -955,14 +1427,17 @@ function createPATTURNCredentials(options) {
955
1427
  if (claims.type !== "pat") {
956
1428
  throw new Error("TURN PAT mode requires a PAT token.");
957
1429
  }
958
- const tokenEndpoint = normalizeOptionalString(claims.token_endpoint);
1430
+ const tokenEndpoint = normalizeOptionalString2(options.tokenEndpoint);
959
1431
  if (!tokenEndpoint) {
960
- throw new Error(
961
- "TURN PAT mode requires a PAT token carrying a token endpoint."
962
- );
1432
+ throw new Error("Token endpoint is required for TURN PAT mode.");
1433
+ }
1434
+ if (claims.token_endpoint !== void 0 && claims.token_endpoint !== tokenEndpoint) {
1435
+ throw new Error("Token endpoint does not match the PAT token.");
963
1436
  }
1437
+ const now = normalizeNow(options.now);
964
1438
  const ttlSeconds = normalizeTTLSeconds(options.ttlSeconds);
965
- const exp = normalizeNow(options.now) + ttlSeconds;
1439
+ const exp = Math.min(now + ttlSeconds, requirePATExpiration(claims, now));
1440
+ const ttl = exp - now;
966
1441
  const username = `v1:${exp}:pat:${projectEndpoint}:${tokenEndpoint}`;
967
1442
  const tokenHash = import_crypto.default.createHash("sha256").update(token, "utf8").digest();
968
1443
  const key = Buffer.from(
@@ -977,7 +1452,7 @@ function createPATTURNCredentials(options) {
977
1452
  const credential = import_crypto.default.createHmac("sha256", key).update(username, "utf8").digest("base64");
978
1453
  return import_rstream11.turnCredentialsSchema.parse({
979
1454
  credential,
980
- ttl: ttlSeconds,
1455
+ ttl,
981
1456
  urls: createTURNURLs(clusterDomain, options.turnPort, options.turnsPort),
982
1457
  username
983
1458
  });
@@ -987,15 +1462,15 @@ async function createAPPTURNCredentials(options) {
987
1462
  if (!clusterDomain) {
988
1463
  throw new Error("Cluster domain is required for TURN APP mode.");
989
1464
  }
990
- const projectEndpoint = normalizeOptionalString(options.projectEndpoint);
1465
+ const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
991
1466
  if (!projectEndpoint) {
992
1467
  throw new Error("Project endpoint is required for TURN APP mode.");
993
1468
  }
994
- const clientId = normalizeOptionalString(options.clientId);
1469
+ const clientId = normalizeOptionalString2(options.clientId);
995
1470
  if (!clientId) {
996
1471
  throw new Error("Client ID is required for TURN APP mode.");
997
1472
  }
998
- const clientSecret = normalizeOptionalString(options.clientSecret);
1473
+ const clientSecret = normalizeOptionalString2(options.clientSecret);
999
1474
  if (!clientSecret) {
1000
1475
  throw new Error("Client secret is required for TURN APP mode.");
1001
1476
  }
@@ -1007,7 +1482,7 @@ async function createAPPTURNCredentials(options) {
1007
1482
  format: "der",
1008
1483
  type: "pkcs8"
1009
1484
  });
1010
- const serverPublicKeyHex = normalizeOptionalString(options.serverPublicKeyHex) ?? await loadTURNServerPublicKeyHex(clusterDomain, options);
1485
+ const serverPublicKeyHex = normalizeOptionalString2(options.serverPublicKeyHex) ?? await loadTURNServerPublicKeyHex(clusterDomain, options);
1011
1486
  const serverPublicKey = import_crypto.default.createPublicKey({
1012
1487
  key: Buffer.from(serverPublicKeyHex, "hex"),
1013
1488
  format: "der",
@@ -1037,7 +1512,7 @@ async function createAPPTURNCredentials(options) {
1037
1512
  async function createTURNCredentials(options) {
1038
1513
  const credentials = options.credentials;
1039
1514
  const mode = resolveMode(credentials, options.mode);
1040
- const projectEndpoint = normalizeOptionalString(options.projectEndpoint);
1515
+ const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
1041
1516
  if (mode === "api") {
1042
1517
  const controlPlaneCredentials = options.controlPlaneCredentials ?? credentials;
1043
1518
  if (!controlPlaneCredentials) {
@@ -1068,6 +1543,10 @@ async function createTURNCredentials(options) {
1068
1543
  "Project endpoint is required for TURN PAT mode."
1069
1544
  ),
1070
1545
  token: credentials.token,
1546
+ tokenEndpoint: requireOption(
1547
+ normalizeOptionalString2(options.tokenEndpoint),
1548
+ "Token endpoint is required for TURN PAT mode."
1549
+ ),
1071
1550
  ttlSeconds: options.ttlSeconds,
1072
1551
  turnPort: options.turnPort,
1073
1552
  turnsPort: options.turnsPort
@@ -1117,6 +1596,7 @@ var RstreamTURNResource = class {
1117
1596
  projectEndpoint: options.projectEndpoint ?? this.client.projectEndpoint,
1118
1597
  projectId: options.projectId,
1119
1598
  serverPublicKeyHex: options.serverPublicKeyHex,
1599
+ tokenEndpoint: options.tokenEndpoint,
1120
1600
  ttlSeconds: options.ttlSeconds,
1121
1601
  turnPort: options.turnPort,
1122
1602
  turnsPort: options.turnsPort
@@ -1126,6 +1606,41 @@ var RstreamTURNResource = class {
1126
1606
 
1127
1607
  // src/webhooks-resource.ts
1128
1608
  var import_crypto2 = __toESM(require("crypto"));
1609
+ var SIGNATURE_HEX_BYTES = 32;
1610
+ var signatureHexSchema = /^[0-9a-fA-F]{64}$/;
1611
+ var timestampSchema = /^\d+$/;
1612
+ function parseSignatureTimestamp(timestamp) {
1613
+ if (!timestampSchema.test(timestamp)) {
1614
+ throw new Error("Invalid signature timestamp");
1615
+ }
1616
+ const parsed = Number(timestamp);
1617
+ if (!Number.isSafeInteger(parsed)) {
1618
+ throw new Error("Invalid signature timestamp");
1619
+ }
1620
+ return parsed;
1621
+ }
1622
+ function normalizeTolerance(tolerance) {
1623
+ if (!Number.isFinite(tolerance) || !Number.isInteger(tolerance) || tolerance < 0) {
1624
+ throw new Error("Invalid signature tolerance");
1625
+ }
1626
+ return tolerance;
1627
+ }
1628
+ function readSignature(signature) {
1629
+ if (!signatureHexSchema.test(signature)) {
1630
+ return void 0;
1631
+ }
1632
+ const parsed = Buffer.from(signature, "hex");
1633
+ return parsed.length === SIGNATURE_HEX_BYTES ? parsed : void 0;
1634
+ }
1635
+ function signatureMatches(signature, expected) {
1636
+ const parsed = readSignature(signature);
1637
+ return parsed === void 0 ? false : import_crypto2.default.timingSafeEqual(parsed, expected);
1638
+ }
1639
+ function assertSecretConfigured(secret) {
1640
+ if (secret.trim().length === 0) {
1641
+ throw new Error("Webhook signing secret is required");
1642
+ }
1643
+ }
1129
1644
  var RstreamWebhookResource = class {
1130
1645
  async event(payload, header, secret, tolerance = 300, receivedAt = Date.now()) {
1131
1646
  const payloadBuffer = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
@@ -1133,31 +1648,26 @@ var RstreamWebhookResource = class {
1133
1648
  if (!signatureHeader) {
1134
1649
  throw new Error("No signature header");
1135
1650
  }
1651
+ assertSecretConfigured(secret);
1136
1652
  const elements = signatureHeader.split(",").map((element) => element.trim());
1137
1653
  const timestamp = elements.find((element) => element.startsWith("t="))?.substring(2);
1138
1654
  const signatures = elements.filter((element) => element.startsWith("v1=")).map((element) => element.substring(3));
1139
1655
  if (!timestamp || signatures.length === 0) {
1140
1656
  throw new Error("Invalid signature header format");
1141
1657
  }
1142
- const timestampNum = parseInt(timestamp, 10);
1658
+ const timestampNum = parseSignatureTimestamp(timestamp);
1659
+ const signatureTolerance = normalizeTolerance(tolerance);
1143
1660
  const now = Math.floor(receivedAt / 1e3);
1144
- if (Math.abs(now - timestampNum) > tolerance) {
1661
+ if (Math.abs(now - timestampNum) > signatureTolerance) {
1145
1662
  throw new Error(
1146
1663
  `Webhook signature timestamp outside tolerance: ${now}, ${timestampNum}`
1147
1664
  );
1148
1665
  }
1149
1666
  const signedPayload = `${timestamp}.${payloadBuffer.toString("utf8")}`;
1150
- const expectedSignature = import_crypto2.default.createHmac("sha256", secret).update(signedPayload).digest("hex");
1151
- const signatureMatched = signatures.some((signature) => {
1152
- try {
1153
- return import_crypto2.default.timingSafeEqual(
1154
- Buffer.from(signature),
1155
- Buffer.from(expectedSignature)
1156
- );
1157
- } catch {
1158
- return false;
1159
- }
1160
- });
1667
+ const expectedSignature = import_crypto2.default.createHmac("sha256", secret).update(signedPayload).digest();
1668
+ const signatureMatched = signatures.some(
1669
+ (signature) => signatureMatches(signature, expectedSignature)
1670
+ );
1161
1671
  if (!signatureMatched) {
1162
1672
  throw new Error("Signature verification failed");
1163
1673
  }
@@ -1175,8 +1685,16 @@ var RstreamWebhookResource = class {
1175
1685
  };
1176
1686
 
1177
1687
  // src/tunnels.ts
1688
+ var engineAuthTokenScopes = {
1689
+ tunnels: {
1690
+ connect: true,
1691
+ create: true,
1692
+ list: true
1693
+ }
1694
+ };
1178
1695
  var RstreamTunnelsClient = class {
1179
1696
  config;
1697
+ managedProject;
1180
1698
  constructor(config) {
1181
1699
  this.config = config;
1182
1700
  }
@@ -1196,6 +1714,21 @@ var RstreamTunnelsClient = class {
1196
1714
  return this.config?.projectEndpoint;
1197
1715
  }
1198
1716
  async getEngine() {
1717
+ const projectEndpoint = normalizeOptionalString3(
1718
+ this.config?.projectEndpoint
1719
+ );
1720
+ if (projectEndpoint !== void 0 && normalizeOptionalString3(this.config?.engine) === void 0 && (0, import_rstream14.readEnvironment)().engine === void 0) {
1721
+ const project = await this.getManagedProject();
1722
+ if (project !== void 0) {
1723
+ const engine = (0, import_rstream12.getTunnelsProjectEngine)(project);
1724
+ if (engine) {
1725
+ return normalizeEngineAddress(engine);
1726
+ }
1727
+ }
1728
+ throw new Error(
1729
+ "Failed to resolve the engine address from the managed tunnels project."
1730
+ );
1731
+ }
1199
1732
  return await resolveTunnelsEngine({
1200
1733
  apiUrl: this.config?.apiUrl,
1201
1734
  controlPlaneCredentials: this.controlPlaneCredentials,
@@ -1204,6 +1737,57 @@ var RstreamTunnelsClient = class {
1204
1737
  projectEndpoint: this.config?.projectEndpoint
1205
1738
  });
1206
1739
  }
1740
+ async getManagedProject() {
1741
+ const projectEndpoint = normalizeOptionalString3(
1742
+ this.config?.projectEndpoint
1743
+ );
1744
+ if (projectEndpoint === void 0) {
1745
+ return void 0;
1746
+ }
1747
+ if (this.managedProject === void 0) {
1748
+ this.managedProject = resolveManagedTunnelsProject({
1749
+ apiUrl: this.config?.apiUrl,
1750
+ controlPlaneCredentials: this.controlPlaneCredentials,
1751
+ credentials: this.credentials,
1752
+ projectEndpoint
1753
+ }).catch((error) => {
1754
+ this.managedProject = void 0;
1755
+ throw error;
1756
+ });
1757
+ }
1758
+ return await this.managedProject;
1759
+ }
1760
+ async getEngineAuthTokenParams() {
1761
+ const target = await this.getEngineAuthTokenTarget();
1762
+ return {
1763
+ tunnelsGrants: [
1764
+ {
1765
+ ...target,
1766
+ scopes: engineAuthTokenScopes
1767
+ }
1768
+ ]
1769
+ };
1770
+ }
1771
+ async getEngineAuthTokenTarget() {
1772
+ const projectId = normalizeOptionalString3(this.config?.projectId);
1773
+ const workspaceId = normalizeOptionalString3(this.config?.workspaceId);
1774
+ if (projectId !== void 0 && workspaceId !== void 0) {
1775
+ throw new Error("Use either projectId or workspaceId, not both.");
1776
+ }
1777
+ if (projectId !== void 0) {
1778
+ return { projects: [projectId] };
1779
+ }
1780
+ if (workspaceId !== void 0) {
1781
+ return { workspaces: [workspaceId] };
1782
+ }
1783
+ const project = await this.getManagedProject();
1784
+ if (project !== void 0) {
1785
+ return { projects: [project.id] };
1786
+ }
1787
+ throw new Error(
1788
+ "Application credentials require projectId, workspaceId, or projectEndpoint to create scoped engine tokens."
1789
+ );
1790
+ }
1207
1791
  get auth() {
1208
1792
  return new RstreamAuthResource(this);
1209
1793
  }
@@ -1224,16 +1808,20 @@ var RstreamTunnelsClient = class {
1224
1808
  if (!credentials) {
1225
1809
  return void 0;
1226
1810
  }
1227
- if (!(0, import_rstream12.isClientCredentials)(credentials)) {
1811
+ if (!(0, import_rstream13.isClientCredentials)(credentials)) {
1228
1812
  return credentials.token;
1229
1813
  }
1230
- return (await this.auth.createAuthToken(void 0, {
1814
+ const tokenParams = await this.getEngineAuthTokenParams();
1815
+ return (await this.auth.createAuthToken(tokenParams, {
1231
1816
  credentials,
1232
1817
  engine
1233
1818
  })).token;
1234
1819
  }
1235
1820
  async request(path, options) {
1236
- const engine = await this.getEngine();
1821
+ const engine = normalizeEngineAddress(await this.getEngine());
1822
+ if (!path.startsWith("/") || path.startsWith("//")) {
1823
+ throw new Error("Engine request path must be a relative absolute path.");
1824
+ }
1237
1825
  const url = `https://${engine}/api${path}`;
1238
1826
  const headers = new Headers(options?.headers || {});
1239
1827
  const token = await this.getToken(engine);
@@ -1242,7 +1830,8 @@ var RstreamTunnelsClient = class {
1242
1830
  }
1243
1831
  const response = await fetch(url, {
1244
1832
  ...options,
1245
- headers
1833
+ headers,
1834
+ redirect: options?.redirect ?? "manual"
1246
1835
  });
1247
1836
  if (!response.ok) {
1248
1837
  const errorText = await response.text();
@@ -1251,8 +1840,68 @@ var RstreamTunnelsClient = class {
1251
1840
  return await response.json();
1252
1841
  }
1253
1842
  };
1843
+ function normalizeOptionalString3(value) {
1844
+ const normalized = value?.trim();
1845
+ return normalized ? normalized : void 0;
1846
+ }
1254
1847
 
1255
1848
  // src/watch.ts
1849
+ var import_jsonwebtoken = __toESM(require("jsonwebtoken"));
1850
+ var maxWatchTokenLifetimeSeconds = 3600;
1851
+ var watchTokenIssuedAtSkewSeconds = 300;
1852
+ function normalizeTransport(transport) {
1853
+ if (transport === void 0) {
1854
+ return "sse";
1855
+ }
1856
+ if (transport === "sse" || transport === "websocket") {
1857
+ return transport;
1858
+ }
1859
+ throw new Error("Watch: Unsupported transport.");
1860
+ }
1861
+ function validateWatchQueryToken(token) {
1862
+ const parsed = authTokenSchema.safeParse(import_jsonwebtoken.default.decode(token));
1863
+ if (!parsed.success || parsed.data.type === "pat") {
1864
+ throw new Error(
1865
+ "Watch requires a short-lived auth or app token for URL-based authentication."
1866
+ );
1867
+ }
1868
+ const iat = parsed.data.iat;
1869
+ const exp = parsed.data.exp;
1870
+ const now = Math.floor(Date.now() / 1e3);
1871
+ if (typeof iat !== "number" || typeof exp !== "number" || !Number.isInteger(iat) || !Number.isInteger(exp) || exp <= iat || exp <= now || exp > now + maxWatchTokenLifetimeSeconds || iat > now + watchTokenIssuedAtSkewSeconds || exp - iat > maxWatchTokenLifetimeSeconds) {
1872
+ throw new Error(
1873
+ "Watch requires a token with a bounded lifetime of at most 3600 seconds."
1874
+ );
1875
+ }
1876
+ if (!hasWatchOnlyTunnelGrants(parsed.data)) {
1877
+ throw new Error(
1878
+ "Watch requires a fine-grained token limited to tunnel list grants for URL-based authentication."
1879
+ );
1880
+ }
1881
+ }
1882
+ function hasWatchOnlyTunnelGrants(token) {
1883
+ if (!hasWatchOnlyPermissions(token.permissions)) {
1884
+ return false;
1885
+ }
1886
+ if (!Array.isArray(token.tunnelsGrants) || token.tunnelsGrants.length === 0) {
1887
+ return false;
1888
+ }
1889
+ return token.tunnelsGrants.every((grant) => {
1890
+ const tunnels = grant.scopes?.tunnels;
1891
+ if (tunnels === void 0) {
1892
+ return false;
1893
+ }
1894
+ return tunnels.list !== void 0 && tunnels.create === void 0 && tunnels.connect === void 0;
1895
+ });
1896
+ }
1897
+ function hasWatchOnlyPermissions(permissions) {
1898
+ if (!Array.isArray(permissions)) {
1899
+ return true;
1900
+ }
1901
+ return permissions.every(
1902
+ (permission) => permission === "tunnels.resources.read-only" || permission === "network.resources.read-only"
1903
+ );
1904
+ }
1256
1905
  var Watch = class {
1257
1906
  connection = null;
1258
1907
  connectionState = "preparing";
@@ -1267,45 +1916,56 @@ var Watch = class {
1267
1916
  throw new Error("Watch: Connection already started or closed.");
1268
1917
  }
1269
1918
  this.connectionState = "connecting";
1270
- const token = typeof this.config.auth === "function" ? await this.config.auth() : this.config.auth;
1271
- const engine = await resolveTunnelsEngine({
1272
- apiUrl: this.config.apiUrl,
1273
- controlPlaneCredentials: this.config.controlPlaneCredentials,
1274
- engine: this.config.engine,
1275
- projectEndpoint: this.config.projectEndpoint,
1276
- token
1277
- });
1278
- const transport = this.config.transport ?? "sse";
1279
- const base = `https://${engine}`;
1280
- if (transport === "sse") {
1281
- const url = new URL(`/api/sse`, base);
1282
- url.searchParams.set("rstream.token", token);
1283
- this.connection = new EventSource(url.toString());
1284
- } else {
1285
- const url = new URL(`/api/websocket`, base);
1286
- url.searchParams.set("rstream.token", token);
1287
- url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
1288
- this.connection = new WebSocket(url.toString());
1289
- }
1290
- this.connection.onopen = () => {
1291
- this.connectionState = "connected";
1292
- this.events.onConnect?.();
1293
- };
1294
- this.connection.onmessage = (msg) => {
1295
- const parsed = wsEventsSchema.parse(JSON.parse(msg.data));
1296
- this.events.onEvent?.(parsed);
1297
- };
1298
- this.connection.onerror = () => {
1299
- if (this.connectionState !== "closed") {
1300
- this.disconnect();
1919
+ try {
1920
+ const token = typeof this.config.auth === "function" ? await this.config.auth() : this.config.auth;
1921
+ validateWatchQueryToken(token);
1922
+ const engine = await resolveTunnelsEngine({
1923
+ apiUrl: this.config.apiUrl,
1924
+ controlPlaneCredentials: this.config.controlPlaneCredentials,
1925
+ engine: this.config.engine,
1926
+ projectEndpoint: this.config.projectEndpoint,
1927
+ token
1928
+ });
1929
+ const transport = normalizeTransport(this.config.transport);
1930
+ const base = `https://${engine}`;
1931
+ if (transport === "sse") {
1932
+ const url = new URL(`/api/sse`, base);
1933
+ url.searchParams.set("rstream.token", token);
1934
+ this.connection = new EventSource(url.toString());
1935
+ } else {
1936
+ const url = new URL(`/api/websocket`, base);
1937
+ url.searchParams.set("rstream.token", token);
1938
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
1939
+ this.connection = new WebSocket(url.toString());
1301
1940
  }
1302
- };
1303
- if (this.connection instanceof WebSocket) {
1304
- this.connection.onclose = () => {
1941
+ this.connection.onopen = () => {
1942
+ this.connectionState = "connected";
1943
+ this.events.onConnect?.();
1944
+ };
1945
+ this.connection.onmessage = (msg) => this.handleMessage(msg.data);
1946
+ this.connection.onerror = () => {
1305
1947
  if (this.connectionState !== "closed") {
1306
1948
  this.disconnect();
1307
1949
  }
1308
1950
  };
1951
+ if (this.connection instanceof WebSocket) {
1952
+ this.connection.onclose = () => {
1953
+ if (this.connectionState !== "closed") {
1954
+ this.disconnect();
1955
+ }
1956
+ };
1957
+ }
1958
+ } catch (error) {
1959
+ this.connectionState = "preparing";
1960
+ throw error;
1961
+ }
1962
+ }
1963
+ handleMessage(data) {
1964
+ try {
1965
+ const parsed = wsEventsSchema.parse(JSON.parse(data));
1966
+ this.events.onEvent?.(parsed);
1967
+ } catch {
1968
+ this.disconnect();
1309
1969
  }
1310
1970
  }
1311
1971
  disconnect() {
@@ -1324,7 +1984,7 @@ var Watch = class {
1324
1984
  };
1325
1985
 
1326
1986
  // src/webtty.ts
1327
- var z8 = __toESM(require("zod"));
1987
+ var z9 = __toESM(require("zod"));
1328
1988
  var osFamilies = [
1329
1989
  "linux",
1330
1990
  "macos",
@@ -1333,25 +1993,25 @@ var osFamilies = [
1333
1993
  "openbsd",
1334
1994
  "freebsd"
1335
1995
  ];
1336
- var webttyServerSchema = z8.object({
1337
- tunnel_id: z8.string(),
1338
- host: z8.string(),
1339
- token_auth: z8.boolean(),
1340
- os_family: z8.enum(osFamilies).optional(),
1341
- arch: z8.string().optional(),
1342
- os_id: z8.string().optional(),
1996
+ var webttyServerSchema = z9.object({
1997
+ tunnel_id: z9.string(),
1998
+ host: z9.string(),
1999
+ token_auth: z9.boolean(),
2000
+ os_family: z9.enum(osFamilies).optional(),
2001
+ arch: z9.string().optional(),
2002
+ os_id: z9.string().optional(),
1343
2003
  // /etc/os-release::ID (ubuntu, debian, rocky, etc.)
1344
- os_version_id: z8.string().optional(),
2004
+ os_version_id: z9.string().optional(),
1345
2005
  // /etc/os-release::VERSION_ID (24.04, 22.04, 11, 10.0.19045, ...)
1346
- os_version_codename: z8.string().optional(),
2006
+ os_version_codename: z9.string().optional(),
1347
2007
  // /etc/os-release::VERSION_CODENAME (jammy, noble...)
1348
- os_pretty_name: z8.string().optional(),
2008
+ os_pretty_name: z9.string().optional(),
1349
2009
  // /etc/os-release::PRETTY_NAME
1350
- kernel_release: z8.string().optional(),
2010
+ kernel_release: z9.string().optional(),
1351
2011
  // uname -r
1352
- hostname: z8.string().optional(),
2012
+ hostname: z9.string().optional(),
1353
2013
  // uname -n
1354
- labels: z8.record(z8.string(), z8.string().optional()).optional()
2014
+ labels: z9.record(z9.string(), z9.string().optional()).optional()
1355
2015
  });
1356
2016
  function parser(tunnel) {
1357
2017
  if (tunnel.status !== "online") return null;
@@ -1409,6 +2069,7 @@ function parseWebTTYServers(tunnels) {
1409
2069
  authTokenScopesSchema,
1410
2070
  authTokenTunnelGrantSchema,
1411
2071
  authTokenTunnelsScopesSchema,
2072
+ clientFilterSchema,
1412
2073
  clientSchema,
1413
2074
  createAPITURNCredentials,
1414
2075
  createAPPTURNCredentials,
@@ -1417,14 +2078,18 @@ function parseWebTTYServers(tunnels) {
1417
2078
  createAuthTokenResponseSchema,
1418
2079
  createPATTURNCredentials,
1419
2080
  createTURNCredentials,
2081
+ engineOpenApiDocument,
2082
+ engineOpenApiJson,
1420
2083
  eventSchema,
1421
2084
  formatTunnelHost,
1422
2085
  listClientsParamsSchema,
1423
2086
  listClientsResponseSchema,
1424
2087
  listTunnelsParamsSchema,
1425
2088
  listTunnelsResponseSchema,
2089
+ normalizeEngineAddress,
1426
2090
  parseWebTTYServers,
1427
2091
  resolveControlPlaneCredentials,
2092
+ resolveManagedTunnelsProject,
1428
2093
  resolveTunnelsAPIURL,
1429
2094
  resolveTunnelsCredentials,
1430
2095
  resolveTunnelsEngine,
@@ -1432,6 +2097,7 @@ function parseWebTTYServers(tunnels) {
1432
2097
  streamSummarySchema,
1433
2098
  streamTLSInfoSchema,
1434
2099
  tokenSchema,
2100
+ tunnelFilterSchema,
1435
2101
  tunnelSchema,
1436
2102
  turnCredentialModeSchema,
1437
2103
  webhookEventsSchema,