@rstreamlabs/tunnels 1.0.7 → 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-OPJKEDVS.mjs → chunk-7RZ743EF.mjs} +4 -10
- package/dist/index.d.mts +93 -73
- package/dist/index.d.ts +93 -73
- package/dist/index.js +453 -163
- package/dist/index.mjs +447 -153
- package/dist/openapi.d.mts +8 -30
- package/dist/openapi.d.ts +8 -30
- package/dist/openapi.js +4 -10
- package/dist/openapi.mjs +1 -1
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
tunnelSchema,
|
|
17
17
|
webhookEventsSchema,
|
|
18
18
|
wsEventsSchema
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-7RZ743EF.mjs";
|
|
20
20
|
|
|
21
21
|
// src/auth-resource.ts
|
|
22
22
|
import { createClientCredentialsToken } from "@rstreamlabs/rstream";
|
|
@@ -25,9 +25,9 @@ import { createClientCredentialsToken } from "@rstreamlabs/rstream";
|
|
|
25
25
|
import * as z from "zod";
|
|
26
26
|
var StringFilter = z.union([
|
|
27
27
|
z.string(),
|
|
28
|
-
z.object({ exact: z.string() }),
|
|
29
|
-
z.object({ oneof: z.array(z.string()) }),
|
|
30
|
-
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()
|
|
31
31
|
]);
|
|
32
32
|
function transform(field) {
|
|
33
33
|
if (field instanceof z.ZodOptional) {
|
|
@@ -61,7 +61,7 @@ function select(base) {
|
|
|
61
61
|
return [key, z.boolean().optional()];
|
|
62
62
|
});
|
|
63
63
|
const shape = Object.fromEntries(entries);
|
|
64
|
-
return z.object(shape);
|
|
64
|
+
return z.object(shape).strict();
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
// src/auth.ts
|
|
@@ -88,7 +88,7 @@ var authTokenTunnelsScopesSchema = z2.object({
|
|
|
88
88
|
creation_date: true
|
|
89
89
|
})
|
|
90
90
|
).optional()
|
|
91
|
-
})
|
|
91
|
+
}).strict()
|
|
92
92
|
]).optional(),
|
|
93
93
|
// Scopes for connecting to tunnels
|
|
94
94
|
connect: z2.union([
|
|
@@ -100,7 +100,7 @@ var authTokenTunnelsScopesSchema = z2.object({
|
|
|
100
100
|
path: z2.string().optional()
|
|
101
101
|
})
|
|
102
102
|
).optional()
|
|
103
|
-
})
|
|
103
|
+
}).strict()
|
|
104
104
|
]).optional(),
|
|
105
105
|
// Scopes for listing tunnels
|
|
106
106
|
list: z2.union([
|
|
@@ -108,17 +108,32 @@ var authTokenTunnelsScopesSchema = z2.object({
|
|
|
108
108
|
z2.object({
|
|
109
109
|
filters: filters(tunnelSchema).optional(),
|
|
110
110
|
select: select(tunnelSchema).optional()
|
|
111
|
-
})
|
|
111
|
+
}).strict()
|
|
112
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
|
+
}
|
|
113
120
|
});
|
|
114
121
|
var authTokenScopesSchema = z2.object({
|
|
115
122
|
tunnels: authTokenTunnelsScopesSchema.optional()
|
|
116
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
|
+
}
|
|
117
132
|
});
|
|
118
133
|
var authTokenTunnelGrantSchema = z2.object({
|
|
119
134
|
workspaces: z2.array(nonEmptyStringSchema).min(1).optional(),
|
|
120
135
|
projects: z2.array(nonEmptyStringSchema).min(1).optional(),
|
|
121
|
-
scopes: authTokenScopesSchema
|
|
136
|
+
scopes: authTokenScopesSchema
|
|
122
137
|
}).strict().superRefine((grant, ctx) => {
|
|
123
138
|
if (grant.workspaces !== void 0 && grant.projects !== void 0) {
|
|
124
139
|
ctx.addIssue({
|
|
@@ -127,6 +142,13 @@ var authTokenTunnelGrantSchema = z2.object({
|
|
|
127
142
|
path: ["projects"]
|
|
128
143
|
});
|
|
129
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
|
+
}
|
|
130
152
|
});
|
|
131
153
|
var authTokenCommonSchema = {
|
|
132
154
|
aud: z2.union([z2.string(), z2.array(z2.string())]).optional(),
|
|
@@ -162,10 +184,9 @@ var authTokenSchema = z2.union([
|
|
|
162
184
|
]);
|
|
163
185
|
var tokenSchema = authTokenSchema;
|
|
164
186
|
var createAuthTokenParamsSchema = z2.object({
|
|
165
|
-
expires_in: z2.number().default(60),
|
|
166
|
-
// 1 minute
|
|
187
|
+
expires_in: z2.number().int().min(1).max(3600).default(60),
|
|
167
188
|
scopes: authTokenScopesSchema.optional(),
|
|
168
|
-
tunnelsGrants: z2.array(authTokenTunnelGrantSchema).optional(),
|
|
189
|
+
tunnelsGrants: z2.array(authTokenTunnelGrantSchema).min(1).optional(),
|
|
169
190
|
metadata: z2.unknown().optional()
|
|
170
191
|
// Additional metadata
|
|
171
192
|
}).strict().superRefine((params, ctx) => {
|
|
@@ -176,6 +197,13 @@ var createAuthTokenParamsSchema = z2.object({
|
|
|
176
197
|
path: ["tunnelsGrants"]
|
|
177
198
|
});
|
|
178
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
|
+
}
|
|
179
207
|
});
|
|
180
208
|
var createAuthTokenResponseSchema = z2.object({
|
|
181
209
|
token: z2.string()
|
|
@@ -202,12 +230,12 @@ var RstreamAuthResource = class {
|
|
|
202
230
|
}
|
|
203
231
|
};
|
|
204
232
|
function createAuthTokenFromClientCredentials(credentials, params, options) {
|
|
205
|
-
const tokenParams = createAuthTokenParamsSchema.parse(params
|
|
233
|
+
const tokenParams = createAuthTokenParamsSchema.parse(params);
|
|
206
234
|
const payload = {
|
|
207
235
|
metadata: {
|
|
208
236
|
engine: options?.engine
|
|
209
237
|
},
|
|
210
|
-
tunnelsGrants: normalizeAuthTokenTunnelGrants(tokenParams),
|
|
238
|
+
tunnelsGrants: normalizeAuthTokenTunnelGrants(tokenParams, options),
|
|
211
239
|
permissions: null
|
|
212
240
|
};
|
|
213
241
|
const { token } = createClientCredentialsToken(credentials, {
|
|
@@ -216,21 +244,45 @@ function createAuthTokenFromClientCredentials(credentials, params, options) {
|
|
|
216
244
|
});
|
|
217
245
|
return { token };
|
|
218
246
|
}
|
|
219
|
-
function normalizeAuthTokenTunnelGrants(params) {
|
|
220
|
-
if (!params) {
|
|
221
|
-
return void 0;
|
|
222
|
-
}
|
|
247
|
+
function normalizeAuthTokenTunnelGrants(params, options) {
|
|
223
248
|
const tunnelsGrants = params.tunnelsGrants;
|
|
224
249
|
if (tunnelsGrants !== void 0) {
|
|
225
250
|
return [...tunnelsGrants];
|
|
226
251
|
}
|
|
227
252
|
if (params.scopes) {
|
|
228
|
-
return [{ scopes: params.scopes }];
|
|
253
|
+
return [{ ...normalizeTunnelGrantTarget(options), scopes: params.scopes }];
|
|
229
254
|
}
|
|
230
255
|
return void 0;
|
|
231
256
|
}
|
|
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
|
+
}
|
|
232
277
|
|
|
233
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
|
+
}
|
|
234
286
|
var RstreamClientsResource = class {
|
|
235
287
|
client;
|
|
236
288
|
constructor(client) {
|
|
@@ -244,9 +296,12 @@ var RstreamClientsResource = class {
|
|
|
244
296
|
return listClientsResponseSchema.parse(response);
|
|
245
297
|
}
|
|
246
298
|
async get(id) {
|
|
247
|
-
const response = await this.client.request(
|
|
248
|
-
|
|
249
|
-
|
|
299
|
+
const response = await this.client.request(
|
|
300
|
+
`/clients/${clientPathId(id)}`,
|
|
301
|
+
{
|
|
302
|
+
method: "GET"
|
|
303
|
+
}
|
|
304
|
+
);
|
|
250
305
|
return clientSchema.parse(response);
|
|
251
306
|
}
|
|
252
307
|
};
|
|
@@ -257,22 +312,37 @@ import { getTunnelsProjectEngine } from "@rstreamlabs/rstream";
|
|
|
257
312
|
import { isTokenCredentials } from "@rstreamlabs/rstream";
|
|
258
313
|
import { readEnvironment } from "@rstreamlabs/rstream";
|
|
259
314
|
import { RstreamClient } from "@rstreamlabs/rstream";
|
|
260
|
-
import jwt from "jsonwebtoken";
|
|
261
315
|
function trimOptionalString(value) {
|
|
262
316
|
const normalized = value?.trim();
|
|
263
317
|
return normalized ? normalized : void 0;
|
|
264
318
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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;
|
|
268
323
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
+
}
|
|
274
344
|
}
|
|
275
|
-
return
|
|
345
|
+
return `${url.hostname.toLowerCase()}${explicitPort ? `:${explicitPort}` : ""}`;
|
|
276
346
|
}
|
|
277
347
|
function resolveTunnelsAPIURL(apiUrl) {
|
|
278
348
|
return trimOptionalString(apiUrl) ?? readEnvironment().apiUrl ?? DEFAULT_API_URL;
|
|
@@ -306,37 +376,22 @@ function resolveControlPlaneCredentials(controlPlaneCredentials, credentials, au
|
|
|
306
376
|
async function resolveTunnelsEngine(config) {
|
|
307
377
|
const explicitEngine = trimOptionalString(config.engine);
|
|
308
378
|
if (explicitEngine) {
|
|
309
|
-
return explicitEngine;
|
|
379
|
+
return normalizeEngineAddress(explicitEngine);
|
|
310
380
|
}
|
|
311
381
|
const environmentEngine = readEnvironment().engine;
|
|
312
382
|
if (environmentEngine) {
|
|
313
|
-
return environmentEngine;
|
|
383
|
+
return normalizeEngineAddress(environmentEngine);
|
|
314
384
|
}
|
|
315
385
|
const token = trimOptionalString(config.token) ?? (isTokenCredentials(config.credentials) ? config.credentials.token : void 0);
|
|
316
|
-
const tokenEngine = getEngineFromToken(token);
|
|
317
|
-
if (tokenEngine) {
|
|
318
|
-
return tokenEngine;
|
|
319
|
-
}
|
|
320
386
|
const projectEndpoint = trimOptionalString(config.projectEndpoint);
|
|
321
387
|
if (projectEndpoint) {
|
|
322
|
-
const
|
|
323
|
-
config
|
|
324
|
-
config.credentials,
|
|
388
|
+
const project = await resolveManagedTunnelsProject({
|
|
389
|
+
...config,
|
|
325
390
|
token
|
|
326
|
-
);
|
|
327
|
-
if (credentials === void 0) {
|
|
328
|
-
throw new Error(
|
|
329
|
-
"Control-plane credentials are required to resolve a managed project endpoint."
|
|
330
|
-
);
|
|
331
|
-
}
|
|
332
|
-
const controlPlane = new RstreamClient({
|
|
333
|
-
apiUrl: resolveTunnelsAPIURL(config.apiUrl),
|
|
334
|
-
credentials
|
|
335
391
|
});
|
|
336
|
-
const project = await controlPlane.tunnels.projects.resolveByEndpoint(projectEndpoint);
|
|
337
392
|
const engine = getTunnelsProjectEngine(project);
|
|
338
393
|
if (engine) {
|
|
339
|
-
return engine;
|
|
394
|
+
return normalizeEngineAddress(engine);
|
|
340
395
|
}
|
|
341
396
|
throw new Error(
|
|
342
397
|
"Failed to resolve the engine address from the managed tunnels project."
|
|
@@ -346,8 +401,36 @@ async function resolveTunnelsEngine(config) {
|
|
|
346
401
|
"Engine URL is not defined. Provide an engine, use RSTREAM_ENGINE, or configure a managed project endpoint with control-plane access."
|
|
347
402
|
);
|
|
348
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
|
+
}
|
|
349
425
|
|
|
350
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
|
+
}
|
|
351
434
|
var RstreamTunnelsResource = class {
|
|
352
435
|
client;
|
|
353
436
|
constructor(client) {
|
|
@@ -361,15 +444,20 @@ var RstreamTunnelsResource = class {
|
|
|
361
444
|
return listTunnelsResponseSchema.parse(response);
|
|
362
445
|
}
|
|
363
446
|
async get(id) {
|
|
364
|
-
const response = await this.client.request(
|
|
365
|
-
|
|
366
|
-
|
|
447
|
+
const response = await this.client.request(
|
|
448
|
+
`/tunnels/${tunnelPathId(id)}`,
|
|
449
|
+
{
|
|
450
|
+
method: "GET"
|
|
451
|
+
}
|
|
452
|
+
);
|
|
367
453
|
return tunnelSchema.parse(response);
|
|
368
454
|
}
|
|
369
455
|
};
|
|
370
456
|
|
|
371
457
|
// src/tunnels.ts
|
|
458
|
+
import { getTunnelsProjectEngine as getTunnelsProjectEngine2 } from "@rstreamlabs/rstream";
|
|
372
459
|
import { isClientCredentials as isClientCredentials3 } from "@rstreamlabs/rstream";
|
|
460
|
+
import { readEnvironment as readEnvironment2 } from "@rstreamlabs/rstream";
|
|
373
461
|
|
|
374
462
|
// src/turn.ts
|
|
375
463
|
import { isClientCredentials as isClientCredentials2 } from "@rstreamlabs/rstream";
|
|
@@ -380,46 +468,52 @@ import * as z3 from "zod";
|
|
|
380
468
|
import crypto from "crypto";
|
|
381
469
|
var defaultTURNPort = 3478;
|
|
382
470
|
var defaultTURNSPort = 5349;
|
|
383
|
-
var defaultTURNCredentialTTLSeconds =
|
|
471
|
+
var defaultTURNCredentialTTLSeconds = 10 * 60;
|
|
472
|
+
var maxTURNCredentialTTLSeconds = 60 * 60;
|
|
384
473
|
var turnTokenClaimsSchema = z3.object({
|
|
385
|
-
|
|
386
|
-
|
|
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"])
|
|
387
478
|
});
|
|
388
479
|
var turnCredentialModeSchema = z3.enum(["api", "app", "pat"]);
|
|
389
|
-
function
|
|
480
|
+
function normalizeOptionalString2(value) {
|
|
390
481
|
const normalized = value?.trim();
|
|
391
482
|
return normalized ? normalized : void 0;
|
|
392
483
|
}
|
|
393
484
|
function normalizeClusterDomain(clusterDomain) {
|
|
394
|
-
const normalized =
|
|
485
|
+
const normalized = normalizeOptionalString2(clusterDomain);
|
|
395
486
|
return normalized?.toLowerCase();
|
|
396
487
|
}
|
|
397
488
|
function normalizeNow(now) {
|
|
398
|
-
if (now
|
|
399
|
-
return Math.floor(now
|
|
489
|
+
if (now === void 0) {
|
|
490
|
+
return Math.floor(Date.now() / 1e3);
|
|
400
491
|
}
|
|
401
|
-
|
|
402
|
-
|
|
492
|
+
const timestamp = now instanceof Date ? now.getTime() / 1e3 : now;
|
|
493
|
+
if (!Number.isFinite(timestamp)) {
|
|
494
|
+
throw new Error("TURN timestamp must be finite.");
|
|
403
495
|
}
|
|
404
|
-
return Math.floor(
|
|
496
|
+
return Math.floor(timestamp);
|
|
405
497
|
}
|
|
406
498
|
function normalizeTTLSeconds(ttlSeconds) {
|
|
407
|
-
|
|
408
|
-
|
|
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.");
|
|
409
502
|
}
|
|
410
|
-
return
|
|
503
|
+
return ttl;
|
|
411
504
|
}
|
|
412
|
-
function
|
|
413
|
-
|
|
414
|
-
|
|
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.`);
|
|
415
509
|
}
|
|
416
|
-
return
|
|
510
|
+
return normalized;
|
|
511
|
+
}
|
|
512
|
+
function normalizeTURNPort(port) {
|
|
513
|
+
return normalizePort(port, defaultTURNPort, "TURN port");
|
|
417
514
|
}
|
|
418
515
|
function normalizeTURNSPort(port) {
|
|
419
|
-
|
|
420
|
-
return Math.floor(port);
|
|
421
|
-
}
|
|
422
|
-
return defaultTURNSPort;
|
|
516
|
+
return normalizePort(port, defaultTURNSPort, "TURNS port");
|
|
423
517
|
}
|
|
424
518
|
function createTURNURLs(clusterDomain, turnPort, turnsPort) {
|
|
425
519
|
const normalizedTURNPort = normalizeTURNPort(turnPort);
|
|
@@ -437,16 +531,32 @@ function parseTURNTokenClaims(token) {
|
|
|
437
531
|
if (!payloadPart) {
|
|
438
532
|
throw new Error("Invalid token format.");
|
|
439
533
|
}
|
|
440
|
-
|
|
441
|
-
|
|
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);
|
|
442
542
|
if (!parsed.success) {
|
|
443
543
|
throw new Error("Invalid token format.");
|
|
444
544
|
}
|
|
445
545
|
return parsed.data;
|
|
446
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
|
+
}
|
|
447
557
|
function deriveClusterDomainFromEngine(projectEndpoint, engine) {
|
|
448
|
-
const normalizedProjectEndpoint =
|
|
449
|
-
const normalizedEngine =
|
|
558
|
+
const normalizedProjectEndpoint = normalizeOptionalString2(projectEndpoint);
|
|
559
|
+
const normalizedEngine = normalizeOptionalString2(engine);
|
|
450
560
|
if (!normalizedProjectEndpoint || !normalizedEngine) {
|
|
451
561
|
throw new Error(
|
|
452
562
|
"Failed to derive the cluster domain from the engine address."
|
|
@@ -480,17 +590,11 @@ function resolveMode(credentials, mode) {
|
|
|
480
590
|
if (isClientCredentials2(credentials)) {
|
|
481
591
|
return "app";
|
|
482
592
|
}
|
|
483
|
-
if (isTokenCredentials2(credentials)) {
|
|
484
|
-
const claims = parseTURNTokenClaims(credentials.token);
|
|
485
|
-
if (claims.type === "pat" && normalizeOptionalString(claims.token_endpoint)) {
|
|
486
|
-
return "pat";
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
593
|
return "api";
|
|
490
594
|
}
|
|
491
595
|
async function loadTURNServerPublicKeyHex(clusterDomain, options) {
|
|
492
|
-
const configuredKey =
|
|
493
|
-
const keyringBaseURL = configuredKey ??
|
|
596
|
+
const configuredKey = normalizeOptionalString2(options.keyringBaseUrl);
|
|
597
|
+
const keyringBaseURL = configuredKey ?? normalizeOptionalString2(options.apiUrl) ?? "https://rstream.io";
|
|
494
598
|
const url = new URL(
|
|
495
599
|
`/keyrings/turn/${clusterDomain}.spki.der.hex`,
|
|
496
600
|
keyringBaseURL
|
|
@@ -502,8 +606,8 @@ async function loadTURNServerPublicKeyHex(clusterDomain, options) {
|
|
|
502
606
|
return (await response.text()).trim();
|
|
503
607
|
}
|
|
504
608
|
async function createAPITURNCredentials(options) {
|
|
505
|
-
const projectId =
|
|
506
|
-
const projectEndpoint =
|
|
609
|
+
const projectId = normalizeOptionalString2(options.projectId);
|
|
610
|
+
const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
|
|
507
611
|
if (!projectId && !projectEndpoint) {
|
|
508
612
|
throw new Error(
|
|
509
613
|
"Project ID or project endpoint is required for TURN API mode."
|
|
@@ -530,11 +634,11 @@ function createPATTURNCredentials(options) {
|
|
|
530
634
|
if (!clusterDomain) {
|
|
531
635
|
throw new Error("Cluster domain is required for TURN PAT mode.");
|
|
532
636
|
}
|
|
533
|
-
const projectEndpoint =
|
|
637
|
+
const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
|
|
534
638
|
if (!projectEndpoint) {
|
|
535
639
|
throw new Error("Project endpoint is required for TURN PAT mode.");
|
|
536
640
|
}
|
|
537
|
-
const token =
|
|
641
|
+
const token = normalizeOptionalString2(options.token);
|
|
538
642
|
if (!token) {
|
|
539
643
|
throw new Error("Token is required for TURN PAT mode.");
|
|
540
644
|
}
|
|
@@ -542,14 +646,17 @@ function createPATTURNCredentials(options) {
|
|
|
542
646
|
if (claims.type !== "pat") {
|
|
543
647
|
throw new Error("TURN PAT mode requires a PAT token.");
|
|
544
648
|
}
|
|
545
|
-
const tokenEndpoint =
|
|
649
|
+
const tokenEndpoint = normalizeOptionalString2(options.tokenEndpoint);
|
|
546
650
|
if (!tokenEndpoint) {
|
|
547
|
-
throw new Error(
|
|
548
|
-
|
|
549
|
-
|
|
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.");
|
|
550
655
|
}
|
|
656
|
+
const now = normalizeNow(options.now);
|
|
551
657
|
const ttlSeconds = normalizeTTLSeconds(options.ttlSeconds);
|
|
552
|
-
const exp =
|
|
658
|
+
const exp = Math.min(now + ttlSeconds, requirePATExpiration(claims, now));
|
|
659
|
+
const ttl = exp - now;
|
|
553
660
|
const username = `v1:${exp}:pat:${projectEndpoint}:${tokenEndpoint}`;
|
|
554
661
|
const tokenHash = crypto.createHash("sha256").update(token, "utf8").digest();
|
|
555
662
|
const key = Buffer.from(
|
|
@@ -564,7 +671,7 @@ function createPATTURNCredentials(options) {
|
|
|
564
671
|
const credential = crypto.createHmac("sha256", key).update(username, "utf8").digest("base64");
|
|
565
672
|
return turnCredentialsSchema.parse({
|
|
566
673
|
credential,
|
|
567
|
-
ttl
|
|
674
|
+
ttl,
|
|
568
675
|
urls: createTURNURLs(clusterDomain, options.turnPort, options.turnsPort),
|
|
569
676
|
username
|
|
570
677
|
});
|
|
@@ -574,15 +681,15 @@ async function createAPPTURNCredentials(options) {
|
|
|
574
681
|
if (!clusterDomain) {
|
|
575
682
|
throw new Error("Cluster domain is required for TURN APP mode.");
|
|
576
683
|
}
|
|
577
|
-
const projectEndpoint =
|
|
684
|
+
const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
|
|
578
685
|
if (!projectEndpoint) {
|
|
579
686
|
throw new Error("Project endpoint is required for TURN APP mode.");
|
|
580
687
|
}
|
|
581
|
-
const clientId =
|
|
688
|
+
const clientId = normalizeOptionalString2(options.clientId);
|
|
582
689
|
if (!clientId) {
|
|
583
690
|
throw new Error("Client ID is required for TURN APP mode.");
|
|
584
691
|
}
|
|
585
|
-
const clientSecret =
|
|
692
|
+
const clientSecret = normalizeOptionalString2(options.clientSecret);
|
|
586
693
|
if (!clientSecret) {
|
|
587
694
|
throw new Error("Client secret is required for TURN APP mode.");
|
|
588
695
|
}
|
|
@@ -594,7 +701,7 @@ async function createAPPTURNCredentials(options) {
|
|
|
594
701
|
format: "der",
|
|
595
702
|
type: "pkcs8"
|
|
596
703
|
});
|
|
597
|
-
const serverPublicKeyHex =
|
|
704
|
+
const serverPublicKeyHex = normalizeOptionalString2(options.serverPublicKeyHex) ?? await loadTURNServerPublicKeyHex(clusterDomain, options);
|
|
598
705
|
const serverPublicKey = crypto.createPublicKey({
|
|
599
706
|
key: Buffer.from(serverPublicKeyHex, "hex"),
|
|
600
707
|
format: "der",
|
|
@@ -624,7 +731,7 @@ async function createAPPTURNCredentials(options) {
|
|
|
624
731
|
async function createTURNCredentials(options) {
|
|
625
732
|
const credentials = options.credentials;
|
|
626
733
|
const mode = resolveMode(credentials, options.mode);
|
|
627
|
-
const projectEndpoint =
|
|
734
|
+
const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
|
|
628
735
|
if (mode === "api") {
|
|
629
736
|
const controlPlaneCredentials = options.controlPlaneCredentials ?? credentials;
|
|
630
737
|
if (!controlPlaneCredentials) {
|
|
@@ -655,6 +762,10 @@ async function createTURNCredentials(options) {
|
|
|
655
762
|
"Project endpoint is required for TURN PAT mode."
|
|
656
763
|
),
|
|
657
764
|
token: credentials.token,
|
|
765
|
+
tokenEndpoint: requireOption(
|
|
766
|
+
normalizeOptionalString2(options.tokenEndpoint),
|
|
767
|
+
"Token endpoint is required for TURN PAT mode."
|
|
768
|
+
),
|
|
658
769
|
ttlSeconds: options.ttlSeconds,
|
|
659
770
|
turnPort: options.turnPort,
|
|
660
771
|
turnsPort: options.turnsPort
|
|
@@ -704,6 +815,7 @@ var RstreamTURNResource = class {
|
|
|
704
815
|
projectEndpoint: options.projectEndpoint ?? this.client.projectEndpoint,
|
|
705
816
|
projectId: options.projectId,
|
|
706
817
|
serverPublicKeyHex: options.serverPublicKeyHex,
|
|
818
|
+
tokenEndpoint: options.tokenEndpoint,
|
|
707
819
|
ttlSeconds: options.ttlSeconds,
|
|
708
820
|
turnPort: options.turnPort,
|
|
709
821
|
turnsPort: options.turnsPort
|
|
@@ -713,6 +825,41 @@ var RstreamTURNResource = class {
|
|
|
713
825
|
|
|
714
826
|
// src/webhooks-resource.ts
|
|
715
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
|
+
}
|
|
716
863
|
var RstreamWebhookResource = class {
|
|
717
864
|
async event(payload, header, secret, tolerance = 300, receivedAt = Date.now()) {
|
|
718
865
|
const payloadBuffer = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
|
|
@@ -720,31 +867,26 @@ var RstreamWebhookResource = class {
|
|
|
720
867
|
if (!signatureHeader) {
|
|
721
868
|
throw new Error("No signature header");
|
|
722
869
|
}
|
|
870
|
+
assertSecretConfigured(secret);
|
|
723
871
|
const elements = signatureHeader.split(",").map((element) => element.trim());
|
|
724
872
|
const timestamp = elements.find((element) => element.startsWith("t="))?.substring(2);
|
|
725
873
|
const signatures = elements.filter((element) => element.startsWith("v1=")).map((element) => element.substring(3));
|
|
726
874
|
if (!timestamp || signatures.length === 0) {
|
|
727
875
|
throw new Error("Invalid signature header format");
|
|
728
876
|
}
|
|
729
|
-
const timestampNum =
|
|
877
|
+
const timestampNum = parseSignatureTimestamp(timestamp);
|
|
878
|
+
const signatureTolerance = normalizeTolerance(tolerance);
|
|
730
879
|
const now = Math.floor(receivedAt / 1e3);
|
|
731
|
-
if (Math.abs(now - timestampNum) >
|
|
880
|
+
if (Math.abs(now - timestampNum) > signatureTolerance) {
|
|
732
881
|
throw new Error(
|
|
733
882
|
`Webhook signature timestamp outside tolerance: ${now}, ${timestampNum}`
|
|
734
883
|
);
|
|
735
884
|
}
|
|
736
885
|
const signedPayload = `${timestamp}.${payloadBuffer.toString("utf8")}`;
|
|
737
|
-
const expectedSignature = crypto2.createHmac("sha256", secret).update(signedPayload).digest(
|
|
738
|
-
const signatureMatched = signatures.some(
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
Buffer.from(signature),
|
|
742
|
-
Buffer.from(expectedSignature)
|
|
743
|
-
);
|
|
744
|
-
} catch {
|
|
745
|
-
return false;
|
|
746
|
-
}
|
|
747
|
-
});
|
|
886
|
+
const expectedSignature = crypto2.createHmac("sha256", secret).update(signedPayload).digest();
|
|
887
|
+
const signatureMatched = signatures.some(
|
|
888
|
+
(signature) => signatureMatches(signature, expectedSignature)
|
|
889
|
+
);
|
|
748
890
|
if (!signatureMatched) {
|
|
749
891
|
throw new Error("Signature verification failed");
|
|
750
892
|
}
|
|
@@ -762,8 +904,16 @@ var RstreamWebhookResource = class {
|
|
|
762
904
|
};
|
|
763
905
|
|
|
764
906
|
// src/tunnels.ts
|
|
907
|
+
var engineAuthTokenScopes = {
|
|
908
|
+
tunnels: {
|
|
909
|
+
connect: true,
|
|
910
|
+
create: true,
|
|
911
|
+
list: true
|
|
912
|
+
}
|
|
913
|
+
};
|
|
765
914
|
var RstreamTunnelsClient = class {
|
|
766
915
|
config;
|
|
916
|
+
managedProject;
|
|
767
917
|
constructor(config) {
|
|
768
918
|
this.config = config;
|
|
769
919
|
}
|
|
@@ -783,6 +933,21 @@ var RstreamTunnelsClient = class {
|
|
|
783
933
|
return this.config?.projectEndpoint;
|
|
784
934
|
}
|
|
785
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
|
+
}
|
|
786
951
|
return await resolveTunnelsEngine({
|
|
787
952
|
apiUrl: this.config?.apiUrl,
|
|
788
953
|
controlPlaneCredentials: this.controlPlaneCredentials,
|
|
@@ -791,6 +956,57 @@ var RstreamTunnelsClient = class {
|
|
|
791
956
|
projectEndpoint: this.config?.projectEndpoint
|
|
792
957
|
});
|
|
793
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
|
+
}
|
|
794
1010
|
get auth() {
|
|
795
1011
|
return new RstreamAuthResource(this);
|
|
796
1012
|
}
|
|
@@ -814,13 +1030,17 @@ var RstreamTunnelsClient = class {
|
|
|
814
1030
|
if (!isClientCredentials3(credentials)) {
|
|
815
1031
|
return credentials.token;
|
|
816
1032
|
}
|
|
817
|
-
|
|
1033
|
+
const tokenParams = await this.getEngineAuthTokenParams();
|
|
1034
|
+
return (await this.auth.createAuthToken(tokenParams, {
|
|
818
1035
|
credentials,
|
|
819
1036
|
engine
|
|
820
1037
|
})).token;
|
|
821
1038
|
}
|
|
822
1039
|
async request(path, options) {
|
|
823
|
-
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
|
+
}
|
|
824
1044
|
const url = `https://${engine}/api${path}`;
|
|
825
1045
|
const headers = new Headers(options?.headers || {});
|
|
826
1046
|
const token = await this.getToken(engine);
|
|
@@ -829,7 +1049,8 @@ var RstreamTunnelsClient = class {
|
|
|
829
1049
|
}
|
|
830
1050
|
const response = await fetch(url, {
|
|
831
1051
|
...options,
|
|
832
|
-
headers
|
|
1052
|
+
headers,
|
|
1053
|
+
redirect: options?.redirect ?? "manual"
|
|
833
1054
|
});
|
|
834
1055
|
if (!response.ok) {
|
|
835
1056
|
const errorText = await response.text();
|
|
@@ -838,8 +1059,68 @@ var RstreamTunnelsClient = class {
|
|
|
838
1059
|
return await response.json();
|
|
839
1060
|
}
|
|
840
1061
|
};
|
|
1062
|
+
function normalizeOptionalString3(value) {
|
|
1063
|
+
const normalized = value?.trim();
|
|
1064
|
+
return normalized ? normalized : void 0;
|
|
1065
|
+
}
|
|
841
1066
|
|
|
842
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
|
+
}
|
|
843
1124
|
var Watch = class {
|
|
844
1125
|
connection = null;
|
|
845
1126
|
connectionState = "preparing";
|
|
@@ -854,45 +1135,56 @@ var Watch = class {
|
|
|
854
1135
|
throw new Error("Watch: Connection already started or closed.");
|
|
855
1136
|
}
|
|
856
1137
|
this.connectionState = "connecting";
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
const
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
this.connectionState = "connected";
|
|
879
|
-
this.events.onConnect?.();
|
|
880
|
-
};
|
|
881
|
-
this.connection.onmessage = (msg) => {
|
|
882
|
-
const parsed = wsEventsSchema.parse(JSON.parse(msg.data));
|
|
883
|
-
this.events.onEvent?.(parsed);
|
|
884
|
-
};
|
|
885
|
-
this.connection.onerror = () => {
|
|
886
|
-
if (this.connectionState !== "closed") {
|
|
887
|
-
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());
|
|
888
1159
|
}
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
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 = () => {
|
|
892
1166
|
if (this.connectionState !== "closed") {
|
|
893
1167
|
this.disconnect();
|
|
894
1168
|
}
|
|
895
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();
|
|
896
1188
|
}
|
|
897
1189
|
}
|
|
898
1190
|
disconnect() {
|
|
@@ -1012,8 +1304,10 @@ export {
|
|
|
1012
1304
|
listClientsResponseSchema,
|
|
1013
1305
|
listTunnelsParamsSchema,
|
|
1014
1306
|
listTunnelsResponseSchema,
|
|
1307
|
+
normalizeEngineAddress,
|
|
1015
1308
|
parseWebTTYServers,
|
|
1016
1309
|
resolveControlPlaneCredentials,
|
|
1310
|
+
resolveManagedTunnelsProject,
|
|
1017
1311
|
resolveTunnelsAPIURL,
|
|
1018
1312
|
resolveTunnelsCredentials,
|
|
1019
1313
|
resolveTunnelsEngine,
|