@rstreamlabs/tunnels 3.8.3 → 3.10.1

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 CHANGED
@@ -340,3 +340,57 @@ npm --workspace @rstreamlabs/tunnels run type-check
340
340
  npm --workspace @rstreamlabs/tunnels run lint
341
341
  npm --workspace @rstreamlabs/tunnels run build
342
342
  ```
343
+
344
+ ## Building
345
+
346
+ `npm run build` emits declarations with TypeScript before bundling them. This
347
+ keeps inferred schema property order deterministic; per-module declaration
348
+ emission during asynchronous bundling can otherwise change published archive
349
+ bytes between identical builds. `.generated/types/` is temporary build output. The watch
350
+ command continues to infer declarations directly for development.
351
+
352
+ ## Certificate-only Engine access
353
+
354
+ Node.js clients may reuse a certificate identity for Engine inventory, TURN
355
+ issuance and SSE watch streams:
356
+
357
+ ```ts
358
+ const client = new RstreamTunnelsClient({
359
+ engine: "<project-engine-host>:443",
360
+ tls: { cert: deviceCertificatePEM, key: devicePrivateKeyPEM },
361
+ });
362
+ try {
363
+ const turn = await client.turn.createCredentials({ ttlSeconds: 600 });
364
+ const tunnels = await client.tunnels.list();
365
+ } finally {
366
+ await client.close();
367
+ }
368
+ ```
369
+
370
+ Grant the enrolled credential `turn.credentials.create` and the required Engine
371
+ permissions, restricted to its project. Do not also pass token credentials.
372
+ The client anonymously discovers `/.well-known/rstream/engine` and caches its
373
+ project-bound HTTPS `mtlsApiUrl` in memory, normally for one hour. Discovery uses
374
+ verified server TLS without the certificate, cookies, Control plane headers or
375
+ redirects. Optional `mtlsApiUrl` config provides an explicit deployment override;
376
+ no shared YAML change is required. The ordinary browser host never requests a
377
+ client certificate. Browser/token behavior is unchanged.
378
+
379
+ Node.js uses HTTPS/TCP. Certificate watch streams use SSE with this reusable
380
+ client; the WebSocket watch adapter rejects certificate mode. The Engine itself
381
+ also implements HTTP/3, which the Go SDK supports for API/SSE calls.
382
+
383
+ Automatic TURN mode prefers eligible local PAT/APP derivation with known relay
384
+ metadata, then the Control plane, and finally the configured Engine on network
385
+ unavailability or HTTP 502/503/504. Certificate mode goes directly to Engine.
386
+ Auth, TLS verification, schema, throttling and caller cancellation errors never
387
+ cause fallback. Explicit `api`, `engine`, `pat` and `app` modes remain available.
388
+ The default total budget is ten seconds; the automatic Control plane phase gets
389
+ at most five seconds. APP API proofs are locally signed with issuance-only rights.
390
+
391
+ Both managed APIs return `{username, credential, urls, ttl}`; TTL is effective
392
+ remaining seconds, clipped to all backing-proof expirations. Issuance needs
393
+ `turn.credentials.create`; local derivation needs `turn.relay.allocate`. Relay
394
+ membership, scope, revocation and quotas are still checked. Treat the returned
395
+ username/password as opaque and renew before expiry. See the shared PAT/APP
396
+ fixtures and real HTTPS tests in `test/mtls.test.cjs`.
@@ -1000,6 +1000,19 @@ var watchParamsSchema = z7.object({
1000
1000
  tunnels: tunnelFilterSchema.optional()
1001
1001
  });
1002
1002
  var enginePermissions = {
1003
+ "turn.credentials.create": {
1004
+ name: "TURN Credentials: Create",
1005
+ description: "Issue short-lived TURN credentials through either the Control plane or Engine API.",
1006
+ rules: [
1007
+ {
1008
+ resource: [
1009
+ "api.turn-server.credentials",
1010
+ "engine.turn-server.credentials"
1011
+ ],
1012
+ action: "create"
1013
+ }
1014
+ ]
1015
+ },
1003
1016
  "tunnels.resources.read-only": {
1004
1017
  name: "Discovery: Read-Only",
1005
1018
  description: "Read active clients, active tunnels, and live state events.",
@@ -1079,13 +1092,13 @@ var engineOpenApiDocument = {
1079
1092
  }
1080
1093
  }
1081
1094
  ],
1082
- security: [
1083
- {
1084
- bearerAuth: []
1085
- }
1086
- ],
1095
+ security: [{ bearerAuth: [] }, { mutualTLS: [] }],
1087
1096
  components: {
1088
1097
  securitySchemes: {
1098
+ mutualTLS: {
1099
+ type: "mutualTLS",
1100
+ description: "EE project API only, on the dedicated authority returned by anonymous HTTPS discovery. Present one registered client certificate without a bearer or query token. Ordinary browser-facing hosts never request a client certificate."
1101
+ },
1089
1102
  bearerAuth: {
1090
1103
  type: "http",
1091
1104
  scheme: "bearer",
@@ -1153,6 +1166,64 @@ var engineOpenApiDocument = {
1153
1166
  Forbidden: errorResponse("The token is not authorized.")
1154
1167
  },
1155
1168
  schemas: {
1169
+ EngineDiscovery: {
1170
+ type: "object",
1171
+ required: ["version", "projectEndpoint", "mtlsApiUrl", "capabilities"],
1172
+ properties: {
1173
+ version: { type: "integer", const: 1 },
1174
+ projectEndpoint: { type: "string" },
1175
+ mtlsApiUrl: {
1176
+ type: "string",
1177
+ format: "uri",
1178
+ example: "https://project.mtls.engine.example/api"
1179
+ },
1180
+ capabilities: {
1181
+ type: "array",
1182
+ items: { type: "string" },
1183
+ examples: [["engine-api-mtls", "turn-credentials-v2"]]
1184
+ }
1185
+ }
1186
+ },
1187
+ CreateTURNCredentials: {
1188
+ type: "object",
1189
+ additionalProperties: false,
1190
+ properties: {
1191
+ ttlSeconds: {
1192
+ type: "integer",
1193
+ minimum: 1,
1194
+ maximum: 3600,
1195
+ default: 600
1196
+ }
1197
+ }
1198
+ },
1199
+ TURNCredentials: {
1200
+ type: "object",
1201
+ required: ["username", "credential", "urls", "ttl"],
1202
+ properties: {
1203
+ username: {
1204
+ type: "string",
1205
+ maxLength: 512,
1206
+ description: "Opaque short-lived v2 issued identity. Do not parse in clients."
1207
+ },
1208
+ credential: {
1209
+ type: "string",
1210
+ maxLength: 1024,
1211
+ description: "Short-lived TURN password."
1212
+ },
1213
+ urls: {
1214
+ type: "array",
1215
+ minItems: 1,
1216
+ maxItems: 16,
1217
+ items: { type: "string", maxLength: 2048 }
1218
+ },
1219
+ ttl: {
1220
+ type: "integer",
1221
+ minimum: 1,
1222
+ maximum: 3600,
1223
+ description: "Effective remaining lifetime in seconds, clipped to identity, source credential and certificate expiry."
1224
+ }
1225
+ }
1226
+ },
1156
1227
  Client: jsonSchemaFor(clientSchema),
1157
1228
  Event: jsonSchemaFor(wsEventsSchema),
1158
1229
  ListClientsParams: jsonSchemaFor(listClientsParamsSchema),
@@ -1187,6 +1258,133 @@ var engineOpenApiDocument = {
1187
1258
  "read-managed-webtty-recordings": ["webtty.logs.read-only"]
1188
1259
  },
1189
1260
  paths: {
1261
+ "/.well-known/rstream/engine": {
1262
+ get: {
1263
+ tags: ["Discovery"],
1264
+ summary: "Discover the dedicated project mTLS API.",
1265
+ operationId: "get_engine_discovery",
1266
+ description: "Anonymous GET on the ordinary project Engine authority. Follow the returned HTTPS URL; do not infer an mTLS hostname. Cache per client using Cache-Control (normally one hour). No certificate, bearer token, cookie or redirect is required. Available only where an explicit EE mapping is configured.",
1267
+ security: [],
1268
+ responses: {
1269
+ "200": {
1270
+ description: "Project-bound discovery. Capabilities describe enabled server features; callers still need the corresponding rights.",
1271
+ headers: {
1272
+ "Cache-Control": {
1273
+ schema: { type: "string" },
1274
+ example: "public, max-age=3600"
1275
+ }
1276
+ },
1277
+ content: {
1278
+ "application/json": {
1279
+ schema: { $ref: "#/components/schemas/EngineDiscovery" }
1280
+ }
1281
+ }
1282
+ },
1283
+ "404": {
1284
+ description: "Dedicated mTLS API is not configured on this authority."
1285
+ }
1286
+ }
1287
+ }
1288
+ },
1289
+ "/api/turn-server/credentials": {
1290
+ post: {
1291
+ tags: ["TURN"],
1292
+ summary: "Issue short-lived project TURN credentials.",
1293
+ operationId: "create_engine_turn_credentials",
1294
+ description: "EE managed projects. Same request and response contract as Control plane project TURN issuance. Empty body is valid. Issued credentials require turn.credentials.create only; turn.relay.allocate applies to local PAT/APP derivation. Issuance does not allocate a relay and does not bypass relay quotas or revocation checks.",
1295
+ security: [{ bearerAuth: [] }, { mutualTLS: [] }],
1296
+ requestBody: {
1297
+ required: false,
1298
+ content: {
1299
+ "application/json": {
1300
+ schema: { $ref: "#/components/schemas/CreateTURNCredentials" }
1301
+ }
1302
+ }
1303
+ },
1304
+ responses: {
1305
+ "200": {
1306
+ description: "Short-lived project TURN credentials.",
1307
+ headers: {
1308
+ "Cache-Control": {
1309
+ schema: { type: "string" },
1310
+ example: "no-store"
1311
+ }
1312
+ },
1313
+ content: {
1314
+ "application/json": {
1315
+ schema: { $ref: "#/components/schemas/TURNCredentials" }
1316
+ }
1317
+ }
1318
+ },
1319
+ "400": {
1320
+ description: "Malformed, unknown or out-of-range request fields; body exceeds 4 KiB.",
1321
+ headers: {
1322
+ "Cache-Control": {
1323
+ schema: { type: "string" },
1324
+ example: "no-store"
1325
+ }
1326
+ }
1327
+ },
1328
+ "401": {
1329
+ description: "Missing, invalid, expired or mixed authentication proofs.",
1330
+ headers: {
1331
+ "Cache-Control": {
1332
+ schema: { type: "string" },
1333
+ example: "no-store"
1334
+ }
1335
+ }
1336
+ },
1337
+ "403": {
1338
+ description: "Missing turn.credentials.create permission, workspace membership or project resource scope.",
1339
+ headers: {
1340
+ "Cache-Control": {
1341
+ schema: { type: "string" },
1342
+ example: "no-store"
1343
+ }
1344
+ }
1345
+ },
1346
+ "404": {
1347
+ description: "Project not found.",
1348
+ headers: {
1349
+ "Cache-Control": {
1350
+ schema: { type: "string" },
1351
+ example: "no-store"
1352
+ }
1353
+ }
1354
+ },
1355
+ "409": {
1356
+ description: "Project or serving infrastructure is inactive.",
1357
+ headers: {
1358
+ "Cache-Control": {
1359
+ schema: { type: "string" },
1360
+ example: "no-store"
1361
+ }
1362
+ }
1363
+ },
1364
+ "429": {
1365
+ description: "Rate limit exceeded; do not retry through another issuer.",
1366
+ headers: {
1367
+ "Cache-Control": {
1368
+ schema: { type: "string" },
1369
+ example: "no-store"
1370
+ }
1371
+ }
1372
+ },
1373
+ "503": {
1374
+ description: "Authentication backend, serving metadata or issued keyring unavailable.",
1375
+ headers: {
1376
+ "Cache-Control": {
1377
+ schema: { type: "string" },
1378
+ example: "no-store"
1379
+ }
1380
+ }
1381
+ }
1382
+ },
1383
+ "x-rstream-required-rules": [
1384
+ { resource: "engine.turn-server.credentials", action: "create" }
1385
+ ]
1386
+ }
1387
+ },
1190
1388
  "/api/openapi.json": {
1191
1389
  get: {
1192
1390
  tags: ["Discovery"],
@@ -1213,7 +1411,7 @@ var engineOpenApiDocument = {
1213
1411
  summary: "List visible engine clients.",
1214
1412
  operationId: "list_engine_clients",
1215
1413
  parameters: [{ $ref: "#/components/parameters/ListClientsParams" }],
1216
- security: [{ bearerAuth: [] }],
1414
+ security: [{ bearerAuth: [] }, { mutualTLS: [] }],
1217
1415
  responses: {
1218
1416
  "200": jsonArrayResponse(
1219
1417
  "Visible clients.",
@@ -1232,7 +1430,7 @@ var engineOpenApiDocument = {
1232
1430
  summary: "List visible engine tunnels.",
1233
1431
  operationId: "list_engine_tunnels",
1234
1432
  parameters: [{ $ref: "#/components/parameters/ListTunnelsParams" }],
1235
- security: [{ bearerAuth: [] }],
1433
+ security: [{ bearerAuth: [] }, { mutualTLS: [] }],
1236
1434
  responses: {
1237
1435
  "200": jsonArrayResponse(
1238
1436
  "Visible tunnels. Fine-grained list scopes may filter objects and project fields.",
@@ -1252,7 +1450,7 @@ var engineOpenApiDocument = {
1252
1450
  summary: "Subscribe to visible engine state through Server-Sent Events.",
1253
1451
  operationId: "watch_engine_events_sse",
1254
1452
  parameters: [{ $ref: "#/components/parameters/WatchParams" }],
1255
- security: [{ bearerAuth: [] }],
1453
+ security: [{ bearerAuth: [] }, { mutualTLS: [] }],
1256
1454
  responses: {
1257
1455
  "200": streamResponse(
1258
1456
  "SSE stream. Each event is emitted as a data line containing a JSON Event object."
@@ -1271,7 +1469,7 @@ var engineOpenApiDocument = {
1271
1469
  summary: "Subscribe to visible engine state through a WebSocket upgrade.",
1272
1470
  operationId: "watch_engine_events_websocket",
1273
1471
  parameters: [{ $ref: "#/components/parameters/WatchParams" }],
1274
- security: [{ bearerAuth: [] }],
1472
+ security: [{ bearerAuth: [] }, { mutualTLS: [] }],
1275
1473
  responses: {
1276
1474
  "101": {
1277
1475
  description: "WebSocket connection accepted. Frames contain JSON Event objects."