UNPKG

next-sanity

Version:
82 lines (81 loc) 3.84 kB
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