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>
42 lines
1.1 KiB
TypeScript
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;
|
|
};
|