@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.mjs
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
tunnelSchema,
|
|
17
17
|
webhookEventsSchema,
|
|
18
18
|
wsEventsSchema
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-TVDZNVSF.mjs";
|
|
20
20
|
|
|
21
21
|
// src/auth-resource.ts
|
|
22
22
|
import { createClientCredentialsToken } from "@rstreamlabs/rstream";
|
|
@@ -25,9 +25,9 @@ import { createClientCredentialsToken } from "@rstreamlabs/rstream";
|
|
|
25
25
|
import * as z from "zod";
|
|
26
26
|
var StringFilter = z.union([
|
|
27
27
|
z.string(),
|
|
28
|
-
z.object({ exact: z.string() }),
|
|
29
|
-
z.object({ oneof: z.array(z.string()) }),
|
|
30
|
-
z.object({ regex: z.string() })
|
|
28
|
+
z.object({ exact: z.string() }).strict(),
|
|
29
|
+
z.object({ oneof: z.array(z.string()) }).strict(),
|
|
30
|
+
z.object({ regex: z.string() }).strict()
|
|
31
31
|
]);
|
|
32
32
|
function transform(field) {
|
|
33
33
|
if (field instanceof z.ZodOptional) {
|
|
@@ -61,7 +61,7 @@ function select(base) {
|
|
|
61
61
|
return [key, z.boolean().optional()];
|
|
62
62
|
});
|
|
63
63
|
const shape = Object.fromEntries(entries);
|
|
64
|
-
return z.object(shape);
|
|
64
|
+
return z.object(shape).strict();
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
// src/auth.ts
|
|
@@ -88,7 +88,7 @@ var authTokenTunnelsScopesSchema = z2.object({
|
|
|
88
88
|
creation_date: true
|
|
89
89
|
})
|
|
90
90
|
).optional()
|
|
91
|
-
})
|
|
91
|
+
}).strict()
|
|
92
92
|
]).optional(),
|
|
93
93
|
// Scopes for connecting to tunnels
|
|
94
94
|
connect: z2.union([
|
|
@@ -100,7 +100,7 @@ var authTokenTunnelsScopesSchema = z2.object({
|
|
|
100
100
|
path: z2.string().optional()
|
|
101
101
|
})
|
|
102
102
|
).optional()
|
|
103
|
-
})
|
|
103
|
+
}).strict()
|
|
104
104
|
]).optional(),
|
|
105
105
|
// Scopes for listing tunnels
|
|
106
106
|
list: z2.union([
|
|
@@ -108,14 +108,29 @@ var authTokenTunnelsScopesSchema = z2.object({
|
|
|
108
108
|
z2.object({
|
|
109
109
|
filters: filters(tunnelSchema).optional(),
|
|
110
110
|
select: select(tunnelSchema).optional()
|
|
111
|
-
})
|
|
111
|
+
}).strict()
|
|
112
112
|
]).optional()
|
|
113
|
+
}).strict().superRefine((scopes, ctx) => {
|
|
114
|
+
if (scopes.create === void 0 && scopes.connect === void 0 && scopes.list === void 0) {
|
|
115
|
+
ctx.addIssue({
|
|
116
|
+
code: z2.ZodIssueCode.custom,
|
|
117
|
+
message: "At least one tunnel scope action is required."
|
|
118
|
+
});
|
|
119
|
+
}
|
|
113
120
|
});
|
|
114
121
|
var authTokenScopesSchema = z2.object({
|
|
115
122
|
tunnels: authTokenTunnelsScopesSchema.optional()
|
|
116
123
|
// Scopes related to tunnels
|
|
124
|
+
}).strict().superRefine((scopes, ctx) => {
|
|
125
|
+
if (scopes.tunnels === void 0) {
|
|
126
|
+
ctx.addIssue({
|
|
127
|
+
code: z2.ZodIssueCode.custom,
|
|
128
|
+
message: "Tunnel scopes are required.",
|
|
129
|
+
path: ["tunnels"]
|
|
130
|
+
});
|
|
131
|
+
}
|
|
117
132
|
});
|
|
118
|
-
var
|
|
133
|
+
var authTokenTunnelGrantLeafSchema = z2.object({
|
|
119
134
|
workspaces: z2.array(nonEmptyStringSchema).min(1).optional(),
|
|
120
135
|
projects: z2.array(nonEmptyStringSchema).min(1).optional(),
|
|
121
136
|
scopes: authTokenScopesSchema.optional()
|
|
@@ -127,11 +142,25 @@ var authTokenTunnelGrantSchema = z2.object({
|
|
|
127
142
|
path: ["projects"]
|
|
128
143
|
});
|
|
129
144
|
}
|
|
145
|
+
if (grant.workspaces === void 0 && grant.projects === void 0 && grant.scopes === void 0) {
|
|
146
|
+
ctx.addIssue({
|
|
147
|
+
code: z2.ZodIssueCode.custom,
|
|
148
|
+
message: "A grant must target workspaces, projects, or scopes.",
|
|
149
|
+
path: ["workspaces"]
|
|
150
|
+
});
|
|
151
|
+
}
|
|
130
152
|
});
|
|
153
|
+
var authTokenTunnelGrantSchema = z2.lazy(
|
|
154
|
+
() => z2.union([
|
|
155
|
+
authTokenTunnelGrantLeafSchema,
|
|
156
|
+
z2.object({ AND: z2.array(authTokenTunnelGrantSchema).min(1) }).strict(),
|
|
157
|
+
z2.object({ OR: z2.array(authTokenTunnelGrantSchema).min(1) }).strict()
|
|
158
|
+
])
|
|
159
|
+
);
|
|
131
160
|
var authTokenCommonSchema = {
|
|
132
161
|
aud: z2.union([z2.string(), z2.array(z2.string())]).optional(),
|
|
133
162
|
exp: z2.number().optional(),
|
|
134
|
-
tunnelsGrants:
|
|
163
|
+
tunnelsGrants: authTokenTunnelGrantSchema.optional(),
|
|
135
164
|
iat: z2.number().optional(),
|
|
136
165
|
iss: z2.string().optional(),
|
|
137
166
|
jti: z2.string().optional(),
|
|
@@ -150,6 +179,7 @@ var authTokenSchema = z2.union([
|
|
|
150
179
|
}).strict(),
|
|
151
180
|
z2.object({
|
|
152
181
|
type: z2.literal("pat"),
|
|
182
|
+
tokendpoint: z2.string().optional(),
|
|
153
183
|
token_endpoint: z2.string().optional(),
|
|
154
184
|
...authTokenCommonSchema
|
|
155
185
|
}).strict(),
|
|
@@ -162,10 +192,9 @@ var authTokenSchema = z2.union([
|
|
|
162
192
|
]);
|
|
163
193
|
var tokenSchema = authTokenSchema;
|
|
164
194
|
var createAuthTokenParamsSchema = z2.object({
|
|
165
|
-
expires_in: z2.number().default(60),
|
|
166
|
-
// 1 minute
|
|
195
|
+
expires_in: z2.number().int().min(1).max(3600).default(60),
|
|
167
196
|
scopes: authTokenScopesSchema.optional(),
|
|
168
|
-
tunnelsGrants:
|
|
197
|
+
tunnelsGrants: authTokenTunnelGrantSchema.optional(),
|
|
169
198
|
metadata: z2.unknown().optional()
|
|
170
199
|
// Additional metadata
|
|
171
200
|
}).strict().superRefine((params, ctx) => {
|
|
@@ -176,6 +205,13 @@ var createAuthTokenParamsSchema = z2.object({
|
|
|
176
205
|
path: ["tunnelsGrants"]
|
|
177
206
|
});
|
|
178
207
|
}
|
|
208
|
+
if (params.scopes === void 0 && params.tunnelsGrants === void 0) {
|
|
209
|
+
ctx.addIssue({
|
|
210
|
+
code: z2.ZodIssueCode.custom,
|
|
211
|
+
message: "Explicit scopes or tunnelsGrants are required.",
|
|
212
|
+
path: ["scopes"]
|
|
213
|
+
});
|
|
214
|
+
}
|
|
179
215
|
});
|
|
180
216
|
var createAuthTokenResponseSchema = z2.object({
|
|
181
217
|
token: z2.string()
|
|
@@ -196,18 +232,38 @@ var RstreamAuthResource = class {
|
|
|
196
232
|
);
|
|
197
233
|
}
|
|
198
234
|
const engine = options?.engine ?? await this.client.getEngine().catch(() => void 0);
|
|
235
|
+
const scopedOptions = await this.resolveScopeOptions(params, options);
|
|
199
236
|
return createAuthTokenFromClientCredentials(credentials, params, {
|
|
200
|
-
engine
|
|
237
|
+
engine,
|
|
238
|
+
...scopedOptions
|
|
201
239
|
});
|
|
202
240
|
}
|
|
241
|
+
async resolveScopeOptions(params, options) {
|
|
242
|
+
const tokenParams = createAuthTokenParamsSchema.parse(params);
|
|
243
|
+
if (options?.projectScoped === false) {
|
|
244
|
+
return options ?? {};
|
|
245
|
+
}
|
|
246
|
+
if (options?.projectId !== void 0 || options?.workspaceId !== void 0) {
|
|
247
|
+
return options;
|
|
248
|
+
}
|
|
249
|
+
if (tokenParams.scopes === void 0 && !tunnelGrantsNeedDefaultTarget(tokenParams.tunnelsGrants)) {
|
|
250
|
+
return options ?? {};
|
|
251
|
+
}
|
|
252
|
+
const target = await this.client.getEngineAuthTokenTarget();
|
|
253
|
+
return {
|
|
254
|
+
...options,
|
|
255
|
+
projectId: target.projects?.[0],
|
|
256
|
+
workspaceId: target.workspaces?.[0]
|
|
257
|
+
};
|
|
258
|
+
}
|
|
203
259
|
};
|
|
204
260
|
function createAuthTokenFromClientCredentials(credentials, params, options) {
|
|
205
|
-
const tokenParams = createAuthTokenParamsSchema.parse(params
|
|
261
|
+
const tokenParams = createAuthTokenParamsSchema.parse(params);
|
|
206
262
|
const payload = {
|
|
207
263
|
metadata: {
|
|
208
264
|
engine: options?.engine
|
|
209
265
|
},
|
|
210
|
-
tunnelsGrants: normalizeAuthTokenTunnelGrants(tokenParams),
|
|
266
|
+
tunnelsGrants: normalizeAuthTokenTunnelGrants(tokenParams, options),
|
|
211
267
|
permissions: null
|
|
212
268
|
};
|
|
213
269
|
const { token } = createClientCredentialsToken(credentials, {
|
|
@@ -216,21 +272,101 @@ function createAuthTokenFromClientCredentials(credentials, params, options) {
|
|
|
216
272
|
});
|
|
217
273
|
return { token };
|
|
218
274
|
}
|
|
219
|
-
function normalizeAuthTokenTunnelGrants(params) {
|
|
220
|
-
if (!params) {
|
|
221
|
-
return void 0;
|
|
222
|
-
}
|
|
275
|
+
function normalizeAuthTokenTunnelGrants(params, options) {
|
|
223
276
|
const tunnelsGrants = params.tunnelsGrants;
|
|
224
277
|
if (tunnelsGrants !== void 0) {
|
|
225
|
-
return
|
|
278
|
+
return applyDefaultTunnelGrantTarget(tunnelsGrants, options);
|
|
226
279
|
}
|
|
227
280
|
if (params.scopes) {
|
|
228
|
-
return
|
|
281
|
+
return { ...normalizeTunnelGrantTarget(options), scopes: params.scopes };
|
|
229
282
|
}
|
|
230
283
|
return void 0;
|
|
231
284
|
}
|
|
285
|
+
function applyDefaultTunnelGrantTarget(grants, options, inheritedTarget = false) {
|
|
286
|
+
if (options?.projectScoped === false) {
|
|
287
|
+
return grants;
|
|
288
|
+
}
|
|
289
|
+
if ("AND" in grants) {
|
|
290
|
+
const branchHasTarget = inheritedTarget || grants.AND.some((grant) => tunnelGrantTreeHasTarget(grant));
|
|
291
|
+
return {
|
|
292
|
+
AND: grants.AND.map(
|
|
293
|
+
(grant) => applyDefaultTunnelGrantTarget(grant, options, branchHasTarget)
|
|
294
|
+
)
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
if ("OR" in grants) {
|
|
298
|
+
return {
|
|
299
|
+
OR: grants.OR.map(
|
|
300
|
+
(grant) => applyDefaultTunnelGrantTarget(grant, options, inheritedTarget)
|
|
301
|
+
)
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
if (grants.scopes === void 0 || tunnelGrantHasTarget(grants) || inheritedTarget) {
|
|
305
|
+
return grants;
|
|
306
|
+
}
|
|
307
|
+
return { ...normalizeTunnelGrantTarget(options), scopes: grants.scopes };
|
|
308
|
+
}
|
|
309
|
+
function tunnelGrantsNeedDefaultTarget(grants, inheritedTarget = false) {
|
|
310
|
+
if (grants === void 0) {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
if ("AND" in grants) {
|
|
314
|
+
const branchHasTarget = inheritedTarget || grants.AND.some((grant) => tunnelGrantTreeHasTarget(grant));
|
|
315
|
+
return grants.AND.some(
|
|
316
|
+
(grant) => tunnelGrantsNeedDefaultTarget(grant, branchHasTarget)
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
if ("OR" in grants) {
|
|
320
|
+
return grants.OR.some(
|
|
321
|
+
(grant) => tunnelGrantsNeedDefaultTarget(grant, inheritedTarget)
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
return grants.scopes !== void 0 && !tunnelGrantHasTarget(grants) && !inheritedTarget;
|
|
325
|
+
}
|
|
326
|
+
function tunnelGrantHasTarget(grant) {
|
|
327
|
+
return grant.projects !== void 0 || grant.workspaces !== void 0;
|
|
328
|
+
}
|
|
329
|
+
function tunnelGrantTreeHasTarget(grants) {
|
|
330
|
+
if ("AND" in grants) {
|
|
331
|
+
return grants.AND.some((grant) => tunnelGrantTreeHasTarget(grant));
|
|
332
|
+
}
|
|
333
|
+
if ("OR" in grants) {
|
|
334
|
+
return grants.OR.some((grant) => tunnelGrantTreeHasTarget(grant));
|
|
335
|
+
}
|
|
336
|
+
return tunnelGrantHasTarget(grants);
|
|
337
|
+
}
|
|
338
|
+
function normalizeTunnelGrantTarget(options) {
|
|
339
|
+
const projectId = normalizeOptionalString(options?.projectId);
|
|
340
|
+
const workspaceId = normalizeOptionalString(options?.workspaceId);
|
|
341
|
+
if (projectId !== void 0 && workspaceId !== void 0) {
|
|
342
|
+
throw new Error("Use either projectId or workspaceId, not both.");
|
|
343
|
+
}
|
|
344
|
+
if (projectId !== void 0) {
|
|
345
|
+
return { projects: [projectId] };
|
|
346
|
+
}
|
|
347
|
+
if (workspaceId !== void 0) {
|
|
348
|
+
return { workspaces: [workspaceId] };
|
|
349
|
+
}
|
|
350
|
+
if (options?.projectScoped === false) {
|
|
351
|
+
return {};
|
|
352
|
+
}
|
|
353
|
+
throw new Error(
|
|
354
|
+
"Project ID or workspace ID is required when using scope-only tunnel grants."
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
function normalizeOptionalString(value) {
|
|
358
|
+
const normalized = value?.trim();
|
|
359
|
+
return normalized ? normalized : void 0;
|
|
360
|
+
}
|
|
232
361
|
|
|
233
362
|
// src/clients-resource.ts
|
|
363
|
+
function clientPathId(id) {
|
|
364
|
+
const normalized = id.trim();
|
|
365
|
+
if (!normalized) {
|
|
366
|
+
throw new Error("Client ID is required.");
|
|
367
|
+
}
|
|
368
|
+
return encodeURIComponent(normalized);
|
|
369
|
+
}
|
|
234
370
|
var RstreamClientsResource = class {
|
|
235
371
|
client;
|
|
236
372
|
constructor(client) {
|
|
@@ -244,9 +380,12 @@ var RstreamClientsResource = class {
|
|
|
244
380
|
return listClientsResponseSchema.parse(response);
|
|
245
381
|
}
|
|
246
382
|
async get(id) {
|
|
247
|
-
const response = await this.client.request(
|
|
248
|
-
|
|
249
|
-
|
|
383
|
+
const response = await this.client.request(
|
|
384
|
+
`/clients/${clientPathId(id)}`,
|
|
385
|
+
{
|
|
386
|
+
method: "GET"
|
|
387
|
+
}
|
|
388
|
+
);
|
|
250
389
|
return clientSchema.parse(response);
|
|
251
390
|
}
|
|
252
391
|
};
|
|
@@ -257,22 +396,37 @@ import { getTunnelsProjectEngine } from "@rstreamlabs/rstream";
|
|
|
257
396
|
import { isTokenCredentials } from "@rstreamlabs/rstream";
|
|
258
397
|
import { readEnvironment } from "@rstreamlabs/rstream";
|
|
259
398
|
import { RstreamClient } from "@rstreamlabs/rstream";
|
|
260
|
-
import jwt from "jsonwebtoken";
|
|
261
399
|
function trimOptionalString(value) {
|
|
262
400
|
const normalized = value?.trim();
|
|
263
401
|
return normalized ? normalized : void 0;
|
|
264
402
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
403
|
+
var dnsLabelPattern = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i;
|
|
404
|
+
function validateHostname(hostname) {
|
|
405
|
+
if (!hostname || hostname.length > 253 || hostname.includes("..")) {
|
|
406
|
+
return false;
|
|
268
407
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
408
|
+
return hostname.split(".").every((label) => label === "localhost" || dnsLabelPattern.test(label));
|
|
409
|
+
}
|
|
410
|
+
function normalizeEngineAddress(engine) {
|
|
411
|
+
const normalized = engine.trim();
|
|
412
|
+
if (!normalized || /^[a-z][a-z0-9+.-]*:\/\//i.test(normalized) || /[/?#@\\\s]/.test(normalized)) {
|
|
413
|
+
throw new Error("Engine must be a host[:port] value.");
|
|
414
|
+
}
|
|
415
|
+
const explicitPort = normalized.match(/:(\d+)$/)?.[1];
|
|
416
|
+
const url = new URL(`https://${normalized}`);
|
|
417
|
+
if (url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
|
|
418
|
+
throw new Error("Engine must be a host[:port] value.");
|
|
419
|
+
}
|
|
420
|
+
if (!validateHostname(url.hostname)) {
|
|
421
|
+
throw new Error("Engine must use a valid hostname.");
|
|
422
|
+
}
|
|
423
|
+
if (explicitPort) {
|
|
424
|
+
const port = Number(explicitPort);
|
|
425
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
426
|
+
throw new Error("Engine port must be an integer between 1 and 65535.");
|
|
427
|
+
}
|
|
274
428
|
}
|
|
275
|
-
return
|
|
429
|
+
return `${url.hostname.toLowerCase()}${explicitPort ? `:${explicitPort}` : ""}`;
|
|
276
430
|
}
|
|
277
431
|
function resolveTunnelsAPIURL(apiUrl) {
|
|
278
432
|
return trimOptionalString(apiUrl) ?? readEnvironment().apiUrl ?? DEFAULT_API_URL;
|
|
@@ -306,48 +460,61 @@ function resolveControlPlaneCredentials(controlPlaneCredentials, credentials, au
|
|
|
306
460
|
async function resolveTunnelsEngine(config) {
|
|
307
461
|
const explicitEngine = trimOptionalString(config.engine);
|
|
308
462
|
if (explicitEngine) {
|
|
309
|
-
return explicitEngine;
|
|
463
|
+
return normalizeEngineAddress(explicitEngine);
|
|
310
464
|
}
|
|
311
465
|
const environmentEngine = readEnvironment().engine;
|
|
312
466
|
if (environmentEngine) {
|
|
313
|
-
return environmentEngine;
|
|
467
|
+
return normalizeEngineAddress(environmentEngine);
|
|
314
468
|
}
|
|
315
469
|
const token = trimOptionalString(config.token) ?? (isTokenCredentials(config.credentials) ? config.credentials.token : void 0);
|
|
316
|
-
const tokenEngine = getEngineFromToken(token);
|
|
317
|
-
if (tokenEngine) {
|
|
318
|
-
return tokenEngine;
|
|
319
|
-
}
|
|
320
470
|
const projectEndpoint = trimOptionalString(config.projectEndpoint);
|
|
321
471
|
if (projectEndpoint) {
|
|
322
|
-
const
|
|
323
|
-
config
|
|
324
|
-
config.credentials,
|
|
472
|
+
const project = await resolveManagedTunnelsProject({
|
|
473
|
+
...config,
|
|
325
474
|
token
|
|
326
|
-
);
|
|
327
|
-
if (credentials === void 0) {
|
|
328
|
-
throw new Error(
|
|
329
|
-
"Control-plane credentials are required to resolve a managed project endpoint."
|
|
330
|
-
);
|
|
331
|
-
}
|
|
332
|
-
const controlPlane = new RstreamClient({
|
|
333
|
-
apiUrl: resolveTunnelsAPIURL(config.apiUrl),
|
|
334
|
-
credentials
|
|
335
475
|
});
|
|
336
|
-
const project = await controlPlane.tunnels.projects.resolveByEndpoint(projectEndpoint);
|
|
337
476
|
const engine = getTunnelsProjectEngine(project);
|
|
338
477
|
if (engine) {
|
|
339
|
-
return engine;
|
|
478
|
+
return normalizeEngineAddress(engine);
|
|
340
479
|
}
|
|
341
480
|
throw new Error(
|
|
342
481
|
"Failed to resolve the engine address from the managed tunnels project."
|
|
343
482
|
);
|
|
344
483
|
}
|
|
345
484
|
throw new Error(
|
|
346
|
-
"Engine URL is not defined. Provide an engine, use RSTREAM_ENGINE, or configure a managed project endpoint with
|
|
485
|
+
"Engine URL is not defined. Provide an engine, use RSTREAM_ENGINE, or configure a managed project endpoint with Control plane API access."
|
|
347
486
|
);
|
|
348
487
|
}
|
|
488
|
+
async function resolveManagedTunnelsProject(config) {
|
|
489
|
+
const projectEndpoint = trimOptionalString(config.projectEndpoint);
|
|
490
|
+
if (!projectEndpoint) {
|
|
491
|
+
throw new Error("Managed project endpoint is required.");
|
|
492
|
+
}
|
|
493
|
+
const credentials = resolveControlPlaneCredentials(
|
|
494
|
+
config.controlPlaneCredentials,
|
|
495
|
+
config.credentials,
|
|
496
|
+
config.token
|
|
497
|
+
);
|
|
498
|
+
if (credentials === void 0) {
|
|
499
|
+
throw new Error(
|
|
500
|
+
"Control-plane credentials are required to resolve a managed project endpoint."
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
const controlPlane = new RstreamClient({
|
|
504
|
+
apiUrl: resolveTunnelsAPIURL(config.apiUrl),
|
|
505
|
+
credentials
|
|
506
|
+
});
|
|
507
|
+
return await controlPlane.tunnels.projects.resolveByEndpoint(projectEndpoint);
|
|
508
|
+
}
|
|
349
509
|
|
|
350
510
|
// src/tunnels-resource.ts
|
|
511
|
+
function tunnelPathId(id) {
|
|
512
|
+
const normalized = id.trim();
|
|
513
|
+
if (!normalized) {
|
|
514
|
+
throw new Error("Tunnel ID is required.");
|
|
515
|
+
}
|
|
516
|
+
return encodeURIComponent(normalized);
|
|
517
|
+
}
|
|
351
518
|
var RstreamTunnelsResource = class {
|
|
352
519
|
client;
|
|
353
520
|
constructor(client) {
|
|
@@ -361,15 +528,20 @@ var RstreamTunnelsResource = class {
|
|
|
361
528
|
return listTunnelsResponseSchema.parse(response);
|
|
362
529
|
}
|
|
363
530
|
async get(id) {
|
|
364
|
-
const response = await this.client.request(
|
|
365
|
-
|
|
366
|
-
|
|
531
|
+
const response = await this.client.request(
|
|
532
|
+
`/tunnels/${tunnelPathId(id)}`,
|
|
533
|
+
{
|
|
534
|
+
method: "GET"
|
|
535
|
+
}
|
|
536
|
+
);
|
|
367
537
|
return tunnelSchema.parse(response);
|
|
368
538
|
}
|
|
369
539
|
};
|
|
370
540
|
|
|
371
541
|
// src/tunnels.ts
|
|
542
|
+
import { getTunnelsProjectEngine as getTunnelsProjectEngine2 } from "@rstreamlabs/rstream";
|
|
372
543
|
import { isClientCredentials as isClientCredentials3 } from "@rstreamlabs/rstream";
|
|
544
|
+
import { readEnvironment as readEnvironment2 } from "@rstreamlabs/rstream";
|
|
373
545
|
|
|
374
546
|
// src/turn.ts
|
|
375
547
|
import { isClientCredentials as isClientCredentials2 } from "@rstreamlabs/rstream";
|
|
@@ -380,46 +552,53 @@ import * as z3 from "zod";
|
|
|
380
552
|
import crypto from "crypto";
|
|
381
553
|
var defaultTURNPort = 3478;
|
|
382
554
|
var defaultTURNSPort = 5349;
|
|
383
|
-
var defaultTURNCredentialTTLSeconds =
|
|
555
|
+
var defaultTURNCredentialTTLSeconds = 10 * 60;
|
|
556
|
+
var maxTURNCredentialTTLSeconds = 60 * 60;
|
|
384
557
|
var turnTokenClaimsSchema = z3.object({
|
|
385
|
-
|
|
386
|
-
|
|
558
|
+
exp: z3.number().int().optional(),
|
|
559
|
+
iat: z3.number().int().optional(),
|
|
560
|
+
token_endpoint: z3.string().regex(/^[0-9a-f]{8}$/i).optional(),
|
|
561
|
+
tokendpoint: z3.string().regex(/^[0-9a-f]{8}$/i).optional(),
|
|
562
|
+
type: z3.enum(["app", "auth", "pat"])
|
|
387
563
|
});
|
|
388
564
|
var turnCredentialModeSchema = z3.enum(["api", "app", "pat"]);
|
|
389
|
-
function
|
|
565
|
+
function normalizeOptionalString2(value) {
|
|
390
566
|
const normalized = value?.trim();
|
|
391
567
|
return normalized ? normalized : void 0;
|
|
392
568
|
}
|
|
393
569
|
function normalizeClusterDomain(clusterDomain) {
|
|
394
|
-
const normalized =
|
|
570
|
+
const normalized = normalizeOptionalString2(clusterDomain);
|
|
395
571
|
return normalized?.toLowerCase();
|
|
396
572
|
}
|
|
397
573
|
function normalizeNow(now) {
|
|
398
|
-
if (now
|
|
399
|
-
return Math.floor(now
|
|
574
|
+
if (now === void 0) {
|
|
575
|
+
return Math.floor(Date.now() / 1e3);
|
|
400
576
|
}
|
|
401
|
-
|
|
402
|
-
|
|
577
|
+
const timestamp = now instanceof Date ? now.getTime() / 1e3 : now;
|
|
578
|
+
if (!Number.isFinite(timestamp)) {
|
|
579
|
+
throw new Error("TURN timestamp must be finite.");
|
|
403
580
|
}
|
|
404
|
-
return Math.floor(
|
|
581
|
+
return Math.floor(timestamp);
|
|
405
582
|
}
|
|
406
583
|
function normalizeTTLSeconds(ttlSeconds) {
|
|
407
|
-
|
|
408
|
-
|
|
584
|
+
const ttl = ttlSeconds ?? defaultTURNCredentialTTLSeconds;
|
|
585
|
+
if (!Number.isInteger(ttl) || ttl < 1 || ttl > maxTURNCredentialTTLSeconds) {
|
|
586
|
+
throw new Error("TURN TTL must be an integer between 1 and 3600 seconds.");
|
|
409
587
|
}
|
|
410
|
-
return
|
|
588
|
+
return ttl;
|
|
411
589
|
}
|
|
412
|
-
function
|
|
413
|
-
|
|
414
|
-
|
|
590
|
+
function normalizePort(port, fallback, name) {
|
|
591
|
+
const normalized = port ?? fallback;
|
|
592
|
+
if (!Number.isInteger(normalized) || normalized < 1 || normalized > 65535) {
|
|
593
|
+
throw new Error(`${name} must be an integer between 1 and 65535.`);
|
|
415
594
|
}
|
|
416
|
-
return
|
|
595
|
+
return normalized;
|
|
596
|
+
}
|
|
597
|
+
function normalizeTURNPort(port) {
|
|
598
|
+
return normalizePort(port, defaultTURNPort, "TURN port");
|
|
417
599
|
}
|
|
418
600
|
function normalizeTURNSPort(port) {
|
|
419
|
-
|
|
420
|
-
return Math.floor(port);
|
|
421
|
-
}
|
|
422
|
-
return defaultTURNSPort;
|
|
601
|
+
return normalizePort(port, defaultTURNSPort, "TURNS port");
|
|
423
602
|
}
|
|
424
603
|
function createTURNURLs(clusterDomain, turnPort, turnsPort) {
|
|
425
604
|
const normalizedTURNPort = normalizeTURNPort(turnPort);
|
|
@@ -437,16 +616,32 @@ function parseTURNTokenClaims(token) {
|
|
|
437
616
|
if (!payloadPart) {
|
|
438
617
|
throw new Error("Invalid token format.");
|
|
439
618
|
}
|
|
440
|
-
|
|
441
|
-
|
|
619
|
+
let decoded;
|
|
620
|
+
try {
|
|
621
|
+
const payload = Buffer.from(payloadPart, "base64url").toString("utf8");
|
|
622
|
+
decoded = JSON.parse(payload);
|
|
623
|
+
} catch {
|
|
624
|
+
throw new Error("Invalid token format.");
|
|
625
|
+
}
|
|
626
|
+
const parsed = turnTokenClaimsSchema.safeParse(decoded);
|
|
442
627
|
if (!parsed.success) {
|
|
443
628
|
throw new Error("Invalid token format.");
|
|
444
629
|
}
|
|
445
630
|
return parsed.data;
|
|
446
631
|
}
|
|
632
|
+
function requirePATExpiration(claims, now) {
|
|
633
|
+
const exp = claims.exp;
|
|
634
|
+
if (typeof exp !== "number") {
|
|
635
|
+
throw new Error("TURN PAT mode requires a PAT token with expiration.");
|
|
636
|
+
}
|
|
637
|
+
if (exp <= now) {
|
|
638
|
+
throw new Error("TURN PAT mode requires a non-expired PAT token.");
|
|
639
|
+
}
|
|
640
|
+
return exp;
|
|
641
|
+
}
|
|
447
642
|
function deriveClusterDomainFromEngine(projectEndpoint, engine) {
|
|
448
|
-
const normalizedProjectEndpoint =
|
|
449
|
-
const normalizedEngine =
|
|
643
|
+
const normalizedProjectEndpoint = normalizeOptionalString2(projectEndpoint);
|
|
644
|
+
const normalizedEngine = normalizeOptionalString2(engine);
|
|
450
645
|
if (!normalizedProjectEndpoint || !normalizedEngine) {
|
|
451
646
|
throw new Error(
|
|
452
647
|
"Failed to derive the cluster domain from the engine address."
|
|
@@ -480,17 +675,11 @@ function resolveMode(credentials, mode) {
|
|
|
480
675
|
if (isClientCredentials2(credentials)) {
|
|
481
676
|
return "app";
|
|
482
677
|
}
|
|
483
|
-
if (isTokenCredentials2(credentials)) {
|
|
484
|
-
const claims = parseTURNTokenClaims(credentials.token);
|
|
485
|
-
if (claims.type === "pat" && normalizeOptionalString(claims.token_endpoint)) {
|
|
486
|
-
return "pat";
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
678
|
return "api";
|
|
490
679
|
}
|
|
491
680
|
async function loadTURNServerPublicKeyHex(clusterDomain, options) {
|
|
492
|
-
const configuredKey =
|
|
493
|
-
const keyringBaseURL = configuredKey ??
|
|
681
|
+
const configuredKey = normalizeOptionalString2(options.keyringBaseUrl);
|
|
682
|
+
const keyringBaseURL = configuredKey ?? normalizeOptionalString2(options.apiUrl) ?? "https://rstream.io";
|
|
494
683
|
const url = new URL(
|
|
495
684
|
`/keyrings/turn/${clusterDomain}.spki.der.hex`,
|
|
496
685
|
keyringBaseURL
|
|
@@ -502,8 +691,8 @@ async function loadTURNServerPublicKeyHex(clusterDomain, options) {
|
|
|
502
691
|
return (await response.text()).trim();
|
|
503
692
|
}
|
|
504
693
|
async function createAPITURNCredentials(options) {
|
|
505
|
-
const projectId =
|
|
506
|
-
const projectEndpoint =
|
|
694
|
+
const projectId = normalizeOptionalString2(options.projectId);
|
|
695
|
+
const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
|
|
507
696
|
if (!projectId && !projectEndpoint) {
|
|
508
697
|
throw new Error(
|
|
509
698
|
"Project ID or project endpoint is required for TURN API mode."
|
|
@@ -530,11 +719,11 @@ function createPATTURNCredentials(options) {
|
|
|
530
719
|
if (!clusterDomain) {
|
|
531
720
|
throw new Error("Cluster domain is required for TURN PAT mode.");
|
|
532
721
|
}
|
|
533
|
-
const projectEndpoint =
|
|
722
|
+
const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
|
|
534
723
|
if (!projectEndpoint) {
|
|
535
724
|
throw new Error("Project endpoint is required for TURN PAT mode.");
|
|
536
725
|
}
|
|
537
|
-
const token =
|
|
726
|
+
const token = normalizeOptionalString2(options.token);
|
|
538
727
|
if (!token) {
|
|
539
728
|
throw new Error("Token is required for TURN PAT mode.");
|
|
540
729
|
}
|
|
@@ -542,14 +731,18 @@ function createPATTURNCredentials(options) {
|
|
|
542
731
|
if (claims.type !== "pat") {
|
|
543
732
|
throw new Error("TURN PAT mode requires a PAT token.");
|
|
544
733
|
}
|
|
545
|
-
const tokenEndpoint =
|
|
734
|
+
const tokenEndpoint = normalizeOptionalString2(options.tokenEndpoint);
|
|
546
735
|
if (!tokenEndpoint) {
|
|
547
|
-
throw new Error(
|
|
548
|
-
|
|
549
|
-
|
|
736
|
+
throw new Error("Token endpoint is required for TURN PAT mode.");
|
|
737
|
+
}
|
|
738
|
+
const claimsTokenEndpoint = claims.token_endpoint ?? claims.tokendpoint;
|
|
739
|
+
if (claimsTokenEndpoint !== void 0 && claimsTokenEndpoint !== tokenEndpoint) {
|
|
740
|
+
throw new Error("Token endpoint does not match the PAT token.");
|
|
550
741
|
}
|
|
742
|
+
const now = normalizeNow(options.now);
|
|
551
743
|
const ttlSeconds = normalizeTTLSeconds(options.ttlSeconds);
|
|
552
|
-
const exp =
|
|
744
|
+
const exp = Math.min(now + ttlSeconds, requirePATExpiration(claims, now));
|
|
745
|
+
const ttl = exp - now;
|
|
553
746
|
const username = `v1:${exp}:pat:${projectEndpoint}:${tokenEndpoint}`;
|
|
554
747
|
const tokenHash = crypto.createHash("sha256").update(token, "utf8").digest();
|
|
555
748
|
const key = Buffer.from(
|
|
@@ -564,7 +757,7 @@ function createPATTURNCredentials(options) {
|
|
|
564
757
|
const credential = crypto.createHmac("sha256", key).update(username, "utf8").digest("base64");
|
|
565
758
|
return turnCredentialsSchema.parse({
|
|
566
759
|
credential,
|
|
567
|
-
ttl
|
|
760
|
+
ttl,
|
|
568
761
|
urls: createTURNURLs(clusterDomain, options.turnPort, options.turnsPort),
|
|
569
762
|
username
|
|
570
763
|
});
|
|
@@ -574,15 +767,15 @@ async function createAPPTURNCredentials(options) {
|
|
|
574
767
|
if (!clusterDomain) {
|
|
575
768
|
throw new Error("Cluster domain is required for TURN APP mode.");
|
|
576
769
|
}
|
|
577
|
-
const projectEndpoint =
|
|
770
|
+
const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
|
|
578
771
|
if (!projectEndpoint) {
|
|
579
772
|
throw new Error("Project endpoint is required for TURN APP mode.");
|
|
580
773
|
}
|
|
581
|
-
const clientId =
|
|
774
|
+
const clientId = normalizeOptionalString2(options.clientId);
|
|
582
775
|
if (!clientId) {
|
|
583
776
|
throw new Error("Client ID is required for TURN APP mode.");
|
|
584
777
|
}
|
|
585
|
-
const clientSecret =
|
|
778
|
+
const clientSecret = normalizeOptionalString2(options.clientSecret);
|
|
586
779
|
if (!clientSecret) {
|
|
587
780
|
throw new Error("Client secret is required for TURN APP mode.");
|
|
588
781
|
}
|
|
@@ -594,7 +787,7 @@ async function createAPPTURNCredentials(options) {
|
|
|
594
787
|
format: "der",
|
|
595
788
|
type: "pkcs8"
|
|
596
789
|
});
|
|
597
|
-
const serverPublicKeyHex =
|
|
790
|
+
const serverPublicKeyHex = normalizeOptionalString2(options.serverPublicKeyHex) ?? await loadTURNServerPublicKeyHex(clusterDomain, options);
|
|
598
791
|
const serverPublicKey = crypto.createPublicKey({
|
|
599
792
|
key: Buffer.from(serverPublicKeyHex, "hex"),
|
|
600
793
|
format: "der",
|
|
@@ -624,7 +817,7 @@ async function createAPPTURNCredentials(options) {
|
|
|
624
817
|
async function createTURNCredentials(options) {
|
|
625
818
|
const credentials = options.credentials;
|
|
626
819
|
const mode = resolveMode(credentials, options.mode);
|
|
627
|
-
const projectEndpoint =
|
|
820
|
+
const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
|
|
628
821
|
if (mode === "api") {
|
|
629
822
|
const controlPlaneCredentials = options.controlPlaneCredentials ?? credentials;
|
|
630
823
|
if (!controlPlaneCredentials) {
|
|
@@ -655,6 +848,10 @@ async function createTURNCredentials(options) {
|
|
|
655
848
|
"Project endpoint is required for TURN PAT mode."
|
|
656
849
|
),
|
|
657
850
|
token: credentials.token,
|
|
851
|
+
tokenEndpoint: requireOption(
|
|
852
|
+
normalizeOptionalString2(options.tokenEndpoint),
|
|
853
|
+
"Token endpoint is required for TURN PAT mode."
|
|
854
|
+
),
|
|
658
855
|
ttlSeconds: options.ttlSeconds,
|
|
659
856
|
turnPort: options.turnPort,
|
|
660
857
|
turnsPort: options.turnsPort
|
|
@@ -704,6 +901,7 @@ var RstreamTURNResource = class {
|
|
|
704
901
|
projectEndpoint: options.projectEndpoint ?? this.client.projectEndpoint,
|
|
705
902
|
projectId: options.projectId,
|
|
706
903
|
serverPublicKeyHex: options.serverPublicKeyHex,
|
|
904
|
+
tokenEndpoint: options.tokenEndpoint,
|
|
707
905
|
ttlSeconds: options.ttlSeconds,
|
|
708
906
|
turnPort: options.turnPort,
|
|
709
907
|
turnsPort: options.turnsPort
|
|
@@ -713,6 +911,41 @@ var RstreamTURNResource = class {
|
|
|
713
911
|
|
|
714
912
|
// src/webhooks-resource.ts
|
|
715
913
|
import crypto2 from "crypto";
|
|
914
|
+
var SIGNATURE_HEX_BYTES = 32;
|
|
915
|
+
var signatureHexSchema = /^[0-9a-fA-F]{64}$/;
|
|
916
|
+
var timestampSchema = /^\d+$/;
|
|
917
|
+
function parseSignatureTimestamp(timestamp) {
|
|
918
|
+
if (!timestampSchema.test(timestamp)) {
|
|
919
|
+
throw new Error("Invalid signature timestamp");
|
|
920
|
+
}
|
|
921
|
+
const parsed = Number(timestamp);
|
|
922
|
+
if (!Number.isSafeInteger(parsed)) {
|
|
923
|
+
throw new Error("Invalid signature timestamp");
|
|
924
|
+
}
|
|
925
|
+
return parsed;
|
|
926
|
+
}
|
|
927
|
+
function normalizeTolerance(tolerance) {
|
|
928
|
+
if (!Number.isFinite(tolerance) || !Number.isInteger(tolerance) || tolerance < 0) {
|
|
929
|
+
throw new Error("Invalid signature tolerance");
|
|
930
|
+
}
|
|
931
|
+
return tolerance;
|
|
932
|
+
}
|
|
933
|
+
function readSignature(signature) {
|
|
934
|
+
if (!signatureHexSchema.test(signature)) {
|
|
935
|
+
return void 0;
|
|
936
|
+
}
|
|
937
|
+
const parsed = Buffer.from(signature, "hex");
|
|
938
|
+
return parsed.length === SIGNATURE_HEX_BYTES ? parsed : void 0;
|
|
939
|
+
}
|
|
940
|
+
function signatureMatches(signature, expected) {
|
|
941
|
+
const parsed = readSignature(signature);
|
|
942
|
+
return parsed === void 0 ? false : crypto2.timingSafeEqual(parsed, expected);
|
|
943
|
+
}
|
|
944
|
+
function assertSecretConfigured(secret) {
|
|
945
|
+
if (secret.trim().length === 0) {
|
|
946
|
+
throw new Error("Webhook signing secret is required");
|
|
947
|
+
}
|
|
948
|
+
}
|
|
716
949
|
var RstreamWebhookResource = class {
|
|
717
950
|
async event(payload, header, secret, tolerance = 300, receivedAt = Date.now()) {
|
|
718
951
|
const payloadBuffer = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
|
|
@@ -720,31 +953,26 @@ var RstreamWebhookResource = class {
|
|
|
720
953
|
if (!signatureHeader) {
|
|
721
954
|
throw new Error("No signature header");
|
|
722
955
|
}
|
|
956
|
+
assertSecretConfigured(secret);
|
|
723
957
|
const elements = signatureHeader.split(",").map((element) => element.trim());
|
|
724
958
|
const timestamp = elements.find((element) => element.startsWith("t="))?.substring(2);
|
|
725
959
|
const signatures = elements.filter((element) => element.startsWith("v1=")).map((element) => element.substring(3));
|
|
726
960
|
if (!timestamp || signatures.length === 0) {
|
|
727
961
|
throw new Error("Invalid signature header format");
|
|
728
962
|
}
|
|
729
|
-
const timestampNum =
|
|
963
|
+
const timestampNum = parseSignatureTimestamp(timestamp);
|
|
964
|
+
const signatureTolerance = normalizeTolerance(tolerance);
|
|
730
965
|
const now = Math.floor(receivedAt / 1e3);
|
|
731
|
-
if (Math.abs(now - timestampNum) >
|
|
966
|
+
if (Math.abs(now - timestampNum) > signatureTolerance) {
|
|
732
967
|
throw new Error(
|
|
733
968
|
`Webhook signature timestamp outside tolerance: ${now}, ${timestampNum}`
|
|
734
969
|
);
|
|
735
970
|
}
|
|
736
971
|
const signedPayload = `${timestamp}.${payloadBuffer.toString("utf8")}`;
|
|
737
|
-
const expectedSignature = crypto2.createHmac("sha256", secret).update(signedPayload).digest(
|
|
738
|
-
const signatureMatched = signatures.some(
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
Buffer.from(signature),
|
|
742
|
-
Buffer.from(expectedSignature)
|
|
743
|
-
);
|
|
744
|
-
} catch {
|
|
745
|
-
return false;
|
|
746
|
-
}
|
|
747
|
-
});
|
|
972
|
+
const expectedSignature = crypto2.createHmac("sha256", secret).update(signedPayload).digest();
|
|
973
|
+
const signatureMatched = signatures.some(
|
|
974
|
+
(signature) => signatureMatches(signature, expectedSignature)
|
|
975
|
+
);
|
|
748
976
|
if (!signatureMatched) {
|
|
749
977
|
throw new Error("Signature verification failed");
|
|
750
978
|
}
|
|
@@ -762,8 +990,16 @@ var RstreamWebhookResource = class {
|
|
|
762
990
|
};
|
|
763
991
|
|
|
764
992
|
// src/tunnels.ts
|
|
993
|
+
var engineAuthTokenScopes = {
|
|
994
|
+
tunnels: {
|
|
995
|
+
connect: true,
|
|
996
|
+
create: true,
|
|
997
|
+
list: true
|
|
998
|
+
}
|
|
999
|
+
};
|
|
765
1000
|
var RstreamTunnelsClient = class {
|
|
766
1001
|
config;
|
|
1002
|
+
managedProject;
|
|
767
1003
|
constructor(config) {
|
|
768
1004
|
this.config = config;
|
|
769
1005
|
}
|
|
@@ -783,6 +1019,21 @@ var RstreamTunnelsClient = class {
|
|
|
783
1019
|
return this.config?.projectEndpoint;
|
|
784
1020
|
}
|
|
785
1021
|
async getEngine() {
|
|
1022
|
+
const projectEndpoint = normalizeOptionalString3(
|
|
1023
|
+
this.config?.projectEndpoint
|
|
1024
|
+
);
|
|
1025
|
+
if (projectEndpoint !== void 0 && normalizeOptionalString3(this.config?.engine) === void 0 && readEnvironment2().engine === void 0) {
|
|
1026
|
+
const project = await this.getManagedProject();
|
|
1027
|
+
if (project !== void 0) {
|
|
1028
|
+
const engine = getTunnelsProjectEngine2(project);
|
|
1029
|
+
if (engine) {
|
|
1030
|
+
return normalizeEngineAddress(engine);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
throw new Error(
|
|
1034
|
+
"Failed to resolve the engine address from the managed tunnels project."
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
786
1037
|
return await resolveTunnelsEngine({
|
|
787
1038
|
apiUrl: this.config?.apiUrl,
|
|
788
1039
|
controlPlaneCredentials: this.controlPlaneCredentials,
|
|
@@ -791,6 +1042,55 @@ var RstreamTunnelsClient = class {
|
|
|
791
1042
|
projectEndpoint: this.config?.projectEndpoint
|
|
792
1043
|
});
|
|
793
1044
|
}
|
|
1045
|
+
async getManagedProject() {
|
|
1046
|
+
const projectEndpoint = normalizeOptionalString3(
|
|
1047
|
+
this.config?.projectEndpoint
|
|
1048
|
+
);
|
|
1049
|
+
if (projectEndpoint === void 0) {
|
|
1050
|
+
return void 0;
|
|
1051
|
+
}
|
|
1052
|
+
if (this.managedProject === void 0) {
|
|
1053
|
+
this.managedProject = resolveManagedTunnelsProject({
|
|
1054
|
+
apiUrl: this.config?.apiUrl,
|
|
1055
|
+
controlPlaneCredentials: this.controlPlaneCredentials,
|
|
1056
|
+
credentials: this.credentials,
|
|
1057
|
+
projectEndpoint
|
|
1058
|
+
}).catch((error) => {
|
|
1059
|
+
this.managedProject = void 0;
|
|
1060
|
+
throw error;
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
return await this.managedProject;
|
|
1064
|
+
}
|
|
1065
|
+
async getEngineAuthTokenParams() {
|
|
1066
|
+
const target = await this.getEngineAuthTokenTarget();
|
|
1067
|
+
return {
|
|
1068
|
+
tunnelsGrants: {
|
|
1069
|
+
...target,
|
|
1070
|
+
scopes: engineAuthTokenScopes
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
async getEngineAuthTokenTarget() {
|
|
1075
|
+
const projectId = normalizeOptionalString3(this.config?.projectId);
|
|
1076
|
+
const workspaceId = normalizeOptionalString3(this.config?.workspaceId);
|
|
1077
|
+
if (projectId !== void 0 && workspaceId !== void 0) {
|
|
1078
|
+
throw new Error("Use either projectId or workspaceId, not both.");
|
|
1079
|
+
}
|
|
1080
|
+
if (projectId !== void 0) {
|
|
1081
|
+
return { projects: [projectId] };
|
|
1082
|
+
}
|
|
1083
|
+
if (workspaceId !== void 0) {
|
|
1084
|
+
return { workspaces: [workspaceId] };
|
|
1085
|
+
}
|
|
1086
|
+
const project = await this.getManagedProject();
|
|
1087
|
+
if (project !== void 0) {
|
|
1088
|
+
return { projects: [project.id] };
|
|
1089
|
+
}
|
|
1090
|
+
throw new Error(
|
|
1091
|
+
"Application credentials require projectId, workspaceId, or projectEndpoint to create scoped engine tokens."
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
794
1094
|
get auth() {
|
|
795
1095
|
return new RstreamAuthResource(this);
|
|
796
1096
|
}
|
|
@@ -814,13 +1114,17 @@ var RstreamTunnelsClient = class {
|
|
|
814
1114
|
if (!isClientCredentials3(credentials)) {
|
|
815
1115
|
return credentials.token;
|
|
816
1116
|
}
|
|
817
|
-
|
|
1117
|
+
const tokenParams = await this.getEngineAuthTokenParams();
|
|
1118
|
+
return (await this.auth.createAuthToken(tokenParams, {
|
|
818
1119
|
credentials,
|
|
819
1120
|
engine
|
|
820
1121
|
})).token;
|
|
821
1122
|
}
|
|
822
1123
|
async request(path, options) {
|
|
823
|
-
const engine = await this.getEngine();
|
|
1124
|
+
const engine = normalizeEngineAddress(await this.getEngine());
|
|
1125
|
+
if (!path.startsWith("/") || path.startsWith("//")) {
|
|
1126
|
+
throw new Error("Engine request path must be a relative absolute path.");
|
|
1127
|
+
}
|
|
824
1128
|
const url = `https://${engine}/api${path}`;
|
|
825
1129
|
const headers = new Headers(options?.headers || {});
|
|
826
1130
|
const token = await this.getToken(engine);
|
|
@@ -829,7 +1133,8 @@ var RstreamTunnelsClient = class {
|
|
|
829
1133
|
}
|
|
830
1134
|
const response = await fetch(url, {
|
|
831
1135
|
...options,
|
|
832
|
-
headers
|
|
1136
|
+
headers,
|
|
1137
|
+
redirect: options?.redirect ?? "manual"
|
|
833
1138
|
});
|
|
834
1139
|
if (!response.ok) {
|
|
835
1140
|
const errorText = await response.text();
|
|
@@ -838,8 +1143,96 @@ var RstreamTunnelsClient = class {
|
|
|
838
1143
|
return await response.json();
|
|
839
1144
|
}
|
|
840
1145
|
};
|
|
1146
|
+
function normalizeOptionalString3(value) {
|
|
1147
|
+
const normalized = value?.trim();
|
|
1148
|
+
return normalized ? normalized : void 0;
|
|
1149
|
+
}
|
|
841
1150
|
|
|
842
1151
|
// src/watch.ts
|
|
1152
|
+
import jwt from "jsonwebtoken";
|
|
1153
|
+
var maxWatchTokenLifetimeSeconds = 3600;
|
|
1154
|
+
var watchTokenIssuedAtSkewSeconds = 300;
|
|
1155
|
+
function normalizeTransport(transport) {
|
|
1156
|
+
if (transport === void 0) {
|
|
1157
|
+
return "sse";
|
|
1158
|
+
}
|
|
1159
|
+
if (transport === "sse" || transport === "websocket") {
|
|
1160
|
+
return transport;
|
|
1161
|
+
}
|
|
1162
|
+
throw new Error("Watch: Unsupported transport.");
|
|
1163
|
+
}
|
|
1164
|
+
function validateWatchQueryToken(token) {
|
|
1165
|
+
const parsed = authTokenSchema.safeParse(jwt.decode(token));
|
|
1166
|
+
if (!parsed.success || parsed.data.type === "pat") {
|
|
1167
|
+
throw new Error(
|
|
1168
|
+
"Watch requires a short-lived auth or app token for URL-based authentication."
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
const iat = parsed.data.iat;
|
|
1172
|
+
const exp = parsed.data.exp;
|
|
1173
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1174
|
+
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) {
|
|
1175
|
+
throw new Error(
|
|
1176
|
+
"Watch requires a token with a bounded lifetime of at most 3600 seconds."
|
|
1177
|
+
);
|
|
1178
|
+
}
|
|
1179
|
+
if (!hasWatchOnlyTunnelGrants(parsed.data)) {
|
|
1180
|
+
throw new Error(
|
|
1181
|
+
"Watch requires a fine-grained token limited to tunnel list grants for URL-based authentication."
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
function hasWatchOnlyTunnelGrants(token) {
|
|
1186
|
+
if (!hasWatchOnlyPermissions(token.permissions)) {
|
|
1187
|
+
return false;
|
|
1188
|
+
}
|
|
1189
|
+
if (token.tunnelsGrants === void 0) {
|
|
1190
|
+
return false;
|
|
1191
|
+
}
|
|
1192
|
+
return hasWatchOnlyTunnelGrant(token.tunnelsGrants);
|
|
1193
|
+
}
|
|
1194
|
+
function hasWatchOnlyTunnelGrant(grant) {
|
|
1195
|
+
return watchGrantBranches(grant).every(
|
|
1196
|
+
(branch) => branch.hasList && !branch.hasMutationOrConnect
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
function watchGrantBranches(grant) {
|
|
1200
|
+
if ("AND" in grant) {
|
|
1201
|
+
return grant.AND.reduce(
|
|
1202
|
+
(branches, child) => combineWatchGrantBranches(branches, child),
|
|
1203
|
+
[{ hasList: false, hasMutationOrConnect: false }]
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
if ("OR" in grant) {
|
|
1207
|
+
return grant.OR.flatMap(watchGrantBranches);
|
|
1208
|
+
}
|
|
1209
|
+
const tunnels = grant.scopes?.tunnels;
|
|
1210
|
+
if (tunnels === void 0) {
|
|
1211
|
+
return [{ hasList: false, hasMutationOrConnect: false }];
|
|
1212
|
+
}
|
|
1213
|
+
return [
|
|
1214
|
+
{
|
|
1215
|
+
hasList: tunnels.list !== void 0,
|
|
1216
|
+
hasMutationOrConnect: tunnels.create !== void 0 || tunnels.connect !== void 0
|
|
1217
|
+
}
|
|
1218
|
+
];
|
|
1219
|
+
}
|
|
1220
|
+
function combineWatchGrantBranches(branches, child) {
|
|
1221
|
+
return branches.flatMap(
|
|
1222
|
+
(branch) => watchGrantBranches(child).map((childBranch) => ({
|
|
1223
|
+
hasList: branch.hasList || childBranch.hasList,
|
|
1224
|
+
hasMutationOrConnect: branch.hasMutationOrConnect || childBranch.hasMutationOrConnect
|
|
1225
|
+
}))
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
function hasWatchOnlyPermissions(permissions) {
|
|
1229
|
+
if (!Array.isArray(permissions)) {
|
|
1230
|
+
return true;
|
|
1231
|
+
}
|
|
1232
|
+
return permissions.every(
|
|
1233
|
+
(permission) => permission === "tunnels.resources.read-only" || permission === "network.resources.read-only"
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
843
1236
|
var Watch = class {
|
|
844
1237
|
connection = null;
|
|
845
1238
|
connectionState = "preparing";
|
|
@@ -854,45 +1247,56 @@ var Watch = class {
|
|
|
854
1247
|
throw new Error("Watch: Connection already started or closed.");
|
|
855
1248
|
}
|
|
856
1249
|
this.connectionState = "connecting";
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
const
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
this.connectionState = "connected";
|
|
879
|
-
this.events.onConnect?.();
|
|
880
|
-
};
|
|
881
|
-
this.connection.onmessage = (msg) => {
|
|
882
|
-
const parsed = wsEventsSchema.parse(JSON.parse(msg.data));
|
|
883
|
-
this.events.onEvent?.(parsed);
|
|
884
|
-
};
|
|
885
|
-
this.connection.onerror = () => {
|
|
886
|
-
if (this.connectionState !== "closed") {
|
|
887
|
-
this.disconnect();
|
|
1250
|
+
try {
|
|
1251
|
+
const token = typeof this.config.auth === "function" ? await this.config.auth() : this.config.auth;
|
|
1252
|
+
validateWatchQueryToken(token);
|
|
1253
|
+
const engine = await resolveTunnelsEngine({
|
|
1254
|
+
apiUrl: this.config.apiUrl,
|
|
1255
|
+
controlPlaneCredentials: this.config.controlPlaneCredentials,
|
|
1256
|
+
engine: this.config.engine,
|
|
1257
|
+
projectEndpoint: this.config.projectEndpoint,
|
|
1258
|
+
token
|
|
1259
|
+
});
|
|
1260
|
+
const transport = normalizeTransport(this.config.transport);
|
|
1261
|
+
const base = `https://${engine}`;
|
|
1262
|
+
if (transport === "sse") {
|
|
1263
|
+
const url = new URL(`/api/sse`, base);
|
|
1264
|
+
url.searchParams.set("rstream.token", token);
|
|
1265
|
+
this.connection = new EventSource(url.toString());
|
|
1266
|
+
} else {
|
|
1267
|
+
const url = new URL(`/api/websocket`, base);
|
|
1268
|
+
url.searchParams.set("rstream.token", token);
|
|
1269
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
1270
|
+
this.connection = new WebSocket(url.toString());
|
|
888
1271
|
}
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
1272
|
+
this.connection.onopen = () => {
|
|
1273
|
+
this.connectionState = "connected";
|
|
1274
|
+
this.events.onConnect?.();
|
|
1275
|
+
};
|
|
1276
|
+
this.connection.onmessage = (msg) => this.handleMessage(msg.data);
|
|
1277
|
+
this.connection.onerror = () => {
|
|
892
1278
|
if (this.connectionState !== "closed") {
|
|
893
1279
|
this.disconnect();
|
|
894
1280
|
}
|
|
895
1281
|
};
|
|
1282
|
+
if (this.connection instanceof WebSocket) {
|
|
1283
|
+
this.connection.onclose = () => {
|
|
1284
|
+
if (this.connectionState !== "closed") {
|
|
1285
|
+
this.disconnect();
|
|
1286
|
+
}
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
} catch (error) {
|
|
1290
|
+
this.connectionState = "preparing";
|
|
1291
|
+
throw error;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
handleMessage(data) {
|
|
1295
|
+
try {
|
|
1296
|
+
const parsed = wsEventsSchema.parse(JSON.parse(data));
|
|
1297
|
+
this.events.onEvent?.(parsed);
|
|
1298
|
+
} catch {
|
|
1299
|
+
this.disconnect();
|
|
896
1300
|
}
|
|
897
1301
|
}
|
|
898
1302
|
disconnect() {
|
|
@@ -1012,8 +1416,10 @@ export {
|
|
|
1012
1416
|
listClientsResponseSchema,
|
|
1013
1417
|
listTunnelsParamsSchema,
|
|
1014
1418
|
listTunnelsResponseSchema,
|
|
1419
|
+
normalizeEngineAddress,
|
|
1015
1420
|
parseWebTTYServers,
|
|
1016
1421
|
resolveControlPlaneCredentials,
|
|
1422
|
+
resolveManagedTunnelsProject,
|
|
1017
1423
|
resolveTunnelsAPIURL,
|
|
1018
1424
|
resolveTunnelsCredentials,
|
|
1019
1425
|
resolveTunnelsEngine,
|