@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/README.md +21 -10
- package/dist/index.d.mts +90 -79
- package/dist/index.d.ts +90 -79
- package/dist/index.js +121 -83
- package/dist/index.mjs +121 -83
- package/package.json +2 -2
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
|
-
return void 0;
|
|
77
|
+
function normalizeAuthTokenResources(params, options) {
|
|
78
|
+
return {
|
|
79
|
+
tunnels: applyDefaultTunnelResourceTarget(
|
|
80
|
+
requireTunnelResource(params),
|
|
81
|
+
options
|
|
82
|
+
)
|
|
83
|
+
};
|
|
86
84
|
}
|
|
87
|
-
function
|
|
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
|
|
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 {
|
|
@@ -1120,34 +1137,50 @@ var Watch = class {
|
|
|
1120
1137
|
|
|
1121
1138
|
// src/webtty.ts
|
|
1122
1139
|
import * as z2 from "zod";
|
|
1123
|
-
var
|
|
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
|
-
|
|
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": "
|
|
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": "
|
|
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"
|