@liveblocks/node 0.19.9-beta3 → 0.19.9-beta4

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.d.ts CHANGED
@@ -1,3 +1,103 @@
1
+ import { IncomingHttpHeaders } from 'http';
2
+
3
+ declare class WebhookHandler {
4
+ private secretBuffer;
5
+ private static secretPrefix;
6
+ constructor(
7
+ /**
8
+ * The signing secret provided on the dashboard's webhooks page
9
+ * @example "whsec_wPbvQ+u3VtN2e2tRPDKchQ1tBZ3svaHLm"
10
+ */
11
+ secret: string);
12
+ /**
13
+ * Verifies a webhook request and returns the event
14
+ */
15
+ verifyRequest(request: WebhookRequest): WebhookEvent;
16
+ /**
17
+ * Verifies the headers and returns the webhookId, timestamp and rawSignatures
18
+ */
19
+ private verifyHeaders;
20
+ /**
21
+ * Signs the content with the secret
22
+ * @param content
23
+ * @returns `string`
24
+ */
25
+ private sign;
26
+ /**
27
+ * Verifies that the timestamp is not too old or in the future
28
+ */
29
+ private verifyTimestamp;
30
+ /**
31
+ * Ensures that the event is a known event type
32
+ * or throws and prompts the user to upgrade to a higher version of @liveblocks/node
33
+ */
34
+ private verifyWebhookEventType;
35
+ }
36
+ declare type WebhookRequest = {
37
+ /**
38
+ * Headers of the request
39
+ * @example
40
+ * {
41
+ * "webhook-id": "123",
42
+ * "webhook-timestamp": "1614588800000",
43
+ * "webhook-signature": "v1,bm9ldHUjKzFob2VudXRob2VodWUzMjRvdWVvdW9ldQo= v2,MzJsNDk4MzI0K2VvdSMjMTEjQEBAQDEyMzMzMzEyMwo="
44
+ * }
45
+ */
46
+ headers: IncomingHttpHeaders;
47
+ /**
48
+ * Raw body of the request, do not parse it
49
+ * @example '{"type":"storageUpdated","data":{"roomId":"my-room-id","appId":"my-app-id","updatedAt":"2021-03-01T12:00:00.000Z"}}'
50
+ */
51
+ rawBody: string;
52
+ };
53
+ declare type WebhookEvent = StorageUpdatedEvent | UserEnteredEvent | UserLeftEvent;
54
+ declare type StorageUpdatedEvent = {
55
+ type: "storageUpdated";
56
+ data: {
57
+ roomId: string;
58
+ appId: string;
59
+ /**
60
+ * ISO 8601 datestring
61
+ * @example "2021-03-01T12:00:00.000Z"
62
+ */
63
+ updatedAt: string;
64
+ };
65
+ };
66
+ declare type UserEnteredEvent = {
67
+ type: "userEntered";
68
+ data: {
69
+ appId: string;
70
+ roomId: string;
71
+ connectionId: number;
72
+ userId: string | null;
73
+ userInfo: Record<string, unknown> | null;
74
+ /**
75
+ * ISO 8601 datestring
76
+ * @example "2021-03-01T12:00:00.000Z"
77
+ * @description The time when the user entered the room.
78
+ */
79
+ enteredAt: string;
80
+ numActiveUsers: number;
81
+ };
82
+ };
83
+ declare type UserLeftEvent = {
84
+ type: "userLeft";
85
+ data: {
86
+ appId: string;
87
+ roomId: string;
88
+ connectionId: number;
89
+ userId: string | null;
90
+ userInfo: Record<string, unknown> | null;
91
+ /**
92
+ * ISO 8601 datestring
93
+ * @example "2021-03-01T12:00:00.000Z"
94
+ * @description The time when the user left the room.
95
+ */
96
+ leftAt: string;
97
+ numActiveUsers: number;
98
+ };
99
+ };
100
+
1
101
  declare type AuthorizeOptions = {
2
102
  /**
3
103
  * The secret api provided at https://liveblocks.io/dashboard/apikeys
@@ -47,4 +147,4 @@ declare type AuthorizeResponse = {
47
147
  */
48
148
  declare function authorize(options: AuthorizeOptions): Promise<AuthorizeResponse>;
49
149
 
50
- export { authorize };
150
+ export { StorageUpdatedEvent, UserEnteredEvent, UserLeftEvent, WebhookEvent, WebhookHandler, WebhookRequest, authorize };
package/dist/index.js CHANGED
@@ -21,6 +21,84 @@
21
21
 
22
22
  // src/index.ts
23
23
  var _nodefetch = require('node-fetch'); var _nodefetch2 = _interopRequireDefault(_nodefetch);
24
+
25
+ // src/webhooks.ts
26
+ var _crypto = require('crypto'); var _crypto2 = _interopRequireDefault(_crypto);
27
+ var _WebhookHandler = class {
28
+ constructor(secret) {
29
+ if (!secret)
30
+ throw new Error("Secret is required");
31
+ if (typeof secret !== "string")
32
+ throw new Error("Secret must be a string");
33
+ if (secret.startsWith(_WebhookHandler.secretPrefix) === false)
34
+ throw new Error("Invalid secret, must start with whsec_");
35
+ const secretKey = secret.slice(_WebhookHandler.secretPrefix.length);
36
+ this.secretBuffer = Buffer.from(secretKey, "base64");
37
+ }
38
+ verifyRequest(request) {
39
+ const { webhookId, timestamp, rawSignatures } = this.verifyHeaders(
40
+ request.headers
41
+ );
42
+ this.verifyTimestamp(timestamp);
43
+ const signature = this.sign(`${webhookId}.${timestamp}.${request.rawBody}`);
44
+ const expectedSignatures = rawSignatures.split(" ").map((rawSignature) => {
45
+ const [, parsedSignature] = rawSignature.split(",");
46
+ return parsedSignature;
47
+ }).filter(isNotUndefined);
48
+ if (expectedSignatures.includes(signature) === false)
49
+ throw new Error(
50
+ `Invalid signature, expected one of ${expectedSignatures}, got ${signature}`
51
+ );
52
+ const event = JSON.parse(request.rawBody);
53
+ this.verifyWebhookEventType(event);
54
+ return event;
55
+ }
56
+ verifyHeaders(headers) {
57
+ const sanitizedHeaders = {};
58
+ Object.keys(headers).forEach((key) => {
59
+ sanitizedHeaders[key.toLowerCase()] = headers[key];
60
+ });
61
+ const webhookId = sanitizedHeaders["webhook-id"];
62
+ if (typeof webhookId !== "string")
63
+ throw new Error("Invalid webhook-id header");
64
+ const timestamp = sanitizedHeaders["webhook-timestamp"];
65
+ if (typeof timestamp !== "string")
66
+ throw new Error("Invalid webhook-timestamp header");
67
+ const rawSignatures = sanitizedHeaders["webhook-signature"];
68
+ if (typeof rawSignatures !== "string")
69
+ throw new Error("Invalid webhook-signature header");
70
+ return { webhookId, timestamp, rawSignatures };
71
+ }
72
+ sign(content) {
73
+ return _crypto2.default.createHmac("sha256", this.secretBuffer).update(content).digest("base64");
74
+ }
75
+ verifyTimestamp(timestampHeader) {
76
+ const now = Math.floor(Date.now() / 1e3);
77
+ const timestamp = parseInt(timestampHeader, 10);
78
+ if (isNaN(timestamp)) {
79
+ throw new Error("Invalid timestamp");
80
+ }
81
+ if (timestamp < now - WEBHOOK_TOLERANCE_IN_SECONDS) {
82
+ throw new Error("Timestamp too old");
83
+ }
84
+ if (timestamp > now + WEBHOOK_TOLERANCE_IN_SECONDS) {
85
+ throw new Error("Timestamp in the future");
86
+ }
87
+ }
88
+ verifyWebhookEventType(event) {
89
+ if (event && event.type && (event.type === "storageUpdated" || event.type === "userEntered" || event.type === "userLeft"))
90
+ return;
91
+ throw new Error(
92
+ "Unknown event type, please upgrade to a higher version of @liveblocks/node"
93
+ );
94
+ }
95
+ };
96
+ var WebhookHandler = _WebhookHandler;
97
+ WebhookHandler.secretPrefix = "whsec_";
98
+ var WEBHOOK_TOLERANCE_IN_SECONDS = 5 * 60;
99
+ var isNotUndefined = (value) => value !== void 0;
100
+
101
+ // src/index.ts
24
102
  function authorize(options) {
25
103
  return __async(this, null, function* () {
26
104
  try {
@@ -74,4 +152,5 @@ function buildLiveblocksAuthorizeEndpoint(options, roomId) {
74
152
  }
75
153
 
76
154
 
77
- exports.authorize = authorize;
155
+
156
+ exports.WebhookHandler = WebhookHandler; exports.authorize = authorize;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liveblocks/node",
3
- "version": "0.19.9-beta3",
3
+ "version": "0.19.9-beta4",
4
4
  "description": "A server-side utility that lets you set up a Liveblocks authentication endpoint. Liveblocks is the all-in-one toolkit to build collaborative products like Figma, Notion, and more.",
5
5
  "license": "Apache-2.0",
6
6
  "main": "./dist/index.js",
@@ -20,7 +20,8 @@
20
20
  "devDependencies": {
21
21
  "@liveblocks/eslint-config": "*",
22
22
  "@liveblocks/jest-config": "*",
23
- "@types/node-fetch": "^2.5.8"
23
+ "@types/node-fetch": "^2.5.8",
24
+ "svix": "^0.75.0"
24
25
  },
25
26
  "dependencies": {
26
27
  "node-fetch": "^2.6.1"