@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.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
|
|
@@ -219,7 +221,7 @@ var authTokenTunnelsScopesSchema = z3.object({
|
|
|
219
221
|
creation_date: true
|
|
220
222
|
})
|
|
221
223
|
).optional()
|
|
222
|
-
})
|
|
224
|
+
}).strict()
|
|
223
225
|
]).optional(),
|
|
224
226
|
// Scopes for connecting to tunnels
|
|
225
227
|
connect: z3.union([
|
|
@@ -231,7 +233,7 @@ var authTokenTunnelsScopesSchema = z3.object({
|
|
|
231
233
|
path: z3.string().optional()
|
|
232
234
|
})
|
|
233
235
|
).optional()
|
|
234
|
-
})
|
|
236
|
+
}).strict()
|
|
235
237
|
]).optional(),
|
|
236
238
|
// Scopes for listing tunnels
|
|
237
239
|
list: z3.union([
|
|
@@ -239,17 +241,32 @@ var authTokenTunnelsScopesSchema = z3.object({
|
|
|
239
241
|
z3.object({
|
|
240
242
|
filters: filters(tunnelSchema).optional(),
|
|
241
243
|
select: select(tunnelSchema).optional()
|
|
242
|
-
})
|
|
244
|
+
}).strict()
|
|
243
245
|
]).optional()
|
|
246
|
+
}).strict().superRefine((scopes, ctx) => {
|
|
247
|
+
if (scopes.create === void 0 && scopes.connect === void 0 && scopes.list === void 0) {
|
|
248
|
+
ctx.addIssue({
|
|
249
|
+
code: z3.ZodIssueCode.custom,
|
|
250
|
+
message: "At least one tunnel scope action is required."
|
|
251
|
+
});
|
|
252
|
+
}
|
|
244
253
|
});
|
|
245
254
|
var authTokenScopesSchema = z3.object({
|
|
246
255
|
tunnels: authTokenTunnelsScopesSchema.optional()
|
|
247
256
|
// Scopes related to tunnels
|
|
257
|
+
}).strict().superRefine((scopes, ctx) => {
|
|
258
|
+
if (scopes.tunnels === void 0) {
|
|
259
|
+
ctx.addIssue({
|
|
260
|
+
code: z3.ZodIssueCode.custom,
|
|
261
|
+
message: "Tunnel scopes are required.",
|
|
262
|
+
path: ["tunnels"]
|
|
263
|
+
});
|
|
264
|
+
}
|
|
248
265
|
});
|
|
249
266
|
var authTokenTunnelGrantSchema = z3.object({
|
|
250
267
|
workspaces: z3.array(nonEmptyStringSchema).min(1).optional(),
|
|
251
268
|
projects: z3.array(nonEmptyStringSchema).min(1).optional(),
|
|
252
|
-
scopes: authTokenScopesSchema
|
|
269
|
+
scopes: authTokenScopesSchema
|
|
253
270
|
}).strict().superRefine((grant, ctx) => {
|
|
254
271
|
if (grant.workspaces !== void 0 && grant.projects !== void 0) {
|
|
255
272
|
ctx.addIssue({
|
|
@@ -258,6 +275,13 @@ var authTokenTunnelGrantSchema = z3.object({
|
|
|
258
275
|
path: ["projects"]
|
|
259
276
|
});
|
|
260
277
|
}
|
|
278
|
+
if (grant.workspaces === void 0 && grant.projects === void 0) {
|
|
279
|
+
ctx.addIssue({
|
|
280
|
+
code: z3.ZodIssueCode.custom,
|
|
281
|
+
message: "A grant must target workspaces or projects.",
|
|
282
|
+
path: ["workspaces"]
|
|
283
|
+
});
|
|
284
|
+
}
|
|
261
285
|
});
|
|
262
286
|
var authTokenCommonSchema = {
|
|
263
287
|
aud: z3.union([z3.string(), z3.array(z3.string())]).optional(),
|
|
@@ -293,10 +317,9 @@ var authTokenSchema = z3.union([
|
|
|
293
317
|
]);
|
|
294
318
|
var tokenSchema = authTokenSchema;
|
|
295
319
|
var createAuthTokenParamsSchema = z3.object({
|
|
296
|
-
expires_in: z3.number().default(60),
|
|
297
|
-
// 1 minute
|
|
320
|
+
expires_in: z3.number().int().min(1).max(3600).default(60),
|
|
298
321
|
scopes: authTokenScopesSchema.optional(),
|
|
299
|
-
tunnelsGrants: z3.array(authTokenTunnelGrantSchema).optional(),
|
|
322
|
+
tunnelsGrants: z3.array(authTokenTunnelGrantSchema).min(1).optional(),
|
|
300
323
|
metadata: z3.unknown().optional()
|
|
301
324
|
// Additional metadata
|
|
302
325
|
}).strict().superRefine((params, ctx) => {
|
|
@@ -307,6 +330,13 @@ var createAuthTokenParamsSchema = z3.object({
|
|
|
307
330
|
path: ["tunnelsGrants"]
|
|
308
331
|
});
|
|
309
332
|
}
|
|
333
|
+
if (params.scopes === void 0 && params.tunnelsGrants === void 0) {
|
|
334
|
+
ctx.addIssue({
|
|
335
|
+
code: z3.ZodIssueCode.custom,
|
|
336
|
+
message: "Explicit scopes or tunnelsGrants are required.",
|
|
337
|
+
path: ["scopes"]
|
|
338
|
+
});
|
|
339
|
+
}
|
|
310
340
|
});
|
|
311
341
|
var createAuthTokenResponseSchema = z3.object({
|
|
312
342
|
token: z3.string()
|
|
@@ -333,12 +363,12 @@ var RstreamAuthResource = class {
|
|
|
333
363
|
}
|
|
334
364
|
};
|
|
335
365
|
function createAuthTokenFromClientCredentials(credentials, params, options) {
|
|
336
|
-
const tokenParams = createAuthTokenParamsSchema.parse(params
|
|
366
|
+
const tokenParams = createAuthTokenParamsSchema.parse(params);
|
|
337
367
|
const payload = {
|
|
338
368
|
metadata: {
|
|
339
369
|
engine: options?.engine
|
|
340
370
|
},
|
|
341
|
-
tunnelsGrants: normalizeAuthTokenTunnelGrants(tokenParams),
|
|
371
|
+
tunnelsGrants: normalizeAuthTokenTunnelGrants(tokenParams, options),
|
|
342
372
|
permissions: null
|
|
343
373
|
};
|
|
344
374
|
const { token } = (0, import_rstream.createClientCredentialsToken)(credentials, {
|
|
@@ -347,19 +377,36 @@ function createAuthTokenFromClientCredentials(credentials, params, options) {
|
|
|
347
377
|
});
|
|
348
378
|
return { token };
|
|
349
379
|
}
|
|
350
|
-
function normalizeAuthTokenTunnelGrants(params) {
|
|
351
|
-
if (!params) {
|
|
352
|
-
return void 0;
|
|
353
|
-
}
|
|
380
|
+
function normalizeAuthTokenTunnelGrants(params, options) {
|
|
354
381
|
const tunnelsGrants = params.tunnelsGrants;
|
|
355
382
|
if (tunnelsGrants !== void 0) {
|
|
356
383
|
return [...tunnelsGrants];
|
|
357
384
|
}
|
|
358
385
|
if (params.scopes) {
|
|
359
|
-
return [{ scopes: params.scopes }];
|
|
386
|
+
return [{ ...normalizeTunnelGrantTarget(options), scopes: params.scopes }];
|
|
360
387
|
}
|
|
361
388
|
return void 0;
|
|
362
389
|
}
|
|
390
|
+
function normalizeTunnelGrantTarget(options) {
|
|
391
|
+
const projectId = normalizeOptionalString(options?.projectId);
|
|
392
|
+
const workspaceId = normalizeOptionalString(options?.workspaceId);
|
|
393
|
+
if (projectId !== void 0 && workspaceId !== void 0) {
|
|
394
|
+
throw new Error("Use either projectId or workspaceId, not both.");
|
|
395
|
+
}
|
|
396
|
+
if (projectId !== void 0) {
|
|
397
|
+
return { projects: [projectId] };
|
|
398
|
+
}
|
|
399
|
+
if (workspaceId !== void 0) {
|
|
400
|
+
return { workspaces: [workspaceId] };
|
|
401
|
+
}
|
|
402
|
+
throw new Error(
|
|
403
|
+
"Project ID or workspace ID is required when using scope-only tunnel grants."
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
function normalizeOptionalString(value) {
|
|
407
|
+
const normalized = value?.trim();
|
|
408
|
+
return normalized ? normalized : void 0;
|
|
409
|
+
}
|
|
363
410
|
|
|
364
411
|
// src/client.ts
|
|
365
412
|
var z4 = __toESM(require("zod"));
|
|
@@ -400,6 +447,13 @@ var listClientsParamsSchema = z4.object({
|
|
|
400
447
|
var listClientsResponseSchema = z4.array(clientSchema);
|
|
401
448
|
|
|
402
449
|
// src/clients-resource.ts
|
|
450
|
+
function clientPathId(id) {
|
|
451
|
+
const normalized = id.trim();
|
|
452
|
+
if (!normalized) {
|
|
453
|
+
throw new Error("Client ID is required.");
|
|
454
|
+
}
|
|
455
|
+
return encodeURIComponent(normalized);
|
|
456
|
+
}
|
|
403
457
|
var RstreamClientsResource = class {
|
|
404
458
|
client;
|
|
405
459
|
constructor(client) {
|
|
@@ -413,9 +467,12 @@ var RstreamClientsResource = class {
|
|
|
413
467
|
return listClientsResponseSchema.parse(response);
|
|
414
468
|
}
|
|
415
469
|
async get(id) {
|
|
416
|
-
const response = await this.client.request(
|
|
417
|
-
|
|
418
|
-
|
|
470
|
+
const response = await this.client.request(
|
|
471
|
+
`/clients/${clientPathId(id)}`,
|
|
472
|
+
{
|
|
473
|
+
method: "GET"
|
|
474
|
+
}
|
|
475
|
+
);
|
|
419
476
|
return clientSchema.parse(response);
|
|
420
477
|
}
|
|
421
478
|
};
|
|
@@ -831,12 +888,6 @@ var engineOpenApiDocument = {
|
|
|
831
888
|
scheme: "bearer",
|
|
832
889
|
bearerFormat: "rstream token",
|
|
833
890
|
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
891
|
}
|
|
841
892
|
},
|
|
842
893
|
parameters: {
|
|
@@ -956,7 +1007,7 @@ var engineOpenApiDocument = {
|
|
|
956
1007
|
summary: "List visible engine clients.",
|
|
957
1008
|
operationId: "list_engine_clients",
|
|
958
1009
|
parameters: [{ $ref: "#/components/parameters/ListClientsParams" }],
|
|
959
|
-
security: [{ bearerAuth: [] }
|
|
1010
|
+
security: [{ bearerAuth: [] }],
|
|
960
1011
|
responses: {
|
|
961
1012
|
"200": jsonArrayResponse(
|
|
962
1013
|
"Visible clients.",
|
|
@@ -975,7 +1026,7 @@ var engineOpenApiDocument = {
|
|
|
975
1026
|
summary: "List visible engine tunnels.",
|
|
976
1027
|
operationId: "list_engine_tunnels",
|
|
977
1028
|
parameters: [{ $ref: "#/components/parameters/ListTunnelsParams" }],
|
|
978
|
-
security: [{ bearerAuth: [] }
|
|
1029
|
+
security: [{ bearerAuth: [] }],
|
|
979
1030
|
responses: {
|
|
980
1031
|
"200": jsonArrayResponse(
|
|
981
1032
|
"Visible tunnels. Fine-grained list scopes may filter objects and project fields.",
|
|
@@ -995,7 +1046,7 @@ var engineOpenApiDocument = {
|
|
|
995
1046
|
summary: "Subscribe to visible engine state through Server-Sent Events.",
|
|
996
1047
|
operationId: "watch_engine_events_sse",
|
|
997
1048
|
parameters: [{ $ref: "#/components/parameters/WatchParams" }],
|
|
998
|
-
security: [{ bearerAuth: [] }
|
|
1049
|
+
security: [{ bearerAuth: [] }],
|
|
999
1050
|
responses: {
|
|
1000
1051
|
"200": streamResponse(
|
|
1001
1052
|
"SSE stream. Each event is emitted as a data line containing a JSON Event object."
|
|
@@ -1014,7 +1065,7 @@ var engineOpenApiDocument = {
|
|
|
1014
1065
|
summary: "Subscribe to visible engine state through a WebSocket upgrade.",
|
|
1015
1066
|
operationId: "watch_engine_events_websocket",
|
|
1016
1067
|
parameters: [{ $ref: "#/components/parameters/WatchParams" }],
|
|
1017
|
-
security: [{ bearerAuth: [] }
|
|
1068
|
+
security: [{ bearerAuth: [] }],
|
|
1018
1069
|
responses: {
|
|
1019
1070
|
"101": {
|
|
1020
1071
|
description: "WebSocket connection accepted. Frames contain JSON Event objects."
|
|
@@ -1042,22 +1093,37 @@ var import_rstream4 = require("@rstreamlabs/rstream");
|
|
|
1042
1093
|
var import_rstream5 = require("@rstreamlabs/rstream");
|
|
1043
1094
|
var import_rstream6 = require("@rstreamlabs/rstream");
|
|
1044
1095
|
var import_rstream7 = require("@rstreamlabs/rstream");
|
|
1045
|
-
var import_jsonwebtoken = __toESM(require("jsonwebtoken"));
|
|
1046
1096
|
function trimOptionalString(value) {
|
|
1047
1097
|
const normalized = value?.trim();
|
|
1048
1098
|
return normalized ? normalized : void 0;
|
|
1049
1099
|
}
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1100
|
+
var dnsLabelPattern = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i;
|
|
1101
|
+
function validateHostname(hostname) {
|
|
1102
|
+
if (!hostname || hostname.length > 253 || hostname.includes("..")) {
|
|
1103
|
+
return false;
|
|
1053
1104
|
}
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1105
|
+
return hostname.split(".").every((label) => label === "localhost" || dnsLabelPattern.test(label));
|
|
1106
|
+
}
|
|
1107
|
+
function normalizeEngineAddress(engine) {
|
|
1108
|
+
const normalized = engine.trim();
|
|
1109
|
+
if (!normalized || /^[a-z][a-z0-9+.-]*:\/\//i.test(normalized) || /[/?#@\\\s]/.test(normalized)) {
|
|
1110
|
+
throw new Error("Engine must be a host[:port] value.");
|
|
1111
|
+
}
|
|
1112
|
+
const explicitPort = normalized.match(/:(\d+)$/)?.[1];
|
|
1113
|
+
const url = new URL(`https://${normalized}`);
|
|
1114
|
+
if (url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
|
|
1115
|
+
throw new Error("Engine must be a host[:port] value.");
|
|
1116
|
+
}
|
|
1117
|
+
if (!validateHostname(url.hostname)) {
|
|
1118
|
+
throw new Error("Engine must use a valid hostname.");
|
|
1119
|
+
}
|
|
1120
|
+
if (explicitPort) {
|
|
1121
|
+
const port = Number(explicitPort);
|
|
1122
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
1123
|
+
throw new Error("Engine port must be an integer between 1 and 65535.");
|
|
1124
|
+
}
|
|
1059
1125
|
}
|
|
1060
|
-
return
|
|
1126
|
+
return `${url.hostname.toLowerCase()}${explicitPort ? `:${explicitPort}` : ""}`;
|
|
1061
1127
|
}
|
|
1062
1128
|
function resolveTunnelsAPIURL(apiUrl) {
|
|
1063
1129
|
return trimOptionalString(apiUrl) ?? (0, import_rstream6.readEnvironment)().apiUrl ?? import_rstream3.DEFAULT_API_URL;
|
|
@@ -1091,37 +1157,22 @@ function resolveControlPlaneCredentials(controlPlaneCredentials, credentials, au
|
|
|
1091
1157
|
async function resolveTunnelsEngine(config) {
|
|
1092
1158
|
const explicitEngine = trimOptionalString(config.engine);
|
|
1093
1159
|
if (explicitEngine) {
|
|
1094
|
-
return explicitEngine;
|
|
1160
|
+
return normalizeEngineAddress(explicitEngine);
|
|
1095
1161
|
}
|
|
1096
1162
|
const environmentEngine = (0, import_rstream6.readEnvironment)().engine;
|
|
1097
1163
|
if (environmentEngine) {
|
|
1098
|
-
return environmentEngine;
|
|
1164
|
+
return normalizeEngineAddress(environmentEngine);
|
|
1099
1165
|
}
|
|
1100
1166
|
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
1167
|
const projectEndpoint = trimOptionalString(config.projectEndpoint);
|
|
1106
1168
|
if (projectEndpoint) {
|
|
1107
|
-
const
|
|
1108
|
-
config
|
|
1109
|
-
config.credentials,
|
|
1169
|
+
const project = await resolveManagedTunnelsProject({
|
|
1170
|
+
...config,
|
|
1110
1171
|
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
1172
|
});
|
|
1121
|
-
const project = await controlPlane.tunnels.projects.resolveByEndpoint(projectEndpoint);
|
|
1122
1173
|
const engine = (0, import_rstream4.getTunnelsProjectEngine)(project);
|
|
1123
1174
|
if (engine) {
|
|
1124
|
-
return engine;
|
|
1175
|
+
return normalizeEngineAddress(engine);
|
|
1125
1176
|
}
|
|
1126
1177
|
throw new Error(
|
|
1127
1178
|
"Failed to resolve the engine address from the managed tunnels project."
|
|
@@ -1131,8 +1182,36 @@ async function resolveTunnelsEngine(config) {
|
|
|
1131
1182
|
"Engine URL is not defined. Provide an engine, use RSTREAM_ENGINE, or configure a managed project endpoint with control-plane access."
|
|
1132
1183
|
);
|
|
1133
1184
|
}
|
|
1185
|
+
async function resolveManagedTunnelsProject(config) {
|
|
1186
|
+
const projectEndpoint = trimOptionalString(config.projectEndpoint);
|
|
1187
|
+
if (!projectEndpoint) {
|
|
1188
|
+
throw new Error("Managed project endpoint is required.");
|
|
1189
|
+
}
|
|
1190
|
+
const credentials = resolveControlPlaneCredentials(
|
|
1191
|
+
config.controlPlaneCredentials,
|
|
1192
|
+
config.credentials,
|
|
1193
|
+
config.token
|
|
1194
|
+
);
|
|
1195
|
+
if (credentials === void 0) {
|
|
1196
|
+
throw new Error(
|
|
1197
|
+
"Control-plane credentials are required to resolve a managed project endpoint."
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1200
|
+
const controlPlane = new import_rstream7.RstreamClient({
|
|
1201
|
+
apiUrl: resolveTunnelsAPIURL(config.apiUrl),
|
|
1202
|
+
credentials
|
|
1203
|
+
});
|
|
1204
|
+
return await controlPlane.tunnels.projects.resolveByEndpoint(projectEndpoint);
|
|
1205
|
+
}
|
|
1134
1206
|
|
|
1135
1207
|
// src/tunnels-resource.ts
|
|
1208
|
+
function tunnelPathId(id) {
|
|
1209
|
+
const normalized = id.trim();
|
|
1210
|
+
if (!normalized) {
|
|
1211
|
+
throw new Error("Tunnel ID is required.");
|
|
1212
|
+
}
|
|
1213
|
+
return encodeURIComponent(normalized);
|
|
1214
|
+
}
|
|
1136
1215
|
var RstreamTunnelsResource = class {
|
|
1137
1216
|
client;
|
|
1138
1217
|
constructor(client) {
|
|
@@ -1146,15 +1225,20 @@ var RstreamTunnelsResource = class {
|
|
|
1146
1225
|
return listTunnelsResponseSchema.parse(response);
|
|
1147
1226
|
}
|
|
1148
1227
|
async get(id) {
|
|
1149
|
-
const response = await this.client.request(
|
|
1150
|
-
|
|
1151
|
-
|
|
1228
|
+
const response = await this.client.request(
|
|
1229
|
+
`/tunnels/${tunnelPathId(id)}`,
|
|
1230
|
+
{
|
|
1231
|
+
method: "GET"
|
|
1232
|
+
}
|
|
1233
|
+
);
|
|
1152
1234
|
return tunnelSchema.parse(response);
|
|
1153
1235
|
}
|
|
1154
1236
|
};
|
|
1155
1237
|
|
|
1156
1238
|
// src/tunnels.ts
|
|
1157
1239
|
var import_rstream12 = require("@rstreamlabs/rstream");
|
|
1240
|
+
var import_rstream13 = require("@rstreamlabs/rstream");
|
|
1241
|
+
var import_rstream14 = require("@rstreamlabs/rstream");
|
|
1158
1242
|
|
|
1159
1243
|
// src/turn.ts
|
|
1160
1244
|
var import_rstream8 = require("@rstreamlabs/rstream");
|
|
@@ -1165,46 +1249,52 @@ var z8 = __toESM(require("zod"));
|
|
|
1165
1249
|
var import_crypto = __toESM(require("crypto"));
|
|
1166
1250
|
var defaultTURNPort = 3478;
|
|
1167
1251
|
var defaultTURNSPort = 5349;
|
|
1168
|
-
var defaultTURNCredentialTTLSeconds =
|
|
1252
|
+
var defaultTURNCredentialTTLSeconds = 10 * 60;
|
|
1253
|
+
var maxTURNCredentialTTLSeconds = 60 * 60;
|
|
1169
1254
|
var turnTokenClaimsSchema = z8.object({
|
|
1170
|
-
|
|
1171
|
-
|
|
1255
|
+
exp: z8.number().int().optional(),
|
|
1256
|
+
iat: z8.number().int().optional(),
|
|
1257
|
+
token_endpoint: z8.string().regex(/^[0-9a-f]{8}$/i).optional(),
|
|
1258
|
+
type: z8.enum(["app", "auth", "pat"])
|
|
1172
1259
|
});
|
|
1173
1260
|
var turnCredentialModeSchema = z8.enum(["api", "app", "pat"]);
|
|
1174
|
-
function
|
|
1261
|
+
function normalizeOptionalString2(value) {
|
|
1175
1262
|
const normalized = value?.trim();
|
|
1176
1263
|
return normalized ? normalized : void 0;
|
|
1177
1264
|
}
|
|
1178
1265
|
function normalizeClusterDomain(clusterDomain) {
|
|
1179
|
-
const normalized =
|
|
1266
|
+
const normalized = normalizeOptionalString2(clusterDomain);
|
|
1180
1267
|
return normalized?.toLowerCase();
|
|
1181
1268
|
}
|
|
1182
1269
|
function normalizeNow(now) {
|
|
1183
|
-
if (now
|
|
1184
|
-
return Math.floor(now
|
|
1270
|
+
if (now === void 0) {
|
|
1271
|
+
return Math.floor(Date.now() / 1e3);
|
|
1185
1272
|
}
|
|
1186
|
-
|
|
1187
|
-
|
|
1273
|
+
const timestamp = now instanceof Date ? now.getTime() / 1e3 : now;
|
|
1274
|
+
if (!Number.isFinite(timestamp)) {
|
|
1275
|
+
throw new Error("TURN timestamp must be finite.");
|
|
1188
1276
|
}
|
|
1189
|
-
return Math.floor(
|
|
1277
|
+
return Math.floor(timestamp);
|
|
1190
1278
|
}
|
|
1191
1279
|
function normalizeTTLSeconds(ttlSeconds) {
|
|
1192
|
-
|
|
1193
|
-
|
|
1280
|
+
const ttl = ttlSeconds ?? defaultTURNCredentialTTLSeconds;
|
|
1281
|
+
if (!Number.isInteger(ttl) || ttl < 1 || ttl > maxTURNCredentialTTLSeconds) {
|
|
1282
|
+
throw new Error("TURN TTL must be an integer between 1 and 3600 seconds.");
|
|
1194
1283
|
}
|
|
1195
|
-
return
|
|
1284
|
+
return ttl;
|
|
1196
1285
|
}
|
|
1197
|
-
function
|
|
1198
|
-
|
|
1199
|
-
|
|
1286
|
+
function normalizePort(port, fallback, name) {
|
|
1287
|
+
const normalized = port ?? fallback;
|
|
1288
|
+
if (!Number.isInteger(normalized) || normalized < 1 || normalized > 65535) {
|
|
1289
|
+
throw new Error(`${name} must be an integer between 1 and 65535.`);
|
|
1200
1290
|
}
|
|
1201
|
-
return
|
|
1291
|
+
return normalized;
|
|
1292
|
+
}
|
|
1293
|
+
function normalizeTURNPort(port) {
|
|
1294
|
+
return normalizePort(port, defaultTURNPort, "TURN port");
|
|
1202
1295
|
}
|
|
1203
1296
|
function normalizeTURNSPort(port) {
|
|
1204
|
-
|
|
1205
|
-
return Math.floor(port);
|
|
1206
|
-
}
|
|
1207
|
-
return defaultTURNSPort;
|
|
1297
|
+
return normalizePort(port, defaultTURNSPort, "TURNS port");
|
|
1208
1298
|
}
|
|
1209
1299
|
function createTURNURLs(clusterDomain, turnPort, turnsPort) {
|
|
1210
1300
|
const normalizedTURNPort = normalizeTURNPort(turnPort);
|
|
@@ -1222,16 +1312,32 @@ function parseTURNTokenClaims(token) {
|
|
|
1222
1312
|
if (!payloadPart) {
|
|
1223
1313
|
throw new Error("Invalid token format.");
|
|
1224
1314
|
}
|
|
1225
|
-
|
|
1226
|
-
|
|
1315
|
+
let decoded;
|
|
1316
|
+
try {
|
|
1317
|
+
const payload = Buffer.from(payloadPart, "base64url").toString("utf8");
|
|
1318
|
+
decoded = JSON.parse(payload);
|
|
1319
|
+
} catch {
|
|
1320
|
+
throw new Error("Invalid token format.");
|
|
1321
|
+
}
|
|
1322
|
+
const parsed = turnTokenClaimsSchema.safeParse(decoded);
|
|
1227
1323
|
if (!parsed.success) {
|
|
1228
1324
|
throw new Error("Invalid token format.");
|
|
1229
1325
|
}
|
|
1230
1326
|
return parsed.data;
|
|
1231
1327
|
}
|
|
1328
|
+
function requirePATExpiration(claims, now) {
|
|
1329
|
+
const exp = claims.exp;
|
|
1330
|
+
if (typeof exp !== "number") {
|
|
1331
|
+
throw new Error("TURN PAT mode requires a PAT token with expiration.");
|
|
1332
|
+
}
|
|
1333
|
+
if (exp <= now) {
|
|
1334
|
+
throw new Error("TURN PAT mode requires a non-expired PAT token.");
|
|
1335
|
+
}
|
|
1336
|
+
return exp;
|
|
1337
|
+
}
|
|
1232
1338
|
function deriveClusterDomainFromEngine(projectEndpoint, engine) {
|
|
1233
|
-
const normalizedProjectEndpoint =
|
|
1234
|
-
const normalizedEngine =
|
|
1339
|
+
const normalizedProjectEndpoint = normalizeOptionalString2(projectEndpoint);
|
|
1340
|
+
const normalizedEngine = normalizeOptionalString2(engine);
|
|
1235
1341
|
if (!normalizedProjectEndpoint || !normalizedEngine) {
|
|
1236
1342
|
throw new Error(
|
|
1237
1343
|
"Failed to derive the cluster domain from the engine address."
|
|
@@ -1265,17 +1371,11 @@ function resolveMode(credentials, mode) {
|
|
|
1265
1371
|
if ((0, import_rstream8.isClientCredentials)(credentials)) {
|
|
1266
1372
|
return "app";
|
|
1267
1373
|
}
|
|
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
1374
|
return "api";
|
|
1275
1375
|
}
|
|
1276
1376
|
async function loadTURNServerPublicKeyHex(clusterDomain, options) {
|
|
1277
|
-
const configuredKey =
|
|
1278
|
-
const keyringBaseURL = configuredKey ??
|
|
1377
|
+
const configuredKey = normalizeOptionalString2(options.keyringBaseUrl);
|
|
1378
|
+
const keyringBaseURL = configuredKey ?? normalizeOptionalString2(options.apiUrl) ?? "https://rstream.io";
|
|
1279
1379
|
const url = new URL(
|
|
1280
1380
|
`/keyrings/turn/${clusterDomain}.spki.der.hex`,
|
|
1281
1381
|
keyringBaseURL
|
|
@@ -1287,8 +1387,8 @@ async function loadTURNServerPublicKeyHex(clusterDomain, options) {
|
|
|
1287
1387
|
return (await response.text()).trim();
|
|
1288
1388
|
}
|
|
1289
1389
|
async function createAPITURNCredentials(options) {
|
|
1290
|
-
const projectId =
|
|
1291
|
-
const projectEndpoint =
|
|
1390
|
+
const projectId = normalizeOptionalString2(options.projectId);
|
|
1391
|
+
const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
|
|
1292
1392
|
if (!projectId && !projectEndpoint) {
|
|
1293
1393
|
throw new Error(
|
|
1294
1394
|
"Project ID or project endpoint is required for TURN API mode."
|
|
@@ -1315,11 +1415,11 @@ function createPATTURNCredentials(options) {
|
|
|
1315
1415
|
if (!clusterDomain) {
|
|
1316
1416
|
throw new Error("Cluster domain is required for TURN PAT mode.");
|
|
1317
1417
|
}
|
|
1318
|
-
const projectEndpoint =
|
|
1418
|
+
const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
|
|
1319
1419
|
if (!projectEndpoint) {
|
|
1320
1420
|
throw new Error("Project endpoint is required for TURN PAT mode.");
|
|
1321
1421
|
}
|
|
1322
|
-
const token =
|
|
1422
|
+
const token = normalizeOptionalString2(options.token);
|
|
1323
1423
|
if (!token) {
|
|
1324
1424
|
throw new Error("Token is required for TURN PAT mode.");
|
|
1325
1425
|
}
|
|
@@ -1327,14 +1427,17 @@ function createPATTURNCredentials(options) {
|
|
|
1327
1427
|
if (claims.type !== "pat") {
|
|
1328
1428
|
throw new Error("TURN PAT mode requires a PAT token.");
|
|
1329
1429
|
}
|
|
1330
|
-
const tokenEndpoint =
|
|
1430
|
+
const tokenEndpoint = normalizeOptionalString2(options.tokenEndpoint);
|
|
1331
1431
|
if (!tokenEndpoint) {
|
|
1332
|
-
throw new Error(
|
|
1333
|
-
"TURN PAT mode requires a PAT token carrying a token endpoint."
|
|
1334
|
-
);
|
|
1432
|
+
throw new Error("Token endpoint is required for TURN PAT mode.");
|
|
1335
1433
|
}
|
|
1434
|
+
if (claims.token_endpoint !== void 0 && claims.token_endpoint !== tokenEndpoint) {
|
|
1435
|
+
throw new Error("Token endpoint does not match the PAT token.");
|
|
1436
|
+
}
|
|
1437
|
+
const now = normalizeNow(options.now);
|
|
1336
1438
|
const ttlSeconds = normalizeTTLSeconds(options.ttlSeconds);
|
|
1337
|
-
const exp =
|
|
1439
|
+
const exp = Math.min(now + ttlSeconds, requirePATExpiration(claims, now));
|
|
1440
|
+
const ttl = exp - now;
|
|
1338
1441
|
const username = `v1:${exp}:pat:${projectEndpoint}:${tokenEndpoint}`;
|
|
1339
1442
|
const tokenHash = import_crypto.default.createHash("sha256").update(token, "utf8").digest();
|
|
1340
1443
|
const key = Buffer.from(
|
|
@@ -1349,7 +1452,7 @@ function createPATTURNCredentials(options) {
|
|
|
1349
1452
|
const credential = import_crypto.default.createHmac("sha256", key).update(username, "utf8").digest("base64");
|
|
1350
1453
|
return import_rstream11.turnCredentialsSchema.parse({
|
|
1351
1454
|
credential,
|
|
1352
|
-
ttl
|
|
1455
|
+
ttl,
|
|
1353
1456
|
urls: createTURNURLs(clusterDomain, options.turnPort, options.turnsPort),
|
|
1354
1457
|
username
|
|
1355
1458
|
});
|
|
@@ -1359,15 +1462,15 @@ async function createAPPTURNCredentials(options) {
|
|
|
1359
1462
|
if (!clusterDomain) {
|
|
1360
1463
|
throw new Error("Cluster domain is required for TURN APP mode.");
|
|
1361
1464
|
}
|
|
1362
|
-
const projectEndpoint =
|
|
1465
|
+
const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
|
|
1363
1466
|
if (!projectEndpoint) {
|
|
1364
1467
|
throw new Error("Project endpoint is required for TURN APP mode.");
|
|
1365
1468
|
}
|
|
1366
|
-
const clientId =
|
|
1469
|
+
const clientId = normalizeOptionalString2(options.clientId);
|
|
1367
1470
|
if (!clientId) {
|
|
1368
1471
|
throw new Error("Client ID is required for TURN APP mode.");
|
|
1369
1472
|
}
|
|
1370
|
-
const clientSecret =
|
|
1473
|
+
const clientSecret = normalizeOptionalString2(options.clientSecret);
|
|
1371
1474
|
if (!clientSecret) {
|
|
1372
1475
|
throw new Error("Client secret is required for TURN APP mode.");
|
|
1373
1476
|
}
|
|
@@ -1379,7 +1482,7 @@ async function createAPPTURNCredentials(options) {
|
|
|
1379
1482
|
format: "der",
|
|
1380
1483
|
type: "pkcs8"
|
|
1381
1484
|
});
|
|
1382
|
-
const serverPublicKeyHex =
|
|
1485
|
+
const serverPublicKeyHex = normalizeOptionalString2(options.serverPublicKeyHex) ?? await loadTURNServerPublicKeyHex(clusterDomain, options);
|
|
1383
1486
|
const serverPublicKey = import_crypto.default.createPublicKey({
|
|
1384
1487
|
key: Buffer.from(serverPublicKeyHex, "hex"),
|
|
1385
1488
|
format: "der",
|
|
@@ -1409,7 +1512,7 @@ async function createAPPTURNCredentials(options) {
|
|
|
1409
1512
|
async function createTURNCredentials(options) {
|
|
1410
1513
|
const credentials = options.credentials;
|
|
1411
1514
|
const mode = resolveMode(credentials, options.mode);
|
|
1412
|
-
const projectEndpoint =
|
|
1515
|
+
const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
|
|
1413
1516
|
if (mode === "api") {
|
|
1414
1517
|
const controlPlaneCredentials = options.controlPlaneCredentials ?? credentials;
|
|
1415
1518
|
if (!controlPlaneCredentials) {
|
|
@@ -1440,6 +1543,10 @@ async function createTURNCredentials(options) {
|
|
|
1440
1543
|
"Project endpoint is required for TURN PAT mode."
|
|
1441
1544
|
),
|
|
1442
1545
|
token: credentials.token,
|
|
1546
|
+
tokenEndpoint: requireOption(
|
|
1547
|
+
normalizeOptionalString2(options.tokenEndpoint),
|
|
1548
|
+
"Token endpoint is required for TURN PAT mode."
|
|
1549
|
+
),
|
|
1443
1550
|
ttlSeconds: options.ttlSeconds,
|
|
1444
1551
|
turnPort: options.turnPort,
|
|
1445
1552
|
turnsPort: options.turnsPort
|
|
@@ -1489,6 +1596,7 @@ var RstreamTURNResource = class {
|
|
|
1489
1596
|
projectEndpoint: options.projectEndpoint ?? this.client.projectEndpoint,
|
|
1490
1597
|
projectId: options.projectId,
|
|
1491
1598
|
serverPublicKeyHex: options.serverPublicKeyHex,
|
|
1599
|
+
tokenEndpoint: options.tokenEndpoint,
|
|
1492
1600
|
ttlSeconds: options.ttlSeconds,
|
|
1493
1601
|
turnPort: options.turnPort,
|
|
1494
1602
|
turnsPort: options.turnsPort
|
|
@@ -1498,6 +1606,41 @@ var RstreamTURNResource = class {
|
|
|
1498
1606
|
|
|
1499
1607
|
// src/webhooks-resource.ts
|
|
1500
1608
|
var import_crypto2 = __toESM(require("crypto"));
|
|
1609
|
+
var SIGNATURE_HEX_BYTES = 32;
|
|
1610
|
+
var signatureHexSchema = /^[0-9a-fA-F]{64}$/;
|
|
1611
|
+
var timestampSchema = /^\d+$/;
|
|
1612
|
+
function parseSignatureTimestamp(timestamp) {
|
|
1613
|
+
if (!timestampSchema.test(timestamp)) {
|
|
1614
|
+
throw new Error("Invalid signature timestamp");
|
|
1615
|
+
}
|
|
1616
|
+
const parsed = Number(timestamp);
|
|
1617
|
+
if (!Number.isSafeInteger(parsed)) {
|
|
1618
|
+
throw new Error("Invalid signature timestamp");
|
|
1619
|
+
}
|
|
1620
|
+
return parsed;
|
|
1621
|
+
}
|
|
1622
|
+
function normalizeTolerance(tolerance) {
|
|
1623
|
+
if (!Number.isFinite(tolerance) || !Number.isInteger(tolerance) || tolerance < 0) {
|
|
1624
|
+
throw new Error("Invalid signature tolerance");
|
|
1625
|
+
}
|
|
1626
|
+
return tolerance;
|
|
1627
|
+
}
|
|
1628
|
+
function readSignature(signature) {
|
|
1629
|
+
if (!signatureHexSchema.test(signature)) {
|
|
1630
|
+
return void 0;
|
|
1631
|
+
}
|
|
1632
|
+
const parsed = Buffer.from(signature, "hex");
|
|
1633
|
+
return parsed.length === SIGNATURE_HEX_BYTES ? parsed : void 0;
|
|
1634
|
+
}
|
|
1635
|
+
function signatureMatches(signature, expected) {
|
|
1636
|
+
const parsed = readSignature(signature);
|
|
1637
|
+
return parsed === void 0 ? false : import_crypto2.default.timingSafeEqual(parsed, expected);
|
|
1638
|
+
}
|
|
1639
|
+
function assertSecretConfigured(secret) {
|
|
1640
|
+
if (secret.trim().length === 0) {
|
|
1641
|
+
throw new Error("Webhook signing secret is required");
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1501
1644
|
var RstreamWebhookResource = class {
|
|
1502
1645
|
async event(payload, header, secret, tolerance = 300, receivedAt = Date.now()) {
|
|
1503
1646
|
const payloadBuffer = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
|
|
@@ -1505,31 +1648,26 @@ var RstreamWebhookResource = class {
|
|
|
1505
1648
|
if (!signatureHeader) {
|
|
1506
1649
|
throw new Error("No signature header");
|
|
1507
1650
|
}
|
|
1651
|
+
assertSecretConfigured(secret);
|
|
1508
1652
|
const elements = signatureHeader.split(",").map((element) => element.trim());
|
|
1509
1653
|
const timestamp = elements.find((element) => element.startsWith("t="))?.substring(2);
|
|
1510
1654
|
const signatures = elements.filter((element) => element.startsWith("v1=")).map((element) => element.substring(3));
|
|
1511
1655
|
if (!timestamp || signatures.length === 0) {
|
|
1512
1656
|
throw new Error("Invalid signature header format");
|
|
1513
1657
|
}
|
|
1514
|
-
const timestampNum =
|
|
1658
|
+
const timestampNum = parseSignatureTimestamp(timestamp);
|
|
1659
|
+
const signatureTolerance = normalizeTolerance(tolerance);
|
|
1515
1660
|
const now = Math.floor(receivedAt / 1e3);
|
|
1516
|
-
if (Math.abs(now - timestampNum) >
|
|
1661
|
+
if (Math.abs(now - timestampNum) > signatureTolerance) {
|
|
1517
1662
|
throw new Error(
|
|
1518
1663
|
`Webhook signature timestamp outside tolerance: ${now}, ${timestampNum}`
|
|
1519
1664
|
);
|
|
1520
1665
|
}
|
|
1521
1666
|
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
|
-
});
|
|
1667
|
+
const expectedSignature = import_crypto2.default.createHmac("sha256", secret).update(signedPayload).digest();
|
|
1668
|
+
const signatureMatched = signatures.some(
|
|
1669
|
+
(signature) => signatureMatches(signature, expectedSignature)
|
|
1670
|
+
);
|
|
1533
1671
|
if (!signatureMatched) {
|
|
1534
1672
|
throw new Error("Signature verification failed");
|
|
1535
1673
|
}
|
|
@@ -1547,8 +1685,16 @@ var RstreamWebhookResource = class {
|
|
|
1547
1685
|
};
|
|
1548
1686
|
|
|
1549
1687
|
// src/tunnels.ts
|
|
1688
|
+
var engineAuthTokenScopes = {
|
|
1689
|
+
tunnels: {
|
|
1690
|
+
connect: true,
|
|
1691
|
+
create: true,
|
|
1692
|
+
list: true
|
|
1693
|
+
}
|
|
1694
|
+
};
|
|
1550
1695
|
var RstreamTunnelsClient = class {
|
|
1551
1696
|
config;
|
|
1697
|
+
managedProject;
|
|
1552
1698
|
constructor(config) {
|
|
1553
1699
|
this.config = config;
|
|
1554
1700
|
}
|
|
@@ -1568,6 +1714,21 @@ var RstreamTunnelsClient = class {
|
|
|
1568
1714
|
return this.config?.projectEndpoint;
|
|
1569
1715
|
}
|
|
1570
1716
|
async getEngine() {
|
|
1717
|
+
const projectEndpoint = normalizeOptionalString3(
|
|
1718
|
+
this.config?.projectEndpoint
|
|
1719
|
+
);
|
|
1720
|
+
if (projectEndpoint !== void 0 && normalizeOptionalString3(this.config?.engine) === void 0 && (0, import_rstream14.readEnvironment)().engine === void 0) {
|
|
1721
|
+
const project = await this.getManagedProject();
|
|
1722
|
+
if (project !== void 0) {
|
|
1723
|
+
const engine = (0, import_rstream12.getTunnelsProjectEngine)(project);
|
|
1724
|
+
if (engine) {
|
|
1725
|
+
return normalizeEngineAddress(engine);
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
throw new Error(
|
|
1729
|
+
"Failed to resolve the engine address from the managed tunnels project."
|
|
1730
|
+
);
|
|
1731
|
+
}
|
|
1571
1732
|
return await resolveTunnelsEngine({
|
|
1572
1733
|
apiUrl: this.config?.apiUrl,
|
|
1573
1734
|
controlPlaneCredentials: this.controlPlaneCredentials,
|
|
@@ -1576,6 +1737,57 @@ var RstreamTunnelsClient = class {
|
|
|
1576
1737
|
projectEndpoint: this.config?.projectEndpoint
|
|
1577
1738
|
});
|
|
1578
1739
|
}
|
|
1740
|
+
async getManagedProject() {
|
|
1741
|
+
const projectEndpoint = normalizeOptionalString3(
|
|
1742
|
+
this.config?.projectEndpoint
|
|
1743
|
+
);
|
|
1744
|
+
if (projectEndpoint === void 0) {
|
|
1745
|
+
return void 0;
|
|
1746
|
+
}
|
|
1747
|
+
if (this.managedProject === void 0) {
|
|
1748
|
+
this.managedProject = resolveManagedTunnelsProject({
|
|
1749
|
+
apiUrl: this.config?.apiUrl,
|
|
1750
|
+
controlPlaneCredentials: this.controlPlaneCredentials,
|
|
1751
|
+
credentials: this.credentials,
|
|
1752
|
+
projectEndpoint
|
|
1753
|
+
}).catch((error) => {
|
|
1754
|
+
this.managedProject = void 0;
|
|
1755
|
+
throw error;
|
|
1756
|
+
});
|
|
1757
|
+
}
|
|
1758
|
+
return await this.managedProject;
|
|
1759
|
+
}
|
|
1760
|
+
async getEngineAuthTokenParams() {
|
|
1761
|
+
const target = await this.getEngineAuthTokenTarget();
|
|
1762
|
+
return {
|
|
1763
|
+
tunnelsGrants: [
|
|
1764
|
+
{
|
|
1765
|
+
...target,
|
|
1766
|
+
scopes: engineAuthTokenScopes
|
|
1767
|
+
}
|
|
1768
|
+
]
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
async getEngineAuthTokenTarget() {
|
|
1772
|
+
const projectId = normalizeOptionalString3(this.config?.projectId);
|
|
1773
|
+
const workspaceId = normalizeOptionalString3(this.config?.workspaceId);
|
|
1774
|
+
if (projectId !== void 0 && workspaceId !== void 0) {
|
|
1775
|
+
throw new Error("Use either projectId or workspaceId, not both.");
|
|
1776
|
+
}
|
|
1777
|
+
if (projectId !== void 0) {
|
|
1778
|
+
return { projects: [projectId] };
|
|
1779
|
+
}
|
|
1780
|
+
if (workspaceId !== void 0) {
|
|
1781
|
+
return { workspaces: [workspaceId] };
|
|
1782
|
+
}
|
|
1783
|
+
const project = await this.getManagedProject();
|
|
1784
|
+
if (project !== void 0) {
|
|
1785
|
+
return { projects: [project.id] };
|
|
1786
|
+
}
|
|
1787
|
+
throw new Error(
|
|
1788
|
+
"Application credentials require projectId, workspaceId, or projectEndpoint to create scoped engine tokens."
|
|
1789
|
+
);
|
|
1790
|
+
}
|
|
1579
1791
|
get auth() {
|
|
1580
1792
|
return new RstreamAuthResource(this);
|
|
1581
1793
|
}
|
|
@@ -1596,16 +1808,20 @@ var RstreamTunnelsClient = class {
|
|
|
1596
1808
|
if (!credentials) {
|
|
1597
1809
|
return void 0;
|
|
1598
1810
|
}
|
|
1599
|
-
if (!(0,
|
|
1811
|
+
if (!(0, import_rstream13.isClientCredentials)(credentials)) {
|
|
1600
1812
|
return credentials.token;
|
|
1601
1813
|
}
|
|
1602
|
-
|
|
1814
|
+
const tokenParams = await this.getEngineAuthTokenParams();
|
|
1815
|
+
return (await this.auth.createAuthToken(tokenParams, {
|
|
1603
1816
|
credentials,
|
|
1604
1817
|
engine
|
|
1605
1818
|
})).token;
|
|
1606
1819
|
}
|
|
1607
1820
|
async request(path, options) {
|
|
1608
|
-
const engine = await this.getEngine();
|
|
1821
|
+
const engine = normalizeEngineAddress(await this.getEngine());
|
|
1822
|
+
if (!path.startsWith("/") || path.startsWith("//")) {
|
|
1823
|
+
throw new Error("Engine request path must be a relative absolute path.");
|
|
1824
|
+
}
|
|
1609
1825
|
const url = `https://${engine}/api${path}`;
|
|
1610
1826
|
const headers = new Headers(options?.headers || {});
|
|
1611
1827
|
const token = await this.getToken(engine);
|
|
@@ -1614,7 +1830,8 @@ var RstreamTunnelsClient = class {
|
|
|
1614
1830
|
}
|
|
1615
1831
|
const response = await fetch(url, {
|
|
1616
1832
|
...options,
|
|
1617
|
-
headers
|
|
1833
|
+
headers,
|
|
1834
|
+
redirect: options?.redirect ?? "manual"
|
|
1618
1835
|
});
|
|
1619
1836
|
if (!response.ok) {
|
|
1620
1837
|
const errorText = await response.text();
|
|
@@ -1623,8 +1840,68 @@ var RstreamTunnelsClient = class {
|
|
|
1623
1840
|
return await response.json();
|
|
1624
1841
|
}
|
|
1625
1842
|
};
|
|
1843
|
+
function normalizeOptionalString3(value) {
|
|
1844
|
+
const normalized = value?.trim();
|
|
1845
|
+
return normalized ? normalized : void 0;
|
|
1846
|
+
}
|
|
1626
1847
|
|
|
1627
1848
|
// src/watch.ts
|
|
1849
|
+
var import_jsonwebtoken = __toESM(require("jsonwebtoken"));
|
|
1850
|
+
var maxWatchTokenLifetimeSeconds = 3600;
|
|
1851
|
+
var watchTokenIssuedAtSkewSeconds = 300;
|
|
1852
|
+
function normalizeTransport(transport) {
|
|
1853
|
+
if (transport === void 0) {
|
|
1854
|
+
return "sse";
|
|
1855
|
+
}
|
|
1856
|
+
if (transport === "sse" || transport === "websocket") {
|
|
1857
|
+
return transport;
|
|
1858
|
+
}
|
|
1859
|
+
throw new Error("Watch: Unsupported transport.");
|
|
1860
|
+
}
|
|
1861
|
+
function validateWatchQueryToken(token) {
|
|
1862
|
+
const parsed = authTokenSchema.safeParse(import_jsonwebtoken.default.decode(token));
|
|
1863
|
+
if (!parsed.success || parsed.data.type === "pat") {
|
|
1864
|
+
throw new Error(
|
|
1865
|
+
"Watch requires a short-lived auth or app token for URL-based authentication."
|
|
1866
|
+
);
|
|
1867
|
+
}
|
|
1868
|
+
const iat = parsed.data.iat;
|
|
1869
|
+
const exp = parsed.data.exp;
|
|
1870
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1871
|
+
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) {
|
|
1872
|
+
throw new Error(
|
|
1873
|
+
"Watch requires a token with a bounded lifetime of at most 3600 seconds."
|
|
1874
|
+
);
|
|
1875
|
+
}
|
|
1876
|
+
if (!hasWatchOnlyTunnelGrants(parsed.data)) {
|
|
1877
|
+
throw new Error(
|
|
1878
|
+
"Watch requires a fine-grained token limited to tunnel list grants for URL-based authentication."
|
|
1879
|
+
);
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
function hasWatchOnlyTunnelGrants(token) {
|
|
1883
|
+
if (!hasWatchOnlyPermissions(token.permissions)) {
|
|
1884
|
+
return false;
|
|
1885
|
+
}
|
|
1886
|
+
if (!Array.isArray(token.tunnelsGrants) || token.tunnelsGrants.length === 0) {
|
|
1887
|
+
return false;
|
|
1888
|
+
}
|
|
1889
|
+
return token.tunnelsGrants.every((grant) => {
|
|
1890
|
+
const tunnels = grant.scopes?.tunnels;
|
|
1891
|
+
if (tunnels === void 0) {
|
|
1892
|
+
return false;
|
|
1893
|
+
}
|
|
1894
|
+
return tunnels.list !== void 0 && tunnels.create === void 0 && tunnels.connect === void 0;
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
function hasWatchOnlyPermissions(permissions) {
|
|
1898
|
+
if (!Array.isArray(permissions)) {
|
|
1899
|
+
return true;
|
|
1900
|
+
}
|
|
1901
|
+
return permissions.every(
|
|
1902
|
+
(permission) => permission === "tunnels.resources.read-only" || permission === "network.resources.read-only"
|
|
1903
|
+
);
|
|
1904
|
+
}
|
|
1628
1905
|
var Watch = class {
|
|
1629
1906
|
connection = null;
|
|
1630
1907
|
connectionState = "preparing";
|
|
@@ -1639,45 +1916,56 @@ var Watch = class {
|
|
|
1639
1916
|
throw new Error("Watch: Connection already started or closed.");
|
|
1640
1917
|
}
|
|
1641
1918
|
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();
|
|
1919
|
+
try {
|
|
1920
|
+
const token = typeof this.config.auth === "function" ? await this.config.auth() : this.config.auth;
|
|
1921
|
+
validateWatchQueryToken(token);
|
|
1922
|
+
const engine = await resolveTunnelsEngine({
|
|
1923
|
+
apiUrl: this.config.apiUrl,
|
|
1924
|
+
controlPlaneCredentials: this.config.controlPlaneCredentials,
|
|
1925
|
+
engine: this.config.engine,
|
|
1926
|
+
projectEndpoint: this.config.projectEndpoint,
|
|
1927
|
+
token
|
|
1928
|
+
});
|
|
1929
|
+
const transport = normalizeTransport(this.config.transport);
|
|
1930
|
+
const base = `https://${engine}`;
|
|
1931
|
+
if (transport === "sse") {
|
|
1932
|
+
const url = new URL(`/api/sse`, base);
|
|
1933
|
+
url.searchParams.set("rstream.token", token);
|
|
1934
|
+
this.connection = new EventSource(url.toString());
|
|
1935
|
+
} else {
|
|
1936
|
+
const url = new URL(`/api/websocket`, base);
|
|
1937
|
+
url.searchParams.set("rstream.token", token);
|
|
1938
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
1939
|
+
this.connection = new WebSocket(url.toString());
|
|
1673
1940
|
}
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1941
|
+
this.connection.onopen = () => {
|
|
1942
|
+
this.connectionState = "connected";
|
|
1943
|
+
this.events.onConnect?.();
|
|
1944
|
+
};
|
|
1945
|
+
this.connection.onmessage = (msg) => this.handleMessage(msg.data);
|
|
1946
|
+
this.connection.onerror = () => {
|
|
1677
1947
|
if (this.connectionState !== "closed") {
|
|
1678
1948
|
this.disconnect();
|
|
1679
1949
|
}
|
|
1680
1950
|
};
|
|
1951
|
+
if (this.connection instanceof WebSocket) {
|
|
1952
|
+
this.connection.onclose = () => {
|
|
1953
|
+
if (this.connectionState !== "closed") {
|
|
1954
|
+
this.disconnect();
|
|
1955
|
+
}
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
} catch (error) {
|
|
1959
|
+
this.connectionState = "preparing";
|
|
1960
|
+
throw error;
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
handleMessage(data) {
|
|
1964
|
+
try {
|
|
1965
|
+
const parsed = wsEventsSchema.parse(JSON.parse(data));
|
|
1966
|
+
this.events.onEvent?.(parsed);
|
|
1967
|
+
} catch {
|
|
1968
|
+
this.disconnect();
|
|
1681
1969
|
}
|
|
1682
1970
|
}
|
|
1683
1971
|
disconnect() {
|
|
@@ -1798,8 +2086,10 @@ function parseWebTTYServers(tunnels) {
|
|
|
1798
2086
|
listClientsResponseSchema,
|
|
1799
2087
|
listTunnelsParamsSchema,
|
|
1800
2088
|
listTunnelsResponseSchema,
|
|
2089
|
+
normalizeEngineAddress,
|
|
1801
2090
|
parseWebTTYServers,
|
|
1802
2091
|
resolveControlPlaneCredentials,
|
|
2092
|
+
resolveManagedTunnelsProject,
|
|
1803
2093
|
resolveTunnelsAPIURL,
|
|
1804
2094
|
resolveTunnelsCredentials,
|
|
1805
2095
|
resolveTunnelsEngine,
|