Dark Mode

Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Zero-dependency webhook signature verification for Stripe, GitHub, Shopify, and more.

License

Notifications You must be signed in to change notification settings

HookInbox/hookinbox-verify

Repository files navigation

@hookinbox/verify

Zero-dependency webhook signature verification for Stripe, GitHub, Shopify, and more.

Most platform SDKs validate signatures but throw generic errors like:

"Invalid signature"

This library returns structured failure reasons (timestamp too old, body modified, wrong algorithm, etc.) to make debugging easier.

const result = verifyStripe(...)

if (!result.ok) {
console.log(result.kind)
}
// "timestamp_too_old"
// "signature_mismatch"
// "missing_header"

Why @hookinbox/verify?

Why use this instead of platform SDKs?

  • Zero dependencies - No bloat, just verification
  • Detailed diagnostics - Know exactly why verification failed
  • Type-safe - Full TypeScript support
  • Multi-platform - One package for all platforms
  • Well-tested - 80%+ code coverage
  • Lightweight - < 5KB minified

Common issues we help debug:

  • Timestamp too old/future (with exact age)
  • Wrong secret key used
  • Body modified by middleware
  • SHA-1 vs SHA-256 confusion
  • Base64 encoding issues

Installation

npm install @hookinbox/verify

Usage

Stripe

import { verifyStripe } from '@hookinbox/verify';

const result = verifyStripe({
rawBodyBytes: rawBody, // string | Buffer | Uint8Array
stripeSignatureHeader: req.headers['stripe-signature'],
signingSecret: 'whsec_...', // must start with whsec_
toleranceSec: 300, // optional, default 300
});

if (result.ok) {
console.log(' Valid signature');
console.log('Timestamp:', result.timestamp);
console.log('Age:', result.ageSec, 'seconds');
} else {
console.error(' Error:', result.kind);

if (result.kind === 'timestamp_too_old') {
console.error(`Timestamp is ${result.ageSec}s old (max: ${result.toleranceSec}s)`);
}

if (result.kind === 'signature_mismatch') {
console.error('Expected:', result.expectedHex);
console.error('Received:', result.receivedV1);
}
}

GitHub

import { verifyGitHub } from '@hookinbox/verify';

const result = verifyGitHub({
rawBodyBytes: rawBody,
signature256: req.headers['x-hub-signature-256'], // preferred
signature: req.headers['x-hub-signature'], // fallback (sha1)
secret: 'your-secret',
});

if (result.ok) {
console.log(' Valid signature');
console.log('Algorithm:', result.algorithm);
} else {
console.error(' Error:', result.kind);
}

Shopify

import { verifyShopify } from '@hookinbox/verify';

const result = verifyShopify({
rawBodyBytes: rawBody,
hmacHeader: req.headers['x-shopify-hmac-sha256'],
secret: 'your-secret',
});

if (result.ok) {
console.log(' Valid signature');
} else {
console.error(' Error:', result.kind);
}

Utilities

The package also exports timing-safe comparison utilities:

Timing-Safe String Comparison

import { timingSafeEqual } from '@hookinbox/verify';

const isValid = timingSafeEqual(expectedSignature, receivedSignature);

Timing-Safe Byte Comparison

import { constantTimeEqual } from '@hookinbox/verify';

const a = new Uint8Array([1, 2, 3]);
const b = new Uint8Array([1, 2, 3]);
const isEqual = constantTimeEqual(a, b); // true

Hex Utilities

import { hexToBytes, bytesToHex } from '@hookinbox/verify';

// Convert hex string to bytes
const bytes = hexToBytes('deadbeef');
// Uint8Array([0xDE, 0xAD, 0xBE, 0xEF])

// Convert bytes to hex string
const hex = bytesToHex(bytes);
// 'deadbeef'

Security

All signature comparisons use constant-time comparison to prevent timing attacks. This means the comparison time doesn't leak information about how many characters match.

Why this matters:

  • Standard === comparison short-circuits on first mismatch
  • Attackers can measure response times to guess secrets
  • Our timingSafeEqual always compares all bytes

Features

  • Zero dependencies
  • TypeScript support
  • Detailed error messages
  • Timing-safe comparisons
  • Works in Node.js, Edge, Cloudflare Workers

Development

Running Tests

# Run all tests
npm test

# Run in watch mode
npm run test:watch

# Check coverage
npm run test:coverage

Building

npm run build

Contributing

We welcome contributions! See CONTRIBUTING.md for guidelines.

Looking for ideas? Check out platforms we'd love to support:

  • Clerk, Resend, Discord, Twilio, SendGrid
  • Lemon Squeezy, Paddle, Chargebee
  • Vercel, Railway, Linear

Testing

Comprehensive test suite with 80%+ coverage:

  • Valid signature verification
  • Invalid signature detection
  • Edge cases (empty body, special chars, etc.)
  • Timing-safe comparisons
  • Platform-specific rules

License

MIT (c) HookInbox

Related


Maintained by the HookInbox project.

About

Zero-dependency webhook signature verification for Stripe, GitHub, Shopify, and more.

Topics

Resources

Readme

License

MIT license

Code of conduct

Code of conduct

Contributing

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

Contributors