@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 +101 -1
- package/dist/index.js +80 -1
- package/package.json +3 -2
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
|
-
|
|
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-
|
|
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"
|