@sanity/webhook
Version:
Toolkit for dealing with GROQ-powered webhooks delivered by Sanity.io
120 lines (119 loc) • 5.58 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: !0 });
class WebhookSignatureValueError extends Error {
type = "WebhookSignatureValueError";
statusCode = 401;
}
class WebhookSignatureFormatError extends Error {
type = "WebhookSignatureFormatError";
statusCode = 400;
}
function isSignatureError(error) {
return typeof error == "object" && error !== null && "type" in error && ["WebhookSignatureValueError", "WebhookSignatureFormatError"].includes(
error.type
);
}
const MINIMUM_TIMESTAMP = 16094592e5, SIGNATURE_HEADER_REGEX = /^t=(\d+)[, ]+v1=([^, ]+)$/, SIGNATURE_HEADER_NAME = "sanity-webhook-signature";
async function assertValidSignature(stringifiedPayload, signature, secret) {
const { timestamp } = decodeSignatureHeader(signature), encoded = await encodeSignatureHeader(stringifiedPayload, timestamp, secret);
if (signature !== encoded)
throw new WebhookSignatureValueError("Signature is invalid");
}
async function isValidSignature(stringifiedPayload, signature, secret) {
try {
return await assertValidSignature(stringifiedPayload, signature, secret), !0;
} catch (err) {
if (isSignatureError(err))
return !1;
throw err;
}
}
async function assertValidRequest(request, secret) {
const signature = request.headers[SIGNATURE_HEADER_NAME];
if (Array.isArray(signature))
throw new WebhookSignatureFormatError("Multiple signature headers received");
if (typeof signature != "string")
throw new WebhookSignatureValueError("Request contained no signature header");
if (typeof request.body > "u")
throw new WebhookSignatureFormatError("Request contained no parsed request body");
if (typeof request.body == "string" || Buffer.isBuffer(request.body))
await assertValidSignature(request.body.toString("utf8"), signature, secret);
else
throw new Error(
"[@sanity/webhook] `request.body` was not a string/buffer - this can lead to invalid signatures. See the [migration docs](https://github.com/sanity-io/webhook-toolkit#from-parsed-to-unparsed-body) for details on how to fix this."
);
}
async function isValidRequest(request, secret) {
try {
return await assertValidRequest(request, secret), !0;
} catch (err) {
if (isSignatureError(err))
return !1;
throw err;
}
}
async function encodeSignatureHeader(stringifiedPayload, timestamp, secret) {
const signature = await createHS256Signature(stringifiedPayload, timestamp, secret);
return `t=${timestamp},v1=${signature}`;
}
function decodeSignatureHeader(signaturePayload) {
if (!signaturePayload)
throw new WebhookSignatureFormatError("Missing or empty signature header");
const [, timestamp, hashedPayload] = signaturePayload.trim().match(SIGNATURE_HEADER_REGEX) || [];
if (!timestamp || !hashedPayload)
throw new WebhookSignatureFormatError("Invalid signature payload format");
return {
timestamp: parseInt(timestamp, 10),
hashedPayload
};
}
async function createHS256Signature(stringifiedPayload, timestamp, secret) {
if (typeof crypto > "u")
throw new TypeError(
"The Web Crypto API is not available in this environment, either polyfill `globalThis.crypto` or downgrade to `@sanity/webhook@3` which uses the Node.js `crypto` module."
);
if (!secret || typeof secret != "string")
throw new WebhookSignatureFormatError("Invalid secret provided");
if (!stringifiedPayload)
throw new WebhookSignatureFormatError("Can not create signature for empty payload");
if (typeof stringifiedPayload != "string")
throw new WebhookSignatureFormatError("Payload must be a JSON-encoded string");
if (typeof timestamp != "number" || isNaN(timestamp) || timestamp < MINIMUM_TIMESTAMP)
throw new WebhookSignatureFormatError(
"Invalid signature timestamp, must be a unix timestamp with millisecond precision"
);
const enc = new TextEncoder(), key = await crypto.subtle.importKey(
"raw",
enc.encode(secret),
{ name: "HMAC", hash: "SHA-256" },
!1,
["sign"]
), signaturePayload = `${timestamp}.${stringifiedPayload}`, signature = await crypto.subtle.sign("HMAC", key, enc.encode(signaturePayload)), signatureArray = Array.from(new Uint8Array(signature));
return btoa(String.fromCharCode.apply(null, signatureArray)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
function requireSignedRequest(options) {
const parseBody = typeof options.parseBody > "u" ? !0 : options.parseBody, respondOnError = typeof options.respondOnError > "u" ? !0 : options.respondOnError;
return async function(request, response, next) {
try {
await assertValidRequest(request, options.secret), parseBody && typeof request.body == "string" && (request.body = JSON.parse(request.body)), next();
} catch (err) {
if (!respondOnError || !isSignatureError(err)) {
next(err);
return;
}
response.status(err.statusCode).json({ message: err.message });
}
};
}
exports.SIGNATURE_HEADER_NAME = SIGNATURE_HEADER_NAME;
exports.WebhookSignatureFormatError = WebhookSignatureFormatError;
exports.WebhookSignatureValueError = WebhookSignatureValueError;
exports.assertValidRequest = assertValidRequest;
exports.assertValidSignature = assertValidSignature;
exports.decodeSignatureHeader = decodeSignatureHeader;
exports.encodeSignatureHeader = encodeSignatureHeader;
exports.isSignatureError = isSignatureError;
exports.isValidRequest = isValidRequest;
exports.isValidSignature = isValidSignature;
exports.requireSignedRequest = requireSignedRequest;
//# sourceMappingURL=index.js.map