106 lines
4.2 KiB
JavaScript
106 lines
4.2 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.Webhook = exports.WebhookVerificationError = void 0;
|
|
const timing_safe_equal_1 = require("./timing_safe_equal");
|
|
const base64 = require("@stablelib/base64");
|
|
const sha256 = require("fast-sha256");
|
|
const WEBHOOK_TOLERANCE_IN_SECONDS = 5 * 60;
|
|
class ExtendableError extends Error {
|
|
constructor(message) {
|
|
super(message);
|
|
Object.setPrototypeOf(this, ExtendableError.prototype);
|
|
this.name = "ExtendableError";
|
|
this.stack = new Error(message).stack;
|
|
}
|
|
}
|
|
class WebhookVerificationError extends ExtendableError {
|
|
constructor(message) {
|
|
super(message);
|
|
Object.setPrototypeOf(this, WebhookVerificationError.prototype);
|
|
this.name = "WebhookVerificationError";
|
|
}
|
|
}
|
|
exports.WebhookVerificationError = WebhookVerificationError;
|
|
class Webhook {
|
|
constructor(secret, options) {
|
|
if (!secret) {
|
|
throw new Error("Secret can't be empty.");
|
|
}
|
|
if ((options === null || options === void 0 ? void 0 : options.format) === "raw") {
|
|
if (secret instanceof Uint8Array) {
|
|
this.key = secret;
|
|
}
|
|
else {
|
|
this.key = Uint8Array.from(secret, (c) => c.charCodeAt(0));
|
|
}
|
|
}
|
|
else {
|
|
if (typeof secret !== "string") {
|
|
throw new Error("Expected secret to be of type string");
|
|
}
|
|
if (secret.startsWith(Webhook.prefix)) {
|
|
secret = secret.substring(Webhook.prefix.length);
|
|
}
|
|
this.key = base64.decode(secret);
|
|
}
|
|
}
|
|
verify(payload, headers_) {
|
|
const headers = {};
|
|
for (const key of Object.keys(headers_)) {
|
|
headers[key.toLowerCase()] = headers_[key];
|
|
}
|
|
const msgId = headers["webhook-id"];
|
|
const msgSignature = headers["webhook-signature"];
|
|
const msgTimestamp = headers["webhook-timestamp"];
|
|
if (!msgSignature || !msgId || !msgTimestamp) {
|
|
throw new WebhookVerificationError("Missing required headers");
|
|
}
|
|
const timestamp = this.verifyTimestamp(msgTimestamp);
|
|
const computedSignature = this.sign(msgId, timestamp, payload);
|
|
const expectedSignature = computedSignature.split(",")[1];
|
|
const passedSignatures = msgSignature.split(" ");
|
|
const encoder = new globalThis.TextEncoder();
|
|
for (const versionedSignature of passedSignatures) {
|
|
const [version, signature] = versionedSignature.split(",");
|
|
if (version !== "v1") {
|
|
continue;
|
|
}
|
|
if ((0, timing_safe_equal_1.timingSafeEqual)(encoder.encode(signature), encoder.encode(expectedSignature))) {
|
|
return JSON.parse(payload.toString());
|
|
}
|
|
}
|
|
throw new WebhookVerificationError("No matching signature found");
|
|
}
|
|
sign(msgId, timestamp, payload) {
|
|
if (typeof payload === "string") {
|
|
}
|
|
else if (payload.constructor.name === "Buffer") {
|
|
payload = payload.toString();
|
|
}
|
|
else {
|
|
throw new Error("Expected payload to be of type string or Buffer.");
|
|
}
|
|
const encoder = new TextEncoder();
|
|
const timestampNumber = Math.floor(timestamp.getTime() / 1000);
|
|
const toSign = encoder.encode(`${msgId}.${timestampNumber}.${payload}`);
|
|
const expectedSignature = base64.encode(sha256.hmac(this.key, toSign));
|
|
return `v1,${expectedSignature}`;
|
|
}
|
|
verifyTimestamp(timestampHeader) {
|
|
const now = Math.floor(Date.now() / 1000);
|
|
const timestamp = parseInt(timestampHeader, 10);
|
|
if (isNaN(timestamp)) {
|
|
throw new WebhookVerificationError("Invalid Signature Headers");
|
|
}
|
|
if (now - timestamp > WEBHOOK_TOLERANCE_IN_SECONDS) {
|
|
throw new WebhookVerificationError("Message timestamp too old");
|
|
}
|
|
if (timestamp > now + WEBHOOK_TOLERANCE_IN_SECONDS) {
|
|
throw new WebhookVerificationError("Message timestamp too new");
|
|
}
|
|
return new Date(timestamp * 1000);
|
|
}
|
|
}
|
|
exports.Webhook = Webhook;
|
|
Webhook.prefix = "whsec_";
|
|
//# sourceMappingURL=index.js.map
|