@rstreamlabs/tunnels 3.2.0 → 3.2.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
@@ -190,7 +190,8 @@ For a receiving backend, verify the signature before touching the JSON payload
190
190
  and use the event id as the idempotency key.
191
191
 
192
192
  ```ts
193
- import { RstreamWebhookResource, type WebhookEvent } from "@rstreamlabs/tunnels";
193
+ import { RstreamWebhookResource } from "@rstreamlabs/tunnels";
194
+ import { type WebhookEvent } from "@rstreamlabs/tunnels";
194
195
 
195
196
  const webhooks = new RstreamWebhookResource();
196
197
 
@@ -225,18 +226,55 @@ export async function receiveWebhook(request: Request) {
225
226
  const rawBody = Buffer.from(await request.arrayBuffer());
226
227
  const signature = request.headers.get("rstream-signature");
227
228
  if (!signature) throw new Error("Missing webhook signature.");
228
-
229
229
  const event = await webhooks.event(
230
230
  rawBody,
231
231
  signature,
232
232
  process.env.WEBHOOK_SECRET!,
233
233
  );
234
234
  if (!event.id) throw new Error("Missing webhook event id.");
235
-
236
235
  await handleLifecycleEvent(event);
237
236
  }
238
237
  ```
239
238
 
239
+ For local receiver tests, the SDK exposes the same signing primitives used by
240
+ the CLI and Engine dispatcher:
241
+
242
+ ```ts
243
+ import { buildWebhookHeaders } from "@rstreamlabs/tunnels";
244
+ import { generateWebhookSigningSecret } from "@rstreamlabs/tunnels";
245
+ import { type WebhookEvent } from "@rstreamlabs/tunnels";
246
+
247
+ const secret = generateWebhookSigningSecret();
248
+ const event: WebhookEvent & { id: string } = {
249
+ id: "evt_test",
250
+ type: "tunnel.created",
251
+ created_at: new Date().toISOString(),
252
+ object: {
253
+ id: "tunnel_test",
254
+ labels: {
255
+ device: "device_123",
256
+ },
257
+ },
258
+ };
259
+ const body = JSON.stringify(event);
260
+ const headers = buildWebhookHeaders(body, event, secret, {
261
+ deliveryId: "del_test",
262
+ webhookId: "we_test",
263
+ });
264
+
265
+ await fetch("http://localhost:3000/api/rstream/webhook", {
266
+ body,
267
+ headers: {
268
+ "content-type": "application/json",
269
+ ...headers,
270
+ },
271
+ method: "POST",
272
+ });
273
+ ```
274
+
275
+ `rstream events --webhook` uses the same request body and header contract when
276
+ forwarding live local events to a receiver.
277
+
240
278
  ## WebTTY Helpers
241
279
 
242
280
  Use `parseWebTTYServers()` to derive WebTTY server metadata from tunnel
package/dist/index.d.mts CHANGED
@@ -2967,8 +2967,26 @@ type WebhookEvent = z.infer<typeof webhookEventsSchema>;
2967
2967
  type ProjectLogEvent = z.infer<typeof projectLogEventsSchema>;
2968
2968
  type Event = z.infer<typeof eventSchema>;
2969
2969
 
2970
+ declare const WEBHOOK_SIGNATURE_HEADER = "rstream-signature";
2971
+ declare const WEBHOOK_EVENT_ID_HEADER = "rstream-event-id";
2972
+ declare const WEBHOOK_EVENT_TYPE_HEADER = "rstream-event-type";
2973
+ declare const WEBHOOK_ID_HEADER = "rstream-webhook-id";
2974
+ declare const WEBHOOK_DELIVERY_ID_HEADER = "rstream-delivery-id";
2975
+ type WebhookPayload = string | Buffer;
2976
+ type WebhookHeaders = Record<typeof WEBHOOK_SIGNATURE_HEADER | typeof WEBHOOK_EVENT_ID_HEADER | typeof WEBHOOK_EVENT_TYPE_HEADER | typeof WEBHOOK_ID_HEADER | typeof WEBHOOK_DELIVERY_ID_HEADER, string>;
2977
+ interface BuildWebhookHeadersOptions {
2978
+ deliveryId: string;
2979
+ timestamp?: Date | number;
2980
+ webhookId: string;
2981
+ }
2982
+ declare function generateWebhookSigningSecret(): string;
2983
+ declare function signWebhookPayload(payload: WebhookPayload, secret: string, timestamp?: Date | number): string;
2984
+ declare function buildWebhookHeaders(payload: WebhookPayload, event: WebhookEvent, secret: string, options: BuildWebhookHeadersOptions): WebhookHeaders;
2970
2985
  declare class RstreamWebhookResource {
2971
- event(payload: string | Buffer, header: string | Buffer | Array<string>, secret: string, tolerance?: number, receivedAt?: number): Promise<WebhookEvent>;
2986
+ event(payload: WebhookPayload, header: string | Buffer | Array<string>, secret: string, tolerance?: number, receivedAt?: number): Promise<WebhookEvent>;
2987
+ generateSigningSecret(): string;
2988
+ sign(payload: WebhookPayload, secret: string, timestamp?: Date | number): string;
2989
+ headers(payload: WebhookPayload, event: WebhookEvent, secret: string, options: BuildWebhookHeadersOptions): WebhookHeaders;
2972
2990
  }
2973
2991
 
2974
2992
  interface RstreamTunnelsConfig {
@@ -3682,4 +3700,4 @@ declare const webttyServerSchema: z.ZodObject<{
3682
3700
  type WebTTYServer = z.infer<typeof webttyServerSchema>;
3683
3701
  declare function parseWebTTYServers(tunnels: Tunnel[]): WebTTYServer[];
3684
3702
 
3685
- export { type Client, type ClientFilter, type CreateAPITURNCredentialsOptions, type CreateAPPTURNCredentialsOptions, type CreateAuthTokenFromClientCredentialsOptions, type CreatePATTURNCredentialsOptions, type CreateTURNCredentialsOptions, type Event, type InitialState, type ListClientsParams, type ListClientsResponse, type ListTunnelsParams, type ListTunnelsResponse, type ProjectLogEvent, type RstreamAuth, RstreamAuthResource, RstreamAuthResource as RstreamAuthRessource, RstreamTunnelsClient as RstreamClient, RstreamClientsResource, RstreamClientsResource as RstreamClientsRessource, RstreamTURNResource, RstreamTunnelsClient, type RstreamTunnelsConfig, RstreamTunnelsResource, RstreamTunnelsResource as RstreamTunnelsRessource, RstreamWebhookResource, RstreamWebhookResource as RstreamWebhookRessource, type StreamEndpoint, type StreamSummary, type StreamTLSInfo, type TURNCredentialMode, type TunnelFilter, RstreamTunnelsClient as Tunnels, type TunnelsResolutionConfig, Watch, type WatchConfig, type WatchWsEvents, type WebTTYServer, type WebhookEvent, type WsEvent, clientFilterSchema, clientSchema, createAPITURNCredentials, createAPPTURNCredentials, createAuthTokenFromClientCredentials, createPATTURNCredentials, createTURNCredentials, eventSchema, formatTunnelHost, listClientsParamsSchema, listClientsResponseSchema, listTunnelsParamsSchema, listTunnelsResponseSchema, normalizeEngineAddress, parseWebTTYServers, projectLogEventsSchema, resolveControlPlaneCredentials, resolveManagedTunnelsProject, resolveTunnelsAPIURL, resolveTunnelsCredentials, resolveTunnelsEngine, streamEndpointSchema, streamSummaryEventSchema, streamSummarySchema, streamTLSInfoSchema, tunnelFilterSchema, turnCredentialModeSchema, webhookEventsSchema, webttyServerSchema, wsEventsSchema };
3703
+ export { type BuildWebhookHeadersOptions, type Client, type ClientFilter, type CreateAPITURNCredentialsOptions, type CreateAPPTURNCredentialsOptions, type CreateAuthTokenFromClientCredentialsOptions, type CreatePATTURNCredentialsOptions, type CreateTURNCredentialsOptions, type Event, type InitialState, type ListClientsParams, type ListClientsResponse, type ListTunnelsParams, type ListTunnelsResponse, type ProjectLogEvent, type RstreamAuth, RstreamAuthResource, RstreamAuthResource as RstreamAuthRessource, RstreamTunnelsClient as RstreamClient, RstreamClientsResource, RstreamClientsResource as RstreamClientsRessource, RstreamTURNResource, RstreamTunnelsClient, type RstreamTunnelsConfig, RstreamTunnelsResource, RstreamTunnelsResource as RstreamTunnelsRessource, RstreamWebhookResource, RstreamWebhookResource as RstreamWebhookRessource, type StreamEndpoint, type StreamSummary, type StreamTLSInfo, type TURNCredentialMode, type TunnelFilter, RstreamTunnelsClient as Tunnels, type TunnelsResolutionConfig, WEBHOOK_DELIVERY_ID_HEADER, WEBHOOK_EVENT_ID_HEADER, WEBHOOK_EVENT_TYPE_HEADER, WEBHOOK_ID_HEADER, WEBHOOK_SIGNATURE_HEADER, Watch, type WatchConfig, type WatchWsEvents, type WebTTYServer, type WebhookEvent, type WebhookHeaders, type WebhookPayload, type WsEvent, buildWebhookHeaders, clientFilterSchema, clientSchema, createAPITURNCredentials, createAPPTURNCredentials, createAuthTokenFromClientCredentials, createPATTURNCredentials, createTURNCredentials, eventSchema, formatTunnelHost, generateWebhookSigningSecret, listClientsParamsSchema, listClientsResponseSchema, listTunnelsParamsSchema, listTunnelsResponseSchema, normalizeEngineAddress, parseWebTTYServers, projectLogEventsSchema, resolveControlPlaneCredentials, resolveManagedTunnelsProject, resolveTunnelsAPIURL, resolveTunnelsCredentials, resolveTunnelsEngine, signWebhookPayload, streamEndpointSchema, streamSummaryEventSchema, streamSummarySchema, streamTLSInfoSchema, tunnelFilterSchema, turnCredentialModeSchema, webhookEventsSchema, webttyServerSchema, wsEventsSchema };
package/dist/index.d.ts CHANGED
@@ -2967,8 +2967,26 @@ type WebhookEvent = z.infer<typeof webhookEventsSchema>;
2967
2967
  type ProjectLogEvent = z.infer<typeof projectLogEventsSchema>;
2968
2968
  type Event = z.infer<typeof eventSchema>;
2969
2969
 
2970
+ declare const WEBHOOK_SIGNATURE_HEADER = "rstream-signature";
2971
+ declare const WEBHOOK_EVENT_ID_HEADER = "rstream-event-id";
2972
+ declare const WEBHOOK_EVENT_TYPE_HEADER = "rstream-event-type";
2973
+ declare const WEBHOOK_ID_HEADER = "rstream-webhook-id";
2974
+ declare const WEBHOOK_DELIVERY_ID_HEADER = "rstream-delivery-id";
2975
+ type WebhookPayload = string | Buffer;
2976
+ type WebhookHeaders = Record<typeof WEBHOOK_SIGNATURE_HEADER | typeof WEBHOOK_EVENT_ID_HEADER | typeof WEBHOOK_EVENT_TYPE_HEADER | typeof WEBHOOK_ID_HEADER | typeof WEBHOOK_DELIVERY_ID_HEADER, string>;
2977
+ interface BuildWebhookHeadersOptions {
2978
+ deliveryId: string;
2979
+ timestamp?: Date | number;
2980
+ webhookId: string;
2981
+ }
2982
+ declare function generateWebhookSigningSecret(): string;
2983
+ declare function signWebhookPayload(payload: WebhookPayload, secret: string, timestamp?: Date | number): string;
2984
+ declare function buildWebhookHeaders(payload: WebhookPayload, event: WebhookEvent, secret: string, options: BuildWebhookHeadersOptions): WebhookHeaders;
2970
2985
  declare class RstreamWebhookResource {
2971
- event(payload: string | Buffer, header: string | Buffer | Array<string>, secret: string, tolerance?: number, receivedAt?: number): Promise<WebhookEvent>;
2986
+ event(payload: WebhookPayload, header: string | Buffer | Array<string>, secret: string, tolerance?: number, receivedAt?: number): Promise<WebhookEvent>;
2987
+ generateSigningSecret(): string;
2988
+ sign(payload: WebhookPayload, secret: string, timestamp?: Date | number): string;
2989
+ headers(payload: WebhookPayload, event: WebhookEvent, secret: string, options: BuildWebhookHeadersOptions): WebhookHeaders;
2972
2990
  }
2973
2991
 
2974
2992
  interface RstreamTunnelsConfig {
@@ -3682,4 +3700,4 @@ declare const webttyServerSchema: z.ZodObject<{
3682
3700
  type WebTTYServer = z.infer<typeof webttyServerSchema>;
3683
3701
  declare function parseWebTTYServers(tunnels: Tunnel[]): WebTTYServer[];
3684
3702
 
3685
- export { type Client, type ClientFilter, type CreateAPITURNCredentialsOptions, type CreateAPPTURNCredentialsOptions, type CreateAuthTokenFromClientCredentialsOptions, type CreatePATTURNCredentialsOptions, type CreateTURNCredentialsOptions, type Event, type InitialState, type ListClientsParams, type ListClientsResponse, type ListTunnelsParams, type ListTunnelsResponse, type ProjectLogEvent, type RstreamAuth, RstreamAuthResource, RstreamAuthResource as RstreamAuthRessource, RstreamTunnelsClient as RstreamClient, RstreamClientsResource, RstreamClientsResource as RstreamClientsRessource, RstreamTURNResource, RstreamTunnelsClient, type RstreamTunnelsConfig, RstreamTunnelsResource, RstreamTunnelsResource as RstreamTunnelsRessource, RstreamWebhookResource, RstreamWebhookResource as RstreamWebhookRessource, type StreamEndpoint, type StreamSummary, type StreamTLSInfo, type TURNCredentialMode, type TunnelFilter, RstreamTunnelsClient as Tunnels, type TunnelsResolutionConfig, Watch, type WatchConfig, type WatchWsEvents, type WebTTYServer, type WebhookEvent, type WsEvent, clientFilterSchema, clientSchema, createAPITURNCredentials, createAPPTURNCredentials, createAuthTokenFromClientCredentials, createPATTURNCredentials, createTURNCredentials, eventSchema, formatTunnelHost, listClientsParamsSchema, listClientsResponseSchema, listTunnelsParamsSchema, listTunnelsResponseSchema, normalizeEngineAddress, parseWebTTYServers, projectLogEventsSchema, resolveControlPlaneCredentials, resolveManagedTunnelsProject, resolveTunnelsAPIURL, resolveTunnelsCredentials, resolveTunnelsEngine, streamEndpointSchema, streamSummaryEventSchema, streamSummarySchema, streamTLSInfoSchema, tunnelFilterSchema, turnCredentialModeSchema, webhookEventsSchema, webttyServerSchema, wsEventsSchema };
3703
+ export { type BuildWebhookHeadersOptions, type Client, type ClientFilter, type CreateAPITURNCredentialsOptions, type CreateAPPTURNCredentialsOptions, type CreateAuthTokenFromClientCredentialsOptions, type CreatePATTURNCredentialsOptions, type CreateTURNCredentialsOptions, type Event, type InitialState, type ListClientsParams, type ListClientsResponse, type ListTunnelsParams, type ListTunnelsResponse, type ProjectLogEvent, type RstreamAuth, RstreamAuthResource, RstreamAuthResource as RstreamAuthRessource, RstreamTunnelsClient as RstreamClient, RstreamClientsResource, RstreamClientsResource as RstreamClientsRessource, RstreamTURNResource, RstreamTunnelsClient, type RstreamTunnelsConfig, RstreamTunnelsResource, RstreamTunnelsResource as RstreamTunnelsRessource, RstreamWebhookResource, RstreamWebhookResource as RstreamWebhookRessource, type StreamEndpoint, type StreamSummary, type StreamTLSInfo, type TURNCredentialMode, type TunnelFilter, RstreamTunnelsClient as Tunnels, type TunnelsResolutionConfig, WEBHOOK_DELIVERY_ID_HEADER, WEBHOOK_EVENT_ID_HEADER, WEBHOOK_EVENT_TYPE_HEADER, WEBHOOK_ID_HEADER, WEBHOOK_SIGNATURE_HEADER, Watch, type WatchConfig, type WatchWsEvents, type WebTTYServer, type WebhookEvent, type WebhookHeaders, type WebhookPayload, type WsEvent, buildWebhookHeaders, clientFilterSchema, clientSchema, createAPITURNCredentials, createAPPTURNCredentials, createAuthTokenFromClientCredentials, createPATTURNCredentials, createTURNCredentials, eventSchema, formatTunnelHost, generateWebhookSigningSecret, listClientsParamsSchema, listClientsResponseSchema, listTunnelsParamsSchema, listTunnelsResponseSchema, normalizeEngineAddress, parseWebTTYServers, projectLogEventsSchema, resolveControlPlaneCredentials, resolveManagedTunnelsProject, resolveTunnelsAPIURL, resolveTunnelsCredentials, resolveTunnelsEngine, signWebhookPayload, streamEndpointSchema, streamSummaryEventSchema, streamSummarySchema, streamTLSInfoSchema, tunnelFilterSchema, turnCredentialModeSchema, webhookEventsSchema, webttyServerSchema, wsEventsSchema };
package/dist/index.js CHANGED
@@ -42,7 +42,13 @@ __export(index_exports, {
42
42
  RstreamWebhookResource: () => RstreamWebhookResource,
43
43
  RstreamWebhookRessource: () => RstreamWebhookResource,
44
44
  Tunnels: () => RstreamTunnelsClient,
45
+ WEBHOOK_DELIVERY_ID_HEADER: () => WEBHOOK_DELIVERY_ID_HEADER,
46
+ WEBHOOK_EVENT_ID_HEADER: () => WEBHOOK_EVENT_ID_HEADER,
47
+ WEBHOOK_EVENT_TYPE_HEADER: () => WEBHOOK_EVENT_TYPE_HEADER,
48
+ WEBHOOK_ID_HEADER: () => WEBHOOK_ID_HEADER,
49
+ WEBHOOK_SIGNATURE_HEADER: () => WEBHOOK_SIGNATURE_HEADER,
45
50
  Watch: () => Watch,
51
+ buildWebhookHeaders: () => buildWebhookHeaders,
46
52
  clientFilterSchema: () => clientFilterSchema,
47
53
  clientSchema: () => clientSchema,
48
54
  createAPITURNCredentials: () => createAPITURNCredentials,
@@ -54,6 +60,7 @@ __export(index_exports, {
54
60
  engineOpenApiJson: () => engineOpenApiJson,
55
61
  eventSchema: () => eventSchema,
56
62
  formatTunnelHost: () => formatTunnelHost,
63
+ generateWebhookSigningSecret: () => generateWebhookSigningSecret,
57
64
  listClientsParamsSchema: () => listClientsParamsSchema,
58
65
  listClientsResponseSchema: () => listClientsResponseSchema,
59
66
  listTunnelsParamsSchema: () => listTunnelsParamsSchema,
@@ -66,6 +73,7 @@ __export(index_exports, {
66
73
  resolveTunnelsAPIURL: () => resolveTunnelsAPIURL,
67
74
  resolveTunnelsCredentials: () => resolveTunnelsCredentials,
68
75
  resolveTunnelsEngine: () => resolveTunnelsEngine,
76
+ signWebhookPayload: () => signWebhookPayload,
69
77
  streamEndpointSchema: () => streamEndpointSchema,
70
78
  streamSummaryEventSchema: () => streamSummaryEventSchema,
71
79
  streamSummarySchema: () => streamSummarySchema,
@@ -1477,9 +1485,18 @@ var RstreamTURNResource = class {
1477
1485
 
1478
1486
  // src/webhooks-resource.ts
1479
1487
  var import_crypto2 = __toESM(require("crypto"));
1488
+ var WEBHOOK_SIGNATURE_HEADER = "rstream-signature";
1489
+ var WEBHOOK_EVENT_ID_HEADER = "rstream-event-id";
1490
+ var WEBHOOK_EVENT_TYPE_HEADER = "rstream-event-type";
1491
+ var WEBHOOK_ID_HEADER = "rstream-webhook-id";
1492
+ var WEBHOOK_DELIVERY_ID_HEADER = "rstream-delivery-id";
1480
1493
  var SIGNATURE_HEX_BYTES = 32;
1494
+ var webhookSecretBytes = 32;
1481
1495
  var signatureHexSchema = /^[0-9a-fA-F]{64}$/;
1482
1496
  var timestampSchema = /^\d+$/;
1497
+ function payloadBuffer(payload) {
1498
+ return Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
1499
+ }
1483
1500
  function parseSignatureTimestamp(timestamp) {
1484
1501
  if (!timestampSchema.test(timestamp)) {
1485
1502
  throw new Error("Invalid signature timestamp");
@@ -1496,6 +1513,19 @@ function normalizeTolerance(tolerance) {
1496
1513
  }
1497
1514
  return tolerance;
1498
1515
  }
1516
+ function normalizeTimestamp(timestamp) {
1517
+ if (timestamp === void 0) {
1518
+ return Math.floor(Date.now() / 1e3);
1519
+ }
1520
+ const seconds = timestamp instanceof Date ? timestamp.getTime() / 1e3 : timestamp;
1521
+ if (!Number.isFinite(seconds) || seconds < 0) {
1522
+ throw new Error("Invalid webhook signature timestamp");
1523
+ }
1524
+ return Math.floor(seconds);
1525
+ }
1526
+ function signedPayload(timestamp, payload) {
1527
+ return Buffer.concat([Buffer.from(`${timestamp}.`, "utf8"), payload]);
1528
+ }
1499
1529
  function readSignature(signature) {
1500
1530
  if (!signatureHexSchema.test(signature)) {
1501
1531
  return void 0;
@@ -1512,9 +1542,42 @@ function assertSecretConfigured(secret) {
1512
1542
  throw new Error("Webhook signing secret is required");
1513
1543
  }
1514
1544
  }
1545
+ function assertNonEmpty(value, label) {
1546
+ const normalized = value.trim();
1547
+ if (normalized.length === 0) {
1548
+ throw new Error(`${label} is required`);
1549
+ }
1550
+ return normalized;
1551
+ }
1552
+ function generateWebhookSigningSecret() {
1553
+ return `whsec_${import_crypto2.default.randomBytes(webhookSecretBytes).toString("base64url")}`;
1554
+ }
1555
+ function signWebhookPayload(payload, secret, timestamp) {
1556
+ assertSecretConfigured(secret);
1557
+ const timestampSeconds = normalizeTimestamp(timestamp).toString();
1558
+ const signature = import_crypto2.default.createHmac("sha256", secret).update(signedPayload(timestampSeconds, payloadBuffer(payload))).digest("hex");
1559
+ return `t=${timestampSeconds},v1=${signature}`;
1560
+ }
1561
+ function buildWebhookHeaders(payload, event, secret, options) {
1562
+ const eventId = assertNonEmpty(event.id ?? "", "Webhook event id");
1563
+ const eventType = assertNonEmpty(event.type, "Webhook event type");
1564
+ const webhookId = assertNonEmpty(options.webhookId, "Webhook id");
1565
+ const deliveryId = assertNonEmpty(options.deliveryId, "Webhook delivery id");
1566
+ return {
1567
+ [WEBHOOK_SIGNATURE_HEADER]: signWebhookPayload(
1568
+ payload,
1569
+ secret,
1570
+ options.timestamp
1571
+ ),
1572
+ [WEBHOOK_EVENT_ID_HEADER]: eventId,
1573
+ [WEBHOOK_EVENT_TYPE_HEADER]: eventType,
1574
+ [WEBHOOK_ID_HEADER]: webhookId,
1575
+ [WEBHOOK_DELIVERY_ID_HEADER]: deliveryId
1576
+ };
1577
+ }
1515
1578
  var RstreamWebhookResource = class {
1516
1579
  async event(payload, header, secret, tolerance = 300, receivedAt = Date.now()) {
1517
- const payloadBuffer = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
1580
+ const rawPayload = payloadBuffer(payload);
1518
1581
  const signatureHeader = Array.isArray(header) ? header[0] : header.toString();
1519
1582
  if (!signatureHeader) {
1520
1583
  throw new Error("No signature header");
@@ -1534,8 +1597,7 @@ var RstreamWebhookResource = class {
1534
1597
  `Webhook signature timestamp outside tolerance: ${now}, ${timestampNum}`
1535
1598
  );
1536
1599
  }
1537
- const signedPayload = `${timestamp}.${payloadBuffer.toString("utf8")}`;
1538
- const expectedSignature = import_crypto2.default.createHmac("sha256", secret).update(signedPayload).digest();
1600
+ const expectedSignature = import_crypto2.default.createHmac("sha256", secret).update(signedPayload(timestamp, rawPayload)).digest();
1539
1601
  const signatureMatched = signatures.some(
1540
1602
  (signature) => signatureMatches(signature, expectedSignature)
1541
1603
  );
@@ -1543,9 +1605,7 @@ var RstreamWebhookResource = class {
1543
1605
  throw new Error("Signature verification failed");
1544
1606
  }
1545
1607
  try {
1546
- return webhookEventsSchema.parse(
1547
- JSON.parse(payloadBuffer.toString("utf8"))
1548
- );
1608
+ return webhookEventsSchema.parse(JSON.parse(rawPayload.toString("utf8")));
1549
1609
  } catch (error) {
1550
1610
  throw new Error(
1551
1611
  `Failed to parse webhook payload: ${error instanceof Error ? error.message : String(error)}`,
@@ -1553,6 +1613,15 @@ var RstreamWebhookResource = class {
1553
1613
  );
1554
1614
  }
1555
1615
  }
1616
+ generateSigningSecret() {
1617
+ return generateWebhookSigningSecret();
1618
+ }
1619
+ sign(payload, secret, timestamp) {
1620
+ return signWebhookPayload(payload, secret, timestamp);
1621
+ }
1622
+ headers(payload, event, secret, options) {
1623
+ return buildWebhookHeaders(payload, event, secret, options);
1624
+ }
1556
1625
  };
1557
1626
 
1558
1627
  // src/tunnels.ts
@@ -1985,7 +2054,13 @@ function parseWebTTYServers(tunnels) {
1985
2054
  RstreamWebhookResource,
1986
2055
  RstreamWebhookRessource,
1987
2056
  Tunnels,
2057
+ WEBHOOK_DELIVERY_ID_HEADER,
2058
+ WEBHOOK_EVENT_ID_HEADER,
2059
+ WEBHOOK_EVENT_TYPE_HEADER,
2060
+ WEBHOOK_ID_HEADER,
2061
+ WEBHOOK_SIGNATURE_HEADER,
1988
2062
  Watch,
2063
+ buildWebhookHeaders,
1989
2064
  clientFilterSchema,
1990
2065
  clientSchema,
1991
2066
  createAPITURNCredentials,
@@ -1997,6 +2072,7 @@ function parseWebTTYServers(tunnels) {
1997
2072
  engineOpenApiJson,
1998
2073
  eventSchema,
1999
2074
  formatTunnelHost,
2075
+ generateWebhookSigningSecret,
2000
2076
  listClientsParamsSchema,
2001
2077
  listClientsResponseSchema,
2002
2078
  listTunnelsParamsSchema,
@@ -2009,6 +2085,7 @@ function parseWebTTYServers(tunnels) {
2009
2085
  resolveTunnelsAPIURL,
2010
2086
  resolveTunnelsCredentials,
2011
2087
  resolveTunnelsEngine,
2088
+ signWebhookPayload,
2012
2089
  streamEndpointSchema,
2013
2090
  streamSummaryEventSchema,
2014
2091
  streamSummarySchema,
package/dist/index.mjs CHANGED
@@ -730,9 +730,18 @@ var RstreamTURNResource = class {
730
730
 
731
731
  // src/webhooks-resource.ts
732
732
  import crypto2 from "crypto";
733
+ var WEBHOOK_SIGNATURE_HEADER = "rstream-signature";
734
+ var WEBHOOK_EVENT_ID_HEADER = "rstream-event-id";
735
+ var WEBHOOK_EVENT_TYPE_HEADER = "rstream-event-type";
736
+ var WEBHOOK_ID_HEADER = "rstream-webhook-id";
737
+ var WEBHOOK_DELIVERY_ID_HEADER = "rstream-delivery-id";
733
738
  var SIGNATURE_HEX_BYTES = 32;
739
+ var webhookSecretBytes = 32;
734
740
  var signatureHexSchema = /^[0-9a-fA-F]{64}$/;
735
741
  var timestampSchema = /^\d+$/;
742
+ function payloadBuffer(payload) {
743
+ return Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
744
+ }
736
745
  function parseSignatureTimestamp(timestamp) {
737
746
  if (!timestampSchema.test(timestamp)) {
738
747
  throw new Error("Invalid signature timestamp");
@@ -749,6 +758,19 @@ function normalizeTolerance(tolerance) {
749
758
  }
750
759
  return tolerance;
751
760
  }
761
+ function normalizeTimestamp(timestamp) {
762
+ if (timestamp === void 0) {
763
+ return Math.floor(Date.now() / 1e3);
764
+ }
765
+ const seconds = timestamp instanceof Date ? timestamp.getTime() / 1e3 : timestamp;
766
+ if (!Number.isFinite(seconds) || seconds < 0) {
767
+ throw new Error("Invalid webhook signature timestamp");
768
+ }
769
+ return Math.floor(seconds);
770
+ }
771
+ function signedPayload(timestamp, payload) {
772
+ return Buffer.concat([Buffer.from(`${timestamp}.`, "utf8"), payload]);
773
+ }
752
774
  function readSignature(signature) {
753
775
  if (!signatureHexSchema.test(signature)) {
754
776
  return void 0;
@@ -765,9 +787,42 @@ function assertSecretConfigured(secret) {
765
787
  throw new Error("Webhook signing secret is required");
766
788
  }
767
789
  }
790
+ function assertNonEmpty(value, label) {
791
+ const normalized = value.trim();
792
+ if (normalized.length === 0) {
793
+ throw new Error(`${label} is required`);
794
+ }
795
+ return normalized;
796
+ }
797
+ function generateWebhookSigningSecret() {
798
+ return `whsec_${crypto2.randomBytes(webhookSecretBytes).toString("base64url")}`;
799
+ }
800
+ function signWebhookPayload(payload, secret, timestamp) {
801
+ assertSecretConfigured(secret);
802
+ const timestampSeconds = normalizeTimestamp(timestamp).toString();
803
+ const signature = crypto2.createHmac("sha256", secret).update(signedPayload(timestampSeconds, payloadBuffer(payload))).digest("hex");
804
+ return `t=${timestampSeconds},v1=${signature}`;
805
+ }
806
+ function buildWebhookHeaders(payload, event, secret, options) {
807
+ const eventId = assertNonEmpty(event.id ?? "", "Webhook event id");
808
+ const eventType = assertNonEmpty(event.type, "Webhook event type");
809
+ const webhookId = assertNonEmpty(options.webhookId, "Webhook id");
810
+ const deliveryId = assertNonEmpty(options.deliveryId, "Webhook delivery id");
811
+ return {
812
+ [WEBHOOK_SIGNATURE_HEADER]: signWebhookPayload(
813
+ payload,
814
+ secret,
815
+ options.timestamp
816
+ ),
817
+ [WEBHOOK_EVENT_ID_HEADER]: eventId,
818
+ [WEBHOOK_EVENT_TYPE_HEADER]: eventType,
819
+ [WEBHOOK_ID_HEADER]: webhookId,
820
+ [WEBHOOK_DELIVERY_ID_HEADER]: deliveryId
821
+ };
822
+ }
768
823
  var RstreamWebhookResource = class {
769
824
  async event(payload, header, secret, tolerance = 300, receivedAt = Date.now()) {
770
- const payloadBuffer = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
825
+ const rawPayload = payloadBuffer(payload);
771
826
  const signatureHeader = Array.isArray(header) ? header[0] : header.toString();
772
827
  if (!signatureHeader) {
773
828
  throw new Error("No signature header");
@@ -787,8 +842,7 @@ var RstreamWebhookResource = class {
787
842
  `Webhook signature timestamp outside tolerance: ${now}, ${timestampNum}`
788
843
  );
789
844
  }
790
- const signedPayload = `${timestamp}.${payloadBuffer.toString("utf8")}`;
791
- const expectedSignature = crypto2.createHmac("sha256", secret).update(signedPayload).digest();
845
+ const expectedSignature = crypto2.createHmac("sha256", secret).update(signedPayload(timestamp, rawPayload)).digest();
792
846
  const signatureMatched = signatures.some(
793
847
  (signature) => signatureMatches(signature, expectedSignature)
794
848
  );
@@ -796,9 +850,7 @@ var RstreamWebhookResource = class {
796
850
  throw new Error("Signature verification failed");
797
851
  }
798
852
  try {
799
- return webhookEventsSchema.parse(
800
- JSON.parse(payloadBuffer.toString("utf8"))
801
- );
853
+ return webhookEventsSchema.parse(JSON.parse(rawPayload.toString("utf8")));
802
854
  } catch (error) {
803
855
  throw new Error(
804
856
  `Failed to parse webhook payload: ${error instanceof Error ? error.message : String(error)}`,
@@ -806,6 +858,15 @@ var RstreamWebhookResource = class {
806
858
  );
807
859
  }
808
860
  }
861
+ generateSigningSecret() {
862
+ return generateWebhookSigningSecret();
863
+ }
864
+ sign(payload, secret, timestamp) {
865
+ return signWebhookPayload(payload, secret, timestamp);
866
+ }
867
+ headers(payload, event, secret, options) {
868
+ return buildWebhookHeaders(payload, event, secret, options);
869
+ }
809
870
  };
810
871
 
811
872
  // src/tunnels.ts
@@ -1237,7 +1298,13 @@ export {
1237
1298
  RstreamWebhookResource,
1238
1299
  RstreamWebhookResource as RstreamWebhookRessource,
1239
1300
  RstreamTunnelsClient as Tunnels,
1301
+ WEBHOOK_DELIVERY_ID_HEADER,
1302
+ WEBHOOK_EVENT_ID_HEADER,
1303
+ WEBHOOK_EVENT_TYPE_HEADER,
1304
+ WEBHOOK_ID_HEADER,
1305
+ WEBHOOK_SIGNATURE_HEADER,
1240
1306
  Watch,
1307
+ buildWebhookHeaders,
1241
1308
  clientFilterSchema,
1242
1309
  clientSchema,
1243
1310
  createAPITURNCredentials,
@@ -1249,6 +1316,7 @@ export {
1249
1316
  engineOpenApiJson,
1250
1317
  eventSchema,
1251
1318
  formatTunnelHost,
1319
+ generateWebhookSigningSecret,
1252
1320
  listClientsParamsSchema,
1253
1321
  listClientsResponseSchema,
1254
1322
  listTunnelsParamsSchema,
@@ -1261,6 +1329,7 @@ export {
1261
1329
  resolveTunnelsAPIURL,
1262
1330
  resolveTunnelsCredentials,
1263
1331
  resolveTunnelsEngine,
1332
+ signWebhookPayload,
1264
1333
  streamEndpointSchema,
1265
1334
  streamSummaryEventSchema,
1266
1335
  streamSummarySchema,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rstreamlabs/tunnels",
3
- "version": "3.2.0",
3
+ "version": "3.2.1",
4
4
  "description": "JS/TS SDK for the rstream Engine API and tunnel runtime APIs.",
5
5
  "author": "@uartnet <hello@rstream.io>",
6
6
  "repository": {