Initial commit
This commit is contained in:
295
commands/webhook-setup.md
Normal file
295
commands/webhook-setup.md
Normal file
@@ -0,0 +1,295 @@
|
||||
# Payment Webhook Configuration
|
||||
|
||||
Generate secure webhook handlers for payment providers.
|
||||
|
||||
## Task
|
||||
|
||||
You are a payment webhook security expert. Generate secure, production-ready webhook handlers.
|
||||
|
||||
### Steps:
|
||||
|
||||
1. **Ask for Provider**:
|
||||
- Stripe
|
||||
- PayPal
|
||||
- Square
|
||||
- Custom payment gateway
|
||||
|
||||
2. **Generate Webhook Endpoint** (Stripe):
|
||||
|
||||
```typescript
|
||||
import crypto from 'crypto';
|
||||
import express from 'express';
|
||||
import Stripe from 'stripe';
|
||||
|
||||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
|
||||
const app = express();
|
||||
|
||||
// CRITICAL: Use raw body for webhook signature verification
|
||||
app.post(
|
||||
'/api/webhooks/stripe',
|
||||
express.raw({ type: 'application/json' }),
|
||||
async (req, res) => {
|
||||
const sig = req.headers['stripe-signature'];
|
||||
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
|
||||
|
||||
let event;
|
||||
|
||||
try {
|
||||
// Verify webhook signature
|
||||
event = stripe.webhooks.constructEvent(
|
||||
req.body,
|
||||
sig!,
|
||||
webhookSecret
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(`Webhook signature verification failed: ${err.message}`);
|
||||
return res.status(400).send(`Webhook Error: ${err.message}`);
|
||||
}
|
||||
|
||||
// Handle event idempotently
|
||||
const eventId = event.id;
|
||||
const existingEvent = await db.webhookEvents.findUnique({
|
||||
where: { stripeEventId: eventId },
|
||||
});
|
||||
|
||||
if (existingEvent) {
|
||||
console.log(`Duplicate webhook event: ${eventId}`);
|
||||
return res.status(200).json({ received: true });
|
||||
}
|
||||
|
||||
// Store event (prevents duplicate processing)
|
||||
await db.webhookEvents.create({
|
||||
data: {
|
||||
stripeEventId: eventId,
|
||||
type: event.type,
|
||||
processedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
// Process event in background to return 200 quickly
|
||||
processWebhookEvent(event).catch((error) => {
|
||||
console.error(`Failed to process webhook: ${error.message}`);
|
||||
// Alert ops team
|
||||
});
|
||||
|
||||
res.status(200).json({ received: true });
|
||||
}
|
||||
);
|
||||
|
||||
async function processWebhookEvent(event: Stripe.Event) {
|
||||
switch (event.type) {
|
||||
case 'checkout.session.completed':
|
||||
await handleCheckoutComplete(event.data.object);
|
||||
break;
|
||||
|
||||
case 'customer.subscription.created':
|
||||
case 'customer.subscription.updated':
|
||||
await handleSubscriptionChange(event.data.object);
|
||||
break;
|
||||
|
||||
case 'customer.subscription.deleted':
|
||||
await handleSubscriptionCanceled(event.data.object);
|
||||
break;
|
||||
|
||||
case 'invoice.paid':
|
||||
await handleInvoicePaid(event.data.object);
|
||||
break;
|
||||
|
||||
case 'invoice.payment_failed':
|
||||
await handleInvoicePaymentFailed(event.data.object);
|
||||
break;
|
||||
|
||||
case 'payment_intent.succeeded':
|
||||
await handlePaymentSuccess(event.data.object);
|
||||
break;
|
||||
|
||||
case 'payment_intent.payment_failed':
|
||||
await handlePaymentFailed(event.data.object);
|
||||
break;
|
||||
|
||||
case 'charge.dispute.created':
|
||||
await handleDisputeCreated(event.data.object);
|
||||
break;
|
||||
|
||||
case 'customer.created':
|
||||
await handleCustomerCreated(event.data.object);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log(`Unhandled event type: ${event.type}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **Generate PayPal Webhook**:
|
||||
|
||||
```typescript
|
||||
import crypto from 'crypto';
|
||||
|
||||
app.post('/api/webhooks/paypal', express.json(), async (req, res) => {
|
||||
const webhookId = process.env.PAYPAL_WEBHOOK_ID!;
|
||||
const webhookEvent = req.body;
|
||||
|
||||
// Verify PayPal webhook signature
|
||||
const isValid = await verifyPayPalWebhook(req, webhookId);
|
||||
|
||||
if (!isValid) {
|
||||
return res.status(400).send('Invalid webhook signature');
|
||||
}
|
||||
|
||||
const eventType = webhookEvent.event_type;
|
||||
|
||||
switch (eventType) {
|
||||
case 'PAYMENT.CAPTURE.COMPLETED':
|
||||
await handlePayPalPaymentCompleted(webhookEvent.resource);
|
||||
break;
|
||||
|
||||
case 'BILLING.SUBSCRIPTION.CREATED':
|
||||
await handlePayPalSubscriptionCreated(webhookEvent.resource);
|
||||
break;
|
||||
|
||||
case 'BILLING.SUBSCRIPTION.CANCELLED':
|
||||
await handlePayPalSubscriptionCancelled(webhookEvent.resource);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log(`Unhandled PayPal event: ${eventType}`);
|
||||
}
|
||||
|
||||
res.status(200).json({ received: true });
|
||||
});
|
||||
|
||||
async function verifyPayPalWebhook(req, webhookId) {
|
||||
const transmissionId = req.headers['paypal-transmission-id'];
|
||||
const timestamp = req.headers['paypal-transmission-time'];
|
||||
const signature = req.headers['paypal-transmission-sig'];
|
||||
const certUrl = req.headers['paypal-cert-url'];
|
||||
|
||||
const response = await fetch(
|
||||
`https://api.paypal.com/v1/notifications/verify-webhook-signature`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${await getPayPalAccessToken()}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
transmission_id: transmissionId,
|
||||
transmission_time: timestamp,
|
||||
cert_url: certUrl,
|
||||
auth_algo: req.headers['paypal-auth-algo'],
|
||||
transmission_sig: signature,
|
||||
webhook_id: webhookId,
|
||||
webhook_event: req.body,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
return data.verification_status === 'SUCCESS';
|
||||
}
|
||||
```
|
||||
|
||||
4. **Generate Webhook Testing Script**:
|
||||
|
||||
```typescript
|
||||
// test-webhook.ts
|
||||
import { exec } from 'child_process';
|
||||
import util from 'util';
|
||||
|
||||
const execPromise = util.promisify(exec);
|
||||
|
||||
async function testWebhook() {
|
||||
// Install Stripe CLI: brew install stripe/stripe-cli/stripe
|
||||
|
||||
// Listen to webhooks
|
||||
const { stdout } = await execPromise('stripe listen --forward-to localhost:3000/api/webhooks/stripe');
|
||||
console.log(stdout);
|
||||
|
||||
// Trigger test events
|
||||
await execPromise('stripe trigger payment_intent.succeeded');
|
||||
await execPromise('stripe trigger customer.subscription.created');
|
||||
await execPromise('stripe trigger invoice.payment_failed');
|
||||
}
|
||||
|
||||
testWebhook();
|
||||
```
|
||||
|
||||
5. **Generate Monitoring & Alerting**:
|
||||
|
||||
```typescript
|
||||
// Monitor webhook failures
|
||||
async function monitorWebhooks() {
|
||||
const failedEvents = await db.webhookEvents.findMany({
|
||||
where: {
|
||||
processed: false,
|
||||
createdAt: {
|
||||
lt: new Date(Date.now() - 5 * 60 * 1000), // 5 minutes ago
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (failedEvents.length > 0) {
|
||||
await sendAlert({
|
||||
type: 'webhook_failure',
|
||||
count: failedEvents.length,
|
||||
events: failedEvents.map((e) => e.type),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Retry failed webhooks
|
||||
async function retryFailedWebhooks() {
|
||||
const failedEvents = await db.webhookEvents.findMany({
|
||||
where: { processed: false },
|
||||
take: 10,
|
||||
});
|
||||
|
||||
for (const event of failedEvents) {
|
||||
try {
|
||||
await processWebhookEvent(event.data);
|
||||
await db.webhookEvents.update({
|
||||
where: { id: event.id },
|
||||
data: { processed: true, processedAt: new Date() },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Retry failed for event ${event.id}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
6. **Generate Webhook Schema** (Database):
|
||||
|
||||
```sql
|
||||
CREATE TABLE webhook_events (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
stripe_event_id VARCHAR(255) UNIQUE NOT NULL,
|
||||
type VARCHAR(100) NOT NULL,
|
||||
data JSONB NOT NULL,
|
||||
processed BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
processed_at TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX idx_webhook_events_type ON webhook_events(type);
|
||||
CREATE INDEX idx_webhook_events_processed ON webhook_events(processed);
|
||||
```
|
||||
|
||||
### Security Best Practices:
|
||||
|
||||
- ✅ Verify webhook signatures (prevent spoofing)
|
||||
- ✅ Use raw request body for signature validation
|
||||
- ✅ Idempotency (track event IDs, prevent duplicate processing)
|
||||
- ✅ Return 200 immediately (process in background)
|
||||
- ✅ Retry logic for failures
|
||||
- ✅ Monitoring and alerting
|
||||
- ✅ HTTPS only (secure transmission)
|
||||
- ✅ IP whitelisting (optional but recommended)
|
||||
|
||||
### Example Usage:
|
||||
|
||||
```
|
||||
User: "Set up secure Stripe webhook handler"
|
||||
Result: Complete webhook endpoint with signature verification, idempotency, and monitoring
|
||||
```
|
||||
Reference in New Issue
Block a user