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