{"version":3,"file":"index.mjs","sources":["../src/errors.ts","../src/signature.ts","../src/middleware.ts"],"sourcesContent":["/**\n * Error types used on signature errors.\n * Includes `type` and `statusCode` properties.\n *\n * @public\n */\nexport type WebhookSignatureError = WebhookSignatureValueError | WebhookSignatureFormatError\n\n/**\n * Error thrown when the signature value does not match the expected value.\n *\n * @public\n */\nexport class WebhookSignatureValueError extends Error {\n  public type = 'WebhookSignatureValueError'\n  public statusCode = 401\n}\n\n/**\n * Error thrown when the signature format is invalid.\n * This can happen when the signature is not a string or is not in the format of `t=<timestamp>,v=<signature>`.\n * This error is also thrown when the timestamp is not a number or is not within the tolerance time.\n *\n * @public\n */\nexport class WebhookSignatureFormatError extends Error {\n  public type = 'WebhookSignatureFormatError'\n  public statusCode = 400\n}\n\n/**\n * Checks whether or not the given error is a signature error.\n *\n * @param error - The error to check.\n * @returns `true` if the error is a signature error, otherwise `false`.\n * @public\n */\nexport function isSignatureError(error: unknown): error is WebhookSignatureError {\n  return (\n    typeof error === 'object' &&\n    error !== null &&\n    'type' in error &&\n    ['WebhookSignatureValueError', 'WebhookSignatureFormatError'].includes(\n      (error as WebhookSignatureError).type,\n    )\n  )\n}\n","import {isSignatureError, WebhookSignatureFormatError, WebhookSignatureValueError} from './errors'\nimport type {ConnectLikeRequest, DecodedSignature} from './types'\n\n/**\n * We didn't send signed payloads prior to 2021 (2021-01-01T00:00:00.000Z)\n */\nconst MINIMUM_TIMESTAMP = 1609459200000\n\nconst SIGNATURE_HEADER_REGEX = /^t=(\\d+)[, ]+v1=([^, ]+)$/\n\n/**\n * The name of the header that contains the signature.\n *\n * @public\n */\nexport const SIGNATURE_HEADER_NAME = 'sanity-webhook-signature'\n\n/**\n * Asserts that the given signature is valid.\n * Throws an error if the signature is invalid.\n *\n * @param stringifiedPayload - The stringified payload to verify - should be straight from the request, not a re-encoded JSON string, as this in certain cases will yield mismatches due to inconsistent encoding.\n * @param signature - The signature to verify against\n * @param secret - The secret to use for verifying the signature\n * @public\n */\nexport async function assertValidSignature(\n  stringifiedPayload: string,\n  signature: string,\n  secret: string,\n): Promise<void> {\n  const {timestamp} = decodeSignatureHeader(signature)\n  const encoded = await encodeSignatureHeader(stringifiedPayload, timestamp, secret)\n  if (signature !== encoded) {\n    throw new WebhookSignatureValueError('Signature is invalid')\n  }\n}\n\n/**\n * Checks if the given signature is valid.\n *\n * @param stringifiedPayload - The stringified payload to verify - should be straight from the request, not a re-encoded JSON string, as this in certain cases will yield mismatches due to inconsistent encoding.\n * @param signature - The signature to verify against\n * @param secret - The secret to use for verifying the signature\n * @returns A promise that resolves to `true` if the signature is valid, `false` otherwise.\n * @public\n */\nexport async function isValidSignature(\n  stringifiedPayload: string,\n  signature: string,\n  secret: string,\n): Promise<boolean> {\n  try {\n    await assertValidSignature(stringifiedPayload, signature, secret)\n    return true\n  } catch (err) {\n    if (isSignatureError(err)) {\n      return false\n    }\n    throw err\n  }\n}\n\n/**\n * Asserts that the given request is valid.\n * Throws an error if the request is invalid.\n *\n * @param request - The Connect/Express-like request to verify\n * @param secret - The secret to use for verifying the signature\n * @public\n */\nexport async function assertValidRequest(\n  request: ConnectLikeRequest,\n  secret: string,\n): Promise<void> {\n  const signature = request.headers[SIGNATURE_HEADER_NAME]\n  if (Array.isArray(signature)) {\n    throw new WebhookSignatureFormatError('Multiple signature headers received')\n  }\n\n  if (typeof signature !== 'string') {\n    throw new WebhookSignatureValueError('Request contained no signature header')\n  }\n\n  if (typeof request.body === 'undefined') {\n    throw new WebhookSignatureFormatError('Request contained no parsed request body')\n  }\n\n  if (typeof request.body === 'string' || Buffer.isBuffer(request.body)) {\n    await assertValidSignature(request.body.toString('utf8'), signature, secret)\n  } else {\n    throw new Error(\n      '[@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.',\n    )\n  }\n}\n\n/**\n * Checks if the given request is valid.\n *\n * @param request - The Connect/Express-like request to verify\n * @param secret - The secret to use for verifying the signature\n * @returns Promise that resolves to `true` if the request is valid, `false` otherwise.\n * @public\n */\nexport async function isValidRequest(\n  request: ConnectLikeRequest,\n  secret: string,\n): Promise<boolean> {\n  try {\n    await assertValidRequest(request, secret)\n    return true\n  } catch (err) {\n    if (isSignatureError(err)) {\n      return false\n    }\n    throw err\n  }\n}\n\n/**\n * Encodes a signature header for the given payload and timestamp.\n *\n * @param stringifiedPayload - The stringified payload to verify - should be straight from the request, not a re-encoded JSON string, as this in certain cases will yield mismatches due to inconsistent encoding.\n * @param timestamp - The timestamp to use for the signature\n * @param secret - The secret to use for verifying the signature\n * @returns A promise that resolves to the encoded signature header\n * @public\n */\nexport async function encodeSignatureHeader(\n  stringifiedPayload: string,\n  timestamp: number,\n  secret: string,\n): Promise<string> {\n  const signature = await createHS256Signature(stringifiedPayload, timestamp, secret)\n  return `t=${timestamp},v1=${signature}`\n}\n\n/**\n * Decode a signature header into a timestamp and hashed payload.\n *\n * @param signaturePayload - The signature header to decode\n * @returns An object with the decoded timestamp and hashed payload\n * @public\n */\nexport function decodeSignatureHeader(signaturePayload: string): DecodedSignature {\n  if (!signaturePayload) {\n    throw new WebhookSignatureFormatError('Missing or empty signature header')\n  }\n\n  const [, timestamp, hashedPayload] = signaturePayload.trim().match(SIGNATURE_HEADER_REGEX) || []\n  if (!timestamp || !hashedPayload) {\n    throw new WebhookSignatureFormatError('Invalid signature payload format')\n  }\n\n  return {\n    timestamp: parseInt(timestamp, 10),\n    hashedPayload,\n  }\n}\n\n/**\n * Creates a HS256 signature for the given payload and timestamp.\n *\n * @param stringifiedPayload - The stringified payload to verify - should be straight from the request, not a re-encoded JSON string, as this in certain cases will yield mismatches due to inconsistent encoding.\n * @param timestamp - The timestamp to use for the signature\n * @param secret - The secret to use for verifying the signature\n * @returns A promise that resolves to the encoded signature\n * @internal\n */\nasync function createHS256Signature(\n  stringifiedPayload: string,\n  timestamp: number,\n  secret: string,\n): Promise<string> {\n  if (typeof crypto === 'undefined') {\n    throw new TypeError(\n      '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.',\n    )\n  }\n  if (!secret || typeof secret !== 'string') {\n    throw new WebhookSignatureFormatError('Invalid secret provided')\n  }\n\n  if (!stringifiedPayload) {\n    throw new WebhookSignatureFormatError('Can not create signature for empty payload')\n  }\n\n  if (typeof stringifiedPayload !== 'string') {\n    throw new WebhookSignatureFormatError('Payload must be a JSON-encoded string')\n  }\n\n  if (typeof timestamp !== 'number' || isNaN(timestamp) || timestamp < MINIMUM_TIMESTAMP) {\n    throw new WebhookSignatureFormatError(\n      'Invalid signature timestamp, must be a unix timestamp with millisecond precision',\n    )\n  }\n\n  const enc = new TextEncoder()\n  const key = await crypto.subtle.importKey(\n    'raw',\n    enc.encode(secret),\n    {name: 'HMAC', hash: 'SHA-256'},\n    false,\n    ['sign'],\n  )\n  const signaturePayload = `${timestamp}.${stringifiedPayload}`\n  const signature = await crypto.subtle.sign('HMAC', key, enc.encode(signaturePayload))\n\n  // Encode as base64url\n  const signatureArray = Array.from(new Uint8Array(signature))\n  return btoa(String.fromCharCode.apply(null, signatureArray))\n    .replace(/\\+/g, '-') // Replace '+' with '-'\n    .replace(/\\//g, '_') // Replace '/' with '_'\n    .replace(/=+$/, '') // Remove padding\n}\n","import type {RequestHandler} from 'express'\n\nimport {isSignatureError} from './errors'\nimport {assertValidRequest} from './signature'\n\n/**\n * Options for the `requireSignedRequest` middleware\n *\n * @public\n */\nexport interface SignatureMiddlewareOptions {\n  /**\n   * The secret to use for verifying the signature\n   */\n  secret: string\n\n  /**\n   * Whether or not to parse the request body as JSON on success (assigns it to `request.body`).\n   * Default: `true`\n   */\n  parseBody?: boolean\n\n  /**\n   * Whether or not to respond with an error when the signature is invalid.\n   * If `false`, it will call the `next` function with the error instead.\n   * Default: `true`\n   */\n  respondOnError?: boolean\n}\n\n/**\n * Express/Connect style middleware that verifies the signature of a request.\n * Should be added _after_ a body parser that parses the request body to _text_, not parsed JSON.\n *\n * @example\n * ```ts\n * import express from 'express'\n * import bodyParser from 'body-parser'\n * import {requireSignedRequest} from '@sanity/webhook'\n *\n * express()\n *   .use(bodyParser.text({type: 'application/json'}))\n *   .post(\n *     '/hook',\n *     requireSignedRequest({secret: process.env.MY_WEBHOOK_SECRET, parseBody: true}),\n *     function myRequestHandler(req, res) {\n *       // Note that `req.body` is now a parsed version, set `parseBody` to `false`\n *       // if you want the raw text version of the request body\n *     },\n *   )\n *   .listen(1337)\n * ```\n *\n * @param options - Options for the middleware\n * @returns A middleware function\n * @public\n */\nexport function requireSignedRequest(options: SignatureMiddlewareOptions): RequestHandler {\n  const parseBody = typeof options.parseBody === 'undefined' ? true : options.parseBody\n  const respondOnError =\n    typeof options.respondOnError === 'undefined' ? true : options.respondOnError\n\n  return async function ensureSignedRequest(request, response, next) {\n    try {\n      await assertValidRequest(request, options.secret)\n      if (parseBody && typeof request.body === 'string') {\n        request.body = JSON.parse(request.body)\n      }\n      next()\n    } catch (err) {\n      if (!respondOnError || !isSignatureError(err)) {\n        next(err)\n        return\n      }\n\n      response.status(err.statusCode).json({message: err.message})\n    }\n  }\n}\n"],"names":[],"mappings":"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"}