@rstreamlabs/tunnels 1.0.7 → 1.0.9
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 +20 -16
- package/dist/{chunk-OPJKEDVS.mjs → chunk-TVDZNVSF.mjs} +7 -14
- package/dist/index.d.mts +224 -2658
- package/dist/index.d.ts +224 -2658
- package/dist/index.js +572 -171
- package/dist/index.mjs +563 -157
- package/dist/openapi.d.mts +8 -30
- package/dist/openapi.d.ts +8 -30
- package/dist/openapi.js +7 -14
- package/dist/openapi.mjs +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -65,8 +65,10 @@ __export(index_exports, {
|
|
|
65
65
|
listClientsResponseSchema: () => listClientsResponseSchema,
|
|
66
66
|
listTunnelsParamsSchema: () => listTunnelsParamsSchema,
|
|
67
67
|
listTunnelsResponseSchema: () => listTunnelsResponseSchema,
|
|
68
|
+
normalizeEngineAddress: () => normalizeEngineAddress,
|
|
68
69
|
parseWebTTYServers: () => parseWebTTYServers,
|
|
69
70
|
resolveControlPlaneCredentials: () => resolveControlPlaneCredentials,
|
|
71
|
+
resolveManagedTunnelsProject: () => resolveManagedTunnelsProject,
|
|
70
72
|
resolveTunnelsAPIURL: () => resolveTunnelsAPIURL,
|
|
71
73
|
resolveTunnelsCredentials: () => resolveTunnelsCredentials,
|
|
72
74
|
resolveTunnelsEngine: () => resolveTunnelsEngine,
|
|
@@ -90,9 +92,9 @@ var import_rstream = require("@rstreamlabs/rstream");
|
|
|
90
92
|
var z = __toESM(require("zod"));
|
|
91
93
|
var StringFilter = z.union([
|
|
92
94
|
z.string(),
|
|
93
|
-
z.object({ exact: z.string() }),
|
|
94
|
-
z.object({ oneof: z.array(z.string()) }),
|
|
95
|
-
z.object({ regex: z.string() })
|
|
95
|
+
z.object({ exact: z.string() }).strict(),
|
|
96
|
+
z.object({ oneof: z.array(z.string()) }).strict(),
|
|
97
|
+
z.object({ regex: z.string() }).strict()
|
|
96
98
|
]);
|
|
97
99
|
function transform(field) {
|
|
98
100
|
if (field instanceof z.ZodOptional) {
|
|
@@ -126,7 +128,7 @@ function select(base) {
|
|
|
126
128
|
return [key, z.boolean().optional()];
|
|
127
129
|
});
|
|
128
130
|
const shape = Object.fromEntries(entries);
|
|
129
|
-
return z.object(shape);
|
|
131
|
+
return z.object(shape).strict();
|
|
130
132
|
}
|
|
131
133
|
|
|
132
134
|
// src/tunnel.ts
|
|
@@ -160,8 +162,7 @@ var tunnelSchema = z2.object({
|
|
|
160
162
|
tls_alpns: z2.array(z2.string()).optional(),
|
|
161
163
|
tls_min_version: z2.string().optional(),
|
|
162
164
|
tls_ciphers: z2.array(z2.string()).optional(),
|
|
163
|
-
|
|
164
|
-
mtls_ca_cert_pem: z2.string().optional(),
|
|
165
|
+
mtls_auth: z2.boolean().optional(),
|
|
165
166
|
http_version: z2.enum(["http/1.1", "h2", "h2c", "h3"]).optional(),
|
|
166
167
|
http_use_tls: z2.boolean().optional(),
|
|
167
168
|
upstream_tls: z2.boolean().optional(),
|
|
@@ -219,7 +220,7 @@ var authTokenTunnelsScopesSchema = z3.object({
|
|
|
219
220
|
creation_date: true
|
|
220
221
|
})
|
|
221
222
|
).optional()
|
|
222
|
-
})
|
|
223
|
+
}).strict()
|
|
223
224
|
]).optional(),
|
|
224
225
|
// Scopes for connecting to tunnels
|
|
225
226
|
connect: z3.union([
|
|
@@ -231,7 +232,7 @@ var authTokenTunnelsScopesSchema = z3.object({
|
|
|
231
232
|
path: z3.string().optional()
|
|
232
233
|
})
|
|
233
234
|
).optional()
|
|
234
|
-
})
|
|
235
|
+
}).strict()
|
|
235
236
|
]).optional(),
|
|
236
237
|
// Scopes for listing tunnels
|
|
237
238
|
list: z3.union([
|
|
@@ -239,14 +240,29 @@ var authTokenTunnelsScopesSchema = z3.object({
|
|
|
239
240
|
z3.object({
|
|
240
241
|
filters: filters(tunnelSchema).optional(),
|
|
241
242
|
select: select(tunnelSchema).optional()
|
|
242
|
-
})
|
|
243
|
+
}).strict()
|
|
243
244
|
]).optional()
|
|
245
|
+
}).strict().superRefine((scopes, ctx) => {
|
|
246
|
+
if (scopes.create === void 0 && scopes.connect === void 0 && scopes.list === void 0) {
|
|
247
|
+
ctx.addIssue({
|
|
248
|
+
code: z3.ZodIssueCode.custom,
|
|
249
|
+
message: "At least one tunnel scope action is required."
|
|
250
|
+
});
|
|
251
|
+
}
|
|
244
252
|
});
|
|
245
253
|
var authTokenScopesSchema = z3.object({
|
|
246
254
|
tunnels: authTokenTunnelsScopesSchema.optional()
|
|
247
255
|
// Scopes related to tunnels
|
|
256
|
+
}).strict().superRefine((scopes, ctx) => {
|
|
257
|
+
if (scopes.tunnels === void 0) {
|
|
258
|
+
ctx.addIssue({
|
|
259
|
+
code: z3.ZodIssueCode.custom,
|
|
260
|
+
message: "Tunnel scopes are required.",
|
|
261
|
+
path: ["tunnels"]
|
|
262
|
+
});
|
|
263
|
+
}
|
|
248
264
|
});
|
|
249
|
-
var
|
|
265
|
+
var authTokenTunnelGrantLeafSchema = z3.object({
|
|
250
266
|
workspaces: z3.array(nonEmptyStringSchema).min(1).optional(),
|
|
251
267
|
projects: z3.array(nonEmptyStringSchema).min(1).optional(),
|
|
252
268
|
scopes: authTokenScopesSchema.optional()
|
|
@@ -258,11 +274,25 @@ var authTokenTunnelGrantSchema = z3.object({
|
|
|
258
274
|
path: ["projects"]
|
|
259
275
|
});
|
|
260
276
|
}
|
|
277
|
+
if (grant.workspaces === void 0 && grant.projects === void 0 && grant.scopes === void 0) {
|
|
278
|
+
ctx.addIssue({
|
|
279
|
+
code: z3.ZodIssueCode.custom,
|
|
280
|
+
message: "A grant must target workspaces, projects, or scopes.",
|
|
281
|
+
path: ["workspaces"]
|
|
282
|
+
});
|
|
283
|
+
}
|
|
261
284
|
});
|
|
285
|
+
var authTokenTunnelGrantSchema = z3.lazy(
|
|
286
|
+
() => z3.union([
|
|
287
|
+
authTokenTunnelGrantLeafSchema,
|
|
288
|
+
z3.object({ AND: z3.array(authTokenTunnelGrantSchema).min(1) }).strict(),
|
|
289
|
+
z3.object({ OR: z3.array(authTokenTunnelGrantSchema).min(1) }).strict()
|
|
290
|
+
])
|
|
291
|
+
);
|
|
262
292
|
var authTokenCommonSchema = {
|
|
263
293
|
aud: z3.union([z3.string(), z3.array(z3.string())]).optional(),
|
|
264
294
|
exp: z3.number().optional(),
|
|
265
|
-
tunnelsGrants:
|
|
295
|
+
tunnelsGrants: authTokenTunnelGrantSchema.optional(),
|
|
266
296
|
iat: z3.number().optional(),
|
|
267
297
|
iss: z3.string().optional(),
|
|
268
298
|
jti: z3.string().optional(),
|
|
@@ -281,6 +311,7 @@ var authTokenSchema = z3.union([
|
|
|
281
311
|
}).strict(),
|
|
282
312
|
z3.object({
|
|
283
313
|
type: z3.literal("pat"),
|
|
314
|
+
tokendpoint: z3.string().optional(),
|
|
284
315
|
token_endpoint: z3.string().optional(),
|
|
285
316
|
...authTokenCommonSchema
|
|
286
317
|
}).strict(),
|
|
@@ -293,10 +324,9 @@ var authTokenSchema = z3.union([
|
|
|
293
324
|
]);
|
|
294
325
|
var tokenSchema = authTokenSchema;
|
|
295
326
|
var createAuthTokenParamsSchema = z3.object({
|
|
296
|
-
expires_in: z3.number().default(60),
|
|
297
|
-
// 1 minute
|
|
327
|
+
expires_in: z3.number().int().min(1).max(3600).default(60),
|
|
298
328
|
scopes: authTokenScopesSchema.optional(),
|
|
299
|
-
tunnelsGrants:
|
|
329
|
+
tunnelsGrants: authTokenTunnelGrantSchema.optional(),
|
|
300
330
|
metadata: z3.unknown().optional()
|
|
301
331
|
// Additional metadata
|
|
302
332
|
}).strict().superRefine((params, ctx) => {
|
|
@@ -307,6 +337,13 @@ var createAuthTokenParamsSchema = z3.object({
|
|
|
307
337
|
path: ["tunnelsGrants"]
|
|
308
338
|
});
|
|
309
339
|
}
|
|
340
|
+
if (params.scopes === void 0 && params.tunnelsGrants === void 0) {
|
|
341
|
+
ctx.addIssue({
|
|
342
|
+
code: z3.ZodIssueCode.custom,
|
|
343
|
+
message: "Explicit scopes or tunnelsGrants are required.",
|
|
344
|
+
path: ["scopes"]
|
|
345
|
+
});
|
|
346
|
+
}
|
|
310
347
|
});
|
|
311
348
|
var createAuthTokenResponseSchema = z3.object({
|
|
312
349
|
token: z3.string()
|
|
@@ -327,18 +364,38 @@ var RstreamAuthResource = class {
|
|
|
327
364
|
);
|
|
328
365
|
}
|
|
329
366
|
const engine = options?.engine ?? await this.client.getEngine().catch(() => void 0);
|
|
367
|
+
const scopedOptions = await this.resolveScopeOptions(params, options);
|
|
330
368
|
return createAuthTokenFromClientCredentials(credentials, params, {
|
|
331
|
-
engine
|
|
369
|
+
engine,
|
|
370
|
+
...scopedOptions
|
|
332
371
|
});
|
|
333
372
|
}
|
|
373
|
+
async resolveScopeOptions(params, options) {
|
|
374
|
+
const tokenParams = createAuthTokenParamsSchema.parse(params);
|
|
375
|
+
if (options?.projectScoped === false) {
|
|
376
|
+
return options ?? {};
|
|
377
|
+
}
|
|
378
|
+
if (options?.projectId !== void 0 || options?.workspaceId !== void 0) {
|
|
379
|
+
return options;
|
|
380
|
+
}
|
|
381
|
+
if (tokenParams.scopes === void 0 && !tunnelGrantsNeedDefaultTarget(tokenParams.tunnelsGrants)) {
|
|
382
|
+
return options ?? {};
|
|
383
|
+
}
|
|
384
|
+
const target = await this.client.getEngineAuthTokenTarget();
|
|
385
|
+
return {
|
|
386
|
+
...options,
|
|
387
|
+
projectId: target.projects?.[0],
|
|
388
|
+
workspaceId: target.workspaces?.[0]
|
|
389
|
+
};
|
|
390
|
+
}
|
|
334
391
|
};
|
|
335
392
|
function createAuthTokenFromClientCredentials(credentials, params, options) {
|
|
336
|
-
const tokenParams = createAuthTokenParamsSchema.parse(params
|
|
393
|
+
const tokenParams = createAuthTokenParamsSchema.parse(params);
|
|
337
394
|
const payload = {
|
|
338
395
|
metadata: {
|
|
339
396
|
engine: options?.engine
|
|
340
397
|
},
|
|
341
|
-
tunnelsGrants: normalizeAuthTokenTunnelGrants(tokenParams),
|
|
398
|
+
tunnelsGrants: normalizeAuthTokenTunnelGrants(tokenParams, options),
|
|
342
399
|
permissions: null
|
|
343
400
|
};
|
|
344
401
|
const { token } = (0, import_rstream.createClientCredentialsToken)(credentials, {
|
|
@@ -347,19 +404,92 @@ function createAuthTokenFromClientCredentials(credentials, params, options) {
|
|
|
347
404
|
});
|
|
348
405
|
return { token };
|
|
349
406
|
}
|
|
350
|
-
function normalizeAuthTokenTunnelGrants(params) {
|
|
351
|
-
if (!params) {
|
|
352
|
-
return void 0;
|
|
353
|
-
}
|
|
407
|
+
function normalizeAuthTokenTunnelGrants(params, options) {
|
|
354
408
|
const tunnelsGrants = params.tunnelsGrants;
|
|
355
409
|
if (tunnelsGrants !== void 0) {
|
|
356
|
-
return
|
|
410
|
+
return applyDefaultTunnelGrantTarget(tunnelsGrants, options);
|
|
357
411
|
}
|
|
358
412
|
if (params.scopes) {
|
|
359
|
-
return
|
|
413
|
+
return { ...normalizeTunnelGrantTarget(options), scopes: params.scopes };
|
|
360
414
|
}
|
|
361
415
|
return void 0;
|
|
362
416
|
}
|
|
417
|
+
function applyDefaultTunnelGrantTarget(grants, options, inheritedTarget = false) {
|
|
418
|
+
if (options?.projectScoped === false) {
|
|
419
|
+
return grants;
|
|
420
|
+
}
|
|
421
|
+
if ("AND" in grants) {
|
|
422
|
+
const branchHasTarget = inheritedTarget || grants.AND.some((grant) => tunnelGrantTreeHasTarget(grant));
|
|
423
|
+
return {
|
|
424
|
+
AND: grants.AND.map(
|
|
425
|
+
(grant) => applyDefaultTunnelGrantTarget(grant, options, branchHasTarget)
|
|
426
|
+
)
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
if ("OR" in grants) {
|
|
430
|
+
return {
|
|
431
|
+
OR: grants.OR.map(
|
|
432
|
+
(grant) => applyDefaultTunnelGrantTarget(grant, options, inheritedTarget)
|
|
433
|
+
)
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
if (grants.scopes === void 0 || tunnelGrantHasTarget(grants) || inheritedTarget) {
|
|
437
|
+
return grants;
|
|
438
|
+
}
|
|
439
|
+
return { ...normalizeTunnelGrantTarget(options), scopes: grants.scopes };
|
|
440
|
+
}
|
|
441
|
+
function tunnelGrantsNeedDefaultTarget(grants, inheritedTarget = false) {
|
|
442
|
+
if (grants === void 0) {
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
if ("AND" in grants) {
|
|
446
|
+
const branchHasTarget = inheritedTarget || grants.AND.some((grant) => tunnelGrantTreeHasTarget(grant));
|
|
447
|
+
return grants.AND.some(
|
|
448
|
+
(grant) => tunnelGrantsNeedDefaultTarget(grant, branchHasTarget)
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
if ("OR" in grants) {
|
|
452
|
+
return grants.OR.some(
|
|
453
|
+
(grant) => tunnelGrantsNeedDefaultTarget(grant, inheritedTarget)
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
return grants.scopes !== void 0 && !tunnelGrantHasTarget(grants) && !inheritedTarget;
|
|
457
|
+
}
|
|
458
|
+
function tunnelGrantHasTarget(grant) {
|
|
459
|
+
return grant.projects !== void 0 || grant.workspaces !== void 0;
|
|
460
|
+
}
|
|
461
|
+
function tunnelGrantTreeHasTarget(grants) {
|
|
462
|
+
if ("AND" in grants) {
|
|
463
|
+
return grants.AND.some((grant) => tunnelGrantTreeHasTarget(grant));
|
|
464
|
+
}
|
|
465
|
+
if ("OR" in grants) {
|
|
466
|
+
return grants.OR.some((grant) => tunnelGrantTreeHasTarget(grant));
|
|
467
|
+
}
|
|
468
|
+
return tunnelGrantHasTarget(grants);
|
|
469
|
+
}
|
|
470
|
+
function normalizeTunnelGrantTarget(options) {
|
|
471
|
+
const projectId = normalizeOptionalString(options?.projectId);
|
|
472
|
+
const workspaceId = normalizeOptionalString(options?.workspaceId);
|
|
473
|
+
if (projectId !== void 0 && workspaceId !== void 0) {
|
|
474
|
+
throw new Error("Use either projectId or workspaceId, not both.");
|
|
475
|
+
}
|
|
476
|
+
if (projectId !== void 0) {
|
|
477
|
+
return { projects: [projectId] };
|
|
478
|
+
}
|
|
479
|
+
if (workspaceId !== void 0) {
|
|
480
|
+
return { workspaces: [workspaceId] };
|
|
481
|
+
}
|
|
482
|
+
if (options?.projectScoped === false) {
|
|
483
|
+
return {};
|
|
484
|
+
}
|
|
485
|
+
throw new Error(
|
|
486
|
+
"Project ID or workspace ID is required when using scope-only tunnel grants."
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
function normalizeOptionalString(value) {
|
|
490
|
+
const normalized = value?.trim();
|
|
491
|
+
return normalized ? normalized : void 0;
|
|
492
|
+
}
|
|
363
493
|
|
|
364
494
|
// src/client.ts
|
|
365
495
|
var z4 = __toESM(require("zod"));
|
|
@@ -400,6 +530,13 @@ var listClientsParamsSchema = z4.object({
|
|
|
400
530
|
var listClientsResponseSchema = z4.array(clientSchema);
|
|
401
531
|
|
|
402
532
|
// src/clients-resource.ts
|
|
533
|
+
function clientPathId(id) {
|
|
534
|
+
const normalized = id.trim();
|
|
535
|
+
if (!normalized) {
|
|
536
|
+
throw new Error("Client ID is required.");
|
|
537
|
+
}
|
|
538
|
+
return encodeURIComponent(normalized);
|
|
539
|
+
}
|
|
403
540
|
var RstreamClientsResource = class {
|
|
404
541
|
client;
|
|
405
542
|
constructor(client) {
|
|
@@ -413,9 +550,12 @@ var RstreamClientsResource = class {
|
|
|
413
550
|
return listClientsResponseSchema.parse(response);
|
|
414
551
|
}
|
|
415
552
|
async get(id) {
|
|
416
|
-
const response = await this.client.request(
|
|
417
|
-
|
|
418
|
-
|
|
553
|
+
const response = await this.client.request(
|
|
554
|
+
`/clients/${clientPathId(id)}`,
|
|
555
|
+
{
|
|
556
|
+
method: "GET"
|
|
557
|
+
}
|
|
558
|
+
);
|
|
419
559
|
return clientSchema.parse(response);
|
|
420
560
|
}
|
|
421
561
|
};
|
|
@@ -563,7 +703,7 @@ var routedTunnelSchema = z5.object({
|
|
|
563
703
|
httpUseTls: z5.boolean().optional(),
|
|
564
704
|
upstreamTls: z5.boolean().optional(),
|
|
565
705
|
tokenAuth: z5.boolean().optional(),
|
|
566
|
-
|
|
706
|
+
mtls_auth: z5.boolean().optional()
|
|
567
707
|
});
|
|
568
708
|
var routingSchema = z5.discriminatedUnion("decision", [
|
|
569
709
|
z5.object({ decision: z5.literal("routed"), tunnel: routedTunnelSchema }),
|
|
@@ -805,7 +945,7 @@ var engineOpenApiDocument = {
|
|
|
805
945
|
info: {
|
|
806
946
|
title: "rstream Engine API",
|
|
807
947
|
version: "1.0.0",
|
|
808
|
-
description: "Operational
|
|
948
|
+
description: "Operational Engine API exposed by rstream-engine. This surface is scoped to the authenticated engine or project endpoint and covers live clients, live tunnels, and state-change streams."
|
|
809
949
|
},
|
|
810
950
|
servers: [
|
|
811
951
|
{
|
|
@@ -831,12 +971,6 @@ var engineOpenApiDocument = {
|
|
|
831
971
|
scheme: "bearer",
|
|
832
972
|
bearerFormat: "rstream token",
|
|
833
973
|
description: "Preferred authentication mechanism. Send an OAuth-issued rstream access token or compatible rstream credential in Authorization: Bearer <token>."
|
|
834
|
-
},
|
|
835
|
-
queryToken: {
|
|
836
|
-
type: "apiKey",
|
|
837
|
-
in: "query",
|
|
838
|
-
name: "rstream.token",
|
|
839
|
-
description: "Fallback authentication mechanism for transports where setting Authorization is impractical. Prefer bearerAuth whenever possible."
|
|
840
974
|
}
|
|
841
975
|
},
|
|
842
976
|
parameters: {
|
|
@@ -956,7 +1090,7 @@ var engineOpenApiDocument = {
|
|
|
956
1090
|
summary: "List visible engine clients.",
|
|
957
1091
|
operationId: "list_engine_clients",
|
|
958
1092
|
parameters: [{ $ref: "#/components/parameters/ListClientsParams" }],
|
|
959
|
-
security: [{ bearerAuth: [] }
|
|
1093
|
+
security: [{ bearerAuth: [] }],
|
|
960
1094
|
responses: {
|
|
961
1095
|
"200": jsonArrayResponse(
|
|
962
1096
|
"Visible clients.",
|
|
@@ -975,7 +1109,7 @@ var engineOpenApiDocument = {
|
|
|
975
1109
|
summary: "List visible engine tunnels.",
|
|
976
1110
|
operationId: "list_engine_tunnels",
|
|
977
1111
|
parameters: [{ $ref: "#/components/parameters/ListTunnelsParams" }],
|
|
978
|
-
security: [{ bearerAuth: [] }
|
|
1112
|
+
security: [{ bearerAuth: [] }],
|
|
979
1113
|
responses: {
|
|
980
1114
|
"200": jsonArrayResponse(
|
|
981
1115
|
"Visible tunnels. Fine-grained list scopes may filter objects and project fields.",
|
|
@@ -995,7 +1129,7 @@ var engineOpenApiDocument = {
|
|
|
995
1129
|
summary: "Subscribe to visible engine state through Server-Sent Events.",
|
|
996
1130
|
operationId: "watch_engine_events_sse",
|
|
997
1131
|
parameters: [{ $ref: "#/components/parameters/WatchParams" }],
|
|
998
|
-
security: [{ bearerAuth: [] }
|
|
1132
|
+
security: [{ bearerAuth: [] }],
|
|
999
1133
|
responses: {
|
|
1000
1134
|
"200": streamResponse(
|
|
1001
1135
|
"SSE stream. Each event is emitted as a data line containing a JSON Event object."
|
|
@@ -1014,7 +1148,7 @@ var engineOpenApiDocument = {
|
|
|
1014
1148
|
summary: "Subscribe to visible engine state through a WebSocket upgrade.",
|
|
1015
1149
|
operationId: "watch_engine_events_websocket",
|
|
1016
1150
|
parameters: [{ $ref: "#/components/parameters/WatchParams" }],
|
|
1017
|
-
security: [{ bearerAuth: [] }
|
|
1151
|
+
security: [{ bearerAuth: [] }],
|
|
1018
1152
|
responses: {
|
|
1019
1153
|
"101": {
|
|
1020
1154
|
description: "WebSocket connection accepted. Frames contain JSON Event objects."
|
|
@@ -1042,22 +1176,37 @@ var import_rstream4 = require("@rstreamlabs/rstream");
|
|
|
1042
1176
|
var import_rstream5 = require("@rstreamlabs/rstream");
|
|
1043
1177
|
var import_rstream6 = require("@rstreamlabs/rstream");
|
|
1044
1178
|
var import_rstream7 = require("@rstreamlabs/rstream");
|
|
1045
|
-
var import_jsonwebtoken = __toESM(require("jsonwebtoken"));
|
|
1046
1179
|
function trimOptionalString(value) {
|
|
1047
1180
|
const normalized = value?.trim();
|
|
1048
1181
|
return normalized ? normalized : void 0;
|
|
1049
1182
|
}
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1183
|
+
var dnsLabelPattern = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i;
|
|
1184
|
+
function validateHostname(hostname) {
|
|
1185
|
+
if (!hostname || hostname.length > 253 || hostname.includes("..")) {
|
|
1186
|
+
return false;
|
|
1053
1187
|
}
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1188
|
+
return hostname.split(".").every((label) => label === "localhost" || dnsLabelPattern.test(label));
|
|
1189
|
+
}
|
|
1190
|
+
function normalizeEngineAddress(engine) {
|
|
1191
|
+
const normalized = engine.trim();
|
|
1192
|
+
if (!normalized || /^[a-z][a-z0-9+.-]*:\/\//i.test(normalized) || /[/?#@\\\s]/.test(normalized)) {
|
|
1193
|
+
throw new Error("Engine must be a host[:port] value.");
|
|
1194
|
+
}
|
|
1195
|
+
const explicitPort = normalized.match(/:(\d+)$/)?.[1];
|
|
1196
|
+
const url = new URL(`https://${normalized}`);
|
|
1197
|
+
if (url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
|
|
1198
|
+
throw new Error("Engine must be a host[:port] value.");
|
|
1199
|
+
}
|
|
1200
|
+
if (!validateHostname(url.hostname)) {
|
|
1201
|
+
throw new Error("Engine must use a valid hostname.");
|
|
1202
|
+
}
|
|
1203
|
+
if (explicitPort) {
|
|
1204
|
+
const port = Number(explicitPort);
|
|
1205
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
1206
|
+
throw new Error("Engine port must be an integer between 1 and 65535.");
|
|
1207
|
+
}
|
|
1059
1208
|
}
|
|
1060
|
-
return
|
|
1209
|
+
return `${url.hostname.toLowerCase()}${explicitPort ? `:${explicitPort}` : ""}`;
|
|
1061
1210
|
}
|
|
1062
1211
|
function resolveTunnelsAPIURL(apiUrl) {
|
|
1063
1212
|
return trimOptionalString(apiUrl) ?? (0, import_rstream6.readEnvironment)().apiUrl ?? import_rstream3.DEFAULT_API_URL;
|
|
@@ -1091,48 +1240,61 @@ function resolveControlPlaneCredentials(controlPlaneCredentials, credentials, au
|
|
|
1091
1240
|
async function resolveTunnelsEngine(config) {
|
|
1092
1241
|
const explicitEngine = trimOptionalString(config.engine);
|
|
1093
1242
|
if (explicitEngine) {
|
|
1094
|
-
return explicitEngine;
|
|
1243
|
+
return normalizeEngineAddress(explicitEngine);
|
|
1095
1244
|
}
|
|
1096
1245
|
const environmentEngine = (0, import_rstream6.readEnvironment)().engine;
|
|
1097
1246
|
if (environmentEngine) {
|
|
1098
|
-
return environmentEngine;
|
|
1247
|
+
return normalizeEngineAddress(environmentEngine);
|
|
1099
1248
|
}
|
|
1100
1249
|
const token = trimOptionalString(config.token) ?? ((0, import_rstream5.isTokenCredentials)(config.credentials) ? config.credentials.token : void 0);
|
|
1101
|
-
const tokenEngine = getEngineFromToken(token);
|
|
1102
|
-
if (tokenEngine) {
|
|
1103
|
-
return tokenEngine;
|
|
1104
|
-
}
|
|
1105
1250
|
const projectEndpoint = trimOptionalString(config.projectEndpoint);
|
|
1106
1251
|
if (projectEndpoint) {
|
|
1107
|
-
const
|
|
1108
|
-
config
|
|
1109
|
-
config.credentials,
|
|
1252
|
+
const project = await resolveManagedTunnelsProject({
|
|
1253
|
+
...config,
|
|
1110
1254
|
token
|
|
1111
|
-
);
|
|
1112
|
-
if (credentials === void 0) {
|
|
1113
|
-
throw new Error(
|
|
1114
|
-
"Control-plane credentials are required to resolve a managed project endpoint."
|
|
1115
|
-
);
|
|
1116
|
-
}
|
|
1117
|
-
const controlPlane = new import_rstream7.RstreamClient({
|
|
1118
|
-
apiUrl: resolveTunnelsAPIURL(config.apiUrl),
|
|
1119
|
-
credentials
|
|
1120
1255
|
});
|
|
1121
|
-
const project = await controlPlane.tunnels.projects.resolveByEndpoint(projectEndpoint);
|
|
1122
1256
|
const engine = (0, import_rstream4.getTunnelsProjectEngine)(project);
|
|
1123
1257
|
if (engine) {
|
|
1124
|
-
return engine;
|
|
1258
|
+
return normalizeEngineAddress(engine);
|
|
1125
1259
|
}
|
|
1126
1260
|
throw new Error(
|
|
1127
1261
|
"Failed to resolve the engine address from the managed tunnels project."
|
|
1128
1262
|
);
|
|
1129
1263
|
}
|
|
1130
1264
|
throw new Error(
|
|
1131
|
-
"Engine URL is not defined. Provide an engine, use RSTREAM_ENGINE, or configure a managed project endpoint with
|
|
1265
|
+
"Engine URL is not defined. Provide an engine, use RSTREAM_ENGINE, or configure a managed project endpoint with Control plane API access."
|
|
1132
1266
|
);
|
|
1133
1267
|
}
|
|
1268
|
+
async function resolveManagedTunnelsProject(config) {
|
|
1269
|
+
const projectEndpoint = trimOptionalString(config.projectEndpoint);
|
|
1270
|
+
if (!projectEndpoint) {
|
|
1271
|
+
throw new Error("Managed project endpoint is required.");
|
|
1272
|
+
}
|
|
1273
|
+
const credentials = resolveControlPlaneCredentials(
|
|
1274
|
+
config.controlPlaneCredentials,
|
|
1275
|
+
config.credentials,
|
|
1276
|
+
config.token
|
|
1277
|
+
);
|
|
1278
|
+
if (credentials === void 0) {
|
|
1279
|
+
throw new Error(
|
|
1280
|
+
"Control-plane credentials are required to resolve a managed project endpoint."
|
|
1281
|
+
);
|
|
1282
|
+
}
|
|
1283
|
+
const controlPlane = new import_rstream7.RstreamClient({
|
|
1284
|
+
apiUrl: resolveTunnelsAPIURL(config.apiUrl),
|
|
1285
|
+
credentials
|
|
1286
|
+
});
|
|
1287
|
+
return await controlPlane.tunnels.projects.resolveByEndpoint(projectEndpoint);
|
|
1288
|
+
}
|
|
1134
1289
|
|
|
1135
1290
|
// src/tunnels-resource.ts
|
|
1291
|
+
function tunnelPathId(id) {
|
|
1292
|
+
const normalized = id.trim();
|
|
1293
|
+
if (!normalized) {
|
|
1294
|
+
throw new Error("Tunnel ID is required.");
|
|
1295
|
+
}
|
|
1296
|
+
return encodeURIComponent(normalized);
|
|
1297
|
+
}
|
|
1136
1298
|
var RstreamTunnelsResource = class {
|
|
1137
1299
|
client;
|
|
1138
1300
|
constructor(client) {
|
|
@@ -1146,15 +1308,20 @@ var RstreamTunnelsResource = class {
|
|
|
1146
1308
|
return listTunnelsResponseSchema.parse(response);
|
|
1147
1309
|
}
|
|
1148
1310
|
async get(id) {
|
|
1149
|
-
const response = await this.client.request(
|
|
1150
|
-
|
|
1151
|
-
|
|
1311
|
+
const response = await this.client.request(
|
|
1312
|
+
`/tunnels/${tunnelPathId(id)}`,
|
|
1313
|
+
{
|
|
1314
|
+
method: "GET"
|
|
1315
|
+
}
|
|
1316
|
+
);
|
|
1152
1317
|
return tunnelSchema.parse(response);
|
|
1153
1318
|
}
|
|
1154
1319
|
};
|
|
1155
1320
|
|
|
1156
1321
|
// src/tunnels.ts
|
|
1157
1322
|
var import_rstream12 = require("@rstreamlabs/rstream");
|
|
1323
|
+
var import_rstream13 = require("@rstreamlabs/rstream");
|
|
1324
|
+
var import_rstream14 = require("@rstreamlabs/rstream");
|
|
1158
1325
|
|
|
1159
1326
|
// src/turn.ts
|
|
1160
1327
|
var import_rstream8 = require("@rstreamlabs/rstream");
|
|
@@ -1165,46 +1332,53 @@ var z8 = __toESM(require("zod"));
|
|
|
1165
1332
|
var import_crypto = __toESM(require("crypto"));
|
|
1166
1333
|
var defaultTURNPort = 3478;
|
|
1167
1334
|
var defaultTURNSPort = 5349;
|
|
1168
|
-
var defaultTURNCredentialTTLSeconds =
|
|
1335
|
+
var defaultTURNCredentialTTLSeconds = 10 * 60;
|
|
1336
|
+
var maxTURNCredentialTTLSeconds = 60 * 60;
|
|
1169
1337
|
var turnTokenClaimsSchema = z8.object({
|
|
1170
|
-
|
|
1171
|
-
|
|
1338
|
+
exp: z8.number().int().optional(),
|
|
1339
|
+
iat: z8.number().int().optional(),
|
|
1340
|
+
token_endpoint: z8.string().regex(/^[0-9a-f]{8}$/i).optional(),
|
|
1341
|
+
tokendpoint: z8.string().regex(/^[0-9a-f]{8}$/i).optional(),
|
|
1342
|
+
type: z8.enum(["app", "auth", "pat"])
|
|
1172
1343
|
});
|
|
1173
1344
|
var turnCredentialModeSchema = z8.enum(["api", "app", "pat"]);
|
|
1174
|
-
function
|
|
1345
|
+
function normalizeOptionalString2(value) {
|
|
1175
1346
|
const normalized = value?.trim();
|
|
1176
1347
|
return normalized ? normalized : void 0;
|
|
1177
1348
|
}
|
|
1178
1349
|
function normalizeClusterDomain(clusterDomain) {
|
|
1179
|
-
const normalized =
|
|
1350
|
+
const normalized = normalizeOptionalString2(clusterDomain);
|
|
1180
1351
|
return normalized?.toLowerCase();
|
|
1181
1352
|
}
|
|
1182
1353
|
function normalizeNow(now) {
|
|
1183
|
-
if (now
|
|
1184
|
-
return Math.floor(now
|
|
1354
|
+
if (now === void 0) {
|
|
1355
|
+
return Math.floor(Date.now() / 1e3);
|
|
1185
1356
|
}
|
|
1186
|
-
|
|
1187
|
-
|
|
1357
|
+
const timestamp = now instanceof Date ? now.getTime() / 1e3 : now;
|
|
1358
|
+
if (!Number.isFinite(timestamp)) {
|
|
1359
|
+
throw new Error("TURN timestamp must be finite.");
|
|
1188
1360
|
}
|
|
1189
|
-
return Math.floor(
|
|
1361
|
+
return Math.floor(timestamp);
|
|
1190
1362
|
}
|
|
1191
1363
|
function normalizeTTLSeconds(ttlSeconds) {
|
|
1192
|
-
|
|
1193
|
-
|
|
1364
|
+
const ttl = ttlSeconds ?? defaultTURNCredentialTTLSeconds;
|
|
1365
|
+
if (!Number.isInteger(ttl) || ttl < 1 || ttl > maxTURNCredentialTTLSeconds) {
|
|
1366
|
+
throw new Error("TURN TTL must be an integer between 1 and 3600 seconds.");
|
|
1194
1367
|
}
|
|
1195
|
-
return
|
|
1368
|
+
return ttl;
|
|
1196
1369
|
}
|
|
1197
|
-
function
|
|
1198
|
-
|
|
1199
|
-
|
|
1370
|
+
function normalizePort(port, fallback, name) {
|
|
1371
|
+
const normalized = port ?? fallback;
|
|
1372
|
+
if (!Number.isInteger(normalized) || normalized < 1 || normalized > 65535) {
|
|
1373
|
+
throw new Error(`${name} must be an integer between 1 and 65535.`);
|
|
1200
1374
|
}
|
|
1201
|
-
return
|
|
1375
|
+
return normalized;
|
|
1376
|
+
}
|
|
1377
|
+
function normalizeTURNPort(port) {
|
|
1378
|
+
return normalizePort(port, defaultTURNPort, "TURN port");
|
|
1202
1379
|
}
|
|
1203
1380
|
function normalizeTURNSPort(port) {
|
|
1204
|
-
|
|
1205
|
-
return Math.floor(port);
|
|
1206
|
-
}
|
|
1207
|
-
return defaultTURNSPort;
|
|
1381
|
+
return normalizePort(port, defaultTURNSPort, "TURNS port");
|
|
1208
1382
|
}
|
|
1209
1383
|
function createTURNURLs(clusterDomain, turnPort, turnsPort) {
|
|
1210
1384
|
const normalizedTURNPort = normalizeTURNPort(turnPort);
|
|
@@ -1222,16 +1396,32 @@ function parseTURNTokenClaims(token) {
|
|
|
1222
1396
|
if (!payloadPart) {
|
|
1223
1397
|
throw new Error("Invalid token format.");
|
|
1224
1398
|
}
|
|
1225
|
-
|
|
1226
|
-
|
|
1399
|
+
let decoded;
|
|
1400
|
+
try {
|
|
1401
|
+
const payload = Buffer.from(payloadPart, "base64url").toString("utf8");
|
|
1402
|
+
decoded = JSON.parse(payload);
|
|
1403
|
+
} catch {
|
|
1404
|
+
throw new Error("Invalid token format.");
|
|
1405
|
+
}
|
|
1406
|
+
const parsed = turnTokenClaimsSchema.safeParse(decoded);
|
|
1227
1407
|
if (!parsed.success) {
|
|
1228
1408
|
throw new Error("Invalid token format.");
|
|
1229
1409
|
}
|
|
1230
1410
|
return parsed.data;
|
|
1231
1411
|
}
|
|
1412
|
+
function requirePATExpiration(claims, now) {
|
|
1413
|
+
const exp = claims.exp;
|
|
1414
|
+
if (typeof exp !== "number") {
|
|
1415
|
+
throw new Error("TURN PAT mode requires a PAT token with expiration.");
|
|
1416
|
+
}
|
|
1417
|
+
if (exp <= now) {
|
|
1418
|
+
throw new Error("TURN PAT mode requires a non-expired PAT token.");
|
|
1419
|
+
}
|
|
1420
|
+
return exp;
|
|
1421
|
+
}
|
|
1232
1422
|
function deriveClusterDomainFromEngine(projectEndpoint, engine) {
|
|
1233
|
-
const normalizedProjectEndpoint =
|
|
1234
|
-
const normalizedEngine =
|
|
1423
|
+
const normalizedProjectEndpoint = normalizeOptionalString2(projectEndpoint);
|
|
1424
|
+
const normalizedEngine = normalizeOptionalString2(engine);
|
|
1235
1425
|
if (!normalizedProjectEndpoint || !normalizedEngine) {
|
|
1236
1426
|
throw new Error(
|
|
1237
1427
|
"Failed to derive the cluster domain from the engine address."
|
|
@@ -1265,17 +1455,11 @@ function resolveMode(credentials, mode) {
|
|
|
1265
1455
|
if ((0, import_rstream8.isClientCredentials)(credentials)) {
|
|
1266
1456
|
return "app";
|
|
1267
1457
|
}
|
|
1268
|
-
if ((0, import_rstream9.isTokenCredentials)(credentials)) {
|
|
1269
|
-
const claims = parseTURNTokenClaims(credentials.token);
|
|
1270
|
-
if (claims.type === "pat" && normalizeOptionalString(claims.token_endpoint)) {
|
|
1271
|
-
return "pat";
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
1458
|
return "api";
|
|
1275
1459
|
}
|
|
1276
1460
|
async function loadTURNServerPublicKeyHex(clusterDomain, options) {
|
|
1277
|
-
const configuredKey =
|
|
1278
|
-
const keyringBaseURL = configuredKey ??
|
|
1461
|
+
const configuredKey = normalizeOptionalString2(options.keyringBaseUrl);
|
|
1462
|
+
const keyringBaseURL = configuredKey ?? normalizeOptionalString2(options.apiUrl) ?? "https://rstream.io";
|
|
1279
1463
|
const url = new URL(
|
|
1280
1464
|
`/keyrings/turn/${clusterDomain}.spki.der.hex`,
|
|
1281
1465
|
keyringBaseURL
|
|
@@ -1287,8 +1471,8 @@ async function loadTURNServerPublicKeyHex(clusterDomain, options) {
|
|
|
1287
1471
|
return (await response.text()).trim();
|
|
1288
1472
|
}
|
|
1289
1473
|
async function createAPITURNCredentials(options) {
|
|
1290
|
-
const projectId =
|
|
1291
|
-
const projectEndpoint =
|
|
1474
|
+
const projectId = normalizeOptionalString2(options.projectId);
|
|
1475
|
+
const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
|
|
1292
1476
|
if (!projectId && !projectEndpoint) {
|
|
1293
1477
|
throw new Error(
|
|
1294
1478
|
"Project ID or project endpoint is required for TURN API mode."
|
|
@@ -1315,11 +1499,11 @@ function createPATTURNCredentials(options) {
|
|
|
1315
1499
|
if (!clusterDomain) {
|
|
1316
1500
|
throw new Error("Cluster domain is required for TURN PAT mode.");
|
|
1317
1501
|
}
|
|
1318
|
-
const projectEndpoint =
|
|
1502
|
+
const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
|
|
1319
1503
|
if (!projectEndpoint) {
|
|
1320
1504
|
throw new Error("Project endpoint is required for TURN PAT mode.");
|
|
1321
1505
|
}
|
|
1322
|
-
const token =
|
|
1506
|
+
const token = normalizeOptionalString2(options.token);
|
|
1323
1507
|
if (!token) {
|
|
1324
1508
|
throw new Error("Token is required for TURN PAT mode.");
|
|
1325
1509
|
}
|
|
@@ -1327,14 +1511,18 @@ function createPATTURNCredentials(options) {
|
|
|
1327
1511
|
if (claims.type !== "pat") {
|
|
1328
1512
|
throw new Error("TURN PAT mode requires a PAT token.");
|
|
1329
1513
|
}
|
|
1330
|
-
const tokenEndpoint =
|
|
1514
|
+
const tokenEndpoint = normalizeOptionalString2(options.tokenEndpoint);
|
|
1331
1515
|
if (!tokenEndpoint) {
|
|
1332
|
-
throw new Error(
|
|
1333
|
-
|
|
1334
|
-
|
|
1516
|
+
throw new Error("Token endpoint is required for TURN PAT mode.");
|
|
1517
|
+
}
|
|
1518
|
+
const claimsTokenEndpoint = claims.token_endpoint ?? claims.tokendpoint;
|
|
1519
|
+
if (claimsTokenEndpoint !== void 0 && claimsTokenEndpoint !== tokenEndpoint) {
|
|
1520
|
+
throw new Error("Token endpoint does not match the PAT token.");
|
|
1335
1521
|
}
|
|
1522
|
+
const now = normalizeNow(options.now);
|
|
1336
1523
|
const ttlSeconds = normalizeTTLSeconds(options.ttlSeconds);
|
|
1337
|
-
const exp =
|
|
1524
|
+
const exp = Math.min(now + ttlSeconds, requirePATExpiration(claims, now));
|
|
1525
|
+
const ttl = exp - now;
|
|
1338
1526
|
const username = `v1:${exp}:pat:${projectEndpoint}:${tokenEndpoint}`;
|
|
1339
1527
|
const tokenHash = import_crypto.default.createHash("sha256").update(token, "utf8").digest();
|
|
1340
1528
|
const key = Buffer.from(
|
|
@@ -1349,7 +1537,7 @@ function createPATTURNCredentials(options) {
|
|
|
1349
1537
|
const credential = import_crypto.default.createHmac("sha256", key).update(username, "utf8").digest("base64");
|
|
1350
1538
|
return import_rstream11.turnCredentialsSchema.parse({
|
|
1351
1539
|
credential,
|
|
1352
|
-
ttl
|
|
1540
|
+
ttl,
|
|
1353
1541
|
urls: createTURNURLs(clusterDomain, options.turnPort, options.turnsPort),
|
|
1354
1542
|
username
|
|
1355
1543
|
});
|
|
@@ -1359,15 +1547,15 @@ async function createAPPTURNCredentials(options) {
|
|
|
1359
1547
|
if (!clusterDomain) {
|
|
1360
1548
|
throw new Error("Cluster domain is required for TURN APP mode.");
|
|
1361
1549
|
}
|
|
1362
|
-
const projectEndpoint =
|
|
1550
|
+
const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
|
|
1363
1551
|
if (!projectEndpoint) {
|
|
1364
1552
|
throw new Error("Project endpoint is required for TURN APP mode.");
|
|
1365
1553
|
}
|
|
1366
|
-
const clientId =
|
|
1554
|
+
const clientId = normalizeOptionalString2(options.clientId);
|
|
1367
1555
|
if (!clientId) {
|
|
1368
1556
|
throw new Error("Client ID is required for TURN APP mode.");
|
|
1369
1557
|
}
|
|
1370
|
-
const clientSecret =
|
|
1558
|
+
const clientSecret = normalizeOptionalString2(options.clientSecret);
|
|
1371
1559
|
if (!clientSecret) {
|
|
1372
1560
|
throw new Error("Client secret is required for TURN APP mode.");
|
|
1373
1561
|
}
|
|
@@ -1379,7 +1567,7 @@ async function createAPPTURNCredentials(options) {
|
|
|
1379
1567
|
format: "der",
|
|
1380
1568
|
type: "pkcs8"
|
|
1381
1569
|
});
|
|
1382
|
-
const serverPublicKeyHex =
|
|
1570
|
+
const serverPublicKeyHex = normalizeOptionalString2(options.serverPublicKeyHex) ?? await loadTURNServerPublicKeyHex(clusterDomain, options);
|
|
1383
1571
|
const serverPublicKey = import_crypto.default.createPublicKey({
|
|
1384
1572
|
key: Buffer.from(serverPublicKeyHex, "hex"),
|
|
1385
1573
|
format: "der",
|
|
@@ -1409,7 +1597,7 @@ async function createAPPTURNCredentials(options) {
|
|
|
1409
1597
|
async function createTURNCredentials(options) {
|
|
1410
1598
|
const credentials = options.credentials;
|
|
1411
1599
|
const mode = resolveMode(credentials, options.mode);
|
|
1412
|
-
const projectEndpoint =
|
|
1600
|
+
const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
|
|
1413
1601
|
if (mode === "api") {
|
|
1414
1602
|
const controlPlaneCredentials = options.controlPlaneCredentials ?? credentials;
|
|
1415
1603
|
if (!controlPlaneCredentials) {
|
|
@@ -1440,6 +1628,10 @@ async function createTURNCredentials(options) {
|
|
|
1440
1628
|
"Project endpoint is required for TURN PAT mode."
|
|
1441
1629
|
),
|
|
1442
1630
|
token: credentials.token,
|
|
1631
|
+
tokenEndpoint: requireOption(
|
|
1632
|
+
normalizeOptionalString2(options.tokenEndpoint),
|
|
1633
|
+
"Token endpoint is required for TURN PAT mode."
|
|
1634
|
+
),
|
|
1443
1635
|
ttlSeconds: options.ttlSeconds,
|
|
1444
1636
|
turnPort: options.turnPort,
|
|
1445
1637
|
turnsPort: options.turnsPort
|
|
@@ -1489,6 +1681,7 @@ var RstreamTURNResource = class {
|
|
|
1489
1681
|
projectEndpoint: options.projectEndpoint ?? this.client.projectEndpoint,
|
|
1490
1682
|
projectId: options.projectId,
|
|
1491
1683
|
serverPublicKeyHex: options.serverPublicKeyHex,
|
|
1684
|
+
tokenEndpoint: options.tokenEndpoint,
|
|
1492
1685
|
ttlSeconds: options.ttlSeconds,
|
|
1493
1686
|
turnPort: options.turnPort,
|
|
1494
1687
|
turnsPort: options.turnsPort
|
|
@@ -1498,6 +1691,41 @@ var RstreamTURNResource = class {
|
|
|
1498
1691
|
|
|
1499
1692
|
// src/webhooks-resource.ts
|
|
1500
1693
|
var import_crypto2 = __toESM(require("crypto"));
|
|
1694
|
+
var SIGNATURE_HEX_BYTES = 32;
|
|
1695
|
+
var signatureHexSchema = /^[0-9a-fA-F]{64}$/;
|
|
1696
|
+
var timestampSchema = /^\d+$/;
|
|
1697
|
+
function parseSignatureTimestamp(timestamp) {
|
|
1698
|
+
if (!timestampSchema.test(timestamp)) {
|
|
1699
|
+
throw new Error("Invalid signature timestamp");
|
|
1700
|
+
}
|
|
1701
|
+
const parsed = Number(timestamp);
|
|
1702
|
+
if (!Number.isSafeInteger(parsed)) {
|
|
1703
|
+
throw new Error("Invalid signature timestamp");
|
|
1704
|
+
}
|
|
1705
|
+
return parsed;
|
|
1706
|
+
}
|
|
1707
|
+
function normalizeTolerance(tolerance) {
|
|
1708
|
+
if (!Number.isFinite(tolerance) || !Number.isInteger(tolerance) || tolerance < 0) {
|
|
1709
|
+
throw new Error("Invalid signature tolerance");
|
|
1710
|
+
}
|
|
1711
|
+
return tolerance;
|
|
1712
|
+
}
|
|
1713
|
+
function readSignature(signature) {
|
|
1714
|
+
if (!signatureHexSchema.test(signature)) {
|
|
1715
|
+
return void 0;
|
|
1716
|
+
}
|
|
1717
|
+
const parsed = Buffer.from(signature, "hex");
|
|
1718
|
+
return parsed.length === SIGNATURE_HEX_BYTES ? parsed : void 0;
|
|
1719
|
+
}
|
|
1720
|
+
function signatureMatches(signature, expected) {
|
|
1721
|
+
const parsed = readSignature(signature);
|
|
1722
|
+
return parsed === void 0 ? false : import_crypto2.default.timingSafeEqual(parsed, expected);
|
|
1723
|
+
}
|
|
1724
|
+
function assertSecretConfigured(secret) {
|
|
1725
|
+
if (secret.trim().length === 0) {
|
|
1726
|
+
throw new Error("Webhook signing secret is required");
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1501
1729
|
var RstreamWebhookResource = class {
|
|
1502
1730
|
async event(payload, header, secret, tolerance = 300, receivedAt = Date.now()) {
|
|
1503
1731
|
const payloadBuffer = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
|
|
@@ -1505,31 +1733,26 @@ var RstreamWebhookResource = class {
|
|
|
1505
1733
|
if (!signatureHeader) {
|
|
1506
1734
|
throw new Error("No signature header");
|
|
1507
1735
|
}
|
|
1736
|
+
assertSecretConfigured(secret);
|
|
1508
1737
|
const elements = signatureHeader.split(",").map((element) => element.trim());
|
|
1509
1738
|
const timestamp = elements.find((element) => element.startsWith("t="))?.substring(2);
|
|
1510
1739
|
const signatures = elements.filter((element) => element.startsWith("v1=")).map((element) => element.substring(3));
|
|
1511
1740
|
if (!timestamp || signatures.length === 0) {
|
|
1512
1741
|
throw new Error("Invalid signature header format");
|
|
1513
1742
|
}
|
|
1514
|
-
const timestampNum =
|
|
1743
|
+
const timestampNum = parseSignatureTimestamp(timestamp);
|
|
1744
|
+
const signatureTolerance = normalizeTolerance(tolerance);
|
|
1515
1745
|
const now = Math.floor(receivedAt / 1e3);
|
|
1516
|
-
if (Math.abs(now - timestampNum) >
|
|
1746
|
+
if (Math.abs(now - timestampNum) > signatureTolerance) {
|
|
1517
1747
|
throw new Error(
|
|
1518
1748
|
`Webhook signature timestamp outside tolerance: ${now}, ${timestampNum}`
|
|
1519
1749
|
);
|
|
1520
1750
|
}
|
|
1521
1751
|
const signedPayload = `${timestamp}.${payloadBuffer.toString("utf8")}`;
|
|
1522
|
-
const expectedSignature = import_crypto2.default.createHmac("sha256", secret).update(signedPayload).digest(
|
|
1523
|
-
const signatureMatched = signatures.some(
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
Buffer.from(signature),
|
|
1527
|
-
Buffer.from(expectedSignature)
|
|
1528
|
-
);
|
|
1529
|
-
} catch {
|
|
1530
|
-
return false;
|
|
1531
|
-
}
|
|
1532
|
-
});
|
|
1752
|
+
const expectedSignature = import_crypto2.default.createHmac("sha256", secret).update(signedPayload).digest();
|
|
1753
|
+
const signatureMatched = signatures.some(
|
|
1754
|
+
(signature) => signatureMatches(signature, expectedSignature)
|
|
1755
|
+
);
|
|
1533
1756
|
if (!signatureMatched) {
|
|
1534
1757
|
throw new Error("Signature verification failed");
|
|
1535
1758
|
}
|
|
@@ -1547,8 +1770,16 @@ var RstreamWebhookResource = class {
|
|
|
1547
1770
|
};
|
|
1548
1771
|
|
|
1549
1772
|
// src/tunnels.ts
|
|
1773
|
+
var engineAuthTokenScopes = {
|
|
1774
|
+
tunnels: {
|
|
1775
|
+
connect: true,
|
|
1776
|
+
create: true,
|
|
1777
|
+
list: true
|
|
1778
|
+
}
|
|
1779
|
+
};
|
|
1550
1780
|
var RstreamTunnelsClient = class {
|
|
1551
1781
|
config;
|
|
1782
|
+
managedProject;
|
|
1552
1783
|
constructor(config) {
|
|
1553
1784
|
this.config = config;
|
|
1554
1785
|
}
|
|
@@ -1568,6 +1799,21 @@ var RstreamTunnelsClient = class {
|
|
|
1568
1799
|
return this.config?.projectEndpoint;
|
|
1569
1800
|
}
|
|
1570
1801
|
async getEngine() {
|
|
1802
|
+
const projectEndpoint = normalizeOptionalString3(
|
|
1803
|
+
this.config?.projectEndpoint
|
|
1804
|
+
);
|
|
1805
|
+
if (projectEndpoint !== void 0 && normalizeOptionalString3(this.config?.engine) === void 0 && (0, import_rstream14.readEnvironment)().engine === void 0) {
|
|
1806
|
+
const project = await this.getManagedProject();
|
|
1807
|
+
if (project !== void 0) {
|
|
1808
|
+
const engine = (0, import_rstream12.getTunnelsProjectEngine)(project);
|
|
1809
|
+
if (engine) {
|
|
1810
|
+
return normalizeEngineAddress(engine);
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
throw new Error(
|
|
1814
|
+
"Failed to resolve the engine address from the managed tunnels project."
|
|
1815
|
+
);
|
|
1816
|
+
}
|
|
1571
1817
|
return await resolveTunnelsEngine({
|
|
1572
1818
|
apiUrl: this.config?.apiUrl,
|
|
1573
1819
|
controlPlaneCredentials: this.controlPlaneCredentials,
|
|
@@ -1576,6 +1822,55 @@ var RstreamTunnelsClient = class {
|
|
|
1576
1822
|
projectEndpoint: this.config?.projectEndpoint
|
|
1577
1823
|
});
|
|
1578
1824
|
}
|
|
1825
|
+
async getManagedProject() {
|
|
1826
|
+
const projectEndpoint = normalizeOptionalString3(
|
|
1827
|
+
this.config?.projectEndpoint
|
|
1828
|
+
);
|
|
1829
|
+
if (projectEndpoint === void 0) {
|
|
1830
|
+
return void 0;
|
|
1831
|
+
}
|
|
1832
|
+
if (this.managedProject === void 0) {
|
|
1833
|
+
this.managedProject = resolveManagedTunnelsProject({
|
|
1834
|
+
apiUrl: this.config?.apiUrl,
|
|
1835
|
+
controlPlaneCredentials: this.controlPlaneCredentials,
|
|
1836
|
+
credentials: this.credentials,
|
|
1837
|
+
projectEndpoint
|
|
1838
|
+
}).catch((error) => {
|
|
1839
|
+
this.managedProject = void 0;
|
|
1840
|
+
throw error;
|
|
1841
|
+
});
|
|
1842
|
+
}
|
|
1843
|
+
return await this.managedProject;
|
|
1844
|
+
}
|
|
1845
|
+
async getEngineAuthTokenParams() {
|
|
1846
|
+
const target = await this.getEngineAuthTokenTarget();
|
|
1847
|
+
return {
|
|
1848
|
+
tunnelsGrants: {
|
|
1849
|
+
...target,
|
|
1850
|
+
scopes: engineAuthTokenScopes
|
|
1851
|
+
}
|
|
1852
|
+
};
|
|
1853
|
+
}
|
|
1854
|
+
async getEngineAuthTokenTarget() {
|
|
1855
|
+
const projectId = normalizeOptionalString3(this.config?.projectId);
|
|
1856
|
+
const workspaceId = normalizeOptionalString3(this.config?.workspaceId);
|
|
1857
|
+
if (projectId !== void 0 && workspaceId !== void 0) {
|
|
1858
|
+
throw new Error("Use either projectId or workspaceId, not both.");
|
|
1859
|
+
}
|
|
1860
|
+
if (projectId !== void 0) {
|
|
1861
|
+
return { projects: [projectId] };
|
|
1862
|
+
}
|
|
1863
|
+
if (workspaceId !== void 0) {
|
|
1864
|
+
return { workspaces: [workspaceId] };
|
|
1865
|
+
}
|
|
1866
|
+
const project = await this.getManagedProject();
|
|
1867
|
+
if (project !== void 0) {
|
|
1868
|
+
return { projects: [project.id] };
|
|
1869
|
+
}
|
|
1870
|
+
throw new Error(
|
|
1871
|
+
"Application credentials require projectId, workspaceId, or projectEndpoint to create scoped engine tokens."
|
|
1872
|
+
);
|
|
1873
|
+
}
|
|
1579
1874
|
get auth() {
|
|
1580
1875
|
return new RstreamAuthResource(this);
|
|
1581
1876
|
}
|
|
@@ -1596,16 +1891,20 @@ var RstreamTunnelsClient = class {
|
|
|
1596
1891
|
if (!credentials) {
|
|
1597
1892
|
return void 0;
|
|
1598
1893
|
}
|
|
1599
|
-
if (!(0,
|
|
1894
|
+
if (!(0, import_rstream13.isClientCredentials)(credentials)) {
|
|
1600
1895
|
return credentials.token;
|
|
1601
1896
|
}
|
|
1602
|
-
|
|
1897
|
+
const tokenParams = await this.getEngineAuthTokenParams();
|
|
1898
|
+
return (await this.auth.createAuthToken(tokenParams, {
|
|
1603
1899
|
credentials,
|
|
1604
1900
|
engine
|
|
1605
1901
|
})).token;
|
|
1606
1902
|
}
|
|
1607
1903
|
async request(path, options) {
|
|
1608
|
-
const engine = await this.getEngine();
|
|
1904
|
+
const engine = normalizeEngineAddress(await this.getEngine());
|
|
1905
|
+
if (!path.startsWith("/") || path.startsWith("//")) {
|
|
1906
|
+
throw new Error("Engine request path must be a relative absolute path.");
|
|
1907
|
+
}
|
|
1609
1908
|
const url = `https://${engine}/api${path}`;
|
|
1610
1909
|
const headers = new Headers(options?.headers || {});
|
|
1611
1910
|
const token = await this.getToken(engine);
|
|
@@ -1614,7 +1913,8 @@ var RstreamTunnelsClient = class {
|
|
|
1614
1913
|
}
|
|
1615
1914
|
const response = await fetch(url, {
|
|
1616
1915
|
...options,
|
|
1617
|
-
headers
|
|
1916
|
+
headers,
|
|
1917
|
+
redirect: options?.redirect ?? "manual"
|
|
1618
1918
|
});
|
|
1619
1919
|
if (!response.ok) {
|
|
1620
1920
|
const errorText = await response.text();
|
|
@@ -1623,8 +1923,96 @@ var RstreamTunnelsClient = class {
|
|
|
1623
1923
|
return await response.json();
|
|
1624
1924
|
}
|
|
1625
1925
|
};
|
|
1926
|
+
function normalizeOptionalString3(value) {
|
|
1927
|
+
const normalized = value?.trim();
|
|
1928
|
+
return normalized ? normalized : void 0;
|
|
1929
|
+
}
|
|
1626
1930
|
|
|
1627
1931
|
// src/watch.ts
|
|
1932
|
+
var import_jsonwebtoken = __toESM(require("jsonwebtoken"));
|
|
1933
|
+
var maxWatchTokenLifetimeSeconds = 3600;
|
|
1934
|
+
var watchTokenIssuedAtSkewSeconds = 300;
|
|
1935
|
+
function normalizeTransport(transport) {
|
|
1936
|
+
if (transport === void 0) {
|
|
1937
|
+
return "sse";
|
|
1938
|
+
}
|
|
1939
|
+
if (transport === "sse" || transport === "websocket") {
|
|
1940
|
+
return transport;
|
|
1941
|
+
}
|
|
1942
|
+
throw new Error("Watch: Unsupported transport.");
|
|
1943
|
+
}
|
|
1944
|
+
function validateWatchQueryToken(token) {
|
|
1945
|
+
const parsed = authTokenSchema.safeParse(import_jsonwebtoken.default.decode(token));
|
|
1946
|
+
if (!parsed.success || parsed.data.type === "pat") {
|
|
1947
|
+
throw new Error(
|
|
1948
|
+
"Watch requires a short-lived auth or app token for URL-based authentication."
|
|
1949
|
+
);
|
|
1950
|
+
}
|
|
1951
|
+
const iat = parsed.data.iat;
|
|
1952
|
+
const exp = parsed.data.exp;
|
|
1953
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1954
|
+
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) {
|
|
1955
|
+
throw new Error(
|
|
1956
|
+
"Watch requires a token with a bounded lifetime of at most 3600 seconds."
|
|
1957
|
+
);
|
|
1958
|
+
}
|
|
1959
|
+
if (!hasWatchOnlyTunnelGrants(parsed.data)) {
|
|
1960
|
+
throw new Error(
|
|
1961
|
+
"Watch requires a fine-grained token limited to tunnel list grants for URL-based authentication."
|
|
1962
|
+
);
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
function hasWatchOnlyTunnelGrants(token) {
|
|
1966
|
+
if (!hasWatchOnlyPermissions(token.permissions)) {
|
|
1967
|
+
return false;
|
|
1968
|
+
}
|
|
1969
|
+
if (token.tunnelsGrants === void 0) {
|
|
1970
|
+
return false;
|
|
1971
|
+
}
|
|
1972
|
+
return hasWatchOnlyTunnelGrant(token.tunnelsGrants);
|
|
1973
|
+
}
|
|
1974
|
+
function hasWatchOnlyTunnelGrant(grant) {
|
|
1975
|
+
return watchGrantBranches(grant).every(
|
|
1976
|
+
(branch) => branch.hasList && !branch.hasMutationOrConnect
|
|
1977
|
+
);
|
|
1978
|
+
}
|
|
1979
|
+
function watchGrantBranches(grant) {
|
|
1980
|
+
if ("AND" in grant) {
|
|
1981
|
+
return grant.AND.reduce(
|
|
1982
|
+
(branches, child) => combineWatchGrantBranches(branches, child),
|
|
1983
|
+
[{ hasList: false, hasMutationOrConnect: false }]
|
|
1984
|
+
);
|
|
1985
|
+
}
|
|
1986
|
+
if ("OR" in grant) {
|
|
1987
|
+
return grant.OR.flatMap(watchGrantBranches);
|
|
1988
|
+
}
|
|
1989
|
+
const tunnels = grant.scopes?.tunnels;
|
|
1990
|
+
if (tunnels === void 0) {
|
|
1991
|
+
return [{ hasList: false, hasMutationOrConnect: false }];
|
|
1992
|
+
}
|
|
1993
|
+
return [
|
|
1994
|
+
{
|
|
1995
|
+
hasList: tunnels.list !== void 0,
|
|
1996
|
+
hasMutationOrConnect: tunnels.create !== void 0 || tunnels.connect !== void 0
|
|
1997
|
+
}
|
|
1998
|
+
];
|
|
1999
|
+
}
|
|
2000
|
+
function combineWatchGrantBranches(branches, child) {
|
|
2001
|
+
return branches.flatMap(
|
|
2002
|
+
(branch) => watchGrantBranches(child).map((childBranch) => ({
|
|
2003
|
+
hasList: branch.hasList || childBranch.hasList,
|
|
2004
|
+
hasMutationOrConnect: branch.hasMutationOrConnect || childBranch.hasMutationOrConnect
|
|
2005
|
+
}))
|
|
2006
|
+
);
|
|
2007
|
+
}
|
|
2008
|
+
function hasWatchOnlyPermissions(permissions) {
|
|
2009
|
+
if (!Array.isArray(permissions)) {
|
|
2010
|
+
return true;
|
|
2011
|
+
}
|
|
2012
|
+
return permissions.every(
|
|
2013
|
+
(permission) => permission === "tunnels.resources.read-only" || permission === "network.resources.read-only"
|
|
2014
|
+
);
|
|
2015
|
+
}
|
|
1628
2016
|
var Watch = class {
|
|
1629
2017
|
connection = null;
|
|
1630
2018
|
connectionState = "preparing";
|
|
@@ -1639,45 +2027,56 @@ var Watch = class {
|
|
|
1639
2027
|
throw new Error("Watch: Connection already started or closed.");
|
|
1640
2028
|
}
|
|
1641
2029
|
this.connectionState = "connecting";
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
const
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
this.connectionState = "connected";
|
|
1664
|
-
this.events.onConnect?.();
|
|
1665
|
-
};
|
|
1666
|
-
this.connection.onmessage = (msg) => {
|
|
1667
|
-
const parsed = wsEventsSchema.parse(JSON.parse(msg.data));
|
|
1668
|
-
this.events.onEvent?.(parsed);
|
|
1669
|
-
};
|
|
1670
|
-
this.connection.onerror = () => {
|
|
1671
|
-
if (this.connectionState !== "closed") {
|
|
1672
|
-
this.disconnect();
|
|
2030
|
+
try {
|
|
2031
|
+
const token = typeof this.config.auth === "function" ? await this.config.auth() : this.config.auth;
|
|
2032
|
+
validateWatchQueryToken(token);
|
|
2033
|
+
const engine = await resolveTunnelsEngine({
|
|
2034
|
+
apiUrl: this.config.apiUrl,
|
|
2035
|
+
controlPlaneCredentials: this.config.controlPlaneCredentials,
|
|
2036
|
+
engine: this.config.engine,
|
|
2037
|
+
projectEndpoint: this.config.projectEndpoint,
|
|
2038
|
+
token
|
|
2039
|
+
});
|
|
2040
|
+
const transport = normalizeTransport(this.config.transport);
|
|
2041
|
+
const base = `https://${engine}`;
|
|
2042
|
+
if (transport === "sse") {
|
|
2043
|
+
const url = new URL(`/api/sse`, base);
|
|
2044
|
+
url.searchParams.set("rstream.token", token);
|
|
2045
|
+
this.connection = new EventSource(url.toString());
|
|
2046
|
+
} else {
|
|
2047
|
+
const url = new URL(`/api/websocket`, base);
|
|
2048
|
+
url.searchParams.set("rstream.token", token);
|
|
2049
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
2050
|
+
this.connection = new WebSocket(url.toString());
|
|
1673
2051
|
}
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
2052
|
+
this.connection.onopen = () => {
|
|
2053
|
+
this.connectionState = "connected";
|
|
2054
|
+
this.events.onConnect?.();
|
|
2055
|
+
};
|
|
2056
|
+
this.connection.onmessage = (msg) => this.handleMessage(msg.data);
|
|
2057
|
+
this.connection.onerror = () => {
|
|
1677
2058
|
if (this.connectionState !== "closed") {
|
|
1678
2059
|
this.disconnect();
|
|
1679
2060
|
}
|
|
1680
2061
|
};
|
|
2062
|
+
if (this.connection instanceof WebSocket) {
|
|
2063
|
+
this.connection.onclose = () => {
|
|
2064
|
+
if (this.connectionState !== "closed") {
|
|
2065
|
+
this.disconnect();
|
|
2066
|
+
}
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
2069
|
+
} catch (error) {
|
|
2070
|
+
this.connectionState = "preparing";
|
|
2071
|
+
throw error;
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
handleMessage(data) {
|
|
2075
|
+
try {
|
|
2076
|
+
const parsed = wsEventsSchema.parse(JSON.parse(data));
|
|
2077
|
+
this.events.onEvent?.(parsed);
|
|
2078
|
+
} catch {
|
|
2079
|
+
this.disconnect();
|
|
1681
2080
|
}
|
|
1682
2081
|
}
|
|
1683
2082
|
disconnect() {
|
|
@@ -1798,8 +2197,10 @@ function parseWebTTYServers(tunnels) {
|
|
|
1798
2197
|
listClientsResponseSchema,
|
|
1799
2198
|
listTunnelsParamsSchema,
|
|
1800
2199
|
listTunnelsResponseSchema,
|
|
2200
|
+
normalizeEngineAddress,
|
|
1801
2201
|
parseWebTTYServers,
|
|
1802
2202
|
resolveControlPlaneCredentials,
|
|
2203
|
+
resolveManagedTunnelsProject,
|
|
1803
2204
|
resolveTunnelsAPIURL,
|
|
1804
2205
|
resolveTunnelsCredentials,
|
|
1805
2206
|
resolveTunnelsEngine,
|