@sanity/webhook
Version:
Toolkit for dealing with GROQ-powered webhooks delivered by Sanity.io
157 lines (105 loc) • 6.12 kB
Markdown
# /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 /webhook
```
## Usage
```js
// ESM / TypeScript
import {isValidSignature} from '/webhook'
// CommonJS
const {isValidSignature} = require('/webhook')
```
### Usage with Express.js (or similar)
```ts
import express from 'express'
import bodyParser from 'body-parser'
import {requireSignedRequest} from '/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 '/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.