@liveblocks/node 1.1.0 → 1.1.1-dual2

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.
@@ -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.js CHANGED
@@ -84,7 +84,7 @@ function buildLiveblocksAuthorizeEndpoint(options, roomId) {
84
84
 
85
85
  // src/webhooks.ts
86
86
  var _crypto = require('crypto'); var _crypto2 = _interopRequireDefault(_crypto);
87
- var _WebhookHandler = class {
87
+ var _WebhookHandler = class _WebhookHandler {
88
88
  constructor(secret) {
89
89
  if (!secret)
90
90
  throw new Error("Secret is required");
@@ -179,8 +179,8 @@ var _WebhookHandler = class {
179
179
  );
180
180
  }
181
181
  };
182
+ _WebhookHandler.secretPrefix = "whsec_";
182
183
  var WebhookHandler = _WebhookHandler;
183
- WebhookHandler.secretPrefix = "whsec_";
184
184
  var WEBHOOK_TOLERANCE_IN_SECONDS = 5 * 60;
185
185
  var isNotUndefined = (value) => value !== void 0;
186
186
 
package/dist/index.mjs ADDED
@@ -0,0 +1,189 @@
1
+ var __async = (__this, __arguments, generator) => {
2
+ return new Promise((resolve, reject) => {
3
+ var fulfilled = (value) => {
4
+ try {
5
+ step(generator.next(value));
6
+ } catch (e) {
7
+ reject(e);
8
+ }
9
+ };
10
+ var rejected = (value) => {
11
+ try {
12
+ step(generator.throw(value));
13
+ } catch (e) {
14
+ reject(e);
15
+ }
16
+ };
17
+ var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
18
+ step((generator = generator.apply(__this, __arguments)).next());
19
+ });
20
+ };
21
+
22
+ // src/authorize.ts
23
+ import fetch from "node-fetch";
24
+ function authorize(options) {
25
+ return __async(this, null, function* () {
26
+ try {
27
+ const { room, secret, userId, userInfo, groupIds } = options;
28
+ if (!(typeof room === "string" && room.length > 0)) {
29
+ throw new Error(
30
+ "Invalid room. Please provide a non-empty string as the room. For more information: https://liveblocks.io/docs/api-reference/liveblocks-node#authorize"
31
+ );
32
+ }
33
+ if (!(typeof userId === "string" && userId.length > 0)) {
34
+ throw new Error(
35
+ "Invalid userId. Please provide a non-empty string as the userId. For more information: https://liveblocks.io/docs/api-reference/liveblocks-node#authorize"
36
+ );
37
+ }
38
+ const resp = yield fetch(buildLiveblocksAuthorizeEndpoint(options, room), {
39
+ method: "POST",
40
+ headers: {
41
+ Authorization: `Bearer ${secret}`,
42
+ "Content-Type": "application/json"
43
+ },
44
+ body: JSON.stringify({
45
+ userId,
46
+ userInfo,
47
+ groupIds
48
+ })
49
+ });
50
+ if (resp.ok) {
51
+ return {
52
+ status: 200,
53
+ body: yield resp.text()
54
+ };
55
+ }
56
+ if (resp.status >= 500) {
57
+ return {
58
+ status: 503,
59
+ body: yield resp.text()
60
+ };
61
+ } else {
62
+ return {
63
+ status: 403,
64
+ body: yield resp.text()
65
+ };
66
+ }
67
+ } catch (er) {
68
+ return {
69
+ status: 503,
70
+ body: 'Call to "https://api.liveblocks.io/v2/rooms/:roomId/authorize" failed. See "error" for more information.',
71
+ error: er
72
+ };
73
+ }
74
+ });
75
+ }
76
+ function buildLiveblocksAuthorizeEndpoint(options, roomId) {
77
+ if (options.liveblocksAuthorizeEndpoint) {
78
+ return options.liveblocksAuthorizeEndpoint.replace("{roomId}", roomId);
79
+ }
80
+ return `https://api.liveblocks.io/v2/rooms/${encodeURIComponent(
81
+ roomId
82
+ )}/authorize`;
83
+ }
84
+
85
+ // src/webhooks.ts
86
+ import crypto from "crypto";
87
+ var _WebhookHandler = class _WebhookHandler {
88
+ constructor(secret) {
89
+ if (!secret)
90
+ throw new Error("Secret is required");
91
+ if (typeof secret !== "string")
92
+ throw new Error("Secret must be a string");
93
+ if (secret.startsWith(_WebhookHandler.secretPrefix) === false)
94
+ throw new Error("Invalid secret, must start with whsec_");
95
+ const secretKey = secret.slice(_WebhookHandler.secretPrefix.length);
96
+ this.secretBuffer = Buffer.from(secretKey, "base64");
97
+ }
98
+ /**
99
+ * Verifies a webhook request and returns the event
100
+ */
101
+ verifyRequest(request) {
102
+ const { webhookId, timestamp, rawSignatures } = this.verifyHeaders(
103
+ request.headers
104
+ );
105
+ this.verifyTimestamp(timestamp);
106
+ const signature = this.sign(`${webhookId}.${timestamp}.${request.rawBody}`);
107
+ const expectedSignatures = rawSignatures.split(" ").map((rawSignature) => {
108
+ const [, parsedSignature] = rawSignature.split(",");
109
+ return parsedSignature;
110
+ }).filter(isNotUndefined);
111
+ if (expectedSignatures.includes(signature) === false)
112
+ throw new Error(
113
+ `Invalid signature, expected one of ${expectedSignatures.join(
114
+ ", "
115
+ )}, got ${signature}`
116
+ );
117
+ const event = JSON.parse(request.rawBody);
118
+ this.verifyWebhookEventType(event);
119
+ return event;
120
+ }
121
+ /**
122
+ * Verifies the headers and returns the webhookId, timestamp and rawSignatures
123
+ */
124
+ verifyHeaders(headers) {
125
+ const sanitizedHeaders = {};
126
+ Object.keys(headers).forEach((key) => {
127
+ sanitizedHeaders[key.toLowerCase()] = headers[key];
128
+ });
129
+ const webhookId = sanitizedHeaders["webhook-id"];
130
+ if (typeof webhookId !== "string")
131
+ throw new Error("Invalid webhook-id header");
132
+ const timestamp = sanitizedHeaders["webhook-timestamp"];
133
+ if (typeof timestamp !== "string")
134
+ throw new Error("Invalid webhook-timestamp header");
135
+ const rawSignatures = sanitizedHeaders["webhook-signature"];
136
+ if (typeof rawSignatures !== "string")
137
+ throw new Error("Invalid webhook-signature header");
138
+ return { webhookId, timestamp, rawSignatures };
139
+ }
140
+ /**
141
+ * Signs the content with the secret
142
+ * @param content
143
+ * @returns `string`
144
+ */
145
+ sign(content) {
146
+ return crypto.createHmac("sha256", this.secretBuffer).update(content).digest("base64");
147
+ }
148
+ /**
149
+ * Verifies that the timestamp is not too old or in the future
150
+ */
151
+ verifyTimestamp(timestampHeader) {
152
+ const now = Math.floor(Date.now() / 1e3);
153
+ const timestamp = parseInt(timestampHeader, 10);
154
+ if (isNaN(timestamp)) {
155
+ throw new Error("Invalid timestamp");
156
+ }
157
+ if (timestamp < now - WEBHOOK_TOLERANCE_IN_SECONDS) {
158
+ throw new Error("Timestamp too old");
159
+ }
160
+ if (timestamp > now + WEBHOOK_TOLERANCE_IN_SECONDS) {
161
+ throw new Error("Timestamp in the future");
162
+ }
163
+ }
164
+ /**
165
+ * Ensures that the event is a known event type
166
+ * or throws and prompts the user to upgrade to a higher version of @liveblocks/node
167
+ */
168
+ verifyWebhookEventType(event) {
169
+ if (event && event.type && [
170
+ "storageUpdated",
171
+ "userEntered",
172
+ "userLeft",
173
+ "roomCreated",
174
+ "roomDeleted"
175
+ ].includes(event.type))
176
+ return;
177
+ throw new Error(
178
+ "Unknown event type, please upgrade to a higher version of @liveblocks/node"
179
+ );
180
+ }
181
+ };
182
+ _WebhookHandler.secretPrefix = "whsec_";
183
+ var WebhookHandler = _WebhookHandler;
184
+ var WEBHOOK_TOLERANCE_IN_SECONDS = 5 * 60;
185
+ var isNotUndefined = (value) => value !== void 0;
186
+ export {
187
+ WebhookHandler,
188
+ authorize
189
+ };
package/package.json CHANGED
@@ -1,10 +1,23 @@
1
1
  {
2
2
  "name": "@liveblocks/node",
3
- "version": "1.1.0",
3
+ "version": "1.1.1-dual2",
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"
@@ -12,8 +25,9 @@
12
25
  "scripts": {
13
26
  "dev": "tsup --watch",
14
27
  "build": "tsup",
15
- "format": "eslint --fix src/; prettier --write src/",
28
+ "format": "(eslint --fix src/ || true) && prettier --write src/",
16
29
  "lint": "eslint src/",
30
+ "lint:package": "publint --strict && attw --pack",
17
31
  "test": "jest --silent --verbose --color=always",
18
32
  "test:watch": "jest --silent --verbose --color=always --watch"
19
33
  },