next-sanity
Version:
Sanity.io toolkit for Next.js
82 lines (81 loc) • 3.84 kB
JavaScript
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 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(/=+$/, "");
}
async function parseBody(req, secret, waitForContentLakeEventualConsistency = !0) {
const signature = req.headers.get(SIGNATURE_HEADER_NAME);
if (!signature)
return console.error("Missing signature header"), { body: null, isValidSignature: null };
const body = await req.text(), validSignature = secret ? await isValidSignature(body, signature, secret.trim()) : null;
return validSignature !== !1 && waitForContentLakeEventualConsistency && await new Promise((resolve) => setTimeout(resolve, 3e3)), {
body: body.trim() ? JSON.parse(body) : null,
isValidSignature: validSignature
};
}
export {
parseBody
};
//# sourceMappingURL=webhook.js.map