@rstreamlabs/tunnels 2.0.0 → 3.1.0

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
@@ -48,7 +48,7 @@ var RstreamAuthResource = class {
48
48
  if (options?.projectId !== void 0 || options?.workspaceId !== void 0) {
49
49
  return options;
50
50
  }
51
- if (tokenParams.scopes === void 0 && !tunnelGrantsNeedDefaultTarget(tokenParams.tunnelsGrants)) {
51
+ if (!tunnelResourcesNeedDefaultTarget(tokenParams.resources?.tunnels)) {
52
52
  return options ?? {};
53
53
  }
54
54
  const target = await this.client.getEngineAuthTokenTarget();
@@ -65,8 +65,8 @@ function createAuthTokenFromClientCredentials(credentials, params, options) {
65
65
  metadata: {
66
66
  engine: options?.engine
67
67
  },
68
- tunnelsGrants: normalizeAuthTokenTunnelGrants(tokenParams, options),
69
- permissions: null
68
+ permissions: null,
69
+ resources: normalizeAuthTokenResources(tokenParams, options)
70
70
  };
71
71
  const { token } = createClientCredentialsToken(credentials, {
72
72
  claims: payload,
@@ -74,70 +74,73 @@ function createAuthTokenFromClientCredentials(credentials, params, options) {
74
74
  });
75
75
  return { token };
76
76
  }
77
- function normalizeAuthTokenTunnelGrants(params, options) {
78
- const tunnelsGrants = params.tunnelsGrants;
79
- if (tunnelsGrants !== void 0) {
80
- return applyDefaultTunnelGrantTarget(tunnelsGrants, options);
81
- }
82
- if (params.scopes) {
83
- return { ...normalizeTunnelGrantTarget(options), scopes: params.scopes };
84
- }
85
- return void 0;
77
+ function normalizeAuthTokenResources(params, options) {
78
+ return {
79
+ tunnels: applyDefaultTunnelResourceTarget(
80
+ requireTunnelResource(params),
81
+ options
82
+ )
83
+ };
86
84
  }
87
- function applyDefaultTunnelGrantTarget(grants, options, inheritedTarget = false) {
85
+ function requireTunnelResource(params) {
86
+ const tunnels = params.resources?.tunnels;
87
+ if (tunnels !== void 0) return tunnels;
88
+ throw new Error("Explicit resources.tunnels is required.");
89
+ }
90
+ function applyDefaultTunnelResourceTarget(resource, options, inheritedTarget = false) {
88
91
  if (options?.projectScoped === false) {
89
- return grants;
92
+ return resource;
90
93
  }
91
- if ("AND" in grants) {
92
- const branchHasTarget = inheritedTarget || grants.AND.some((grant) => tunnelGrantTreeHasTarget(grant));
94
+ if ("AND" in resource) {
95
+ const branchHasTarget = inheritedTarget || resource.AND.some((child) => tunnelResourceTreeHasTarget(child));
93
96
  return {
94
- AND: grants.AND.map(
95
- (grant) => applyDefaultTunnelGrantTarget(grant, options, branchHasTarget)
97
+ AND: resource.AND.map(
98
+ (child) => applyDefaultTunnelResourceTarget(child, options, branchHasTarget)
96
99
  )
97
100
  };
98
101
  }
99
- if ("OR" in grants) {
102
+ if ("OR" in resource) {
100
103
  return {
101
- OR: grants.OR.map(
102
- (grant) => applyDefaultTunnelGrantTarget(grant, options, inheritedTarget)
104
+ OR: resource.OR.map(
105
+ (child) => applyDefaultTunnelResourceTarget(child, options, inheritedTarget)
103
106
  )
104
107
  };
105
108
  }
106
- if (grants.scopes === void 0 || tunnelGrantHasTarget(grants) || inheritedTarget) {
107
- return grants;
109
+ if (resource.scopes === void 0 || tunnelResourceHasTarget(resource) || inheritedTarget) {
110
+ return resource;
108
111
  }
109
- return { ...normalizeTunnelGrantTarget(options), scopes: grants.scopes };
112
+ return { ...normalizeTunnelResourceTarget(options), scopes: resource.scopes };
110
113
  }
111
- function tunnelGrantsNeedDefaultTarget(grants, inheritedTarget = false) {
112
- if (grants === void 0) {
114
+ function tunnelResourcesNeedDefaultTarget(resource, inheritedTarget = false) {
115
+ if (resource === void 0) {
113
116
  return false;
114
117
  }
115
- if ("AND" in grants) {
116
- const branchHasTarget = inheritedTarget || grants.AND.some((grant) => tunnelGrantTreeHasTarget(grant));
117
- return grants.AND.some(
118
- (grant) => tunnelGrantsNeedDefaultTarget(grant, branchHasTarget)
118
+ if ("AND" in resource) {
119
+ const branchHasTarget = inheritedTarget || resource.AND.some((child) => tunnelResourceTreeHasTarget(child));
120
+ return resource.AND.some(
121
+ (child) => tunnelResourcesNeedDefaultTarget(child, branchHasTarget)
119
122
  );
120
123
  }
121
- if ("OR" in grants) {
122
- return grants.OR.some(
123
- (grant) => tunnelGrantsNeedDefaultTarget(grant, inheritedTarget)
124
+ if ("OR" in resource) {
125
+ return resource.OR.some(
126
+ (child) => tunnelResourcesNeedDefaultTarget(child, inheritedTarget)
124
127
  );
125
128
  }
126
- return grants.scopes !== void 0 && !tunnelGrantHasTarget(grants) && !inheritedTarget;
129
+ return resource.scopes !== void 0 && !tunnelResourceHasTarget(resource) && !inheritedTarget;
127
130
  }
128
- function tunnelGrantHasTarget(grant) {
129
- return grant.projects !== void 0 || grant.workspaces !== void 0;
131
+ function tunnelResourceHasTarget(resource) {
132
+ return resource.projects !== void 0 || resource.workspaces !== void 0;
130
133
  }
131
- function tunnelGrantTreeHasTarget(grants) {
132
- if ("AND" in grants) {
133
- return grants.AND.some((grant) => tunnelGrantTreeHasTarget(grant));
134
+ function tunnelResourceTreeHasTarget(resource) {
135
+ if ("AND" in resource) {
136
+ return resource.AND.some((child) => tunnelResourceTreeHasTarget(child));
134
137
  }
135
- if ("OR" in grants) {
136
- return grants.OR.some((grant) => tunnelGrantTreeHasTarget(grant));
138
+ if ("OR" in resource) {
139
+ return resource.OR.some((child) => tunnelResourceTreeHasTarget(child));
137
140
  }
138
- return tunnelGrantHasTarget(grants);
141
+ return tunnelResourceHasTarget(resource);
139
142
  }
140
- function normalizeTunnelGrantTarget(options) {
143
+ function normalizeTunnelResourceTarget(options) {
141
144
  const projectId = normalizeOptionalString(options?.projectId);
142
145
  const workspaceId = normalizeOptionalString(options?.workspaceId);
143
146
  if (projectId !== void 0 && workspaceId !== void 0) {
@@ -153,7 +156,7 @@ function normalizeTunnelGrantTarget(options) {
153
156
  return {};
154
157
  }
155
158
  throw new Error(
156
- "Project ID or workspace ID is required when using scope-only tunnel grants."
159
+ "Project ID or workspace ID is required when using scope-only tunnel resources."
157
160
  );
158
161
  }
159
162
  function normalizeOptionalString(value) {
@@ -390,6 +393,9 @@ function normalizeTTLSeconds(ttlSeconds) {
390
393
  }
391
394
  return ttl;
392
395
  }
396
+ function createAPITURNCredentialsParams(ttlSeconds) {
397
+ return ttlSeconds === void 0 ? {} : { ttlSeconds: normalizeTTLSeconds(ttlSeconds) };
398
+ }
393
399
  function normalizePort(port, fallback, name) {
394
400
  const normalized = port ?? fallback;
395
401
  if (!Number.isInteger(normalized) || normalized < 1 || normalized > 65535) {
@@ -419,19 +425,21 @@ function parseTURNTokenClaims(token) {
419
425
  if (!payloadPart) {
420
426
  throw new Error("Invalid token format.");
421
427
  }
422
- let decoded;
423
- try {
424
- const payload = Buffer.from(payloadPart, "base64url").toString("utf8");
425
- decoded = JSON.parse(payload);
426
- } catch {
427
- throw new Error("Invalid token format.");
428
- }
428
+ const decoded = parseTURNTokenPayload(payloadPart);
429
429
  const parsed = turnTokenClaimsSchema.safeParse(decoded);
430
430
  if (!parsed.success) {
431
431
  throw new Error("Invalid token format.");
432
432
  }
433
433
  return parsed.data;
434
434
  }
435
+ function parseTURNTokenPayload(payloadPart) {
436
+ try {
437
+ const payload = Buffer.from(payloadPart, "base64url").toString("utf8");
438
+ return JSON.parse(payload);
439
+ } catch {
440
+ throw new Error("Invalid token format.");
441
+ }
442
+ }
435
443
  function requirePATExpiration(claims, now) {
436
444
  const exp = claims.exp;
437
445
  if (typeof exp !== "number") {
@@ -505,8 +513,12 @@ async function createAPITURNCredentials(options) {
505
513
  apiUrl: options.apiUrl,
506
514
  credentials: options.credentials
507
515
  });
516
+ const params = createAPITURNCredentialsParams(options.ttlSeconds);
508
517
  if (projectId) {
509
- return await client.tunnels.projects.createTurnCredentials(projectId);
518
+ return await client.tunnels.projects.createTurnCredentials(
519
+ projectId,
520
+ params
521
+ );
510
522
  }
511
523
  if (!projectEndpoint) {
512
524
  throw new Error(
@@ -514,7 +526,8 @@ async function createAPITURNCredentials(options) {
514
526
  );
515
527
  }
516
528
  return await client.tunnels.projects.createTurnCredentialsByEndpoint(
517
- projectEndpoint
529
+ projectEndpoint,
530
+ params
518
531
  );
519
532
  }
520
533
  function createPATTURNCredentials(options) {
@@ -632,7 +645,8 @@ async function createTURNCredentials(options) {
632
645
  apiUrl: options.apiUrl,
633
646
  credentials: controlPlaneCredentials,
634
647
  projectEndpoint,
635
- projectId: options.projectId
648
+ projectId: options.projectId,
649
+ ttlSeconds: options.ttlSeconds
636
650
  });
637
651
  }
638
652
  const clusterDomain = normalizeClusterDomain(options.clusterDomain) ?? (projectEndpoint && options.engine ? deriveClusterDomainFromEngine(projectEndpoint, options.engine) : void 0);
@@ -868,9 +882,11 @@ var RstreamTunnelsClient = class {
868
882
  async getEngineAuthTokenParams() {
869
883
  const target = await this.getEngineAuthTokenTarget();
870
884
  return {
871
- tunnelsGrants: {
872
- ...target,
873
- scopes: engineAuthTokenScopes
885
+ resources: {
886
+ tunnels: {
887
+ ...target,
888
+ scopes: engineAuthTokenScopes
889
+ }
874
890
  }
875
891
  };
876
892
  }
@@ -980,37 +996,38 @@ function validateWatchQueryToken(token) {
980
996
  "Watch requires a token with a bounded lifetime of at most 3600 seconds."
981
997
  );
982
998
  }
983
- if (!hasWatchOnlyTunnelGrants(parsed.data)) {
999
+ if (!hasWatchOnlyTunnelResources(parsed.data)) {
984
1000
  throw new Error(
985
- "Watch requires a fine-grained token limited to tunnel list grants for URL-based authentication."
1001
+ "Watch requires a fine-grained token limited to tunnel list resources for URL-based authentication."
986
1002
  );
987
1003
  }
988
1004
  }
989
- function hasWatchOnlyTunnelGrants(token) {
1005
+ function hasWatchOnlyTunnelResources(token) {
990
1006
  if (!hasWatchOnlyPermissions(token.permissions)) {
991
1007
  return false;
992
1008
  }
993
- if (token.tunnelsGrants === void 0) {
1009
+ const resource = token.resources?.tunnels;
1010
+ if (resource === void 0) {
994
1011
  return false;
995
1012
  }
996
- return hasWatchOnlyTunnelGrant(token.tunnelsGrants);
1013
+ return hasWatchOnlyTunnelResource(resource);
997
1014
  }
998
- function hasWatchOnlyTunnelGrant(grant) {
999
- return watchGrantBranches(grant).every(
1015
+ function hasWatchOnlyTunnelResource(resource) {
1016
+ return watchResourceBranches(resource).every(
1000
1017
  (branch) => branch.hasList && !branch.hasMutationOrConnect
1001
1018
  );
1002
1019
  }
1003
- function watchGrantBranches(grant) {
1004
- if ("AND" in grant) {
1005
- return grant.AND.reduce(
1006
- (branches, child) => combineWatchGrantBranches(branches, child),
1020
+ function watchResourceBranches(resource) {
1021
+ if ("AND" in resource) {
1022
+ return resource.AND.reduce(
1023
+ (branches, child) => combineWatchResourceBranches(branches, child),
1007
1024
  [{ hasList: false, hasMutationOrConnect: false }]
1008
1025
  );
1009
1026
  }
1010
- if ("OR" in grant) {
1011
- return grant.OR.flatMap(watchGrantBranches);
1027
+ if ("OR" in resource) {
1028
+ return resource.OR.flatMap(watchResourceBranches);
1012
1029
  }
1013
- const tunnels = grant.scopes?.tunnels;
1030
+ const tunnels = resource.scopes?.tunnels;
1014
1031
  if (tunnels === void 0) {
1015
1032
  return [{ hasList: false, hasMutationOrConnect: false }];
1016
1033
  }
@@ -1021,9 +1038,9 @@ function watchGrantBranches(grant) {
1021
1038
  }
1022
1039
  ];
1023
1040
  }
1024
- function combineWatchGrantBranches(branches, child) {
1041
+ function combineWatchResourceBranches(branches, child) {
1025
1042
  return branches.flatMap(
1026
- (branch) => watchGrantBranches(child).map((childBranch) => ({
1043
+ (branch) => watchResourceBranches(child).map((childBranch) => ({
1027
1044
  hasList: branch.hasList || childBranch.hasList,
1028
1045
  hasMutationOrConnect: branch.hasMutationOrConnect || childBranch.hasMutationOrConnect
1029
1046
  }))
@@ -1034,7 +1051,7 @@ function hasWatchOnlyPermissions(permissions) {
1034
1051
  return true;
1035
1052
  }
1036
1053
  return permissions.every(
1037
- (permission) => permission === "tunnels.resources.read-only" || permission === "network.resources.read-only"
1054
+ (permission) => permission === "tunnels.resources.read-only"
1038
1055
  );
1039
1056
  }
1040
1057
  var Watch = class {
@@ -1120,34 +1137,50 @@ var Watch = class {
1120
1137
 
1121
1138
  // src/webtty.ts
1122
1139
  import * as z2 from "zod";
1123
- var osFamilies = [
1140
+ var osFamilySchema = z2.enum([
1124
1141
  "linux",
1125
1142
  "macos",
1126
1143
  "windows",
1127
1144
  "netbsd",
1128
1145
  "openbsd",
1129
1146
  "freebsd"
1130
- ];
1147
+ ]);
1148
+ var webttyCapabilitySchema = z2.enum(["exec", "fs"]);
1149
+ var webttyFSModeSchema = z2.enum(["read-write", "read-only"]);
1131
1150
  var webttyServerSchema = z2.object({
1132
1151
  tunnel_id: z2.string(),
1133
1152
  host: z2.string(),
1134
1153
  token_auth: z2.boolean(),
1135
- os_family: z2.enum(osFamilies).optional(),
1154
+ capabilities: z2.array(webttyCapabilitySchema).optional(),
1155
+ exec_path: z2.string().optional(),
1156
+ fs_path: z2.string().optional(),
1157
+ fs_mode: webttyFSModeSchema.optional(),
1158
+ os_family: osFamilySchema.optional(),
1136
1159
  arch: z2.string().optional(),
1137
1160
  os_id: z2.string().optional(),
1138
- // /etc/os-release::ID (ubuntu, debian, rocky, etc.)
1139
1161
  os_version_id: z2.string().optional(),
1140
- // /etc/os-release::VERSION_ID (24.04, 22.04, 11, 10.0.19045, ...)
1141
1162
  os_version_codename: z2.string().optional(),
1142
- // /etc/os-release::VERSION_CODENAME (jammy, noble...)
1143
1163
  os_pretty_name: z2.string().optional(),
1144
- // /etc/os-release::PRETTY_NAME
1145
1164
  kernel_release: z2.string().optional(),
1146
- // uname -r
1147
1165
  hostname: z2.string().optional(),
1148
- // uname -n
1149
1166
  labels: z2.record(z2.string(), z2.string().optional()).optional()
1150
1167
  });
1168
+ function isWebTTYCapability(value) {
1169
+ return webttyCapabilitySchema.safeParse(value).success;
1170
+ }
1171
+ function parseWebTTYCapabilities(value) {
1172
+ const values = (value ?? "").split(",").map((capability) => capability.trim()).filter(isWebTTYCapability);
1173
+ return webttyCapabilitySchema.options.filter(
1174
+ (capability) => values.includes(capability)
1175
+ );
1176
+ }
1177
+ function webTTYCapabilitiesForLabels(labels) {
1178
+ const parsed = parseWebTTYCapabilities(labels["rstream.webtty.capabilities"]);
1179
+ return parsed.length > 0 ? parsed : ["exec"];
1180
+ }
1181
+ function webTTYHasCapability(capabilities, capability) {
1182
+ return capabilities.includes(capability);
1183
+ }
1151
1184
  function parser(tunnel) {
1152
1185
  if (tunnel.status !== "online") return null;
1153
1186
  if (tunnel.publish !== true) return null;
@@ -1156,6 +1189,7 @@ function parser(tunnel) {
1156
1189
  if (tunnelLabels["application-protocol"] !== "rstream.webtty") {
1157
1190
  return null;
1158
1191
  }
1192
+ const capabilities = webTTYCapabilitiesForLabels(tunnelLabels);
1159
1193
  const labels = {};
1160
1194
  for (const [key, value] of Object.entries(tunnelLabels)) {
1161
1195
  if (!key.startsWith("rstream.webtty.label.")) continue;
@@ -1167,6 +1201,10 @@ function parser(tunnel) {
1167
1201
  tunnel_id: tunnel.id,
1168
1202
  host: formatTunnelHost(tunnel),
1169
1203
  token_auth: tunnel.token_auth === true,
1204
+ capabilities,
1205
+ exec_path: webTTYHasCapability(capabilities, "exec") ? tunnelLabels["rstream.webtty.exec.path"] ?? "/" : void 0,
1206
+ fs_path: webTTYHasCapability(capabilities, "fs") ? tunnelLabels["rstream.webtty.fs.path"] ?? "/fs" : void 0,
1207
+ fs_mode: webTTYHasCapability(capabilities, "fs") ? tunnelLabels["rstream.webtty.fs.mode"] ?? "read-write" : void 0,
1170
1208
  os_family: tunnelLabels["rstream.webtty.os_family"],
1171
1209
  arch: tunnelLabels["rstream.webtty.arch"],
1172
1210
  os_id: tunnelLabels["rstream.webtty.os_id"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rstreamlabs/tunnels",
3
- "version": "2.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "JS/TS SDK for the rstream Engine API and tunnel runtime APIs.",
5
5
  "author": "@uartnet <hello@rstream.io>",
6
6
  "repository": {
@@ -47,7 +47,7 @@
47
47
  "typescript-config": "*"
48
48
  },
49
49
  "dependencies": {
50
- "@rstreamlabs/rstream": "2.1.5",
50
+ "@rstreamlabs/rstream": "3.0.0",
51
51
  "@types/jsonwebtoken": "^9.0.10",
52
52
  "jsonwebtoken": "^9.0.3",
53
53
  "zod": "^4.3.6"