From 302b733fa1abf635de706850781ced715b794d1e Mon Sep 17 00:00:00 2001 From: feispro <262678496+feiscs@users.noreply.github.com> Date: Fri, 15 May 2026 03:20:52 -0400 Subject: [PATCH] Add Stripe webhook endpoint and env var docs --- .env.example | 31 ++++------------- api/stripe-webhook.js | 77 +++++++++++++++++++++++++++++++++++++++++++ docs/INTEGRATIONS.md | 14 ++++++++ 3 files changed, 97 insertions(+), 25 deletions(-) create mode 100644 api/stripe-webhook.js diff --git a/.env.example b/.env.example index 938561a..4dbe43b 100644 --- a/.env.example +++ b/.env.example @@ -1,26 +1,7 @@ -# FORMA public runtime configuration for local demos or Vercel builds. -# Copy to .env locally if needed. Never commit .env with real values. +# Stripe +STRIPE_SECRET_KEY=sk_live_or_sk_test_xxx +STRIPE_PUBLISHABLE_KEY=pk_live_or_pk_test_xxx +STRIPE_WEBHOOK_SECRET=whsec_xxx -# Shopify Storefront API (browser-safe storefront token, not Admin API token) -SHOPIFY_DOMAIN=feispla.myshopify.com -SHOPIFY_STOREFRONT_ACCESS_TOKEN= -SHOPIFY_API_VERSION=2025-04 -SHOPIFY_ENABLE_REMOTE_PRODUCTS=true -SHOPIFY_PRODUCT_LIMIT=50000 - -# Supabase public anon configuration -SUPABASE_URL=https://nejzzerwtgtbqawaizuo.supabase.co -SUPABASE_ANON_KEY=sb_publishable_BPOIpRJaqBftujcnHY0mvw_jh8-88Kh -SUPABASE_EVENTS_TABLE=store_events -SUPABASE_NEWSLETTER_TABLE=newsletter_signups - -# Chatbase widget -CHATBASE_BOT_ID= -CHATBASE_ENABLED=false - -# Optional agent metadata -GITHUB_AGENT_PROVIDER=Claude -GITHUB_AGENT_REPO=FEISHTML - -# Optional output override for testing the config generator -FORMA_CONFIG_OUTPUT=assets/config.js +# App +APP_URL=https://feishtml.vercel.app diff --git a/api/stripe-webhook.js b/api/stripe-webhook.js new file mode 100644 index 0000000..3dddbb6 --- /dev/null +++ b/api/stripe-webhook.js @@ -0,0 +1,77 @@ +const crypto = require('crypto'); + +function badRequest(message) { + return { + statusCode: 400, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ok: false, error: message }), + }; +} + +function parseStripeSignature(signatureHeader) { + return String(signatureHeader || '') + .split(',') + .map((entry) => entry.trim()) + .reduce((acc, pair) => { + const [key, value] = pair.split('='); + if (key && value) acc[key] = value; + return acc; + }, {}); +} + +function secureCompare(a, b) { + const first = Buffer.from(String(a || ''), 'utf8'); + const second = Buffer.from(String(b || ''), 'utf8'); + if (first.length !== second.length) return false; + return crypto.timingSafeEqual(first, second); +} + +module.exports = async (req, res) => { + if (req.method !== 'POST') { + res.setHeader('Allow', 'POST'); + return res.status(405).json({ ok: false, error: 'Method not allowed' }); + } + + const secret = process.env.STRIPE_WEBHOOK_SECRET; + if (!secret) { + return res.status(500).json({ ok: false, error: 'Missing STRIPE_WEBHOOK_SECRET' }); + } + + const signatureHeader = req.headers['stripe-signature']; + if (!signatureHeader) { + return res.status(400).json({ ok: false, error: 'Missing Stripe-Signature header' }); + } + + const chunks = []; + for await (const chunk of req) chunks.push(chunk); + const rawBody = Buffer.concat(chunks).toString('utf8'); + + const parsedSig = parseStripeSignature(signatureHeader); + const timestamp = parsedSig.t; + const signature = parsedSig.v1; + if (!timestamp || !signature) { + return res.status(400).json({ ok: false, error: 'Malformed Stripe-Signature header' }); + } + + const payload = `${timestamp}.${rawBody}`; + const expected = crypto.createHmac('sha256', secret).update(payload, 'utf8').digest('hex'); + + if (!secureCompare(signature, expected)) { + return res.status(400).json({ ok: false, error: 'Invalid signature' }); + } + + let event; + try { + event = JSON.parse(rawBody); + } catch (error) { + return res.status(400).json({ ok: false, error: 'Invalid JSON payload' }); + } + + console.log('[stripe-webhook] verified event', { + id: event.id, + type: event.type, + livemode: event.livemode, + }); + + return res.status(200).json({ ok: true, received: true }); +}; diff --git a/docs/INTEGRATIONS.md b/docs/INTEGRATIONS.md index d617f43..6f4d1a9 100644 --- a/docs/INTEGRATIONS.md +++ b/docs/INTEGRATIONS.md @@ -71,6 +71,20 @@ SHOPIFY_PRODUCT_LIMIT=5 bundle exec ruby scripts/list_products.rb The static Vercel storefront can keep using `assets/integrations.js` for safe Storefront/demo behavior, while this Ruby adapter handles private Admin API work. + +## Stripe + Vercel webhook path + +1. Rotate any exposed Stripe credentials before configuration. +2. In Vercel Project Settings → Environment Variables, set: + - `STRIPE_SECRET_KEY` + - `STRIPE_PUBLISHABLE_KEY` + - `STRIPE_WEBHOOK_SECRET` (must start with `whsec_`) + - `APP_URL` (for this project: `https://feishtml.vercel.app`) +3. In Stripe Dashboard, configure a webhook endpoint to: + - `https://feishtml.vercel.app/api/stripe-webhook` +4. Select required events (for example `checkout.session.completed`) and use the generated `whsec_...` value in Vercel as `STRIPE_WEBHOOK_SECRET`. +5. Deploy, then send a test event from Stripe and verify `200` responses from the webhook route. + ## Supabase path 1. Create a Supabase project.