├── .gitignore
├── package.json
├── LICENSE
├── views
└── index.ejs
├── README.md
├── index.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "main": "index.js",
4 | "scripts": {
5 | "start": "node index.js"
6 | },
7 | "dependencies": {
8 | "body-parser": "^1.17.2",
9 | "ejs": "^2.5.7",
10 | "express": "^4.15.4",
11 | "morgan": "^1.8.2",
12 | "node-fetch": "^1.7.2",
13 | "stripe": "^4.24.1"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Ezekiel Gabrielse
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/views/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Purchase Software
6 |
7 |
8 | Purchase Software
9 |
39 |
40 |
92 |
93 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Example Keygen + Stripe integration
2 | The following web app is written in Node.js and shows how to integrate
3 | [Keygen](https://keygen.sh) and [Stripe](https://stripe.com) together
4 | using webhooks. Much more could be done to automate e.g. license
5 | revocation when a subscription is canceled, etc.
6 |
7 | > **This example application is not 100% production-ready**, but it should
8 | > get you 90% of the way there. You may need to add additional logging,
9 | > error handling, as well as listening for additional webhook events.
10 |
11 | 🚨 Don't want to host your own webhook server? Check out [our Zapier integration](https://keygen.sh/integrate/zapier/).
12 |
13 | ## Running the app
14 |
15 | First up, configure a few environment variables:
16 | ```bash
17 | # Stripe publishable key
18 | export STRIPE_PUBLISHABLE_KEY="YOUR_STRIPE_PUBLISHABLE_KEY"
19 |
20 | # Stripe secret key (don't share this!)
21 | export STRIPE_SECRET_KEY="YOUR_STRIPE_SECRET_KEY"
22 |
23 | # The Stripe plan to subscribe new customers to
24 | export STRIPE_PLAN_ID="YOUR_STRIPE_PLAN_ID"
25 |
26 | # Keygen product token (don't share this!)
27 | export KEYGEN_PRODUCT_TOKEN="YOUR_KEYGEN_PRODUCT_TOKEN"
28 |
29 | # Your Keygen account ID
30 | export KEYGEN_ACCOUNT_ID="YOUR_KEYGEN_ACCOUNT_ID"
31 |
32 | # The Keygen policy to use when creating licenses for new users
33 | # after they successfully subscribe to a plan
34 | export KEYGEN_POLICY_ID="YOUR_KEYGEN_POLICY_ID"
35 | ```
36 |
37 | You can either run each line above within your terminal session before
38 | starting the app, or you can add the above contents to your `~/.bashrc`
39 | file and then run `source ~/.bashrc` after saving the file.
40 |
41 | Next, install dependencies with [`yarn`](https://yarnpkg.comg):
42 | ```
43 | yarn
44 | ```
45 |
46 | Then start the app:
47 | ```
48 | yarn start
49 | ```
50 |
51 | ## Configuring the webhooks
52 |
53 | For local development, create an [`ngrok`](https://ngrok.com) tunnel to your
54 | local development server:
55 | ```
56 | ngrok http 8080
57 | ```
58 |
59 | Next up, add the generated `ngrok` URL to your Stripe and Keygen accounts to
60 | listen for webhooks.
61 |
62 | 1. **Stripe:** add `https://{YOUR_NGROK_URL}/stripe-webhooks` to https://dashboard.stripe.com/account/webhooks
63 | 1. **Keygen:** add `https://{YOUR_NGROK_URL}/keygen-webhooks` to https://app.keygen.sh/webhook-endpoints
64 |
65 | > **In a production environment, you would use your actual server info in place of
66 | > the `ngrok` URLs above.**
67 |
68 | ## Testing the integration
69 |
70 | Visit the following url: http://localhost:8080 and fill out the purchase form.
71 |
72 | ## Common Issues
73 |
74 | ### Incorrect ENV variables
75 |
76 | In case of errors, please double check all of your environment variables.
77 | If one of the variables are incorrect, it may cause API authentication
78 | issues.
79 |
80 | ### Protected account
81 |
82 | **Please note that this example requires that your Keygen account is
83 | set to unprotected**, because this example handles user creation
84 | on the front-end. You can update this setting on your [account's
85 | settings page](https://app.keygen.sh/settings). If you would prefer
86 | to keep your account protected, the logic for user creation would
87 | need to be moved to a server-side URL.
88 |
89 | ### Other issues
90 |
91 | Here's a few things to double check when a problem arises:
92 |
93 | 1. Make sure you're using the correct account ID (find yours [here](https://app.keygen.sh/settings))
94 | 1. Make sure you're using a product token or admin token (the token should start with `prod-` or `admi-`)
95 | 1. Make sure you're using the correct policy ID (it should be a UUID)
96 | 1. Make sure that your Stripe environment variables are correct
97 | 1. Make sure all dependencies have been installed via `yarn install`
98 | 1. Make sure you have correctly configured webhooks for both Keygen _and_ Stripe
99 | 1. Make sure that the webhook URL is accessible from the public internet via `ngrok`
100 |
101 | ## Questions?
102 |
103 | Reach out at [support@keygen.sh](mailto:support@keygen.sh) if you have any
104 | questions or concerns!
105 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | // Be sure to add these ENV variables!
2 | const {
3 | STRIPE_PUBLISHABLE_KEY,
4 | STRIPE_SECRET_KEY,
5 | STRIPE_PLAN_ID,
6 | KEYGEN_PRODUCT_TOKEN,
7 | KEYGEN_ACCOUNT_ID,
8 | KEYGEN_POLICY_ID,
9 | PORT = 8080
10 | } = process.env
11 |
12 | const stripe = require("stripe")(STRIPE_SECRET_KEY)
13 | const fetch = require("node-fetch")
14 | const express = require("express")
15 | const bodyParser = require("body-parser")
16 | const morgan = require('morgan')
17 | const app = express()
18 |
19 | app.use(bodyParser.json({ type: "application/vnd.api+json" }))
20 | app.use(bodyParser.json({ type: "application/json" }))
21 | app.use(morgan('combined'))
22 |
23 | app.set('view engine', 'ejs')
24 |
25 | app.post("/keygen-webhooks", async (req, res) => {
26 | const { data: { id: keygenEventId } } = req.body
27 |
28 | // Fetch the webhook to validate it and get its most up-to-date state
29 | const keygenWebhook = await fetch(`https://api.keygen.sh/v1/accounts/${KEYGEN_ACCOUNT_ID}/webhook-events/${keygenEventId}`, {
30 | method: "GET",
31 | headers: {
32 | "Authorization": `Bearer ${KEYGEN_PRODUCT_TOKEN}`,
33 | "Accept": "application/vnd.api+json"
34 | }
35 | })
36 |
37 | const { data: keygenEvent, errors } = await keygenWebhook.json()
38 | if (errors) {
39 | return res.sendStatus(200) // Event does not exist (wasn't sent from Keygen)
40 | }
41 |
42 | switch (keygenEvent.attributes.event) {
43 | // 1. Respond to user creation events within your Keygen account. Here, we'll create
44 | // a new Stripe customer account for new Keygen users.
45 | case "user.created":
46 | const { data: keygenUser } = JSON.parse(keygenEvent.attributes.payload)
47 |
48 | // Make sure our Keygen user has a Stripe token, or else we can't charge them later on..
49 | if (!keygenUser.attributes.metadata.stripeToken) {
50 | throw new Error(`User ${keygenUser.id} does not have a Stripe token attached to their user account!`)
51 | }
52 |
53 | // 2. Create a Stripe customer, making sure we use our Stripe token as their payment
54 | // method of choice.
55 | const stripeCustomer = await stripe.customers.create({
56 | description: `Customer for Keygen user ${keygenUser.attributes.email}`,
57 | email: keygenUser.attributes.email,
58 | // Source is a Stripe token obtained with Stripe.js during user creation and
59 | // temporarily stored in the user's metadata attribute.
60 | source: keygenUser.attributes.metadata.stripeToken,
61 | // Store the user's Keygen ID within the Stripe customer so that we can lookup
62 | // a Stripe customer's Keygen account.
63 | metadata: { keygenUserId: keygenUser.id }
64 | })
65 |
66 | // 3. Add the user's Stripe customer ID to the user's metadata attribute so that
67 | // we can lookup their Stripe customer account when needed.
68 | const update = await fetch(`https://api.keygen.sh/v1/accounts/${KEYGEN_ACCOUNT_ID}/users/${keygenUser.id}`, {
69 | method: "PATCH",
70 | headers: {
71 | "Authorization": `Bearer ${KEYGEN_PRODUCT_TOKEN}`,
72 | "Content-Type": "application/vnd.api+json",
73 | "Accept": "application/vnd.api+json"
74 | },
75 | body: JSON.stringify({
76 | data: {
77 | type: "users",
78 | attributes: {
79 | metadata: { stripeCustomerId: stripeCustomer.id }
80 | }
81 | }
82 | })
83 | })
84 |
85 | const { data, errors } = await update.json()
86 | if (errors) {
87 | throw new Error(errors.map(e => e.detail).toString())
88 | }
89 |
90 | // All is good! Stripe customer was successfully created for the new Keygen
91 | // user. Let Keygen know the event was received successfully.
92 | res.sendStatus(200)
93 | break
94 | default:
95 | // For events we don't care about, let Keygen know all is good.
96 | res.sendStatus(200)
97 | }
98 | })
99 |
100 | app.post("/stripe-webhooks", async (req, res) => {
101 | const { body: stripeEvent } = req
102 |
103 | switch (stripeEvent.type) {
104 | // 4. Respond to customer creation events within your Stripe account. Here, we'll
105 | // create a new Stripe subscription for the customer as well as a Keygen license
106 | // for the Keygen user that belongs to the Stripe customer.
107 | case "customer.created":
108 | const { object: stripeCustomer } = stripeEvent.data
109 |
110 | // Make sure our Stripe customer has a Keygen user ID, or else we can't work with it.
111 | if (!stripeCustomer.metadata.keygenUserId) {
112 | throw new Error(`Customer ${stripeCustomer.id} does not have a Keygen user ID attached to their customer account!`)
113 | }
114 |
115 | // 5. Create a subscription for the new Stripe customer. This will charge the
116 | // Stripe customer. (You may or may not want to also check if the customer
117 | // already has an existing subscription.)
118 | const stripeSubscription = await stripe.subscriptions.create({
119 | customer: stripeCustomer.id,
120 | plan: STRIPE_PLAN_ID
121 | }, {
122 | // Use an idempotency key so that we don't charge a customer more than one
123 | // time regardless of how many times this webhook is retried.
124 | // See: https://stripe.com/docs/api/node#idempotent_requests
125 | idempotency_key: stripeCustomer.metadata.keygenUserId
126 | })
127 |
128 | // 6. Create a license for the new Stripe customer after we create a subscription
129 | // for them. We're pulling the Keygen user's ID from the Stripe customer's
130 | // metadata attribute (we stored it there earler).
131 | const keygenLicense = await fetch(`https://api.keygen.sh/v1/accounts/${KEYGEN_ACCOUNT_ID}/licenses`, {
132 | method: "POST",
133 | headers: {
134 | "Authorization": `Bearer ${KEYGEN_PRODUCT_TOKEN}`,
135 | "Content-Type": "application/vnd.api+json",
136 | "Accept": "application/vnd.api+json"
137 | },
138 | body: JSON.stringify({
139 | data: {
140 | type: "licenses",
141 | attributes: {
142 | metadata: { stripeSubscriptionId: stripeSubscription.id }
143 | },
144 | relationships: {
145 | policy: {
146 | data: { type: "policies", id: KEYGEN_POLICY_ID }
147 | },
148 | user: {
149 | data: { type: "users", id: stripeCustomer.metadata.keygenUserId }
150 | }
151 | }
152 | }
153 | })
154 | })
155 |
156 | const { data, errors } = await keygenLicense.json()
157 | if (errors) {
158 | res.sendStatus(500)
159 |
160 | // If you receive an error here, then you may want to handle the fact the customer
161 | // may have been charged for a license that they didn't receive e.g. easiest way
162 | // would be to create it manually, or refund their subscription charge.
163 | throw new Error(errors.map(e => e.detail).toString())
164 | }
165 |
166 | // All is good! License was successfully created for the new Stripe customer's
167 | // Keygen user account. Next up would be for us to email the license key to
168 | // our user's email using `stripeCustomer.email` or something similar.
169 |
170 | // Let Stripe know the event was received successfully.
171 | res.sendStatus(200)
172 | break
173 | default:
174 | // For events we don't care about, let Stripe know all is good.
175 | res.sendStatus(200)
176 | }
177 | })
178 |
179 | app.get('/', async (req, res) => {
180 | res.render('index', {
181 | STRIPE_PUBLISHABLE_KEY,
182 | KEYGEN_ACCOUNT_ID
183 | })
184 | })
185 |
186 | process.on('unhandledRejection', err => {
187 | console.error(`Unhandled rejection: ${err}`, err.stack)
188 | })
189 |
190 | const server = app.listen(PORT, 'localhost', () => {
191 | const { address, port } = server.address()
192 |
193 | console.log(`Listening at http://${address}:${port}`)
194 | })
--------------------------------------------------------------------------------
/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | accepts@~1.3.3:
6 | version "1.3.4"
7 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.4.tgz#86246758c7dd6d21a6474ff084a4740ec05eb21f"
8 | dependencies:
9 | mime-types "~2.1.16"
10 | negotiator "0.6.1"
11 |
12 | array-flatten@1.1.1:
13 | version "1.1.1"
14 | resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
15 |
16 | basic-auth@~1.1.0:
17 | version "1.1.0"
18 | resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-1.1.0.tgz#45221ee429f7ee1e5035be3f51533f1cdfd29884"
19 |
20 | bluebird@^2.10.2:
21 | version "2.11.0"
22 | resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1"
23 |
24 | body-parser@^1.17.2:
25 | version "1.17.2"
26 | resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.17.2.tgz#f8892abc8f9e627d42aedafbca66bf5ab99104ee"
27 | dependencies:
28 | bytes "2.4.0"
29 | content-type "~1.0.2"
30 | debug "2.6.7"
31 | depd "~1.1.0"
32 | http-errors "~1.6.1"
33 | iconv-lite "0.4.15"
34 | on-finished "~2.3.0"
35 | qs "6.4.0"
36 | raw-body "~2.2.0"
37 | type-is "~1.6.15"
38 |
39 | bytes@2.4.0:
40 | version "2.4.0"
41 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.4.0.tgz#7d97196f9d5baf7f6935e25985549edd2a6c2339"
42 |
43 | content-disposition@0.5.2:
44 | version "0.5.2"
45 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4"
46 |
47 | content-type@~1.0.2:
48 | version "1.0.2"
49 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed"
50 |
51 | cookie-signature@1.0.6:
52 | version "1.0.6"
53 | resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
54 |
55 | cookie@0.3.1:
56 | version "0.3.1"
57 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
58 |
59 | debug@2.6.7:
60 | version "2.6.7"
61 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.7.tgz#92bad1f6d05bbb6bba22cca88bcd0ec894c2861e"
62 | dependencies:
63 | ms "2.0.0"
64 |
65 | debug@2.6.8:
66 | version "2.6.8"
67 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc"
68 | dependencies:
69 | ms "2.0.0"
70 |
71 | depd@1.1.1, depd@~1.1.0, depd@~1.1.1:
72 | version "1.1.1"
73 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359"
74 |
75 | destroy@~1.0.4:
76 | version "1.0.4"
77 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
78 |
79 | ee-first@1.1.1:
80 | version "1.1.1"
81 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
82 |
83 | ejs@^2.5.7:
84 | version "2.5.7"
85 | resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.7.tgz#cc872c168880ae3c7189762fd5ffc00896c9518a"
86 |
87 | encodeurl@~1.0.1:
88 | version "1.0.1"
89 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20"
90 |
91 | encoding@^0.1.11:
92 | version "0.1.12"
93 | resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb"
94 | dependencies:
95 | iconv-lite "~0.4.13"
96 |
97 | escape-html@~1.0.3:
98 | version "1.0.3"
99 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
100 |
101 | etag@~1.8.0:
102 | version "1.8.0"
103 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.0.tgz#6f631aef336d6c46362b51764044ce216be3c051"
104 |
105 | express@^4.15.4:
106 | version "4.15.4"
107 | resolved "https://registry.yarnpkg.com/express/-/express-4.15.4.tgz#032e2253489cf8fce02666beca3d11ed7a2daed1"
108 | dependencies:
109 | accepts "~1.3.3"
110 | array-flatten "1.1.1"
111 | content-disposition "0.5.2"
112 | content-type "~1.0.2"
113 | cookie "0.3.1"
114 | cookie-signature "1.0.6"
115 | debug "2.6.8"
116 | depd "~1.1.1"
117 | encodeurl "~1.0.1"
118 | escape-html "~1.0.3"
119 | etag "~1.8.0"
120 | finalhandler "~1.0.4"
121 | fresh "0.5.0"
122 | merge-descriptors "1.0.1"
123 | methods "~1.1.2"
124 | on-finished "~2.3.0"
125 | parseurl "~1.3.1"
126 | path-to-regexp "0.1.7"
127 | proxy-addr "~1.1.5"
128 | qs "6.5.0"
129 | range-parser "~1.2.0"
130 | send "0.15.4"
131 | serve-static "1.12.4"
132 | setprototypeof "1.0.3"
133 | statuses "~1.3.1"
134 | type-is "~1.6.15"
135 | utils-merge "1.0.0"
136 | vary "~1.1.1"
137 |
138 | finalhandler@~1.0.4:
139 | version "1.0.4"
140 | resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.4.tgz#18574f2e7c4b98b8ae3b230c21f201f31bdb3fb7"
141 | dependencies:
142 | debug "2.6.8"
143 | encodeurl "~1.0.1"
144 | escape-html "~1.0.3"
145 | on-finished "~2.3.0"
146 | parseurl "~1.3.1"
147 | statuses "~1.3.1"
148 | unpipe "~1.0.0"
149 |
150 | forwarded@~0.1.0:
151 | version "0.1.0"
152 | resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.0.tgz#19ef9874c4ae1c297bcf078fde63a09b66a84363"
153 |
154 | fresh@0.5.0:
155 | version "0.5.0"
156 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.0.tgz#f474ca5e6a9246d6fd8e0953cfa9b9c805afa78e"
157 |
158 | http-errors@~1.6.1, http-errors@~1.6.2:
159 | version "1.6.2"
160 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736"
161 | dependencies:
162 | depd "1.1.1"
163 | inherits "2.0.3"
164 | setprototypeof "1.0.3"
165 | statuses ">= 1.3.1 < 2"
166 |
167 | iconv-lite@0.4.15:
168 | version "0.4.15"
169 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb"
170 |
171 | iconv-lite@~0.4.13:
172 | version "0.4.18"
173 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.18.tgz#23d8656b16aae6742ac29732ea8f0336a4789cf2"
174 |
175 | inherits@2.0.3:
176 | version "2.0.3"
177 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
178 |
179 | ipaddr.js@1.4.0:
180 | version "1.4.0"
181 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.4.0.tgz#296aca878a821816e5b85d0a285a99bcff4582f0"
182 |
183 | is-stream@^1.0.1:
184 | version "1.1.0"
185 | resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
186 |
187 | lodash.isplainobject@^4.0.6:
188 | version "4.0.6"
189 | resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
190 |
191 | media-typer@0.3.0:
192 | version "0.3.0"
193 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
194 |
195 | merge-descriptors@1.0.1:
196 | version "1.0.1"
197 | resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
198 |
199 | methods@~1.1.2:
200 | version "1.1.2"
201 | resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
202 |
203 | mime-db@~1.29.0:
204 | version "1.29.0"
205 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.29.0.tgz#48d26d235589651704ac5916ca06001914266878"
206 |
207 | mime-types@~2.1.15, mime-types@~2.1.16:
208 | version "2.1.16"
209 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.16.tgz#2b858a52e5ecd516db897ac2be87487830698e23"
210 | dependencies:
211 | mime-db "~1.29.0"
212 |
213 | mime@1.3.4:
214 | version "1.3.4"
215 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53"
216 |
217 | morgan@^1.8.2:
218 | version "1.8.2"
219 | resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.8.2.tgz#784ac7734e4a453a9c6e6e8680a9329275c8b687"
220 | dependencies:
221 | basic-auth "~1.1.0"
222 | debug "2.6.8"
223 | depd "~1.1.0"
224 | on-finished "~2.3.0"
225 | on-headers "~1.0.1"
226 |
227 | ms@2.0.0:
228 | version "2.0.0"
229 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
230 |
231 | negotiator@0.6.1:
232 | version "0.6.1"
233 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
234 |
235 | node-fetch@^1.7.2:
236 | version "1.7.2"
237 | resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.2.tgz#c54e9aac57e432875233525f3c891c4159ffefd7"
238 | dependencies:
239 | encoding "^0.1.11"
240 | is-stream "^1.0.1"
241 |
242 | object-assign@^4.1.0:
243 | version "4.1.1"
244 | resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
245 |
246 | on-finished@~2.3.0:
247 | version "2.3.0"
248 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
249 | dependencies:
250 | ee-first "1.1.1"
251 |
252 | on-headers@~1.0.1:
253 | version "1.0.1"
254 | resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7"
255 |
256 | parseurl@~1.3.1:
257 | version "1.3.1"
258 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.1.tgz#c8ab8c9223ba34888aa64a297b28853bec18da56"
259 |
260 | path-to-regexp@0.1.7:
261 | version "0.1.7"
262 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
263 |
264 | proxy-addr@~1.1.5:
265 | version "1.1.5"
266 | resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.5.tgz#71c0ee3b102de3f202f3b64f608d173fcba1a918"
267 | dependencies:
268 | forwarded "~0.1.0"
269 | ipaddr.js "1.4.0"
270 |
271 | qs@6.4.0:
272 | version "6.4.0"
273 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
274 |
275 | qs@6.5.0:
276 | version "6.5.0"
277 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.0.tgz#8d04954d364def3efc55b5a0793e1e2c8b1e6e49"
278 |
279 | qs@~6.0.4:
280 | version "6.0.4"
281 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.0.4.tgz#51019d84720c939b82737e84556a782338ecea7b"
282 |
283 | range-parser@~1.2.0:
284 | version "1.2.0"
285 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
286 |
287 | raw-body@~2.2.0:
288 | version "2.2.0"
289 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.2.0.tgz#994976cf6a5096a41162840492f0bdc5d6e7fb96"
290 | dependencies:
291 | bytes "2.4.0"
292 | iconv-lite "0.4.15"
293 | unpipe "1.0.0"
294 |
295 | send@0.15.4:
296 | version "0.15.4"
297 | resolved "https://registry.yarnpkg.com/send/-/send-0.15.4.tgz#985faa3e284b0273c793364a35c6737bd93905b9"
298 | dependencies:
299 | debug "2.6.8"
300 | depd "~1.1.1"
301 | destroy "~1.0.4"
302 | encodeurl "~1.0.1"
303 | escape-html "~1.0.3"
304 | etag "~1.8.0"
305 | fresh "0.5.0"
306 | http-errors "~1.6.2"
307 | mime "1.3.4"
308 | ms "2.0.0"
309 | on-finished "~2.3.0"
310 | range-parser "~1.2.0"
311 | statuses "~1.3.1"
312 |
313 | serve-static@1.12.4:
314 | version "1.12.4"
315 | resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.12.4.tgz#9b6aa98eeb7253c4eedc4c1f6fdbca609901a961"
316 | dependencies:
317 | encodeurl "~1.0.1"
318 | escape-html "~1.0.3"
319 | parseurl "~1.3.1"
320 | send "0.15.4"
321 |
322 | setprototypeof@1.0.3:
323 | version "1.0.3"
324 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04"
325 |
326 | "statuses@>= 1.3.1 < 2", statuses@~1.3.1:
327 | version "1.3.1"
328 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e"
329 |
330 | stripe@^4.24.1:
331 | version "4.24.1"
332 | resolved "https://registry.yarnpkg.com/stripe/-/stripe-4.24.1.tgz#b177079e132d2f71cd28a010f5cfb0572e13dbd0"
333 | dependencies:
334 | bluebird "^2.10.2"
335 | lodash.isplainobject "^4.0.6"
336 | object-assign "^4.1.0"
337 | qs "~6.0.4"
338 |
339 | type-is@~1.6.15:
340 | version "1.6.15"
341 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410"
342 | dependencies:
343 | media-typer "0.3.0"
344 | mime-types "~2.1.15"
345 |
346 | unpipe@1.0.0, unpipe@~1.0.0:
347 | version "1.0.0"
348 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
349 |
350 | utils-merge@1.0.0:
351 | version "1.0.0"
352 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8"
353 |
354 | vary@~1.1.1:
355 | version "1.1.1"
356 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.1.tgz#67535ebb694c1d52257457984665323f587e8d37"
357 |
--------------------------------------------------------------------------------