Git Product home page Git Product logo

webhook-toolkit's Introduction

@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

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

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

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

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.

webhook-toolkit's People

Contributors

dependabot[bot] avatar ecospark[bot] avatar fernandolucchesi avatar judofyr avatar renovate-bot avatar renovate[bot] avatar rexxars avatar semantic-release-bot avatar sjelfull avatar stipsan avatar tbassetto avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

webhook-toolkit's Issues

validation for certain documents _always_ fails

The validation for certain documents always fails. I have no Idea what causes the issue but it makes the validation pointless. The documents that it fails on look exactly like documents that pass the validation. Any pointers would be appreciated as I have to disable the validation until this works as expected

isValidRequest with AWS Lambda handler

Hi, this might not be an issue. I just can find how to validate the secret in my AWS Lambda handler.

What I have tried is:

const { isValidRequest } = require("@sanity/webhook");

exports.handler = async (event, context) => {
  try {
    if (
      !isValidRequest(JSON.parse(event.body), process.env.SANITY_HOOK_SECRET)
    ) {
      console.log("UNAUTHORIZED");
      return ApiResponse.Unauthorized("Unauthorized");
    }
}

and also

exports.handler = async (event, context) => {
  try {
    if (
      !isValidRequest(event, process.env.SANITY_HOOK_SECRET)
    ) {
      console.log("UNAUTHORIZED");
      return ApiResponse.Unauthorized("Unauthorized");
    }
}

I get Unauthorized within these 2 approaches. As I can see in event.body I get value _rev: 'something'.
In event.headers : 'sanity-webhook-signature': 't=1634asdasdasd'

Suggestions?

Dependency Dashboard

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.


Using a curated preset maintained by


Sanity: The Composable Content Cloud

Pending Approval

These branches will be created by Renovate only once you click their checkbox below.

  • chore(deps): update non-major (@sanity/pkg-utils, @typescript-eslint/eslint-plugin, @typescript-eslint/parser, peter-evans/create-pull-request, prettier-plugin-packagejson, vitest)
  • chore(deps): update dependency supertest to v7
  • chore(deps): lock file maintenance
  • ๐Ÿ” Create all pending approval PRs at once ๐Ÿ”

Detected dependencies

github-actions
.github/workflows/browserslist.yml
  • actions/checkout v4
  • actions/setup-node v4
  • actions/create-github-app-token v1
  • peter-evans/create-pull-request v6@70a41aba780001da0a30141984ae2a0c95d8704e
.github/workflows/lock.yml
  • dessant/lock-threads v5@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771
.github/workflows/main.yml
  • actions/checkout v4
  • actions/setup-node v4
  • actions/checkout v4
  • actions/setup-node v4
  • actions/checkout v4
  • actions/setup-node v4
npm
package.json
  • @sanity/pkg-utils ^6.4.1
  • @sanity/semantic-release-preset ^4.1.7
  • @types/express ^4.17.21
  • @types/supertest ^6.0.2
  • @typescript-eslint/eslint-plugin ^7.6.0
  • @typescript-eslint/parser ^7.6.0
  • body-parser ^1.20.2
  • eslint ^8.57.0
  • eslint-config-prettier ^9.1.0
  • eslint-config-sanity ^7.1.2
  • eslint-plugin-prettier ^5.1.3
  • express ^4.19.2
  • ls-engines ^0.9.1
  • prettier ^3.2.5
  • prettier-plugin-packagejson ^2.4.14
  • semantic-release ^23.0.8
  • supertest ^6.3.4
  • typescript ^5.4.5
  • vitest ^1.4.0
  • node >=20.0.0

  • Check this box to trigger a request for Renovate to run again on this repository

v4 is not compatible with node 18

Node 18 doesn't have Web Crypto API, so the v4 is not compatible with node 18 even if in the package.json the node version is >= 18

"engines": {
    "node": ">=18.0.0"
  }

To Reproduce

set node version to 18 and run test suite

Incompatible with edge environment due to "crypto" import

Describe the bug

When building an API endpoint that uses import {isValidSignature} from '@sanity/webhook' for an edge worker environment, the build fails with e.g.

./node_modules/@sanity/webhook/dist/index.mjs:1:19: ERROR: Could not resolve "crypto"

Looks like this is because this package imports the crypto node module, which unfortunately is not available in an edge environment. As a result, we cannot deploy endpoints using @sanity/webhook to the edge, and have to deploy to a node environment instead.

isValidRequest randomly failing

The isValidRequest is randomly failing and returning a false value despite our secret never changing nor our site code, only sanity content. This issue has arisen out of nowhere and even after making a new API secret, still failing. We have been successfully using this method for a couple of months prior.

readBody hangs forever

Hi, I don't know how to replicate the issue I'm facing but the application gets stuck on this line:

for await (const chunk of readable) { ...

The problem is that I don't have any feedback, it just hangs forever.

It's now happening on localhost.

Unfortunately I don't have so much knowledge about Nodejs buffer.

I've run

console.log(readable)

and you can find the result here (without sensitive data).

Does it ring any bell? How can I debug it?

Would it make sense to add a timeout? To be honest I don't know what could be the correct approach to implement it as I'm dealing with a for await ... of and not a simple Promise.

Using isValidSignature with body as an object

I'm attempting to use this to verify a signature but I'm getting the request body already parsed as an object, and attempting to run this as-is with a JSON stringified body object isn't working. How can I use this if I don't have direct access to the request object?

Simplify library

Is your feature request related to a problem? Please describe.
Looking at the code, I was surprised how complex the implementation is.

Describe the solution youโ€™d like
A less-than-10-lines solution for this simple problem.

Describe alternatives youโ€™ve considered

import {createHmac} from 'node:crypto'

export default (hmacHeader: string, secret: string, body: string) => {
  const [, timestamp, signature] =
    hmacHeader?.match(/^t=(\d+),v1=([^,]+)/) ?? []

  const timeSinceSignature = Date.now() - Number(timestamp)
  if (timeSinceSignature > 60_000 || timeSinceSignature < 0)
    throw 'HMAC signature is outdated'

  if (
    signature !==
    createHmac('SHA-256', secret)
      .update(`${timestamp}.${body}`)
      .digest('base64url')
  )
    throw 'HMAC signature verification failed'
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.