chatbot-template/lib/billing/gateway.ts

106 lines
2.7 KiB
TypeScript
Raw Permalink Normal View History

import { createHmac, timingSafeEqual } from "node:crypto";
const PAYMENT_GATEWAY_URL = process.env.EGBE_PAYMENT_GATEWAY_URL ?? "";
const GATEWAY_TOKEN = process.env.EGBE_GATEWAYS_TOKEN ?? "";
const WEBHOOK_SECRET = process.env.EGBE_PAYMENT_GATEWAY_WEBHOOK_SECRET ?? "";
const WEBHOOK_HEADER =
process.env.EGBE_PAYMENT_GATEWAY_WEBHOOK_HEADER ??
"x-egbe-payment-signature";
export function isBillingConfigured(): boolean {
return Boolean(
PAYMENT_GATEWAY_URL && GATEWAY_TOKEN && process.env.PRO_STRIPE_PRICE_ID
);
}
export type CheckoutSessionArgs = {
userId: string;
customerEmail?: string;
appSlug: string;
baseUrl: string;
};
export async function createCheckoutSession(args: CheckoutSessionArgs) {
const priceId = process.env.PRO_STRIPE_PRICE_ID;
if (!priceId) {
throw new Error("PRO_STRIPE_PRICE_ID not set");
}
if (!(PAYMENT_GATEWAY_URL && GATEWAY_TOKEN)) {
throw new Error("Payment gateway not configured");
}
const body = {
price: priceId,
mode: "subscription",
quantity: 1,
successUrl: `${args.baseUrl}/pricing?upgraded=1`,
cancelUrl: `${args.baseUrl}/pricing?canceled=1`,
customerEmail: args.customerEmail,
appSlug: args.appSlug,
webhookPath: "/api/billing/webhook",
metadata: { userId: args.userId },
};
const response = await fetch(`${PAYMENT_GATEWAY_URL}/v1/checkout-sessions`, {
method: "POST",
headers: {
Authorization: `Bearer ${GATEWAY_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (!response.ok) {
const text = await response.text();
throw new Error(`Payment gateway error ${response.status}: ${text}`);
}
return response.json() as Promise<{ id: string; url: string }>;
}
export const webhookHeaderName = WEBHOOK_HEADER;
export function verifyWebhookSignature(
rawBody: string,
signatureHeader: string | null,
toleranceSeconds = 300
): boolean {
if (!(WEBHOOK_SECRET && signatureHeader)) {
return false;
}
const parts = Object.fromEntries(
signatureHeader.split(",").map((part) => {
const idx = part.indexOf("=");
return idx === -1 ? [part, ""] : [part.slice(0, idx), part.slice(idx + 1)];
})
);
const timestamp = parts.t;
const v1 = parts.v1;
if (!(timestamp && v1)) {
return false;
}
const tsNum = Number(timestamp);
if (Number.isFinite(tsNum)) {
const ageSeconds = Math.abs(Date.now() / 1000 - tsNum);
if (ageSeconds > toleranceSeconds) {
return false;
}
}
const expected = createHmac("sha256", WEBHOOK_SECRET)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
try {
return timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(v1, "hex")
);
} catch {
return false;
}
}