@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 +17 -10
- package/dist/index.d.mts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +91 -74
- package/dist/index.mjs +91 -74
- package/package.json +2 -2
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
|
|
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
|
-
`
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
143
|
-
|
|
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
package/dist/index.d.ts
CHANGED
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 (
|
|
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
|
-
|
|
128
|
-
|
|
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
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
149
|
+
function applyDefaultTunnelResourceTarget(resource, options, inheritedTarget = false) {
|
|
147
150
|
if (options?.projectScoped === false) {
|
|
148
|
-
return
|
|
151
|
+
return resource;
|
|
149
152
|
}
|
|
150
|
-
if ("AND" in
|
|
151
|
-
const branchHasTarget = inheritedTarget ||
|
|
153
|
+
if ("AND" in resource) {
|
|
154
|
+
const branchHasTarget = inheritedTarget || resource.AND.some((child) => tunnelResourceTreeHasTarget(child));
|
|
152
155
|
return {
|
|
153
|
-
AND:
|
|
154
|
-
(
|
|
156
|
+
AND: resource.AND.map(
|
|
157
|
+
(child) => applyDefaultTunnelResourceTarget(child, options, branchHasTarget)
|
|
155
158
|
)
|
|
156
159
|
};
|
|
157
160
|
}
|
|
158
|
-
if ("OR" in
|
|
161
|
+
if ("OR" in resource) {
|
|
159
162
|
return {
|
|
160
|
-
OR:
|
|
161
|
-
(
|
|
163
|
+
OR: resource.OR.map(
|
|
164
|
+
(child) => applyDefaultTunnelResourceTarget(child, options, inheritedTarget)
|
|
162
165
|
)
|
|
163
166
|
};
|
|
164
167
|
}
|
|
165
|
-
if (
|
|
166
|
-
return
|
|
168
|
+
if (resource.scopes === void 0 || tunnelResourceHasTarget(resource) || inheritedTarget) {
|
|
169
|
+
return resource;
|
|
167
170
|
}
|
|
168
|
-
return { ...
|
|
171
|
+
return { ...normalizeTunnelResourceTarget(options), scopes: resource.scopes };
|
|
169
172
|
}
|
|
170
|
-
function
|
|
171
|
-
if (
|
|
173
|
+
function tunnelResourcesNeedDefaultTarget(resource, inheritedTarget = false) {
|
|
174
|
+
if (resource === void 0) {
|
|
172
175
|
return false;
|
|
173
176
|
}
|
|
174
|
-
if ("AND" in
|
|
175
|
-
const branchHasTarget = inheritedTarget ||
|
|
176
|
-
return
|
|
177
|
-
(
|
|
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
|
|
181
|
-
return
|
|
182
|
-
(
|
|
183
|
+
if ("OR" in resource) {
|
|
184
|
+
return resource.OR.some(
|
|
185
|
+
(child) => tunnelResourcesNeedDefaultTarget(child, inheritedTarget)
|
|
183
186
|
);
|
|
184
187
|
}
|
|
185
|
-
return
|
|
188
|
+
return resource.scopes !== void 0 && !tunnelResourceHasTarget(resource) && !inheritedTarget;
|
|
186
189
|
}
|
|
187
|
-
function
|
|
188
|
-
return
|
|
190
|
+
function tunnelResourceHasTarget(resource) {
|
|
191
|
+
return resource.projects !== void 0 || resource.workspaces !== void 0;
|
|
189
192
|
}
|
|
190
|
-
function
|
|
191
|
-
if ("AND" in
|
|
192
|
-
return
|
|
193
|
+
function tunnelResourceTreeHasTarget(resource) {
|
|
194
|
+
if ("AND" in resource) {
|
|
195
|
+
return resource.AND.some((child) => tunnelResourceTreeHasTarget(child));
|
|
193
196
|
}
|
|
194
|
-
if ("OR" in
|
|
195
|
-
return
|
|
197
|
+
if ("OR" in resource) {
|
|
198
|
+
return resource.OR.some((child) => tunnelResourceTreeHasTarget(child));
|
|
196
199
|
}
|
|
197
|
-
return
|
|
200
|
+
return tunnelResourceHasTarget(resource);
|
|
198
201
|
}
|
|
199
|
-
function
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
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 (!
|
|
1738
|
+
if (!hasWatchOnlyTunnelResources(parsed.data)) {
|
|
1723
1739
|
throw new Error(
|
|
1724
|
-
"Watch requires a fine-grained token limited to tunnel list
|
|
1740
|
+
"Watch requires a fine-grained token limited to tunnel list resources for URL-based authentication."
|
|
1725
1741
|
);
|
|
1726
1742
|
}
|
|
1727
1743
|
}
|
|
1728
|
-
function
|
|
1744
|
+
function hasWatchOnlyTunnelResources(token) {
|
|
1729
1745
|
if (!hasWatchOnlyPermissions(token.permissions)) {
|
|
1730
1746
|
return false;
|
|
1731
1747
|
}
|
|
1732
|
-
|
|
1748
|
+
const resource = token.resources?.tunnels;
|
|
1749
|
+
if (resource === void 0) {
|
|
1733
1750
|
return false;
|
|
1734
1751
|
}
|
|
1735
|
-
return
|
|
1752
|
+
return hasWatchOnlyTunnelResource(resource);
|
|
1736
1753
|
}
|
|
1737
|
-
function
|
|
1738
|
-
return
|
|
1754
|
+
function hasWatchOnlyTunnelResource(resource) {
|
|
1755
|
+
return watchResourceBranches(resource).every(
|
|
1739
1756
|
(branch) => branch.hasList && !branch.hasMutationOrConnect
|
|
1740
1757
|
);
|
|
1741
1758
|
}
|
|
1742
|
-
function
|
|
1743
|
-
if ("AND" in
|
|
1744
|
-
return
|
|
1745
|
-
(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
|
|
1750
|
-
return
|
|
1766
|
+
if ("OR" in resource) {
|
|
1767
|
+
return resource.OR.flatMap(watchResourceBranches);
|
|
1751
1768
|
}
|
|
1752
|
-
const 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
|
|
1780
|
+
function combineWatchResourceBranches(branches, child) {
|
|
1764
1781
|
return branches.flatMap(
|
|
1765
|
-
(branch) =>
|
|
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"
|
|
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 (
|
|
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
|
-
|
|
69
|
-
|
|
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
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
90
|
+
function applyDefaultTunnelResourceTarget(resource, options, inheritedTarget = false) {
|
|
88
91
|
if (options?.projectScoped === false) {
|
|
89
|
-
return
|
|
92
|
+
return resource;
|
|
90
93
|
}
|
|
91
|
-
if ("AND" in
|
|
92
|
-
const branchHasTarget = inheritedTarget ||
|
|
94
|
+
if ("AND" in resource) {
|
|
95
|
+
const branchHasTarget = inheritedTarget || resource.AND.some((child) => tunnelResourceTreeHasTarget(child));
|
|
93
96
|
return {
|
|
94
|
-
AND:
|
|
95
|
-
(
|
|
97
|
+
AND: resource.AND.map(
|
|
98
|
+
(child) => applyDefaultTunnelResourceTarget(child, options, branchHasTarget)
|
|
96
99
|
)
|
|
97
100
|
};
|
|
98
101
|
}
|
|
99
|
-
if ("OR" in
|
|
102
|
+
if ("OR" in resource) {
|
|
100
103
|
return {
|
|
101
|
-
OR:
|
|
102
|
-
(
|
|
104
|
+
OR: resource.OR.map(
|
|
105
|
+
(child) => applyDefaultTunnelResourceTarget(child, options, inheritedTarget)
|
|
103
106
|
)
|
|
104
107
|
};
|
|
105
108
|
}
|
|
106
|
-
if (
|
|
107
|
-
return
|
|
109
|
+
if (resource.scopes === void 0 || tunnelResourceHasTarget(resource) || inheritedTarget) {
|
|
110
|
+
return resource;
|
|
108
111
|
}
|
|
109
|
-
return { ...
|
|
112
|
+
return { ...normalizeTunnelResourceTarget(options), scopes: resource.scopes };
|
|
110
113
|
}
|
|
111
|
-
function
|
|
112
|
-
if (
|
|
114
|
+
function tunnelResourcesNeedDefaultTarget(resource, inheritedTarget = false) {
|
|
115
|
+
if (resource === void 0) {
|
|
113
116
|
return false;
|
|
114
117
|
}
|
|
115
|
-
if ("AND" in
|
|
116
|
-
const branchHasTarget = inheritedTarget ||
|
|
117
|
-
return
|
|
118
|
-
(
|
|
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
|
|
122
|
-
return
|
|
123
|
-
(
|
|
124
|
+
if ("OR" in resource) {
|
|
125
|
+
return resource.OR.some(
|
|
126
|
+
(child) => tunnelResourcesNeedDefaultTarget(child, inheritedTarget)
|
|
124
127
|
);
|
|
125
128
|
}
|
|
126
|
-
return
|
|
129
|
+
return resource.scopes !== void 0 && !tunnelResourceHasTarget(resource) && !inheritedTarget;
|
|
127
130
|
}
|
|
128
|
-
function
|
|
129
|
-
return
|
|
131
|
+
function tunnelResourceHasTarget(resource) {
|
|
132
|
+
return resource.projects !== void 0 || resource.workspaces !== void 0;
|
|
130
133
|
}
|
|
131
|
-
function
|
|
132
|
-
if ("AND" in
|
|
133
|
-
return
|
|
134
|
+
function tunnelResourceTreeHasTarget(resource) {
|
|
135
|
+
if ("AND" in resource) {
|
|
136
|
+
return resource.AND.some((child) => tunnelResourceTreeHasTarget(child));
|
|
134
137
|
}
|
|
135
|
-
if ("OR" in
|
|
136
|
-
return
|
|
138
|
+
if ("OR" in resource) {
|
|
139
|
+
return resource.OR.some((child) => tunnelResourceTreeHasTarget(child));
|
|
137
140
|
}
|
|
138
|
-
return
|
|
141
|
+
return tunnelResourceHasTarget(resource);
|
|
139
142
|
}
|
|
140
|
-
function
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
872
|
-
|
|
873
|
-
|
|
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 (!
|
|
999
|
+
if (!hasWatchOnlyTunnelResources(parsed.data)) {
|
|
984
1000
|
throw new Error(
|
|
985
|
-
"Watch requires a fine-grained token limited to tunnel list
|
|
1001
|
+
"Watch requires a fine-grained token limited to tunnel list resources for URL-based authentication."
|
|
986
1002
|
);
|
|
987
1003
|
}
|
|
988
1004
|
}
|
|
989
|
-
function
|
|
1005
|
+
function hasWatchOnlyTunnelResources(token) {
|
|
990
1006
|
if (!hasWatchOnlyPermissions(token.permissions)) {
|
|
991
1007
|
return false;
|
|
992
1008
|
}
|
|
993
|
-
|
|
1009
|
+
const resource = token.resources?.tunnels;
|
|
1010
|
+
if (resource === void 0) {
|
|
994
1011
|
return false;
|
|
995
1012
|
}
|
|
996
|
-
return
|
|
1013
|
+
return hasWatchOnlyTunnelResource(resource);
|
|
997
1014
|
}
|
|
998
|
-
function
|
|
999
|
-
return
|
|
1015
|
+
function hasWatchOnlyTunnelResource(resource) {
|
|
1016
|
+
return watchResourceBranches(resource).every(
|
|
1000
1017
|
(branch) => branch.hasList && !branch.hasMutationOrConnect
|
|
1001
1018
|
);
|
|
1002
1019
|
}
|
|
1003
|
-
function
|
|
1004
|
-
if ("AND" in
|
|
1005
|
-
return
|
|
1006
|
-
(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
|
|
1011
|
-
return
|
|
1027
|
+
if ("OR" in resource) {
|
|
1028
|
+
return resource.OR.flatMap(watchResourceBranches);
|
|
1012
1029
|
}
|
|
1013
|
-
const 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
|
|
1041
|
+
function combineWatchResourceBranches(branches, child) {
|
|
1025
1042
|
return branches.flatMap(
|
|
1026
|
-
(branch) =>
|
|
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"
|
|
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": "
|
|
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": "
|
|
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"
|