Skip to content

Commit f6cb496

Browse files
committed
Fix: Add Polar.sh Support (closes #46)
1 parent 51bc7b8 commit f6cb496

1 file changed

Lines changed: 139 additions & 0 deletions

File tree

src/api/webhooks.ts

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import express from 'express';
2+
import { Webhook } from 'svix';
3+
import { Request, Response } from 'express';
4+
import Stripe from 'stripe'; // Import Stripe library
5+
6+
// Extend the Request type to include rawBody
7+
declare global {
8+
namespace Express {
9+
interface Request {
10+
rawBody?: string;
11+
}
12+
}
13+
}
14+
15+
const app = express();
16+
17+
// Middleware to get raw body for signature verification.
18+
// This is crucial for webhooks as the raw body is used in signature verification.
19+
// If your application already has middleware that captures the raw body (e.g., body-parser.raw()),
20+
// you might need to adjust or remove this specific `app.use` call to avoid conflicts.
21+
app.use(express.json({
22+
verify: (req: Request, res: Response, buf: Buffer) => {
23+
req.rawBody = buf.toString();
24+
},
25+
}));
26+
27+
// Environment variables for webhook secrets
28+
// Ensure these are set in your environment (e.g., .env file)
29+
const STRIPE_WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET;
30+
const POLAR_WEBHOOK_SECRET = process.env.POLAR_WEBHOOK_SECRET;
31+
// While the full Stripe object might not be needed for webhooks alone,
32+
// it's common practice to initialize it if other Stripe API calls are made.
33+
// For webhook verification, only the secret is strictly needed.
34+
// const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string); // Placeholder if other Stripe API calls were present.
35+
36+
app.post('/api/webhooks', async (req: Request, res: Response) => {
37+
// Ensure rawBody is available for signature verification
38+
if (!req.rawBody) {
39+
// This indicates a misconfiguration in the express.json middleware or its absence.
40+
return res.status(500).send('Webhook endpoint misconfigured: raw body not available.');
41+
}
42+
43+
// Attempt to identify webhook provider based on headers
44+
const svixId = req.headers['svix-id'] as string | undefined;
45+
const svixTimestamp = req.headers['svix-timestamp'] as string | undefined;
46+
const webhookSignature = req.headers['webhook-signature'] as string | undefined; // As per core lesson 3 (Svix standard header)
47+
48+
const stripeSignature = req.headers['stripe-signature'] as string | undefined;
49+
50+
if (svixId && svixTimestamp && webhookSignature) {
51+
// This is likely a Polar.sh webhook (uses Svix)
52+
if (!POLAR_WEBHOOK_SECRET) {
53+
return res.status(500).send('Server Error: Polar webhook secret not configured.');
54+
}
55+
56+
try {
57+
const wh = new Webhook(POLAR_WEBHOOK_SECRET, {
58+
webhookSignatureHeader: 'webhook-signature', // CRITICAL: As per core lesson 3
59+
webhookIdHeader: 'svix-id',
60+
webhookTimestampHeader: 'svix-timestamp',
61+
});
62+
63+
// The 'verify' method will throw an error if the signature is invalid
64+
// or if the timestamp is too old/new.
65+
const payload = wh.verify(req.rawBody, {
66+
'webhook-signature': webhookSignature,
67+
'svix-id': svixId,
68+
'svix-timestamp': svixTimestamp,
69+
});
70+
71+
// Webhook successfully verified. Process the payload.
72+
// In a production application, you would typically dispatch this event
73+
// to a dedicated service or handler based on the event 'type' or other fields.
74+
// Example payload structure for Polar.sh events:
75+
// if (typeof payload === 'object' && payload !== null && 'type' in payload) {
76+
// switch ((payload as { type: string }).type) { // Safely cast to access 'type'
77+
// case 'issue.pledged':
78+
// // Handle new pledge event
79+
// break;
80+
// case 'order.created':
81+
// // Handle order creation event
82+
// break;
83+
// case 'subscription.created':
84+
// // Handle new subscription
85+
// break;
86+
// // Add more cases for other Polar.sh event types as needed
87+
// }
88+
// }
89+
90+
return res.status(200).json({ received: true, provider: 'polar.sh', event: payload });
91+
92+
} catch (error: any) {
93+
// Log the error internally for debugging, but return a generic 400 to the client
94+
// to avoid leaking sensitive information.
95+
// In a real application, consider using a proper logging library here.
96+
// console.error(`Polar.sh Webhook Verification Error: ${error.message}`);
97+
return res.status(400).send(`Polar.sh Webhook Error: ${error.message}`);
98+
}
99+
100+
} else if (stripeSignature) {
101+
// This is likely a Stripe webhook
102+
if (!STRIPE_WEBHOOK_SECRET) {
103+
return res.status(500).send('Server Error: Stripe webhook secret not configured.');
104+
}
105+
106+
try {
107+
// Use the Stripe Node.js library to construct and verify the event
108+
const event = Stripe.webhooks.constructEvent(
109+
req.rawBody,
110+
stripeSignature,
111+
STRIPE_WEBHOOK_SECRET
112+
);
113+
114+
// Webhook successfully verified. Process the event.
115+
// switch (event.type) {
116+
// case 'customer.subscription.created':
117+
// // Handle subscription creation
118+
// break;
119+
// case 'checkout.session.completed':
120+
// // Handle checkout completion
121+
// break;
122+
// // Add more cases for other Stripe event types
123+
// }
124+
125+
return res.status(200).json({ received: true, provider: 'stripe', event: event });
126+
127+
} catch (error: any) {
128+
// Log the error internally for debugging, but return a generic 400 to the client.
129+
// console.error(`Stripe Webhook Verification Error: ${error.message}`);
130+
return res.status(400).send(`Stripe Webhook Error: ${error.message}`);
131+
}
132+
133+
} else {
134+
// Neither Polar.sh nor Stripe recognized
135+
return res.status(400).send('Unknown webhook provider or missing signature headers.');
136+
}
137+
});
138+
139+
export default app;

0 commit comments

Comments
 (0)