@rstreamlabs/tunnels 1.0.6 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -3
- package/dist/chunk-7RZ743EF.mjs +733 -0
- package/dist/index.d.mts +165 -73
- package/dist/index.d.ts +165 -73
- package/dist/index.js +849 -183
- package/dist/index.mjs +532 -566
- package/dist/openapi.d.mts +288 -0
- package/dist/openapi.d.ts +288 -0
- package/dist/openapi.js +747 -0
- package/dist/openapi.mjs +8 -0
- package/package.json +7 -2
package/dist/index.js
CHANGED
|
@@ -48,6 +48,7 @@ __export(index_exports, {
|
|
|
48
48
|
authTokenScopesSchema: () => authTokenScopesSchema,
|
|
49
49
|
authTokenTunnelGrantSchema: () => authTokenTunnelGrantSchema,
|
|
50
50
|
authTokenTunnelsScopesSchema: () => authTokenTunnelsScopesSchema,
|
|
51
|
+
clientFilterSchema: () => clientFilterSchema,
|
|
51
52
|
clientSchema: () => clientSchema,
|
|
52
53
|
createAPITURNCredentials: () => createAPITURNCredentials,
|
|
53
54
|
createAPPTURNCredentials: () => createAPPTURNCredentials,
|
|
@@ -56,14 +57,18 @@ __export(index_exports, {
|
|
|
56
57
|
createAuthTokenResponseSchema: () => createAuthTokenResponseSchema,
|
|
57
58
|
createPATTURNCredentials: () => createPATTURNCredentials,
|
|
58
59
|
createTURNCredentials: () => createTURNCredentials,
|
|
60
|
+
engineOpenApiDocument: () => engineOpenApiDocument,
|
|
61
|
+
engineOpenApiJson: () => engineOpenApiJson,
|
|
59
62
|
eventSchema: () => eventSchema,
|
|
60
63
|
formatTunnelHost: () => formatTunnelHost,
|
|
61
64
|
listClientsParamsSchema: () => listClientsParamsSchema,
|
|
62
65
|
listClientsResponseSchema: () => listClientsResponseSchema,
|
|
63
66
|
listTunnelsParamsSchema: () => listTunnelsParamsSchema,
|
|
64
67
|
listTunnelsResponseSchema: () => listTunnelsResponseSchema,
|
|
68
|
+
normalizeEngineAddress: () => normalizeEngineAddress,
|
|
65
69
|
parseWebTTYServers: () => parseWebTTYServers,
|
|
66
70
|
resolveControlPlaneCredentials: () => resolveControlPlaneCredentials,
|
|
71
|
+
resolveManagedTunnelsProject: () => resolveManagedTunnelsProject,
|
|
67
72
|
resolveTunnelsAPIURL: () => resolveTunnelsAPIURL,
|
|
68
73
|
resolveTunnelsCredentials: () => resolveTunnelsCredentials,
|
|
69
74
|
resolveTunnelsEngine: () => resolveTunnelsEngine,
|
|
@@ -71,6 +76,7 @@ __export(index_exports, {
|
|
|
71
76
|
streamSummarySchema: () => streamSummarySchema,
|
|
72
77
|
streamTLSInfoSchema: () => streamTLSInfoSchema,
|
|
73
78
|
tokenSchema: () => tokenSchema,
|
|
79
|
+
tunnelFilterSchema: () => tunnelFilterSchema,
|
|
74
80
|
tunnelSchema: () => tunnelSchema,
|
|
75
81
|
turnCredentialModeSchema: () => turnCredentialModeSchema,
|
|
76
82
|
webhookEventsSchema: () => webhookEventsSchema,
|
|
@@ -86,9 +92,9 @@ var import_rstream = require("@rstreamlabs/rstream");
|
|
|
86
92
|
var z = __toESM(require("zod"));
|
|
87
93
|
var StringFilter = z.union([
|
|
88
94
|
z.string(),
|
|
89
|
-
z.object({ exact: z.string() }),
|
|
90
|
-
z.object({ oneof: z.array(z.string()) }),
|
|
91
|
-
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()
|
|
92
98
|
]);
|
|
93
99
|
function transform(field) {
|
|
94
100
|
if (field instanceof z.ZodOptional) {
|
|
@@ -122,7 +128,7 @@ function select(base) {
|
|
|
122
128
|
return [key, z.boolean().optional()];
|
|
123
129
|
});
|
|
124
130
|
const shape = Object.fromEntries(entries);
|
|
125
|
-
return z.object(shape);
|
|
131
|
+
return z.object(shape).strict();
|
|
126
132
|
}
|
|
127
133
|
|
|
128
134
|
// src/tunnel.ts
|
|
@@ -165,16 +171,22 @@ var tunnelSchema = z2.object({
|
|
|
165
171
|
rstream_auth: z2.boolean().optional(),
|
|
166
172
|
challenge_mode: z2.boolean().optional()
|
|
167
173
|
});
|
|
174
|
+
var tunnelFilterSchema = tunnelSchema.pick({
|
|
175
|
+
id: true,
|
|
176
|
+
name: true,
|
|
177
|
+
type: true,
|
|
178
|
+
status: true,
|
|
179
|
+
client_id: true,
|
|
180
|
+
user_id: true,
|
|
181
|
+
publish: true,
|
|
182
|
+
protocol: true,
|
|
183
|
+
hostname: true,
|
|
184
|
+
http_version: true,
|
|
185
|
+
labels: true
|
|
186
|
+
}).partial();
|
|
168
187
|
var listTunnelsParamsSchema = z2.object({
|
|
169
|
-
limit: z2.number().optional(),
|
|
170
|
-
filters:
|
|
171
|
-
status: true,
|
|
172
|
-
client_id: true,
|
|
173
|
-
hostname: true,
|
|
174
|
-
protocol: true,
|
|
175
|
-
publish: true,
|
|
176
|
-
labels: true
|
|
177
|
-
}).partial().optional()
|
|
188
|
+
limit: z2.number().int().min(1).optional(),
|
|
189
|
+
filters: tunnelFilterSchema.optional()
|
|
178
190
|
});
|
|
179
191
|
var listTunnelsResponseSchema = z2.array(tunnelSchema);
|
|
180
192
|
function formatTunnelHost(tunnel) {
|
|
@@ -209,7 +221,7 @@ var authTokenTunnelsScopesSchema = z3.object({
|
|
|
209
221
|
creation_date: true
|
|
210
222
|
})
|
|
211
223
|
).optional()
|
|
212
|
-
})
|
|
224
|
+
}).strict()
|
|
213
225
|
]).optional(),
|
|
214
226
|
// Scopes for connecting to tunnels
|
|
215
227
|
connect: z3.union([
|
|
@@ -221,7 +233,7 @@ var authTokenTunnelsScopesSchema = z3.object({
|
|
|
221
233
|
path: z3.string().optional()
|
|
222
234
|
})
|
|
223
235
|
).optional()
|
|
224
|
-
})
|
|
236
|
+
}).strict()
|
|
225
237
|
]).optional(),
|
|
226
238
|
// Scopes for listing tunnels
|
|
227
239
|
list: z3.union([
|
|
@@ -229,17 +241,32 @@ var authTokenTunnelsScopesSchema = z3.object({
|
|
|
229
241
|
z3.object({
|
|
230
242
|
filters: filters(tunnelSchema).optional(),
|
|
231
243
|
select: select(tunnelSchema).optional()
|
|
232
|
-
})
|
|
244
|
+
}).strict()
|
|
233
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
|
+
}
|
|
234
253
|
});
|
|
235
254
|
var authTokenScopesSchema = z3.object({
|
|
236
255
|
tunnels: authTokenTunnelsScopesSchema.optional()
|
|
237
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
|
+
}
|
|
238
265
|
});
|
|
239
266
|
var authTokenTunnelGrantSchema = z3.object({
|
|
240
267
|
workspaces: z3.array(nonEmptyStringSchema).min(1).optional(),
|
|
241
268
|
projects: z3.array(nonEmptyStringSchema).min(1).optional(),
|
|
242
|
-
scopes: authTokenScopesSchema
|
|
269
|
+
scopes: authTokenScopesSchema
|
|
243
270
|
}).strict().superRefine((grant, ctx) => {
|
|
244
271
|
if (grant.workspaces !== void 0 && grant.projects !== void 0) {
|
|
245
272
|
ctx.addIssue({
|
|
@@ -248,6 +275,13 @@ var authTokenTunnelGrantSchema = z3.object({
|
|
|
248
275
|
path: ["projects"]
|
|
249
276
|
});
|
|
250
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
|
+
}
|
|
251
285
|
});
|
|
252
286
|
var authTokenCommonSchema = {
|
|
253
287
|
aud: z3.union([z3.string(), z3.array(z3.string())]).optional(),
|
|
@@ -283,10 +317,9 @@ var authTokenSchema = z3.union([
|
|
|
283
317
|
]);
|
|
284
318
|
var tokenSchema = authTokenSchema;
|
|
285
319
|
var createAuthTokenParamsSchema = z3.object({
|
|
286
|
-
expires_in: z3.number().default(60),
|
|
287
|
-
// 1 minute
|
|
320
|
+
expires_in: z3.number().int().min(1).max(3600).default(60),
|
|
288
321
|
scopes: authTokenScopesSchema.optional(),
|
|
289
|
-
tunnelsGrants: z3.array(authTokenTunnelGrantSchema).optional(),
|
|
322
|
+
tunnelsGrants: z3.array(authTokenTunnelGrantSchema).min(1).optional(),
|
|
290
323
|
metadata: z3.unknown().optional()
|
|
291
324
|
// Additional metadata
|
|
292
325
|
}).strict().superRefine((params, ctx) => {
|
|
@@ -297,6 +330,13 @@ var createAuthTokenParamsSchema = z3.object({
|
|
|
297
330
|
path: ["tunnelsGrants"]
|
|
298
331
|
});
|
|
299
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
|
+
}
|
|
300
340
|
});
|
|
301
341
|
var createAuthTokenResponseSchema = z3.object({
|
|
302
342
|
token: z3.string()
|
|
@@ -323,12 +363,12 @@ var RstreamAuthResource = class {
|
|
|
323
363
|
}
|
|
324
364
|
};
|
|
325
365
|
function createAuthTokenFromClientCredentials(credentials, params, options) {
|
|
326
|
-
const tokenParams = createAuthTokenParamsSchema.parse(params
|
|
366
|
+
const tokenParams = createAuthTokenParamsSchema.parse(params);
|
|
327
367
|
const payload = {
|
|
328
368
|
metadata: {
|
|
329
369
|
engine: options?.engine
|
|
330
370
|
},
|
|
331
|
-
tunnelsGrants: normalizeAuthTokenTunnelGrants(tokenParams),
|
|
371
|
+
tunnelsGrants: normalizeAuthTokenTunnelGrants(tokenParams, options),
|
|
332
372
|
permissions: null
|
|
333
373
|
};
|
|
334
374
|
const { token } = (0, import_rstream.createClientCredentialsToken)(credentials, {
|
|
@@ -337,19 +377,36 @@ function createAuthTokenFromClientCredentials(credentials, params, options) {
|
|
|
337
377
|
});
|
|
338
378
|
return { token };
|
|
339
379
|
}
|
|
340
|
-
function normalizeAuthTokenTunnelGrants(params) {
|
|
341
|
-
if (!params) {
|
|
342
|
-
return void 0;
|
|
343
|
-
}
|
|
380
|
+
function normalizeAuthTokenTunnelGrants(params, options) {
|
|
344
381
|
const tunnelsGrants = params.tunnelsGrants;
|
|
345
382
|
if (tunnelsGrants !== void 0) {
|
|
346
383
|
return [...tunnelsGrants];
|
|
347
384
|
}
|
|
348
385
|
if (params.scopes) {
|
|
349
|
-
return [{ scopes: params.scopes }];
|
|
386
|
+
return [{ ...normalizeTunnelGrantTarget(options), scopes: params.scopes }];
|
|
350
387
|
}
|
|
351
388
|
return void 0;
|
|
352
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
|
+
}
|
|
353
410
|
|
|
354
411
|
// src/client.ts
|
|
355
412
|
var z4 = __toESM(require("zod"));
|
|
@@ -371,15 +428,32 @@ var clientSchema = z4.object({
|
|
|
371
428
|
protocol_version: z4.string().optional(),
|
|
372
429
|
labels: z4.record(z4.string(), z4.string()).optional()
|
|
373
430
|
});
|
|
431
|
+
var clientFilterSchema = clientSchema.pick({
|
|
432
|
+
id: true,
|
|
433
|
+
status: true,
|
|
434
|
+
user_id: true,
|
|
435
|
+
agent: true,
|
|
436
|
+
channel: true,
|
|
437
|
+
version: true,
|
|
438
|
+
os: true,
|
|
439
|
+
arch: true,
|
|
440
|
+
protocol_version: true,
|
|
441
|
+
labels: true
|
|
442
|
+
}).partial();
|
|
374
443
|
var listClientsParamsSchema = z4.object({
|
|
375
|
-
limit: z4.number().optional(),
|
|
376
|
-
filters:
|
|
377
|
-
labels: true
|
|
378
|
-
}).partial().optional()
|
|
444
|
+
limit: z4.number().int().min(1).optional(),
|
|
445
|
+
filters: clientFilterSchema.optional()
|
|
379
446
|
});
|
|
380
447
|
var listClientsResponseSchema = z4.array(clientSchema);
|
|
381
448
|
|
|
382
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
|
+
}
|
|
383
457
|
var RstreamClientsResource = class {
|
|
384
458
|
client;
|
|
385
459
|
constructor(client) {
|
|
@@ -393,9 +467,12 @@ var RstreamClientsResource = class {
|
|
|
393
467
|
return listClientsResponseSchema.parse(response);
|
|
394
468
|
}
|
|
395
469
|
async get(id) {
|
|
396
|
-
const response = await this.client.request(
|
|
397
|
-
|
|
398
|
-
|
|
470
|
+
const response = await this.client.request(
|
|
471
|
+
`/clients/${clientPathId(id)}`,
|
|
472
|
+
{
|
|
473
|
+
method: "GET"
|
|
474
|
+
}
|
|
475
|
+
);
|
|
399
476
|
return clientSchema.parse(response);
|
|
400
477
|
}
|
|
401
478
|
};
|
|
@@ -664,28 +741,389 @@ var webhookEventsSchema = z6.union([
|
|
|
664
741
|
]);
|
|
665
742
|
var eventSchema = z6.union([wsEventsSchema, webhookEventsSchema]);
|
|
666
743
|
|
|
744
|
+
// src/openapi.ts
|
|
745
|
+
var z7 = __toESM(require("zod"));
|
|
746
|
+
var jsonContentType = "application/json";
|
|
747
|
+
var openApiContentType = "application/vnd.oai.openapi+json;version=3.1";
|
|
748
|
+
function isRecord(value) {
|
|
749
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
750
|
+
}
|
|
751
|
+
function dropSchemaKeyword(value) {
|
|
752
|
+
if (!isRecord(value)) return value;
|
|
753
|
+
return Object.fromEntries(
|
|
754
|
+
Object.entries(value).filter(([key]) => key !== "$schema")
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
function jsonSchemaFor(schema) {
|
|
758
|
+
return dropSchemaKeyword(
|
|
759
|
+
z7.toJSONSchema(schema, {
|
|
760
|
+
unrepresentable: "any",
|
|
761
|
+
override: (ctx) => {
|
|
762
|
+
if (ctx.zodSchema instanceof z7.ZodDate) {
|
|
763
|
+
ctx.jsonSchema.type = "string";
|
|
764
|
+
ctx.jsonSchema.format = "date-time";
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
})
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
function jsonArrayResponse(description, itemSchemaRef) {
|
|
771
|
+
return {
|
|
772
|
+
description,
|
|
773
|
+
content: {
|
|
774
|
+
[jsonContentType]: {
|
|
775
|
+
schema: {
|
|
776
|
+
type: "array",
|
|
777
|
+
items: {
|
|
778
|
+
$ref: itemSchemaRef
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
function streamResponse(description) {
|
|
786
|
+
return {
|
|
787
|
+
description,
|
|
788
|
+
content: {
|
|
789
|
+
"text/event-stream": {
|
|
790
|
+
schema: {
|
|
791
|
+
type: "string"
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
function errorResponse(description) {
|
|
798
|
+
return {
|
|
799
|
+
description
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
function queryJsonParameter(name, description, schemaRef, examples) {
|
|
803
|
+
return {
|
|
804
|
+
name,
|
|
805
|
+
in: "query",
|
|
806
|
+
required: false,
|
|
807
|
+
description,
|
|
808
|
+
content: {
|
|
809
|
+
[jsonContentType]: {
|
|
810
|
+
schema: {
|
|
811
|
+
$ref: schemaRef
|
|
812
|
+
},
|
|
813
|
+
...examples ? { examples } : {}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
var readEngineStateRule = {
|
|
819
|
+
resource: ["engine.tunnels", "engine.streams"],
|
|
820
|
+
action: "read"
|
|
821
|
+
};
|
|
822
|
+
var readEngineStateRules = [readEngineStateRule];
|
|
823
|
+
var readEngineStreamsRules = [
|
|
824
|
+
{
|
|
825
|
+
resource: "engine.streams",
|
|
826
|
+
action: "read"
|
|
827
|
+
}
|
|
828
|
+
];
|
|
829
|
+
var watchParamsSchema = z7.object({
|
|
830
|
+
clients: clientFilterSchema.optional(),
|
|
831
|
+
tunnels: tunnelFilterSchema.optional()
|
|
832
|
+
});
|
|
833
|
+
var enginePermissions = {
|
|
834
|
+
"tunnels.resources.read-only": {
|
|
835
|
+
name: "Discovery: Read-Only",
|
|
836
|
+
description: "Read active clients, active tunnels, and live state events.",
|
|
837
|
+
rules: [readEngineStateRule]
|
|
838
|
+
},
|
|
839
|
+
"tunnels.tunnels.create-delete": {
|
|
840
|
+
name: "Tunnels: Create and Delete",
|
|
841
|
+
description: "Create and close tunnels through the CLI, SDK, or engine control channel.",
|
|
842
|
+
rules: [
|
|
843
|
+
{
|
|
844
|
+
resource: "engine.tunnels",
|
|
845
|
+
action: ["create", "delete"]
|
|
846
|
+
}
|
|
847
|
+
]
|
|
848
|
+
},
|
|
849
|
+
"tunnels.streams.create-delete": {
|
|
850
|
+
name: "Connections: Create and Delete",
|
|
851
|
+
description: "Open and close private tunnel streams and WebTTY connections.",
|
|
852
|
+
rules: [
|
|
853
|
+
{
|
|
854
|
+
resource: "engine.streams",
|
|
855
|
+
action: ["create", "delete"]
|
|
856
|
+
}
|
|
857
|
+
]
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
var engineOpenApiDocument = {
|
|
861
|
+
openapi: "3.1.0",
|
|
862
|
+
info: {
|
|
863
|
+
title: "rstream Engine API",
|
|
864
|
+
version: "1.0.0",
|
|
865
|
+
description: "Operational data-plane API exposed by rstream-engine. This surface is scoped to the authenticated engine or project endpoint and covers live clients, live tunnels, and state-change streams."
|
|
866
|
+
},
|
|
867
|
+
servers: [
|
|
868
|
+
{
|
|
869
|
+
url: "https://{engine}",
|
|
870
|
+
description: "Project-scoped rstream engine endpoint.",
|
|
871
|
+
variables: {
|
|
872
|
+
engine: {
|
|
873
|
+
default: "project-endpoint.cluster-host:443",
|
|
874
|
+
description: "Use the engine value returned by project discovery or configured in RSTREAM_ENGINE."
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
],
|
|
879
|
+
security: [
|
|
880
|
+
{
|
|
881
|
+
bearerAuth: []
|
|
882
|
+
}
|
|
883
|
+
],
|
|
884
|
+
components: {
|
|
885
|
+
securitySchemes: {
|
|
886
|
+
bearerAuth: {
|
|
887
|
+
type: "http",
|
|
888
|
+
scheme: "bearer",
|
|
889
|
+
bearerFormat: "rstream token",
|
|
890
|
+
description: "Preferred authentication mechanism. Send an OAuth-issued rstream access token or compatible rstream credential in Authorization: Bearer <token>."
|
|
891
|
+
}
|
|
892
|
+
},
|
|
893
|
+
parameters: {
|
|
894
|
+
ListClientsParams: queryJsonParameter(
|
|
895
|
+
"params",
|
|
896
|
+
"JSON-encoded ListClientsParams object.",
|
|
897
|
+
"#/components/schemas/ListClientsParams",
|
|
898
|
+
{
|
|
899
|
+
filterByAgent: {
|
|
900
|
+
value: {
|
|
901
|
+
limit: 10,
|
|
902
|
+
filters: {
|
|
903
|
+
agent: "rstream",
|
|
904
|
+
channel: "stable"
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
),
|
|
910
|
+
ListTunnelsParams: queryJsonParameter(
|
|
911
|
+
"params",
|
|
912
|
+
"JSON-encoded ListTunnelsParams object.",
|
|
913
|
+
"#/components/schemas/ListTunnelsParams",
|
|
914
|
+
{
|
|
915
|
+
filterByLabels: {
|
|
916
|
+
value: {
|
|
917
|
+
filters: {
|
|
918
|
+
labels: {
|
|
919
|
+
service: "ssh",
|
|
920
|
+
env: "prod"
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
),
|
|
927
|
+
WatchParams: queryJsonParameter(
|
|
928
|
+
"params",
|
|
929
|
+
"JSON-encoded WatchParams object. The same shape is accepted by SSE and WebSocket transports.",
|
|
930
|
+
"#/components/schemas/WatchParams",
|
|
931
|
+
{
|
|
932
|
+
watchTaggedTunnels: {
|
|
933
|
+
value: {
|
|
934
|
+
clients: {
|
|
935
|
+
agent: "rstream"
|
|
936
|
+
},
|
|
937
|
+
tunnels: {
|
|
938
|
+
labels: {
|
|
939
|
+
service: "ssh"
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
)
|
|
946
|
+
},
|
|
947
|
+
responses: {
|
|
948
|
+
BadRequest: errorResponse("Request validation failed."),
|
|
949
|
+
Unauthorized: errorResponse("Authentication is required."),
|
|
950
|
+
Forbidden: errorResponse("The token is not authorized.")
|
|
951
|
+
},
|
|
952
|
+
schemas: {
|
|
953
|
+
Client: jsonSchemaFor(clientSchema),
|
|
954
|
+
Event: jsonSchemaFor(wsEventsSchema),
|
|
955
|
+
ListClientsParams: jsonSchemaFor(listClientsParamsSchema),
|
|
956
|
+
ListTunnelsParams: jsonSchemaFor(listTunnelsParamsSchema),
|
|
957
|
+
TunnelListItem: jsonSchemaFor(tunnelSchema),
|
|
958
|
+
WatchParams: jsonSchemaFor(watchParamsSchema)
|
|
959
|
+
}
|
|
960
|
+
},
|
|
961
|
+
"x-rstream-agent-authentication": {
|
|
962
|
+
oauth_authorization_server_metadata: "https://rstream.io/.well-known/oauth-authorization-server",
|
|
963
|
+
oauth_protected_resource_metadata: "https://rstream.io/.well-known/oauth-protected-resource",
|
|
964
|
+
scope_selection: "Use x-rstream-required-rules to understand backend authorization checks. Choose OAuth scopes from x-rstream-permissions or use the task bundles in x-rstream-use-case-permissions."
|
|
965
|
+
},
|
|
966
|
+
"x-rstream-permissions": enginePermissions,
|
|
967
|
+
"x-rstream-use-case-permissions": {
|
|
968
|
+
"inspect-engine-state": ["tunnels.resources.read-only"],
|
|
969
|
+
"expose-service-from-cli": [
|
|
970
|
+
"tunnels.resources.read-only",
|
|
971
|
+
"tunnels.tunnels.create-delete"
|
|
972
|
+
],
|
|
973
|
+
"connect-private-tunnel": [
|
|
974
|
+
"tunnels.resources.read-only",
|
|
975
|
+
"tunnels.streams.create-delete"
|
|
976
|
+
],
|
|
977
|
+
"operate-webtty": [
|
|
978
|
+
"tunnels.resources.read-only",
|
|
979
|
+
"tunnels.tunnels.create-delete",
|
|
980
|
+
"tunnels.streams.create-delete"
|
|
981
|
+
]
|
|
982
|
+
},
|
|
983
|
+
paths: {
|
|
984
|
+
"/api/openapi.json": {
|
|
985
|
+
get: {
|
|
986
|
+
tags: ["Discovery"],
|
|
987
|
+
summary: "Return this OpenAPI description.",
|
|
988
|
+
operationId: "get_engine_openapi",
|
|
989
|
+
security: [],
|
|
990
|
+
responses: {
|
|
991
|
+
"200": {
|
|
992
|
+
description: "OpenAPI document.",
|
|
993
|
+
content: {
|
|
994
|
+
[openApiContentType]: {
|
|
995
|
+
schema: {
|
|
996
|
+
type: "object"
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
},
|
|
1004
|
+
"/api/clients": {
|
|
1005
|
+
get: {
|
|
1006
|
+
tags: ["Runtime inventory"],
|
|
1007
|
+
summary: "List visible engine clients.",
|
|
1008
|
+
operationId: "list_engine_clients",
|
|
1009
|
+
parameters: [{ $ref: "#/components/parameters/ListClientsParams" }],
|
|
1010
|
+
security: [{ bearerAuth: [] }],
|
|
1011
|
+
responses: {
|
|
1012
|
+
"200": jsonArrayResponse(
|
|
1013
|
+
"Visible clients.",
|
|
1014
|
+
"#/components/schemas/Client"
|
|
1015
|
+
),
|
|
1016
|
+
"400": { $ref: "#/components/responses/BadRequest" },
|
|
1017
|
+
"401": { $ref: "#/components/responses/Unauthorized" },
|
|
1018
|
+
"403": { $ref: "#/components/responses/Forbidden" }
|
|
1019
|
+
},
|
|
1020
|
+
"x-rstream-required-rules": readEngineStreamsRules
|
|
1021
|
+
}
|
|
1022
|
+
},
|
|
1023
|
+
"/api/tunnels": {
|
|
1024
|
+
get: {
|
|
1025
|
+
tags: ["Runtime inventory"],
|
|
1026
|
+
summary: "List visible engine tunnels.",
|
|
1027
|
+
operationId: "list_engine_tunnels",
|
|
1028
|
+
parameters: [{ $ref: "#/components/parameters/ListTunnelsParams" }],
|
|
1029
|
+
security: [{ bearerAuth: [] }],
|
|
1030
|
+
responses: {
|
|
1031
|
+
"200": jsonArrayResponse(
|
|
1032
|
+
"Visible tunnels. Fine-grained list scopes may filter objects and project fields.",
|
|
1033
|
+
"#/components/schemas/TunnelListItem"
|
|
1034
|
+
),
|
|
1035
|
+
"400": { $ref: "#/components/responses/BadRequest" },
|
|
1036
|
+
"401": { $ref: "#/components/responses/Unauthorized" },
|
|
1037
|
+
"403": { $ref: "#/components/responses/Forbidden" }
|
|
1038
|
+
},
|
|
1039
|
+
"x-rstream-required-rules": readEngineStateRules,
|
|
1040
|
+
"x-rstream-token-metadata-scope": "metadata.scopes.tunnels.list"
|
|
1041
|
+
}
|
|
1042
|
+
},
|
|
1043
|
+
"/api/sse": {
|
|
1044
|
+
get: {
|
|
1045
|
+
tags: ["Runtime watch"],
|
|
1046
|
+
summary: "Subscribe to visible engine state through Server-Sent Events.",
|
|
1047
|
+
operationId: "watch_engine_events_sse",
|
|
1048
|
+
parameters: [{ $ref: "#/components/parameters/WatchParams" }],
|
|
1049
|
+
security: [{ bearerAuth: [] }],
|
|
1050
|
+
responses: {
|
|
1051
|
+
"200": streamResponse(
|
|
1052
|
+
"SSE stream. Each event is emitted as a data line containing a JSON Event object."
|
|
1053
|
+
),
|
|
1054
|
+
"400": { $ref: "#/components/responses/BadRequest" },
|
|
1055
|
+
"401": { $ref: "#/components/responses/Unauthorized" },
|
|
1056
|
+
"403": { $ref: "#/components/responses/Forbidden" }
|
|
1057
|
+
},
|
|
1058
|
+
"x-rstream-event-schema": "#/components/schemas/Event",
|
|
1059
|
+
"x-rstream-required-rules": readEngineStateRules
|
|
1060
|
+
}
|
|
1061
|
+
},
|
|
1062
|
+
"/api/websocket": {
|
|
1063
|
+
get: {
|
|
1064
|
+
tags: ["Runtime watch"],
|
|
1065
|
+
summary: "Subscribe to visible engine state through a WebSocket upgrade.",
|
|
1066
|
+
operationId: "watch_engine_events_websocket",
|
|
1067
|
+
parameters: [{ $ref: "#/components/parameters/WatchParams" }],
|
|
1068
|
+
security: [{ bearerAuth: [] }],
|
|
1069
|
+
responses: {
|
|
1070
|
+
"101": {
|
|
1071
|
+
description: "WebSocket connection accepted. Frames contain JSON Event objects."
|
|
1072
|
+
},
|
|
1073
|
+
"400": { $ref: "#/components/responses/BadRequest" },
|
|
1074
|
+
"401": { $ref: "#/components/responses/Unauthorized" },
|
|
1075
|
+
"403": { $ref: "#/components/responses/Forbidden" }
|
|
1076
|
+
},
|
|
1077
|
+
"x-rstream-event-schema": "#/components/schemas/Event",
|
|
1078
|
+
"x-rstream-required-rules": readEngineStateRules
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
};
|
|
1083
|
+
var engineOpenApiJson = `${JSON.stringify(
|
|
1084
|
+
engineOpenApiDocument,
|
|
1085
|
+
null,
|
|
1086
|
+
2
|
|
1087
|
+
)}
|
|
1088
|
+
`;
|
|
1089
|
+
|
|
667
1090
|
// src/resolution.ts
|
|
668
1091
|
var import_rstream3 = require("@rstreamlabs/rstream");
|
|
669
1092
|
var import_rstream4 = require("@rstreamlabs/rstream");
|
|
670
1093
|
var import_rstream5 = require("@rstreamlabs/rstream");
|
|
671
1094
|
var import_rstream6 = require("@rstreamlabs/rstream");
|
|
672
1095
|
var import_rstream7 = require("@rstreamlabs/rstream");
|
|
673
|
-
var import_jsonwebtoken = __toESM(require("jsonwebtoken"));
|
|
674
1096
|
function trimOptionalString(value) {
|
|
675
1097
|
const normalized = value?.trim();
|
|
676
1098
|
return normalized ? normalized : void 0;
|
|
677
1099
|
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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;
|
|
681
1104
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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
|
+
}
|
|
687
1125
|
}
|
|
688
|
-
return
|
|
1126
|
+
return `${url.hostname.toLowerCase()}${explicitPort ? `:${explicitPort}` : ""}`;
|
|
689
1127
|
}
|
|
690
1128
|
function resolveTunnelsAPIURL(apiUrl) {
|
|
691
1129
|
return trimOptionalString(apiUrl) ?? (0, import_rstream6.readEnvironment)().apiUrl ?? import_rstream3.DEFAULT_API_URL;
|
|
@@ -719,37 +1157,22 @@ function resolveControlPlaneCredentials(controlPlaneCredentials, credentials, au
|
|
|
719
1157
|
async function resolveTunnelsEngine(config) {
|
|
720
1158
|
const explicitEngine = trimOptionalString(config.engine);
|
|
721
1159
|
if (explicitEngine) {
|
|
722
|
-
return explicitEngine;
|
|
1160
|
+
return normalizeEngineAddress(explicitEngine);
|
|
723
1161
|
}
|
|
724
1162
|
const environmentEngine = (0, import_rstream6.readEnvironment)().engine;
|
|
725
1163
|
if (environmentEngine) {
|
|
726
|
-
return environmentEngine;
|
|
1164
|
+
return normalizeEngineAddress(environmentEngine);
|
|
727
1165
|
}
|
|
728
1166
|
const token = trimOptionalString(config.token) ?? ((0, import_rstream5.isTokenCredentials)(config.credentials) ? config.credentials.token : void 0);
|
|
729
|
-
const tokenEngine = getEngineFromToken(token);
|
|
730
|
-
if (tokenEngine) {
|
|
731
|
-
return tokenEngine;
|
|
732
|
-
}
|
|
733
1167
|
const projectEndpoint = trimOptionalString(config.projectEndpoint);
|
|
734
1168
|
if (projectEndpoint) {
|
|
735
|
-
const
|
|
736
|
-
config
|
|
737
|
-
config.credentials,
|
|
1169
|
+
const project = await resolveManagedTunnelsProject({
|
|
1170
|
+
...config,
|
|
738
1171
|
token
|
|
739
|
-
);
|
|
740
|
-
if (credentials === void 0) {
|
|
741
|
-
throw new Error(
|
|
742
|
-
"Control-plane credentials are required to resolve a managed project endpoint."
|
|
743
|
-
);
|
|
744
|
-
}
|
|
745
|
-
const controlPlane = new import_rstream7.RstreamClient({
|
|
746
|
-
apiUrl: resolveTunnelsAPIURL(config.apiUrl),
|
|
747
|
-
credentials
|
|
748
1172
|
});
|
|
749
|
-
const project = await controlPlane.tunnels.projects.resolveByEndpoint(projectEndpoint);
|
|
750
1173
|
const engine = (0, import_rstream4.getTunnelsProjectEngine)(project);
|
|
751
1174
|
if (engine) {
|
|
752
|
-
return engine;
|
|
1175
|
+
return normalizeEngineAddress(engine);
|
|
753
1176
|
}
|
|
754
1177
|
throw new Error(
|
|
755
1178
|
"Failed to resolve the engine address from the managed tunnels project."
|
|
@@ -759,8 +1182,36 @@ async function resolveTunnelsEngine(config) {
|
|
|
759
1182
|
"Engine URL is not defined. Provide an engine, use RSTREAM_ENGINE, or configure a managed project endpoint with control-plane access."
|
|
760
1183
|
);
|
|
761
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
|
+
}
|
|
762
1206
|
|
|
763
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
|
+
}
|
|
764
1215
|
var RstreamTunnelsResource = class {
|
|
765
1216
|
client;
|
|
766
1217
|
constructor(client) {
|
|
@@ -774,65 +1225,76 @@ var RstreamTunnelsResource = class {
|
|
|
774
1225
|
return listTunnelsResponseSchema.parse(response);
|
|
775
1226
|
}
|
|
776
1227
|
async get(id) {
|
|
777
|
-
const response = await this.client.request(
|
|
778
|
-
|
|
779
|
-
|
|
1228
|
+
const response = await this.client.request(
|
|
1229
|
+
`/tunnels/${tunnelPathId(id)}`,
|
|
1230
|
+
{
|
|
1231
|
+
method: "GET"
|
|
1232
|
+
}
|
|
1233
|
+
);
|
|
780
1234
|
return tunnelSchema.parse(response);
|
|
781
1235
|
}
|
|
782
1236
|
};
|
|
783
1237
|
|
|
784
1238
|
// src/tunnels.ts
|
|
785
1239
|
var import_rstream12 = require("@rstreamlabs/rstream");
|
|
1240
|
+
var import_rstream13 = require("@rstreamlabs/rstream");
|
|
1241
|
+
var import_rstream14 = require("@rstreamlabs/rstream");
|
|
786
1242
|
|
|
787
1243
|
// src/turn.ts
|
|
788
1244
|
var import_rstream8 = require("@rstreamlabs/rstream");
|
|
789
1245
|
var import_rstream9 = require("@rstreamlabs/rstream");
|
|
790
1246
|
var import_rstream10 = require("@rstreamlabs/rstream");
|
|
791
1247
|
var import_rstream11 = require("@rstreamlabs/rstream");
|
|
792
|
-
var
|
|
1248
|
+
var z8 = __toESM(require("zod"));
|
|
793
1249
|
var import_crypto = __toESM(require("crypto"));
|
|
794
1250
|
var defaultTURNPort = 3478;
|
|
795
1251
|
var defaultTURNSPort = 5349;
|
|
796
|
-
var defaultTURNCredentialTTLSeconds =
|
|
797
|
-
var
|
|
798
|
-
|
|
799
|
-
|
|
1252
|
+
var defaultTURNCredentialTTLSeconds = 10 * 60;
|
|
1253
|
+
var maxTURNCredentialTTLSeconds = 60 * 60;
|
|
1254
|
+
var turnTokenClaimsSchema = z8.object({
|
|
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"])
|
|
800
1259
|
});
|
|
801
|
-
var turnCredentialModeSchema =
|
|
802
|
-
function
|
|
1260
|
+
var turnCredentialModeSchema = z8.enum(["api", "app", "pat"]);
|
|
1261
|
+
function normalizeOptionalString2(value) {
|
|
803
1262
|
const normalized = value?.trim();
|
|
804
1263
|
return normalized ? normalized : void 0;
|
|
805
1264
|
}
|
|
806
1265
|
function normalizeClusterDomain(clusterDomain) {
|
|
807
|
-
const normalized =
|
|
1266
|
+
const normalized = normalizeOptionalString2(clusterDomain);
|
|
808
1267
|
return normalized?.toLowerCase();
|
|
809
1268
|
}
|
|
810
1269
|
function normalizeNow(now) {
|
|
811
|
-
if (now
|
|
812
|
-
return Math.floor(now
|
|
1270
|
+
if (now === void 0) {
|
|
1271
|
+
return Math.floor(Date.now() / 1e3);
|
|
813
1272
|
}
|
|
814
|
-
|
|
815
|
-
|
|
1273
|
+
const timestamp = now instanceof Date ? now.getTime() / 1e3 : now;
|
|
1274
|
+
if (!Number.isFinite(timestamp)) {
|
|
1275
|
+
throw new Error("TURN timestamp must be finite.");
|
|
816
1276
|
}
|
|
817
|
-
return Math.floor(
|
|
1277
|
+
return Math.floor(timestamp);
|
|
818
1278
|
}
|
|
819
1279
|
function normalizeTTLSeconds(ttlSeconds) {
|
|
820
|
-
|
|
821
|
-
|
|
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.");
|
|
822
1283
|
}
|
|
823
|
-
return
|
|
1284
|
+
return ttl;
|
|
824
1285
|
}
|
|
825
|
-
function
|
|
826
|
-
|
|
827
|
-
|
|
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.`);
|
|
828
1290
|
}
|
|
829
|
-
return
|
|
1291
|
+
return normalized;
|
|
1292
|
+
}
|
|
1293
|
+
function normalizeTURNPort(port) {
|
|
1294
|
+
return normalizePort(port, defaultTURNPort, "TURN port");
|
|
830
1295
|
}
|
|
831
1296
|
function normalizeTURNSPort(port) {
|
|
832
|
-
|
|
833
|
-
return Math.floor(port);
|
|
834
|
-
}
|
|
835
|
-
return defaultTURNSPort;
|
|
1297
|
+
return normalizePort(port, defaultTURNSPort, "TURNS port");
|
|
836
1298
|
}
|
|
837
1299
|
function createTURNURLs(clusterDomain, turnPort, turnsPort) {
|
|
838
1300
|
const normalizedTURNPort = normalizeTURNPort(turnPort);
|
|
@@ -850,16 +1312,32 @@ function parseTURNTokenClaims(token) {
|
|
|
850
1312
|
if (!payloadPart) {
|
|
851
1313
|
throw new Error("Invalid token format.");
|
|
852
1314
|
}
|
|
853
|
-
|
|
854
|
-
|
|
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);
|
|
855
1323
|
if (!parsed.success) {
|
|
856
1324
|
throw new Error("Invalid token format.");
|
|
857
1325
|
}
|
|
858
1326
|
return parsed.data;
|
|
859
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
|
+
}
|
|
860
1338
|
function deriveClusterDomainFromEngine(projectEndpoint, engine) {
|
|
861
|
-
const normalizedProjectEndpoint =
|
|
862
|
-
const normalizedEngine =
|
|
1339
|
+
const normalizedProjectEndpoint = normalizeOptionalString2(projectEndpoint);
|
|
1340
|
+
const normalizedEngine = normalizeOptionalString2(engine);
|
|
863
1341
|
if (!normalizedProjectEndpoint || !normalizedEngine) {
|
|
864
1342
|
throw new Error(
|
|
865
1343
|
"Failed to derive the cluster domain from the engine address."
|
|
@@ -893,17 +1371,11 @@ function resolveMode(credentials, mode) {
|
|
|
893
1371
|
if ((0, import_rstream8.isClientCredentials)(credentials)) {
|
|
894
1372
|
return "app";
|
|
895
1373
|
}
|
|
896
|
-
if ((0, import_rstream9.isTokenCredentials)(credentials)) {
|
|
897
|
-
const claims = parseTURNTokenClaims(credentials.token);
|
|
898
|
-
if (claims.type === "pat" && normalizeOptionalString(claims.token_endpoint)) {
|
|
899
|
-
return "pat";
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
1374
|
return "api";
|
|
903
1375
|
}
|
|
904
1376
|
async function loadTURNServerPublicKeyHex(clusterDomain, options) {
|
|
905
|
-
const configuredKey =
|
|
906
|
-
const keyringBaseURL = configuredKey ??
|
|
1377
|
+
const configuredKey = normalizeOptionalString2(options.keyringBaseUrl);
|
|
1378
|
+
const keyringBaseURL = configuredKey ?? normalizeOptionalString2(options.apiUrl) ?? "https://rstream.io";
|
|
907
1379
|
const url = new URL(
|
|
908
1380
|
`/keyrings/turn/${clusterDomain}.spki.der.hex`,
|
|
909
1381
|
keyringBaseURL
|
|
@@ -915,8 +1387,8 @@ async function loadTURNServerPublicKeyHex(clusterDomain, options) {
|
|
|
915
1387
|
return (await response.text()).trim();
|
|
916
1388
|
}
|
|
917
1389
|
async function createAPITURNCredentials(options) {
|
|
918
|
-
const projectId =
|
|
919
|
-
const projectEndpoint =
|
|
1390
|
+
const projectId = normalizeOptionalString2(options.projectId);
|
|
1391
|
+
const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
|
|
920
1392
|
if (!projectId && !projectEndpoint) {
|
|
921
1393
|
throw new Error(
|
|
922
1394
|
"Project ID or project endpoint is required for TURN API mode."
|
|
@@ -943,11 +1415,11 @@ function createPATTURNCredentials(options) {
|
|
|
943
1415
|
if (!clusterDomain) {
|
|
944
1416
|
throw new Error("Cluster domain is required for TURN PAT mode.");
|
|
945
1417
|
}
|
|
946
|
-
const projectEndpoint =
|
|
1418
|
+
const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
|
|
947
1419
|
if (!projectEndpoint) {
|
|
948
1420
|
throw new Error("Project endpoint is required for TURN PAT mode.");
|
|
949
1421
|
}
|
|
950
|
-
const token =
|
|
1422
|
+
const token = normalizeOptionalString2(options.token);
|
|
951
1423
|
if (!token) {
|
|
952
1424
|
throw new Error("Token is required for TURN PAT mode.");
|
|
953
1425
|
}
|
|
@@ -955,14 +1427,17 @@ function createPATTURNCredentials(options) {
|
|
|
955
1427
|
if (claims.type !== "pat") {
|
|
956
1428
|
throw new Error("TURN PAT mode requires a PAT token.");
|
|
957
1429
|
}
|
|
958
|
-
const tokenEndpoint =
|
|
1430
|
+
const tokenEndpoint = normalizeOptionalString2(options.tokenEndpoint);
|
|
959
1431
|
if (!tokenEndpoint) {
|
|
960
|
-
throw new Error(
|
|
961
|
-
|
|
962
|
-
|
|
1432
|
+
throw new Error("Token endpoint is required for TURN PAT mode.");
|
|
1433
|
+
}
|
|
1434
|
+
if (claims.token_endpoint !== void 0 && claims.token_endpoint !== tokenEndpoint) {
|
|
1435
|
+
throw new Error("Token endpoint does not match the PAT token.");
|
|
963
1436
|
}
|
|
1437
|
+
const now = normalizeNow(options.now);
|
|
964
1438
|
const ttlSeconds = normalizeTTLSeconds(options.ttlSeconds);
|
|
965
|
-
const exp =
|
|
1439
|
+
const exp = Math.min(now + ttlSeconds, requirePATExpiration(claims, now));
|
|
1440
|
+
const ttl = exp - now;
|
|
966
1441
|
const username = `v1:${exp}:pat:${projectEndpoint}:${tokenEndpoint}`;
|
|
967
1442
|
const tokenHash = import_crypto.default.createHash("sha256").update(token, "utf8").digest();
|
|
968
1443
|
const key = Buffer.from(
|
|
@@ -977,7 +1452,7 @@ function createPATTURNCredentials(options) {
|
|
|
977
1452
|
const credential = import_crypto.default.createHmac("sha256", key).update(username, "utf8").digest("base64");
|
|
978
1453
|
return import_rstream11.turnCredentialsSchema.parse({
|
|
979
1454
|
credential,
|
|
980
|
-
ttl
|
|
1455
|
+
ttl,
|
|
981
1456
|
urls: createTURNURLs(clusterDomain, options.turnPort, options.turnsPort),
|
|
982
1457
|
username
|
|
983
1458
|
});
|
|
@@ -987,15 +1462,15 @@ async function createAPPTURNCredentials(options) {
|
|
|
987
1462
|
if (!clusterDomain) {
|
|
988
1463
|
throw new Error("Cluster domain is required for TURN APP mode.");
|
|
989
1464
|
}
|
|
990
|
-
const projectEndpoint =
|
|
1465
|
+
const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
|
|
991
1466
|
if (!projectEndpoint) {
|
|
992
1467
|
throw new Error("Project endpoint is required for TURN APP mode.");
|
|
993
1468
|
}
|
|
994
|
-
const clientId =
|
|
1469
|
+
const clientId = normalizeOptionalString2(options.clientId);
|
|
995
1470
|
if (!clientId) {
|
|
996
1471
|
throw new Error("Client ID is required for TURN APP mode.");
|
|
997
1472
|
}
|
|
998
|
-
const clientSecret =
|
|
1473
|
+
const clientSecret = normalizeOptionalString2(options.clientSecret);
|
|
999
1474
|
if (!clientSecret) {
|
|
1000
1475
|
throw new Error("Client secret is required for TURN APP mode.");
|
|
1001
1476
|
}
|
|
@@ -1007,7 +1482,7 @@ async function createAPPTURNCredentials(options) {
|
|
|
1007
1482
|
format: "der",
|
|
1008
1483
|
type: "pkcs8"
|
|
1009
1484
|
});
|
|
1010
|
-
const serverPublicKeyHex =
|
|
1485
|
+
const serverPublicKeyHex = normalizeOptionalString2(options.serverPublicKeyHex) ?? await loadTURNServerPublicKeyHex(clusterDomain, options);
|
|
1011
1486
|
const serverPublicKey = import_crypto.default.createPublicKey({
|
|
1012
1487
|
key: Buffer.from(serverPublicKeyHex, "hex"),
|
|
1013
1488
|
format: "der",
|
|
@@ -1037,7 +1512,7 @@ async function createAPPTURNCredentials(options) {
|
|
|
1037
1512
|
async function createTURNCredentials(options) {
|
|
1038
1513
|
const credentials = options.credentials;
|
|
1039
1514
|
const mode = resolveMode(credentials, options.mode);
|
|
1040
|
-
const projectEndpoint =
|
|
1515
|
+
const projectEndpoint = normalizeOptionalString2(options.projectEndpoint);
|
|
1041
1516
|
if (mode === "api") {
|
|
1042
1517
|
const controlPlaneCredentials = options.controlPlaneCredentials ?? credentials;
|
|
1043
1518
|
if (!controlPlaneCredentials) {
|
|
@@ -1068,6 +1543,10 @@ async function createTURNCredentials(options) {
|
|
|
1068
1543
|
"Project endpoint is required for TURN PAT mode."
|
|
1069
1544
|
),
|
|
1070
1545
|
token: credentials.token,
|
|
1546
|
+
tokenEndpoint: requireOption(
|
|
1547
|
+
normalizeOptionalString2(options.tokenEndpoint),
|
|
1548
|
+
"Token endpoint is required for TURN PAT mode."
|
|
1549
|
+
),
|
|
1071
1550
|
ttlSeconds: options.ttlSeconds,
|
|
1072
1551
|
turnPort: options.turnPort,
|
|
1073
1552
|
turnsPort: options.turnsPort
|
|
@@ -1117,6 +1596,7 @@ var RstreamTURNResource = class {
|
|
|
1117
1596
|
projectEndpoint: options.projectEndpoint ?? this.client.projectEndpoint,
|
|
1118
1597
|
projectId: options.projectId,
|
|
1119
1598
|
serverPublicKeyHex: options.serverPublicKeyHex,
|
|
1599
|
+
tokenEndpoint: options.tokenEndpoint,
|
|
1120
1600
|
ttlSeconds: options.ttlSeconds,
|
|
1121
1601
|
turnPort: options.turnPort,
|
|
1122
1602
|
turnsPort: options.turnsPort
|
|
@@ -1126,6 +1606,41 @@ var RstreamTURNResource = class {
|
|
|
1126
1606
|
|
|
1127
1607
|
// src/webhooks-resource.ts
|
|
1128
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
|
+
}
|
|
1129
1644
|
var RstreamWebhookResource = class {
|
|
1130
1645
|
async event(payload, header, secret, tolerance = 300, receivedAt = Date.now()) {
|
|
1131
1646
|
const payloadBuffer = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
|
|
@@ -1133,31 +1648,26 @@ var RstreamWebhookResource = class {
|
|
|
1133
1648
|
if (!signatureHeader) {
|
|
1134
1649
|
throw new Error("No signature header");
|
|
1135
1650
|
}
|
|
1651
|
+
assertSecretConfigured(secret);
|
|
1136
1652
|
const elements = signatureHeader.split(",").map((element) => element.trim());
|
|
1137
1653
|
const timestamp = elements.find((element) => element.startsWith("t="))?.substring(2);
|
|
1138
1654
|
const signatures = elements.filter((element) => element.startsWith("v1=")).map((element) => element.substring(3));
|
|
1139
1655
|
if (!timestamp || signatures.length === 0) {
|
|
1140
1656
|
throw new Error("Invalid signature header format");
|
|
1141
1657
|
}
|
|
1142
|
-
const timestampNum =
|
|
1658
|
+
const timestampNum = parseSignatureTimestamp(timestamp);
|
|
1659
|
+
const signatureTolerance = normalizeTolerance(tolerance);
|
|
1143
1660
|
const now = Math.floor(receivedAt / 1e3);
|
|
1144
|
-
if (Math.abs(now - timestampNum) >
|
|
1661
|
+
if (Math.abs(now - timestampNum) > signatureTolerance) {
|
|
1145
1662
|
throw new Error(
|
|
1146
1663
|
`Webhook signature timestamp outside tolerance: ${now}, ${timestampNum}`
|
|
1147
1664
|
);
|
|
1148
1665
|
}
|
|
1149
1666
|
const signedPayload = `${timestamp}.${payloadBuffer.toString("utf8")}`;
|
|
1150
|
-
const expectedSignature = import_crypto2.default.createHmac("sha256", secret).update(signedPayload).digest(
|
|
1151
|
-
const signatureMatched = signatures.some(
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
Buffer.from(signature),
|
|
1155
|
-
Buffer.from(expectedSignature)
|
|
1156
|
-
);
|
|
1157
|
-
} catch {
|
|
1158
|
-
return false;
|
|
1159
|
-
}
|
|
1160
|
-
});
|
|
1667
|
+
const expectedSignature = import_crypto2.default.createHmac("sha256", secret).update(signedPayload).digest();
|
|
1668
|
+
const signatureMatched = signatures.some(
|
|
1669
|
+
(signature) => signatureMatches(signature, expectedSignature)
|
|
1670
|
+
);
|
|
1161
1671
|
if (!signatureMatched) {
|
|
1162
1672
|
throw new Error("Signature verification failed");
|
|
1163
1673
|
}
|
|
@@ -1175,8 +1685,16 @@ var RstreamWebhookResource = class {
|
|
|
1175
1685
|
};
|
|
1176
1686
|
|
|
1177
1687
|
// src/tunnels.ts
|
|
1688
|
+
var engineAuthTokenScopes = {
|
|
1689
|
+
tunnels: {
|
|
1690
|
+
connect: true,
|
|
1691
|
+
create: true,
|
|
1692
|
+
list: true
|
|
1693
|
+
}
|
|
1694
|
+
};
|
|
1178
1695
|
var RstreamTunnelsClient = class {
|
|
1179
1696
|
config;
|
|
1697
|
+
managedProject;
|
|
1180
1698
|
constructor(config) {
|
|
1181
1699
|
this.config = config;
|
|
1182
1700
|
}
|
|
@@ -1196,6 +1714,21 @@ var RstreamTunnelsClient = class {
|
|
|
1196
1714
|
return this.config?.projectEndpoint;
|
|
1197
1715
|
}
|
|
1198
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
|
+
}
|
|
1199
1732
|
return await resolveTunnelsEngine({
|
|
1200
1733
|
apiUrl: this.config?.apiUrl,
|
|
1201
1734
|
controlPlaneCredentials: this.controlPlaneCredentials,
|
|
@@ -1204,6 +1737,57 @@ var RstreamTunnelsClient = class {
|
|
|
1204
1737
|
projectEndpoint: this.config?.projectEndpoint
|
|
1205
1738
|
});
|
|
1206
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
|
+
}
|
|
1207
1791
|
get auth() {
|
|
1208
1792
|
return new RstreamAuthResource(this);
|
|
1209
1793
|
}
|
|
@@ -1224,16 +1808,20 @@ var RstreamTunnelsClient = class {
|
|
|
1224
1808
|
if (!credentials) {
|
|
1225
1809
|
return void 0;
|
|
1226
1810
|
}
|
|
1227
|
-
if (!(0,
|
|
1811
|
+
if (!(0, import_rstream13.isClientCredentials)(credentials)) {
|
|
1228
1812
|
return credentials.token;
|
|
1229
1813
|
}
|
|
1230
|
-
|
|
1814
|
+
const tokenParams = await this.getEngineAuthTokenParams();
|
|
1815
|
+
return (await this.auth.createAuthToken(tokenParams, {
|
|
1231
1816
|
credentials,
|
|
1232
1817
|
engine
|
|
1233
1818
|
})).token;
|
|
1234
1819
|
}
|
|
1235
1820
|
async request(path, options) {
|
|
1236
|
-
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
|
+
}
|
|
1237
1825
|
const url = `https://${engine}/api${path}`;
|
|
1238
1826
|
const headers = new Headers(options?.headers || {});
|
|
1239
1827
|
const token = await this.getToken(engine);
|
|
@@ -1242,7 +1830,8 @@ var RstreamTunnelsClient = class {
|
|
|
1242
1830
|
}
|
|
1243
1831
|
const response = await fetch(url, {
|
|
1244
1832
|
...options,
|
|
1245
|
-
headers
|
|
1833
|
+
headers,
|
|
1834
|
+
redirect: options?.redirect ?? "manual"
|
|
1246
1835
|
});
|
|
1247
1836
|
if (!response.ok) {
|
|
1248
1837
|
const errorText = await response.text();
|
|
@@ -1251,8 +1840,68 @@ var RstreamTunnelsClient = class {
|
|
|
1251
1840
|
return await response.json();
|
|
1252
1841
|
}
|
|
1253
1842
|
};
|
|
1843
|
+
function normalizeOptionalString3(value) {
|
|
1844
|
+
const normalized = value?.trim();
|
|
1845
|
+
return normalized ? normalized : void 0;
|
|
1846
|
+
}
|
|
1254
1847
|
|
|
1255
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
|
+
}
|
|
1256
1905
|
var Watch = class {
|
|
1257
1906
|
connection = null;
|
|
1258
1907
|
connectionState = "preparing";
|
|
@@ -1267,45 +1916,56 @@ var Watch = class {
|
|
|
1267
1916
|
throw new Error("Watch: Connection already started or closed.");
|
|
1268
1917
|
}
|
|
1269
1918
|
this.connectionState = "connecting";
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
const
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
this.connectionState = "connected";
|
|
1292
|
-
this.events.onConnect?.();
|
|
1293
|
-
};
|
|
1294
|
-
this.connection.onmessage = (msg) => {
|
|
1295
|
-
const parsed = wsEventsSchema.parse(JSON.parse(msg.data));
|
|
1296
|
-
this.events.onEvent?.(parsed);
|
|
1297
|
-
};
|
|
1298
|
-
this.connection.onerror = () => {
|
|
1299
|
-
if (this.connectionState !== "closed") {
|
|
1300
|
-
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());
|
|
1301
1940
|
}
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
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 = () => {
|
|
1305
1947
|
if (this.connectionState !== "closed") {
|
|
1306
1948
|
this.disconnect();
|
|
1307
1949
|
}
|
|
1308
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();
|
|
1309
1969
|
}
|
|
1310
1970
|
}
|
|
1311
1971
|
disconnect() {
|
|
@@ -1324,7 +1984,7 @@ var Watch = class {
|
|
|
1324
1984
|
};
|
|
1325
1985
|
|
|
1326
1986
|
// src/webtty.ts
|
|
1327
|
-
var
|
|
1987
|
+
var z9 = __toESM(require("zod"));
|
|
1328
1988
|
var osFamilies = [
|
|
1329
1989
|
"linux",
|
|
1330
1990
|
"macos",
|
|
@@ -1333,25 +1993,25 @@ var osFamilies = [
|
|
|
1333
1993
|
"openbsd",
|
|
1334
1994
|
"freebsd"
|
|
1335
1995
|
];
|
|
1336
|
-
var webttyServerSchema =
|
|
1337
|
-
tunnel_id:
|
|
1338
|
-
host:
|
|
1339
|
-
token_auth:
|
|
1340
|
-
os_family:
|
|
1341
|
-
arch:
|
|
1342
|
-
os_id:
|
|
1996
|
+
var webttyServerSchema = z9.object({
|
|
1997
|
+
tunnel_id: z9.string(),
|
|
1998
|
+
host: z9.string(),
|
|
1999
|
+
token_auth: z9.boolean(),
|
|
2000
|
+
os_family: z9.enum(osFamilies).optional(),
|
|
2001
|
+
arch: z9.string().optional(),
|
|
2002
|
+
os_id: z9.string().optional(),
|
|
1343
2003
|
// /etc/os-release::ID (ubuntu, debian, rocky, etc.)
|
|
1344
|
-
os_version_id:
|
|
2004
|
+
os_version_id: z9.string().optional(),
|
|
1345
2005
|
// /etc/os-release::VERSION_ID (24.04, 22.04, 11, 10.0.19045, ...)
|
|
1346
|
-
os_version_codename:
|
|
2006
|
+
os_version_codename: z9.string().optional(),
|
|
1347
2007
|
// /etc/os-release::VERSION_CODENAME (jammy, noble...)
|
|
1348
|
-
os_pretty_name:
|
|
2008
|
+
os_pretty_name: z9.string().optional(),
|
|
1349
2009
|
// /etc/os-release::PRETTY_NAME
|
|
1350
|
-
kernel_release:
|
|
2010
|
+
kernel_release: z9.string().optional(),
|
|
1351
2011
|
// uname -r
|
|
1352
|
-
hostname:
|
|
2012
|
+
hostname: z9.string().optional(),
|
|
1353
2013
|
// uname -n
|
|
1354
|
-
labels:
|
|
2014
|
+
labels: z9.record(z9.string(), z9.string().optional()).optional()
|
|
1355
2015
|
});
|
|
1356
2016
|
function parser(tunnel) {
|
|
1357
2017
|
if (tunnel.status !== "online") return null;
|
|
@@ -1409,6 +2069,7 @@ function parseWebTTYServers(tunnels) {
|
|
|
1409
2069
|
authTokenScopesSchema,
|
|
1410
2070
|
authTokenTunnelGrantSchema,
|
|
1411
2071
|
authTokenTunnelsScopesSchema,
|
|
2072
|
+
clientFilterSchema,
|
|
1412
2073
|
clientSchema,
|
|
1413
2074
|
createAPITURNCredentials,
|
|
1414
2075
|
createAPPTURNCredentials,
|
|
@@ -1417,14 +2078,18 @@ function parseWebTTYServers(tunnels) {
|
|
|
1417
2078
|
createAuthTokenResponseSchema,
|
|
1418
2079
|
createPATTURNCredentials,
|
|
1419
2080
|
createTURNCredentials,
|
|
2081
|
+
engineOpenApiDocument,
|
|
2082
|
+
engineOpenApiJson,
|
|
1420
2083
|
eventSchema,
|
|
1421
2084
|
formatTunnelHost,
|
|
1422
2085
|
listClientsParamsSchema,
|
|
1423
2086
|
listClientsResponseSchema,
|
|
1424
2087
|
listTunnelsParamsSchema,
|
|
1425
2088
|
listTunnelsResponseSchema,
|
|
2089
|
+
normalizeEngineAddress,
|
|
1426
2090
|
parseWebTTYServers,
|
|
1427
2091
|
resolveControlPlaneCredentials,
|
|
2092
|
+
resolveManagedTunnelsProject,
|
|
1428
2093
|
resolveTunnelsAPIURL,
|
|
1429
2094
|
resolveTunnelsCredentials,
|
|
1430
2095
|
resolveTunnelsEngine,
|
|
@@ -1432,6 +2097,7 @@ function parseWebTTYServers(tunnels) {
|
|
|
1432
2097
|
streamSummarySchema,
|
|
1433
2098
|
streamTLSInfoSchema,
|
|
1434
2099
|
tokenSchema,
|
|
2100
|
+
tunnelFilterSchema,
|
|
1435
2101
|
tunnelSchema,
|
|
1436
2102
|
turnCredentialModeSchema,
|
|
1437
2103
|
webhookEventsSchema,
|