@liveblocks/node
Advanced tools
Comparing version 0.19.9-beta3 to 0.19.9-beta4
@@ -0,1 +1,101 @@ | ||
import { IncomingHttpHeaders } from 'http'; | ||
declare class WebhookHandler { | ||
private secretBuffer; | ||
private static secretPrefix; | ||
constructor( | ||
/** | ||
* The signing secret provided on the dashboard's webhooks page | ||
* @example "whsec_wPbvQ+u3VtN2e2tRPDKchQ1tBZ3svaHLm" | ||
*/ | ||
secret: string); | ||
/** | ||
* Verifies a webhook request and returns the event | ||
*/ | ||
verifyRequest(request: WebhookRequest): WebhookEvent; | ||
/** | ||
* Verifies the headers and returns the webhookId, timestamp and rawSignatures | ||
*/ | ||
private verifyHeaders; | ||
/** | ||
* Signs the content with the secret | ||
* @param content | ||
* @returns `string` | ||
*/ | ||
private sign; | ||
/** | ||
* Verifies that the timestamp is not too old or in the future | ||
*/ | ||
private verifyTimestamp; | ||
/** | ||
* Ensures that the event is a known event type | ||
* or throws and prompts the user to upgrade to a higher version of @liveblocks/node | ||
*/ | ||
private verifyWebhookEventType; | ||
} | ||
declare type WebhookRequest = { | ||
/** | ||
* Headers of the request | ||
* @example | ||
* { | ||
* "webhook-id": "123", | ||
* "webhook-timestamp": "1614588800000", | ||
* "webhook-signature": "v1,bm9ldHUjKzFob2VudXRob2VodWUzMjRvdWVvdW9ldQo= v2,MzJsNDk4MzI0K2VvdSMjMTEjQEBAQDEyMzMzMzEyMwo=" | ||
* } | ||
*/ | ||
headers: IncomingHttpHeaders; | ||
/** | ||
* Raw body of the request, do not parse it | ||
* @example '{"type":"storageUpdated","data":{"roomId":"my-room-id","appId":"my-app-id","updatedAt":"2021-03-01T12:00:00.000Z"}}' | ||
*/ | ||
rawBody: string; | ||
}; | ||
declare type WebhookEvent = StorageUpdatedEvent | UserEnteredEvent | UserLeftEvent; | ||
declare type StorageUpdatedEvent = { | ||
type: "storageUpdated"; | ||
data: { | ||
roomId: string; | ||
appId: string; | ||
/** | ||
* ISO 8601 datestring | ||
* @example "2021-03-01T12:00:00.000Z" | ||
*/ | ||
updatedAt: string; | ||
}; | ||
}; | ||
declare type UserEnteredEvent = { | ||
type: "userEntered"; | ||
data: { | ||
appId: string; | ||
roomId: string; | ||
connectionId: number; | ||
userId: string | null; | ||
userInfo: Record<string, unknown> | null; | ||
/** | ||
* ISO 8601 datestring | ||
* @example "2021-03-01T12:00:00.000Z" | ||
* @description The time when the user entered the room. | ||
*/ | ||
enteredAt: string; | ||
numActiveUsers: number; | ||
}; | ||
}; | ||
declare type UserLeftEvent = { | ||
type: "userLeft"; | ||
data: { | ||
appId: string; | ||
roomId: string; | ||
connectionId: number; | ||
userId: string | null; | ||
userInfo: Record<string, unknown> | null; | ||
/** | ||
* ISO 8601 datestring | ||
* @example "2021-03-01T12:00:00.000Z" | ||
* @description The time when the user left the room. | ||
*/ | ||
leftAt: string; | ||
numActiveUsers: number; | ||
}; | ||
}; | ||
declare type AuthorizeOptions = { | ||
@@ -50,2 +150,2 @@ /** | ||
export { authorize }; | ||
export { StorageUpdatedEvent, UserEnteredEvent, UserLeftEvent, WebhookEvent, WebhookHandler, WebhookRequest, authorize }; |
@@ -24,2 +24,80 @@ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }var __async = (__this, __arguments, generator) => { | ||
var _nodefetch = require('node-fetch'); var _nodefetch2 = _interopRequireDefault(_nodefetch); | ||
// src/webhooks.ts | ||
var _crypto = require('crypto'); var _crypto2 = _interopRequireDefault(_crypto); | ||
var _WebhookHandler = class { | ||
constructor(secret) { | ||
if (!secret) | ||
throw new Error("Secret is required"); | ||
if (typeof secret !== "string") | ||
throw new Error("Secret must be a string"); | ||
if (secret.startsWith(_WebhookHandler.secretPrefix) === false) | ||
throw new Error("Invalid secret, must start with whsec_"); | ||
const secretKey = secret.slice(_WebhookHandler.secretPrefix.length); | ||
this.secretBuffer = Buffer.from(secretKey, "base64"); | ||
} | ||
verifyRequest(request) { | ||
const { webhookId, timestamp, rawSignatures } = this.verifyHeaders( | ||
request.headers | ||
); | ||
this.verifyTimestamp(timestamp); | ||
const signature = this.sign(`${webhookId}.${timestamp}.${request.rawBody}`); | ||
const expectedSignatures = rawSignatures.split(" ").map((rawSignature) => { | ||
const [, parsedSignature] = rawSignature.split(","); | ||
return parsedSignature; | ||
}).filter(isNotUndefined); | ||
if (expectedSignatures.includes(signature) === false) | ||
throw new Error( | ||
`Invalid signature, expected one of ${expectedSignatures}, got ${signature}` | ||
); | ||
const event = JSON.parse(request.rawBody); | ||
this.verifyWebhookEventType(event); | ||
return event; | ||
} | ||
verifyHeaders(headers) { | ||
const sanitizedHeaders = {}; | ||
Object.keys(headers).forEach((key) => { | ||
sanitizedHeaders[key.toLowerCase()] = headers[key]; | ||
}); | ||
const webhookId = sanitizedHeaders["webhook-id"]; | ||
if (typeof webhookId !== "string") | ||
throw new Error("Invalid webhook-id header"); | ||
const timestamp = sanitizedHeaders["webhook-timestamp"]; | ||
if (typeof timestamp !== "string") | ||
throw new Error("Invalid webhook-timestamp header"); | ||
const rawSignatures = sanitizedHeaders["webhook-signature"]; | ||
if (typeof rawSignatures !== "string") | ||
throw new Error("Invalid webhook-signature header"); | ||
return { webhookId, timestamp, rawSignatures }; | ||
} | ||
sign(content) { | ||
return _crypto2.default.createHmac("sha256", this.secretBuffer).update(content).digest("base64"); | ||
} | ||
verifyTimestamp(timestampHeader) { | ||
const now = Math.floor(Date.now() / 1e3); | ||
const timestamp = parseInt(timestampHeader, 10); | ||
if (isNaN(timestamp)) { | ||
throw new Error("Invalid timestamp"); | ||
} | ||
if (timestamp < now - WEBHOOK_TOLERANCE_IN_SECONDS) { | ||
throw new Error("Timestamp too old"); | ||
} | ||
if (timestamp > now + WEBHOOK_TOLERANCE_IN_SECONDS) { | ||
throw new Error("Timestamp in the future"); | ||
} | ||
} | ||
verifyWebhookEventType(event) { | ||
if (event && event.type && (event.type === "storageUpdated" || event.type === "userEntered" || event.type === "userLeft")) | ||
return; | ||
throw new Error( | ||
"Unknown event type, please upgrade to a higher version of @liveblocks/node" | ||
); | ||
} | ||
}; | ||
var WebhookHandler = _WebhookHandler; | ||
WebhookHandler.secretPrefix = "whsec_"; | ||
var WEBHOOK_TOLERANCE_IN_SECONDS = 5 * 60; | ||
var isNotUndefined = (value) => value !== void 0; | ||
// src/index.ts | ||
function authorize(options) { | ||
@@ -78,2 +156,3 @@ return __async(this, null, function* () { | ||
exports.authorize = authorize; | ||
exports.WebhookHandler = WebhookHandler; exports.authorize = authorize; |
{ | ||
"name": "@liveblocks/node", | ||
"version": "0.19.9-beta3", | ||
"version": "0.19.9-beta4", | ||
"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.", | ||
@@ -23,3 +23,4 @@ "license": "Apache-2.0", | ||
"@liveblocks/jest-config": "*", | ||
"@types/node-fetch": "^2.5.8" | ||
"@types/node-fetch": "^2.5.8", | ||
"svix": "^0.75.0" | ||
}, | ||
@@ -26,0 +27,0 @@ "dependencies": { |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
13304
297
4