@rstreamlabs/tunnels 2.0.0 → 3.0.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/README.md CHANGED
@@ -112,7 +112,7 @@ await watch.connect();
112
112
  ```
113
113
 
114
114
  URL-based watch authentication requires a short-lived token with bounded
115
- lifetime and watch-only tunnel list grants.
115
+ lifetime and watch-only tunnel list resources.
116
116
 
117
117
  Browser integrations should provide `auth` as a function that calls a backend
118
118
  token route and returns a fresh watch token for each connection attempt. Do not
@@ -121,17 +121,19 @@ store the `rstream.token` query token as durable browser state.
121
121
  ## Scoped Auth Tokens
122
122
 
123
123
  `auth.createAuthToken()` mints short-lived engine tokens. Prefer
124
- `tunnelsGrants` when delegating capabilities to devices, browser sessions, or
125
- other services.
124
+ `resources.tunnels` when delegating capabilities to devices, browser sessions,
125
+ or other services.
126
126
 
127
127
  ```ts
128
128
  const { token } = await client.auth.createAuthToken({
129
129
  expires_in: 60,
130
- tunnelsGrants: {
131
- scopes: {
132
- tunnels: {
133
- list: { select: { id: true, name: true, protocol: true } },
134
- connect: { params: { path: { regex: "^/api" } } },
130
+ resources: {
131
+ tunnels: {
132
+ scopes: {
133
+ tunnels: {
134
+ list: { select: { id: true, name: true, protocol: true } },
135
+ connect: { params: { path: { regex: "^/api" } } },
136
+ },
135
137
  },
136
138
  },
137
139
  },
@@ -139,8 +141,8 @@ const { token } = await client.auth.createAuthToken({
139
141
  ```
140
142
 
141
143
  When the client is configured with `projectEndpoint` or `projectId`, scope-only
142
- requests and scope-only grant leaves are project-scoped by default. Pass
143
- `projectScoped: false` only when a global scope-only grant is intentional.
144
+ tunnel resources are project-scoped by default. Pass `projectScoped: false`
145
+ only when a global scope-only tunnel resource is intentional.
144
146
 
145
147
  ## TURN Credentials
146
148
 
@@ -160,9 +162,14 @@ const turn = await createTURNCredentials({
160
162
  },
161
163
  projectEndpoint: "project-endpoint",
162
164
  clusterDomain: "cluster.example.rstream.test",
165
+ ttlSeconds: 600,
163
166
  });
164
167
  ```
165
168
 
169
+ TURN credentials are short-lived. `ttlSeconds` is optional, defaults to 600 for
170
+ local derivation, and must be an integer between 1 and 3600 seconds. PAT-backed
171
+ credentials are additionally capped by the PAT expiration.
172
+
166
173
  ## Webhooks
167
174
 
168
175
  Validate signed engine webhook payloads with constant-time signature comparison
package/dist/index.d.mts CHANGED
@@ -229,6 +229,7 @@ interface CreateAPITURNCredentialsOptions {
229
229
  credentials: RstreamCredentials;
230
230
  projectEndpoint?: string;
231
231
  projectId?: string;
232
+ ttlSeconds?: number;
232
233
  }
233
234
  interface CreatePATTURNCredentialsOptions {
234
235
  clusterDomain: string;
package/dist/index.d.ts CHANGED
@@ -229,6 +229,7 @@ interface CreateAPITURNCredentialsOptions {
229
229
  credentials: RstreamCredentials;
230
230
  projectEndpoint?: string;
231
231
  projectId?: string;
232
+ ttlSeconds?: number;
232
233
  }
233
234
  interface CreatePATTURNCredentialsOptions {
234
235
  clusterDomain: string;
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 {
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
+ };
84
+ }
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.");
86
89
  }
87
- function applyDefaultTunnelGrantTarget(grants, options, inheritedTarget = false) {
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rstreamlabs/tunnels",
3
- "version": "2.0.0",
3
+ "version": "3.0.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"