@rstreamlabs/rstream 1.1.0

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/dist/index.mjs ADDED
@@ -0,0 +1,490 @@
1
+ // src/auth-ressource.ts
2
+ import crypto from "crypto";
3
+ import jwt from "jsonwebtoken";
4
+ var RstreamAuthRessource = class {
5
+ client;
6
+ constructor(client) {
7
+ this.client = client;
8
+ }
9
+ async createShortTermToken(params, options) {
10
+ const credentials = options?.credentials || this.client.credentials;
11
+ if (!credentials || !("clientId" in credentials)) {
12
+ throw new Error(
13
+ "Application credentials (client id, client secret) are required to create a short term token."
14
+ );
15
+ }
16
+ const now = Math.floor(Date.now() / 1e3);
17
+ const exp = now + (params?.expires_in ?? 60);
18
+ const payload = {
19
+ iat: now,
20
+ // Issued at
21
+ exp,
22
+ // Expiration time
23
+ type: "app",
24
+ clientId: credentials.clientId,
25
+ metadata: {
26
+ engine: this.client.engine,
27
+ permissions: params?.permissions
28
+ }
29
+ };
30
+ const pk = crypto.createPrivateKey({
31
+ key: Buffer.from(credentials.clientSecret, "hex"),
32
+ format: "der",
33
+ type: "pkcs8"
34
+ });
35
+ const token = jwt.sign(payload, pk, {
36
+ algorithm: "ES512"
37
+ });
38
+ const result = {
39
+ token
40
+ };
41
+ return result;
42
+ }
43
+ };
44
+
45
+ // src/zod.ts
46
+ import * as z from "zod";
47
+ var StringFilter = z.union([
48
+ z.string(),
49
+ z.object({ exact: z.string() }),
50
+ z.object({ oneof: z.array(z.string()) }),
51
+ z.object({ regex: z.string() })
52
+ ]);
53
+ function transform(field) {
54
+ if (field instanceof z.ZodOptional) {
55
+ return transform(field.unwrap()).optional();
56
+ }
57
+ if (field instanceof z.ZodString) {
58
+ return StringFilter.optional();
59
+ }
60
+ if (field instanceof z.ZodRecord) {
61
+ return z.record(field.keySchema, transform(field.valueSchema)).optional();
62
+ }
63
+ return field.optional();
64
+ }
65
+ function filters(base) {
66
+ const entries = Object.entries(base.shape).map(([key, field]) => [
67
+ key,
68
+ transform(field)
69
+ ]);
70
+ const shape = Object.fromEntries(entries);
71
+ const node = z.lazy(
72
+ () => z.union([
73
+ z.object(shape),
74
+ z.object({ AND: z.array(node) }),
75
+ z.object({ OR: z.array(node) })
76
+ ])
77
+ );
78
+ return node;
79
+ }
80
+ function select(base) {
81
+ const entries = Object.entries(base.shape).map(([key]) => {
82
+ return [key, z.boolean().optional()];
83
+ });
84
+ const shape = Object.fromEntries(entries);
85
+ return z.object(shape);
86
+ }
87
+
88
+ // src/tunnel.ts
89
+ import * as z2 from "zod";
90
+ var tunnelSchema = z2.object({
91
+ id: z2.string(),
92
+ client_id: z2.string(),
93
+ user_id: z2.string(),
94
+ status: z2.enum(["online", "offline"]),
95
+ name: z2.string().optional(),
96
+ protocol: z2.string(),
97
+ publish: z2.boolean(),
98
+ labels: z2.record(z2.string().optional()).optional(),
99
+ host: z2.string().optional(),
100
+ tls_mode: z2.string(),
101
+ tls_min_version: z2.string().optional(),
102
+ mtls: z2.boolean(),
103
+ token_auth: z2.boolean(),
104
+ path: z2.string().optional()
105
+ });
106
+ var listTunnelsParamsSchema = z2.object({
107
+ limit: z2.number().optional(),
108
+ filters: tunnelSchema.pick({
109
+ status: true,
110
+ client_id: true,
111
+ protocol: true,
112
+ publish: true,
113
+ labels: true
114
+ }).partial().optional()
115
+ });
116
+ var listTunnelsResponseSchema = z2.object({
117
+ tunnels: z2.array(tunnelSchema)
118
+ });
119
+
120
+ // src/auth.ts
121
+ import * as z3 from "zod";
122
+ var createShortTermTokenParamsSchema = z3.object({
123
+ expires_in: z3.number().default(60),
124
+ // 1 minute
125
+ permissions: z3.object({
126
+ // Permissions for creating a tunnel
127
+ create: z3.union([
128
+ z3.boolean(),
129
+ z3.object({
130
+ filters: filters(tunnelSchema).optional()
131
+ })
132
+ ]).optional(),
133
+ // Permissions for connecting to a tunnel
134
+ connect: z3.union([
135
+ z3.boolean(),
136
+ z3.object({
137
+ filters: filters(tunnelSchema).optional()
138
+ })
139
+ ]).optional(),
140
+ // Permissions for listing tunnels
141
+ list: z3.union([
142
+ z3.boolean(),
143
+ z3.object({
144
+ filters: filters(tunnelSchema).optional(),
145
+ select: select(tunnelSchema).optional()
146
+ })
147
+ ]).optional()
148
+ }),
149
+ // Additional metadata
150
+ metadata: z3.unknown().optional()
151
+ });
152
+ var createShortTermTokenResponseSchema = z3.object({
153
+ token: z3.string()
154
+ });
155
+ var rstreamAuthPayloadSchema = z3.discriminatedUnion("type", [
156
+ z3.object({
157
+ type: z3.literal("pat")
158
+ }),
159
+ z3.object({
160
+ type: z3.literal("app"),
161
+ clientId: z3.string()
162
+ })
163
+ ]).and(
164
+ z3.object({
165
+ metadata: z3.object({
166
+ engine: z3.string().optional(),
167
+ permissions: createShortTermTokenParamsSchema.shape.permissions.optional()
168
+ }).optional()
169
+ })
170
+ );
171
+
172
+ // src/client.ts
173
+ import * as z4 from "zod";
174
+ var clientSchema = z4.object({
175
+ id: z4.string(),
176
+ user_id: z4.string(),
177
+ status: z4.enum(["online", "offline"]),
178
+ details: z4.object({
179
+ agent: z4.string().optional(),
180
+ os: z4.string().optional(),
181
+ version: z4.string().optional(),
182
+ protocol_version: z4.string().optional()
183
+ }).optional(),
184
+ labels: z4.record(z4.string().optional()).optional()
185
+ });
186
+ var listClientsParamsSchema = z4.object({
187
+ limit: z4.number().optional(),
188
+ filters: clientSchema.pick({
189
+ labels: true
190
+ }).partial().optional()
191
+ });
192
+ var listClientsResponseSchema = z4.object({
193
+ clients: z4.array(clientSchema)
194
+ });
195
+
196
+ // src/clients-ressource.ts
197
+ var RstreamClientsRessource = class {
198
+ client;
199
+ constructor(client) {
200
+ this.client = client;
201
+ }
202
+ async list(params) {
203
+ const response = await this.client.request(
204
+ `/clients?params=${encodeURIComponent(JSON.stringify(params))}`,
205
+ {
206
+ method: "GET"
207
+ }
208
+ );
209
+ return listClientsResponseSchema.parse(response);
210
+ }
211
+ async get(id) {
212
+ const response = await this.client.request(`/clients/${id}`, {
213
+ method: "GET"
214
+ });
215
+ return clientSchema.parse(response);
216
+ }
217
+ };
218
+
219
+ // src/event.ts
220
+ import * as z5 from "zod";
221
+ var eventSchema = z5.union([
222
+ z5.object({
223
+ type: z5.literal("client.created"),
224
+ object: clientSchema
225
+ }),
226
+ z5.object({
227
+ type: z5.literal("client.updated"),
228
+ object: clientSchema
229
+ }),
230
+ z5.object({
231
+ type: z5.literal("client.deleted"),
232
+ object: clientSchema
233
+ }),
234
+ z5.object({
235
+ type: z5.literal("tunnel.created"),
236
+ object: tunnelSchema
237
+ }),
238
+ z5.object({
239
+ type: z5.literal("tunnel.updated"),
240
+ object: tunnelSchema
241
+ }),
242
+ z5.object({
243
+ type: z5.literal("tunnel.deleted"),
244
+ object: tunnelSchema
245
+ })
246
+ ]);
247
+
248
+ // src/tunnels-ressource.ts
249
+ var RstreamTunnelsRessource = class {
250
+ client;
251
+ constructor(client) {
252
+ this.client = client;
253
+ }
254
+ async list(params) {
255
+ const response = await this.client.request(
256
+ `/tunnels?params=${encodeURIComponent(JSON.stringify(params))}`,
257
+ {
258
+ method: "GET"
259
+ }
260
+ );
261
+ return listTunnelsResponseSchema.parse(response);
262
+ }
263
+ async get(id) {
264
+ const response = await this.client.request(`/tunnels/${id}`, {
265
+ method: "GET"
266
+ });
267
+ return tunnelSchema.parse(response);
268
+ }
269
+ };
270
+
271
+ // src/webhooks-ressource.ts
272
+ import crypto2 from "crypto";
273
+ var RstreamWebHooksRessource = class {
274
+ client;
275
+ constructor(client) {
276
+ this.client = client;
277
+ }
278
+ // Constructs and verifies the signature of an Event from the provided details.
279
+ async event(payload, header, secret, tolerance = 300, receivedAt = Date.now()) {
280
+ const payloadBuffer = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
281
+ const signatureHeader = Array.isArray(header) ? header[0] : header.toString();
282
+ if (!signatureHeader) {
283
+ throw new Error("No signature header");
284
+ }
285
+ const elements = signatureHeader.split(",").map((element) => element.trim());
286
+ let timestamp;
287
+ const signatures = [];
288
+ for (const element of elements) {
289
+ if (element.startsWith("t=")) {
290
+ timestamp = element.substring(2);
291
+ } else if (element.startsWith("v1=")) {
292
+ signatures.push(element.substring(3));
293
+ }
294
+ }
295
+ if (!timestamp || signatures.length === 0) {
296
+ throw new Error("Invalid signature header format");
297
+ }
298
+ const timestampNum = parseInt(timestamp, 10);
299
+ const now = Math.floor(receivedAt / 1e3);
300
+ if (Math.abs(now - timestampNum) > tolerance) {
301
+ throw new Error(
302
+ `Webhook signature timestamp outside tolerance: ${now}, ${timestampNum}`
303
+ );
304
+ }
305
+ const signedPayload = `${timestamp}.${payloadBuffer.toString("utf8")}`;
306
+ const expectedSignature = crypto2.createHmac("sha256", secret).update(signedPayload).digest("hex");
307
+ let signatureMatched = false;
308
+ for (const signature of signatures) {
309
+ try {
310
+ if (crypto2.timingSafeEqual(
311
+ Buffer.from(signature),
312
+ Buffer.from(expectedSignature)
313
+ )) {
314
+ signatureMatched = true;
315
+ break;
316
+ }
317
+ } catch (error) {
318
+ console.log("Error comparing signatures:", error);
319
+ continue;
320
+ }
321
+ }
322
+ if (!signatureMatched) {
323
+ throw new Error("Signature verification failed");
324
+ }
325
+ try {
326
+ return eventSchema.parse(JSON.parse(payloadBuffer.toString("utf8")));
327
+ } catch (error) {
328
+ throw new Error(
329
+ `Failed to parse webhook payload: ${error instanceof Error ? error.message : String(error)}`
330
+ );
331
+ }
332
+ }
333
+ };
334
+
335
+ // src/rstream.ts
336
+ var RstreamClient = class {
337
+ cfg;
338
+ constructor(config) {
339
+ this.cfg = config;
340
+ }
341
+ get engine() {
342
+ return this.cfg?.engine || process.env.RSTREAM_DEFAULT_ENGINE || "engine.rstream.io:443";
343
+ }
344
+ get credentials() {
345
+ if (this.cfg?.credentials && "token" in this.cfg.credentials) {
346
+ return this.cfg.credentials;
347
+ }
348
+ if (process.env.RSTREAM_DEFAULT_AUTHENTICATION_TOKEN) {
349
+ return {
350
+ token: process.env.RSTREAM_DEFAULT_AUTHENTICATION_TOKEN
351
+ };
352
+ }
353
+ if (this.cfg?.credentials && "clientId" in this.cfg.credentials) {
354
+ return this.cfg.credentials;
355
+ }
356
+ if (process.env.RSTREAM_DEFAULT_CLIENT_ID && process.env.RSTREAM_DEFAULT_CLIENT_SECRET) {
357
+ return {
358
+ clientId: process.env.RSTREAM_DEFAULT_CLIENT_ID,
359
+ clientSecret: process.env.RSTREAM_DEFAULT_CLIENT_SECRET
360
+ };
361
+ }
362
+ return void 0;
363
+ }
364
+ get api() {
365
+ return `https://${this.engine}/api`;
366
+ }
367
+ get auth() {
368
+ return new RstreamAuthRessource(this);
369
+ }
370
+ get clients() {
371
+ return new RstreamClientsRessource(this);
372
+ }
373
+ get tunnels() {
374
+ return new RstreamTunnelsRessource(this);
375
+ }
376
+ get webhooks() {
377
+ return new RstreamWebHooksRessource(this);
378
+ }
379
+ async getToken() {
380
+ const credentials = this.credentials;
381
+ if (credentials && "token" in credentials) {
382
+ return credentials.token;
383
+ }
384
+ if (credentials && "clientId" in credentials) {
385
+ return (await this.auth.createShortTermToken(void 0, {
386
+ credentials: {
387
+ clientId: credentials.clientId,
388
+ clientSecret: credentials.clientSecret
389
+ }
390
+ })).token;
391
+ }
392
+ return void 0;
393
+ }
394
+ async request(path, options) {
395
+ const url = `${this.api}${path}`;
396
+ const headers = new Headers(options?.headers || {});
397
+ const token = await this.getToken();
398
+ if (token) {
399
+ headers.set("Authorization", `Bearer ${token}`);
400
+ }
401
+ const response = await fetch(url, {
402
+ ...options,
403
+ headers
404
+ });
405
+ if (!response.ok) {
406
+ const errorText = await response.text();
407
+ throw new Error(`HTTP error ${response.status}: ${errorText}`);
408
+ }
409
+ return await response.json();
410
+ }
411
+ };
412
+
413
+ // src/watch.ts
414
+ import jwt2 from "jsonwebtoken";
415
+ var Watch = class {
416
+ connection = null;
417
+ connectionState = "preparing";
418
+ config;
419
+ events;
420
+ constructor(config, events) {
421
+ this.config = config;
422
+ this.events = events;
423
+ }
424
+ async connect() {
425
+ if (this.connectionState !== "preparing") {
426
+ throw new Error("Watch: Connection already started or closed.");
427
+ }
428
+ this.connectionState = "connecting";
429
+ const token = await this.config.auth.token();
430
+ const payload = rstreamAuthPayloadSchema.parse(
431
+ jwt2.decode(token, { complete: false })
432
+ );
433
+ const base = `https://${this.config.engine || payload.metadata?.engine || "engine.rstream.io:443"}`;
434
+ if (this.config.transport === "sse") {
435
+ const url = new URL(`/api/sse`, base);
436
+ url.searchParams.set("rstream.token", token);
437
+ this.connection = new EventSource(url.toString());
438
+ } else {
439
+ const url = new URL(`/api/websocket`, base);
440
+ url.searchParams.set("rstream.token", token);
441
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
442
+ this.connection = new WebSocket(url.toString());
443
+ }
444
+ this.connection.onopen = () => {
445
+ this.connectionState = "connected";
446
+ this.events.onConnect?.();
447
+ };
448
+ this.connection.onmessage = (msg) => {
449
+ const parsed = eventSchema.parse(JSON.parse(msg.data));
450
+ this.events.onEvent?.(parsed);
451
+ };
452
+ this.connection.onerror = () => {
453
+ if (this.connectionState !== "closed") {
454
+ this.disconnect();
455
+ }
456
+ };
457
+ }
458
+ disconnect() {
459
+ if (this.connection) {
460
+ this.connection.onerror = null;
461
+ this.connection.onmessage = null;
462
+ this.connection.onopen = null;
463
+ this.connection.close();
464
+ this.connection = null;
465
+ }
466
+ if (this.connectionState !== "closed") {
467
+ this.connectionState = "closed";
468
+ this.events.onClose?.();
469
+ }
470
+ }
471
+ };
472
+ export {
473
+ RstreamClient as Rstream,
474
+ RstreamAuthRessource,
475
+ RstreamClient,
476
+ RstreamClientsRessource,
477
+ RstreamTunnelsRessource,
478
+ RstreamWebHooksRessource,
479
+ Watch,
480
+ clientSchema,
481
+ createShortTermTokenParamsSchema,
482
+ createShortTermTokenResponseSchema,
483
+ eventSchema,
484
+ listClientsParamsSchema,
485
+ listClientsResponseSchema,
486
+ listTunnelsParamsSchema,
487
+ listTunnelsResponseSchema,
488
+ rstreamAuthPayloadSchema,
489
+ tunnelSchema
490
+ };
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@rstreamlabs/rstream",
3
+ "version": "1.1.0",
4
+ "description": "JS SDK for rstream.",
5
+ "license": "Apache-2.0",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.mjs",
8
+ "types": "./dist/index.d.ts",
9
+ "files": [
10
+ "dist",
11
+ "README.md"
12
+ ],
13
+ "scripts": {
14
+ "build:watch": "tsup --watch",
15
+ "build": "tsup",
16
+ "clean": "rm -rf dist",
17
+ "lint": "eslint \"./**/*.ts*\"",
18
+ "type-check": "tsc --noEmit"
19
+ },
20
+ "devDependencies": {
21
+ "@turbo/gen": "^2.4.0",
22
+ "@types/jsonwebtoken": "^9.0.9",
23
+ "@types/node": "^22",
24
+ "eslint-config": "*",
25
+ "eslint": "9.19.0",
26
+ "tsup": "^8.3.6",
27
+ "typescript-config": "*",
28
+ "typescript": "5.7.3"
29
+ },
30
+ "dependencies": {
31
+ "jsonwebtoken": "^9.0.2",
32
+ "zod": "^3.22.4"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ }
37
+ }