standardwebhooks
Version:
Standard Webhooks for TypeScript
106 lines • 4.28 kB
JavaScript
;
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