type StripeCheckoutSession = { payment_status?: string; customer_email?: string | null; customer_details?: { email?: string | null }; }; function sessionPurchaserEmail(session: StripeCheckoutSession): string | null { return session.customer_details?.email ?? session.customer_email ?? null; } type VerifyResult = | { ok: true; orderId: string; purchaserEmail: string | null } | { ok: false }; /** * Stripe Checkout session ids are opaque tokens (`cs_test_…` / `cs_live_…`). * They are interpolated into the Stripe API URL, so an unvalidated value can * inject `?`/`#`/`../` and retarget the request *with the Stripe secret key * attached* (CodeQL js/request-forgery). Restrict to an explicit charset and * encode before interpolation. */ const STRIPE_SESSION_ID_RE = /^[A-Za-z0-9_-]{1,256}$/; function safeStripeSessionId(sessionId: string): string | null { if (typeof sessionId !== "string" || !STRIPE_SESSION_ID_RE.test(sessionId)) { return null; } return encodeURIComponent(sessionId); } /** * Confirms a Stripe Checkout session is paid before allowing post-payment flows. * Mock sessions are only accepted when Stripe is not configured (local/dev builds). */ export async function verifyCheckoutSession( sessionId: string, templateId: string, ): Promise { if (sessionId === "mock") { if (process.env.STRIPE_SECRET_KEY || !templateId) { return { ok: false }; } return { ok: true, orderId: templateId, purchaserEmail: null }; } const safeSessionId = safeStripeSessionId(sessionId); if (!safeSessionId) { return { ok: false }; } const stripeKey = process.env.STRIPE_SECRET_KEY; if (!stripeKey) { return { ok: false }; } const resp = await fetch(`https://api.stripe.com/v1/checkout/sessions/${safeSessionId}`, { headers: { Authorization: `Bearer ${stripeKey}` }, }); if (!resp.ok) { return { ok: false }; } const session = (await resp.json()) as StripeCheckoutSession; if (session.payment_status !== "paid") { return { ok: false }; } return { ok: true, orderId: sessionId, purchaserEmail: sessionPurchaserEmail(session) }; }