@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.js CHANGED
@@ -107,7 +107,7 @@ var RstreamAuthResource = class {
107
107
  if (options?.projectId !== void 0 || options?.workspaceId !== void 0) {
108
108
  return options;
109
109
  }
110
- if (tokenParams.scopes === void 0 && !tunnelGrantsNeedDefaultTarget(tokenParams.tunnelsGrants)) {
110
+ if (!tunnelResourcesNeedDefaultTarget(tokenParams.resources?.tunnels)) {
111
111
  return options ?? {};
112
112
  }
113
113
  const target = await this.client.getEngineAuthTokenTarget();
@@ -124,8 +124,8 @@ function createAuthTokenFromClientCredentials(credentials, params, options) {
124
124
  metadata: {
125
125
  engine: options?.engine
126
126
  },
127
- tunnelsGrants: normalizeAuthTokenTunnelGrants(tokenParams, options),
128
- permissions: null
127
+ permissions: null,
128
+ resources: normalizeAuthTokenResources(tokenParams, options)
129
129
  };
130
130
  const { token } = (0, import_rstream.createClientCredentialsToken)(credentials, {
131
131
  claims: payload,
@@ -133,70 +133,73 @@ function createAuthTokenFromClientCredentials(credentials, params, options) {
133
133
  });
134
134
  return { token };
135
135
  }
136
- function normalizeAuthTokenTunnelGrants(params, options) {
137
- const tunnelsGrants = params.tunnelsGrants;
138
- if (tunnelsGrants !== void 0) {
139
- return applyDefaultTunnelGrantTarget(tunnelsGrants, options);
140
- }
141
- if (params.scopes) {
142
- return { ...normalizeTunnelGrantTarget(options), scopes: params.scopes };
143
- }
144
- return void 0;
136
+ function normalizeAuthTokenResources(params, options) {
137
+ return {
138
+ tunnels: applyDefaultTunnelResourceTarget(
139
+ requireTunnelResource(params),
140
+ options
141
+ )
142
+ };
143
+ }
144
+ function requireTunnelResource(params) {
145
+ const tunnels = params.resources?.tunnels;
146
+ if (tunnels !== void 0) return tunnels;
147
+ throw new Error("Explicit resources.tunnels is required.");
145
148
  }
146
- function applyDefaultTunnelGrantTarget(grants, options, inheritedTarget = false) {
149
+ function applyDefaultTunnelResourceTarget(resource, options, inheritedTarget = false) {
147
150
  if (options?.projectScoped === false) {
148
- return grants;
151
+ return resource;
149
152
  }
150
- if ("AND" in grants) {
151
- const branchHasTarget = inheritedTarget || grants.AND.some((grant) => tunnelGrantTreeHasTarget(grant));
153
+ if ("AND" in resource) {
154
+ const branchHasTarget = inheritedTarget || resource.AND.some((child) => tunnelResourceTreeHasTarget(child));
152
155
  return {
153
- AND: grants.AND.map(
154
- (grant) => applyDefaultTunnelGrantTarget(grant, options, branchHasTarget)
156
+ AND: resource.AND.map(
157
+ (child) => applyDefaultTunnelResourceTarget(child, options, branchHasTarget)
155
158
  )
156
159
  };
157
160
  }
158
- if ("OR" in grants) {
161
+ if ("OR" in resource) {
159
162
  return {
160
- OR: grants.OR.map(
161
- (grant) => applyDefaultTunnelGrantTarget(grant, options, inheritedTarget)
163
+ OR: resource.OR.map(
164
+ (child) => applyDefaultTunnelResourceTarget(child, options, inheritedTarget)
162
165
  )
163
166
  };
164
167
  }
165
- if (grants.scopes === void 0 || tunnelGrantHasTarget(grants) || inheritedTarget) {
166
- return grants;
168
+ if (resource.scopes === void 0 || tunnelResourceHasTarget(resource) || inheritedTarget) {
169
+ return resource;
167
170
  }
168
- return { ...normalizeTunnelGrantTarget(options), scopes: grants.scopes };
171
+ return { ...normalizeTunnelResourceTarget(options), scopes: resource.scopes };
169
172
  }
170
- function tunnelGrantsNeedDefaultTarget(grants, inheritedTarget = false) {
171
- if (grants === void 0) {
173
+ function tunnelResourcesNeedDefaultTarget(resource, inheritedTarget = false) {
174
+ if (resource === void 0) {
172
175
  return false;
173
176
  }
174
- if ("AND" in grants) {
175
- const branchHasTarget = inheritedTarget || grants.AND.some((grant) => tunnelGrantTreeHasTarget(grant));
176
- return grants.AND.some(
177
- (grant) => tunnelGrantsNeedDefaultTarget(grant, branchHasTarget)
177
+ if ("AND" in resource) {
178
+ const branchHasTarget = inheritedTarget || resource.AND.some((child) => tunnelResourceTreeHasTarget(child));
179
+ return resource.AND.some(
180
+ (child) => tunnelResourcesNeedDefaultTarget(child, branchHasTarget)
178
181
  );
179
182
  }
180
- if ("OR" in grants) {
181
- return grants.OR.some(
182
- (grant) => tunnelGrantsNeedDefaultTarget(grant, inheritedTarget)
183
+ if ("OR" in resource) {
184
+ return resource.OR.some(
185
+ (child) => tunnelResourcesNeedDefaultTarget(child, inheritedTarget)
183
186
  );
184
187
  }
185
- return grants.scopes !== void 0 && !tunnelGrantHasTarget(grants) && !inheritedTarget;
188
+ return resource.scopes !== void 0 && !tunnelResourceHasTarget(resource) && !inheritedTarget;
186
189
  }
187
- function tunnelGrantHasTarget(grant) {
188
- return grant.projects !== void 0 || grant.workspaces !== void 0;
190
+ function tunnelResourceHasTarget(resource) {
191
+ return resource.projects !== void 0 || resource.workspaces !== void 0;
189
192
  }
190
- function tunnelGrantTreeHasTarget(grants) {
191
- if ("AND" in grants) {
192
- return grants.AND.some((grant) => tunnelGrantTreeHasTarget(grant));
193
+ function tunnelResourceTreeHasTarget(resource) {
194
+ if ("AND" in resource) {
195
+ return resource.AND.some((child) => tunnelResourceTreeHasTarget(child));
193
196
  }
194
- if ("OR" in grants) {
195
- return grants.OR.some((grant) => tunnelGrantTreeHasTarget(grant));
197
+ if ("OR" in resource) {
198
+ return resource.OR.some((child) => tunnelResourceTreeHasTarget(child));
196
199
  }
197
- return tunnelGrantHasTarget(grants);
200
+ return tunnelResourceHasTarget(resource);
198
201
  }
199
- function normalizeTunnelGrantTarget(options) {
202
+ function normalizeTunnelResourceTarget(options) {
200
203
  const projectId = normalizeOptionalString(options?.projectId);
201
204
  const workspaceId = normalizeOptionalString(options?.workspaceId);
202
205
  if (projectId !== void 0 && workspaceId !== void 0) {
@@ -212,7 +215,7 @@ function normalizeTunnelGrantTarget(options) {
212
215
  return {};
213
216
  }
214
217
  throw new Error(
215
- "Project ID or workspace ID is required when using scope-only tunnel grants."
218
+ "Project ID or workspace ID is required when using scope-only tunnel resources."
216
219
  );
217
220
  }
218
221
  function normalizeOptionalString(value) {
@@ -1129,6 +1132,9 @@ function normalizeTTLSeconds(ttlSeconds) {
1129
1132
  }
1130
1133
  return ttl;
1131
1134
  }
1135
+ function createAPITURNCredentialsParams(ttlSeconds) {
1136
+ return ttlSeconds === void 0 ? {} : { ttlSeconds: normalizeTTLSeconds(ttlSeconds) };
1137
+ }
1132
1138
  function normalizePort(port, fallback, name) {
1133
1139
  const normalized = port ?? fallback;
1134
1140
  if (!Number.isInteger(normalized) || normalized < 1 || normalized > 65535) {
@@ -1158,19 +1164,21 @@ function parseTURNTokenClaims(token) {
1158
1164
  if (!payloadPart) {
1159
1165
  throw new Error("Invalid token format.");
1160
1166
  }
1161
- let decoded;
1162
- try {
1163
- const payload = Buffer.from(payloadPart, "base64url").toString("utf8");
1164
- decoded = JSON.parse(payload);
1165
- } catch {
1166
- throw new Error("Invalid token format.");
1167
- }
1167
+ const decoded = parseTURNTokenPayload(payloadPart);
1168
1168
  const parsed = turnTokenClaimsSchema.safeParse(decoded);
1169
1169
  if (!parsed.success) {
1170
1170
  throw new Error("Invalid token format.");
1171
1171
  }
1172
1172
  return parsed.data;
1173
1173
  }
1174
+ function parseTURNTokenPayload(payloadPart) {
1175
+ try {
1176
+ const payload = Buffer.from(payloadPart, "base64url").toString("utf8");
1177
+ return JSON.parse(payload);
1178
+ } catch {
1179
+ throw new Error("Invalid token format.");
1180
+ }
1181
+ }
1174
1182
  function requirePATExpiration(claims, now) {
1175
1183
  const exp = claims.exp;
1176
1184
  if (typeof exp !== "number") {
@@ -1244,8 +1252,12 @@ async function createAPITURNCredentials(options) {
1244
1252
  apiUrl: options.apiUrl,
1245
1253
  credentials: options.credentials
1246
1254
  });
1255
+ const params = createAPITURNCredentialsParams(options.ttlSeconds);
1247
1256
  if (projectId) {
1248
- return await client.tunnels.projects.createTurnCredentials(projectId);
1257
+ return await client.tunnels.projects.createTurnCredentials(
1258
+ projectId,
1259
+ params
1260
+ );
1249
1261
  }
1250
1262
  if (!projectEndpoint) {
1251
1263
  throw new Error(
@@ -1253,7 +1265,8 @@ async function createAPITURNCredentials(options) {
1253
1265
  );
1254
1266
  }
1255
1267
  return await client.tunnels.projects.createTurnCredentialsByEndpoint(
1256
- projectEndpoint
1268
+ projectEndpoint,
1269
+ params
1257
1270
  );
1258
1271
  }
1259
1272
  function createPATTURNCredentials(options) {
@@ -1371,7 +1384,8 @@ async function createTURNCredentials(options) {
1371
1384
  apiUrl: options.apiUrl,
1372
1385
  credentials: controlPlaneCredentials,
1373
1386
  projectEndpoint,
1374
- projectId: options.projectId
1387
+ projectId: options.projectId,
1388
+ ttlSeconds: options.ttlSeconds
1375
1389
  });
1376
1390
  }
1377
1391
  const clusterDomain = normalizeClusterDomain(options.clusterDomain) ?? (projectEndpoint && options.engine ? deriveClusterDomainFromEngine(projectEndpoint, options.engine) : void 0);
@@ -1607,9 +1621,11 @@ var RstreamTunnelsClient = class {
1607
1621
  async getEngineAuthTokenParams() {
1608
1622
  const target = await this.getEngineAuthTokenTarget();
1609
1623
  return {
1610
- tunnelsGrants: {
1611
- ...target,
1612
- scopes: engineAuthTokenScopes
1624
+ resources: {
1625
+ tunnels: {
1626
+ ...target,
1627
+ scopes: engineAuthTokenScopes
1628
+ }
1613
1629
  }
1614
1630
  };
1615
1631
  }
@@ -1719,37 +1735,38 @@ function validateWatchQueryToken(token) {
1719
1735
  "Watch requires a token with a bounded lifetime of at most 3600 seconds."
1720
1736
  );
1721
1737
  }
1722
- if (!hasWatchOnlyTunnelGrants(parsed.data)) {
1738
+ if (!hasWatchOnlyTunnelResources(parsed.data)) {
1723
1739
  throw new Error(
1724
- "Watch requires a fine-grained token limited to tunnel list grants for URL-based authentication."
1740
+ "Watch requires a fine-grained token limited to tunnel list resources for URL-based authentication."
1725
1741
  );
1726
1742
  }
1727
1743
  }
1728
- function hasWatchOnlyTunnelGrants(token) {
1744
+ function hasWatchOnlyTunnelResources(token) {
1729
1745
  if (!hasWatchOnlyPermissions(token.permissions)) {
1730
1746
  return false;
1731
1747
  }
1732
- if (token.tunnelsGrants === void 0) {
1748
+ const resource = token.resources?.tunnels;
1749
+ if (resource === void 0) {
1733
1750
  return false;
1734
1751
  }
1735
- return hasWatchOnlyTunnelGrant(token.tunnelsGrants);
1752
+ return hasWatchOnlyTunnelResource(resource);
1736
1753
  }
1737
- function hasWatchOnlyTunnelGrant(grant) {
1738
- return watchGrantBranches(grant).every(
1754
+ function hasWatchOnlyTunnelResource(resource) {
1755
+ return watchResourceBranches(resource).every(
1739
1756
  (branch) => branch.hasList && !branch.hasMutationOrConnect
1740
1757
  );
1741
1758
  }
1742
- function watchGrantBranches(grant) {
1743
- if ("AND" in grant) {
1744
- return grant.AND.reduce(
1745
- (branches, child) => combineWatchGrantBranches(branches, child),
1759
+ function watchResourceBranches(resource) {
1760
+ if ("AND" in resource) {
1761
+ return resource.AND.reduce(
1762
+ (branches, child) => combineWatchResourceBranches(branches, child),
1746
1763
  [{ hasList: false, hasMutationOrConnect: false }]
1747
1764
  );
1748
1765
  }
1749
- if ("OR" in grant) {
1750
- return grant.OR.flatMap(watchGrantBranches);
1766
+ if ("OR" in resource) {
1767
+ return resource.OR.flatMap(watchResourceBranches);
1751
1768
  }
1752
- const tunnels = grant.scopes?.tunnels;
1769
+ const tunnels = resource.scopes?.tunnels;
1753
1770
  if (tunnels === void 0) {
1754
1771
  return [{ hasList: false, hasMutationOrConnect: false }];
1755
1772
  }
@@ -1760,9 +1777,9 @@ function watchGrantBranches(grant) {
1760
1777
  }
1761
1778
  ];
1762
1779
  }
1763
- function combineWatchGrantBranches(branches, child) {
1780
+ function combineWatchResourceBranches(branches, child) {
1764
1781
  return branches.flatMap(
1765
- (branch) => watchGrantBranches(child).map((childBranch) => ({
1782
+ (branch) => watchResourceBranches(child).map((childBranch) => ({
1766
1783
  hasList: branch.hasList || childBranch.hasList,
1767
1784
  hasMutationOrConnect: branch.hasMutationOrConnect || childBranch.hasMutationOrConnect
1768
1785
  }))
@@ -1773,7 +1790,7 @@ function hasWatchOnlyPermissions(permissions) {
1773
1790
  return true;
1774
1791
  }
1775
1792
  return permissions.every(
1776
- (permission) => permission === "tunnels.resources.read-only" || permission === "network.resources.read-only"
1793
+ (permission) => permission === "tunnels.resources.read-only"
1777
1794
  );
1778
1795
  }
1779
1796
  var Watch = class {
@@ -1859,34 +1876,50 @@ var Watch = class {
1859
1876
 
1860
1877
  // src/webtty.ts
1861
1878
  var z7 = __toESM(require("zod"));
1862
- var osFamilies = [
1879
+ var osFamilySchema = z7.enum([
1863
1880
  "linux",
1864
1881
  "macos",
1865
1882
  "windows",
1866
1883
  "netbsd",
1867
1884
  "openbsd",
1868
1885
  "freebsd"
1869
- ];
1886
+ ]);
1887
+ var webttyCapabilitySchema = z7.enum(["exec", "fs"]);
1888
+ var webttyFSModeSchema = z7.enum(["read-write", "read-only"]);
1870
1889
  var webttyServerSchema = z7.object({
1871
1890
  tunnel_id: z7.string(),
1872
1891
  host: z7.string(),
1873
1892
  token_auth: z7.boolean(),
1874
- os_family: z7.enum(osFamilies).optional(),
1893
+ capabilities: z7.array(webttyCapabilitySchema).optional(),
1894
+ exec_path: z7.string().optional(),
1895
+ fs_path: z7.string().optional(),
1896
+ fs_mode: webttyFSModeSchema.optional(),
1897
+ os_family: osFamilySchema.optional(),
1875
1898
  arch: z7.string().optional(),
1876
1899
  os_id: z7.string().optional(),
1877
- // /etc/os-release::ID (ubuntu, debian, rocky, etc.)
1878
1900
  os_version_id: z7.string().optional(),
1879
- // /etc/os-release::VERSION_ID (24.04, 22.04, 11, 10.0.19045, ...)
1880
1901
  os_version_codename: z7.string().optional(),
1881
- // /etc/os-release::VERSION_CODENAME (jammy, noble...)
1882
1902
  os_pretty_name: z7.string().optional(),
1883
- // /etc/os-release::PRETTY_NAME
1884
1903
  kernel_release: z7.string().optional(),
1885
- // uname -r
1886
1904
  hostname: z7.string().optional(),
1887
- // uname -n
1888
1905
  labels: z7.record(z7.string(), z7.string().optional()).optional()
1889
1906
  });
1907
+ function isWebTTYCapability(value) {
1908
+ return webttyCapabilitySchema.safeParse(value).success;
1909
+ }
1910
+ function parseWebTTYCapabilities(value) {
1911
+ const values = (value ?? "").split(",").map((capability) => capability.trim()).filter(isWebTTYCapability);
1912
+ return webttyCapabilitySchema.options.filter(
1913
+ (capability) => values.includes(capability)
1914
+ );
1915
+ }
1916
+ function webTTYCapabilitiesForLabels(labels) {
1917
+ const parsed = parseWebTTYCapabilities(labels["rstream.webtty.capabilities"]);
1918
+ return parsed.length > 0 ? parsed : ["exec"];
1919
+ }
1920
+ function webTTYHasCapability(capabilities, capability) {
1921
+ return capabilities.includes(capability);
1922
+ }
1890
1923
  function parser(tunnel) {
1891
1924
  if (tunnel.status !== "online") return null;
1892
1925
  if (tunnel.publish !== true) return null;
@@ -1895,6 +1928,7 @@ function parser(tunnel) {
1895
1928
  if (tunnelLabels["application-protocol"] !== "rstream.webtty") {
1896
1929
  return null;
1897
1930
  }
1931
+ const capabilities = webTTYCapabilitiesForLabels(tunnelLabels);
1898
1932
  const labels = {};
1899
1933
  for (const [key, value] of Object.entries(tunnelLabels)) {
1900
1934
  if (!key.startsWith("rstream.webtty.label.")) continue;
@@ -1906,6 +1940,10 @@ function parser(tunnel) {
1906
1940
  tunnel_id: tunnel.id,
1907
1941
  host: formatTunnelHost(tunnel),
1908
1942
  token_auth: tunnel.token_auth === true,
1943
+ capabilities,
1944
+ exec_path: webTTYHasCapability(capabilities, "exec") ? tunnelLabels["rstream.webtty.exec.path"] ?? "/" : void 0,
1945
+ fs_path: webTTYHasCapability(capabilities, "fs") ? tunnelLabels["rstream.webtty.fs.path"] ?? "/fs" : void 0,
1946
+ fs_mode: webTTYHasCapability(capabilities, "fs") ? tunnelLabels["rstream.webtty.fs.mode"] ?? "read-write" : void 0,
1909
1947
  os_family: tunnelLabels["rstream.webtty.os_family"],
1910
1948
  arch: tunnelLabels["rstream.webtty.arch"],
1911
1949
  os_id: tunnelLabels["rstream.webtty.os_id"],