├── .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 |
10 | 13 |
14 | 15 |
16 | 19 |
20 | 21 |
22 | 25 |
26 | 27 |
28 | 31 |
32 | 33 |
34 |
35 | 38 |
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 | --------------------------------------------------------------------------------