Files
turbostarter/packages/billing/src/providers/lemon-squeezy/webhook/signing.ts
Alejandro Gutiérrez 3527e732d4 feat: turbostarter boilerplate
Production-ready Next.js boilerplate with:
- Runtime env validation (fail-fast on missing vars)
- Feature-gated config (S3, Stripe, email, OAuth)
- Docker + Coolify deployment pipeline
- PostgreSQL + pgvector, MinIO S3, Better Auth
- TypeScript strict mode (no ignoreBuildErrors)
- i18n (en/es), AI modules, billing, monitoring

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 01:01:55 +01:00

42 lines
1.1 KiB
TypeScript

import { HttpStatusCode } from "@turbostarter/shared/constants";
import { HttpException } from "@turbostarter/shared/utils";
export const validateSignature = async (
sig: string,
secret: string,
body: string,
) => {
const keyData = new TextEncoder().encode(secret);
const bodyData = new TextEncoder().encode(body);
const key = await crypto.subtle.importKey(
"raw",
keyData,
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"],
);
const signatureBuffer = await crypto.subtle.sign("HMAC", key, bodyData);
const expectedSignature = [...new Uint8Array(signatureBuffer)]
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
if (!timingSafeEqual(expectedSignature, sig)) {
throw new HttpException(HttpStatusCode.BAD_REQUEST, {
code: "billing:error.webhook.signatureInvalid",
});
}
};
const timingSafeEqual = (a: string, b: string) => {
if (a.length !== b.length) return false;
let result = 0;
for (let i = 0; i < a.length; i++) {
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
}
return result === 0;
};