Skip to content
This repository was archived by the owner on Jan 23, 2021. It is now read-only.

Commit 6e5f091

Browse files
Address feedback from @mg-stripe
1 parent 5c5b576 commit 6e5f091

File tree

5 files changed

+127
-124
lines changed

5 files changed

+127
-124
lines changed

README.md

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,6 @@ Webhooks require a public URL that Stripe will ping to notify the monitor of new
3131

3232
If you have a [__Basic__](https://ngrok.com/pricing) ngrok subscription, you can specify a custom subdomain that will stay reserved for your account.
3333

34-
#### Optional: Verify Webhooks are signed
35-
[Stripe can optionally sign Webhook events to your endpoints](https://stripe.com/docs/webhooks/signatures). If you'd like the Webhook Monitor to verify these for you, simply modify the `signingSecret` value in `config.js` with the endpoint-specific signing secret key found at the bottom of the Webhook details page.
36-
37-
Unverified Webhooks will return a `400` response to Stripe, and log an error to your console.
38-
3934
### Start receiving changes
4035

4136
To start the monitor:
@@ -45,17 +40,9 @@ npm install
4540
npm start
4641
```
4742

48-
Take note of the public URL provided by ngrok: it should be listed when the monitor starts.
49-
50-
**Don't want to use ngrok?** As long as Stripe can reach the webhooks endpoint via a public URL, you'll receive updates.
51-
52-
### Subscribe to webhook notifications
53-
54-
In your Stripe Dashboard, go to the _API_ section, then click on the _Webhooks_ tab.
55-
56-
You should add a receiving endpoint by clicking _Add Endpoint_. Fill in the public URL provided by ngrok, or any other public URL that can reach the webhook monitor.
43+
A [Stripe webhook endpoint](https://stripe.com/docs/webhooks/setup) will be automatically provisioned and pointed at your ngrok tunnel, subscribed to all Stripe events.
5744

58-
![](https://raw.githubusercontent.com/stripe/stripe-webhook-monitor/master/screenshots/setting-up-webhooks.png)
45+
**Don't want to use ngrok?** As long as Stripe can reach the webhooks endpoint via a public URL, you'll receive updates. Set the `webhookSigningSecret` configuration to ensure your events are signed correctly.
5946

6047
## Troubleshooting
6148

config.sample.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ module.exports = {
55
stripe: {
66
// Include your Stripe secret key here
77
secretKey: 'YOUR_STRIPE_SECRET_KEY',
8-
signingSecret: null
8+
// If not using ngrok, include your Webhook signing secret here
9+
webhookSigningSecret: null
910
},
1011
/*
1112
Stripe needs a public URL for our server that it can ping with new events.

package-lock.json

Lines changed: 11 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323
"body-parser": "^1.17.2",
2424
"boxen": "^1.2.1",
2525
"express": "^4.15.3",
26-
"ngrok": "^3.1.1",
26+
"ngrok": "^3.2.4",
2727
"socket.io": "^2.2.0",
28-
"stripe": "^4.23.1"
28+
"stripe": "^7.5.0"
2929
}
3030
}

server.js

Lines changed: 110 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"use strict";
22

33
const config = require("./config.js");
4-
const boxen = require("boxen");
54
const stripe = require("stripe")(config.stripe.secretKey);
65
const express = require("express");
76
const socketio = require("socket.io");
@@ -10,106 +9,126 @@ const bodyParser = require("body-parser");
109
const path = require("path");
1110
const chalk = require("chalk");
1211

13-
/*
14-
We use two different Express servers for security reasons: our webhooks
15-
endpoint needs to be publicly accessible, but we don't want our monitoring
16-
dashboard to be publicly accessible since it may contain sensitive data.
17-
*/
18-
19-
// The first Express server will serve Stripe Monitor (on a different port).
20-
const monitor = express();
21-
const monitorServer = http.Server(monitor);
22-
// We'll set up Socket.io to notify us of new events
23-
const io = socketio(monitorServer);
24-
let recentEvents = [];
25-
26-
// Serve static files and start the server
27-
monitor.use(express.static(path.join(__dirname, "public")));
28-
monitorServer.listen(config.port, () => {
29-
console.log(`Stripe Monitor is up: http://localhost:${config.port}`);
30-
});
31-
32-
// Provides environment details: the Dashboard URL will vary based on whether we're in test or live mode
33-
monitor.get("/environment", async (req, res) => {
12+
(async () => {
13+
const webhooksPort = config.port + 1;
14+
15+
let webhookSigningSecret,
16+
ngrokUrl;
17+
18+
// Set the Stripe dashboard URL and append in testmode
3419
let dashboardUrl = "https://dashboard.stripe.com/";
3520
if (config.stripe.secretKey.startsWith("sk_test")) {
3621
dashboardUrl += "test/";
3722
}
38-
res.send({ dashboardUrl });
39-
});
40-
41-
// Provides the 20 most recent events (useful when the app first loads)
42-
monitor.get("/recent-events", async (req, res) => {
43-
let response = await stripe.events.list({ limit: 20 });
44-
recentEvents = response.data;
45-
res.send(recentEvents);
46-
});
47-
48-
// The second Express server will receive webhooks
49-
const webhooks = express();
50-
const webhooksPort = config.port + 1;
51-
52-
webhooks.listen(webhooksPort, () => {
53-
console.log(`Listening for webhooks: http://localhost:${webhooksPort}`);
54-
});
55-
56-
// Provides an endpoint to receive webhooks
57-
webhooks.post("/", bodyParser.raw({ type: "application/json" }), async (req, res) => {
58-
let event;
59-
60-
// Check if signing secret has been set
61-
if (config.stripe.signingSecret) {
62-
const sig = req.headers['stripe-signature'];
63-
64-
try {
65-
event = stripe.webhooks.constructEvent(req.body, sig, config.stripe.signingSecret);
66-
} catch (err) {
67-
console.log(
68-
chalk.red(`Failed to verify webhook signature: ${err.message}`)
69-
);
70-
res.sendStatus(400);
71-
return;
72-
}
73-
} else {
74-
// Signing secret is not set, just pass through
75-
event = req.body;
76-
}
77-
78-
// Send a notification that we have a new event
79-
// Here we're using Socket.io, but server-sent events or another mechanism can be used.
80-
io.emit("event", event);
81-
// Stripe needs to receive a 200 status from any webhooks endpoint
82-
res.sendStatus(200);
83-
});
8423

85-
// Use ngrok to provide a public URL for receiving webhooks
86-
if (config.ngrok.enabled) {
87-
const ngrok = require("ngrok");
88-
const boxen = require("boxen");
24+
if (config.ngrok.enabled) {
25+
const ngrok = require("ngrok");
8926

90-
ngrok.connect(
91-
{
27+
// Provision ngrok tunnel
28+
ngrokUrl = await ngrok.connect({
9229
addr: webhooksPort,
9330
subdomain: config.ngrok.subdomain,
9431
authtoken: config.ngrok.authtoken
95-
},
96-
function(err, url) {
97-
if (err) {
98-
console.log(err);
99-
if (err.code === "ECONNREFUSED") {
100-
console.log(
101-
chalk.red(`Connection refused at ${err.address}:${err.port}`)
102-
);
103-
process.exit(1);
104-
}
105-
console.log(chalk.yellow(`ngrok reported an error: ${err.msg}`));
32+
}).catch(error => {
33+
console.log("Error starting ngrok tunnel", error);
34+
process.exit(1);
35+
});
36+
37+
// Provision Stripe webhook endpoint
38+
const webhookEndpoint = await stripe.webhookEndpoints.create({
39+
url: ngrokUrl,
40+
enabled_events: ['*']
41+
}).catch(error => {
42+
console.log("Error provisioning webhook endpoint", error);
43+
process.exit(1);
44+
});
45+
46+
// Save webhook signing secret key
47+
webhookSigningSecret = webhookEndpoint.secret;
48+
49+
// Tear down Stripe webhook endpoint on CTRL+C
50+
// (ngrok does this automatically)
51+
process.on('SIGINT', async () => {
52+
await stripe.webhookEndpoints.del(webhookEndpoint.id).catch(error => {
53+
const webhookManagementUrl = `${dashboardUrl}/webhooks/${webhookEndpoint.id}`;
54+
console.log(`Error deleting webhook endpoint, visit ${webhookManagementUrl}`, error);
55+
process.exit(1);
56+
});
57+
58+
process.exit(0);
59+
});
60+
} else {
61+
// Not using ngrok, but setting signing secret via config
62+
if (config.stripe.webhookSigningSecret) {
63+
webhookSigningSecret = config.stripe.webhookSigningSecret;
64+
}
65+
}
66+
67+
/*
68+
We use two different Express servers for security reasons: our webhooks
69+
endpoint needs to be publicly accessible, but we don't want our monitoring
70+
dashboard to be publicly accessible since it may contain sensitive data.
71+
*/
72+
73+
// The first Express server will serve Stripe Monitor (on a different port).
74+
const monitor = express();
75+
const monitorServer = http.Server(monitor);
76+
// We'll set up Socket.io to notify us of new events
77+
const io = socketio(monitorServer);
78+
let recentEvents = [];
79+
80+
// Serve static files and start the server
81+
monitor.use(express.static(path.join(__dirname, "public")));
82+
monitorServer.listen(config.port, () => {
83+
console.log(`Stripe Monitor is up: http://localhost:${config.port}`);
84+
});
85+
86+
// Provides environment details: the Dashboard URL will vary based on whether we're in test or live mode
87+
monitor.get("/environment", async (req, res) => {
88+
res.send({ dashboardUrl });
89+
});
90+
91+
// Provides the 20 most recent events (useful when the app first loads)
92+
monitor.get("/recent-events", async (req, res) => {
93+
let response = await stripe.events.list({ limit: 20 });
94+
recentEvents = response.data;
95+
res.send(recentEvents);
96+
});
97+
98+
// The second Express server will receive webhooks
99+
const webhooks = express();
100+
101+
webhooks.listen(webhooksPort, () => {
102+
const url = ngrokUrl ? ngrokUrl : `http://localhost:${webhooksPort}`;
103+
console.log(`Listening for webhooks: ${url}`);
104+
});
105+
106+
// Provides an endpoint to receive webhooks
107+
webhooks.post("/", bodyParser.raw({ type: "application/json" }), async (req, res) => {
108+
let event;
109+
110+
// Check if signing secret has been set
111+
if (webhookSigningSecret) {
112+
const sig = req.headers['stripe-signature'];
113+
114+
try {
115+
event = stripe.webhooks.constructEvent(req.body, sig, webhookSigningSecret);
116+
} catch (err) {
106117
console.log(
107-
boxen(err.details.err.trim(), {
108-
padding: { top: 0, right: 2, bottom: 0, left: 2 }
109-
})
118+
chalk.red(`Failed to verify webhook signature: ${err.message}`)
110119
);
120+
res.sendStatus(400);
121+
return;
111122
}
112-
console.log(` └ Public URL for receiving Stripe webhooks: ${url}`);
123+
} else {
124+
// Signing secret is not set, just pass through
125+
event = req.body;
113126
}
114-
);
115-
}
127+
128+
// Send a notification that we have a new event
129+
// Here we're using Socket.io, but server-sent events or another mechanism can be used.
130+
io.emit("event", event);
131+
// Stripe needs to receive a 200 status from any webhooks endpoint
132+
res.sendStatus(200);
133+
});
134+
})();

0 commit comments

Comments
 (0)