Détail du package

@sanity/webhook

sanity-io444.2kMIT4.0.4

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

webhook, webhooks, sanity-io, sanity

readme

@sanity/webhook

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

Installing

$ npm install @sanity/webhook

Usage

// ESM / TypeScript
import {isValidSignature} from '@sanity/webhook'

// CommonJS
const {isValidSignature} = require('@sanity/webhook')

Usage with Express.js (or similar)

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

// 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 middleware registered for the route (with {type: 'application/json'}).

Functions

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, 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:

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.

changelog

📓 Changelog

All notable changes to this project will be documented in this file. See Conventional Commits for commit guidelines.

4.0.4 (2024-04-11)

Bug Fixes

  • deps: update dependency @sanity/pkg-utils to v6 (#75) (fcbae6b)

4.0.3 (2024-03-18)

Bug Fixes

  • deps: update dependency @sanity/pkg-utils to v5 (#68) (ee9da2a)

4.0.2 (2024-02-26)

Bug Fixes

  • rethrow errors that are not signature errors (2cea92f)
  • throw when Web Crypto isn't available (3dd652f)

4.0.1 (2024-01-25)

Bug Fixes

  • update engines.node to v20 (8f40440), closes #44

4.0.0 (2023-11-27)

⚠ BREAKING CHANGES

  • Replace the Node.js crypto API with the Web Crypto API, enabling usage in more environments. All assertion/checking functions are now async, eg return Promises instead of straight booleans.
  • Only Node.js version 18 and higher is now supported.

  • feat: add tsdoc for all exported members

  • test: only test on lts node.js engines

Features

3.0.1 (2023-08-19)

Bug Fixes

  • improve TS export of definitions (77770e7)

3.0.0 (2023-08-18)

Features

2.0.0

BREAKING

  • BREAKING: The requireSignedRequest(), assertValidRequest() and isValidRequest() methods now require a body in string/buffer format, and will throw an error if it is already parsed. This is due to potential signature mismatches when re-encoding JSON. See the migration docs for more information.

1.1.0

Changed

  • Generating signatures should now be done based on the raw body of the request (as text/buffer) instead of re-encoding the body to JSON and comparing it. This fixes a few issues where the JSON encoding in v8 would differ from the JSON encoding of the server, leading to signature mismatches. A warning is now emitted when comparing a parsed body - see the migration docs for more information.
  • The requireSignedRequest() method now takes an additional option - parseBody. By default, it is set to true, and will parse the incoming JSON request and assign it to request.body. If set to false, the body is left untouched and has to be parsed by the user.