Files
isletmem-kovakcrm/src/lib/payments/polar.ts
T

95 lines
2.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 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");
} 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;
}
});
}