@liveblocks/node 1.1.5-test3 → 1.1.5-test5
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.mts +201 -0
- package/dist/index.mjs +167 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +14 -1
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { IncomingHttpHeaders } from 'http';
|
|
2
|
+
|
|
3
|
+
declare type AuthorizeOptions = {
|
|
4
|
+
/**
|
|
5
|
+
* The secret API key for your Liveblocks account. You can find it on
|
|
6
|
+
* https://liveblocks.io/dashboard/apikeys
|
|
7
|
+
*/
|
|
8
|
+
secret: string;
|
|
9
|
+
/**
|
|
10
|
+
* The room ID for which to authorize the user. This will authorize the user
|
|
11
|
+
* to enter the Liveblocks room.
|
|
12
|
+
*/
|
|
13
|
+
room: string;
|
|
14
|
+
/**
|
|
15
|
+
* Associates a user ID to the session that is being authorized. The user ID
|
|
16
|
+
* is typically set to the user ID from your own database.
|
|
17
|
+
*
|
|
18
|
+
* It can also be used to generate a token that gives access to a private
|
|
19
|
+
* room where the userId is configured in the room accesses.
|
|
20
|
+
*
|
|
21
|
+
* This user ID will be used as the unique identifier to compute your
|
|
22
|
+
* Liveblocks account's Monthly Active Users.
|
|
23
|
+
*/
|
|
24
|
+
userId: string;
|
|
25
|
+
/**
|
|
26
|
+
* Arbitrary metadata associated to this user session.
|
|
27
|
+
*
|
|
28
|
+
* You can use it to store a small amount of static metadata for a user
|
|
29
|
+
* session. It is public information, that will be visible to other users in
|
|
30
|
+
* the same room, like name, avatar URL, etc.
|
|
31
|
+
*
|
|
32
|
+
* It's only suitable for static info that won't change during a session. If
|
|
33
|
+
* you want to store dynamic metadata on a user session, don't keep that in
|
|
34
|
+
* the session token, but use Presence instead.
|
|
35
|
+
*
|
|
36
|
+
* Can't exceed 1KB when serialized as JSON.
|
|
37
|
+
*/
|
|
38
|
+
userInfo?: unknown;
|
|
39
|
+
/**
|
|
40
|
+
* Tell Liveblocks which group IDs this user belongs to. This will authorize
|
|
41
|
+
* the user session to access private rooms that have at least one of these
|
|
42
|
+
* group IDs listed in their room access configuration.
|
|
43
|
+
*
|
|
44
|
+
* See https://liveblocks.io/docs/guides/managing-rooms-users-permissions#permissions
|
|
45
|
+
* for how to configure your room's permissions to use this feature.
|
|
46
|
+
*/
|
|
47
|
+
groupIds?: string[];
|
|
48
|
+
};
|
|
49
|
+
declare type AuthorizeResponse = {
|
|
50
|
+
status: number;
|
|
51
|
+
body: string;
|
|
52
|
+
error?: Error;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Tells Liveblocks that a user should be allowed access to a room, which user
|
|
56
|
+
* this session is for, and what metadata to associate with the user (like
|
|
57
|
+
* name, avatar, etc.)
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* export default async function auth(req, res) {
|
|
61
|
+
*
|
|
62
|
+
* // Implement your own security here.
|
|
63
|
+
*
|
|
64
|
+
* const room = req.body.room;
|
|
65
|
+
* const response = await authorize({
|
|
66
|
+
* room,
|
|
67
|
+
* secret,
|
|
68
|
+
* userId: "123",
|
|
69
|
+
* userInfo: { // Optional
|
|
70
|
+
* name: "Ada Lovelace"
|
|
71
|
+
* },
|
|
72
|
+
* groupIds: ["group1"] // Optional
|
|
73
|
+
* });
|
|
74
|
+
* return res.status(response.status).end(response.body);
|
|
75
|
+
* }
|
|
76
|
+
*/
|
|
77
|
+
declare function authorize(options: AuthorizeOptions): Promise<AuthorizeResponse>;
|
|
78
|
+
|
|
79
|
+
declare class WebhookHandler {
|
|
80
|
+
private secretBuffer;
|
|
81
|
+
private static secretPrefix;
|
|
82
|
+
constructor(
|
|
83
|
+
/**
|
|
84
|
+
* The signing secret provided on the dashboard's webhooks page
|
|
85
|
+
* @example "whsec_wPbvQ+u3VtN2e2tRPDKchQ1tBZ3svaHLm"
|
|
86
|
+
*/
|
|
87
|
+
secret: string);
|
|
88
|
+
/**
|
|
89
|
+
* Verifies a webhook request and returns the event
|
|
90
|
+
*/
|
|
91
|
+
verifyRequest(request: WebhookRequest): WebhookEvent;
|
|
92
|
+
/**
|
|
93
|
+
* Verifies the headers and returns the webhookId, timestamp and rawSignatures
|
|
94
|
+
*/
|
|
95
|
+
private verifyHeaders;
|
|
96
|
+
/**
|
|
97
|
+
* Signs the content with the secret
|
|
98
|
+
* @param content
|
|
99
|
+
* @returns `string`
|
|
100
|
+
*/
|
|
101
|
+
private sign;
|
|
102
|
+
/**
|
|
103
|
+
* Verifies that the timestamp is not too old or in the future
|
|
104
|
+
*/
|
|
105
|
+
private verifyTimestamp;
|
|
106
|
+
/**
|
|
107
|
+
* Ensures that the event is a known event type
|
|
108
|
+
* or throws and prompts the user to upgrade to a higher version of @liveblocks/node
|
|
109
|
+
*/
|
|
110
|
+
private verifyWebhookEventType;
|
|
111
|
+
}
|
|
112
|
+
declare type WebhookRequest = {
|
|
113
|
+
/**
|
|
114
|
+
* Headers of the request
|
|
115
|
+
* @example
|
|
116
|
+
* {
|
|
117
|
+
* "webhook-id": "123",
|
|
118
|
+
* "webhook-timestamp": "1614588800000",
|
|
119
|
+
* "webhook-signature": "v1,bm9ldHUjKzFob2VudXRob2VodWUzMjRvdWVvdW9ldQo= v2,MzJsNDk4MzI0K2VvdSMjMTEjQEBAQDEyMzMzMzEyMwo="
|
|
120
|
+
* }
|
|
121
|
+
*/
|
|
122
|
+
headers: IncomingHttpHeaders;
|
|
123
|
+
/**
|
|
124
|
+
* Raw body of the request, do not parse it
|
|
125
|
+
* @example '{"type":"storageUpdated","data":{"roomId":"my-room-id","appId":"my-app-id","updatedAt":"2021-03-01T12:00:00.000Z"}}'
|
|
126
|
+
*/
|
|
127
|
+
rawBody: string;
|
|
128
|
+
};
|
|
129
|
+
declare type WebhookEvent = StorageUpdatedEvent | UserEnteredEvent | UserLeftEvent | RoomCreatedEvent | RoomDeletedEvent;
|
|
130
|
+
declare type StorageUpdatedEvent = {
|
|
131
|
+
type: "storageUpdated";
|
|
132
|
+
data: {
|
|
133
|
+
roomId: string;
|
|
134
|
+
appId: string;
|
|
135
|
+
/**
|
|
136
|
+
* ISO 8601 datestring
|
|
137
|
+
* @example "2021-03-01T12:00:00.000Z"
|
|
138
|
+
*/
|
|
139
|
+
updatedAt: string;
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
declare type UserEnteredEvent = {
|
|
143
|
+
type: "userEntered";
|
|
144
|
+
data: {
|
|
145
|
+
appId: string;
|
|
146
|
+
roomId: string;
|
|
147
|
+
connectionId: number;
|
|
148
|
+
userId: string | null;
|
|
149
|
+
userInfo: Record<string, unknown> | null;
|
|
150
|
+
/**
|
|
151
|
+
* ISO 8601 datestring
|
|
152
|
+
* @example "2021-03-01T12:00:00.000Z"
|
|
153
|
+
* @description The time when the user entered the room.
|
|
154
|
+
*/
|
|
155
|
+
enteredAt: string;
|
|
156
|
+
numActiveUsers: number;
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
declare type UserLeftEvent = {
|
|
160
|
+
type: "userLeft";
|
|
161
|
+
data: {
|
|
162
|
+
appId: string;
|
|
163
|
+
roomId: string;
|
|
164
|
+
connectionId: number;
|
|
165
|
+
userId: string | null;
|
|
166
|
+
userInfo: Record<string, unknown> | null;
|
|
167
|
+
/**
|
|
168
|
+
* ISO 8601 datestring
|
|
169
|
+
* @example "2021-03-01T12:00:00.000Z"
|
|
170
|
+
* @description The time when the user left the room.
|
|
171
|
+
*/
|
|
172
|
+
leftAt: string;
|
|
173
|
+
numActiveUsers: number;
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
declare type RoomCreatedEvent = {
|
|
177
|
+
type: "roomCreated";
|
|
178
|
+
data: {
|
|
179
|
+
appId: string;
|
|
180
|
+
roomId: string;
|
|
181
|
+
/**
|
|
182
|
+
* ISO 8601 datestring
|
|
183
|
+
* @example "2021-03-01T12:00:00.000Z"
|
|
184
|
+
*/
|
|
185
|
+
createdAt: string;
|
|
186
|
+
};
|
|
187
|
+
};
|
|
188
|
+
declare type RoomDeletedEvent = {
|
|
189
|
+
type: "roomDeleted";
|
|
190
|
+
data: {
|
|
191
|
+
appId: string;
|
|
192
|
+
roomId: string;
|
|
193
|
+
/**
|
|
194
|
+
* ISO 8601 datestring
|
|
195
|
+
* @example "2021-03-01T12:00:00.000Z"
|
|
196
|
+
*/
|
|
197
|
+
deletedAt: string;
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
export { StorageUpdatedEvent, UserEnteredEvent, UserLeftEvent, WebhookEvent, WebhookHandler, WebhookRequest, authorize };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// src/authorize.ts
|
|
2
|
+
import fetch from "node-fetch";
|
|
3
|
+
async function authorize(options) {
|
|
4
|
+
try {
|
|
5
|
+
const { room, secret, userId, userInfo, groupIds } = options;
|
|
6
|
+
if (!(typeof room === "string" && room.length > 0)) {
|
|
7
|
+
throw new Error(
|
|
8
|
+
"Invalid room. Please provide a non-empty string as the room. For more information: https://liveblocks.io/docs/api-reference/liveblocks-node#authorize"
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
if (!(typeof userId === "string" && userId.length > 0)) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
"Invalid userId. Please provide a non-empty string as the userId. For more information: https://liveblocks.io/docs/api-reference/liveblocks-node#authorize"
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
const resp = await fetch(buildLiveblocksAuthorizeEndpoint(options, room), {
|
|
17
|
+
method: "POST",
|
|
18
|
+
headers: {
|
|
19
|
+
Authorization: `Bearer ${secret}`,
|
|
20
|
+
"Content-Type": "application/json"
|
|
21
|
+
},
|
|
22
|
+
body: JSON.stringify({
|
|
23
|
+
userId,
|
|
24
|
+
userInfo,
|
|
25
|
+
groupIds
|
|
26
|
+
})
|
|
27
|
+
});
|
|
28
|
+
if (resp.ok) {
|
|
29
|
+
return {
|
|
30
|
+
status: 200,
|
|
31
|
+
body: await resp.text()
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (resp.status >= 500) {
|
|
35
|
+
return {
|
|
36
|
+
status: 503,
|
|
37
|
+
body: await resp.text()
|
|
38
|
+
};
|
|
39
|
+
} else {
|
|
40
|
+
return {
|
|
41
|
+
status: 403,
|
|
42
|
+
body: await resp.text()
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
} catch (er) {
|
|
46
|
+
return {
|
|
47
|
+
status: 503,
|
|
48
|
+
body: 'Call to "https://api.liveblocks.io/v2/rooms/:roomId/authorize" failed. See "error" for more information.',
|
|
49
|
+
error: er
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function buildLiveblocksAuthorizeEndpoint(options, roomId) {
|
|
54
|
+
if (options.liveblocksAuthorizeEndpoint) {
|
|
55
|
+
return options.liveblocksAuthorizeEndpoint.replace("{roomId}", roomId);
|
|
56
|
+
}
|
|
57
|
+
return `https://api.liveblocks.io/v2/rooms/${encodeURIComponent(
|
|
58
|
+
roomId
|
|
59
|
+
)}/authorize`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/webhooks.ts
|
|
63
|
+
import crypto from "crypto";
|
|
64
|
+
var _WebhookHandler = class _WebhookHandler {
|
|
65
|
+
constructor(secret) {
|
|
66
|
+
if (!secret)
|
|
67
|
+
throw new Error("Secret is required");
|
|
68
|
+
if (typeof secret !== "string")
|
|
69
|
+
throw new Error("Secret must be a string");
|
|
70
|
+
if (secret.startsWith(_WebhookHandler.secretPrefix) === false)
|
|
71
|
+
throw new Error("Invalid secret, must start with whsec_");
|
|
72
|
+
const secretKey = secret.slice(_WebhookHandler.secretPrefix.length);
|
|
73
|
+
this.secretBuffer = Buffer.from(secretKey, "base64");
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Verifies a webhook request and returns the event
|
|
77
|
+
*/
|
|
78
|
+
verifyRequest(request) {
|
|
79
|
+
const { webhookId, timestamp, rawSignatures } = this.verifyHeaders(
|
|
80
|
+
request.headers
|
|
81
|
+
);
|
|
82
|
+
this.verifyTimestamp(timestamp);
|
|
83
|
+
const signature = this.sign(`${webhookId}.${timestamp}.${request.rawBody}`);
|
|
84
|
+
const expectedSignatures = rawSignatures.split(" ").map((rawSignature) => {
|
|
85
|
+
const [, parsedSignature] = rawSignature.split(",");
|
|
86
|
+
return parsedSignature;
|
|
87
|
+
}).filter(isNotUndefined);
|
|
88
|
+
if (expectedSignatures.includes(signature) === false)
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Invalid signature, expected one of ${expectedSignatures.join(
|
|
91
|
+
", "
|
|
92
|
+
)}, got ${signature}`
|
|
93
|
+
);
|
|
94
|
+
const event = JSON.parse(request.rawBody);
|
|
95
|
+
this.verifyWebhookEventType(event);
|
|
96
|
+
return event;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Verifies the headers and returns the webhookId, timestamp and rawSignatures
|
|
100
|
+
*/
|
|
101
|
+
verifyHeaders(headers) {
|
|
102
|
+
const sanitizedHeaders = {};
|
|
103
|
+
Object.keys(headers).forEach((key) => {
|
|
104
|
+
sanitizedHeaders[key.toLowerCase()] = headers[key];
|
|
105
|
+
});
|
|
106
|
+
const webhookId = sanitizedHeaders["webhook-id"];
|
|
107
|
+
if (typeof webhookId !== "string")
|
|
108
|
+
throw new Error("Invalid webhook-id header");
|
|
109
|
+
const timestamp = sanitizedHeaders["webhook-timestamp"];
|
|
110
|
+
if (typeof timestamp !== "string")
|
|
111
|
+
throw new Error("Invalid webhook-timestamp header");
|
|
112
|
+
const rawSignatures = sanitizedHeaders["webhook-signature"];
|
|
113
|
+
if (typeof rawSignatures !== "string")
|
|
114
|
+
throw new Error("Invalid webhook-signature header");
|
|
115
|
+
return { webhookId, timestamp, rawSignatures };
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Signs the content with the secret
|
|
119
|
+
* @param content
|
|
120
|
+
* @returns `string`
|
|
121
|
+
*/
|
|
122
|
+
sign(content) {
|
|
123
|
+
return crypto.createHmac("sha256", this.secretBuffer).update(content).digest("base64");
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Verifies that the timestamp is not too old or in the future
|
|
127
|
+
*/
|
|
128
|
+
verifyTimestamp(timestampHeader) {
|
|
129
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
130
|
+
const timestamp = parseInt(timestampHeader, 10);
|
|
131
|
+
if (isNaN(timestamp)) {
|
|
132
|
+
throw new Error("Invalid timestamp");
|
|
133
|
+
}
|
|
134
|
+
if (timestamp < now - WEBHOOK_TOLERANCE_IN_SECONDS) {
|
|
135
|
+
throw new Error("Timestamp too old");
|
|
136
|
+
}
|
|
137
|
+
if (timestamp > now + WEBHOOK_TOLERANCE_IN_SECONDS) {
|
|
138
|
+
throw new Error("Timestamp in the future");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Ensures that the event is a known event type
|
|
143
|
+
* or throws and prompts the user to upgrade to a higher version of @liveblocks/node
|
|
144
|
+
*/
|
|
145
|
+
verifyWebhookEventType(event) {
|
|
146
|
+
if (event && event.type && [
|
|
147
|
+
"storageUpdated",
|
|
148
|
+
"userEntered",
|
|
149
|
+
"userLeft",
|
|
150
|
+
"roomCreated",
|
|
151
|
+
"roomDeleted"
|
|
152
|
+
].includes(event.type))
|
|
153
|
+
return;
|
|
154
|
+
throw new Error(
|
|
155
|
+
"Unknown event type, please upgrade to a higher version of @liveblocks/node"
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
_WebhookHandler.secretPrefix = "whsec_";
|
|
160
|
+
var WebhookHandler = _WebhookHandler;
|
|
161
|
+
var WEBHOOK_TOLERANCE_IN_SECONDS = 5 * 60;
|
|
162
|
+
var isNotUndefined = (value) => value !== void 0;
|
|
163
|
+
export {
|
|
164
|
+
WebhookHandler,
|
|
165
|
+
authorize
|
|
166
|
+
};
|
|
167
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/authorize.ts","../src/webhooks.ts"],"sourcesContent":["import fetch from \"node-fetch\";\n\ntype AuthorizeOptions = {\n /**\n * The secret API key for your Liveblocks account. You can find it on\n * https://liveblocks.io/dashboard/apikeys\n */\n secret: string;\n /**\n * The room ID for which to authorize the user. This will authorize the user\n * to enter the Liveblocks room.\n */\n room: string;\n /**\n * Associates a user ID to the session that is being authorized. The user ID\n * is typically set to the user ID from your own database.\n *\n * It can also be used to generate a token that gives access to a private\n * room where the userId is configured in the room accesses.\n *\n * This user ID will be used as the unique identifier to compute your\n * Liveblocks account's Monthly Active Users.\n */\n userId: string;\n /**\n * Arbitrary metadata associated to this user session.\n *\n * You can use it to store a small amount of static metadata for a user\n * session. It is public information, that will be visible to other users in\n * the same room, like name, avatar URL, etc.\n *\n * It's only suitable for static info that won't change during a session. If\n * you want to store dynamic metadata on a user session, don't keep that in\n * the session token, but use Presence instead.\n *\n * Can't exceed 1KB when serialized as JSON.\n */\n userInfo?: unknown;\n /**\n * Tell Liveblocks which group IDs this user belongs to. This will authorize\n * the user session to access private rooms that have at least one of these\n * group IDs listed in their room access configuration.\n *\n * See https://liveblocks.io/docs/guides/managing-rooms-users-permissions#permissions\n * for how to configure your room's permissions to use this feature.\n */\n groupIds?: string[];\n};\n\n/** @internal */\ntype AllAuthorizeOptions = AuthorizeOptions & {\n liveblocksAuthorizeEndpoint?: string;\n};\n\ntype AuthorizeResponse = {\n status: number;\n body: string;\n error?: Error;\n};\n\n/**\n * Tells Liveblocks that a user should be allowed access to a room, which user\n * this session is for, and what metadata to associate with the user (like\n * name, avatar, etc.)\n *\n * @example\n * export default async function auth(req, res) {\n *\n * // Implement your own security here.\n *\n * const room = req.body.room;\n * const response = await authorize({\n * room,\n * secret,\n * userId: \"123\",\n * userInfo: { // Optional\n * name: \"Ada Lovelace\"\n * },\n * groupIds: [\"group1\"] // Optional\n * });\n * return res.status(response.status).end(response.body);\n * }\n */\nexport async function authorize(\n options: AuthorizeOptions\n): Promise<AuthorizeResponse> {\n try {\n const { room, secret, userId, userInfo, groupIds } = options;\n\n if (!(typeof room === \"string\" && room.length > 0)) {\n throw new Error(\n \"Invalid room. Please provide a non-empty string as the room. For more information: https://liveblocks.io/docs/api-reference/liveblocks-node#authorize\"\n );\n }\n\n if (!(typeof userId === \"string\" && userId.length > 0)) {\n throw new Error(\n \"Invalid userId. Please provide a non-empty string as the userId. For more information: https://liveblocks.io/docs/api-reference/liveblocks-node#authorize\"\n );\n }\n\n const resp = await fetch(buildLiveblocksAuthorizeEndpoint(options, room), {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${secret}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n userId,\n userInfo,\n groupIds,\n }),\n });\n\n if (resp.ok) {\n return {\n status: 200 /* OK */,\n body: await resp.text(),\n };\n }\n\n if (resp.status >= 500) {\n return {\n status: 503 /* Service Unavailable */,\n body: await resp.text(),\n };\n } else {\n return {\n status: 403 /* Unauthorized */,\n body: await resp.text(),\n };\n }\n } catch (er) {\n return {\n status: 503 /* Service Unavailable */,\n body: 'Call to \"https://api.liveblocks.io/v2/rooms/:roomId/authorize\" failed. See \"error\" for more information.',\n error: er as Error | undefined,\n };\n }\n}\n\nfunction buildLiveblocksAuthorizeEndpoint(\n options: AllAuthorizeOptions,\n roomId: string\n): string {\n // INTERNAL override for testing purpose.\n if (options.liveblocksAuthorizeEndpoint) {\n return options.liveblocksAuthorizeEndpoint.replace(\"{roomId}\", roomId);\n }\n\n return `https://api.liveblocks.io/v2/rooms/${encodeURIComponent(\n roomId\n )}/authorize`;\n}\n","import crypto from \"crypto\";\nimport type { IncomingHttpHeaders } from \"http\";\n\nexport class WebhookHandler {\n private secretBuffer: Buffer;\n private static secretPrefix = \"whsec_\";\n constructor(\n /**\n * The signing secret provided on the dashboard's webhooks page\n * @example \"whsec_wPbvQ+u3VtN2e2tRPDKchQ1tBZ3svaHLm\"\n */\n secret: string\n ) {\n if (!secret) throw new Error(\"Secret is required\");\n if (typeof secret !== \"string\") throw new Error(\"Secret must be a string\");\n\n if (secret.startsWith(WebhookHandler.secretPrefix) === false)\n throw new Error(\"Invalid secret, must start with whsec_\");\n\n const secretKey = secret.slice(WebhookHandler.secretPrefix.length);\n this.secretBuffer = Buffer.from(secretKey, \"base64\");\n }\n\n /**\n * Verifies a webhook request and returns the event\n */\n public verifyRequest(request: WebhookRequest): WebhookEvent {\n const { webhookId, timestamp, rawSignatures } = this.verifyHeaders(\n request.headers\n );\n\n this.verifyTimestamp(timestamp);\n\n const signature = this.sign(`${webhookId}.${timestamp}.${request.rawBody}`);\n\n const expectedSignatures = rawSignatures\n .split(\" \")\n .map((rawSignature) => {\n const [, parsedSignature] = rawSignature.split(\",\");\n return parsedSignature;\n })\n .filter(isNotUndefined);\n\n if (expectedSignatures.includes(signature) === false)\n throw new Error(\n `Invalid signature, expected one of ${expectedSignatures.join(\n \", \"\n )}, got ${signature}`\n );\n\n const event: WebhookEvent = JSON.parse(request.rawBody) as WebhookEvent;\n\n this.verifyWebhookEventType(event);\n\n return event;\n }\n\n /**\n * Verifies the headers and returns the webhookId, timestamp and rawSignatures\n */\n private verifyHeaders(headers: IncomingHttpHeaders) {\n const sanitizedHeaders: IncomingHttpHeaders = {};\n Object.keys(headers).forEach((key) => {\n sanitizedHeaders[key.toLowerCase()] = headers[key];\n });\n\n const webhookId = sanitizedHeaders[\"webhook-id\"];\n if (typeof webhookId !== \"string\")\n throw new Error(\"Invalid webhook-id header\");\n\n const timestamp = sanitizedHeaders[\"webhook-timestamp\"];\n if (typeof timestamp !== \"string\")\n throw new Error(\"Invalid webhook-timestamp header\");\n\n const rawSignatures = sanitizedHeaders[\"webhook-signature\"];\n if (typeof rawSignatures !== \"string\")\n throw new Error(\"Invalid webhook-signature header\");\n\n return { webhookId, timestamp, rawSignatures };\n }\n\n /**\n * Signs the content with the secret\n * @param content\n * @returns `string`\n */\n private sign(content: string): string {\n return crypto\n .createHmac(\"sha256\", this.secretBuffer)\n .update(content)\n .digest(\"base64\");\n }\n\n /**\n * Verifies that the timestamp is not too old or in the future\n */\n private verifyTimestamp(timestampHeader: string) {\n const now = Math.floor(Date.now() / 1000);\n const timestamp = parseInt(timestampHeader, 10);\n\n if (isNaN(timestamp)) {\n throw new Error(\"Invalid timestamp\");\n }\n\n // Check if timestamp is too old\n if (timestamp < now - WEBHOOK_TOLERANCE_IN_SECONDS) {\n throw new Error(\"Timestamp too old\");\n }\n\n // Check if timestamp is in the future\n if (timestamp > now + WEBHOOK_TOLERANCE_IN_SECONDS) {\n throw new Error(\"Timestamp in the future\");\n }\n }\n\n /**\n * Ensures that the event is a known event type\n * or throws and prompts the user to upgrade to a higher version of @liveblocks/node\n */\n private verifyWebhookEventType(\n event: WebhookEvent\n ): asserts event is WebhookEvent {\n if (\n event &&\n event.type &&\n [\n \"storageUpdated\",\n \"userEntered\",\n \"userLeft\",\n \"roomCreated\",\n \"roomDeleted\",\n ].includes(event.type)\n )\n return;\n\n throw new Error(\n \"Unknown event type, please upgrade to a higher version of @liveblocks/node\"\n );\n }\n}\n\nconst WEBHOOK_TOLERANCE_IN_SECONDS = 5 * 60; // 5 minutes\n\nconst isNotUndefined = <T>(value: T | undefined): value is T =>\n value !== undefined;\n\ntype WebhookRequest = {\n /**\n * Headers of the request\n * @example\n * {\n * \"webhook-id\": \"123\",\n * \"webhook-timestamp\": \"1614588800000\",\n * \"webhook-signature\": \"v1,bm9ldHUjKzFob2VudXRob2VodWUzMjRvdWVvdW9ldQo= v2,MzJsNDk4MzI0K2VvdSMjMTEjQEBAQDEyMzMzMzEyMwo=\"\n * }\n */\n headers: IncomingHttpHeaders;\n /**\n * Raw body of the request, do not parse it\n * @example '{\"type\":\"storageUpdated\",\"data\":{\"roomId\":\"my-room-id\",\"appId\":\"my-app-id\",\"updatedAt\":\"2021-03-01T12:00:00.000Z\"}}'\n */\n rawBody: string;\n};\n\ntype WebhookEvent =\n | StorageUpdatedEvent\n | UserEnteredEvent\n | UserLeftEvent\n | RoomCreatedEvent\n | RoomDeletedEvent;\n\ntype StorageUpdatedEvent = {\n type: \"storageUpdated\";\n data: {\n roomId: string;\n appId: string;\n /**\n * ISO 8601 datestring\n * @example \"2021-03-01T12:00:00.000Z\"\n */\n updatedAt: string;\n };\n};\n\ntype UserEnteredEvent = {\n type: \"userEntered\";\n data: {\n appId: string;\n roomId: string;\n connectionId: number;\n userId: string | null;\n userInfo: Record<string, unknown> | null;\n /**\n * ISO 8601 datestring\n * @example \"2021-03-01T12:00:00.000Z\"\n * @description The time when the user entered the room.\n */\n enteredAt: string;\n numActiveUsers: number;\n };\n};\n\ntype UserLeftEvent = {\n type: \"userLeft\";\n data: {\n appId: string;\n roomId: string;\n connectionId: number;\n userId: string | null;\n userInfo: Record<string, unknown> | null;\n /**\n * ISO 8601 datestring\n * @example \"2021-03-01T12:00:00.000Z\"\n * @description The time when the user left the room.\n */\n leftAt: string;\n numActiveUsers: number;\n };\n};\n\ntype RoomCreatedEvent = {\n type: \"roomCreated\";\n data: {\n appId: string;\n roomId: string;\n /**\n * ISO 8601 datestring\n * @example \"2021-03-01T12:00:00.000Z\"\n */\n createdAt: string;\n };\n};\n\ntype RoomDeletedEvent = {\n type: \"roomDeleted\";\n data: {\n appId: string;\n roomId: string;\n /**\n * ISO 8601 datestring\n * @example \"2021-03-01T12:00:00.000Z\"\n */\n deletedAt: string;\n };\n};\n\nexport type {\n RoomCreatedEvent,\n RoomDeletedEvent,\n StorageUpdatedEvent,\n UserEnteredEvent,\n UserLeftEvent,\n WebhookEvent,\n WebhookRequest,\n};\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liveblocks/node",
|
|
3
|
-
"version": "1.1.5-
|
|
3
|
+
"version": "1.1.5-test5",
|
|
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",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": {
|
|
11
|
+
"types": "./dist/index.d.mts",
|
|
12
|
+
"default": "./dist/index.mjs"
|
|
13
|
+
},
|
|
14
|
+
"require": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"module": "./dist/index.mjs",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
8
21
|
"files": [
|
|
9
22
|
"dist/**",
|
|
10
23
|
"README.md"
|