UNPKG

@sanity/webhook

Version:

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

157 lines (105 loc) 6.12 kB
# @sanity/webhook Toolkit for dealing with [GROQ-powered webhooks](https://www.sanity.io/docs/webhooks) delivered by [Sanity.io](https://www.sanity.io/). ## Installing ```sh $ npm install @sanity/webhook ``` ## Usage ```js // ESM / TypeScript import {isValidSignature} from '@sanity/webhook' // CommonJS const {isValidSignature} = require('@sanity/webhook') ``` ### Usage with Express.js (or similar) ```ts import express from 'express' import bodyParser from 'body-parser' import {requireSignedRequest} from '@sanity/webhook' express() .use(bodyParser.text({type: 'application/json'})) .post( '/hook', requireSignedRequest({secret: process.env.MY_WEBHOOK_SECRET, parseBody: true}), function myRequestHandler(req, res) { // Note that `req.body` is now a parsed version, set `parseBody` to `false` // if you want the raw text version of the request body }, ) .listen(1337) ``` ### Usage with Next.js ```ts // pages/api/hook.js import {isValidSignature, SIGNATURE_HEADER_NAME} from '@sanity/webhook' const secret = process.env.MY_WEBHOOK_SECRET export default async function handler(req, res) { const signature = req.headers[SIGNATURE_HEADER_NAME] const body = await readBody(req) // Read the body into a string if (!(await isValidSignature(body, signature, secret))) { res.status(401).json({success: false, message: 'Invalid signature'}) return } const jsonBody = JSON.parse(body) doSomeMagicWithPayload(jsonBody) res.json({success: true}) } // Next.js will by default parse the body, which can lead to invalid signatures export const config = { api: { bodyParser: false, }, } async function readBody(readable) { const chunks = [] for await (const chunk of readable) { chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk) } return Buffer.concat(chunks).toString('utf8') } ``` ## Documentation Note that the functions `requireSignedRequest`, `assertValidRequest` and `isValidRequest` all require that the request object should have a text `body` property. E.g. if you're using Express.js or Connect, make sure you have a [Text body-parser](https://github.com/expressjs/body-parser#bodyparsertextoptions) middleware registered for the route (with `{type: 'application/json'}`). ### Functions - [requireSignedRequest](README.md#requiresignedrequest) - [assertValidSignature](README.md#assertvalidsignature) - [isValidSignature](README.md#isvalidsignature) - [assertValidRequest](README.md#assertvalidrequest) - [isValidRequest](README.md#isvalidrequest) ### requireSignedRequest **requireSignedRequest**(`options`: _SignatureMiddlewareOptions_): _RequestHandler_ Returns an Express.js/Connect-compatible middleware which validates incoming requests to ensure they are correctly signed. This middleware will also parse the request body into JSON: The next handler will have `req.body` parsed into a plain JavaScript object. **Options**: - `secret` (_string_, **required**) - the secret to use for validating the request. - `parseBody` (_boolean_, optional, default: _true_) - whether or not to parse the body as JSON and set `request.body` to the parsed value. - `respondOnError` (_boolean_, optional, default: _true_) - whether or not the request should automatically respond to the request with an error, or (if `false`) pass the error on to the next registered error middleware. ### assertValidSignature **assertValidSignature**(`stringifiedPayload`: _string_, `signature`: _string_, `secret`: _string_): _Promise<void>_ Asserts that the given payload and signature matches and is valid, given the specified secret. If it is not valid, the function will throw an error with a descriptive `message` property. ### isValidSignature **isValidSignature**(`stringifiedPayload`: _string_, `signature`: _string_, `secret`: _string_): _Promise<boolean>_ Returns whether or not the given payload and signature matches and is valid, given the specified secret. On invalid, missing or mishaped signatures, this function will return `false` instead of throwing. ### assertValidRequest **assertValidRequest**(`request`: _ConnectLikeRequest_, `secret`: _string_): _Promise<void>_ Asserts that the given request has a request body which matches the received signature, and that the signature is valid given the specified secret. If it is not valid, the function will throw an error with a descriptive `message` property. ### isValidRequest **isValidRequest**(`request`: _ConnectLikeRequest_, `secret`: _string_): _Promise<boolean>_ Returns whether or not the given request has a request body which matches the received signature, and that the signature is valid given the specified secret. ## Migration ### From version 3.x to 4.x In versions 3.x and below, this library would syncronously assert/return boolean values. From v4.0.0 and up, we now return promises instead. This allows using the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API), available in a broader range of environments. v4 also requires Node.js 18 or higher. ### From parsed to unparsed body In versions 1.0.2 and below, this library would accept a parsed request body as the input for `requireSignedRequest()`, `assertValidRequest()` and `isValidRequest()`. These methods would internally call `JSON.stringify()` on the body in these cases, then compare it to the signature. This works in _most_ cases, but because of slightly different JSON-encoding behavior between environments, it could sometimes lead to a mismatch in signatures. To prevent these situations from occuring, we now _highly_ recommend that you aquire the raw request body when using these methods. See the usage examples further up for how to do this: - [Express.js or similar](#usage-with-expressjs-or-similar) - [Next.js](#usage-with-nextjs) Differences in behavior: - In version 2.0.0 and above, an error will be thrown if the request body is not a string or a buffer. - In version 1.1.0, a warning will be printed to the console if the request body is not a string or buffer. ## License MIT-licensed. See LICENSE.