Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 6 additions & 25 deletions .env.example
Original file line number Diff line number Diff line change
@@ -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
77 changes: 77 additions & 0 deletions api/stripe-webhook.js
Original file line number Diff line number Diff line change
@@ -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 });
};
14 changes: 14 additions & 0 deletions docs/INTEGRATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading