Facturation & abonnements
createBilling enveloppe l'API Stripe (checkout, portail, abonnements) sans le SDK — juste fetch — et fournit la vérification de signature de webhook, la partie critique que tout le monde rate. Ici le transport Stripe est simulé (aucune clé requise) ; l'état d'abonnement est réellement persisté via l'ORM.
Astuce
Le transport HTTP est injectable : passez secretKey pour du vrai Stripe, ou un mock pour les tests. constructEvent() reconstruit ${t}.${payload}, calcule le HMAC-SHA256 hex, compare en temps constant au v1 de l'en-tête et applique une fenêtre anti-rejeu — la protection webhook que Cashier vous donne.
Statut
✓ abonné — plan Pro
Vérification de signature de webhook
Stripe-Signature (t, v1)signature valide: acceptée ✓Stripe-Signature (altered v1)signature altérée: rejetée ✗import { createBilling, subscribed, constructEvent } from "@bext-stack/framework/billing";
const billing = createBilling({ secretKey: process.env.STRIPE_SECRET_KEY });
// start a subscription checkout:
const { url } = await billing.checkoutSession({
priceId: "price_pro", mode: "subscription",
successUrl: "/ok", cancelUrl: "/cancel", customer: "cus_1",
}); // redirect the user to `url`
// gate features on an active subscription:
const subs = await billing.listSubscriptions({ customer: "cus_1" });
if (subscribed(subs)) { /* unlock */ }
// webhook route — verify the signature (constant-time HMAC + replay window):
export async function POST({ request }) {
const body = await request.text();
const event = constructEvent(body, request.headers.get("stripe-signature"), process.env.STRIPE_WEBHOOK_SECRET);
if (!event) return new Response("bad signature", { status: 400 });
if (event.type === "checkout.session.completed") { /* mark active */ }
return new Response(null, { status: 200 });
}