@rstreamlabs/tunnels 1.0.6 → 1.0.8
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 +8 -3
- package/dist/chunk-7RZ743EF.mjs +733 -0
- package/dist/index.d.mts +165 -73
- package/dist/index.d.ts +165 -73
- package/dist/index.js +849 -183
- package/dist/index.mjs +532 -566
- package/dist/openapi.d.mts +288 -0
- package/dist/openapi.d.ts +288 -0
- package/dist/openapi.js +747 -0
- package/dist/openapi.mjs +8 -0
- package/package.json +7 -2
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
clientFilterSchema,
|
|
3
|
+
clientSchema,
|
|
4
|
+
engineOpenApiDocument,
|
|
5
|
+
engineOpenApiJson,
|
|
6
|
+
eventSchema,
|
|
7
|
+
formatTunnelHost,
|
|
8
|
+
listClientsParamsSchema,
|
|
9
|
+
listClientsResponseSchema,
|
|
10
|
+
listTunnelsParamsSchema,
|
|
11
|
+
listTunnelsResponseSchema,
|
|
12
|
+
streamEndpointSchema,
|
|
13
|
+
streamSummarySchema,
|
|
14
|
+
streamTLSInfoSchema,
|
|
15
|
+
tunnelFilterSchema,
|
|
16
|
+
tunnelSchema,
|
|
17
|
+
webhookEventsSchema,
|
|
18
|
+
wsEventsSchema
|
|
19
|
+
} from "./chunk-7RZ743EF.mjs";
|
|
20
|
+
|
|
1
21
|
// src/auth-resource.ts
|
|
2
22
|
import { createClientCredentialsToken } from "@rstreamlabs/rstream";
|
|
3
23
|
|
|
@@ -5,9 +25,9 @@ import { createClientCredentialsToken } from "@rstreamlabs/rstream";
|
|
|
5
25
|
import * as z from "zod";
|
|
6
26
|
var StringFilter = z.union([
|
|
7
27
|
z.string(),
|
|
8
|
-
z.object({ exact: z.string() }),
|
|
9
|
-
z.object({ oneof: z.array(z.string()) }),
|
|
10
|
-
z.object({ regex: z.string() })
|
|
28
|
+
z.object({ exact: z.string() }).strict(),
|
|
29
|
+
z.object({ oneof: z.array(z.string()) }).strict(),
|
|
30
|
+
z.object({ regex: z.string() }).strict()
|
|
11
31
|
]);
|
|
12
32
|
function transform(field) {
|
|
13
33
|
if (field instanceof z.ZodOptional) {
|
|
@@ -41,78 +61,18 @@ function select(base) {
|
|
|
41
61
|
return [key, z.boolean().optional()];
|
|
42
62
|
});
|
|
43
63
|
const shape = Object.fromEntries(entries);
|
|
44
|
-
return z.object(shape);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// src/tunnel.ts
|
|
48
|
-
import * as z2 from "zod";
|
|
49
|
-
var DEFAULT_PUBLISHED_PORT = 443;
|
|
50
|
-
var tunnelSchema = z2.object({
|
|
51
|
-
// common properties
|
|
52
|
-
client_id: z2.string(),
|
|
53
|
-
user_id: z2.string().optional(),
|
|
54
|
-
workspace_id: z2.string().optional(),
|
|
55
|
-
project_id: z2.string().optional(),
|
|
56
|
-
cluster_id: z2.string().optional(),
|
|
57
|
-
plan: z2.string().optional(),
|
|
58
|
-
provider: z2.string().optional(),
|
|
59
|
-
region: z2.string().optional(),
|
|
60
|
-
status: z2.enum(["online", "offline"]),
|
|
61
|
-
// tunnel properties
|
|
62
|
-
id: z2.string(),
|
|
63
|
-
creation_date: z2.union([z2.string().datetime(), z2.date()]).optional(),
|
|
64
|
-
name: z2.string().optional(),
|
|
65
|
-
type: z2.enum(["bytestream", "datagram"]).optional(),
|
|
66
|
-
publish: z2.boolean().optional(),
|
|
67
|
-
protocol: z2.enum(["tls", "dtls", "quic", "http"]).optional(),
|
|
68
|
-
labels: z2.record(z2.string(), z2.string()).optional(),
|
|
69
|
-
geo_ip: z2.array(z2.string()).optional(),
|
|
70
|
-
trusted_ips: z2.array(z2.string()).optional(),
|
|
71
|
-
host: z2.string().optional(),
|
|
72
|
-
hostname: z2.string().optional(),
|
|
73
|
-
port: z2.number().int().min(1).max(65535).optional(),
|
|
74
|
-
tls_mode: z2.enum(["passthrough", "terminated"]).optional(),
|
|
75
|
-
tls_alpns: z2.array(z2.string()).optional(),
|
|
76
|
-
tls_min_version: z2.string().optional(),
|
|
77
|
-
tls_ciphers: z2.array(z2.string()).optional(),
|
|
78
|
-
mtls: z2.boolean().optional(),
|
|
79
|
-
mtls_ca_cert_pem: z2.string().optional(),
|
|
80
|
-
http_version: z2.enum(["http/1.1", "h2", "h2c", "h3"]).optional(),
|
|
81
|
-
http_use_tls: z2.boolean().optional(),
|
|
82
|
-
upstream_tls: z2.boolean().optional(),
|
|
83
|
-
token_auth: z2.boolean().optional(),
|
|
84
|
-
rstream_auth: z2.boolean().optional(),
|
|
85
|
-
challenge_mode: z2.boolean().optional()
|
|
86
|
-
});
|
|
87
|
-
var listTunnelsParamsSchema = z2.object({
|
|
88
|
-
limit: z2.number().optional(),
|
|
89
|
-
filters: tunnelSchema.pick({
|
|
90
|
-
status: true,
|
|
91
|
-
client_id: true,
|
|
92
|
-
hostname: true,
|
|
93
|
-
protocol: true,
|
|
94
|
-
publish: true,
|
|
95
|
-
labels: true
|
|
96
|
-
}).partial().optional()
|
|
97
|
-
});
|
|
98
|
-
var listTunnelsResponseSchema = z2.array(tunnelSchema);
|
|
99
|
-
function formatTunnelHost(tunnel) {
|
|
100
|
-
if (!tunnel.hostname) return tunnel.host;
|
|
101
|
-
if (!tunnel.port || tunnel.port === DEFAULT_PUBLISHED_PORT) {
|
|
102
|
-
return tunnel.hostname;
|
|
103
|
-
}
|
|
104
|
-
return `${tunnel.hostname}:${tunnel.port}`;
|
|
64
|
+
return z.object(shape).strict();
|
|
105
65
|
}
|
|
106
66
|
|
|
107
67
|
// src/auth.ts
|
|
108
|
-
import * as
|
|
109
|
-
var nonEmptyStringSchema =
|
|
110
|
-
var authTokenPermissionsSchema =
|
|
111
|
-
var authTokenTunnelsScopesSchema =
|
|
68
|
+
import * as z2 from "zod";
|
|
69
|
+
var nonEmptyStringSchema = z2.string().trim().min(1);
|
|
70
|
+
var authTokenPermissionsSchema = z2.array(z2.string());
|
|
71
|
+
var authTokenTunnelsScopesSchema = z2.object({
|
|
112
72
|
// Scopes for creating tunnels
|
|
113
|
-
create:
|
|
114
|
-
|
|
115
|
-
|
|
73
|
+
create: z2.union([
|
|
74
|
+
z2.boolean(),
|
|
75
|
+
z2.object({
|
|
116
76
|
filters: filters(
|
|
117
77
|
tunnelSchema.omit({
|
|
118
78
|
client_id: true,
|
|
@@ -128,97 +88,125 @@ var authTokenTunnelsScopesSchema = z3.object({
|
|
|
128
88
|
creation_date: true
|
|
129
89
|
})
|
|
130
90
|
).optional()
|
|
131
|
-
})
|
|
91
|
+
}).strict()
|
|
132
92
|
]).optional(),
|
|
133
93
|
// Scopes for connecting to tunnels
|
|
134
|
-
connect:
|
|
135
|
-
|
|
136
|
-
|
|
94
|
+
connect: z2.union([
|
|
95
|
+
z2.boolean(),
|
|
96
|
+
z2.object({
|
|
137
97
|
filters: filters(tunnelSchema).optional(),
|
|
138
98
|
params: filters(
|
|
139
|
-
|
|
140
|
-
path:
|
|
99
|
+
z2.object({
|
|
100
|
+
path: z2.string().optional()
|
|
141
101
|
})
|
|
142
102
|
).optional()
|
|
143
|
-
})
|
|
103
|
+
}).strict()
|
|
144
104
|
]).optional(),
|
|
145
105
|
// Scopes for listing tunnels
|
|
146
|
-
list:
|
|
147
|
-
|
|
148
|
-
|
|
106
|
+
list: z2.union([
|
|
107
|
+
z2.boolean(),
|
|
108
|
+
z2.object({
|
|
149
109
|
filters: filters(tunnelSchema).optional(),
|
|
150
110
|
select: select(tunnelSchema).optional()
|
|
151
|
-
})
|
|
111
|
+
}).strict()
|
|
152
112
|
]).optional()
|
|
113
|
+
}).strict().superRefine((scopes, ctx) => {
|
|
114
|
+
if (scopes.create === void 0 && scopes.connect === void 0 && scopes.list === void 0) {
|
|
115
|
+
ctx.addIssue({
|
|
116
|
+
code: z2.ZodIssueCode.custom,
|
|
117
|
+
message: "At least one tunnel scope action is required."
|
|
118
|
+
});
|
|
119
|
+
}
|
|
153
120
|
});
|
|
154
|
-
var authTokenScopesSchema =
|
|
121
|
+
var authTokenScopesSchema = z2.object({
|
|
155
122
|
tunnels: authTokenTunnelsScopesSchema.optional()
|
|
156
123
|
// Scopes related to tunnels
|
|
124
|
+
}).strict().superRefine((scopes, ctx) => {
|
|
125
|
+
if (scopes.tunnels === void 0) {
|
|
126
|
+
ctx.addIssue({
|
|
127
|
+
code: z2.ZodIssueCode.custom,
|
|
128
|
+
message: "Tunnel scopes are required.",
|
|
129
|
+
path: ["tunnels"]
|
|
130
|
+
});
|
|
131
|
+
}
|
|
157
132
|
});
|
|
158
|
-
var authTokenTunnelGrantSchema =
|
|
159
|
-
workspaces:
|
|
160
|
-
projects:
|
|
161
|
-
scopes: authTokenScopesSchema
|
|
133
|
+
var authTokenTunnelGrantSchema = z2.object({
|
|
134
|
+
workspaces: z2.array(nonEmptyStringSchema).min(1).optional(),
|
|
135
|
+
projects: z2.array(nonEmptyStringSchema).min(1).optional(),
|
|
136
|
+
scopes: authTokenScopesSchema
|
|
162
137
|
}).strict().superRefine((grant, ctx) => {
|
|
163
138
|
if (grant.workspaces !== void 0 && grant.projects !== void 0) {
|
|
164
139
|
ctx.addIssue({
|
|
165
|
-
code:
|
|
140
|
+
code: z2.ZodIssueCode.custom,
|
|
166
141
|
message: "A grant cannot target workspaces and projects at the same time.",
|
|
167
142
|
path: ["projects"]
|
|
168
143
|
});
|
|
169
144
|
}
|
|
145
|
+
if (grant.workspaces === void 0 && grant.projects === void 0) {
|
|
146
|
+
ctx.addIssue({
|
|
147
|
+
code: z2.ZodIssueCode.custom,
|
|
148
|
+
message: "A grant must target workspaces or projects.",
|
|
149
|
+
path: ["workspaces"]
|
|
150
|
+
});
|
|
151
|
+
}
|
|
170
152
|
});
|
|
171
153
|
var authTokenCommonSchema = {
|
|
172
|
-
aud:
|
|
173
|
-
exp:
|
|
174
|
-
tunnelsGrants:
|
|
175
|
-
iat:
|
|
176
|
-
iss:
|
|
177
|
-
jti:
|
|
178
|
-
metadata:
|
|
179
|
-
engine:
|
|
154
|
+
aud: z2.union([z2.string(), z2.array(z2.string())]).optional(),
|
|
155
|
+
exp: z2.number().optional(),
|
|
156
|
+
tunnelsGrants: z2.array(authTokenTunnelGrantSchema).optional(),
|
|
157
|
+
iat: z2.number().optional(),
|
|
158
|
+
iss: z2.string().optional(),
|
|
159
|
+
jti: z2.string().optional(),
|
|
160
|
+
metadata: z2.object({
|
|
161
|
+
engine: z2.string().optional()
|
|
180
162
|
}).strict().optional(),
|
|
181
|
-
nbf:
|
|
182
|
-
sub:
|
|
163
|
+
nbf: z2.number().optional(),
|
|
164
|
+
sub: z2.string().optional()
|
|
183
165
|
};
|
|
184
|
-
var authTokenSchema =
|
|
185
|
-
|
|
186
|
-
type:
|
|
187
|
-
userId:
|
|
166
|
+
var authTokenSchema = z2.union([
|
|
167
|
+
z2.object({
|
|
168
|
+
type: z2.literal("auth"),
|
|
169
|
+
userId: z2.string(),
|
|
188
170
|
permissions: authTokenPermissionsSchema.nullable(),
|
|
189
171
|
...authTokenCommonSchema
|
|
190
172
|
}).strict(),
|
|
191
|
-
|
|
192
|
-
type:
|
|
193
|
-
token_endpoint:
|
|
173
|
+
z2.object({
|
|
174
|
+
type: z2.literal("pat"),
|
|
175
|
+
token_endpoint: z2.string().optional(),
|
|
194
176
|
...authTokenCommonSchema
|
|
195
177
|
}).strict(),
|
|
196
|
-
|
|
197
|
-
type:
|
|
198
|
-
clientId:
|
|
178
|
+
z2.object({
|
|
179
|
+
type: z2.literal("app"),
|
|
180
|
+
clientId: z2.string(),
|
|
199
181
|
permissions: authTokenPermissionsSchema.nullable(),
|
|
200
182
|
...authTokenCommonSchema
|
|
201
183
|
}).strict()
|
|
202
184
|
]);
|
|
203
185
|
var tokenSchema = authTokenSchema;
|
|
204
|
-
var createAuthTokenParamsSchema =
|
|
205
|
-
expires_in:
|
|
206
|
-
// 1 minute
|
|
186
|
+
var createAuthTokenParamsSchema = z2.object({
|
|
187
|
+
expires_in: z2.number().int().min(1).max(3600).default(60),
|
|
207
188
|
scopes: authTokenScopesSchema.optional(),
|
|
208
|
-
tunnelsGrants:
|
|
209
|
-
metadata:
|
|
189
|
+
tunnelsGrants: z2.array(authTokenTunnelGrantSchema).min(1).optional(),
|
|
190
|
+
metadata: z2.unknown().optional()
|
|
210
191
|
// Additional metadata
|
|
211
192
|
}).strict().superRefine((params, ctx) => {
|
|
212
193
|
if (params.scopes !== void 0 && params.tunnelsGrants !== void 0) {
|
|
213
194
|
ctx.addIssue({
|
|
214
|
-
code:
|
|
195
|
+
code: z2.ZodIssueCode.custom,
|
|
215
196
|
message: "Use either scopes or tunnelsGrants, not both.",
|
|
216
197
|
path: ["tunnelsGrants"]
|
|
217
198
|
});
|
|
218
199
|
}
|
|
200
|
+
if (params.scopes === void 0 && params.tunnelsGrants === void 0) {
|
|
201
|
+
ctx.addIssue({
|
|
202
|
+
code: z2.ZodIssueCode.custom,
|
|
203
|
+
message: "Explicit scopes or tunnelsGrants are required.",
|
|
204
|
+
path: ["scopes"]
|
|
205
|
+
});
|
|
206
|
+
}
|
|
219
207
|
});
|
|
220
|
-
var createAuthTokenResponseSchema =
|
|
221
|
-
token:
|
|
208
|
+
var createAuthTokenResponseSchema = z2.object({
|
|
209
|
+
token: z2.string()
|
|
222
210
|
});
|
|
223
211
|
|
|
224
212
|
// src/auth-resource.ts
|
|
@@ -242,12 +230,12 @@ var RstreamAuthResource = class {
|
|
|
242
230
|
}
|
|
243
231
|
};
|
|
244
232
|
function createAuthTokenFromClientCredentials(credentials, params, options) {
|
|
245
|
-
const tokenParams = createAuthTokenParamsSchema.parse(params
|
|
233
|
+
const tokenParams = createAuthTokenParamsSchema.parse(params);
|
|
246
234
|
const payload = {
|
|
247
235
|
metadata: {
|
|
248
236
|
engine: options?.engine
|
|
249
237
|
},
|
|
250
|
-
tunnelsGrants: normalizeAuthTokenTunnelGrants(tokenParams),
|
|
238
|
+
tunnelsGrants: normalizeAuthTokenTunnelGrants(tokenParams, options),
|
|
251
239
|
permissions: null
|
|
252
240
|
};
|
|
253
241
|
const { token } = createClientCredentialsToken(credentials, {
|
|
@@ -256,49 +244,45 @@ function createAuthTokenFromClientCredentials(credentials, params, options) {
|
|
|
256
244
|
});
|
|
257
245
|
return { token };
|
|
258
246
|
}
|
|
259
|
-
function normalizeAuthTokenTunnelGrants(params) {
|
|
260
|
-
if (!params) {
|
|
261
|
-
return void 0;
|
|
262
|
-
}
|
|
247
|
+
function normalizeAuthTokenTunnelGrants(params, options) {
|
|
263
248
|
const tunnelsGrants = params.tunnelsGrants;
|
|
264
249
|
if (tunnelsGrants !== void 0) {
|
|
265
250
|
return [...tunnelsGrants];
|
|
266
251
|
}
|
|
267
252
|
if (params.scopes) {
|
|
268
|
-
return [{ scopes: params.scopes }];
|
|
253
|
+
return [{ ...normalizeTunnelGrantTarget(options), scopes: params.scopes }];
|
|
269
254
|
}
|
|
270
255
|
return void 0;
|
|
271
256
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
});
|
|
293
|
-
var listClientsParamsSchema = z4.object({
|
|
294
|
-
limit: z4.number().optional(),
|
|
295
|
-
filters: clientSchema.pick({
|
|
296
|
-
labels: true
|
|
297
|
-
}).partial().optional()
|
|
298
|
-
});
|
|
299
|
-
var listClientsResponseSchema = z4.array(clientSchema);
|
|
257
|
+
function normalizeTunnelGrantTarget(options) {
|
|
258
|
+
const projectId = normalizeOptionalString(options?.projectId);
|
|
259
|
+
const workspaceId = normalizeOptionalString(options?.workspaceId);
|
|
260
|
+
if (projectId !== void 0 && workspaceId !== void 0) {
|
|
261
|
+
throw new Error("Use either projectId or workspaceId, not both.");
|
|
262
|
+
}
|
|
263
|
+
if (projectId !== void 0) {
|
|
264
|
+
return { projects: [projectId] };
|
|
265
|
+
}
|
|
266
|
+
if (workspaceId !== void 0) {
|
|
267
|
+
return { workspaces: [workspaceId] };
|
|
268
|
+
}
|
|
269
|
+
throw new Error(
|
|
270
|
+
"Project ID or workspace ID is required when using scope-only tunnel grants."
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
function normalizeOptionalString(value) {
|
|
274
|
+
const normalized = value?.trim();
|
|
275
|
+
return normalized ? normalized : void 0;
|
|
276
|
+
}
|
|
300
277
|
|
|
301
278
|
// src/clients-resource.ts
|
|
279
|
+
function clientPathId(id) {
|
|
280
|
+
const normalized = id.trim();
|
|
281
|
+
if (!normalized) {
|
|
282
|
+
throw new Error("Client ID is required.");
|
|
283
|
+
}
|
|
284
|
+
return encodeURIComponent(normalized);
|
|
285
|
+
}
|
|
302
286
|
var RstreamClientsResource = class {
|
|
303
287
|
client;
|
|
304
288
|
constructor(client) {
|
|
@@ -312,299 +296,53 @@ var RstreamClientsResource = class {
|
|
|
312
296
|
return listClientsResponseSchema.parse(response);
|
|
313
297
|
}
|
|
314
298
|
async get(id) {
|
|
315
|
-
const response = await this.client.request(
|
|
316
|
-
|
|
317
|
-
|
|
299
|
+
const response = await this.client.request(
|
|
300
|
+
`/clients/${clientPathId(id)}`,
|
|
301
|
+
{
|
|
302
|
+
method: "GET"
|
|
303
|
+
}
|
|
304
|
+
);
|
|
318
305
|
return clientSchema.parse(response);
|
|
319
306
|
}
|
|
320
307
|
};
|
|
321
308
|
|
|
322
|
-
// src/stream.ts
|
|
323
|
-
import * as z5 from "zod";
|
|
324
|
-
var nonEmptyStringSchema2 = z5.string().min(1);
|
|
325
|
-
var isoDateTimeSchema = z5.string().datetime({ offset: true });
|
|
326
|
-
var ipAddressSchema = z5.string();
|
|
327
|
-
var intNonNegSchema = z5.number().int().nonnegative();
|
|
328
|
-
var numNonNegSchema = z5.number().nonnegative();
|
|
329
|
-
var geolocationSchema = z5.object({
|
|
330
|
-
country: z5.string().optional(),
|
|
331
|
-
region: z5.string().optional()
|
|
332
|
-
});
|
|
333
|
-
var streamTLSInfoSchema = z5.object({
|
|
334
|
-
version: z5.string().min(1).optional(),
|
|
335
|
-
cipher: z5.string().min(1).optional(),
|
|
336
|
-
protocol: z5.string().min(1).optional(),
|
|
337
|
-
curve: z5.string().min(1).optional(),
|
|
338
|
-
ech: z5.boolean().optional()
|
|
339
|
-
});
|
|
340
|
-
var streamEndpointSchema = z5.object({
|
|
341
|
-
ip: ipAddressSchema.optional(),
|
|
342
|
-
geolocation: geolocationSchema.optional(),
|
|
343
|
-
tls: streamTLSInfoSchema.optional()
|
|
344
|
-
});
|
|
345
|
-
var httpHeadersSchema = z5.record(
|
|
346
|
-
z5.string(),
|
|
347
|
-
z5.union([z5.string(), z5.array(z5.string())])
|
|
348
|
-
);
|
|
349
|
-
var httpProtocolSchema = z5.enum(["http/1.1", "h2", "h2c", "h3"]);
|
|
350
|
-
var httpRequestSchema = z5.object({
|
|
351
|
-
method: nonEmptyStringSchema2,
|
|
352
|
-
scheme: z5.string().min(1).optional(),
|
|
353
|
-
host: nonEmptyStringSchema2.optional(),
|
|
354
|
-
path: nonEmptyStringSchema2.optional(),
|
|
355
|
-
query: z5.string().optional(),
|
|
356
|
-
url: nonEmptyStringSchema2.optional(),
|
|
357
|
-
headers: httpHeadersSchema.optional(),
|
|
358
|
-
userAgent: z5.string().optional()
|
|
359
|
-
});
|
|
360
|
-
var httpResponseSchema = z5.object({
|
|
361
|
-
status: z5.number().int().min(100).max(999),
|
|
362
|
-
headers: httpHeadersSchema.optional()
|
|
363
|
-
});
|
|
364
|
-
var httpRequestStageSchema = z5.object({
|
|
365
|
-
protocol: httpProtocolSchema.optional(),
|
|
366
|
-
request: httpRequestSchema
|
|
367
|
-
});
|
|
368
|
-
var httpResponseStageSchema = z5.object({
|
|
369
|
-
protocol: httpProtocolSchema.optional(),
|
|
370
|
-
response: httpResponseSchema
|
|
371
|
-
});
|
|
372
|
-
var tokenSourceSchema = z5.enum([
|
|
373
|
-
"authorization",
|
|
374
|
-
"query",
|
|
375
|
-
"client",
|
|
376
|
-
"unknown"
|
|
377
|
-
]);
|
|
378
|
-
var tokenTypeSchema = z5.enum(["auth", "pat", "app", "unknown"]);
|
|
379
|
-
var firewallRuleSchema = z5.discriminatedUnion("kind", [
|
|
380
|
-
z5.object({ kind: z5.literal("none") }),
|
|
381
|
-
z5.object({ kind: z5.literal("mtls") }),
|
|
382
|
-
z5.object({
|
|
383
|
-
kind: z5.literal("rstream_auth"),
|
|
384
|
-
source: z5.literal("rstream_auth")
|
|
385
|
-
}),
|
|
386
|
-
z5.object({
|
|
387
|
-
kind: z5.literal("token"),
|
|
388
|
-
source: tokenSourceSchema.optional(),
|
|
389
|
-
tokenType: tokenTypeSchema.optional()
|
|
390
|
-
})
|
|
391
|
-
]);
|
|
392
|
-
var unauthorizedSourceSchema = z5.object({
|
|
393
|
-
source: tokenSourceSchema
|
|
394
|
-
});
|
|
395
|
-
var unauthorizedReasonSchema = (shape) => z5.object(shape).merge(unauthorizedSourceSchema);
|
|
396
|
-
var unauthorizedSchema = z5.discriminatedUnion("reason", [
|
|
397
|
-
unauthorizedReasonSchema({
|
|
398
|
-
reason: z5.literal("missing_token")
|
|
399
|
-
}),
|
|
400
|
-
unauthorizedReasonSchema({
|
|
401
|
-
reason: z5.literal("invalid_token")
|
|
402
|
-
}),
|
|
403
|
-
unauthorizedReasonSchema({
|
|
404
|
-
reason: z5.literal("expired_token")
|
|
405
|
-
}),
|
|
406
|
-
unauthorizedReasonSchema({
|
|
407
|
-
reason: z5.literal("invalid_secret")
|
|
408
|
-
}),
|
|
409
|
-
unauthorizedReasonSchema({
|
|
410
|
-
reason: z5.literal("user_mismatch")
|
|
411
|
-
}),
|
|
412
|
-
unauthorizedReasonSchema({
|
|
413
|
-
reason: z5.literal("insufficient_scope")
|
|
414
|
-
}),
|
|
415
|
-
unauthorizedReasonSchema({
|
|
416
|
-
reason: z5.literal("policy_denied"),
|
|
417
|
-
policy: z5.enum(["ip", "geo", "mtls", "challenge", "other"]),
|
|
418
|
-
value: z5.string().optional()
|
|
419
|
-
})
|
|
420
|
-
]);
|
|
421
|
-
var failureSchema = z5.discriminatedUnion("kind", [
|
|
422
|
-
z5.object({
|
|
423
|
-
kind: z5.literal("timeout"),
|
|
424
|
-
timeoutMs: intNonNegSchema.optional(),
|
|
425
|
-
message: z5.string().optional()
|
|
426
|
-
}),
|
|
427
|
-
z5.object({
|
|
428
|
-
kind: z5.literal("unauthorized"),
|
|
429
|
-
why: unauthorizedSchema,
|
|
430
|
-
message: z5.string().optional()
|
|
431
|
-
}),
|
|
432
|
-
z5.object({
|
|
433
|
-
kind: z5.literal("not_found"),
|
|
434
|
-
resource: z5.enum(["tunnel", "client", "route"]),
|
|
435
|
-
message: z5.string().optional()
|
|
436
|
-
}),
|
|
437
|
-
z5.object({
|
|
438
|
-
kind: z5.literal("refused"),
|
|
439
|
-
reason: z5.enum(["capacity", "policy", "unsupported", "other"]),
|
|
440
|
-
message: z5.string().optional()
|
|
441
|
-
}),
|
|
442
|
-
z5.object({ kind: z5.literal("io"), message: z5.string().optional() }),
|
|
443
|
-
z5.object({ kind: z5.literal("internal"), message: z5.string().optional() })
|
|
444
|
-
]);
|
|
445
|
-
var firewallSchema = z5.discriminatedUnion("decision", [
|
|
446
|
-
z5.object({ decision: z5.literal("allowed"), rule: firewallRuleSchema }),
|
|
447
|
-
z5.object({
|
|
448
|
-
decision: z5.literal("denied"),
|
|
449
|
-
rule: firewallRuleSchema,
|
|
450
|
-
failure: failureSchema
|
|
451
|
-
})
|
|
452
|
-
]);
|
|
453
|
-
var routedTunnelSchema = z5.object({
|
|
454
|
-
tunnelId: nonEmptyStringSchema2.optional(),
|
|
455
|
-
tunnelName: z5.string().optional(),
|
|
456
|
-
hostname: z5.string().optional(),
|
|
457
|
-
protocol: z5.string().optional(),
|
|
458
|
-
published: z5.boolean().optional(),
|
|
459
|
-
type: z5.string().optional(),
|
|
460
|
-
tlsMode: z5.string().optional(),
|
|
461
|
-
httpVersion: z5.string().optional(),
|
|
462
|
-
httpUseTls: z5.boolean().optional(),
|
|
463
|
-
upstreamTls: z5.boolean().optional(),
|
|
464
|
-
tokenAuth: z5.boolean().optional(),
|
|
465
|
-
mtls: z5.boolean().optional()
|
|
466
|
-
});
|
|
467
|
-
var routingSchema = z5.discriminatedUnion("decision", [
|
|
468
|
-
z5.object({ decision: z5.literal("routed"), tunnel: routedTunnelSchema }),
|
|
469
|
-
z5.object({ decision: z5.literal("not_routed"), failure: failureSchema })
|
|
470
|
-
]);
|
|
471
|
-
var streamConnectedMetricsSchema = z5.object({
|
|
472
|
-
establish_ms: numNonNegSchema,
|
|
473
|
-
duration_ms: numNonNegSchema,
|
|
474
|
-
upstream_bytes: intNonNegSchema,
|
|
475
|
-
downstream_bytes: intNonNegSchema
|
|
476
|
-
});
|
|
477
|
-
var streamFailedMetricsSchema = z5.object({
|
|
478
|
-
time_to_fail_ms: numNonNegSchema.optional(),
|
|
479
|
-
establish_ms: numNonNegSchema.optional()
|
|
480
|
-
});
|
|
481
|
-
var terminationSchema = z5.object({
|
|
482
|
-
by: z5.enum(["downstream", "upstream", "server", "unknown"]),
|
|
483
|
-
reason: z5.enum(["closed", "reset", "timeout", "error", "unknown"]),
|
|
484
|
-
message: z5.string().optional()
|
|
485
|
-
});
|
|
486
|
-
var responseConnectedSchema = z5.object({
|
|
487
|
-
outcome: z5.literal("connected"),
|
|
488
|
-
established_at: isoDateTimeSchema,
|
|
489
|
-
upstream: streamEndpointSchema.optional(),
|
|
490
|
-
metrics: streamConnectedMetricsSchema,
|
|
491
|
-
termination: terminationSchema.optional(),
|
|
492
|
-
http: httpResponseStageSchema.optional()
|
|
493
|
-
});
|
|
494
|
-
var responseFailedSchema = z5.object({
|
|
495
|
-
outcome: z5.literal("failed"),
|
|
496
|
-
failure: failureSchema,
|
|
497
|
-
upstream: streamEndpointSchema.optional(),
|
|
498
|
-
metrics: streamFailedMetricsSchema.optional(),
|
|
499
|
-
termination: terminationSchema.optional(),
|
|
500
|
-
http: httpResponseStageSchema.optional()
|
|
501
|
-
});
|
|
502
|
-
var responseSchema = z5.discriminatedUnion("outcome", [
|
|
503
|
-
responseConnectedSchema,
|
|
504
|
-
responseFailedSchema
|
|
505
|
-
]);
|
|
506
|
-
var streamSummarySchema = z5.object({
|
|
507
|
-
workspace_id: nonEmptyStringSchema2.optional(),
|
|
508
|
-
project_id: nonEmptyStringSchema2.optional(),
|
|
509
|
-
cluster_id: nonEmptyStringSchema2.optional(),
|
|
510
|
-
user_id: nonEmptyStringSchema2.optional(),
|
|
511
|
-
client_id: nonEmptyStringSchema2.optional(),
|
|
512
|
-
tunnel_id: nonEmptyStringSchema2.optional(),
|
|
513
|
-
stream_id: nonEmptyStringSchema2,
|
|
514
|
-
created_at: isoDateTimeSchema,
|
|
515
|
-
terminated_at: isoDateTimeSchema,
|
|
516
|
-
request: z5.object({
|
|
517
|
-
downstream: streamEndpointSchema,
|
|
518
|
-
http: httpRequestStageSchema.optional()
|
|
519
|
-
}),
|
|
520
|
-
firewall: firewallSchema,
|
|
521
|
-
routing: routingSchema,
|
|
522
|
-
response: responseSchema
|
|
523
|
-
});
|
|
524
|
-
|
|
525
|
-
// src/event.ts
|
|
526
|
-
import * as z6 from "zod";
|
|
527
|
-
var isoDateTimeSchema2 = z6.string().datetime({ offset: true });
|
|
528
|
-
var initialStateSchema = z6.object({
|
|
529
|
-
snapshot_at: isoDateTimeSchema2,
|
|
530
|
-
clients: z6.array(clientSchema),
|
|
531
|
-
tunnels: z6.array(tunnelSchema)
|
|
532
|
-
});
|
|
533
|
-
var eventBaseSchema = z6.object({
|
|
534
|
-
id: z6.string().optional(),
|
|
535
|
-
created_at: isoDateTimeSchema2.optional(),
|
|
536
|
-
user_id: z6.string().optional(),
|
|
537
|
-
workspace_id: z6.string().optional(),
|
|
538
|
-
project_id: z6.string().optional(),
|
|
539
|
-
cluster_id: z6.string().optional()
|
|
540
|
-
});
|
|
541
|
-
var withEventBase = (shape) => z6.object(shape).merge(eventBaseSchema).passthrough();
|
|
542
|
-
var stateInitialEventSchema = withEventBase({
|
|
543
|
-
type: z6.literal("state.initial"),
|
|
544
|
-
object: initialStateSchema
|
|
545
|
-
});
|
|
546
|
-
var streamSummaryEventSchema = withEventBase({
|
|
547
|
-
type: z6.literal("stream.summary"),
|
|
548
|
-
object: streamSummarySchema
|
|
549
|
-
});
|
|
550
|
-
var commonEventsSchema = [
|
|
551
|
-
withEventBase({
|
|
552
|
-
type: z6.literal("client.created"),
|
|
553
|
-
object: clientSchema
|
|
554
|
-
}),
|
|
555
|
-
withEventBase({
|
|
556
|
-
type: z6.literal("client.updated"),
|
|
557
|
-
object: clientSchema
|
|
558
|
-
}),
|
|
559
|
-
withEventBase({
|
|
560
|
-
type: z6.literal("client.deleted"),
|
|
561
|
-
object: clientSchema
|
|
562
|
-
}),
|
|
563
|
-
withEventBase({
|
|
564
|
-
type: z6.literal("tunnel.created"),
|
|
565
|
-
object: tunnelSchema
|
|
566
|
-
}),
|
|
567
|
-
withEventBase({
|
|
568
|
-
type: z6.literal("tunnel.updated"),
|
|
569
|
-
object: tunnelSchema
|
|
570
|
-
}),
|
|
571
|
-
withEventBase({
|
|
572
|
-
type: z6.literal("tunnel.deleted"),
|
|
573
|
-
object: tunnelSchema
|
|
574
|
-
})
|
|
575
|
-
];
|
|
576
|
-
var wsEventsSchema = z6.union([
|
|
577
|
-
stateInitialEventSchema,
|
|
578
|
-
...commonEventsSchema
|
|
579
|
-
]);
|
|
580
|
-
var webhookEventsSchema = z6.union([
|
|
581
|
-
...commonEventsSchema,
|
|
582
|
-
streamSummaryEventSchema
|
|
583
|
-
]);
|
|
584
|
-
var eventSchema = z6.union([wsEventsSchema, webhookEventsSchema]);
|
|
585
|
-
|
|
586
309
|
// src/resolution.ts
|
|
587
310
|
import { DEFAULT_API_URL } from "@rstreamlabs/rstream";
|
|
588
311
|
import { getTunnelsProjectEngine } from "@rstreamlabs/rstream";
|
|
589
312
|
import { isTokenCredentials } from "@rstreamlabs/rstream";
|
|
590
313
|
import { readEnvironment } from "@rstreamlabs/rstream";
|
|
591
314
|
import { RstreamClient } from "@rstreamlabs/rstream";
|
|
592
|
-
import jwt from "jsonwebtoken";
|
|
593
315
|
function trimOptionalString(value) {
|
|
594
316
|
const normalized = value?.trim();
|
|
595
317
|
return normalized ? normalized : void 0;
|
|
596
318
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
319
|
+
var dnsLabelPattern = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i;
|
|
320
|
+
function validateHostname(hostname) {
|
|
321
|
+
if (!hostname || hostname.length > 253 || hostname.includes("..")) {
|
|
322
|
+
return false;
|
|
600
323
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
324
|
+
return hostname.split(".").every((label) => label === "localhost" || dnsLabelPattern.test(label));
|
|
325
|
+
}
|
|
326
|
+
function normalizeEngineAddress(engine) {
|
|
327
|
+
const normalized = engine.trim();
|
|
328
|
+
if (!normalized || /^[a-z][a-z0-9+.-]*:\/\//i.test(normalized) || /[/?#@\\\s]/.test(normalized)) {
|
|
329
|
+
throw new Error("Engine must be a host[:port] value.");
|
|
330
|
+
}
|
|
331
|
+
const explicitPort = normalized.match(/:(\d+)$/)?.[1];
|
|
332
|
+
const url = new URL(`https://${normalized}`);
|
|
333
|
+
if (url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
|
|
334
|
+
throw new Error("Engine must be a host[:port] value.");
|
|
335
|
+
}
|
|
336
|
+
if (!validateHostname(url.hostname)) {
|
|
337
|
+
throw new Error("Engine must use a valid hostname.");
|
|
338
|
+
}
|
|
339
|
+
if (explicitPort) {
|
|
340
|
+
const port = Number(explicitPort);
|
|
341
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
342
|
+
throw new Error("Engine port must be an integer between 1 and 65535.");
|
|
343
|
+
}
|
|
606
344
|
}
|
|
607
|
-
return
|
|
345
|
+
return `${url.hostname.toLowerCase()}${explicitPort ? `:${explicitPort}` : ""}`;
|
|
608
346
|
}
|
|
609
347
|
function resolveTunnelsAPIURL(apiUrl) {
|
|
610
348
|
return trimOptionalString(apiUrl) ?? readEnvironment().apiUrl ?? DEFAULT_API_URL;
|
|
@@ -638,37 +376,22 @@ function resolveControlPlaneCredentials(controlPlaneCredentials, credentials, au
|
|
|
638
376
|
async function resolveTunnelsEngine(config) {
|
|
639
377
|
const explicitEngine = trimOptionalString(config.engine);
|
|
640
378
|
if (explicitEngine) {
|
|
641
|
-
return explicitEngine;
|
|
379
|
+
return normalizeEngineAddress(explicitEngine);
|
|
642
380
|
}
|
|
643
381
|
const environmentEngine = readEnvironment().engine;
|
|
644
382
|
if (environmentEngine) {
|
|
645
|
-
return environmentEngine;
|
|
383
|
+
return normalizeEngineAddress(environmentEngine);
|
|
646
384
|
}
|
|
647
385
|
const token = trimOptionalString(config.token) ?? (isTokenCredentials(config.credentials) ? config.credentials.token : void 0);
|
|
648
|
-
const tokenEngine = getEngineFromToken(token);
|
|
649
|
-
if (tokenEngine) {
|
|
650
|
-
return tokenEngine;
|
|
651
|
-
}
|
|
652
386
|
const projectEndpoint = trimOptionalString(config.projectEndpoint);
|
|
653
387
|
if (projectEndpoint) {
|
|
654
|
-
const
|
|
655
|
-
config
|
|
656
|
-
config.credentials,
|
|
388
|
+
const project = await resolveManagedTunnelsProject({
|
|
389
|
+
...config,
|
|
657
390
|
token
|
|
658
|
-
);
|
|
659
|
-
if (credentials === void 0) {
|
|
660
|
-
throw new Error(
|
|
661
|
-
"Control-plane credentials are required to resolve a managed project endpoint."
|
|
662
|
-
);
|
|
663
|
-
}
|
|
664
|
-
const controlPlane = new RstreamClient({
|
|
665
|
-
apiUrl: resolveTunnelsAPIURL(config.apiUrl),
|
|
666
|
-
credentials
|
|
667
391
|
});
|
|
668
|
-
const project = await controlPlane.tunnels.projects.resolveByEndpoint(projectEndpoint);
|
|
669
392
|
const engine = getTunnelsProjectEngine(project);
|
|
670
393
|
if (engine) {
|
|
671
|
-
return engine;
|
|
394
|
+
return normalizeEngineAddress(engine);
|
|
672
395
|
}
|
|
673
396
|
throw new Error(
|
|
674
397
|
"Failed to resolve the engine address from the managed tunnels project."
|
|
@@ -678,8 +401,36 @@ async function resolveTunnelsEngine(config) {
|
|
|
678
401
|
"Engine URL is not defined. Provide an engine, use RSTREAM_ENGINE, or configure a managed project endpoint with control-plane access."
|
|
679
402
|
);
|
|
680
403
|
}
|
|
404
|
+
async function resolveManagedTunnelsProject(config) {
|
|
405
|
+
const projectEndpoint = trimOptionalString(config.projectEndpoint);
|
|
406
|
+
if (!projectEndpoint) {
|
|
407
|
+
throw new Error("Managed project endpoint is required.");
|
|
408
|
+
}
|
|
409
|
+
const credentials = resolveControlPlaneCredentials(
|
|
410
|
+
config.controlPlaneCredentials,
|
|
411
|
+
config.credentials,
|
|
412
|
+
config.token
|
|
413
|
+
);
|
|
414
|
+
if (credentials === void 0) {
|
|
415
|
+
throw new Error(
|
|
416
|
+
"Control-plane credentials are required to resolve a managed project endpoint."
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
const controlPlane = new RstreamClient({
|
|
420
|
+
apiUrl: resolveTunnelsAPIURL(config.apiUrl),
|
|
421
|
+
credentials
|
|
422
|
+
});
|
|
423
|
+
return await controlPlane.tunnels.projects.resolveByEndpoint(projectEndpoint);
|
|
424
|
+
}
|
|
681
425
|
|
|
682
426
|
// src/tunnels-resource.ts
|
|
427
|
+
function tunnelPathId(id) {
|
|
428
|
+
const normalized = id.trim();
|
|
429
|
+
if (!normalized) {
|
|
430
|
+
throw new Error("Tunnel ID is required.");
|
|
431
|
+
}
|
|
432
|
+
return encodeURIComponent(normalized);
|
|
433
|
+
}
|
|
683
434
|
var RstreamTunnelsResource = class {
|
|
684
435
|
client;
|
|
685
436
|
constructor(client) {
|
|
@@ -693,65 +444,76 @@ var RstreamTunnelsResource = class {
|
|
|
693
444
|
return listTunnelsResponseSchema.parse(response);
|
|
694
445
|
}
|
|
695
446
|
async get(id) {
|
|
696
|
-
const response = await this.client.request(
|
|
697
|
-
|
|
698
|
-
|
|
447
|
+
const response = await this.client.request(
|
|
448
|
+
`/tunnels/${tunnelPathId(id)}`,
|
|
449
|
+
{
|
|
450
|
+
method: "GET"
|
|
451
|
+
}
|
|
452
|
+
);
|
|
699
453
|
return tunnelSchema.parse(response);
|
|
700
454
|
}
|
|
701
455
|
};
|
|
702
456
|
|
|
703
457
|
// src/tunnels.ts
|
|
458
|
+
import { getTunnelsProjectEngine as getTunnelsProjectEngine2 } from "@rstreamlabs/rstream";
|
|
704
459
|
import { isClientCredentials as isClientCredentials3 } from "@rstreamlabs/rstream";
|
|
460
|
+
import { readEnvironment as readEnvironment2 } from "@rstreamlabs/rstream";
|
|
705
461
|
|
|
706
462
|
// src/turn.ts
|
|
707
463
|
import { isClientCredentials as isClientCredentials2 } from "@rstreamlabs/rstream";
|
|
708
464
|
import { isTokenCredentials as isTokenCredentials2 } from "@rstreamlabs/rstream";
|
|
709
465
|
import { RstreamClient as RstreamClient2 } from "@rstreamlabs/rstream";
|
|
710
466
|
import { turnCredentialsSchema } from "@rstreamlabs/rstream";
|
|
711
|
-
import * as
|
|
467
|
+
import * as z3 from "zod";
|
|
712
468
|
import crypto from "crypto";
|
|
713
469
|
var defaultTURNPort = 3478;
|
|
714
470
|
var defaultTURNSPort = 5349;
|
|
715
|
-
var defaultTURNCredentialTTLSeconds =
|
|
716
|
-
var
|
|
717
|
-
|
|
718
|
-
|
|
471
|
+
var defaultTURNCredentialTTLSeconds = 10 * 60;
|
|
472
|
+
var maxTURNCredentialTTLSeconds = 60 * 60;
|
|
473
|
+
var turnTokenClaimsSchema = z3.object({
|
|
474
|
+
exp: z3.number().int().optional(),
|
|
475
|
+
iat: z3.number().int().optional(),
|
|
476
|
+
token_endpoint: z3.string().regex(/^[0-9a-f]{8}$/i).optional(),
|
|
477
|
+
type: z3.enum(["app", "auth", "pat"])
|
|
719
478
|
});
|
|
720
|
-
var turnCredentialModeSchema =
|
|
721
|
-
function
|
|
479
|
+
var turnCredentialModeSchema = z3.enum(["api", "app", "pat"]);
|
|
480
|
+
function normalizeOptionalString2(value) {
|
|
722
481
|
const normalized = value?.trim();
|
|
723
482
|
return normalized ? normalized : void 0;
|
|
724
483
|
}
|
|
725
484
|
function normalizeClusterDomain(clusterDomain) {
|
|
726
|
-
const normalized =
|
|
485
|
+
const normalized = normalizeOptionalString2(clusterDomain);
|
|
727
486
|
return normalized?.toLowerCase();
|
|
728
487
|
}
|
|
729
488
|
function normalizeNow(now) {
|
|
730
|
-
if (now
|
|
731
|
-
return Math.floor(now
|
|
489
|
+
if (now === void 0) {
|
|
490
|
+
return Math.floor(Date.now() / 1e3);
|
|
732
491
|
}
|
|
733
|
-
|
|
734
|
-
|
|
492
|
+
const timestamp = now instanceof Date ? now.getTime() / 1e3 : now;
|
|
493
|
+
if (!Number.isFinite(timestamp)) {
|
|
494
|
+
throw new Error("TURN timestamp must be finite.");
|
|
735
495
|
}
|
|
736
|
-
return Math.floor(
|
|
496
|
+
return Math.floor(timestamp);
|
|
737
497
|
}
|
|
738
498
|
function normalizeTTLSeconds(ttlSeconds) {
|
|
739
|
-
|
|
740
|
-
|
|
499
|
+
const ttl = ttlSeconds ?? defaultTURNCredentialTTLSeconds;
|
|
500
|
+
if (!Number.isInteger(ttl) || ttl < 1 || ttl > maxTURNCredentialTTLSeconds) {
|
|
501
|
+
throw new Error("TURN TTL must be an integer between 1 and 3600 seconds.");
|
|
741
502
|
}
|
|
742
|
-
return
|
|
503
|
+
return ttl;
|
|
743
504
|
}
|
|
744
|
-
function
|
|
745
|
-
|
|
746
|
-
|
|
505
|
+
function normalizePort(port, fallback, name) {
|
|
506
|
+
const normalized = port ?? fallback;
|
|
507
|
+
if (!Number.isInteger(normalized) || normalized < 1 || normalized > 65535) {
|
|
508
|
+
throw new Error(`${name} must be an integer between 1 and 65535.`);
|
|
747
509
|
}
|
|
748
|
-
return
|
|
510
|
+
return normalized;
|
|
511
|
+
}
|
|
512
|
+
function normalizeTURNPort(port) {
|
|
513
|
+
return normalizePort(port, defaultTURNPort, "TURN port");
|
|
749
514
|
}
|
|
750
515
|
function normalizeTURNSPort(port) {
|
|
751
|
-
|
|
752
|
-
return Math.floor(port);
|
|
753
|
-
}
|
|
754
|
-
return defaultTURNSPort;
|
|
516
|
+
return normalizePort(port, defaultTURNSPort, "TURNS port");
|
|
755
517
|
}
|
|
756
518
|
function createTURNURLs(clusterDomain, turnPort, turnsPort) {
|
|
757
519
|
const normalizedTURNPort = normalizeTURNPort(turnPort);
|
|
@@ -769,16 +531,32 @@ function parseTURNTokenClaims(token) {
|
|
|
769
531
|
if (!payloadPart) {
|
|
770
532
|
throw new Error("Invalid token format.");
|
|
771
533
|
}
|
|
772
|
-
|
|
773
|
-
|
|
534
|
+
let decoded;
|
|
535
|
+
try {
|
|
536
|
+
const payload = Buffer.from(payloadPart, "base64url").toString("utf8");
|
|
537
|
+
decoded = JSON.parse(payload);
|
|
538
|
+
} catch {
|
|
539
|
+
throw new Error("Invalid token format.");
|
|
540
|
+
}
|
|
541
|
+
const parsed = turnTokenClaimsSchema.safeParse(decoded);
|
|
774
542
|
if (!parsed.success) {
|
|
775
543
|
throw new Error("Invalid token format.");
|
|
776
544
|
}
|
|
777
545
|
return parsed.data;
|
|
778
546
|
}
|
|
547
|
+
function requirePATExpiration(claims, now) {
|
|
548
|
+
const exp = claims.exp;
|
|
549
|
+
if (typeof exp !== "number") {
|
|
550
|
+
throw new Error("TURN PAT mode requires a PAT token with expiration.");
|
|
551
|
+
}
|
|
552
|
+
if (exp <= now) {
|
|
553
|
+
throw new Error("TURN PAT mode requires a non-expired PAT token.");
|
|
554
|
+
}
|
|
555
|
+
return exp;
|
|
556
|
+
}
|
|
779
557
|
function deriveClusterDomainFromEngine(projectEndpoint, engine) {
|
|
780
|
-
const normalizedProjectEndpoint =
|
|
781
|
-
const normalizedEngine =
|
|
558
|
+
const normalizedProjectEndpoint = normalizeOptionalString2(projectEndpoint);
|
|
559
|
+
const normalizedEngine = normalizeOptionalString2(engine);
|
|
782
560
|
if (!normalizedProjectEndpoint || !normalizedEngine) {
|
|
783
561
|
throw new Error(
|
|
784
562
|
"Failed to derive the cluster domain from the engine address."
|
|
@@ -812,17 +590,11 @@ function resolveMode(credentials, mode) {
|
|
|
812
590
|
if (isClientCredentials2(credentials)) {
|
|
813
591
|
return "app";
|
|
814
592
|
}
|
|
815
|
-
if (isTokenCredentials2(credentials)) {
|
|
816
|
-
const claims = parseTURNTokenClaims(credentials.token);
|
|
817
|
-
if (claims.type === "pat" && normalizeOptionalString(claims.token_endpoint)) {
|
|
818
|
-
return "pat";
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
593
|
return "api";
|
|
822
594
|
}
|
|
823
595
|
async function loadTURNServerPublicKeyHex(clusterDomain, options) {
|
|
824
|
-
const configuredKey =
|
|
825
|
-
const keyringBaseURL = configuredKey ??
|
|
596
|
+
const configuredKey = normalizeOptionalString2(options.keyringBaseUrl);
|
|
597
|
+
const keyringBaseURL = configuredKey ?? normalizeOptionalString2(options.apiUrl) ?? "https://rstream.io";
|
|
826
598
|
const url = new URL(
|
|
827
599
|
`/keyrings/turn/${clusterDomain}.spki.der.hex`,
|
|
828
600
|
keyringBaseURL
|
|
@@ -834,8 +606,8 @@ async function loadTURNServerPublicKeyHex(clusterDomain, options) {
|
|
|
834
606
|
return (await response.text()).trim();
|
|
835
607
|
}
|
|
836
608
|
async function createAPITURNCredentials(options) {
|
|
837
|
-
const projectId =
|
|
838
|
-
const projectEndpoint =
|
|
609
|
+
const projectId = normalizeOptionalString2(options.projectId);
|
|
610
|
+
const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
|
|
839
611
|
if (!projectId && !projectEndpoint) {
|
|
840
612
|
throw new Error(
|
|
841
613
|
"Project ID or project endpoint is required for TURN API mode."
|
|
@@ -862,11 +634,11 @@ function createPATTURNCredentials(options) {
|
|
|
862
634
|
if (!clusterDomain) {
|
|
863
635
|
throw new Error("Cluster domain is required for TURN PAT mode.");
|
|
864
636
|
}
|
|
865
|
-
const projectEndpoint =
|
|
637
|
+
const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
|
|
866
638
|
if (!projectEndpoint) {
|
|
867
639
|
throw new Error("Project endpoint is required for TURN PAT mode.");
|
|
868
640
|
}
|
|
869
|
-
const token =
|
|
641
|
+
const token = normalizeOptionalString2(options.token);
|
|
870
642
|
if (!token) {
|
|
871
643
|
throw new Error("Token is required for TURN PAT mode.");
|
|
872
644
|
}
|
|
@@ -874,14 +646,17 @@ function createPATTURNCredentials(options) {
|
|
|
874
646
|
if (claims.type !== "pat") {
|
|
875
647
|
throw new Error("TURN PAT mode requires a PAT token.");
|
|
876
648
|
}
|
|
877
|
-
const tokenEndpoint =
|
|
649
|
+
const tokenEndpoint = normalizeOptionalString2(options.tokenEndpoint);
|
|
878
650
|
if (!tokenEndpoint) {
|
|
879
|
-
throw new Error(
|
|
880
|
-
|
|
881
|
-
|
|
651
|
+
throw new Error("Token endpoint is required for TURN PAT mode.");
|
|
652
|
+
}
|
|
653
|
+
if (claims.token_endpoint !== void 0 && claims.token_endpoint !== tokenEndpoint) {
|
|
654
|
+
throw new Error("Token endpoint does not match the PAT token.");
|
|
882
655
|
}
|
|
656
|
+
const now = normalizeNow(options.now);
|
|
883
657
|
const ttlSeconds = normalizeTTLSeconds(options.ttlSeconds);
|
|
884
|
-
const exp =
|
|
658
|
+
const exp = Math.min(now + ttlSeconds, requirePATExpiration(claims, now));
|
|
659
|
+
const ttl = exp - now;
|
|
885
660
|
const username = `v1:${exp}:pat:${projectEndpoint}:${tokenEndpoint}`;
|
|
886
661
|
const tokenHash = crypto.createHash("sha256").update(token, "utf8").digest();
|
|
887
662
|
const key = Buffer.from(
|
|
@@ -896,7 +671,7 @@ function createPATTURNCredentials(options) {
|
|
|
896
671
|
const credential = crypto.createHmac("sha256", key).update(username, "utf8").digest("base64");
|
|
897
672
|
return turnCredentialsSchema.parse({
|
|
898
673
|
credential,
|
|
899
|
-
ttl
|
|
674
|
+
ttl,
|
|
900
675
|
urls: createTURNURLs(clusterDomain, options.turnPort, options.turnsPort),
|
|
901
676
|
username
|
|
902
677
|
});
|
|
@@ -906,15 +681,15 @@ async function createAPPTURNCredentials(options) {
|
|
|
906
681
|
if (!clusterDomain) {
|
|
907
682
|
throw new Error("Cluster domain is required for TURN APP mode.");
|
|
908
683
|
}
|
|
909
|
-
const projectEndpoint =
|
|
684
|
+
const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
|
|
910
685
|
if (!projectEndpoint) {
|
|
911
686
|
throw new Error("Project endpoint is required for TURN APP mode.");
|
|
912
687
|
}
|
|
913
|
-
const clientId =
|
|
688
|
+
const clientId = normalizeOptionalString2(options.clientId);
|
|
914
689
|
if (!clientId) {
|
|
915
690
|
throw new Error("Client ID is required for TURN APP mode.");
|
|
916
691
|
}
|
|
917
|
-
const clientSecret =
|
|
692
|
+
const clientSecret = normalizeOptionalString2(options.clientSecret);
|
|
918
693
|
if (!clientSecret) {
|
|
919
694
|
throw new Error("Client secret is required for TURN APP mode.");
|
|
920
695
|
}
|
|
@@ -926,7 +701,7 @@ async function createAPPTURNCredentials(options) {
|
|
|
926
701
|
format: "der",
|
|
927
702
|
type: "pkcs8"
|
|
928
703
|
});
|
|
929
|
-
const serverPublicKeyHex =
|
|
704
|
+
const serverPublicKeyHex = normalizeOptionalString2(options.serverPublicKeyHex) ?? await loadTURNServerPublicKeyHex(clusterDomain, options);
|
|
930
705
|
const serverPublicKey = crypto.createPublicKey({
|
|
931
706
|
key: Buffer.from(serverPublicKeyHex, "hex"),
|
|
932
707
|
format: "der",
|
|
@@ -956,7 +731,7 @@ async function createAPPTURNCredentials(options) {
|
|
|
956
731
|
async function createTURNCredentials(options) {
|
|
957
732
|
const credentials = options.credentials;
|
|
958
733
|
const mode = resolveMode(credentials, options.mode);
|
|
959
|
-
const projectEndpoint =
|
|
734
|
+
const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
|
|
960
735
|
if (mode === "api") {
|
|
961
736
|
const controlPlaneCredentials = options.controlPlaneCredentials ?? credentials;
|
|
962
737
|
if (!controlPlaneCredentials) {
|
|
@@ -987,6 +762,10 @@ async function createTURNCredentials(options) {
|
|
|
987
762
|
"Project endpoint is required for TURN PAT mode."
|
|
988
763
|
),
|
|
989
764
|
token: credentials.token,
|
|
765
|
+
tokenEndpoint: requireOption(
|
|
766
|
+
normalizeOptionalString2(options.tokenEndpoint),
|
|
767
|
+
"Token endpoint is required for TURN PAT mode."
|
|
768
|
+
),
|
|
990
769
|
ttlSeconds: options.ttlSeconds,
|
|
991
770
|
turnPort: options.turnPort,
|
|
992
771
|
turnsPort: options.turnsPort
|
|
@@ -1036,6 +815,7 @@ var RstreamTURNResource = class {
|
|
|
1036
815
|
projectEndpoint: options.projectEndpoint ?? this.client.projectEndpoint,
|
|
1037
816
|
projectId: options.projectId,
|
|
1038
817
|
serverPublicKeyHex: options.serverPublicKeyHex,
|
|
818
|
+
tokenEndpoint: options.tokenEndpoint,
|
|
1039
819
|
ttlSeconds: options.ttlSeconds,
|
|
1040
820
|
turnPort: options.turnPort,
|
|
1041
821
|
turnsPort: options.turnsPort
|
|
@@ -1045,6 +825,41 @@ var RstreamTURNResource = class {
|
|
|
1045
825
|
|
|
1046
826
|
// src/webhooks-resource.ts
|
|
1047
827
|
import crypto2 from "crypto";
|
|
828
|
+
var SIGNATURE_HEX_BYTES = 32;
|
|
829
|
+
var signatureHexSchema = /^[0-9a-fA-F]{64}$/;
|
|
830
|
+
var timestampSchema = /^\d+$/;
|
|
831
|
+
function parseSignatureTimestamp(timestamp) {
|
|
832
|
+
if (!timestampSchema.test(timestamp)) {
|
|
833
|
+
throw new Error("Invalid signature timestamp");
|
|
834
|
+
}
|
|
835
|
+
const parsed = Number(timestamp);
|
|
836
|
+
if (!Number.isSafeInteger(parsed)) {
|
|
837
|
+
throw new Error("Invalid signature timestamp");
|
|
838
|
+
}
|
|
839
|
+
return parsed;
|
|
840
|
+
}
|
|
841
|
+
function normalizeTolerance(tolerance) {
|
|
842
|
+
if (!Number.isFinite(tolerance) || !Number.isInteger(tolerance) || tolerance < 0) {
|
|
843
|
+
throw new Error("Invalid signature tolerance");
|
|
844
|
+
}
|
|
845
|
+
return tolerance;
|
|
846
|
+
}
|
|
847
|
+
function readSignature(signature) {
|
|
848
|
+
if (!signatureHexSchema.test(signature)) {
|
|
849
|
+
return void 0;
|
|
850
|
+
}
|
|
851
|
+
const parsed = Buffer.from(signature, "hex");
|
|
852
|
+
return parsed.length === SIGNATURE_HEX_BYTES ? parsed : void 0;
|
|
853
|
+
}
|
|
854
|
+
function signatureMatches(signature, expected) {
|
|
855
|
+
const parsed = readSignature(signature);
|
|
856
|
+
return parsed === void 0 ? false : crypto2.timingSafeEqual(parsed, expected);
|
|
857
|
+
}
|
|
858
|
+
function assertSecretConfigured(secret) {
|
|
859
|
+
if (secret.trim().length === 0) {
|
|
860
|
+
throw new Error("Webhook signing secret is required");
|
|
861
|
+
}
|
|
862
|
+
}
|
|
1048
863
|
var RstreamWebhookResource = class {
|
|
1049
864
|
async event(payload, header, secret, tolerance = 300, receivedAt = Date.now()) {
|
|
1050
865
|
const payloadBuffer = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
|
|
@@ -1052,31 +867,26 @@ var RstreamWebhookResource = class {
|
|
|
1052
867
|
if (!signatureHeader) {
|
|
1053
868
|
throw new Error("No signature header");
|
|
1054
869
|
}
|
|
870
|
+
assertSecretConfigured(secret);
|
|
1055
871
|
const elements = signatureHeader.split(",").map((element) => element.trim());
|
|
1056
872
|
const timestamp = elements.find((element) => element.startsWith("t="))?.substring(2);
|
|
1057
873
|
const signatures = elements.filter((element) => element.startsWith("v1=")).map((element) => element.substring(3));
|
|
1058
874
|
if (!timestamp || signatures.length === 0) {
|
|
1059
875
|
throw new Error("Invalid signature header format");
|
|
1060
876
|
}
|
|
1061
|
-
const timestampNum =
|
|
877
|
+
const timestampNum = parseSignatureTimestamp(timestamp);
|
|
878
|
+
const signatureTolerance = normalizeTolerance(tolerance);
|
|
1062
879
|
const now = Math.floor(receivedAt / 1e3);
|
|
1063
|
-
if (Math.abs(now - timestampNum) >
|
|
880
|
+
if (Math.abs(now - timestampNum) > signatureTolerance) {
|
|
1064
881
|
throw new Error(
|
|
1065
882
|
`Webhook signature timestamp outside tolerance: ${now}, ${timestampNum}`
|
|
1066
883
|
);
|
|
1067
884
|
}
|
|
1068
885
|
const signedPayload = `${timestamp}.${payloadBuffer.toString("utf8")}`;
|
|
1069
|
-
const expectedSignature = crypto2.createHmac("sha256", secret).update(signedPayload).digest(
|
|
1070
|
-
const signatureMatched = signatures.some(
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
Buffer.from(signature),
|
|
1074
|
-
Buffer.from(expectedSignature)
|
|
1075
|
-
);
|
|
1076
|
-
} catch {
|
|
1077
|
-
return false;
|
|
1078
|
-
}
|
|
1079
|
-
});
|
|
886
|
+
const expectedSignature = crypto2.createHmac("sha256", secret).update(signedPayload).digest();
|
|
887
|
+
const signatureMatched = signatures.some(
|
|
888
|
+
(signature) => signatureMatches(signature, expectedSignature)
|
|
889
|
+
);
|
|
1080
890
|
if (!signatureMatched) {
|
|
1081
891
|
throw new Error("Signature verification failed");
|
|
1082
892
|
}
|
|
@@ -1094,8 +904,16 @@ var RstreamWebhookResource = class {
|
|
|
1094
904
|
};
|
|
1095
905
|
|
|
1096
906
|
// src/tunnels.ts
|
|
907
|
+
var engineAuthTokenScopes = {
|
|
908
|
+
tunnels: {
|
|
909
|
+
connect: true,
|
|
910
|
+
create: true,
|
|
911
|
+
list: true
|
|
912
|
+
}
|
|
913
|
+
};
|
|
1097
914
|
var RstreamTunnelsClient = class {
|
|
1098
915
|
config;
|
|
916
|
+
managedProject;
|
|
1099
917
|
constructor(config) {
|
|
1100
918
|
this.config = config;
|
|
1101
919
|
}
|
|
@@ -1115,6 +933,21 @@ var RstreamTunnelsClient = class {
|
|
|
1115
933
|
return this.config?.projectEndpoint;
|
|
1116
934
|
}
|
|
1117
935
|
async getEngine() {
|
|
936
|
+
const projectEndpoint = normalizeOptionalString3(
|
|
937
|
+
this.config?.projectEndpoint
|
|
938
|
+
);
|
|
939
|
+
if (projectEndpoint !== void 0 && normalizeOptionalString3(this.config?.engine) === void 0 && readEnvironment2().engine === void 0) {
|
|
940
|
+
const project = await this.getManagedProject();
|
|
941
|
+
if (project !== void 0) {
|
|
942
|
+
const engine = getTunnelsProjectEngine2(project);
|
|
943
|
+
if (engine) {
|
|
944
|
+
return normalizeEngineAddress(engine);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
throw new Error(
|
|
948
|
+
"Failed to resolve the engine address from the managed tunnels project."
|
|
949
|
+
);
|
|
950
|
+
}
|
|
1118
951
|
return await resolveTunnelsEngine({
|
|
1119
952
|
apiUrl: this.config?.apiUrl,
|
|
1120
953
|
controlPlaneCredentials: this.controlPlaneCredentials,
|
|
@@ -1123,6 +956,57 @@ var RstreamTunnelsClient = class {
|
|
|
1123
956
|
projectEndpoint: this.config?.projectEndpoint
|
|
1124
957
|
});
|
|
1125
958
|
}
|
|
959
|
+
async getManagedProject() {
|
|
960
|
+
const projectEndpoint = normalizeOptionalString3(
|
|
961
|
+
this.config?.projectEndpoint
|
|
962
|
+
);
|
|
963
|
+
if (projectEndpoint === void 0) {
|
|
964
|
+
return void 0;
|
|
965
|
+
}
|
|
966
|
+
if (this.managedProject === void 0) {
|
|
967
|
+
this.managedProject = resolveManagedTunnelsProject({
|
|
968
|
+
apiUrl: this.config?.apiUrl,
|
|
969
|
+
controlPlaneCredentials: this.controlPlaneCredentials,
|
|
970
|
+
credentials: this.credentials,
|
|
971
|
+
projectEndpoint
|
|
972
|
+
}).catch((error) => {
|
|
973
|
+
this.managedProject = void 0;
|
|
974
|
+
throw error;
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
return await this.managedProject;
|
|
978
|
+
}
|
|
979
|
+
async getEngineAuthTokenParams() {
|
|
980
|
+
const target = await this.getEngineAuthTokenTarget();
|
|
981
|
+
return {
|
|
982
|
+
tunnelsGrants: [
|
|
983
|
+
{
|
|
984
|
+
...target,
|
|
985
|
+
scopes: engineAuthTokenScopes
|
|
986
|
+
}
|
|
987
|
+
]
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
async getEngineAuthTokenTarget() {
|
|
991
|
+
const projectId = normalizeOptionalString3(this.config?.projectId);
|
|
992
|
+
const workspaceId = normalizeOptionalString3(this.config?.workspaceId);
|
|
993
|
+
if (projectId !== void 0 && workspaceId !== void 0) {
|
|
994
|
+
throw new Error("Use either projectId or workspaceId, not both.");
|
|
995
|
+
}
|
|
996
|
+
if (projectId !== void 0) {
|
|
997
|
+
return { projects: [projectId] };
|
|
998
|
+
}
|
|
999
|
+
if (workspaceId !== void 0) {
|
|
1000
|
+
return { workspaces: [workspaceId] };
|
|
1001
|
+
}
|
|
1002
|
+
const project = await this.getManagedProject();
|
|
1003
|
+
if (project !== void 0) {
|
|
1004
|
+
return { projects: [project.id] };
|
|
1005
|
+
}
|
|
1006
|
+
throw new Error(
|
|
1007
|
+
"Application credentials require projectId, workspaceId, or projectEndpoint to create scoped engine tokens."
|
|
1008
|
+
);
|
|
1009
|
+
}
|
|
1126
1010
|
get auth() {
|
|
1127
1011
|
return new RstreamAuthResource(this);
|
|
1128
1012
|
}
|
|
@@ -1146,13 +1030,17 @@ var RstreamTunnelsClient = class {
|
|
|
1146
1030
|
if (!isClientCredentials3(credentials)) {
|
|
1147
1031
|
return credentials.token;
|
|
1148
1032
|
}
|
|
1149
|
-
|
|
1033
|
+
const tokenParams = await this.getEngineAuthTokenParams();
|
|
1034
|
+
return (await this.auth.createAuthToken(tokenParams, {
|
|
1150
1035
|
credentials,
|
|
1151
1036
|
engine
|
|
1152
1037
|
})).token;
|
|
1153
1038
|
}
|
|
1154
1039
|
async request(path, options) {
|
|
1155
|
-
const engine = await this.getEngine();
|
|
1040
|
+
const engine = normalizeEngineAddress(await this.getEngine());
|
|
1041
|
+
if (!path.startsWith("/") || path.startsWith("//")) {
|
|
1042
|
+
throw new Error("Engine request path must be a relative absolute path.");
|
|
1043
|
+
}
|
|
1156
1044
|
const url = `https://${engine}/api${path}`;
|
|
1157
1045
|
const headers = new Headers(options?.headers || {});
|
|
1158
1046
|
const token = await this.getToken(engine);
|
|
@@ -1161,7 +1049,8 @@ var RstreamTunnelsClient = class {
|
|
|
1161
1049
|
}
|
|
1162
1050
|
const response = await fetch(url, {
|
|
1163
1051
|
...options,
|
|
1164
|
-
headers
|
|
1052
|
+
headers,
|
|
1053
|
+
redirect: options?.redirect ?? "manual"
|
|
1165
1054
|
});
|
|
1166
1055
|
if (!response.ok) {
|
|
1167
1056
|
const errorText = await response.text();
|
|
@@ -1170,8 +1059,68 @@ var RstreamTunnelsClient = class {
|
|
|
1170
1059
|
return await response.json();
|
|
1171
1060
|
}
|
|
1172
1061
|
};
|
|
1062
|
+
function normalizeOptionalString3(value) {
|
|
1063
|
+
const normalized = value?.trim();
|
|
1064
|
+
return normalized ? normalized : void 0;
|
|
1065
|
+
}
|
|
1173
1066
|
|
|
1174
1067
|
// src/watch.ts
|
|
1068
|
+
import jwt from "jsonwebtoken";
|
|
1069
|
+
var maxWatchTokenLifetimeSeconds = 3600;
|
|
1070
|
+
var watchTokenIssuedAtSkewSeconds = 300;
|
|
1071
|
+
function normalizeTransport(transport) {
|
|
1072
|
+
if (transport === void 0) {
|
|
1073
|
+
return "sse";
|
|
1074
|
+
}
|
|
1075
|
+
if (transport === "sse" || transport === "websocket") {
|
|
1076
|
+
return transport;
|
|
1077
|
+
}
|
|
1078
|
+
throw new Error("Watch: Unsupported transport.");
|
|
1079
|
+
}
|
|
1080
|
+
function validateWatchQueryToken(token) {
|
|
1081
|
+
const parsed = authTokenSchema.safeParse(jwt.decode(token));
|
|
1082
|
+
if (!parsed.success || parsed.data.type === "pat") {
|
|
1083
|
+
throw new Error(
|
|
1084
|
+
"Watch requires a short-lived auth or app token for URL-based authentication."
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
const iat = parsed.data.iat;
|
|
1088
|
+
const exp = parsed.data.exp;
|
|
1089
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1090
|
+
if (typeof iat !== "number" || typeof exp !== "number" || !Number.isInteger(iat) || !Number.isInteger(exp) || exp <= iat || exp <= now || exp > now + maxWatchTokenLifetimeSeconds || iat > now + watchTokenIssuedAtSkewSeconds || exp - iat > maxWatchTokenLifetimeSeconds) {
|
|
1091
|
+
throw new Error(
|
|
1092
|
+
"Watch requires a token with a bounded lifetime of at most 3600 seconds."
|
|
1093
|
+
);
|
|
1094
|
+
}
|
|
1095
|
+
if (!hasWatchOnlyTunnelGrants(parsed.data)) {
|
|
1096
|
+
throw new Error(
|
|
1097
|
+
"Watch requires a fine-grained token limited to tunnel list grants for URL-based authentication."
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
function hasWatchOnlyTunnelGrants(token) {
|
|
1102
|
+
if (!hasWatchOnlyPermissions(token.permissions)) {
|
|
1103
|
+
return false;
|
|
1104
|
+
}
|
|
1105
|
+
if (!Array.isArray(token.tunnelsGrants) || token.tunnelsGrants.length === 0) {
|
|
1106
|
+
return false;
|
|
1107
|
+
}
|
|
1108
|
+
return token.tunnelsGrants.every((grant) => {
|
|
1109
|
+
const tunnels = grant.scopes?.tunnels;
|
|
1110
|
+
if (tunnels === void 0) {
|
|
1111
|
+
return false;
|
|
1112
|
+
}
|
|
1113
|
+
return tunnels.list !== void 0 && tunnels.create === void 0 && tunnels.connect === void 0;
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
function hasWatchOnlyPermissions(permissions) {
|
|
1117
|
+
if (!Array.isArray(permissions)) {
|
|
1118
|
+
return true;
|
|
1119
|
+
}
|
|
1120
|
+
return permissions.every(
|
|
1121
|
+
(permission) => permission === "tunnels.resources.read-only" || permission === "network.resources.read-only"
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1175
1124
|
var Watch = class {
|
|
1176
1125
|
connection = null;
|
|
1177
1126
|
connectionState = "preparing";
|
|
@@ -1186,45 +1135,56 @@ var Watch = class {
|
|
|
1186
1135
|
throw new Error("Watch: Connection already started or closed.");
|
|
1187
1136
|
}
|
|
1188
1137
|
this.connectionState = "connecting";
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
const
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
this.connectionState = "connected";
|
|
1211
|
-
this.events.onConnect?.();
|
|
1212
|
-
};
|
|
1213
|
-
this.connection.onmessage = (msg) => {
|
|
1214
|
-
const parsed = wsEventsSchema.parse(JSON.parse(msg.data));
|
|
1215
|
-
this.events.onEvent?.(parsed);
|
|
1216
|
-
};
|
|
1217
|
-
this.connection.onerror = () => {
|
|
1218
|
-
if (this.connectionState !== "closed") {
|
|
1219
|
-
this.disconnect();
|
|
1138
|
+
try {
|
|
1139
|
+
const token = typeof this.config.auth === "function" ? await this.config.auth() : this.config.auth;
|
|
1140
|
+
validateWatchQueryToken(token);
|
|
1141
|
+
const engine = await resolveTunnelsEngine({
|
|
1142
|
+
apiUrl: this.config.apiUrl,
|
|
1143
|
+
controlPlaneCredentials: this.config.controlPlaneCredentials,
|
|
1144
|
+
engine: this.config.engine,
|
|
1145
|
+
projectEndpoint: this.config.projectEndpoint,
|
|
1146
|
+
token
|
|
1147
|
+
});
|
|
1148
|
+
const transport = normalizeTransport(this.config.transport);
|
|
1149
|
+
const base = `https://${engine}`;
|
|
1150
|
+
if (transport === "sse") {
|
|
1151
|
+
const url = new URL(`/api/sse`, base);
|
|
1152
|
+
url.searchParams.set("rstream.token", token);
|
|
1153
|
+
this.connection = new EventSource(url.toString());
|
|
1154
|
+
} else {
|
|
1155
|
+
const url = new URL(`/api/websocket`, base);
|
|
1156
|
+
url.searchParams.set("rstream.token", token);
|
|
1157
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
1158
|
+
this.connection = new WebSocket(url.toString());
|
|
1220
1159
|
}
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1160
|
+
this.connection.onopen = () => {
|
|
1161
|
+
this.connectionState = "connected";
|
|
1162
|
+
this.events.onConnect?.();
|
|
1163
|
+
};
|
|
1164
|
+
this.connection.onmessage = (msg) => this.handleMessage(msg.data);
|
|
1165
|
+
this.connection.onerror = () => {
|
|
1224
1166
|
if (this.connectionState !== "closed") {
|
|
1225
1167
|
this.disconnect();
|
|
1226
1168
|
}
|
|
1227
1169
|
};
|
|
1170
|
+
if (this.connection instanceof WebSocket) {
|
|
1171
|
+
this.connection.onclose = () => {
|
|
1172
|
+
if (this.connectionState !== "closed") {
|
|
1173
|
+
this.disconnect();
|
|
1174
|
+
}
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
} catch (error) {
|
|
1178
|
+
this.connectionState = "preparing";
|
|
1179
|
+
throw error;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
handleMessage(data) {
|
|
1183
|
+
try {
|
|
1184
|
+
const parsed = wsEventsSchema.parse(JSON.parse(data));
|
|
1185
|
+
this.events.onEvent?.(parsed);
|
|
1186
|
+
} catch {
|
|
1187
|
+
this.disconnect();
|
|
1228
1188
|
}
|
|
1229
1189
|
}
|
|
1230
1190
|
disconnect() {
|
|
@@ -1243,7 +1203,7 @@ var Watch = class {
|
|
|
1243
1203
|
};
|
|
1244
1204
|
|
|
1245
1205
|
// src/webtty.ts
|
|
1246
|
-
import * as
|
|
1206
|
+
import * as z4 from "zod";
|
|
1247
1207
|
var osFamilies = [
|
|
1248
1208
|
"linux",
|
|
1249
1209
|
"macos",
|
|
@@ -1252,25 +1212,25 @@ var osFamilies = [
|
|
|
1252
1212
|
"openbsd",
|
|
1253
1213
|
"freebsd"
|
|
1254
1214
|
];
|
|
1255
|
-
var webttyServerSchema =
|
|
1256
|
-
tunnel_id:
|
|
1257
|
-
host:
|
|
1258
|
-
token_auth:
|
|
1259
|
-
os_family:
|
|
1260
|
-
arch:
|
|
1261
|
-
os_id:
|
|
1215
|
+
var webttyServerSchema = z4.object({
|
|
1216
|
+
tunnel_id: z4.string(),
|
|
1217
|
+
host: z4.string(),
|
|
1218
|
+
token_auth: z4.boolean(),
|
|
1219
|
+
os_family: z4.enum(osFamilies).optional(),
|
|
1220
|
+
arch: z4.string().optional(),
|
|
1221
|
+
os_id: z4.string().optional(),
|
|
1262
1222
|
// /etc/os-release::ID (ubuntu, debian, rocky, etc.)
|
|
1263
|
-
os_version_id:
|
|
1223
|
+
os_version_id: z4.string().optional(),
|
|
1264
1224
|
// /etc/os-release::VERSION_ID (24.04, 22.04, 11, 10.0.19045, ...)
|
|
1265
|
-
os_version_codename:
|
|
1225
|
+
os_version_codename: z4.string().optional(),
|
|
1266
1226
|
// /etc/os-release::VERSION_CODENAME (jammy, noble...)
|
|
1267
|
-
os_pretty_name:
|
|
1227
|
+
os_pretty_name: z4.string().optional(),
|
|
1268
1228
|
// /etc/os-release::PRETTY_NAME
|
|
1269
|
-
kernel_release:
|
|
1229
|
+
kernel_release: z4.string().optional(),
|
|
1270
1230
|
// uname -r
|
|
1271
|
-
hostname:
|
|
1231
|
+
hostname: z4.string().optional(),
|
|
1272
1232
|
// uname -n
|
|
1273
|
-
labels:
|
|
1233
|
+
labels: z4.record(z4.string(), z4.string().optional()).optional()
|
|
1274
1234
|
});
|
|
1275
1235
|
function parser(tunnel) {
|
|
1276
1236
|
if (tunnel.status !== "online") return null;
|
|
@@ -1327,6 +1287,7 @@ export {
|
|
|
1327
1287
|
authTokenScopesSchema,
|
|
1328
1288
|
authTokenTunnelGrantSchema,
|
|
1329
1289
|
authTokenTunnelsScopesSchema,
|
|
1290
|
+
clientFilterSchema,
|
|
1330
1291
|
clientSchema,
|
|
1331
1292
|
createAPITURNCredentials,
|
|
1332
1293
|
createAPPTURNCredentials,
|
|
@@ -1335,14 +1296,18 @@ export {
|
|
|
1335
1296
|
createAuthTokenResponseSchema,
|
|
1336
1297
|
createPATTURNCredentials,
|
|
1337
1298
|
createTURNCredentials,
|
|
1299
|
+
engineOpenApiDocument,
|
|
1300
|
+
engineOpenApiJson,
|
|
1338
1301
|
eventSchema,
|
|
1339
1302
|
formatTunnelHost,
|
|
1340
1303
|
listClientsParamsSchema,
|
|
1341
1304
|
listClientsResponseSchema,
|
|
1342
1305
|
listTunnelsParamsSchema,
|
|
1343
1306
|
listTunnelsResponseSchema,
|
|
1307
|
+
normalizeEngineAddress,
|
|
1344
1308
|
parseWebTTYServers,
|
|
1345
1309
|
resolveControlPlaneCredentials,
|
|
1310
|
+
resolveManagedTunnelsProject,
|
|
1346
1311
|
resolveTunnelsAPIURL,
|
|
1347
1312
|
resolveTunnelsCredentials,
|
|
1348
1313
|
resolveTunnelsEngine,
|
|
@@ -1350,6 +1315,7 @@ export {
|
|
|
1350
1315
|
streamSummarySchema,
|
|
1351
1316
|
streamTLSInfoSchema,
|
|
1352
1317
|
tokenSchema,
|
|
1318
|
+
tunnelFilterSchema,
|
|
1353
1319
|
tunnelSchema,
|
|
1354
1320
|
turnCredentialModeSchema,
|
|
1355
1321
|
webhookEventsSchema,
|