fix: use svix library for Polar webhook signature verification

This commit is contained in:
kovakmedya
2026-05-04 18:47:28 +03:00
parent 89830aa28f
commit afbb029c67
4 changed files with 53 additions and 44 deletions
+5 -11
View File
@@ -9,14 +9,6 @@ import { verifyPolarWebhook } from "@/lib/payments/polar";
const PRO_VALIDITY_DAYS = 30;
export async function POST(req: NextRequest): Promise<NextResponse> {
// Polar, Svix altyapısı kullandığından hem webhook-* hem svix-* header'ları destekle
const webhookId =
req.headers.get("webhook-id") ?? req.headers.get("svix-id") ?? "";
const webhookTimestamp =
req.headers.get("webhook-timestamp") ?? req.headers.get("svix-timestamp") ?? "";
const webhookSignature =
req.headers.get("webhook-signature") ?? req.headers.get("svix-signature") ?? "";
let rawBody: string;
try {
rawBody = await req.text();
@@ -24,7 +16,11 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
return NextResponse.json({ error: "invalid body" }, { status: 400 });
}
if (!verifyPolarWebhook(webhookId, webhookTimestamp, webhookSignature, rawBody)) {
// Svix tüm headerları obje olarak alır
const headers: Record<string, string> = {};
req.headers.forEach((value, key) => { headers[key] = value; });
if (!verifyPolarWebhook(headers, rawBody)) {
console.error("[polar/callback] signature mismatch");
return NextResponse.json({ error: "signature mismatch" }, { status: 403 });
}
@@ -46,7 +42,6 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
return new NextResponse("OK", { status: 200 });
}
// metadata'dan crm_order_id al
const metadata = (event.data as { metadata?: Record<string, string> }).metadata ?? {};
const crmOrderId = metadata.crm_order_id ?? "";
@@ -73,7 +68,6 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
return new NextResponse("OK", { status: 200 });
}
// Idempotency
if (payment.status === "success" || payment.status === "failed") {
return new NextResponse("OK", { status: 200 });
}
+7 -33
View File
@@ -1,6 +1,6 @@
import "server-only";
import { createHmac, timingSafeEqual } from "crypto";
import { Webhook } from "svix";
const POLAR_API_BASE = "https://api.polar.sh";
const ACCESS_TOKEN = process.env.POLAR_ACCESS_TOKEN ?? "";
@@ -52,43 +52,17 @@ export async function createPolarCheckout(params: {
return res.json() as Promise<PolarCheckout>;
}
// Standard Webhooks imza doğrulama
// Header: webhook-id, webhook-timestamp, webhook-signature
// Signed content: "{webhook-id}.{webhook-timestamp}.{rawBody}"
// Signature: "v1," + base64(HMAC-SHA256(secret, signedContent))
// Svix kullanarak Polar webhook imzasını doğrula
export function verifyPolarWebhook(
webhookId: string,
webhookTimestamp: string,
webhookSignature: string,
headers: Record<string, string>,
rawBody: string,
): boolean {
if (!WEBHOOK_SECRET) return false;
// Timestamp replay koruması (1 saat — Polar retry aralığı uzun olabilir)
const ts = parseInt(webhookTimestamp, 10);
if (isNaN(ts) || Math.abs(Date.now() / 1000 - ts) > 3600) return false;
const signedContent = `${webhookId}.${webhookTimestamp}.${rawBody}`;
// Polar secret: "polar_whs_<base64>" — prefix soyulup base64 decode edilir
let secretBytes: Buffer;
try {
const raw = WEBHOOK_SECRET.replace(/^(whsec_|polar_whs_)/, "");
secretBytes = Buffer.from(raw, "base64");
const wh = new Webhook(WEBHOOK_SECRET);
wh.verify(rawBody, headers);
return true;
} catch {
secretBytes = Buffer.from(WEBHOOK_SECRET);
return false;
}
const expected = createHmac("sha256", secretBytes).update(signedContent).digest("base64");
const expectedFull = `v1,${expected}`;
// Header birden fazla imza içerebilir (space ile ayrılmış)
const signatures = webhookSignature.split(" ");
return signatures.some((sig) => {
try {
return timingSafeEqual(Buffer.from(expectedFull), Buffer.from(sig));
} catch {
return false;
}
});
}