feat: Polar.sh payment integration, replace Shopier store approach

This commit is contained in:
kovakmedya
2026-05-04 17:45:30 +03:00
parent f43818a51a
commit e8a766c60a
9 changed files with 395 additions and 342 deletions
+93
View File
@@ -0,0 +1,93 @@
import "server-only";
import { createHmac, timingSafeEqual } from "crypto";
const POLAR_API_BASE = "https://api.polar.sh";
const ACCESS_TOKEN = process.env.POLAR_ACCESS_TOKEN ?? "";
const WEBHOOK_SECRET = process.env.POLAR_WEBHOOK_SECRET ?? "";
export const POLAR_PRODUCT_ID = process.env.POLAR_PRODUCT_ID ?? "";
export function isPolarEnabled(): boolean {
return (
process.env.PAYMENT_PROVIDER === "polar" &&
Boolean(ACCESS_TOKEN) &&
Boolean(POLAR_PRODUCT_ID)
);
}
export type PolarCheckout = {
id: string;
url: string;
};
export async function createPolarCheckout(params: {
orderId: string;
tenantId: string;
userEmail: string;
successUrl: string;
}): Promise<PolarCheckout> {
const res = await fetch(`${POLAR_API_BASE}/v1/checkouts/`, {
method: "POST",
headers: {
Authorization: `Bearer ${ACCESS_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
products: [POLAR_PRODUCT_ID],
customer_email: params.userEmail,
success_url: params.successUrl,
metadata: {
crm_order_id: params.orderId,
tenant_id: params.tenantId,
},
}),
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Polar checkout oluşturulamadı: ${text}`);
}
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))
export function verifyPolarWebhook(
webhookId: string,
webhookTimestamp: string,
webhookSignature: string,
rawBody: string,
): boolean {
if (!WEBHOOK_SECRET) return false;
// Timestamp replay saldırısı koruması (5 dakika tolerans)
const ts = parseInt(webhookTimestamp, 10);
if (isNaN(ts) || Math.abs(Date.now() / 1000 - ts) > 300) return false;
const signedContent = `${webhookId}.${webhookTimestamp}.${rawBody}`;
// Secret base64 olabilir
let secretBytes: Buffer;
try {
secretBytes = Buffer.from(WEBHOOK_SECRET.replace(/^whsec_/, ""), "base64");
} catch {
secretBytes = Buffer.from(WEBHOOK_SECRET);
}
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;
}
});
}