UNPKG

@sanity/webhook

Version:

Toolkit for dealing with GROQ-powered webhooks delivered by Sanity.io

120 lines (119 loc) 5.58 kB
"use strict"; 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